]> git.xolatile.top Git - xolatile-badassbug.git/commitdiff
Source code, broken...
authorxolatile <xolatile@proton.me>
Wed, 16 Jul 2025 21:07:43 +0000 (23:07 +0200)
committerxolatile <xolatile@proton.me>
Wed, 16 Jul 2025 21:07:43 +0000 (23:07 +0200)
91 files changed:
src/Makefile [new file with mode: 0644]
src/engine/3dgui.cpp [new file with mode: 0644]
src/engine/animmodel.h [new file with mode: 0644]
src/engine/bih.cpp [new file with mode: 0644]
src/engine/bih.h [new file with mode: 0644]
src/engine/blend.cpp [new file with mode: 0644]
src/engine/blob.cpp [new file with mode: 0644]
src/engine/client.cpp [new file with mode: 0644]
src/engine/command.cpp [new file with mode: 0644]
src/engine/console.cpp [new file with mode: 0644]
src/engine/decal.cpp [new file with mode: 0644]
src/engine/depthfx.h [new file with mode: 0644]
src/engine/dynlight.cpp [new file with mode: 0644]
src/engine/engine.h [new file with mode: 0644]
src/engine/explosion.h [new file with mode: 0644]
src/engine/glare.cpp [new file with mode: 0644]
src/engine/grass.cpp [new file with mode: 0644]
src/engine/lensflare.h [new file with mode: 0644]
src/engine/lightmap.cpp [new file with mode: 0644]
src/engine/lightmap.h [new file with mode: 0644]
src/engine/lightning.h [new file with mode: 0644]
src/engine/main.cpp [new file with mode: 0644]
src/engine/master.cpp [new file with mode: 0644]
src/engine/material.cpp [new file with mode: 0644]
src/engine/md3.h [new file with mode: 0644]
src/engine/md5.h [new file with mode: 0644]
src/engine/menus.cpp [new file with mode: 0644]
src/engine/model.h [new file with mode: 0644]
src/engine/movie.cpp [new file with mode: 0644]
src/engine/mpr.h [new file with mode: 0644]
src/engine/normal.cpp [new file with mode: 0644]
src/engine/obj.h [new file with mode: 0644]
src/engine/octa.cpp [new file with mode: 0644]
src/engine/octa.h [new file with mode: 0644]
src/engine/octaedit.cpp [new file with mode: 0644]
src/engine/octarender.cpp [new file with mode: 0644]
src/engine/physics.cpp [new file with mode: 0644]
src/engine/pvs.cpp [new file with mode: 0644]
src/engine/ragdoll.h [new file with mode: 0644]
src/engine/rendergl.cpp [new file with mode: 0644]
src/engine/rendermodel.cpp [new file with mode: 0644]
src/engine/renderparticles.cpp [new file with mode: 0644]
src/engine/rendersky.cpp [new file with mode: 0644]
src/engine/rendertarget.h [new file with mode: 0644]
src/engine/rendertext.cpp [new file with mode: 0644]
src/engine/renderva.cpp [new file with mode: 0644]
src/engine/server.cpp [new file with mode: 0644]
src/engine/serverbrowser.cpp [new file with mode: 0644]
src/engine/shader.cpp [new file with mode: 0644]
src/engine/shadowmap.cpp [new file with mode: 0644]
src/engine/skelmodel.h [new file with mode: 0644]
src/engine/smd.h [new file with mode: 0644]
src/engine/sound.cpp [new file with mode: 0644]
src/engine/textedit.h [new file with mode: 0644]
src/engine/texture.cpp [new file with mode: 0644]
src/engine/texture.h [new file with mode: 0644]
src/engine/vertmodel.h [new file with mode: 0644]
src/engine/water.cpp [new file with mode: 0644]
src/engine/world.cpp [new file with mode: 0644]
src/engine/world.h [new file with mode: 0644]
src/engine/worldio.cpp [new file with mode: 0644]
src/fpsgame/ai.cpp [new file with mode: 0644]
src/fpsgame/ai.h [new file with mode: 0644]
src/fpsgame/aiman.h [new file with mode: 0644]
src/fpsgame/client.cpp [new file with mode: 0644]
src/fpsgame/entities.cpp [new file with mode: 0644]
src/fpsgame/extinfo.h [new file with mode: 0644]
src/fpsgame/fps.cpp [new file with mode: 0644]
src/fpsgame/game.h [new file with mode: 0644]
src/fpsgame/render.cpp [new file with mode: 0644]
src/fpsgame/scoreboard.cpp [new file with mode: 0644]
src/fpsgame/server.cpp [new file with mode: 0644]
src/fpsgame/waypoint.cpp [new file with mode: 0644]
src/fpsgame/weapon.cpp [new file with mode: 0644]
src/readme_source.txt [new file with mode: 0644]
src/shared/command.h [new file with mode: 0644]
src/shared/crypto.cpp [new file with mode: 0644]
src/shared/cube.h [new file with mode: 0644]
src/shared/cube2font.c [new file with mode: 0644]
src/shared/ents.h [new file with mode: 0644]
src/shared/geom.cpp [new file with mode: 0644]
src/shared/geom.h [new file with mode: 0644]
src/shared/glemu.cpp [new file with mode: 0644]
src/shared/glemu.h [new file with mode: 0644]
src/shared/glexts.h [new file with mode: 0644]
src/shared/iengine.h [new file with mode: 0644]
src/shared/igame.h [new file with mode: 0644]
src/shared/stream.cpp [new file with mode: 0644]
src/shared/tools.cpp [new file with mode: 0644]
src/shared/tools.h [new file with mode: 0644]
src/shared/zip.cpp [new file with mode: 0644]

diff --git a/src/Makefile b/src/Makefile
new file mode 100644 (file)
index 0000000..bb15b25
--- /dev/null
@@ -0,0 +1,545 @@
+CXXFLAGS= -O3 -fomit-frame-pointer -ffast-math
+override CXXFLAGS+= -Wall -fsigned-char -fno-exceptions -fno-rtti
+
+PLATFORM= $(shell uname -s | tr '[:lower:]' '[:upper:]')
+PLATFORM_PREFIX= native
+
+INCLUDES= -Ishared -Iengine -Ifpsgame -Ienet/include
+
+STRIP=
+ifeq (,$(findstring -g,$(CXXFLAGS)))
+ifeq (,$(findstring -pg,$(CXXFLAGS)))
+  STRIP=strip
+endif
+endif
+
+MV=mv
+
+ifneq (,$(findstring MINGW,$(PLATFORM)))
+WINDRES= windres
+ifneq (,$(findstring 64,$(PLATFORM)))
+ifneq (,$(findstring CROSS,$(PLATFORM)))
+  CXX=x86_64-w64-mingw32-g++
+  WINDRES=x86_64-w64-mingw32-windres
+ifneq (,$(STRIP))
+  STRIP=x86_64-w64-mingw32-strip
+endif
+endif
+WINLIB=lib64
+WINBIN=../bin64
+override CXX+= -m64
+override WINDRES+= -F pe-x86-64
+else
+ifneq (,$(findstring CROSS,$(PLATFORM)))
+  CXX=i686-w64-mingw32-g++
+  WINDRES=i686-w64-mingw32-windres
+ifneq (,$(STRIP))
+  STRIP=i686-w64-mingw32-strip
+endif
+endif
+WINLIB=lib
+WINBIN=../bin
+override CXX+= -m32
+override WINDRES+= -F pe-i386
+endif
+CLIENT_INCLUDES= $(INCLUDES) -Iinclude
+STD_LIBS= -static-libgcc -static-libstdc++
+CLIENT_LIBS= -mwindows $(STD_LIBS) -L$(WINBIN) -L$(WINLIB) -lSDL2 -lSDL2_image -lSDL2_mixer -lzlib1 -lopengl32 -lenet -lws2_32 -lwinmm
+else
+ifneq (,$(findstring DARWIN,$(PLATFORM)))
+ifneq (,$(findstring CROSS,$(PLATFORM)))
+  TOOLCHAINTARGET= $(shell osxcross-conf | grep -m1 "TARGET=" | cut -b24-)
+  TOOLCHAIN= x86_64-apple-$(TOOLCHAINTARGET)-
+  AR= $(TOOLCHAIN)ar
+  CXX= $(TOOLCHAIN)clang++
+  CC= $(TOOLCHAIN)clang
+ifneq (,$(STRIP))
+  STRIP= $(TOOLCHAIN)strip
+endif
+endif
+OSXMIN= 10.9
+override CC+= -arch x86_64 -mmacosx-version-min=$(OSXMIN)
+override CXX+= -arch x86_64 -mmacosx-version-min=$(OSXMIN)
+CLIENT_INCLUDES= $(INCLUDES) -Iinclude
+CLIENT_LIBS= -F../sauerbraten.app/Contents/Frameworks/ -framework SDL2 -framework SDL2_image
+CLIENT_LIBS+= -framework SDL2_mixer -framework CoreAudio -framework AudioToolbox
+CLIENT_LIBS+= -framework AudioUnit -framework OpenGL -framework Cocoa -lz -Lenet -lenet
+else
+CLIENT_INCLUDES= $(INCLUDES) -I/usr/X11R6/include `sdl2-config --cflags`
+CLIENT_LIBS= -Lenet -lenet -L/usr/X11R6/lib -lX11 `sdl2-config --libs` -lSDL2_image -lSDL2_mixer -lz -lGL
+endif
+endif
+ifeq ($(PLATFORM),LINUX)
+CLIENT_LIBS+= -lrt
+else
+ifneq (,$(findstring GNU,$(PLATFORM)))
+CLIENT_LIBS+= -lrt
+endif
+endif
+CLIENT_OBJS= \
+       shared/crypto.o \
+       shared/geom.o \
+       shared/glemu.o \
+       shared/stream.o \
+       shared/tools.o \
+       shared/zip.o \
+       engine/3dgui.o \
+       engine/bih.o \
+       engine/blend.o \
+       engine/blob.o \
+       engine/client.o \
+       engine/command.o \
+       engine/console.o \
+       engine/cubeloader.o \
+       engine/decal.o \
+       engine/dynlight.o \
+       engine/glare.o \
+       engine/grass.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/water.o \
+       engine/world.o \
+       engine/worldio.o \
+       fpsgame/ai.o \
+       fpsgame/client.o \
+       fpsgame/entities.o \
+       fpsgame/fps.o \
+       fpsgame/monster.o \
+       fpsgame/movable.o \
+       fpsgame/render.o \
+       fpsgame/scoreboard.o \
+       fpsgame/server.o \
+       fpsgame/waypoint.o \
+       fpsgame/weapon.o
+
+CLIENT_PCH= shared/cube.h.gch engine/engine.h.gch fpsgame/game.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
+MASTER_LIBS= $(STD_LIBS) -L$(WINBIN) -L$(WINLIB) -lzlib1 -lenet -lws2_32 -lwinmm
+else
+SERVER_INCLUDES= -DSTANDALONE $(INCLUDES)
+SERVER_LIBS= -Lenet -lenet -lz
+MASTER_LIBS= $(SERVER_LIBS)
+endif
+SERVER_OBJS= \
+       shared/crypto-standalone.o \
+       shared/stream-standalone.o \
+       shared/tools-standalone.o \
+       engine/command-standalone.o \
+       engine/server-standalone.o \
+       engine/worldio-standalone.o \
+       fpsgame/entities-standalone.o \
+       fpsgame/server-standalone.o
+
+MASTER_OBJS= \
+       shared/crypto-standalone.o \
+       shared/stream-standalone.o \
+       shared/tools-standalone.o \
+       engine/command-standalone.o \
+       engine/master-standalone.o
+
+SERVER_MASTER_OBJS= $(SERVER_OBJS) $(filter-out $(SERVER_OBJS),$(MASTER_OBJS))
+
+default: all
+
+all: client server
+
+clean:
+       -$(RM) $(CLIENT_PCH) $(CLIENT_OBJS) $(SERVER_MASTER_OBJS) sauer_client sauer_server sauer_master
+
+$(filter-out shared/%,$(CLIENT_PCH)): $(filter shared/%,$(CLIENT_PCH))
+
+%.h.gch: %.h
+       $(CXX) $(CXXFLAGS) -x c++-header -o $@.tmp $<
+       $(MV) $@.tmp $@
+
+%-standalone.o: %.cpp
+       $(CXX) $(CXXFLAGS) -c -o $@ $<
+
+$(CLIENT_OBJS): CXXFLAGS += $(CLIENT_INCLUDES)
+$(filter shared/%,$(CLIENT_OBJS)): $(filter shared/%,$(CLIENT_PCH))
+$(filter engine/%,$(CLIENT_OBJS)): $(filter engine/%,$(CLIENT_PCH))
+$(filter fpsgame/%,$(CLIENT_OBJS)): $(filter fpsgame/%,$(CLIENT_PCH))
+
+$(SERVER_MASTER_OBJS): CXXFLAGS += $(SERVER_INCLUDES)
+
+ifneq (,$(findstring MINGW,$(PLATFORM)))
+client: $(CLIENT_OBJS)
+       $(WINDRES) -I vcpp -i vcpp/mingw.rc -J rc -o vcpp/mingw.res -O coff
+       $(CXX) $(CXXFLAGS) -o $(WINBIN)/sauerbraten.exe vcpp/mingw.res $(CLIENT_OBJS) $(CLIENT_LIBS)
+
+server: $(SERVER_OBJS)
+       $(WINDRES) -I vcpp -i vcpp/mingw.rc -J rc -o vcpp/mingw.res -O coff
+       $(CXX) $(CXXFLAGS) -o $(WINBIN)/sauer_server.exe vcpp/mingw.res $(SERVER_OBJS) $(SERVER_LIBS)
+
+master: $(MASTER_OBJS)
+       $(CXX) $(CXXFLAGS) -o $(WINBIN)/sauer_master.exe $(MASTER_OBJS) $(MASTER_LIBS)
+
+install: all
+else
+client:        libenet $(CLIENT_OBJS)
+       $(CXX) $(CXXFLAGS) -o sauer_client $(CLIENT_OBJS) $(CLIENT_LIBS)
+ifneq (,$(findstring DARWIN,$(PLATFORM)))
+       install_name_tool -add_rpath @executable_path/../Frameworks sauer_client
+endif
+
+server:        libenet $(SERVER_OBJS)
+       $(CXX) $(CXXFLAGS) -o sauer_server $(SERVER_OBJS) $(SERVER_LIBS)
+
+master: libenet $(MASTER_OBJS)
+       $(CXX) $(CXXFLAGS) -o sauer_master $(MASTER_OBJS) $(MASTER_LIBS)
+
+shared/cube2font.o: shared/cube2font.c
+       $(CXX) $(CXXFLAGS) -c -o $@ $< `freetype-config --cflags`
+
+cube2font: shared/cube2font.o
+       $(CXX) $(CXXFLAGS) -o cube2font shared/cube2font.o `freetype-config --libs` -lz
+
+ifneq (,$(findstring DARWIN,$(PLATFORM)))
+install: client
+       cp sauer_client ../sauerbraten.app/Contents/MacOS/sauerbraten_universal
+else
+install: all
+       cp sauer_client ../bin_unix/$(PLATFORM_PREFIX)_client
+       cp sauer_server ../bin_unix/$(PLATFORM_PREFIX)_server
+ifneq (,$(STRIP))
+       $(STRIP) ../bin_unix/$(PLATFORM_PREFIX)_client
+       $(STRIP) ../bin_unix/$(PLATFORM_PREFIX)_server
+endif
+endif
+endif
+
+enet/libenet.a:
+       $(MAKE) -C enet CC='$(CC)' AR='$(AR)'
+libenet: enet/libenet.a
+
+depend:
+       makedepend -Y -Ishared -Iengine -Ifpsgame $(CLIENT_OBJS:.o=.cpp)
+       makedepend -a -o.h.gch -Y -Ishared -Iengine -Ifpsgame $(CLIENT_PCH:.h.gch=.h)
+       makedepend -a -o-standalone.o -Y -DSTANDALONE -Ishared -Iengine -Ifpsgame $(SERVER_MASTER_OBJS:-standalone.o=.cpp)
+
+# DO NOT DELETE
+
+shared/crypto.o: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+shared/crypto.o: shared/command.h shared/glexts.h shared/glemu.h
+shared/crypto.o: shared/iengine.h shared/igame.h
+shared/geom.o: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+shared/geom.o: shared/command.h shared/glexts.h shared/glemu.h
+shared/geom.o: shared/iengine.h shared/igame.h
+shared/glemu.o: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+shared/glemu.o: shared/command.h shared/glexts.h shared/glemu.h
+shared/glemu.o: shared/iengine.h shared/igame.h
+shared/stream.o: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+shared/stream.o: shared/command.h shared/glexts.h shared/glemu.h
+shared/stream.o: shared/iengine.h shared/igame.h
+shared/tools.o: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+shared/tools.o: shared/command.h shared/glexts.h shared/glemu.h
+shared/tools.o: shared/iengine.h shared/igame.h
+shared/zip.o: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+shared/zip.o: shared/command.h shared/glexts.h shared/glemu.h
+shared/zip.o: shared/iengine.h shared/igame.h
+engine/3dgui.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/3dgui.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/3dgui.o: shared/iengine.h shared/igame.h engine/world.h engine/octa.h
+engine/3dgui.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/3dgui.o: engine/model.h engine/textedit.h
+engine/bih.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/bih.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/bih.o: shared/iengine.h shared/igame.h engine/world.h engine/octa.h
+engine/bih.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
+engine/blend.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/blend.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/blend.o: shared/iengine.h shared/igame.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/blob.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/blob.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/blob.o: shared/iengine.h shared/igame.h engine/world.h engine/octa.h
+engine/blob.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
+engine/client.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/client.o: shared/ents.h shared/command.h shared/glexts.h
+engine/client.o: shared/glemu.h shared/iengine.h shared/igame.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/command.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/command.o: shared/ents.h shared/command.h shared/glexts.h
+engine/command.o: shared/glemu.h shared/iengine.h shared/igame.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/console.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/console.o: shared/ents.h shared/command.h shared/glexts.h
+engine/console.o: shared/glemu.h shared/iengine.h shared/igame.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/cubeloader.o: engine/engine.h shared/cube.h shared/tools.h
+engine/cubeloader.o: shared/geom.h shared/ents.h shared/command.h
+engine/cubeloader.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/cubeloader.o: shared/igame.h engine/world.h engine/octa.h
+engine/cubeloader.o: engine/lightmap.h engine/bih.h engine/texture.h
+engine/cubeloader.o: engine/model.h
+engine/decal.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/decal.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/decal.o: shared/iengine.h shared/igame.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/dynlight.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/dynlight.o: shared/ents.h shared/command.h shared/glexts.h
+engine/dynlight.o: shared/glemu.h shared/iengine.h shared/igame.h
+engine/dynlight.o: engine/world.h engine/octa.h engine/lightmap.h
+engine/dynlight.o: engine/bih.h engine/texture.h engine/model.h
+engine/glare.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/glare.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/glare.o: shared/iengine.h shared/igame.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/rendertarget.h
+engine/grass.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/grass.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/grass.o: shared/iengine.h shared/igame.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/lightmap.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/lightmap.o: shared/ents.h shared/command.h shared/glexts.h
+engine/lightmap.o: shared/glemu.h shared/iengine.h shared/igame.h
+engine/lightmap.o: engine/world.h engine/octa.h engine/lightmap.h
+engine/lightmap.o: engine/bih.h engine/texture.h engine/model.h
+engine/main.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/main.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/main.o: shared/iengine.h shared/igame.h engine/world.h engine/octa.h
+engine/main.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
+engine/material.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/material.o: shared/ents.h shared/command.h shared/glexts.h
+engine/material.o: shared/glemu.h shared/iengine.h shared/igame.h
+engine/material.o: engine/world.h engine/octa.h engine/lightmap.h
+engine/material.o: engine/bih.h engine/texture.h engine/model.h
+engine/menus.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/menus.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/menus.o: shared/iengine.h shared/igame.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/movie.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/movie.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/movie.o: shared/iengine.h shared/igame.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/normal.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/normal.o: shared/ents.h shared/command.h shared/glexts.h
+engine/normal.o: shared/glemu.h shared/iengine.h shared/igame.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/octa.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/octa.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/octa.o: shared/iengine.h shared/igame.h engine/world.h engine/octa.h
+engine/octa.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
+engine/octaedit.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/octaedit.o: shared/ents.h shared/command.h shared/glexts.h
+engine/octaedit.o: shared/glemu.h shared/iengine.h shared/igame.h
+engine/octaedit.o: engine/world.h engine/octa.h engine/lightmap.h
+engine/octaedit.o: engine/bih.h engine/texture.h engine/model.h
+engine/octarender.o: engine/engine.h shared/cube.h shared/tools.h
+engine/octarender.o: shared/geom.h shared/ents.h shared/command.h
+engine/octarender.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/octarender.o: shared/igame.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/physics.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/physics.o: shared/ents.h shared/command.h shared/glexts.h
+engine/physics.o: shared/glemu.h shared/iengine.h shared/igame.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/mpr.h
+engine/pvs.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/pvs.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/pvs.o: shared/iengine.h shared/igame.h engine/world.h engine/octa.h
+engine/pvs.o: engine/lightmap.h engine/bih.h engine/texture.h engine/model.h
+engine/rendergl.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/rendergl.o: shared/ents.h shared/command.h shared/glexts.h
+engine/rendergl.o: shared/glemu.h shared/iengine.h shared/igame.h
+engine/rendergl.o: engine/world.h engine/octa.h engine/lightmap.h
+engine/rendergl.o: engine/bih.h engine/texture.h engine/model.h
+engine/rendermodel.o: engine/engine.h shared/cube.h shared/tools.h
+engine/rendermodel.o: shared/geom.h shared/ents.h shared/command.h
+engine/rendermodel.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/rendermodel.o: shared/igame.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/ragdoll.h engine/animmodel.h
+engine/rendermodel.o: engine/vertmodel.h engine/skelmodel.h engine/md2.h
+engine/rendermodel.o: engine/md3.h engine/md5.h engine/obj.h engine/smd.h
+engine/rendermodel.o: engine/iqm.h
+engine/renderparticles.o: engine/engine.h shared/cube.h shared/tools.h
+engine/renderparticles.o: shared/geom.h shared/ents.h shared/command.h
+engine/renderparticles.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/renderparticles.o: shared/igame.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/rendertarget.h
+engine/renderparticles.o: engine/depthfx.h engine/explosion.h
+engine/renderparticles.o: engine/lensflare.h engine/lightning.h
+engine/rendersky.o: engine/engine.h shared/cube.h shared/tools.h
+engine/rendersky.o: shared/geom.h shared/ents.h shared/command.h
+engine/rendersky.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/rendersky.o: shared/igame.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/rendertext.o: engine/engine.h shared/cube.h shared/tools.h
+engine/rendertext.o: shared/geom.h shared/ents.h shared/command.h
+engine/rendertext.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/rendertext.o: shared/igame.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/renderva.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/renderva.o: shared/ents.h shared/command.h shared/glexts.h
+engine/renderva.o: shared/glemu.h shared/iengine.h shared/igame.h
+engine/renderva.o: engine/world.h engine/octa.h engine/lightmap.h
+engine/renderva.o: engine/bih.h engine/texture.h engine/model.h
+engine/server.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/server.o: shared/ents.h shared/command.h shared/glexts.h
+engine/server.o: shared/glemu.h shared/iengine.h shared/igame.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/serverbrowser.o: engine/engine.h shared/cube.h shared/tools.h
+engine/serverbrowser.o: shared/geom.h shared/ents.h shared/command.h
+engine/serverbrowser.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/serverbrowser.o: shared/igame.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/shader.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/shader.o: shared/ents.h shared/command.h shared/glexts.h
+engine/shader.o: shared/glemu.h shared/iengine.h shared/igame.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/shadowmap.o: engine/engine.h shared/cube.h shared/tools.h
+engine/shadowmap.o: shared/geom.h shared/ents.h shared/command.h
+engine/shadowmap.o: shared/glexts.h shared/glemu.h shared/iengine.h
+engine/shadowmap.o: shared/igame.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/rendertarget.h
+engine/sound.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/sound.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/sound.o: shared/iengine.h shared/igame.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/texture.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/texture.o: shared/ents.h shared/command.h shared/glexts.h
+engine/texture.o: shared/glemu.h shared/iengine.h shared/igame.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/water.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/water.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/water.o: shared/iengine.h shared/igame.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/world.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/world.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+engine/world.o: shared/iengine.h shared/igame.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/worldio.o: engine/engine.h shared/cube.h shared/tools.h shared/geom.h
+engine/worldio.o: shared/ents.h shared/command.h shared/glexts.h
+engine/worldio.o: shared/glemu.h shared/iengine.h shared/igame.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
+fpsgame/ai.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/ai.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+fpsgame/ai.o: shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/client.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/client.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/client.o: shared/glemu.h shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/client.o: fpsgame/capture.h fpsgame/ctf.h fpsgame/collect.h
+fpsgame/entities.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/entities.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/entities.o: shared/glemu.h shared/iengine.h shared/igame.h
+fpsgame/entities.o: fpsgame/ai.h
+fpsgame/fps.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/fps.o: shared/ents.h shared/command.h shared/glexts.h shared/glemu.h
+fpsgame/fps.o: shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/monster.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/monster.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/monster.o: shared/glemu.h shared/iengine.h shared/igame.h
+fpsgame/monster.o: fpsgame/ai.h
+fpsgame/movable.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/movable.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/movable.o: shared/glemu.h shared/iengine.h shared/igame.h
+fpsgame/movable.o: fpsgame/ai.h
+fpsgame/render.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/render.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/render.o: shared/glemu.h shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/scoreboard.o: fpsgame/game.h shared/cube.h shared/tools.h
+fpsgame/scoreboard.o: shared/geom.h shared/ents.h shared/command.h
+fpsgame/scoreboard.o: shared/glexts.h shared/glemu.h shared/iengine.h
+fpsgame/scoreboard.o: shared/igame.h fpsgame/ai.h
+fpsgame/server.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/server.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/server.o: shared/glemu.h shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/server.o: fpsgame/capture.h fpsgame/ctf.h fpsgame/collect.h
+fpsgame/server.o: fpsgame/extinfo.h fpsgame/aiman.h
+fpsgame/waypoint.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/waypoint.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/waypoint.o: shared/glemu.h shared/iengine.h shared/igame.h
+fpsgame/waypoint.o: fpsgame/ai.h
+fpsgame/weapon.o: fpsgame/game.h shared/cube.h shared/tools.h shared/geom.h
+fpsgame/weapon.o: shared/ents.h shared/command.h shared/glexts.h
+fpsgame/weapon.o: shared/glemu.h shared/iengine.h shared/igame.h fpsgame/ai.h
+
+shared/cube.h.gch: shared/tools.h shared/geom.h shared/ents.h
+shared/cube.h.gch: shared/command.h shared/glexts.h shared/glemu.h
+shared/cube.h.gch: shared/iengine.h shared/igame.h
+engine/engine.h.gch: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+engine/engine.h.gch: shared/command.h shared/glexts.h shared/glemu.h
+engine/engine.h.gch: shared/iengine.h shared/igame.h engine/world.h
+engine/engine.h.gch: engine/octa.h engine/lightmap.h engine/bih.h
+engine/engine.h.gch: engine/texture.h engine/model.h
+fpsgame/game.h.gch: shared/cube.h shared/tools.h shared/geom.h shared/ents.h
+fpsgame/game.h.gch: shared/command.h shared/glexts.h shared/glemu.h
+fpsgame/game.h.gch: shared/iengine.h shared/igame.h fpsgame/ai.h
+
+shared/crypto-standalone.o: shared/cube.h shared/tools.h shared/geom.h
+shared/crypto-standalone.o: shared/ents.h shared/command.h shared/iengine.h
+shared/crypto-standalone.o: shared/igame.h
+shared/stream-standalone.o: shared/cube.h shared/tools.h shared/geom.h
+shared/stream-standalone.o: shared/ents.h shared/command.h shared/iengine.h
+shared/stream-standalone.o: shared/igame.h
+shared/tools-standalone.o: shared/cube.h shared/tools.h shared/geom.h
+shared/tools-standalone.o: shared/ents.h shared/command.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/geom.h shared/ents.h shared/command.h
+engine/command-standalone.o: shared/iengine.h shared/igame.h engine/world.h
+engine/server-standalone.o: engine/engine.h shared/cube.h shared/tools.h
+engine/server-standalone.o: shared/geom.h shared/ents.h shared/command.h
+engine/server-standalone.o: shared/iengine.h shared/igame.h engine/world.h
+engine/worldio-standalone.o: engine/engine.h shared/cube.h shared/tools.h
+engine/worldio-standalone.o: shared/geom.h shared/ents.h shared/command.h
+engine/worldio-standalone.o: shared/iengine.h shared/igame.h engine/world.h
+fpsgame/entities-standalone.o: fpsgame/game.h shared/cube.h shared/tools.h
+fpsgame/entities-standalone.o: shared/geom.h shared/ents.h shared/command.h
+fpsgame/entities-standalone.o: shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/server-standalone.o: fpsgame/game.h shared/cube.h shared/tools.h
+fpsgame/server-standalone.o: shared/geom.h shared/ents.h shared/command.h
+fpsgame/server-standalone.o: shared/iengine.h shared/igame.h fpsgame/ai.h
+fpsgame/server-standalone.o: fpsgame/capture.h fpsgame/ctf.h
+fpsgame/server-standalone.o: fpsgame/collect.h fpsgame/extinfo.h
+fpsgame/server-standalone.o: fpsgame/aiman.h
+engine/master-standalone.o: shared/cube.h shared/tools.h shared/geom.h
+engine/master-standalone.o: shared/ents.h shared/command.h shared/iengine.h
+engine/master-standalone.o: shared/igame.h
diff --git a/src/engine/3dgui.cpp b/src/engine/3dgui.cpp
new file mode 100644 (file)
index 0000000..f1f6ef2
--- /dev/null
@@ -0,0 +1,1398 @@
+// creates multiple gui windows that float inside the 3d world
+
+// special feature is that its mostly *modeless*: you can use this menu while playing, without turning menus on or off
+// implementationwise, it is *stateless*: it keeps no internal gui structure, hit tests are instant, usage & implementation is greatly simplified
+
+#include "engine.h"
+
+#include "textedit.h"
+
+static bool layoutpass, actionon = false;
+static int mousebuttons = 0;
+static struct gui *windowhit = NULL;
+
+static float firstx, firsty;
+
+enum {FIELDCOMMIT, FIELDABORT, FIELDEDIT, FIELDSHOW, FIELDKEY};
+
+static int fieldmode = FIELDSHOW; 
+static bool fieldsactive = false;
+
+static bool hascursor;
+static float cursorx = 0.5f, cursory = 0.5f;
+
+#define SHADOW 4
+#define ICON_SIZE (FONTH-SHADOW)
+#define SKIN_W 256
+#define SKIN_H 128
+#define SKIN_SCALE 4
+#define INSERT (3*SKIN_SCALE)
+#define MAXCOLUMNS 16
+
+VARP(guiautotab, 6, 16, 40);
+VARP(guiclicktab, 0, 0, 1);
+VARP(guifadein, 0, 1, 1);
+VARP(guipreviewtime, 0, 15, 1000);
+
+static int lastpreview = 0;
+
+static inline bool throttlepreview(bool loaded)
+{
+    if(loaded) return true;
+    if(totalmillis - lastpreview < guipreviewtime) return false;
+    lastpreview = totalmillis;
+    return true;
+}
+
+struct gui : g3d_gui
+{
+    struct list
+    {
+        int parent, w, h, springs, curspring, column;
+    };
+
+    int firstlist, nextlist;
+    int columns[MAXCOLUMNS];
+
+    static vector<list> lists;
+    static float hitx, hity;
+    static int curdepth, curlist, xsize, ysize, curx, cury;
+    static bool shouldmergehits, shouldautotab;
+
+    static void reset()
+    {
+        lists.setsize(0);
+    }
+
+    static int ty, tx, tpos, *tcurrent, tcolor; //tracking tab size and position since uses different layout method...
+
+    bool allowautotab(bool on)
+    {
+        bool oldval = shouldautotab;
+        shouldautotab = on;
+        return oldval;
+    }
+
+    void autotab() 
+    { 
+        if(tcurrent)
+        {
+            if(layoutpass && !tpos) tcurrent = NULL; //disable tabs because you didn't start with one
+            if(shouldautotab && !curdepth && (layoutpass ? 0 : cury) + ysize > guiautotab*FONTH) tab(NULL, tcolor); 
+        }
+    }
+
+    bool shouldtab()
+    {
+        if(tcurrent && shouldautotab)
+        {
+            if(layoutpass)
+            {
+                int space = guiautotab*FONTH - ysize;
+                if(space < 0) return true;
+                int l = lists[curlist].parent;
+                while(l >= 0)
+                {
+                    space -= lists[l].h;
+                    if(space < 0) return true;
+                    l = lists[l].parent;
+                }
+            }
+            else
+            {
+                int space = guiautotab*FONTH - cury;
+                if(ysize > space) return true;
+                int l = lists[curlist].parent;
+                while(l >= 0)
+                {
+                    if(lists[l].h > space) return true;
+                    l = lists[l].parent;
+                }
+            }
+        }
+        return false;
+    }
+
+    bool visible() { return (!tcurrent || tpos==*tcurrent) && !layoutpass; }
+
+    //tab is always at top of page
+    void tab(const char *name, int color) 
+    {
+        if(curdepth != 0) return;
+        if(color) tcolor = color;
+        tpos++; 
+        if(!name) name = intstr(tpos); 
+        int w = max(text_width(name) - 2*INSERT, 0);
+        if(layoutpass) 
+        {  
+            ty = max(ty, ysize); 
+            ysize = 0;
+        }
+        else 
+        {      
+            cury = -ysize;
+            int h = FONTH-2*INSERT,
+                x1 = curx + tx,
+                x2 = x1 + w + ((skinx[3]-skinx[2]) + (skinx[5]-skinx[4]))*SKIN_SCALE,
+                y1 = cury - ((skiny[6]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE-h,
+                y2 = cury;
+            bool hit = tcurrent && windowhit==this && hitx>=x1 && hity>=y1 && hitx<x2 && hity<y2;
+            if(hit && (!guiclicktab || mousebuttons&G3D_DOWN)) 
+                *tcurrent = tpos; //roll-over to switch tab
+            
+            drawskin(x1-skinx[visible()?2:6]*SKIN_SCALE, y1-skiny[1]*SKIN_SCALE, w, h, visible()?10:19, 9, gui2d ? 1 : 2, light, alpha);
+            text_(name, x1 + (skinx[3]-skinx[2])*SKIN_SCALE - (w ? INSERT : INSERT/2), y1 + (skiny[2]-skiny[1])*SKIN_SCALE - INSERT, tcolor, visible());
+        }
+        tx += w + ((skinx[5]-skinx[4]) + (skinx[3]-skinx[2]))*SKIN_SCALE; 
+    }
+
+    bool ishorizontal() const { return curdepth&1; }
+    bool isvertical() const { return !ishorizontal(); }
+
+    void pushlist()
+    {  
+        if(layoutpass)
+        {
+            if(curlist>=0)
+            {
+                lists[curlist].w = xsize;
+                lists[curlist].h = ysize;
+            }
+            list &l = lists.add();
+            l.parent = curlist;
+            l.springs = 0;
+            l.column = -1;
+            curlist = lists.length()-1;
+            xsize = ysize = 0;
+        }
+        else
+        {
+            curlist = nextlist++;
+            if(curlist >= lists.length()) // should never get here unless script code doesn't use same amount of lists in layout and render passes
+            {
+                list &l = lists.add();
+                l.parent = curlist;
+                l.springs = 0;
+                l.column = -1;
+                l.w = l.h = 0;
+            }
+            list &l = lists[curlist];
+            l.curspring = 0;
+            if(l.springs > 0)
+            {
+                if(ishorizontal()) xsize = l.w; else ysize = l.h;
+            }
+            else
+            {
+                xsize = l.w;
+                ysize = l.h;
+            }
+        }
+        curdepth++;    
+    }
+
+    void poplist()
+    {
+        if(!lists.inrange(curlist)) return;
+        list &l = lists[curlist];
+        if(layoutpass)
+        {
+            l.w = xsize;
+            l.h = ysize;
+            if(l.column >= 0) columns[l.column] = max(columns[l.column], ishorizontal() ? ysize : xsize);
+        }
+        curlist = l.parent;
+        curdepth--;
+        if(lists.inrange(curlist))
+        {   
+            int w = xsize, h = ysize;
+            if(ishorizontal()) cury -= h; else curx -= w;
+            list &p = lists[curlist];
+            xsize = p.w;
+            ysize = p.h;
+            if(!layoutpass && p.springs > 0)
+            {
+                list &s = lists[p.parent];
+                if(ishorizontal()) xsize = s.w; else ysize = s.h;
+            } 
+            layout(w, h);
+        }
+    }
+
+    int text  (const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, false, false); }
+    int button(const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, true, false); }
+    int title (const char *text, int color, const char *icon) { autotab(); return button_(text, color, icon, false, true); }
+
+    void separator() { autotab(); line_(FONTH/3); }
+    void progress(float percent) { autotab(); line_((FONTH*4)/5, percent); }
+
+    //use to set min size (useful when you have progress bars)
+    void strut(float size) { layout(isvertical() ? int(size*FONTW) : 0, isvertical() ? 0 : int(size*FONTH)); }
+    //add space between list items
+    void space(float size) { layout(isvertical() ? 0 : int(size*FONTW), isvertical() ? int(size*FONTH) : 0); }
+
+    void spring(int weight) 
+    { 
+        if(curlist < 0) return;
+        list &l = lists[curlist];
+        if(layoutpass) { if(l.parent >= 0) l.springs += weight; return; }
+        int nextspring = min(l.curspring + weight, l.springs);
+        if(nextspring <= l.curspring) return;
+        if(ishorizontal())
+        {
+            int w = xsize - l.w;
+            layout((w*nextspring)/l.springs - (w*l.curspring)/l.springs, 0);
+        }
+        else
+        {
+            int h = ysize - l.h;
+            layout(0, (h*nextspring)/l.springs - (h*l.curspring)/l.springs);
+        }
+        l.curspring = nextspring;
+    }
+
+    void column(int col)
+    {
+        if(curlist < 0 || !layoutpass || col < 0 || col >= MAXCOLUMNS) return;
+        list &l = lists[curlist];
+        l.column = col;
+    }
+
+    int layout(int w, int h)
+    {
+        if(layoutpass)
+        {
+            if(ishorizontal())
+            {
+                xsize += w;
+                ysize = max(ysize, h);
+            }
+            else
+            {
+                xsize = max(xsize, w);
+                ysize += h;
+            }
+            return 0;
+        }
+        else
+        {
+            bool hit = ishit(w, h);
+            if(ishorizontal()) curx += w;
+            else cury += h;
+            return (hit && visible()) ? mousebuttons|G3D_ROLLOVER : 0;
+        }
+    }
+
+    bool mergehits(bool on) 
+    { 
+        bool oldval = shouldmergehits;
+        shouldmergehits = on; 
+        return oldval;
+    }
+
+    bool ishit(int w, int h, int x = curx, int y = cury)
+    {
+        if(shouldmergehits) return windowhit==this && (ishorizontal() ? hitx>=x && hitx<x+w : hity>=y && hity<y+h);
+        if(ishorizontal()) h = ysize;
+        else w = xsize;
+        return windowhit==this && hitx>=x && hity>=y && hitx<x+w && hity<y+h;
+    }
+
+    int image(Texture *t, float scale, const char *overlaid)
+    {
+        autotab();
+        if(scale==0) scale = 1;
+        int size = (int)(scale*2*FONTH)-SHADOW;
+        if(visible()) icon_(t, overlaid!=NULL, curx, cury, size, ishit(size+SHADOW, size+SHADOW), overlaid);
+        return layout(size+SHADOW, size+SHADOW);
+    }
+    
+    int texture(VSlot &vslot, float scale, bool overlaid)
+    {
+        autotab();
+        if(scale==0) scale = 1;
+        int size = (int)(scale*2*FONTH)-SHADOW;
+        if(visible()) previewslot(vslot, overlaid, curx, cury, size, ishit(size+SHADOW, size+SHADOW));
+        return layout(size+SHADOW, size+SHADOW);
+    }
+
+    int playerpreview(int model, int team, int weap, float sizescale, const char *overlaid)
+    {
+        autotab();
+        if(sizescale==0) sizescale = 1;
+        int size = (int)(sizescale*2*FONTH)-SHADOW;
+        if(model>=0 && visible())
+        {
+            bool hit = ishit(size+SHADOW, size+SHADOW);
+            float xs = size, ys = size, xi = curx, yi = cury;
+            if(overlaid && hit && actionon)
+            {
+                hudnotextureshader->set();
+                gle::colorf(0, 0, 0, 0.75f);
+                rect_(xi+SHADOW, yi+SHADOW, xs, ys);
+                hudshader->set();
+            }
+            int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))),
+                x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y))));
+            glDisable(GL_BLEND);
+            modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL);
+            game::renderplayerpreview(model, team, weap);
+            modelpreview::end();
+            hudshader->set();
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            glEnable(GL_BLEND);
+            if(overlaid)
+            {
+                if(hit)
+                {
+                    hudnotextureshader->set();
+                    glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+                    gle::colorf(1, 0.5f, 0.5f);
+                    rect_(xi, yi, xs, ys);
+                    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                    hudshader->set();
+                }
+                if(overlaid[0]) text_(overlaid, xi + xs/12, yi + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit, hit);
+                if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+                gle::color(light);
+                glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+                rect_(xi, yi, xs, ys, 0);
+            }
+        }
+        return layout(size+SHADOW, size+SHADOW);
+    }
+
+    int modelpreview(const char *name, int anim, float sizescale, const char *overlaid, bool throttle)
+    {
+        autotab();
+        if(sizescale==0) sizescale = 1;
+        int size = (int)(sizescale*2*FONTH)-SHADOW;
+        if(name[0] && visible() && (!throttle || throttlepreview(modelloaded(name))))
+        {
+            bool hit = ishit(size+SHADOW, size+SHADOW);
+            float xs = size, ys = size, xi = curx, yi = cury;
+            if(overlaid && hit && actionon)
+            {
+                hudnotextureshader->set();
+                gle::colorf(0, 0, 0, 0.75f);
+                rect_(xi+SHADOW, yi+SHADOW, xs, ys);
+                hudshader->set();
+            }
+            int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))),
+                x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y))));
+            glDisable(GL_BLEND);
+            modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL);
+            model *m = loadmodel(name);
+            if(m)
+            {
+                entitylight light;
+                light.color = vec(1, 1, 1);
+                light.dir = vec(0, -1, 2).normalize();
+                vec center, radius;
+                m->boundbox(center, radius);
+                float yaw;
+                vec o = calcmodelpreviewpos(radius, yaw).sub(center);
+                rendermodel(&light, name, anim, o, yaw, 0, 0, NULL, NULL, 0);
+            }
+            modelpreview::end();
+            hudshader->set();
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            glEnable(GL_BLEND);
+            if(overlaid)
+            {
+                if(hit)
+                {
+                    hudnotextureshader->set();
+                    glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+                    gle::colorf(1, 0.5f, 0.5f);
+                    rect_(xi, yi, xs, ys);
+                    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                    hudshader->set();
+                }
+                if(overlaid[0]) text_(overlaid, xi + xs/12, yi + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit, hit);
+                if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+                gle::color(light);
+                glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+                rect_(xi, yi, xs, ys, 0);
+            }
+        }
+        return layout(size+SHADOW, size+SHADOW);
+    }
+
+    int prefabpreview(const char *prefab, const vec &color, float sizescale, const char *overlaid, bool throttle)
+    {
+        autotab();
+        if(sizescale==0) sizescale = 1;
+        int size = (int)(sizescale*2*FONTH)-SHADOW;
+        if(prefab[0] && visible() && (!throttle || throttlepreview(prefabloaded(prefab))))
+        {
+            bool hit = ishit(size+SHADOW, size+SHADOW);
+            float xs = size, ys = size, xi = curx, yi = cury;
+            if(overlaid && hit && actionon)
+            {
+                hudnotextureshader->set();
+                gle::colorf(0, 0, 0, 0.75f);
+                rect_(xi+SHADOW, yi+SHADOW, xs, ys);
+                hudshader->set();
+            }
+            int x1 = int(floor(screenw*(xi*scale.x+origin.x))), y1 = int(floor(screenh*(1 - ((yi+ys)*scale.y+origin.y)))),
+                x2 = int(ceil(screenw*((xi+xs)*scale.x+origin.x))), y2 = int(ceil(screenh*(1 - (yi*scale.y+origin.y))));
+            glDisable(GL_BLEND);
+            modelpreview::start(x1, y1, x2-x1, y2-y1, overlaid!=NULL);
+            previewprefab(prefab, color);
+            modelpreview::end();
+            hudshader->set();
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            glEnable(GL_BLEND);
+            if(overlaid)
+            {
+                if(hit)
+                {
+                    hudnotextureshader->set();
+                    glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+                    gle::colorf(1, 0.5f, 0.5f);
+                    rect_(xi, yi, xs, ys);
+                    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                    hudshader->set();
+                }
+                if(overlaid[0]) text_(overlaid, xi + FONTH/2, yi + FONTH/2, hit ? 0xFF0000 : 0xFFFFFF, hit, hit);
+                if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+                gle::color(light);
+                glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+                rect_(xi, yi, xs, ys, 0);
+            }
+        }
+        return layout(size+SHADOW, size+SHADOW);
+    }
+    void slider(int &val, int vmin, int vmax, int color, const char *label)
+    {
+        autotab();
+        int x = curx;
+        int y = cury;
+        line_((FONTH*2)/3);
+        if(visible())
+        {
+            if(!label) label = intstr(val);
+            int w = text_width(label);
+
+            bool hit;
+            int px, py, offset = vmin < vmax ? clamp(val, vmin, vmax) : clamp(val, vmax, vmin);
+            if(ishorizontal())
+            {
+                hit = ishit(FONTH, ysize, x, y);
+                px = x + (FONTH-w)/2;
+                py = y + (ysize-FONTH) - ((ysize-FONTH)*(offset-vmin))/((vmax==vmin) ? 1 : (vmax-vmin)); //vmin at bottom
+            }
+            else
+            {
+                hit = ishit(xsize, FONTH, x, y);
+                px = x + FONTH/2 - w/2 + ((xsize-w)*(offset-vmin))/((vmax==vmin) ? 1 : (vmax-vmin)); //vmin at left
+                py = y;
+            }
+
+            if(hit) color = 0xFF0000;
+            text_(label, px, py, color, hit && actionon, hit);
+            if(hit && actionon)
+            {
+                int vnew = (vmin < vmax ? 1 : -1)+vmax-vmin;
+                if(ishorizontal()) vnew = int((vnew*(y+ysize-FONTH/2-hity))/(ysize-FONTH));
+                else vnew = int((vnew*(hitx-x-FONTH/2))/(xsize-w));
+                vnew += vmin;
+                vnew = vmin < vmax ? clamp(vnew, vmin, vmax) : clamp(vnew, vmax, vmin);
+                if(vnew != val) val = vnew;
+            }
+        }
+    }
+
+    char *field(const char *name, int color, int length, int height, const char *initval, int initmode)
+    {
+        return field_(name, color, length, height, initval, initmode, FIELDEDIT);
+    }
+
+    char *keyfield(const char *name, int color, int length, int height, const char *initval, int initmode)
+    {
+        return field_(name, color, length, height, initval, initmode, FIELDKEY);
+    }
+
+    char *field_(const char *name, int color, int length, int height, const char *initval, int initmode, int fieldtype = FIELDEDIT)
+    {  
+        editor *e = useeditor(name, initmode, false, initval); // generate a new editor if necessary
+        if(layoutpass)
+        {
+            if(initval && e->mode==EDITORFOCUSED && (e!=currentfocus() || fieldmode == FIELDSHOW))
+            {
+                if(strcmp(e->lines[0].text, initval)) e->clear(initval);
+            }
+            e->linewrap = (length<0);
+            e->maxx = (e->linewrap) ? -1 : length;
+            e->maxy = (height<=0)?1:-1;
+            e->pixelwidth = abs(length)*FONTW;
+            if(e->linewrap && e->maxy==1) 
+            {
+                int temp;
+                text_bounds(e->lines[0].text, temp, e->pixelheight, e->pixelwidth); //only single line editors can have variable height
+            }
+            else 
+                e->pixelheight = FONTH*max(height, 1); 
+        }
+        int h = e->pixelheight;
+        int w = e->pixelwidth + FONTW;
+        
+        bool wasvertical = isvertical();
+        if(wasvertical && e->maxy != 1) pushlist();
+        
+        char *result = NULL;
+        if(visible() && !layoutpass)
+        {
+            e->rendered = true;
+
+            bool hit = ishit(w, h);
+            if(hit) 
+            {
+                if(mousebuttons&G3D_DOWN) //mouse request focus
+                {   
+                    if(fieldtype==FIELDKEY) e->clear();
+                    useeditor(name, initmode, true); 
+                    e->mark(false);
+                    fieldmode = fieldtype;
+                } 
+            }
+            bool editing = (fieldmode != FIELDSHOW) && (e==currentfocus());
+            if(hit && editing && (mousebuttons&G3D_PRESSED)!=0 && fieldtype==FIELDEDIT) e->hit(int(floor(hitx-(curx+FONTW/2))), int(floor(hity-cury)), (mousebuttons&G3D_DRAGGED)!=0); //mouse request position
+            if(editing && ((fieldmode==FIELDCOMMIT) || (fieldmode==FIELDABORT) || !hit)) // commit field if user pressed enter or wandered out of focus 
+            {
+                if(fieldmode==FIELDCOMMIT || (fieldmode!=FIELDABORT && !hit)) result = e->currentline().text;
+                e->active = (e->mode!=EDITORFOCUSED);
+                fieldmode = FIELDSHOW;
+            } 
+            else fieldsactive = true;
+            
+            e->draw(curx+FONTW/2, cury, color, hit && editing);
+            
+            hudnotextureshader->set();
+            glDisable(GL_BLEND);
+            if(editing) gle::colorf(1, 0, 0);
+            else gle::colorub(color>>16, (color>>8)&0xFF, color&0xFF);
+            rect_(curx, cury, w, h, true);
+            glEnable(GL_BLEND);
+            hudshader->set();
+        }
+        layout(w, h);
+        
+        if(e->maxy != 1)
+        {
+            int slines = e->limitscrolly();
+            if(slines > 0) 
+            {
+                int pos = e->scrolly;
+                slider(e->scrolly, slines, 0, color, NULL);
+                if(pos != e->scrolly) e->cy = e->scrolly; 
+            }
+            if(wasvertical) poplist();
+        }
+        
+        return result;
+    }
+
+    void rect_(float x, float y, float w, float h, bool lines = false)
+    {
+        gle::defvertex(2);
+        gle::begin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
+        gle::attribf(x, y);
+        gle::attribf(x + w, y);
+        if(lines) gle::attribf(x + w, y + h);
+        gle::attribf(x, y + h);
+        if(!lines) gle::attribf(x + w, y + h);
+        xtraverts += gle::end();
+    }
+
+    void rect_(float x, float y, float w, float h, int usetc)
+    {
+        gle::defvertex(2);
+        gle::deftexcoord0();
+        gle::begin(GL_TRIANGLE_STRIP);
+        static const vec2 tc[5] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1), vec2(0, 0) };
+        gle::attribf(x, y); gle::attrib(tc[usetc]);
+        gle::attribf(x + w, y); gle::attrib(tc[usetc+1]);
+        gle::attribf(x, y + h); gle::attrib(tc[usetc+3]);
+        gle::attribf(x + w, y + h); gle::attrib(tc[usetc+2]);
+        xtraverts += gle::end();
+    }
+
+    void text_(const char *text, int x, int y, int color, bool shadow, bool force = false) 
+    {
+        if(shadow) draw_text(text, x+SHADOW, y+SHADOW, 0x00, 0x00, 0x00, -0xC0);
+        draw_text(text, x, y, color>>16, (color>>8)&0xFF, color&0xFF, force ? -0xFF : 0xFF);
+    }
+
+    void background(int color, int inheritw, int inherith)
+    {
+        if(layoutpass) return;
+        hudnotextureshader->set();
+        gle::colorub(color>>16, (color>>8)&0xFF, color&0xFF, 0x80);
+        int w = xsize, h = ysize;
+        if(inheritw>0) 
+        {
+            int parentw = curlist, parentdepth = 0;
+            for(;parentdepth < inheritw && lists[parentw].parent>=0; parentdepth++)
+                parentw = lists[parentw].parent;
+            list &p = lists[parentw];
+            w = p.springs > 0 && (curdepth-parentdepth)&1 ? lists[p.parent].w : p.w;
+        }
+        if(inherith>0)
+        {
+            int parenth = curlist, parentdepth = 0;
+            for(;parentdepth < inherith && lists[parenth].parent>=0; parentdepth++)
+                parenth = lists[parenth].parent;
+            list &p = lists[parenth];
+            h = p.springs > 0 && !((curdepth-parentdepth)&1) ? lists[p.parent].h : p.h;
+        }
+        rect_(curx, cury, w, h);
+        hudshader->set();
+    }
+
+    void icon_(Texture *t, bool overlaid, int x, int y, int size, bool hit, const char *title = NULL)
+    {
+        float scale = float(size)/max(t->xs, t->ys); //scale and preserve aspect ratio
+        float xs = t->xs*scale, ys = t->ys*scale;
+        x += int((size-xs)/2);
+        y += int((size-ys)/2);
+        const vec &color = hit ? vec(1, 0.5f, 0.5f) : (overlaid ? vec(1, 1, 1) : light);
+        glBindTexture(GL_TEXTURE_2D, t->id);
+        if(hit && actionon)
+        {
+            gle::colorf(0, 0, 0, 0.75f);
+            rect_(x+SHADOW, y+SHADOW, xs, ys, 0);
+        }
+        gle::color(color);
+        rect_(x, y, xs, ys, 0);
+
+        if(overlaid)
+        {
+            if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+            glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+            gle::color(light);
+            rect_(x, y, xs, ys, 0);
+            if(title) text_(title, x + xs/12, y + ys - ys/12 - FONTH, hit ? 0xFF0000 : 0xFFFFFF, hit && actionon, hit);
+        }
+    }        
+
+    void previewslot(VSlot &vslot, bool overlaid, int x, int y, int size, bool hit)
+    {
+        Slot &slot = *vslot.slot;
+        if(slot.sts.empty()) return;
+        VSlot *layer = NULL;
+        Texture *t = NULL, *glowtex = NULL, *layertex = NULL;
+        if(slot.loaded)
+        {
+            t = slot.sts[0].t;
+            if(t == notexture) return;
+            Slot &slot = *vslot.slot;
+            if(slot.texmask&(1<<TEX_GLOW)) { loopvj(slot.sts) if(slot.sts[j].type==TEX_GLOW) { glowtex = slot.sts[j].t; break; } }
+            if(vslot.layer)
+            {
+                layer = &lookupvslot(vslot.layer);
+                if(!layer->slot->sts.empty()) layertex = layer->slot->sts[0].t;
+            }
+        }
+        else if(slot.thumbnail && slot.thumbnail != notexture) t = slot.thumbnail;
+        else return;
+        float xt = min(1.0f, t->xs/(float)t->ys), yt = min(1.0f, t->ys/(float)t->xs), xs = size, ys = size;
+        if(hit && actionon) 
+        {
+            hudnotextureshader->set();
+            gle::colorf(0, 0, 0, 0.75f);
+            rect_(x+SHADOW, y+SHADOW, xs, ys);
+            hudshader->set();  
+        }
+        SETSHADER(hudrgb);
+        gle::defvertex(2);
+        gle::deftexcoord0();
+        const vec &color = hit ? vec(1, 0.5f, 0.5f) : (overlaid ? vec(1, 1, 1) : light);
+        vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
+        float xoff = vslot.offset.x, yoff = vslot.offset.y;
+        if(vslot.rotation)
+        {
+            const texrotation &r = texrotations[vslot.rotation];
+            if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); }
+            if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; }
+            if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; }
+        }
+        loopk(4) { tc[k].x = tc[k].x/xt - xoff/t->xs; tc[k].y = tc[k].y/yt - yoff/t->ys; } 
+        if(slot.loaded) gle::color(vec(color).mul(vslot.colorscale));
+        else gle::color(color);
+        glBindTexture(GL_TEXTURE_2D, t->id);
+        gle::begin(GL_TRIANGLE_STRIP);
+        gle::attribf(x,    y);    gle::attrib(tc[0]);
+        gle::attribf(x+xs, y);    gle::attrib(tc[1]);
+        gle::attribf(x,    y+ys); gle::attrib(tc[3]);
+        gle::attribf(x+xs, y+ys); gle::attrib(tc[2]);
+        gle::end();
+        if(glowtex)
+        {
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+            glBindTexture(GL_TEXTURE_2D, glowtex->id);
+            if(hit || overlaid) gle::color(vec(vslot.glowcolor).mul(color));
+            else gle::color(vslot.glowcolor);
+            gle::begin(GL_TRIANGLE_STRIP);
+            gle::attribf(x,    y);    gle::attrib(tc[0]);
+            gle::attribf(x+xs, y);    gle::attrib(tc[1]);
+            gle::attribf(x,    y+ys); gle::attrib(tc[3]);
+            gle::attribf(x+xs, y+ys); gle::attrib(tc[2]);
+            gle::end();
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        }
+        if(layertex)
+        {
+            glBindTexture(GL_TEXTURE_2D, layertex->id);
+            gle::color(vec(color).mul(layer->colorscale));
+            gle::begin(GL_TRIANGLE_STRIP);
+            gle::attribf(x+xs/2, y+ys/2); gle::attrib(tc[0]);
+            gle::attribf(x+xs,   y+ys/2); gle::attrib(tc[1]);
+            gle::attribf(x+xs/2, y+ys);   gle::attrib(tc[3]);
+            gle::attribf(x+xs,   y+ys);   gle::attrib(tc[2]);
+            gle::end();
+        }
+            
+        hudshader->set();
+        if(overlaid) 
+        {
+            if(!overlaytex) overlaytex = textureload("data/guioverlay.png", 3);
+            glBindTexture(GL_TEXTURE_2D, overlaytex->id);
+            gle::color(light);
+            rect_(x, y, xs, ys, 0);
+        }
+    }
+
+    void line_(int size, float percent = 1.0f)
+    {          
+        if(visible())
+        {
+            if(!slidertex) slidertex = textureload("data/guislider.png", 3);
+            glBindTexture(GL_TEXTURE_2D, slidertex->id);
+            if(percent < 0.99f) 
+            {
+                gle::colorf(light.x, light.y, light.z, 0.375f);
+                if(ishorizontal()) 
+                    rect_(curx + FONTH/2 - size/2, cury, size, ysize, 0);
+                else
+                    rect_(curx, cury + FONTH/2 - size/2, xsize, size, 1);
+            }
+            gle::color(light);
+            if(ishorizontal()) 
+                rect_(curx + FONTH/2 - size/2, cury + ysize*(1-percent), size, ysize*percent, 0);
+            else 
+                rect_(curx, cury + FONTH/2 - size/2, xsize*percent, size, 1);
+        }
+        layout(ishorizontal() ? FONTH : 0, ishorizontal() ? 0 : FONTH);
+    }
+
+    void textbox(const char *text, int width, int height, int color) 
+    {
+        width *= FONTW;
+        height *= FONTH;
+        int w, h;
+        text_bounds(text, w, h, width);
+        if(h > height) height = h;
+        if(visible()) draw_text(text, curx, cury, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, -1, width);
+        layout(width, height);
+    }
+
+    int button_(const char *text, int color, const char *icon, bool clickable, bool center)
+    {
+        const int padding = 10;
+        int w = 0;
+        if(icon) w += ICON_SIZE;
+        if(icon && text) w += padding;
+        if(text) w += text_width(text);
+    
+        if(visible())
+        {
+            bool hit = ishit(w, FONTH);
+            if(hit && clickable) color = 0xFF0000;     
+            int x = curx;      
+            if(isvertical() && center) x += (xsize-w)/2;
+        
+            if(icon)
+            {
+                if(icon[0] != ' ')
+                {
+                    const char *ext = strrchr(icon, '.');
+                    defformatstring(tname, "packages/icons/%s%s", icon, ext ? "" : ".jpg");
+                    icon_(textureload(tname, 3), false, x, cury, ICON_SIZE, clickable && hit);
+                }
+                x += ICON_SIZE;
+            }
+            if(icon && text) x += padding;
+            if(text) text_(text, x, cury, color, center || (hit && clickable && actionon), hit && clickable);
+        }
+        return layout(w, FONTH);
+    }
+
+    static Texture *skintex, *overlaytex, *slidertex;
+    static const int skinx[], skiny[];
+    static const struct patch { ushort left, right, top, bottom; uchar flags; } patches[];
+
+    static void drawskin(int x, int y, int gapw, int gaph, int start, int n, int passes = 1, const vec &light = vec(1, 1, 1), float alpha = 0.80f)//int vleft, int vright, int vtop, int vbottom, int start, int n) 
+    {
+        if(!skintex) skintex = textureload("data/guiskin.png", 3);
+        glBindTexture(GL_TEXTURE_2D, skintex->id);
+        int gapx1 = INT_MAX, gapy1 = INT_MAX, gapx2 = INT_MAX, gapy2 = INT_MAX;
+        float wscale = 1.0f/(SKIN_W*SKIN_SCALE), hscale = 1.0f/(SKIN_H*SKIN_SCALE);
+        
+        loopj(passes)
+        {      
+            bool quads = false;
+            if(passes>1) glDepthFunc(j ? GL_LEQUAL : GL_GREATER);
+            gle::color(j ? light : vec(1, 1, 1), passes<=1 || j ? alpha : alpha/2); //ghost when its behind something in depth
+            loopi(n)
+            {
+                const patch &p = patches[start+i];
+                int left = skinx[p.left]*SKIN_SCALE, right = skinx[p.right]*SKIN_SCALE,
+                    top = skiny[p.top]*SKIN_SCALE, bottom = skiny[p.bottom]*SKIN_SCALE;
+                float tleft = left*wscale, tright = right*wscale,
+                      ttop = top*hscale, tbottom = bottom*hscale;
+                if(p.flags&0x1)
+                {
+                    gapx1 = left;
+                    gapx2 = right;
+                }
+                else if(left >= gapx2)
+                {
+                    left += gapw - (gapx2-gapx1);
+                    right += gapw - (gapx2-gapx1);
+                }
+                if(p.flags&0x10)
+                {
+                    gapy1 = top;
+                    gapy2 = bottom;
+                }
+                else if(top >= gapy2)
+                {
+                    top += gaph - (gapy2-gapy1);
+                    bottom += gaph - (gapy2-gapy1);
+                }
+               
+                //multiple tiled quads if necessary rather than a single stretched one
+                int ystep = bottom-top;
+                int yo = y+top;
+                while(ystep > 0) 
+                {
+                    if(p.flags&0x10 && yo+ystep-(y+top) > gaph) 
+                    {
+                        ystep = gaph+y+top-yo;
+                        tbottom = ttop+ystep*hscale;
+                    }
+                    int xstep = right-left;
+                    int xo = x+left;
+                    float tright2 = tright;
+                    while(xstep > 0) 
+                    {
+                        if(p.flags&0x01 && xo+xstep-(x+left) > gapw) 
+                        {
+                            xstep = gapw+x+left-xo; 
+                            tright = tleft+xstep*wscale;
+                        }
+                        if(!quads)
+                        {
+                            quads = true;
+                            gle::defvertex(2);
+                            gle::deftexcoord0();
+                            gle::begin(GL_QUADS);
+                        }
+                        gle::attribf(xo,       yo);       gle::attribf(tleft,  ttop);
+                        gle::attribf(xo+xstep, yo);       gle::attribf(tright, ttop);
+                        gle::attribf(xo+xstep, yo+ystep); gle::attribf(tright, tbottom);
+                        gle::attribf(xo,       yo+ystep); gle::attribf(tleft,  tbottom);
+                        if(!(p.flags&0x01)) break;
+                        xo += xstep;
+                    }
+                    tright = tright2;
+                    if(!(p.flags&0x10)) break;
+                    yo += ystep;
+                }
+            }
+            if(quads) xtraverts += gle::end();
+            else break; //if it didn't happen on the first pass, it won't happen on the second..
+        }
+        if(passes>1) glDepthFunc(GL_ALWAYS);
+    } 
+
+    vec origin, scale, *savedorigin;
+    float dist;
+    g3d_callback *cb;
+    bool gui2d;
+
+    static float basescale, maxscale;
+    static bool passthrough;
+    static float alpha;
+    static vec light;
+
+    void adjustscale()
+    {
+        int w = xsize + (skinx[2]-skinx[1])*SKIN_SCALE + (skinx[10]-skinx[9])*SKIN_SCALE, h = ysize + (skiny[9]-skiny[7])*SKIN_SCALE;
+        if(tcurrent) h += ((skiny[5]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE + FONTH-2*INSERT;
+        else h += (skiny[6]-skiny[3])*SKIN_SCALE;
+
+        float aspect = forceaspect ? 1.0f/forceaspect : float(screenh)/float(screenw), fit = 1.0f;
+        if(w*aspect*basescale>1.0f) fit = 1.0f/(w*aspect*basescale);
+        if(h*basescale*fit>maxscale) fit *= maxscale/(h*basescale*fit);
+        origin = vec(0.5f-((w-xsize)/2 - (skinx[2]-skinx[1])*SKIN_SCALE)*aspect*scale.x*fit, 0.5f + (0.5f*h-(skiny[9]-skiny[7])*SKIN_SCALE)*scale.y*fit, 0);
+        scale = vec(aspect*scale.x*fit, scale.y*fit, 1);
+    }
+
+    void start(int starttime, float initscale, int *tab, bool allowinput)
+    {  
+        if(gui2d) 
+        {
+            initscale *= 0.025f; 
+            if(allowinput) hascursor = true;
+        }
+        basescale = initscale;
+        if(layoutpass) scale.x = scale.y = scale.z = guifadein ? basescale*min((totalmillis-starttime)/300.0f, 1.0f) : basescale;
+        alpha = allowinput ? 0.80f : 0.60f;
+        passthrough = scale.x<basescale || !allowinput;
+        curdepth = -1;
+        curlist = -1;
+        tpos = 0;
+        tx = 0;
+        ty = 0;
+        tcurrent = tab;
+        tcolor = 0xFFFFFF;
+        pushlist();
+        if(layoutpass) 
+        {
+            firstlist = nextlist = curlist;
+            memset(columns, 0, sizeof(columns));
+        }
+        else
+        {
+            if(tcurrent && !*tcurrent) tcurrent = NULL;
+            cury = -ysize; 
+            curx = -xsize/2;
+            
+            if(gui2d)
+            {
+                hudmatrix.ortho(0, 1, 1, 0, -1, 1);
+                hudmatrix.translate(origin);        
+                hudmatrix.scale(scale);
+
+                light = vec(1, 1, 1);
+            }
+            else
+            {
+                float yaw = atan2f(origin.y-camera1->o.y, origin.x-camera1->o.x);
+                hudmatrix = camprojmatrix;
+                hudmatrix.translate(origin);
+                hudmatrix.rotate_around_z(yaw - 90*RAD);
+                hudmatrix.rotate_around_x(-90*RAD);
+                hudmatrix.scale(-scale.x, scale.y, scale.z);
+            
+                vec dir;
+                lightreaching(origin, light, dir, false, 0, 0.5f); 
+                float intensity = vec(yaw, 0.0f).dot(dir);
+                light.mul(1.0f + max(intensity, 0.0f));
+            }
+
+            resethudmatrix();
+            hudshader->set();
+
+            drawskin(curx-skinx[2]*SKIN_SCALE, cury-skiny[6]*SKIN_SCALE, xsize, ysize, 0, 9, gui2d ? 1 : 2, light, alpha);
+            if(!tcurrent) drawskin(curx-skinx[5]*SKIN_SCALE, cury-skiny[6]*SKIN_SCALE, xsize, 0, 9, 1, gui2d ? 1 : 2, light, alpha);
+        }
+    }
+
+    void adjusthorizontalcolumn(int col, int i)
+    {
+        int h = columns[col], dh = 0;
+        for(int d = 1; i >= 0; d ^= 1)
+        {
+            list &p = lists[i];
+            if(d&1) { dh = h - p.h; if(dh <= 0) break; p.h = h; }
+            else { p.h += dh; h = p.h; }
+            i = p.parent;
+        }
+        ysize += max(dh, 0);
+    }
+
+    void adjustverticalcolumn(int col, int i)
+    {
+        int w = columns[col], dw = 0;
+        for(int d = 0; i >= 0; d ^= 1)
+        {
+            list &p = lists[i];
+            if(d&1) { p.w += dw; w = p.w; }
+            else { dw = w - p.w; if(dw <= 0) break; p.w = w; }
+            i = p.parent;
+        }
+        xsize = max(xsize, w);
+    }
+        
+    void adjustcolumns()
+    {
+        if(lists.inrange(curlist))
+        {
+            list &l = lists[curlist];
+            if(l.column >= 0) columns[l.column] = max(columns[l.column], ishorizontal() ? ysize : xsize);
+        }
+        int parent = -1, depth = 0;
+        for(int i = firstlist; i < lists.length(); i++)
+        {
+            list &l = lists[i];
+            if(l.parent > parent) { parent = l.parent; depth++; }
+            else if(l.parent < parent)
+            {
+                while(parent > l.parent && depth > 0)
+                {
+                    parent = lists[parent].parent;
+                    depth--;
+                }
+            }
+            if(l.column >= 0)
+            {
+                if(depth&1) adjusthorizontalcolumn(l.column, i); 
+                else adjustverticalcolumn(l.column, i);
+            }
+        }
+    }
+
+    void end()
+    {
+        if(layoutpass)
+        {      
+            adjustcolumns();
+            xsize = max(tx, xsize);
+            ysize = max(ty, ysize);
+            ysize = max(ysize, (skiny[7]-skiny[6])*SKIN_SCALE);
+            if(tcurrent) *tcurrent = max(1, min(*tcurrent, tpos));
+            if(gui2d) adjustscale();
+            if(!windowhit && !passthrough)
+            {
+                float dist = 0;
+                if(gui2d)
+                {
+                    hitx = (cursorx - origin.x)/scale.x;
+                    hity = (cursory - origin.y)/scale.y;
+                }
+                else
+                {
+                    plane p;
+                    p.toplane(vec(origin).sub(camera1->o).set(2, 0).normalize(), origin);
+                    if(p.rayintersect(camera1->o, camdir, dist) && dist>=0)
+                    {
+                        vec hitpos(camdir);
+                        hitpos.mul(dist).add(camera1->o).sub(origin);
+                        hitx = vec(-p.y, p.x, 0).dot(hitpos)/scale.x;
+                        hity = -hitpos.z/scale.y;
+                    }
+                }
+                if((mousebuttons & G3D_PRESSED) && (fabs(hitx-firstx) > 2 || fabs(hity - firsty) > 2)) mousebuttons |= G3D_DRAGGED;
+                if(dist>=0 && hitx>=-xsize/2 && hitx<=xsize/2 && hity<=0)
+                {
+                    if(hity>=-ysize || (tcurrent && hity>=-ysize-(FONTH-2*INSERT)-((skiny[6]-skiny[1])-(skiny[3]-skiny[2]))*SKIN_SCALE && hitx<=tx-xsize/2))
+                        windowhit = this;
+                }
+            }
+        }
+        else
+        {
+            if(tcurrent && tx<xsize) drawskin(curx+tx-skinx[5]*SKIN_SCALE, -ysize-skiny[6]*SKIN_SCALE, xsize-tx, FONTH, 9, 1, gui2d ? 1 : 2, light, alpha);
+        }
+        poplist();
+    }
+
+    void draw()
+    {
+        cb->gui(*this, layoutpass);
+    }
+};
+
+Texture *gui::skintex = NULL, *gui::overlaytex = NULL, *gui::slidertex = NULL;
+
+//chop skin into a grid
+const int gui::skiny[] = {0, 7, 21, 34, 43, 48, 56, 104, 111, 117, 128},
+          gui::skinx[] = {0, 11, 23, 37, 105, 119, 137, 151, 215, 229, 246, 256};
+//Note: skinx[3]-skinx[2] = skinx[7]-skinx[6]
+//      skinx[5]-skinx[4] = skinx[9]-skinx[8]           
+const gui::patch gui::patches[] = 
+{ //arguably this data can be compressed - it depends on what else needs to be skinned in the future
+    {1,2,3,6,  0},    // body
+    {2,9,5,6,  0x01},
+    {9,10,3,6, 0},
+
+    {1,2,6,7,  0x10},
+    {2,9,6,7,  0x11},
+    {9,10,6,7, 0x10},
+
+    {1,2,7,9,  0},
+    {2,9,7,9,  0x01},
+    {9,10,7,9, 0},
+
+    {5,6,3,5, 0x01}, // top
+
+    {2,3,1,2, 0},    // selected tab
+    {3,4,1,2, 0x01},
+    {4,5,1,2, 0},
+    {2,3,2,3, 0x10},
+    {3,4,2,3, 0x11},
+    {4,5,2,3, 0x10},
+    {2,3,3,5, 0},
+    {3,4,3,5, 0x01},
+    {4,5,3,5, 0},
+
+    {6,7,1,2, 0},    // deselected tab
+    {7,8,1,2, 0x01},
+    {8,9,1,2, 0},
+    {6,7,2,3, 0x10},
+    {7,8,2,3, 0x11},
+    {8,9,2,3, 0x10},
+    {6,7,3,5, 0},
+    {7,8,3,5, 0x01},
+    {8,9,3,5, 0},
+};
+
+vector<gui::list> gui::lists;
+float gui::basescale, gui::maxscale = 1, gui::hitx, gui::hity, gui::alpha;
+bool gui::passthrough, gui::shouldmergehits = false, gui::shouldautotab = true;
+vec gui::light;
+int gui::curdepth, gui::curlist, gui::xsize, gui::ysize, gui::curx, gui::cury;
+int gui::ty, gui::tx, gui::tpos, *gui::tcurrent, gui::tcolor;
+static vector<gui> guis2d, guis3d;
+
+VARP(guipushdist, 1, 4, 64);
+
+bool g3d_input(const char *str, int len)
+{
+    editor *e = currentfocus();
+    if(fieldmode == FIELDKEY || fieldmode == FIELDSHOW || !e) return false;
+    
+    e->input(str, len);
+    return true;
+}
+
+bool g3d_key(int code, bool isdown)
+{
+    editor *e = currentfocus();
+    if(fieldmode == FIELDKEY)
+    {
+        switch(code)
+        {
+            case SDLK_ESCAPE:
+                if(isdown) fieldmode = FIELDCOMMIT;
+                return true;
+        }
+        const char *keyname = getkeyname(code);
+        if(keyname && isdown)
+        {
+            if(e->lines.length()!=1 || !e->lines[0].empty()) e->insert(" ");
+            e->insert(keyname);
+        }
+        return true;
+    }
+
+    if(code==-1 && g3d_windowhit(isdown, true)) return true;
+    else if(code==-3 && g3d_windowhit(isdown, false)) return true;
+
+    if(fieldmode == FIELDSHOW || !e)
+    {
+        if(windowhit) switch(code)
+        {
+            case -4: // window "management" 
+                if(isdown)
+                {
+                    if(windowhit->gui2d) 
+                    {
+                        vec origin = *guis2d.last().savedorigin;
+                        int i = windowhit - &guis2d[0];
+                        for(int j = guis2d.length()-1; j > i; j--) *guis2d[j].savedorigin = *guis2d[j-1].savedorigin;
+                        *windowhit->savedorigin = origin;
+                        if(guis2d.length() > 1)
+                        {
+                            if(camera1->o.dist(*windowhit->savedorigin) <= camera1->o.dist(*guis2d.last().savedorigin))
+                                windowhit->savedorigin->add(camdir);
+                        }
+                    }
+                    else windowhit->savedorigin->add(vec(camdir).mul(guipushdist));
+                }
+                return true;
+            case -5:
+                if(isdown)
+                {
+                    if(windowhit->gui2d)
+                    {
+                        vec origin = *guis2d[0].savedorigin;
+                        loopj(guis2d.length()-1) *guis2d[j].savedorigin = *guis2d[j + 1].savedorigin;
+                        *guis2d.last().savedorigin = origin;
+                        if(guis2d.length() > 1)
+                        {
+                            if(camera1->o.dist(*guis2d.last().savedorigin) >= camera1->o.dist(*guis2d[0].savedorigin))
+                                guis2d.last().savedorigin->sub(camdir);
+                        }
+                    }
+                    else windowhit->savedorigin->sub(vec(camdir).mul(guipushdist));
+                }
+                return true;
+        }
+
+        return false;
+    }
+    switch(code)
+    {
+        case SDLK_ESCAPE: //cancel editing without commit
+            if(isdown) fieldmode = FIELDABORT;
+            return true;
+        case SDLK_RETURN:
+        case SDLK_TAB:
+            if(e->maxy != 1) break;
+        case SDLK_KP_ENTER:
+            if(isdown) fieldmode = FIELDCOMMIT; //signal field commit (handled when drawing field)
+            return true;
+    }
+    if(isdown) e->key(code);
+    return true;
+}
+
+void g3d_cursorpos(float &x, float &y)
+{
+    if(guis2d.length()) { x = cursorx; y = cursory; }
+    else x = y = 0.5f;
+}
+
+void g3d_resetcursor()
+{
+    cursorx = cursory = 0.5f;
+}
+
+FVARP(guisens, 1e-3f, 1, 1e3f);
+
+bool g3d_movecursor(int dx, int dy)
+{
+    if(!guis2d.length() || !hascursor) return false;
+    const float CURSORSCALE = 500.0f;
+    cursorx = max(0.0f, min(1.0f, cursorx+guisens*dx*(screenh/(screenw*CURSORSCALE))));
+    cursory = max(0.0f, min(1.0f, cursory+guisens*dy/CURSORSCALE));
+    return true;
+}
+
+VARNP(guifollow, useguifollow, 0, 1, 1);
+VARNP(gui2d, usegui2d, 0, 1, 1);
+
+void g3d_addgui(g3d_callback *cb, vec &origin, int flags)
+{
+    bool gui2d = flags&GUI_FORCE_2D || (flags&GUI_2D && usegui2d) || mainmenu;
+    if(!gui2d && flags&GUI_FOLLOW && useguifollow) origin.z = player->o.z-(player->eyeheight-1);
+    gui &g = (gui2d ? guis2d : guis3d).add();
+    g.cb = cb;
+    g.origin = origin;
+    g.savedorigin = &origin;
+    g.dist = flags&GUI_BOTTOM && gui2d ? 1e16f : camera1->o.dist(g.origin);
+    g.gui2d = gui2d;
+}
+
+void g3d_limitscale(float scale)
+{
+    gui::maxscale = scale;
+}
+
+static inline bool g3d_sort(const gui &a, const gui &b) { return a.dist < b.dist; }
+
+bool g3d_windowhit(bool on, bool act)
+{
+    extern int cleargui(int n);
+    if(act) 
+    {
+        if(actionon || windowhit)
+        {
+            if(on) { firstx = gui::hitx; firsty = gui::hity; }
+            mousebuttons |= (actionon=on) ? G3D_DOWN : G3D_UP;
+        }
+    } else if(!on && windowhit) cleargui(1);
+    return (guis2d.length() && hascursor) || (windowhit && !windowhit->gui2d);
+}
+
+void g3d_render()   
+{
+    windowhit = NULL;    
+    if(actionon) mousebuttons |= G3D_PRESSED;
+   
+    gui::reset(); 
+    guis2d.shrink(0);
+    guis3d.shrink(0);
+    // call all places in the engine that may want to render a gui from here, they call g3d_addgui()
+    extern void g3d_texturemenu();
+    
+    if(!mainmenu) g3d_texturemenu();
+    g3d_mainmenu();
+    if(!mainmenu) game::g3d_gamemenus();
+
+    guis2d.sort(g3d_sort);
+    guis3d.sort(g3d_sort);
+    
+    readyeditors();
+    fieldsactive = false;
+
+    hascursor = false;
+
+    layoutpass = true;
+    loopv(guis2d) guis2d[i].draw();
+    loopv(guis3d) guis3d[i].draw();
+    layoutpass = false;
+
+    if(guis3d.length())
+    {
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+        glEnable(GL_DEPTH_TEST);
+        glDepthFunc(GL_ALWAYS);
+        glDepthMask(GL_FALSE);
+
+        loopvrev(guis3d) guis3d[i].draw();
+
+        glDepthFunc(GL_LESS);
+        glDepthMask(GL_TRUE);
+        glDisable(GL_DEPTH_TEST);
+
+        glDisable(GL_BLEND);
+    }
+}
+
+void g3d_render2d()
+{
+    if(guis2d.length())
+    {
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+        loopvrev(guis2d) guis2d[i].draw();
+
+        glDisable(GL_BLEND);
+    }
+
+    flusheditors();
+    if(!fieldsactive) fieldmode = FIELDSHOW; //didn't draw any fields, so lose focus - mainly for menu closed
+    textinput(fieldmode!=FIELDSHOW, TI_GUI);
+    keyrepeat(fieldmode!=FIELDSHOW, KR_GUI);
+
+    mousebuttons = 0;
+}
+
+void consolebox(int x1, int y1, int x2, int y2)
+{
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    float bw = x2 - x1, bh = y2 - y1, aspect = bw/bh, sh = bh, sw = sh*aspect;
+    bw *= float(4*FONTH)/(SKIN_H*SKIN_SCALE);
+    bh *= float(4*FONTH)/(SKIN_H*SKIN_SCALE);
+    sw /= bw + (gui::skinx[2]-gui::skinx[1] + gui::skinx[10]-gui::skinx[9])*SKIN_SCALE;
+    sh /= bh + (gui::skiny[9]-gui::skiny[7] + gui::skiny[6]-gui::skiny[4])*SKIN_SCALE;
+    pushhudmatrix();
+    hudmatrix.translate(x1, y1, 0);
+    hudmatrix.scale(sw, sh, 1);
+    flushhudmatrix();
+    gui::drawskin(-gui::skinx[1]*SKIN_SCALE, -gui::skiny[4]*SKIN_SCALE, int(bw), int(bh), 0, 9, 1, vec(1, 1, 1), 0.60f);
+    gui::drawskin((-gui::skinx[1] + gui::skinx[2] - gui::skinx[5])*SKIN_SCALE, -gui::skiny[4]*SKIN_SCALE, int(bw), 0, 9, 1, 1, vec(1, 1, 1), 0.60f);
+    pophudmatrix();
+}
+
diff --git a/src/engine/animmodel.h b/src/engine/animmodel.h
new file mode 100644 (file)
index 0000000..16d5189
--- /dev/null
@@ -0,0 +1,1617 @@
+VARFP(envmapmodels, 0, 1, 1, preloadmodelshaders(true));
+VARFP(bumpmodels, 0, 1, 1, preloadmodelshaders(true));
+VARP(fullbrightmodels, 0, 0, 200);
+
+struct animmodel : model
+{
+    struct animspec
+    {
+        int frame, range;
+        float speed;
+        int priority;
+    };
+
+    struct animpos
+    {
+        int anim, fr1, fr2;
+        float t;
+
+        void setframes(const animinfo &info)
+        {
+            anim = info.anim;
+            if(info.range<=1) 
+            {
+                fr1 = 0;
+                t = 0;
+            }
+            else
+            {
+                int time = info.anim&ANIM_SETTIME ? info.basetime : lastmillis-info.basetime;
+                fr1 = (int)(time/info.speed); // round to full frames
+                t = (time-fr1*info.speed)/info.speed; // progress of the frame, value from 0.0f to 1.0f
+            }
+            if(info.anim&ANIM_LOOP)
+            {
+                fr1 = fr1%info.range+info.frame;
+                fr2 = fr1+1;
+                if(fr2>=info.frame+info.range) fr2 = info.frame;
+            }
+            else
+            {
+                fr1 = min(fr1, info.range-1)+info.frame;
+                fr2 = min(fr1+1, info.frame+info.range-1);
+            }
+            if(info.anim&ANIM_REVERSE)
+            {
+                fr1 = (info.frame+info.range-1)-(fr1-info.frame);
+                fr2 = (info.frame+info.range-1)-(fr2-info.frame);
+            }
+        }
+
+        bool operator==(const animpos &a) const { return fr1==a.fr1 && fr2==a.fr2 && (fr1==fr2 || t==a.t); }
+        bool operator!=(const animpos &a) const { return fr1!=a.fr1 || fr2!=a.fr2 || (fr1!=fr2 && t!=a.t); }
+    };
+
+    struct part;
+
+    struct animstate
+    {
+        part *owner;
+        animpos cur, prev;
+        float interp;
+
+        bool operator==(const animstate &a) const { return cur==a.cur && (interp<1 ? interp==a.interp && prev==a.prev : a.interp>=1); }
+        bool operator!=(const animstate &a) const { return cur!=a.cur || (interp<1 ? interp!=a.interp || prev!=a.prev : a.interp<1); }
+    };
+
+    struct linkedpart;
+    struct mesh;
+
+    struct shaderparams
+    {
+        float spec, ambient, glow, glowdelta, glowpulse, specglare, glowglare, fullbright, envmapmin, envmapmax, scrollu, scrollv, alphatest;
+
+        shaderparams() : spec(1.0f), ambient(0.3f), glow(3.0f), glowdelta(0), glowpulse(0), specglare(1), glowglare(1), fullbright(0), envmapmin(0), envmapmax(0), scrollu(0), scrollv(0), alphatest(0.9f) {}
+    };
+
+    struct shaderparamskey
+    {
+        static hashtable<shaderparams, shaderparamskey> keys;
+        static int firstversion, lastversion;
+
+        int version;
+
+        shaderparamskey() : version(-1) {}
+
+        bool checkversion()
+        {
+            if(version >= firstversion) return true;
+            version = lastversion;
+            if(++lastversion <= 0)
+            {
+                enumerate(keys, shaderparamskey, key, key.version = -1);
+                firstversion = 0;
+                lastversion = 1;
+                version = 0;
+            }
+            return false;
+        }
+
+        static inline void invalidate()
+        {
+            firstversion = lastversion;
+        }
+    };
+
+    struct skin : shaderparams
+    {
+        part *owner;
+        Texture *tex, *masks, *envmap, *normalmap;
+        Shader *shader;
+        bool alphablend, cullface;
+        shaderparamskey *key;
+
+        skin() : owner(0), tex(notexture), masks(notexture), envmap(NULL), normalmap(NULL), shader(NULL), alphablend(true), cullface(true), key(NULL) {}
+
+        bool masked() const { return masks != notexture; }
+        bool envmapped() { return envmapmax>0 && envmapmodels; }
+        bool bumpmapped() { return normalmap && bumpmodels; }
+        bool tangents() { return bumpmapped(); }
+        bool alphatested() const { return alphatest > 0 && tex->type&Texture::ALPHA; }
+
+        void setkey()
+        {
+            key = &shaderparamskey::keys[*this];
+        }
+
+        void setshaderparams(mesh *m, const animstate *as)
+        {
+            if(!Shader::lastshader) return;
+
+            float mincolor = as->cur.anim&ANIM_FULLBRIGHT ? fullbrightmodels/100.0f : 0.0f;
+            if(fullbright)
+            {
+                gle::colorf(fullbright/2, fullbright/2, fullbright/2, transparent);
+            }
+            else
+            {
+                gle::color(vec(lightcolor).max(mincolor), transparent);
+            }
+
+            if(key->checkversion() && Shader::lastshader->owner == key) return;
+            Shader::lastshader->owner = key;
+
+            if(alphatested()) LOCALPARAMF(alphatest, alphatest);
+
+            if(fullbright)
+            {
+                LOCALPARAMF(lightscale, 0, 0, 2);
+            }
+            else
+            {
+                float bias = max(mincolor-1.0f, 0.2f), scale = 0.5f*max(0.8f-bias, 0.0f), 
+                      minshade = scale*max(ambient, mincolor);
+                LOCALPARAMF(lightscale, scale - minshade, scale, minshade + bias);
+            }
+            float curglow = glow;
+            if(glowpulse > 0)
+            {
+                float curpulse = lastmillis*glowpulse;
+                curpulse -= floor(curpulse);
+                curglow += glowdelta*2*fabs(curpulse - 0.5f);
+            }
+            LOCALPARAMF(maskscale, 0.5f*spec, 0.5f*curglow, 16*specglare, 4*glowglare);
+            LOCALPARAMF(texscroll, scrollu*lastmillis/1000.0f, scrollv*lastmillis/1000.0f);
+            if(envmapped()) LOCALPARAMF(envmapscale, envmapmin-envmapmax, envmapmax);
+        }
+
+        Shader *loadshader()
+        {
+            #define DOMODELSHADER(name, body) \
+                do { \
+                    static Shader *name##shader = NULL; \
+                    if(!name##shader) name##shader = useshaderbyname(#name); \
+                    body; \
+                } while(0)
+            #define LOADMODELSHADER(name) DOMODELSHADER(name, return name##shader)
+            #define SETMODELSHADER(m, name) DOMODELSHADER(name, (m)->setshader(name##shader))
+            if(shader) return shader;
+
+            string opts;
+            int optslen = 0; 
+            if(alphatested()) opts[optslen++] = 'a';
+            if(owner->tangents()) opts[optslen++] = 'q';
+            if(bumpmapped()) opts[optslen++] = 'n';
+            if(envmapped()) opts[optslen++] = 'e';
+            if(masked()) opts[optslen++] = 'm';
+            if(!fullbright && (masked() || spec>=0.01f)) opts[optslen++] = 's';
+            opts[optslen++] = '\0';
+
+            defformatstring(name, "model%s", opts);
+            shader = generateshader(name, "modelshader \"%s\"", opts);
+            return shader;
+        }
+
+        void cleanup()
+        {
+            if(shader && shader->standard) shader = NULL;
+        }
+
+        void preloadBIH()
+        {
+            if(tex->type&Texture::ALPHA && !tex->alphamask) loadalphamask(tex);
+        }
+        void preloadshader(bool force)
+        {
+            if(force) cleanup();
+            loadshader();
+        }
+        void setshader(mesh *m, const animstate *as)
+        {
+            m->setshader(loadshader());
+        }
+
+        void bind(mesh *b, const animstate *as)
+        {
+            if(!cullface && enablecullface) { glDisable(GL_CULL_FACE); enablecullface = false; }
+            else if(cullface && !enablecullface) { glEnable(GL_CULL_FACE); enablecullface = true; }
+
+            if(as->cur.anim&ANIM_NOSKIN)
+            {
+                if(enablealphablend) { glDisable(GL_BLEND); enablealphablend = false; }
+                if(shadowmapping) SETMODELSHADER(b, shadowmapcaster);
+                else /*if(as->cur.anim&ANIM_SHADOW)*/ SETMODELSHADER(b, notexturemodel);
+                return;
+            }
+            setshader(b, as);
+            setshaderparams(b, as);
+            int activetmu = 0;
+            if(tex!=lasttex)
+            {
+                glBindTexture(GL_TEXTURE_2D, tex->id);
+                lasttex = tex;
+            }
+            if(bumpmapped() && normalmap !=lastnormalmap)
+            {
+                glActiveTexture_(GL_TEXTURE3);
+                activetmu = 3;
+                glBindTexture(GL_TEXTURE_2D, normalmap->id);
+                lastnormalmap = normalmap;
+            }
+            if(tex->type&Texture::ALPHA)
+            {
+                if(alphablend)
+                {
+                    if(!enablealphablend && !reflecting && !refracting)
+                    {
+                        glEnable(GL_BLEND);
+                        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                        enablealphablend = true;
+                    }
+                }
+                else if(enablealphablend) { glDisable(GL_BLEND); enablealphablend = false; }
+            }
+            else if(enablealphablend && transparent>=1) { glDisable(GL_BLEND); enablealphablend = false; }
+            if(masked() && masks!=lastmasks)
+            {
+                glActiveTexture_(GL_TEXTURE1);
+                activetmu = 1;
+                glBindTexture(GL_TEXTURE_2D, masks->id);
+                lastmasks = masks;
+            }
+            if(envmapped())
+            {
+                GLuint emtex = envmap ? envmap->id : closestenvmaptex;
+                if(lastenvmaptex!=emtex)
+                {
+                    glActiveTexture_(GL_TEXTURE2);
+                    activetmu = 2;
+                    glBindTexture(GL_TEXTURE_CUBE_MAP, emtex);
+                    lastenvmaptex = emtex;
+                }
+            }
+            if(activetmu != 0) glActiveTexture_(GL_TEXTURE0);
+        }
+    };
+
+    struct meshgroup;
+
+    struct mesh
+    {
+        meshgroup *group;
+        char *name;
+        bool noclip;
+
+        mesh() : group(NULL), name(NULL), noclip(false)
+        {
+        }
+
+        virtual ~mesh()
+        {
+            DELETEA(name);
+        }
+
+        virtual void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) {}
+
+        virtual void genBIH(BIH::mesh &m) {}
+        void genBIH(skin &s, vector<BIH::mesh> &bih, const matrix4x3 &t)
+        {
+            BIH::mesh &m = bih.add();
+            m.xform = t;
+            m.tex = s.tex;
+            if(s.tex->type&Texture::ALPHA) m.flags |= BIH::MESH_ALPHA;
+            if(noclip) m.flags |= BIH::MESH_NOCLIP;
+            if(s.cullface) m.flags |= BIH::MESH_CULLFACE;
+            genBIH(m);
+            while(bih.last().numtris > BIH::mesh::MAXTRIS)
+            {
+                BIH::mesh &overflow = bih.dup();
+                overflow.tris += BIH::mesh::MAXTRIS;
+                overflow.numtris -= BIH::mesh::MAXTRIS;
+                bih[bih.length()-2].numtris = BIH::mesh::MAXTRIS;
+            }
+        }
+
+        virtual void setshader(Shader *s) 
+        { 
+            if(glaring) s->setvariant(0, 1);
+            else s->set(); 
+        }
+
+        template<class V, class T> void smoothnorms(V *verts, int numverts, T *tris, int numtris, float limit, bool areaweight)
+        {
+            hashtable<vec, int> share;
+            int *next = new int[numverts];
+            memset(next, -1, numverts*sizeof(int));
+            loopi(numverts)
+            {
+                V &v = verts[i];
+                v.norm = vec(0, 0, 0);
+                int idx = share.access(v.pos, i);
+                if(idx != i) { next[i] = next[idx]; next[idx] = i; }
+            }
+            loopi(numtris)
+            {
+                T &t = tris[i];
+                V &v1 = verts[t.vert[0]], &v2 = verts[t.vert[1]], &v3 = verts[t.vert[2]];
+                vec norm;
+                norm.cross(vec(v2.pos).sub(v1.pos), vec(v3.pos).sub(v1.pos));
+                if(!areaweight) norm.normalize();
+                v1.norm.add(norm);
+                v2.norm.add(norm);
+                v3.norm.add(norm);
+            }
+            vec *norms = new vec[numverts];
+            memclear(norms, numverts);
+            loopi(numverts)
+            {
+                V &v = verts[i];
+                norms[i].add(v.norm);
+                if(next[i] >= 0)
+                {
+                    float vlimit = limit*v.norm.magnitude();
+                    for(int j = next[i]; j >= 0; j = next[j])
+                    {
+                        V &o = verts[j];
+                        if(v.norm.dot(o.norm) >= vlimit*o.norm.magnitude())
+                        {
+                            norms[i].add(o.norm);
+                            norms[j].add(v.norm);
+                        }
+                    }
+                }
+            }
+            loopi(numverts) verts[i].norm = norms[i].normalize();
+            delete[] next;
+            delete[] norms;
+        }
+
+        template<class V, class T> void buildnorms(V *verts, int numverts, T *tris, int numtris, bool areaweight)
+        {
+            loopi(numverts) verts[i].norm = vec(0, 0, 0);
+            loopi(numtris)
+            {
+                T &t = tris[i];
+                V &v1 = verts[t.vert[0]], &v2 = verts[t.vert[1]], &v3 = verts[t.vert[2]];
+                vec norm;
+                norm.cross(vec(v2.pos).sub(v1.pos), vec(v3.pos).sub(v1.pos));
+                if(!areaweight) norm.normalize();
+                v1.norm.add(norm);
+                v2.norm.add(norm);
+                v3.norm.add(norm);
+            }
+            loopi(numverts) verts[i].norm.normalize();
+        }
+      
+        template<class V, class T> void buildnorms(V *verts, int numverts, T *tris, int numtris, bool areaweight, int numframes)
+        {
+            if(!numverts) return;
+            loopi(numframes) buildnorms(&verts[i*numverts], numverts, tris, numtris, areaweight);
+        }
+        static inline void fixqtangent(quat &q, float bt)
+        {
+            static const float bias = -1.5f/65535, biasscale = sqrtf(1 - bias*bias);
+            if(bt < 0)
+            {
+                if(q.w >= 0) q.neg();
+                if(q.w > bias) { q.mul3(biasscale); q.w = bias; }
+            }
+            else if(q.w < 0) q.neg();
+        }
+
+        template<class V> static inline void calctangent(V &v, const vec &n, const vec &t, float bt)
+        {
+            matrix3 m;
+            m.c = n;
+            m.a = t;
+            m.b.cross(m.c, m.a);
+            quat q(m);
+            fixqtangent(q, bt);
+            v.tangent = q;
+        }
+        template<class B, class V, class TC, class T> void calctangents(B *bumpverts, V *verts, TC *tcverts, int numverts, T *tris, int numtris, bool areaweight)
+        {
+            vec *tangent = new vec[2*numverts], *bitangent = tangent+numverts;
+            memclear(tangent, 2*numverts);
+            loopi(numtris)
+            {
+                const T &t = tris[i];
+                const vec &e0 = verts[t.vert[0]].pos;
+                vec e1 = vec(verts[t.vert[1]].pos).sub(e0), e2 = vec(verts[t.vert[2]].pos).sub(e0);
+
+                const vec2 &tc0 = tcverts[t.vert[0]].tc,
+                           &tc1 = tcverts[t.vert[1]].tc,
+                           &tc2 = tcverts[t.vert[2]].tc;
+                float u1 = tc1.x - tc0.x, v1 = tc1.y - tc0.y,
+                      u2 = tc2.x - tc0.x, v2 = tc2.y - tc0.y;
+                vec u(e2), v(e2);
+                u.mul(v1).sub(vec(e1).mul(v2));
+                v.mul(u1).sub(vec(e1).mul(u2));
+
+                if(vec().cross(e2, e1).dot(vec().cross(v, u)) >= 0)
+                {
+                    u.neg();
+                    v.neg();
+                }
+
+                if(!areaweight)
+                {
+                    u.normalize();
+                    v.normalize();
+                }
+
+                loopj(3)
+                {
+                    tangent[t.vert[j]].sub(u);
+                    bitangent[t.vert[j]].add(v);
+                }
+            }
+            loopi(numverts)
+            {
+                const vec &n = verts[i].norm,
+                          &t = tangent[i],
+                          &bt = bitangent[i];
+                B &bv = bumpverts[i];
+                matrix3 m;
+                m.c = n;
+                (m.a = t).project(m.c).normalize();
+                m.b.cross(m.c, m.a);
+                quat q(m);
+                fixqtangent(q, m.b.dot(bt));
+                bv.tangent = q;
+            }
+            delete[] tangent;
+        }
+
+        template<class B, class V, class TC, class T> void calctangents(B *bumpverts, V *verts, TC *tcverts, int numverts, T *tris, int numtris, bool areaweight, int numframes)
+        {
+            loopi(numframes) calctangents(&bumpverts[i*numverts], &verts[i*numverts], tcverts, numverts, tris, numtris, areaweight);
+        }
+    };
+
+    struct meshgroup
+    {
+        meshgroup *next;
+        int shared;
+        char *name;
+        vector<mesh *> meshes;
+
+        meshgroup() : next(NULL), shared(0), name(NULL)
+        {
+        }
+
+        virtual ~meshgroup()
+        {
+            DELETEA(name);
+            meshes.deletecontents();
+            DELETEP(next);
+        }            
+
+        virtual int findtag(const char *name) { return -1; }
+        virtual void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n) {}
+
+        void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+        {
+            loopv(meshes) meshes[i]->calcbb(bbmin, bbmax, m);
+        }
+
+        void genBIH(vector<skin> &skins, vector<BIH::mesh> &bih, const matrix4x3 &t)
+        {
+            loopv(meshes) meshes[i]->genBIH(skins[i], bih, t);
+        }
+
+        virtual void *animkey() { return this; }
+        virtual int totalframes() const { return 1; }
+        bool hasframe(int i) const { return i>=0 && i<totalframes(); }
+        bool hasframes(int i, int n) const { return i>=0 && i+n<=totalframes(); }
+        int clipframes(int i, int n) const { return min(n, totalframes() - i); }
+
+        virtual void cleanup() {}
+        virtual void preload(part *p) {}
+        virtual void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p) {}
+
+        void bindpos(GLuint ebuf, GLuint vbuf, void *v, int stride)
+        {
+            if(lastebuf!=ebuf)
+            {
+                gle::bindebo(ebuf);
+                lastebuf = ebuf;
+            }
+            if(lastvbuf!=vbuf)
+            {
+                gle::bindvbo(vbuf);
+                if(!lastvbuf) gle::enablevertex();
+                gle::vertexpointer(stride, v);
+                lastvbuf = vbuf;
+            }
+        }
+
+        void bindtc(void *v, int stride)
+        {
+            if(!enabletc)
+            {
+                gle::enabletexcoord0();
+                enabletc = true;
+            }
+            if(lasttcbuf!=lastvbuf)
+            {
+                gle::texcoord0pointer(stride, v);
+                lasttcbuf = lastvbuf;
+            }
+        }
+
+        void bindnormals(void *v, int stride)
+        {
+            if(!enablenormals)
+            {
+                gle::enablenormal();
+                enablenormals = true;
+            }
+            if(lastnbuf!=lastvbuf)
+            {
+                gle::normalpointer(stride, v);
+                lastnbuf = lastvbuf;
+            }
+        }
+
+        void bindtangents(void *v, int stride)
+        {
+            if(!enabletangents)
+            {
+                gle::enabletangent();
+                enabletangents = true;
+            }
+            if(lastxbuf!=lastvbuf)
+            {
+                gle::tangentpointer(stride, v, GL_SHORT);
+                lastxbuf = lastvbuf;
+            }
+        }
+
+        void bindbones(void *wv, void *bv, int stride)
+        {
+            if(!enablebones)
+            {
+                gle::enableboneweight();
+                gle::enableboneindex();
+                enablebones = true;
+            }
+            if(lastbbuf!=lastvbuf)
+            {
+                gle::boneweightpointer(stride, wv);
+                gle::boneindexpointer(stride, bv);
+                lastbbuf = lastvbuf;
+            }
+        }
+    };
+
+    virtual meshgroup *loadmeshes(const char *name, va_list args) { return NULL; }
+
+    meshgroup *sharemeshes(const char *name, ...)
+    {
+        static hashnameset<meshgroup *> meshgroups;
+        if(!meshgroups.access(name))
+        {
+            va_list args;
+            va_start(args, name);
+            meshgroup *group = loadmeshes(name, args);
+            va_end(args);
+            if(!group) return NULL;
+            meshgroups.add(group);
+        }
+        return meshgroups[name];
+    }
+
+    struct linkedpart
+    {
+        part *p;
+        int tag, anim, basetime;
+        vec translate;
+        vec *pos;
+        matrix4 matrix;
+
+        linkedpart() : p(NULL), tag(-1), anim(-1), basetime(0), translate(0, 0, 0), pos(NULL) {}
+    };
+
+    struct part
+    {
+        animmodel *model;
+        int index;
+        meshgroup *meshes;
+        vector<linkedpart> links;
+        vector<skin> skins;
+        vector<animspec> *anims[MAXANIMPARTS];
+        int numanimparts;
+        float pitchscale, pitchoffset, pitchmin, pitchmax;
+        vec translate;
+
+        part(animmodel *model, int index = 0) : model(model), index(index), meshes(NULL), numanimparts(1), pitchscale(1), pitchoffset(0), pitchmin(0), pitchmax(0), translate(0, 0, 0)
+        {
+            loopk(MAXANIMPARTS) anims[k] = NULL;
+        }
+        virtual ~part()
+        {
+            loopk(MAXANIMPARTS) DELETEA(anims[k]);
+        }
+
+        virtual void cleanup()
+        {
+            if(meshes) meshes->cleanup();
+            loopv(skins) skins[i].cleanup();
+        }
+
+        void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+        {
+            matrix4x3 t = m;
+            t.scale(model->scale);
+            t.translate(translate);
+            meshes->calcbb(bbmin, bbmax, t);
+            loopv(links)
+            {
+                matrix4x3 n;
+                meshes->concattagtransform(this, links[i].tag, m, n);
+                n.translate(links[i].translate, model->scale);
+                links[i].p->calcbb(bbmin, bbmax, n);
+            }
+        }
+
+        void genBIH(vector<BIH::mesh> &bih, const matrix4x3 &m)
+        {
+            matrix4x3 t = m;
+            t.scale(model->scale);
+            t.translate(translate);
+            meshes->genBIH(skins, bih, t);
+            loopv(links)
+            {
+                matrix4x3 n;
+                meshes->concattagtransform(this, links[i].tag, m, n);
+                n.translate(links[i].translate, model->scale);
+                links[i].p->genBIH(bih, n);
+            }
+        }
+
+        bool link(part *p, const char *tag, const vec &translate = vec(0, 0, 0), int anim = -1, int basetime = 0, vec *pos = NULL)
+        {
+            int i = meshes ? meshes->findtag(tag) : -1;
+            if(i<0)
+            {
+                loopv(links) if(links[i].p && links[i].p->link(p, tag, translate, anim, basetime, pos)) return true;
+                return false;
+            }
+            linkedpart &l = links.add();
+            l.p = p;
+            l.tag = i;
+            l.anim = anim;
+            l.basetime = basetime;
+            l.translate = translate;
+            l.pos = pos;
+            return true;
+        }
+
+        bool unlink(part *p)
+        {
+            loopvrev(links) if(links[i].p==p) { links.remove(i, 1); return true; }
+            loopv(links) if(links[i].p && links[i].p->unlink(p)) return true;
+            return false;
+        }
+
+        void initskins(Texture *tex = notexture, Texture *masks = notexture, int limit = 0)
+        {
+            if(!limit)
+            {
+                if(!meshes) return;
+                limit = meshes->meshes.length();
+            }
+            while(skins.length() < limit)
+            {
+                skin &s = skins.add();
+                s.owner = this;
+                s.tex = tex;
+                s.masks = masks;
+            }
+        }
+
+        bool envmapped()
+        {
+            loopv(skins) if(skins[i].envmapped()) return true;
+            return false;
+        }
+
+        bool tangents()
+        {
+            loopv(skins) if(skins[i].tangents()) return true;
+            return false;
+        }
+
+        void preloadBIH()
+        {
+            loopv(skins) skins[i].preloadBIH();
+        }
+
+        void preloadshaders(bool force)
+        {
+            loopv(skins) skins[i].preloadshader(force);
+        }
+
+        void preloadmeshes()
+        {
+            if(meshes) meshes->preload(this);
+        }
+
+        virtual void getdefaultanim(animinfo &info, int anim, uint varseed, dynent *d)
+        {
+            info.frame = 0;
+            info.range = 1;
+        }
+
+        bool calcanim(int animpart, int anim, int basetime, int basetime2, dynent *d, int interp, animinfo &info, int &aitime)
+        {
+            uint varseed = uint((size_t)d);
+            info.anim = anim;
+            info.basetime = basetime;
+            info.varseed = varseed;
+            info.speed = anim&ANIM_SETSPEED ? basetime2 : 100.0f;
+            if((anim&ANIM_INDEX)==ANIM_ALL)
+            {
+                info.frame = 0;
+                info.range = meshes->totalframes();
+            }
+            else 
+            {
+                animspec *spec = NULL;
+                if(anims[animpart])
+                {
+                    int primaryidx = anim&ANIM_INDEX;
+                    if(primaryidx < NUMANIMS)
+                    {
+                        vector<animspec> &primary = anims[animpart][primaryidx];
+                        if(primary.length()) spec = &primary[uint(varseed + basetime)%primary.length()];
+                    }
+                    if((anim>>ANIM_SECONDARY)&(ANIM_INDEX|ANIM_DIR))
+                    {
+                        int secondaryidx = (anim>>ANIM_SECONDARY)&ANIM_INDEX;
+                        if(secondaryidx < NUMANIMS)
+                        { 
+                            vector<animspec> &secondary = anims[animpart][secondaryidx];
+                            if(secondary.length())
+                            {
+                                animspec &spec2 = secondary[uint(varseed + basetime2)%secondary.length()];
+                                if(!spec || spec2.priority > spec->priority)
+                                {
+                                    spec = &spec2;
+                                    info.anim >>= ANIM_SECONDARY;
+                                    info.basetime = basetime2;
+                                }
+                            }
+                        }
+                    }
+                }
+                if(spec)
+                {
+                    info.frame = spec->frame;
+                    info.range = spec->range;
+                    if(spec->speed>0) info.speed = 1000.0f/spec->speed;
+                }
+                else getdefaultanim(info, anim, uint(varseed + info.basetime), d);
+            }
+
+            info.anim &= (1<<ANIM_SECONDARY)-1;
+            info.anim |= anim&ANIM_FLAGS;
+            if((info.anim&ANIM_CLAMP) != ANIM_CLAMP)
+            {
+                if(info.anim&(ANIM_LOOP|ANIM_START|ANIM_END))
+                {
+                    info.anim &= ~ANIM_SETTIME;
+                    if(!info.basetime) info.basetime = -((int)(size_t)d&0xFFF);
+                }
+                if(info.anim&(ANIM_START|ANIM_END))
+                {
+                    if(info.anim&ANIM_END) info.frame += info.range-1;
+                    info.range = 1;
+                }
+            }
+
+            if(!meshes->hasframes(info.frame, info.range))
+            {
+                if(!meshes->hasframe(info.frame)) return false;
+                info.range = meshes->clipframes(info.frame, info.range);
+            }
+
+            if(d && interp>=0)
+            {
+                animinterpinfo &ai = d->animinterp[interp];
+                if((info.anim&ANIM_CLAMP)==ANIM_CLAMP) aitime = min(aitime, int(info.range*info.speed*0.5e-3f));
+                void *ak = meshes->animkey();
+                if(d->ragdoll && !(anim&ANIM_RAGDOLL)) 
+                {
+                    ai.prev.range = ai.cur.range = 0;
+                    ai.lastswitch = -1;
+                }
+                else if(ai.lastmodel!=ak || ai.lastswitch<0 || lastmillis-d->lastrendered>aitime)
+                {
+                    ai.prev = ai.cur = info;
+                    ai.lastswitch = lastmillis-aitime*2;
+                }
+                else if(ai.cur!=info)
+                {
+                    if(lastmillis-ai.lastswitch>aitime/2) ai.prev = ai.cur;
+                    ai.cur = info;
+                    ai.lastswitch = lastmillis;
+                }
+                else if(info.anim&ANIM_SETTIME) ai.cur.basetime = info.basetime;
+                ai.lastmodel = ak;
+            }
+            return true;
+        }
+
+        void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d)
+        {
+            animstate as[MAXANIMPARTS];
+            render(anim, basetime, basetime2, pitch, axis, forward, d, as);
+        }
+
+        void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d, animstate *as)
+        {
+            if(!(anim&ANIM_REUSE)) loopi(numanimparts)
+            {
+                animinfo info;
+                int interp = d && index+numanimparts<=MAXANIMPARTS ? index+i : -1, aitime = animationinterpolationtime;
+                if(!calcanim(i, anim, basetime, basetime2, d, interp, info, aitime)) return;
+                animstate &p = as[i];
+                p.owner = this;
+                p.cur.setframes(info);
+                p.interp = 1;
+                if(interp>=0 && d->animinterp[interp].prev.range>0)
+                {
+                    int diff = lastmillis-d->animinterp[interp].lastswitch;
+                    if(diff<aitime)
+                    {
+                        p.prev.setframes(d->animinterp[interp].prev);
+                        p.interp = diff/float(aitime);
+                    }
+                }
+            }
+
+            vec oaxis, oforward;
+            matrixstack[matrixpos].transposedtransformnormal(axis, oaxis);
+            float pitchamount = pitchscale*pitch + pitchoffset;
+            if((pitchmin || pitchmax) && pitchmin <= pitchmax) pitchamount = clamp(pitchamount, pitchmin, pitchmax);
+            if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH))
+                pitchamount *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp);
+            if(pitchamount)
+            {
+                ++matrixpos;
+                matrixstack[matrixpos] = matrixstack[matrixpos-1];
+                matrixstack[matrixpos].rotate(pitchamount*RAD, oaxis);
+            }
+            matrixstack[matrixpos].transposedtransformnormal(forward, oforward);
+
+            if(!(anim&ANIM_NORENDER))
+            {
+                matrix4 modelmatrix;
+                modelmatrix.mul(shadowmapping ? shadowmatrix : camprojmatrix, matrixstack[matrixpos]);
+                if(model->scale!=1) modelmatrix.scale(model->scale);
+                if(!translate.iszero()) modelmatrix.translate(translate);
+                GLOBALPARAM(modelmatrix, modelmatrix);
+
+                if(!(anim&ANIM_NOSKIN))
+                {
+                    if(envmapped()) GLOBALPARAM(modelworld, matrix3(matrixstack[matrixpos]));
+
+                    vec odir, ocampos;
+                    matrixstack[matrixpos].transposedtransformnormal(lightdir, odir);
+                    GLOBALPARAM(lightdir, odir);
+                    matrixstack[matrixpos].transposedtransform(camera1->o, ocampos);
+                    ocampos.div(model->scale).sub(translate);
+                    GLOBALPARAM(modelcamera, ocampos);
+                }
+            }
+
+            meshes->render(as, pitch, oaxis, oforward, d, this);
+
+            if(!(anim&ANIM_REUSE)) 
+            {
+                loopv(links)
+                {
+                    linkedpart &link = links[i];
+                    link.matrix.translate(links[i].translate, model->scale);
+
+                    matrixpos++;
+                    matrixstack[matrixpos].mul(matrixstack[matrixpos-1], link.matrix);
+
+                    if(link.pos) *link.pos = matrixstack[matrixpos].gettranslation();
+
+                    if(!link.p)
+                    {
+                        matrixpos--;
+                        continue;
+                    }
+
+                    int nanim = anim, nbasetime = basetime, nbasetime2 = basetime2;
+                    if(link.anim>=0)
+                    {
+                        nanim = link.anim | (anim&ANIM_FLAGS);
+                        nbasetime = link.basetime;
+                        nbasetime2 = 0;
+                    }
+                    link.p->render(nanim, nbasetime, nbasetime2, pitch, axis, forward, d);
+
+                    matrixpos--;
+                }
+            }
+
+            if(pitchamount) matrixpos--;
+        }
+
+        void setanim(int animpart, int num, int frame, int range, float speed, int priority = 0)
+        {
+            if(animpart<0 || animpart>=MAXANIMPARTS) return;
+            if(frame<0 || range<=0 || !meshes || !meshes->hasframes(frame, range))
+            {
+                conoutf(CON_ERROR, "invalid frame %d, range %d in model %s", frame, range, model->name);
+                return;
+            }
+            if(!anims[animpart]) anims[animpart] = new vector<animspec>[NUMANIMS];
+            animspec &spec = anims[animpart][num].add();
+            spec.frame = frame;
+            spec.range = range;
+            spec.speed = speed;
+            spec.priority = priority;
+        }
+
+        virtual void loaded()
+        {
+            meshes->shared++;
+            loopv(skins) skins[i].setkey();
+        }
+    };
+
+    enum
+    {
+        LINK_TAG = 0,
+        LINK_COOP,
+        LINK_REUSE
+    };
+
+    virtual int linktype(animmodel *m) const { return LINK_TAG; }
+
+    void render(int anim, int basetime, int basetime2, float pitch, const vec &axis, const vec &forward, dynent *d, modelattach *a)
+    {
+        int numtags = 0;
+        if(a)
+        {
+            int index = parts.last()->index + parts.last()->numanimparts;
+            for(int i = 0; a[i].tag; i++)
+            {
+                numtags++;
+
+                animmodel *m = (animmodel *)a[i].m;
+                if(!m)
+                {
+                    if(a[i].pos) link(NULL, a[i].tag, vec(0, 0, 0), 0, 0, a[i].pos);
+                    continue;
+                }
+                part *p = m->parts[0];
+                switch(linktype(m))
+                {
+                    case LINK_TAG:
+                        p->index = link(p, a[i].tag, vec(0, 0, 0), a[i].anim, a[i].basetime, a[i].pos) ? index : -1;
+                        break;
+
+                    case LINK_COOP:
+                        p->index = index;
+                        break;
+
+                    default:
+                        continue;
+                }
+                index += p->numanimparts;
+            }
+        }
+
+        animstate as[MAXANIMPARTS];
+        parts[0]->render(anim, basetime, basetime2, pitch, axis, forward, d, as);
+
+        if(a) for(int i = numtags-1; i >= 0; i--)
+        {
+            animmodel *m = (animmodel *)a[i].m;
+            if(!m)
+            {
+                if(a[i].pos) unlink(NULL);
+                continue;
+            }
+            part *p = m->parts[0];
+            switch(linktype(m))
+            {
+                case LINK_TAG:    
+                    if(p->index >= 0) unlink(p);
+                    p->index = 0;
+                    break;
+
+                case LINK_COOP:
+                    p->render(anim, basetime, basetime2, pitch, axis, forward, d);
+                    p->index = 0;
+                    break;
+
+                case LINK_REUSE:
+                    p->render(anim | ANIM_REUSE, basetime, basetime2, pitch, axis, forward, d, as); 
+                    break;
+            }
+        }
+    }
+
+    void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, dynent *d, modelattach *a, const vec &color, const vec &dir, float trans)
+    {
+        yaw += spinyaw*lastmillis/1000.0f;
+        pitch += offsetpitch + spinpitch*lastmillis/1000.0f;
+
+        vec axis(0, -1, 0), forward(1, 0, 0);
+
+        matrixpos = 0;
+        matrixstack[0].identity();
+        if(!d || !d->ragdoll || anim&ANIM_RAGDOLL)
+        {
+            matrixstack[0].settranslation(o);
+            matrixstack[0].rotate_around_z(yaw*RAD);
+            matrixstack[0].transformnormal(vec(axis), axis);
+            matrixstack[0].transformnormal(vec(forward), forward);
+            if(offsetyaw) matrixstack[0].rotate_around_z(offsetyaw*RAD);
+        }
+        else pitch = 0;
+
+        if(anim&ANIM_NORENDER)
+        {
+            render(anim, basetime, basetime2, pitch, axis, forward, d, a);
+            if(d) d->lastrendered = lastmillis;
+            return;
+        }
+
+        if(!(anim&ANIM_NOSKIN))
+        {
+            transparent = trans;
+            lightdir = dir;
+            lightcolor = color;
+
+            if(envmapped())
+            {
+            setupenvmap:
+                closestenvmaptex = lookupenvmap(closestenvmap(o));
+                GLOBALPARAM(lightdirworld, dir);
+            }
+            else if(a) for(int i = 0; a[i].tag; i++) if(a[i].m && a[i].m->envmapped()) goto setupenvmap;
+        }
+
+        if(depthoffset && !enabledepthoffset)
+        {
+            enablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+            enabledepthoffset = true;
+        }
+
+        if(transparent<1)
+        {
+            if(anim&ANIM_GHOST) 
+            {
+                glDepthFunc(GL_GREATER);
+                glDepthMask(GL_FALSE);
+            }
+            else if(alphadepth)
+            {
+                glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+                render(anim|ANIM_NOSKIN, basetime, basetime2, pitch, axis, forward, d, a);
+                glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, fading ? GL_FALSE : GL_TRUE);
+
+                glDepthFunc(GL_LEQUAL);
+            }
+
+            if(!enablealphablend)
+            {
+                glEnable(GL_BLEND);
+                glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                enablealphablend = true;
+            }
+        }
+
+        render(anim, basetime, basetime2, pitch, axis, forward, d, a);
+
+        if(transparent<1 && (alphadepth || anim&ANIM_GHOST)) 
+        {
+            glDepthFunc(GL_LESS);
+            if(anim&ANIM_GHOST) glDepthMask(GL_TRUE);
+        }
+
+        if(d) d->lastrendered = lastmillis;
+    }
+
+    vector<part *> parts;
+
+    animmodel(const char *name) : model(name)
+    {
+    }
+
+    ~animmodel()
+    {
+        parts.deletecontents();
+    }
+
+    void cleanup()
+    {
+        loopv(parts) parts[i]->cleanup();
+    }
+
+    virtual void flushpart() {}
+
+    part &addpart()
+    {
+        flushpart();
+        part *p = new part(this, parts.length());
+        parts.add(p);
+        return *p;
+    }
+
+    void initmatrix(matrix4x3 &m)
+    {
+        m.identity();
+        if(offsetyaw) m.rotate_around_z(offsetyaw*RAD);
+        if(offsetpitch) m.rotate_around_y(-offsetpitch*RAD);
+    }
+
+    void genBIH(vector<BIH::mesh> &bih)
+    {
+        if(parts.empty()) return;
+        matrix4x3 m;
+        initmatrix(m);
+        parts[0]->genBIH(bih, m);
+    }
+
+    void preloadBIH()
+    {
+        model::preloadBIH();
+        if(bih) loopv(parts) parts[i]->preloadBIH();
+    }
+
+    BIH *setBIH()
+    {
+        if(bih) return bih;
+        vector<BIH::mesh> meshes;
+        genBIH(meshes);
+        bih = new BIH(meshes);
+        return bih;
+    }
+
+    bool link(part *p, const char *tag, const vec &translate = vec(0, 0, 0), int anim = -1, int basetime = 0, vec *pos = NULL)
+    {
+        if(parts.empty()) return false;
+        return parts[0]->link(p, tag, translate, anim, basetime, pos);
+    }
+
+    bool unlink(part *p)
+    {
+        if(parts.empty()) return false;
+        return parts[0]->unlink(p);
+    }
+
+    bool envmapped()
+    {
+        loopv(parts) if(parts[i]->envmapped()) return true;
+        return false;
+    }
+
+    virtual bool flipy() const { return false; }
+    virtual bool loadconfig() { return false; }
+    virtual bool loaddefaultparts() { return false; }
+    virtual void startload() {}
+    virtual void endload() {}
+
+    bool load()
+    {
+        startload();
+        bool success = loadconfig() && parts.length(); // configured model, will call the model commands below
+        if(!success)
+            success = loaddefaultparts(); // model without configuration, try default tris and skin
+        flushpart();
+        endload();
+        if(flipy()) translate.y = -translate.y;
+
+        if(!success) return false;
+        loopv(parts) if(!parts[i]->meshes) return false;
+
+        loaded();
+        return true;
+    }
+
+    void preloadshaders(bool force)
+    {
+        loopv(parts) parts[i]->preloadshaders(force);
+    }
+
+    void preloadmeshes()
+    {
+        loopv(parts) parts[i]->preloadmeshes();
+    }
+
+    void setshader(Shader *shader)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].shader = shader;
+    }
+
+    void setenvmap(float envmapmin, float envmapmax, Texture *envmap)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins)
+        {
+            skin &s = parts[i]->skins[j];
+            if(envmapmax)
+            {
+                s.envmapmin = envmapmin;
+                s.envmapmax = envmapmax;
+            }
+            if(envmap) s.envmap = envmap;
+        }
+    }
+
+    void setspec(float spec)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].spec = spec;
+    }
+
+    void setambient(float ambient)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].ambient = ambient;
+    }
+
+    void setglow(float glow, float delta, float pulse)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) 
+        {
+            skin &s = parts[i]->skins[j];
+            s.glow = glow;
+            s.glowdelta = delta;
+            s.glowpulse = pulse;
+        }
+    }
+
+    void setglare(float specglare, float glowglare)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins)
+        {
+            skin &s = parts[i]->skins[j];
+            s.specglare = specglare;
+            s.glowglare = glowglare;
+        }
+    }
+
+    void setalphatest(float alphatest)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].alphatest = alphatest;
+    }
+
+    void setalphablend(bool alphablend)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].alphablend = alphablend;
+    }
+
+    void setfullbright(float fullbright)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].fullbright = fullbright;
+    }
+
+    void setcullface(bool cullface)
+    {
+        if(parts.empty()) loaddefaultparts();
+        loopv(parts) loopvj(parts[i]->skins) parts[i]->skins[j].cullface = cullface;
+    }
+
+    void calcbb(vec &center, vec &radius)
+    {
+        if(parts.empty()) return;
+        vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f);
+        matrix4x3 m;
+        initmatrix(m); 
+        parts[0]->calcbb(bbmin, bbmax, m);
+        radius = bbmax;
+        radius.sub(bbmin);
+        radius.mul(0.5f);
+        center = bbmin;
+        center.add(radius);
+    }
+
+    virtual void loaded()
+    {
+        scale /= 4;
+        if(parts.length()) parts[0]->translate = translate;
+        loopv(parts) parts[i]->loaded();
+    }
+
+    static bool enabletc, enablealphablend, enablecullface, enablenormals, enabletangents, enablebones, enabledepthoffset;
+    static vec lightdir, lightcolor;
+    static float transparent, lastalphatest;
+    static GLuint lastvbuf, lasttcbuf, lastnbuf, lastxbuf, lastbbuf, lastebuf, lastenvmaptex, closestenvmaptex;
+    static Texture *lasttex, *lastmasks, *lastnormalmap;
+    static int matrixpos;
+    static matrix4 matrixstack[64];
+
+    void startrender()
+    {
+        enabletc = enablealphablend = enablenormals = enabletangents = enablebones = enabledepthoffset = false;
+        enablecullface = true;
+        lastalphatest = -1;
+        lastvbuf = lasttcbuf = lastxbuf = lastnbuf = lastbbuf = lastebuf = lastenvmaptex = closestenvmaptex = 0;
+        lasttex = lastmasks = lastnormalmap = NULL;
+        transparent = 1;
+        shaderparamskey::invalidate();
+    }
+
+    static void disablebones()
+    {
+        gle::disableboneweight();
+        gle::disableboneindex();
+        enablebones = false;
+    }
+
+    static void disabletangents()
+    {
+        gle::disabletangent();
+        enabletangents = false;
+    }
+
+    static void disabletc()
+    {
+        gle::disabletexcoord0();
+        enabletc = false;
+    }
+
+    static void disablenormals()
+    {
+        gle::disablenormal();
+        enablenormals = false;
+    }
+
+    static void disablevbo()
+    {
+        if(lastebuf) gle::clearebo();
+        if(lastvbuf)
+        {
+            gle::clearvbo();
+            gle::disablevertex();
+        }
+        if(enabletc) disabletc();
+        if(enablenormals) disablenormals();
+        if(enabletangents) disabletangents();
+        if(enablebones) disablebones();
+        lastvbuf = lasttcbuf = lastxbuf = lastnbuf = lastbbuf = lastebuf = 0;
+    }
+
+    void endrender()
+    {
+        if(lastvbuf || lastebuf) disablevbo();
+        if(enablealphablend) glDisable(GL_BLEND);
+        if(!enablecullface) glEnable(GL_CULL_FACE);
+        if(enabledepthoffset) disablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+    }
+};
+
+bool animmodel::enabletc = false, animmodel::enablealphablend = false,
+     animmodel::enablecullface = true,
+     animmodel::enablenormals = false, animmodel::enabletangents = false, animmodel::enablebones = false, animmodel::enabledepthoffset = false;
+vec animmodel::lightdir(0, 0, 1), animmodel::lightcolor(1, 1, 1);
+float animmodel::transparent = 1, animmodel::lastalphatest = -1;
+GLuint animmodel::lastvbuf = 0, animmodel::lasttcbuf = 0, animmodel::lastnbuf = 0, animmodel::lastxbuf = 0, animmodel::lastbbuf = 0,
+       animmodel::lastebuf = 0, animmodel::lastenvmaptex = 0, animmodel::closestenvmaptex = 0;
+Texture *animmodel::lasttex = NULL, *animmodel::lastmasks = NULL, *animmodel::lastnormalmap = NULL;
+int animmodel::matrixpos = 0;
+matrix4 animmodel::matrixstack[64];
+
+static inline uint hthash(const animmodel::shaderparams &k)
+{
+    return memhash(&k, sizeof(k));
+}
+
+static inline bool htcmp(const animmodel::shaderparams &x, const animmodel::shaderparams &y)
+{
+    return !memcmp(&x, &y, sizeof(animmodel::shaderparams));
+}
+
+hashtable<animmodel::shaderparams, animmodel::shaderparamskey> animmodel::shaderparamskey::keys;
+int animmodel::shaderparamskey::firstversion = 0, animmodel::shaderparamskey::lastversion = 1;
+
+template<class MDL, class BASE> struct modelloader : BASE
+{
+    static MDL *loading;
+    static string dir;
+
+    modelloader(const char *name) : BASE(name) {}
+
+    static bool animated() { return true; }
+    static bool multiparted() { return true; }
+    static bool multimeshed() { return true; }
+
+    void startload()
+    {
+        loading = (MDL *)this;
+    }
+
+    void endload()
+    {
+        loading = NULL;
+    }
+
+    bool loadconfig()
+    {
+        formatstring(dir, "packages/models/%s", BASE::name);
+        defformatstring(cfgname, "packages/models/%s/%s.cfg", BASE::name, MDL::formatname());
+
+        identflags &= ~IDF_PERSIST;
+        bool success = execfile(cfgname, false);
+        identflags |= IDF_PERSIST;
+        return success;
+    }
+};
+
+template<class MDL, class BASE> MDL *modelloader<MDL, BASE>::loading = NULL;
+template<class MDL, class BASE> string modelloader<MDL, BASE>::dir = {'\0'}; // crashes clang if "" is used here
+
+template<class MDL, class MESH> struct modelcommands
+{
+    typedef struct MDL::part part;
+    typedef struct MDL::skin skin;
+
+    static void setdir(char *name)
+    {
+        if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        formatstring(MDL::dir, "packages/models/%s", name);
+    }
+
+    #define loopmeshes(meshname, m, body) \
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; } \
+        part &mdl = *MDL::loading->parts.last(); \
+        if(!mdl.meshes) return; \
+        loopv(mdl.meshes->meshes) \
+        { \
+            MESH &m = *(MESH *)mdl.meshes->meshes[i]; \
+            if(!strcmp(meshname, "*") || (m.name && !strcmp(m.name, meshname))) \
+            { \
+                body; \
+            } \
+        }
+
+    #define loopskins(meshname, s, body) loopmeshes(meshname, m, { skin &s = mdl.skins[i]; body; })
+    
+    static void setskin(char *meshname, char *tex, char *masks, float *envmapmax, float *envmapmin)
+    {
+        loopskins(meshname, s,
+            s.tex = textureload(makerelpath(MDL::dir, tex), 0, true, false);
+            if(*masks)
+            {
+                s.masks = textureload(makerelpath(MDL::dir, masks), 0, true, false);
+                s.envmapmax = *envmapmax;
+                s.envmapmin = *envmapmin;
+            }
+        );
+    }
+    
+    static void setspec(char *meshname, int *percent)
+    {
+        float spec = 1.0f;
+        if(*percent>0) spec = *percent/100.0f;
+        else if(*percent<0) spec = 0.0f;
+        loopskins(meshname, s, s.spec = spec);
+    }
+    
+    static void setambient(char *meshname, int *percent)
+    {
+        float ambient = 0.3f;
+        if(*percent>0) ambient = *percent/100.0f;
+        else if(*percent<0) ambient = 0.0f;
+        loopskins(meshname, s, s.ambient = ambient);
+    }
+    
+    static void setglow(char *meshname, int *percent, int *delta, float *pulse)
+    {
+        float glow = 3.0f, glowdelta = *delta/100.0f, glowpulse = *pulse > 0 ? *pulse/1000.0f : 0;
+        if(*percent>0) glow = *percent/100.0f;
+        else if(*percent<0) glow = 0.0f;
+        glowdelta -= glow;
+        loopskins(meshname, s, { s.glow = glow; s.glowdelta = glowdelta; s.glowpulse = glowpulse; });
+    }
+    
+    static void setglare(char *meshname, float *specglare, float *glowglare)
+    {
+        loopskins(meshname, s, { s.specglare = *specglare; s.glowglare = *glowglare; });
+    }
+    
+    static void setalphatest(char *meshname, float *cutoff)
+    {
+        loopskins(meshname, s, s.alphatest = max(0.0f, min(1.0f, *cutoff)));
+    }
+    
+    static void setalphablend(char *meshname, int *blend)
+    {
+        loopskins(meshname, s, s.alphablend = *blend!=0);
+    }
+    
+    static void setcullface(char *meshname, int *cullface)
+    {
+        loopskins(meshname, s, s.cullface = *cullface!=0);
+    }
+    
+    static void setenvmap(char *meshname, char *envmap)
+    {
+        Texture *tex = cubemapload(envmap);
+        loopskins(meshname, s, s.envmap = tex);
+    }
+    
+    static void setbumpmap(char *meshname, char *normalmapfile)
+    {
+        Texture *normalmaptex = textureload(makerelpath(MDL::dir, normalmapfile), 0, true, false);
+        loopskins(meshname, s, s.normalmap = normalmaptex);
+    }
+    
+    static void setfullbright(char *meshname, float *fullbright)
+    {
+        loopskins(meshname, s, s.fullbright = *fullbright);
+    }
+    
+    static void setshader(char *meshname, char *shader)
+    {
+        loopskins(meshname, s, s.shader = lookupshaderbyname(shader));
+    }
+    
+    static void setscroll(char *meshname, float *scrollu, float *scrollv)
+    {
+        loopskins(meshname, s, { s.scrollu = *scrollu; s.scrollv = *scrollv; });
+    }
+    
+    static void setnoclip(char *meshname, int *noclip)
+    {
+        loopmeshes(meshname, m, m.noclip = *noclip!=0);
+    }
+  
+    static void setlink(int *parent, int *child, char *tagname, float *x, float *y, float *z)
+    {
+        if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        if(!MDL::loading->parts.inrange(*parent) || !MDL::loading->parts.inrange(*child)) { conoutf(CON_ERROR, "no models loaded to link"); return; }
+        if(!MDL::loading->parts[*parent]->link(MDL::loading->parts[*child], tagname, vec(*x, *y, *z))) conoutf(CON_ERROR, "could not link model %s", MDL::loading->name);
+    }
+    template<class F> void modelcommand(F *fun, const char *suffix, const char *args)
+    {
+        defformatstring(name, "%s%s", MDL::formatname(), suffix);
+        addcommand(newstring(name), (void (*)())fun, args);
+    }
+
+    modelcommands()
+    {
+        modelcommand(setdir, "dir", "s");
+        if(MDL::multimeshed())
+        {
+            modelcommand(setskin, "skin", "sssff");
+            modelcommand(setspec, "spec", "si");
+            modelcommand(setambient, "ambient", "si");
+            modelcommand(setglow, "glow", "siif");
+            modelcommand(setglare, "glare", "sff");
+            modelcommand(setalphatest, "alphatest", "sf");
+            modelcommand(setalphablend, "alphablend", "si");
+            modelcommand(setcullface, "cullface", "si");
+            modelcommand(setenvmap, "envmap", "ss");
+            modelcommand(setbumpmap, "bumpmap", "ss");
+            modelcommand(setfullbright, "fullbright", "sf");
+            modelcommand(setshader, "shader", "ss");
+            modelcommand(setscroll, "scroll", "sff");
+            modelcommand(setnoclip, "noclip", "si");
+        }
+        if(MDL::multiparted()) modelcommand(setlink, "link", "iisfff");
+    }
+};
+
diff --git a/src/engine/bih.cpp b/src/engine/bih.cpp
new file mode 100644 (file)
index 0000000..e735f38
--- /dev/null
@@ -0,0 +1,330 @@
+#include "engine.h"
+
+bool BIH::triintersect(const mesh &m, int tidx, const vec &mo, const vec &mray, float maxdist, float &dist, int mode)
+{
+    const tri &t = m.tris[tidx];
+    vec a = m.getpos(t.vert[0]), b = m.getpos(t.vert[1]).sub(a), c = m.getpos(t.vert[2]).sub(a),
+        n = vec().cross(b, c), r = vec(a).sub(mo), e = vec().cross(r, mray);
+    float det = mray.dot(n), v, w, f;
+    if(det >= 0)
+    {
+        if(!(mode&RAY_SHADOW) && m.flags&MESH_CULLFACE) return false;
+        v = e.dot(c);
+        if(v < 0 || v > det) return false;
+        w = -e.dot(b);
+        if(w < 0 || v + w > det) return false;
+        f = r.dot(n)*m.scale;
+        if(f < 0 || f > maxdist*det || !det) return false;
+    }
+    else
+    {
+        v = e.dot(c);
+        if(v > 0 || v < det) return false;
+        w = -e.dot(b);
+        if(w > 0 || v + w < det) return false;
+        f = r.dot(n)*m.scale;
+        if(f > 0 || f < maxdist*det) return false;
+    }
+    float invdet = 1/det;
+    if(m.flags&MESH_ALPHA && (mode&RAY_ALPHAPOLY)==RAY_ALPHAPOLY && (m.tex->alphamask || (lightmapping <= 1 && loadalphamask(m.tex))))
+    {
+        vec2 at = m.gettc(t.vert[0]), bt = m.gettc(t.vert[1]).sub(at).mul(v*invdet), ct = m.gettc(t.vert[2]).sub(at).mul(w*invdet);
+        at.add(bt).add(ct);
+        int si = clamp(int(m.tex->xs * at.x), 0, m.tex->xs-1),
+            ti = clamp(int(m.tex->ys * at.y), 0, m.tex->ys-1);
+        if(!(m.tex->alphamask[ti*((m.tex->xs+7)/8) + si/8] & (1<<(si%8)))) return false;
+    }
+    dist = f*invdet;
+    return true;
+}
+
+struct traversestate
+{
+    BIH::node *node;
+    float tmin, tmax;
+};
+
+inline bool BIH::traverse(const mesh &m, const vec &o, const vec &ray, const vec &invray, float maxdist, float &dist, int mode, node *curnode, float tmin, float tmax)
+{
+    traversestate stack[128];
+    int stacksize = 0;
+    ivec order(ray.x>0 ? 0 : 1, ray.y>0 ? 0 : 1, ray.z>0 ? 0 : 1);
+    vec mo = m.invxform.transform(o), mray = m.invxformnorm.transform(ray);
+    for(;;)
+    {
+        int axis = curnode->axis();
+        int nearidx = order[axis], faridx = nearidx^1;
+        float nearsplit = (curnode->split[nearidx] - o[axis])*invray[axis],
+              farsplit = (curnode->split[faridx] - o[axis])*invray[axis];
+
+        if(nearsplit <= tmin)
+        {
+            if(farsplit < tmax)
+            {
+                if(!curnode->isleaf(faridx))
+                {
+                    curnode += curnode->childindex(faridx);
+                    tmin = max(tmin, farsplit);
+                    continue;
+                }
+                else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true;
+            }
+        }
+        else if(curnode->isleaf(nearidx))
+        {
+            if(triintersect(m, curnode->childindex(nearidx), mo, mray, maxdist, dist, mode)) return true;
+            if(farsplit < tmax)
+            {
+                if(!curnode->isleaf(faridx))
+                {
+                    curnode += curnode->childindex(faridx);
+                    tmin = max(tmin, farsplit);
+                    continue;
+                }
+                else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true;
+            }
+        }
+        else
+        {
+            if(farsplit < tmax)
+            {
+                if(!curnode->isleaf(faridx))
+                {
+                    if(stacksize < int(sizeof(stack)/sizeof(stack[0])))
+                    {
+                        traversestate &save = stack[stacksize++];
+                        save.node = curnode + curnode->childindex(faridx);
+                        save.tmin = max(tmin, farsplit);
+                        save.tmax = tmax;
+                    }
+                    else
+                    {
+                        if(traverse(m, o, ray, invray, maxdist, dist, mode, curnode + curnode->childindex(nearidx), tmin, min(tmax, nearsplit))) return true;
+                        curnode += curnode->childindex(faridx);
+                        tmin = max(tmin, farsplit);
+                        continue;
+                    }
+                }
+                else if(triintersect(m, curnode->childindex(faridx), mo, mray, maxdist, dist, mode)) return true;
+            }
+            curnode += curnode->childindex(nearidx);
+            tmax = min(tmax, nearsplit);
+            continue;
+        }
+        if(stacksize <= 0) return false;
+        traversestate &restore = stack[--stacksize];
+        curnode = restore.node;
+        tmin = restore.tmin;
+        tmax = restore.tmax;
+    }
+}
+
+inline bool BIH::traverse(const vec &o, const vec &ray, float maxdist, float &dist, int mode)
+{
+    vec invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f);
+    loopi(nummeshes)
+    {
+        mesh &m = meshes[i];
+        if(!(mode&RAY_SHADOW) && m.flags&MESH_NOCLIP) continue;
+        float t1 = (m.bbmin.x - o.x)*invray.x,
+              t2 = (m.bbmax.x - o.x)*invray.x,
+              tmin, tmax;
+        if(invray.x > 0) { tmin = t1; tmax = t2; } else { tmin = t2; tmax = t1; }
+        t1 = (m.bbmin.y - o.y)*invray.y;
+        t2 = (m.bbmax.y - o.y)*invray.y;
+        if(invray.y > 0) { tmin = max(tmin, t1); tmax = min(tmax, t2); } else { tmin = max(tmin, t2); tmax = min(tmax, t1); }
+        t1 = (m.bbmin.z - o.z)*invray.z;
+        t2 = (m.bbmax.z - o.z)*invray.z;
+        if(invray.z > 0) { tmin = max(tmin, t1); tmax = min(tmax, t2); } else { tmin = max(tmin, t2); tmax = min(tmax, t1); }
+        tmax = min(tmax, maxdist);
+        if(tmin < tmax && traverse(m, o, ray, invray, maxdist, dist, mode, m.nodes, tmin, tmax)) return true;
+    }
+    return false;
+}
+
+void BIH::build(mesh &m, ushort *indices, int numindices, const ivec &vmin, const ivec &vmax)
+{
+    int axis = 2;
+    loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k;
+
+    ivec leftmin, leftmax, rightmin, rightmax;
+    int splitleft, splitright;
+    int left, right;
+    loopk(3)
+    {
+        leftmin = rightmin = ivec(INT_MAX, INT_MAX, INT_MAX);
+        leftmax = rightmax = ivec(INT_MIN, INT_MIN, INT_MIN);
+        int split = (vmax[axis] + vmin[axis])/2;
+        for(left = 0, right = numindices, splitleft = SHRT_MIN, splitright = SHRT_MAX; left < right;)
+        {
+            const tribb &tri = m.tribbs[indices[left]];
+            ivec trimin = ivec(tri.center).sub(ivec(tri.radius)),
+                 trimax = ivec(tri.center).add(ivec(tri.radius));
+            int amin = trimin[axis], amax = trimax[axis];
+            if(max(split - amin, 0) > max(amax - split, 0))
+            {
+                ++left;
+                splitleft = max(splitleft, amax);
+                leftmin.min(trimin);
+                leftmax.max(trimax);
+            }
+            else
+            {
+                --right;
+                swap(indices[left], indices[right]);
+                splitright = min(splitright, amin);
+                rightmin.min(trimin);
+                rightmax.max(trimax);
+            }
+        }
+        if(left > 0 && right < numindices) break;
+        axis = (axis+1)%3;
+    }
+
+    if(!left || right==numindices)
+    {
+        leftmin = rightmin = ivec(INT_MAX, INT_MAX, INT_MAX);
+        leftmax = rightmax = ivec(INT_MIN, INT_MIN, INT_MIN);
+        left = right = numindices/2;
+        splitleft = SHRT_MIN;
+        splitright = SHRT_MAX;
+        loopi(numindices)
+        {
+            const tribb &tri = m.tribbs[indices[i]];
+            ivec trimin = ivec(tri.center).sub(ivec(tri.radius)),
+                 trimax = ivec(tri.center).add(ivec(tri.radius));
+            if(i < left)
+            {
+                splitleft = max(splitleft, trimax[axis]);
+                leftmin.min(trimin);
+                leftmax.max(trimax);
+            }
+            else
+            {
+                splitright = min(splitright, trimin[axis]);
+                rightmin.min(trimin);
+                rightmax.max(trimax);
+            }
+        }
+    }
+
+    int offset = m.numnodes++;
+    node &curnode = m.nodes[offset];
+    curnode.split[0] = short(splitleft);
+    curnode.split[1] = short(splitright);
+
+    if(left==1) curnode.child[0] = (axis<<14) | indices[0];
+    else
+    {
+        curnode.child[0] = (axis<<14) | (m.numnodes - offset);
+        build(m, indices, left, leftmin, leftmax);
+    }
+
+    if(numindices-right==1) curnode.child[1] = (1<<15) | (left==1 ? 1<<14 : 0) | indices[right];
+    else
+    {
+        curnode.child[1] = (left==1 ? 1<<14 : 0) | (m.numnodes - offset);
+        build(m, &indices[right], numindices-right, rightmin, rightmax);
+    }
+}
+
+BIH::BIH(vector<mesh> &buildmeshes)
+  : meshes(NULL), nummeshes(0), nodes(NULL), numnodes(0), tribbs(NULL), numtris(0), bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f), center(0, 0, 0), radius(0), entradius(0)
+{
+    if(buildmeshes.empty()) return;
+    loopv(buildmeshes) numtris += buildmeshes[i].numtris;
+    if(!numtris) return;
+
+    nummeshes = buildmeshes.length();
+    meshes = new mesh[nummeshes];
+    memcpy(meshes, buildmeshes.getbuf(), sizeof(mesh)*buildmeshes.length());
+    tribbs = new tribb[numtris];
+    tribb *dsttri = tribbs;
+    loopi(nummeshes)
+    {
+        mesh &m = meshes[i];
+        m.scale = m.xform.a.magnitude();
+        m.invscale = 1/m.scale;
+        m.xformnorm = matrix3(m.xform);
+        m.xformnorm.normalize();
+        m.invxform.invert(m.xform);
+        m.invxformnorm = matrix3(m.invxform);
+        m.invxformnorm.normalize();
+        m.tribbs = dsttri;
+        const tri *srctri = m.tris;
+        vec mmin(1e16f, 1e16f, 1e16f), mmax(-1e16f, -1e16f, -1e16f);
+        loopj(m.numtris)
+        {
+            vec s0 = m.getpos(srctri->vert[0]), s1 = m.getpos(srctri->vert[1]), s2 = m.getpos(srctri->vert[2]),
+                v0 = m.xform.transform(s0), v1 = m.xform.transform(s1), v2 = m.xform.transform(s2),
+                vmin = vec(v0).min(v1).min(v2),
+                vmax = vec(v0).max(v1).max(v2);
+            mmin.min(vmin);
+            mmax.max(vmax);
+            ivec imin = ivec::floor(vmin), imax = ivec::ceil(vmax);
+            dsttri->center = svec(ivec(imin).add(imax).div(2));
+            dsttri->radius = svec(ivec(imax).sub(imin).add(1).div(2));
+            ++srctri;
+            ++dsttri;
+        }
+        loopk(3) if(fabs(mmax[k] - mmin[k]) < 0.125f)
+        {
+            float mid = (mmin[k] + mmax[k]) / 2;
+            mmin[k] = mid - 0.0625f;
+            mmax[k] = mid + 0.0625f;
+        }
+        m.bbmin = mmin;
+        m.bbmax = mmax;
+        bbmin.min(mmin);
+        bbmax.max(mmax);
+    }
+
+    center = vec(bbmin).add(bbmax).mul(0.5f);
+    radius = vec(bbmax).sub(bbmin).mul(0.5f).magnitude();
+    entradius = max(bbmin.squaredlen(), bbmax.squaredlen());
+
+    nodes = new node[numtris];
+    node *curnode = nodes;
+    ushort *indices = new ushort[numtris];
+    loopi(nummeshes)
+    {
+        mesh &m = meshes[i];
+        m.nodes = curnode;
+        loopj(m.numtris) indices[j] = j;
+        build(m, indices, m.numtris, ivec::floor(m.bbmin), ivec::ceil(m.bbmax));
+        curnode += m.numnodes;
+    }
+    delete[] indices;
+    numnodes = int(curnode - nodes);
+}
+
+BIH::~BIH()
+{
+    delete[] meshes;
+    delete[] nodes;
+    delete[] tribbs;
+}
+
+bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist)
+{
+    model *m = loadmapmodel(e.attr2);
+    if(!m) return false;
+    if(mode&RAY_SHADOW)
+    {
+        if(!m->shadow || e.flags&EF_NOSHADOW) return false;
+    }
+    else if((mode&RAY_ENTS)!=RAY_ENTS && (!m->collide || e.flags&EF_NOCOLLIDE)) return false;
+    if(!m->bih && (lightmapping > 1 || !m->setBIH())) return false;
+    vec mo = vec(o).sub(e.o), mray(ray);
+    float v = mo.dot(mray), inside = m->bih->entradius - mo.squaredlen();
+    if((inside < 0 && v > 0) || inside + v*v < 0) return false;
+    int yaw = e.attr1;
+    if(yaw != 0) 
+    {
+        const vec2 &rot = sincosmod360(-yaw);
+        mo.rotate_around_z(rot);
+        mray.rotate_around_z(rot);
+    }
+    return m->bih->traverse(mo, mray, maxdist ? maxdist : 1e16f, dist, mode);
+}
+
diff --git a/src/engine/bih.h b/src/engine/bih.h
new file mode 100644 (file)
index 0000000..0f9482f
--- /dev/null
@@ -0,0 +1,79 @@
+struct BIH
+{
+    struct node
+    {
+        short split[2];
+        ushort child[2];
+
+        int axis() const { return child[0]>>14; }
+        int childindex(int which) const { return child[which]&0x3FFF; }
+        bool isleaf(int which) const { return (child[1]&(1<<(14+which)))!=0; }
+    };
+
+    struct tri
+    {
+        ushort vert[3];
+    };
+
+    struct tribb
+    {
+        svec center, radius;
+
+        bool outside(const ivec &bo, const ivec &br) const
+        {
+            return abs(bo.x - center.x) > br.x + radius.x ||
+                   abs(bo.y - center.y) > br.y + radius.y ||
+                   abs(bo.z - center.z) > br.z + radius.z;
+        }
+    };
+
+    enum { MESH_NOCLIP = 1<<0, MESH_ALPHA = 1<<1, MESH_CULLFACE = 1<<2 };
+
+    struct mesh
+    {
+        enum { MAXTRIS = 1<<14 };
+
+        matrix4x3 xform, invxform;
+        matrix3 xformnorm, invxformnorm;
+        float scale, invscale;
+        node *nodes;
+        int numnodes;
+        const tri *tris;
+        const tribb *tribbs;
+        int numtris;
+        const uchar *pos, *tc;
+        int posstride, tcstride;
+        Texture *tex;
+        int flags;
+        vec bbmin, bbmax;
+
+        mesh() : numnodes(0), numtris(0), tex(NULL), flags(0) {}
+
+        vec getpos(int i) const { return *(const vec *)(pos + i*posstride); }
+        vec2 gettc(int i) const { return *(const vec2 *)(tc + i*tcstride); }
+    };
+
+    mesh *meshes;
+    int nummeshes;
+    node *nodes;
+    int numnodes;
+    tribb *tribbs;
+    int numtris;
+    vec bbmin, bbmax, center;
+    float radius, entradius;
+
+    BIH(vector<mesh> &buildmeshes);
+
+    ~BIH();
+
+    void build(mesh &m, ushort *indices, int numindices, const ivec &vmin, const ivec &vmax);
+
+    bool traverse(const vec &o, const vec &ray, float maxdist, float &dist, int mode);
+    bool traverse(const mesh &m, const vec &o, const vec &ray, const vec &invray, float maxdist, float &dist, int mode, node *curnode, float tmin, float tmax);
+    bool triintersect(const mesh &m, int tidx, const vec &mo, const vec &mray, float maxdist, float &dist, int mode);
+    
+    void preload();
+};
+
+extern bool mmintersect(const extentity &e, const vec &o, const vec &ray, float maxdist, int mode, float &dist);
+
diff --git a/src/engine/blend.cpp b/src/engine/blend.cpp
new file mode 100644 (file)
index 0000000..16f21c2
--- /dev/null
@@ -0,0 +1,862 @@
+#include "engine.h"
+
+enum
+{
+    BM_BRANCH = 0,
+    BM_SOLID,
+    BM_IMAGE
+};
+
+struct BlendMapBranch;
+struct BlendMapSolid;
+struct BlendMapImage;
+
+struct BlendMapNode
+{
+    union
+    {
+        BlendMapBranch *branch;
+        BlendMapSolid *solid;
+        BlendMapImage *image;
+    };
+
+    void cleanup(int type);
+    void splitsolid(uchar &type, uchar val);
+};
+
+struct BlendMapBranch
+{
+    uchar type[4];
+    BlendMapNode children[4];
+
+    ~BlendMapBranch()
+    {
+        loopi(4) children[i].cleanup(type[i]);
+    }
+
+    uchar shrink(BlendMapNode &child, int quadrant);
+};
+
+struct BlendMapSolid
+{
+    uchar val;
+
+    BlendMapSolid(uchar val) : val(val) {}
+};
+
+#define BM_SCALE 1
+#define BM_IMAGE_SIZE 64
+
+struct BlendMapImage
+{
+    uchar data[BM_IMAGE_SIZE*BM_IMAGE_SIZE];
+};
+
+void BlendMapNode::cleanup(int type)
+{
+    switch(type)
+    {
+        case BM_BRANCH: delete branch; break;
+        case BM_IMAGE: delete image; break;
+    }
+}
+
+#define DEFBMSOLIDS(n) n, n+1, n+2, n+3, n+4, n+5, n+6, n+7, n+8, n+9, n+10, n+11, n+12, n+13, n+14, n+15
+
+static BlendMapSolid bmsolids[256] = 
+{
+    DEFBMSOLIDS(0x00), DEFBMSOLIDS(0x10), DEFBMSOLIDS(0x20), DEFBMSOLIDS(0x30),
+    DEFBMSOLIDS(0x40), DEFBMSOLIDS(0x50), DEFBMSOLIDS(0x60), DEFBMSOLIDS(0x70),
+    DEFBMSOLIDS(0x80), DEFBMSOLIDS(0x90), DEFBMSOLIDS(0xA0), DEFBMSOLIDS(0xB0),
+    DEFBMSOLIDS(0xC0), DEFBMSOLIDS(0xD0), DEFBMSOLIDS(0xE0), DEFBMSOLIDS(0xF0),
+};
+
+void BlendMapNode::splitsolid(uchar &type, uchar val)
+{
+    cleanup(type);
+    type = BM_BRANCH;
+    branch = new BlendMapBranch;
+    loopi(4)
+    {
+        branch->type[i] = BM_SOLID;
+        branch->children[i].solid = &bmsolids[val];
+    }
+}
+
+uchar BlendMapBranch::shrink(BlendMapNode &child, int quadrant)
+{
+    uchar childtype = type[quadrant];
+    child = children[quadrant];
+    type[quadrant] = BM_SOLID;
+    children[quadrant].solid = &bmsolids[0];
+    return childtype;
+}
+
+struct BlendMapRoot : BlendMapNode
+{
+    uchar type;
+
+    BlendMapRoot() : type(BM_SOLID) { solid = &bmsolids[0xFF]; }
+    BlendMapRoot(uchar type, const BlendMapNode &node) : BlendMapNode(node), type(type) {}
+
+    void cleanup() { BlendMapNode::cleanup(type); }
+
+    void shrink(int quadrant)
+    {
+        if(type == BM_BRANCH) 
+        {
+            BlendMapRoot oldroot = *this;
+            type = branch->shrink(*this, quadrant);
+            oldroot.cleanup();
+        }
+    }
+};
+
+static BlendMapRoot blendmap;
+
+struct BlendMapCache
+{
+    BlendMapRoot node;
+    int scale;
+    ivec2 origin;
+};
+
+BlendMapCache *newblendmapcache() { return new BlendMapCache; }
+
+void freeblendmapcache(BlendMapCache *&cache) { delete cache; cache = NULL; }
+
+bool setblendmaporigin(BlendMapCache *cache, const ivec &o, int size)
+{
+    if(blendmap.type!=BM_BRANCH)
+    {
+        cache->node = blendmap;
+        cache->scale = worldscale-BM_SCALE;
+        cache->origin = ivec2(0, 0);
+        return cache->node.solid!=&bmsolids[0xFF];
+    }
+
+    BlendMapBranch *bm = blendmap.branch;
+    int bmscale = worldscale-BM_SCALE, bmsize = 1<<bmscale,
+        x = o.x>>BM_SCALE, y = o.y>>BM_SCALE,
+        x1 = max(x-1, 0), y1 = max(y-1, 0),
+        x2 = min(((o.x + size + (1<<BM_SCALE)-1)>>BM_SCALE) + 1, bmsize),
+        y2 = min(((o.y + size + (1<<BM_SCALE)-1)>>BM_SCALE) + 1, bmsize),
+        diff = (x1^x2)|(y1^y2);
+    if(diff < bmsize) while(!(diff&(1<<(bmscale-1))))
+    {
+        bmscale--;
+        int n = (((y1>>bmscale)&1)<<1) | ((x1>>bmscale)&1);
+        if(bm->type[n]!=BM_BRANCH)
+        {
+            cache->node = BlendMapRoot(bm->type[n], bm->children[n]);
+            cache->scale = bmscale;
+            cache->origin = ivec2(x1&(~0U<<bmscale), y1&(~0U<<bmscale));
+            return cache->node.solid!=&bmsolids[0xFF];
+        }
+        bm = bm->children[n].branch;
+    }
+
+    cache->node.type = BM_BRANCH;
+    cache->node.branch = bm;
+    cache->scale = bmscale;
+    cache->origin = ivec2(x1&(~0U<<bmscale), y1&(~0U<<bmscale));
+    return true;
+}
+
+bool hasblendmap(BlendMapCache *cache)
+{
+    return cache->node.solid!=&bmsolids[0xFF];
+}
+
+static uchar lookupblendmap(int x, int y, BlendMapBranch *bm, int bmscale)
+{
+    for(;;)
+    {
+        bmscale--;
+        int n = (((y>>bmscale)&1)<<1) | ((x>>bmscale)&1);
+        switch(bm->type[n])
+        {
+            case BM_SOLID: return bm->children[n].solid->val;
+            case BM_IMAGE: return bm->children[n].image->data[(y&((1<<bmscale)-1))*BM_IMAGE_SIZE + (x&((1<<bmscale)-1))];
+        }
+        bm = bm->children[n].branch;
+    }
+}
+    
+uchar lookupblendmap(BlendMapCache *cache, const vec &pos)
+{
+    if(cache->node.type==BM_SOLID) return cache->node.solid->val;
+    
+    uchar vals[4], *val = vals;
+    float bx = pos.x/(1<<BM_SCALE) - 0.5f, by = pos.y/(1<<BM_SCALE) - 0.5f;
+    int ix = (int)floor(bx), iy = (int)floor(by),
+        rx = ix-cache->origin.x, ry = iy-cache->origin.y;
+    loop(vy, 2) loop(vx, 2)
+    {
+        int cx = clamp(rx+vx, 0, (1<<cache->scale)-1), cy = clamp(ry+vy, 0, (1<<cache->scale)-1);
+        if(cache->node.type==BM_IMAGE)
+            *val++ = cache->node.image->data[cy*BM_IMAGE_SIZE + cx];
+        else *val++ = lookupblendmap(cx, cy, cache->node.branch, cache->scale);
+    }
+    float fx = bx - ix, fy = by - iy;
+    return uchar((1-fy)*((1-fx)*vals[0] + fx*vals[1]) +
+                 fy*((1-fx)*vals[2] + fx*vals[3]));
+}
+
+static void fillblendmap(uchar &type, BlendMapNode &node, int size, uchar val, int x1, int y1, int x2, int y2)
+{
+    if(max(x1, y1) <= 0 && min(x2, y2) >= size)
+    {
+        node.cleanup(type);
+        type = BM_SOLID;
+        node.solid = &bmsolids[val];
+        return;
+    }
+
+    if(type==BM_BRANCH)
+    {
+        size /= 2;
+        if(y1 < size)
+        {
+            if(x1 < size) fillblendmap(node.branch->type[0], node.branch->children[0], size, val,
+                                        x1, y1, min(x2, size), min(y2, size));
+            if(x2 > size) fillblendmap(node.branch->type[1], node.branch->children[1], size, val, 
+                                        max(x1-size, 0), y1, x2-size, min(y2, size));
+        }
+        if(y2 > size)
+        {
+            if(x1 < size) fillblendmap(node.branch->type[2], node.branch->children[2], size, val,
+                                        x1, max(y1-size, 0), min(x2, size), y2-size);
+            if(x2 > size) fillblendmap(node.branch->type[3], node.branch->children[3], size, val,
+                                        max(x1-size, 0), max(y1-size, 0), x2-size, y2-size);
+        }
+        loopi(4) if(node.branch->type[i]!=BM_SOLID || node.branch->children[i].solid->val!=val) return;
+        node.cleanup(type);
+        type = BM_SOLID;
+        node.solid = &bmsolids[val];
+        return;
+    }             
+    else if(type==BM_SOLID)
+    {
+        uchar oldval = node.solid->val;
+        if(oldval==val) return;
+
+        if(size > BM_IMAGE_SIZE)
+        {
+            node.splitsolid(type, oldval);
+            fillblendmap(type, node, size, val, x1, y1, x2, y2);
+            return;
+        }
+        type = BM_IMAGE;
+        node.image = new BlendMapImage;
+        memset(node.image->data, oldval, sizeof(node.image->data));
+    }
+    
+    uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1];
+    loopi(y2-y1)
+    {
+        memset(dst, val, x2-x1);
+        dst += BM_IMAGE_SIZE;
+    }
+}
+
+void fillblendmap(int x, int y, int w, int h, uchar val)
+{
+    int bmsize = worldsize>>BM_SCALE,
+        x1 = clamp(x, 0, bmsize),
+        y1 = clamp(y, 0, bmsize),
+        x2 = clamp(x+w, 0, bmsize),
+        y2 = clamp(y+h, 0, bmsize);
+    if(max(x1, y1) >= bmsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return;
+    fillblendmap(blendmap.type, blendmap, bmsize, val, x1, y1, x2, y2);
+}
+
+static void invertblendmap(uchar &type, BlendMapNode &node, int size, int x1, int y1, int x2, int y2)
+{
+    if(type==BM_BRANCH)
+    {
+        size /= 2;
+        if(y1 < size)
+        {
+            if(x1 < size) invertblendmap(node.branch->type[0], node.branch->children[0], size,
+                                        x1, y1, min(x2, size), min(y2, size));
+            if(x2 > size) invertblendmap(node.branch->type[1], node.branch->children[1], size,
+                                        max(x1-size, 0), y1, x2-size, min(y2, size));
+        }
+        if(y2 > size)
+        {
+            if(x1 < size) invertblendmap(node.branch->type[2], node.branch->children[2], size,
+                                        x1, max(y1-size, 0), min(x2, size), y2-size);
+            if(x2 > size) invertblendmap(node.branch->type[3], node.branch->children[3], size,
+                                        max(x1-size, 0), max(y1-size, 0), x2-size, y2-size);
+        }
+        return;
+    }
+    else if(type==BM_SOLID)
+    {
+        fillblendmap(type, node, size, 255-node.solid->val, x1, y1, x2, y2);
+    }
+    else if(type==BM_IMAGE)
+    {
+        uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1];
+        loopi(y2-y1)
+        {
+            loopj(x2-x1) dst[j] = 255-dst[j];
+            dst += BM_IMAGE_SIZE;
+        }
+    }
+}
+
+void invertblendmap(int x, int y, int w, int h)
+{
+    int bmsize = worldsize>>BM_SCALE,
+        x1 = clamp(x, 0, bmsize),
+        y1 = clamp(y, 0, bmsize),
+        x2 = clamp(x+w, 0, bmsize),
+        y2 = clamp(y+h, 0, bmsize);
+    if(max(x1, y1) >= bmsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return;
+    invertblendmap(blendmap.type, blendmap, bmsize, x1, y1, x2, y2);
+}
+
+static void optimizeblendmap(uchar &type, BlendMapNode &node)
+{
+    switch(type)
+    {
+        case BM_IMAGE:
+        {
+            uint val = node.image->data[0];
+            val |= val<<8;
+            val |= val<<16;
+            for(uint *data = (uint *)node.image->data, *end = &data[sizeof(node.image->data)/sizeof(uint)]; data < end; data++) 
+                if(*data != val) return;
+            node.cleanup(type);
+            type = BM_SOLID;
+            node.solid = &bmsolids[val&0xFF];
+            break;
+        }
+        case BM_BRANCH:
+        {
+            loopi(4) optimizeblendmap(node.branch->type[i], node.branch->children[i]);
+            if(node.branch->type[3]!=BM_SOLID) return;
+            uint val = node.branch->children[3].solid->val;
+            loopi(3) if(node.branch->type[i]!=BM_SOLID || node.branch->children[i].solid->val != val) return;
+            node.cleanup(type);
+            type = BM_SOLID;
+            node.solid = &bmsolids[val];
+            break;
+        }
+    }
+}
+
+void optimizeblendmap()
+{
+    optimizeblendmap(blendmap.type, blendmap);
+}
+
+VARF(blendpaintmode, 0, 0, 5,
+{
+    if(!blendpaintmode) stoppaintblendmap();
+});
+
+static void blitblendmap(uchar &type, BlendMapNode &node, int bmx, int bmy, int bmsize, uchar *src, int sx, int sy, int sw, int sh, int smode)
+{
+    if(type==BM_BRANCH)
+    {
+        bmsize /= 2;
+        if(sy < bmy + bmsize)
+        {
+            if(sx < bmx + bmsize) blitblendmap(node.branch->type[0], node.branch->children[0], bmx, bmy, bmsize, src, sx, sy, sw, sh, smode);
+            if(sx + sw > bmx + bmsize) blitblendmap(node.branch->type[1], node.branch->children[1], bmx+bmsize, bmy, bmsize, src, sx, sy, sw, sh, smode);
+        }
+        if(sy + sh > bmy + bmsize)
+        {
+            if(sx < bmx + bmsize) blitblendmap(node.branch->type[2], node.branch->children[2], bmx, bmy+bmsize, bmsize, src, sx, sy, sw, sh, smode);
+            if(sx + sw > bmx + bmsize) blitblendmap(node.branch->type[3], node.branch->children[3], bmx+bmsize, bmy+bmsize, bmsize, src, sx, sy, sw, sh, smode);
+        }
+        return;
+    }
+    if(type==BM_SOLID)
+    {
+        uchar val = node.solid->val;
+        if(bmsize > BM_IMAGE_SIZE)
+        {
+            node.splitsolid(type, val);
+            blitblendmap(type, node, bmx, bmy, bmsize, src, sx, sy, sw, sh, smode);
+            return;
+        }
+
+        type = BM_IMAGE;
+        node.image = new BlendMapImage;
+        memset(node.image->data, val, sizeof(node.image->data));
+    }
+
+    int x1 = clamp(sx - bmx, 0, bmsize), y1 = clamp(sy - bmy, 0, bmsize),
+        x2 = clamp(sx+sw - bmx, 0, bmsize), y2 = clamp(sy+sh - bmy, 0, bmsize);
+    uchar *dst = &node.image->data[y1*BM_IMAGE_SIZE + x1];
+    src += max(bmy - sy, 0)*sw + max(bmx - sx, 0);
+    loopi(y2-y1)
+    {
+        switch(smode)
+        {
+            case 1:
+                memcpy(dst, src, x2 - x1);
+                break;
+
+            case 2:
+                loopi(x2 - x1) dst[i] = min(dst[i], src[i]); 
+                break;
+
+            case 3:
+                loopi(x2 - x1) dst[i] = max(dst[i], src[i]);
+                break;
+
+            case 4:
+                loopi(x2 - x1) dst[i] = min(dst[i], uchar(0xFF - src[i]));
+                break;
+
+            case 5:
+                loopi(x2 - x1) dst[i] = max(dst[i], uchar(0xFF - src[i]));
+                break;
+        }
+        dst += BM_IMAGE_SIZE;
+        src += sw;
+    } 
+}
+
+void blitblendmap(uchar *src, int sx, int sy, int sw, int sh, int smode)
+{
+    int bmsize = worldsize>>BM_SCALE;
+    if(max(sx, sy) >= bmsize || min(sx+sw, sy+sh) <= 0 || min(sw, sh) <= 0) return;
+    blitblendmap(blendmap.type, blendmap, 0, 0, bmsize, src, sx, sy, sw, sh, smode);
+}
+        
+void resetblendmap()
+{
+    blendmap.cleanup();
+    blendmap.type = BM_SOLID;
+    blendmap.solid = &bmsolids[0xFF];
+}
+
+void enlargeblendmap()
+{
+    if(blendmap.type == BM_SOLID) return;
+    BlendMapBranch *branch = new BlendMapBranch;
+    branch->type[0] = blendmap.type;
+    branch->children[0] = blendmap;
+    loopi(3)
+    {
+        branch->type[i+1] = BM_SOLID;
+        branch->children[i+1].solid = &bmsolids[0xFF];
+    }
+    blendmap.type = BM_BRANCH;
+    blendmap.branch = branch;
+}
+
+void shrinkblendmap(int octant)
+{
+    blendmap.shrink(octant&3);
+}
+
+void moveblendmap(uchar type, BlendMapNode &node, int size, int x, int y, int dx, int dy)
+{
+    if(type == BM_BRANCH)
+    {
+        size /= 2;
+        moveblendmap(node.branch->type[0], node.branch->children[0], size, x, y, dx, dy);
+        moveblendmap(node.branch->type[1], node.branch->children[1], size, x + size, y, dx, dy);
+        moveblendmap(node.branch->type[2], node.branch->children[2], size, x, y + size, dx, dy);
+        moveblendmap(node.branch->type[3], node.branch->children[3], size, x + size, y + size, dx, dy);
+        return;
+    }
+    else if(type == BM_SOLID)
+    {
+        fillblendmap(x+dx, y+dy, size, size, node.solid->val);
+    }
+    else if(type == BM_IMAGE)
+    {
+        blitblendmap(node.image->data, x+dx, y+dy, size, size, 1);
+    }
+}
+
+void moveblendmap(int dx, int dy)
+{
+    BlendMapRoot old = blendmap;
+    blendmap.type = BM_SOLID;
+    blendmap.solid = &bmsolids[0xFF];
+    moveblendmap(old.type, old, worldsize>>BM_SCALE, 0, 0, dx, dy);
+    old.cleanup();
+}
+struct BlendBrush
+{
+    char *name;
+    int w, h;
+    uchar *data;
+    GLuint tex;
+   
+    BlendBrush(const char *name, int w, int h) :
+      name(newstring(name)), w(w), h(h), data(new uchar[w*h]), tex(0)
+    {}
+
+    ~BlendBrush()
+    {
+        cleanup();
+        delete[] name;
+        if(data) delete[] data;
+    }
+
+    void cleanup()
+    {
+        if(tex) { glDeleteTextures(1, &tex); tex = 0; }
+    }
+
+    void gentex()
+    {
+        if(!tex) glGenTextures(1, &tex);
+        uchar *buf = new uchar[2*w*h];
+        uchar *dst = buf, *src = data;
+        loopi(h)
+        {
+            loopj(w) *dst++ = 255 - *src++;
+        }
+        createtexture(tex, w, h, buf, 3, 1, hasTRG ? GL_R8 : GL_LUMINANCE8);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+        GLfloat border[4] = { 0, 0, 0, 0 };
+        glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
+        delete[] buf;
+    }
+    
+    void reorient(bool flipx, bool flipy, bool swapxy)
+    {
+        uchar *rdata = new uchar[w*h];
+        int stridex = 1, stridey = 1;
+        if(swapxy) stridex *= h; else stridey *= w;
+        uchar *src = data, *dst = rdata;
+        if(flipx) { dst += (w-1)*stridex; stridex = -stridex; }
+        if(flipy) { dst += (h-1)*stridey; stridey = -stridey; }
+        loopi(h)
+        {
+            uchar *curdst = dst;
+            loopj(w) 
+            {
+                *curdst = *src++;
+                curdst += stridex;
+            }
+            dst += stridey;
+        }
+        if(swapxy) swap(w, h);
+        delete[] data;
+        data = rdata;
+        if(tex) gentex();
+    } 
+};
+
+static vector<BlendBrush *> brushes;
+static int curbrush = -1;
+
+void cleanupblendmap()
+{
+    loopv(brushes) brushes[i]->cleanup();
+}
+
+void clearblendbrushes()
+{
+    while(brushes.length()) delete brushes.pop();
+    curbrush = -1;
+}
+
+void delblendbrush(const char *name)
+{
+    loopv(brushes) if(!strcmp(brushes[i]->name, name)) 
+    {
+        delete brushes[i];
+        brushes.remove(i--);
+    }
+    curbrush = brushes.empty() ? -1 : clamp(curbrush, 0, brushes.length()-1);
+}
+
+void addblendbrush(const char *name, const char *imgname)
+{
+    delblendbrush(name);
+
+    ImageData s;
+    if(!loadimage(imgname, s)) { conoutf(CON_ERROR, "could not load blend brush image %s", imgname); return; }
+    if(max(s.w, s.h) > (1<<12))
+    {
+        conoutf(CON_ERROR, "blend brush image size exceeded %dx%d pixels: %s", 1<<12, 1<<12, imgname);
+        return;
+    }
+    
+    BlendBrush *brush = new BlendBrush(name, s.w, s.h);
+
+    uchar *dst = brush->data, *srcrow = s.data;
+    loopi(s.h)
+    {
+        for(uchar *src = srcrow, *end = &srcrow[s.w*s.bpp]; src < end; src += s.bpp)
+            *dst++ = src[0];
+        srcrow += s.pitch;
+    }
+
+    brushes.add(brush);
+    if(curbrush < 0) curbrush = 0;
+    else if(curbrush >= brushes.length()) curbrush = brushes.length()-1;
+
+}
+
+void nextblendbrush(int *dir)
+{
+    curbrush += *dir < 0 ? -1 : 1;
+    if(brushes.empty()) curbrush = -1;
+    else if(!brushes.inrange(curbrush)) curbrush = *dir < 0 ? brushes.length()-1 : 0;
+}
+
+void setblendbrush(const char *name)
+{
+    loopv(brushes) if(!strcmp(brushes[i]->name, name)) { curbrush = i; break; }
+}
+
+void getblendbrushname(int *n)
+{
+    result(brushes.inrange(*n) ? brushes[*n]->name : "");
+}
+
+void curblendbrush()
+{
+    intret(curbrush);
+}
+
+COMMAND(clearblendbrushes, "");
+COMMAND(delblendbrush, "s");
+COMMAND(addblendbrush, "ss");
+COMMAND(nextblendbrush, "i");
+COMMAND(setblendbrush, "s");
+COMMAND(getblendbrushname, "i");
+COMMAND(curblendbrush, "");
+
+extern int nompedit;
+
+bool canpaintblendmap(bool brush = true, bool sel = false, bool msg = true)
+{
+    if(noedit(!sel, msg) || (nompedit && multiplayer())) return false;
+    if(!blendpaintmode)
+    {
+        if(msg) conoutf(CON_ERROR, "operation only allowed in blend paint mode");
+        return false;
+    }
+    if(brush && !brushes.inrange(curbrush))
+    {
+        if(msg) conoutf(CON_ERROR, "no blend brush selected");
+        return false;
+    }
+    return true;
+}
+
+void rotateblendbrush(int *val)
+{
+    if(!canpaintblendmap()) return;
+    BlendBrush *brush = brushes[curbrush];
+    const texrotation &r = texrotations[*val < 0 ? 3 : clamp(*val, 1, 7)];
+    brush->reorient(r.flipx, r.flipy, r.swapxy);
+}
+
+COMMAND(rotateblendbrush, "i");
+
+void paintblendmap(bool msg)
+{
+    if(!canpaintblendmap(true, false, msg)) return;
+
+    BlendBrush *brush = brushes[curbrush];
+    int x = (int)floor(clamp(worldpos.x, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->w),
+        y = (int)floor(clamp(worldpos.y, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->h);
+    blitblendmap(brush->data, x, y, brush->w, brush->h, blendpaintmode);
+    previewblends(ivec((x-1)<<BM_SCALE, (y-1)<<BM_SCALE, 0),
+                  ivec((x+brush->w+1)<<BM_SCALE, (y+brush->h+1)<<BM_SCALE, worldsize));
+}
+
+VAR(paintblendmapdelay, 1, 500, 3000);
+VAR(paintblendmapinterval, 1, 30, 3000);
+
+int paintingblendmap = 0, lastpaintblendmap = 0;
+
+void stoppaintblendmap()
+{
+    paintingblendmap = 0;
+    lastpaintblendmap = 0;
+}
+
+void trypaintblendmap()
+{
+    if(!paintingblendmap || totalmillis - paintingblendmap < paintblendmapdelay) return;
+    if(lastpaintblendmap)
+    {
+        int diff = totalmillis - lastpaintblendmap;
+        if(diff < paintblendmapinterval) return;
+        lastpaintblendmap = (diff - diff%paintblendmapinterval) + lastpaintblendmap;
+    }
+    else lastpaintblendmap = totalmillis;
+    paintblendmap(false);
+}
+
+ICOMMAND(paintblendmap, "D", (int *isdown),
+{
+    if(*isdown)
+    {
+        if(!paintingblendmap) { paintblendmap(true); paintingblendmap = totalmillis; }
+    }
+    else stoppaintblendmap();
+});
+    
+void clearblendmapsel()
+{
+    if(noedit(false) || (nompedit && multiplayer())) return;
+    extern selinfo sel;
+    int x1 = sel.o.x>>BM_SCALE, y1 = sel.o.y>>BM_SCALE,
+        x2 = (sel.o.x+sel.s.x*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE,
+        y2 = (sel.o.y+sel.s.y*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE;
+    fillblendmap(x1, y1, x2-x1, y2-y1, 0xFF);
+    previewblends(ivec(x1<<BM_SCALE, y1<<BM_SCALE, 0),
+                  ivec(x2<<BM_SCALE, y2<<BM_SCALE, worldsize));
+}
+
+COMMAND(clearblendmapsel, "");
+
+void invertblendmapsel()
+{
+    if(noedit(false) || (nompedit && multiplayer())) return;
+    extern selinfo sel;
+    int x1 = sel.o.x>>BM_SCALE, y1 = sel.o.y>>BM_SCALE,
+        x2 = (sel.o.x+sel.s.x*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE,
+        y2 = (sel.o.y+sel.s.y*sel.grid+(1<<BM_SCALE)-1)>>BM_SCALE;
+    invertblendmap(x1, y1, x2-x1, y2-y1);
+    previewblends(ivec(x1<<BM_SCALE, y1<<BM_SCALE, 0),
+                  ivec(x2<<BM_SCALE, y2<<BM_SCALE, worldsize));
+}
+
+COMMAND(invertblendmapsel, "");
+
+void invertblendmap()
+{
+    if(noedit(false) || (nompedit && multiplayer())) return;
+    invertblendmap(0, 0, worldsize>>BM_SCALE, worldsize>>BM_SCALE);
+    previewblends(ivec(0, 0, 0), ivec(worldsize, worldsize, worldsize));
+}
+
+COMMAND(invertblendmap, "");
+
+void showblendmap()
+{
+    if(noedit(true) || (nompedit && multiplayer())) return;
+    previewblends(ivec(0, 0, 0), ivec(worldsize, worldsize, worldsize));
+}
+
+COMMAND(showblendmap, "");
+COMMAND(optimizeblendmap, "");
+ICOMMAND(clearblendmap, "", (),
+{
+    if(noedit(true) || (nompedit && multiplayer())) return;
+    resetblendmap();
+    showblendmap();
+});
+
+ICOMMAND(moveblendmap, "ii", (int *dx, int *dy),
+{
+    if(noedit(true) || (nompedit && multiplayer())) return;
+    if(*dx%(BM_IMAGE_SIZE<<BM_SCALE) || *dy%(BM_IMAGE_SIZE<<BM_SCALE)) 
+    {
+        conoutf(CON_ERROR, "blendmap movement must be in multiples of %d", BM_IMAGE_SIZE<<BM_SCALE);
+        return;
+    }
+    if(*dx <= -worldsize || *dx >= worldsize || *dy <= -worldsize || *dy >= worldsize)
+        resetblendmap();
+    else
+        moveblendmap(*dx>>BM_SCALE, *dy>>BM_SCALE);
+    showblendmap();
+});
+
+void renderblendbrush()
+{
+    if(!blendpaintmode || !brushes.inrange(curbrush)) return;
+
+    BlendBrush *brush = brushes[curbrush];
+    int x1 = (int)floor(clamp(worldpos.x, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->w) << BM_SCALE,
+        y1 = (int)floor(clamp(worldpos.y, 0.0f, float(worldsize))/(1<<BM_SCALE) - 0.5f*brush->h) << BM_SCALE,
+        x2 = x1 + (brush->w << BM_SCALE),
+        y2 = y1 + (brush->h << BM_SCALE);
+
+    if(max(x1, y1) >= worldsize || min(x2, y2) <= 0 || x1>=x2 || y1>=y2) return;
+
+    if(!brush->tex) brush->gentex();
+    renderblendbrush(brush->tex, x1, y1, x2 - x1, y2 - y1);
+}
+
+bool loadblendmap(stream *f, uchar &type, BlendMapNode &node)
+{
+    type = f->getchar();
+    switch(type)
+    {
+        case BM_SOLID:
+        {
+            int val = f->getchar();
+            if(val<0 || val>0xFF) return false;
+            node.solid = &bmsolids[val];
+            break;
+        }
+
+        case BM_IMAGE:
+            node.image = new BlendMapImage;
+            if(f->read(node.image->data, sizeof(node.image->data)) != sizeof(node.image->data))
+                return false;
+            break;
+
+        case BM_BRANCH:
+            node.branch = new BlendMapBranch;
+            loopi(4) { node.branch->type[i] = BM_SOLID; node.branch->children[i].solid = &bmsolids[0xFF]; }
+            loopi(4) if(!loadblendmap(f, node.branch->type[i], node.branch->children[i]))
+                return false;
+            break;
+
+        default:
+            type = BM_SOLID;
+            node.solid = &bmsolids[0xFF];
+            return false;
+    }
+    return true;
+}
+
+bool loadblendmap(stream *f, int info)
+{
+    resetblendmap();
+    return loadblendmap(f, blendmap.type, blendmap);
+}
+
+void saveblendmap(stream *f, uchar type, BlendMapNode &node)
+{
+    f->putchar(type);
+    switch(type)
+    {
+        case BM_SOLID:
+            f->putchar(node.solid->val);
+            break;
+        
+        case BM_IMAGE:
+            f->write(node.image->data, sizeof(node.image->data));
+            break;
+
+        case BM_BRANCH:
+            loopi(4) saveblendmap(f, node.branch->type[i], node.branch->children[i]);
+            break;
+    }
+}
+
+void saveblendmap(stream *f)
+{
+    saveblendmap(f, blendmap.type, blendmap);
+}
+
+uchar shouldsaveblendmap()
+{
+    return blendmap.solid!=&bmsolids[0xFF] ? 1 : 0;
+}
+    
diff --git a/src/engine/blob.cpp b/src/engine/blob.cpp
new file mode 100644 (file)
index 0000000..a587971
--- /dev/null
@@ -0,0 +1,727 @@
+#include "engine.h"
+
+extern int intel_mapbufferrange_bug;
+
+VARNP(blobs, showblobs, 0, 1, 1);
+VARFP(blobintensity, 0, 60, 100, resetblobs());
+VARFP(blobheight, 1, 32, 128, resetblobs());
+VARFP(blobfadelow, 1, 8, 32, resetblobs());
+VARFP(blobfadehigh, 1, 8, 32, resetblobs());
+VARFP(blobmargin, 0, 1, 16, resetblobs());
+
+VAR(dbgblob, 0, 0, 1);
+
+enum
+{
+    BL_DUP    = 1<<0,
+    BL_RENDER = 1<<1
+};
+
+struct blobinfo
+{
+    vec o;
+    float radius;
+    int millis;
+    ushort startindex, endindex, startvert, endvert, next, flags;
+};
+
+struct blobvert
+{
+    vec pos;
+    bvec4 color;
+    vec2 tc;
+};
+
+struct blobrenderer
+{
+    const char *texname;
+    Texture *tex;
+    ushort *cache;
+    int cachesize;
+    blobinfo *blobs;
+    int maxblobs, startblob, endblob;
+    blobvert *verts;
+    int maxverts, startvert, endvert, availverts;
+    ushort *indexes;
+    int maxindexes, startindex, endindex, availindexes;
+    GLuint ebo, vbo;
+    ushort *edata;
+    blobvert *vdata;
+    int numedata, numvdata;
+    blobinfo *startrender, *endrender;
+    blobinfo *lastblob;
+
+    vec blobmin, blobmax;
+    ivec bbmin, bbmax;
+    float blobalphalow, blobalphahigh;
+    uchar blobalpha;
+
+    blobrenderer(const char *texname)
+      : texname(texname), tex(NULL),
+        cache(NULL), cachesize(0),
+        blobs(NULL), maxblobs(0), startblob(0), endblob(0),
+        verts(NULL), maxverts(0), startvert(0), endvert(0), availverts(0),
+        indexes(NULL), maxindexes(0), startindex(0), endindex(0), availindexes(0),
+        ebo(0), vbo(0), edata(NULL), vdata(NULL), numedata(0), numvdata(0),
+        startrender(NULL), endrender(NULL), lastblob(NULL)
+    {}
+
+    ~blobrenderer()
+    {
+        DELETEA(cache);
+        DELETEA(blobs);
+        DELETEA(verts);
+        DELETEA(indexes);
+    }
+
+    void cleanup()
+    {
+        if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; }
+        if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+        DELETEA(edata);
+        DELETEA(vdata);
+        numedata = numvdata = 0;
+        startrender = endrender = NULL;
+    }
+
+    void init(int tris)
+    {
+        cleanup();
+        if(cache)
+        {
+            DELETEA(cache);
+            cachesize = 0;
+        }
+        if(blobs)
+        {
+            DELETEA(blobs);
+            maxblobs = startblob = endblob = 0;
+        }
+        if(verts)
+        {
+            DELETEA(verts);
+            maxverts = startvert = endvert = availverts = 0;
+        }
+        if(indexes)
+        {
+            DELETEA(indexes);
+            maxindexes = startindex = endindex = availindexes = 0;
+        }
+        if(!tris) return;
+        tex = textureload(texname, 3);
+        cachesize = tris/2;
+        cache = new ushort[cachesize];
+        memset(cache, 0xFF, cachesize * sizeof(ushort));
+        maxblobs = tris/2;
+        blobs = new blobinfo[maxblobs];
+        memclear(blobs, maxblobs);
+        maxindexes = tris*3 + 3;
+        availindexes = maxindexes - 3;
+        indexes = new ushort[maxindexes];
+        maxverts = min(tris*3/2 + 1, (1<<16)-1);
+        availverts = maxverts - 1;
+        verts = new blobvert[maxverts];
+    }
+
+    bool freeblob()
+    {
+        blobinfo &b = blobs[startblob];
+        if(&b == lastblob) return false;
+
+        if(b.flags & BL_RENDER) flushblobs();
+
+        startblob++;
+        if(startblob >= maxblobs) startblob = 0;
+
+        startvert = b.endvert;
+        if(startvert>=maxverts) startvert = 0;
+        availverts += b.endvert - b.startvert;
+
+        startindex = b.endindex;
+        if(startindex>=maxindexes) startindex = 0;
+        availindexes += b.endindex - b.startindex;
+
+        b.millis = lastreset;
+        b.flags = 0;
+
+        return true;
+    }
+
+    blobinfo &newblob(const vec &o, float radius)
+    {
+        blobinfo &b = blobs[endblob];
+        int next = endblob + 1;
+        if(next>=maxblobs) next = 0;
+        if(next==startblob) 
+        {
+            lastblob = &b;
+            freeblob();
+        }
+        endblob = next;
+        b.o = o;
+        b.radius = radius;
+        b.millis = totalmillis;
+        b.flags = 0;
+        b.next = 0xFFFF;
+        b.startindex = b.endindex = endindex;
+        b.startvert = b.endvert = endvert;
+        lastblob = &b;
+        return b;
+    }
+
+    template<int C>
+    static int split(const vec *in, int numin, float below, float above, vec *out)
+    {
+        int numout = 0;
+        const vec *p = &in[numin-1];
+        float pc = (*p)[C];
+        loopi(numin)
+        {
+            const vec &v = in[i];
+            float c = v[C];
+            if(c < below)
+            {
+                if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+                if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+            }
+            else if(c > above)
+            {
+                if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+                if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+            }
+            else if(pc < below)
+            {
+                if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+            }
+            else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+            out[numout++] = v;
+            p = &v;
+            pc = c;
+        }
+        return numout;
+    }
+
+    template<int C>
+    static int clip(const vec *in, int numin, float below, float above, vec *out)
+    {
+        int numout = 0;
+        const vec *p = &in[numin-1];
+        float pc = (*p)[C];
+        loopi(numin)
+        {
+            const vec &v = in[i];
+            float c = v[C];
+            if(c < below)
+            {
+                if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+                if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+            }
+            else if(c > above)
+            {
+                if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+                if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+            }
+            else
+            {
+                if(pc < below)  
+                {
+                    if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+                }
+                else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+                out[numout++] = v;
+            }
+            p = &v;
+            pc = c;
+        }
+        return numout;
+    }
+        
+    void dupblob()
+    {
+        if(lastblob->startvert >= lastblob->endvert) 
+        {
+            lastblob->startindex = lastblob->endindex = endindex;
+            lastblob->startvert = lastblob->endvert = endvert;
+            return; 
+        }
+        blobinfo &b = newblob(lastblob->o, lastblob->radius);
+        b.flags |= BL_DUP;
+    }
+
+    inline int addvert(const vec &pos)
+    {
+        blobvert &v = verts[endvert];
+        v.pos = pos;
+        v.tc = vec2((pos.x - blobmin.x) / (blobmax.x - blobmin.x),
+                    (pos.y - blobmin.y) / (blobmax.y - blobmin.y));
+        uchar alpha;
+        if(pos.z < blobmin.z + blobfadelow) alpha = uchar(blobalphalow * (pos.z - blobmin.z));
+        else if(pos.z > blobmax.z - blobfadehigh) alpha = uchar(blobalphahigh * (blobmax.z - pos.z));
+        else alpha = blobalpha;
+        v.color = bvec4(255, 255, 255, alpha);
+        return endvert++;
+    }
+
+    void addtris(const vec *v, int numv)
+    {
+        if(endvert != int(lastblob->endvert) || endindex != int(lastblob->endindex)) dupblob();
+        for(const vec *cur = &v[2], *end = &v[numv];;)
+        {
+            int limit = maxverts - endvert - 2;
+            if(limit <= 0)
+            {
+                while(availverts < limit+2) if(!freeblob()) return;
+                availverts -= limit+2;
+                lastblob->endvert = maxverts;
+                endvert = 0;
+                dupblob();
+                limit = maxverts - 2;
+            }
+            limit = min(int(end - cur), min(limit, (maxindexes - endindex)/3));
+            while(availverts < limit+2) if(!freeblob()) return;
+            while(availindexes < limit*3) if(!freeblob()) return;
+
+            int i1 = addvert(v[0]), i2 = addvert(cur[-1]);
+            loopk(limit)
+            {
+                indexes[endindex++] = i1;
+                indexes[endindex++] = i2;
+                i2 = addvert(*cur++);
+                indexes[endindex++] = i2; 
+            }
+
+            availverts -= endvert - lastblob->endvert;
+            availindexes -= endindex - lastblob->endindex;
+            lastblob->endvert = endvert;
+            lastblob->endindex = endindex;
+            if(endvert >= maxverts) endvert = 0;
+            if(endindex >= maxindexes) endindex = 0;
+
+            if(cur >= end) break;
+            dupblob();
+        }
+    }
+
+    void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0)
+    {
+        vec pos[MAXFACEVERTS+8];
+        int dim = dimension(orient), numverts = 0, numplanes = 1, flat = -1;
+        if(mat)
+        {
+            switch(orient)
+            {
+            #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+                case orient: v0 v1 v2 v3 break;
+            #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \
+                    pos[numverts++] = vec(x xv, y yv, z zv);
+                GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f);
+            #undef GENFACEORIENT
+            #undef GENFACEVERT 
+                default:
+                    return;
+            }
+            flat = dim;
+        }
+        else if(cu.texture[orient] == DEFAULT_SKY) return;
+        else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS))
+        {
+            vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts;
+            ivec vo = ivec(o).mask(~0xFFF).shl(3);
+            loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f);
+            if(numverts >= 4 && !(cu.merged&(1<<orient)) && !flataxisface(cu, orient) && faceconvexity(verts, numverts, size)) numplanes++;
+            else flat = dim;
+        }
+        else if(cu.merged&(1<<orient)) return; 
+        else if(!vismask || (vismask&0x40 && visibleface(cu, orient, o, size, MAT_AIR, (cu.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA)))
+        {
+            ivec v[4];
+            genfaceverts(cu, orient, v);
+            int vis = 3, convex = faceconvexity(v, vis), order = convex < 0 ? 1 : 0;
+            vec vo(o);
+            pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+            if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+            pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+            if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+            if(convex) numplanes++;
+            else flat = dim;
+        }
+        else return;
+
+        if(flat >= 0)
+        {
+            float offset = pos[0][dim];
+            if(offset < blobmin[dim] || offset > blobmax[dim]) return;
+            flat = dim;
+        }
+    
+        vec vmin = pos[0], vmax = pos[0];
+        for(int i = 1; i < numverts; i++) { vmin.min(pos[i]); vmax.max(pos[i]); }
+        if(vmax.x < blobmin.x || vmin.x > blobmax.x || vmax.y < blobmin.y || vmin.y > blobmax.y ||
+           vmax.z < blobmin.z || vmin.z > blobmax.z)
+            return;
+
+        vec v1[MAXFACEVERTS+6+4], v2[MAXFACEVERTS+6+4];
+        loopl(numplanes)
+        {
+            vec *v = pos;
+            int numv = numverts;
+            if(numplanes >= 2)
+            {
+                if(l) { pos[1] = pos[2]; pos[2] = pos[3]; }
+                numv = 3;
+            }
+            if(vec().cross(v[0], v[1], v[2]).z <= 0) continue;
+            #define CLIPSIDE(clip, below, above) \
+                { \
+                    vec *in = v; \
+                    v = in==v1 ? v2 : v1; \
+                    numv = clip(in, numv, below, above, v); \
+                    if(numv < 3) continue; \
+                }
+            if(flat!=0) CLIPSIDE(clip<0>, blobmin.x, blobmax.x);
+            if(flat!=1) CLIPSIDE(clip<1>, blobmin.y, blobmax.y);
+            if(flat!=2) 
+            {
+                CLIPSIDE(clip<2>, blobmin.z, blobmax.z);
+                CLIPSIDE(split<2>, blobmin.z + blobfadelow, blobmax.z - blobfadehigh);
+            }
+            addtris(v, numv);
+        }
+    }
+
+    void findmaterials(vtxarray *va)
+    {
+        materialsurface *matbuf = va->matbuf;
+        int matsurfs = va->matsurfs;
+        loopi(matsurfs)
+        {
+            materialsurface &m = matbuf[i];
+            if(!isclipped(m.material&MATF_VOLUME) || m.orient == O_BOTTOM) { i += m.skip; continue; }
+            int dim = dimension(m.orient), c = C[dim], r = R[dim];
+            for(;;)
+            {
+                materialsurface &m = matbuf[i];
+                if(m.o[dim] >= blobmin[dim] && m.o[dim] <= blobmax[dim] &&
+                   m.o[c] + m.csize >= blobmin[c] && m.o[c] <= blobmax[c] &&
+                   m.o[r] + m.rsize >= blobmin[r] && m.o[r] <= blobmax[r])
+                {
+                    static cube dummy;
+                    gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m);
+                }
+                if(i+1 >= matsurfs) break;
+                materialsurface &n = matbuf[i+1];
+                if(n.material != m.material || n.orient != m.orient) break;
+                i++;
+            }
+        }
+    }
+
+    void findescaped(cube *cu, const ivec &o, int size, int escaped)
+    {
+        loopi(8)
+        {
+            if(escaped&(1<<i))
+            {
+                ivec co(i, o, size);
+                if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+                else
+                {
+                    int vismask = cu[i].merged;
+                    if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+                }
+            }
+        }
+    }
+
+    void gentris(cube *cu, const ivec &o, int size, int escaped = 0)
+    {
+        int overlap = octaboxoverlap(o, size, bbmin, bbmax);
+        loopi(8)
+        {
+            if(overlap&(1<<i))
+            {
+                ivec co(i, o, size);
+                if(cu[i].ext && cu[i].ext->va && cu[i].ext->va->matsurfs)
+                    findmaterials(cu[i].ext->va);
+                if(cu[i].children) gentris(cu[i].children, co, size>>1, cu[i].escaped);
+                else
+                {
+                    int vismask = cu[i].visible;
+                    if(vismask&0xC0) 
+                    {
+                        if(vismask&0x80) loopj(6) gentris(cu[i], j, co, size, NULL, vismask);
+                        else loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+                    }
+                }
+            }
+            else if(escaped&(1<<i))
+            {
+                ivec co(i, o, size);
+                if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+                else
+                {
+                    int vismask = cu[i].merged;
+                    if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+                }
+            }
+        }
+    }
+
+    blobinfo *addblob(const vec &o, float radius, float fade)
+    {
+        lastblob = &blobs[endblob];
+        blobinfo &b = newblob(o, radius);
+        blobmin = blobmax = o;
+        blobmin.x -= radius;
+        blobmin.y -= radius;
+        blobmin.z -= blobheight + blobfadelow;
+        blobmax.x += radius;
+        blobmax.y += radius;
+        blobmax.z += blobfadehigh;
+        (bbmin = ivec(blobmin)).sub(2);
+        (bbmax = ivec(blobmax)).add(2);
+        float scale =  fade*blobintensity*255/100.0f;
+        blobalphalow = scale / blobfadelow;
+        blobalphahigh = scale / blobfadehigh;
+        blobalpha = uchar(scale);
+        gentris(worldroot, ivec(0, 0, 0), worldsize>>1);
+        return !(b.flags & BL_DUP) ? &b : NULL;
+    } 
+
+    static void setuprenderstate()
+    {
+        foggedshader->set();
+
+        enablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+
+        glDepthMask(GL_FALSE);
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        if(!dbgblob) glEnable(GL_BLEND);
+
+        gle::enablevertex();
+        gle::enabletexcoord0();
+        gle::enablecolor();
+    }
+
+    static void cleanuprenderstate()
+    {
+        gle::disablevertex();
+        gle::disabletexcoord0();
+        gle::disablecolor();
+
+        gle::clearvbo();
+        if(glversion >= 300) gle::clearebo();
+
+        glDepthMask(GL_TRUE);
+        glDisable(GL_BLEND);
+
+        disablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+    }
+
+    static int lastreset;
+
+    static void reset()
+    {
+        lastreset = totalmillis;
+    }
+
+    static blobrenderer *lastrender;
+
+    void fadeblob(blobinfo *b, float fade)
+    {
+        float minz = b->o.z - (blobheight + blobfadelow), maxz = b->o.z + blobfadehigh,
+              scale = fade*blobintensity*255/100.0f, scalelow = scale / blobfadelow, scalehigh = scale / blobfadehigh;
+        uchar alpha = uchar(scale); 
+        b->millis = totalmillis;
+        do
+        {
+            if(b->endvert - b->startvert >= 3) for(blobvert *v = &verts[b->startvert], *end = &verts[b->endvert]; v < end; v++)
+            {
+                float z = v->pos.z;
+                if(z < minz + blobfadelow) v->color.a = uchar(scalelow * (z - minz));
+                else if(z > maxz - blobfadehigh) v->color.a = uchar(scalehigh * (maxz - z));
+                else v->color.a = alpha;
+            }
+            int offset = b - &blobs[0] + 1;
+            if(offset >= maxblobs) offset = 0;
+            if(offset < endblob ? offset > startblob || startblob > endblob : offset > startblob) b = &blobs[offset];
+            else break;
+        } while(b->flags & BL_DUP);
+    }
+
+    void renderblob(const vec &o, float radius, float fade)
+    {
+        if(!blobs) initblobs();
+
+        if(glversion < 300 && lastrender != this)
+        {
+            if(!lastrender) setuprenderstate();
+            gle::vertexpointer(sizeof(blobvert), verts->pos.v);
+            gle::texcoord0pointer(sizeof(blobvert), verts->tc.v);
+            gle::colorpointer(sizeof(blobvert), verts->color.v);
+            if(!lastrender || lastrender->tex != tex) glBindTexture(GL_TEXTURE_2D, tex->id);
+            lastrender = this;
+        }
+    
+        union { int i; float f; } ox, oy;
+        ox.f = o.x; oy.f = o.y;
+        uint hash = uint(ox.i^~oy.i^(INT_MAX-oy.i)^uint(radius));
+        hash %= cachesize;
+        blobinfo *b = &blobs[cache[hash]];
+        if(b >= &blobs[maxblobs] || b->millis - lastreset <= 0 || b->o!=o || b->radius!=radius)
+        {
+            b = addblob(o, radius, fade);
+            cache[hash] = ushort(b - blobs);
+            if(!b) return;
+        }
+        else if(fade < 1 && totalmillis - b->millis > 0) fadeblob(b, fade); 
+        do
+        {
+            if(b->endvert - b->startvert >= 3)
+            {
+                if(glversion >= 300)
+                {
+                    if(!startrender) { numedata = numvdata = 0; startrender = endrender = b; }
+                    else { endrender->next = ushort(b - blobs); endrender = b; }
+                    b->flags |= BL_RENDER;
+                    b->next = 0xFFFF;
+                    numedata += b->endindex - b->startindex;
+                    numvdata += b->endvert - b->startvert;
+                }
+                else
+                {
+                    glDrawRangeElements_(GL_TRIANGLES, b->startvert, b->endvert-1, b->endindex - b->startindex, GL_UNSIGNED_SHORT, &indexes[b->startindex]);
+                    xtravertsva += b->endvert - b->startvert;
+                }
+            }
+            int offset = b - &blobs[0] + 1;
+            if(offset >= maxblobs) offset = 0; 
+            if(offset < endblob ? offset > startblob || startblob > endblob : offset > startblob) b = &blobs[offset];
+            else break; 
+        } while(b->flags & BL_DUP);
+    }
+
+    void flushblobs()
+    {
+        if(glversion < 300 || !startrender) return;
+
+        if(lastrender != this)
+        {
+            if(!lastrender) setuprenderstate();
+            lastrender = this;
+        }
+
+        if(!ebo) glGenBuffers_(1, &ebo);
+        if(!vbo) glGenBuffers_(1, &vbo);
+
+        gle::bindebo(ebo);
+        glBufferData_(GL_ELEMENT_ARRAY_BUFFER, maxindexes*sizeof(ushort), NULL, GL_STREAM_DRAW);
+        gle::bindvbo(vbo);
+        glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(blobvert), NULL, GL_STREAM_DRAW);
+
+        ushort *estart;
+        blobvert *vstart;
+        if(intel_mapbufferrange_bug)
+        {
+            if(!edata) edata = new ushort[maxindexes];
+            if(!vdata) vdata = new blobvert[maxverts];
+            estart = edata;
+            vstart = vdata;
+        }
+        else
+        {
+            estart = (ushort *)glMapBufferRange_(GL_ELEMENT_ARRAY_BUFFER, 0, numedata*sizeof(ushort), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT);
+            vstart = (blobvert *)glMapBufferRange_(GL_ARRAY_BUFFER, 0, numvdata*sizeof(blobvert), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT);
+            if(!estart || !vstart)
+            {
+                if(estart) glUnmapBuffer_(GL_ELEMENT_ARRAY_BUFFER);
+                if(vstart) glUnmapBuffer_(GL_ARRAY_BUFFER);
+                for(blobinfo *b = startrender;; b = &blobs[b->next])
+                {   
+                    b->flags &= ~BL_RENDER; 
+                    if(b->next >= maxblobs) break;
+                }
+                startrender = endrender = NULL;
+                return;
+            }
+        }
+
+        ushort *edst = estart;
+        blobvert *vdst = vstart;
+        for(blobinfo *b = startrender;; b = &blobs[b->next])
+        {
+            b->flags &= ~BL_RENDER;
+            ushort offset = ushort(vdst - vstart) - b->startvert; 
+            for(int i = b->startindex; i < b->endindex; ++i)
+                *edst++ = indexes[i] + offset;
+            memcpy(vdst, &verts[b->startvert], (b->endvert - b->startvert)*sizeof(blobvert));
+            vdst += b->endvert - b->startvert;
+            if(b->next >= maxblobs) break;
+        }
+        startrender = endrender = NULL;
+
+        if(intel_mapbufferrange_bug)
+        {
+            glBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, 0, numedata*sizeof(ushort), estart);
+            glBufferSubData_(GL_ARRAY_BUFFER, 0, numvdata*sizeof(blobvert), vstart);
+        }
+        else
+        {
+            glUnmapBuffer_(GL_ELEMENT_ARRAY_BUFFER);
+            glUnmapBuffer_(GL_ARRAY_BUFFER);
+        }
+
+        const blobvert *ptr = 0;
+        gle::vertexpointer(sizeof(blobvert), ptr->pos.v);
+        gle::texcoord0pointer(sizeof(blobvert), ptr->tc.v);
+        gle::colorpointer(sizeof(blobvert), ptr->color.v);
+
+        glBindTexture(GL_TEXTURE_2D, tex->id);
+
+        glDrawRangeElements_(GL_TRIANGLES, 0, numvdata-1, numedata, GL_UNSIGNED_SHORT, (ushort *)0);
+    }
+};
+
+int blobrenderer::lastreset = 0;
+blobrenderer *blobrenderer::lastrender = NULL;
+
+VARFP(blobstattris, 128, 4096, 16384, initblobs(BLOB_STATIC));
+VARFP(blobdyntris, 128, 4096, 16384, initblobs(BLOB_DYNAMIC));
+
+static blobrenderer blobs[] = 
+{
+    blobrenderer("<grey>packages/particles/blob.png"),
+    blobrenderer("<grey>packages/particles/blob.png")
+};
+
+void initblobs(int type)
+{
+    if(type < 0 || (type==BLOB_STATIC && blobs[BLOB_STATIC].blobs)) blobs[BLOB_STATIC].init(showblobs ? blobstattris : 0);
+    if(type < 0 || (type==BLOB_DYNAMIC && blobs[BLOB_DYNAMIC].blobs)) blobs[BLOB_DYNAMIC].init(showblobs ? blobdyntris : 0);
+}
+
+void resetblobs()
+{
+    blobrenderer::reset();
+}
+
+void renderblob(int type, const vec &o, float radius, float fade)
+{
+    if(!showblobs) return;
+    if(refracting < 0 && o.z - blobheight - blobfadelow >= reflectz) return;
+    blobs[type].renderblob(o, radius + blobmargin, fade);
+}
+
+void flushblobs()
+{
+    loopi(sizeof(blobs)/sizeof(blobs[0])) blobs[i].flushblobs();
+    if(blobrenderer::lastrender) blobrenderer::cleanuprenderstate();
+    blobrenderer::lastrender = NULL;
+}
+
+void cleanupblobs()
+{
+    loopi(sizeof(blobs)/sizeof(blobs[0])) blobs[i].cleanup();
+}
+
diff --git a/src/engine/client.cpp b/src/engine/client.cpp
new file mode 100644 (file)
index 0000000..3cfef8e
--- /dev/null
@@ -0,0 +1,274 @@
+// client.cpp, mostly network related client game code
+
+#include "engine.h"
+
+ENetHost *clienthost = NULL;
+ENetPeer *curpeer = NULL, *connpeer = NULL;
+int connmillis = 0, connattempts = 0, discmillis = 0;
+
+bool multiplayer(bool msg)
+{
+    bool val = curpeer || hasnonlocalclients(); 
+    if(val && msg) conoutf(CON_ERROR, "operation not available in multiplayer");
+    return val;
+}
+
+void setrate(int rate)
+{
+   if(!curpeer) return;
+   enet_host_bandwidth_limit(clienthost, rate*1024, rate*1024);
+}
+
+VARF(rate, 0, 0, 1024, setrate(rate));
+
+void throttle();
+
+VARF(throttle_interval, 0, 5, 30, throttle());
+VARF(throttle_accel,    0, 2, 32, throttle());
+VARF(throttle_decel,    0, 2, 32, throttle());
+
+void throttle()
+{
+    if(!curpeer) return;
+    ASSERT(ENET_PEER_PACKET_THROTTLE_SCALE==32);
+    enet_peer_throttle_configure(curpeer, throttle_interval*1000, throttle_accel, throttle_decel);
+}
+
+bool isconnected(bool attempt, bool local)
+{
+    return curpeer || (attempt && connpeer) || (local && haslocalclients());
+}
+
+ICOMMAND(isconnected, "bb", (int *attempt, int *local), intret(isconnected(*attempt > 0, *local != 0) ? 1 : 0));
+
+const ENetAddress *connectedpeer()
+{
+    return curpeer ? &curpeer->address : NULL;
+}
+
+ICOMMAND(connectedip, "", (),
+{
+    const ENetAddress *address = connectedpeer();
+    string hostname;
+    result(address && enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0 ? hostname : "");
+});
+
+ICOMMAND(connectedport, "", (),
+{
+    const ENetAddress *address = connectedpeer();
+    intret(address ? address->port : -1);
+});
+
+void abortconnect()
+{
+    if(!connpeer) return;
+    game::connectfail();
+    if(connpeer->state!=ENET_PEER_STATE_DISCONNECTED) enet_peer_reset(connpeer);
+    connpeer = NULL;
+    if(curpeer) return;
+    enet_host_destroy(clienthost);
+    clienthost = NULL;
+}
+
+SVARP(connectname, "");
+VARP(connectport, 0, 0, 0xFFFF);
+
+void connectserv(const char *servername, int serverport, const char *serverpassword)
+{   
+    if(connpeer)
+    {
+        conoutf("aborting connection attempt");
+        abortconnect();
+    }
+
+    if(serverport <= 0) serverport = server::serverport();
+
+    ENetAddress address;
+    address.port = serverport;
+
+    if(servername)
+    {
+        if(strcmp(servername, connectname)) setsvar("connectname", servername);
+        if(serverport != connectport) setvar("connectport", serverport);
+        addserver(servername, serverport, serverpassword && serverpassword[0] ? serverpassword : NULL);
+        conoutf("attempting to connect to %s:%d", servername, serverport);
+        if(!resolverwait(servername, &address))
+        {
+            conoutf(CON_ERROR, "\f3could not resolve server %s", servername);
+            return;
+        }
+    }
+    else
+    {
+        setsvar("connectname", "");
+        setvar("connectport", 0);
+        conoutf("attempting to connect over LAN");
+        address.host = ENET_HOST_BROADCAST;
+    }
+
+    if(!clienthost) 
+    {
+        clienthost = enet_host_create(NULL, 2, server::numchannels(), rate*1024, rate*1024);
+        if(!clienthost)
+        {
+            conoutf(CON_ERROR, "\f3could not connect to server");
+            return;
+        }
+        clienthost->duplicatePeers = 0;
+    }
+
+    connpeer = enet_host_connect(clienthost, &address, server::numchannels(), 0); 
+    enet_host_flush(clienthost);
+    connmillis = totalmillis;
+    connattempts = 0;
+
+    game::connectattempt(servername ? servername : "", serverpassword ? serverpassword : "", address);
+}
+
+void reconnect(const char *serverpassword)
+{
+    if(!connectname[0] || connectport <= 0)
+    {
+        conoutf(CON_ERROR, "no previous connection");
+        return;
+    }
+
+    connectserv(connectname, connectport, serverpassword);
+}
+
+void disconnect(bool async, bool cleanup)
+{
+    if(curpeer) 
+    {
+        if(!discmillis)
+        {
+            enet_peer_disconnect(curpeer, DISC_NONE);
+            enet_host_flush(clienthost);
+            discmillis = totalmillis;
+        }
+        if(curpeer->state!=ENET_PEER_STATE_DISCONNECTED)
+        {
+            if(async) return;
+            enet_peer_reset(curpeer);
+        }
+        curpeer = NULL;
+        discmillis = 0;
+        conoutf("disconnected");
+        game::gamedisconnect(cleanup);
+        mainmenu = 1;
+    }
+    if(!connpeer && clienthost)
+    {
+        enet_host_destroy(clienthost);
+        clienthost = NULL;
+    }
+}
+
+void trydisconnect(bool local)
+{
+    if(connpeer)
+    {
+        conoutf("aborting connection attempt");
+        abortconnect();
+    }
+    else if(curpeer)
+    {
+        conoutf("attempting to disconnect...");
+        disconnect(!discmillis);
+    }
+    else if(local && haslocalclients()) localdisconnect();
+    else conoutf(CON_WARN, "not connected");
+}
+
+ICOMMAND(connect, "sis", (char *name, int *port, char *pw), connectserv(name, *port, pw));
+ICOMMAND(lanconnect, "is", (int *port, char *pw), connectserv(NULL, *port, pw));
+COMMAND(reconnect, "s");
+ICOMMAND(disconnect, "b", (int *local), trydisconnect(*local != 0));
+ICOMMAND(localconnect, "", (), { if(!isconnected()) localconnect(); });
+ICOMMAND(localdisconnect, "", (), { if(haslocalclients()) localdisconnect(); });
+
+void sendclientpacket(ENetPacket *packet, int chan)
+{
+    if(curpeer) enet_peer_send(curpeer, chan, packet);
+    else localclienttoserver(chan, packet);
+}
+
+void flushclient()
+{
+    if(clienthost) enet_host_flush(clienthost);
+}
+
+void neterr(const char *s, bool disc)
+{
+    conoutf(CON_ERROR, "\f3illegal network message (%s)", s);
+    if(disc) disconnect();
+}
+
+void localservertoclient(int chan, ENetPacket *packet)   // processes any updates from the server
+{
+    packetbuf p(packet);
+    game::parsepacketclient(chan, p);
+}
+
+void clientkeepalive() { if(clienthost) enet_host_service(clienthost, NULL, 0); }
+
+void gets2c()           // get updates from the server
+{
+    ENetEvent event;
+    if(!clienthost) return;
+    if(connpeer && totalmillis/3000 > connmillis/3000)
+    {
+        conoutf("attempting to connect...");
+        connmillis = totalmillis;
+        ++connattempts; 
+        if(connattempts > 3)
+        {
+            conoutf(CON_ERROR, "\f3could not connect to server");
+            abortconnect();
+            return;
+        }
+    }
+    while(clienthost && enet_host_service(clienthost, &event, 0)>0)
+    switch(event.type)
+    {
+        case ENET_EVENT_TYPE_CONNECT:
+            disconnect(false, false); 
+            localdisconnect(false);
+            curpeer = connpeer;
+            connpeer = NULL;
+            conoutf("connected to server");
+            throttle();
+            if(rate) setrate(rate);
+            game::gameconnect(true);
+            break;
+         
+        case ENET_EVENT_TYPE_RECEIVE:
+            if(discmillis) conoutf("attempting to disconnect...");
+            else localservertoclient(event.channelID, event.packet);
+            enet_packet_destroy(event.packet);
+            break;
+
+        case ENET_EVENT_TYPE_DISCONNECT:
+            if(event.data>=DISC_NUM) event.data = DISC_NONE;
+            if(event.peer==connpeer)
+            {
+                conoutf(CON_ERROR, "\f3could not connect to server");
+                abortconnect();
+            }
+            else
+            {
+                if(!discmillis || event.data)
+                {
+                    const char *msg = disconnectreason(event.data);
+                    if(msg) conoutf(CON_ERROR, "\f3server network error, disconnecting (%s) ...", msg);
+                    else conoutf(CON_ERROR, "\f3server network error, disconnecting...");
+                }
+                disconnect();
+            }
+            return;
+
+        default:
+            break;
+    }
+}
+
diff --git a/src/engine/command.cpp b/src/engine/command.cpp
new file mode 100644 (file)
index 0000000..74efff2
--- /dev/null
@@ -0,0 +1,3484 @@
+// command.cpp: implements the parsing and execution of a tiny script language which
+// is largely backwards compatible with the quake console language.
+
+#include "engine.h"
+
+hashnameset<ident> idents; // contains ALL vars/commands/aliases
+vector<ident *> identmap;
+ident *dummyident = NULL;
+
+int identflags = 0;
+
+enum
+{
+    MAXARGS = 25,
+    MAXCOMARGS = 12
+};
+
+VARN(numargs, _numargs, MAXARGS, 0, 0);
+
+static inline void freearg(tagval &v)
+{
+    switch(v.type)
+    {
+        case VAL_STR: delete[] v.s; break;
+        case VAL_CODE: if(v.code[-1] == CODE_START) delete[] (uchar *)&v.code[-1]; break;
+    }
+}
+
+static inline void forcenull(tagval &v)
+{
+    switch(v.type)
+    {
+        case VAL_NULL: return;
+    }
+    freearg(v);
+    v.setnull();
+}
+
+static inline float forcefloat(tagval &v)
+{
+    float f = 0.0f;
+    switch(v.type)
+    {
+        case VAL_INT: f = v.i; break;
+        case VAL_STR: f = parsefloat(v.s); break;
+        case VAL_MACRO: f = parsefloat(v.s); break;
+        case VAL_FLOAT: return v.f;
+    }
+    freearg(v);
+    v.setfloat(f);
+    return f;
+}
+
+static inline int forceint(tagval &v)
+{
+    int i = 0;
+    switch(v.type)
+    {
+        case VAL_FLOAT: i = v.f; break;
+        case VAL_STR: i = parseint(v.s); break;
+        case VAL_MACRO: i = parseint(v.s); break;
+        case VAL_INT: return v.i;
+    }
+    freearg(v);
+    v.setint(i);
+    return i;
+}
+
+static inline const char *forcestr(tagval &v)
+{
+    const char *s = "";
+    switch(v.type)
+    {
+        case VAL_FLOAT: s = floatstr(v.f); break;
+        case VAL_INT: s = intstr(v.i); break;
+        case VAL_STR: case VAL_MACRO: return v.s;
+    }
+    freearg(v);
+    v.setstr(newstring(s));
+    return s;
+}
+
+static inline void forcearg(tagval &v, int type)
+{
+    switch(type)
+    {
+        case RET_STR: if(v.type != VAL_STR) forcestr(v); break;
+        case RET_INT: if(v.type != VAL_INT) forceint(v); break;
+        case RET_FLOAT: if(v.type != VAL_FLOAT) forcefloat(v); break;
+    }
+}
+
+static inline ident *forceident(tagval &v)
+{
+    switch(v.type)
+    {
+        case VAL_IDENT: return v.id;
+        case VAL_MACRO:
+        {
+            ident *id = newident(v.s, IDF_UNKNOWN);
+            v.setident(id);
+            return id;
+        }
+        case VAL_STR: 
+        { 
+            ident *id = newident(v.s, IDF_UNKNOWN); 
+            delete[] v.s; 
+            v.setident(id); 
+            return id; 
+        }
+    }
+    freearg(v);
+    v.setident(dummyident);
+    return dummyident;
+}
+
+void tagval::cleanup()
+{
+    freearg(*this);
+}
+
+static inline void freeargs(tagval *args, int &oldnum, int newnum)
+{
+    for(int i = newnum; i < oldnum; i++) freearg(args[i]);
+    oldnum = newnum;
+}
+
+static inline void cleancode(ident &id)
+{
+    if(id.code)
+    {
+        id.code[0] -= 0x100;
+        if(int(id.code[0]) < 0x100) delete[] id.code;
+        id.code = NULL;
+    }
+}
+
+struct nullval : tagval
+{
+    nullval() { setnull(); }
+} nullval;
+tagval noret = nullval, *commandret = &noret;
+
+void clear_command()
+{
+    enumerate(idents, ident, i, 
+    {
+        if(i.type==ID_ALIAS) 
+        { 
+            DELETEA(i.name);    
+            i.forcenull();
+            DELETEA(i.code);
+        }
+    });
+}
+
+void clearoverride(ident &i)
+{
+    if(!(i.flags&IDF_OVERRIDDEN)) return;
+    switch(i.type)
+    {
+        case ID_ALIAS:
+            if(i.valtype==VAL_STR) 
+            {
+                if(!i.val.s[0]) break;
+                delete[] i.val.s;
+            }
+            cleancode(i);
+            i.valtype = VAL_STR;
+            i.val.s = newstring("");
+            break;
+        case ID_VAR:
+            *i.storage.i = i.overrideval.i;
+            i.changed();
+            break;
+        case ID_FVAR:
+            *i.storage.f = i.overrideval.f;
+            i.changed();
+            break;
+        case ID_SVAR:
+            delete[] *i.storage.s;
+            *i.storage.s = i.overrideval.s;
+            i.changed();
+            break;
+    }
+    i.flags &= ~IDF_OVERRIDDEN;
+}
+
+void clearoverrides()
+{
+    enumerate(idents, ident, i, clearoverride(i));
+}
+
+static bool initedidents = false;
+static vector<ident> *identinits = NULL;
+
+static inline ident *addident(const ident &id)
+{
+    if(!initedidents)
+    {
+        if(!identinits) identinits = new vector<ident>;
+        identinits->add(id);
+        return NULL;
+    }
+    ident &def = idents.access(id.name, id);
+    def.index = identmap.length();
+    return identmap.add(&def);
+}
+
+static bool initidents()
+{
+    initedidents = true;
+    for(int i = 0; i < MAXARGS; i++)
+    {
+        defformatstring(argname, "arg%d", i+1);
+        newident(argname, IDF_ARG);
+    }
+    dummyident = newident("//dummy", IDF_UNKNOWN);
+    if(identinits) 
+    {
+        loopv(*identinits) addident((*identinits)[i]);
+        DELETEP(identinits);
+    }
+    return true;
+}
+UNUSED static bool forceinitidents = initidents();
+
+static const char *sourcefile = NULL, *sourcestr = NULL;
+
+static const char *debugline(const char *p, const char *fmt)
+{
+    if(!sourcestr) return fmt;
+    int num = 1;
+    const char *line = sourcestr;
+    for(;;)
+    {
+        const char *end = strchr(line, '\n');
+        if(!end) end = line + strlen(line);
+        if(p >= line && p <= end)
+        {
+            static string buf;
+            if(sourcefile) formatstring(buf, "%s:%d: %s", sourcefile, num, fmt);
+            else formatstring(buf, "%d: %s", num, fmt);
+            return buf;
+        }
+        if(!*end) break;
+        line = end + 1;
+        num++;
+    }
+    return fmt;
+}
+
+static struct identlink
+{
+    ident *id;
+    identlink *next;
+    int usedargs;
+    identstack *argstack;
+} noalias = { NULL, NULL, (1<<MAXARGS)-1, NULL }, *aliasstack = &noalias;
+
+VAR(dbgalias, 0, 4, 1000);
+
+static void debugalias()
+{
+    if(!dbgalias) return;
+    int total = 0, depth = 0;
+    for(identlink *l = aliasstack; l != &noalias; l = l->next) total++;
+    for(identlink *l = aliasstack; l != &noalias; l = l->next)
+    {
+        ident *id = l->id;
+        ++depth;
+        if(depth < dbgalias) conoutf(CON_ERROR, "  %d) %s", total-depth+1, id->name);
+        else if(l->next == &noalias) conoutf(CON_ERROR, depth == dbgalias ? "  %d) %s" : "  ..%d) %s", total-depth+1, id->name);
+    }
+}
+
+static int nodebug = 0;
+
+static void debugcode(const char *fmt, ...) PRINTFARGS(1, 2);
+
+static void debugcode(const char *fmt, ...)
+{
+    if(nodebug) return;
+
+    va_list args;
+    va_start(args, fmt);
+    conoutfv(CON_ERROR, fmt, args);
+    va_end(args);
+
+    debugalias();
+}
+
+static void debugcodeline(const char *p, const char *fmt, ...) PRINTFARGS(2, 3);
+
+static void debugcodeline(const char *p, const char *fmt, ...)
+{
+    if(nodebug) return;
+
+    va_list args;
+    va_start(args, fmt);
+    conoutfv(CON_ERROR, debugline(p, fmt), args);
+    va_end(args);
+
+    debugalias();
+}
+
+ICOMMAND(nodebug, "e", (uint *body), { nodebug++; executeret(body, *commandret); nodebug--; });
+       
+void addident(ident *id)
+{
+    addident(*id);
+}
+
+static inline void pusharg(ident &id, const tagval &v, identstack &stack)
+{
+    stack.val = id.val;
+    stack.valtype = id.valtype;
+    stack.next = id.stack;
+    id.stack = &stack;
+    id.setval(v);
+    cleancode(id);
+} 
+
+static inline void poparg(ident &id)
+{
+    if(!id.stack) return;
+    identstack *stack = id.stack;
+    if(id.valtype == VAL_STR) delete[] id.val.s;
+    id.setval(*stack);
+    cleancode(id);
+    id.stack = stack->next;
+}
+
+ICOMMAND(push, "rte", (ident *id, tagval *v, uint *code),
+{
+    if(id->type != ID_ALIAS || id->index < MAXARGS) return;
+    identstack stack;
+    pusharg(*id, *v, stack);
+    v->type = VAL_NULL;
+    id->flags &= ~IDF_UNKNOWN;
+    executeret(code, *commandret);
+    poparg(*id);
+});
+
+static inline void pushalias(ident &id, identstack &stack)
+{
+    if(id.type == ID_ALIAS && id.index >= MAXARGS) 
+    {
+        pusharg(id, nullval, stack);
+        id.flags &= ~IDF_UNKNOWN;
+    }
+}
+
+static inline void popalias(ident &id)
+{
+    if(id.type == ID_ALIAS && id.index >= MAXARGS) poparg(id);
+}
+
+KEYWORD(local, ID_LOCAL);
+
+static inline bool checknumber(const char *s)
+{
+    if(isdigit(s[0])) return true;
+    else switch(s[0])
+    {
+        case '+': case '-': return isdigit(s[1]) || (s[1] == '.' && isdigit(s[2]));
+        case '.': return isdigit(s[1]) != 0;
+        default: return false;
+    }
+}
+
+ident *newident(const char *name, int flags)
+{
+    ident *id = idents.access(name);
+    if(!id)
+    {
+        if(checknumber(name)) 
+        {
+            debugcode("number %s is not a valid identifier name", name);
+            return dummyident;
+        }
+        id = addident(ident(ID_ALIAS, newstring(name), flags));
+    }
+    return id;
+}
+
+ident *writeident(const char *name, int flags)
+{
+    ident *id = newident(name, flags);
+    if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index)))
+    {
+        pusharg(*id, nullval, aliasstack->argstack[id->index]);
+        aliasstack->usedargs |= 1<<id->index;
+    }
+    return id;
+}
+
+ident *readident(const char *name)
+{
+    ident *id = idents.access(name);
+    if(id && id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index)))
+       return NULL;
+    return id;
+}
+void resetvar(char *name)
+{
+    ident *id = idents.access(name);
+    if(!id) return;
+    if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+    else clearoverride(*id);
+}
+
+COMMAND(resetvar, "s");
+
+static inline void setarg(ident &id, tagval &v)
+{
+    if(aliasstack->usedargs&(1<<id.index))
+    {
+        if(id.valtype == VAL_STR) delete[] id.val.s;
+        id.setval(v);
+        cleancode(id);
+    }
+    else
+    {
+        pusharg(id, v, aliasstack->argstack[id.index]);
+        aliasstack->usedargs |= 1<<id.index;
+    }
+}
+
+static inline void setalias(ident &id, tagval &v)
+{
+    if(id.valtype == VAL_STR) delete[] id.val.s;
+    id.setval(v);
+    cleancode(id);
+    id.flags = (id.flags & identflags) | identflags;
+}
+
+static void setalias(const char *name, tagval &v)
+{
+    ident *id = idents.access(name);
+    if(id) 
+    {
+        if(id->type == ID_ALIAS) 
+        {
+            if(id->index < MAXARGS) setarg(*id, v); else setalias(*id, v);
+        }
+        else
+        {
+            debugcode("cannot redefine builtin %s with an alias", id->name);
+            freearg(v);
+        }
+    }
+    else if(checknumber(name)) 
+    {
+        debugcode("cannot alias number %s", name);
+        freearg(v);
+    }
+    else
+    {
+        addident(ident(ID_ALIAS, newstring(name), v, identflags));
+    }
+}
+
+void alias(const char *name, const char *str)
+{ 
+    tagval v;
+    v.setstr(newstring(str));
+    setalias(name, v);
+}
+
+void alias(const char *name, tagval &v)
+{
+    setalias(name, v);
+}
+
+ICOMMAND(alias, "st", (const char *name, tagval *v),
+{
+    setalias(name, *v);
+    v->type = VAL_NULL;
+});
+
+// variable's and commands are registered through globals, see cube.h
+
+int variable(const char *name, int min, int cur, int max, int *storage, identfun fun, int flags)
+{
+    addident(ident(ID_VAR, name, min, max, storage, (void *)fun, flags));
+    return cur;
+}
+
+float fvariable(const char *name, float min, float cur, float max, float *storage, identfun fun, int flags)
+{
+    addident(ident(ID_FVAR, name, min, max, storage, (void *)fun, flags));
+    return cur;
+}
+
+char *svariable(const char *name, const char *cur, char **storage, identfun fun, int flags)
+{
+    addident(ident(ID_SVAR, name, storage, (void *)fun, flags));
+    return newstring(cur);
+}
+
+#define _GETVAR(id, vartype, name, retval) \
+    ident *id = idents.access(name); \
+    if(!id || id->type!=vartype) return retval;
+#define GETVAR(id, name, retval) _GETVAR(id, ID_VAR, name, retval)
+#define OVERRIDEVAR(errorval, saveval, resetval, clearval) \
+    if(identflags&IDF_OVERRIDDEN || id->flags&IDF_OVERRIDE) \
+    { \
+        if(id->flags&IDF_PERSIST) \
+        { \
+            debugcode("cannot override persistent variable %s", id->name); \
+            errorval; \
+        } \
+        if(!(id->flags&IDF_OVERRIDDEN)) { saveval; id->flags |= IDF_OVERRIDDEN; } \
+        else { clearval; } \
+    } \
+    else \
+    { \
+        if(id->flags&IDF_OVERRIDDEN) { resetval; id->flags &= ~IDF_OVERRIDDEN; } \
+        clearval; \
+    }
+
+void setvar(const char *name, int i, bool dofunc, bool doclamp)
+{
+    GETVAR(id, name, );
+    OVERRIDEVAR(return, id->overrideval.i = *id->storage.i, , )
+    if(doclamp) *id->storage.i = clamp(i, id->minval, id->maxval);
+    else *id->storage.i = i;
+    if(dofunc) id->changed();
+}
+void setfvar(const char *name, float f, bool dofunc, bool doclamp)
+{
+    _GETVAR(id, ID_FVAR, name, );
+    OVERRIDEVAR(return, id->overrideval.f = *id->storage.f, , );
+    if(doclamp) *id->storage.f = clamp(f, id->minvalf, id->maxvalf);
+    else *id->storage.f = f;
+    if(dofunc) id->changed();
+}
+void setsvar(const char *name, const char *str, bool dofunc)
+{
+    _GETVAR(id, ID_SVAR, name, );
+    OVERRIDEVAR(return, id->overrideval.s = *id->storage.s, delete[] id->overrideval.s, delete[] *id->storage.s);
+    *id->storage.s = newstring(str);
+    if(dofunc) id->changed();
+}
+int getvar(const char *name)
+{
+    GETVAR(id, name, 0);
+    return *id->storage.i;
+}
+int getvarmin(const char *name)
+{
+    GETVAR(id, name, 0);
+    return id->minval;
+}
+int getvarmax(const char *name)
+{
+    GETVAR(id, name, 0);
+    return id->maxval;
+}
+float getfvarmin(const char *name)
+{
+    _GETVAR(id, ID_FVAR, name, 0);
+    return id->minvalf;
+}
+float getfvarmax(const char *name)
+{
+    _GETVAR(id, ID_FVAR, name, 0);
+    return id->maxvalf;
+}
+
+ICOMMAND(getvarmin, "s", (char *s), intret(getvarmin(s)));
+ICOMMAND(getvarmax, "s", (char *s), intret(getvarmax(s)));
+ICOMMAND(getfvarmin, "s", (char *s), floatret(getfvarmin(s)));
+ICOMMAND(getfvarmax, "s", (char *s), floatret(getfvarmax(s)));
+
+bool identexists(const char *name) { return idents.access(name)!=NULL; }
+ident *getident(const char *name) { return idents.access(name); }
+
+void touchvar(const char *name)
+{
+    ident *id = idents.access(name);
+    if(id) switch(id->type)
+    {
+        case ID_VAR:
+        case ID_FVAR:
+        case ID_SVAR:
+            id->changed();
+            break;
+    }
+}
+
+const char *getalias(const char *name)
+{
+    ident *i = idents.access(name);
+    return i && i->type==ID_ALIAS && (i->index >= MAXARGS || aliasstack->usedargs&(1<<i->index)) ? i->getstr() : "";
+}
+
+ICOMMAND(getalias, "s", (char *s), result(getalias(s)));
+
+int clampvar(ident *id, int val, int minval, int maxval)
+{
+    if(val < minval) val = minval;
+    else if(val > maxval) val = maxval;
+    else return val;
+    debugcode(id->flags&IDF_HEX ?
+            (minval <= 255 ? "valid range for %s is %d..0x%X" : "valid range for %s is 0x%X..0x%X") :
+            "valid range for %s is %d..%d",
+        id->name, minval, maxval);
+    return val;
+}
+
+void setvarchecked(ident *id, int val)
+{
+    if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+#ifndef STANDALONE
+    else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle())
+#else
+    else
+#endif
+    {
+        OVERRIDEVAR(return, id->overrideval.i = *id->storage.i, , )
+        if(val < id->minval || val > id->maxval) val = clampvar(id, val, id->minval, id->maxval);
+        *id->storage.i = val;
+        id->changed();                                             // call trigger function if available
+#ifndef STANDALONE
+        if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id);
+#endif
+    }
+}
+
+static inline void setvarchecked(ident *id, tagval *args, int numargs)
+{
+    int val = forceint(args[0]);
+    if(id->flags&IDF_HEX && numargs > 1)
+    {
+        val = (val << 16) | (forceint(args[1])<<8);
+        if(numargs > 2) val |= forceint(args[2]);
+    }
+    setvarchecked(id, val);
+}
+
+float clampfvar(ident *id, float val, float minval, float maxval)
+{
+    if(val < minval) val = minval;
+    else if(val > maxval) val = maxval;
+    else return val;
+    debugcode("valid range for %s is %s..%s", id->name, floatstr(minval), floatstr(maxval));
+    return val;
+}
+
+void setfvarchecked(ident *id, float val)
+{
+    if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+#ifndef STANDALONE
+    else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle())
+#else
+    else
+#endif
+    {
+        OVERRIDEVAR(return, id->overrideval.f = *id->storage.f, , );
+        if(val < id->minvalf || val > id->maxvalf) val = clampfvar(id, val, id->minvalf, id->maxvalf);
+        *id->storage.f = val;
+        id->changed();
+#ifndef STANDALONE
+        if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id);
+#endif
+    }
+}
+
+void setsvarchecked(ident *id, const char *val)
+{
+    if(id->flags&IDF_READONLY) debugcode("variable %s is read-only", id->name);
+#ifndef STANDALONE
+    else if(!(id->flags&IDF_OVERRIDE) || identflags&IDF_OVERRIDDEN || game::allowedittoggle())
+#else
+    else
+#endif
+    {
+        OVERRIDEVAR(return, id->overrideval.s = *id->storage.s, delete[] id->overrideval.s, delete[] *id->storage.s);
+        *id->storage.s = newstring(val);
+        id->changed();
+#ifndef STANDALONE
+        if(id->flags&IDF_OVERRIDE && !(identflags&IDF_OVERRIDDEN)) game::vartrigger(id);
+#endif
+    }
+}
+
+ICOMMAND(set, "rt", (ident *id, tagval *v),
+{
+    switch(id->type)
+    {
+        case ID_ALIAS:
+            if(id->index < MAXARGS) setarg(*id, *v); else setalias(*id, *v);
+            v->type = VAL_NULL;
+            break;
+        case ID_VAR:
+            setvarchecked(id, forceint(*v));
+            break;
+        case ID_FVAR:
+            setfvarchecked(id, forcefloat(*v));
+            break;
+        case ID_SVAR:
+            setsvarchecked(id, forcestr(*v));
+            break;
+        case ID_COMMAND:
+            if(id->flags&IDF_EMUVAR)
+            {
+                execute(id, v, 1);
+                v->type = VAL_NULL;
+                break;
+            }
+            // fall through
+        default:
+            debugcode("cannot redefine builtin %s with an alias", id->name);
+            break;
+    }
+});
+
+bool addcommand(const char *name, identfun fun, const char *args)
+{
+    uint argmask = 0;
+    int numargs = 0, flags = 0;
+    bool limit = true;
+    for(const char *fmt = args; *fmt; fmt++) switch(*fmt)
+    {
+        case 'i': case 'b': case 'f': case 't': case 'N': case 'D': if(numargs < MAXARGS) numargs++; break;
+        case '$': flags |= IDF_EMUVAR; // fall through
+        case 's': case 'e': case 'r': if(numargs < MAXARGS) { argmask |= 1<<numargs; numargs++; } break;
+        case '1': case '2': case '3': case '4': if(numargs < MAXARGS) fmt -= *fmt-'0'+1; break;
+        case 'C': case 'V': limit = false; break;
+        default: fatal("builtin %s declared with illegal type: %s", name, args); break;
+    }
+    if(limit && numargs > MAXCOMARGS) fatal("builtin %s declared with too many args: %d", name, numargs);
+    addident(ident(ID_COMMAND, name, args, argmask, numargs, (void *)fun, flags));
+    return false;
+}
+
+bool addkeyword(int type, const char *name)
+{
+    addident(ident(type, name, "", 0, 0, NULL));
+    return true;
+}
+
+const char *parsestring(const char *p)
+{
+    for(; *p; p++) switch(*p)
+    {
+        case '\r':
+        case '\n':
+        case '\"':
+            return p;
+        case '^':
+            if(*++p) break;
+            return p;
+    }
+    return p;
+}
+
+int unescapestring(char *dst, const char *src, const char *end)
+{
+    char *start = dst;
+    while(src < end)
+    {
+        int c = *src++;
+        if(c == '^')
+        {
+            if(src >= end) break;
+            int e = *src++;
+            switch(e)
+            {
+                case 'n': *dst++ = '\n'; break;
+                case 't': *dst++ = '\t'; break;
+                case 'f': *dst++ = '\f'; break;
+                default: *dst++ = e; break;
+            }
+        }
+        else *dst++ = c;
+    }
+    return dst - start;
+}
+
+static char *conc(vector<char> &buf, tagval *v, int n, bool space, const char *prefix = NULL, int prefixlen = 0)
+{
+    if(prefix)
+    {
+        buf.put(prefix, prefixlen);
+        if(space && n) buf.add(' ');
+    }
+    loopi(n)
+    {
+        const char *s = "";
+        int len = 0;
+        switch(v[i].type)
+        {
+            case VAL_INT: s = intstr(v[i].i); break;
+            case VAL_FLOAT: s = floatstr(v[i].f); break;
+            case VAL_STR: s = v[i].s; break;
+            case VAL_MACRO: s = v[i].s; len = v[i].code[-1]>>8; goto haslen;
+        }
+        len = int(strlen(s));
+    haslen:
+        buf.put(s, len);
+        if(i == n-1) break;
+        if(space) buf.add(' ');
+    }
+    buf.add('\0');
+    return buf.getbuf();
+}
+
+static char *conc(tagval *v, int n, bool space, const char *prefix, int prefixlen)
+{
+    static int vlen[MAXARGS];
+    static char numbuf[3*MAXSTRLEN];
+    int len = prefixlen, numlen = 0, i = 0;
+    for(; i < n; i++) switch(v[i].type)
+    {
+        case VAL_MACRO: len += (vlen[i] = v[i].code[-1]>>8); break;
+        case VAL_STR: len += (vlen[i] = int(strlen(v[i].s))); break;
+        case VAL_INT:
+            if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow;
+            intformat(&numbuf[numlen], v[i].i);
+            numlen += (vlen[i] = strlen(&numbuf[numlen]));
+            break;
+        case VAL_FLOAT:
+            if(numlen + MAXSTRLEN > int(sizeof(numbuf))) goto overflow;
+            floatformat(&numbuf[numlen], v[i].f);
+            numlen += (vlen[i] = strlen(&numbuf[numlen]));
+            break;
+        default: vlen[i] = 0; break;
+    }
+overflow:
+    if(space) len += max(prefix ? i : i-1, 0);
+    char *buf = newstring(len + numlen);
+    int offset = 0, numoffset = 0;
+    if(prefix)
+    {
+        memcpy(buf, prefix, prefixlen);
+        offset += prefixlen;
+        if(space && i) buf[offset++] = ' ';
+    }
+    loopj(i)
+    {
+        if(v[j].type == VAL_INT || v[j].type == VAL_FLOAT)
+        {
+            memcpy(&buf[offset], &numbuf[numoffset], vlen[j]);
+            numoffset += vlen[j];
+        }
+        else if(vlen[j]) memcpy(&buf[offset], v[j].s, vlen[j]);
+        offset += vlen[j];
+        if(j==i-1) break;
+        if(space) buf[offset++] = ' ';
+    }
+    buf[offset] = '\0';
+    if(i < n)
+    {
+        char *morebuf = conc(&v[i], n-i, space, buf, offset);
+        delete[] buf;
+        return morebuf;
+    }
+    return buf;
+}
+
+static inline char *conc(tagval *v, int n, bool space)
+{
+    return conc(v, n, space, NULL, 0);
+}
+
+static inline char *conc(tagval *v, int n, bool space, const char *prefix)
+{
+    return conc(v, n, space, prefix, strlen(prefix));
+}
+
+static inline void skipcomments(const char *&p)
+{
+    for(;;)
+    {
+        p += strspn(p, " \t\r");
+        if(p[0]!='/' || p[1]!='/') break;
+        p += strcspn(p, "\n\0");
+    }
+}
+
+static inline char *cutstring(const char *&p, int &len)
+{
+    p++;
+    const char *end = parsestring(p);
+    char *s = newstring(end - p);         
+    len = unescapestring(s, p, end);
+    s[len] = '\0';
+    p = end;
+    if(*p=='\"') p++;
+    return s;
+}
+
+static inline const char *parseword(const char *p)
+{
+    const int maxbrak = 100;
+    static char brakstack[maxbrak];
+    int brakdepth = 0;
+    for(;; p++)
+    {
+        p += strcspn(p, "\"/;()[] \t\r\n\0");
+        switch(p[0])
+        {
+            case '"': case ';': case ' ': case '\t': case '\r': case '\n': case '\0': return p;
+            case '/': if(p[1] == '/') return p; break;
+            case '[': case '(': if(brakdepth >= maxbrak) return p; brakstack[brakdepth++] = p[0]; break;
+            case ']': if(brakdepth <= 0 || brakstack[--brakdepth] != '[') return p; break;
+            case ')': if(brakdepth <= 0 || brakstack[--brakdepth] != '(') return p; break;
+        }
+    }
+    return p;
+}
+
+static inline char *cutword(const char *&p, int &len)
+{
+    const char *word = p;
+    p = parseword(p);
+    len = p-word;
+    if(!len) return NULL;
+    return newstring(word, len);
+}
+
+static inline void compilestr(vector<uint> &code, const char *word, int len, bool macro = false)
+{
+    if(len <= 3 && !macro)
+    {
+        uint op = CODE_VALI|RET_STR;
+        for(int i = 0; i < len; i++) op |= uint(uchar(word[i]))<<((i+1)*8);
+        code.add(op);
+        return;
+    }
+    code.add((macro ? CODE_MACRO : CODE_VAL|RET_STR)|(len<<8));
+    code.put((const uint *)word, len/sizeof(uint));
+    size_t endlen = len%sizeof(uint);
+    union
+    {
+        char c[sizeof(uint)];
+        uint u;
+    } end;
+    end.u = 0;
+    memcpy(end.c, word + len - endlen, endlen);
+    code.add(end.u);
+}
+
+static inline void compilestr(vector<uint> &code, const char *word = NULL)
+{
+    if(!word) { code.add(CODE_VALI|RET_STR); return; }
+    compilestr(code, word, int(strlen(word)));
+}
+
+static inline void compileint(vector<uint> &code, int i)
+{
+    if(i >= -0x800000 && i <= 0x7FFFFF)
+        code.add(CODE_VALI|RET_INT|(i<<8));
+    else
+    {
+        code.add(CODE_VAL|RET_INT);
+        code.add(i);
+    }
+}
+
+static inline void compilenull(vector<uint> &code)
+{
+    code.add(CODE_VALI|RET_NULL);
+}
+
+static inline void compileblock(vector<uint> &code)
+{
+    int start = code.length();
+    code.add(CODE_BLOCK);
+    code.add(CODE_OFFSET|((start+2)<<8));
+    code.add(CODE_EXIT);
+    code[start] |= uint(code.length() - (start + 1))<<8;
+}
+
+static inline void compileident(vector<uint> &code, ident *id)
+{
+    code.add((id->index < MAXARGS ? CODE_IDENTARG : CODE_IDENT)|(id->index<<8));
+}
+    
+static inline void compileident(vector<uint> &code, const char *word = NULL)
+{
+    compileident(code, word ? newident(word, IDF_UNKNOWN) : dummyident);
+}
+
+static inline void compileint(vector<uint> &code, const char *word = NULL)
+{
+    return compileint(code, word ? parseint(word) : 0);
+}
+
+static inline void compilefloat(vector<uint> &code, float f)
+{
+    if(int(f) == f && f >= -0x800000 && f <= 0x7FFFFF)
+        code.add(CODE_VALI|RET_FLOAT|(int(f)<<8));
+    else
+    {
+        union { float f; uint u; } conv;
+        conv.f = f;
+        code.add(CODE_VAL|RET_FLOAT);
+        code.add(conv.u);
+    }
+}
+
+static inline void compilefloat(vector<uint> &code, const char *word = NULL)
+{
+    return compilefloat(code, word ? parsefloat(word) : 0.0f);
+}
+
+static bool compilearg(vector<uint> &code, const char *&p, int wordtype);
+static void compilestatements(vector<uint> &code, const char *&p, int rettype, int brak = '\0');
+
+static inline void compileval(vector<uint> &code, int wordtype, char *word, int wordlen)
+{
+    switch(wordtype)
+    {
+        case VAL_STR: compilestr(code, word, wordlen, true); break;
+        case VAL_ANY: compilestr(code, word, wordlen); break;
+        case VAL_FLOAT: compilefloat(code, word); break;
+        case VAL_INT: compileint(code, word); break;
+        case VAL_CODE: 
+        {
+            int start = code.length();
+            code.add(CODE_BLOCK);
+            code.add(CODE_OFFSET|((start+2)<<8));
+            const char *p = word;
+            if(p) compilestatements(code, p, VAL_ANY);
+            code.add(CODE_EXIT|RET_STR);
+            code[start] |= uint(code.length() - (start + 1))<<8;
+            break;
+        }
+        case VAL_IDENT: compileident(code, word); break;
+        default:
+            break;
+    }
+}
+
+static bool compileword(vector<uint> &code, const char *&p, int wordtype, char *&word, int &wordlen);
+
+static void compilelookup(vector<uint> &code, const char *&p, int ltype)
+{
+    char *lookup = NULL;
+    int lookuplen = 0;
+    switch(*++p)
+    {
+        case '(':
+        case '[':
+            if(!compileword(code, p, VAL_STR, lookup, lookuplen)) goto invalid;
+            break;
+        case '$':
+            compilelookup(code, p, VAL_STR);
+            break;
+        case '\"':
+            lookup = cutstring(p, lookuplen);
+            goto lookupid;
+        default:
+        {
+            lookup = cutword(p, lookuplen);
+            if(!lookup) goto invalid;
+        lookupid:
+            ident *id = newident(lookup, IDF_UNKNOWN);
+            if(id) switch(id->type)
+            {
+                case ID_VAR: code.add(CODE_IVAR|((ltype >= VAL_ANY ? VAL_INT : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+                case ID_FVAR: code.add(CODE_FVAR|((ltype >= VAL_ANY ? VAL_FLOAT : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+                case ID_SVAR: code.add(CODE_SVAR|((ltype >= VAL_ANY ? VAL_STR : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+                case ID_ALIAS: code.add((id->index < MAXARGS ? CODE_LOOKUPARG : CODE_LOOKUP)|((ltype >= VAL_ANY ? VAL_STR : ltype)<<CODE_RET)|(id->index<<8)); goto done;
+                case ID_COMMAND:
+                {
+                    int comtype = CODE_COM, numargs = 0;
+                    code.add(CODE_ENTER);
+                    for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt)
+                    {
+                        case 's': compilestr(code, NULL, 0, true); numargs++; break;
+                        case 'i': compileint(code); numargs++; break;         
+                        case 'b': compileint(code, INT_MIN); numargs++; break;
+                        case 'f': compilefloat(code); numargs++; break;
+                        case 't': compilenull(code); numargs++; break;
+                        case 'e': compileblock(code); numargs++; break;
+                        case 'r': compileident(code); numargs++; break;
+                        case '$': compileident(code, id); numargs++; break;
+                        case 'N': compileint(code, -1); numargs++; break;
+#ifndef STANDALONE
+                        case 'D': comtype = CODE_COMD; numargs++; break;
+#endif
+                        case 'C': comtype = CODE_COMC; numargs = 1; goto endfmt;
+                        case 'V': comtype = CODE_COMV; numargs = 2; goto endfmt;
+                        case '1': case '2': case '3': case '4': break; 
+                    }
+                endfmt:
+                    code.add(comtype|(ltype < VAL_ANY ? ltype<<CODE_RET : 0)|(id->index<<8));
+                    code.add(CODE_EXIT|(ltype < VAL_ANY ? ltype<<CODE_RET : 0));
+                    goto done;
+                }
+                default: goto invalid;
+            }
+            compilestr(code, lookup, lookuplen, true);
+            break;
+        }
+    }
+    code.add(CODE_LOOKUPU|((ltype < VAL_ANY ? ltype<<CODE_RET : 0)));
+done:
+    delete[] lookup;
+    switch(ltype)
+    {
+        case VAL_CODE: code.add(CODE_COMPILE); break;
+        case VAL_IDENT: code.add(CODE_IDENTU); break;
+    }
+    return;
+invalid:
+    switch(ltype)
+    {
+        case VAL_NULL: case VAL_ANY: compilenull(code); break;
+        default: compileval(code, ltype, NULL, 0); break;
+    }
+}
+
+static bool compileblockstr(vector<uint> &code, const char *str, const char *end, bool macro)
+{
+    int start = code.length();
+    code.add(macro ? CODE_MACRO : CODE_VAL|RET_STR); 
+    char *buf = (char *)code.reserve((end-str)/sizeof(uint)+1).buf;
+    int len = 0;
+    while(str < end)
+    {
+        int n = strcspn(str, "\r/\"@]\0");
+        memcpy(&buf[len], str, n);
+        len += n;
+        str += n;
+        switch(*str)
+        {
+            case '\r': str++; break;
+            case '\"':
+            {
+                const char *start = str;
+                str = parsestring(str+1);
+                if(*str=='\"') str++;
+                memcpy(&buf[len], start, str-start);
+                len += str-start;
+                break;
+            }
+            case '/':
+                if(str[1] == '/')
+                {
+                    size_t comment = strcspn(str, "\n\0");
+                    if (iscubepunct(str[2]))
+                    {
+                        memcpy(&buf[len], str, comment);
+                        len += comment;
+                    }
+                    str += comment;
+                }
+                else buf[len++] = *str++;
+                break;
+            case '@':
+            case ']':
+                if(str < end) { buf[len++] = *str++; break; }
+            case '\0': goto done;
+        }
+    }
+done:
+    memset(&buf[len], '\0', sizeof(uint)-len%sizeof(uint));
+    code.advance(len/sizeof(uint)+1);
+    code[start] |= len<<8;
+    return true;
+}
+
+static bool compileblocksub(vector<uint> &code, const char *&p)
+{
+    char *lookup = NULL;
+    int lookuplen = 0;
+    switch(*p)
+    {
+        case '(':
+            if(!compilearg(code, p, VAL_STR)) return false;
+            break;
+        case '[':
+            if(!compilearg(code, p, VAL_STR)) return false;
+            code.add(CODE_LOOKUPU|RET_STR);
+            break;
+        case '\"':
+            lookup = cutstring(p, lookuplen);
+            goto lookupid;
+        default:
+        {
+            {
+                const char *start = p;
+                while(iscubealnum(*p) || *p=='_') p++;
+                lookuplen = p-start;
+                if(!lookuplen) return false;
+                lookup = newstring(start, lookuplen);
+            }
+        lookupid:
+            ident *id = newident(lookup, IDF_UNKNOWN);
+            if(id) switch(id->type)
+            {
+            case ID_VAR: code.add(CODE_IVAR|RET_STR|(id->index<<8)); goto done;
+            case ID_FVAR: code.add(CODE_FVAR|RET_STR|(id->index<<8)); goto done;
+            case ID_SVAR: code.add(CODE_SVAR|RET_STR|(id->index<<8)); goto done;
+            case ID_ALIAS: code.add((id->index < MAXARGS ? CODE_LOOKUPARG : CODE_LOOKUP)|RET_STR|(id->index<<8)); goto done;
+            }
+            compilestr(code, lookup, lookuplen, true);
+            code.add(CODE_LOOKUPU|RET_STR);
+        done:
+            delete[] lookup;
+            break;
+        }
+    }
+    return true;
+}
+
+static void compileblock(vector<uint> &code, const char *&p, int wordtype)
+{
+    const char *line = p, *start = p;
+    int concs = 0;
+    for(int brak = 1; brak;)
+    {
+        p += strcspn(p, "@\"/[]\0");
+        int c = *p++;
+        switch(c)
+        {
+            case '\0':
+                debugcodeline(line, "missing \"]\"");
+                p--;
+                goto done;
+            case '\"':
+                p = parsestring(p);
+                if(*p=='\"') p++;
+                break;
+            case '/':
+                if(*p=='/') p += strcspn(p, "\n\0");
+                break;
+            case '[': brak++; break;
+            case ']': brak--; break;
+            case '@': 
+            {
+                const char *esc = p;
+                while(*p == '@') p++; 
+                int level = p - (esc - 1);
+                if(brak > level) continue;
+                else if(brak < level) debugcodeline(line, "too many @s");
+                if(!concs) code.add(CODE_ENTER);
+                if(concs + 2 > MAXARGS)
+                {
+                    code.add(CODE_CONCW|RET_STR|(concs<<8));
+                    concs = 1;
+                }
+                if(compileblockstr(code, start, esc-1, true)) concs++;
+                if(compileblocksub(code, p)) concs++;
+                if(!concs) code.pop();
+                else start = p;
+                break;
+            }
+        }
+    }
+done:
+    if(p-1 > start) 
+    {
+        if(!concs) switch(wordtype)
+        {
+            case VAL_CODE:
+            {
+                p = start;
+                int inst = code.length();
+                code.add(CODE_BLOCK);
+                code.add(CODE_OFFSET|((inst+2)<<8));
+                compilestatements(code, p, VAL_ANY, ']');
+                code.add(CODE_EXIT);
+                code[inst] |= uint(code.length() - (inst + 1))<<8;
+                return;
+            }
+            case VAL_IDENT:
+            {
+                char *name = newstring(start, p-1-start);
+                compileident(code, name);
+                delete[] name;
+                return;
+            }
+        }
+        compileblockstr(code, start, p-1, concs > 0);
+        if(concs > 1) concs++;
+    }        
+    if(concs)
+    {
+        code.add(CODE_CONCM|(wordtype < VAL_ANY ? wordtype<<CODE_RET : RET_STR)|(concs<<8));
+        code.add(CODE_EXIT|(wordtype < VAL_ANY ? wordtype<<CODE_RET : RET_STR));
+    }
+    switch(wordtype)
+    {
+        case VAL_CODE: if(!concs && p-1 <= start) compileblock(code); else code.add(CODE_COMPILE); break;
+        case VAL_IDENT: if(!concs && p-1 <= start) compileident(code); else code.add(CODE_IDENTU); break;
+        case VAL_STR: case VAL_NULL: case VAL_ANY:
+            if(!concs && p-1 <= start) compilestr(code);
+            break;
+        default: 
+            if(!concs) 
+            {
+                if(p-1 <= start) compileval(code, wordtype, NULL, 0);
+                else code.add(CODE_FORCE|(wordtype<<CODE_RET));
+            }
+            break;
+    }
+} 
+    
+static bool compileword(vector<uint> &code, const char *&p, int wordtype, char *&word, int &wordlen)
+{
+    skipcomments(p);
+    switch(*p)
+    {
+        case '\"': word = cutstring(p, wordlen); break;
+        case '$': compilelookup(code, p, wordtype); return true;
+        case '(':
+            p++;
+            code.add(CODE_ENTER);
+            compilestatements(code, p, VAL_ANY, ')');
+            code.add(CODE_EXIT|(wordtype < VAL_ANY ? wordtype<<CODE_RET : 0));
+            switch(wordtype)
+            {
+                case VAL_CODE: code.add(CODE_COMPILE); break;
+                case VAL_IDENT: code.add(CODE_IDENTU); break;
+            }
+            return true;        
+        case '[':
+            p++;
+            compileblock(code, p, wordtype);
+            return true;
+        default: word = cutword(p, wordlen); break;
+    }
+    return word!=NULL;
+}
+
+static inline bool compilearg(vector<uint> &code, const char *&p, int wordtype)
+{
+    char *word = NULL;
+    int wordlen = 0;
+    bool more = compileword(code, p, wordtype, word, wordlen);
+    if(!more) return false;
+    if(word) 
+    {
+        compileval(code, wordtype, word, wordlen);
+        delete[] word;
+    }
+    return true;
+}
+
+static void compilestatements(vector<uint> &code, const char *&p, int rettype, int brak)
+{
+    const char *line = p;
+    char *idname = NULL;
+    int idlen = 0;
+    ident *id = NULL;
+    int numargs = 0;
+    for(;;)
+    {
+        skipcomments(p);
+        idname = NULL;
+        bool more = compileword(code, p, VAL_ANY, idname, idlen);
+        if(!more) goto endstatement;
+        skipcomments(p);
+        if(p[0] == '=') switch(p[1]) 
+        { 
+            case '/': 
+                if(p[2] != '/') break;
+            case ';': case ' ': case '\t': case '\r': case '\n': case '\0':
+                p++;
+                if(idname) 
+                {
+                    id = newident(idname, IDF_UNKNOWN);
+                    if(id) switch(id->type)
+                    {
+                        case ID_ALIAS:
+                            if(!(more = compilearg(code, p, VAL_ANY))) compilestr(code);
+                            code.add((id->index < MAXARGS ? CODE_ALIASARG : CODE_ALIAS)|(id->index<<8));
+                            goto endcommand;
+                        case ID_VAR:
+                            if(!(more = compilearg(code, p, VAL_INT))) compileint(code);
+                            code.add(CODE_IVAR1|(id->index<<8));
+                            goto endcommand;
+                        case ID_FVAR:
+                            if(!(more = compilearg(code, p, VAL_FLOAT))) compilefloat(code);
+                            code.add(CODE_FVAR1|(id->index<<8));
+                            goto endcommand;
+                        case ID_SVAR:
+                            if(!(more = compilearg(code, p, VAL_STR))) compilestr(code);
+                            code.add(CODE_SVAR1|(id->index<<8));
+                            goto endcommand;
+                        case ID_COMMAND:
+                            if(id->flags&IDF_EMUVAR) goto compilecommand;
+                            break;
+                    }
+                    compilestr(code, idname, idlen, true);
+                    delete[] idname;
+                }
+                if(!(more = compilearg(code, p, VAL_ANY))) compilestr(code);
+                code.add(CODE_ALIASU);
+                goto endstatement;
+        }
+    compilecommand:
+        numargs = 0;
+        if(!idname)
+        {
+        noid:
+            while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++;
+            code.add(CODE_CALLU);
+        }
+        else
+        {
+            id = idents.access(idname);
+            if(!id) 
+            {
+                if(!checknumber(idname)) { compilestr(code, idname, idlen); delete[] idname; goto noid; }
+                char *end = idname;
+                int val = int(strtoul(idname, &end, 0));
+                if(*end) compilestr(code, idname, idlen);
+                else compileint(code, val);    
+                code.add(CODE_RESULT);
+            }
+            else switch(id->type)
+            {
+                case ID_ALIAS:
+                    while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++;
+                    code.add((id->index < MAXARGS ? CODE_CALLARG : CODE_CALL)|(id->index<<8));
+                    break;
+                case ID_COMMAND:
+                {
+                    int comtype = CODE_COM, fakeargs = 0;
+                    bool rep = false;
+                    for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt)
+                    {
+                    case 's': 
+                        if(more) more = compilearg(code, p, VAL_STR);
+                        if(!more) 
+                        {
+                            if(rep) break;
+                            compilestr(code, NULL, 0, true);
+                            fakeargs++;
+                        }
+                        else if(!fmt[1])
+                        {
+                            int numconc = 0;
+                            while(numargs + numconc < MAXARGS && (more = compilearg(code, p, VAL_STR))) numconc++;
+                            if(numconc > 0) code.add(CODE_CONC|RET_STR|((numconc+1)<<8));
+                        }
+                        numargs++;
+                        break;
+                    case 'i': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code); fakeargs++; } numargs++; break;
+                    case 'b': if(more) more = compilearg(code, p, VAL_INT); if(!more) { if(rep) break; compileint(code, INT_MIN); fakeargs++; } numargs++; break;
+                    case 'f': if(more) more = compilearg(code, p, VAL_FLOAT); if(!more) { if(rep) break; compilefloat(code); fakeargs++; } numargs++; break; 
+                    case 't': if(more) more = compilearg(code, p, VAL_ANY); if(!more) { if(rep) break; compilenull(code); fakeargs++; } numargs++; break;
+                    case 'e': if(more) more = compilearg(code, p, VAL_CODE); if(!more) { if(rep) break; compileblock(code); fakeargs++; } numargs++; break;
+                    case 'r': if(more) more = compilearg(code, p, VAL_IDENT); if(!more) { if(rep) break; compileident(code); fakeargs++; } numargs++; break;
+                    case '$': compileident(code, id); numargs++; break;
+                    case 'N': compileint(code, numargs-fakeargs); numargs++; break;
+#ifndef STANDALONE
+                    case 'D': comtype = CODE_COMD; numargs++; break;
+#endif
+                    case 'C': comtype = CODE_COMC; if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; numargs = 1; goto endfmt;
+                    case 'V': comtype = CODE_COMV; if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numargs++; numargs = 2; goto endfmt;
+                    case '1': case '2': case '3': case '4':
+                        if(more && numargs < MAXARGS)
+                        {
+                            int numrep = *fmt-'0'+1;
+                            fmt -= numrep;
+                            rep = true;
+                        }
+                        else for(; numargs > MAXARGS; numargs--) code.add(CODE_POP);
+                        break;
+                    }
+                endfmt:
+                    code.add(comtype|(rettype < VAL_ANY ? rettype<<CODE_RET : 0)|(id->index<<8));
+                    break;
+                }
+                case ID_LOCAL:
+                    if(more) while(numargs < MAXARGS && (more = compilearg(code, p, VAL_IDENT))) numargs++;
+                    if(more) while((more = compilearg(code, p, VAL_ANY))) code.add(CODE_POP);
+                    code.add(CODE_LOCAL);
+                    break;
+                case ID_VAR:
+                    if(!(more = compilearg(code, p, VAL_INT))) code.add(CODE_PRINT|(id->index<<8));
+                    else if(!(id->flags&IDF_HEX) || !(more = compilearg(code, p, VAL_INT))) code.add(CODE_IVAR1|(id->index<<8));
+                    else if(!(more = compilearg(code, p, VAL_INT))) code.add(CODE_IVAR2|(id->index<<8));
+                    else code.add(CODE_IVAR3|(id->index<<8));
+                    break;
+                case ID_FVAR:
+                    if(!(more = compilearg(code, p, VAL_FLOAT))) code.add(CODE_PRINT|(id->index<<8));
+                    else code.add(CODE_FVAR1|(id->index<<8));
+                    break;
+                case ID_SVAR:
+                    if(!(more = compilearg(code, p, VAL_STR))) code.add(CODE_PRINT|(id->index<<8));
+                    else 
+                    {
+                        int numconc = 0;
+                        while(numconc+1 < MAXARGS && (more = compilearg(code, p, VAL_ANY))) numconc++;
+                        if(numconc > 0) code.add(CODE_CONC|RET_STR|((numconc+1)<<8));
+                        code.add(CODE_SVAR1|(id->index<<8));
+                    }
+                    break;
+            }        
+        endcommand:
+            delete[] idname;
+        }
+    endstatement:
+        if(more) while(compilearg(code, p, VAL_ANY)) code.add(CODE_POP); 
+        p += strcspn(p, ")];/\n\0");
+        int c = *p++;
+        switch(c)
+        {
+            case '\0':
+                if(c != brak) debugcodeline(line, "missing \"%c\"", brak);
+                p--;
+                return;
+
+            case ')':
+            case ']':
+                if(c == brak) return;
+                debugcodeline(line, "unexpected \"%c\"", c); 
+                break;
+
+            case '/':
+                if(*p == '/') p += strcspn(p, "\n\0");
+                goto endstatement;
+        }
+    }
+}
+
+static void compilemain(vector<uint> &code, const char *p, int rettype = VAL_ANY)
+{
+    code.add(CODE_START);
+    compilestatements(code, p, VAL_ANY);
+    code.add(CODE_EXIT|(rettype < VAL_ANY ? rettype<<CODE_RET : 0));
+}
+
+uint *compilecode(const char *p)
+{
+    vector<uint> buf;
+    buf.reserve(64);
+    compilemain(buf, p);
+    uint *code = new uint[buf.length()];
+    memcpy(code, buf.getbuf(), buf.length()*sizeof(uint));
+    code[0] += 0x100;
+    return code;
+}
+
+void keepcode(uint *code)
+{
+    if(!code) return;
+    switch(*code&CODE_OP_MASK)
+    {
+        case CODE_START:
+            *code += 0x100;
+            return;
+    }
+    switch(code[-1]&CODE_OP_MASK)
+    {
+        case CODE_START:
+            code[-1] += 0x100;
+            break;
+        case CODE_OFFSET:
+            code -= int(code[-1]>>8);
+            *code += 0x100;
+            break;
+    }
+}
+
+void freecode(uint *code)
+{
+    if(!code) return;
+    switch(*code&CODE_OP_MASK)
+    {   
+        case CODE_START:
+            *code -= 0x100;
+            if(int(*code) < 0x100) delete[] code;
+            return;
+    }
+    switch(code[-1]&CODE_OP_MASK)
+    {
+        case CODE_START:
+            code[-1] -= 0x100;
+            if(int(code[-1]) < 0x100) delete[] &code[-1];
+            break;
+        case CODE_OFFSET:
+            code -= int(code[-1]>>8);
+            *code -= 0x100;
+            if(int(*code) < 0x100) delete[] code;
+            break;
+    }
+}
+
+void printvar(ident *id, int i)
+{
+    if(i < 0) conoutf(CON_INFO, id->index, "%s = %d", id->name, i);
+    else if(id->flags&IDF_HEX && id->maxval==0xFFFFFF)
+        conoutf(CON_INFO, id->index, "%s = 0x%.6X (%d, %d, %d)", id->name, i, (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF);
+    else
+        conoutf(CON_INFO, id->index, id->flags&IDF_HEX ? "%s = 0x%X" : "%s = %d", id->name, i);
+}
+
+void printfvar(ident *id, float f)
+{
+    conoutf(CON_INFO, id->index, "%s = %s", id->name, floatstr(f));
+}
+
+void printsvar(ident *id, const char *s)
+{
+    conoutf(CON_INFO, id->index, strchr(s, '"') ? "%s = [%s]" : "%s = \"%s\"", id->name, s);
+}
+
+template <class V>
+static void printvar(ident *id, int type, V &val)
+{
+    switch(type)
+    {
+        case VAL_INT: printvar(id, val.getint()); break;
+        case VAL_FLOAT: printfvar(id, val.getfloat()); break;
+        default: printsvar(id, val.getstr()); break;
+    }
+}
+
+void printvar(ident *id)
+{
+    switch(id->type)
+    {
+        case ID_VAR: printvar(id, *id->storage.i); break;
+        case ID_FVAR: printfvar(id, *id->storage.f); break;
+        case ID_SVAR: printsvar(id, *id->storage.s); break;
+        case ID_ALIAS: printvar(id, id->valtype, *id); break;
+        case ID_COMMAND:
+            if(id->flags&IDF_EMUVAR)
+            {
+                tagval result;
+                executeret(id, NULL, 0, true, result);
+                printvar(id, result.type, result);
+                freearg(result);
+            }
+            break;
+    }
+}
+ICOMMAND(printvar, "r", (ident *id), printvar(id));
+
+typedef void (__cdecl *comfun)();
+typedef void (__cdecl *comfun1)(void *);
+typedef void (__cdecl *comfun2)(void *, void *);
+typedef void (__cdecl *comfun3)(void *, void *, void *);
+typedef void (__cdecl *comfun4)(void *, void *, void *, void *);
+typedef void (__cdecl *comfun5)(void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun6)(void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun7)(void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun8)(void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun9)(void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun10)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun11)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfun12)(void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *, void *);
+typedef void (__cdecl *comfunv)(tagval *, int);
+
+static const uint *skipcode(const uint *code, tagval &result)
+{
+    int depth = 0;
+    for(;;)
+    {
+        uint op = *code++;
+        switch(op&0xFF)
+        {
+            case CODE_MACRO:
+            case CODE_VAL|RET_STR:
+            {
+                uint len = op>>8;
+                code += len/sizeof(uint) + 1;
+                continue;
+            }
+            case CODE_BLOCK:
+            {
+                uint len = op>>8;
+                code += len;
+                continue;
+            }
+            case CODE_ENTER:
+                ++depth;
+                continue;
+            case CODE_EXIT|RET_NULL: case CODE_EXIT|RET_STR: case CODE_EXIT|RET_INT: case CODE_EXIT|RET_FLOAT:
+                if(depth <= 0)
+                {
+                    forcearg(result, op&CODE_RET_MASK);
+                    return code;
+                }
+                --depth;
+                continue;
+        }
+    }
+}
+
+static inline void callcommand(ident *id, tagval *args, int numargs, bool lookup = false)
+{
+    int i = -1, fakeargs = 0;
+    bool rep = false;
+    for(const char *fmt = id->args; *fmt; fmt++) switch(*fmt)
+    {
+        case 'i': if(++i >= numargs) { if(rep) break; args[i].setint(0); fakeargs++; } else forceint(args[i]); break;
+        case 'b': if(++i >= numargs) { if(rep) break; args[i].setint(INT_MIN); fakeargs++; } else forceint(args[i]); break;
+        case 'f': if(++i >= numargs) { if(rep) break; args[i].setfloat(0.0f); fakeargs++; } else forcefloat(args[i]); break;
+        case 's': if(++i >= numargs) { if(rep) break; args[i].setstr(newstring("")); fakeargs++; } else forcestr(args[i]); break;
+        case 't': if(++i >= numargs) { if(rep) break; args[i].setnull(); fakeargs++; } break;
+        case 'e':
+            if(++i >= numargs)
+            {
+                if(rep) break;
+                static uint buf[2] = { CODE_START + 0x100, CODE_EXIT };
+                args[i].setcode(buf);
+                fakeargs++;
+            }
+            else
+            {
+                vector<uint> buf;
+                buf.reserve(64);
+                compilemain(buf, numargs <= i ? "" : args[i].getstr());
+                freearg(args[i]);
+                args[i].setcode(buf.getbuf()+1);
+                buf.disown();
+            }
+            break;
+        case 'r': if(++i >= numargs) { if(rep) break; args[i].setident(dummyident); fakeargs++; } else forceident(args[i]); break;
+        case '$': if(++i < numargs) freearg(args[i]); args[i].setident(id); break;
+        case 'N': if(++i < numargs) freearg(args[i]); args[i].setint(lookup ? -1 : i-fakeargs); break;
+#ifndef STANDALONE
+        case 'D': if(++i < numargs) freearg(args[i]); args[i].setint(addreleaseaction(conc(args, i, true, id->name)) ? 1 : 0); fakeargs++; break;
+#endif
+        case 'C': { i = max(i+1, numargs); vector<char> buf; ((comfun1)id->fun)(conc(buf, args, i, true)); goto cleanup; }
+        case 'V': i = max(i+1, numargs); ((comfunv)id->fun)(args, i); goto cleanup;
+        case '1': case '2': case '3': case '4': if(i+1 < numargs) { fmt -= *fmt-'0'+1; rep = true; } break;
+    }
+    #define ARG(n) (id->argmask&(1<<n) ? (void *)args[n].s : (void *)&args[n].i)
+    #define CALLCOM(n) \
+        switch(n) \
+        { \
+            case 0: ((comfun)id->fun)(); break; \
+            case 1: ((comfun1)id->fun)(ARG(0)); break; \
+            case 2: ((comfun2)id->fun)(ARG(0), ARG(1)); break; \
+            case 3: ((comfun3)id->fun)(ARG(0), ARG(1), ARG(2)); break; \
+            case 4: ((comfun4)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3)); break; \
+            case 5: ((comfun5)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4)); break; \
+            case 6: ((comfun6)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5)); break; \
+            case 7: ((comfun7)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6)); break; \
+            case 8: ((comfun8)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7)); break; \
+            case 9: ((comfun9)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8)); break; \
+            case 10: ((comfun10)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9)); break; \
+            case 11: ((comfun11)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9), ARG(10)); break; \
+            case 12: ((comfun12)id->fun)(ARG(0), ARG(1), ARG(2), ARG(3), ARG(4), ARG(5), ARG(6), ARG(7), ARG(8), ARG(9), ARG(10), ARG(11)); break; \
+        }
+    ++i;
+    CALLCOM(i)
+cleanup:
+    loopk(i) freearg(args[k]);
+    for(; i < numargs; i++) freearg(args[i]);
+}
+
+#define MAXRUNDEPTH 255
+static int rundepth = 0;
+static const uint *runcode(const uint *code, tagval &result)
+{
+    result.setnull();
+    if(rundepth >= MAXRUNDEPTH)
+    {
+        debugcode("exceeded recursion limit");
+        return skipcode(code, result);
+    }
+    ++rundepth;
+    ident *id = NULL;
+    int numargs = 0;
+    tagval args[MAXARGS+1], *prevret = commandret;
+    commandret = &result;
+    for(;;)
+    {
+        uint op = *code++;
+        switch(op&0xFF)
+        {
+            case CODE_START: case CODE_OFFSET: continue;
+
+            case CODE_POP:
+                freearg(args[--numargs]);
+                continue;        
+            case CODE_ENTER: 
+                code = runcode(code, args[numargs++]); 
+                continue;
+            case CODE_EXIT|RET_NULL: case CODE_EXIT|RET_STR: case CODE_EXIT|RET_INT: case CODE_EXIT|RET_FLOAT: 
+                forcearg(result, op&CODE_RET_MASK);
+                goto exit;
+            case CODE_PRINT:
+                printvar(identmap[op>>8]);
+                continue;
+            case CODE_LOCAL:
+            {
+                identstack locals[MAXARGS];
+                freearg(result);
+                loopi(numargs) pushalias(*args[i].id, locals[i]);
+                code = runcode(code, result);
+                loopi(numargs) popalias(*args[i].id);
+                goto exit;
+            }
+        
+            case CODE_MACRO:
+            {
+                uint len = op>>8;
+                args[numargs++].setmacro(code);
+                code += len/sizeof(uint) + 1;
+                continue;
+            }
+
+            case CODE_VAL|RET_STR:
+            {
+                uint len = op>>8;
+                args[numargs++].setstr(newstring((const char *)code, len));
+                code += len/sizeof(uint) + 1;
+                continue;
+            }
+            case CODE_VALI|RET_STR:
+            {
+                char s[4] = { char((op>>8)&0xFF), char((op>>16)&0xFF), char((op>>24)&0xFF), '\0' };
+                args[numargs++].setstr(newstring(s));
+                continue;
+            }
+            case CODE_VAL|RET_NULL:
+            case CODE_VALI|RET_NULL: args[numargs++].setnull(); continue;
+            case CODE_VAL|RET_INT: args[numargs++].setint(int(*code++)); continue;
+            case CODE_VALI|RET_INT: args[numargs++].setint(int(op)>>8); continue;
+            case CODE_VAL|RET_FLOAT: args[numargs++].setfloat(*(const float *)code++); continue;
+            case CODE_VALI|RET_FLOAT: args[numargs++].setfloat(float(int(op)>>8)); continue;
+
+            case CODE_FORCE|RET_STR: forcestr(args[numargs-1]); continue;
+            case CODE_FORCE|RET_INT: forceint(args[numargs-1]); continue;
+            case CODE_FORCE|RET_FLOAT: forcefloat(args[numargs-1]); continue;
+
+            case CODE_RESULT|RET_NULL: case CODE_RESULT|RET_STR: case CODE_RESULT|RET_INT: case CODE_RESULT|RET_FLOAT:
+            litval:
+                freearg(result);
+                result = args[0];
+                forcearg(result, op&CODE_RET_MASK);
+                args[0].setnull();
+                freeargs(args, numargs, 0);
+                continue;
+
+            case CODE_BLOCK:
+            {
+                uint len = op>>8;
+                args[numargs++].setcode(code+1);
+                code += len;
+                continue;
+            }
+            case CODE_COMPILE:
+            {
+                tagval &arg = args[numargs-1];
+                vector<uint> buf;
+                switch(arg.type)
+                {
+                    case VAL_INT: buf.reserve(8); buf.add(CODE_START); compileint(buf, arg.i); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break;
+                    case VAL_FLOAT: buf.reserve(8); buf.add(CODE_START); compilefloat(buf, arg.f); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break;
+                    case VAL_STR: case VAL_MACRO: buf.reserve(64); compilemain(buf, arg.s); freearg(arg); break;
+                    default: buf.reserve(8); buf.add(CODE_START); compilenull(buf); buf.add(CODE_RESULT); buf.add(CODE_EXIT); break;
+                }
+                arg.setcode(buf.getbuf()+1);
+                buf.disown();
+                continue;
+            }
+
+            case CODE_IDENT:
+                args[numargs++].setident(identmap[op>>8]);
+                continue;
+            case CODE_IDENTARG:
+            {
+                ident *id = identmap[op>>8];
+                if(!(aliasstack->usedargs&(1<<id->index)))
+                {
+                    pusharg(*id, nullval, aliasstack->argstack[id->index]);
+                    aliasstack->usedargs |= 1<<id->index;
+                } 
+                args[numargs++].setident(id);
+                continue;
+            }
+            case CODE_IDENTU:
+            {
+                tagval &arg = args[numargs-1];
+                ident *id = arg.type == VAL_STR || arg.type == VAL_MACRO ? newident(arg.s, IDF_UNKNOWN) : dummyident; 
+                if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index)))
+                {
+                    pusharg(*id, nullval, aliasstack->argstack[id->index]);
+                    aliasstack->usedargs |= 1<<id->index;
+                } 
+                freearg(arg);
+                arg.setident(id);
+                continue;
+            }
+
+            case CODE_LOOKUPU|RET_STR:
+                #define LOOKUPU(aval, sval, ival, fval, nval) { \
+                    tagval &arg = args[numargs-1]; \
+                    if(arg.type != VAL_STR && arg.type != VAL_MACRO) continue; \
+                    id = idents.access(arg.s); \
+                    if(id) switch(id->type) \
+                    { \
+                        case ID_ALIAS: \
+                            if(id->flags&IDF_UNKNOWN) break; \
+                            freearg(arg); \
+                            if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index))) { nval; continue; } \
+                            aval; \
+                            continue; \
+                        case ID_SVAR: freearg(arg); sval; continue; \
+                        case ID_VAR: freearg(arg); ival; continue; \
+                        case ID_FVAR: freearg(arg); fval; continue; \
+                        case ID_COMMAND: \
+                        { \
+                            freearg(arg); \
+                            arg.setnull(); \
+                            commandret = &arg; \
+                            tagval buf[MAXARGS]; \
+                            callcommand(id, buf, 0, true); \
+                            forcearg(arg, op&CODE_RET_MASK); \
+                            commandret = &result; \
+                            continue; \
+                        } \
+                        default: freearg(arg); nval; continue; \
+                    } \
+                    debugcode("unknown alias lookup: %s", arg.s); \
+                    freearg(arg); \
+                    nval; \
+                    continue; \
+                }
+                LOOKUPU(arg.setstr(newstring(id->getstr())), 
+                        arg.setstr(newstring(*id->storage.s)),
+                        arg.setstr(newstring(intstr(*id->storage.i))),
+                        arg.setstr(newstring(floatstr(*id->storage.f))),
+                        arg.setstr(newstring("")));
+            case CODE_LOOKUP|RET_STR:
+                #define LOOKUP(aval) { \
+                    id = identmap[op>>8]; \
+                    if(id->flags&IDF_UNKNOWN) debugcode("unknown alias lookup: %s", id->name); \
+                    aval; \
+                    continue; \
+                }
+                LOOKUP(args[numargs++].setstr(newstring(id->getstr())));
+            case CODE_LOOKUPARG|RET_STR:
+                #define LOOKUPARG(aval, nval) { \
+                    id = identmap[op>>8]; \
+                    if(!(aliasstack->usedargs&(1<<id->index))) { nval; continue; } \
+                    aval; \
+                    continue; \
+                }
+                LOOKUPARG(args[numargs++].setstr(newstring(id->getstr())), args[numargs++].setstr(newstring("")));
+            case CODE_LOOKUPU|RET_INT:
+                LOOKUPU(arg.setint(id->getint()),
+                        arg.setint(parseint(*id->storage.s)),
+                        arg.setint(*id->storage.i),
+                        arg.setint(int(*id->storage.f)),
+                        arg.setint(0));
+            case CODE_LOOKUP|RET_INT:
+                LOOKUP(args[numargs++].setint(id->getint()));
+            case CODE_LOOKUPARG|RET_INT:
+                LOOKUPARG(args[numargs++].setint(id->getint()), args[numargs++].setint(0));
+            case CODE_LOOKUPU|RET_FLOAT:
+                LOOKUPU(arg.setfloat(id->getfloat()),
+                        arg.setfloat(parsefloat(*id->storage.s)),
+                        arg.setfloat(float(*id->storage.i)),
+                        arg.setfloat(*id->storage.f),
+                        arg.setfloat(0.0f));
+            case CODE_LOOKUP|RET_FLOAT:
+                LOOKUP(args[numargs++].setfloat(id->getfloat()));
+            case CODE_LOOKUPARG|RET_FLOAT:
+                LOOKUPARG(args[numargs++].setfloat(id->getfloat()), args[numargs++].setfloat(0.0f));
+            case CODE_LOOKUPU|RET_NULL:
+                LOOKUPU(id->getval(arg),
+                        arg.setstr(newstring(*id->storage.s)),
+                        arg.setint(*id->storage.i),
+                        arg.setfloat(*id->storage.f),
+                        arg.setnull());
+            case CODE_LOOKUP|RET_NULL:
+                LOOKUP(id->getval(args[numargs++]));
+            case CODE_LOOKUPARG|RET_NULL:
+                LOOKUPARG(id->getval(args[numargs++]), args[numargs++].setnull());
+
+            case CODE_SVAR|RET_STR: case CODE_SVAR|RET_NULL: args[numargs++].setstr(newstring(*identmap[op>>8]->storage.s)); continue;
+            case CODE_SVAR|RET_INT: args[numargs++].setint(parseint(*identmap[op>>8]->storage.s)); continue;
+            case CODE_SVAR|RET_FLOAT: args[numargs++].setfloat(parsefloat(*identmap[op>>8]->storage.s)); continue;
+            case CODE_SVAR1: setsvarchecked(identmap[op>>8], args[0].s); freeargs(args, numargs, 0); continue;
+
+            case CODE_IVAR|RET_INT: case CODE_IVAR|RET_NULL: args[numargs++].setint(*identmap[op>>8]->storage.i); continue;
+            case CODE_IVAR|RET_STR: args[numargs++].setstr(newstring(intstr(*identmap[op>>8]->storage.i))); continue;
+            case CODE_IVAR|RET_FLOAT: args[numargs++].setfloat(float(*identmap[op>>8]->storage.i)); continue;
+            case CODE_IVAR1: setvarchecked(identmap[op>>8], args[0].i); numargs = 0; continue;
+            case CODE_IVAR2: setvarchecked(identmap[op>>8], (args[0].i<<16)|(args[1].i<<8)); numargs = 0; continue;
+            case CODE_IVAR3: setvarchecked(identmap[op>>8], (args[0].i<<16)|(args[1].i<<8)|args[2].i); numargs = 0; continue;
+
+            case CODE_FVAR|RET_FLOAT: case CODE_FVAR|RET_NULL: args[numargs++].setfloat(*identmap[op>>8]->storage.f); continue;
+            case CODE_FVAR|RET_STR: args[numargs++].setstr(newstring(floatstr(*identmap[op>>8]->storage.f))); continue;
+            case CODE_FVAR|RET_INT: args[numargs++].setint(int(*identmap[op>>8]->storage.f)); continue;
+            case CODE_FVAR1: setfvarchecked(identmap[op>>8], args[0].f); numargs = 0; continue;
+           
+            case CODE_COM|RET_NULL: case CODE_COM|RET_STR: case CODE_COM|RET_FLOAT: case CODE_COM|RET_INT:
+                id = identmap[op>>8];
+#ifndef STANDALONE
+            callcom:
+#endif
+                forcenull(result);
+                CALLCOM(numargs) 
+            forceresult:
+                freeargs(args, numargs, 0);
+                forcearg(result, op&CODE_RET_MASK);
+                continue;
+#ifndef STANDALONE
+            case CODE_COMD|RET_NULL: case CODE_COMD|RET_STR: case CODE_COMD|RET_FLOAT: case CODE_COMD|RET_INT:
+                id = identmap[op>>8];
+                args[numargs].setint(addreleaseaction(conc(args, numargs, true, id->name)) ? 1 : 0);
+                numargs++;
+                goto callcom;
+#endif
+            case CODE_COMV|RET_NULL: case CODE_COMV|RET_STR: case CODE_COMV|RET_FLOAT: case CODE_COMV|RET_INT:
+                id = identmap[op>>8];
+                forcenull(result);
+                ((comfunv)id->fun)(args, numargs);
+                goto forceresult; 
+            case CODE_COMC|RET_NULL: case CODE_COMC|RET_STR: case CODE_COMC|RET_FLOAT: case CODE_COMC|RET_INT:
+                id = identmap[op>>8];
+                forcenull(result);
+                {
+                    vector<char> buf;
+                    buf.reserve(MAXSTRLEN);
+                    ((comfun1)id->fun)(conc(buf, args, numargs, true));
+                }
+                goto forceresult;
+
+            case CODE_CONC|RET_NULL: case CODE_CONC|RET_STR: case CODE_CONC|RET_FLOAT: case CODE_CONC|RET_INT:
+            case CODE_CONCW|RET_NULL: case CODE_CONCW|RET_STR: case CODE_CONCW|RET_FLOAT: case CODE_CONCW|RET_INT:
+            {
+                int numconc = op>>8;
+                char *s = conc(&args[numargs-numconc], numconc, (op&CODE_OP_MASK)==CODE_CONC);
+                freeargs(args, numargs, numargs-numconc);
+                args[numargs++].setstr(s);
+                forcearg(args[numargs-1], op&CODE_RET_MASK);
+                continue;
+            }
+
+            case CODE_CONCM|RET_NULL: case CODE_CONCM|RET_STR: case CODE_CONCM|RET_FLOAT: case CODE_CONCM|RET_INT:
+            {
+                int numconc = op>>8;
+                char *s = conc(&args[numargs-numconc], numconc, false);
+                freeargs(args, numargs, numargs-numconc);
+                result.setstr(s);
+                forcearg(result, op&CODE_RET_MASK);
+                continue;
+            }
+
+            case CODE_ALIAS:
+                setalias(*identmap[op>>8], args[--numargs]);
+                freeargs(args, numargs, 0);
+                continue;
+            case CODE_ALIASARG:
+                setarg(*identmap[op>>8], args[--numargs]);
+                freeargs(args, numargs, 0);
+                continue;
+            case CODE_ALIASU:
+                forcestr(args[0]);
+                setalias(args[0].s, args[--numargs]);
+                freeargs(args, numargs, 0);
+                continue;
+
+            case CODE_CALL|RET_NULL: case CODE_CALL|RET_STR: case CODE_CALL|RET_FLOAT: case CODE_CALL|RET_INT:
+                #define CALLALIAS(offset) { \
+                    identstack argstack[MAXARGS]; \
+                    for(int i = 0; i < numargs-offset; i++) \
+                        pusharg(*identmap[i], args[i+offset], argstack[i]); \
+                    int oldargs = _numargs, newargs = numargs-offset; \
+                    _numargs = newargs; \
+                    int oldflags = identflags; \
+                    identflags |= id->flags&IDF_OVERRIDDEN; \
+                    identlink aliaslink = { id, aliasstack, (1<<newargs)-1, argstack }; \
+                    aliasstack = &aliaslink; \
+                    if(!id->code) id->code = compilecode(id->getstr()); \
+                    uint *code = id->code; \
+                    code[0] += 0x100; \
+                    runcode(code+1, result); \
+                    code[0] -= 0x100; \
+                    if(int(code[0]) < 0x100) delete[] code; \
+                    aliasstack = aliaslink.next; \
+                    identflags = oldflags; \
+                    for(int i = 0; i < newargs; i++) \
+                        poparg(*identmap[i]); \
+                    for(int argmask = aliaslink.usedargs&(~0U<<newargs), i = newargs; argmask; i++) \
+                        if(argmask&(1<<i)) { poparg(*identmap[i]); argmask &= ~(1<<i); } \
+                    forcearg(result, op&CODE_RET_MASK); \
+                    _numargs = oldargs; \
+                    numargs = 0; \
+                }
+                forcenull(result);
+                id = identmap[op>>8];
+                if(id->flags&IDF_UNKNOWN)
+                {
+                    debugcode("unknown command: %s", id->name);
+                    goto forceresult;
+                }
+                CALLALIAS(0);
+                continue;
+            case CODE_CALLARG|RET_NULL: case CODE_CALLARG|RET_STR: case CODE_CALLARG|RET_FLOAT: case CODE_CALLARG|RET_INT:
+                forcenull(result);
+                id = identmap[op>>8];
+                if(!(aliasstack->usedargs&(1<<id->index))) goto forceresult;
+                CALLALIAS(0);
+                continue;
+
+            case CODE_CALLU|RET_NULL: case CODE_CALLU|RET_STR: case CODE_CALLU|RET_FLOAT: case CODE_CALLU|RET_INT:
+                if(args[0].type != VAL_STR) goto litval;
+                id = idents.access(args[0].s);
+                if(!id)
+                {
+                noid:
+                    if(checknumber(args[0].s)) goto litval;
+                    debugcode("unknown command: %s", args[0].s);
+                    forcenull(result);
+                    goto forceresult;
+                } 
+                forcenull(result);
+                switch(id->type)
+                {
+                    case ID_COMMAND:
+                        freearg(args[0]);
+                        callcommand(id, args+1, numargs-1);
+                        forcearg(result, op&CODE_RET_MASK);
+                        numargs = 0;
+                        continue;
+                    case ID_LOCAL:
+                    {
+                        identstack locals[MAXARGS];
+                        freearg(args[0]);
+                        loopj(numargs-1) pushalias(*forceident(args[j+1]), locals[j]);
+                        code = runcode(code, result);
+                        loopj(numargs-1) popalias(*args[j+1].id);
+                        goto exit;  
+                    }
+                    case ID_VAR:
+                        if(numargs <= 1) printvar(id); else setvarchecked(id, &args[1], numargs-1);
+                        goto forceresult;
+                    case ID_FVAR:
+                        if(numargs <= 1) printvar(id); else setfvarchecked(id, forcefloat(args[1]));
+                        goto forceresult;
+                    case ID_SVAR:
+                        if(numargs <= 1) printvar(id); else setsvarchecked(id, forcestr(args[1]));
+                        goto forceresult; 
+                    case ID_ALIAS:
+                        if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index))) goto forceresult;
+                        if(id->valtype==VAL_NULL) goto noid;
+                        freearg(args[0]);
+                        CALLALIAS(1);
+                        continue;
+                    default:
+                        goto forceresult;
+                }
+        }
+    }
+exit:
+    commandret = prevret;
+    --rundepth;
+    return code;
+}
+                 
+void executeret(const uint *code, tagval &result)
+{
+    runcode(code, result); 
+}
+
+void executeret(const char *p, tagval &result)
+{
+    vector<uint> code;
+    code.reserve(64);
+    compilemain(code, p, VAL_ANY);
+    runcode(code.getbuf()+1, result);
+    if(int(code[0]) >= 0x100) code.disown();
+}
+
+void executeret(ident *id, tagval *args, int numargs, bool lookup, tagval &result)
+{
+    result.setnull();
+    ++rundepth;
+    tagval *prevret = commandret;
+    commandret = &result;
+    if(rundepth > MAXRUNDEPTH) debugcode("exceeded recursion limit");
+    else if(id) switch(id->type)
+    {
+        default:
+            if(!id->fun) break;
+            // fall-through
+        case ID_COMMAND:
+            if(numargs < id->numargs)
+            {
+                tagval buf[MAXARGS];
+                memcpy(buf, args, numargs*sizeof(tagval));
+                callcommand(id, buf, numargs, lookup);
+            }
+            else callcommand(id, args, numargs, lookup);
+            numargs = 0;
+            break;
+        case ID_VAR:
+            if(numargs <= 0) printvar(id); else setvarchecked(id, args, numargs);
+            break;
+        case ID_FVAR:
+            if(numargs <= 0) printvar(id); else setfvarchecked(id, forcefloat(args[0]));
+            break;
+        case ID_SVAR:
+            if(numargs <= 0) printvar(id); else setsvarchecked(id, forcestr(args[0]));
+            break;
+        case ID_ALIAS:
+            if(id->index < MAXARGS && !(aliasstack->usedargs&(1<<id->index))) break;
+            if(id->valtype==VAL_NULL) break;
+            #define op RET_NULL
+            CALLALIAS(0);
+            #undef op
+            break;
+    }
+    freeargs(args, numargs, 0);
+    commandret = prevret;
+    --rundepth;
+}
+
+char *executestr(const uint *code)
+{
+    tagval result;
+    runcode(code, result);
+    if(result.type == VAL_NULL) return NULL;
+    forcestr(result);
+    return result.s;
+}
+
+char *executestr(const char *p)
+{
+    tagval result;
+    executeret(p, result);
+    if(result.type == VAL_NULL) return NULL;
+    forcestr(result);
+    return result.s;
+}
+
+char *executestr(ident *id, tagval *args, int numargs, bool lookup)
+{
+    tagval result; 
+    executeret(id, args, numargs, lookup, result);
+    if(result.type == VAL_NULL) return NULL;
+    forcestr(result);
+    return result.s;
+}
+
+char *execidentstr(const char *name, bool lookup)
+{
+    ident *id = idents.access(name);
+    return id ? executestr(id, NULL, 0, lookup) : NULL;
+}
+
+int execute(const uint *code)
+{
+    tagval result;
+    runcode(code, result);
+    int i = result.getint();
+    freearg(result);
+    return i;
+}
+
+int execute(const char *p)
+{
+    vector<uint> code;
+    code.reserve(64);
+    compilemain(code, p, VAL_INT);
+    tagval result;
+    runcode(code.getbuf()+1, result);
+    if(int(code[0]) >= 0x100) code.disown();
+    int i = result.getint();
+    freearg(result);
+    return i;
+}
+
+int execute(ident *id, tagval *args, int numargs, bool lookup)
+{
+    tagval result;
+    executeret(id, args, numargs, lookup, result);
+    int i = result.getint();
+    freearg(result);
+    return i;
+}
+
+int execident(const char *name, int noid, bool lookup)
+{
+    ident *id = idents.access(name);
+    return id ? execute(id, NULL, 0, lookup) : noid;
+}
+
+static inline bool getbool(const char *s)
+{
+    switch(s[0])
+    {
+        case '+': case '-': 
+            switch(s[1])
+            {
+                case '0': break;
+                case '.': return !isdigit(s[2]) || parsefloat(s) != 0;
+                default: return true;
+            }
+            // fall through
+        case '0':
+        {
+            char *end;
+            int val = int(strtoul((char *)s, &end, 0));
+            if(val) return true;
+            switch(*end)
+            {
+                case 'e': case '.': return parsefloat(s) != 0;
+                default: return false;
+            }
+        }
+        case '.': return !isdigit(s[1]) || parsefloat(s) != 0;
+        case '\0': return false;
+        default: return true;
+    }
+}
+
+static inline bool getbool(const tagval &v)
+{
+    switch(v.type)
+    {
+        case VAL_FLOAT: return v.f!=0;
+        case VAL_INT: return v.i!=0;
+        case VAL_STR: case VAL_MACRO: return getbool(v.s);
+        default: return false;
+    }
+}
+
+bool executebool(const uint *code)
+{
+    tagval result;
+    runcode(code, result);
+    bool b = getbool(result);
+    freearg(result);
+    return b;
+}
+
+bool executebool(const char *p)
+{
+    tagval result;
+    executeret(p, result);
+    bool b = getbool(result);
+    freearg(result);
+    return b;
+}
+
+bool executebool(ident *id, tagval *args, int numargs, bool lookup)
+{
+    tagval result;
+    executeret(id, args, numargs, lookup, result);
+    bool b = getbool(result);
+    freearg(result);
+    return b;
+}
+
+bool execidentbool(const char *name, bool noid, bool lookup)
+{
+    ident *id = idents.access(name);
+    return id ? executebool(id, NULL, 0, lookup) : noid;
+}
+
+bool execfile(const char *cfgfile, bool msg)
+{
+    string s;
+    copystring(s, cfgfile);
+    char *buf = loadfile(path(s), NULL);
+    if(!buf)
+    {
+        if(msg) conoutf(CON_ERROR, "could not read \"%s\"", cfgfile);
+        return false;
+    }
+    const char *oldsourcefile = sourcefile, *oldsourcestr = sourcestr;
+    sourcefile = cfgfile;
+    sourcestr = buf;
+    execute(buf);
+    sourcefile = oldsourcefile;
+    sourcestr = oldsourcestr;
+    delete[] buf;
+    return true;
+}
+ICOMMAND(exec, "sb", (char *file, int *msg), intret(execfile(file, *msg != 0) ? 1 : 0));
+
+const char *escapestring(const char *s)
+{
+    static vector<char> strbuf[3];
+    static int stridx = 0;
+    stridx = (stridx + 1)%3;
+    vector<char> &buf = strbuf[stridx];
+    buf.setsize(0);
+    buf.add('"');
+    for(; *s; s++) switch(*s)
+    {
+        case '\n': buf.put("^n", 2); break;
+        case '\t': buf.put("^t", 2); break;
+        case '\f': buf.put("^f", 2); break;
+        case '"': buf.put("^\"", 2); break;
+        case '^': buf.put("^^", 2); break;
+        default: buf.add(*s); break;
+    }
+    buf.put("\"\0", 2);
+    return buf.getbuf();
+}
+
+ICOMMAND(escape, "s", (char *s), result(escapestring(s)));
+ICOMMAND(unescape, "s", (char *s),
+{
+    int len = strlen(s);
+    char *d = newstring(len);
+    d[unescapestring(d, s, &s[len])] = '\0';
+    stringret(d);
+});
+
+const char *escapeid(const char *s)
+{
+    const char *end = s + strcspn(s, "\"/;()[]@ \f\t\r\n\0");
+    return *end ? escapestring(s) : s;
+}
+
+bool validateblock(const char *s)
+{
+    const int maxbrak = 100;
+    static char brakstack[maxbrak];
+    int brakdepth = 0;
+    for(; *s; s++) switch(*s)
+    {
+        case '[': case '(': if(brakdepth >= maxbrak) return false; brakstack[brakdepth++] = *s; break;
+        case ']': if(brakdepth <= 0 || brakstack[--brakdepth] != '[') return false; break;
+        case ')': if(brakdepth <= 0 || brakstack[--brakdepth] != '(') return false; break;
+        case '"': s = parsestring(s + 1); if(*s != '"') return false; break;
+        case '/': if(s[1] == '/') return false; break;
+        case '@': case '\f': return false;
+    }
+    return brakdepth == 0;
+}
+
+#ifndef STANDALONE
+void writecfg(const char *name)
+{
+    stream *f = openutf8file(path(name && name[0] ? name : game::savedconfig(), true), "w");
+    if(!f) return;
+    f->printf("// automatically written on exit, DO NOT MODIFY\n// delete this file to have %s overwrite these settings\n// modify settings in game, or put settings in %s to override anything\n\n", game::defaultconfig(), game::autoexec());
+    game::writeclientinfo(f);
+    f->printf("\n");
+    writecrosshairs(f);
+    vector<ident *> ids;
+    enumerate(idents, ident, id, ids.add(&id));
+    ids.sortname();
+    loopv(ids)
+    {
+        ident &id = *ids[i];
+        if(id.flags&IDF_PERSIST) switch(id.type)
+        {
+            case ID_VAR: f->printf("%s %d\n", escapeid(id), *id.storage.i); break;
+            case ID_FVAR: f->printf("%s %s\n", escapeid(id), floatstr(*id.storage.f)); break;
+            case ID_SVAR: f->printf("%s %s\n", escapeid(id), escapestring(*id.storage.s)); break;
+        }
+    }
+    f->printf("\n");
+    writebinds(f);
+    f->printf("\n");
+    loopv(ids)
+    {
+        ident &id = *ids[i];
+        if(id.type==ID_ALIAS && id.flags&IDF_PERSIST && !(id.flags&IDF_OVERRIDDEN)) switch(id.valtype)
+        {
+        case VAL_STR:
+            if(!id.val.s[0]) break;
+            if(!validateblock(id.val.s)) { f->printf("%s = %s\n", escapeid(id), escapestring(id.val.s)); break; }
+        case VAL_FLOAT:
+        case VAL_INT: 
+            f->printf("%s = [%s]\n", escapeid(id), id.getstr()); break;
+        }
+    }
+    f->printf("\n");
+    writecompletions(f);
+    delete f;
+}
+
+COMMAND(writecfg, "s");
+#endif
+
+void changedvars()
+{
+    vector<ident *> ids;
+    enumerate(idents, ident, id, if(id.flags&IDF_OVERRIDDEN) ids.add(&id));
+    ids.sortname();
+    loopv(ids) printvar(ids[i]);
+}   
+COMMAND(changedvars, "");
+
+// below the commands that implement a small imperative language. thanks to the semantics of
+// () and [] expressions, any control construct can be defined trivially.
+
+static string retbuf[4];
+static int retidx = 0;
+
+const char *intstr(int v)
+{
+    retidx = (retidx + 1)%4;
+    intformat(retbuf[retidx], v);
+    return retbuf[retidx];
+}
+
+void intret(int v)
+{
+    commandret->setint(v);
+}
+
+const char *floatstr(float v)
+{
+    retidx = (retidx + 1)%4;
+    floatformat(retbuf[retidx], v);
+    return retbuf[retidx];
+}
+
+void floatret(float v)
+{
+    commandret->setfloat(v);
+}
+
+#undef ICOMMANDNAME
+#define ICOMMANDNAME(name) _stdcmd
+
+ICOMMAND(do, "e", (uint *body), executeret(body, *commandret));
+ICOMMAND(if, "tee", (tagval *cond, uint *t, uint *f), executeret(getbool(*cond) ? t : f, *commandret));
+ICOMMAND(?, "ttt", (tagval *cond, tagval *t, tagval *f), result(*(getbool(*cond) ? t : f)));
+
+ICOMMAND(pushif, "rte", (ident *id, tagval *v, uint *code),
+{
+    if(id->type != ID_ALIAS || id->index < MAXARGS) return;
+    if(getbool(*v))
+    {
+        identstack stack;
+        pusharg(*id, *v, stack);
+        v->type = VAL_NULL;
+        id->flags &= ~IDF_UNKNOWN;
+        executeret(code, *commandret);
+        poparg(*id);
+    }
+});
+
+void loopiter(ident *id, identstack &stack, const tagval &v)
+{
+    if(id->stack != &stack)
+    {
+        pusharg(*id, v, stack);
+        id->flags &= ~IDF_UNKNOWN;
+    }
+    else
+    {
+        if(id->valtype == VAL_STR) delete[] id->val.s;
+        cleancode(*id);
+        id->setval(v);
+    }
+}
+
+void loopend(ident *id, identstack &stack)
+{
+    if(id->stack == &stack) poparg(*id);
+}
+
+static inline void setiter(ident &id, int i, identstack &stack)
+{
+    if(id.stack == &stack)
+    {
+        if(id.valtype != VAL_INT)
+        {
+            if(id.valtype == VAL_STR) delete[] id.val.s;
+            cleancode(id);
+            id.valtype = VAL_INT;
+        }
+        id.val.i = i;
+    }
+    else
+    {
+        tagval t;
+        t.setint(i);
+        pusharg(id, t, stack);
+        id.flags &= ~IDF_UNKNOWN;
+    }
+}
+ICOMMAND(loop, "rie", (ident *id, int *n, uint *body),
+{
+    if(*n <= 0 || id->type!=ID_ALIAS) return;
+    identstack stack;
+    loopi(*n)
+    {
+        setiter(*id, i, stack);
+        execute(body);
+    }
+    poparg(*id);
+});
+ICOMMAND(loopwhile, "riee", (ident *id, int *n, uint *cond, uint *body),
+{
+    if(*n <= 0 || id->type!=ID_ALIAS) return;
+    identstack stack;
+    loopi(*n)
+    {
+        setiter(*id, i, stack);
+        if(!executebool(cond)) break;
+        execute(body);
+    }
+    poparg(*id);
+});
+ICOMMAND(while, "ee", (uint *cond, uint *body), while(executebool(cond)) execute(body));
+
+char *loopconc(ident *id, int n, uint *body, bool space)
+{
+    identstack stack;
+    vector<char> s;
+    loopi(n)
+    {
+        setiter(*id, i, stack);
+        tagval v;
+        executeret(body, v);
+        const char *vstr = v.getstr();
+        int len = strlen(vstr);
+        if(space && i) s.add(' ');
+        s.put(vstr, len);
+        freearg(v);
+    }
+    if(n > 0) poparg(*id);
+    s.add('\0');
+    return newstring(s.getbuf(), s.length()-1);
+}
+
+ICOMMAND(loopconcat, "rie", (ident *id, int *n, uint *body),
+{
+    if(*n > 0 && id->type==ID_ALIAS) commandret->setstr(loopconc(id, *n, body, true));
+});
+
+ICOMMAND(loopconcatword, "rie", (ident *id, int *n, uint *body),
+{
+    if(*n > 0 && id->type==ID_ALIAS) commandret->setstr(loopconc(id, *n, body, false));
+});
+
+void concat(tagval *v, int n)
+{ 
+    commandret->setstr(conc(v, n, true));
+}
+COMMAND(concat, "V");
+
+void concatword(tagval *v, int n)
+{ 
+    commandret->setstr(conc(v, n, false));
+}   
+COMMAND(concatword, "V");
+
+void append(ident *id, tagval *v, bool space)
+{
+    if(id->type != ID_ALIAS || v->type == VAL_NULL) return;
+    if(id->valtype == VAL_NULL)
+    {
+    noprefix:
+        if(id->index < MAXARGS) setarg(*id, *v); else setalias(*id, *v);
+        v->type = VAL_NULL;
+    }
+    else
+    {
+        const char *prefix = id->getstr();
+        if(!prefix[0]) goto noprefix;
+        tagval r;
+        r.setstr(conc(v, 1, space, prefix));
+        if(id->index < MAXARGS) setarg(*id, r); else setalias(*id, r);
+    }
+}
+ICOMMAND(append, "rt", (ident *id, tagval *v), append(id, v, true));
+ICOMMAND(appendword, "rt", (ident *id, tagval *v), append(id, v, false));
+
+void result(tagval &v)
+{
+    *commandret = v;
+    v.type = VAL_NULL;
+}
+
+void stringret(char *s)
+{
+    commandret->setstr(s);
+}
+
+void result(const char *s)
+{
+    commandret->setstr(newstring(s));
+}
+
+ICOMMAND(result, "t", (tagval *v),
+{
+    *commandret = *v;
+    v->type = VAL_NULL;
+});
+
+void format(tagval *args, int numargs)
+{
+    vector<char> s;
+    const char *f = args[0].getstr();
+    while(*f)
+    {
+        int c = *f++;
+        if(c == '%')
+        {
+            int i = *f++;
+            if(i >= '1' && i <= '9')
+            {
+                i -= '0';
+                const char *sub = i < numargs ? args[i].getstr() : "";
+                while(*sub) s.add(*sub++);
+            }
+            else s.add(i);
+        }
+        else s.add(c);
+    }
+    s.add('\0');
+    result(s.getbuf());
+}
+COMMAND(format, "V");
+
+static const char *liststart = NULL, *listend = NULL, *listquotestart = NULL, *listquoteend = NULL;
+
+static inline void skiplist(const char *&p)
+{
+    for(;;)
+    {
+        p += strspn(p, " \t\r\n");
+        if(p[0]!='/' || p[1]!='/') break;
+        p += strcspn(p, "\n\0");
+    }
+}
+
+static bool parselist(const char *&s, const char *&start = liststart, const char *&end = listend, const char *&quotestart = listquotestart, const char *&quoteend = listquoteend)
+{
+    skiplist(s);
+    switch(*s)
+    {
+        case '"': quotestart = s++; start = s; s = parsestring(s); end = s; if(*s == '"') s++; quoteend = s; break;
+        case '(': case '[':
+            quotestart = s;
+            start = s+1;
+            for(int braktype = *s++, brak = 1;;)
+            {
+                s += strcspn(s, "\"/;()[]\0");
+                int c = *s++;
+                switch(c)
+                {
+                    case '\0': s--; quoteend = end = s; return true;
+                    case '"': s = parsestring(s); if(*s == '"') s++; break;
+                    case '/': if(*s == '/') s += strcspn(s, "\n\0"); break;
+                    case '(': case '[': if(c == braktype) brak++; break;
+                    case ')': if(braktype == '(' && --brak <= 0) goto endblock; break;
+                    case ']': if(braktype == '[' && --brak <= 0) goto endblock; break;
+                } 
+            }
+        endblock:
+            end = s-1;
+            quoteend = s;
+            break;
+        case '\0': case ')': case ']': return false;
+        default: quotestart = start = s; s = parseword(s); quoteend = end = s; break;
+    }
+    skiplist(s);
+    if(*s == ';') s++;
+    return true;
+}
+     
+void explodelist(const char *s, vector<char *> &elems, int limit)
+{
+    const char *start, *end;
+    while((limit < 0 || elems.length() < limit) && parselist(s, start, end))
+        elems.add(newstring(start, end-start));
+}
+
+char *indexlist(const char *s, int pos)
+{
+    loopi(pos) if(!parselist(s)) return newstring("");
+    const char *start, *end;
+    return parselist(s, start, end) ? newstring(start, end-start) : newstring("");
+}
+
+int listlen(const char *s)
+{
+    int n = 0;
+    while(parselist(s)) n++;
+    return n;
+}
+ICOMMAND(listlen, "s", (char *s), intret(listlen(s)));
+
+void at(tagval *args, int numargs)
+{
+    if(!numargs) return;
+    const char *start = args[0].getstr(), *end = start + strlen(start);
+    for(int i = 1; i < numargs; i++)
+    {
+        const char *list = start;
+        int pos = args[i].getint();
+        for(; pos > 0; pos--) if(!parselist(list)) break; 
+        if(pos > 0 || !parselist(list, start, end)) start = end = "";
+    }
+    commandret->setstr(newstring(start, end-start));
+}
+COMMAND(at, "si1V");
+
+void substr(char *s, int *start, int *count, int *numargs)
+{
+    int len = strlen(s), offset = clamp(*start, 0, len);
+    commandret->setstr(newstring(&s[offset], *numargs >= 3 ? clamp(*count, 0, len - offset) : len - offset));
+}
+COMMAND(substr, "siiN");
+
+void chopstr(char *s, int *lim, char *ellipsis)
+{
+    int len = strlen(s), maxlen = abs(*lim);
+    if(len > maxlen)
+    {
+        int elen = strlen(ellipsis);
+        maxlen = max(maxlen, elen);
+        char *chopped = newstring(maxlen);
+        if(*lim < 0)
+        {
+            memcpy(chopped, ellipsis, elen);
+            memcpy(&chopped[elen], &s[len - (maxlen - elen)], maxlen - elen);
+        }
+        else
+        {
+            memcpy(chopped, s, maxlen - elen);
+            memcpy(&chopped[maxlen - elen], ellipsis, elen);
+        }
+        chopped[maxlen] = '\0';
+        commandret->setstr(chopped);
+    }
+    else result(s);
+}
+COMMAND(chopstr, "sis");
+
+void sublist(const char *s, int *skip, int *count, int *numargs)
+{
+    int offset = max(*skip, 0), len = *numargs >= 3 ? max(*count, 0) : -1;
+    loopi(offset) if(!parselist(s)) break;
+    if(len < 0) { if(offset > 0) skiplist(s); commandret->setstr(newstring(s)); return; }
+    const char *list = s, *start, *end, *qstart, *qend = s;
+    if(len > 0 && parselist(s, start, end, list, qend)) while(--len > 0 && parselist(s, start, end, qstart, qend));
+    commandret->setstr(newstring(list, qend - list)); 
+}
+COMMAND(sublist, "siiN");
+
+ICOMMAND(stripcolors, "s", (char *s),
+{
+    int len = strlen(s);
+    char *d = newstring(len);
+    filtertext(d, s, true, false, len);
+    stringret(d);
+});
+
+static inline void setiter(ident &id, char *val, identstack &stack)
+{
+    if(id.stack == &stack)
+    {
+        if(id.valtype == VAL_STR) delete[] id.val.s;
+        else id.valtype = VAL_STR;
+        cleancode(id);
+        id.val.s = val;
+    }
+    else
+    {
+        tagval t;
+        t.setstr(val);
+        pusharg(id, t, stack);
+        id.flags &= ~IDF_UNKNOWN;
+    }
+}
+
+void listfind(ident *id, const char *list, const uint *body)
+{
+    if(id->type!=ID_ALIAS) { intret(-1); return; }
+    identstack stack;
+    int n = -1;
+    for(const char *s = list, *start, *end; parselist(s, start, end);)
+    {
+        ++n;
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+        if(executebool(body)) { intret(n); goto found; }
+    }
+    intret(-1);
+found:
+    if(n >= 0) poparg(*id);
+}
+COMMAND(listfind, "rse");
+
+void looplist(ident *id, const char *list, const uint *body)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    int n = 0;
+    for(const char *s = list, *start, *end; parselist(s, start, end); n++)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+        execute(body);
+    }
+    if(n) poparg(*id);
+}
+COMMAND(looplist, "rse");
+
+void loopsublist(ident *id, const char *list, int *skip, int *count, const uint *body)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    int n = 0, offset = max(*skip, 0), len = *count < 0 ? INT_MAX : offset + *count;
+    for(const char *s = list, *start, *end; parselist(s, start, end) && n < len; n++) if(n >= offset)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+        execute(body);
+    }
+    if(n) poparg(*id);
+}
+COMMAND(loopsublist, "rsiie");
+
+void looplistconc(ident *id, const char *list, const uint *body, bool space)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    vector<char> r;
+    int n = 0;
+    for(const char *s = list, *start, *end; parselist(s, start, end); n++)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+
+        if(n && space) r.add(' ');
+
+        tagval v;
+        executeret(body, v);
+        const char *vstr = v.getstr();
+        int len = strlen(vstr);
+        r.put(vstr, len);
+        freearg(v);
+    }
+    if(n) poparg(*id);
+    r.add('\0');
+    commandret->setstr(newstring(r.getbuf(), r.length()-1));
+}
+ICOMMAND(looplistconcat, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, true));
+ICOMMAND(looplistconcatword, "rse", (ident *id, char *list, uint *body), looplistconc(id, list, body, false));
+
+void listfilter(ident *id, const char *list, const uint *body)
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    vector<char> r;
+    int n = 0;
+    for(const char *s = list, *start, *end, *quotestart, *quoteend; parselist(s, start, end, quotestart, quoteend); n++)
+    {
+        char *val = newstring(start, end-start);
+        setiter(*id, val, stack);
+
+        if(executebool(body))
+        {
+            if(r.length()) r.add(' ');
+            r.put(quotestart, quoteend-quotestart);
+        }
+    }
+    if(n) poparg(*id);
+    r.add('\0');
+    commandret->setstr(newstring(r.getbuf(), r.length()-1));
+}
+COMMAND(listfilter, "rse");
+
+void prettylist(const char *s, const char *conj)
+{
+    vector<char> p;
+    const char *start, *end;
+    for(int len = listlen(s), n = 0; parselist(s, start, end); n++)
+    {
+        p.put(start, end - start);
+        if(n+1 < len)
+        {
+            if(len > 2 || !conj[0]) p.add(',');
+            if(n+2 == len && conj[0])
+            {
+                p.add(' ');
+                p.put(conj, strlen(conj));
+            }
+            p.add(' ');
+        }
+    } 
+    p.add('\0');
+    result(p.getbuf());
+}
+COMMAND(prettylist, "ss");
+
+int listincludes(const char *list, const char *needle, int needlelen)
+{
+    int offset = 0;
+    for(const char *s = list, *start, *end; parselist(s, start, end);)
+    {
+        int len = end - start;
+        if(needlelen == len && !strncmp(needle, start, len)) return offset;
+        offset++;
+    }
+    return -1;
+}
+ICOMMAND(indexof, "ss", (char *list, char *elem), intret(listincludes(list, elem, strlen(elem))));
+    
+char *listdel(const char *s, const char *del)
+{
+    vector<char> p;
+    for(const char *start, *end, *qstart, *qend; parselist(s, start, end, qstart, qend);)
+    {
+        if(listincludes(del, start, end-start) < 0)
+        {
+            if(!p.empty()) p.add(' ');
+            p.put(qstart, qend-qstart);
+        }
+    }
+    p.add('\0');
+    return newstring(p.getbuf(), p.length()-1);
+}
+ICOMMAND(listdel, "ss", (char *list, char *del), commandret->setstr(listdel(list, del)));
+
+void listsplice(const char *s, const char *vals, int *skip, int *count)
+{
+    int offset = max(*skip, 0), len = max(*count, 0);
+    const char *list = s, *start, *end, *qstart, *qend = s;
+    loopi(offset) if(!parselist(s, start, end, qstart, qend)) break;
+    vector<char> p;
+    if(qend > list) p.put(list, qend-list);
+    if(*vals)
+    {
+        if(!p.empty()) p.add(' ');
+        p.put(vals, strlen(vals));
+    }
+    loopi(len) if(!parselist(s)) break;
+    skiplist(s);
+    switch(*s)
+    {
+        case '\0': case ')': case ']': break;
+        default:
+            if(!p.empty()) p.add(' ');
+            p.put(s, strlen(s));
+            break;
+    }
+    p.add('\0');
+    commandret->setstr(newstring(p.getbuf(), p.length()-1));
+}
+COMMAND(listsplice, "ssii");
+
+ICOMMAND(loopfiles, "rsse", (ident *id, char *dir, char *ext, uint *body),
+{
+    if(id->type!=ID_ALIAS) return;
+    identstack stack;
+    vector<char *> files;
+    listfiles(dir, ext[0] ? ext : NULL, files);
+    loopvrev(files)
+    {
+        char *file = files[i];
+        bool redundant = false;
+        loopj(i) if(!strcmp(files[j], file)) { redundant = true; break; }
+        if(redundant) delete[] files.removeunordered(i);
+    } 
+    loopv(files)
+    {
+        char *file = files[i];
+        if(i) 
+        {
+            if(id->valtype == VAL_STR) delete[] id->val.s;
+            else id->valtype = VAL_STR;
+            id->val.s = file;
+        }
+        else 
+        {
+            tagval t;
+            t.setstr(file);
+            pusharg(*id, t, stack);
+            id->flags &= ~IDF_UNKNOWN;
+        }
+        execute(body);
+    }
+    if(files.length()) poparg(*id);
+});
+
+void findfile_(char *name)
+{ 
+    string fname;
+    copystring(fname, name);
+    path(fname);
+    intret(
+#ifndef STANDALONE
+        findzipfile(fname) ||
+#endif
+        fileexists(fname, "e") || findfile(fname, "e") ? 1 : 0
+    );
+}
+COMMANDN(findfile, findfile_, "s");
+
+struct sortitem
+{
+    const char *str, *quotestart, *quoteend;
+};
+   
+struct sortfun
+{
+    ident *x, *y;
+    uint *body;
+
+    bool operator()(const sortitem &xval, const sortitem &yval)
+    {
+        if(x->valtype != VAL_MACRO) x->valtype = VAL_MACRO;
+        cleancode(*x);
+        x->val.code = (const uint *)xval.str;
+        if(y->valtype != VAL_MACRO) y->valtype = VAL_MACRO;
+        cleancode(*y);
+        y->val.code = (const uint *)yval.str;
+        return executebool(body);
+    }
+};
+     
+void sortlist(char *list, ident *x, ident *y, uint *body)
+{
+    if(x == y || x->type != ID_ALIAS || y->type != ID_ALIAS) return;
+
+    vector<sortitem> items;
+    int macrolen = strlen(list), total = 0;
+    char *macros = newstring(list, macrolen);
+    const char *curlist = list, *start, *end, *quotestart, *quoteend;
+    while(parselist(curlist, start, end, quotestart, quoteend))
+    {
+        macros[end - list] = '\0';
+        sortitem item = { &macros[start - list], quotestart, quoteend };
+        items.add(item);
+        total += int(quoteend - quotestart);
+    } 
+
+    identstack xstack, ystack;
+    pusharg(*x, nullval, xstack); x->flags &= ~IDF_UNKNOWN;
+    pusharg(*y, nullval, ystack); y->flags &= ~IDF_UNKNOWN;
+
+    sortfun f = { x, y, body };
+    items.sort(f);
+
+    poparg(*x);
+    poparg(*y);
+    
+    char *sorted = macros;
+    int sortedlen = total + max(items.length() - 1, 0);
+    if(macrolen < sortedlen)
+    {
+        delete[] macros;
+        sorted = newstring(sortedlen);
+    }
+
+    int offset = 0;
+    loopv(items)
+    {
+        sortitem &item = items[i];
+        int len = int(item.quoteend - item.quotestart);
+        if(i) sorted[offset++] = ' ';
+        memcpy(&sorted[offset], item.quotestart, len);
+        offset += len;
+    }
+    sorted[offset] = '\0';
+    commandret->setstr(sorted);
+}
+COMMAND(sortlist, "srre");
+
+ICOMMAND(+, "ii", (int *a, int *b), intret(*a + *b));
+ICOMMAND(*, "ii", (int *a, int *b), intret(*a * *b));
+ICOMMAND(-, "ii", (int *a, int *b), intret(*a - *b));
+ICOMMAND(+f, "ff", (float *a, float *b), floatret(*a + *b));
+ICOMMAND(*f, "ff", (float *a, float *b), floatret(*a * *b));
+ICOMMAND(-f, "ff", (float *a, float *b), floatret(*a - *b));
+ICOMMAND(=, "ii", (int *a, int *b), intret((int)(*a == *b)));
+ICOMMAND(!=, "ii", (int *a, int *b), intret((int)(*a != *b)));
+ICOMMAND(<, "ii", (int *a, int *b), intret((int)(*a < *b)));
+ICOMMAND(>, "ii", (int *a, int *b), intret((int)(*a > *b)));
+ICOMMAND(<=, "ii", (int *a, int *b), intret((int)(*a <= *b)));
+ICOMMAND(>=, "ii", (int *a, int *b), intret((int)(*a >= *b)));
+ICOMMAND(=f, "ff", (float *a, float *b), intret((int)(*a == *b)));
+ICOMMAND(!=f, "ff", (float *a, float *b), intret((int)(*a != *b)));
+ICOMMAND(<f, "ff", (float *a, float *b), intret((int)(*a < *b)));
+ICOMMAND(>f, "ff", (float *a, float *b), intret((int)(*a > *b)));
+ICOMMAND(<=f, "ff", (float *a, float *b), intret((int)(*a <= *b)));
+ICOMMAND(>=f, "ff", (float *a, float *b), intret((int)(*a >= *b)));
+ICOMMAND(^, "ii", (int *a, int *b), intret(*a ^ *b));
+ICOMMAND(!, "t", (tagval *a), intret(!getbool(*a)));
+ICOMMAND(&, "ii", (int *a, int *b), intret(*a & *b));
+ICOMMAND(|, "ii", (int *a, int *b), intret(*a | *b));
+ICOMMAND(~, "i", (int *a), intret(~*a));
+ICOMMAND(^~, "ii", (int *a, int *b), intret(*a ^ ~*b));
+ICOMMAND(&~, "ii", (int *a, int *b), intret(*a & ~*b));
+ICOMMAND(|~, "ii", (int *a, int *b), intret(*a | ~*b));
+ICOMMAND(<<, "ii", (int *a, int *b), intret(*b < 32 ? *a << max(*b, 0) : 0));
+ICOMMAND(>>, "ii", (int *a, int *b), intret(*a >> clamp(*b, 0, 31)));
+ICOMMAND(&&, "e1V", (tagval *args, int numargs),
+{
+    if(!numargs) intret(1);
+    else loopi(numargs) 
+    {   
+        if(i) freearg(*commandret);
+        executeret(args[i].code, *commandret);
+        if(!getbool(*commandret)) break;
+    }
+});
+ICOMMAND(||, "e1V", (tagval *args, int numargs),
+{
+    if(!numargs) intret(0);
+    else loopi(numargs)
+    { 
+        if(i) freearg(*commandret);
+        executeret(args[i].code, *commandret);
+        if(getbool(*commandret)) break;
+    }
+});
+
+ICOMMAND(div, "ii", (int *a, int *b), intret(*b ? *a / *b : 0));
+ICOMMAND(mod, "ii", (int *a, int *b), intret(*b ? *a % *b : 0));
+ICOMMAND(divf, "ff", (float *a, float *b), floatret(*b ? *a / *b : 0));
+ICOMMAND(modf, "ff", (float *a, float *b), floatret(*b ? fmod(*a, *b) : 0));
+ICOMMAND(sin, "f", (float *a), floatret(sin(*a*RAD)));
+ICOMMAND(cos, "f", (float *a), floatret(cos(*a*RAD)));
+ICOMMAND(tan, "f", (float *a), floatret(tan(*a*RAD)));
+ICOMMAND(asin, "f", (float *a), floatret(asin(*a)/RAD));
+ICOMMAND(acos, "f", (float *a), floatret(acos(*a)/RAD));
+ICOMMAND(atan, "f", (float *a), floatret(atan(*a)/RAD));
+ICOMMAND(atan2, "ff", (float *y, float *x), floatret(atan2(*y, *x)/RAD));
+ICOMMAND(sqrt, "f", (float *a), floatret(sqrt(*a)));
+ICOMMAND(pow, "ff", (float *a, float *b), floatret(pow(*a, *b)));
+ICOMMAND(loge, "f", (float *a), floatret(log(*a)));
+ICOMMAND(log2, "f", (float *a), floatret(log(*a)/M_LN2));
+ICOMMAND(log10, "f", (float *a), floatret(log10(*a)));
+ICOMMAND(exp, "f", (float *a), floatret(exp(*a)));
+ICOMMAND(min, "V", (tagval *args, int numargs),
+{
+    int val = numargs > 0 ? args[numargs - 1].getint() : 0;
+    loopi(numargs - 1) val = min(val, args[i].getint());
+    intret(val);
+});
+ICOMMAND(max, "V", (tagval *args, int numargs),
+{
+    int val = numargs > 0 ? args[numargs - 1].getint() : 0;
+    loopi(numargs - 1) val = max(val, args[i].getint());
+    intret(val);
+});
+ICOMMAND(minf, "V", (tagval *args, int numargs),
+{
+    float val = numargs > 0 ? args[numargs - 1].getfloat() : 0.0f;
+    loopi(numargs - 1) val = min(val, args[i].getfloat());
+    floatret(val);
+});
+ICOMMAND(maxf, "V", (tagval *args, int numargs),
+{
+    float val = numargs > 0 ? args[numargs - 1].getfloat() : 0.0f;
+    loopi(numargs - 1) val = max(val, args[i].getfloat());
+    floatret(val);
+});
+ICOMMAND(abs, "i", (int *n), intret(abs(*n)));
+ICOMMAND(absf, "f", (float *n), floatret(fabs(*n)));
+
+ICOMMAND(floor, "f", (float *n), floatret(floor(*n)));
+ICOMMAND(ceil, "f", (float *n), floatret(ceil(*n)));
+ICOMMAND(round, "ff", (float *n, float *k),
+{
+    double step = *k;
+    double r = *n;
+    if(step > 0)
+    {
+        r += step * (r < 0 ? -0.5 : 0.5);
+        r -= fmod(r, step);
+    }
+    else r = r < 0 ? ceil(r - 0.5) : floor(r + 0.5);
+    floatret(float(r));
+});
+
+ICOMMAND(cond, "ee2V", (tagval *args, int numargs),
+{
+    for(int i = 0; i < numargs; i += 2)
+    {
+        if(i+1 < numargs)
+        {
+            if(executebool(args[i].code))
+            {
+                executeret(args[i+1].code, *commandret);
+                break;
+            }
+        }
+        else
+        {
+            executeret(args[i].code, *commandret);
+            break;
+        }
+    }
+});
+#define CASECOMMAND(name, fmt, type, acc, compare) \
+    ICOMMAND(name, fmt "te2V", (tagval *args, int numargs), \
+    { \
+        type val = acc; \
+        int i; \
+        for(i = 1; i+1 < numargs; i += 2) \
+        { \
+            if(compare) \
+            { \
+                executeret(args[i+1].code, *commandret); \
+                return; \
+            } \
+        } \
+    })
+CASECOMMAND(case, "i", int, args[0].getint(), args[i].type == VAL_NULL || args[i].getint() == val);
+CASECOMMAND(casef, "f", float, args[0].getfloat(), args[i].type == VAL_NULL || args[i].getfloat() == val);
+CASECOMMAND(cases, "s", const char *, args[0].getstr(), args[i].type == VAL_NULL || !strcmp(args[i].getstr(), val));
+
+ICOMMAND(rnd, "ii", (int *a, int *b), intret(*a - *b > 0 ? rnd(*a - *b) + *b : *b));
+ICOMMAND(rndstr, "i", (int *len),
+{
+    int n = clamp(*len, 0, 10000);
+    char *s = newstring(n);
+    for(int i = 0; i < n;)
+    {
+        uint r = randomMT();
+        for(int j = min(i + 4, n); i < j; i++)
+        {
+            s[i] = (r%255) + 1;
+            r /= 255;
+        }
+    }
+    s[n] = '\0';
+    stringret(s);
+});
+
+ICOMMAND(strcmp, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
+ICOMMAND(=s, "ss", (char *a, char *b), intret(strcmp(a,b)==0));
+ICOMMAND(!=s, "ss", (char *a, char *b), intret(strcmp(a,b)!=0));
+ICOMMAND(<s, "ss", (char *a, char *b), intret(strcmp(a,b)<0));
+ICOMMAND(>s, "ss", (char *a, char *b), intret(strcmp(a,b)>0));
+ICOMMAND(<=s, "ss", (char *a, char *b), intret(strcmp(a,b)<=0));
+ICOMMAND(>=s, "ss", (char *a, char *b), intret(strcmp(a,b)>=0));
+ICOMMAND(echo, "C", (char *s), conoutf(CON_ECHO, "\f1%s", s));
+ICOMMAND(error, "C", (char *s), conoutf(CON_ERROR, "%s", s));
+ICOMMAND(strstr, "ss", (char *a, char *b), { char *s = strstr(a, b); intret(s ? s-a : -1); });
+ICOMMAND(strlen, "s", (char *s), intret(strlen(s)));
+ICOMMAND(strcode, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : uchar(s[*i])) : uchar(s[0])));
+ICOMMAND(codestr, "i", (int *i), { char *s = newstring(1); s[0] = char(*i); s[1] = '\0'; stringret(s); });
+ICOMMAND(struni, "si", (char *s, int *i), intret(*i > 0 ? (memchr(s, 0, *i) ? 0 : cube2uni(s[*i])) : cube2uni(s[0])));
+ICOMMAND(unistr, "i", (int *i), { char *s = newstring(1); s[0] = uni2cube(*i); s[1] = '\0'; stringret(s); }); 
+
+int naturalsort(const char *a, const char *b)
+{
+    for(;;)
+    {
+        int ac = *a, bc = *b;
+        if(!ac) return bc ? -1 : 0;
+        else if(!bc) return 1;
+        else if(isdigit(ac) && isdigit(bc))
+        {
+            while(*a == '0') a++;
+            while(*b == '0') b++;
+            const char *a0 = a, *b0 = b;
+            while(isdigit(*a)) a++;
+            while(isdigit(*b)) b++;
+            int alen = a - a0, blen = b - b0;
+            if(alen != blen) return alen - blen;
+            int n = memcmp(a0, b0, alen);
+            if(n < 0) return -1;
+            else if(n > 0) return 1;
+        }
+        else if(ac != bc) return ac - bc;
+        else { ++a; ++b; }
+    }
+}
+ICOMMAND(naturalsort, "ss", (char *a, char *b), intret(naturalsort(a,b)<=0));
+
+#define STRMAPCOMMAND(name, map) \
+    ICOMMAND(name, "s", (char *s), \
+    { \
+        int len = strlen(s); \
+        char *m = newstring(len); \
+        loopi(len) m[i] = map(s[i]); \
+        m[len] = '\0'; \
+        stringret(m); \
+    })
+
+STRMAPCOMMAND(strlower, cubelower);
+STRMAPCOMMAND(strupper, cubeupper);
+
+char *strreplace(const char *s, const char *oldval, const char *newval)
+{
+    vector<char> buf;
+
+    int oldlen = strlen(oldval);
+    if(!oldlen) return newstring(s);
+    for(;;)
+    {
+        const char *found = strstr(s, oldval);
+        if(found)
+        {
+            while(s < found) buf.add(*s++);
+            for(const char *n = newval; *n; n++) buf.add(*n);
+            s = found + oldlen;
+        }
+        else
+        {
+            while(*s) buf.add(*s++);
+            buf.add('\0');
+            return newstring(buf.getbuf(), buf.length());
+        }
+    }
+}
+
+ICOMMAND(strreplace, "sss", (char *s, char *o, char *n), commandret->setstr(strreplace(s, o, n)));
+
+void strsplice(const char *s, const char *vals, int *skip, int *count)
+{
+    int slen = strlen(s), vlen = strlen(vals),
+        offset = clamp(*skip, 0, slen),
+        len = clamp(*count, 0, slen - offset);
+    char *p = newstring(slen - len + vlen);
+    if(offset) memcpy(p, s, offset);
+    if(vlen) memcpy(&p[offset], vals, vlen);
+    if(offset + len < slen) memcpy(&p[offset + vlen], &s[offset + len], slen - (offset + len));
+    p[slen - len + vlen] = '\0';
+    commandret->setstr(p);
+}
+COMMAND(strsplice, "ssii");
+
+#ifndef STANDALONE
+ICOMMAND(getmillis, "i", (int *total), intret(*total ? totalmillis : lastmillis));
+
+struct sleepcmd
+{
+    int delay, millis, flags;
+    char *command;
+};
+vector<sleepcmd> sleepcmds;
+
+void addsleep(int *msec, char *cmd)
+{
+    sleepcmd &s = sleepcmds.add();
+    s.delay = max(*msec, 1);
+    s.millis = lastmillis;
+    s.command = newstring(cmd);
+    s.flags = identflags;
+}
+
+COMMANDN(sleep, addsleep, "is");
+
+void checksleep(int millis)
+{
+    loopv(sleepcmds)
+    {
+        sleepcmd &s = sleepcmds[i];
+        if(millis - s.millis >= s.delay)
+        {
+            char *cmd = s.command; // execute might create more sleep commands
+            s.command = NULL;
+            int oldflags = identflags;
+            identflags = s.flags;
+            execute(cmd);
+            identflags = oldflags;
+            delete[] cmd;
+            if(sleepcmds.inrange(i) && !sleepcmds[i].command) sleepcmds.remove(i--);
+        }
+    }
+}
+
+void clearsleep(bool clearoverrides)
+{
+    int len = 0;
+    loopv(sleepcmds) if(sleepcmds[i].command)
+    {
+        if(clearoverrides && !(sleepcmds[i].flags&IDF_OVERRIDDEN)) sleepcmds[len++] = sleepcmds[i];
+        else delete[] sleepcmds[i].command;
+    }
+    sleepcmds.shrink(len);
+}
+
+void clearsleep_(int *clearoverrides)
+{
+    clearsleep(*clearoverrides!=0 || identflags&IDF_OVERRIDDEN);
+}
+
+COMMANDN(clearsleep, clearsleep_, "i");
+#endif
+
diff --git a/src/engine/console.cpp b/src/engine/console.cpp
new file mode 100644 (file)
index 0000000..d6e83a1
--- /dev/null
@@ -0,0 +1,785 @@
+// console.cpp: the console buffer, its display, and command line control
+
+#include "engine.h"
+
+#define MAXCONLINES 1000
+struct cline { char *line; int type, outtime; };
+reversequeue<cline, MAXCONLINES> conlines;
+
+int commandmillis = -1;
+string commandbuf;
+char *commandaction = NULL, *commandprompt = NULL;
+enum { CF_COMPLETE = 1<<0, CF_EXECUTE = 1<<1 };
+int commandflags = 0, commandpos = -1;
+
+VARFP(maxcon, 10, 200, MAXCONLINES, { while(conlines.length() > maxcon) delete[] conlines.pop().line; });
+
+#define CONSTRLEN 512
+
+VARP(contags, 0, 3, 3);
+
+void conline(int type, const char *sf)        // add a line to the console buffer
+{
+    char *buf = NULL;
+    if(type&CON_TAG_MASK) for(int i = conlines.length()-1; i >= max(conlines.length()-contags, 0); i--)
+    {
+        int prev = conlines.removing(i).type;
+        if(!(prev&CON_TAG_MASK)) break;
+        if(type == prev)
+        {
+            buf = conlines.remove(i).line;
+            break;
+        }
+    }
+    if(!buf) buf = conlines.length() >= maxcon ? conlines.remove().line : newstring("", CONSTRLEN-1);
+    cline &cl = conlines.add();
+    cl.line = buf;
+    cl.type = type;
+    cl.outtime = totalmillis;                // for how long to keep line on screen
+    copystring(cl.line, sf, CONSTRLEN);
+}
+
+void conoutfv(int type, const char *fmt, va_list args)
+{
+    static char buf[CONSTRLEN];
+    vformatstring(buf, fmt, args, sizeof(buf));
+    conline(type, buf);
+    logoutf("%s", buf);
+}
+
+VAR(fullconsole, 0, 0, 1);
+ICOMMAND(toggleconsole, "", (), { fullconsole ^= 1; });
+
+int rendercommand(int x, int y, int w)
+{
+    if(commandmillis < 0) return 0;
+
+    defformatstring(s, "%s %s", commandprompt ? commandprompt : ">", commandbuf);
+    int width, height;
+    text_bounds(s, width, height, w);
+    y -= height;
+    draw_text(s, x, y, 0xFF, 0xFF, 0xFF, 0xFF, (commandpos>=0) ? (commandpos+1+(commandprompt?strlen(commandprompt):1)) : strlen(s), w);
+    return height;
+}
+
+VARP(consize, 0, 5, 100);
+VARP(miniconsize, 0, 5, 100);
+VARP(miniconwidth, 0, 40, 100);
+VARP(confade, 0, 30, 60);
+VARP(miniconfade, 0, 30, 60);
+VARP(fullconsize, 0, 75, 100);
+HVARP(confilter, 0, 0x7FFFFFF, 0x7FFFFFF);
+HVARP(fullconfilter, 0, 0x7FFFFFF, 0x7FFFFFF);
+HVARP(miniconfilter, 0, 0, 0x7FFFFFF);
+
+int conskip = 0, miniconskip = 0;
+
+void setconskip(int &skip, int filter, int n)
+{
+    filter &= CON_FLAGS;
+    int offset = abs(n), dir = n < 0 ? -1 : 1;
+    skip = clamp(skip, 0, conlines.length()-1);
+    while(offset)
+    {
+        skip += dir;
+        if(!conlines.inrange(skip))
+        {
+            skip = clamp(skip, 0, conlines.length()-1);
+            return;
+        }
+        if(conlines[skip].type&filter) --offset;
+    }
+}
+
+ICOMMAND(conskip, "i", (int *n), setconskip(conskip, fullconsole ? fullconfilter : confilter, *n));
+ICOMMAND(miniconskip, "i", (int *n), setconskip(miniconskip, miniconfilter, *n));
+
+ICOMMAND(clearconsole, "", (), { while(conlines.length()) delete[] conlines.pop().line; });
+
+int drawconlines(int conskip, int confade, int conwidth, int conheight, int conoff, int filter, int y = 0, int dir = 1)
+{
+    filter &= CON_FLAGS;
+    int numl = conlines.length(), offset = min(conskip, numl);
+
+    if(confade)
+    {
+        if(!conskip)
+        {
+            numl = 0;
+            loopvrev(conlines) if(totalmillis-conlines[i].outtime < confade*1000) { numl = i+1; break; }
+        }
+        else offset--;
+    }
+
+    int totalheight = 0;
+    loopi(numl) //determine visible height
+    {
+        // shuffle backwards to fill if necessary
+        int idx = offset+i < numl ? offset+i : --offset;
+        if(!(conlines[idx].type&filter)) continue;
+        char *line = conlines[idx].line;
+        int width, height;
+        text_bounds(line, width, height, conwidth);
+        if(totalheight + height > conheight) { numl = i; if(offset == idx) ++offset; break; }
+        totalheight += height;
+    }
+    if(dir > 0) y = conoff;
+    loopi(numl)
+    {
+        int idx = offset + (dir > 0 ? numl-i-1 : i);
+        if(!(conlines[idx].type&filter)) continue;
+        char *line = conlines[idx].line;
+        int width, height;
+        text_bounds(line, width, height, conwidth);
+        if(dir <= 0) y -= height; 
+        draw_text(line, conoff, y, 0xFF, 0xFF, 0xFF, 0xFF, -1, conwidth);
+        if(dir > 0) y += height;
+    }
+    return y+conoff;
+}
+
+int renderconsole(int w, int h, int abovehud)                   // render buffer taking into account time & scrolling
+{
+    int conpad = fullconsole ? 0 : FONTH/4,
+        conoff = fullconsole ? FONTH : FONTH/3,
+        conheight = min(fullconsole ? ((h*fullconsize/100)/FONTH)*FONTH : FONTH*consize, h - 2*(conpad + conoff)),
+        conwidth = w - 2*(conpad + conoff) - (fullconsole ? 0 : game::clipconsole(w, h));
+    
+    extern void consolebox(int x1, int y1, int x2, int y2);
+    if(fullconsole) consolebox(conpad, conpad, conwidth+conpad+2*conoff, conheight+conpad+2*conoff);
+    
+    int y = drawconlines(conskip, fullconsole ? 0 : confade, conwidth, conheight, conpad+conoff, fullconsole ? fullconfilter : confilter);
+    if(!fullconsole && (miniconsize && miniconwidth))
+        drawconlines(miniconskip, miniconfade, (miniconwidth*(w - 2*(conpad + conoff)))/100, min(FONTH*miniconsize, abovehud - y), conpad+conoff, miniconfilter, abovehud, -1);
+    return fullconsole ? conheight + 2*(conpad + conoff) : y;
+}
+
+// keymap is defined externally in keymap.cfg
+
+struct keym
+{
+    enum
+    {
+        ACTION_DEFAULT = 0,
+        ACTION_SPECTATOR,
+        ACTION_EDITING,
+        NUMACTIONS
+    };
+    
+    int code;
+    char *name;
+    char *actions[NUMACTIONS];
+    bool pressed;
+
+    keym() : code(-1), name(NULL), pressed(false) { loopi(NUMACTIONS) actions[i] = newstring(""); }
+    ~keym() { DELETEA(name); loopi(NUMACTIONS) DELETEA(actions[i]); }
+};
+
+hashtable<int, keym> keyms(128);
+
+void keymap(int *code, char *key)
+{
+    if(identflags&IDF_OVERRIDDEN) { conoutf(CON_ERROR, "cannot override keymap %d", *code); return; }
+    keym &km = keyms[*code];
+    km.code = *code;
+    DELETEA(km.name);
+    km.name = newstring(key);
+}
+    
+COMMAND(keymap, "is");
+
+keym *keypressed = NULL;
+char *keyaction = NULL;
+
+const char *getkeyname(int code)
+{
+    keym *km = keyms.access(code);
+    return km ? km->name : NULL;
+}
+
+void searchbinds(char *action, int type)
+{
+    vector<char> names;
+    enumerate(keyms, keym, km,
+    {
+        if(!strcmp(km.actions[type], action))
+        {
+            if(names.length()) names.add(' ');
+            names.put(km.name, strlen(km.name));
+        }
+    });
+    names.add('\0');
+    result(names.getbuf());
+}
+
+keym *findbind(char *key)
+{
+    enumerate(keyms, keym, km,
+    {
+        if(!strcasecmp(km.name, key)) return &km;
+    });
+    return NULL;
+}   
+    
+void getbind(char *key, int type)
+{
+    keym *km = findbind(key);
+    result(km ? km->actions[type] : "");
+}   
+
+void bindkey(char *key, char *action, int state, const char *cmd)
+{
+    if(identflags&IDF_OVERRIDDEN) { conoutf(CON_ERROR, "cannot override %s \"%s\"", cmd, key); return; }
+    keym *km = findbind(key);
+    if(!km) { conoutf(CON_ERROR, "unknown key \"%s\"", key); return; }
+    char *&binding = km->actions[state];
+    if(!keypressed || keyaction!=binding) delete[] binding;
+    // trim white-space to make searchbinds more reliable
+    while(iscubespace(*action)) action++;
+    int len = strlen(action);
+    while(len>0 && iscubespace(action[len-1])) len--;
+    binding = newstring(action, len);
+}
+
+ICOMMAND(bind,     "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_DEFAULT, "bind"));
+ICOMMAND(specbind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_SPECTATOR, "specbind"));
+ICOMMAND(editbind, "ss", (char *key, char *action), bindkey(key, action, keym::ACTION_EDITING, "editbind"));
+ICOMMAND(getbind,     "s", (char *key), getbind(key, keym::ACTION_DEFAULT));
+ICOMMAND(getspecbind, "s", (char *key), getbind(key, keym::ACTION_SPECTATOR));
+ICOMMAND(geteditbind, "s", (char *key), getbind(key, keym::ACTION_EDITING));
+ICOMMAND(searchbinds,     "s", (char *action), searchbinds(action, keym::ACTION_DEFAULT));
+ICOMMAND(searchspecbinds, "s", (char *action), searchbinds(action, keym::ACTION_SPECTATOR));
+ICOMMAND(searcheditbinds, "s", (char *action), searchbinds(action, keym::ACTION_EDITING));
+
+void inputcommand(char *init, char *action = NULL, char *prompt = NULL, char *flags = NULL) // turns input to the command line on or off
+{
+    commandmillis = init ? totalmillis : -1;
+    textinput(commandmillis >= 0, TI_CONSOLE);
+    keyrepeat(commandmillis >= 0, KR_CONSOLE);
+    copystring(commandbuf, init ? init : "");
+    DELETEA(commandaction);
+    DELETEA(commandprompt);
+    commandpos = -1;
+    if(action && action[0]) commandaction = newstring(action);
+    if(prompt && prompt[0]) commandprompt = newstring(prompt);
+    commandflags = 0;
+    if(flags) while(*flags) switch(*flags++)
+    {
+        case 'c': commandflags |= CF_COMPLETE; break;
+        case 'x': commandflags |= CF_EXECUTE; break;
+        case 's': commandflags |= CF_COMPLETE|CF_EXECUTE; break;
+    }
+    else if(init) commandflags |= CF_COMPLETE|CF_EXECUTE;
+}
+
+ICOMMAND(saycommand, "C", (char *init), inputcommand(init));
+COMMAND(inputcommand, "ssss");
+
+void pasteconsole()
+{
+    if(!SDL_HasClipboardText()) return;
+    char *cb = SDL_GetClipboardText();
+    if(!cb) return;
+    size_t cblen = strlen(cb),
+           commandlen = strlen(commandbuf),
+           decoded = decodeutf8((uchar *)&commandbuf[commandlen], sizeof(commandbuf)-1-commandlen, (const uchar *)cb, cblen);
+    commandbuf[commandlen + decoded] = '\0';
+    SDL_free(cb);
+}
+
+struct hline
+{
+    char *buf, *action, *prompt;
+    int flags;
+
+    hline() : buf(NULL), action(NULL), prompt(NULL), flags(0) {}
+    ~hline()
+    {
+        DELETEA(buf);
+        DELETEA(action);
+        DELETEA(prompt);
+    }
+
+    void restore()
+    {
+        copystring(commandbuf, buf);
+        if(commandpos >= (int)strlen(commandbuf)) commandpos = -1;
+        DELETEA(commandaction);
+        DELETEA(commandprompt);
+        if(action) commandaction = newstring(action);
+        if(prompt) commandprompt = newstring(prompt);
+        commandflags = flags;
+    }
+
+    bool shouldsave()
+    {
+        return strcmp(commandbuf, buf) ||
+               (commandaction ? !action || strcmp(commandaction, action) : action!=NULL) ||
+               (commandprompt ? !prompt || strcmp(commandprompt, prompt) : prompt!=NULL) ||
+               commandflags != flags;
+    }
+    
+    void save()
+    {
+        buf = newstring(commandbuf);
+        if(commandaction) action = newstring(commandaction);
+        if(commandprompt) prompt = newstring(commandprompt);
+        flags = commandflags;
+    }
+
+    void run()
+    {
+        if(flags&CF_EXECUTE && buf[0]=='/') execute(buf+1);
+        else if(action)
+        {
+            alias("commandbuf", buf);
+            execute(action);
+        }
+        else game::toserver(buf);
+    }
+};
+vector<hline *> history;
+int histpos = 0;
+
+VARP(maxhistory, 0, 1000, 10000);
+
+void history_(int *n)
+{
+    static bool inhistory = false;
+    if(!inhistory && history.inrange(*n))
+    {
+        inhistory = true;
+        history[history.length()-*n-1]->run();
+        inhistory = false;
+    }
+}
+
+COMMANDN(history, history_, "i");
+
+struct releaseaction
+{
+    keym *key;
+    char *action;
+};
+vector<releaseaction> releaseactions;
+
+const char *addreleaseaction(char *s)
+{
+    if(!keypressed) { delete[] s; return NULL; }
+    releaseaction &ra = releaseactions.add();
+    ra.key = keypressed;
+    ra.action = s;
+    return keypressed->name;
+}
+
+void onrelease(const char *s)
+{
+    addreleaseaction(newstring(s));
+}
+
+COMMAND(onrelease, "s");
+
+void execbind(keym &k, bool isdown)
+{
+    loopv(releaseactions)
+    {
+        releaseaction &ra = releaseactions[i];
+        if(ra.key==&k)
+        {
+            if(!isdown) execute(ra.action);
+            delete[] ra.action;
+            releaseactions.remove(i--);
+        }
+    }
+    if(isdown)
+    {
+        int state = keym::ACTION_DEFAULT;
+        if(!mainmenu)
+        {
+            if(editmode) state = keym::ACTION_EDITING;
+            else if(player->state==CS_SPECTATOR) state = keym::ACTION_SPECTATOR;
+        }
+        char *&action = k.actions[state][0] ? k.actions[state] : k.actions[keym::ACTION_DEFAULT];
+        keyaction = action;
+        keypressed = &k;
+        execute(keyaction);
+        keypressed = NULL;
+        if(keyaction!=action) delete[] keyaction;
+    }
+    k.pressed = isdown;
+}
+
+bool consoleinput(const char *str, int len)
+{
+    if(commandmillis < 0) return false;
+
+    resetcomplete();
+    int cmdlen = (int)strlen(commandbuf), cmdspace = int(sizeof(commandbuf)) - (cmdlen+1);
+    len = min(len, cmdspace);
+    if(commandpos<0)
+    {
+        memcpy(&commandbuf[cmdlen], str, len);
+    }
+    else
+    {
+        memmove(&commandbuf[commandpos+len], &commandbuf[commandpos], cmdlen - commandpos);
+        memcpy(&commandbuf[commandpos], str, len);
+        commandpos += len;
+    }
+    commandbuf[cmdlen + len] = '\0';
+
+    return true;
+}
+
+bool consolekey(int code, bool isdown)
+{
+    if(commandmillis < 0) return false;
+
+    #ifdef __APPLE__
+        #define MOD_KEYS (KMOD_LGUI|KMOD_RGUI)
+    #else
+        #define MOD_KEYS (KMOD_LCTRL|KMOD_RCTRL)
+    #endif
+
+    if(isdown)
+    {
+        switch(code)
+        {
+            case SDLK_RETURN:
+            case SDLK_KP_ENTER:
+                break;
+
+            case SDLK_HOME:
+                if(strlen(commandbuf)) commandpos = 0;
+                break;
+
+            case SDLK_END:
+                commandpos = -1;
+                break;
+
+            case SDLK_DELETE:
+            {
+                int len = (int)strlen(commandbuf);
+                if(commandpos<0) break;
+                memmove(&commandbuf[commandpos], &commandbuf[commandpos+1], len - commandpos);
+                resetcomplete();
+                if(commandpos>=len-1) commandpos = -1;
+                break;
+            }
+
+            case SDLK_BACKSPACE:
+            {
+                int len = (int)strlen(commandbuf), i = commandpos>=0 ? commandpos : len;
+                if(i<1) break;
+                memmove(&commandbuf[i-1], &commandbuf[i], len - i + 1);
+                resetcomplete();
+                if(commandpos>0) commandpos--;
+                else if(!commandpos && len<=1) commandpos = -1;
+                break;
+            }
+
+            case SDLK_LEFT:
+                if(commandpos>0) commandpos--;
+                else if(commandpos<0) commandpos = (int)strlen(commandbuf)-1;
+                break;
+
+            case SDLK_RIGHT:
+                if(commandpos>=0 && ++commandpos>=(int)strlen(commandbuf)) commandpos = -1;
+                break;
+
+            case SDLK_UP:
+                if(histpos > history.length()) histpos = history.length();
+                if(histpos > 0) history[--histpos]->restore(); 
+                break;
+
+            case SDLK_DOWN:
+                if(histpos + 1 < history.length()) history[++histpos]->restore();
+                break;
+
+            case SDLK_TAB:
+                if(commandflags&CF_COMPLETE)
+                {
+                    complete(commandbuf, sizeof(commandbuf), commandflags&CF_EXECUTE ? "/" : NULL);
+                    if(commandpos>=0 && commandpos>=(int)strlen(commandbuf)) commandpos = -1;
+                }
+                break;
+
+            case SDLK_v:
+                if(SDL_GetModState()&MOD_KEYS) pasteconsole();
+                break;
+        }
+    }
+    else
+    {
+        if(code==SDLK_RETURN || code==SDLK_KP_ENTER)
+        {
+            hline *h = NULL;
+            if(commandbuf[0])
+            {
+                if(history.empty() || history.last()->shouldsave())
+                {
+                    if(maxhistory && history.length() >= maxhistory)
+                    {
+                        loopi(history.length()-maxhistory+1) delete history[i];
+                        history.remove(0, history.length()-maxhistory+1);
+                    }
+                    history.add(h = new hline)->save();
+                }
+                else h = history.last();
+            }
+            histpos = history.length();
+            inputcommand(NULL);
+            if(h) h->run();
+        }
+        else if(code==SDLK_ESCAPE)
+        {
+            histpos = history.length();
+            inputcommand(NULL);
+        }
+    }
+
+    return true;
+}
+
+void processtextinput(const char *str, int len)
+{
+    if(!g3d_input(str, len))
+        consoleinput(str, len);
+}
+
+void processkey(int code, bool isdown, int modstate)
+{
+    switch(code)
+    {
+        case SDLK_LGUI: case SDLK_RGUI:
+            return;
+    }
+    keym *haskey = keyms.access(code);
+    if(haskey && haskey->pressed) execbind(*haskey, isdown); // allow pressed keys to release
+    else if(!g3d_key(code, isdown)) // 3D GUI mouse button intercept   
+    {
+        if(!consolekey(code, isdown))
+        {
+            if(modstate&KMOD_GUI) return;
+            if(haskey) execbind(*haskey, isdown);
+        }
+    }
+}
+
+void clear_console()
+{
+    keyms.clear();
+}
+
+void writebinds(stream *f)
+{
+    static const char * const cmds[3] = { "bind", "specbind", "editbind" };
+    vector<keym *> binds;
+    enumerate(keyms, keym, km, binds.add(&km));
+    binds.sortname();
+    loopj(3)
+    {
+        loopv(binds)
+        {
+            keym &km = *binds[i];
+            if(*km.actions[j]) 
+            {
+                if(validateblock(km.actions[j])) f->printf("%s %s [%s]\n", cmds[j], escapestring(km.name), km.actions[j]);
+                else f->printf("%s %s %s\n", cmds[j], escapestring(km.name), escapestring(km.actions[j]));
+            }
+        }
+    }
+}
+
+// tab-completion of all idents and base maps
+
+enum { FILES_DIR = 0, FILES_VAR, FILES_LIST };
+
+struct fileskey
+{
+    int type;
+    const char *dir, *ext;
+
+    fileskey() {}
+    fileskey(int type, const char *dir, const char *ext) : type(type), dir(dir), ext(ext) {}
+};
+
+static void cleanfilesdir(char *dir)
+{
+    int dirlen = (int)strlen(dir);
+    while(dirlen > 0 && (dir[dirlen-1] == '/' || dir[dirlen-1] == '\\'))
+        dir[--dirlen] = '\0';
+}
+
+struct filesval
+{
+    int type;
+    char *dir, *ext;
+    vector<char *> files;
+    int millis;
+    
+    filesval(int type, const char *dir, const char *ext) : type(type), dir(newstring(dir)), ext(ext && ext[0] ? newstring(ext) : NULL), millis(-1) {}
+    ~filesval() { DELETEA(dir); DELETEA(ext); files.deletearrays(); }
+
+    void update()
+    {
+        if((type!=FILES_DIR && type!=FILES_VAR) || millis >= commandmillis) return;
+        files.deletearrays();        
+        if(type==FILES_VAR)
+        {
+            string buf;
+            buf[0] = '\0';
+            if(ident *id = readident(dir)) switch(id->type)
+            {
+                case ID_SVAR: copystring(buf, *id->storage.s); break;
+                case ID_ALIAS: copystring(buf, id->getstr()); break;
+            }
+            if(!buf[0]) copystring(buf, ".");
+            cleanfilesdir(buf);
+            listfiles(buf, ext, files);
+        }
+        else listfiles(dir, ext, files);
+        files.sort();
+        loopv(files) if(i && !strcmp(files[i], files[i-1])) delete[] files.remove(i--);
+        millis = totalmillis;
+    }
+};
+
+static inline bool htcmp(const fileskey &x, const fileskey &y)
+{
+    return x.type==y.type && !strcmp(x.dir, y.dir) && (x.ext == y.ext || (x.ext && y.ext && !strcmp(x.ext, y.ext)));
+}
+
+static inline uint hthash(const fileskey &k)
+{
+    return hthash(k.dir);
+}
+
+static hashtable<fileskey, filesval *> completefiles;
+static hashtable<char *, filesval *> completions;
+
+int completesize = 0;
+char *lastcomplete = NULL;
+
+void resetcomplete() { completesize = 0; }
+
+void addcomplete(char *command, int type, char *dir, char *ext)
+{
+    if(identflags&IDF_OVERRIDDEN)
+    {
+        conoutf(CON_ERROR, "cannot override complete %s", command);
+        return;
+    }
+    if(!dir[0])
+    {
+        filesval **hasfiles = completions.access(command);
+        if(hasfiles) *hasfiles = NULL;
+        return;
+    }
+    if(type==FILES_DIR) cleanfilesdir(dir);
+    if(ext)
+    {
+        if(strchr(ext, '*')) ext[0] = '\0';
+        if(!ext[0]) ext = NULL;
+    }
+    fileskey key(type, dir, ext);
+    filesval **val = completefiles.access(key);
+    if(!val)
+    {
+        filesval *f = new filesval(type, dir, ext);
+        if(type==FILES_LIST) explodelist(dir, f->files); 
+        val = &completefiles[fileskey(type, f->dir, f->ext)];
+        *val = f;
+    }
+    filesval **hasfiles = completions.access(command);
+    if(hasfiles) *hasfiles = *val;
+    else completions[newstring(command)] = *val;
+}
+
+void addfilecomplete(char *command, char *dir, char *ext)
+{
+    addcomplete(command, FILES_DIR, dir, ext);
+}
+
+void addvarcomplete(char *command, char *var, char *ext)
+{
+    addcomplete(command, FILES_VAR, var, ext);
+}
+
+void addlistcomplete(char *command, char *list)
+{
+    addcomplete(command, FILES_LIST, list, NULL);
+}
+
+COMMANDN(complete, addfilecomplete, "sss");
+COMMANDN(varcomplete, addvarcomplete, "sss");
+COMMANDN(listcomplete, addlistcomplete, "ss");
+
+void complete(char *s, int maxlen, const char *cmdprefix)
+{
+    int cmdlen = 0;
+    if(cmdprefix)
+    {
+        cmdlen = strlen(cmdprefix);
+        if(strncmp(s, cmdprefix, cmdlen)) prependstring(s, cmdprefix, maxlen);
+    }
+    if(!s[cmdlen]) return;
+    if(!completesize) { completesize = (int)strlen(&s[cmdlen]); DELETEA(lastcomplete); }
+
+    filesval *f = NULL;
+    if(completesize)
+    {
+        char *end = strchr(&s[cmdlen], ' ');
+        if(end) f = completions.find(stringslice(&s[cmdlen], end), NULL);
+    }
+
+    const char *nextcomplete = NULL;
+    if(f) // complete using filenames
+    {
+        int commandsize = strchr(&s[cmdlen], ' ')+1-s;
+        f->update();
+        loopv(f->files)
+        {
+            if(strncmp(f->files[i], &s[commandsize], completesize+cmdlen-commandsize)==0 &&
+               (!lastcomplete || strcmp(f->files[i], lastcomplete) > 0) && (!nextcomplete || strcmp(f->files[i], nextcomplete) < 0))
+                nextcomplete = f->files[i];
+        }
+        cmdprefix = s;
+        cmdlen = commandsize;
+    }
+    else // complete using command names
+    {
+        enumerate(idents, ident, id,
+            if(strncmp(id.name, &s[cmdlen], completesize)==0 &&
+               (!lastcomplete || strcmp(id.name, lastcomplete) > 0) && (!nextcomplete || strcmp(id.name, nextcomplete) < 0))
+                nextcomplete = id.name;
+        );
+    }
+    DELETEA(lastcomplete);
+    if(nextcomplete)
+    {
+        cmdlen = min(cmdlen, maxlen-1);
+        if(cmdlen) memmove(s, cmdprefix, cmdlen);
+        copystring(&s[cmdlen], nextcomplete, maxlen-cmdlen);
+        lastcomplete = newstring(nextcomplete);
+    }
+}
+
+void writecompletions(stream *f)
+{
+    vector<char *> cmds;
+    enumeratekt(completions, char *, k, filesval *, v, { if(v) cmds.add(k); });
+    cmds.sort();
+    loopv(cmds)
+    {
+        char *k = cmds[i];
+        filesval *v = completions[k];
+        if(v->type==FILES_LIST) 
+        {
+            if(validateblock(v->dir)) f->printf("listcomplete %s [%s]\n", escapeid(k), v->dir);
+            else f->printf("listcomplete %s %s\n", escapeid(k), escapestring(v->dir));
+        }
+        else f->printf("%s %s %s %s\n", v->type==FILES_VAR ? "varcomplete" : "complete", escapeid(k), escapestring(v->dir), escapestring(v->ext ? v->ext : "*"));
+    }
+}
+
diff --git a/src/engine/decal.cpp b/src/engine/decal.cpp
new file mode 100644 (file)
index 0000000..98787dd
--- /dev/null
@@ -0,0 +1,642 @@
+#include "engine.h"
+
+struct decalvert
+{
+    vec pos;
+    bvec4 color;
+    vec2 tc;
+};
+
+struct decalinfo
+{
+    int millis;
+    bvec color;
+    ushort startvert, endvert;
+};
+
+enum
+{
+    DF_RND4       = 1<<0,
+    DF_ROTATE     = 1<<1,
+    DF_INVMOD     = 1<<2,
+    DF_OVERBRIGHT = 1<<3,
+    DF_ADD        = 1<<4,
+    DF_SATURATE   = 1<<5
+};
+
+VARFP(maxdecaltris, 1, 1024, 16384, initdecals());
+VARP(decalfade, 1000, 10000, 60000);
+VAR(dbgdec, 0, 0, 1);
+
+struct decalrenderer
+{
+    const char *texname;
+    int flags, fadeintime, fadeouttime, timetolive;
+    Texture *tex;
+    decalinfo *decals;
+    int maxdecals, startdecal, enddecal;
+    decalvert *verts;
+    int maxverts, startvert, endvert, lastvert, availverts;
+    GLuint vbo;
+    bool dirty;
+
+    decalrenderer(const char *texname, int flags = 0, int fadeintime = 0, int fadeouttime = 1000, int timetolive = -1)
+        : texname(texname), flags(flags),
+          fadeintime(fadeintime), fadeouttime(fadeouttime), timetolive(timetolive),
+          tex(NULL),
+          decals(NULL), maxdecals(0), startdecal(0), enddecal(0),
+          verts(NULL), maxverts(0), startvert(0), endvert(0), lastvert(0), availverts(0),
+          vbo(0), dirty(false),
+          decalu(0), decalv(0)
+    {
+    }
+
+    ~decalrenderer()
+    {
+        DELETEA(decals);
+        DELETEA(verts);
+    }
+
+    void init(int tris)
+    {
+        if(decals)
+        {
+            DELETEA(decals);
+            maxdecals = startdecal = enddecal = 0;
+        }
+        if(verts)
+        {
+            DELETEA(verts);
+            maxverts = startvert = endvert = lastvert = availverts = 0;
+        }
+        decals = new decalinfo[tris];
+        maxdecals = tris;
+        tex = textureload(texname, 3);
+        maxverts = tris*3 + 3;
+        availverts = maxverts - 3; 
+        verts = new decalvert[maxverts];
+    }
+
+    int hasdecals()
+    {
+        return enddecal < startdecal ? maxdecals - (startdecal - enddecal) : enddecal - startdecal;
+    }
+
+    void cleanup()
+    {
+        if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+    }
+
+    void cleardecals()
+    {
+        startdecal = enddecal = 0;
+        startvert = endvert = lastvert = 0;
+        availverts = maxverts - 3;
+        dirty = true;
+    }
+
+    int freedecal()
+    {
+        if(startdecal==enddecal) return 0;
+
+        decalinfo &d = decals[startdecal];
+        startdecal++;
+        if(startdecal >= maxdecals) startdecal = 0;
+        
+        int removed = d.endvert < d.startvert ? maxverts - (d.startvert - d.endvert) : d.endvert - d.startvert;
+        startvert = d.endvert;
+        if(startvert==endvert) startvert = endvert = lastvert = 0;
+        availverts += removed;
+
+        return removed;
+    }
+
+    void fadedecal(decalinfo &d, uchar alpha)
+    {
+        bvec rgb;
+        if(flags&DF_OVERBRIGHT) rgb = bvec(128, 128, 128);
+        else
+        {
+            rgb = d.color;
+            if(flags&(DF_ADD|DF_INVMOD)) rgb.scale(alpha, 255);
+        }
+        bvec4 color(rgb, alpha);
+
+        decalvert *vert = &verts[d.startvert],
+                  *end = &verts[d.endvert < d.startvert ? maxverts : d.endvert]; 
+        while(vert < end)
+        {
+            vert->color = color;
+            vert++;
+        }
+        if(d.endvert < d.startvert)
+        {
+            vert = verts;
+            end = &verts[d.endvert];
+            while(vert < end)
+            {
+                vert->color = color;
+                vert++;
+            }
+        }
+        dirty = true;
+    }
+
+    void clearfadeddecals()
+    {
+        int threshold = lastmillis - (timetolive>=0 ? timetolive : decalfade) - fadeouttime;
+        decalinfo *d = &decals[startdecal],
+                  *end = &decals[enddecal < startdecal ? maxdecals : enddecal];
+        while(d < end && d->millis <= threshold) d++;
+        if(d >= end && enddecal < startdecal)
+        {
+            d = decals;
+            end = &decals[enddecal];
+            while(d < end && d->millis <= threshold) d++;
+        }
+        int prevstart = startdecal;
+        startdecal = d - decals;
+        if(prevstart == startdecal) return;
+        if(startdecal!=enddecal) startvert = decals[startdecal].startvert;
+        else startvert = endvert = lastvert = 0;
+        availverts = endvert < startvert ? startvert - endvert - 3 : maxverts - 3 - (endvert - startvert);
+        dirty = true;
+    }
+    void fadeindecals()
+    {
+        if(!fadeintime) return;
+        decalinfo *d = &decals[enddecal],
+                  *end = &decals[enddecal < startdecal ? 0 : startdecal];
+        while(d > end)
+        {
+            d--;
+            int fade = lastmillis - d->millis;
+            if(fade >= fadeintime) return;
+            fadedecal(*d, (fade<<8)/fadeintime);
+        }
+        if(enddecal < startdecal)
+        {
+            d = &decals[maxdecals];
+            end = &decals[startdecal];
+            while(d > end)
+            {
+                d--;
+                int fade = lastmillis - d->millis;
+                if(fade >= fadeintime) return;
+                fadedecal(*d, (fade<<8)/fadeintime);
+            }
+        }
+    }
+
+    void fadeoutdecals()
+    {
+        decalinfo *d = &decals[startdecal],
+                  *end = &decals[enddecal < startdecal ? maxdecals : enddecal];
+        int offset = (timetolive>=0 ? timetolive : decalfade) + fadeouttime - lastmillis;
+        while(d < end)
+        {
+            int fade = d->millis + offset;
+            if(fade >= fadeouttime) return;
+            fadedecal(*d, (fade<<8)/fadeouttime);
+            d++;
+        }
+        if(enddecal < startdecal)
+        {
+            d = decals;
+            end = &decals[enddecal];
+            while(d < end)
+            {
+                int fade = d->millis + offset;
+                if(fade >= fadeouttime) return;
+                fadedecal(*d, (fade<<8)/fadeouttime);
+                d++;
+            }
+        }
+    }
+         
+    static void setuprenderstate()
+    {
+        enablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+
+        glDepthMask(GL_FALSE);
+        glEnable(GL_BLEND);
+
+        gle::enablevertex();
+        gle::enabletexcoord0();
+        gle::enablecolor();
+    }
+
+    static void cleanuprenderstate()
+    {
+        gle::clearvbo();
+
+        gle::disablevertex();
+        gle::disabletexcoord0();
+        gle::disablecolor();
+
+        glDepthMask(GL_TRUE);
+        glDisable(GL_BLEND);
+
+        disablepolygonoffset(GL_POLYGON_OFFSET_FILL);
+    }
+
+    void render()
+    {
+        if(startvert==endvert) return;
+
+        if(flags&DF_OVERBRIGHT) 
+        {
+            glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR); 
+            SETSHADER(overbrightdecal);
+        }
+        else 
+        {
+            if(flags&DF_INVMOD) { glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); zerofogcolor(); }
+            else if(flags&DF_ADD) { glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); zerofogcolor(); }
+            else glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+            if(flags&DF_SATURATE) SETSHADER(saturatedecal);
+            else foggedshader->set();
+        }
+
+        glBindTexture(GL_TEXTURE_2D, tex->id);
+
+        if(!vbo) { glGenBuffers_(1, &vbo); dirty = true; }
+        gle::bindvbo(vbo);
+    
+        int count = endvert < startvert ? maxverts - startvert : endvert - startvert;
+        if(dirty)
+        {
+            glBufferData_(GL_ARRAY_BUFFER, maxverts*sizeof(decalvert), NULL, GL_STREAM_DRAW);
+            glBufferSubData_(GL_ARRAY_BUFFER, 0, count*sizeof(decalvert), &verts[startvert]);
+            if(endvert < startvert)
+            {
+                glBufferSubData_(GL_ARRAY_BUFFER, count*sizeof(decalvert), endvert*sizeof(decalvert), verts);
+                count += endvert;
+            }
+            dirty = false;
+        }
+        else if(endvert < startvert) count += endvert;
+
+        const decalvert *ptr = 0;
+        gle::vertexpointer(sizeof(decalvert), ptr->pos.v);
+        gle::texcoord0pointer(sizeof(decalvert), ptr->tc.v);
+        gle::colorpointer(sizeof(decalvert), ptr->color.v);
+
+        glDrawArrays(GL_TRIANGLES, 0, count);
+        xtravertsva += count;
+
+        if(flags&(DF_ADD|DF_INVMOD)) resetfogcolor();
+
+        extern int intel_vertexarray_bug;
+        if(intel_vertexarray_bug) glFlush();
+    }
+
+    decalinfo &newdecal()
+    {
+        decalinfo &d = decals[enddecal];
+        int next = enddecal + 1;
+        if(next>=maxdecals) next = 0;
+        if(next==startdecal) freedecal();
+        enddecal = next;
+        dirty = true;
+        return d;
+    }
+
+    ivec bbmin, bbmax;
+    vec decalcenter, decalnormal, decaltangent, decalbitangent;
+    float decalradius, decalu, decalv;
+    bvec4 decalcolor;
+
+    void adddecal(const vec &center, const vec &dir, float radius, const bvec &color, int info)
+    {
+        if(dir.iszero()) return;
+
+        int bbradius = int(ceil(radius));
+        bbmin = ivec(center).sub(bbradius);
+        bbmax = ivec(center).add(bbradius);
+
+        decalcolor = bvec4(color, 255);
+        decalcenter = center;
+        decalradius = radius;
+        decalnormal = dir;
+#if 0
+        decaltangent.orthogonal(dir);
+#else
+        decaltangent = vec(dir.z, -dir.x, dir.y);
+        decaltangent.sub(vec(dir).mul(decaltangent.dot(dir)));
+#endif
+        if(flags&DF_ROTATE) decaltangent.rotate(rnd(360)*RAD, dir);
+        decaltangent.normalize();
+        decalbitangent.cross(decaltangent, dir);
+        if(flags&DF_RND4)
+        {
+            decalu = 0.5f*(info&1);
+            decalv = 0.5f*((info>>1)&1);
+        }
+
+        lastvert = endvert;
+        gentris(worldroot, ivec(0, 0, 0), worldsize>>1);
+        if(dbgdec)
+        {
+            int nverts = endvert < lastvert ? endvert + maxverts - lastvert : endvert - lastvert;
+            conoutf(CON_DEBUG, "tris = %d, verts = %d, total tris = %d", nverts/3, nverts, (maxverts - 3 - availverts)/3);
+        }
+        if(endvert==lastvert) return;
+
+        decalinfo &d = newdecal();
+        d.color = color;
+        d.millis = lastmillis;
+        d.startvert = lastvert;
+        d.endvert = endvert;
+    }
+
+    static int clip(const vec *in, int numin, const vec &dir, float below, float above, vec *out)
+    {
+        int numout = 0;
+        const vec *p = &in[numin-1];
+        float pc = dir.dot(*p);
+        loopi(numin)
+        {
+            const vec &v = in[i];
+            float c = dir.dot(v);
+            if(c < below)
+            {
+                if(pc > above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+                if(pc > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+            }
+            else if(c > above)
+            {
+                if(pc < below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+                if(pc < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+            }
+            else
+            {
+                if(pc < below)
+                {
+                    if(c > below) out[numout++] = vec(*p).sub(v).mul((below - c)/(pc - c)).add(v);
+                }
+                else if(pc > above && c < above) out[numout++] = vec(*p).sub(v).mul((above - c)/(pc - c)).add(v);
+                out[numout++] = v;
+            }
+            p = &v;
+            pc = c;
+        }
+        return numout;
+    }
+
+    void gentris(cube &cu, int orient, const ivec &o, int size, materialsurface *mat = NULL, int vismask = 0)
+    {
+        vec pos[MAXFACEVERTS+4];
+        int numverts = 0, numplanes = 1;
+        vec planes[2];
+        if(mat)
+        {
+            planes[0] = vec(0, 0, 0);
+            switch(orient)
+            {
+            #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+                case orient: \
+                    planes[0][dimension(orient)] = dimcoord(orient) ? 1 : -1; \
+                    v0 v1 v2 v3 \
+                    break;
+            #define GENFACEVERT(orient, vert, x,y,z, xv,yv,zv) \
+                    pos[numverts++] = vec(x xv, y yv, z zv);
+                GENFACEVERTS(o.x, o.x, o.y, o.y, o.z, o.z, , + mat->csize, , + mat->rsize, + 0.1f, - 0.1f);
+            #undef GENFACEORIENT
+            #undef GENFACEVERT 
+            }
+        }
+        else if(cu.texture[orient] == DEFAULT_SKY) return;
+        else if(cu.ext && (numverts = cu.ext->surfaces[orient].numverts&MAXFACEVERTS))
+        {
+            vertinfo *verts = cu.ext->verts() + cu.ext->surfaces[orient].verts;
+            ivec vo = ivec(o).mask(~0xFFF).shl(3);
+            loopj(numverts) pos[j] = vec(verts[j].getxyz().add(vo)).mul(1/8.0f);
+            planes[0].cross(pos[0], pos[1], pos[2]).normalize();
+            if(numverts >= 4 && !(cu.merged&(1<<orient)) && !flataxisface(cu, orient) && faceconvexity(verts, numverts, size))
+            {
+                planes[1].cross(pos[0], pos[2], pos[3]).normalize();
+                numplanes++;
+            }
+        }
+        else if(cu.merged&(1<<orient)) return;
+        else if(!vismask || (vismask&0x40 && visibleface(cu, orient, o, size, MAT_AIR, (cu.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA)))
+        {
+            ivec v[4];
+            genfaceverts(cu, orient, v);
+            int vis = 3, convex = faceconvexity(v, vis), order = convex < 0 ? 1 : 0;
+            vec vo(o);
+            pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+            if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+            pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+            if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+            planes[0].cross(pos[0], pos[1], pos[2]).normalize();
+            if(convex) { planes[1].cross(pos[0], pos[2], pos[3]).normalize(); numplanes++; }
+        } 
+        else return;
+
+        loopl(numplanes)
+        {
+            const vec &n = planes[l];
+            float facing = n.dot(decalnormal);
+            if(facing <= 0) continue;
+            vec p = vec(pos[0]).sub(decalcenter);
+#if 0
+            // intersect ray along decal normal with plane
+            float dist = n.dot(p) / facing;
+            if(fabs(dist) > decalradius) continue;
+            vec pcenter = vec(decalnormal).mul(dist).add(decalcenter);
+#else
+            // travel back along plane normal from the decal center
+            float dist = n.dot(p);
+            if(fabs(dist) > decalradius) continue;
+            vec pcenter = vec(n).mul(dist).add(decalcenter);
+#endif
+            vec ft, fb;
+            ft.orthogonal(n);
+            ft.normalize();
+            fb.cross(ft, n);
+            vec pt = vec(ft).mul(ft.dot(decaltangent)).add(vec(fb).mul(fb.dot(decaltangent))).normalize(),
+                pb = vec(ft).mul(ft.dot(decalbitangent)).add(vec(fb).mul(fb.dot(decalbitangent))).normalize();
+            // orthonormalize projected bitangent to prevent streaking
+            pb.sub(vec(pt).mul(pt.dot(pb))).normalize();
+            vec v1[MAXFACEVERTS+4], v2[MAXFACEVERTS+4];
+            float ptc = pt.dot(pcenter), pbc = pb.dot(pcenter);
+            int numv;
+            if(numplanes >= 2)
+            {
+                if(l) { pos[1] = pos[2]; pos[2] = pos[3]; } 
+                numv = clip(pos, 3, pt, ptc - decalradius, ptc + decalradius, v1);
+                if(numv<3) continue;
+            }
+            else
+            {
+                numv = clip(pos, numverts, pt, ptc - decalradius, ptc + decalradius, v1);
+                if(numv<3) continue;
+            }
+            numv = clip(v1, numv, pb, pbc - decalradius, pbc + decalradius, v2);
+            if(numv<3) continue;
+            float tsz = flags&DF_RND4 ? 0.5f : 1.0f, scale = tsz*0.5f/decalradius,
+                  tu = decalu + tsz*0.5f - ptc*scale, tv = decalv + tsz*0.5f - pbc*scale;
+            pt.mul(scale); pb.mul(scale);
+            decalvert dv1 = { v2[0], decalcolor, vec2(pt.dot(v2[0]) + tu, pb.dot(v2[0]) + tv) },
+                      dv2 = { v2[1], decalcolor, vec2(pt.dot(v2[1]) + tu, pb.dot(v2[1]) + tv) };
+            int totalverts = 3*(numv-2);
+            if(totalverts > maxverts-3) return;
+            while(availverts < totalverts)
+            {
+                if(!freedecal()) return;
+            }
+            availverts -= totalverts;
+            loopk(numv-2)
+            {
+                verts[endvert++] = dv1;
+                verts[endvert++] = dv2;
+                dv2.pos = v2[k+2];
+                dv2.tc = vec2(pt.dot(v2[k+2]) + tu, pb.dot(v2[k+2]) + tv);
+                verts[endvert++] = dv2;
+                if(endvert>=maxverts) endvert = 0;
+            }
+        }
+    }
+
+    void findmaterials(vtxarray *va)
+    {
+        materialsurface *matbuf = va->matbuf;
+        int matsurfs = va->matsurfs;
+        loopi(matsurfs)
+        {
+            materialsurface &m = matbuf[i];
+            if(!isclipped(m.material&MATF_VOLUME)) { i += m.skip; continue; }
+            int dim = dimension(m.orient), dc = dimcoord(m.orient);
+            if(dc ? decalnormal[dim] <= 0 : decalnormal[dim] >= 0) { i += m.skip; continue; }
+            int c = C[dim], r = R[dim];
+            for(;;)
+            {
+                materialsurface &m = matbuf[i];
+                if(m.o[dim] >= bbmin[dim] && m.o[dim] <= bbmax[dim] &&
+                   m.o[c] + m.csize >= bbmin[c] && m.o[c] <= bbmax[c] &&
+                   m.o[r] + m.rsize >= bbmin[r] && m.o[r] <= bbmax[r])
+                {
+                    static cube dummy;
+                    gentris(dummy, m.orient, m.o, max(m.csize, m.rsize), &m); 
+                }
+                if(i+1 >= matsurfs) break;
+                materialsurface &n = matbuf[i+1];
+                if(n.material != m.material || n.orient != m.orient) break;
+                i++;
+            } 
+        }
+    }
+
+    void findescaped(cube *cu, const ivec &o, int size, int escaped)
+    {
+        loopi(8)
+        {
+            if(escaped&(1<<i)) 
+            { 
+                ivec co(i, o, size);
+                if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+                else
+                {
+                    int vismask = cu[i].merged;
+                    if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+                }
+            } 
+        }
+    }
+
+    void gentris(cube *cu, const ivec &o, int size, int escaped = 0)
+    {
+        int overlap = octaboxoverlap(o, size, bbmin, bbmax);
+        loopi(8) 
+        {
+            if(overlap&(1<<i))
+            {
+                ivec co(i, o, size);
+                if(cu[i].ext && cu[i].ext->va && cu[i].ext->va->matsurfs)
+                    findmaterials(cu[i].ext->va);
+                if(cu[i].children) gentris(cu[i].children, co, size>>1, cu[i].escaped);
+                else 
+                {
+                    int vismask = cu[i].visible;
+                    if(vismask&0xC0)
+                    {
+                        if(vismask&0x80) loopj(6) gentris(cu[i], j, co, size, NULL, vismask);
+                        else loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+                    }
+                }
+            }
+            else if(escaped&(1<<i))
+            {
+                ivec co(i, o, size);
+                if(cu[i].children) findescaped(cu[i].children, co, size>>1, cu[i].escaped);
+                else
+                {
+                    int vismask = cu[i].merged;
+                    if(vismask) loopj(6) if(vismask&(1<<j)) gentris(cu[i], j, co, size);
+                }
+            } 
+        }
+    }
+};
+
+decalrenderer decals[] =
+{
+    decalrenderer("<grey>packages/particles/scorch.png", DF_ROTATE, 500),
+    decalrenderer("<grey>packages/particles/blood.png", DF_RND4|DF_ROTATE|DF_INVMOD),
+    decalrenderer("<grey>packages/particles/bullet.png", DF_OVERBRIGHT)
+};
+
+void initdecals()
+{
+    loopi(sizeof(decals)/sizeof(decals[0])) decals[i].init(maxdecaltris);
+}
+
+void cleardecals()
+{
+    loopi(sizeof(decals)/sizeof(decals[0])) decals[i].cleardecals();
+}
+
+void cleanupdecals()
+{
+    loopi(sizeof(decals)/sizeof(decals[0])) decals[i].cleanup();
+}
+
+VARNP(decals, showdecals, 0, 1, 1);
+
+void renderdecals(bool mainpass)
+{
+    bool rendered = false;
+    loopi(sizeof(decals)/sizeof(decals[0]))
+    {
+        decalrenderer &d = decals[i];
+        if(mainpass)
+        {
+            d.clearfadeddecals();
+            d.fadeindecals();
+            d.fadeoutdecals();
+        }
+        if(!showdecals || !d.hasdecals()) continue;
+        if(!rendered)
+        {
+            rendered = true;
+            decalrenderer::setuprenderstate();
+        }
+        d.render();
+    }
+    if(!rendered) return;
+    decalrenderer::cleanuprenderstate();
+}
+
+VARP(maxdecaldistance, 1, 512, 10000);
+
+void adddecal(int type, const vec &center, const vec &surface, float radius, const bvec &color, int info)
+{
+    if(!showdecals || type<0 || (size_t)type>=sizeof(decals)/sizeof(decals[0]) || center.dist(camera1->o) - radius > maxdecaldistance) return;
+    decalrenderer &d = decals[type];
+    d.adddecal(center, surface, radius, color, info);
+}
diff --git a/src/engine/depthfx.h b/src/engine/depthfx.h
new file mode 100644 (file)
index 0000000..a9c0fdf
--- /dev/null
@@ -0,0 +1,193 @@
+// eye space depth texture for soft particles, done at low res then blurred to prevent ugly jaggies
+VARP(depthfxfpscale, 1, 1<<12, 1<<16);
+VARP(depthfxscale, 1, 1<<6, 1<<8);
+VARP(depthfxblend, 1, 16, 64);
+VARP(depthfxpartblend, 1, 8, 64);
+VAR(depthfxmargin, 0, 16, 64);
+VAR(depthfxbias, 0, 1, 64);
+
+extern void cleanupdepthfx();
+VARFP(fpdepthfx, 0, 0, 1, cleanupdepthfx());
+VARP(depthfxemuprecision, 0, 1, 1);
+VARFP(depthfxsize, 6, 7, 12, cleanupdepthfx());
+VARP(depthfx, 0, 1, 1);
+VARP(depthfxparts, 0, 1, 1);
+VARP(blurdepthfx, 0, 1, 7);
+VARP(blurdepthfxsigma, 1, 50, 200);
+VAR(depthfxscissor, 0, 2, 2);
+VAR(debugdepthfx, 0, 0, 1);
+
+#define MAXDFXRANGES 4
+
+void *depthfxowners[MAXDFXRANGES];
+float depthfxranges[MAXDFXRANGES];
+int numdepthfxranges = 0;
+vec depthfxmin(1e16f, 1e16f, 1e16f), depthfxmax(1e16f, 1e16f, 1e16f);
+
+static struct depthfxtexture : rendertarget
+{
+    const GLenum *colorformats() const
+    {
+        static const GLenum colorfmts[] = { GL_RG16F, GL_RGB16F, GL_RGBA, GL_RGBA8, GL_RGB, GL_RGB8, GL_FALSE };
+        return &colorfmts[fpdepthfx && hasTF ? (hasTRG ? 0 : 1) : 2];
+    }
+
+    float eyedepth(const vec &p) const
+    {
+        return max(-cammatrix.transform<vec>(p).z, 0.0f);
+    }
+
+    void addscissorvert(const vec &v, float &sx1, float &sy1, float &sx2, float &sy2)
+    {
+        vec p = camprojmatrix.perspectivetransform(v);
+        sx1 = min(sx1, p.x);
+        sy1 = min(sy1, p.y);
+        sx2 = max(sx2, p.x);
+        sy2 = max(sy2, p.y);
+    }
+
+    bool addscissorbox(const vec &center, float size)
+    {
+        float sx1, sy1, sx2, sy2;
+        calcspherescissor(center, size, sx1, sy1, sx2, sy2);
+        return addblurtiles(sx1, sy1, sx2, sy2);
+    }
+
+    bool addscissorbox(const vec &bbmin, const vec &bbmax)
+    {
+        float sx1 = 1, sy1 = 1, sx2 = -1, sy2 = -1;
+        loopi(8)
+        {
+            vec v(i&1 ? bbmax.x : bbmin.x, i&2 ? bbmax.y : bbmin.y, i&4 ? bbmax.z : bbmin.z);
+            addscissorvert(v, sx1, sy1, sx2, sy2);
+        }
+        return addblurtiles(sx1, sy1, sx2, sy2);
+    }
+
+    bool screenrect() const { return true; }
+    bool filter() const { return blurdepthfx!=0; }
+    bool highprecision() const { return colorfmt==GL_RG16F || colorfmt==GL_RGB16F; }
+    bool emulatehighprecision() const { return depthfxemuprecision && !blurdepthfx; }
+
+    bool shouldrender()
+    {
+        extern void finddepthfxranges();
+        finddepthfxranges();
+        return (numdepthfxranges && scissorx1 < scissorx2 && scissory1 < scissory2) || debugdepthfx;
+    }
+
+    bool dorender()
+    {
+        glClearColor(1, 1, 1, 1);
+        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+        depthfxing = true;
+        refracting = -1;
+
+        extern void renderdepthobstacles(const vec &bbmin, const vec &bbmax, float scale, float *ranges, int numranges);
+        float scale = depthfxscale;
+        float *ranges = depthfxranges;
+        int numranges = numdepthfxranges;
+        if(highprecision())
+        {
+            scale = depthfxfpscale;
+            ranges = NULL;
+            numranges = 0;
+        }
+        else if(emulatehighprecision())
+        {
+            scale = depthfxfpscale;
+            ranges = NULL;
+            numranges = -3;
+        }
+        renderdepthobstacles(depthfxmin, depthfxmax, scale, ranges, numranges);
+
+        refracting = 0;
+        depthfxing = false;
+
+        return numdepthfxranges > 0;
+    }
+
+    void dodebug(int w, int h)
+    {
+        if(numdepthfxranges > 0)
+        {
+            gle::colorf(0, 1, 0);
+            debugscissor(w, h, true);
+            gle::colorf(0, 0, 1);
+            debugblurtiles(w, h, true);
+            gle::colorf(1, 1, 1);
+        }
+    }
+} depthfxtex;
+
+void cleanupdepthfx()
+{
+    depthfxtex.cleanup(true);
+}
+
+void viewdepthfxtex()
+{
+    if(!depthfx) return;
+    depthfxtex.debug();
+}
+
+bool depthfxing = false;
+
+bool binddepthfxtex()
+{
+    if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0)        
+    {
+        glActiveTexture_(GL_TEXTURE2);
+        glBindTexture(GL_TEXTURE_2D, depthfxtex.rendertex);
+        glActiveTexture_(GL_TEXTURE0);
+        return true;
+    }
+    return false;
+}
+
+void binddepthfxparams(float blend, float minblend = 0, bool allow = true, void *owner = NULL)
+{
+    if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0)
+    {
+        float scale = 0, offset = -1, texscale = 0;
+        if(!depthfxtex.highprecision())
+        {
+            float select[4] = { 0, 0, 0, 0 };
+            if(!depthfxtex.emulatehighprecision())
+            {
+                loopi(numdepthfxranges) if(depthfxowners[i]==owner)
+                {
+                    select[i] = float(depthfxscale)/blend;
+                    scale = 1.0f/blend;
+                    offset = -float(depthfxranges[i] - depthfxbias)/blend;
+                    break;
+                }
+            }
+            else if(allow)
+            {
+                select[0] = float(depthfxfpscale)/blend;
+                select[1] = select[0]/256;
+                select[2] = select[1]/256;
+                scale = 1.0f/blend;
+                offset = 0;
+            }
+            LOCALPARAMF(depthfxselect, select[0], select[1], select[2], select[3]);
+        }
+        else if(allow)
+        {
+            scale = 1.0f/blend;
+            offset = 0;
+            texscale = float(depthfxfpscale)/blend;
+        }
+        LOCALPARAMF(depthfxparams, scale, offset, texscale, minblend);
+    }
+}
+
+void drawdepthfxtex()
+{
+    if(!depthfx) return;
+
+    depthfxtex.render(1<<depthfxsize, 1<<depthfxsize, blurdepthfx, blurdepthfxsigma/100.0f);
+}
+
diff --git a/src/engine/dynlight.cpp b/src/engine/dynlight.cpp
new file mode 100644 (file)
index 0000000..835c631
--- /dev/null
@@ -0,0 +1,227 @@
+#include "engine.h"
+
+VARP(maxdynlights, 0, min(3, MAXDYNLIGHTS), MAXDYNLIGHTS);
+VARP(dynlightdist, 0, 1024, 10000);
+
+struct dynlight
+{
+    vec o, hud;
+    float radius, initradius, curradius, dist;
+    vec color, initcolor, curcolor;
+    int fade, peak, expire, flags;
+    physent *owner;
+
+    void calcradius()
+    {
+        if(fade + peak > 0)
+        {
+            int remaining = expire - lastmillis;
+            if(flags&DL_EXPAND)
+                curradius = initradius + (radius - initradius) * (1.0f - remaining/float(fade + peak));
+            else if(!(flags&DL_FLASH) && remaining > fade)
+                curradius = initradius + (radius - initradius) * (1.0f - float(remaining - fade)/peak);
+            else if(flags&DL_SHRINK)
+                curradius = (radius*remaining)/fade;
+            else curradius = radius;
+        }
+        else curradius = radius;
+    }
+
+    void calccolor()
+    {
+        if(flags&DL_FLASH || peak <= 0) curcolor = color;
+        else
+        {
+            int peaking = expire - lastmillis - fade;
+            if(peaking <= 0) curcolor = color;
+            else curcolor.lerp(initcolor, color, 1.0f - float(peaking)/peak);
+        }
+
+        float intensity = 1.0f;
+        if(fade > 0)
+        {
+            int fading = expire - lastmillis;
+            if(fading < fade) intensity = float(fading)/fade;
+        }
+        curcolor.mul(intensity);
+        // KLUGE: this prevents nvidia drivers from trying to recompile dynlight fragment programs
+        loopk(3) if(fmod(curcolor[k], 1.0f/256) < 0.001f) curcolor[k] += 0.001f;
+    }
+};
+
+vector<dynlight> dynlights;
+vector<dynlight *> closedynlights;
+
+void adddynlight(const vec &o, float radius, const vec &color, int fade, int peak, int flags, float initradius, const vec &initcolor, physent *owner)
+{
+    if(!maxdynlights) return;
+    if(o.dist(camera1->o) > dynlightdist || radius <= 0) return;
+
+    int insert = 0, expire = fade + peak + lastmillis;
+    loopvrev(dynlights) if(expire>=dynlights[i].expire) { insert = i+1; break; }
+    dynlight d;
+    d.o = d.hud = o;
+    d.radius = radius;
+    d.initradius = initradius;
+    d.color = color;
+    d.initcolor = initcolor;
+    d.fade = fade;
+    d.peak = peak;
+    d.expire = expire;
+    d.flags = flags;
+    d.owner = owner;
+    dynlights.insert(insert, d);
+}
+
+void cleardynlights()
+{
+    int faded = -1;
+    loopv(dynlights) if(lastmillis<dynlights[i].expire) { faded = i; break; }
+    if(faded<0) dynlights.setsize(0);
+    else if(faded>0) dynlights.remove(0, faded);
+}
+
+void removetrackeddynlights(physent *owner)
+{
+    loopvrev(dynlights) if(owner ? dynlights[i].owner == owner : dynlights[i].owner != NULL) dynlights.remove(i);
+}
+
+void updatedynlights()
+{
+    cleardynlights();
+    game::adddynlights();
+
+    loopv(dynlights)
+    {
+        dynlight &d = dynlights[i];
+        if(d.owner) game::dynlighttrack(d.owner, d.o, d.hud);
+        d.calcradius();
+        d.calccolor();
+    }
+}
+
+int finddynlights()
+{
+    closedynlights.setsize(0);
+    if(!maxdynlights) return 0;
+    physent e;
+    e.type = ENT_CAMERA;
+    loopvj(dynlights)
+    {
+        dynlight &d = dynlights[j];
+        if(d.curradius <= 0) continue;
+        d.dist = camera1->o.dist(d.o) - d.curradius;
+        if(d.dist > dynlightdist || isfoggedsphere(d.curradius, d.o) || pvsoccludedsphere(d.o, d.curradius))
+            continue;
+        if(reflecting || refracting > 0)
+        {
+            if(d.o.z + d.curradius < reflectz) continue;
+        }
+        else if(refracting < 0 && d.o.z - d.curradius > reflectz) continue;
+        e.o = d.o;
+        e.radius = e.xradius = e.yradius = e.eyeheight = e.aboveeye = d.curradius;
+        if(!collide(&e, vec(0, 0, 0), 0, false)) continue;
+
+        int insert = 0;
+        loopvrev(closedynlights) if(d.dist >= closedynlights[i]->dist) { insert = i+1; break; }
+        closedynlights.insert(insert, &d);
+        if(closedynlights.length() >= DYNLIGHTMASK) break;
+    }
+    return closedynlights.length();
+}
+
+bool getdynlight(int n, vec &o, float &radius, vec &color)
+{
+    if(!closedynlights.inrange(n)) return false;
+    dynlight &d = *closedynlights[n];
+    o = d.o;
+    radius = d.curradius;
+    color = d.curcolor;
+    return true;
+}
+
+void dynlightreaching(const vec &target, vec &color, vec &dir, bool hud)
+{
+    vec dyncolor(0, 0, 0);//, dyndir(0, 0, 0);
+    loopv(dynlights)
+    {
+        dynlight &d = dynlights[i];
+        if(d.curradius<=0) continue;
+
+        vec ray(hud ? d.hud : d.o);
+        ray.sub(target);
+        float mag = ray.squaredlen();
+        if(mag >= d.curradius*d.curradius) continue;
+
+        vec color = d.curcolor;
+        color.mul(1 - sqrtf(mag)/d.curradius);
+        dyncolor.add(color);
+        //dyndir.add(ray.mul(intensity/mag));
+    }
+#if 0
+    if(!dyndir.iszero())
+    {
+        dyndir.normalize();
+        float x = dyncolor.magnitude(), y = color.magnitude();
+        if(x+y>0)
+        {
+            dir.mul(x);
+            dyndir.mul(y); 
+            dir.add(dyndir).div(x+y);
+            if(dir.iszero()) dir = vec(0, 0, 1);
+            else dir.normalize();
+        }
+    }
+#endif
+    color.add(dyncolor);
+}
+
+void calcdynlightmask(vtxarray *va)
+{
+    uint mask = 0;
+    int offset = 0;
+    loopv(closedynlights)
+    {
+        dynlight &d = *closedynlights[i];
+        if(d.o.dist_to_bb(va->geommin, va->geommax) >= d.curradius) continue;
+
+        mask |= (i+1)<<offset;
+        offset += DYNLIGHTBITS;
+        if(offset >= maxdynlights*DYNLIGHTBITS) break;
+    }
+    va->dynlightmask = mask;
+}
+
+int setdynlights(vtxarray *va)
+{
+    if(closedynlights.empty() || !va->dynlightmask) return 0;
+
+    extern bool minimizedynlighttcusage();
+
+    static vec4 posv[MAXDYNLIGHTS];
+    static vec colorv[MAXDYNLIGHTS];
+
+    int index = 0;
+    for(uint mask = va->dynlightmask; mask; mask >>= DYNLIGHTBITS, index++)
+    {
+        dynlight &d = *closedynlights[(mask&DYNLIGHTMASK)-1];
+
+        float scale = 1.0f/d.curradius;
+        vec origin = vec(d.o).mul(-scale);
+
+        if(index>0 && minimizedynlighttcusage())
+        {
+            scale /= posv[0].w;
+            origin.sub(vec(posv[0]).mul(scale));
+        }
+
+        posv[index] = vec4(origin, scale);
+        colorv[index] = d.curcolor;
+    }
+
+    GLOBALPARAMV(dynlightpos, posv, index);
+    GLOBALPARAMV(dynlightcolor, colorv, index);
+
+    return index;
+}
+
diff --git a/src/engine/engine.h b/src/engine/engine.h
new file mode 100644 (file)
index 0000000..9f41f45
--- /dev/null
@@ -0,0 +1,613 @@
+#ifndef __ENGINE_H__
+#define __ENGINE_H__
+
+#include "cube.h"
+#include "world.h"
+
+#ifndef STANDALONE
+
+#include "octa.h"
+#include "lightmap.h"
+#include "bih.h"
+#include "texture.h"
+#include "model.h"
+
+extern dynent *player;
+extern physent *camera1;                // special ent that acts as camera, same object as player1 in FPS mode
+
+extern int worldscale, worldsize;
+extern int mapversion;
+extern char *maptitle;
+extern vector<ushort> texmru;
+extern int xtraverts, xtravertsva;
+extern const ivec cubecoords[8];
+extern const ivec facecoords[6][4];
+extern const uchar fv[6][4];
+extern const uchar fvmasks[64];
+extern const uchar faceedgesidx[6][4];
+extern bool inbetweenframes, renderedframe;
+
+extern SDL_Window *screen;
+extern int screenw, screenh;
+extern int zpass;
+
+extern vector<int> entgroup;
+
+// rendertext
+struct font
+{
+    struct charinfo
+    {
+        short x, y, w, h, offsetx, offsety, advance, tex;
+    };
+
+    char *name;
+    vector<Texture *> texs;
+    vector<charinfo> chars;
+    int charoffset, defaultw, defaulth, scale;
+
+    font() : name(NULL) {}
+    ~font() { DELETEA(name); }
+};
+
+#define FONTH (curfont->scale)
+#define FONTW (FONTH/2)
+#define MINRESW 640
+#define MINRESH 480
+
+extern font *curfont;
+extern const matrix4x3 *textmatrix;
+
+extern void reloadfonts();
+
+// texture
+extern int hwtexsize, hwcubetexsize, hwmaxaniso, maxtexsize;
+
+extern Texture *textureload(const char *name, int clamp = 0, bool mipit = true, bool msg = true);
+extern int texalign(const void *data, int w, int bpp);
+extern void cleanuptexture(Texture *t);
+extern uchar *loadalphamask(Texture *t);
+extern void loadlayermasks();
+extern Texture *cubemapload(const char *name, bool mipit = true, bool msg = true, bool transient = false);
+extern void drawcubemap(int size, const vec &o, float yaw, float pitch, const cubemapside &side, bool onlysky = false);
+extern void loadshaders();
+extern void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum format = GL_RGB, GLenum target = GL_TEXTURE_2D, bool swizzle = false);
+extern void createtexture(int tnum, int w, int h, void *pixels, int clamp, int filter, GLenum component = GL_RGB, GLenum target = GL_TEXTURE_2D, int pw = 0, int ph = 0, int pitch = 0, bool resize = true, GLenum format = GL_FALSE, bool swizzle = false);
+extern void blurtexture(int n, int bpp, int w, int h, uchar *dst, const uchar *src, int margin = 0);
+extern void blurnormals(int n, int w, int h, bvec *dst, const bvec *src, int margin = 0);
+extern void renderpostfx();
+extern void initenvmaps();
+extern void genenvmaps();
+extern ushort closestenvmap(const vec &o);
+extern ushort closestenvmap(int orient, const ivec &co, int size);
+extern GLuint lookupenvmap(ushort emid);
+extern GLuint lookupenvmap(Slot &slot);
+extern bool reloadtexture(Texture &tex);
+extern bool reloadtexture(const char *name);
+extern void setuptexcompress();
+extern void clearslots();
+extern void compacteditvslots();
+extern void compactmruvslots();
+extern void compactvslots(cube *c, int n = 8);
+extern void compactvslot(int &index);
+extern void compactvslot(VSlot &vs);
+extern int compactvslots();
+extern void reloadtextures();
+extern void cleanuptextures();
+
+// shadowmap
+
+extern int shadowmap, shadowmapcasters;
+extern bool shadowmapping;
+extern matrix4 shadowmatrix;
+
+extern bool isshadowmapcaster(const vec &o, float rad);
+extern bool addshadowmapcaster(const vec &o, float xyrad, float zrad);
+extern bool isshadowmapreceiver(vtxarray *va);
+extern void rendershadowmap();
+extern void pushshadowmap();
+extern void popshadowmap();
+extern void rendershadowmapreceivers();
+extern void guessshadowdir();
+
+// pvs
+extern void clearpvs();
+extern bool pvsoccluded(const ivec &bbmin, const ivec &bbmax);
+extern bool pvsoccludedsphere(const vec &center, float radius);
+extern bool waterpvsoccluded(int height);
+extern void setviewcell(const vec &p);
+extern void savepvs(stream *f);
+extern void loadpvs(stream *f, int numpvs);
+extern int getnumviewcells();
+
+static inline bool pvsoccluded(const ivec &bborigin, int size)
+{
+    return pvsoccluded(bborigin, ivec(bborigin).add(size));
+}
+
+// rendergl
+extern bool hasVAO, hasFBO, hasAFBO, hasDS, hasTF, hasTRG, hasTSW, hasS3TC, hasFXT1, hasLATC, hasRGTC, hasAF, hasFBB, hasUBO, hasMBR;
+extern int glversion, glslversion, glcompat;
+
+enum { DRAWTEX_NONE = 0, DRAWTEX_ENVMAP, DRAWTEX_MINIMAP, DRAWTEX_MODELPREVIEW };
+
+extern float curfov, fovy, aspect, forceaspect;
+extern int drawtex;
+extern bool renderedgame;
+extern const matrix4 viewmatrix;
+extern matrix4 cammatrix, projmatrix, camprojmatrix, invcammatrix, invcamprojmatrix;
+extern bvec fogcolor;
+extern vec curfogcolor;
+extern int fog;
+extern float curfogstart, curfogend;
+
+extern void gl_checkextensions();
+extern void gl_init();
+extern void gl_resize();
+extern void cleanupgl();
+extern void rendergame(bool mainpass = false);
+extern void invalidatepostfx();
+extern void gl_drawhud();
+extern void gl_drawframe();
+extern void gl_drawmainmenu();
+extern void drawminimap();
+extern void drawtextures();
+extern void enablepolygonoffset(GLenum type);
+extern void disablepolygonoffset(GLenum type);
+extern void calcspherescissor(const vec &center, float size, float &sx1, float &sy1, float &sx2, float &sy2);
+extern int pushscissor(float sx1, float sy1, float sx2, float sy2);
+extern void popscissor();
+extern void recomputecamera();
+extern void screenquad();
+extern void screenquad(float sw, float sh);
+extern void screenquadflipped(float sw, float sh);
+extern void screenquad(float sw, float sh, float sw2, float sh2);
+extern void screenquadoffset(float x, float y, float w, float h);
+extern void screenquadoffset(float x, float y, float w, float h, float x2, float y2, float w2, float h2);
+extern void hudquad(float x, float y, float w, float h, float tx = 0, float ty = 0, float tw = 1, float th = 1);
+extern void setfogcolor(const vec &v);
+extern void zerofogcolor();
+extern void resetfogcolor();
+extern void setfogdist(float start, float end);
+extern void clearfogdist();
+extern void resetfogdist();
+extern void writecrosshairs(stream *f);
+
+namespace modelpreview
+{
+    extern void start(int x, int y, int w, int h, bool background = true);
+    extern void end();
+}
+
+// renderextras
+extern void render3dbox(vec &o, float tofloor, float toceil, float xradius, float yradius = 0);
+
+// octa
+extern cube *newcubes(uint face = F_EMPTY, int mat = MAT_AIR);
+extern cubeext *growcubeext(cubeext *ext, int maxverts);
+extern void setcubeext(cube &c, cubeext *ext);
+extern cubeext *newcubeext(cube &c, int maxverts = 0, bool init = true);
+extern void getcubevector(cube &c, int d, int x, int y, int z, ivec &p);
+extern void setcubevector(cube &c, int d, int x, int y, int z, const ivec &p);
+extern int familysize(const cube &c);
+extern void freeocta(cube *c);
+extern void discardchildren(cube &c, bool fixtex = false, int depth = 0);
+extern void optiface(uchar *p, cube &c);
+extern void validatec(cube *c, int size = 0);
+extern bool isvalidcube(const cube &c);
+extern ivec lu;
+extern int lusize;
+extern cube &lookupcube(const ivec &to, int tsize = 0, ivec &ro = lu, int &rsize = lusize);
+extern const cube *neighbourstack[32];
+extern int neighbourdepth;
+extern const cube &neighbourcube(const cube &c, int orient, const ivec &co, int size, ivec &ro = lu, int &rsize = lusize);
+extern void resetclipplanes();
+extern int getmippedtexture(const cube &p, int orient);
+extern void forcemip(cube &c, bool fixtex = true);
+extern bool subdividecube(cube &c, bool fullcheck=true, bool brighten=true);
+extern void edgespan2vectorcube(cube &c);
+extern int faceconvexity(const ivec v[4]);
+extern int faceconvexity(const ivec v[4], int &vis);
+extern int faceconvexity(const vertinfo *verts, int numverts, int size);
+extern int faceconvexity(const cube &c, int orient);
+extern void calcvert(const cube &c, const ivec &co, int size, ivec &vert, int i, bool solid = false);
+extern void calcvert(const cube &c, const ivec &co, int size, vec &vert, int i, bool solid = false);
+extern uint faceedges(const cube &c, int orient);
+extern bool collapsedface(const cube &c, int orient);
+extern bool touchingface(const cube &c, int orient);
+extern bool flataxisface(const cube &c, int orient);
+extern bool collideface(const cube &c, int orient);
+extern int genclipplane(const cube &c, int i, vec *v, plane *clip);
+extern void genclipplanes(const cube &c, const ivec &co, int size, clipplanes &p, bool collide = true);
+extern bool visibleface(const cube &c, int orient, const ivec &co, int size, ushort mat = MAT_AIR, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME);
+extern int classifyface(const cube &c, int orient, const ivec &co, int size);
+extern int visibletris(const cube &c, int orient, const ivec &co, int size, ushort nmat = MAT_ALPHA, ushort matmask = MAT_ALPHA);
+extern int visibleorient(const cube &c, int orient);
+extern void genfaceverts(const cube &c, int orient, ivec v[4]);
+extern int calcmergedsize(int orient, const ivec &co, int size, const vertinfo *verts, int numverts);
+extern void invalidatemerges(cube &c, const ivec &co, int size, bool msg);
+extern void calcmerges();
+
+extern int mergefaces(int orient, facebounds *m, int sz);
+extern void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat = MAT_AIR, ushort matmask = MATF_VOLUME);
+
+static inline cubeext &ext(cube &c)
+{
+    return *(c.ext ? c.ext : newcubeext(c));
+}
+
+// ents
+extern char *entname(entity &e);
+extern bool haveselent();
+extern undoblock *copyundoents(undoblock *u);
+extern void pasteundoent(int idx, const entity &ue);
+extern void pasteundoents(undoblock *u);
+
+// octaedit
+extern void cancelsel();
+extern void rendertexturepanel(int w, int h);
+extern void addundo(undoblock *u);
+extern void commitchanges(bool force = false);
+extern void rendereditcursor();
+extern void tryedit();
+
+extern bool prefabloaded(const char *name);
+extern void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size = 1, const vec &color = vec(1, 1, 1));
+extern void previewprefab(const char *name, const vec &color);
+
+// octarender
+extern vector<tjoint> tjoints;
+extern vector<vtxarray *> varoot, valist;
+
+extern ushort encodenormal(const vec &n);
+extern vec decodenormal(ushort norm);
+extern void guessnormals(const vec *pos, int numverts, vec *normals);
+extern void reduceslope(ivec &n);
+extern void findtjoints();
+extern void octarender();
+extern void allchanged(bool load = false);
+extern void clearvas(cube *c);
+extern void destroyva(vtxarray *va, bool reparent = true);
+extern bool readva(vtxarray *va, ushort *&edata, vertex *&vdata);
+extern void updatevabb(vtxarray *va, bool force = false);
+extern void updatevabbs(bool force = false);
+
+// renderva
+extern vtxarray *visibleva, *reflectedva;
+
+extern void visiblecubes(bool cull = true);
+extern void setvfcP(float z = -1, const vec &bbmin = vec(-1, -1, -1), const vec &bbmax = vec(1, 1, 1));
+extern void savevfcP();
+extern void restorevfcP();
+extern void rendergeom(float causticspass = 0, bool fogpass = false);
+extern void renderalphageom(bool fogpass = false);
+extern void rendermapmodels();
+extern void renderreflectedgeom(bool causticspass = false, bool fogpass = false);
+extern void renderreflectedmapmodels();
+extern void renderoutline();
+extern bool rendersky(bool explicitonly = false);
+
+extern bool isfoggedsphere(float rad, const vec &cv);
+extern int isvisiblesphere(float rad, const vec &cv);
+extern bool bboccluded(const ivec &bo, const ivec &br);
+extern occludequery *newquery(void *owner);
+extern void startquery(occludequery *query);
+extern void endquery(occludequery *query);
+extern bool checkquery(occludequery *query, bool nowait = false);
+extern void resetqueries();
+extern int getnumqueries();
+extern void startbb(bool mask = true);
+extern void endbb(bool mask = true);
+extern void drawbb(const ivec &bo, const ivec &br);
+
+extern int oqfrags;
+
+// dynlight
+
+extern void updatedynlights();
+extern int finddynlights();
+extern void calcdynlightmask(vtxarray *va);
+extern int setdynlights(vtxarray *va);
+extern bool getdynlight(int n, vec &o, float &radius, vec &color);
+
+// material
+
+extern int showmat;
+
+extern int findmaterial(const char *name);
+extern const char *findmaterialname(int mat);
+extern const char *getmaterialdesc(int mat, const char *prefix = "");
+extern void genmatsurfs(const cube &c, const ivec &co, int size, vector<materialsurface> &matsurfs);
+extern void rendermatsurfs(materialsurface *matbuf, int matsurfs);
+extern void rendermatgrid(materialsurface *matbuf, int matsurfs);
+extern int optimizematsurfs(materialsurface *matbuf, int matsurfs);
+extern void setupmaterials(int start = 0, int len = 0);
+extern void rendermaterials();
+extern int visiblematerial(const cube &c, int orient, const ivec &co, int size, ushort matmask = MATF_VOLUME);
+
+// water
+extern int refracting, refractfog;
+extern bool reflecting, fading, fogging;
+extern float reflectz;
+extern int reflectdist, vertwater, waterrefract, waterreflect, waterfade, caustics, waterfallrefract;
+
+#define GETMATIDXVAR(name, var, type) \
+    type get##name##var(int mat) \
+    { \
+        switch(mat&MATF_INDEX) \
+        { \
+            default: case 0: return name##var; \
+            case 1: return name##2##var; \
+            case 2: return name##3##var; \
+            case 3: return name##4##var; \
+        } \
+    }
+
+extern const bvec &getwatercolor(int mat);
+extern const bvec &getwaterfallcolor(int mat);
+extern int getwaterfog(int mat);
+extern const bvec &getlavacolor(int mat);
+extern int getlavafog(int mat);
+extern const bvec &getglasscolor(int mat);
+
+extern void cleanreflections();
+extern void queryreflections();
+extern void drawreflections();
+extern void renderwater();
+extern void setuplava(Texture *tex, float scale);
+extern void renderlava(const materialsurface &m);
+extern void flushlava();
+extern void loadcaustics(bool force = false);
+extern void preloadwatershaders(bool force = false);
+
+// glare
+extern bool glaring;
+
+extern void drawglaretex();
+extern void addglare();
+
+// depthfx
+extern bool depthfxing;
+
+extern void drawdepthfxtex();
+
+// server
+extern vector<const char *> gameargs;
+
+extern void initserver(bool listen, bool dedicated);
+extern void cleanupserver();
+extern void serverslice(bool dedicated, uint timeout);
+extern void updatetime();
+
+extern ENetSocket connectmaster(bool wait);
+extern void localclienttoserver(int chan, ENetPacket *);
+extern void localconnect();
+extern bool serveroption(char *opt);
+
+// serverbrowser
+extern bool resolverwait(const char *name, ENetAddress *address);
+extern int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address);
+extern void addserver(const char *name, int port = 0, const char *password = NULL, bool keep = false);
+extern void writeservercfg();
+
+// client
+extern void localdisconnect(bool cleanup = true);
+extern void localservertoclient(int chan, ENetPacket *packet);
+extern void connectserv(const char *servername, int port, const char *serverpassword);
+extern void abortconnect();
+extern void clientkeepalive();
+
+// command
+extern hashnameset<ident> idents;
+extern int identflags;
+
+extern void clearoverrides();
+extern void writecfg(const char *name = NULL);
+
+extern void checksleep(int millis);
+extern void clearsleep(bool clearoverrides = true);
+
+// console
+extern void processtextinput(const char *str, int len);
+extern void processkey(int code, bool isdown, int modstate = 0);
+extern int rendercommand(int x, int y, int w);
+extern int renderconsole(int w, int h, int abovehud);
+extern void conoutf(const char *s, ...) PRINTFARGS(1, 2);
+extern void conoutf(int type, const char *s, ...) PRINTFARGS(2, 3);
+extern void resetcomplete();
+extern void complete(char *s, int maxlen, const char *cmdprefix);
+const char *getkeyname(int code);
+extern const char *addreleaseaction(char *s);
+extern void writebinds(stream *f);
+extern void writecompletions(stream *f);
+
+// main
+enum
+{
+    NOT_INITING = 0,
+    INIT_GAME,
+    INIT_LOAD,
+    INIT_RESET
+};
+extern int initing, numcpus;
+
+enum
+{
+    CHANGE_GFX   = 1<<0,
+    CHANGE_SOUND = 1<<1
+};
+extern bool initwarning(const char *desc, int level = INIT_RESET, int type = CHANGE_GFX);
+
+extern bool grabinput, minimized;
+
+extern bool interceptkey(int sym);
+
+extern float loadprogress;
+extern void renderbackground(const char *caption = NULL, Texture *mapshot = NULL, const char *mapname = NULL, const char *mapinfo = NULL, bool restore = false, bool force = false);
+extern void renderprogress(float bar, const char *text, GLuint tex = 0, bool background = false);
+
+extern void getfps(int &fps, int &bestdiff, int &worstdiff);
+extern void swapbuffers(bool overlay = true);
+extern int getclockmillis();
+
+enum { KR_CONSOLE = 1<<0, KR_GUI = 1<<1, KR_EDITMODE = 1<<2 };
+
+extern void keyrepeat(bool on, int mask = ~0);
+
+enum { TI_CONSOLE = 1<<0, TI_GUI = 1<<1 };
+
+extern void textinput(bool on, int mask = ~0);
+
+// menu
+extern void menuprocess();
+extern void addchange(const char *desc, int type);
+extern void clearchanges(int type);
+
+// physics
+extern void mousemove(int dx, int dy);
+extern bool overlapsdynent(const vec &o, float radius);
+extern void rotatebb(vec &center, vec &radius, int yaw);
+extern float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity *t = NULL);
+struct ShadowRayCache;
+extern ShadowRayCache *newshadowraycache();
+extern void freeshadowraycache(ShadowRayCache *&cache);
+extern void resetshadowraycache(ShadowRayCache *cache);
+extern float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radius, int mode, extentity *t = NULL);
+
+// world
+
+extern vector<int> outsideents;
+
+extern void entcancel();
+extern void entitiesinoctanodes();
+extern void attachentities();
+extern void freeoctaentities(cube &c);
+extern bool pointinsel(const selinfo &sel, const vec &o);
+
+extern void resetmap();
+extern void startmap(const char *name);
+
+// rendermodel
+struct mapmodelinfo { string name; model *m; };
+
+extern bool modelloaded(const char *name);
+extern void findanims(const char *pattern, vector<int> &anims);
+extern void loadskin(const char *dir, const char *altdir, Texture *&skin, Texture *&masks);
+extern mapmodelinfo *getmminfo(int i);
+extern void startmodelquery(occludequery *query);
+extern void endmodelquery();
+extern void preloadmodelshaders(bool force = false);
+extern void preloadusedmapmodels(bool msg = false, bool bih = false);
+
+static inline model *loadmapmodel(int n)
+{
+    extern vector<mapmodelinfo> mapmodels;
+    if(mapmodels.inrange(n))
+    {
+        model *m = mapmodels[n].m;
+        return m ? m : loadmodel(NULL, n);
+    }
+    return NULL;
+}
+
+// renderparticles
+extern void initparticles();
+extern void clearparticles();
+extern void clearparticleemitters();
+extern void seedparticles();
+extern void updateparticles();
+extern void renderparticles(bool mainpass = false);
+extern bool printparticles(extentity &e, char *buf, int len);
+
+// decal
+extern void initdecals();
+extern void cleardecals();
+extern void renderdecals(bool mainpass = false);
+
+// blob
+
+enum
+{
+    BLOB_STATIC = 0,
+    BLOB_DYNAMIC
+};
+
+extern int showblobs;
+
+extern void initblobs(int type = -1);
+extern void resetblobs();
+extern void renderblob(int type, const vec &o, float radius, float fade = 1);
+extern void flushblobs();
+
+// rendersky
+extern int explicitsky;
+extern double skyarea;
+extern char *skybox;
+
+extern void setupsky();
+extern void drawskybox(int farplane, bool limited, bool force = false);
+extern bool limitsky();
+extern bool shouldrenderskyenvmap();
+extern bool shouldclearskyboxglare();
+
+// 3dgui
+extern void g3d_render();
+extern void g3d_render2d();
+extern bool g3d_windowhit(bool on, bool act);
+extern bool g3d_key(int code, bool isdown);
+extern bool g3d_input(const char *str, int len);
+// menus
+extern int mainmenu;
+
+extern void clearmainmenu();
+extern void g3d_mainmenu();
+
+// sound
+extern void clearmapsounds();
+extern void checkmapsounds();
+extern void updatesounds();
+extern void preloadmapsounds();
+
+extern void initmumble();
+extern void closemumble();
+extern void updatemumble();
+
+// grass
+extern void generategrass();
+extern void rendergrass();
+extern void cleanupgrass();
+
+// blendmap
+extern int blendpaintmode;
+
+struct BlendMapCache;
+extern BlendMapCache *newblendmapcache();
+extern void freeblendmapcache(BlendMapCache *&cache);
+extern bool setblendmaporigin(BlendMapCache *cache, const ivec &o, int size);
+extern bool hasblendmap(BlendMapCache *cache);
+extern uchar lookupblendmap(BlendMapCache *cache, const vec &pos);
+extern void resetblendmap();
+extern void enlargeblendmap();
+extern void shrinkblendmap(int octant);
+extern void optimizeblendmap();
+extern void stoppaintblendmap();
+extern void trypaintblendmap();
+extern void renderblendbrush(GLuint tex, float x, float y, float w, float h);
+extern void renderblendbrush();
+extern bool loadblendmap(stream *f, int info);
+extern void saveblendmap(stream *f);
+extern uchar shouldsaveblendmap();
+
+// recorder
+
+namespace recorder
+{
+    extern void stop();
+    extern void capture(bool overlay = true);
+    extern void cleanup();
+}
+
+#endif
+
+#endif
+
diff --git a/src/engine/explosion.h b/src/engine/explosion.h
new file mode 100644 (file)
index 0000000..626d5bf
--- /dev/null
@@ -0,0 +1,249 @@
+namespace sphere
+{
+
+    struct vert
+    {
+        vec pos;
+        ushort s, t;
+    } *verts = NULL;
+    GLushort *indices = NULL;
+    int numverts = 0, numindices = 0;
+    GLuint vbuf = 0, ebuf = 0;
+
+    void init(int slices, int stacks)
+    {
+        numverts = (stacks+1)*(slices+1);
+        verts = new vert[numverts];
+        float ds = 1.0f/slices, dt = 1.0f/stacks, t = 1.0f;
+        loopi(stacks+1)
+        {
+            float rho = M_PI*(1-t), s = 0.0f, sinrho = i && i < stacks ? sin(rho) : 0, cosrho = !i ? 1 : (i < stacks ? cos(rho) : -1);
+            loopj(slices+1)
+            {
+                float theta = j==slices ? 0 : 2*M_PI*s;
+                vert &v = verts[i*(slices+1) + j];
+                v.pos = vec(-sin(theta)*sinrho, cos(theta)*sinrho, cosrho);
+                v.s = ushort(s*0xFFFF);
+                v.t = ushort(t*0xFFFF);
+                s += ds;
+            }
+            t -= dt;
+        }
+
+        numindices = (stacks-1)*slices*3*2;
+        indices = new ushort[numindices];
+        GLushort *curindex = indices;
+        loopi(stacks)
+        {
+            loopk(slices)
+            {
+                int j = i%2 ? slices-k-1 : k;
+                if(i)
+                {
+                    *curindex++ = i*(slices+1)+j;
+                    *curindex++ = (i+1)*(slices+1)+j;
+                    *curindex++ = i*(slices+1)+j+1;
+                }
+                if(i+1 < stacks)
+                {
+                    *curindex++ = i*(slices+1)+j+1;
+                    *curindex++ = (i+1)*(slices+1)+j;
+                    *curindex++ = (i+1)*(slices+1)+j+1;
+                }
+            }
+        }
+
+        if(!vbuf) glGenBuffers_(1, &vbuf);
+        gle::bindvbo(vbuf);
+        glBufferData_(GL_ARRAY_BUFFER, numverts*sizeof(vert), verts, GL_STATIC_DRAW);
+        DELETEA(verts);
+
+        if(!ebuf) glGenBuffers_(1, &ebuf);
+        gle::bindebo(ebuf);
+        glBufferData_(GL_ELEMENT_ARRAY_BUFFER, numindices*sizeof(GLushort), indices, GL_STATIC_DRAW);
+        DELETEA(indices);
+    }
+
+    void enable()
+    {
+        if(!vbuf) init(12, 6);
+
+        gle::bindvbo(vbuf);
+        gle::bindebo(ebuf);
+   
+        gle::vertexpointer(sizeof(vert), &verts->pos);
+        gle::texcoord0pointer(sizeof(vert), &verts->s, GL_UNSIGNED_SHORT, 2, GL_TRUE);
+        gle::enablevertex();
+        gle::enabletexcoord0();
+    }
+
+    void draw()
+    {
+        glDrawRangeElements_(GL_TRIANGLES, 0, numverts-1, numindices, GL_UNSIGNED_SHORT, indices);
+        xtraverts += numindices;
+        glde++;
+    }
+
+    void disable()
+    {
+        gle::disablevertex();
+        gle::disabletexcoord0();
+
+        gle::clearvbo();
+        gle::clearebo();
+    }
+
+    void cleanup()
+    {
+        if(vbuf) { glDeleteBuffers_(1, &vbuf); vbuf = 0; }
+        if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+    }
+}
+
+static const float WOBBLE = 1.25f;
+
+struct fireballrenderer : listrenderer
+{
+    fireballrenderer(const char *texname)
+        : listrenderer(texname, 0, PT_FIREBALL|PT_GLARE|PT_SHADER)
+    {}
+
+    void startrender()
+    {
+        if(glaring) SETSHADER(explosionglare);
+        else if(!reflecting && !refracting && depthfx && depthfxtex.rendertex && numdepthfxranges>0)
+        {
+            if(!depthfxtex.highprecision()) SETSHADER(explosionsoft8);
+            else SETSHADER(explosionsoft);
+        }
+        else SETSHADER(explosion);
+
+        sphere::enable();
+    }
+
+    void endrender()
+    {
+        sphere::disable();
+    }
+
+    void cleanup()
+    {
+        sphere::cleanup();
+    }
+
+    int finddepthfxranges(void **owners, float *ranges, int numranges, int maxranges, vec &bbmin, vec &bbmax)
+    {
+        static struct fireballent : physent
+        {
+            fireballent()
+            {
+                type = ENT_CAMERA;
+            }
+        } e;
+
+        for(listparticle *p = list; p; p = p->next)
+        {
+            int ts = p->fade <= 5 ? 1 : lastmillis-p->millis;
+            float pmax = p->val,
+                  size = p->fade ? float(ts)/p->fade : 1,
+                  psize = (p->size + pmax * size)*WOBBLE;
+            if(2*(p->size + pmax)*WOBBLE < depthfxblend ||
+               (!depthfxtex.highprecision() && !depthfxtex.emulatehighprecision() && psize > depthfxscale - depthfxbias) ||
+               isfoggedsphere(psize, p->o)) continue;
+
+            e.o = p->o;
+            e.radius = e.xradius = e.yradius = e.eyeheight = e.aboveeye = psize;
+            if(!::collide(&e, vec(0, 0, 0), 0, false)) continue;
+
+            if(depthfxscissor==2 && !depthfxtex.addscissorbox(p->o, psize)) continue;
+
+            vec dir = camera1->o;
+            dir.sub(p->o);
+            float dist = dir.magnitude();
+            dir.mul(psize/dist).add(p->o);
+            float depth = depthfxtex.eyedepth(dir);
+
+            loopk(3)
+            {
+                bbmin[k] = min(bbmin[k], p->o[k] - psize);
+                bbmax[k] = max(bbmax[k], p->o[k] + psize);
+            }
+
+            int pos = numranges;
+            loopi(numranges) if(depth < ranges[i]) { pos = i; break; }
+            if(pos >= maxranges) continue;
+
+            if(numranges > pos)
+            {
+                int moved = min(numranges-pos, maxranges-(pos+1));
+                memmove(&ranges[pos+1], &ranges[pos], moved*sizeof(float));
+                memmove(&owners[pos+1], &owners[pos], moved*sizeof(void *));
+            }
+            if(numranges < maxranges) numranges++;
+
+            ranges[pos] = depth;
+            owners[pos] = p;
+        }
+
+        return numranges;
+    }
+
+    void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+    {
+        pe.maxfade = max(pe.maxfade, fade);
+        pe.extendbb(o, (size+1+pe.ent->attr2)*WOBBLE); 
+    }
+
+    void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+    {
+        float pmax = p->val,
+              size = p->fade ? float(ts)/p->fade : 1,
+              psize = p->size + pmax * size;
+
+        if(isfoggedsphere(psize*WOBBLE, p->o)) return;
+
+        vec dir = vec(o).sub(camera1->o), s, t;
+        float dist = dir.magnitude();
+        bool inside = dist <= psize*WOBBLE;
+        if(inside)
+        {
+            s = camright;
+            t = camup;
+        }
+        else
+        {
+            if(reflecting) { dir.z = o.z - reflectz; dist = dir.magnitude(); }
+            float mag2 = dir.magnitude2();
+            dir.x /= mag2;
+            dir.y /= mag2;
+            dir.z /= dist;
+            s = vec(dir.y, -dir.x, 0);
+            t = vec(dir.x*dir.z, dir.y*dir.z, -mag2/dist);
+        }
+
+        matrix3 rot(lastmillis/1000.0f*143*RAD, vec(1/SQRT3, 1/SQRT3, 1/SQRT3));
+        LOCALPARAM(texgenS, rot.transposedtransform(s));
+        LOCALPARAM(texgenT, rot.transposedtransform(t));
+
+        matrix4 m(rot, o);
+        m.scale(psize, psize, inside ? -psize : psize);
+        m.mul(camprojmatrix, m);
+        LOCALPARAM(explosionmatrix, m);
+
+        LOCALPARAM(center, o);
+        LOCALPARAMF(millis, lastmillis/1000.0f);
+        LOCALPARAMF(blendparams, inside ? 0.5f : 4, inside ? 0.25f : 0);
+        binddepthfxparams(depthfxblend, inside ? blend/(2*255.0f) : 0, 2*(p->size + pmax)*WOBBLE >= depthfxblend, p);
+
+        int passes = !reflecting && !refracting && inside ? 2 : 1;
+        loopi(passes)
+        {
+            gle::color(p->color, i ? blend/2 : blend);
+            if(i) glDepthFunc(GL_GEQUAL);
+            sphere::draw();
+            if(i) glDepthFunc(GL_LESS);
+        }
+    }
+};
+static fireballrenderer fireballs("packages/particles/explosion.png"), bluefireballs("packages/particles/plasma.png");
+
diff --git a/src/engine/glare.cpp b/src/engine/glare.cpp
new file mode 100644 (file)
index 0000000..dc639ad
--- /dev/null
@@ -0,0 +1,71 @@
+#include "engine.h"
+#include "rendertarget.h"
+
+static struct glaretexture : rendertarget
+{
+    bool dorender()
+    {
+        extern void drawglare();
+        drawglare();
+        return true;
+    }
+} glaretex;
+
+void cleanupglare()
+{
+    glaretex.cleanup(true);
+}
+
+VARFP(glaresize, 6, 8, 10, cleanupglare());
+VARP(glare, 0, 0, 1);
+VARP(blurglare, 0, 4, 7);
+VARP(blurglareaspect, 0, 1, 1);
+VARP(blurglaresigma, 1, 50, 200);
+
+VAR(debugglare, 0, 0, 1);
+
+void viewglaretex()
+{
+    if(!glare) return;
+    glaretex.debug();
+}
+
+bool glaring = false;
+
+void drawglaretex()
+{
+    if(!glare) return;
+
+    int w = 1<<glaresize, h = 1<<glaresize, blury = blurglare;
+    if(blurglare && blurglareaspect)
+    {
+        while(h > (1<<5) && (screenw*h)/w >= (screenh*4)/3) h /= 2;
+        blury = ((1 + 4*blurglare)*(screenw*h)/w + screenh*2)/(screenh*4);
+        blury = clamp(blury, 1, MAXBLURRADIUS);
+    }
+
+    glaretex.render(w, h, blurglare, blurglaresigma/100.0f, blury);
+}
+
+FVAR(glaremod, 0.5f, 0.75f, 1);
+FVARP(glarescale, 0, 1, 8);
+
+void addglare()
+{
+    if(!glare) return;
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_ONE, GL_ONE);
+
+    SETSHADER(screenrect);
+
+    glBindTexture(GL_TEXTURE_2D, glaretex.rendertex);
+
+    float g = glarescale*glaremod;
+    gle::colorf(g, g, g);
+
+    screenquad(1, 1);
+
+    glDisable(GL_BLEND);
+}
+     
diff --git a/src/engine/grass.cpp b/src/engine/grass.cpp
new file mode 100644 (file)
index 0000000..f91b6dc
--- /dev/null
@@ -0,0 +1,356 @@
+#include "engine.h"
+
+VARP(grass, 0, 0, 1);
+VAR(dbggrass, 0, 0, 1);
+VARP(grassdist, 0, 256, 10000);
+FVARP(grasstaper, 0, 0.2, 1);
+FVARP(grassstep, 0.5, 3, 8);
+VARP(grassheight, 1, 4, 64);
+VARP(grassmargin, 0, 8, 32);
+FVAR(grassmarginfade, 0, 0.5f, 1);
+
+#define NUMGRASSWEDGES 8
+
+static struct grasswedge
+{
+    vec dir, across, edge1, edge2;
+    plane bound1, bound2;
+    bvec4 vertbounds;
+
+    grasswedge(int i) :
+      dir(2*M_PI*(i+0.5f)/float(NUMGRASSWEDGES), 0),
+      across(2*M_PI*((i+0.5f)/float(NUMGRASSWEDGES) + 0.25f), 0),
+      edge1(vec(2*M_PI*i/float(NUMGRASSWEDGES), 0).div(cos(M_PI/NUMGRASSWEDGES))),
+      edge2(vec(2*M_PI*(i+1)/float(NUMGRASSWEDGES), 0).div(cos(M_PI/NUMGRASSWEDGES))),
+      bound1(vec(2*M_PI*(i/float(NUMGRASSWEDGES) - 0.25f), 0), 0),
+      bound2(vec(2*M_PI*((i+1)/float(NUMGRASSWEDGES) + 0.25f), 0), 0)
+    {
+        across.div(-across.dot(bound1));
+
+        bvec vertbound1(bound1), vertbound2(bound2);
+        vertbounds = bvec4(vertbound1.x, vertbound1.y, vertbound2.x, vertbound2.y);
+        vertbounds.flip();
+    }
+} grasswedges[NUMGRASSWEDGES] = { 0, 1, 2, 3, 4, 5, 6, 7 };
+
+struct grassvert
+{
+    vec pos;
+    bvec4 color;
+    vec2 tc;
+    svec2 lm;
+    bvec4 bounds;
+};
+
+static vector<grassvert> grassverts;
+static GLuint grassvbo = 0;
+static int grassvbosize = 0;
+
+struct grassgroup
+{
+    const grasstri *tri;
+    float dist;
+    int tex, lmtex, offset, numquads;
+};
+
+static vector<grassgroup> grassgroups;
+
+#define NUMGRASSOFFSETS 32
+
+static float grassoffsets[NUMGRASSOFFSETS] = { -1 }, grassanimoffsets[NUMGRASSOFFSETS];
+static int lastgrassanim = -1;
+
+VARR(grassanimmillis, 0, 3000, 60000);
+FVARR(grassanimscale, 0, 0.03f, 1);
+
+static void animategrass()
+{
+    loopi(NUMGRASSOFFSETS) grassanimoffsets[i] = grassanimscale*sinf(2*M_PI*(grassoffsets[i] + lastmillis/float(grassanimmillis)));
+    lastgrassanim = lastmillis;
+}
+
+VARR(grassscale, 1, 2, 64);
+bvec grasscolor(255, 255, 255);
+HVARFR(grasscolour, 0, 0xFFFFFF, 0xFFFFFF,
+{
+    if(!grasscolour) grasscolour = 0xFFFFFF;
+    grasscolor = bvec((grasscolour>>16)&0xFF, (grasscolour>>8)&0xFF, grasscolour&0xFF);
+});
+FVARR(grassalpha, 0, 1, 1);
+
+static void gengrassquads(grassgroup *&group, const grasswedge &w, const grasstri &g, Texture *tex)
+{
+    float t = camera1->o.dot(w.dir);
+    int tstep = int(ceil(t/grassstep));
+    float tstart = tstep*grassstep,
+          t0 = w.dir.dot(g.v[0]), t1 = w.dir.dot(g.v[1]), t2 = w.dir.dot(g.v[2]), t3 = w.dir.dot(g.v[3]),
+          tmin = min(min(t0, t1), min(t2, t3)),
+          tmax = max(max(t0, t1), max(t2, t3));
+    if(tmax < tstart || tmin > t + grassdist) return;
+
+    int minstep = max(int(ceil(tmin/grassstep)) - tstep, 1),
+        maxstep = int(floor(min(tmax, t + grassdist)/grassstep)) - tstep,
+        numsteps = maxstep - minstep + 1;
+    
+    float texscale = (grassscale*tex->ys)/float(grassheight*tex->xs), animscale = grassheight*texscale;
+    vec tc;
+    tc.cross(g.surface, w.dir).mul(texscale);
+
+    int offset = tstep + maxstep;
+    if(offset < 0) offset = NUMGRASSOFFSETS - (-offset)%NUMGRASSOFFSETS;
+    offset += numsteps + NUMGRASSOFFSETS - numsteps%NUMGRASSOFFSETS;
+
+    float leftdist = t0;
+    const vec *leftv = &g.v[0];
+    if(t1 > leftdist) { leftv = &g.v[1]; leftdist = t1; }
+    if(t2 > leftdist) { leftv = &g.v[2]; leftdist = t2; }
+    if(t3 > leftdist) { leftv = &g.v[3]; leftdist = t3; }
+    float rightdist = leftdist;
+    const vec *rightv = leftv;
+
+    vec across(w.across.x, w.across.y, g.surface.zdelta(w.across)), leftdir(0, 0, 0), rightdir(0, 0, 0), leftp = *leftv, rightp = *rightv;
+    float taperdist = grassdist*grasstaper,
+          taperscale = 1.0f / (grassdist - taperdist),
+          dist = maxstep*grassstep + tstart,
+          leftb = 0, rightb = 0, leftdb = 0, rightdb = 0;
+    for(int i = maxstep; i >= minstep; i--, offset--, leftp.add(leftdir), rightp.add(rightdir), leftb += leftdb, rightb += rightdb, dist -= grassstep)
+    {
+        if(dist <= leftdist)
+        {
+            const vec *prev = leftv;
+            float prevdist = leftdist;
+            if(--leftv < g.v) leftv += g.numv;
+            leftdist = leftv->dot(w.dir);
+            if(dist <= leftdist)
+            {
+                prev = leftv;
+                prevdist = leftdist;
+                if(--leftv < g.v) leftv += g.numv;
+                leftdist = leftv->dot(w.dir);
+            }
+            leftdir = vec(*leftv).sub(*prev);
+            leftdir.mul(grassstep/-w.dir.dot(leftdir));
+            leftp = vec(leftdir).mul((prevdist - dist)/grassstep).add(*prev);
+            leftb = w.bound1.dist(leftp);
+            leftdb = w.bound1.dot(leftdir);
+        }
+        if(dist <= rightdist)
+        {
+            const vec *prev = rightv;
+            float prevdist = rightdist;
+            if(++rightv >= &g.v[g.numv]) rightv = g.v;
+            rightdist = rightv->dot(w.dir);
+            if(dist <= rightdist) 
+            {
+                prev = rightv;
+                prevdist = rightdist;
+                if(++rightv >= &g.v[g.numv]) rightv = g.v;
+                rightdist = rightv->dot(w.dir);
+            }
+            rightdir = vec(*rightv).sub(*prev);
+            rightdir.mul(grassstep/-w.dir.dot(rightdir));
+            rightp = vec(rightdir).mul((prevdist - dist)/grassstep).add(*prev);
+            rightb = w.bound2.dist(rightp);
+            rightdb = w.bound2.dot(rightdir);
+        }
+        vec p1 = leftp, p2 = rightp;
+        if(leftb > grassmargin)
+        {
+            if(w.bound1.dist(p2) >= grassmargin) continue;
+            p1.add(vec(across).mul(leftb - grassmargin));
+        } 
+        if(rightb > grassmargin)
+        {
+            if(w.bound2.dist(p1) >= grassmargin) continue;
+            p2.sub(vec(across).mul(rightb - grassmargin));
+        }
+
+        if(!group)
+        {
+            group = &grassgroups.add();
+            group->tri = &g;
+            group->tex = tex->id;
+            extern bool brightengeom;
+            extern int fullbright;
+            int lmid = brightengeom && (g.lmid < LMID_RESERVED || (fullbright && editmode)) ? LMID_BRIGHT : g.lmid;
+            group->lmtex = lightmaptexs.inrange(lmid) ? lightmaptexs[lmid].id : notexture->id;
+            group->offset = grassverts.length()/4;
+            group->numquads = 0;
+            if(lastgrassanim!=lastmillis) animategrass();
+        }
+  
+        group->numquads++;
+        float tcoffset = grassoffsets[offset%NUMGRASSOFFSETS],
+              animoffset = animscale*grassanimoffsets[offset%NUMGRASSOFFSETS],
+              tc1 = tc.dot(p1) + tcoffset, tc2 = tc.dot(p2) + tcoffset,
+              fade = dist - t > taperdist ? (grassdist - (dist - t))*taperscale : 1,
+              height = grassheight * fade;
+        svec2 lm1(short(g.tcu.dot(p1)), short(g.tcv.dot(p1))),
+              lm2(short(g.tcu.dot(p2)), short(g.tcv.dot(p2)));
+        bvec4 color(grasscolor, uchar(fade*grassalpha*255));
+
+        #define GRASSVERT(n, tcv, modify) { \
+            grassvert &gv = grassverts.add(); \
+            gv.pos = p##n; \
+            gv.color = color; \
+            gv.tc = vec2(tc##n, tcv); \
+            gv.lm = lm##n; \
+            gv.bounds = w.vertbounds; \
+            modify; \
+        }
+    
+        GRASSVERT(2, 0, { gv.pos.z += height; gv.tc.x += animoffset; });
+        GRASSVERT(1, 0, { gv.pos.z += height; gv.tc.x += animoffset; });
+        GRASSVERT(1, 1, );
+        GRASSVERT(2, 1, );
+    }
+}             
+
+static void gengrassquads(vtxarray *va)
+{
+    loopv(va->grasstris)
+    {
+        grasstri &g = va->grasstris[i];
+        if(isfoggedsphere(g.radius, g.center)) continue;
+        float dist = g.center.dist(camera1->o);
+        if(dist - g.radius > grassdist) continue;
+            
+        Slot &s = *lookupvslot(g.texture, false).slot;
+        if(!s.grasstex) 
+        {
+            if(!s.autograss) continue;
+            s.grasstex = textureload(s.autograss, 2);
+        }
+
+        grassgroup *group = NULL;
+        loopi(NUMGRASSWEDGES)
+        {
+            grasswedge &w = grasswedges[i];    
+            if(w.bound1.dist(g.center) > g.radius + grassmargin || w.bound2.dist(g.center) > g.radius + grassmargin) continue;
+            gengrassquads(group, w, g, s.grasstex);
+        }
+        if(group) group->dist = dist;
+    }
+}
+
+static inline bool comparegrassgroups(const grassgroup &x, const grassgroup &y)
+{
+    return x.dist > y.dist;
+}
+
+void generategrass()
+{
+    if(!grass || !grassdist) return;
+
+    grassgroups.setsize(0);
+    grassverts.setsize(0);
+
+    if(grassoffsets[0] < 0) loopi(NUMGRASSOFFSETS) grassoffsets[i] = rnd(0x1000000)/float(0x1000000);
+
+    loopi(NUMGRASSWEDGES)
+    {
+        grasswedge &w = grasswedges[i];
+        w.bound1.offset = -camera1->o.dot(w.bound1);
+        w.bound2.offset = -camera1->o.dot(w.bound2);
+    }
+
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(va->grasstris.empty() || va->occluded >= OCCLUDE_GEOM) continue;
+        if(va->distance > grassdist) continue;
+        if(reflecting || refracting>0 ? va->o.z+va->size<reflectz : va->o.z>=reflectz) continue;
+        gengrassquads(va);
+    }
+
+    if(grassgroups.empty()) return;
+
+    grassgroups.sort(comparegrassgroups);
+
+    if(!grassvbo) glGenBuffers_(1, &grassvbo);
+    gle::bindvbo(grassvbo);
+    int size = grassverts.length()*sizeof(grassvert);
+    grassvbosize = max(grassvbosize, size);
+    glBufferData_(GL_ARRAY_BUFFER, grassvbosize, size == grassvbosize ? grassverts.getbuf() : NULL, GL_STREAM_DRAW);
+    if(size != grassvbosize) glBufferSubData_(GL_ARRAY_BUFFER, 0, size, grassverts.getbuf());
+    gle::clearvbo();
+}
+
+void rendergrass()
+{
+    if(!grass || !grassdist || grassgroups.empty() || dbggrass) return;
+
+    glDisable(GL_CULL_FACE);
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+    glDepthMask(GL_FALSE);
+
+    GLOBALPARAM(camera, camera1->o);
+
+    SETSHADER(grass);
+
+    LOCALPARAMF(grassmargin, grassmargin, grassmargin ? grassmarginfade / grassmargin : 0.0f, grassmargin ? grassmarginfade : 1.0f);
+    gle::bindvbo(grassvbo);
+
+    const grassvert *ptr = 0;
+    gle::vertexpointer(sizeof(grassvert), ptr->pos.v);
+    gle::colorpointer(sizeof(grassvert), ptr->color.v);
+    gle::texcoord0pointer(sizeof(grassvert), ptr->tc.v);
+    gle::texcoord1pointer(sizeof(grassvert), ptr->lm.v, GL_SHORT);
+    gle::tangentpointer(sizeof(grassvert), ptr->bounds.v, GL_BYTE);
+    gle::enablevertex();
+    gle::enablecolor();
+    gle::enabletexcoord0();
+    gle::enabletexcoord1();
+    gle::enabletangent();
+    gle::enablequads();
+
+    int texid = -1, lmtexid = -1;
+    loopv(grassgroups)
+    {
+        grassgroup &g = grassgroups[i];
+
+        if(reflecting || refracting)
+        {
+            if(refracting < 0 ? g.tri->minz > reflectz : g.tri->maxz + grassheight < reflectz) continue;
+            if(isfoggedsphere(g.tri->radius, g.tri->center)) continue;
+        }
+
+        if(texid != g.tex)
+        {
+            glBindTexture(GL_TEXTURE_2D, g.tex);
+            texid = g.tex;
+        }
+        if(lmtexid != g.lmtex)
+        {
+            glActiveTexture_(GL_TEXTURE1);
+            glBindTexture(GL_TEXTURE_2D, g.lmtex);
+            glActiveTexture_(GL_TEXTURE0);
+            lmtexid = g.lmtex;
+        }
+
+        gle::drawquads(g.offset, g.numquads);
+        xtravertsva += 4*g.numquads;
+    }
+
+    gle::disablequads();
+    gle::disablevertex();
+    gle::disablecolor();
+    gle::disabletexcoord0();
+    gle::disabletexcoord1();
+    gle::disabletangent();
+
+    gle::clearvbo();
+
+    glDisable(GL_BLEND);
+    glDepthMask(GL_TRUE);
+    glEnable(GL_CULL_FACE);
+}
+
+void cleanupgrass()
+{   
+    if(grassvbo) { glDeleteBuffers_(1, &grassvbo); grassvbo = 0; }
+    grassvbosize = 0;
+}
+
diff --git a/src/engine/lensflare.h b/src/engine/lensflare.h
new file mode 100644 (file)
index 0000000..1618e52
--- /dev/null
@@ -0,0 +1,193 @@
+static struct flaretype
+{
+    int type;             /* flaretex index, 0..5, -1 for 6+random shine */
+    float loc;            /* postion on axis */
+    float scale;          /* texture scaling */
+    uchar alpha;          /* color alpha */
+} flaretypes[] =
+{
+    {2,  1.30f, 0.04f, 153}, //flares
+    {3,  1.00f, 0.10f, 102},
+    {1,  0.50f, 0.20f, 77},
+    {3,  0.20f, 0.05f, 77},
+    {0,  0.00f, 0.04f, 77},
+    {5, -0.25f, 0.07f, 127},
+    {5, -0.40f, 0.02f, 153},
+    {5, -0.60f, 0.04f, 102},
+    {5, -1.00f, 0.03f, 51},
+    {-1, 1.00f, 0.30f, 255}, //shine - red, green, blue
+    {-2, 1.00f, 0.20f, 255},
+    {-3, 1.00f, 0.25f, 255}
+};
+
+struct flare
+{
+    vec o, center;
+    float size;
+    bvec color;
+    bool sparkle;
+};
+
+VAR(flarelights, 0, 0, 1);
+VARP(flarecutoff, 0, 1000, 10000);
+VARP(flaresize, 20, 100, 500);
+
+struct flarerenderer : partrenderer
+{
+    int maxflares, numflares;
+    unsigned int shinetime;
+    flare *flares;
+
+    flarerenderer(const char *texname, int maxflares)
+        : partrenderer(texname, 3, PT_FLARE|PT_SHADER), maxflares(maxflares), numflares(0), shinetime(0)
+    {
+        flares = new flare[maxflares];
+    }
+    ~flarerenderer()
+    {
+        delete[] flares;
+    }
+
+    void reset()
+    {
+        numflares = 0;
+    }
+
+    void newflare(vec &o,  const vec &center, uchar r, uchar g, uchar b, float mod, float size, bool sun, bool sparkle)
+    {
+        if(numflares >= maxflares) return;
+        vec target; //occlusion check (neccessary as depth testing is turned off)
+        if(!raycubelos(o, camera1->o, target)) return;
+        flare &f = flares[numflares++];
+        f.o = o;
+        f.center = center;
+        f.size = size;
+        f.color = bvec(uchar(r*mod), uchar(g*mod), uchar(b*mod));
+        f.sparkle = sparkle;
+    }
+
+    void addflare(vec &o, uchar r, uchar g, uchar b, bool sun, bool sparkle)
+    {
+        //frustrum + fog check
+        if(isvisiblesphere(0.0f, o) > (sun?VFC_FOGGED:VFC_FULL_VISIBLE)) return;
+        //find closest point between camera line of sight and flare pos
+        vec flaredir = vec(o).sub(camera1->o);
+        vec center = vec(camdir).mul(flaredir.dot(camdir)).add(camera1->o);
+        float mod, size;
+        if(sun) //fixed size
+        {
+            mod = 1.0;
+            size = flaredir.magnitude() * flaresize / 100.0f;
+        }
+        else
+        {
+            mod = (flarecutoff-vec(o).sub(center).squaredlen())/flarecutoff;
+            if(mod < 0.0f) return;
+            size = flaresize / 5.0f;
+        }
+        newflare(o, center, r, g, b, mod, size, sun, sparkle);
+    }
+
+    void makelightflares()
+    {
+        numflares = 0; //regenerate flarelist each frame
+        shinetime = lastmillis/10;
+
+        if(editmode || !flarelights) return;
+
+        const vector<extentity *> &ents = entities::getents();
+        extern const vector<int> &checklightcache(int x, int y);
+        const vector<int> &lights = checklightcache(int(camera1->o.x), int(camera1->o.y));
+        loopv(lights)
+        {
+            entity &e = *ents[lights[i]];
+            if(e.type != ET_LIGHT) continue;
+            bool sun = (e.attr1==0);
+            float radius = float(e.attr1);
+            vec flaredir = vec(e.o).sub(camera1->o);
+            float len = flaredir.magnitude();
+            if(!sun && (len > radius)) continue;
+            if(isvisiblesphere(0.0f, e.o) > (sun?VFC_FOGGED:VFC_FULL_VISIBLE)) continue;
+            vec center = vec(camdir).mul(flaredir.dot(camdir)).add(camera1->o);
+            float mod, size;
+            if(sun) //fixed size
+            {
+                mod = 1.0;
+                size = len * flaresize / 100.0f;
+            }
+            else
+            {
+                mod = (radius-len)/radius;
+                size = flaresize / 5.0f;
+            }
+            newflare(e.o, center, e.attr2, e.attr3, e.attr4, mod, size, sun, sun);
+        }
+    }
+
+    int count()
+    {
+        return numflares;
+    }
+
+    bool haswork()
+    {
+        return (numflares != 0) && !glaring && !reflecting  && !refracting;
+    }
+
+    void render()
+    {
+        textureshader->set();
+        glDisable(GL_DEPTH_TEST);
+        if(!tex) tex = textureload(texname);
+        glBindTexture(GL_TEXTURE_2D, tex->id);
+        gle::defattrib(gle::ATTRIB_VERTEX, 3, GL_FLOAT);
+        gle::defattrib(gle::ATTRIB_TEXCOORD0, 2, GL_FLOAT);
+        gle::defattrib(gle::ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE);
+        gle::begin(GL_QUADS);
+        loopi(numflares)
+        {
+            const flare &f = flares[i];
+            vec center = f.center;
+            vec axis = vec(f.o).sub(center);
+            bvec4 color(f.color, 255);
+            loopj(f.sparkle?12:9)
+            {
+                const flaretype &ft = flaretypes[j];
+                vec o = vec(axis).mul(ft.loc).add(center);
+                float sz = ft.scale * f.size;
+                int tex = ft.type;
+                if(ft.type < 0) //sparkles - always done last
+                {
+                    shinetime = (shinetime + 1) % 10;
+                    tex = 6+shinetime;
+                    color.r = 0;
+                    color.g = 0;
+                    color.b = 0;
+                    color[-ft.type-1] = f.color[-ft.type-1]; //only want a single channel
+                }
+                color.a = ft.alpha;
+                const float tsz = 0.25; //flares are aranged in 4x4 grid
+                float tx = tsz*(tex&0x03), ty = tsz*((tex>>2)&0x03);
+                gle::attribf(o.x+(-camright.x+camup.x)*sz, o.y+(-camright.y+camup.y)*sz, o.z+(-camright.z+camup.z)*sz);
+                    gle::attribf(tx,     ty+tsz);
+                    gle::attrib(color);
+                gle::attribf(o.x+( camright.x+camup.x)*sz, o.y+( camright.y+camup.y)*sz, o.z+( camright.z+camup.z)*sz);
+                    gle::attribf(tx+tsz, ty+tsz);
+                    gle::attrib(color);
+                gle::attribf(o.x+( camright.x-camup.x)*sz, o.y+( camright.y-camup.y)*sz, o.z+( camright.z-camup.z)*sz);
+                    gle::attribf(tx+tsz, ty);
+                    gle::attrib(color);
+                gle::attribf(o.x+(-camright.x-camup.x)*sz, o.y+(-camright.y-camup.y)*sz, o.z+(-camright.z-camup.z)*sz);
+                    gle::attribf(tx,     ty);
+                    gle::attrib(color);
+            }
+        }
+        gle::end();
+        glEnable(GL_DEPTH_TEST);
+    }
+
+    //square per round hole - use addflare(..) instead
+    particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) { return NULL; }
+};
+static flarerenderer flares("<grey>packages/particles/lensflares.png", 64);
+
diff --git a/src/engine/lightmap.cpp b/src/engine/lightmap.cpp
new file mode 100644 (file)
index 0000000..b4090e2
--- /dev/null
@@ -0,0 +1,2729 @@
+#include "engine.h"
+
+#define MAXLIGHTMAPTASKS 4096
+#define LIGHTMAPBUFSIZE (2*1024*1024)
+
+struct lightmapinfo;
+struct lightmaptask;
+
+struct lightmapworker
+{
+    uchar *buf;
+    int bufstart, bufused;
+    lightmapinfo *firstlightmap, *lastlightmap, *curlightmaps;
+    cube *c;
+    cubeext *ext;
+    uchar *colorbuf;
+    bvec *raybuf;
+    uchar *ambient, *blur;
+    vec *colordata, *raydata;
+    int type, bpp, w, h, orient, rotate;
+    VSlot *vslot;
+    Slot *slot;
+    vector<const extentity *> lights;
+    ShadowRayCache *shadowraycache;
+    BlendMapCache *blendmapcache;
+    bool needspace, doneworking;
+    SDL_cond *spacecond;
+    SDL_Thread *thread;
+
+    lightmapworker();
+    ~lightmapworker();
+
+    void reset();
+    bool setupthread();
+    void cleanupthread();
+
+    static int work(void *data);
+};
+
+struct lightmapinfo
+{
+    lightmapinfo *next;
+    cube *c;
+    uchar *colorbuf;
+    bvec *raybuf;
+    bool packed;
+    int type, w, h, bpp, bufsize, surface, layers;
+};
+
+struct lightmaptask
+{
+    ivec o;
+    int size, usefaces, progress;
+    cube *c;
+    cubeext *ext;
+    lightmapinfo *lightmaps;
+    lightmapworker *worker;
+};
+
+struct lightmapext
+{
+    cube *c;
+    cubeext *ext;
+};
+
+static vector<lightmapworker *> lightmapworkers;
+static vector<lightmaptask> lightmaptasks[2];
+static vector<lightmapext> lightmapexts;
+static int packidx = 0, allocidx = 0;
+static SDL_mutex *lightlock = NULL, *tasklock = NULL;
+static SDL_cond *fullcond = NULL, *emptycond = NULL;
+
+int lightmapping = 0;
+
+vector<LightMap> lightmaps;
+
+VARR(lightprecision, 1, 32, 1024);
+VARR(lighterror, 1, 8, 16);
+VARR(bumperror, 1, 3, 16);
+VARR(lightlod, 0, 0, 10);
+bvec ambientcolor(0x19, 0x19, 0x19), skylightcolor(0, 0, 0);
+HVARFR(ambient, 1, 0x191919, 0xFFFFFF, 
+{
+    if(ambient <= 255) ambient |= (ambient<<8) | (ambient<<16);
+    ambientcolor = bvec((ambient>>16)&0xFF, (ambient>>8)&0xFF, ambient&0xFF);
+});
+HVARFR(skylight, 0, 0, 0xFFFFFF, 
+{
+    if(skylight <= 255) skylight |= (skylight<<8) | (skylight<<16);
+    skylightcolor = bvec((skylight>>16)&0xFF, (skylight>>8)&0xFF, skylight&0xFF);
+});
+
+extern void setupsunlight();
+bvec sunlightcolor(0, 0, 0);
+HVARFR(sunlight, 0, 0, 0xFFFFFF,
+{
+    if(sunlight <= 255) sunlight |= (sunlight<<8) | (sunlight<<16);
+    sunlightcolor = bvec((sunlight>>16)&0xFF, (sunlight>>8)&0xFF, sunlight&0xFF);
+    setupsunlight();
+});
+FVARFR(sunlightscale, 0, 1, 16, setupsunlight());
+vec sunlightdir(0, 0, 1);
+extern void setsunlightdir();
+VARFR(sunlightyaw, 0, 0, 360, setsunlightdir());
+VARFR(sunlightpitch, -90, 90, 90, setsunlightdir());
+
+void setsunlightdir() 
+{ 
+    sunlightdir = vec(sunlightyaw*RAD, sunlightpitch*RAD); 
+    loopk(3) if(fabs(sunlightdir[k]) < 1e-5f) sunlightdir[k] = 0;
+    sunlightdir.normalize();
+    setupsunlight(); 
+}
+
+entity sunlightent;
+void setupsunlight()
+{
+    memclear(sunlightent);
+    sunlightent.type = ET_LIGHT;
+    sunlightent.attr1 = 0;
+    sunlightent.attr2 = int(sunlightcolor.x*sunlightscale);
+    sunlightent.attr3 = int(sunlightcolor.y*sunlightscale);
+    sunlightent.attr4 = int(sunlightcolor.z*sunlightscale);
+    float dist = min(min(sunlightdir.x ? 1/fabs(sunlightdir.x) : 1e16f, sunlightdir.y ? 1/fabs(sunlightdir.y) : 1e16f), sunlightdir.z ? 1/fabs(sunlightdir.z) : 1e16f);
+    sunlightent.o = vec(sunlightdir).mul(dist*worldsize).add(vec(worldsize/2, worldsize/2, worldsize/2)); 
+}
+
+VARR(skytexturelight, 0, 1, 1);
+extern int useskytexture;
+
+static const surfaceinfo brightsurfaces[6] =
+{
+    brightsurface,
+    brightsurface,
+    brightsurface,
+    brightsurface,
+    brightsurface,
+    brightsurface
+};
+
+void brightencube(cube &c)
+{
+    if(!c.ext) newcubeext(c, 0, false);
+    memcpy(c.ext->surfaces, brightsurfaces, sizeof(brightsurfaces));
+}
+
+void setsurfaces(cube &c, const surfaceinfo *surfs, const vertinfo *verts, int numverts)
+{
+    if(!c.ext || c.ext->maxverts < numverts) newcubeext(c, numverts, false);
+    memcpy(c.ext->surfaces, surfs, sizeof(c.ext->surfaces));
+    memcpy(c.ext->verts(), verts, numverts*sizeof(vertinfo));
+}
+
+void setsurface(cube &c, int orient, const surfaceinfo &src, const vertinfo *srcverts, int numsrcverts)
+{
+    int dstoffset = 0;
+    if(!c.ext) newcubeext(c, numsrcverts, true);
+    else
+    {
+        int numbefore = 0, beforeoffset = 0;
+        loopi(orient)
+        {
+            surfaceinfo &surf = c.ext->surfaces[i];
+            int numverts = surf.totalverts();
+            if(!numverts) continue;
+            numbefore += numverts;
+            beforeoffset = surf.verts + numverts;
+        }
+        int numafter = 0, afteroffset = c.ext->maxverts;
+        for(int i = 5; i > orient; i--)
+        {
+            surfaceinfo &surf = c.ext->surfaces[i];
+            int numverts = surf.totalverts();
+            if(!numverts) continue;
+            numafter += numverts;
+            afteroffset = surf.verts;
+        }
+        if(afteroffset - beforeoffset >= numsrcverts) dstoffset = beforeoffset;
+        else
+        {
+            cubeext *ext = c.ext;
+            if(numbefore + numsrcverts + numafter > c.ext->maxverts)
+            {
+                ext = growcubeext(c.ext, numbefore + numsrcverts + numafter);
+                memcpy(ext->surfaces, c.ext->surfaces, sizeof(ext->surfaces));
+            }
+            int offset = 0;
+            if(numbefore == beforeoffset)
+            {
+                if(numbefore && c.ext != ext) memcpy(ext->verts(), c.ext->verts(), numbefore*sizeof(vertinfo));
+                offset = numbefore;
+            }
+            else loopi(orient)
+            {
+                surfaceinfo &surf = ext->surfaces[i];
+                int numverts = surf.totalverts();
+                if(!numverts) continue;
+                memmove(ext->verts() + offset, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+                surf.verts = offset;
+                offset += numverts;
+            }
+            dstoffset = offset;
+            offset += numsrcverts;
+            if(numafter && offset > afteroffset)
+            {
+                offset += numafter;
+                for(int i = 5; i > orient; i--)
+                {
+                    surfaceinfo &surf = ext->surfaces[i];
+                    int numverts = surf.totalverts();
+                    if(!numverts) continue;
+                    offset -= numverts;
+                    memmove(ext->verts() + offset, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+                    surf.verts = offset;
+                }
+            }
+            if(c.ext != ext) setcubeext(c, ext);
+        }
+    }
+    surfaceinfo &dst = c.ext->surfaces[orient];
+    dst = src;
+    dst.verts = dstoffset;
+    if(srcverts) memcpy(c.ext->verts() + dstoffset, srcverts, numsrcverts*sizeof(vertinfo));
+}
+
+// quality parameters, set by the calclight arg
+VARN(lmshadows, lmshadows_, 0, 2, 2);
+VARN(lmaa, lmaa_, 0, 3, 3);
+VARN(lerptjoints, lerptjoints_, 0, 1, 1);
+static int lmshadows = 2, lmaa = 3, lerptjoints = 1;
+
+static uint progress = 0, taskprogress = 0;
+static GLuint progresstex = 0;
+static int progresstexticks = 0, progresslightmap = -1;
+
+bool calclight_canceled = false;
+volatile bool check_calclight_progress = false;
+
+void check_calclight_canceled()
+{
+    if(interceptkey(SDLK_ESCAPE)) 
+    {
+        calclight_canceled = true;
+        loopv(lightmapworkers) lightmapworkers[i]->doneworking = true;
+    }
+    if(!calclight_canceled) check_calclight_progress = false;
+}
+
+void show_calclight_progress()
+{
+    float bar1 = float(progress) / float(allocnodes);
+    defformatstring(text1, "%d%% using %d textures", int(bar1 * 100), lightmaps.length());
+
+    if(LM_PACKW <= hwtexsize && !progresstex)
+    {
+        glGenTextures(1, &progresstex);
+        createtexture(progresstex, LM_PACKW, LM_PACKH, NULL, 3, 1, GL_RGB);
+    }
+
+    // only update once a sec (4 * 250 ms ticks) to not kill performance
+    if(progresstex && !calclight_canceled && progresslightmap >= 0 && !(progresstexticks++ % 4)) 
+    {
+        if(tasklock) SDL_LockMutex(tasklock);
+        LightMap &lm = lightmaps[progresslightmap];
+        uchar *data = lm.data;
+        int bpp = lm.bpp;
+        if(tasklock) SDL_UnlockMutex(tasklock);
+        glBindTexture(GL_TEXTURE_2D, progresstex);
+        glPixelStorei(GL_UNPACK_ALIGNMENT, texalign(data, LM_PACKW, bpp));
+        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LM_PACKW, LM_PACKH, bpp > 3 ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, data);
+    }
+    renderprogress(bar1, text1, progresstexticks ? progresstex : 0);
+}
+
+#define CHECK_PROGRESS_LOCKED(exit, before, after) CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, before, after)
+#define CHECK_PROGRESS(exit) CHECK_PROGRESS_LOCKED(exit, , )
+
+bool PackNode::insert(ushort &tx, ushort &ty, ushort tw, ushort th)
+{
+    if((available < tw && available < th) || w < tw || h < th)
+        return false;
+    if(child1)
+    {
+        bool inserted = child1->insert(tx, ty, tw, th) ||
+                        child2->insert(tx, ty, tw, th);
+        available = max(child1->available, child2->available);
+        if(!available) clear();
+        return inserted;    
+    }
+    if(w == tw && h == th)
+    {
+        available = 0;
+        tx = x;
+        ty = y;
+        return true;
+    }
+    
+    if(w - tw > h - th)
+    {
+        child1 = new PackNode(x, y, tw, h);
+        child2 = new PackNode(x + tw, y, w - tw, h);
+    }
+    else
+    {
+        child1 = new PackNode(x, y, w, th);
+        child2 = new PackNode(x, y + th, w, h - th);
+    }
+
+    bool inserted = child1->insert(tx, ty, tw, th);
+    available = max(child1->available, child2->available);
+    return inserted;
+}
+
+bool LightMap::insert(ushort &tx, ushort &ty, uchar *src, ushort tw, ushort th)
+{
+    if((type&LM_TYPE) != LM_BUMPMAP1 && !packroot.insert(tx, ty, tw, th))
+        return false;
+
+    copy(tx, ty, src, tw, th);
+    return true;
+}
+
+void LightMap::copy(ushort tx, ushort ty, uchar *src, ushort tw, ushort th)
+{
+    uchar *dst = data + bpp * tx + ty * bpp * LM_PACKW;
+    loopi(th)
+    {
+        memcpy(dst, src, bpp * tw);
+        dst += bpp * LM_PACKW;
+        src += bpp * tw;
+    }
+    ++lightmaps;
+    lumels += tw * th;
+}
+
+static void insertunlit(int i)
+{
+    LightMap &l = lightmaps[i];
+    if((l.type&LM_TYPE) == LM_BUMPMAP1)
+    {
+        l.unlitx = l.unlity = -1;
+        return;
+    }
+    ushort x, y;
+    uchar unlit[4] = { ambientcolor[0], ambientcolor[1], ambientcolor[2], 255 };
+    if(l.insert(x, y, unlit, 1, 1))
+    {
+        if((l.type&LM_TYPE) == LM_BUMPMAP0)
+        {
+            bvec front(128, 128, 255);
+            ASSERT(lightmaps[i+1].insert(x, y, front.v, 1, 1));
+        }
+        l.unlitx = x;
+        l.unlity = y;
+    }
+}
+
+struct layoutinfo
+{
+    ushort x, y, lmid;
+    uchar w, h;
+};
+
+static void insertlightmap(lightmapinfo &li, layoutinfo &si)
+{
+    loopv(lightmaps)
+    {
+        if(lightmaps[i].type == li.type && lightmaps[i].insert(si.x, si.y, li.colorbuf, si.w, si.h))
+        {
+            si.lmid = i + LMID_RESERVED;
+            if((li.type&LM_TYPE) == LM_BUMPMAP0) ASSERT(lightmaps[i+1].insert(si.x, si.y, (uchar *)li.raybuf, si.w, si.h));
+            return;
+        }
+    }
+
+    progresslightmap = lightmaps.length();
+
+    si.lmid = lightmaps.length() + LMID_RESERVED;
+    LightMap &l = lightmaps.add();
+    l.type = li.type;
+    l.bpp = li.bpp;
+    l.data = new uchar[li.bpp*LM_PACKW*LM_PACKH];
+    memset(l.data, 0, li.bpp*LM_PACKW*LM_PACKH);
+    ASSERT(l.insert(si.x, si.y, li.colorbuf, si.w, si.h));
+    if((li.type&LM_TYPE) == LM_BUMPMAP0)
+    {
+        LightMap &r = lightmaps.add();
+        r.type = LM_BUMPMAP1 | (li.type&~LM_TYPE);
+        r.bpp = 3;
+        r.data = new uchar[3*LM_PACKW*LM_PACKH];
+        memset(r.data, 0, 3*LM_PACKW*LM_PACKH);
+        ASSERT(r.insert(si.x, si.y, (uchar *)li.raybuf, si.w, si.h));
+    }
+}
+
+static void copylightmap(lightmapinfo &li, layoutinfo &si)
+{
+    lightmaps[si.lmid-LMID_RESERVED].copy(si.x, si.y, li.colorbuf, si.w, si.h);
+    if((li.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(si.lmid+1-LMID_RESERVED))
+        lightmaps[si.lmid+1-LMID_RESERVED].copy(si.x, si.y, (uchar *)li.raybuf, si.w, si.h);
+}
+
+static inline bool htcmp(const lightmapinfo &k, const layoutinfo &v)
+{
+    int kw = k.w, kh = k.h;
+    if(kw != v.w || kh != v.h) return false;
+    LightMap &vlm = lightmaps[v.lmid - LMID_RESERVED];
+    int ktype = k.type;
+    if(ktype != vlm.type) return false;
+    int kbpp = k.bpp;
+    const uchar *kcolor = k.colorbuf, *vcolor = vlm.data + kbpp*(v.x + v.y*LM_PACKW);
+    loopi(kh)
+    {
+        if(memcmp(kcolor, vcolor, kbpp*kw)) return false;
+        kcolor += kbpp*kw;
+        vcolor += kbpp*LM_PACKW;
+    }
+    if((ktype&LM_TYPE) != LM_BUMPMAP0) return true;
+    const bvec *kdir = k.raybuf, *vdir = (const bvec *)lightmaps[v.lmid+1 - LMID_RESERVED].data + (v.x + v.y*LM_PACKW);
+    loopi(kh)
+    {
+        if(memcmp(kdir, vdir, kw*sizeof(bvec))) return false;
+        kdir += kw;
+        vdir += LM_PACKW;
+    }
+    return true;
+}
+    
+static inline uint hthash(const lightmapinfo &k)
+{
+    int kw = k.w, kh = k.h, kbpp = k.bpp; 
+    uint hash = kw + (kh<<8);
+    const uchar *color = k.colorbuf;
+    loopi(kw*kh)
+    {
+       hash ^= color[0] + (color[1] << 4) + (color[2] << 8);
+       color += kbpp;
+    }
+    return hash;  
+}
+
+static hashset<layoutinfo> compressed;
+
+VAR(lightcompress, 0, 3, 6);
+
+static bool packlightmap(lightmapinfo &l, layoutinfo &surface) 
+{
+    surface.w = l.w;
+    surface.h = l.h;
+    if((int)l.w <= lightcompress && (int)l.h <= lightcompress)
+    {
+        layoutinfo *val = compressed.access(l);
+        if(!val)
+        {
+            insertlightmap(l, surface);
+            compressed[l] = surface;
+        }
+        else
+        {
+            surface.x = val->x;
+            surface.y = val->y;
+            surface.lmid = val->lmid;
+            return false;
+        }
+    }
+    else insertlightmap(l, surface);
+    return true;
+}
+
+static void updatelightmap(const layoutinfo &surface)
+{
+    if(max(LM_PACKW, LM_PACKH) > hwtexsize || !lightmaps.inrange(surface.lmid-LMID_RESERVED)) return;
+
+    LightMap &lm = lightmaps[surface.lmid-LMID_RESERVED];
+    if(lm.tex < 0)
+    {
+        lm.offsetx = lm.offsety = 0;
+        lm.tex = lightmaptexs.length();
+        LightMapTexture &tex = lightmaptexs.add();
+        tex.type = lm.type;
+        tex.w = LM_PACKW;
+        tex.h = LM_PACKH;
+        tex.unlitx = lm.unlitx;
+        tex.unlity = lm.unlity;
+        glGenTextures(1, &tex.id);
+        createtexture(tex.id, tex.w, tex.h, NULL, 3, 1, tex.type&LM_ALPHA ? GL_RGBA : GL_RGB);
+        if((lm.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(surface.lmid+1-LMID_RESERVED))
+        {
+            LightMap &lm2 = lightmaps[surface.lmid+1-LMID_RESERVED];
+            lm2.offsetx = lm2.offsety = 0;
+            lm2.tex = lightmaptexs.length();
+            LightMapTexture &tex2 = lightmaptexs.add();
+            tex2.type = (lm.type&~LM_TYPE) | LM_BUMPMAP0;
+            tex2.w = LM_PACKW;
+            tex2.h = LM_PACKH;
+            tex2.unlitx = lm2.unlitx;
+            tex2.unlity = lm2.unlity;
+            glGenTextures(1, &tex2.id);
+            createtexture(tex2.id, tex2.w, tex2.h, NULL, 3, 1, GL_RGB);
+        }
+    }
+
+    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, LM_PACKW);
+
+    glBindTexture(GL_TEXTURE_2D, lightmaptexs[lm.tex].id);
+    glTexSubImage2D(GL_TEXTURE_2D, 0, lm.offsetx + surface.x, lm.offsety + surface.y, surface.w, surface.h, lm.type&LM_ALPHA ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, &lm.data[(surface.y*LM_PACKW + surface.x)*lm.bpp]);
+    if((lm.type&LM_TYPE)==LM_BUMPMAP0 && lightmaps.inrange(surface.lmid+1-LMID_RESERVED))
+    {
+        LightMap &lm2 = lightmaps[surface.lmid+1-LMID_RESERVED];
+        glBindTexture(GL_TEXTURE_2D, lightmaptexs[lm2.tex].id);
+        glTexSubImage2D(GL_TEXTURE_2D, 0, lm2.offsetx + surface.x, lm2.offsety + surface.y, surface.w, surface.h, GL_RGB, GL_UNSIGNED_BYTE, &lm2.data[(surface.y*LM_PACKW + surface.x)*3]);
+    }
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+}
+        
+static uint generatelumel(lightmapworker *w, const float tolerance, uint lightmask, const vector<const extentity *> &lights, const vec &target, const vec &normal, vec &sample, int x, int y)
+{
+    vec avgray(0, 0, 0);
+    float r = 0, g = 0, b = 0;
+    uint lightused = 0;
+    loopv(lights)
+    {
+        if(lightmask&(1<<i)) continue;
+        const extentity &light = *lights[i];
+        vec ray = target;
+        ray.sub(light.o);
+        float mag = ray.magnitude();
+        if(!mag) continue;
+        float attenuation = 1;
+        if(light.attr1)
+        {
+            attenuation -= mag / float(light.attr1);
+            if(attenuation <= 0) continue;
+        }
+        ray.mul(1.0f / mag);
+        float angle = -ray.dot(normal);
+        if(angle <= 0) continue;
+        if(light.attached && light.attached->type==ET_SPOTLIGHT)
+        {
+            vec spot = vec(light.attached->o).sub(light.o).normalize();
+            float maxatten = sincos360[clamp(int(light.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten);
+            if(spotatten <= 0) continue;
+            attenuation *= spotatten;
+        }
+        if(lmshadows && mag)
+        {
+            float dist = shadowray(w->shadowraycache, light.o, ray, mag - tolerance, RAY_SHADOW | (lmshadows > 1 ? RAY_ALPHAPOLY : 0));
+            if(dist < mag - tolerance) continue;
+        }
+        lightused |= 1<<i;
+        float intensity;
+        switch(w->type&LM_TYPE)
+        {
+            case LM_BUMPMAP0: 
+                intensity = attenuation; 
+                avgray.add(ray.mul(-attenuation));
+                break;
+            default:
+                intensity = angle * attenuation;
+                break;
+        }
+        r += intensity * float(light.attr2);
+        g += intensity * float(light.attr3);
+        b += intensity * float(light.attr4);
+    }
+    if(sunlight)
+    {
+        float angle = sunlightdir.dot(normal);
+        if(angle > 0 &&
+           (!lmshadows ||
+            shadowray(w->shadowraycache, vec(sunlightdir).mul(tolerance).add(target), sunlightdir, 1e16f, RAY_SHADOW | (lmshadows > 1 ? RAY_ALPHAPOLY : 0) | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0)) > 1e15f))
+        {
+            float intensity;
+            switch(w->type&LM_TYPE)
+            {
+                case LM_BUMPMAP0:
+                    intensity = 1;
+                    avgray.add(sunlightdir);
+                    break;
+                default:
+                    intensity = angle;
+                    break;
+            }
+            r += intensity * (sunlightcolor.x*sunlightscale);
+            g += intensity * (sunlightcolor.y*sunlightscale);
+            b += intensity * (sunlightcolor.z*sunlightscale);
+        }
+    }
+    switch(w->type&LM_TYPE)
+    {
+        case LM_BUMPMAP0:
+            if(avgray.iszero()) break;
+            // transform to tangent space
+            extern const vec orientation_tangent[8][3];
+            extern const vec orientation_bitangent[8][3];
+            vec S(orientation_tangent[w->rotate][dimension(w->orient)]),
+                T(orientation_bitangent[w->rotate][dimension(w->orient)]);
+            normal.orthonormalize(S, T);
+            avgray.normalize();
+            w->raydata[y*w->w+x].add(vec(S.dot(avgray)/S.magnitude(), T.dot(avgray)/T.magnitude(), normal.dot(avgray)));
+            break;
+    }
+    sample.x = min(255.0f, max(r, float(ambientcolor[0])));
+    sample.y = min(255.0f, max(g, float(ambientcolor[1])));
+    sample.z = min(255.0f, max(b, float(ambientcolor[2])));
+    return lightused;
+}
+
+static bool lumelsample(const vec &sample, int aasample, int stride)
+{
+    if(sample.x >= int(ambientcolor[0])+1 || sample.y >= int(ambientcolor[1])+1 || sample.z >= int(ambientcolor[2])+1) return true;
+#define NCHECK(n) \
+    if((n).x >= int(ambientcolor[0])+1 || (n).y >= int(ambientcolor[1])+1 || (n).z >= int(ambientcolor[2])+1) \
+        return true;
+    const vec *n = &sample - stride - aasample;
+    NCHECK(n[0]); NCHECK(n[aasample]); NCHECK(n[2*aasample]);
+    n += stride;
+    NCHECK(n[0]); NCHECK(n[2*aasample]);
+    n += stride;
+    NCHECK(n[0]); NCHECK(n[aasample]); NCHECK(n[2*aasample]);
+    return false;
+}
+
+static void calcskylight(lightmapworker *w, const vec &o, const vec &normal, float tolerance, uchar *skylight, int flags = RAY_ALPHAPOLY, extentity *t = NULL)
+{
+    static const vec rays[17] =
+    {
+        vec(cosf(21*RAD)*cosf(50*RAD), sinf(21*RAD)*cosf(50*RAD), sinf(50*RAD)),
+        vec(cosf(111*RAD)*cosf(50*RAD), sinf(111*RAD)*cosf(50*RAD), sinf(50*RAD)),
+        vec(cosf(201*RAD)*cosf(50*RAD), sinf(201*RAD)*cosf(50*RAD), sinf(50*RAD)),
+        vec(cosf(291*RAD)*cosf(50*RAD), sinf(291*RAD)*cosf(50*RAD), sinf(50*RAD)),
+
+        vec(cosf(66*RAD)*cosf(70*RAD), sinf(66*RAD)*cosf(70*RAD), sinf(70*RAD)),
+        vec(cosf(156*RAD)*cosf(70*RAD), sinf(156*RAD)*cosf(70*RAD), sinf(70*RAD)),
+        vec(cosf(246*RAD)*cosf(70*RAD), sinf(246*RAD)*cosf(70*RAD), sinf(70*RAD)),
+        vec(cosf(336*RAD)*cosf(70*RAD), sinf(336*RAD)*cosf(70*RAD), sinf(70*RAD)),
+       
+        vec(0, 0, 1),
+
+        vec(cosf(43*RAD)*cosf(60*RAD), sinf(43*RAD)*cosf(60*RAD), sinf(60*RAD)),
+        vec(cosf(133*RAD)*cosf(60*RAD), sinf(133*RAD)*cosf(60*RAD), sinf(60*RAD)),
+        vec(cosf(223*RAD)*cosf(60*RAD), sinf(223*RAD)*cosf(60*RAD), sinf(60*RAD)),
+        vec(cosf(313*RAD)*cosf(60*RAD), sinf(313*RAD)*cosf(60*RAD), sinf(60*RAD)),
+
+        vec(cosf(88*RAD)*cosf(80*RAD), sinf(88*RAD)*cosf(80*RAD), sinf(80*RAD)),
+        vec(cosf(178*RAD)*cosf(80*RAD), sinf(178*RAD)*cosf(80*RAD), sinf(80*RAD)),
+        vec(cosf(268*RAD)*cosf(80*RAD), sinf(268*RAD)*cosf(80*RAD), sinf(80*RAD)),
+        vec(cosf(358*RAD)*cosf(80*RAD), sinf(358*RAD)*cosf(80*RAD), sinf(80*RAD)),
+
+    };
+    flags |= RAY_SHADOW;
+    if(skytexturelight) flags |= RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0);
+    int hit = 0;
+    if(w) loopi(17) 
+    {
+        if(normal.dot(rays[i])>=0 && shadowray(w->shadowraycache, vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++;
+    }
+    else loopi(17) 
+    {
+        if(normal.dot(rays[i])>=0 && shadowray(vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++;
+    }
+
+    loopk(3) skylight[k] = uchar(ambientcolor[k] + (max(skylightcolor[k], ambientcolor[k]) - ambientcolor[k])*hit/17.0f);
+}
+
+static inline bool hasskylight()
+{
+    return skylightcolor[0]>ambientcolor[0] || skylightcolor[1]>ambientcolor[1] || skylightcolor[2]>ambientcolor[2];
+}
+
+VARR(blurlms, 0, 0, 2);
+VARR(blurskylight, 0, 0, 2);
+
+static inline void generatealpha(lightmapworker *w, float tolerance, const vec &pos, uchar &alpha)
+{
+    alpha = lookupblendmap(w->blendmapcache, pos);
+    if(w->slot->layermask)
+    {
+        static const int sdim[] = { 1, 0, 0 }, tdim[] = { 2, 2, 1 };
+        int dim = dimension(w->orient);
+        float k = 8.0f/w->vslot->scale,
+              s = (pos[sdim[dim]] * k - w->vslot->offset.y) / w->slot->layermaskscale,
+              t = (pos[tdim[dim]] * (dim <= 1 ? -k : k) - w->vslot->offset.y) / w->slot->layermaskscale;
+        const texrotation &r = texrotations[w->rotate];
+        if(r.swapxy) swap(s, t);
+        if(r.flipx) s = -s;
+        if(r.flipy) t = -t;
+        const ImageData &mask = *w->slot->layermask;
+        int mx = int(floor(s))%mask.w, my = int(floor(t))%mask.h;
+        if(mx < 0) mx += mask.w;
+        if(my < 0) my += mask.h;
+        uchar maskval = mask.data[mask.bpp*(mx + 1) - 1 + mask.pitch*my];
+        switch(w->slot->layermaskmode)
+        {
+            case 2: alpha = min(alpha, maskval); break;
+            case 3: alpha = max(alpha, maskval); break;
+            case 4: alpha = min(alpha, uchar(0xFF - maskval)); break;
+            case 5: alpha = max(alpha, uchar(0xFF - maskval)); break;
+            default: alpha = maskval; break;
+        }
+    }
+}
+        
+VAR(edgetolerance, 1, 4, 64);
+VAR(adaptivesample, 0, 2, 2);
+
+enum
+{
+    NO_SURFACE = 0,
+    SURFACE_AMBIENT_BOTTOM,
+    SURFACE_AMBIENT_TOP,
+    SURFACE_LIGHTMAP_BOTTOM,
+    SURFACE_LIGHTMAP_TOP,
+    SURFACE_LIGHTMAP_BLEND 
+};
+
+#define SURFACE_AMBIENT SURFACE_AMBIENT_BOTTOM
+#define SURFACE_LIGHTMAP SURFACE_LIGHTMAP_BOTTOM
+
+static bool generatelightmap(lightmapworker *w, float lpu, const lerpvert *lv, int numv, vec origin1, const vec &xstep1, const vec &ystep1, vec origin2, const vec &xstep2, const vec &ystep2, float side0, float sidestep)
+{
+    static const float aacoords[8][2] =
+    {
+        {0.0f, 0.0f},
+        {-0.5f, -0.5f},
+        {0.0f, -0.5f},
+        {-0.5f, 0.0f},
+
+        {0.3f, -0.6f},
+        {0.6f, 0.3f},
+        {-0.3f, 0.6f},
+        {-0.6f, -0.3f},
+    };
+    float tolerance = 0.5 / lpu;
+    uint lightmask = 0, lightused = 0;
+    vec offsets1[8], offsets2[8];
+    loopi(8) 
+    {
+        offsets1[i] = vec(xstep1).mul(aacoords[i][0]).add(vec(ystep1).mul(aacoords[i][1]));
+        offsets2[i] = vec(xstep2).mul(aacoords[i][0]).add(vec(ystep2).mul(aacoords[i][1]));
+    }
+    if((w->type&LM_TYPE) == LM_BUMPMAP0) memclear(w->raydata, (LM_MAXW + 4)*(LM_MAXH + 4));
+
+    origin1.sub(vec(ystep1).add(xstep1).mul(blurlms));
+    origin2.sub(vec(ystep2).add(xstep2).mul(blurlms));
+
+    int aasample = min(1 << lmaa, 4);
+    int stride = aasample*(w->w+1);
+    vec *sample = w->colordata;
+    uchar *skylight = w->ambient;
+    lerpbounds start, end;
+    initlerpbounds(-blurlms, -blurlms, lv, numv, start, end);
+    float sidex = side0 + blurlms*sidestep;
+    for(int y = 0; y < w->h; ++y, sidex += sidestep) 
+    {
+        vec normal, nstep;
+        lerpnormal(-blurlms, y - blurlms, lv, numv, start, end, normal, nstep);
+        
+        for(int x = 0; x < w->w; ++x, normal.add(nstep), skylight += w->bpp) 
+        {
+#define EDGE_TOLERANCE(x, y) \
+    (x < blurlms \
+     || x+1 > w->w - blurlms \
+     || y < blurlms \
+     || y+1 > w->h - blurlms \
+     ? edgetolerance : 1)
+            float t = EDGE_TOLERANCE(x, y) * tolerance;
+            vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2);
+            lightused |= generatelumel(w, t, 0, w->lights, u, vec(normal).normalize(), *sample, x, y);
+            if(hasskylight())
+            {
+                if((w->type&LM_TYPE)==LM_BUMPMAP0 || !adaptivesample || sample->x<skylightcolor[0] || sample->y<skylightcolor[1] || sample->z<skylightcolor[2])
+                    calcskylight(w, u, normal, t, skylight, lmshadows > 1 ? RAY_ALPHAPOLY : 0);
+                else loopk(3) skylight[k] = max(skylightcolor[k], ambientcolor[k]);
+            }
+            else loopk(3) skylight[k] = ambientcolor[k];
+            if(w->type&LM_ALPHA) generatealpha(w, t, u, skylight[3]);
+            sample += aasample;
+        }
+        sample += aasample;
+    }
+    if(adaptivesample > 1 && min(w->w, w->h) >= 2) lightmask = ~lightused;
+    sample = w->colordata;
+    initlerpbounds(-blurlms, -blurlms, lv, numv, start, end);
+    sidex = side0 + blurlms*sidestep;
+    for(int y = 0; y < w->h; ++y, sidex += sidestep)
+    {
+        vec normal, nstep;
+        lerpnormal(-blurlms, y - blurlms, lv, numv, start, end, normal, nstep);
+
+        for(int x = 0; x < w->w; ++x, normal.add(nstep)) 
+        {
+            vec &center = *sample++;
+            if(adaptivesample && x > 0 && x+1 < w->w && y > 0 && y+1 < w->h && !lumelsample(center, aasample, stride))
+                loopi(aasample-1) *sample++ = center;
+            else
+            {
+#define AA_EDGE_TOLERANCE(x, y, i) EDGE_TOLERANCE(x + aacoords[i][0], y + aacoords[i][1])
+                vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2);
+                const vec *offsets = x < sidex ? offsets1 : offsets2;
+                vec n = vec(normal).normalize();
+                loopi(aasample-1)
+                    generatelumel(w, AA_EDGE_TOLERANCE(x, y, i+1) * tolerance, lightmask, w->lights, vec(u).add(offsets[i+1]), n, *sample++, x, y);
+                if(lmaa == 3) 
+                {
+                    loopi(4)
+                    {
+                        vec s;
+                        generatelumel(w, AA_EDGE_TOLERANCE(x, y, i+4) * tolerance, lightmask, w->lights, vec(u).add(offsets[i+4]), n, s, x, y);
+                        center.add(s);
+                    }
+                    center.div(5);
+                }
+            }
+        }
+        if(aasample > 1)
+        {
+            vec u = w->w < sidex ? vec(xstep1).mul(w->w).add(vec(ystep1).mul(y)).add(origin1) : vec(xstep2).mul(w->w).add(vec(ystep2).mul(y)).add(origin2);
+            const vec *offsets = w->w < sidex ? offsets1 : offsets2;
+            vec n = vec(normal).normalize();
+            generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[1]), n, sample[1], w->w-1, y);
+            if(aasample > 2)
+                generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[3]), n, sample[3], w->w-1, y);
+        }
+        sample += aasample;
+    }
+
+    if(aasample > 1)
+    {
+        vec normal, nstep;
+        lerpnormal(-blurlms, w->h - blurlms, lv, numv, start, end, normal, nstep);
+
+        for(int x = 0; x <= w->w; ++x, normal.add(nstep))
+        {
+            vec u = x < sidex ? vec(xstep1).mul(x).add(vec(ystep1).mul(w->h)).add(origin1) : vec(xstep2).mul(x).add(vec(ystep2).mul(w->h)).add(origin2);
+            const vec *offsets = x < sidex ? offsets1 : offsets2;
+            vec n = vec(normal).normalize();
+            generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[1]), n, sample[1], min(x, w->w-1), w->h-1);
+            if(aasample > 2)
+                generatelumel(w, edgetolerance * tolerance, lightmask, w->lights, vec(u).add(offsets[2]), n, sample[2], min(x, w->w-1), w->h-1);
+            sample += aasample;
+        }
+    }
+    return true;
+}
+     
+static int finishlightmap(lightmapworker *w)
+{ 
+    if(hasskylight() && blurskylight && (w->w>1 || w->h>1)) 
+    {
+        blurtexture(blurskylight, w->bpp, w->w, w->h, w->blur, w->ambient);
+        swap(w->blur, w->ambient);
+    }
+    vec *sample = w->colordata;
+    int aasample = min(1 << lmaa, 4), stride = aasample*(w->w+1);
+    float weight = 1.0f / (1.0f + 4.0f*lmaa),
+          cweight = weight * (lmaa == 3 ? 5.0f : 1.0f);
+    uchar *skylight = w->ambient;
+    vec *ray = w->raydata;
+    uchar *dstcolor = blurlms && (w->w > 1 || w->h > 1) ? w->blur : w->colorbuf;
+    uchar mincolor[4] = { 255, 255, 255, 255 }, maxcolor[4] = { 0, 0, 0, 0 };
+    bvec *dstray = blurlms && (w->w > 1 || w->h > 1) ? (bvec *)w->raydata : w->raybuf;
+    bvec minray(255, 255, 255), maxray(0, 0, 0);
+    loop(y, w->h)
+    {
+        loop(x, w->w)
+        {
+            vec l(0, 0, 0);
+            const vec &center = *sample++;
+            loopi(aasample-1) l.add(*sample++);
+            if(aasample > 1)
+            {
+                l.add(sample[1]);
+                if(aasample > 2) l.add(sample[3]);
+            }
+            vec *next = sample + stride - aasample;
+            if(aasample > 1)
+            {
+                l.add(next[1]);
+                if(aasample > 2) l.add(next[2]);
+                l.add(next[aasample+1]);
+            }
+
+            int r = int(center.x*cweight + l.x*weight),
+                g = int(center.y*cweight + l.y*weight),
+                b = int(center.z*cweight + l.z*weight),
+                ar = skylight[0], ag = skylight[1], ab = skylight[2];
+            dstcolor[0] = max(ar, r);
+            dstcolor[1] = max(ag, g);
+            dstcolor[2] = max(ab, b);
+            loopk(3)
+            {
+                mincolor[k] = min(mincolor[k], dstcolor[k]);
+                maxcolor[k] = max(maxcolor[k], dstcolor[k]);
+            }
+            if(w->type&LM_ALPHA)
+            {
+                dstcolor[3] = skylight[3];
+                mincolor[3] = min(mincolor[3], dstcolor[3]);
+                maxcolor[3] = max(maxcolor[3], dstcolor[3]);
+            }
+            if((w->type&LM_TYPE) == LM_BUMPMAP0)
+            {
+                if(ray->iszero()) dstray[0] = bvec(128, 128, 255);
+                else
+                {
+                    // bias the normals towards the amount of ambient/skylight in the lumel 
+                    // this is necessary to prevent the light values in shaders from dropping too far below the skylight (to the ambient) if N.L is small 
+                    ray->normalize();
+                    int l = max(r, max(g, b)), a = max(ar, max(ag, ab));
+                    ray->mul(max(l-a, 0));
+                    ray->z += a;
+                    dstray[0] = bvec(ray->normalize());
+                }
+                loopk(3)
+                {
+                    minray[k] = min(minray[k], dstray[0][k]);
+                    maxray[k] = max(maxray[k], dstray[0][k]);
+                }
+                ray++;
+                dstray++;
+            }
+            dstcolor += w->bpp;
+            skylight += w->bpp;
+        }
+        sample += aasample;
+    }
+    if(int(maxcolor[0]) - int(mincolor[0]) <= lighterror &&
+       int(maxcolor[1]) - int(mincolor[1]) <= lighterror &&
+       int(maxcolor[2]) - int(mincolor[2]) <= lighterror &&
+       mincolor[3] >= maxcolor[3])
+    {
+        uchar color[3];
+        loopk(3) color[k] = (int(maxcolor[k]) + int(mincolor[k])) / 2;
+        if(color[0] <= int(ambientcolor[0]) + lighterror && 
+           color[1] <= int(ambientcolor[1]) + lighterror && 
+           color[2] <= int(ambientcolor[2]) + lighterror &&
+           (maxcolor[3]==0 || mincolor[3]==255))
+            return mincolor[3]==255 ? SURFACE_AMBIENT_TOP : SURFACE_AMBIENT_BOTTOM;
+        if((w->type&LM_TYPE) != LM_BUMPMAP0 || 
+            (int(maxray.x) - int(minray.x) <= bumperror &&
+             int(maxray.y) - int(minray.z) <= bumperror &&
+             int(maxray.z) - int(minray.z) <= bumperror))
+
+        {
+            memcpy(w->colorbuf, color, 3);
+            if(w->type&LM_ALPHA) w->colorbuf[3] = mincolor[3];
+            if((w->type&LM_TYPE) == LM_BUMPMAP0) 
+            {
+                loopk(3) w->raybuf[0][k] = uchar((int(maxray[k])+int(minray[k]))/2);
+            }
+            w->lastlightmap->w = w->w = 1;
+            w->lastlightmap->h = w->h = 1;
+        }
+    }
+    if(blurlms && (w->w>1 || w->h>1)) 
+    {
+        blurtexture(blurlms, w->bpp, w->w, w->h, w->colorbuf, w->blur, blurlms);
+        if((w->type&LM_TYPE) == LM_BUMPMAP0) blurnormals(blurlms, w->w, w->h, w->raybuf, (const bvec *)w->raydata, blurlms);
+        w->lastlightmap->w = (w->w -= 2*blurlms);
+        w->lastlightmap->h = (w->h -= 2*blurlms);
+    }
+    if(mincolor[3]==255) return SURFACE_LIGHTMAP_TOP;
+    else if(maxcolor[3]==0) return SURFACE_LIGHTMAP_BOTTOM;
+    else return SURFACE_LIGHTMAP_BLEND;
+}
+
+static int previewlightmapalpha(lightmapworker *w, float lpu, const vec &origin1, const vec &xstep1, const vec &ystep1, const vec &origin2, const vec &xstep2, const vec &ystep2, float side0, float sidestep)
+{
+    extern int fullbrightlevel;
+    float tolerance = 0.5 / lpu;
+    uchar *dst = w->colorbuf;
+    uchar minalpha = 255, maxalpha = 0;
+    float sidex = side0;
+    for(int y = 0; y < w->h; ++y, sidex += sidestep)
+    {
+        for(int x = 0; x < w->w; ++x, dst += 4)
+        {
+            vec u = x < sidex ? 
+                vec(xstep1).mul(x).add(vec(ystep1).mul(y)).add(origin1) :
+                vec(xstep2).mul(x).add(vec(ystep2).mul(y)).add(origin2);    
+            loopk(3) dst[k] = fullbrightlevel;        
+            generatealpha(w, tolerance, u, dst[3]);
+            minalpha = min(minalpha, dst[3]);
+            maxalpha = max(maxalpha, dst[3]);
+        }
+    }
+    if(minalpha==255) return SURFACE_AMBIENT_TOP;
+    if(maxalpha==0) return SURFACE_AMBIENT_BOTTOM;
+    if(minalpha==maxalpha) w->w = w->h = 1;    
+    if((w->type&LM_TYPE) == LM_BUMPMAP0) loopi(w->w*w->h) w->raybuf[i] = bvec(128, 128, 255);
+    return SURFACE_LIGHTMAP_BLEND;
+}        
+
+static void clearsurfaces(cube *c)
+{
+    loopi(8)
+    {
+        if(c[i].ext)
+        {
+            loopj(6) 
+            {
+                surfaceinfo &surf = c[i].ext->surfaces[j];
+                if(!surf.used()) continue;
+                surf.clear();
+                int numverts = surf.numverts&MAXFACEVERTS;
+                if(numverts)
+                {
+                    if(!(c[i].merged&(1<<j))) { surf.numverts &= ~MAXFACEVERTS; continue; }
+
+                    vertinfo *verts = c[i].ext->verts() + surf.verts;
+                    loopk(numverts)
+                    {
+                        vertinfo &v = verts[k];
+                        v.u = 0;
+                        v.v = 0;
+                        v.norm = 0;
+                    }
+                }
+            } 
+        }
+        if(c[i].children) clearsurfaces(c[i].children);
+    }
+}
+
+#define LIGHTCACHESIZE 1024
+
+static struct lightcacheentry
+{
+    int x, y;
+    vector<int> lights;
+} lightcache[LIGHTCACHESIZE];
+
+#define LIGHTCACHEHASH(x, y) (((((x)^(y))<<5) + (((x)^(y))>>5)) & (LIGHTCACHESIZE - 1))
+
+VARF(lightcachesize, 4, 6, 12, clearlightcache());
+
+void clearlightcache(int id)
+{
+    if(id >= 0)
+    {
+        const extentity &light = *entities::getents()[id];
+        int radius = light.attr1;
+        if(radius)
+        {
+            for(int x = int(max(light.o.x-radius, 0.0f))>>lightcachesize, ex = int(min(light.o.x+radius, worldsize-1.0f))>>lightcachesize; x <= ex; x++)
+            for(int y = int(max(light.o.y-radius, 0.0f))>>lightcachesize, ey = int(min(light.o.y+radius, worldsize-1.0f))>>lightcachesize; y <= ey; y++)
+            {
+                lightcacheentry &lce = lightcache[LIGHTCACHEHASH(x, y)];
+                if(lce.x != x || lce.y != y) continue;
+                lce.x = -1;
+                lce.lights.setsize(0);
+            }
+            return;
+        }
+    }
+
+    for(lightcacheentry *lce = lightcache; lce < &lightcache[LIGHTCACHESIZE]; lce++)
+    {
+        lce->x = -1;
+        lce->lights.setsize(0);
+    }
+}
+
+const vector<int> &checklightcache(int x, int y)
+{
+    x >>= lightcachesize;
+    y >>= lightcachesize; 
+    lightcacheentry &lce = lightcache[LIGHTCACHEHASH(x, y)];
+    if(lce.x == x && lce.y == y) return lce.lights;
+
+    lce.lights.setsize(0);
+    int csize = 1<<lightcachesize, cx = x<<lightcachesize, cy = y<<lightcachesize;
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        const extentity &light = *ents[i];
+        switch(light.type)
+        {
+            case ET_LIGHT:
+            {
+                int radius = light.attr1;
+                if(radius > 0)
+                {
+                    if(light.o.x + radius < cx || light.o.x - radius > cx + csize ||
+                       light.o.y + radius < cy || light.o.y - radius > cy + csize)
+                        continue;
+                }
+                break;
+            }
+            default: continue;
+        }
+        lce.lights.add(i);
+    }
+
+    lce.x = x;
+    lce.y = y;
+    return lce.lights;
+}
+
+static inline void addlight(lightmapworker *w, const extentity &light, int cx, int cy, int cz, int size, const vec *v, const vec *n, int numv)
+{
+    int radius = light.attr1;
+    if(radius > 0)
+    {
+        if(light.o.x + radius < cx || light.o.x - radius > cx + size ||
+           light.o.y + radius < cy || light.o.y - radius > cy + size ||
+           light.o.z + radius < cz || light.o.z - radius > cz + size)
+            return;
+    }
+
+    loopi(4)
+    {
+        vec p(light.o);
+        p.sub(v[i]);
+        float dist = p.dot(n[i]);
+        if(dist >= 0 && (!radius || dist < radius)) 
+        {
+            w->lights.add(&light);
+            break;
+        }
+    }
+} 
+
+static bool findlights(lightmapworker *w, int cx, int cy, int cz, int size, const vec *v, const vec *n, int numv, const Slot &slot, const VSlot &vslot)
+{
+    w->lights.setsize(0);
+    const vector<extentity *> &ents = entities::getents();
+    static volatile bool usinglightcache = false;
+    if(size <= 1<<lightcachesize && (!lightlock || !usinglightcache))
+    {
+        if(lightlock) { SDL_LockMutex(lightlock); usinglightcache = true; }
+        const vector<int> &lights = checklightcache(cx, cy);
+        loopv(lights)
+        {
+            const extentity &light = *ents[lights[i]];
+            switch(light.type)
+            {
+                case ET_LIGHT: addlight(w, light, cx, cy, cz, size, v, n, numv); break;
+            }
+        }
+        if(lightlock) { usinglightcache = false; SDL_UnlockMutex(lightlock); }
+    }
+    else loopv(ents)
+    {
+        const extentity &light = *ents[i];
+        switch(light.type)
+        {
+            case ET_LIGHT: addlight(w, light, cx, cy, cz, size, v, n, numv); break;
+        }
+    }
+    if(vslot.layer && (setblendmaporigin(w->blendmapcache, ivec(cx, cy, cz), size) || slot.layermask)) return true;
+    return w->lights.length() || hasskylight() || sunlight;
+}
+
+static int packlightmaps(lightmapworker *w = NULL)
+{
+    int numpacked = 0;
+    for(; packidx < lightmaptasks[0].length(); packidx++, numpacked++)
+    {
+        lightmaptask &t = lightmaptasks[0][packidx];
+        if(!t.lightmaps) break;
+        if(t.ext && t.c->ext != t.ext) 
+        {
+            lightmapext &e = lightmapexts.add();
+            e.c = t.c;
+            e.ext = t.ext;
+        }
+        progress = t.progress;
+        lightmapinfo *l = t.lightmaps;
+        if(l == (lightmapinfo *)-1) continue;
+        int space = 0; 
+        for(; l && l->c == t.c; l = l->next)
+        {
+            l->packed = true;
+            space += l->bufsize;
+            if(l->surface < 0 || !t.ext) continue; 
+            surfaceinfo &surf = t.ext->surfaces[l->surface];
+            layoutinfo layout;
+            packlightmap(*l, layout);
+            int numverts = surf.numverts&MAXFACEVERTS;
+            vertinfo *verts = t.ext->verts() + surf.verts;
+            if(l->layers&LAYER_DUP)
+            {
+                if(l->type&LM_ALPHA) surf.lmid[0] = layout.lmid;
+                else { surf.lmid[1] = layout.lmid; verts += numverts; }
+            }
+            else
+            {
+                if(l->layers&LAYER_TOP) surf.lmid[0] = layout.lmid;
+                if(l->layers&LAYER_BOTTOM) surf.lmid[1] = layout.lmid;
+            }
+            ushort offsetx = layout.x*((USHRT_MAX+1)/LM_PACKW), offsety = layout.y*((USHRT_MAX+1)/LM_PACKH);
+            loopk(numverts)
+            {
+                vertinfo &v = verts[k];
+                v.u += offsetx;
+                v.v += offsety;
+            }
+        }
+        if(t.worker == w)
+        {
+            w->bufused -= space;
+            w->bufstart = (w->bufstart + space)%LIGHTMAPBUFSIZE;
+            w->firstlightmap = l;
+            if(!l) 
+            {
+                w->lastlightmap = NULL;
+                w->bufstart = w->bufused = 0;
+            }
+        }
+        if(t.worker->needspace) SDL_CondSignal(t.worker->spacecond);
+    }
+    return numpacked;
+}
+
+static lightmapinfo *alloclightmap(lightmapworker *w)
+{
+    int needspace1 = sizeof(lightmapinfo) + w->w*w->h*w->bpp,
+        needspace2 = (w->type&LM_TYPE) == LM_BUMPMAP0 ? w->w*w->h*3 : 0,
+        needspace = needspace1 + needspace2,
+        bufend = (w->bufstart + w->bufused)%LIGHTMAPBUFSIZE, 
+        availspace = LIGHTMAPBUFSIZE - w->bufused,
+        availspace1 = min(availspace, LIGHTMAPBUFSIZE - bufend),
+        availspace2 = min(availspace, w->bufstart);
+    if(availspace < needspace || (max(availspace1, availspace2) < needspace && (availspace1 < needspace1 || availspace2 < needspace2)))
+    {
+        if(tasklock) SDL_LockMutex(tasklock);
+        while(!w->doneworking)
+        {
+            lightmapinfo *l = w->firstlightmap;
+            for(; l && l->packed; l = l->next)
+            {
+                w->bufused -= l->bufsize;
+                w->bufstart = (w->bufstart + l->bufsize)%LIGHTMAPBUFSIZE;
+            }
+            w->firstlightmap = l;
+            if(!l) 
+            {
+                w->lastlightmap = NULL;
+                w->bufstart = w->bufused = 0;
+            }
+            bufend = (w->bufstart + w->bufused)%LIGHTMAPBUFSIZE;
+            availspace = LIGHTMAPBUFSIZE - w->bufused;
+            availspace1 = min(availspace, LIGHTMAPBUFSIZE - bufend);
+            availspace2 = min(availspace, w->bufstart);
+            if(availspace >= needspace && (max(availspace1, availspace2) >= needspace || (availspace1 >= needspace1 && availspace2 >= needspace2))) break;
+            if(packlightmaps(w)) continue;
+            if(!w->spacecond || !tasklock) break;
+            w->needspace = true;
+            SDL_CondWait(w->spacecond, tasklock);
+            w->needspace = false;
+        }
+        if(tasklock) SDL_UnlockMutex(tasklock);
+    }
+    int usedspace = needspace;
+    lightmapinfo *l = NULL;
+    if(availspace1 >= needspace1)
+    {
+        l = (lightmapinfo *)&w->buf[bufend];
+        w->colorbuf = (uchar *)(l + 1);
+        if((w->type&LM_TYPE) != LM_BUMPMAP0) w->raybuf = NULL;
+        else if(availspace1 >= needspace) w->raybuf = (bvec *)&w->buf[bufend + needspace1];
+        else
+        {
+            w->raybuf = (bvec *)w->buf;
+            usedspace += availspace1 - needspace1;
+        }
+    }
+    else if(availspace2 >= needspace)
+    {
+        usedspace += availspace1;
+        l = (lightmapinfo *)w->buf;
+        w->colorbuf = (uchar *)(l + 1);
+        w->raybuf = (w->type&LM_TYPE) == LM_BUMPMAP0 ? (bvec *)&w->buf[needspace1] : NULL;
+    }
+    else return NULL;
+    w->bufused += usedspace;
+    l->next = NULL;
+    l->c = w->c;
+    l->type = w->type;
+    l->w = w->w;
+    l->h = w->h;
+    l->bpp = w->bpp;
+    l->colorbuf = w->colorbuf;
+    l->raybuf = w->raybuf;
+    l->packed = false;
+    l->bufsize = usedspace;
+    l->surface = -1;
+    l->layers = 0;
+    if(!w->firstlightmap) w->firstlightmap = l;
+    if(w->lastlightmap) w->lastlightmap->next = l;
+    w->lastlightmap = l;
+    if(!w->curlightmaps) w->curlightmaps = l;
+    return l;
+}
+
+static void freelightmap(lightmapworker *w)
+{
+    lightmapinfo *l = w->lastlightmap;
+    if(!l || l->surface >= 0) return;
+    if(w->firstlightmap == w->lastlightmap)
+    {
+        w->firstlightmap = w->lastlightmap = w->curlightmaps = NULL;
+        w->bufstart = w->bufused = 0;
+    }
+    else
+    {
+        w->bufused -= l->bufsize - sizeof(lightmapinfo);
+        l->bufsize = sizeof(lightmapinfo);
+        l->packed = true;
+    }
+    if(w->curlightmaps == l) w->curlightmaps = NULL;
+}
+
+static int setupsurface(lightmapworker *w, plane planes[2], int numplanes, const vec *p, const vec *n, int numverts, vertinfo *litverts, bool preview = false)
+{
+    vec u, v, t;
+    vec2 c[MAXFACEVERTS];
+
+    u = vec(p[2]).sub(p[0]).normalize();
+    v.cross(planes[0], u);
+    c[0] = vec2(0, 0);
+    if(numplanes >= 2) t.cross(planes[1], u); else t = v;
+    vec r1 = vec(p[1]).sub(p[0]);
+    c[1] = vec2(r1.dot(u), min(r1.dot(v), 0.0f));
+    c[2] = vec2(vec(p[2]).sub(p[0]).dot(u), 0);
+    for(int i = 3; i < numverts; i++)
+    {
+        vec r = vec(p[i]).sub(p[0]);
+        c[i] = vec2(r.dot(u), max(r.dot(t), 0.0f));
+    }
+
+    float carea = 1e16f;
+    vec2 cx(0, 0), cy(0, 0), co(0, 0), cmin(0, 0), cmax(0, 0);
+    loopi(numverts)
+    {
+        vec2 px = vec2(c[i+1 < numverts ? i+1 : 0]).sub(c[i]);
+        float len = px.squaredlen();
+        if(!len) continue;
+        px.mul(1/sqrtf(len));
+        vec2 py(-px.y, px.x), pmin(0, 0), pmax(0, 0);
+        if(numplanes >= 2 && (i == 0 || i >= 3)) px.neg();
+        loopj(numverts)
+        {
+            vec2 rj = vec2(c[j]).sub(c[i]), pj(rj.dot(px), rj.dot(py));
+            pmin.x = min(pmin.x, pj.x);
+            pmin.y = min(pmin.y, pj.y);
+            pmax.x = max(pmax.x, pj.x);
+            pmax.y = max(pmax.y, pj.y);
+        }
+        float area = (pmax.x-pmin.x)*(pmax.y-pmin.y);
+        if(area < carea) { carea = area; cx = px; cy = py; co = c[i]; cmin = pmin; cmax = pmax; }
+    }
+    
+    int scale = int(min(cmax.x - cmin.x, cmax.y - cmin.y));
+    float lpu = 16.0f / float(lightlod && scale < (1 << lightlod) ? max(lightprecision / 2, 1) : lightprecision);
+    int lw = clamp(int(ceil((cmax.x - cmin.x + 1)*lpu)), LM_MINW, LM_MAXW), lh = clamp(int(ceil((cmax.y - cmin.y + 1)*lpu)), LM_MINH, LM_MAXH);
+    w->w = lw;
+    w->h = lh;
+    if(!preview)
+    {
+        w->w += 2*blurlms;
+        w->h += 2*blurlms;
+    }
+    if(!alloclightmap(w)) return NO_SURFACE;
+        
+    vec2 cscale = vec2(cmax).sub(cmin).div(vec2(lw-1, lh-1)),
+         comin = vec2(cx).mul(cmin.x).add(vec2(cy).mul(cmin.y)).add(co);
+    loopi(numverts)
+    {
+        vec2 ri = vec2(c[i]).sub(comin);
+        c[i] = vec2(ri.dot(cx)/cscale.x, ri.dot(cy)/cscale.y);
+    }
+
+    vec xstep1 = vec(v).mul(cx.y).add(vec(u).mul(cx.x)).mul(cscale.x),
+        ystep1 = vec(v).mul(cy.y).add(vec(u).mul(cy.x)).mul(cscale.y),
+        origin1 = vec(v).mul(comin.y).add(vec(u).mul(comin.x)).add(p[0]),
+        xstep2 = xstep1, ystep2 = ystep1, origin2 = origin1;
+    float side0 = LM_MAXW + 1, sidestep = 0;
+    if(numplanes >= 2)
+    {
+        xstep2 = vec(t).mul(cx.y).add(vec(u).mul(cx.x)).mul(cscale.x);
+        ystep2 = vec(t).mul(cy.y).add(vec(u).mul(cy.x)).mul(cscale.y);
+        origin2 = vec(t).mul(comin.y).add(vec(u).mul(comin.x)).add(p[0]);
+        if(cx.y) { side0 = comin.y/-(cx.y*cscale.x); sidestep = cy.y*cscale.y/-(cx.y*cscale.x); }
+        else if(cy.y) { side0 = ceil(comin.y/-(cy.y*cscale.y))*(LM_MAXW + 1); sidestep = -(LM_MAXW + 1); if(cy.y < 0) { side0 = (LM_MAXW + 1) - side0; sidestep = -sidestep; } }
+        else side0 = comin.y <= 0 ? LM_MAXW + 1 : -1;
+    }
+
+    int surftype = NO_SURFACE;
+    if(preview)
+    {
+        surftype = previewlightmapalpha(w, lpu, origin1, xstep1, ystep1, origin2, xstep2, ystep2, side0, sidestep);
+    }
+    else
+    {
+        lerpvert lv[MAXFACEVERTS];
+        int numv = numverts;
+        calclerpverts(c, n, lv, numv);
+
+        if(!generatelightmap(w, lpu, lv, numv, origin1, xstep1, ystep1, origin2, xstep2, ystep2, side0, sidestep)) return NO_SURFACE;
+        surftype = finishlightmap(w);
+    }
+    if(surftype<SURFACE_LIGHTMAP) return surftype;
+
+    vec2 texscale(float(USHRT_MAX+1)/LM_PACKW, float(USHRT_MAX+1)/LM_PACKH);
+    if(lw != w->w) texscale.x *= float(w->w - 1) / (lw - 1);
+    if(lh != w->h) texscale.y *= float(w->h - 1) / (lh - 1);
+    loopk(numverts)
+    {
+        litverts[k].u = ushort(floor(clamp(c[k].x*texscale.x, 0.0f, float(USHRT_MAX))));
+        litverts[k].v = ushort(floor(clamp(c[k].y*texscale.y, 0.0f, float(USHRT_MAX)))); 
+    }
+    return surftype;
+}
+
+static void removelmalpha(lightmapworker *w)
+{
+    if(!(w->type&LM_ALPHA)) return;
+    for(uchar *dst = w->colorbuf, *src = w->colorbuf, *end = &src[w->w*w->h*4];
+        src < end;
+        dst += 3, src += 4)
+    {
+        dst[0] = src[0];
+        dst[1] = src[1];
+        dst[2] = src[2];
+    }
+    w->type &= ~LM_ALPHA;
+    w->bpp = 3;
+    w->lastlightmap->type = w->type;
+    w->lastlightmap->bpp = w->bpp;
+}
+
+static lightmapinfo *setupsurfaces(lightmapworker *w, lightmaptask &task)
+{
+    cube &c = *task.c;
+    const ivec &co = task.o;
+    int size = task.size, usefacemask = task.usefaces;
+    
+    w->curlightmaps = NULL;
+    w->c = &c;
+
+    surfaceinfo surfaces[6];
+    vertinfo litverts[6*2*MAXFACEVERTS];
+    int numlitverts = 0;
+    memclear(surfaces);
+    loopi(6)
+    {
+        int usefaces = usefacemask&0xF;
+        usefacemask >>= 4;
+        if(!usefaces)
+        {
+            if(!c.ext) continue;
+            surfaceinfo &surf = surfaces[i];
+            surf = c.ext->surfaces[i];
+            int numverts = surf.totalverts();
+            if(numverts)
+            {
+                memcpy(&litverts[numlitverts], c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+                surf.verts = numlitverts;
+                numlitverts += numverts;
+            }
+            continue;
+        }
+
+        VSlot &vslot = lookupvslot(c.texture[i], false),
+             *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, false) : NULL;
+        Shader *shader = vslot.slot->shader;
+        int shadertype = shader->type;
+        if(layer) shadertype |= layer->slot->shader->type;
+
+        surfaceinfo &surf = surfaces[i];
+        vertinfo *curlitverts = &litverts[numlitverts];
+        int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0;
+        ivec mo(co);
+        int msz = size, convex = 0;
+        if(numverts)
+        {
+            vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts;
+            loopj(numverts) curlitverts[j].set(verts[j].getxyz());
+            if(c.merged&(1<<i))
+            {
+                msz = 1<<calcmergedsize(i, mo, size, verts, numverts);
+                mo.mask(~(msz-1));
+
+                if(!(surf.numverts&MAXFACEVERTS))
+                {
+                    surf.verts = numlitverts;
+                    surf.numverts |= numverts;
+                    numlitverts += numverts;
+                }
+            }
+            else if(!flataxisface(c, i)) convex = faceconvexity(verts, numverts, size);
+        }
+        else
+        {
+            ivec v[4];
+            genfaceverts(c, i, v);
+            if(!flataxisface(c, i)) convex = faceconvexity(v);
+            int order = usefaces&4 || convex < 0 ? 1 : 0;
+            ivec vo = ivec(co).mask(0xFFF).shl(3);
+            curlitverts[numverts++].set(v[order].mul(size).add(vo));
+            if(usefaces&1) curlitverts[numverts++].set(v[order+1].mul(size).add(vo));
+            curlitverts[numverts++].set(v[order+2].mul(size).add(vo));
+            if(usefaces&2) curlitverts[numverts++].set(v[(order+3)&3].mul(size).add(vo));
+        }
+
+        vec pos[MAXFACEVERTS], n[MAXFACEVERTS], po(ivec(co).mask(~0xFFF));
+        loopj(numverts) pos[j] = vec(curlitverts[j].getxyz()).mul(1.0f/8).add(po);
+
+        plane planes[2];
+        int numplanes = 0;
+        planes[numplanes++].toplane(pos[0], pos[1], pos[2]);
+        if(numverts < 4 || !convex) loopk(numverts) findnormal(pos[k], planes[0], n[k]);
+        else
+        {
+            planes[numplanes++].toplane(pos[0], pos[2], pos[3]);
+            vec avg = vec(planes[0]).add(planes[1]).normalize();
+            findnormal(pos[0], avg, n[0]);
+            findnormal(pos[1], planes[0], n[1]);
+            findnormal(pos[2], avg, n[2]);
+            for(int k = 3; k < numverts; k++) findnormal(pos[k], planes[1], n[k]);
+        }
+
+        if(shadertype&(SHADER_NORMALSLMS | SHADER_ENVMAP))
+        {
+            loopk(numverts) curlitverts[k].norm = encodenormal(n[k]);
+            if(!(surf.numverts&MAXFACEVERTS))
+            {
+                surf.verts = numlitverts;
+                surf.numverts |= numverts;
+                numlitverts += numverts;
+            }
+        }
+
+        if(!findlights(w, mo.x, mo.y, mo.z, msz, pos, n, numverts, *vslot.slot, vslot))
+        {
+            if(surf.numverts&MAXFACEVERTS) surf.numverts |= LAYER_TOP;
+            continue;
+        }
+
+        w->slot = vslot.slot;
+        w->vslot = &vslot;
+        w->type = shader->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE;
+        if(layer) w->type |= LM_ALPHA;
+        w->bpp = w->type&LM_ALPHA ? 4 : 3;
+        w->orient = i;
+        w->rotate = vslot.rotation;
+        int surftype = setupsurface(w, planes, numplanes, pos, n, numverts, curlitverts);
+        switch(surftype)
+        {
+            case SURFACE_LIGHTMAP_BOTTOM:
+                if((shader->type^layer->slot->shader->type)&SHADER_NORMALSLMS ||
+                   (shader->type&SHADER_NORMALSLMS && vslot.rotation!=layer->rotation))
+                {
+                    freelightmap(w);
+                    break;
+                }
+                // fall through
+            case SURFACE_LIGHTMAP_BLEND:
+            case SURFACE_LIGHTMAP_TOP:
+            {
+                if(!(surf.numverts&MAXFACEVERTS))
+                {
+                    surf.verts = numlitverts;
+                    surf.numverts |= numverts;
+                    numlitverts += numverts;
+                }
+
+                w->lastlightmap->surface = i;
+                w->lastlightmap->layers = (surftype==SURFACE_LIGHTMAP_BOTTOM ? LAYER_BOTTOM : LAYER_TOP);
+                if(surftype==SURFACE_LIGHTMAP_BLEND) 
+                {
+                    surf.numverts |= LAYER_BLEND;
+                    w->lastlightmap->layers = LAYER_TOP;
+                    if((shader->type^layer->slot->shader->type)&SHADER_NORMALSLMS ||
+                       (shader->type&SHADER_NORMALSLMS && vslot.rotation!=layer->rotation))
+                        break;
+                    w->lastlightmap->layers |= LAYER_BOTTOM;
+                }
+                else
+                {
+                    if(surftype==SURFACE_LIGHTMAP_BOTTOM) 
+                    { 
+                        surf.numverts |= LAYER_BOTTOM; 
+                        w->lastlightmap->layers = LAYER_BOTTOM; 
+                    }
+                    else 
+                    { 
+                        surf.numverts |= LAYER_TOP; 
+                        w->lastlightmap->layers = LAYER_TOP; 
+                    }
+                    if(w->type&LM_ALPHA) removelmalpha(w);
+                } 
+                continue;
+            }
+
+            case SURFACE_AMBIENT_BOTTOM:
+                freelightmap(w);
+                surf.numverts |= layer ? LAYER_BOTTOM : LAYER_TOP;
+                continue;
+
+            case SURFACE_AMBIENT_TOP: 
+                freelightmap(w);
+                surf.numverts |= LAYER_TOP;
+                continue;
+
+            default:
+                freelightmap(w);
+                continue;
+        }
+
+        w->slot = layer->slot;
+        w->vslot = layer;
+        w->type = layer->slot->shader->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE;
+        w->bpp = 3;
+        w->rotate = layer->rotation;
+        vertinfo *blendverts = surf.numverts&MAXFACEVERTS ? &curlitverts[numverts] : curlitverts;
+        switch(setupsurface(w, planes, numplanes, pos, n, numverts, blendverts))
+        {
+            case SURFACE_LIGHTMAP_TOP:
+            {
+                if(!(surf.numverts&MAXFACEVERTS))
+                {
+                    surf.verts = numlitverts;
+                    surf.numverts |= numverts;
+                    numlitverts += numverts;
+                }
+                else if(!(surf.numverts&LAYER_DUP))
+                {
+                    surf.numverts |= LAYER_DUP;
+                    w->lastlightmap->layers |= LAYER_DUP;
+                    loopk(numverts)
+                    {
+                        vertinfo &src = curlitverts[k];
+                        vertinfo &dst = blendverts[k];
+                        dst.setxyz(src.getxyz());
+                        dst.norm = src.norm;
+                    }
+                    numlitverts += numverts;
+                }
+                surf.numverts |= LAYER_BOTTOM;
+                w->lastlightmap->layers |= LAYER_BOTTOM;
+
+                w->lastlightmap->surface = i;
+                break;
+            }
+
+            case SURFACE_AMBIENT_TOP:
+            {
+                freelightmap(w);
+                surf.numverts |= LAYER_BOTTOM;
+                break;
+            }
+
+            default: freelightmap(w); break;
+        }
+    }
+    loopk(6)
+    {
+        surfaceinfo &surf = surfaces[k];
+        if(surf.used())
+        {
+            cubeext *ext = c.ext && c.ext->maxverts >= numlitverts ? c.ext : growcubeext(c.ext, numlitverts);
+            memcpy(ext->surfaces, surfaces, sizeof(ext->surfaces));
+            memcpy(ext->verts(), litverts, numlitverts*sizeof(vertinfo));
+            task.ext = ext;
+            break;
+        }
+    }
+    return w->curlightmaps ? w->curlightmaps : (lightmapinfo *)-1;
+}
+
+int lightmapworker::work(void *data)
+{
+    lightmapworker *w = (lightmapworker *)data;
+    SDL_LockMutex(tasklock);
+    while(!w->doneworking)
+    {
+        if(allocidx < lightmaptasks[0].length())
+        {
+            lightmaptask &t = lightmaptasks[0][allocidx++];
+            t.worker = w;
+            SDL_UnlockMutex(tasklock);
+            lightmapinfo *l = setupsurfaces(w, t);
+            SDL_LockMutex(tasklock);
+            t.lightmaps = l;
+            packlightmaps(w);
+        }
+        else 
+        {
+            if(packidx >= lightmaptasks[0].length()) SDL_CondSignal(emptycond);   
+            SDL_CondWait(fullcond, tasklock);
+        }
+    }
+    SDL_UnlockMutex(tasklock);
+    return 0;
+}
+
+static bool processtasks(bool finish = false)
+{
+    if(tasklock) SDL_LockMutex(tasklock);
+    while(finish || lightmaptasks[1].length())
+    {
+        if(packidx >= lightmaptasks[0].length())
+        {
+            if(lightmaptasks[1].empty()) break;
+            lightmaptasks[0].setsize(0);
+            lightmaptasks[0].move(lightmaptasks[1]);
+            packidx = allocidx = 0;
+            if(fullcond) SDL_CondBroadcast(fullcond);
+        }
+        else if(lightmapping > 1)
+        {
+            SDL_CondWaitTimeout(emptycond, tasklock, 250);
+            CHECK_PROGRESS_LOCKED({ SDL_UnlockMutex(tasklock); return false; }, SDL_UnlockMutex(tasklock), SDL_LockMutex(tasklock));
+        }
+        else 
+        {
+            while(allocidx < lightmaptasks[0].length())
+            {
+                lightmaptask &t = lightmaptasks[0][allocidx++];
+                t.worker = lightmapworkers[0];
+                t.lightmaps = setupsurfaces(lightmapworkers[0], t);
+                packlightmaps(lightmapworkers[0]);
+                CHECK_PROGRESS(return false);
+            }
+        }
+    }
+    if(tasklock) SDL_UnlockMutex(tasklock);
+    return true;
+}
+
+static void generatelightmaps(cube *c, const ivec &co, int size)
+{
+    CHECK_PROGRESS(return);
+
+    taskprogress++;
+
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        if(c[i].children)
+            generatelightmaps(c[i].children, o, size >> 1);
+        else if(!isempty(c[i]))
+        {
+            if(c[i].ext)
+            {
+                loopj(6) 
+                {
+                    surfaceinfo &surf = c[i].ext->surfaces[j];
+                    if(surf.lmid[0] >= LMID_RESERVED || surf.lmid[1] >= LMID_RESERVED) goto nextcube;
+                    surf.clear();
+                }
+            }
+            int usefacemask = 0;
+            loopj(6) if(c[i].texture[j] != DEFAULT_SKY && (!(c[i].merged&(1<<j)) || (c[i].ext && c[i].ext->surfaces[j].numverts&MAXFACEVERTS)))
+            {   
+                usefacemask |= visibletris(c[i], j, o, size)<<(4*j);
+            }
+            if(usefacemask)
+            {
+                lightmaptask &t = lightmaptasks[1].add();
+                t.o = o;
+                t.size = size;
+                t.usefaces = usefacemask;
+                t.c = &c[i]; 
+                t.ext = NULL;
+                t.lightmaps = NULL;
+                t.progress = taskprogress;
+                if(lightmaptasks[1].length() >= MAXLIGHTMAPTASKS && !processtasks()) return;
+            }
+        }
+    nextcube:;
+    } 
+}
+
+static bool previewblends(lightmapworker *w, cube &c, const ivec &co, int size)
+{
+    if(isempty(c) || c.material&MAT_ALPHA) return false;
+
+    int usefacemask = 0;
+    loopi(6) if(c.texture[i] != DEFAULT_SKY && lookupvslot(c.texture[i], false).layer)
+        usefacemask |= visibletris(c, i, co, size)<<(4*i);
+    if(!usefacemask) return false;
+
+    if(!setblendmaporigin(w->blendmapcache, co, size))
+    {
+        if(!c.ext) return false;
+        bool blends = false;
+        loopi(6) if(c.ext->surfaces[i].numverts&LAYER_BOTTOM)
+        {
+            c.ext->surfaces[i].brighten();
+            blends = true;
+        }
+        return blends;
+    }
+
+    w->firstlightmap = w->lastlightmap = w->curlightmaps = NULL;
+    w->bufstart = w->bufused = 0;
+    w->c = &c;
+
+    surfaceinfo surfaces[6];
+    vertinfo litverts[6*2*MAXFACEVERTS];
+    int numlitverts = 0;
+    memcpy(surfaces, c.ext ? c.ext->surfaces : brightsurfaces, sizeof(surfaces));
+    loopi(6)
+    {
+        int usefaces = usefacemask&0xF;
+        usefacemask >>= 4;
+        if(!usefaces) 
+        {
+            surfaceinfo &surf = surfaces[i];
+            int numverts = surf.totalverts();
+            if(numverts)
+            {
+                memcpy(&litverts[numlitverts], c.ext->verts() + surf.verts, numverts*sizeof(vertinfo));
+                surf.verts = numlitverts;
+                numlitverts += numverts;
+            }
+            continue;
+        }
+
+        VSlot &vslot = lookupvslot(c.texture[i], false),
+              &layer = lookupvslot(vslot.layer, false);
+        Shader *shader = vslot.slot->shader;
+        int shadertype = shader->type | layer.slot->shader->type;
+            
+        vertinfo *curlitverts = &litverts[numlitverts]; 
+        int numverts = 0;
+        ivec v[4];
+        genfaceverts(c, i, v);
+        int convex = flataxisface(c, i) ? 0 : faceconvexity(v),
+            order = usefaces&4 || convex < 0 ? 1 : 0;
+        ivec vo = ivec(co).mask(0xFFF).shl(3);
+        curlitverts[numverts++].set(v[order].mul(size).add(vo));
+        if(usefaces&1) curlitverts[numverts++].set(v[order+1].mul(size).add(vo));
+        curlitverts[numverts++].set(v[order+2].mul(size).add(vo));
+        if(usefaces&2) curlitverts[numverts++].set(v[(order+3)&3].mul(size).add(vo));
+
+        vec pos[4], n[4], po(ivec(co).mask(~0xFFF));
+        loopj(numverts) pos[j] = vec(curlitverts[j].getxyz()).mul(1.0f/8).add(po);
+
+        plane planes[2];
+        int numplanes = 0;
+        planes[numplanes++].toplane(pos[0], pos[1], pos[2]);
+        if(numverts < 4 || !convex) loopk(numverts) n[k] = planes[0];
+        else
+        {
+            planes[numplanes++].toplane(pos[0], pos[2], pos[3]);
+            vec avg = vec(planes[0]).add(planes[1]).normalize();
+            n[0] = avg;
+            n[1] = planes[0];
+            n[2] = avg;
+            for(int k = 3; k < numverts; k++) n[k] = planes[1];
+        }
+
+        surfaceinfo &surf = surfaces[i];
+        w->slot = vslot.slot;
+        w->vslot = &vslot;
+        w->type = shadertype&SHADER_NORMALSLMS ? LM_BUMPMAP0|LM_ALPHA : LM_DIFFUSE|LM_ALPHA;
+        w->bpp = 4;
+        w->orient = i;
+        w->rotate = vslot.rotation;
+        int surftype = setupsurface(w, planes, numplanes, pos, n, numverts, curlitverts, true);
+        switch(surftype)
+        {
+            case SURFACE_AMBIENT_TOP:
+                surf = brightsurface;
+                continue;
+
+            case SURFACE_AMBIENT_BOTTOM:
+                surf = brightbottomsurface;
+                continue;
+
+            case SURFACE_LIGHTMAP_BLEND:
+            {
+                if(surf.numverts == (LAYER_BLEND|numverts) &&
+                   surf.lmid[0] == surf.lmid[1] &&
+                   (surf.numverts&MAXFACEVERTS) == numverts &&
+                   !memcmp(curlitverts, c.ext->verts() + surf.verts, numverts*sizeof(vertinfo)) &&
+                   lightmaps.inrange(surf.lmid[0]-LMID_RESERVED) &&
+                   lightmaps[surf.lmid[0]-LMID_RESERVED].type==w->type)
+                {
+                    vertinfo *oldverts = c.ext->verts() + surf.verts;
+                    layoutinfo layout;
+                    layout.w = w->w;
+                    layout.h = w->h;
+                    layout.x = (oldverts[0].x - curlitverts[0].x)/((USHRT_MAX+1)/LM_PACKW);
+                    layout.y = (oldverts[0].y - curlitverts[0].y)/((USHRT_MAX+1)/LM_PACKH);
+                    if(LM_PACKW - layout.x >= w->w && LM_PACKH - layout.y >= w->h)
+                    {
+                        layout.lmid = surf.lmid[0];
+                        copylightmap(*w->lastlightmap, layout);
+                        updatelightmap(layout);
+                        surf.verts = numlitverts;
+                        numlitverts += numverts;
+                        continue;
+                    }
+                }
+                
+                surf.verts = numlitverts;
+                surf.numverts = LAYER_BLEND|numverts;
+                numlitverts += numverts;
+                layoutinfo layout;
+                if(packlightmap(*w->lastlightmap, layout)) updatelightmap(layout);
+                surf.lmid[0] = surf.lmid[1] = layout.lmid;
+                ushort offsetx = layout.x*((USHRT_MAX+1)/LM_PACKW), offsety = layout.y*((USHRT_MAX+1)/LM_PACKH);
+                loopk(numverts)
+                {
+                    vertinfo &v = curlitverts[k];
+                    v.u += offsetx;
+                    v.v += offsety;
+                }
+                continue;
+            }
+        }
+    }
+        
+    setsurfaces(c, surfaces, litverts, numlitverts);
+    return true;
+}
+
+static bool previewblends(lightmapworker *w, cube *c, const ivec &co, int size, const ivec &bo, const ivec &bs)
+{
+    bool changed = false;
+    loopoctabox(co, size, bo, bs)
+    {
+        ivec o(i, co, size);
+        cubeext *ext = c[i].ext;
+        if(ext && ext->va && ext->va->hasmerges)
+        {
+            changed = true;
+            destroyva(ext->va);
+            ext->va = NULL;
+            invalidatemerges(c[i], co, size, true);
+        }
+        if(c[i].children ? previewblends(w, c[i].children, o, size/2, bo, bs) : previewblends(w, c[i], o, size))  
+        {
+            changed = true;
+            ext = c[i].ext;
+            if(ext && ext->va)
+            {
+                destroyva(ext->va);
+                ext->va = NULL;
+            }
+        }
+    }
+    return changed;
+}
+
+void previewblends(const ivec &bo, const ivec &bs)
+{
+    loadlayermasks();
+    if(lightmapworkers.empty()) lightmapworkers.add(new lightmapworker);
+    lightmapworkers[0]->reset();
+    if(previewblends(lightmapworkers[0], worldroot, ivec(0, 0, 0), worldsize/2, bo, bs))
+        commitchanges(true);
+}
+                            
+void cleanuplightmaps()
+{
+    loopv(lightmaps)
+    {
+        LightMap &lm = lightmaps[i];
+        lm.tex = lm.offsetx = lm.offsety = -1;
+    }
+    loopv(lightmaptexs) glDeleteTextures(1, &lightmaptexs[i].id);
+    lightmaptexs.shrink(0);
+    if(progresstex) { glDeleteTextures(1, &progresstex); progresstex = 0; }
+}
+
+void resetlightmaps(bool fullclean)
+{
+    cleanuplightmaps();
+    lightmaps.shrink(0);
+    compressed.clear();
+    clearlightcache();
+    if(fullclean) while(lightmapworkers.length()) delete lightmapworkers.pop();
+}
+
+lightmapworker::lightmapworker()
+{
+    buf = new uchar[LIGHTMAPBUFSIZE];
+    bufstart = bufused = 0;
+    firstlightmap = lastlightmap = curlightmaps = NULL;
+    ambient = new uchar[4*(LM_MAXW + 4)*(LM_MAXH + 4)];
+    blur = new uchar[4*(LM_MAXW + 4)*(LM_MAXH + 4)];
+    colordata = new vec[4*(LM_MAXW+1 + 4)*(LM_MAXH+1 + 4)];
+    raydata = new vec[(LM_MAXW + 4)*(LM_MAXH + 4)];
+    shadowraycache = newshadowraycache();
+    blendmapcache = newblendmapcache();
+    needspace = doneworking = false;
+    spacecond = NULL;
+    thread = NULL;
+}
+
+lightmapworker::~lightmapworker()
+{
+    cleanupthread();
+    delete[] buf;
+    delete[] ambient;
+    delete[] blur;
+    delete[] colordata;
+    delete[] raydata;
+    freeshadowraycache(shadowraycache);
+    freeblendmapcache(blendmapcache);
+}
+
+void lightmapworker::cleanupthread()
+{
+    if(spacecond) { SDL_DestroyCond(spacecond); spacecond = NULL; }
+    thread = NULL;
+}
+
+void lightmapworker::reset()
+{
+    bufstart = bufused = 0;
+    firstlightmap = lastlightmap = curlightmaps = NULL;
+    needspace = doneworking = false;
+    resetshadowraycache(shadowraycache);
+}
+
+bool lightmapworker::setupthread()
+{
+    if(!spacecond) spacecond = SDL_CreateCond();
+    if(!spacecond) return false;
+    thread = SDL_CreateThread(work, "lightmap worker", this);
+    return thread!=NULL;
+}
+
+static Uint32 calclighttimer(Uint32 interval, void *param)
+{
+    check_calclight_progress = true;
+    return interval;
+}
+
+bool setlightmapquality(int quality)
+{
+    switch(quality)
+    {
+        case  1: lmshadows = 2; lmaa = 3; lerptjoints = 1; break;
+        case  0: lmshadows = lmshadows_; lmaa = lmaa_; lerptjoints = lerptjoints_; break;
+        case -1: lmshadows = 1; lmaa = 0; lerptjoints = 0; break;
+        default: return false;
+    }
+    return true;
+}
+
+VARP(lightthreads, 0, 0, 16);
+
+#define ALLOCLOCK(name, init) { if(lightmapping > 1) name = init(); if(!name) lightmapping = 1; }
+#define FREELOCK(name, destroy) { if(name) { destroy(name); name = NULL; } }
+
+static void cleanuplocks()
+{
+    FREELOCK(lightlock, SDL_DestroyMutex);
+    FREELOCK(tasklock, SDL_DestroyMutex);
+    FREELOCK(fullcond, SDL_DestroyCond);
+    FREELOCK(emptycond, SDL_DestroyCond);
+}
+
+static void setupthreads(int numthreads)
+{
+    loopi(2) lightmaptasks[i].setsize(0);
+    lightmapexts.setsize(0);
+    packidx = allocidx = 0;
+    lightmapping = numthreads;
+    if(lightmapping > 1)
+    {
+        ALLOCLOCK(lightlock, SDL_CreateMutex);
+        ALLOCLOCK(tasklock, SDL_CreateMutex);
+        ALLOCLOCK(fullcond, SDL_CreateCond);
+        ALLOCLOCK(emptycond, SDL_CreateCond);
+    }
+    while(lightmapworkers.length() < lightmapping) lightmapworkers.add(new lightmapworker);
+    loopi(lightmapping)
+    {
+        lightmapworker *w = lightmapworkers[i];
+        w->reset();
+        if(lightmapping <= 1 || w->setupthread()) continue;
+        w->cleanupthread();
+        lightmapping = i >= 1 ? max(i, 2) : 1;
+        break;
+    }
+    if(lightmapping <= 1) cleanuplocks();
+}
+
+static void cleanupthreads()
+{
+    processtasks(true);
+    if(lightmapping > 1)
+    {
+        SDL_LockMutex(tasklock);
+        loopv(lightmapworkers) lightmapworkers[i]->doneworking = true;
+        SDL_CondBroadcast(fullcond);
+        loopv(lightmapworkers)
+        {
+            lightmapworker *w = lightmapworkers[i];
+            if(w->needspace && w->spacecond) SDL_CondSignal(w->spacecond);
+        }
+        SDL_UnlockMutex(tasklock);
+        loopv(lightmapworkers) 
+        {
+            lightmapworker *w = lightmapworkers[i];
+            if(w->thread) SDL_WaitThread(w->thread, NULL);
+        }
+    }
+    loopv(lightmapexts)
+    {
+        lightmapext &e = lightmapexts[i];
+        setcubeext(*e.c, e.ext);
+    }
+    loopv(lightmapworkers) lightmapworkers[i]->cleanupthread();
+    cleanuplocks();
+    lightmapping = 0;
+}
+
+void calclight(int *quality)
+{
+    if(!setlightmapquality(*quality))
+    {
+        conoutf(CON_ERROR, "valid range for calclight quality is -1..1"); 
+        return;
+    }
+    renderbackground("computing lightmaps... (esc to abort)");
+    mpremip(true);
+    optimizeblendmap();
+    loadlayermasks();
+    int numthreads = lightthreads > 0 ? lightthreads : numcpus;
+    if(numthreads > 1) preloadusedmapmodels(false, true);
+    resetlightmaps(false);
+    clearsurfaces(worldroot);
+    taskprogress = progress = 0;
+    progresstexticks = 0;
+    progresslightmap = -1;
+    calclight_canceled = false;
+    check_calclight_progress = false;
+    SDL_TimerID timer = SDL_AddTimer(250, calclighttimer, NULL);
+    Uint32 start = SDL_GetTicks();
+    calcnormals(lerptjoints > 0);
+    show_calclight_progress();
+    setupthreads(numthreads);
+    generatelightmaps(worldroot, ivec(0, 0, 0), worldsize >> 1);
+    cleanupthreads();
+    clearnormals();
+    Uint32 end = SDL_GetTicks();
+    if(timer) SDL_RemoveTimer(timer);
+    uint total = 0, lumels = 0;
+    loopv(lightmaps)
+    {
+        insertunlit(i);
+        if(!editmode) lightmaps[i].finalize();
+        total += lightmaps[i].lightmaps;
+        lumels += lightmaps[i].lumels;
+    }
+    if(!editmode) compressed.clear();
+    initlights();
+    renderbackground("lighting done...");
+    allchanged();
+    if(calclight_canceled)
+        conoutf("calclight aborted");
+    else
+        conoutf("generated %d lightmaps using %d%% of %d textures (%.1f seconds)",
+            total,
+            lightmaps.length() ? lumels * 100 / (lightmaps.length() * LM_PACKW * LM_PACKH) : 0,
+            lightmaps.length(),
+            (end - start) / 1000.0f);
+}
+
+COMMAND(calclight, "i");
+
+VAR(patchnormals, 0, 0, 1);
+
+void patchlight(int *quality)
+{
+    if(noedit(true)) return;
+    if(!setlightmapquality(*quality))
+    {
+        conoutf(CON_ERROR, "valid range for patchlight quality is -1..1"); 
+        return;
+    }
+    renderbackground("patching lightmaps... (esc to abort)");
+    loadlayermasks();
+    int numthreads = lightthreads > 0 ? lightthreads : numcpus;
+    if(numthreads > 1) preloadusedmapmodels(false, true);
+    cleanuplightmaps();
+    taskprogress = progress = 0;
+    progresstexticks = 0;
+    progresslightmap = -1;
+    int total = 0, lumels = 0;
+    loopv(lightmaps)
+    {
+        if((lightmaps[i].type&LM_TYPE) != LM_BUMPMAP1) progresslightmap = i;
+        total -= lightmaps[i].lightmaps;
+        lumels -= lightmaps[i].lumels;
+    }
+    calclight_canceled = false;
+    check_calclight_progress = false;
+    SDL_TimerID timer = SDL_AddTimer(250, calclighttimer, NULL);
+    if(patchnormals) renderprogress(0, "computing normals...");
+    Uint32 start = SDL_GetTicks();
+    if(patchnormals) calcnormals(lerptjoints > 0);
+    show_calclight_progress();
+    setupthreads(numthreads);
+    generatelightmaps(worldroot, ivec(0, 0, 0), worldsize >> 1);
+    cleanupthreads();
+    if(patchnormals) clearnormals();
+    Uint32 end = SDL_GetTicks();
+    if(timer) SDL_RemoveTimer(timer);
+    loopv(lightmaps)
+    {
+        total += lightmaps[i].lightmaps;
+        lumels += lightmaps[i].lumels;
+    }
+    initlights();
+    renderbackground("lighting done...");
+    allchanged();
+    if(calclight_canceled)
+        conoutf("patchlight aborted");
+    else
+        conoutf("patched %d lightmaps using %d%% of %d textures (%.1f seconds)",
+            total,
+            lightmaps.length() ? lumels * 100 / (lightmaps.length() * LM_PACKW * LM_PACKH) : 0,
+            lightmaps.length(),
+            (end - start) / 1000.0f); 
+}
+
+COMMAND(patchlight, "i");
+
+void clearlightmaps()
+{
+    if(noedit(true)) return;
+    renderprogress(0, "clearing lightmaps...");
+    resetlightmaps(false);
+    clearsurfaces(worldroot);
+    initlights();
+    allchanged();
+}
+
+COMMAND(clearlightmaps, "");
+
+void setfullbrightlevel(int fullbrightlevel)
+{
+    if(lightmaptexs.length() > LMID_BRIGHT)
+    {
+        uchar bright[3] = { uchar(fullbrightlevel), uchar(fullbrightlevel), uchar(fullbrightlevel) };
+        createtexture(lightmaptexs[LMID_BRIGHT].id, 1, 1, bright, 0, 1);
+    }
+    initlights();
+}
+
+VARF(fullbright, 0, 0, 1, if(lightmaptexs.length()) { initlights(); lightents(); });
+VARF(fullbrightlevel, 0, 128, 255, setfullbrightlevel(fullbrightlevel));
+
+vector<LightMapTexture> lightmaptexs;
+
+static void rotatenormals(LightMap &lmlv, int x, int y, int w, int h, bool flipx, bool flipy, bool swapxy)
+{
+    uchar *lv = lmlv.data + 3*(y*LM_PACKW + x);
+    int stride = 3*(LM_PACKW-w);
+    loopi(h)
+    {
+        loopj(w)
+        {
+            if(flipx) lv[0] = 255 - lv[0];
+            if(flipy) lv[1] = 255 - lv[1];
+            if(swapxy) swap(lv[0], lv[1]);
+            lv += 3;
+        }
+        lv += stride;
+    }
+}
+
+static void rotatenormals(cube *c)
+{
+    loopi(8)
+    {
+        cube &ch = c[i];
+        if(ch.children)
+        {
+            rotatenormals(ch.children);
+            continue;
+        }
+        else if(!ch.ext) continue;
+        loopj(6) if(lightmaps.inrange(ch.ext->surfaces[j].lmid[0]+1-LMID_RESERVED))
+        {
+            VSlot &vslot = lookupvslot(ch.texture[j], false);
+            if(!vslot.rotation) continue;
+            surfaceinfo &surface = ch.ext->surfaces[j];
+            int numverts = surface.numverts&MAXFACEVERTS;
+            if(!numverts) continue;
+            LightMap &lmlv = lightmaps[surface.lmid[0]+1-LMID_RESERVED];
+            if((lmlv.type&LM_TYPE)!=LM_BUMPMAP1) continue;
+            ushort x1 = USHRT_MAX, y1 = USHRT_MAX, x2 = 0, y2 = 0;
+            vertinfo *verts = ch.ext->verts() + surface.verts;
+            loopk(numverts)
+            {
+                vertinfo &v = verts[k];
+                x1 = min(x1, v.u);
+                y1 = min(y1, v.u);
+                x2 = max(x2, v.u);
+                y2 = max(y2, v.v);
+            }
+            if(x1 > x2 || y1 > y2) continue;
+            x1 /= (USHRT_MAX+1)/LM_PACKW;
+            y1 /= (USHRT_MAX+1)/LM_PACKH;
+            x2 /= (USHRT_MAX+1)/LM_PACKW;
+            y2 /= (USHRT_MAX+1)/LM_PACKH;
+            const texrotation &r = texrotations[vslot.rotation < 4 ? 4-vslot.rotation : vslot.rotation];
+            rotatenormals(lmlv, x1, y1, x2-x1, y1-y1, r.flipx, r.flipy, r.swapxy);
+        }
+    }
+}
+
+void fixlightmapnormals()
+{
+    rotatenormals(worldroot);
+}
+
+void fixrotatedlightmaps(cube &c, const ivec &co, int size)
+{
+    if(c.children)
+    {
+        loopi(8) fixrotatedlightmaps(c.children[i], ivec(i, co, size>>1), size>>1);
+        return;
+    }
+    if(!c.ext) return;
+    loopi(6) 
+    {
+        if(c.merged&(1<<i)) continue;
+        surfaceinfo &surf = c.ext->surfaces[i];
+        int numverts = surf.numverts&MAXFACEVERTS;
+        if(numverts!=4 || (surf.lmid[0] < LMID_RESERVED && surf.lmid[1] < LMID_RESERVED)) continue;
+        vertinfo *verts = c.ext->verts() + surf.verts;
+        int vis = visibletris(c, i, co, size);
+        if(!vis || vis==3) continue;
+        if((verts[0].u != verts[1].u || verts[0].v != verts[1].v) &&
+           (verts[0].u != verts[3].u || verts[0].v != verts[3].v) &&
+           (verts[2].u != verts[1].u || verts[2].v != verts[1].v) &&
+           (verts[2].u != verts[3].u || verts[2].v != verts[3].v))
+            continue;
+        if(vis&4)
+        {
+            vertinfo tmp = verts[0];
+            verts[0].x = verts[1].x; verts[0].y = verts[1].y; verts[0].z = verts[1].z;
+            verts[1].x = verts[2].x; verts[1].y = verts[2].y; verts[1].z = verts[2].z;
+            verts[2].x = verts[3].x; verts[2].y = verts[3].y; verts[2].z = verts[3].z;
+            verts[3].x = tmp.x; verts[3].y = tmp.y; verts[3].z = tmp.z;
+            if(surf.numverts&LAYER_DUP) loopk(4) 
+            {
+                vertinfo &v = verts[k], &b = verts[k+4];
+                b.x = v.x;
+                b.y = v.y;
+                b.z = v.z;
+            }
+        }
+        surf.numverts = (surf.numverts & ~MAXFACEVERTS) | 3;
+        if(vis&2)
+        {
+            verts[1] = verts[2]; verts[2] = verts[3];
+            if(surf.numverts&LAYER_DUP) { verts[3] = verts[4]; verts[4] = verts[6]; verts[5] = verts[7]; }
+        }
+        else if(surf.numverts&LAYER_DUP) { verts[3] = verts[4]; verts[4] = verts[5]; verts[5] = verts[6]; }
+    }
+}
+
+void fixrotatedlightmaps()
+{
+    loopi(8) fixrotatedlightmaps(worldroot[i], ivec(i, ivec(0, 0, 0), worldsize>>1), worldsize>>1);
+}
+
+static void copylightmap(LightMap &lm, uchar *dst, size_t stride)
+{
+    const uchar *c = lm.data;
+    loopi(LM_PACKH)
+    {
+        memcpy(dst, c, lm.bpp*LM_PACKW);
+        c += lm.bpp*LM_PACKW;
+        dst += stride;
+    }
+}
+
+void genreservedlightmaptexs()
+{
+    while(lightmaptexs.length() < LMID_RESERVED)
+    {
+        LightMapTexture &tex = lightmaptexs.add();
+        tex.type = lightmaptexs.length()&1 ? LM_DIFFUSE : LM_BUMPMAP1;
+        glGenTextures(1, &tex.id);
+    }
+    uchar unlit[3] = { ambientcolor[0], ambientcolor[1], ambientcolor[2] };
+    createtexture(lightmaptexs[LMID_AMBIENT].id, 1, 1, unlit, 0, 1);
+    bvec front(128, 128, 255);
+    createtexture(lightmaptexs[LMID_AMBIENT1].id, 1, 1, &front, 0, 1);
+    uchar bright[3] = { uchar(fullbrightlevel), uchar(fullbrightlevel), uchar(fullbrightlevel) };
+    createtexture(lightmaptexs[LMID_BRIGHT].id, 1, 1, bright, 0, 1);
+    createtexture(lightmaptexs[LMID_BRIGHT1].id, 1, 1, &front, 0, 1);
+    uchar dark[3] = { 0, 0, 0 };
+    createtexture(lightmaptexs[LMID_DARK].id, 1, 1, dark, 0, 1);
+    createtexture(lightmaptexs[LMID_DARK1].id, 1, 1, &front, 0, 1);
+}
+
+static void findunlit(int i)
+{
+    LightMap &lm = lightmaps[i];
+    if(lm.unlitx>=0) return;
+    else if((lm.type&LM_TYPE)==LM_BUMPMAP0)
+    {
+        if(i+1>=lightmaps.length() || (lightmaps[i+1].type&LM_TYPE)!=LM_BUMPMAP1) return;
+    }
+    else if((lm.type&LM_TYPE)!=LM_DIFFUSE) return;
+    uchar *data = lm.data;
+    loop(y, 2) loop(x, LM_PACKW)
+    {
+        if(!data[0] && !data[1] && !data[2])
+        {
+            memcpy(data, ambientcolor.v, 3);
+            if((lm.type&LM_TYPE)==LM_BUMPMAP0) ((bvec *)lightmaps[i+1].data)[y*LM_PACKW + x] = bvec(128, 128, 255);
+            lm.unlitx = x;
+            lm.unlity = y;
+            return;
+        }
+        if(data[0]==ambientcolor[0] && data[1]==ambientcolor[1] && data[2]==ambientcolor[2])
+        {
+            if((lm.type&LM_TYPE)!=LM_BUMPMAP0 || ((bvec *)lightmaps[i+1].data)[y*LM_PACKW + x] == bvec(128, 128, 255))
+            {
+                lm.unlitx = x;
+                lm.unlity = y;
+                return;
+            }
+        }
+        data += lm.bpp;
+    }
+}
+
+VARF(roundlightmaptex, 0, 4, 16, { cleanuplightmaps(); initlights(); allchanged(); });
+VARF(batchlightmaps, 0, 4, 256, { cleanuplightmaps(); initlights(); allchanged(); });
+
+void genlightmaptexs(int flagmask, int flagval)
+{
+    if(lightmaptexs.length() < LMID_RESERVED) genreservedlightmaptexs();
+
+    int remaining[LM_TYPE+1] = { 0 }, total = 0; 
+    loopv(lightmaps) 
+    {
+        LightMap &lm = lightmaps[i];
+        if(lm.tex >= 0 || (lm.type&flagmask)!=flagval) continue;
+        int type = lm.type&LM_TYPE;
+        remaining[type]++; 
+        total++;
+        if(lm.unlitx < 0) findunlit(i);
+    }
+
+    int sizelimit = (maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize)/max(LM_PACKW, LM_PACKH);
+    sizelimit = min(batchlightmaps, sizelimit*sizelimit);
+    while(total)
+    {
+        int type = LM_DIFFUSE;
+        LightMap *firstlm = NULL;
+        loopv(lightmaps)
+        {
+            LightMap &lm = lightmaps[i];
+            if(lm.tex >= 0 || (lm.type&flagmask) != flagval) continue;
+            type = lm.type&LM_TYPE;
+            firstlm = &lm; 
+            break; 
+        }
+        if(!firstlm) break;
+        int used = 0, uselimit = min(remaining[type], sizelimit);
+        do used++; while((1<<used) <= uselimit);
+        used--;
+        int oldval = remaining[type];
+        remaining[type] -= 1<<used;
+        if(remaining[type] && (2<<used) <= min(roundlightmaptex, sizelimit))
+        {
+            remaining[type] -= min(remaining[type], 1<<used);
+            used++;
+        }
+        total -= oldval - remaining[type];
+        LightMapTexture &tex = lightmaptexs.add();
+        tex.type = firstlm->type;
+        tex.w = LM_PACKW<<((used+1)/2);
+        tex.h = LM_PACKH<<(used/2);
+        int bpp = firstlm->bpp;
+        uchar *data = used ? new uchar[bpp*tex.w*tex.h] : NULL;
+        int offsetx = 0, offsety = 0;
+        loopv(lightmaps)
+        {
+            LightMap &lm = lightmaps[i];
+            if(lm.tex >= 0 || (lm.type&flagmask) != flagval || (lm.type&LM_TYPE) != type) continue;
+
+            lm.tex = lightmaptexs.length()-1;
+            lm.offsetx = offsetx;
+            lm.offsety = offsety;
+            if(tex.unlitx < 0 && lm.unlitx >= 0) 
+            { 
+                tex.unlitx = offsetx + lm.unlitx; 
+                tex.unlity = offsety + lm.unlity;
+            }
+
+            if(data) copylightmap(lm, &data[bpp*(offsety*tex.w + offsetx)], bpp*tex.w);
+
+            offsetx += LM_PACKW;
+            if(offsetx >= tex.w) { offsetx = 0; offsety += LM_PACKH; }
+            if(offsety >= tex.h) break;
+        }
+        
+        glGenTextures(1, &tex.id);
+        createtexture(tex.id, tex.w, tex.h, data ? data : firstlm->data, 3, 1, bpp==4 ? GL_RGBA : GL_RGB);
+        if(data) delete[] data;
+    }        
+}
+
+bool brightengeom = false, shouldlightents = false;
+
+void clearlights()
+{
+    clearlightcache();
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        extentity &e = *ents[i];
+        e.light.color = vec(1, 1, 1);
+        e.light.dir = vec(0, 0, 1);
+    }
+    shouldlightents = false;
+
+    genlightmaptexs(LM_ALPHA, 0);
+    genlightmaptexs(LM_ALPHA, LM_ALPHA);
+    brightengeom = true;
+}
+
+void lightent(extentity &e, float height)
+{
+    if(e.type==ET_LIGHT) return;
+    float ambient = 0.0f;
+    if(e.type==ET_MAPMODEL)
+    {
+        model *m = loadmodel(NULL, e.attr2);
+        if(m) height = m->above()*0.75f;
+    }
+    else if(e.type>=ET_GAMESPECIFIC) ambient = 0.4f;
+    vec target(e.o.x, e.o.y, e.o.z + height);
+    lightreaching(target, e.light.color, e.light.dir, false, &e, ambient);
+}
+
+void lightents(bool force)
+{
+    if(!force && !shouldlightents) return;
+
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents) lightent(*ents[i]);
+
+    shouldlightents = false;
+}
+
+void initlights()
+{
+    if((fullbright && editmode) || lightmaps.empty())
+    {
+        clearlights();
+        return;
+    }
+
+    clearlightcache();
+    genlightmaptexs(LM_ALPHA, 0);
+    genlightmaptexs(LM_ALPHA, LM_ALPHA);
+    brightengeom = false;
+    shouldlightents = true; 
+}
+
+static inline void fastskylight(const vec &o, float tolerance, uchar *skylight, int flags = RAY_ALPHAPOLY, extentity *t = NULL, bool fast = false)
+{
+    flags |= RAY_SHADOW;
+    if(skytexturelight) flags |= RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0);
+    if(fast)
+    {
+        static const vec ray(0, 0, 1);
+        if(shadowray(vec(ray).mul(tolerance).add(o), ray, 1e16f, flags, t)>1e15f)
+            memcpy(skylight, skylightcolor.v, 3);
+        else memcpy(skylight, ambientcolor.v, 3);
+    }
+    else
+    {
+        static const vec rays[5] =
+        {
+            vec(cosf(66*RAD)*cosf(65*RAD), sinf(66*RAD)*cosf(65*RAD), sinf(65*RAD)),
+            vec(cosf(156*RAD)*cosf(65*RAD), sinf(156*RAD)*cosf(65*RAD), sinf(65*RAD)),
+            vec(cosf(246*RAD)*cosf(65*RAD), sinf(246*RAD)*cosf(65*RAD), sinf(65*RAD)),
+            vec(cosf(336*RAD)*cosf(65*RAD), sinf(336*RAD)*cosf(65*RAD), sinf(65*RAD)),
+            vec(0, 0, 1),
+        };
+        int hit = 0;
+        loopi(5) if(shadowray(vec(rays[i]).mul(tolerance).add(o), rays[i], 1e16f, flags, t)>1e15f) hit++;
+        loopk(3) skylight[k] = uchar(ambientcolor[k] + (max(skylightcolor[k], ambientcolor[k]) - ambientcolor[k])*hit/5.0f);
+    }
+}
+
+void lightreaching(const vec &target, vec &color, vec &dir, bool fast, extentity *t, float ambient)
+{
+    if((fullbright && editmode) || lightmaps.empty())
+    {
+        color = vec(1, 1, 1);
+        dir = vec(0, 0, 1);
+        return;
+    }
+
+    color = dir = vec(0, 0, 0);
+    const vector<extentity *> &ents = entities::getents();
+    const vector<int> &lights = checklightcache(int(target.x), int(target.y));
+    loopv(lights)
+    {
+        extentity &e = *ents[lights[i]];
+        if(e.type != ET_LIGHT)
+            continue;
+    
+        vec ray(target);
+        ray.sub(e.o);
+        float mag = ray.magnitude();
+        if(e.attr1 && mag >= float(e.attr1))
+            continue;
+    
+        if(mag < 1e-4f) ray = vec(0, 0, -1);
+        else
+        {
+            ray.div(mag);
+            if(shadowray(e.o, ray, mag, RAY_SHADOW | RAY_POLY, t) < mag)
+                continue;
+        }
+
+        float intensity = 1;
+        if(e.attr1)
+            intensity -= mag / float(e.attr1);
+        if(e.attached && e.attached->type==ET_SPOTLIGHT)
+        {
+            vec spot = vec(e.attached->o).sub(e.o).normalize();
+            float maxatten = sincos360[clamp(int(e.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten);
+            if(spotatten <= 0) continue;
+            intensity *= spotatten;
+        }
+
+        //if(target==player->o)
+        //{
+        //    conoutf(CON_DEBUG, "%d - %f %f", i, intensity, mag);
+        //}
+        vec lightcol = vec(e.attr2, e.attr3, e.attr4).mul(1.0f/255);
+        color.add(vec(lightcol).mul(intensity));
+        dir.add(vec(ray).mul(-intensity*lightcol.x*lightcol.y*lightcol.z));
+    }
+    if(sunlight && shadowray(target, sunlightdir, 1e16f, RAY_SHADOW | RAY_POLY | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0), t) > 1e15f) 
+    {
+        vec lightcol = vec(sunlightcolor.x, sunlightcolor.y, sunlightcolor.z).mul(sunlightscale/255);
+        color.add(lightcol);
+        dir.add(vec(sunlightdir).mul(lightcol.x*lightcol.y*lightcol.z));
+    }
+    if(hasskylight())
+    {
+        uchar skylight[3];
+        if(t) calcskylight(NULL, target, vec(0, 0, 0), 0.5f, skylight, RAY_POLY, t);
+        else fastskylight(target, 0.5f, skylight, RAY_POLY, t, fast);
+        loopk(3) color[k] = min(1.5f, max(max(skylight[k]/255.0f, ambient), color[k]));
+    }
+    else loopk(3) color[k] = min(1.5f, max(max(ambientcolor[k]/255.0f, ambient), color[k]));
+    if(dir.iszero()) dir = vec(0, 0, 1);
+    else dir.normalize();
+}
+
+entity *brightestlight(const vec &target, const vec &dir)
+{
+    if(sunlight && sunlightdir.dot(dir) > 0 && shadowray(target, sunlightdir, 1e16f, RAY_SHADOW | RAY_POLY | (skytexturelight ? RAY_SKIPSKY | (useskytexture ? RAY_SKYTEX : 0) : 0)) > 1e15f)    
+        return &sunlightent;
+    const vector<extentity *> &ents = entities::getents();
+    const vector<int> &lights = checklightcache(int(target.x), int(target.y));
+    extentity *brightest = NULL;
+    float bintensity = 0;
+    loopv(lights)
+    {
+        extentity &e = *ents[lights[i]];
+        if(e.type != ET_LIGHT || vec(e.o).sub(target).dot(dir)<0)
+            continue;
+
+        vec ray(target);
+        ray.sub(e.o);
+        float mag = ray.magnitude();
+        if(e.attr1 && mag >= float(e.attr1))
+             continue;
+
+        ray.div(mag);
+        if(shadowray(e.o, ray, mag, RAY_SHADOW | RAY_POLY) < mag)
+            continue;
+        float intensity = 1;
+        if(e.attr1)
+            intensity -= mag / float(e.attr1);
+        if(e.attached && e.attached->type==ET_SPOTLIGHT)
+        {
+            vec spot = vec(e.attached->o).sub(e.o).normalize();
+            float maxatten = sincos360[clamp(int(e.attached->attr1), 1, 89)].x, spotatten = (ray.dot(spot) - maxatten) / (1 - maxatten);
+            if(spotatten <= 0) continue;
+            intensity *= spotatten;
+        }
+
+        if(!brightest || intensity > bintensity)
+        {
+            brightest = &e;
+            bintensity = intensity;
+        }
+    }
+    return brightest;
+}
+
+void dumplms()
+{
+    loopv(lightmaps)
+    {
+        ImageData temp(LM_PACKW, LM_PACKH, lightmaps[i].bpp, lightmaps[i].data);
+        const char *map = game::getclientmap(), *name = strrchr(map, '/');
+        defformatstring(buf, "lightmap_%s_%d.png", name ? name+1 : map, i);
+        savepng(buf, temp, true);
+    }
+}
+
+COMMAND(dumplms, "");
+
diff --git a/src/engine/lightmap.h b/src/engine/lightmap.h
new file mode 100644 (file)
index 0000000..51ddc73
--- /dev/null
@@ -0,0 +1,146 @@
+#define LM_MINW 2
+#define LM_MINH 2
+#define LM_MAXW 128
+#define LM_MAXH 128
+#define LM_PACKW 512
+#define LM_PACKH 512
+
+struct PackNode
+{
+    PackNode *child1, *child2;
+    ushort x, y, w, h;
+    int available;
+
+    PackNode() : child1(0), child2(0), x(0), y(0), w(LM_PACKW), h(LM_PACKH), available(min(LM_PACKW, LM_PACKH)) {}
+    PackNode(ushort x, ushort y, ushort w, ushort h) : child1(0), child2(0), x(x), y(y), w(w), h(h), available(min(w, h)) {}
+
+    void clear()
+    {
+        DELETEP(child1);
+        DELETEP(child2);
+    }
+
+    ~PackNode()
+    {
+        clear();
+    }
+
+    bool insert(ushort &tx, ushort &ty, ushort tw, ushort th);
+};
+
+enum 
+{ 
+    LM_DIFFUSE = 0, 
+    LM_BUMPMAP0, 
+    LM_BUMPMAP1, 
+    LM_TYPE = 0x0F,
+
+    LM_ALPHA = 1<<4,  
+    LM_FLAGS = 0xF0 
+};
+
+struct LightMap
+{
+    int type, bpp, tex, offsetx, offsety;
+    PackNode packroot;
+    uint lightmaps, lumels;
+    int unlitx, unlity; 
+    uchar *data;
+
+    LightMap()
+     : type(LM_DIFFUSE), bpp(3), tex(-1), offsetx(-1), offsety(-1),
+       lightmaps(0), lumels(0), unlitx(-1), unlity(-1),
+       data(NULL)
+    {
+    }
+
+    ~LightMap()
+    {
+        if(data) delete[] data;
+    }
+
+    void finalize()
+    {
+        packroot.clear();
+        packroot.available = 0;
+    }
+
+    void copy(ushort tx, ushort ty, uchar *src, ushort tw, ushort th);
+    bool insert(ushort &tx, ushort &ty, uchar *src, ushort tw, ushort th);
+};
+
+extern vector<LightMap> lightmaps;
+
+struct LightMapTexture
+{
+    int w, h, type;
+    GLuint id;
+    int unlitx, unlity;
+
+    LightMapTexture()
+     : w(0), h(0), type(LM_DIFFUSE), id(0), unlitx(-1), unlity(-1)
+    {}
+};
+
+extern vector<LightMapTexture> lightmaptexs;
+
+extern bvec ambientcolor, skylightcolor, sunlightcolor;
+extern float sunlightscale;
+extern vec sunlightdir;
+
+extern void clearlights();
+extern void initlights();
+extern void lightents(bool force = false);
+extern void clearlightcache(int id = -1);
+extern void resetlightmaps(bool fullclean = true);
+extern void brightencube(cube &c);
+extern void setsurfaces(cube &c, const surfaceinfo *surfs, const vertinfo *verts, int numverts);
+extern void setsurface(cube &c, int orient, const surfaceinfo &surf, const vertinfo *verts, int numverts);
+extern void previewblends(const ivec &bo, const ivec &bs);
+
+struct lerpvert
+{
+    vec normal;
+    vec2 tc;
+
+    bool operator==(const lerpvert &l) const { return tc == l.tc;; }
+    bool operator!=(const lerpvert &l) const { return tc != l.tc; }
+};
+    
+struct lerpbounds
+{
+    const lerpvert *min;
+    const lerpvert *max;
+    float u, ustep;
+    vec normal, nstep;
+    int winding;
+};
+
+extern void calcnormals(bool lerptjoints = false);
+extern void clearnormals();
+extern void findnormal(const vec &key, const vec &surface, vec &v);
+extern void calclerpverts(const vec2 *c, const vec *n, lerpvert *lv, int &numv);
+extern void initlerpbounds(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end);
+extern void lerpnormal(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end, vec &normal, vec &nstep);
+
+#define CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, before, after) \
+    if(check_calclight_progress) \
+    { \
+        if(!calclight_canceled) \
+        { \
+            before; \
+            show_calclight_progress(); \
+            check_calclight_canceled(); \
+            after; \
+        } \
+        if(calclight_canceled) { exit; } \
+    }
+#define CHECK_CALCLIGHT_PROGRESS(exit, show_calclight_progress) CHECK_CALCLIGHT_PROGRESS_LOCKED(exit, show_calclight_progress, , )
+
+extern bool calclight_canceled;
+extern volatile bool check_calclight_progress;
+
+extern void check_calclight_canceled();
+
+extern int lightmapping;
+
diff --git a/src/engine/lightning.h b/src/engine/lightning.h
new file mode 100644 (file)
index 0000000..bc6e21c
--- /dev/null
@@ -0,0 +1,123 @@
+#define MAXLIGHTNINGSTEPS 64
+#define LIGHTNINGSTEP 8
+int lnjitterx[2][MAXLIGHTNINGSTEPS], lnjittery[2][MAXLIGHTNINGSTEPS];
+int lnjitterframe = 0, lastlnjitter = 0;
+
+VAR(lnjittermillis, 0, 100, 1000);
+VAR(lnjitterradius, 0, 4, 100);
+FVAR(lnjitterscale, 0, 0.5f, 10);
+VAR(lnscrollmillis, 1, 300, 5000);
+FVAR(lnscrollscale, 0, 0.125f, 10);
+FVAR(lnblendpower, 0, 0.25f, 1000);
+
+static void calclightningjitter(int frame)
+{
+    loopi(MAXLIGHTNINGSTEPS)
+    {
+        lnjitterx[lnjitterframe][i] = -lnjitterradius + rnd(2*lnjitterradius + 1);
+        lnjittery[lnjitterframe][i] = -lnjitterradius + rnd(2*lnjitterradius + 1);
+    }
+}
+
+static void setuplightning()
+{
+    if(!lastlnjitter || lastmillis-lastlnjitter > lnjittermillis)
+    {
+        if(!lastlnjitter) calclightningjitter(lnjitterframe);
+        lastlnjitter = lastmillis - (lastmillis%lnjittermillis);
+        calclightningjitter(lnjitterframe ^= 1);
+    }
+}
+
+static void renderlightning(Texture *tex, const vec &o, const vec &d, float sz)
+{
+    vec step(d);
+    step.sub(o);
+    float len = step.magnitude();
+    int numsteps = clamp(int(ceil(len/LIGHTNINGSTEP)), 2, MAXLIGHTNINGSTEPS);
+    step.div(numsteps+1);
+    int jitteroffset = detrnd(int(d.x+d.y+d.z), MAXLIGHTNINGSTEPS);
+    vec cur(o), up, right;
+    up.orthogonal(step);
+    up.normalize();
+    right.cross(up, step);
+    right.normalize();
+    float scroll = -float(lastmillis%lnscrollmillis)/lnscrollmillis, 
+          scrollscale = lnscrollscale*(LIGHTNINGSTEP*tex->ys)/(sz*tex->xs),
+          blend = pow(clamp(float(lastmillis - lastlnjitter)/lnjittermillis, 0.0f, 1.0f), lnblendpower),
+          jitter0 = (1-blend)*lnjitterscale*sz/lnjitterradius, jitter1 = blend*lnjitterscale*sz/lnjitterradius; 
+    gle::begin(GL_TRIANGLE_STRIP);
+    loopj(numsteps)
+    {
+        vec next(cur);
+        next.add(step);
+        if(j+1==numsteps) next = d;
+        else
+        {
+            int lj = (j+jitteroffset)%MAXLIGHTNINGSTEPS;
+            next.add(vec(right).mul((jitter1*lnjitterx[lnjitterframe][lj] + jitter0*lnjitterx[lnjitterframe^1][lj])));
+            next.add(vec(up).mul((jitter1*lnjittery[lnjitterframe][lj] + jitter0*lnjittery[lnjitterframe^1][lj])));
+        }
+        vec dir1 = next, dir2 = next, across;
+        dir1.sub(cur);
+        dir2.sub(camera1->o);
+        across.cross(dir2, dir1).normalize().mul(sz);
+        gle::attribf(cur.x-across.x, cur.y-across.y, cur.z-across.z);
+            gle::attribf(scroll, 1);
+        gle::attribf(cur.x+across.x, cur.y+across.y, cur.z+across.z);
+            gle::attribf(scroll, 0);
+        scroll += scrollscale;
+        if(j+1==numsteps)
+        {
+            gle::attribf(next.x-across.x, next.y-across.y, next.z-across.z);
+                gle::attribf(scroll, 1);
+            gle::attribf(next.x+across.x, next.y+across.y, next.z+across.z);
+                gle::attribf(scroll, 0);
+        }
+        cur = next;
+    }
+    gle::end();
+}
+
+struct lightningrenderer : listrenderer
+{
+    lightningrenderer()
+        : listrenderer("packages/particles/lightning.jpg", 2, PT_LIGHTNING|PT_TRACK|PT_GLARE)
+    {}
+
+    void startrender()
+    {
+        glDisable(GL_CULL_FACE);
+        gle::defattrib(gle::ATTRIB_VERTEX, 3, GL_FLOAT);
+        gle::defattrib(gle::ATTRIB_TEXCOORD0, 2, GL_FLOAT);
+    }
+
+    void endrender()
+    {
+        glEnable(GL_CULL_FACE);
+    }
+
+    void update()
+    {
+        setuplightning();
+    }
+
+    void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+    {
+        pe.maxfade = max(pe.maxfade, fade);
+        pe.extendbb(o, size);
+        pe.extendbb(d, size);
+    }
+
+    void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+    {
+        blend = min(blend<<2, 255);
+        if(type&PT_MOD) //multiply alpha into color
+            gle::colorub((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8);
+        else
+            gle::color(p->color, blend);
+        renderlightning(tex, o, d, p->size);
+    }
+};
+static lightningrenderer lightnings;
+
diff --git a/src/engine/main.cpp b/src/engine/main.cpp
new file mode 100644 (file)
index 0000000..f522479
--- /dev/null
@@ -0,0 +1,1422 @@
+// main.cpp: initialisation & main loop
+
+#include "engine.h"
+
+#ifdef SDL_VIDEO_DRIVER_X11
+#include "SDL_syswm.h"
+#endif
+
+extern void cleargamma();
+
+void cleanup()
+{
+    recorder::stop();
+    cleanupserver();
+    SDL_ShowCursor(SDL_TRUE);
+    SDL_SetRelativeMouseMode(SDL_FALSE);
+    if(screen) SDL_SetWindowGrab(screen, SDL_FALSE);
+    cleargamma();
+    freeocta(worldroot);
+    extern void clear_command(); clear_command();
+    extern void clear_console(); clear_console();
+    extern void clear_mdls();    clear_mdls();
+    extern void clear_sound();   clear_sound();
+    closelogfile();
+    #ifdef __APPLE__
+        if(screen) SDL_SetWindowFullscreen(screen, 0);
+    #endif
+    SDL_Quit();
+}
+
+extern void writeinitcfg();
+
+void quit()                     // normal exit
+{
+    writeinitcfg();
+    writeservercfg();
+    abortconnect();
+    disconnect();
+    localdisconnect();
+    writecfg();
+    cleanup();
+    exit(EXIT_SUCCESS);
+}
+
+void fatal(const char *s, ...)    // failure exit
+{
+    static int errors = 0;
+    errors++;
+
+    if(errors <= 2) // print up to one extra recursive error
+    {
+        defvformatstring(msg,s,s);
+        logoutf("%s", msg);
+
+        if(errors <= 1) // avoid recursion
+        {
+            if(SDL_WasInit(SDL_INIT_VIDEO))
+            {
+                SDL_ShowCursor(SDL_TRUE);
+                SDL_SetRelativeMouseMode(SDL_FALSE);
+                if(screen) SDL_SetWindowGrab(screen, SDL_FALSE);
+                cleargamma();
+                #ifdef __APPLE__
+                    if(screen) SDL_SetWindowFullscreen(screen, 0);
+                #endif
+            }
+            SDL_Quit();
+            SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Cube 2: Sauerbraten fatal error", msg, NULL);
+        }
+    }
+
+    exit(EXIT_FAILURE);
+}
+
+int curtime = 0, lastmillis = 1, elapsedtime = 0, totalmillis = 1;
+
+dynent *player = NULL;
+
+int initing = NOT_INITING;
+
+bool initwarning(const char *desc, int level, int type)
+{
+    if(initing < level)
+    {
+        addchange(desc, type);
+        return true;
+    }
+    return false;
+}
+
+VAR(desktopw, 1, 0, 0);
+VAR(desktoph, 1, 0, 0);
+int screenw = 0, screenh = 0;
+SDL_Window *screen = NULL;
+SDL_GLContext glcontext = NULL;
+
+#define SCR_MINW 320
+#define SCR_MINH 200
+#define SCR_MAXW 10000
+#define SCR_MAXH 10000
+#define SCR_DEFAULTW 1024
+#define SCR_DEFAULTH 768
+VARF(scr_w, SCR_MINW, -1, SCR_MAXW, initwarning("screen resolution"));
+VARF(scr_h, SCR_MINH, -1, SCR_MAXH, initwarning("screen resolution"));
+VARF(depthbits, 0, 0, 32, initwarning("depth-buffer precision"));
+VARF(fsaa, -1, -1, 16, initwarning("anti-aliasing"));
+
+void writeinitcfg()
+{
+    stream *f = openutf8file("init.cfg", "w");
+    if(!f) return;
+    f->printf("// automatically written on exit, DO NOT MODIFY\n// modify settings in game\n");
+    extern int fullscreen, fullscreendesktop;
+    f->printf("fullscreen %d\n", fullscreen);
+    f->printf("fullscreendesktop %d\n", fullscreendesktop);
+    f->printf("scr_w %d\n", scr_w);
+    f->printf("scr_h %d\n", scr_h);
+    f->printf("depthbits %d\n", depthbits);
+    f->printf("fsaa %d\n", fsaa);
+    extern int usesound, soundchans, soundfreq, soundbufferlen;
+    extern char *audiodriver;
+    f->printf("usesound %d\n", usesound);
+    f->printf("soundchans %d\n", soundchans);
+    f->printf("soundfreq %d\n", soundfreq);
+    f->printf("soundbufferlen %d\n", soundbufferlen);
+    if(audiodriver[0]) f->printf("audiodriver %s\n", escapestring(audiodriver));
+    delete f;
+}
+
+COMMAND(quit, "");
+
+static void getbackgroundres(int &w, int &h)
+{
+    float wk = 1, hk = 1;
+    if(w < 1024) wk = 1024.0f/w;
+    if(h < 768) hk = 768.0f/h;
+    wk = hk = max(wk, hk);
+    w = int(ceil(w*wk));
+    h = int(ceil(h*hk));
+}
+
+string backgroundcaption = "";
+Texture *backgroundmapshot = NULL;
+string backgroundmapname = "";
+char *backgroundmapinfo = NULL;
+
+void setbackgroundinfo(const char *caption = NULL, Texture *mapshot = NULL, const char *mapname = NULL, const char *mapinfo = NULL)
+{
+    renderedframe = false;
+    copystring(backgroundcaption, caption ? caption : "");
+    backgroundmapshot = mapshot;
+    copystring(backgroundmapname, mapname ? mapname : "");
+    if(mapinfo != backgroundmapinfo)
+    {
+        DELETEA(backgroundmapinfo);
+        if(mapinfo) backgroundmapinfo = newstring(mapinfo);
+    }
+}
+
+void restorebackground(bool force = false)
+{
+    if(renderedframe)
+    {
+        if(!force) return;
+        setbackgroundinfo();
+    }
+    renderbackground(backgroundcaption[0] ? backgroundcaption : NULL, backgroundmapshot, backgroundmapname[0] ? backgroundmapname : NULL, backgroundmapinfo, true);
+}
+
+void bgquad(float x, float y, float w, float h, float tx = 0, float ty = 0, float tw = 1, float th = 1)
+{
+    gle::begin(GL_TRIANGLE_STRIP);
+    gle::attribf(x,   y);   gle::attribf(tx,      ty);
+    gle::attribf(x+w, y);   gle::attribf(tx + tw, ty);
+    gle::attribf(x,   y+h); gle::attribf(tx,      ty + th);
+    gle::attribf(x+w, y+h); gle::attribf(tx + tw, ty + th);
+    gle::end();
+}
+
+#include <sys/stat.h>
+
+static bool file_does_indeed_exist(const char *name) {
+    struct stat buffer;
+    return !stat(name, & buffer);
+}
+
+void renderbackground(const char *caption, Texture *mapshot, const char *mapname, const char *mapinfo, bool restore, bool force)
+{
+    if(!inbetweenframes && !force) return;
+
+    if(!restore || force) stopsounds(); // stop sounds while loading
+
+    int w = screenw, h = screenh;
+    if(forceaspect) w = int(ceil(h*forceaspect));
+    getbackgroundres(w, h);
+    gettextres(w, h);
+
+    static int lastupdate = -1, lastw = -1, lasth = -1;
+    static float backgroundu = 0, backgroundv = 0, detailu = 0, detailv = 0;
+    static int numdecals = 0;
+    static struct decal { float x, y, size; int side; } decals[12];
+    if((renderedframe && !mainmenu && lastupdate != lastmillis) || lastw != w || lasth != h)
+    {
+        lastupdate = lastmillis;
+        lastw = w;
+        lasth = h;
+
+        backgroundu = rndscale(1);
+        backgroundv = rndscale(1);
+        detailu = rndscale(1);
+        detailv = rndscale(1);
+        numdecals = sizeof(decals)/sizeof(decals[0]);
+        numdecals = numdecals/3 + rnd((numdecals*2)/3 + 1);
+        float maxsize = min(w, h)/16.0f;
+        loopi(numdecals)
+        {
+            decal d = { rndscale(w), rndscale(h), maxsize/2 + rndscale(maxsize/2), rnd(2) };
+            decals[i] = d;
+        }
+    }
+    else if(lastupdate != lastmillis) lastupdate = lastmillis;
+
+    //~if (mapname) {
+        //~defformatstring(backpath, "background/%s.png", mapname);
+        //~if (file_does_indeed_exist(backpath)) {
+            //~settexture(backpath, 0);
+            //~bgquad(0, 0, 1920, 1080, 0, 0, 1920, 1080);
+            //~gle::begin(GL_TRIANGLE_STRIP);
+            //~gle::attribf(0.0f, 0.0f); gle::attribf(0.0f, 0.0f);
+            //~gle::attribf(1.0f, 0.0f); gle::attribf(1920.0f, 0.0f);
+            //~gle::attribf(0.0f, 1.0f); gle::attribf(0.0f, 1.0f);
+            //~gle::attribf(1.0f, 1.0f); gle::attribf(1920.0f, 1.0f);
+            //~gle::end();
+        //~}
+    //~} else {
+        loopi(restore ? 1 : 3)
+        {
+            hudmatrix.ortho(0, w, h, 0, -1, 1);
+            resethudmatrix();
+
+            hudshader->set();
+            gle::colorf(1, 1, 1);
+
+            gle::defvertex(2);
+            gle::deftexcoord0();
+
+        //~defformatstring(backpath, "background/%s.png", mapname);
+        //~if (file_does_indeed_exist(backpath)) {
+            //~settexture(backpath, 0);
+            settexture("background/daemex.png", 0);
+            bgquad(0, 0, screenw, screenh, 0, 0, 1, 1);
+            //~settexture("data/background.png", 0);
+            //~float bu = w*0.67f/256.0f + backgroundu, bv = h*0.67f/256.0f + backgroundv;
+            //~bgquad(0, 0, w, h, 0, 0, bu, bv);
+            //~glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            glEnable(GL_BLEND);
+            //~settexture("data/background_detail.png", 0);
+            //~float du = w*0.8f/512.0f + detailu, dv = h*0.8f/512.0f + detailv;
+            //~bgquad(0, 0, w, h, 0, 0, du, dv);
+            //~settexture("data/background_decal.png", 3);
+            //~gle::begin(GL_QUADS);
+            //~loopj(numdecals)
+            //~{
+                //~float hsz = decals[j].size, hx = clamp(decals[j].x, hsz, w-hsz), hy = clamp(decals[j].y, hsz, h-hsz), side = decals[j].side;
+                //~gle::attribf(hx-hsz, hy-hsz); gle::attribf(side,   0);
+                //~gle::attribf(hx+hsz, hy-hsz); gle::attribf(1-side, 0);
+                //~gle::attribf(hx+hsz, hy+hsz); gle::attribf(1-side, 1);
+                //~gle::attribf(hx-hsz, hy+hsz); gle::attribf(side,   1);
+            //~}
+            //~gle::end();
+            float lh = 0.5f*min(w, h), lw = lh*2,
+                  lx = 0.5f*(w - lw), ly = 0.5f*(h*0.5f - lh);
+            settexture((maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize) >= 1024 && (screenw > 1280 || screenh > 800) ? "data/logo_1024.png" : "data/logo.png", 3);
+            bgquad(lx, ly, lw, lh);
+            if(caption)
+            {
+                int tw = text_width(caption);
+                float tsz = 0.04f*min(w, h)/FONTH,
+                      tx = 0.5f*(w - tw*tsz), ty = h - 0.075f*1.5f*min(w, h) - 1.25f*FONTH*tsz;
+                pushhudmatrix();
+                hudmatrix.translate(tx, ty, 0);
+                hudmatrix.scale(tsz, tsz, 1);
+                flushhudmatrix();
+                draw_text(caption, 0, 0);
+                pophudmatrix();
+            }
+            if(mapshot || mapname)
+            {
+                int infowidth = 12*FONTH;
+                float sz = 0.35f*min(w, h), msz = (0.75f*min(w, h) - sz)/(infowidth + FONTH), x = 0.5f*(w-sz), y = ly+lh - sz/15;
+                if(mapinfo)
+                {
+                    int mw, mh;
+                    text_bounds(mapinfo, mw, mh, infowidth);
+                    x -= 0.5f*(mw*msz + FONTH*msz);
+                }
+                if(mapshot && mapshot!=notexture)
+                {
+                    glBindTexture(GL_TEXTURE_2D, mapshot->id);
+                    bgquad(x, y, sz, sz);
+                }
+                else
+                {
+                    int qw, qh;
+                    text_bounds("?", qw, qh);
+                    float qsz = sz*0.5f/max(qw, qh);
+                    pushhudmatrix();
+                    hudmatrix.translate(x + 0.5f*(sz - qw*qsz), y + 0.5f*(sz - qh*qsz), 0);
+                    hudmatrix.scale(qsz, qsz, 1);
+                    flushhudmatrix();
+                    draw_text("?", 0, 0);
+                    pophudmatrix();
+                    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                }
+                settexture("data/mapshot_frame.png", 3);
+                bgquad(x, y, sz, sz);
+                if(mapname)
+                {
+                    int tw = text_width(mapname);
+                    float tsz = sz/(8*FONTH),
+                          tx = 0.9f*sz - tw*tsz, ty = 0.9f*sz - FONTH*tsz;
+                    if(tx < 0.1f*sz) { tsz = 0.1f*sz/tw; tx = 0.1f; }
+                    pushhudmatrix();
+                    hudmatrix.translate(x+tx, y+ty, 0);
+                    hudmatrix.scale(tsz, tsz, 1);
+                    flushhudmatrix();
+                    draw_text(mapname, 0, 0);
+                    pophudmatrix();
+                }
+                if(mapinfo)
+                {
+                    pushhudmatrix();
+                    hudmatrix.translate(x+sz+FONTH*msz, y, 0);
+                    hudmatrix.scale(msz, msz, 1);
+                    flushhudmatrix();
+                    draw_text(mapinfo, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, -1, infowidth);
+                    pophudmatrix();
+                }
+            }
+            glDisable(GL_BLEND);
+            if(!restore) swapbuffers(false);
+        }
+    //~}
+
+    if(!restore) setbackgroundinfo(caption, mapshot, mapname, mapinfo);
+}
+
+VAR(progressbackground, 0, 0, 1);
+
+float loadprogress = 0;
+
+void renderprogress(float bar, const char *text, GLuint tex, bool background)   // also used during loading
+{
+    if(!inbetweenframes || drawtex) return;
+
+    extern int menufps, maxfps;
+    int fps = menufps ? (maxfps ? min(maxfps, menufps) : menufps) : maxfps;
+    if(fps)
+    {
+        static int lastprogress = 0;
+        int ticks = SDL_GetTicks(), diff = ticks - lastprogress;
+        if(bar > 0 && diff >= 0 && diff < (1000 + fps-1)/fps) return;
+        lastprogress = ticks;
+    }
+
+    clientkeepalive();      // make sure our connection doesn't time out while loading maps etc.
+
+    SDL_PumpEvents(); // keep the event queue awake to avoid 'beachball' cursor
+
+    extern int mesa_swap_bug, curvsync;
+    bool forcebackground = progressbackground || (mesa_swap_bug && (curvsync || totalmillis==1));
+    if(background || forcebackground) restorebackground(forcebackground);
+
+    int w = screenw, h = screenh;
+    if(forceaspect) w = int(ceil(h*forceaspect));
+    getbackgroundres(w, h);
+    gettextres(w, h);
+
+    hudmatrix.ortho(0, w, h, 0, -1, 1);
+    resethudmatrix();
+
+    hudshader->set();
+    gle::colorf(1, 1, 1);
+
+    gle::defvertex(2);
+    gle::deftexcoord0();
+
+    float fh = 0.075f*min(w, h), fw = fh*10,
+          fx = renderedframe ? w - fw - fh/4 : 0.5f*(w - fw),
+          fy = renderedframe ? fh/4 : h - fh*1.5f,
+          fu1 = 0/512.0f, fu2 = 511/512.0f,
+          fv1 = 0/64.0f, fv2 = 52/64.0f;
+    settexture("data/loading_frame.png", 3);
+    bgquad(fx, fy, fw, fh, fu1, fv1, fu2-fu1, fv2-fv1);
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+    float bw = fw*(511 - 2*17)/511.0f, bh = fh*20/52.0f,
+          bx = fx + fw*17/511.0f, by = fy + fh*16/52.0f,
+          bv1 = 0/32.0f, bv2 = 20/32.0f,
+          su1 = 0/32.0f, su2 = 7/32.0f, sw = fw*7/511.0f,
+          eu1 = 23/32.0f, eu2 = 30/32.0f, ew = fw*7/511.0f,
+          mw = bw - sw - ew,
+          ex = bx+sw + max(mw*bar, fw*7/511.0f);
+    if(bar > 0)
+    {
+        settexture("data/loading_bar.png", 3);
+        gle::begin(GL_QUADS);
+        gle::attribf(bx,    by);    gle::attribf(su1, bv1);
+        gle::attribf(bx+sw, by);    gle::attribf(su2, bv1);
+        gle::attribf(bx+sw, by+bh); gle::attribf(su2, bv2);
+        gle::attribf(bx,    by+bh); gle::attribf(su1, bv2);
+
+        gle::attribf(bx+sw, by);    gle::attribf(su2, bv1);
+        gle::attribf(ex,    by);    gle::attribf(eu1, bv1);
+        gle::attribf(ex,    by+bh); gle::attribf(eu1, bv2);
+        gle::attribf(bx+sw, by+bh); gle::attribf(su2, bv2);
+
+        gle::attribf(ex,    by);    gle::attribf(eu1, bv1);
+        gle::attribf(ex+ew, by);    gle::attribf(eu2, bv1);
+        gle::attribf(ex+ew, by+bh); gle::attribf(eu2, bv2);
+        gle::attribf(ex,    by+bh); gle::attribf(eu1, bv2);
+        gle::end();
+    }
+
+    if(text)
+    {
+        int tw = text_width(text);
+        float tsz = bh*0.8f/FONTH;
+        if(tw*tsz > mw) tsz = mw/tw;
+        pushhudmatrix();
+        hudmatrix.translate(bx+sw, by + (bh - FONTH*tsz)/2, 0);
+        hudmatrix.scale(tsz, tsz, 1);
+        flushhudmatrix();
+        draw_text(text, 0, 0);
+        pophudmatrix();
+    }
+
+    glDisable(GL_BLEND);
+
+    if(tex)
+    {
+        glBindTexture(GL_TEXTURE_2D, tex);
+        float sz = 0.35f*min(w, h), x = 0.5f*(w-sz), y = 0.5f*min(w, h) - sz/15;
+        bgquad(x, y, sz, sz);
+
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        settexture("data/mapshot_frame.png", 3);
+        bgquad(x, y, sz, sz);
+        glDisable(GL_BLEND);
+    }
+
+    swapbuffers(false);
+}
+
+int keyrepeatmask = 0, textinputmask = 0;
+Uint32 textinputtime = 0;
+VAR(textinputfilter, 0, 5, 1000);
+
+void keyrepeat(bool on, int mask)
+{
+    if(on) keyrepeatmask |= mask;
+    else keyrepeatmask &= ~mask;
+}
+
+void textinput(bool on, int mask)
+{
+    if(on)
+    {
+        if(!textinputmask)
+        {
+            SDL_StartTextInput();
+            textinputtime = SDL_GetTicks();
+        }
+        textinputmask |= mask;
+    }
+    else if(textinputmask)
+    {
+        textinputmask &= ~mask;
+        if(!textinputmask) SDL_StopTextInput();
+    }
+}
+
+#ifdef WIN32
+// SDL_WarpMouseInWindow behaves erratically on Windows, so force relative mouse instead.
+VARN(relativemouse, userelativemouse, 1, 1, 0);
+#else
+VARNP(relativemouse, userelativemouse, 0, 1, 1);
+#endif
+
+bool shouldgrab = false, grabinput = false, minimized = false, canrelativemouse = true, relativemouse = false;
+
+#ifdef SDL_VIDEO_DRIVER_X11
+VAR(sdl_xgrab_bug, 0, 0, 1);
+#endif
+
+void inputgrab(bool on, bool delay = false)
+{
+#ifdef SDL_VIDEO_DRIVER_X11
+    bool wasrelativemouse = relativemouse;
+#endif
+    if(on)
+    {
+        SDL_ShowCursor(SDL_FALSE);
+        if(canrelativemouse && userelativemouse)
+        {
+            if(SDL_SetRelativeMouseMode(SDL_TRUE) >= 0)
+            {
+                SDL_SetWindowGrab(screen, SDL_TRUE);
+                relativemouse = true;
+            }
+            else
+            {
+                SDL_SetWindowGrab(screen, SDL_FALSE);
+                canrelativemouse = false;
+                relativemouse = false;
+            }
+        }
+    }
+    else
+    {
+        SDL_ShowCursor(SDL_TRUE);
+        if(relativemouse)
+        {
+            SDL_SetWindowGrab(screen, SDL_FALSE);
+            SDL_SetRelativeMouseMode(SDL_FALSE);
+            relativemouse = false;
+        }
+    }
+    shouldgrab = delay;
+
+#ifdef SDL_VIDEO_DRIVER_X11
+    if((relativemouse || wasrelativemouse) && sdl_xgrab_bug)
+    {
+        // Workaround for buggy SDL X11 pointer grabbing
+        union { SDL_SysWMinfo info; uchar buf[sizeof(SDL_SysWMinfo) + 128]; };
+        SDL_GetVersion(&info.version);
+        if(SDL_GetWindowWMInfo(screen, &info) && info.subsystem == SDL_SYSWM_X11)
+        {
+            if(relativemouse)
+            {
+                uint mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask | FocusChangeMask;
+                XGrabPointer(info.info.x11.display, info.info.x11.window, True, mask, GrabModeAsync, GrabModeAsync, info.info.x11.window, None, CurrentTime);
+            }
+            else XUngrabPointer(info.info.x11.display, CurrentTime);
+        }
+    }
+#endif
+}
+
+bool initwindowpos = false;
+
+void setfullscreen(bool enable)
+{
+    if(!screen) return;
+    //initwarning(enable ? "fullscreen" : "windowed");
+    extern int fullscreendesktop;
+    SDL_SetWindowFullscreen(screen, enable ? (fullscreendesktop ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN) : 0);
+    if(!enable)
+    {
+        SDL_SetWindowSize(screen, scr_w, scr_h);
+        if(initwindowpos)
+        {
+            int winx = SDL_WINDOWPOS_CENTERED, winy = SDL_WINDOWPOS_CENTERED;
+            SDL_SetWindowPosition(screen, winx, winy);
+            initwindowpos = false;
+        }
+    }
+}
+
+#ifdef _DEBUG
+VARF(fullscreen, 0, 0, 1, setfullscreen(fullscreen!=0));
+#else
+VARF(fullscreen, 0, 1, 1, setfullscreen(fullscreen!=0));
+#endif
+
+void resetfullscreen()
+{
+    setfullscreen(false);
+    setfullscreen(true);
+}
+
+VARF(fullscreendesktop, 0, 0, 1, if(fullscreen) resetfullscreen());
+
+void screenres(int w, int h)
+{
+    scr_w = clamp(w, SCR_MINW, SCR_MAXW);
+    scr_h = clamp(h, SCR_MINH, SCR_MAXH);
+    if(screen)
+    {
+        if(fullscreendesktop)
+        {
+            scr_w = min(scr_w, desktopw);
+            scr_h = min(scr_h, desktoph);
+        }
+        if(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN)
+        {
+            if(fullscreendesktop) gl_resize();
+            else resetfullscreen();
+            initwindowpos = true;
+        }
+        else
+        {
+            SDL_SetWindowSize(screen, scr_w, scr_h);
+            SDL_SetWindowPosition(screen, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+            initwindowpos = false;
+        }
+    }
+    else
+    {
+        initwarning("screen resolution");
+    }
+}
+
+ICOMMAND(screenres, "ii", (int *w, int *h), screenres(*w, *h));
+
+static void setgamma(int val)
+{
+    if(screen && SDL_SetWindowBrightness(screen, val/100.0f) < 0) conoutf(CON_ERROR, "Could not set gamma: %s", SDL_GetError());
+}
+
+static int curgamma = 100;
+VARFNP(gamma, reqgamma, 30, 100, 300,
+{
+    if(initing || reqgamma == curgamma) return;
+    curgamma = reqgamma;
+    setgamma(curgamma);
+});
+
+void restoregamma()
+{
+    if(initing || reqgamma == 100) return;
+    curgamma = reqgamma;
+    setgamma(curgamma);
+}
+
+void cleargamma()
+{
+    if(curgamma != 100 && screen) SDL_SetWindowBrightness(screen, 1.0f);
+}
+
+int curvsync = -1;
+void restorevsync()
+{
+    if(initing || !glcontext) return;
+    extern int vsync, vsynctear;
+    if(!SDL_GL_SetSwapInterval(vsync ? (vsynctear ? -1 : 1) : 0))
+        curvsync = vsync;
+}
+
+VARFP(vsync, 0, 0, 1, restorevsync());
+VARFP(vsynctear, 0, 0, 1, { if(vsync) restorevsync(); });
+
+void setupscreen()
+{
+    if(glcontext)
+    {
+        SDL_GL_DeleteContext(glcontext);
+        glcontext = NULL;
+    }
+    if(screen)
+    {
+        SDL_DestroyWindow(screen);
+        screen = NULL;
+    }
+    curvsync = -1;
+
+    SDL_Rect desktop;
+    if(SDL_GetDisplayBounds(0, &desktop) < 0) fatal("failed querying desktop bounds: %s", SDL_GetError());
+    desktopw = desktop.w;
+    desktoph = desktop.h;
+
+    if(scr_h < 0) scr_h = fullscreen ? desktoph : SCR_DEFAULTH;
+    if(scr_w < 0) scr_w = (scr_h*desktopw)/desktoph;
+    scr_w = clamp(scr_w, SCR_MINW, SCR_MAXW);
+    scr_h = clamp(scr_h, SCR_MINH, SCR_MAXH);
+    if(fullscreendesktop)
+    {
+        scr_w = min(scr_w, desktopw);
+        scr_h = min(scr_h, desktoph);
+    }
+
+    int winx = SDL_WINDOWPOS_UNDEFINED, winy = SDL_WINDOWPOS_UNDEFINED, winw = scr_w, winh = scr_h, flags = SDL_WINDOW_RESIZABLE;
+    if(fullscreen)
+    {
+        if(fullscreendesktop)
+        {
+            winw = desktopw;
+            winh = desktoph;
+            flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
+        }
+        else flags |= SDL_WINDOW_FULLSCREEN;
+        initwindowpos = true;
+    }
+
+    SDL_GL_ResetAttributes();
+    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+    #if !defined(WIN32) && !defined(__APPLE__)
+    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
+    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
+    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
+    #endif
+    static const int configs[] =
+    {
+        0x3, /* try everything */
+        0x2, 0x1, /* try disabling one at a time */
+        0 /* try disabling everything */
+    };
+    int config = 0;
+    if(!depthbits) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+    if(!fsaa)
+    {
+        SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
+        SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
+    }
+    loopi(sizeof(configs)/sizeof(configs[0]))
+    {
+        config = configs[i];
+        if(!depthbits && config&1) continue;
+        if(fsaa<=0 && config&2) continue;
+        if(depthbits) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, config&1 ? depthbits : 24);
+        if(fsaa>0)
+        {
+            SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, config&2 ? 1 : 0);
+            SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, config&2 ? fsaa : 0);
+        }
+        screen = SDL_CreateWindow("Cube 2: Sauerbraten", winx, winy, winw, winh, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS | flags);
+        if(!screen) continue;
+
+    #ifdef __APPLE__
+        static const int glversions[] = { 32, 20 };
+    #else
+        static const int glversions[] = { 33, 32, 31, 30, 20 };
+    #endif
+        loopj(sizeof(glversions)/sizeof(glversions[0]))
+        {
+            glcompat = glversions[j] <= 30 ? 1 : 0;
+            SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glversions[j] / 10);
+            SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glversions[j] % 10);
+            SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, glversions[j] >= 32 ? SDL_GL_CONTEXT_PROFILE_CORE : 0);
+            glcontext = SDL_GL_CreateContext(screen);
+            if(glcontext) break;
+        }
+        if(glcontext) break;
+    }
+    if(!screen) fatal("failed to create OpenGL window: %s", SDL_GetError());
+    else if(!glcontext) fatal("failed to create OpenGL context: %s", SDL_GetError());
+    else
+    {
+        if(depthbits && (config&1)==0) conoutf(CON_WARN, "%d bit z-buffer not supported - disabling", depthbits);
+        if(fsaa>0 && (config&2)==0) conoutf(CON_WARN, "%dx anti-aliasing not supported - disabling", fsaa);
+    }
+
+    SDL_SetWindowMinimumSize(screen, SCR_MINW, SCR_MINH);
+    SDL_SetWindowMaximumSize(screen, SCR_MAXW, SCR_MAXH);
+
+    SDL_GetWindowSize(screen, &screenw, &screenh);
+}
+
+void resetgl()
+{
+    clearchanges(CHANGE_GFX);
+
+    renderbackground("resetting OpenGL");
+
+    extern void cleanupva();
+    extern void cleanupparticles();
+    extern void cleanupdecals();
+    extern void cleanupblobs();
+    extern void cleanupsky();
+    extern void cleanupmodels();
+    extern void cleanupprefabs();
+    extern void cleanuplightmaps();
+    extern void cleanupblendmap();
+    extern void cleanshadowmap();
+    extern void cleanreflections();
+    extern void cleanupglare();
+    extern void cleanupdepthfx();
+    recorder::cleanup();
+    cleanupva();
+    cleanupparticles();
+    cleanupdecals();
+    cleanupblobs();
+    cleanupsky();
+    cleanupmodels();
+    cleanupprefabs();
+    cleanuptextures();
+    cleanuplightmaps();
+    cleanupblendmap();
+    cleanshadowmap();
+    cleanreflections();
+    cleanupglare();
+    cleanupdepthfx();
+    cleanupshaders();
+    cleanupgl();
+
+    setupscreen();
+    inputgrab(grabinput);
+    gl_init();
+
+    inbetweenframes = false;
+    if(!reloadtexture(*notexture) ||
+       !reloadtexture("data/logo.png") ||
+       !reloadtexture("data/logo_1024.png") ||
+       !reloadtexture("data/background.png") ||
+       !reloadtexture("data/background_detail.png") ||
+       !reloadtexture("data/background_decal.png") ||
+       !reloadtexture("data/mapshot_frame.png") ||
+       !reloadtexture("data/loading_frame.png") ||
+       !reloadtexture("data/loading_bar.png"))
+        fatal("failed to reload core texture");
+    reloadfonts();
+    inbetweenframes = true;
+    renderbackground("initializing...");
+    restoregamma();
+    restorevsync();
+    reloadshaders();
+    reloadtextures();
+    initlights();
+    allchanged(true);
+}
+
+COMMAND(resetgl, "");
+
+static queue<SDL_Event, 32> events;
+
+static inline bool filterevent(const SDL_Event &event)
+{
+    switch(event.type)
+    {
+        case SDL_MOUSEMOTION:
+            if(grabinput && !relativemouse && !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN))
+            {
+                if(event.motion.x == screenw / 2 && event.motion.y == screenh / 2)
+                    return false;  // ignore any motion events generated by SDL_WarpMouse
+                #ifdef __APPLE__
+                if(event.motion.y == 0)
+                    return false;  // let mac users drag windows via the title bar
+                #endif
+            }
+            break;
+    }
+    return true;
+}
+
+template <int SIZE> static inline bool pumpevents(queue<SDL_Event, SIZE> &events)
+{
+    while(events.empty())
+    {
+        SDL_PumpEvents();
+        databuf<SDL_Event> buf = events.reserve(events.capacity());
+        int n = SDL_PeepEvents(buf.getbuf(), buf.remaining(), SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT);
+        if(n <= 0) return false;
+        loopi(n) if(filterevent(buf.buf[i])) buf.put(buf.buf[i]);
+        events.addbuf(buf);
+    }
+    return true;
+}
+
+static int interceptkeysym = 0;
+
+static int interceptevents(void *data, SDL_Event *event)
+{
+    switch(event->type)
+    {
+        case SDL_MOUSEMOTION: return 0;
+        case SDL_KEYDOWN:
+            if(event->key.keysym.sym == interceptkeysym)
+            {
+                interceptkeysym = -interceptkeysym;
+                return 0;
+            }
+            break;
+    }
+    return 1;
+}
+
+static void clearinterceptkey()
+{
+    SDL_DelEventWatch(interceptevents, NULL);
+    interceptkeysym = 0;
+}
+
+bool interceptkey(int sym)
+{
+    if(!interceptkeysym)
+    {
+        interceptkeysym = sym;
+        SDL_FilterEvents(interceptevents, NULL);
+        if(interceptkeysym < 0)
+        {
+            interceptkeysym = 0;
+            return true;
+        }
+        SDL_AddEventWatch(interceptevents, NULL);
+    }
+    else if(abs(interceptkeysym) != sym) interceptkeysym = sym;
+    SDL_PumpEvents();
+    if(interceptkeysym < 0)
+    {
+        clearinterceptkey();
+        interceptkeysym = sym;
+        SDL_FilterEvents(interceptevents, NULL);
+        interceptkeysym = 0;
+        return true;
+    }
+    return false;
+}
+
+static void ignoremousemotion()
+{
+    SDL_PumpEvents();
+    SDL_FlushEvent(SDL_MOUSEMOTION);
+}
+
+static void resetmousemotion()
+{
+    if(grabinput && !relativemouse && !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN))
+    {
+        SDL_WarpMouseInWindow(screen, screenw / 2, screenh / 2);
+    }
+}
+
+static void checkmousemotion(int &dx, int &dy)
+{
+    while(pumpevents(events))
+    {
+        SDL_Event &event = events.removing();
+        if(event.type != SDL_MOUSEMOTION) return;
+        dx += event.motion.xrel;
+        dy += event.motion.yrel;
+        events.remove();
+    }
+}
+
+void checkinput()
+{
+    if(interceptkeysym) clearinterceptkey();
+    //int lasttype = 0, lastbut = 0;
+    bool mousemoved = false;
+    int focused = 0;
+    while(pumpevents(events))
+    {
+        SDL_Event &event = events.remove();
+
+        if(focused && event.type!=SDL_WINDOWEVENT) { if(grabinput != (focused>0)) inputgrab(grabinput = focused>0, shouldgrab); focused = 0; }
+
+        switch(event.type)
+        {
+            case SDL_QUIT:
+                quit();
+                return;
+
+            case SDL_TEXTINPUT:
+                if(textinputmask && int(event.text.timestamp-textinputtime) >= textinputfilter)
+                {
+                    uchar buf[SDL_TEXTINPUTEVENT_TEXT_SIZE+1];
+                    size_t len = decodeutf8(buf, sizeof(buf)-1, (const uchar *)event.text.text, strlen(event.text.text));
+                    if(len > 0) { buf[len] = '\0'; processtextinput((const char *)buf, len); }
+                }
+                break;
+
+            case SDL_KEYDOWN:
+            case SDL_KEYUP:
+                if(keyrepeatmask || !event.key.repeat)
+                    processkey(event.key.keysym.sym, event.key.state==SDL_PRESSED, event.key.keysym.mod | SDL_GetModState());
+                break;
+
+            case SDL_WINDOWEVENT:
+                switch(event.window.event)
+                {
+                    case SDL_WINDOWEVENT_CLOSE:
+                        quit();
+                        break;
+
+                    case SDL_WINDOWEVENT_FOCUS_GAINED:
+                        shouldgrab = true;
+                        break;
+                    case SDL_WINDOWEVENT_ENTER:
+                        shouldgrab = false;
+                        focused = 1;
+                        break;
+
+                    case SDL_WINDOWEVENT_LEAVE:
+                    case SDL_WINDOWEVENT_FOCUS_LOST:
+                        shouldgrab = false;
+                        focused = -1;
+                        break;
+
+                    case SDL_WINDOWEVENT_MINIMIZED:
+                        minimized = true;
+                        break;
+
+                    case SDL_WINDOWEVENT_MAXIMIZED:
+                    case SDL_WINDOWEVENT_RESTORED:
+                        minimized = false;
+                        break;
+
+                    case SDL_WINDOWEVENT_RESIZED:
+                        break;
+
+                    case SDL_WINDOWEVENT_SIZE_CHANGED:
+                    {
+                        SDL_GetWindowSize(screen, &screenw, &screenh);
+                        if(!fullscreendesktop || !(SDL_GetWindowFlags(screen) & SDL_WINDOW_FULLSCREEN))
+                        {
+                            scr_w = clamp(screenw, SCR_MINW, SCR_MAXW);
+                            scr_h = clamp(screenh, SCR_MINH, SCR_MAXH);
+                        }
+                        gl_resize();
+                        break;
+                    }
+                }
+                break;
+
+            case SDL_MOUSEMOTION:
+                if(grabinput)
+                {
+                    int dx = event.motion.xrel, dy = event.motion.yrel;
+                    checkmousemotion(dx, dy);
+                    if(!g3d_movecursor(dx, dy)) mousemove(dx, dy);
+                    mousemoved = true;
+                }
+                else if(shouldgrab) inputgrab(grabinput = true);
+                break;
+
+            case SDL_MOUSEBUTTONDOWN:
+            case SDL_MOUSEBUTTONUP:
+                //if(lasttype==event.type && lastbut==event.button.button) break; // why?? get event twice without it
+                switch(event.button.button)
+                {
+                    case SDL_BUTTON_LEFT: processkey(-1, event.button.state==SDL_PRESSED); break;
+                    case SDL_BUTTON_MIDDLE: processkey(-2, event.button.state==SDL_PRESSED); break;
+                    case SDL_BUTTON_RIGHT: processkey(-3, event.button.state==SDL_PRESSED); break;
+                    case SDL_BUTTON_X1: processkey(-6, event.button.state==SDL_PRESSED); break;
+                    case SDL_BUTTON_X2: processkey(-7, event.button.state==SDL_PRESSED); break;
+                }
+                //lasttype = event.type;
+                //lastbut = event.button.button;
+                break;
+
+            case SDL_MOUSEWHEEL:
+                if(event.wheel.y > 0) { processkey(-4, true); processkey(-4, false); }
+                else if(event.wheel.y < 0) { processkey(-5, true); processkey(-5, false); }
+                break;
+        }
+    }
+    if(focused) { if(grabinput != (focused>0)) inputgrab(grabinput = focused>0, shouldgrab); focused = 0; }
+    if(mousemoved) resetmousemotion();
+}
+
+void swapbuffers(bool overlay)
+{
+    recorder::capture(overlay);
+    gle::disable();
+    SDL_GL_SwapWindow(screen);
+}
+
+VAR(menufps, 0, 60, 1000);
+VARP(maxfps, 0, 200, 1000);
+
+void limitfps(int &millis, int curmillis)
+{
+    int limit = (mainmenu || minimized) && menufps ? (maxfps ? min(maxfps, menufps) : menufps) : maxfps;
+    if(!limit) return;
+    static int fpserror = 0;
+    int delay = 1000/limit - (millis-curmillis);
+    if(delay < 0) fpserror = 0;
+    else
+    {
+        fpserror += 1000%limit;
+        if(fpserror >= limit)
+        {
+            ++delay;
+            fpserror -= limit;
+        }
+        if(delay > 0)
+        {
+            SDL_Delay(delay);
+            millis += delay;
+        }
+    }
+}
+
+#if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__)
+void stackdumper(unsigned int type, EXCEPTION_POINTERS *ep)
+{
+    if(!ep) fatal("unknown type");
+    EXCEPTION_RECORD *er = ep->ExceptionRecord;
+    CONTEXT *context = ep->ContextRecord;
+    char out[512];
+    formatstring(out, "Cube 2: Sauerbraten Win32 Exception: 0x%x [0x%x]\n\n", er->ExceptionCode, er->ExceptionCode==EXCEPTION_ACCESS_VIOLATION ? er->ExceptionInformation[1] : -1);
+    SymInitialize(GetCurrentProcess(), NULL, TRUE);
+#ifdef _AMD64_
+       STACKFRAME64 sf = {{context->Rip, 0, AddrModeFlat}, {}, {context->Rbp, 0, AddrModeFlat}, {context->Rsp, 0, AddrModeFlat}, 0};
+    while(::StackWalk64(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL))
+       {
+               union { IMAGEHLP_SYMBOL64 sym; char symext[sizeof(IMAGEHLP_SYMBOL64) + sizeof(string)]; };
+               sym.SizeOfStruct = sizeof(sym);
+               sym.MaxNameLength = sizeof(symext) - sizeof(sym);
+               IMAGEHLP_LINE64 line;
+               line.SizeOfStruct = sizeof(line);
+        DWORD64 symoff;
+               DWORD lineoff;
+        if(SymGetSymFromAddr64(GetCurrentProcess(), sf.AddrPC.Offset, &symoff, &sym) && SymGetLineFromAddr64(GetCurrentProcess(), sf.AddrPC.Offset, &lineoff, &line))
+#else
+    STACKFRAME sf = {{context->Eip, 0, AddrModeFlat}, {}, {context->Ebp, 0, AddrModeFlat}, {context->Esp, 0, AddrModeFlat}, 0};
+    while(::StackWalk(IMAGE_FILE_MACHINE_I386, GetCurrentProcess(), GetCurrentThread(), &sf, context, NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL))
+       {
+               union { IMAGEHLP_SYMBOL sym; char symext[sizeof(IMAGEHLP_SYMBOL) + sizeof(string)]; };
+               sym.SizeOfStruct = sizeof(sym);
+               sym.MaxNameLength = sizeof(symext) - sizeof(sym);
+               IMAGEHLP_LINE line;
+               line.SizeOfStruct = sizeof(line);
+        DWORD symoff, lineoff;
+        if(SymGetSymFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &symoff, &sym) && SymGetLineFromAddr(GetCurrentProcess(), sf.AddrPC.Offset, &lineoff, &line))
+#endif
+        {
+            char *del = strrchr(line.FileName, '\\');
+            concformatstring(out, "%s - %s [%d]\n", sym.Name, del ? del + 1 : line.FileName, line.LineNumber);
+        }
+    }
+    fatal(out);
+}
+#endif
+
+#define MAXFPSHISTORY 60
+
+int fpspos = 0, fpshistory[MAXFPSHISTORY];
+
+void resetfpshistory()
+{
+    loopi(MAXFPSHISTORY) fpshistory[i] = 1;
+    fpspos = 0;
+}
+
+void updatefpshistory(int millis)
+{
+    fpshistory[fpspos++] = max(1, min(1000, millis));
+    if(fpspos>=MAXFPSHISTORY) fpspos = 0;
+}
+
+void getfps(int &fps, int &bestdiff, int &worstdiff)
+{
+    int total = fpshistory[MAXFPSHISTORY-1], best = total, worst = total;
+    loopi(MAXFPSHISTORY-1)
+    {
+        int millis = fpshistory[i];
+        total += millis;
+        if(millis < best) best = millis;
+        if(millis > worst) worst = millis;
+    }
+
+    fps = (1000*MAXFPSHISTORY)/total;
+    bestdiff = 1000/best-fps;
+    worstdiff = fps-1000/worst;
+}
+
+void getfps_(int *raw)
+{
+    int fps, bestdiff, worstdiff;
+    if(*raw) fps = 1000/fpshistory[(fpspos+MAXFPSHISTORY-1)%MAXFPSHISTORY];
+    else getfps(fps, bestdiff, worstdiff);
+    intret(fps);
+}
+
+COMMANDN(getfps, getfps_, "i");
+
+bool inbetweenframes = false, renderedframe = true;
+
+static bool findarg(int argc, char **argv, const char *str)
+{
+    for(int i = 1; i<argc; i++) if(strstr(argv[i], str)==argv[i]) return true;
+    return false;
+}
+
+static int clockrealbase = 0, clockvirtbase = 0;
+static void clockreset() { clockrealbase = SDL_GetTicks(); clockvirtbase = totalmillis; }
+VARFP(clockerror, 990000, 1000000, 1010000, clockreset());
+VARFP(clockfix, 0, 0, 1, clockreset());
+
+int getclockmillis()
+{
+    int millis = SDL_GetTicks() - clockrealbase;
+    if(clockfix) millis = int(millis*(double(clockerror)/1000000));
+    millis += clockvirtbase;
+    return max(millis, totalmillis);
+}
+
+VAR(numcpus, 1, 1, 16);
+
+int main(int argc, char **argv)
+{
+    #ifdef WIN32
+    //atexit((void (__cdecl *)(void))_CrtDumpMemoryLeaks);
+    #ifndef _DEBUG
+    #ifndef __GNUC__
+    __try {
+    #endif
+    #endif
+    #endif
+
+    setlogfile(NULL);
+
+    int dedicated = 0;
+    char *load = NULL, *initscript = NULL;
+
+    initing = INIT_RESET;
+    // set home dir first
+    for(int i = 1; i<argc; i++) if(argv[i][0]=='-' && argv[i][1] == 'q') { sethomedir(&argv[i][2]); break; }
+    // set log after home dir, but before anything else
+    for(int i = 1; i<argc; i++) if(argv[i][0]=='-' && argv[i][1] == 'g')
+    {
+        const char *file = argv[i][2] ? &argv[i][2] : "log.txt";
+        setlogfile(file);
+        logoutf("Setting log file: %s", file);
+        break;
+    }
+    execfile("init.cfg", false);
+    for(int i = 1; i<argc; i++)
+    {
+        if(argv[i][0]=='-') switch(argv[i][1])
+        {
+            case 'q': if(homedir[0]) logoutf("Using home directory: %s", homedir); break;
+            case 'r': /* compat, ignore */ break;
+            case 'k':
+            {
+                const char *dir = addpackagedir(&argv[i][2]);
+                if(dir) logoutf("Adding package directory: %s", dir);
+                break;
+            }
+            case 'g': break;
+            case 'd': dedicated = atoi(&argv[i][2]); if(dedicated<=0) dedicated = 2; break;
+            case 'w': scr_w = clamp(atoi(&argv[i][2]), SCR_MINW, SCR_MAXW); if(!findarg(argc, argv, "-h")) scr_h = -1; break;
+            case 'h': scr_h = clamp(atoi(&argv[i][2]), SCR_MINH, SCR_MAXH); if(!findarg(argc, argv, "-w")) scr_w = -1; break;
+            case 'z': depthbits = atoi(&argv[i][2]); break;
+            case 'b': /* compat, ignore */ break;
+            case 'a': fsaa = atoi(&argv[i][2]); break;
+            case 'v': /* compat, ignore */ break;
+            case 't': fullscreen = atoi(&argv[i][2]); break;
+            case 's': /* compat, ignore */ break;
+            case 'f': /* compat, ignore */ break;
+            case 'l':
+            {
+                char pkgdir[] = "packages/";
+                load = strstr(path(&argv[i][2]), path(pkgdir));
+                if(load) load += sizeof(pkgdir)-1;
+                else load = &argv[i][2];
+                break;
+            }
+            case 'x': initscript = &argv[i][2]; break;
+            default: if(!serveroption(argv[i])) gameargs.add(argv[i]); break;
+        }
+        else gameargs.add(argv[i]);
+    }
+    initing = NOT_INITING;
+
+    numcpus = clamp(SDL_GetCPUCount(), 1, 16);
+
+    if(dedicated <= 1)
+    {
+        logoutf("init: sdl");
+
+        if(SDL_Init(SDL_INIT_TIMER|SDL_INIT_VIDEO|SDL_INIT_AUDIO)<0) fatal("Unable to initialize SDL: %s", SDL_GetError());
+
+#ifdef SDL_VIDEO_DRIVER_X11
+        SDL_version version;
+        SDL_GetVersion(&version);
+        if (SDL_VERSIONNUM(version.major, version.minor, version.patch) <= SDL_VERSIONNUM(2, 0, 12))
+            sdl_xgrab_bug = 1;
+#endif
+    }
+
+    logoutf("init: net");
+    if(enet_initialize()<0) fatal("Unable to initialise network module");
+    atexit(enet_deinitialize);
+    enet_time_set(0);
+
+    logoutf("init: game");
+    game::parseoptions(gameargs);
+    initserver(dedicated>0, dedicated>1);  // never returns if dedicated
+    ASSERT(dedicated <= 1);
+    game::initclient();
+
+    logoutf("init: video");
+    SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "0");
+    #if !defined(WIN32) && !defined(__APPLE__)
+    SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
+    #endif
+    setupscreen();
+    SDL_ShowCursor(SDL_FALSE);
+    SDL_StopTextInput(); // workaround for spurious text-input events getting sent on first text input toggle?
+
+    logoutf("init: gl");
+    gl_checkextensions();
+    gl_init();
+    notexture = textureload("packages/textures/notexture.png");
+    if(!notexture) fatal("could not find core textures");
+
+    logoutf("init: console");
+    if(!execfile("data/stdlib.cfg", false)) fatal("cannot find data files (you are running from the wrong folder, try .bat file in the main folder)");   // this is the first file we load.
+    if(!execfile("data/font.cfg", false)) fatal("cannot find font definitions");
+    if(!setfont("default")) fatal("no default font specified");
+
+    inbetweenframes = true;
+    renderbackground("initializing...");
+
+    logoutf("init: world");
+    camera1 = player = game::iterdynents(0);
+    emptymap(0, true, NULL, false);
+
+    logoutf("init: sound");
+    initsound();
+
+    logoutf("init: cfg");
+    initing = INIT_LOAD;
+    execfile("data/keymap.cfg");
+    execfile("data/stdedit.cfg");
+    execfile("data/sounds.cfg");
+    execfile("data/menus.cfg");
+    execfile("data/heightmap.cfg");
+    execfile("data/blendbrush.cfg");
+    defformatstring(gamecfgname, "data/game_%s.cfg", game::gameident());
+    execfile(gamecfgname);
+    if(game::savedservers()) execfile(game::savedservers(), false);
+
+    identflags |= IDF_PERSIST;
+
+    if(!execfile(game::savedconfig(), false))
+    {
+        execfile(game::defaultconfig());
+        writecfg(game::restoreconfig());
+    }
+    execfile(game::autoexec(), false);
+
+    identflags &= ~IDF_PERSIST;
+
+    initing = INIT_GAME;
+    game::loadconfigs();
+
+    initing = NOT_INITING;
+
+    logoutf("init: render");
+    restoregamma();
+    restorevsync();
+    loadshaders();
+    initparticles();
+    initdecals();
+
+    identflags |= IDF_PERSIST;
+
+    logoutf("init: mainloop");
+
+    if(execfile("once.cfg", false)) remove(findfile("once.cfg", "rb"));
+
+    if(load)
+    {
+        logoutf("init: localconnect");
+        //localconnect();
+        game::changemap(load);
+    }
+
+    if(initscript) execute(initscript);
+
+    initmumble();
+    resetfpshistory();
+
+    inputgrab(grabinput = true);
+    ignoremousemotion();
+
+    for(;;)
+    {
+        static int frames = 0;
+        int millis = getclockmillis();
+        limitfps(millis, totalmillis);
+        elapsedtime = millis - totalmillis;
+        static int timeerr = 0;
+        int scaledtime = game::scaletime(elapsedtime) + timeerr;
+        curtime = scaledtime/100;
+        timeerr = scaledtime%100;
+        if(!multiplayer(false) && curtime>200) curtime = 200;
+        if(game::ispaused()) curtime = 0;
+               lastmillis += curtime;
+        totalmillis = millis;
+        updatetime();
+
+        checkinput();
+        menuprocess();
+        tryedit();
+
+        if(lastmillis) game::updateworld();
+
+        checksleep(lastmillis);
+
+        serverslice(false, 0);
+
+        if(frames) updatefpshistory(elapsedtime);
+        frames++;
+
+        // miscellaneous general game effects
+        recomputecamera();
+        updateparticles();
+        updatesounds();
+
+        if(minimized) continue;
+
+        inbetweenframes = false;
+        if(mainmenu) gl_drawmainmenu();
+        else gl_drawframe();
+        swapbuffers();
+        renderedframe = inbetweenframes = true;
+    }
+
+    ASSERT(0);
+    return EXIT_FAILURE;
+
+    #if defined(WIN32) && !defined(_DEBUG) && !defined(__GNUC__)
+    } __except(stackdumper(0, GetExceptionInformation()), EXCEPTION_CONTINUE_SEARCH) { return 0; }
+    #endif
+}
diff --git a/src/engine/master.cpp b/src/engine/master.cpp
new file mode 100644 (file)
index 0000000..2522e97
--- /dev/null
@@ -0,0 +1,718 @@
+#ifdef WIN32
+#define FD_SETSIZE 4096
+#else
+#include <sys/types.h>
+#undef __FD_SETSIZE
+#define __FD_SETSIZE 4096
+#endif
+
+#include "cube.h"
+#include <signal.h>
+#include <enet/time.h>
+
+#define INPUT_LIMIT 4096
+#define OUTPUT_LIMIT (64*1024)
+#define CLIENT_TIME (3*60*1000)
+#define AUTH_TIME (30*1000)
+#define AUTH_LIMIT 100
+#define AUTH_THROTTLE 1000
+#define CLIENT_LIMIT 4096
+#define DUP_LIMIT 16
+#define PING_TIME 3000
+#define PING_RETRY 5
+#define KEEPALIVE_TIME (65*60*1000)
+#define SERVER_LIMIT 4096
+#define SERVER_DUP_LIMIT 10
+
+FILE *logfile = NULL;
+
+struct userinfo
+{
+    char *name;
+    void *pubkey;
+};
+hashnameset<userinfo> users;
+
+void adduser(char *name, char *pubkey)
+{
+    name = newstring(name);
+    userinfo &u = users[name];
+    u.name = name;
+    u.pubkey = parsepubkey(pubkey);
+}
+COMMAND(adduser, "ss");
+
+void clearusers()
+{
+    enumerate(users, userinfo, u, { delete[] u.name; freepubkey(u.pubkey); });
+    users.clear();
+}
+COMMAND(clearusers, "");
+
+vector<ipmask> bans, servbans, gbans;
+
+void clearbans()
+{
+    bans.shrink(0);
+    servbans.shrink(0);
+    gbans.shrink(0);
+}
+COMMAND(clearbans, "");
+
+void addban(vector<ipmask> &bans, const char *name)
+{
+    ipmask ban;
+    ban.parse(name);
+    bans.add(ban);
+}
+ICOMMAND(ban, "s", (char *name), addban(bans, name));
+ICOMMAND(servban, "s", (char *name), addban(servbans, name));
+ICOMMAND(gban, "s", (char *name), addban(gbans, name));
+
+bool checkban(vector<ipmask> &bans, enet_uint32 host)
+{
+    loopv(bans) if(bans[i].check(host)) return true;
+    return false;
+}
+
+struct authreq
+{
+    enet_uint32 reqtime;
+    uint id;
+    void *answer;
+};
+
+struct gameserver
+{
+    ENetAddress address;
+    string ip;
+    int port, numpings;
+    enet_uint32 lastping, lastpong;
+};
+vector<gameserver *> gameservers;
+
+struct messagebuf
+{
+    vector<messagebuf *> &owner;
+    vector<char> buf;
+    int refs;
+
+    messagebuf(vector<messagebuf *> &owner) : owner(owner), refs(0) {}
+
+    const char *getbuf() { return buf.getbuf(); }
+    int length() { return buf.length(); }
+    void purge();
+
+    bool equals(const messagebuf &m) const
+    {
+        return buf.length() == m.buf.length() && !memcmp(buf.getbuf(), m.buf.getbuf(), buf.length());
+    }
+
+    bool endswith(const messagebuf &m) const
+    {
+        return buf.length() >= m.buf.length() && !memcmp(&buf[buf.length() - m.buf.length()], m.buf.getbuf(), m.buf.length());
+    }
+
+    void concat(const messagebuf &m)
+    {
+        if(buf.length() && buf.last() == '\0') buf.pop();
+        buf.put(m.buf.getbuf(), m.buf.length());
+    }
+};
+vector<messagebuf *> gameserverlists, gbanlists;
+bool updateserverlist = true;
+
+struct client
+{
+    ENetAddress address;
+    ENetSocket socket;
+    char input[INPUT_LIMIT];
+    messagebuf *message;
+    vector<char> output;
+    int inputpos, outputpos;
+    enet_uint32 connecttime, lastinput;
+    int servport;
+    enet_uint32 lastauth;
+    vector<authreq> authreqs;
+    bool shouldpurge;
+    bool registeredserver;
+
+    client() : message(NULL), inputpos(0), outputpos(0), servport(-1), lastauth(0), shouldpurge(false), registeredserver(false) {}
+};
+vector<client *> clients;
+
+ENetSocket serversocket = ENET_SOCKET_NULL;
+
+time_t starttime;
+enet_uint32 servtime = 0;
+
+void fatal(const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    vfprintf(logfile, fmt, args);
+    fputc('\n', logfile);
+    va_end(args);
+    exit(EXIT_FAILURE);
+}
+
+void conoutfv(int type, const char *fmt, va_list args)
+{
+    vfprintf(logfile, fmt, args);
+    fputc('\n', logfile);
+}
+
+void purgeclient(int n)
+{
+    client &c = *clients[n];
+    if(c.message) c.message->purge();
+    enet_socket_destroy(c.socket);
+    delete clients[n];
+    clients.remove(n);
+}
+
+void output(client &c, const char *msg, int len = 0)
+{
+    if(!len) len = strlen(msg);
+    c.output.put(msg, len);
+}
+
+void outputf(client &c, const char *fmt, ...)
+{
+    string msg;
+    va_list args;
+    va_start(args, fmt);
+    vformatstring(msg, fmt, args);
+    va_end(args);
+
+    output(c, msg);
+}
+
+ENetSocket pingsocket = ENET_SOCKET_NULL;
+
+bool setuppingsocket(ENetAddress *address)
+{
+    if(pingsocket != ENET_SOCKET_NULL) return true;
+    pingsocket = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+    if(pingsocket == ENET_SOCKET_NULL) return false;
+    if(address && enet_socket_bind(pingsocket, address) < 0) return false;
+    enet_socket_set_option(pingsocket, ENET_SOCKOPT_NONBLOCK, 1);
+    return true;
+}
+
+void setupserver(int port, const char *ip = NULL)
+{
+    ENetAddress address;
+    address.host = ENET_HOST_ANY;
+    address.port = port;
+
+    if(ip)
+    {
+        if(enet_address_set_host(&address, ip)<0)
+            fatal("failed to resolve server address: %s", ip);
+    }
+    serversocket = enet_socket_create(ENET_SOCKET_TYPE_STREAM);
+    if(serversocket==ENET_SOCKET_NULL ||
+       enet_socket_set_option(serversocket, ENET_SOCKOPT_REUSEADDR, 1) < 0 ||
+       enet_socket_bind(serversocket, &address) < 0 ||
+       enet_socket_listen(serversocket, -1) < 0)
+        fatal("failed to create server socket");
+    if(enet_socket_set_option(serversocket, ENET_SOCKOPT_NONBLOCK, 1)<0)
+        fatal("failed to make server socket non-blocking");
+    if(!setuppingsocket(&address))
+        fatal("failed to create ping socket");
+
+    enet_time_set(0);
+
+    starttime = time(NULL);
+    char *ct = ctime(&starttime);
+    if(strchr(ct, '\n')) *strchr(ct, '\n') = '\0';
+    conoutf("*** Starting master server on %s %d at %s ***", ip ? ip : "localhost", port, ct);
+}
+
+void genserverlist()
+{
+    if(!updateserverlist) return;
+    while(gameserverlists.length() && gameserverlists.last()->refs<=0)
+        delete gameserverlists.pop();
+    messagebuf *l = new messagebuf(gameserverlists);
+    loopv(gameservers)
+    {
+        gameserver &s = *gameservers[i];
+        if(!s.lastpong) continue;
+        defformatstring(cmd, "addserver %s %d\n", s.ip, s.port);
+        l->buf.put(cmd, strlen(cmd));
+    }
+    l->buf.add('\0');
+    gameserverlists.add(l);
+    updateserverlist = false;
+}
+
+void gengbanlist()
+{
+    messagebuf *l = new messagebuf(gbanlists);
+    const char *header = "cleargbans\n";
+    l->buf.put(header, strlen(header));
+    string cmd = "addgban ";
+    int cmdlen = strlen(cmd);
+    loopv(gbans)
+    {
+        ipmask &b = gbans[i];
+        l->buf.put(cmd, cmdlen + b.print(&cmd[cmdlen])); 
+        l->buf.add('\n');
+    }
+    if(gbanlists.length() && gbanlists.last()->equals(*l))
+    {
+        delete l;
+        return;
+    }
+    while(gbanlists.length() && gbanlists.last()->refs<=0)
+        delete gbanlists.pop();
+    loopv(gbanlists)
+    {
+        messagebuf *m = gbanlists[i];
+        if(m->refs > 0 && !m->endswith(*l)) m->concat(*l);
+    }
+    gbanlists.add(l);
+    loopv(clients)
+    {
+        client &c = *clients[i];
+        if(c.servport >= 0 && !c.message) 
+        {
+            c.message = l;
+            c.message->refs++;
+        }
+    }
+}
+
+void addgameserver(client &c)
+{
+    if(gameservers.length() >= SERVER_LIMIT) return;
+    int dups = 0;
+    loopv(gameservers)
+    {
+        gameserver &s = *gameservers[i];
+        if(s.address.host != c.address.host) continue;
+        ++dups;
+        if(s.port == c.servport)
+        {
+            s.lastping = 0;
+            s.numpings = 0;
+            return;
+        }
+    }
+    if(dups >= SERVER_DUP_LIMIT)
+    {
+        outputf(c, "failreg too many servers on ip\n");
+        return;
+    }
+    string hostname;
+    if(enet_address_get_host_ip(&c.address, hostname, sizeof(hostname)) < 0)
+    {
+        outputf(c, "failreg failed resolving ip\n");
+        return;
+    }
+    gameserver &s = *gameservers.add(new gameserver);
+    s.address.host = c.address.host;
+    s.address.port = c.servport+1;
+    copystring(s.ip, hostname);
+    s.port = c.servport;
+    s.numpings = 0;
+    s.lastping = s.lastpong = 0;
+}
+
+client *findclient(gameserver &s)
+{
+    loopv(clients)
+    {
+        client &c = *clients[i];
+        if(s.address.host == c.address.host && s.port == c.servport)
+            return &c;
+    }
+    return NULL;
+}
+
+void servermessage(gameserver &s, const char *msg)
+{
+    client *c = findclient(s);
+    if(c) outputf(*c, msg);
+}
+
+void checkserverpongs()
+{
+    ENetBuffer buf;
+    ENetAddress addr;
+    static uchar pong[MAXTRANS];
+    for(;;)
+    {
+        buf.data = pong;
+        buf.dataLength = sizeof(pong);
+        int len = enet_socket_receive(pingsocket, &addr, &buf, 1);
+        if(len <= 0) break;
+        loopv(gameservers)
+        {
+            gameserver &s = *gameservers[i];
+            if(s.address.host == addr.host && s.address.port == addr.port)
+            {
+                if(s.lastping && (!s.lastpong || ENET_TIME_GREATER(s.lastping, s.lastpong)))
+                {
+                    client *c = findclient(s);
+                    if(c)
+                    {
+                        c->registeredserver = true;
+                        outputf(*c, "succreg\n");
+                        if(!c->message && gbanlists.length())
+                        {
+                            c->message = gbanlists.last();
+                            c->message->refs++;
+                        }
+                    }
+                }
+                if(!s.lastpong) updateserverlist = true;
+                s.lastpong = servtime ? servtime : 1;
+                break;
+            }
+        }
+    }
+}
+
+void bangameservers()
+{
+    loopvrev(gameservers) if(checkban(servbans, gameservers[i]->address.host))
+    {
+        delete gameservers.remove(i);
+        updateserverlist = true;
+    }
+}
+
+void checkgameservers()
+{
+    ENetBuffer buf;
+    loopv(gameservers)
+    {
+        gameserver &s = *gameservers[i];
+        if(s.lastping && s.lastpong && ENET_TIME_LESS_EQUAL(s.lastping, s.lastpong))
+        {
+            if(ENET_TIME_DIFFERENCE(servtime, s.lastpong) > KEEPALIVE_TIME)
+            {
+                delete gameservers.remove(i--);
+                updateserverlist = true;
+            }
+        }
+        else if(!s.lastping || ENET_TIME_DIFFERENCE(servtime, s.lastping) > PING_TIME)
+        {
+            if(s.numpings >= PING_RETRY)
+            {
+                servermessage(s, "failreg failed pinging server\n");
+                delete gameservers.remove(i--);
+                updateserverlist = true;
+            }
+            else
+            {
+                static const uchar ping[] = { 1 };
+                buf.data = (void *)ping;
+                buf.dataLength = sizeof(ping);
+                s.numpings++;
+                s.lastping = servtime ? servtime : 1;
+                enet_socket_send(pingsocket, &s.address, &buf, 1);
+            }
+        }
+    }
+}
+
+void messagebuf::purge()
+{
+    refs = max(refs - 1, 0);
+    if(refs<=0 && owner.last()!=this)
+    {
+        owner.removeobj(this);
+        delete this;
+    }
+}
+
+void purgeauths(client &c)
+{
+    int expired = 0;
+    loopv(c.authreqs)
+    {
+        if(ENET_TIME_DIFFERENCE(servtime, c.authreqs[i].reqtime) >= AUTH_TIME)
+        {
+            outputf(c, "failauth %u\n", c.authreqs[i].id);
+            freechallenge(c.authreqs[i].answer);
+            expired = i + 1;
+        }
+        else break;
+    }
+    if(expired > 0) c.authreqs.remove(0, expired);
+}
+
+void reqauth(client &c, uint id, char *name)
+{
+    if(ENET_TIME_DIFFERENCE(servtime, c.lastauth) < AUTH_THROTTLE)
+        return;
+
+    c.lastauth = servtime;
+
+    purgeauths(c);
+
+    time_t t = time(NULL);
+    char *ct = ctime(&t);
+    if(ct)
+    {
+        char *newline = strchr(ct, '\n');
+        if(newline) *newline = '\0';
+    }
+    string ip;
+    if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
+    conoutf("%s: attempting \"%s\" as %u from %s", ct ? ct : "-", name, id, ip);
+
+    userinfo *u = users.access(name);
+    if(!u)
+    {
+        outputf(c, "failauth %u\n", id);
+        return;
+    }
+
+    if(c.authreqs.length() >= AUTH_LIMIT)
+    {
+        outputf(c, "failauth %u\n", c.authreqs[0].id);
+        freechallenge(c.authreqs[0].answer);
+        c.authreqs.remove(0);
+    }
+
+    authreq &a = c.authreqs.add();
+    a.reqtime = servtime;
+    a.id = id;
+    uint seed[3] = { uint(starttime), servtime, randomMT() };
+    static vector<char> buf;
+    buf.setsize(0);
+    a.answer = genchallenge(u->pubkey, seed, sizeof(seed), buf);
+
+    outputf(c, "chalauth %u %s\n", id, buf.getbuf());
+}
+
+void confauth(client &c, uint id, const char *val)
+{
+    purgeauths(c);
+
+    loopv(c.authreqs) if(c.authreqs[i].id == id)
+    {
+        string ip;
+        if(enet_address_get_host_ip(&c.address, ip, sizeof(ip)) < 0) copystring(ip, "-");
+        if(checkchallenge(val, c.authreqs[i].answer))
+        {
+            outputf(c, "succauth %u\n", id);
+            conoutf("succeeded %u from %s", id, ip);
+        }
+        else
+        {
+            outputf(c, "failauth %u\n", id);
+            conoutf("failed %u from %s", id, ip);
+        }
+        freechallenge(c.authreqs[i].answer);
+        c.authreqs.remove(i--);
+        return;
+    }
+    outputf(c, "failauth %u\n", id);
+}
+
+bool checkclientinput(client &c)
+{
+    if(c.inputpos<0) return true;
+    char *end = (char *)memchr(c.input, '\n', c.inputpos);
+    while(end)
+    {
+        *end++ = '\0';
+        c.lastinput = servtime;
+
+        int port;
+        uint id;
+        string user, val;
+        if(!strncmp(c.input, "list", 4) && (!c.input[4] || c.input[4] == '\n' || c.input[4] == '\r'))
+        {
+            genserverlist();
+            if(gameserverlists.empty() || c.message) return false;
+            c.message = gameserverlists.last();
+            c.message->refs++;
+            c.output.setsize(0);
+            c.outputpos = 0;
+            c.shouldpurge = true;
+            return true;
+        }
+        else if(sscanf(c.input, "regserv %d", &port) == 1)
+        {
+            if(checkban(servbans, c.address.host)) return false;
+            if(port < 0 || port > 0xFFFF-1 || (c.servport >= 0 && port != c.servport)) outputf(c, "failreg invalid port\n");
+            else
+            {
+                c.servport = port;
+                addgameserver(c);
+            }
+        }
+        else if(sscanf(c.input, "reqauth %u %100s", &id, user) == 2)
+        {
+            reqauth(c, id, user);
+        }
+        else if(sscanf(c.input, "confauth %u %100s", &id, val) == 2)
+        {
+            confauth(c, id, val);
+        }
+        c.inputpos = &c.input[c.inputpos] - end;
+        memmove(c.input, end, c.inputpos);
+
+        end = (char *)memchr(c.input, '\n', c.inputpos);
+    }
+    return c.inputpos<(int)sizeof(c.input);
+}
+
+ENetSocketSet readset, writeset;
+
+void checkclients()
+{
+    ENetSocketSet readset, writeset;
+    ENetSocket maxsock = max(serversocket, pingsocket);
+    ENET_SOCKETSET_EMPTY(readset);
+    ENET_SOCKETSET_EMPTY(writeset);
+    ENET_SOCKETSET_ADD(readset, serversocket);
+    ENET_SOCKETSET_ADD(readset, pingsocket);
+    loopv(clients)
+    {
+        client &c = *clients[i];
+        if(c.authreqs.length()) purgeauths(c);
+        if(c.message || c.output.length()) ENET_SOCKETSET_ADD(writeset, c.socket);
+        else ENET_SOCKETSET_ADD(readset, c.socket);
+        maxsock = max(maxsock, c.socket);
+    }
+    if(enet_socketset_select(maxsock, &readset, &writeset, 1000)<=0) return;
+
+    if(ENET_SOCKETSET_CHECK(readset, pingsocket)) checkserverpongs();
+    if(ENET_SOCKETSET_CHECK(readset, serversocket))
+    {
+        ENetAddress address;
+        ENetSocket clientsocket = enet_socket_accept(serversocket, &address);
+        if(clients.length()>=CLIENT_LIMIT || checkban(bans, address.host)) enet_socket_destroy(clientsocket);
+        else if(clientsocket!=ENET_SOCKET_NULL)
+        {
+            int dups = 0, oldest = -1;
+            loopv(clients) if(clients[i]->address.host == address.host)
+            {
+                dups++;
+                if(oldest<0 || clients[i]->connecttime < clients[oldest]->connecttime) oldest = i;
+            }
+            if(dups >= DUP_LIMIT) purgeclient(oldest);
+
+            client *c = new client;
+            c->address = address;
+            c->socket = clientsocket;
+            c->connecttime = servtime;
+            c->lastinput = servtime;
+            clients.add(c);
+        }
+    }
+
+    loopv(clients)
+    {
+        client &c = *clients[i];
+        if((c.message || c.output.length()) && ENET_SOCKETSET_CHECK(writeset, c.socket))
+        {
+            const char *data = c.output.length() ? c.output.getbuf() : c.message->getbuf();
+            int len = c.output.length() ? c.output.length() : c.message->length();
+            ENetBuffer buf;
+            buf.data = (void *)&data[c.outputpos];
+            buf.dataLength = len-c.outputpos;
+            int res = enet_socket_send(c.socket, NULL, &buf, 1);
+            if(res>=0)
+            {
+                c.outputpos += res;
+                if(c.outputpos>=len)
+                {
+                    if(c.output.length()) c.output.setsize(0);
+                    else
+                    { 
+                        c.message->purge();
+                        c.message = NULL; 
+                    }
+                    c.outputpos = 0;
+                    if(!c.message && c.output.empty() && c.shouldpurge)
+                    {
+                        purgeclient(i--);
+                        continue;
+                    }
+                }
+            }
+            else { purgeclient(i--); continue; }
+        }
+        if(ENET_SOCKETSET_CHECK(readset, c.socket))
+        {
+            ENetBuffer buf;
+            buf.data = &c.input[c.inputpos];
+            buf.dataLength = sizeof(c.input) - c.inputpos;
+            int res = enet_socket_receive(c.socket, NULL, &buf, 1);
+            if(res>0)
+            {
+                c.inputpos += res;
+                c.input[min(c.inputpos, (int)sizeof(c.input)-1)] = '\0';
+                if(!checkclientinput(c)) { purgeclient(i--); continue; }
+            }
+            else { purgeclient(i--); continue; }
+        }
+        if(c.output.length() > OUTPUT_LIMIT) { purgeclient(i--); continue; }
+        if(ENET_TIME_DIFFERENCE(servtime, c.lastinput) >= (c.registeredserver ? KEEPALIVE_TIME : CLIENT_TIME)) { purgeclient(i--); continue; }
+    }
+}
+
+void banclients()
+{
+    loopvrev(clients) if(checkban(bans, clients[i]->address.host)) purgeclient(i);
+}
+
+volatile int reloadcfg = 1;
+
+#ifndef WIN32
+void reloadsignal(int signum)
+{
+    reloadcfg = 1;
+}
+#endif
+
+int main(int argc, char **argv)
+{
+    if(enet_initialize()<0) fatal("Unable to initialise network module");
+    atexit(enet_deinitialize);
+
+    const char *dir = "", *ip = NULL;
+    int port = 28787;
+    if(argc>=2) dir = argv[1];
+    if(argc>=3) port = atoi(argv[2]);
+    if(argc>=4) ip = argv[3];
+    defformatstring(logname, "%smaster.log", dir);
+    defformatstring(cfgname, "%smaster.cfg", dir);
+    path(logname);
+    path(cfgname);
+    logfile = fopen(logname, "a");
+    if(!logfile) logfile = stdout;
+    setvbuf(logfile, NULL, _IOLBF, BUFSIZ);
+#ifndef WIN32
+    signal(SIGUSR1, reloadsignal);
+#endif
+    setupserver(port, ip);
+    for(;;)
+    {
+        if(reloadcfg)
+        {
+            conoutf("reloading master.cfg");
+            execfile(cfgname);
+            bangameservers();
+            banclients();
+            gengbanlist();
+            reloadcfg = 0;
+        }
+
+        servtime = enet_time_get();
+        checkclients();
+        checkgameservers();
+    }
+
+    return EXIT_SUCCESS;
+}
+
diff --git a/src/engine/material.cpp b/src/engine/material.cpp
new file mode 100644 (file)
index 0000000..59f47f8
--- /dev/null
@@ -0,0 +1,886 @@
+#include "engine.h"
+
+struct QuadNode
+{
+    int x, y, size;
+    uint filled;
+    QuadNode *child[4];
+
+    QuadNode(int x, int y, int size) : x(x), y(y), size(size), filled(0) { loopi(4) child[i] = 0; }
+
+    void clear() 
+    {
+        loopi(4) DELETEP(child[i]);
+    }
+    
+    ~QuadNode()
+    {
+        clear();
+    }
+
+    void insert(int mx, int my, int msize)
+    {
+        if(size == msize)
+        {
+            filled = 0xF;
+            return;
+        }
+        int csize = size>>1, i = 0;
+        if(mx >= x+csize) i |= 1;
+        if(my >= y+csize) i |= 2;
+        if(csize == msize)
+        {
+            filled |= (1 << i);
+            return;
+        }
+        if(!child[i]) child[i] = new QuadNode(i&1 ? x+csize : x, i&2 ? y+csize : y, csize);
+        child[i]->insert(mx, my, msize);
+        loopj(4) if(child[j])
+        {
+            if(child[j]->filled == 0xF)
+            {
+                DELETEP(child[j]);
+                filled |= (1 << j);
+            }
+        }
+    }
+
+    void genmatsurf(ushort mat, uchar orient, uchar visible, int x, int y, int z, int size, materialsurface *&matbuf)
+    {
+        materialsurface &m = *matbuf++;
+        m.material = mat;
+        m.orient = orient;
+        m.visible = visible;
+        m.csize = size;
+        m.rsize = size;
+        int dim = dimension(orient);
+        m.o[C[dim]] = x;
+        m.o[R[dim]] = y;
+        m.o[dim] = z;
+    }
+
+    void genmatsurfs(ushort mat, uchar orient, uchar flags, int z, materialsurface *&matbuf)
+    {
+        if(filled == 0xF) genmatsurf(mat, orient, flags, x, y, z, size, matbuf);
+        else if(filled)
+        {
+            int csize = size>>1;
+            loopi(4) if(filled & (1 << i))
+                genmatsurf(mat, orient, flags, i&1 ? x+csize : x, i&2 ? y+csize : y, z, csize, matbuf);
+        }
+        loopi(4) if(child[i]) child[i]->genmatsurfs(mat, orient, flags, z, matbuf);
+    }
+};
+
+static float wfwave;
+
+static const bvec4 matnormals[6] =
+{
+    bvec4(0x80, 0, 0),
+    bvec4(0x7F, 0, 0),
+    bvec4(0, 0x80, 0),
+    bvec4(0, 0x7F, 0),
+    bvec4(0, 0, 0x80),
+    bvec4(0, 0, 0x7F)
+};
+
+static void renderwaterfall(const materialsurface &m, float offset)
+{
+    if(gle::attribbuf.empty())
+    {
+        gle::defvertex();
+        gle::defnormal(4, GL_BYTE);
+        gle::begin(GL_QUADS);
+    }
+    float x = m.o.x, y = m.o.y, zmin = m.o.z, zmax = zmin;
+    if(m.ends&1) zmin += -WATER_OFFSET-WATER_AMPLITUDE;
+    if(m.ends&2) zmax += wfwave;
+    int csize = m.csize, rsize = m.rsize;
+    switch(m.orient)
+    {
+    #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+        case orient: v0 v1 v2 v3 break;
+    #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+        { \
+            gle::attribf(mx sx, my sy, mz sz); \
+            gle::attrib(matnormals[orient]); \
+        }
+        GENFACEVERTSXY(x, x, y, y, zmin, zmax, /**/, + csize, /**/, + rsize, + offset, - offset)
+    #undef GENFACEORIENT
+    #undef GENFACEVERT
+    }
+}
+
+static void drawmaterial(const materialsurface &m, float offset, const bvec4 &color)
+{
+    if(gle::attribbuf.empty())
+    {
+        gle::defvertex();
+        gle::defcolor(4, GL_UNSIGNED_BYTE);
+        gle::begin(GL_QUADS);
+    }
+    float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize, rsize = m.rsize;
+    switch(m.orient)
+    {
+    #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+        case orient: v0 v1 v2 v3 break;
+    #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+        { \
+            gle::attribf(mx sx, my sy, mz sz); \
+            gle::attrib(color); \
+        }
+        GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset)
+    #undef GENFACEORIENT
+    #undef GENFACEVERT
+    }
+}
+
+const struct material
+{
+    const char *name;
+    ushort id;
+} materials[] = 
+{
+    {"air", MAT_AIR},
+    {"water", MAT_WATER}, {"water1", MAT_WATER}, {"water2", MAT_WATER+1}, {"water3", MAT_WATER+2}, {"water4", MAT_WATER+3},
+    {"glass", MAT_GLASS}, {"glass1", MAT_GLASS}, {"glass2", MAT_GLASS+1}, {"glass3", MAT_GLASS+2}, {"glass4", MAT_GLASS+3},
+    {"lava", MAT_LAVA}, {"lava1", MAT_LAVA}, {"lava2", MAT_LAVA+1}, {"lava3", MAT_LAVA+2}, {"lava4", MAT_LAVA+3},
+    {"clip", MAT_CLIP},
+    {"noclip", MAT_NOCLIP},
+    {"gameclip", MAT_GAMECLIP},
+    {"death", MAT_DEATH},
+    {"alpha", MAT_ALPHA}
+};
+
+int findmaterial(const char *name)
+{
+    loopi(sizeof(materials)/sizeof(material))
+    {
+        if(!strcmp(materials[i].name, name)) return materials[i].id;
+    } 
+    return -1;
+}  
+
+const char *findmaterialname(int mat)
+{
+    loopi(sizeof(materials)/sizeof(materials[0])) if(materials[i].id == mat) return materials[i].name;
+    return NULL;
+}
+   
+const char *getmaterialdesc(int mat, const char *prefix)
+{
+    static const ushort matmasks[] = { MATF_VOLUME|MATF_INDEX, MATF_CLIP, MAT_DEATH, MAT_ALPHA };
+    static string desc;
+    desc[0] = '\0';
+    loopi(sizeof(matmasks)/sizeof(matmasks[0])) if(mat&matmasks[i])
+    {
+        const char *matname = findmaterialname(mat&matmasks[i]);
+        if(matname)    
+        {
+            concatstring(desc, desc[0] ? ", " : prefix);
+            concatstring(desc, matname);
+        }
+    }
+    return desc;
+}
+int visiblematerial(const cube &c, int orient, const ivec &co, int size, ushort matmask)
+{   
+    ushort mat = c.material&matmask;
+    switch(mat)
+    {
+    case MAT_AIR:
+         break;
+
+    case MAT_LAVA:
+    case MAT_WATER:
+        if(visibleface(c, orient, co, size, mat, MAT_AIR, matmask))
+            return (orient != O_BOTTOM ? MATSURF_VISIBLE : MATSURF_EDIT_ONLY);
+        break;
+
+    case MAT_GLASS:
+        if(visibleface(c, orient, co, size, MAT_GLASS, MAT_AIR, matmask))
+            return MATSURF_VISIBLE;
+        break;
+
+    default:
+        if(visibleface(c, orient, co, size, mat, MAT_AIR, matmask))
+            return MATSURF_EDIT_ONLY;
+        break;
+    }
+    return MATSURF_NOT_VISIBLE;
+}
+
+void genmatsurfs(const cube &c, const ivec &co, int size, vector<materialsurface> &matsurfs)
+{
+    loopi(6)
+    {
+        static const ushort matmasks[] = { MATF_VOLUME|MATF_INDEX, MATF_CLIP, MAT_DEATH, MAT_ALPHA };
+        loopj(sizeof(matmasks)/sizeof(matmasks[0]))
+        {
+            int matmask = matmasks[j];
+            int vis = visiblematerial(c, i, co, size, matmask&~MATF_INDEX);
+            if(vis != MATSURF_NOT_VISIBLE) 
+            {
+                materialsurface m;
+                m.material = c.material&matmask;
+                m.orient = i;
+                m.visible = vis;
+                m.o = co;
+                m.csize = m.rsize = size;
+                if(dimcoord(i)) m.o[dimension(i)] += size;
+                matsurfs.add(m);
+                break;
+            }
+        }
+    }
+}
+
+static inline bool mergematcmp(const materialsurface &x, const materialsurface &y)
+{
+    int dim = dimension(x.orient), c = C[dim], r = R[dim];
+    if(x.o[r] + x.rsize < y.o[r] + y.rsize) return true;
+    if(x.o[r] + x.rsize > y.o[r] + y.rsize) return false;
+    return x.o[c] < y.o[c];
+}
+
+static int mergematr(materialsurface *m, int sz, materialsurface &n)
+{
+    int dim = dimension(n.orient), c = C[dim], r = R[dim];
+    for(int i = sz-1; i >= 0; --i)
+    {
+        if(m[i].o[r] + m[i].rsize < n.o[r]) break;
+        if(m[i].o[r] + m[i].rsize == n.o[r] && m[i].o[c] == n.o[c] && m[i].csize == n.csize)
+        {
+            n.o[r] = m[i].o[r];
+            n.rsize += m[i].rsize;
+            memmove(&m[i], &m[i+1], (sz - (i+1)) * sizeof(materialsurface));
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int mergematc(materialsurface &m, materialsurface &n)
+{
+    int dim = dimension(n.orient), c = C[dim], r = R[dim];
+    if(m.o[r] == n.o[r] && m.rsize == n.rsize && m.o[c] + m.csize == n.o[c])
+    {
+        n.o[c] = m.o[c];
+        n.csize += m.csize;
+        return 1;
+    }
+    return 0;
+}
+
+static int mergemat(materialsurface *m, int sz, materialsurface &n)
+{
+    for(bool merged = false; sz; merged = true)
+    {
+        int rmerged = mergematr(m, sz, n);
+        sz -= rmerged;
+        if(!rmerged && merged) break;
+        if(!sz) break;
+        int cmerged = mergematc(m[sz-1], n);
+        sz -= cmerged;
+        if(!cmerged) break;
+    }
+    m[sz++] = n;
+    return sz;
+}
+
+static int mergemats(materialsurface *m, int sz)
+{
+    quicksort(m, sz, mergematcmp);
+
+    int nsz = 0;
+    loopi(sz) nsz = mergemat(m, nsz, m[i]);
+    return nsz;
+}
+
+static inline bool optmatcmp(const materialsurface &x, const materialsurface &y)
+{
+    if(x.material < y.material) return true;
+    if(x.material > y.material) return false;
+    if(x.orient > y.orient) return true;
+    if(x.orient < y.orient) return false;
+    int dim = dimension(x.orient);
+    return x.o[dim] < y.o[dim];
+}
+
+VARF(optmats, 0, 1, 1, allchanged());
+
+int optimizematsurfs(materialsurface *matbuf, int matsurfs)
+{
+    quicksort(matbuf, matsurfs, optmatcmp);
+    if(!optmats) return matsurfs;
+    materialsurface *cur = matbuf, *end = matbuf+matsurfs;
+    while(cur < end)
+    {
+         materialsurface *start = cur++;
+         int dim = dimension(start->orient);
+         while(cur < end &&
+               cur->material == start->material &&
+               cur->orient == start->orient &&
+               cur->visible == start->visible && 
+               cur->o[dim] == start->o[dim])
+            ++cur;
+         if(!isliquid(start->material&MATF_VOLUME) || start->orient != O_TOP || !vertwater)
+         {
+            if(start!=matbuf) memmove(matbuf, start, (cur-start)*sizeof(materialsurface));
+            matbuf += mergemats(matbuf, cur-start);
+         }
+         else if(cur-start>=4)
+         {
+            QuadNode vmats(0, 0, worldsize);
+            loopi(cur-start) vmats.insert(start[i].o[C[dim]], start[i].o[R[dim]], start[i].csize);
+            vmats.genmatsurfs(start->material, start->orient, start->visible, start->o[dim], matbuf);
+         }
+         else
+         {
+            if(start!=matbuf) memmove(matbuf, start, (cur-start)*sizeof(materialsurface));
+            matbuf += cur-start;
+         }
+    }
+    return matsurfs - (end-matbuf);
+}
+
+struct waterinfo
+{
+    materialsurface *m;
+    double depth, area;
+};
+
+void setupmaterials(int start, int len)
+{
+    int hasmat = 0;
+    vector<waterinfo> water;
+    unionfind uf;
+    if(!len) len = valist.length();
+    for(int i = start; i < len; i++) 
+    {
+        vtxarray *va = valist[i];
+        materialsurface *skip = NULL;
+        loopj(va->matsurfs)
+        {
+            materialsurface &m = va->matbuf[j];
+            int matvol = m.material&MATF_VOLUME;
+            if(matvol==MAT_WATER && m.orient==O_TOP)
+            {
+                m.index = water.length();
+                loopvk(water)
+                {
+                    materialsurface &n = *water[k].m;
+                    if(m.material!=n.material || m.o.z!=n.o.z) continue;
+                    if(n.o.x+n.rsize==m.o.x || m.o.x+m.rsize==n.o.x)
+                    {
+                        if(n.o.y+n.csize>m.o.y && n.o.y<m.o.y+m.csize) uf.unite(m.index, n.index);
+                    }
+                    else if(n.o.y+n.csize==m.o.y || m.o.y+m.csize==n.o.y)
+                    {
+                        if(n.o.x+n.rsize>m.o.x && n.o.x<m.o.x+m.rsize) uf.unite(m.index, n.index);
+                    }
+                }
+                waterinfo &wi = water.add();
+                wi.m = &m;
+                vec center(m.o.x+m.rsize/2, m.o.y+m.csize/2, m.o.z-WATER_OFFSET);
+                m.light = brightestlight(center, vec(0, 0, 1));
+                float depth = raycube(center, vec(0, 0, -1), 10000);
+                wi.depth = double(depth)*m.rsize*m.csize;
+                wi.area = m.rsize*m.csize; 
+            }
+            else if(isliquid(matvol) && m.orient!=O_BOTTOM && m.orient!=O_TOP)
+            {
+                m.ends = 0;
+                int dim = dimension(m.orient), coord = dimcoord(m.orient);
+                ivec o(m.o);
+                o.z -= 1;
+                o[dim] += coord ? 1 : -1;
+                int minc = o[dim^1], maxc = minc + (C[dim]==2 ? m.rsize : m.csize);
+                ivec co;
+                int csize;
+                while(o[dim^1] < maxc)
+                {
+                    cube &c = lookupcube(o, 0, co, csize);
+                    if(isliquid(c.material&MATF_VOLUME)) { m.ends |= 1; break; }
+                    o[dim^1] += csize;
+                }
+                o[dim^1] = minc;
+                o.z += R[dim]==2 ? m.rsize : m.csize;
+                o[dim] -= coord ? 2 : -2;
+                while(o[dim^1] < maxc)
+                {
+                    cube &c = lookupcube(o, 0, co, csize);
+                    if(visiblematerial(c, O_TOP, co, csize)) { m.ends |= 2; break; }
+                    o[dim^1] += csize;
+                }
+            }
+            else if(matvol==MAT_GLASS)
+            {
+                int dim = dimension(m.orient);
+                vec center(m.o);
+                center[R[dim]] += m.rsize/2;
+                center[C[dim]] += m.csize/2;
+                m.envmap = closestenvmap(center);
+            }
+            if(matvol) hasmat |= 1<<m.material;
+            m.skip = 0;
+            if(skip && m.material == skip->material && m.orient == skip->orient && skip->skip < 0xFFFF)
+                skip->skip++;
+            else 
+                skip = &m;
+        }
+    }
+    loopv(water)
+    {
+        int root = uf.find(i);
+        if(i==root) continue;
+        materialsurface &m = *water[i].m, &n = *water[root].m;
+        if(m.light && (!m.light->attr1 || !n.light || (n.light->attr1 && m.light->attr1 > n.light->attr1))) n.light = m.light;
+        water[root].depth += water[i].depth;
+        water[root].area += water[i].area;
+    }
+    loopv(water)
+    {
+        int root = uf.find(i);
+        water[i].m->light = water[root].m->light;
+        water[i].m->depth = (short)(water[root].depth/water[root].area);
+    }
+    if(hasmat&(0xF<<MAT_WATER))
+    {
+        loadcaustics(true);
+        preloadwatershaders(true);
+        loopi(4) if(hasmat&(1<<(MAT_WATER+i))) lookupmaterialslot(MAT_WATER+i);
+    }
+    if(hasmat&(0xF<<MAT_LAVA)) 
+    {
+        useshaderbyname("lava");
+        useshaderbyname("lavaglare");
+        loopi(4) if(hasmat&(1<<(MAT_LAVA+i))) lookupmaterialslot(MAT_LAVA+i);
+    }
+    if(hasmat&(0xF<<MAT_GLASS)) useshaderbyname("glass");
+}
+
+VARP(showmat, 0, 1, 1);
+
+static int sortdim[3];
+static ivec sortorigin;
+static bool sortedit;
+
+static inline bool vismatcmp(const materialsurface *xm, const materialsurface *ym)
+{
+    const materialsurface &x = *xm, &y = *ym;
+    if(!sortedit)
+    {
+        if((x.material&MATF_VOLUME) == MAT_LAVA) { if((y.material&MATF_VOLUME) != MAT_LAVA) return true; }
+        else if((y.material&MATF_VOLUME) == MAT_LAVA) return false;
+    }
+    int xdim = dimension(x.orient), ydim = dimension(y.orient);
+    loopi(3)
+    {
+        int dim = sortdim[i], xmin, xmax, ymin, ymax;
+        xmin = xmax = x.o[dim];
+        if(dim==C[xdim]) xmax += x.csize;
+        else if(dim==R[xdim]) xmax += x.rsize;
+        ymin = ymax = y.o[dim];
+        if(dim==C[ydim]) ymax += y.csize;
+        else if(dim==R[ydim]) ymax += y.rsize;
+        if(xmax > ymin && ymax > xmin) continue;
+        int c = sortorigin[dim];
+        if(c > xmin && c < xmax) return sortedit;
+        if(c > ymin && c < ymax) return !sortedit;
+        xmin = abs(xmin - c);
+        xmax = abs(xmax - c);
+        ymin = abs(ymin - c);
+        ymax = abs(ymax - c);
+        if(max(xmin, xmax) <= min(ymin, ymax)) return sortedit;
+        else if(max(ymin, ymax) <= min(xmin, xmax)) return !sortedit;
+    }
+    if(x.material < y.material) return sortedit;
+    if(x.material > y.material) return !sortedit;
+    return false;
+}
+
+void sortmaterials(vector<materialsurface *> &vismats)
+{
+    sortorigin = ivec(camera1->o);
+    if(reflecting) sortorigin.z = int(reflectz - (camera1->o.z - reflectz));
+    vec dir;
+    vecfromyawpitch(camera1->yaw, reflecting ? -camera1->pitch : camera1->pitch, 1, 0, dir);
+    loopi(3) { dir[i] = fabs(dir[i]); sortdim[i] = i; }
+    if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]);
+    if(dir[sortdim[1]] > dir[sortdim[0]]) swap(sortdim[1], sortdim[0]);
+    if(dir[sortdim[2]] > dir[sortdim[1]]) swap(sortdim[2], sortdim[1]);
+
+    for(vtxarray *va = reflecting ? reflectedva : visibleva; va; va = reflecting ? va->rnext : va->next)
+    {
+        if(!va->matsurfs || va->occluded >= OCCLUDE_BB) continue;
+        if(reflecting || refracting>0 ? va->o.z+va->size <= reflectz : va->o.z >= reflectz) continue;
+        loopi(va->matsurfs)
+        {
+            materialsurface &m = va->matbuf[i];
+            if(!editmode || !showmat || drawtex)
+            {
+                int matvol = m.material&MATF_VOLUME;
+                if(matvol==MAT_WATER && (m.orient==O_TOP || (refracting<0 && reflectz>worldsize))) { i += m.skip; continue; }
+                if(m.visible == MATSURF_EDIT_ONLY) { i += m.skip; continue; }
+                if(glaring && matvol!=MAT_LAVA) { i += m.skip; continue; }
+            }
+            else if(glaring) continue;
+            vismats.add(&m);
+        }
+    }
+    sortedit = editmode && showmat && !drawtex;
+    vismats.sort(vismatcmp);
+}
+
+void rendermatgrid(vector<materialsurface *> &vismats)
+{
+    enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    int lastmat = -1;
+    bvec4 color(0, 0, 0, 0);
+    loopvrev(vismats)
+    {
+        materialsurface &m = *vismats[i];
+        if(m.material != lastmat)
+        {
+            switch(m.material&~MATF_INDEX)
+            {
+                case MAT_WATER:    color = bvec4( 0,  0, 85, 255); break; // blue
+                case MAT_CLIP:     color = bvec4(85,  0,  0, 255); break; // red
+                case MAT_GLASS:    color = bvec4( 0, 85, 85, 255); break; // cyan
+                case MAT_NOCLIP:   color = bvec4( 0, 85,  0, 255); break; // green
+                case MAT_LAVA:     color = bvec4(85, 40,  0, 255); break; // orange
+                case MAT_GAMECLIP: color = bvec4(85, 85,  0, 255); break; // yellow
+                case MAT_DEATH:    color = bvec4(40, 40, 40, 255); break; // black
+                case MAT_ALPHA:    color = bvec4(85,  0, 85, 255); break; // pink
+                default: continue;
+            }
+            lastmat = m.material;
+        }
+        drawmaterial(m, -0.1f, color);
+    }
+    xtraverts += gle::end();
+    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+    disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+}
+
+#define GLASSVARS(name) \
+    bvec name##color(0x20, 0x80, 0xC0); \
+    HVARFR(name##colour, 0, 0x2080C0, 0xFFFFFF, \
+    { \
+        if(!name##colour) name##colour = 0x2080C0; \
+        name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
+    });
+
+GLASSVARS(glass)
+GLASSVARS(glass2)
+GLASSVARS(glass3)
+GLASSVARS(glass4)
+
+GETMATIDXVAR(glass, colour, int)
+GETMATIDXVAR(glass, color, const bvec &)
+
+VARP(glassenv, 0, 1, 1);
+
+static void drawglass(const materialsurface &m, float offset)
+{
+    if(gle::attribbuf.empty())
+    {
+        gle::defvertex();
+        gle::defnormal(4, GL_BYTE);
+        gle::begin(GL_QUADS);
+    }
+    float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize, rsize = m.rsize;
+    switch(m.orient)
+    {
+    #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+        case orient: v0 v1 v2 v3 break;
+    #define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+        { \
+            gle::attribf(mx sx, my sy, mz sz); \
+            gle::attrib(matnormals[orient]); \
+        }
+        GENFACEVERTS(x, x, y, y, z, z, /**/, + csize, /**/, + rsize, + offset, - offset)
+    #undef GENFACEORIENT
+    #undef GENFACEVERT
+    }
+}
+
+VARFP(waterfallenv, 0, 1, 1, preloadwatershaders());
+
+static inline void changematerial(int mat, int orient)
+{
+    switch(mat&~MATF_INDEX)
+    {
+        case MAT_LAVA:
+            if(orient==O_TOP) flushlava();
+            else xtraverts += gle::end();
+            break;
+        default:
+            xtraverts += gle::end();
+            break;
+    }
+}
+
+void rendermaterials()
+{
+    vector<materialsurface *> vismats;
+    sortmaterials(vismats);
+    if(vismats.empty()) return;
+
+    glDisable(GL_CULL_FACE);
+
+    MSlot *mslot = NULL;
+    int lastorient = -1, lastmat = -1, usedwaterfall = -1;
+    bool depth = true, blended = false;
+    ushort envmapped = EMID_NONE;
+
+    GLOBALPARAM(camera, camera1->o);
+
+    int lastfogtype = 1;
+    if(editmode && showmat && !drawtex)
+    {
+        glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+        glEnable(GL_BLEND); blended = true;
+        foggednotextureshader->set();
+        zerofogcolor(); lastfogtype = 0;
+        bvec4 color(0, 0, 0, 0);
+        loopv(vismats)
+        {
+            const materialsurface &m = *vismats[i];
+            if(lastmat!=m.material)
+            {
+                switch(m.material&~MATF_INDEX)
+                {
+                    case MAT_WATER:    color = bvec4(255, 128,   0, 255); break; // blue
+                    case MAT_CLIP:     color = bvec4(  0, 255, 255, 255); break; // red
+                    case MAT_GLASS:    color = bvec4(255,   0,   0, 255); break; // cyan
+                    case MAT_NOCLIP:   color = bvec4(255,   0, 255, 255); break; // green
+                    case MAT_LAVA:     color = bvec4(  0, 128, 255, 255); break; // orange
+                    case MAT_GAMECLIP: color = bvec4(  0,   0, 255, 255); break; // yellow
+                    case MAT_DEATH:    color = bvec4(192, 192, 192, 255); break; // black
+                    case MAT_ALPHA:    color = bvec4(  0, 255,   0, 255); break; // pink
+                    default: continue;
+                }
+                lastmat = m.material;
+            }
+            drawmaterial(m, -0.1f, color);
+        }
+        xtraverts += gle::end();
+    }
+    else loopv(vismats)
+    {
+        const materialsurface &m = *vismats[i];
+        int matvol = m.material&~MATF_INDEX;
+        if(lastmat!=m.material || lastorient!=m.orient || (matvol==MAT_GLASS && envmapped && m.envmap != envmapped))
+        {
+            int fogtype = lastfogtype;
+            switch(matvol)
+            {
+                case MAT_WATER:
+                    if(m.orient == O_TOP) continue;
+                    if(lastmat == m.material) break;
+                    mslot = &lookupmaterialslot(m.material, false);
+                    if(!mslot->loaded || !mslot->sts.inrange(1)) continue;
+                    else
+                    {
+                        changematerial(lastmat, lastorient);
+                        glBindTexture(GL_TEXTURE_2D, mslot->sts[1].t->id);
+
+                        bvec wfcol = getwaterfallcolor(m.material);
+                        if(wfcol.iszero()) wfcol = getwatercolor(m.material);
+                        gle::color(wfcol, 192);
+
+                        int wfog = getwaterfog(m.material);
+                        if(!wfog && !waterfallenv)
+                        {
+                            foggednotextureshader->set();
+                            fogtype = 1;
+                            if(blended) { glDisable(GL_BLEND); blended = false; }
+                            if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+                        }
+                        else if((!waterfallrefract || reflecting || refracting) && !waterfallenv)
+                        {
+                            glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR);
+                            SETSHADER(waterfall);
+                            fogtype = 0;
+                            if(!blended) { glEnable(GL_BLEND); blended = true; }
+                            if(depth) { glDepthMask(GL_FALSE); depth = false; }
+                        }
+                        else
+                        {
+                            fogtype = 1;
+
+                            if(waterfallrefract && wfog && !reflecting && !refracting)
+                            {
+                                if(waterfallenv) SETSHADER(waterfallenvrefract);    
+                                else SETSHADER(waterfallrefract);
+                                if(blended) { glDisable(GL_BLEND); blended = false; }
+                                if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+                            }
+                            else 
+                            {
+                                SETSHADER(waterfallenv);
+                                glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                                if(wfog)
+                                {
+                                    if(!blended) { glEnable(GL_BLEND); blended = true; }
+                                    if(depth) { glDepthMask(GL_FALSE); depth = false; }
+                                }
+                                else
+                                {
+                                    if(blended) { glDisable(GL_BLEND); blended = false; }
+                                    if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+                                }
+                            }
+
+                            if(usedwaterfall != m.material)
+                            {
+                                Texture *dudv = mslot->sts.inrange(5) ? mslot->sts[5].t : notexture;
+                                float scale = TEX_SCALE/(dudv->ys*mslot->scale);
+                                LOCALPARAMF(dudvoffset, 0, scale*16*lastmillis/1000.0f);
+
+                                glActiveTexture_(GL_TEXTURE1);
+                                glBindTexture(GL_TEXTURE_2D, mslot->sts.inrange(4) ? mslot->sts[4].t->id : notexture->id);
+                                glActiveTexture_(GL_TEXTURE2);
+                                glBindTexture(GL_TEXTURE_2D, mslot->sts.inrange(5) ? mslot->sts[5].t->id : notexture->id);
+                                if(waterfallenv)
+                                {
+                                    glActiveTexture_(GL_TEXTURE3);
+                                    glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(*mslot));
+                                }
+                                if(waterfallrefract && (!reflecting || !refracting) && usedwaterfall < 0)
+                                {
+                                    glActiveTexture_(GL_TEXTURE4);
+                                    extern void setupwaterfallrefract();
+                                    setupwaterfallrefract();
+                                }
+                                glActiveTexture_(GL_TEXTURE0);
+
+                                usedwaterfall = m.material;
+                            }
+                        }
+                        float angle = fmod(float(lastmillis/600.0f/(2*M_PI)), 1.0f), 
+                              s = angle - int(angle) - 0.5f;
+                        s *= 8 - fabs(s)*16;
+                        wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET;
+                        float scroll = 16.0f*lastmillis/1000.0f;
+                        float xscale = TEX_SCALE/(mslot->sts[1].t->xs*mslot->scale);
+                        float yscale = -TEX_SCALE/(mslot->sts[1].t->ys*mslot->scale);
+                        LOCALPARAMF(waterfalltexgen, xscale, yscale, 0.0f, scroll);
+                    }
+                    break;
+
+                case MAT_LAVA:
+                    if(lastmat==m.material && lastorient!=O_TOP && m.orient!=O_TOP) break;
+                    mslot = &lookupmaterialslot(m.material, false);
+                    if(!mslot->loaded) continue;
+                    else
+                    {
+                        int subslot = m.orient==O_TOP ? 0 : 1;
+                        if(!mslot->sts.inrange(subslot)) continue;
+                        changematerial(lastmat, lastorient);
+                        glBindTexture(GL_TEXTURE_2D, mslot->sts[subslot].t->id);
+                    }
+                    if(lastmat!=m.material)
+                    {
+                        if(!depth) { glDepthMask(GL_TRUE); depth = true; }
+                        if(blended) { glDisable(GL_BLEND); blended = false; }
+                        float t = lastmillis/2000.0f;
+                        t -= floor(t);
+                        t = 1.0f - 2*fabs(t-0.5f);
+                        extern int glare;
+                        if(glare) t = 0.625f + 0.075f*t;
+                        else t = 0.5f + 0.5f*t;
+                        gle::colorf(t, t, t);
+                        if(glaring) SETSHADER(lavaglare); else SETSHADER(lava);
+                        fogtype = 1;
+                    }
+                    if(m.orient!=O_TOP)
+                    {
+                        float angle = fmod(float(lastmillis/2000.0f/(2*M_PI)), 1.0f),
+                              s = angle - int(angle) - 0.5f;
+                        s *= 8 - fabs(s)*16;
+                        wfwave = vertwater ? WATER_AMPLITUDE*s-WATER_OFFSET : -WATER_OFFSET;
+                        float scroll = 16.0f*lastmillis/3000.0f;
+                        float xscale = TEX_SCALE/(mslot->sts[1].t->xs*mslot->scale);
+                        float yscale = -TEX_SCALE/(mslot->sts[1].t->ys*mslot->scale);
+                        LOCALPARAMF(lavatexgen, xscale, yscale, 0.0f, scroll);
+                    }
+                    else setuplava(mslot->sts[0].t, mslot->scale);
+                    break;
+
+                case MAT_GLASS:
+                    if((m.envmap==EMID_NONE || !glassenv || envmapped==m.envmap) && lastmat==m.material) break;
+                    changematerial(lastmat, lastorient);
+                    if(m.envmap!=EMID_NONE && glassenv && envmapped!=m.envmap)
+                    {
+                        glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(m.envmap));
+                        envmapped = m.envmap;
+                    }
+                    if(lastmat!=m.material)
+                    {
+                        if(!blended) { glEnable(GL_BLEND); blended = true; }
+                        if(depth) { glDepthMask(GL_FALSE); depth = false; }
+                        const bvec &gcol = getglasscolor(m.material);         
+                        if(m.envmap!=EMID_NONE && glassenv)
+                        {
+                            glBlendFunc(GL_ONE, GL_SRC_ALPHA);
+                            gle::color(gcol);
+                            SETSHADER(glass);
+                        }
+                        else
+                        {
+                            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                            gle::color(gcol, 40);
+                            foggednotextureshader->set();
+                            fogtype = 1;
+                        }
+                    }
+                    break;
+                
+                default: continue;
+            }
+            lastmat = m.material;
+            lastorient = m.orient;
+            if(fogtype!=lastfogtype)
+            {
+                if(fogtype) resetfogcolor();
+                else zerofogcolor();
+                lastfogtype = fogtype;
+            }
+        }
+        switch(matvol)
+        {
+            case MAT_WATER:
+                renderwaterfall(m, 0.1f);
+                break;
+
+            case MAT_LAVA:
+                if(m.orient==O_TOP) renderlava(m);
+                else renderwaterfall(m, 0.1f);
+                break;
+
+            case MAT_GLASS:
+                drawglass(m, 0.1f);
+                break;
+        }
+    }
+
+    if(lastorient >= 0) changematerial(lastmat, lastorient);
+
+    if(!depth) glDepthMask(GL_TRUE);
+    if(blended) glDisable(GL_BLEND);
+    if(!lastfogtype) resetfogcolor();
+    extern int wireframe;
+    if(editmode && showmat && !drawtex && !wireframe)
+    {
+        foggednotextureshader->set();
+        rendermatgrid(vismats);
+    }
+
+    glEnable(GL_CULL_FACE);
+}
+
diff --git a/src/engine/md3.h b/src/engine/md3.h
new file mode 100644 (file)
index 0000000..d560255
--- /dev/null
@@ -0,0 +1,183 @@
+struct md3;
+
+struct md3frame
+{
+    vec bbmin, bbmax, origin;
+    float radius;
+    uchar name[16];
+};
+
+struct md3tag
+{
+    char name[64];
+    float translation[3];
+    float rotation[3][3];
+};
+
+struct md3vertex
+{
+    short vertex[3];
+    short normal;
+};
+
+struct md3triangle
+{
+    int vertexindices[3];
+};
+
+struct md3header
+{
+    char id[4];
+    int version;
+    char name[64];
+    int flags;
+    int numframes, numtags, nummeshes, numskins;
+    int ofs_frames, ofs_tags, ofs_meshes, ofs_eof; // offsets
+};
+
+struct md3meshheader
+{
+    char id[4];
+    char name[64];
+    int flags;
+    int numframes, numshaders, numvertices, numtriangles;
+    int ofs_triangles, ofs_shaders, ofs_uv, ofs_vertices, meshsize; // offsets
+};
+
+struct md3 : vertloader<md3>
+{
+    md3(const char *name) : vertloader(name) {}
+
+    static const char *formatname() { return "md3"; }
+    bool flipy() const { return true; }
+    int type() const { return MDL_MD3; }
+
+    struct md3meshgroup : vertmeshgroup
+    {
+        bool load(const char *path)
+        {
+            stream *f = openfile(path, "rb");
+            if(!f) return false;
+            md3header header;
+            f->read(&header, sizeof(md3header));
+            lilswap(&header.version, 1);
+            lilswap(&header.flags, 9);
+            if(strncmp(header.id, "IDP3", 4) != 0 || header.version != 15) // header check
+            { 
+                delete f;
+                conoutf(CON_ERROR, "md3: corrupted header"); 
+                return false; 
+            }
+
+            name = newstring(path);
+
+            numframes = header.numframes;
+
+            int mesh_offset = header.ofs_meshes;
+            loopi(header.nummeshes)
+            {
+                vertmesh &m = *new vertmesh;
+                m.group = this;
+                meshes.add(&m);
+
+                md3meshheader mheader;
+                f->seek(mesh_offset, SEEK_SET);
+                f->read(&mheader, sizeof(md3meshheader));
+                lilswap(&mheader.flags, 10); 
+
+                m.name = newstring(mheader.name);
+               
+                m.numtris = mheader.numtriangles; 
+                m.tris = new tri[m.numtris];
+                f->seek(mesh_offset + mheader.ofs_triangles, SEEK_SET);
+                loopj(m.numtris)
+                {
+                    md3triangle tri;
+                    f->read(&tri, sizeof(md3triangle)); // read the triangles
+                    lilswap(tri.vertexindices, 3);
+                    loopk(3) m.tris[j].vert[k] = (ushort)tri.vertexindices[k];
+                }
+
+                m.numverts = mheader.numvertices;
+                m.tcverts = new tcvert[m.numverts];
+                f->seek(mesh_offset + mheader.ofs_uv , SEEK_SET); 
+                f->read(m.tcverts, m.numverts*2*sizeof(float)); // read the UV data
+                lilswap(&m.tcverts[0].tc.x, 2*m.numverts);
+                
+                m.verts = new vert[numframes*m.numverts];
+                f->seek(mesh_offset + mheader.ofs_vertices, SEEK_SET); 
+                loopj(numframes*m.numverts)
+                {
+                    md3vertex v;
+                    f->read(&v, sizeof(md3vertex)); // read the vertices
+                    lilswap(v.vertex, 4);
+
+                    m.verts[j].pos = vec(v.vertex[0]/64.0f, -v.vertex[1]/64.0f, v.vertex[2]/64.0f);
+
+                    float lng = (v.normal&0xFF)*2*M_PI/255.0f; // decode vertex normals
+                    float lat = ((v.normal>>8)&0xFF)*2*M_PI/255.0f;
+                    m.verts[j].norm = vec(cosf(lat)*sinf(lng), -sinf(lat)*sinf(lng), cosf(lng));
+                }
+
+                mesh_offset += mheader.meshsize;
+            }
+
+            numtags = header.numtags;
+            if(numtags)
+            {
+                tags = new tag[numframes*numtags];
+                f->seek(header.ofs_tags, SEEK_SET);
+                md3tag tag;
+
+                loopi(header.numframes*header.numtags)
+                {
+                    f->read(&tag, sizeof(md3tag));
+                    lilswap(tag.translation, 12);
+                    if(tag.name[0] && i<header.numtags) tags[i].name = newstring(tag.name);
+                    matrix4x3 &m = tags[i].transform;
+                    tag.translation[1] *= -1;
+                    // undo the -y
+                    loopj(3) tag.rotation[1][j] *= -1;
+                    // then restore it
+                    loopj(3) tag.rotation[j][1] *= -1;
+                    m.a = vec(tag.rotation[0]);
+                    m.b = vec(tag.rotation[1]);
+                    m.c = vec(tag.rotation[2]);
+                    m.d = vec(tag.translation);
+                }
+            }
+
+            delete f;
+            return true;
+        }
+    };
+    
+    meshgroup *loadmeshes(const char *name, va_list args)
+    {
+        md3meshgroup *group = new md3meshgroup;
+        if(!group->load(name)) { delete group; return NULL; }
+        return group;
+    }
+
+    bool loaddefaultparts()
+    {
+        const char *pname = parentdir(name);
+        part &mdl = addpart();
+        defformatstring(name1, "packages/models/%s/tris.md3", name);
+        mdl.meshes = sharemeshes(path(name1));
+        if(!mdl.meshes)
+        {
+            defformatstring(name2, "packages/models/%s/tris.md3", pname);    // try md3 in parent folder (vert sharing)
+            mdl.meshes = sharemeshes(path(name2));
+            if(!mdl.meshes) return false;
+        }
+        Texture *tex, *masks;
+        loadskin(name, pname, tex, masks);
+        mdl.initskins(tex, masks);
+        if(tex==notexture) conoutf(CON_ERROR, "could not load model skin for %s", name1);
+        return true;
+    }
+};
+
+vertcommands<md3> md3commands;
+
diff --git a/src/engine/md5.h b/src/engine/md5.h
new file mode 100644 (file)
index 0000000..0d59586
--- /dev/null
@@ -0,0 +1,419 @@
+struct md5;
+
+struct md5joint
+{
+    vec pos;
+    quat orient;
+};
+
+struct md5weight
+{
+    int joint;
+    float bias;
+    vec pos;
+};  
+
+struct md5vert
+{
+    vec2 tc;
+    ushort start, count;
+};
+
+struct md5hierarchy
+{
+    string name;
+    int parent, flags, start;
+};
+
+struct md5 : skelloader<md5>
+{
+    md5(const char *name) : skelloader(name) {}
+
+    static const char *formatname() { return "md5"; }
+    int type() const { return MDL_MD5; }
+
+    struct md5mesh : skelmesh
+    {
+        md5weight *weightinfo;
+        int numweights;
+        md5vert *vertinfo;
+
+        md5mesh() : weightinfo(NULL), numweights(0), vertinfo(NULL)
+        {
+        }
+
+        ~md5mesh()
+        {
+            cleanup();
+        }
+
+        void cleanup()
+        {
+            DELETEA(weightinfo);
+            DELETEA(vertinfo);
+        }
+
+        void buildverts(vector<md5joint> &joints)
+        {
+            loopi(numverts)
+            {
+                md5vert &v = vertinfo[i];
+                vec pos(0, 0, 0);
+                loopk(v.count)
+                {
+                    md5weight &w = weightinfo[v.start+k];
+                    md5joint &j = joints[w.joint];
+                    vec wpos = j.orient.rotate(w.pos);
+                    wpos.add(j.pos);
+                    wpos.mul(w.bias);
+                    pos.add(wpos);
+                }
+                vert &vv = verts[i];
+                vv.pos = pos;
+                vv.tc = v.tc;
+
+                blendcombo c;
+                int sorted = 0;
+                loopj(v.count)
+                {
+                    md5weight &w = weightinfo[v.start+j];
+                    sorted = c.addweight(sorted, w.bias, w.joint); 
+                }
+                c.finalize(sorted);
+                vv.blend = addblendcombo(c);
+            }
+        }
+
+        void load(stream *f, char *buf, size_t bufsize)
+        {
+            md5weight w;
+            md5vert v;
+            tri t;
+            int index;
+
+            while(f->getline(buf, bufsize) && buf[0]!='}')
+            {
+                if(strstr(buf, "// meshes:"))
+                {
+                    char *start = strchr(buf, ':')+1;
+                    if(*start==' ') start++; 
+                    char *end = start + strlen(start)-1;
+                    while(end >= start && isspace(*end)) end--;
+                    name = newstring(start, end+1-start);
+                }
+                else if(strstr(buf, "shader"))
+                {
+                    char *start = strchr(buf, '"'), *end = start ? strchr(start+1, '"') : NULL;
+                    if(start && end) 
+                    {
+                        char *texname = newstring(start+1, end-(start+1));
+                        part *p = loading->parts.last();
+                        p->initskins(notexture, notexture, group->meshes.length());
+                        skin &s = p->skins.last();
+                        s.tex = textureload(makerelpath(dir, texname), 0, true, false);
+                        delete[] texname;
+                    }
+                }
+                else if(sscanf(buf, " numverts %d", &numverts)==1)
+                {
+                    numverts = max(numverts, 0);        
+                    if(numverts)
+                    {
+                        vertinfo = new md5vert[numverts];
+                        verts = new vert[numverts];
+                    }
+                }
+                else if(sscanf(buf, " numtris %d", &numtris)==1)
+                {
+                    numtris = max(numtris, 0);
+                    if(numtris) tris = new tri[numtris];
+                }
+                else if(sscanf(buf, " numweights %d", &numweights)==1)
+                {
+                    numweights = max(numweights, 0);
+                    if(numweights) weightinfo = new md5weight[numweights];
+                }
+                else if(sscanf(buf, " vert %d ( %f %f ) %hu %hu", &index, &v.tc.x, &v.tc.y, &v.start, &v.count)==5)
+                {
+                    if(index>=0 && index<numverts) vertinfo[index] = v;
+                }
+                else if(sscanf(buf, " tri %d %hu %hu %hu", &index, &t.vert[0], &t.vert[1], &t.vert[2])==4)
+                {
+                    if(index>=0 && index<numtris) tris[index] = t;
+                }
+                else if(sscanf(buf, " weight %d %d %f ( %f %f %f ) ", &index, &w.joint, &w.bias, &w.pos.x, &w.pos.y, &w.pos.z)==6)
+                {
+                    w.pos.y = -w.pos.y;
+                    if(index>=0 && index<numweights) weightinfo[index] = w;
+                }
+            }
+        }
+    };
+
+    struct md5meshgroup : skelmeshgroup
+    {
+        md5meshgroup() 
+        {
+        }
+
+        bool loadmesh(const char *filename, float smooth)
+        {
+            stream *f = openfile(filename, "r");
+            if(!f) return false;
+
+            char buf[512];
+            vector<md5joint> basejoints;
+            while(f->getline(buf, sizeof(buf)))
+            {
+                int tmp;
+                if(sscanf(buf, " MD5Version %d", &tmp)==1)
+                {
+                    if(tmp!=10) { delete f; return false; }
+                }
+                else if(sscanf(buf, " numJoints %d", &tmp)==1)
+                {
+                    if(tmp<1) { delete f; return false; }
+                    if(skel->numbones>0) continue;
+                    skel->numbones = tmp;
+                    skel->bones = new boneinfo[skel->numbones];
+                }
+                else if(sscanf(buf, " numMeshes %d", &tmp)==1)
+                {
+                    if(tmp<1) { delete f; return false; }
+                }
+                else if(strstr(buf, "joints {"))
+                {
+                    string name;
+                    int parent;
+                    md5joint j;
+                    while(f->getline(buf, sizeof(buf)) && buf[0]!='}')
+                    {
+                        char *curbuf = buf, *curname = name;
+                        bool allowspace = false;
+                        while(*curbuf && isspace(*curbuf)) curbuf++;
+                        if(*curbuf == '"') { curbuf++; allowspace = true; }
+                        while(*curbuf && curname < &name[sizeof(name)-1])
+                        {
+                            char c = *curbuf++;
+                            if(c == '"') break; 
+                            if(isspace(c) && !allowspace) break;
+                            *curname++ = c;
+                        } 
+                        *curname = '\0'; 
+                        if(sscanf(curbuf, " %d ( %f %f %f ) ( %f %f %f )",
+                            &parent, &j.pos.x, &j.pos.y, &j.pos.z,
+                            &j.orient.x, &j.orient.y, &j.orient.z)==7)
+                        {
+                            j.pos.y = -j.pos.y;
+                            j.orient.x = -j.orient.x;
+                            j.orient.z = -j.orient.z;
+                            if(basejoints.length()<skel->numbones) 
+                            {
+                                if(!skel->bones[basejoints.length()].name) 
+                                    skel->bones[basejoints.length()].name = newstring(name);
+                                skel->bones[basejoints.length()].parent = parent;
+                            }
+                            j.orient.restorew();
+                            basejoints.add(j);
+                        }
+                    }
+                    if(basejoints.length()!=skel->numbones) { delete f; return false; }
+                }
+                else if(strstr(buf, "mesh {"))
+                {
+                    md5mesh *m = new md5mesh;
+                    m->group = this;
+                    meshes.add(m);
+                    m->load(f, buf, sizeof(buf));
+                    if(!m->numtris || !m->numverts)
+                    {
+                        conoutf(CON_WARN, "empty mesh in %s", filename);
+                        meshes.removeobj(m);
+                        delete m;
+                    }
+                }
+            }
+        
+            if(skel->shared <= 1) 
+            {
+                skel->linkchildren();
+                loopv(basejoints) 
+                {
+                    boneinfo &b = skel->bones[i];
+                    b.base = dualquat(basejoints[i].orient, basejoints[i].pos);
+                    (b.invbase = b.base).invert();
+                }
+            }
+
+            loopv(meshes)
+            {
+                md5mesh &m = *(md5mesh *)meshes[i];
+                m.buildverts(basejoints);
+                if(smooth <= 1) m.smoothnorms(smooth);
+                else m.buildnorms();
+                m.cleanup();
+            }
+            
+            sortblendcombos();
+
+            delete f;
+            return true;
+        }
+
+        skelanimspec *loadanim(const char *filename)
+        {
+            skelanimspec *sa = skel->findskelanim(filename);
+            if(sa) return sa;
+
+            stream *f = openfile(filename, "r");
+            if(!f) return NULL;
+
+            vector<md5hierarchy> hierarchy;
+            vector<md5joint> basejoints;
+            int animdatalen = 0, animframes = 0;
+            float *animdata = NULL;
+            dualquat *animbones = NULL;
+            char buf[512];
+            while(f->getline(buf, sizeof(buf)))
+            {
+                int tmp;
+                if(sscanf(buf, " MD5Version %d", &tmp)==1)
+                {
+                    if(tmp!=10) { delete f; return NULL; }
+                }
+                else if(sscanf(buf, " numJoints %d", &tmp)==1)
+                {
+                    if(tmp!=skel->numbones) { delete f; return NULL; }
+                }
+                else if(sscanf(buf, " numFrames %d", &animframes)==1)
+                {
+                    if(animframes<1) { delete f; return NULL; }
+                }
+                else if(sscanf(buf, " frameRate %d", &tmp)==1);
+                else if(sscanf(buf, " numAnimatedComponents %d", &animdatalen)==1)
+                {
+                    if(animdatalen>0) animdata = new float[animdatalen];
+                }
+                else if(strstr(buf, "bounds {"))
+                {
+                    while(f->getline(buf, sizeof(buf)) && buf[0]!='}');
+                }
+                else if(strstr(buf, "hierarchy {"))
+                {
+                    while(f->getline(buf, sizeof(buf)) && buf[0]!='}')
+                    {
+                        md5hierarchy h;
+                        if(sscanf(buf, " %100s %d %d %d", h.name, &h.parent, &h.flags, &h.start)==4)
+                            hierarchy.add(h);
+                    }
+                }
+                else if(strstr(buf, "baseframe {"))
+                {
+                    while(f->getline(buf, sizeof(buf)) && buf[0]!='}')
+                    {
+                        md5joint j;
+                        if(sscanf(buf, " ( %f %f %f ) ( %f %f %f )", &j.pos.x, &j.pos.y, &j.pos.z, &j.orient.x, &j.orient.y, &j.orient.z)==6)
+                        {
+                            j.pos.y = -j.pos.y;
+                            j.orient.x = -j.orient.x;
+                            j.orient.z = -j.orient.z;
+                            j.orient.restorew();
+                            basejoints.add(j);
+                        }
+                    }
+                    if(basejoints.length()!=skel->numbones) { delete f; if(animdata) delete[] animdata; return NULL; }
+                    animbones = new dualquat[(skel->numframes+animframes)*skel->numbones];
+                    if(skel->framebones)
+                    {
+                        memcpy(animbones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat));
+                        delete[] skel->framebones;
+                    }
+                    skel->framebones = animbones;
+                    animbones += skel->numframes*skel->numbones;
+
+                    sa = &skel->addskelanim(filename);
+                    sa->frame = skel->numframes;
+                    sa->range = animframes;
+
+                    skel->numframes += animframes;
+                }
+                else if(sscanf(buf, " frame %d", &tmp)==1)
+                {
+                    for(int numdata = 0; f->getline(buf, sizeof(buf)) && buf[0]!='}';)
+                    {
+                        for(char *src = buf, *next = src; numdata < animdatalen; numdata++, src = next)
+                        {
+                            animdata[numdata] = strtod(src, &next);
+                            if(next <= src) break;
+                        }
+                    }
+                    dualquat *frame = &animbones[tmp*skel->numbones];
+                    loopv(basejoints)
+                    {
+                        md5hierarchy &h = hierarchy[i];
+                        md5joint j = basejoints[i];
+                        if(h.start < animdatalen && h.flags)
+                        {
+                            float *jdata = &animdata[h.start];
+                            if(h.flags&1) j.pos.x = *jdata++;
+                            if(h.flags&2) j.pos.y = -*jdata++;
+                            if(h.flags&4) j.pos.z = *jdata++;
+                            if(h.flags&8) j.orient.x = -*jdata++;
+                            if(h.flags&16) j.orient.y = *jdata++;
+                            if(h.flags&32) j.orient.z = -*jdata++;
+                            j.orient.restorew();
+                        }
+                        frame[i] = dualquat(j.orient, j.pos);
+                        if(adjustments.inrange(i)) adjustments[i].adjust(frame[i]);
+                        frame[i].mul(skel->bones[i].invbase);
+                        if(h.parent >= 0) frame[i].mul(skel->bones[h.parent].base, dualquat(frame[i]));
+                        frame[i].fixantipodal(skel->framebones[i]);
+                    }
+                }    
+            }
+
+            if(animdata) delete[] animdata;
+            delete f;
+
+            return sa;
+        }
+
+        bool load(const char *meshfile, float smooth)
+        {
+            name = newstring(meshfile);
+
+            if(!loadmesh(meshfile, smooth)) return false;
+            
+            return true;
+        }
+    };            
+
+    meshgroup *loadmeshes(const char *name, va_list args)
+    {
+        md5meshgroup *group = new md5meshgroup;
+        group->shareskeleton(va_arg(args, char *));
+        if(!group->load(name, va_arg(args, double))) { delete group; return NULL; }
+        return group;
+    }
+
+    bool loaddefaultparts()
+    {
+        skelpart &mdl = addpart();
+        mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+        adjustments.setsize(0);
+        const char *fname = name + strlen(name);
+        do --fname; while(fname >= name && *fname!='/' && *fname!='\\');
+        fname++;
+        defformatstring(meshname, "packages/models/%s/%s.md5mesh", name, fname);
+        mdl.meshes = sharemeshes(path(meshname), NULL, 2.0);
+        if(!mdl.meshes) return false;
+        mdl.initanimparts();
+        mdl.initskins();
+        defformatstring(animname, "packages/models/%s/%s.md5anim", name, fname);
+        ((md5meshgroup *)mdl.meshes)->loadanim(path(animname));
+        return true;
+    }
+};
+
+skelcommands<md5> md5commands;
+
diff --git a/src/engine/menus.cpp b/src/engine/menus.cpp
new file mode 100644 (file)
index 0000000..f2d8e3b
--- /dev/null
@@ -0,0 +1,783 @@
+// menus.cpp: ingame menu system (also used for scores and serverlist)
+
+#include "engine.h"
+
+#define GUI_TITLE_COLOR  0xFFDD88
+#define GUI_BUTTON_COLOR 0xFFFFFF
+#define GUI_TEXT_COLOR   0xDDFFDD
+
+static vec menupos;
+static int menustart = 0;
+static g3d_gui *cgui = NULL;
+
+VAR(guitabnum, 1, 0, 0);
+
+struct menu : g3d_callback
+{
+    char *name, *header;
+    uint *contents, *init, *onclear;
+    bool showtab, keeptab;
+    int menutab;
+
+    menu() : name(NULL), header(NULL), contents(NULL), init(NULL), onclear(NULL), showtab(true), keeptab(false), menutab(1) {}
+
+    void gui(g3d_gui &g, bool firstpass)
+    {
+        cgui = &g;
+        guitabnum = menutab;
+        cgui->start(menustart, 0.03f, showtab ? &menutab : NULL);
+        if(showtab) cgui->tab(header ? header : name, GUI_TITLE_COLOR);
+        execute(contents);
+        cgui->end();
+        cgui = NULL;
+        guitabnum = 0;
+    }
+
+    virtual void clear() 
+    {
+        if(onclear) { freecode(onclear); onclear = NULL; }
+    }
+};
+
+struct delayedupdate
+{
+    enum
+    {
+        INT,
+        FLOAT,
+        STRING,
+        ACTION
+    } type;
+    ident *id;
+    union
+    {
+        int i;
+        float f;
+        char *s;
+    } val;
+    delayedupdate() : type(ACTION), id(NULL) { val.s = NULL; }
+    ~delayedupdate() { if(type == STRING || type == ACTION) DELETEA(val.s); }
+
+    void schedule(const char *s) { type = ACTION; val.s = newstring(s); }
+    void schedule(ident *var, int i) { type = INT; id = var; val.i = i; }
+    void schedule(ident *var, float f) { type = FLOAT; id = var; val.f = f; }
+    void schedule(ident *var, char *s) { type = STRING; id = var; val.s = newstring(s); }
+
+    int getint() const
+    {
+        switch(type)
+        {
+            case INT: return val.i;
+            case FLOAT: return int(val.f);
+            case STRING: return int(strtol(val.s, NULL, 0));
+            default: return 0;
+        }
+    }
+
+    float getfloat() const
+    {
+        switch(type)
+        {
+            case INT: return float(val.i);
+            case FLOAT: return val.f;
+            case STRING: return float(parsefloat(val.s));
+            default: return 0;
+        }
+    }
+   
+    const char *getstring() const
+    {
+        switch(type)
+        {
+            case INT: return intstr(val.i);
+            case FLOAT: return intstr(int(floor(val.f)));
+            case STRING: return val.s;
+            default: return "";
+        }
+    }
+
+    void run()
+    {
+        if(type == ACTION) { if(val.s) execute(val.s); }
+        else if(id) switch(id->type)
+        {
+            case ID_VAR: setvarchecked(id, getint()); break;
+            case ID_FVAR: setfvarchecked(id, getfloat()); break;
+            case ID_SVAR: setsvarchecked(id, getstring()); break;
+            case ID_ALIAS: alias(id->name, getstring()); break;
+        }
+    }
+};
+     
+static hashnameset<menu> guis;
+static vector<menu *> guistack;
+static vector<delayedupdate> updatelater;
+static bool shouldclearmenu = true, clearlater = false;
+
+VARP(menudistance,  16, 40,  256);
+VARP(menuautoclose, 32, 120, 4096);
+
+vec menuinfrontofplayer()
+{ 
+    vec dir;
+    vecfromyawpitch(camera1->yaw, 0, 1, 0, dir);
+    dir.mul(menudistance).add(camera1->o);
+    dir.z -= player->eyeheight-1;
+    return dir;
+}
+
+void popgui()
+{
+    menu *m = guistack.pop();
+    m->clear();
+}
+
+void removegui(menu *m)
+{
+    loopv(guistack) if(guistack[i]==m)
+    {
+        guistack.remove(i);
+        m->clear();
+        return;
+    }
+}    
+
+void pushgui(menu *m, int pos = -1)
+{
+    if(guistack.empty())
+    {
+        menupos = menuinfrontofplayer();
+        g3d_resetcursor();
+    }
+    if(pos < 0) guistack.add(m);
+    else guistack.insert(pos, m);
+    if(pos < 0 || pos==guistack.length()-1)
+    {
+        if(!m->keeptab) m->menutab = 1;
+        menustart = totalmillis;
+    }
+    if(m->init) execute(m->init);
+}
+
+void restoregui(int pos)
+{
+    int clear = guistack.length()-pos-1;
+    loopi(clear) popgui();
+    menustart = totalmillis;
+}
+
+void showgui(const char *name)
+{
+    menu *m = guis.access(name);
+    if(!m) return;
+    int pos = guistack.find(m);
+    if(pos<0) pushgui(m);
+    else restoregui(pos);
+}
+
+void hidegui(const char *name)
+{
+    menu *m = guis.access(name);
+    if(m) removegui(m);
+}
+int cleargui(int n)
+{
+    int clear = guistack.length();
+    if(mainmenu && !isconnected(true) && clear > 0 && guistack[0]->name && !strcmp(guistack[0]->name, "main")) 
+    {
+        clear--;
+        if(!clear) return 1;
+    }
+    if(n>0) clear = min(clear, n);
+    loopi(clear) popgui(); 
+    if(!guistack.empty()) restoregui(guistack.length()-1);
+    return clear;
+}
+
+void clearguis(int level = -1)
+{
+    if(level < 0) level = guistack.length();
+    loopvrev(guistack)
+    {
+       menu *m = guistack[i];
+       if(m->onclear)
+       {
+           uint *action = m->onclear;
+           m->onclear = NULL;
+           execute(action);
+           freecode(action);
+       }
+    }
+    cleargui(level);
+}
+
+void guionclear(char *action)
+{
+    if(guistack.empty()) return;
+    menu *m = guistack.last();
+    if(m->onclear) { freecode(m->onclear); m->onclear = NULL; } 
+    if(action[0]) m->onclear = compilecode(action);
+}
+
+void guistayopen(uint *contents)
+{
+    bool oldclearmenu = shouldclearmenu;
+    shouldclearmenu = false;
+    execute(contents);
+    shouldclearmenu = oldclearmenu;
+}
+
+void guinoautotab(uint *contents)
+{
+    if(!cgui) return;
+    bool oldval = cgui->allowautotab(false);
+    execute(contents);
+    cgui->allowautotab(oldval);
+}
+
+void guimerge(uint *contents)
+{
+    if(!cgui) return;
+    bool oldval = cgui->mergehits(true);
+    execute(contents);
+    cgui->mergehits(oldval);
+}
+
+//@DOC name and icon are optional
+void guibutton(char *name, char *action, char *icon)
+{
+    if(!cgui) return;
+    bool hideicon = !strcmp(icon, "0");
+    int ret = cgui->button(name, GUI_BUTTON_COLOR, hideicon ? NULL : (icon[0] ? icon : (strstr(action, "showgui") ? "menu" : "action")));
+    if(ret&G3D_UP) 
+    {
+        updatelater.add().schedule(action[0] ? action : name);
+        if(shouldclearmenu) clearlater = true;
+    }
+    else if(ret&G3D_ROLLOVER)
+    {
+        alias("guirollovername", name);
+        alias("guirolloveraction", action);
+    }
+}
+
+void guiimage(char *path, char *action, float *scale, int *overlaid, char *alt, char *title)
+{
+    if(!cgui) return;
+    Texture *t = textureload(path, 0, true, false);
+    if(t==notexture)
+    {
+        if(alt[0]) t = textureload(alt, 0, true, false);
+        if(t==notexture) return;
+    }
+    int ret = cgui->image(t, *scale, *overlaid!=0 ? title : NULL);
+    if(ret&G3D_UP)
+    {
+        if(*action)
+        {
+            updatelater.add().schedule(action);
+            if(shouldclearmenu) clearlater = true;
+        }
+    }
+    else if(ret&G3D_ROLLOVER)
+    {
+        alias("guirolloverimgpath", path);
+        alias("guirolloverimgaction", action);
+    }
+}
+
+void guicolor(int *color)
+{
+    if(cgui) 
+    {   
+        defformatstring(desc, "0x%06X", *color);
+        cgui->text(desc, *color, NULL);
+    }
+}
+
+void guitextbox(char *text, int *width, int *height, int *color)
+{
+    if(cgui && text[0]) cgui->textbox(text, *width ? *width : 12, *height ? *height : 1, *color ? *color : 0xFFFFFF);
+}
+
+void guitext(char *name, char *icon)
+{
+    bool hideicon = !strcmp(icon, "0");
+    if(cgui) cgui->text(name, !hideicon && icon[0] ? GUI_BUTTON_COLOR : GUI_TEXT_COLOR, hideicon ? NULL : (icon[0] ? icon : "info"));
+}
+
+void guititle(char *name)
+{
+    if(cgui) cgui->title(name, GUI_TITLE_COLOR);
+}
+
+void guitab(char *name)
+{
+    if(cgui) cgui->tab(name, GUI_TITLE_COLOR);
+}
+
+void guibar()
+{
+    if(cgui) cgui->separator();
+}
+
+void guistrut(float *strut, int *alt)
+{
+    if(cgui)
+    {
+        if(*alt) cgui->strut(*strut); else cgui->space(*strut);
+    }
+}
+
+void guispring(int *weight)
+{
+    if(cgui) cgui->spring(max(*weight, 1));
+}
+
+void guicolumn(int *col)
+{
+    if(cgui) cgui->column(*col);
+}
+
+template<class T> static void updateval(char *var, T val, char *onchange)
+{
+    ident *id = writeident(var);
+    updatelater.add().schedule(id, val);
+    if(onchange[0]) updatelater.add().schedule(onchange);
+}
+
+static int getval(char *var)
+{
+    ident *id = readident(var);
+    if(!id) return 0;
+    switch(id->type)
+    {
+        case ID_VAR: return *id->storage.i;
+        case ID_FVAR: return int(*id->storage.f);
+        case ID_SVAR: return parseint(*id->storage.s);
+        case ID_ALIAS: return id->getint();
+        default: return 0;
+    }
+}
+
+static float getfval(char *var)
+{
+    ident *id = readident(var);
+    if(!id) return 0;
+    switch(id->type)
+    {
+        case ID_VAR: return *id->storage.i;
+        case ID_FVAR: return *id->storage.f;
+        case ID_SVAR: return parsefloat(*id->storage.s);
+        case ID_ALIAS: return id->getfloat();
+        default: return 0;
+    }
+}
+
+static const char *getsval(char *var)
+{
+    ident *id = readident(var);
+    if(!id) return "";
+    switch(id->type)
+    {
+        case ID_VAR: return intstr(*id->storage.i);
+        case ID_FVAR: return floatstr(*id->storage.f);
+        case ID_SVAR: return *id->storage.s;
+        case ID_ALIAS: return id->getstr();
+        default: return "";
+    }
+}
+
+void guislider(char *var, int *min, int *max, char *onchange)
+{
+       if(!cgui) return;
+    int oldval = getval(var), val = oldval, vmin = *max > INT_MIN ? *min : getvarmin(var), vmax = *max > INT_MIN ? *max : getvarmax(var);
+    cgui->slider(val, vmin, vmax, GUI_TITLE_COLOR);
+    if(val != oldval) updateval(var, val, onchange);
+}
+
+void guilistslider(char *var, char *list, char *onchange)
+{
+    if(!cgui) return;
+    vector<int> vals;
+    list += strspn(list, "\n\t ");
+    while(*list)
+    {
+        vals.add(parseint(list));
+        list += strcspn(list, "\n\t \0");
+        list += strspn(list, "\n\t ");
+    }
+    if(vals.empty()) return;
+    int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset;
+    loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; }
+    cgui->slider(offset, 0, vals.length()-1, GUI_TITLE_COLOR, intstr(val));
+    if(offset != oldoffset) updateval(var, vals[offset], onchange);
+}
+
+void guinameslider(char *var, char *names, char *list, char *onchange)
+{
+    if(!cgui) return;
+    vector<int> vals;
+    list += strspn(list, "\n\t ");
+    while(*list)
+    {
+        vals.add(parseint(list));
+        list += strcspn(list, "\n\t \0");
+        list += strspn(list, "\n\t ");
+    }
+    if(vals.empty()) return;
+    int val = getval(var), oldoffset = vals.length()-1, offset = oldoffset;
+    loopv(vals) if(val <= vals[i]) { oldoffset = offset = i; break; }
+    char *label = indexlist(names, offset);
+    cgui->slider(offset, 0, vals.length()-1, GUI_TITLE_COLOR, label);
+    if(offset != oldoffset) updateval(var, vals[offset], onchange);
+    delete[] label;
+}
+
+void guicheckbox(char *name, char *var, float *on, float *off, char *onchange)
+{
+    bool enabled = getfval(var)!=*off;
+    if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "checkbox_on" : "checkbox_off")&G3D_UP)
+    {
+        updateval(var, enabled ? *off : (*on || *off ? *on : 1.0f), onchange);
+    }
+}
+
+void guiradio(char *name, char *var, float *n, char *onchange)
+{
+    bool enabled = getfval(var)==*n;
+    if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "radio_on" : "radio_off")&G3D_UP)
+    {
+        if(!enabled) updateval(var, *n, onchange);
+    }
+}
+
+void guibitfield(char *name, char *var, int *mask, char *onchange)
+{
+    int val = getval(var);
+    bool enabled = (val & *mask) != 0;
+    if(cgui && cgui->button(name, GUI_BUTTON_COLOR, enabled ? "checkbox_on" : "checkbox_off")&G3D_UP)
+    {
+        updateval(var, enabled ? val & ~*mask : val | *mask, onchange);
+    }
+}
+
+//-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width
+void guifield(char *var, int *maxlength, char *onchange)
+{   
+    if(!cgui) return;
+    const char *initval = getsval(var);
+       char *result = cgui->field(var, GUI_BUTTON_COLOR, *maxlength ? *maxlength : 12, 0, initval);
+    if(result) updateval(var, result, onchange); 
+}
+
+//-ve maxlength indicates a wrapped text field of any (approx 260 chars) length, |maxlength| is the field width
+void guieditor(char *name, int *maxlength, int *height, int *mode)
+{
+    if(!cgui) return;
+    cgui->field(name, GUI_BUTTON_COLOR, *maxlength ? *maxlength : 12, *height, NULL, *mode<=0 ? EDITORFOREVER : *mode);
+    //returns a non-NULL pointer (the currentline) when the user commits, could then manipulate via text* commands
+}
+
+//-ve length indicates a wrapped text field of any (approx 260 chars) length, |length| is the field width
+void guikeyfield(char *var, int *maxlength, char *onchange)
+{
+    if(!cgui) return;
+    const char *initval = getsval(var);
+    char *result = cgui->keyfield(var, GUI_BUTTON_COLOR, *maxlength ? *maxlength : -8, 0, initval);
+    if(result) updateval(var, result, onchange);
+}
+
+//use text<action> to do more...
+
+
+void guilist(uint *contents)
+{
+    if(!cgui) return;
+    cgui->pushlist();
+    execute(contents);
+    cgui->poplist();
+}
+
+void guialign(int *align, uint *contents)
+{
+    if(!cgui) return;
+    cgui->pushlist();
+    if(*align >= 0) cgui->spring();
+    execute(contents);
+    if(*align == 0) cgui->spring(); 
+    cgui->poplist();
+}
+
+void newgui(char *name, char *contents, char *header, char *init)
+{
+    menu *m = guis.access(name);
+    if(!m)
+    {
+        name = newstring(name);
+        m = &guis[name];
+        m->name = name;
+    }
+    else
+    {
+        DELETEA(m->header);
+        freecode(m->contents);
+        freecode(m->init);
+    }
+    if(header && header[0])
+    {
+        char *end = NULL;
+        int val = strtol(header, &end, 0);
+        if(end && !*end)
+        {
+            m->header = NULL;
+            m->showtab = val != 0;
+        }
+        else
+        {
+            m->header = newstring(header);
+            m->showtab = true;
+        }
+    }
+    else
+    {
+        m->header = NULL;
+        m->showtab = true;
+    }
+    m->contents = compilecode(contents);
+    m->init = init && init[0] ? compilecode(init) : NULL;
+}
+
+menu *guiserversmenu = NULL;
+
+void guiservers(uint *header, int *pagemin, int *pagemax)
+{
+    extern const char *showservers(g3d_gui *cgui, uint *header, int pagemin, int pagemax);
+    if(cgui) 
+    {
+        const char *command = showservers(cgui, header, *pagemin, *pagemax > 0 ? *pagemax : INT_MAX);
+        if(command)
+        {
+            updatelater.add().schedule(command);
+            if(shouldclearmenu) clearlater = true;
+            guiserversmenu = clearlater || guistack.empty() ? NULL : guistack.last();
+        }
+    }
+}
+
+void notifywelcome()
+{
+    if(guiserversmenu)
+    {
+        if(guistack.length() && guistack.last() == guiserversmenu) clearguis();
+        guiserversmenu = NULL;
+    }
+}
+COMMAND(newgui, "ssss");
+COMMAND(guibutton, "sss");
+COMMAND(guitext, "ss");
+COMMAND(guiservers, "eii");
+ICOMMAND(cleargui, "i", (int *n), intret(cleargui(*n)));
+COMMAND(showgui, "s");
+COMMAND(hidegui, "s");
+COMMAND(guionclear, "s");
+COMMAND(guistayopen, "e");
+COMMAND(guinoautotab, "e");
+COMMAND(guimerge, "e");
+ICOMMAND(guikeeptab, "b", (int *keeptab), if(guistack.length()) guistack.last()->keeptab = *keeptab!=0);
+COMMAND(guilist, "e");
+COMMAND(guialign, "ie");
+COMMAND(guititle, "s");
+COMMAND(guibar,"");
+COMMAND(guistrut,"fi");
+COMMAND(guispring, "i");
+COMMAND(guicolumn, "i");
+COMMAND(guiimage,"ssfiss");
+COMMAND(guislider,"sbbs");
+COMMAND(guilistslider, "sss");
+COMMAND(guinameslider, "ssss");
+COMMAND(guiradio,"ssfs");
+COMMAND(guibitfield, "ssis");
+COMMAND(guicheckbox, "ssffs");
+COMMAND(guitab, "s");
+COMMAND(guifield, "sis");
+COMMAND(guikeyfield, "sis");
+COMMAND(guieditor, "siii");
+COMMAND(guicolor, "i");
+COMMAND(guitextbox, "siii");
+
+void guiplayerpreview(int *model, int *team, int *weap, char *action, float *scale, int *overlaid, char *title)
+{
+    if(!cgui) return;
+    int ret = cgui->playerpreview(*model, *team, *weap, *scale, *overlaid!=0 ? title : NULL);
+    if(ret&G3D_UP)
+    {
+        if(*action)
+        {
+            updatelater.add().schedule(action);
+            if(shouldclearmenu) clearlater = true;
+        }
+    }
+}
+COMMAND(guiplayerpreview, "iiisfis");
+
+void guimodelpreview(char *model, char *animspec, char *action, float *scale, int *overlaid, char *title, int *throttle)
+{
+    if(!cgui) return;
+    int anim = ANIM_ALL;
+    if(animspec[0])
+    {
+        if(isdigit(animspec[0])) 
+        {
+            anim = parseint(animspec);
+            if(anim >= 0) anim %= ANIM_INDEX;
+            else anim = ANIM_ALL;
+        }
+        else
+        {
+            vector<int> anims;
+            findanims(animspec, anims);
+            if(anims.length()) anim = anims[0];
+        }
+    }
+    int ret = cgui->modelpreview(model, anim|ANIM_LOOP, *scale, *overlaid!=0 ? title : NULL, *throttle!=0);
+    if(ret&G3D_UP)
+    {
+        if(*action)
+        {
+            updatelater.add().schedule(action);
+            if(shouldclearmenu) clearlater = true;
+        }
+    }
+    else if(ret&G3D_ROLLOVER)
+    {
+        alias("guirolloverpreviewname", model);
+        alias("guirolloverpreviewaction", action);
+    }
+}
+COMMAND(guimodelpreview, "sssfisi");
+
+void guiprefabpreview(char *prefab, int *color, char *action, float *scale, int *overlaid, char *title, int *throttle)
+{
+    if(!cgui) return;
+    int ret = cgui->prefabpreview(prefab, vec::hexcolor(*color), *scale, *overlaid!=0 ? title : NULL, *throttle!=0);
+    if(ret&G3D_UP)
+    {
+        if(*action)
+        {   
+            updatelater.add().schedule(action);
+            if(shouldclearmenu) clearlater = true;
+        }
+    }
+    else if(ret&G3D_ROLLOVER)
+    {
+        alias("guirolloverpreviewname", prefab);
+        alias("guirolloverpreviewaction", action);
+    }
+}
+COMMAND(guiprefabpreview, "sisfisi");
+
+struct change
+{
+    int type;
+    const char *desc;
+
+    change() {}
+    change(int type, const char *desc) : type(type), desc(desc) {}
+};
+static vector<change> needsapply;
+
+static struct applymenu : menu
+{
+    void gui(g3d_gui &g, bool firstpass)
+    {
+        if(guistack.empty()) return;
+        g.start(menustart, 0.03f);
+        g.text("the following settings have changed:", GUI_TEXT_COLOR, "info");
+        loopv(needsapply) g.text(needsapply[i].desc, GUI_TEXT_COLOR, "info");
+        g.separator();
+        g.text("apply changes now?", GUI_TEXT_COLOR, "info");
+        if(g.button("yes", GUI_BUTTON_COLOR, "action")&G3D_UP)
+        {
+            int changetypes = 0;
+            loopv(needsapply) changetypes |= needsapply[i].type;
+            if(changetypes&CHANGE_GFX) updatelater.add().schedule("resetgl");
+            if(changetypes&CHANGE_SOUND) updatelater.add().schedule("resetsound");
+            clearlater = true;
+        }
+        if(g.button("no", GUI_BUTTON_COLOR, "action")&G3D_UP)
+            clearlater = true;
+        g.end();
+    }
+
+    void clear()
+    {
+        menu::clear();
+        needsapply.shrink(0);
+    }
+} applymenu;
+
+VARP(applydialog, 0, 1, 1);
+
+static bool processingmenu = false;
+
+void addchange(const char *desc, int type)
+{
+    if(!applydialog) return;
+    loopv(needsapply) if(!strcmp(needsapply[i].desc, desc)) return;
+    needsapply.add(change(type, desc));
+    if(needsapply.length() && guistack.find(&applymenu) < 0)
+        pushgui(&applymenu, processingmenu ? max(guistack.length()-1, 0) : -1);
+}
+
+void clearchanges(int type)
+{
+    loopv(needsapply)
+    {
+        if(needsapply[i].type&type)
+        {
+            needsapply[i].type &= ~type;
+            if(!needsapply[i].type) needsapply.remove(i--);
+        }
+    }
+    if(needsapply.empty()) removegui(&applymenu);
+}
+
+void menuprocess()
+{
+    processingmenu = true;
+    int wasmain = mainmenu, level = guistack.length();
+    loopv(updatelater) updatelater[i].run();
+    updatelater.shrink(0);
+    if(wasmain > mainmenu || clearlater)
+    {
+        if(wasmain > mainmenu || level==guistack.length()) clearguis(level); 
+        clearlater = false;
+    }
+    if(mainmenu && !isconnected(true) && guistack.empty()) showgui("main");
+    processingmenu = false;
+}
+
+VAR(mainmenu, 1, 1, 0);
+
+void clearmainmenu()
+{
+    if(mainmenu && isconnected())
+    {
+        mainmenu = 0;
+        if(!processingmenu) cleargui();
+    }
+}
+
+void g3d_mainmenu()
+{
+    if(!guistack.empty()) 
+    {   
+        extern int usegui2d;
+        if(!mainmenu && !usegui2d && camera1->o.dist(menupos) > menuautoclose) cleargui();
+        else g3d_addgui(guistack.last(), menupos, GUI_2D | GUI_FOLLOW);
+    }
+}
+
diff --git a/src/engine/model.h b/src/engine/model.h
new file mode 100644 (file)
index 0000000..97264bd
--- /dev/null
@@ -0,0 +1,90 @@
+enum { MDL_MD2 = 0, MDL_MD3, MDL_MD5, MDL_OBJ, MDL_SMD, MDL_IQM, NUMMODELTYPES };
+
+struct model
+{
+    char *name;
+    float spinyaw, spinpitch, offsetyaw, offsetpitch;
+    bool collide, ellipsecollide, shadow, alphadepth, depthoffset;
+    float scale;
+    vec translate;
+    BIH *bih;
+    vec bbcenter, bbradius, bbextend, collidecenter, collideradius;
+    float rejectradius, eyeheight, collidexyradius, collideheight;
+    int batch;
+
+    model(const char *name) : name(name ? newstring(name) : NULL), spinyaw(0), spinpitch(0), offsetyaw(0), offsetpitch(0), collide(true), ellipsecollide(false), shadow(true), alphadepth(true), depthoffset(false), scale(1.0f), translate(0, 0, 0), bih(0), bbcenter(0, 0, 0), bbradius(-1, -1, -1), bbextend(0, 0, 0), collidecenter(0, 0, 0), collideradius(-1, -1, -1), rejectradius(-1), eyeheight(0.9f), collidexyradius(0), collideheight(0), batch(-1) {}
+    virtual ~model() { DELETEA(name); DELETEP(bih); }
+    virtual void calcbb(vec &center, vec &radius) = 0;
+    virtual void render(int anim, int basetime, int basetime2, const vec &o, float yaw, float pitch, dynent *d, modelattach *a = NULL, const vec &color = vec(0, 0, 0), const vec &dir = vec(0, 0, 0), float transparent = 1) = 0;
+    virtual bool load() = 0;
+    virtual int type() const = 0;
+    virtual BIH *setBIH() { return 0; }
+    virtual bool envmapped() { return false; }
+    virtual bool skeletal() const { return false; }
+
+    virtual void setshader(Shader *shader) {}
+    virtual void setenvmap(float envmapmin, float envmapmax, Texture *envmap) {}
+    virtual void setspec(float spec) {}
+    virtual void setambient(float ambient) {}
+    virtual void setglow(float glow, float glowdelta, float glowpulse) {}
+    virtual void setglare(float specglare, float glowglare) {}
+    virtual void setalphatest(float alpha) {}
+    virtual void setalphablend(bool blend) {}
+    virtual void setfullbright(float fullbright) {}
+    virtual void setcullface(bool cullface) {}
+
+    virtual void preloadBIH() { if(!bih) setBIH(); }
+    virtual void preloadshaders(bool force = false) {}
+    virtual void preloadmeshes() {}
+    virtual void cleanup() {}
+
+    virtual void startrender() {}
+    virtual void endrender() {}
+
+    void boundbox(vec &center, vec &radius)
+    {
+        if(bbradius.x < 0)
+        {
+            calcbb(bbcenter, bbradius);
+            bbradius.add(bbextend);
+        }
+        center = bbcenter;
+        radius = bbradius;
+    }
+
+    float collisionbox(vec &center, vec &radius)
+    {
+        if(collideradius.x < 0)
+        {
+            boundbox(collidecenter, collideradius);
+            if(collidexyradius)
+            {
+                collidecenter.x = collidecenter.y = 0;
+                collideradius.x = collideradius.y = collidexyradius;
+            }
+            if(collideheight)
+            {
+                collidecenter.z = collideradius.z = collideheight/2;
+            }
+            rejectradius = vec(collidecenter).abs().add(collideradius).magnitude();
+        }
+        center = collidecenter;
+        radius = collideradius;
+        return rejectradius;
+    }
+
+    float boundsphere(vec &center)
+    {
+        vec radius;
+        boundbox(center, radius);
+        return radius.magnitude();
+    }
+
+    float above()
+    {
+        vec center, radius;
+        boundbox(center, radius);
+        return center.z+radius.z;
+    }
+};
+
diff --git a/src/engine/movie.cpp b/src/engine/movie.cpp
new file mode 100644 (file)
index 0000000..25cb491
--- /dev/null
@@ -0,0 +1,1157 @@
+// Feedback on playing videos:
+//   quicktime - ok
+//   vlc - ok
+//   xine - ok
+//   mplayer - ok
+//   totem - ok
+//   avidemux - ok - 3Apr09-RockKeyman:had to swap UV channels as it showed up blue
+//   kino - ok
+
+#include "engine.h"
+#include "SDL_mixer.h"
+
+VAR(dbgmovie, 0, 0, 1);
+
+struct aviindexentry
+{
+    int frame, type, size;
+    uint offset;
+
+    aviindexentry() {}
+    aviindexentry(int frame, int type, int size, uint offset) : frame(frame), type(type), size(size), offset(offset) {}
+};
+
+struct avisegmentinfo
+{
+    stream::offset offset, videoindexoffset, soundindexoffset;
+    int firstindex;
+    uint videoindexsize, soundindexsize, indexframes, videoframes, soundframes;
+    
+    avisegmentinfo() {}
+    avisegmentinfo(stream::offset offset, int firstindex) : offset(offset), videoindexoffset(0), soundindexoffset(0), firstindex(firstindex), videoindexsize(0), soundindexsize(0), indexframes(0), videoframes(0), soundframes(0) {}
+};
+
+struct aviwriter
+{
+    stream *f;
+    uchar *yuv;
+    uint videoframes;
+    stream::offset totalsize;
+    const uint videow, videoh, videofps;
+    string filename;
+    int soundfrequency, soundchannels;
+    Uint16 soundformat;
+    
+    vector<aviindexentry> index;
+    vector<avisegmentinfo> segments;
+    
+    stream::offset fileframesoffset, fileextframesoffset, filevideooffset, filesoundoffset, superindexvideooffset, superindexsoundoffset;
+    
+    enum { MAX_CHUNK_DEPTH = 16, MAX_SUPER_INDEX = 1024 };
+    stream::offset chunkoffsets[MAX_CHUNK_DEPTH];
+    int chunkdepth;
+    
+    aviindexentry &addindex(int frame, int type, int size)
+    {
+        avisegmentinfo &seg = segments.last();
+        int i = index.length();
+        while(--i >= seg.firstindex)
+        {
+            aviindexentry &e = index[i];
+            if(frame > e.frame || (frame == e.frame && type <= e.type)) break;
+        }
+        return index.insert(i + 1, aviindexentry(frame, type, size, uint(totalsize - chunkoffsets[chunkdepth])));
+    }
+    
+    double filespaceguess() 
+    {
+        return double(totalsize);
+    }
+       
+    void startchunk(const char *fcc, uint size = 0)
+    {
+        f->write(fcc, 4);
+        f->putlil<uint>(size);
+        totalsize += 4 + 4;
+        chunkoffsets[++chunkdepth] = totalsize;
+        totalsize += size;
+    }
+    
+    void listchunk(const char *fcc, const char *lfcc)
+    {
+        startchunk(fcc);
+        f->write(lfcc, 4);
+        totalsize += 4;
+    }
+    
+    void endchunk()
+    {
+        ASSERT(chunkdepth >= 0);
+        --chunkdepth;
+    }
+
+    void endlistchunk()
+    {
+        ASSERT(chunkdepth >= 0);
+        int size = int(totalsize - chunkoffsets[chunkdepth]);
+        f->seek(-4 - size, SEEK_CUR);
+        f->putlil(size);
+        f->seek(0, SEEK_END);
+        if(size & 1) { f->putchar(0x00); totalsize++; }
+        endchunk();
+    }
+        
+    void writechunk(const char *fcc, const void *data, uint len) // simplify startchunk()/endchunk() to avoid f->seek()
+    {
+        f->write(fcc, 4);
+        f->putlil(len);
+        f->write(data, len);
+        totalsize += 4 + 4 + len;
+        if(len & 1) { f->putchar(0x00); totalsize++; }
+    }
+    
+    void close()
+    {
+        if(!f) return;
+        flushsegment();
+
+        uint soundindexes = 0, videoindexes = 0, soundframes = 0, videoframes = 0, indexframes = 0;
+        loopv(segments)
+        {
+            avisegmentinfo &seg = segments[i];
+            if(seg.soundindexsize) soundindexes++;
+            videoindexes++;
+            soundframes += seg.soundframes;
+            videoframes += seg.videoframes;
+            indexframes += seg.indexframes;
+        }
+        if(dbgmovie) conoutf(CON_DEBUG, "fileframes: sound=%d, video=%d+%d(dups)\n", soundframes, videoframes, indexframes-videoframes);
+        f->seek(fileframesoffset, SEEK_SET);
+        f->putlil<uint>(segments[0].indexframes);
+        f->seek(filevideooffset, SEEK_SET);
+        f->putlil<uint>(segments[0].videoframes);
+        if(segments[0].soundframes > 0)
+        {
+            f->seek(filesoundoffset, SEEK_SET);
+            f->putlil<uint>(segments[0].soundframes);
+        }
+        f->seek(fileextframesoffset, SEEK_SET);
+        f->putlil<uint>(indexframes); // total video frames
+
+        f->seek(superindexvideooffset + 2 + 2, SEEK_SET);
+        f->putlil<uint>(videoindexes);
+        f->seek(superindexvideooffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET);
+        loopv(segments)
+        {
+            avisegmentinfo &seg = segments[i];
+            f->putlil<uint>(seg.videoindexoffset&stream::offset(0xFFFFFFFFU));
+            f->putlil<uint>(seg.videoindexoffset>>32);
+            f->putlil<uint>(seg.videoindexsize);
+            f->putlil<uint>(seg.indexframes);
+        }
+
+        if(soundindexes > 0)
+        {
+            f->seek(superindexsoundoffset + 2 + 2, SEEK_SET);
+            f->putlil<uint>(soundindexes);
+            f->seek(superindexsoundoffset + 2 + 2 + 4 + 4 + 4 + 4 + 4, SEEK_SET);
+            loopv(segments)
+            {
+                avisegmentinfo &seg = segments[i];
+                if(!seg.soundindexsize) continue;
+                f->putlil<uint>(seg.soundindexoffset&stream::offset(0xFFFFFFFFU));
+                f->putlil<uint>(seg.soundindexoffset>>32);
+                f->putlil<uint>(seg.soundindexsize);
+                f->putlil<uint>(seg.soundframes);
+            }
+        }
+
+        f->seek(0, SEEK_END);
+        
+        DELETEP(f);
+    }
+    
+    aviwriter(const char *name, uint w, uint h, uint fps, bool sound) : f(NULL), yuv(NULL), videoframes(0), totalsize(0), videow(w&~1), videoh(h&~1), videofps(fps), soundfrequency(0),soundchannels(0),soundformat(0)
+    {
+        copystring(filename, name);
+        path(filename);
+        if(!strrchr(filename, '.')) concatstring(filename, ".avi");
+        
+        extern bool nosound; // sound.cpp
+        if(sound && !nosound) 
+        {
+            Mix_QuerySpec(&soundfrequency, &soundformat, &soundchannels);
+            const char *desc;
+            switch(soundformat)
+            {
+                case AUDIO_U8:     desc = "u8"; break;
+                case AUDIO_S8:     desc = "s8"; break;
+                case AUDIO_U16LSB: desc = "u16l"; break;
+                case AUDIO_U16MSB: desc = "u16b"; break;
+                case AUDIO_S16LSB: desc = "s16l"; break;
+                case AUDIO_S16MSB: desc = "s16b"; break;
+                default:           desc = "unkn";
+            }
+            if(dbgmovie) conoutf(CON_DEBUG, "soundspec: %dhz %s x %d", soundfrequency, desc, soundchannels);
+        }
+    }
+    
+    ~aviwriter()
+    {
+        close();
+        if(yuv) delete [] yuv;
+    }
+    
+    bool open()
+    {
+        f = openfile(filename, "wb");
+        if(!f) return false;
+        
+        chunkdepth = -1;
+        
+        listchunk("RIFF", "AVI ");
+        
+        listchunk("LIST", "hdrl");
+        
+        startchunk("avih", 56);
+        f->putlil<uint>(1000000 / videofps); // microsecsperframe
+        f->putlil<uint>(0); // maxbytespersec
+        f->putlil<uint>(0); // reserved
+        f->putlil<uint>(0x10 | 0x20); // flags - hasindex|mustuseindex
+        fileframesoffset = f->tell();
+        f->putlil<uint>(0); // totalvideoframes
+        f->putlil<uint>(0); // initialframes
+        f->putlil<uint>(soundfrequency > 0 ? 2 : 1); // streams
+        f->putlil<uint>(0); // buffersize
+        f->putlil<uint>(videow); // video width
+        f->putlil<uint>(videoh); // video height
+        loopi(4) f->putlil<uint>(0); // reserved
+        endchunk(); // avih
+        
+        listchunk("LIST", "strl");
+        
+        startchunk("strh", 56);
+        f->write("vids", 4); // fcctype
+        f->write("I420", 4); // fcchandler
+        f->putlil<uint>(0); // flags
+        f->putlil<uint>(0); // priority
+        f->putlil<uint>(0); // initialframes
+        f->putlil<uint>(1); // scale
+        f->putlil<uint>(videofps); // rate
+        f->putlil<uint>(0); // start
+        filevideooffset = f->tell();
+        f->putlil<uint>(0); // length
+        f->putlil<uint>(videow*videoh*3/2); // suggested buffersize
+        f->putlil<uint>(0); // quality
+        f->putlil<uint>(0); // samplesize
+        f->putlil<ushort>(0); // frame left
+        f->putlil<ushort>(0); // frame top
+        f->putlil<ushort>(videow); // frame right
+        f->putlil<ushort>(videoh); // frame bottom
+        endchunk(); // strh
+        
+        startchunk("strf", 40);
+        f->putlil<uint>(40); //headersize
+        f->putlil<uint>(videow); // width
+        f->putlil<uint>(videoh); // height
+        f->putlil<ushort>(3); // planes
+        f->putlil<ushort>(12); // bitcount
+        f->write("I420", 4); // compression
+        f->putlil<uint>(videow*videoh*3/2); // imagesize
+        f->putlil<uint>(0); // xres
+        f->putlil<uint>(0); // yres;
+        f->putlil<uint>(0); // colorsused
+        f->putlil<uint>(0); // colorsrequired
+        endchunk(); // strf
+       
+        startchunk("indx", 24 + 16*MAX_SUPER_INDEX);
+        superindexvideooffset = f->tell();
+        f->putlil<ushort>(4); // longs per entry
+        f->putlil<ushort>(0); // index of indexes
+        f->putlil<uint>(0); // entries in use
+        f->write("00dc", 4); // chunk id
+        f->putlil<uint>(0); // reserved 1
+        f->putlil<uint>(0); // reserved 2
+        f->putlil<uint>(0); // reserved 3
+        loopi(MAX_SUPER_INDEX)
+        {
+            f->putlil<uint>(0); // offset low
+            f->putlil<uint>(0); // offset high
+            f->putlil<uint>(0); // size
+            f->putlil<uint>(0); // duration
+        }
+        endchunk(); // indx
+
+        startchunk("vprp", 68);
+        f->putlil<uint>(0); // video format token
+        f->putlil<uint>(0); // video standard
+        f->putlil<uint>(videofps); // vertical refresh rate
+        f->putlil<uint>(videow); // horizontal total
+        f->putlil<uint>(videoh); // vertical total
+        int gcd = screenw, rem = screenh;
+        while(rem > 0) { gcd %= rem; swap(gcd, rem); }
+        f->putlil<ushort>(screenh/gcd); // aspect denominator
+        f->putlil<ushort>(screenw/gcd); // aspect numerator
+        f->putlil<uint>(videow); // frame width
+        f->putlil<uint>(videoh); // frame height
+        f->putlil<uint>(1); // fields per frame
+        f->putlil<uint>(videoh); // compressed bitmap height
+        f->putlil<uint>(videow); // compressed bitmap width
+        f->putlil<uint>(videoh); // valid bitmap height
+        f->putlil<uint>(videow); // valid bitmap width
+        f->putlil<uint>(0); // valid bitmap x offset
+        f->putlil<uint>(0); // valid bitmap y offset
+        f->putlil<uint>(0); // video x offset
+        f->putlil<uint>(0); // video y start
+        endchunk(); // vprp
+
+        endlistchunk(); // LIST strl
+                
+        if(soundfrequency > 0)
+        {
+            const int bps = (soundformat==AUDIO_U8 || soundformat == AUDIO_S8) ? 1 : 2;
+            
+            listchunk("LIST", "strl");
+            
+            startchunk("strh", 56);
+            f->write("auds", 4); // fcctype
+            f->putlil<uint>(1); // fcchandler - normally 4cc, but audio is a special case
+            f->putlil<uint>(0); // flags
+            f->putlil<uint>(0); // priority
+            f->putlil<uint>(0); // initialframes
+            f->putlil<uint>(1); // scale
+            f->putlil<uint>(soundfrequency); // rate
+            f->putlil<uint>(0); // start
+            filesoundoffset = f->tell();
+            f->putlil<uint>(0); // length
+            f->putlil<uint>(soundfrequency*bps*soundchannels/2); // suggested buffer size (this is a half second)
+            f->putlil<uint>(0); // quality
+            f->putlil<uint>(bps*soundchannels); // samplesize
+            f->putlil<ushort>(0); // frame left
+            f->putlil<ushort>(0); // frame top
+            f->putlil<ushort>(0); // frame right
+            f->putlil<ushort>(0); // frame bottom
+            endchunk(); // strh
+            
+            startchunk("strf", 18);
+            f->putlil<ushort>(1); // format (uncompressed PCM)
+            f->putlil<ushort>(soundchannels); // channels
+            f->putlil<uint>(soundfrequency); // sampleframes per second
+            f->putlil<uint>(soundfrequency*bps*soundchannels); // average bytes per second
+            f->putlil<ushort>(bps*soundchannels); // block align <-- guess
+            f->putlil<ushort>(bps*8); // bits per sample
+            f->putlil<ushort>(0); // size
+            endchunk(); //strf
+
+            startchunk("indx", 24 + 16*MAX_SUPER_INDEX);
+            superindexsoundoffset = f->tell();
+            f->putlil<ushort>(4); // longs per entry
+            f->putlil<ushort>(0); // index of indexes
+            f->putlil<uint>(0); // entries in use
+            f->write("01wb", 4); // chunk id
+            f->putlil<uint>(0); // reserved 1
+            f->putlil<uint>(0); // reserved 2
+            f->putlil<uint>(0); // reserved 3
+            loopi(MAX_SUPER_INDEX)
+            {
+                f->putlil<uint>(0); // offset low
+                f->putlil<uint>(0); // offset high
+                f->putlil<uint>(0); // size
+                f->putlil<uint>(0); // duration
+            }
+            endchunk(); // indx
+
+            endlistchunk(); // LIST strl
+        }
+       
+        listchunk("LIST", "odml");
+        startchunk("dmlh", 4);
+        fileextframesoffset = f->tell();
+        f->putlil<uint>(0);
+        endchunk(); // dmlh
+        endlistchunk(); // LIST odml
+
+        listchunk("LIST", "INFO");
+        const char *software = "Cube 2: Sauerbraten";
+        writechunk("ISFT", software, strlen(software)+1);
+        endlistchunk(); // LIST INFO
+        
+        endlistchunk(); // LIST hdrl
+        
+        nextsegment();
+        return true;
+    }
+  
+    static inline void boxsample(const uchar *src, const uint stride, 
+                                 const uint area, const uint w, uint h, 
+                                 const uint xlow, const uint xhigh, const uint ylow, const uint yhigh,
+                                 uint &bdst, uint &gdst, uint &rdst)
+    {
+        const uchar *end = &src[w<<2];
+        uint bt = 0, gt = 0, rt = 0;
+        for(const uchar *cur = &src[4]; cur < end; cur += 4)
+        {
+            bt += cur[0];
+            gt += cur[1];
+            rt += cur[2];
+        }
+        bt = ylow*(bt + ((src[0]*xlow + end[0]*xhigh)>>12));
+        gt = ylow*(gt + ((src[1]*xlow + end[1]*xhigh)>>12));
+        rt = ylow*(rt + ((src[2]*xlow + end[2]*xhigh)>>12));
+        if(h) 
+        {
+            for(src += stride, end += stride; --h; src += stride, end += stride)  
+            {
+                uint b = 0, g = 0, r = 0;
+                for(const uchar *cur = &src[4]; cur < end; cur += 4)
+                {
+                    b += cur[0];
+                    g += cur[1];
+                    r += cur[2];
+                }
+                bt += (b<<12) + src[0]*xlow + end[0]*xhigh;
+                gt += (g<<12) + src[1]*xlow + end[1]*xhigh;
+                rt += (r<<12) + src[2]*xlow + end[2]*xhigh;
+            }
+            uint b = 0, g = 0, r = 0;
+            for(const uchar *cur = &src[4]; cur < end; cur += 4)
+            {
+                b += cur[0];
+                g += cur[1];
+                r += cur[2];
+            }
+            bt += yhigh*(b + ((src[0]*xlow + end[0]*xhigh)>>12));
+            gt += yhigh*(g + ((src[1]*xlow + end[1]*xhigh)>>12));
+            rt += yhigh*(r + ((src[2]*xlow + end[2]*xhigh)>>12));
+        }
+        bdst = (bt*area)>>24;
+        gdst = (gt*area)>>24;
+        rdst = (rt*area)>>24;
+    }
+    void scaleyuv(const uchar *pixels, uint srcw, uint srch)
+    {
+        const int flip = -1;
+        const uint planesize = videow * videoh;
+        if(!yuv) yuv = new uchar[(planesize*3)/2];
+        uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
+        const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
+        if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
+
+        const uint stride = srcw<<2;
+        srcw &= ~1;
+        srch &= ~1;
+        const uint wfrac = (srcw<<12)/videow, hfrac = (srch<<12)/videoh, 
+                   area = ((ullong)planesize<<12)/(srcw*srch + 1),
+                   dw = videow*wfrac, dh = videoh*hfrac;
+  
+        for(uint y = 0; y < dh;)
+        {
+            uint yn = y + hfrac - 1, yi = y>>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1;
+            y += hfrac;
+            uint y2n = y + hfrac - 1, y2i = y>>12, h2 = (y2n>>12) - y2i, y2low = ((y2n|(-int(h2)>>24))&0xFFFU) + 1 - (y&0xFFFU), y2high = (y2n&0xFFFU) + 1;
+            y += hfrac;
+
+            const uchar *src = &pixels[yi*stride], *src2 = &pixels[y2i*stride];
+            uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
+            for(uint x = 0; x < dw;)
+            {
+                uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1;
+                x += wfrac;
+                uint x2n = x + wfrac - 1, x2i = x>>12, w2 = (x2n>>12) - x2i, x2low = ((w2+0xFFFU)&0x1000U) - (x&0xFFFU), x2high = (x2n&0xFFFU) + 1;
+                x += wfrac;
+
+                uint b1, g1, r1, b2, g2, r2, b3, g3, r3, b4, g4, r4;
+                boxsample(&src[xi<<2], stride, area, w, h, xlow, xhigh, ylow, yhigh, b1, g1, r1);
+                boxsample(&src[x2i<<2], stride, area, w2, h, x2low, x2high, ylow, yhigh, b2, g2, r2);
+                boxsample(&src2[xi<<2], stride, area, w, h2, xlow, xhigh, y2low, y2high, b3, g3, r3);
+                boxsample(&src2[x2i<<2], stride, area, w2, h2, x2low, x2high, y2low, y2high, b4, g4, r4);
+
+
+                // Y  = 16 + 65.481*R + 128.553*G + 24.966*B
+                // Cb = 128 - 37.797*R - 74.203*G + 112.0*B
+                // Cr = 128 + 112.0*R - 93.786*G - 18.214*B
+                *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12;
+                *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12;
+                *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;;
+                *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;;
+
+                const uint b = b1 + b2 + b3 + b4,
+                           g = g1 + g2 + g3 + g4,
+                           r = r1 + r2 + r3 + r4;
+                // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
+                *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12;
+                *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12;
+            }
+
+            yplane += 2*ystride;
+            uplane += uvstride;
+            vplane += uvstride;
+        }
+    }
+
+    void encodeyuv(const uchar *pixels)
+    {
+        const int flip = -1;
+        const uint planesize = videow * videoh;
+        if(!yuv) yuv = new uchar[(planesize*3)/2];
+        uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
+        const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
+        if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
+
+        const uint stride = videow<<2;
+        const uchar *src = pixels, *yend = src + videoh*stride;
+        while(src < yend)    
+        {
+            const uchar *src2 = src + stride, *xend = src2;
+            uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
+            while(src < xend)
+            {
+                const uint b1 = src[0], g1 = src[1], r1 = src[2],
+                           b2 = src[4], g2 = src[5], r2 = src[6],
+                           b3 = src2[0], g3 = src2[1], r3 = src2[2],
+                           b4 = src2[4], g4 = src2[5], r4 = src2[6];
+
+                // Y  = 16 + 65.481*R + 128.553*G + 24.966*B
+                // Cb = 128 - 37.797*R - 74.203*G + 112.0*B
+                // Cr = 128 + 112.0*R - 93.786*G - 18.214*B
+                *ydst++ = ((16<<12) + 1052*r1 + 2065*g1 + 401*b1)>>12;
+                *ydst++ = ((16<<12) + 1052*r2 + 2065*g2 + 401*b2)>>12;
+                *ydst2++ = ((16<<12) + 1052*r3 + 2065*g3 + 401*b3)>>12;;
+                *ydst2++ = ((16<<12) + 1052*r4 + 2065*g4 + 401*b4)>>12;;
+
+                const uint b = b1 + b2 + b3 + b4,
+                           g = g1 + g2 + g3 + g4,
+                           r = r1 + r2 + r3 + r4;
+                // note: weights here are scaled by 1<<10, as opposed to 1<<12, since r/g/b are already *4
+                *udst++ = ((128<<12) - 152*r - 298*g + 450*b)>>12;
+                *vdst++ = ((128<<12) + 450*r - 377*g - 73*b)>>12;
+
+                src += 8;
+                src2 += 8; 
+            }
+            src = src2;
+            yplane += 2*ystride;
+            uplane += uvstride;
+            vplane += uvstride;
+        }
+    }
+
+    void compressyuv(const uchar *pixels)
+    {
+        const int flip = -1;
+        const uint planesize = videow * videoh;
+        if(!yuv) yuv = new uchar[(planesize*3)/2];
+        uchar *yplane = yuv, *uplane = yuv + planesize, *vplane = yuv + planesize + planesize/4;
+        const int ystride = flip*int(videow), uvstride = flip*int(videow)/2;
+        if(flip < 0) { yplane -= int(videoh-1)*ystride; uplane -= int(videoh/2-1)*uvstride; vplane -= int(videoh/2-1)*uvstride; }
+
+        const uint stride = videow<<2;
+        const uchar *src = pixels, *yend = src + videoh*stride;
+        while(src < yend)
+        {
+            const uchar *src2 = src + stride, *xend = src2;
+            uchar *ydst = yplane, *ydst2 = yplane + ystride, *udst = uplane, *vdst = vplane;
+            while(src < xend)
+            {
+                *ydst++ = src[0];
+                *ydst++ = src[4];
+                *ydst2++ = src2[0];
+                *ydst2++ = src2[4];
+                *udst++ = (uint(src[1]) + uint(src[5]) + uint(src2[1]) + uint(src2[5])) >> 2;
+                *vdst++ = (uint(src[2]) + uint(src[6]) + uint(src2[2]) + uint(src2[6])) >> 2;
+
+                src += 8;
+                src2 += 8;
+            }
+            src = src2;
+            yplane += 2*ystride;
+            uplane += uvstride;
+            vplane += uvstride;
+        }
+    }
+
+    bool writesound(uchar *data, uint framesize, uint frame)
+    {
+        // do conversion in-place to little endian format
+        // note that xoring by half the range yields the same bit pattern as subtracting the range regardless of signedness
+        // ... so can toggle signedness just by xoring the high byte with 0x80
+        switch(soundformat)
+        {
+            case AUDIO_U8:
+                for(uchar *dst = data, *end = &data[framesize]; dst < end; dst++) *dst ^= 0x80;
+                break;
+            case AUDIO_S8:
+                break;
+            case AUDIO_U16LSB:
+                for(uchar *dst = &data[1], *end = &data[framesize]; dst < end; dst += 2) *dst ^= 0x80;
+                break;
+            case AUDIO_U16MSB:
+                for(ushort *dst = (ushort *)data, *end = (ushort *)&data[framesize]; dst < end; dst++)
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+                    *dst = endianswap(*dst) ^ 0x0080;
+#else
+                    *dst = endianswap(*dst) ^ 0x8000;
+#endif
+                break;
+            case AUDIO_S16LSB:
+                break;
+            case AUDIO_S16MSB:
+                endianswap((short *)data, framesize/2);
+                break;
+        }
+       
+        if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false;
+        addindex(frame, 1, framesize);
+    
+        writechunk("01wb", data, framesize);
+
+        return true;
+    }
+   
+
+    enum
+    {
+        VID_RGB = 0,
+        VID_YUV,
+        VID_YUV420
+    };
+
+    void flushsegment()
+    {
+        endlistchunk(); // LIST movi
+
+        avisegmentinfo &seg = segments.last();
+
+        uint indexframes = 0, videoframes = 0, soundframes = 0;
+        for(int i = seg.firstindex; i < index.length(); i++)
+        {
+            aviindexentry &e = index[i];
+            if(e.type) soundframes++; 
+            else 
+            {
+                if(i == seg.firstindex || e.offset != index[i-1].offset)
+                    videoframes++;
+                indexframes++;
+            }
+        }
+
+        if(segments.length() == 1)
+        {
+            startchunk("idx1", index.length()*16);
+            loopv(index)
+            {
+                aviindexentry &entry = index[i];
+                // printf("%3d %s %08x\n", i, (entry.type==1)?"s":"v", entry.offset);
+                f->write(entry.type ? "01wb" : "00dc", 4); // chunkid
+                f->putlil<uint>(0x10); // flags - KEYFRAME
+                f->putlil<uint>(entry.offset); // offset (relative to movi)
+                f->putlil<uint>(entry.size); // size
+            }
+            endchunk();
+        }
+
+        seg.videoframes = videoframes;
+        seg.videoindexoffset = totalsize;
+        startchunk("ix00", 24 + indexframes*8);
+        f->putlil<ushort>(2); // longs per entry
+        f->putlil<ushort>(0x0100); // index of chunks
+        f->putlil<uint>(indexframes); // entries in use
+        f->write("00dc", 4); // chunk id
+        f->putlil<uint>(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low
+        f->putlil<uint>(seg.offset>>32); // offset high
+        f->putlil<uint>(0); // reserved 3
+        for(int i = seg.firstindex; i < index.length(); i++)
+        {
+            aviindexentry &e = index[i];
+            if(e.type) continue;
+            f->putlil<uint>(e.offset + 4 + 4);
+            f->putlil<uint>(e.size);
+        }
+        endchunk(); // ix00
+        seg.videoindexsize = uint(totalsize - seg.videoindexoffset);
+
+        if(soundframes)
+        {
+            seg.soundframes = soundframes;
+            seg.soundindexoffset = totalsize;
+            startchunk("ix01", 24 + soundframes*8);
+            f->putlil<ushort>(2); // longs per entry
+            f->putlil<ushort>(0x0100); // index of chunks
+            f->putlil<uint>(soundframes); // entries in use
+            f->write("01wb", 4); // chunk id
+            f->putlil<uint>(seg.offset&stream::offset(0xFFFFFFFFU)); // offset low
+            f->putlil<uint>(seg.offset>>32); // offset high
+            f->putlil<uint>(0); // reserved 3
+            for(int i = seg.firstindex; i < index.length(); i++)
+            {
+                aviindexentry &e = index[i];
+                if(!e.type) continue;
+                f->putlil<uint>(e.offset + 4 + 4);
+                f->putlil<uint>(e.size);
+            }
+            endchunk(); // ix01
+            seg.soundindexsize = uint(totalsize - seg.soundindexoffset);
+        }
+
+        endlistchunk(); // RIFF AVI/AVIX
+    }
+
+    bool nextsegment()
+    {
+        if(segments.length()) 
+        {
+            if(segments.length() >= MAX_SUPER_INDEX) return false;
+            flushsegment();
+            listchunk("RIFF", "AVIX");
+        }
+        listchunk("LIST", "movi");
+        segments.add(avisegmentinfo(chunkoffsets[chunkdepth], index.length()));
+        return true;
+    }
+  
+    bool writevideoframe(const uchar *pixels, uint srcw, uint srch, int format, uint frame)
+    {
+        if(frame < videoframes) return true;
+        
+        switch(format)
+        {
+            case VID_RGB: 
+                if(srcw != videow || srch != videoh) scaleyuv(pixels, srcw, srch);
+                else encodeyuv(pixels);
+                break;
+            case VID_YUV:
+                compressyuv(pixels);
+                break;
+        }
+
+        const uint framesize = (videow * videoh * 3) / 2;
+        if(totalsize - segments.last().offset + framesize > 1000*1000*1000 && !nextsegment()) return false;
+
+        while(videoframes <= frame) addindex(videoframes++, 0, framesize);
+
+        writechunk("00dc", format == VID_YUV420 ? pixels : yuv, framesize);
+
+        return true;
+    }
+    
+};
+
+VAR(movieaccelblit, 0, 0, 1);
+VAR(movieaccelyuv, 0, 1, 1);
+VARP(movieaccel, 0, 1, 1);
+VARP(moviesync, 0, 0, 1);
+FVARP(movieminquality, 0, 0, 1);
+
+namespace recorder
+{
+    static enum { REC_OK = 0, REC_USERHALT, REC_TOOSLOW, REC_FILERROR } state = REC_OK;
+    
+    static aviwriter *file = NULL;
+    static int starttime = 0;
+    
+    static int stats[1000];
+    static int statsindex = 0;
+    static uint dps = 0; // dropped frames per sample
+    
+    enum { MAXSOUNDBUFFERS = 128 }; // sounds queue up until there is a video frame, so at low fps you'll need a bigger queue
+    struct soundbuffer
+    {
+        uchar *sound;
+        uint size, maxsize;
+        uint frame;
+        
+        soundbuffer() : sound(NULL), maxsize(0) {}
+        ~soundbuffer() { cleanup(); }
+        
+        void load(uchar *stream, uint len, uint fnum)
+        {
+            if(len > maxsize)
+            {
+                DELETEA(sound);
+                sound = new uchar[len];
+                maxsize = len;
+            }
+            size = len;
+            frame = fnum;
+            memcpy(sound, stream, len);
+        }
+        
+        void cleanup() { DELETEA(sound); maxsize = 0; }
+    };
+    static queue<soundbuffer, MAXSOUNDBUFFERS> soundbuffers;
+    static SDL_mutex *soundlock = NULL;
+    
+    enum { MAXVIDEOBUFFERS = 2 }; // double buffer
+    struct videobuffer 
+    {
+        uchar *video;
+        uint w, h, bpp, frame;
+        int format;
+
+        videobuffer() : video(NULL){}
+        ~videobuffer() { cleanup(); }
+
+        void init(int nw, int nh, int nbpp)
+        {
+            DELETEA(video);
+            w = nw;
+            h = nh;
+            bpp = nbpp;
+            video = new uchar[w*h*bpp];
+            format = -1;
+        }
+         
+        void cleanup() { DELETEA(video); }
+    };
+    static queue<videobuffer, MAXVIDEOBUFFERS> videobuffers;
+    static uint lastframe = ~0U;
+
+    static GLuint scalefb = 0, scaletex[2] = { 0, 0 };
+    static uint scalew = 0, scaleh = 0;
+    static GLuint encodefb = 0, encoderb = 0;
+
+    static SDL_Thread *thread = NULL;
+    static SDL_mutex *videolock = NULL;
+    static SDL_cond *shouldencode = NULL, *shouldread = NULL;
+
+    bool isrecording() { return file != NULL; }
+    
+    float calcquality()
+    {
+        return 1.0f - float(dps)/float(dps+file->videofps); // strictly speaking should lock to read dps - 1.0=perfect, 0.5=half of frames are beingdropped
+    }
+
+    int gettime()
+    {
+        return inbetweenframes ? getclockmillis() : totalmillis;
+    }
+
+    int videoencoder(void *data) // runs on a separate thread
+    {
+        for(int numvid = 0, numsound = 0;;)
+        {   
+            SDL_LockMutex(videolock);
+            for(; numvid > 0; numvid--) videobuffers.remove();
+            SDL_CondSignal(shouldread);
+            while(videobuffers.empty() && state == REC_OK) SDL_CondWait(shouldencode, videolock);
+            if(state != REC_OK) { SDL_UnlockMutex(videolock); break; }
+            videobuffer &m = videobuffers.removing();
+            numvid++;
+            SDL_UnlockMutex(videolock);
+            
+            if(file->soundfrequency > 0)
+            {
+                // chug data from lock protected buffer to avoid holding lock while writing to file
+                SDL_LockMutex(soundlock);
+                for(; numsound > 0; numsound--) soundbuffers.remove();
+                for(; numsound < soundbuffers.length(); numsound++)
+                {
+                    soundbuffer &s = soundbuffers.removing(numsound);
+                    if(s.frame > m.frame) break; // sync with video
+                }
+                SDL_UnlockMutex(soundlock);
+                loopi(numsound)
+                {
+                    soundbuffer &s = soundbuffers.removing(i);
+                    if(!file->writesound(s.sound, s.size, s.frame)) state = REC_FILERROR;
+                }
+            }
+            
+            int duplicates = m.frame - (int)file->videoframes + 1;
+            if(duplicates > 0) // determine how many frames have been dropped over the sample window
+            {
+                dps -= stats[statsindex];
+                stats[statsindex] = duplicates-1;
+                dps += stats[statsindex];
+                statsindex = (statsindex+1)%file->videofps;
+            }
+            //printf("frame %d->%d (%d dps): sound = %d bytes\n", file->videoframes, nextframenum, dps, m.soundlength);
+            if(calcquality() < movieminquality) state = REC_TOOSLOW;
+            else if(!file->writevideoframe(m.video, m.w, m.h, m.format, m.frame)) state = REC_FILERROR;
+            
+            m.frame = ~0U;
+        }
+        
+        return 0;
+    }
+    
+    void soundencoder(void *udata, Uint8 *stream, int len) // callback occurs on a separate thread
+    {
+        SDL_LockMutex(soundlock);
+        if(soundbuffers.full()) 
+        {
+            if(movieminquality >= 1) state = REC_TOOSLOW;
+        }
+        else if(state == REC_OK)
+        {
+            uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
+            soundbuffer &s = soundbuffers.add();
+            s.load((uchar *)stream, len, nextframe);
+        }
+        SDL_UnlockMutex(soundlock);
+    }
+    
+    void start(const char *filename, int videofps, int videow, int videoh, bool sound) 
+    {
+        if(file) return;
+       
+        useshaderbyname("moviergb");
+        useshaderbyname("movieyuv");
+        useshaderbyname("moviey");
+        useshaderbyname("movieu");
+        useshaderbyname("moviev");
+        int fps, bestdiff, worstdiff;
+        getfps(fps, bestdiff, worstdiff);
+        if(videofps > fps) conoutf(CON_WARN, "frame rate may be too low to capture at %d fps", videofps);
+        
+        if(videow%2) videow += 1;
+        if(videoh%2) videoh += 1;
+
+        file = new aviwriter(filename, videow, videoh, videofps, sound);
+        if(!file->open()) 
+        { 
+            conoutf(CON_ERROR, "unable to create file %s", filename);
+            DELETEP(file);
+            return;
+        }
+        conoutf("movie recording to: %s %dx%d @ %dfps%s", file->filename, file->videow, file->videoh, file->videofps, (file->soundfrequency>0)?" + sound":"");
+        
+        starttime = gettime();
+        loopi(file->videofps) stats[i] = 0;
+        statsindex = 0;
+        dps = 0;
+        
+        lastframe = ~0U;
+        videobuffers.clear();
+        loopi(MAXVIDEOBUFFERS)
+        {
+            uint w = screenw, h = screenw;
+            videobuffers.data[i].init(w, h, 4);
+            videobuffers.data[i].frame = ~0U;
+        }
+        
+        soundbuffers.clear();
+        
+        soundlock = SDL_CreateMutex();
+        videolock = SDL_CreateMutex();
+        shouldencode = SDL_CreateCond();
+        shouldread = SDL_CreateCond();
+        thread = SDL_CreateThread(videoencoder, "video encoder", NULL); 
+        if(file->soundfrequency > 0) Mix_SetPostMix(soundencoder, NULL);
+    }
+    
+    void cleanup()
+    {
+        if(scalefb) { glDeleteFramebuffers_(1, &scalefb); scalefb = 0; }
+        if(scaletex[0] || scaletex[1]) { glDeleteTextures(2, scaletex); memset(scaletex, 0, sizeof(scaletex)); }
+        scalew = scaleh = 0;
+        if(encodefb) { glDeleteFramebuffers_(1, &encodefb); encodefb = 0; }
+        if(encoderb) { glDeleteRenderbuffers_(1, &encoderb); encoderb = 0; }
+    }
+
+    void stop()
+    {
+        if(!file) return;
+        if(state == REC_OK) state = REC_USERHALT;
+        if(file->soundfrequency > 0) Mix_SetPostMix(NULL, NULL);
+        
+        SDL_LockMutex(videolock); // wakeup thread enough to kill it
+        SDL_CondSignal(shouldencode);
+        SDL_UnlockMutex(videolock);
+        
+        SDL_WaitThread(thread, NULL); // block until thread is finished
+
+        cleanup();
+
+        loopi(MAXVIDEOBUFFERS) videobuffers.data[i].cleanup();
+        loopi(MAXSOUNDBUFFERS) soundbuffers.data[i].cleanup();
+
+        SDL_DestroyMutex(soundlock);
+        SDL_DestroyMutex(videolock);
+        SDL_DestroyCond(shouldencode);
+        SDL_DestroyCond(shouldread);
+
+        soundlock = videolock = NULL;
+        shouldencode = shouldread = NULL;
+        thread = NULL;
+        static const char * const mesgs[] = { "ok", "stopped", "computer too slow", "file error"};
+        conoutf("movie recording halted: %s, %d frames", mesgs[state], file->videoframes);
+        
+        DELETEP(file);
+        state = REC_OK;
+    }
+    void readbuffer(videobuffer &m, uint nextframe)
+    {
+        bool accelyuv = movieaccelyuv && !(m.w%8),
+             usefbo = movieaccel && file->videow <= (uint)screenw && file->videoh <= (uint)screenh && (accelyuv || file->videow < (uint)screenw || file->videoh < (uint)screenh);
+        uint w = screenw, h = screenh;
+        if(usefbo) { w = file->videow; h = file->videoh; }
+        if(w != m.w || h != m.h) m.init(w, h, 4);
+        m.format = aviwriter::VID_RGB;
+        m.frame = nextframe;
+
+        glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w, 4));
+        if(usefbo)
+        {
+            uint tw = screenw, th = screenh;
+            if(hasFBB && movieaccelblit) { tw = max(tw/2, m.w); th = max(th/2, m.h); }
+            if(tw != scalew || th != scaleh)
+            {
+                if(!scalefb) glGenFramebuffers_(1, &scalefb);
+                loopi(2)
+                {
+                    if(!scaletex[i]) glGenTextures(1, &scaletex[i]);
+                    createtexture(scaletex[i], tw, th, NULL, 3, 1, GL_RGB);
+                }
+                scalew = tw;
+                scaleh = th;
+            }
+            if(accelyuv && (!encodefb || !encoderb))
+            {
+                if(!encodefb) glGenFramebuffers_(1, &encodefb);
+                glBindFramebuffer_(GL_FRAMEBUFFER, encodefb);
+                if(!encoderb) glGenRenderbuffers_(1, &encoderb);
+                glBindRenderbuffer_(GL_RENDERBUFFER, encoderb);
+                glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA, (m.w*3)/8, m.h);
+                glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, encoderb);
+                glBindRenderbuffer_(GL_RENDERBUFFER, 0);
+                glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+            }
+                     
+            if(tw < (uint)screenw || th < (uint)screenh)
+            {
+                glBindFramebuffer_(GL_READ_FRAMEBUFFER, 0);
+                glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, scalefb);
+                glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[0], 0);
+                glBlitFramebuffer_(0, 0, screenw, screenh, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+                glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, 0);
+            }
+            else
+            {
+                glBindTexture(GL_TEXTURE_2D, scaletex[0]);
+                glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
+            }
+
+            GLOBALPARAMF(moviescale, 1.0f/scalew, 1.0f/scaleh);
+            if(tw > m.w || th > m.h || (!accelyuv && tw >= m.w && th >= m.h))
+            {
+                glBindFramebuffer_(GL_FRAMEBUFFER, scalefb);
+                do
+                {
+                    uint dw = max(tw/2, m.w), dh = max(th/2, m.h);
+                    glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scaletex[1], 0);
+                    glViewport(0, 0, dw, dh);
+                    glBindTexture(GL_TEXTURE_2D, scaletex[0]);
+                    if(dw == m.w && dh == m.h && !accelyuv) { SETSHADER(movieyuv); m.format = aviwriter::VID_YUV; }
+                    else SETSHADER(moviergb);
+                    screenquad(tw/float(scalew), th/float(scaleh));
+                    tw = dw;
+                    th = dh;
+                    swap(scaletex[0], scaletex[1]);
+                } while(tw > m.w || th > m.h);
+            }
+            if(accelyuv)
+            {
+                glBindFramebuffer_(GL_FRAMEBUFFER, encodefb);
+                glBindTexture(GL_TEXTURE_2D, scaletex[0]);
+                glViewport(0, 0, m.w/4, m.h); SETSHADER(moviey); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
+                glViewport(m.w/4, 0, m.w/8, m.h/2); SETSHADER(movieu); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
+                glViewport(m.w/4, m.h/2, m.w/8, m.h/2); SETSHADER(moviev); screenquadflipped(m.w/float(scalew), m.h/float(scaleh));
+                const uint planesize = m.w * m.h;
+                glPixelStorei(GL_PACK_ALIGNMENT, texalign(m.video, m.w/4, 4)); 
+                glReadPixels(0, 0, m.w/4, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
+                glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize], m.w/8, 4));
+                glReadPixels(m.w/4, 0, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize]);
+                glPixelStorei(GL_PACK_ALIGNMENT, texalign(&m.video[planesize + planesize/4], m.w/8, 4));
+                glReadPixels(m.w/4, m.h/2, m.w/8, m.h/2, GL_BGRA, GL_UNSIGNED_BYTE, &m.video[planesize + planesize/4]);
+                m.format = aviwriter::VID_YUV420;
+            }
+            else
+            {
+                glBindFramebuffer_(GL_FRAMEBUFFER, scalefb);
+                glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
+            }
+            glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+            glViewport(0, 0, screenw, screenh);
+
+        }
+        else glReadPixels(0, 0, m.w, m.h, GL_BGRA, GL_UNSIGNED_BYTE, m.video);
+    }
+    bool readbuffer()
+    {
+        if(!file) return false;
+        if(state != REC_OK)
+        {
+            stop();
+            return false;
+        }
+        SDL_LockMutex(videolock);
+        if(moviesync && videobuffers.full()) SDL_CondWait(shouldread, videolock);
+        uint nextframe = (max(gettime() - starttime, 0)*file->videofps)/1000;
+        if(!videobuffers.full() && (lastframe == ~0U || nextframe > lastframe))
+        {
+            videobuffer &m = videobuffers.adding();
+            SDL_UnlockMutex(videolock);
+            readbuffer(m, nextframe);
+            SDL_LockMutex(videolock);
+            lastframe = nextframe;
+            videobuffers.add();
+            SDL_CondSignal(shouldencode);
+        }
+        SDL_UnlockMutex(videolock);
+        return true;
+    }
+
+    void drawhud()
+    {
+        int w = screenw, h = screenh;
+        if(forceaspect) w = int(ceil(h*forceaspect));
+        gettextres(w, h);
+
+        hudmatrix.ortho(0, w, h, 0, -1, 1);
+        hudmatrix.scale(1/3.0f, 1/3.0f, 1);
+        resethudmatrix();
+        hudshader->set();
+
+        glEnable(GL_BLEND);
+
+        double totalsize = file->filespaceguess();
+        const char *unit = "KB";
+        if(totalsize >= 1e9) { totalsize /= 1e9; unit = "GB"; }
+        else if(totalsize >= 1e6) { totalsize /= 1e6; unit = "MB"; }
+        else totalsize /= 1e3;
+
+        draw_textf("recorded %.1f%s %d%%", w*3-10*FONTH, h*3-FONTH-FONTH*3/2, totalsize, unit, int(calcquality()*100)); 
+
+        glDisable(GL_BLEND);
+    }
+
+    void capture(bool overlay)
+    {
+        if(readbuffer() && overlay) drawhud();
+    }
+}
+
+VARP(moview, 0, 320, 10000);
+VARP(movieh, 0, 240, 10000);
+VARP(moviefps, 1, 24, 1000);
+VARP(moviesound, 0, 1, 1);
+
+void movie(char *name)
+{
+    if(name[0] == '\0') recorder::stop();
+    else if(!recorder::isrecording()) recorder::start(name, moviefps, moview ? moview : screenw, movieh ? movieh : screenh, moviesound!=0);
+}
+
+COMMAND(movie, "s");
+ICOMMAND(movierecording, "", (), intret(recorder::isrecording() ? 1 : 0));
+
diff --git a/src/engine/mpr.h b/src/engine/mpr.h
new file mode 100644 (file)
index 0000000..b4cfb59
--- /dev/null
@@ -0,0 +1,575 @@
+// This code is based off the Minkowski Portal Refinement algorithm by Gary Snethen in XenoCollide & Game Programming Gems 7.
+
+namespace mpr
+{
+    struct CubePlanes
+    {
+        const clipplanes &p;
+
+        CubePlanes(const clipplanes &p) : p(p) {}
+
+        vec center() const { return p.o; }
+
+        vec supportpoint(const vec &n) const
+        {
+            int besti = 7;
+            float bestd = n.dot(p.v[7]);
+            loopi(7)
+            {
+                float d = n.dot(p.v[i]);
+                if(d > bestd) { besti = i; bestd = d; }
+            }
+            return p.v[besti];
+        }
+    };
+
+    struct SolidCube
+    {
+        vec o;
+        int size;
+
+        SolidCube(float x, float y, float z, int size) : o(x, y, z), size(size) {}
+        SolidCube(const vec &o, int size) : o(o), size(size) {}
+        SolidCube(const ivec &o, int size) : o(o), size(size) {}
+
+        vec center() const { return vec(o).add(size/2); }
+
+        vec supportpoint(const vec &n) const
+        {
+            vec p(o);
+            if(n.x > 0) p.x += size;
+            if(n.y > 0) p.y += size;
+            if(n.z > 0) p.z += size;
+            return p;
+        }
+    };
+
+    struct Ent
+    {
+        physent *ent;
+
+        Ent(physent *ent) : ent(ent) {}
+
+        vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight)/2); }
+    };
+
+    struct EntOBB : Ent
+    {
+        matrix3 orient;
+        float zmargin;
+
+        EntOBB(physent *ent, float zmargin = 0) : Ent(ent), zmargin(zmargin)
+        {
+            orient.setyaw(ent->yaw*RAD);
+        }
+
+        vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight - zmargin)/2); }
+        
+        vec contactface(const vec &wn, const vec &wdir) const
+        {
+            vec n = orient.transform(wn).div(vec(ent->xradius, ent->yradius, (ent->aboveeye + ent->eyeheight + zmargin)/2)),
+                dir = orient.transform(wdir),
+                an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0),
+                fn(0, 0, 0);
+            if(an.x > an.y)
+            {
+                if(an.x > an.z) fn.x = n.x*dir.x < 0 ? (n.x > 0 ? 1 : -1) : 0;
+                else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+            }
+            else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0;
+            else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+            return orient.transposedtransform(fn);
+        }
+
+        vec localsupportpoint(const vec &ln) const
+        {
+            return vec(ln.x > 0 ? ent->xradius : -ent->xradius,
+                       ln.y > 0 ? ent->yradius : -ent->yradius,
+                       ln.z > 0 ? ent->aboveeye : -ent->eyeheight - zmargin);
+        }
+
+        vec supportpoint(const vec &n) const
+        {
+            return orient.transposedtransform(localsupportpoint(orient.transform(n))).add(ent->o);
+        }
+
+        float supportcoordneg(float a, float b, float c) const
+        {
+            return localsupportpoint(vec(-a, -b, -c)).dot(vec(a, b, c));
+        }
+        float supportcoord(float a, float b, float c) const
+        {
+            return localsupportpoint(vec(a, b, c)).dot(vec(a, b, c));
+        }
+
+        float left() const { return supportcoordneg(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; }
+        float right() const { return supportcoord(orient.a.x, orient.b.x, orient.c.x) + ent->o.x; }
+        float back() const { return supportcoordneg(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; }
+        float front() const { return supportcoord(orient.a.y, orient.b.y, orient.c.y) + ent->o.y; }
+        float bottom() const { return ent->o.z - ent->eyeheight - zmargin; }
+        float top() const { return ent->o.z + ent->aboveeye; }
+    };
+
+    struct EntFuzzy : Ent
+    {
+        EntFuzzy(physent *ent) : Ent(ent) {}
+
+        float left() const { return ent->o.x - ent->radius; }
+        float right() const { return ent->o.x + ent->radius; }
+        float back() const { return ent->o.y - ent->radius; }
+        float front() const { return ent->o.y + ent->radius; }
+        float bottom() const { return ent->o.z - ent->eyeheight; }
+        float top() const { return ent->o.z + ent->aboveeye; }
+    };
+
+    struct EntCylinder : EntFuzzy
+    {
+        float zmargin;
+
+        EntCylinder(physent *ent, float zmargin = 0) : EntFuzzy(ent), zmargin(zmargin) {}
+
+        vec center() const { return vec(ent->o.x, ent->o.y, ent->o.z + (ent->aboveeye - ent->eyeheight - zmargin)/2); }
+
+        float bottom() const { return ent->o.z - ent->eyeheight - zmargin; }
+
+        vec contactface(const vec &n, const vec &dir) const
+        {
+            float dxy = n.dot2(n)/(ent->radius*ent->radius), dz = n.z*n.z*4/(ent->aboveeye + ent->eyeheight + zmargin);
+            vec fn(0, 0, 0);
+            if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+            else if(n.dot2(dir) < 0)
+            {
+                fn.x = n.x;
+                fn.y = n.y;
+                fn.normalize();
+            }
+            return fn;
+        }
+
+        vec supportpoint(const vec &n) const
+        {
+            vec p(ent->o);
+            if(n.z > 0) p.z += ent->aboveeye;
+            else p.z -= ent->eyeheight + zmargin;
+            if(n.x || n.y)
+            {
+                float r = ent->radius / n.magnitude2();
+                p.x += n.x*r;
+                p.y += n.y*r;
+            }
+            return p;
+        }
+    };
+
+    struct EntCapsule : EntFuzzy
+    {
+        EntCapsule(physent *ent) : EntFuzzy(ent) {}
+
+        vec supportpoint(const vec &n) const
+        {
+            vec p(ent->o);
+            if(n.z > 0) p.z += ent->aboveeye - ent->radius;
+            else p.z -= ent->eyeheight - ent->radius;
+            p.add(vec(n).mul(ent->radius / n.magnitude()));
+            return p;
+        }
+    };
+
+    struct EntEllipsoid : EntFuzzy
+    {
+        EntEllipsoid(physent *ent) : EntFuzzy(ent) {}
+
+        vec supportpoint(const vec &dir) const
+        {
+            vec p(ent->o), n = vec(dir).normalize();
+            p.x += ent->radius*n.x;
+            p.y += ent->radius*n.y;
+            p.z += (ent->aboveeye + ent->eyeheight)/2*(1 + n.z) - ent->eyeheight;
+            return p;
+        }
+    };
+
+    struct Model
+    {
+        vec o, radius;
+        matrix3 orient;
+
+        Model(const vec &ent, const vec &center, const vec &radius, int yaw) : o(ent), radius(radius)
+        {
+            orient.setyaw(yaw*RAD);
+            o.add(orient.transposedtransform(center));
+        }
+
+        vec center() const { return o; }
+    };
+
+    struct ModelOBB : Model
+    {
+        ModelOBB(const vec &ent, const vec &center, const vec &radius, int yaw) :
+            Model(ent, center, radius, yaw)
+        {}
+
+        vec contactface(const vec &wn, const vec &wdir) const
+        {
+            vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir),
+                an(fabs(n.x), fabs(n.y), dir.z ? fabs(n.z) : 0),
+                fn(0, 0, 0);
+            if(an.x > an.y)
+            {
+                if(an.x > an.z) fn.x = n.x*dir.x < 0 ? (n.x > 0 ? 1 : -1) : 0;
+                else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+            }
+            else if(an.y > an.z) fn.y = n.y*dir.y < 0 ? (n.y > 0 ? 1 : -1) : 0;
+            else if(an.z > 0) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+            return orient.transposedtransform(fn);
+        }
+
+        vec supportpoint(const vec &n) const
+        {
+            vec ln = orient.transform(n), p(0, 0, 0);
+            if(ln.x > 0) p.x += radius.x;
+            else p.x -= radius.x;
+            if(ln.y > 0) p.y += radius.y;
+            else p.y -= radius.y;
+            if(ln.z > 0) p.z += radius.z;
+            else p.z -= radius.z;
+            return orient.transposedtransform(p).add(o);
+        }
+    };
+
+    struct ModelEllipse : Model
+    {
+        ModelEllipse(const vec &ent, const vec &center, const vec &radius, int yaw) :
+            Model(ent, center, radius, yaw)
+        {}
+
+        vec contactface(const vec &wn, const vec &wdir) const
+        {
+            vec n = orient.transform(wn).div(radius), dir = orient.transform(wdir);
+            float dxy = n.dot2(n), dz = n.z*n.z;
+            vec fn(0, 0, 0);
+            if(dz > dxy && dir.z) fn.z = n.z*dir.z < 0 ? (n.z > 0 ? 1 : -1) : 0;
+            else if(n.dot2(dir) < 0)
+            {
+                fn.x = n.x*radius.y;
+                fn.y = n.y*radius.x;
+                fn.normalize();
+            }
+            return orient.transposedtransform(fn);
+        }
+
+        vec supportpoint(const vec &n) const
+        {
+            vec ln = orient.transform(n), p(0, 0, 0);
+            if(ln.z > 0) p.z += radius.z;
+            else p.z -= radius.z;
+            if(ln.x || ln.y)
+            {
+                float r = ln.magnitude2();
+                p.x += ln.x*radius.x/r;
+                p.y += ln.y*radius.y/r;
+            }
+            return orient.transposedtransform(p).add(o);
+        }
+    };
+
+    const float boundarytolerance = 1e-3f;
+
+    template<class T, class U>
+    bool collide(const T &p1, const U &p2)
+    {
+        // v0 = center of Minkowski difference
+        vec v0 = p2.center().sub(p1.center());
+        if(v0.iszero()) return true;  // v0 and origin overlap ==> hit
+
+        // v1 = support in direction of origin
+        vec n = vec(v0).neg();
+        vec v1 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+        if(v1.dot(n) <= 0) return false;  // origin outside v1 support plane ==> miss
+
+        // v2 = support perpendicular to plane containing origin, v0 and v1
+        n.cross(v1, v0);
+        if(n.iszero()) return true;   // v0, v1 and origin colinear (and origin inside v1 support plane) == > hit
+        vec v2 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+        if(v2.dot(n) <= 0) return false;  // origin outside v2 support plane ==> miss
+
+        // v3 = support perpendicular to plane containing v0, v1 and v2
+        n.cross(v0, v1, v2);
+
+        // If the origin is on the - side of the plane, reverse the direction of the plane
+        if(n.dot(v0) > 0)
+        {
+            swap(v1, v2);
+            n.neg();
+        }
+
+        ///
+        // Phase One: Find a valid portal
+
+        loopi(100)
+        {
+            // Obtain the next support point
+            vec v3 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+            if(v3.dot(n) <= 0) return false;  // origin outside v3 support plane ==> miss
+
+            // If origin is outside (v1,v0,v3), then portal is invalid -- eliminate v2 and find new support outside face
+            vec v3xv0;
+            v3xv0.cross(v3, v0);
+            if(v1.dot(v3xv0) < 0)
+            {
+                v2 = v3;
+                n.cross(v0, v1, v3);
+                continue;
+            }
+
+            // If origin is outside (v3,v0,v2), then portal is invalid -- eliminate v1 and find new support outside face
+            if(v2.dot(v3xv0) > 0)
+            {
+                v1 = v3;
+                n.cross(v0, v3, v2);
+                continue;
+            }
+
+            ///
+            // Phase Two: Refine the portal
+
+            for(int j = 0;; j++)
+            {
+                // Compute outward facing normal of the portal
+                n.cross(v1, v2, v3);
+
+                // If the origin is inside the portal, we have a hit
+                if(n.dot(v1) >= 0) return true;
+
+                n.normalize();
+
+                // Find the support point in the direction of the portal's normal
+                vec v4 = p2.supportpoint(n).sub(p1.supportpoint(vec(n).neg()));
+
+                // If the origin is outside the support plane or the boundary is thin enough, we have a miss
+                if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100) return false;
+
+                // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0)
+                // Note:  We're taking advantage of the triple product identities here as an optimization
+                //        (v1 % v4) * v0 == v1 * (v4 % v0)    > 0 if origin inside (v1, v4, v0)
+                //        (v2 % v4) * v0 == v2 * (v4 % v0)    > 0 if origin inside (v2, v4, v0)
+                //        (v3 % v4) * v0 == v3 * (v4 % v0)    > 0 if origin inside (v3, v4, v0)
+                vec v4xv0;
+                v4xv0.cross(v4, v0);
+                if(v1.dot(v4xv0) > 0)
+                {
+                    if(v2.dot(v4xv0) > 0) v1 = v4;    // Inside v1 & inside v2 ==> eliminate v1
+                    else v3 = v4;                   // Inside v1 & outside v2 ==> eliminate v3
+                }
+                else
+                {
+                    if(v3.dot(v4xv0) > 0) v2 = v4;    // Outside v1 & inside v3 ==> eliminate v2
+                    else v1 = v4;                   // Outside v1 & outside v3 ==> eliminate v1
+                }
+            }
+        }
+        return false;
+    }
+
+    template<class T, class U>
+    bool collide(const T &p1, const U &p2, vec *contactnormal, vec *contactpoint1, vec *contactpoint2)
+    {
+        // v0 = center of Minkowski sum
+        vec v01 = p1.center();
+        vec v02 = p2.center();
+        vec v0 = vec(v02).sub(v01);
+
+        // Avoid case where centers overlap -- any direction is fine in this case
+        if(v0.iszero()) v0 = vec(0, 0, 1e-5f);
+
+        // v1 = support in direction of origin
+        vec n = vec(v0).neg();
+        vec v11 = p1.supportpoint(vec(n).neg());
+        vec v12 = p2.supportpoint(n);
+        vec v1 = vec(v12).sub(v11);
+        if(v1.dot(n) <= 0)
+        {
+            if(contactnormal) *contactnormal = n;
+            return false;
+        }
+
+        // v2 - support perpendicular to v1,v0
+        n.cross(v1, v0);
+        if(n.iszero())
+        {
+            n = vec(v1).sub(v0);
+            n.normalize();
+            if(contactnormal) *contactnormal = n;
+            if(contactpoint1) *contactpoint1 = v11;
+            if(contactpoint2) *contactpoint2 = v12;
+            return true;
+        }
+        vec v21 = p1.supportpoint(vec(n).neg());
+        vec v22 = p2.supportpoint(n);
+        vec v2 = vec(v22).sub(v21);
+        if(v2.dot(n) <= 0)
+        {
+            if(contactnormal) *contactnormal = n;
+            return false;
+        }
+
+        // Determine whether origin is on + or - side of plane (v1,v0,v2)
+        n.cross(v0, v1, v2);
+        ASSERT( !n.iszero() );
+        // If the origin is on the - side of the plane, reverse the direction of the plane
+        if(n.dot(v0) > 0)
+        {
+            swap(v1, v2);
+            swap(v11, v21);
+            swap(v12, v22);
+            n.neg();
+        }
+
+        ///
+        // Phase One: Identify a portal
+
+        loopi(100)
+        {
+            // Obtain the support point in a direction perpendicular to the existing plane
+            // Note: This point is guaranteed to lie off the plane
+            vec v31 = p1.supportpoint(vec(n).neg());
+            vec v32 = p2.supportpoint(n);
+            vec v3 = vec(v32).sub(v31);
+            if(v3.dot(n) <= 0)
+            {
+                if(contactnormal) *contactnormal = n;
+                return false;
+            }
+
+            // If origin is outside (v1,v0,v3), then eliminate v2 and loop
+            vec v3xv0;
+            v3xv0.cross(v3, v0);
+            if(v1.dot(v3xv0) < 0)
+            {
+                v2 = v3;
+                v21 = v31;
+                v22 = v32;
+                n.cross(v0, v1, v3);
+                continue;
+            }
+
+            // If origin is outside (v3,v0,v2), then eliminate v1 and loop
+            if(v2.dot(v3xv0) > 0)
+            {
+                v1 = v3;
+                v11 = v31;
+                v12 = v32;
+                n.cross(v0, v3, v2);
+                continue;
+            }
+
+            bool hit = false;
+
+            ///
+            // Phase Two: Refine the portal
+
+            // We are now inside of a wedge...
+            for(int j = 0;; j++)
+            {
+                // Compute normal of the wedge face
+                n.cross(v1, v2, v3);
+
+                // Can this happen???  Can it be handled more cleanly?
+                if(n.iszero())
+                {
+                    ASSERT(0);
+                    return true;
+                }
+
+                n.normalize();
+
+                // If the origin is inside the wedge, we have a hit
+                if(n.dot(v1) >= 0 && !hit)
+                {
+                    if(contactnormal) *contactnormal = n;
+
+                    // Compute the barycentric coordinates of the origin
+                    if(contactpoint1 || contactpoint2)
+                    {
+                        float b0 = v3.scalartriple(v1, v2),
+                              b1 = v0.scalartriple(v3, v2),
+                              b2 = v3.scalartriple(v0, v1),
+                              b3 = v0.scalartriple(v2, v1),
+                              sum = b0 + b1 + b2 + b3;
+                        if(sum <= 0)
+                        {
+                            b0 = 0;
+                            b1 = n.scalartriple(v2, v3);
+                            b2 = n.scalartriple(v3, v1);
+                            b3 = n.scalartriple(v1, v2);
+                            sum = b1 + b2 + b3;
+                        }
+                        if(contactpoint1)
+                            *contactpoint1 = (vec(v01).mul(b0).add(vec(v11).mul(b1)).add(vec(v21).mul(b2)).add(vec(v31).mul(b3))).mul(1.0f/sum);
+                        if(contactpoint2)
+                            *contactpoint2 = (vec(v02).mul(b0).add(vec(v12).mul(b1)).add(vec(v22).mul(b2)).add(vec(v32).mul(b3))).mul(1.0f/sum);
+                    }
+
+                    // HIT!!!
+                    hit = true;
+                }
+
+                // Find the support point in the direction of the wedge face
+                vec v41 = p1.supportpoint(vec(n).neg());
+                vec v42 = p2.supportpoint(n);
+                vec v4 = vec(v42).sub(v41);
+
+                // If the boundary is thin enough or the origin is outside the support plane for the newly discovered vertex, then we can terminate
+                if(v4.dot(n) <= 0 || vec(v4).sub(v3).dot(n) <= boundarytolerance || j > 100)
+                {
+                    if(contactnormal) *contactnormal = n;
+                    return hit;
+                }
+
+                // Test origin against the three planes that separate the new portal candidates: (v1,v4,v0) (v2,v4,v0) (v3,v4,v0)
+                // Note:  We're taking advantage of the triple product identities here as an optimization
+                //        (v1 % v4) * v0 == v1 * (v4 % v0)    > 0 if origin inside (v1, v4, v0)
+                //        (v2 % v4) * v0 == v2 * (v4 % v0)    > 0 if origin inside (v2, v4, v0)
+                //        (v3 % v4) * v0 == v3 * (v4 % v0)    > 0 if origin inside (v3, v4, v0)
+                vec v4xv0;
+                v4xv0.cross(v4, v0);
+                if(v1.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d1 = (v4,v0,v1)
+                {
+                    if(v2.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d2 = (v4,v0,v2)
+                    {
+                        // Inside d1 & inside d2 ==> eliminate v1
+                        v1 = v4;
+                        v11 = v41;
+                        v12 = v42;
+                    }
+                    else
+                    {
+                        // Inside d1 & outside d2 ==> eliminate v3
+                        v3 = v4;
+                        v31 = v41;
+                        v32 = v42;
+                    }
+                }
+                else
+                {
+                    if(v3.dot(v4xv0) > 0) // Compute the tetrahedron dividing face d3 = (v4,v0,v3)
+                    {
+                        // Outside d1 & inside d3 ==> eliminate v2
+                        v2 = v4;
+                        v21 = v41;
+                        v22 = v42;
+                    }
+                    else
+                    {
+                        // Outside d1 & outside d3 ==> eliminate v1
+                        v1 = v4;
+                        v11 = v41;
+                        v12 = v42;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+}
+
diff --git a/src/engine/normal.cpp b/src/engine/normal.cpp
new file mode 100644 (file)
index 0000000..d8641ab
--- /dev/null
@@ -0,0 +1,383 @@
+#include "engine.h"
+
+struct normalgroup
+{
+    vec pos;
+    int flat, normals, tnormals;
+
+    normalgroup() : flat(0), normals(-1), tnormals(-1) {}
+    normalgroup(const vec &pos) : pos(pos), flat(0), normals(-1), tnormals(-1) {}
+};
+
+static inline bool htcmp(const vec &v, const normalgroup &n) { return v == n.pos; } 
+
+struct normal
+{
+    int next;
+    vec surface;
+};
+
+struct tnormal
+{
+    int next;
+    float offset;
+    int normals[2];
+    normalgroup *groups[2];
+};
+
+hashset<normalgroup> normalgroups(1<<16);
+vector<normal> normals;
+vector<tnormal> tnormals;
+
+VARR(lerpangle, 0, 44, 180);
+
+static float lerpthreshold = 0;
+static bool usetnormals = true;
+
+static int addnormal(const vec &key, const vec &surface)
+{
+    normalgroup &g = normalgroups.access(key, key);
+    normal &n = normals.add();
+    n.next = g.normals;
+    n.surface = surface;
+    return g.normals = normals.length()-1;
+}
+
+static void addtnormal(const vec &key, float offset, int normal1, int normal2, normalgroup *group1, normalgroup *group2)
+{
+    normalgroup &g = normalgroups.access(key, key);
+    tnormal &n = tnormals.add();
+    n.next = g.tnormals;
+    n.offset = offset;
+    n.normals[0] = normal1;
+    n.normals[1] = normal2;
+    n.groups[0] = group1;
+    n.groups[1] = group2;
+    g.tnormals = tnormals.length()-1;
+}
+
+static int addnormal(const vec &key, int axis)
+{
+    normalgroup &g = normalgroups.access(key, key);
+    g.flat += 1<<(4*axis);
+    return axis - 6;
+}
+
+static inline void findnormal(const normalgroup &g, const vec &surface, vec &v)
+{
+    v = vec(0, 0, 0);
+    int total = 0;
+    if(surface.x >= lerpthreshold) { int n = (g.flat>>4)&0xF; v.x += n; total += n; }
+    else if(surface.x <= -lerpthreshold) { int n = g.flat&0xF; v.x -= n; total += n; }
+    if(surface.y >= lerpthreshold) { int n = (g.flat>>12)&0xF; v.y += n; total += n; }
+    else if(surface.y <= -lerpthreshold) { int n = (g.flat>>8)&0xF; v.y -= n; total += n; }
+    if(surface.z >= lerpthreshold) { int n = (g.flat>>20)&0xF; v.z += n; total += n; }
+    else if(surface.z <= -lerpthreshold) { int n = (g.flat>>16)&0xF; v.z -= n; total += n; }
+    for(int cur = g.normals; cur >= 0;)
+    {
+        normal &o = normals[cur];
+        if(o.surface.dot(surface) >= lerpthreshold)
+        {
+            v.add(o.surface);
+            total++;
+        }
+        cur = o.next;
+    }
+    if(total > 1) v.normalize();
+    else if(!total) v = surface;
+}
+
+static inline bool findtnormal(const normalgroup &g, const vec &surface, vec &v)
+{
+    float bestangle = lerpthreshold;
+    tnormal *bestnorm = NULL;
+    for(int cur = g.tnormals; cur >= 0;)
+    {
+        tnormal &o = tnormals[cur];
+        static const vec flats[6] = { vec(-1, 0, 0), vec(1, 0, 0), vec(0, -1, 0), vec(0, 1, 0), vec(0, 0, -1), vec(0, 0, 1) };
+        vec n1 = o.normals[0] < 0 ? flats[o.normals[0]+6] : normals[o.normals[0]].surface,
+            n2 = o.normals[1] < 0 ? flats[o.normals[1]+6] : normals[o.normals[1]].surface,
+            nt;
+        nt.lerp(n1, n2, o.offset).normalize();
+        float tangle = nt.dot(surface);
+        if(tangle >= bestangle)
+        {
+            bestangle = tangle;
+            bestnorm = &o;
+        }
+        cur = o.next;
+    }
+    if(!bestnorm) return false;
+    vec n1, n2;
+    findnormal(*bestnorm->groups[0], surface, n1);
+    findnormal(*bestnorm->groups[1], surface, n2);
+    v.lerp(n1, n2, bestnorm->offset).normalize();
+    return true;
+}
+
+void findnormal(const vec &key, const vec &surface, vec &v)
+{
+    const normalgroup *g = normalgroups.access(key);
+    if(!g) v = surface;
+    else if(g->tnormals < 0 || !findtnormal(*g, surface, v)) 
+        findnormal(*g, surface, v);
+}
+
+VARR(lerpsubdiv, 0, 2, 4);
+VARR(lerpsubdivsize, 4, 4, 128);
+
+static uint progress = 0;
+
+void show_addnormals_progress()
+{
+    float bar1 = float(progress) / float(allocnodes);
+    renderprogress(bar1, "computing normals...");
+}
+
+void addnormals(cube &c, const ivec &o, int size)
+{
+    CHECK_CALCLIGHT_PROGRESS(return, show_addnormals_progress);
+
+    if(c.children)
+    {
+        progress++;
+        size >>= 1;
+        loopi(8) addnormals(c.children[i], ivec(i, o, size), size);
+        return;
+    }
+    else if(isempty(c)) return;
+
+    vec pos[MAXFACEVERTS];
+    int norms[MAXFACEVERTS];
+    int tj = usetnormals && c.ext ? c.ext->tjoints : -1, vis;
+    loopi(6) if((vis = visibletris(c, i, o, size)))
+    {
+        CHECK_CALCLIGHT_PROGRESS(return, show_addnormals_progress);
+        if(c.texture[i] == DEFAULT_SKY) continue;
+
+        vec planes[2];
+        int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0, convex = 0, numplanes = 0;
+        if(numverts)
+        {
+            vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts;
+            vec vo(ivec(o).mask(~0xFFF));
+            loopj(numverts)
+            {
+                vertinfo &v = verts[j];
+                pos[j] = vec(v.x, v.y, v.z).mul(1.0f/8).add(vo);
+            }
+            if(!(c.merged&(1<<i)) && !flataxisface(c, i)) convex = faceconvexity(verts, numverts, size);
+        }
+        else if(c.merged&(1<<i)) continue;
+        else
+        {
+            ivec v[4];
+            genfaceverts(c, i, v);
+            if(!flataxisface(c, i)) convex = faceconvexity(v);
+            int order = vis&4 || convex < 0 ? 1 : 0;
+            vec vo(o);
+            pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+            if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+            pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+            if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+        }
+
+        if(!flataxisface(c, i))
+        {
+            planes[numplanes++].cross(pos[0], pos[1], pos[2]).normalize();
+            if(convex) planes[numplanes++].cross(pos[0], pos[2], pos[3]).normalize();
+        }
+
+        if(!numplanes) loopk(numverts) norms[k] = addnormal(pos[k], i);
+        else if(numplanes==1) loopk(numverts) norms[k] = addnormal(pos[k], planes[0]);
+        else 
+        { 
+            vec avg = vec(planes[0]).add(planes[1]).normalize();
+            norms[0] = addnormal(pos[0], avg);
+            norms[1] = addnormal(pos[1], planes[0]);
+            norms[2] = addnormal(pos[2], avg);
+            for(int k = 3; k < numverts; k++) norms[k] = addnormal(pos[k], planes[1]);
+        }
+
+        while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next;
+        while(tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1))
+        {
+            int edge = tjoints[tj].edge, e1 = edge%(MAXFACEVERTS+1), e2 = (e1+1)%numverts;
+            const vec &v1 = pos[e1], &v2 = pos[e2];
+            ivec d(vec(v2).sub(v1).mul(8));
+            int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2);
+            if(d[axis] < 0) d.neg();
+            reduceslope(d);
+            int origin = int(min(v1[axis], v2[axis])*8)&~0x7FFF,
+                offset1 = (int(v1[axis]*8) - origin) / d[axis],
+                offset2 = (int(v2[axis]*8) - origin) / d[axis];
+            vec o = vec(v1).sub(vec(d).mul(offset1/8.0f)), n1, n2;
+            float doffset = 1.0f / (offset2 - offset1);
+
+            while(tj >= 0)
+            {
+                tjoint &t = tjoints[tj];
+                if(t.edge != edge) break;
+                float offset = (t.offset - offset1) * doffset;
+                vec tpos = vec(d).mul(t.offset/8.0f).add(o); 
+                addtnormal(tpos, offset, norms[e1], norms[e2], normalgroups.access(v1), normalgroups.access(v2));
+                tj = t.next;
+            }
+        }
+    }
+}
+
+void calcnormals(bool lerptjoints)
+{
+    if(!lerpangle) return;
+    usetnormals = lerptjoints; 
+    if(usetnormals) findtjoints();
+    lerpthreshold = cos(lerpangle*RAD) - 1e-5f; 
+    progress = 1;
+    loopi(8) addnormals(worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2);
+}
+
+void clearnormals()
+{
+    normalgroups.clear();
+    normals.setsize(0);
+    tnormals.setsize(0);
+}
+
+void calclerpverts(const vec2 *c, const vec *n, lerpvert *lv, int &numv)
+{
+    int i = 0;
+    loopj(numv)
+    {
+        if(j)
+        {
+            if(c[j] == c[j-1] && n[j] == n[j-1]) continue;
+            if(j == numv-1 && c[j] == c[0] && n[j] == n[0]) continue;
+        }
+        lv[i].normal = n[j];
+        lv[i].tc = c[j];
+        i++;
+    }
+    numv = i;
+}
+
+void setlerpstep(float v, lerpbounds &bounds)
+{
+    if(bounds.min->tc.y + 1 > bounds.max->tc.y)
+    {
+        bounds.nstep = vec(0, 0, 0);
+        bounds.normal = bounds.min->normal;
+        if(bounds.min->normal != bounds.max->normal)
+        {
+            bounds.normal.add(bounds.max->normal);
+            bounds.normal.normalize();
+        }
+        bounds.ustep = 0;
+        bounds.u = bounds.min->tc.x;
+        return;
+    }
+
+    bounds.nstep = bounds.max->normal;
+    bounds.nstep.sub(bounds.min->normal);
+    bounds.nstep.div(bounds.max->tc.y-bounds.min->tc.y);
+
+    bounds.normal = bounds.nstep;
+    bounds.normal.mul(v - bounds.min->tc.y);
+    bounds.normal.add(bounds.min->normal);
+
+    bounds.ustep = (bounds.max->tc.x-bounds.min->tc.x) / (bounds.max->tc.y-bounds.min->tc.y);
+    bounds.u = bounds.ustep * (v-bounds.min->tc.y) + bounds.min->tc.x;
+}
+
+void initlerpbounds(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end)
+{
+    const lerpvert *first = &lv[0], *second = NULL;
+    loopi(numv-1)
+    {
+        if(lv[i+1].tc.y < first->tc.y) { second = first; first = &lv[i+1]; }
+        else if(!second || lv[i+1].tc.y < second->tc.y) second = &lv[i+1];
+    }
+
+    if(int(first->tc.y) < int(second->tc.y)) { start.min = end.min = first; }
+    else if(first->tc.x > second->tc.x) { start.min = second; end.min = first; }
+    else { start.min = first; end.min = second; }
+
+    if((lv[1].tc.x - lv->tc.x)*(lv[2].tc.y - lv->tc.y) > (lv[1].tc.y - lv->tc.y)*(lv[2].tc.x - lv->tc.x))
+    { 
+        start.winding = end.winding = 1;
+        start.max = (start.min == lv ? &lv[numv-1] : start.min-1);
+        end.max = (end.min == &lv[numv-1] ? lv : end.min+1);
+    }
+    else
+    {
+        start.winding = end.winding = -1;
+        start.max = (start.min == &lv[numv-1] ? lv : start.min+1);
+        end.max = (end.min == lv ? &lv[numv-1] : end.min-1);
+    }
+
+    setlerpstep(v, start);
+    setlerpstep(v, end);
+}
+
+void updatelerpbounds(float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end)
+{
+    if(v >= start.max->tc.y)
+    {
+        const lerpvert *next = start.winding > 0 ?
+                (start.max == lv ? &lv[numv-1] : start.max-1) :
+                (start.max == &lv[numv-1] ? lv : start.max+1);
+        if(next->tc.y > start.max->tc.y)
+        {
+            start.min = start.max;
+            start.max = next;
+            setlerpstep(v, start);
+        }
+    }
+    if(v >= end.max->tc.y)
+    {
+        const lerpvert *next = end.winding > 0 ?
+                (end.max == &lv[numv-1] ? lv : end.max+1) :
+                (end.max == lv ? &lv[numv-1] : end.max-1);
+        if(next->tc.y > end.max->tc.y)
+        {
+            end.min = end.max;
+            end.max = next;
+            setlerpstep(v, end);
+        }
+    }
+}
+
+void lerpnormal(float u, float v, const lerpvert *lv, int numv, lerpbounds &start, lerpbounds &end, vec &normal, vec &nstep)
+{   
+    updatelerpbounds(v, lv, numv, start, end);
+
+    if(start.u + 1 > end.u)
+    {
+        nstep = vec(0, 0, 0);
+        normal = start.normal;
+        normal.add(end.normal);
+        normal.normalize();
+    }
+    else
+    {
+        vec nstart(start.normal), nend(end.normal);
+        nstart.normalize();
+        nend.normalize();
+       
+        nstep = nend;
+        nstep.sub(nstart);
+        nstep.div(end.u-start.u);
+
+        normal = nstep;
+        normal.mul(u-start.u);
+        normal.add(nstart);
+        normal.normalize();
+    }
+     
+    start.normal.add(start.nstep);
+    start.u += start.ustep;
+
+    end.normal.add(end.nstep); 
+    end.u += end.ustep;
+}
+
diff --git a/src/engine/obj.h b/src/engine/obj.h
new file mode 100644 (file)
index 0000000..9cdf46a
--- /dev/null
@@ -0,0 +1,191 @@
+struct obj;
+
+struct obj : vertloader<obj>
+{
+    obj(const char *name) : vertloader(name) {}
+
+    static const char *formatname() { return "obj"; }
+    static bool animated() { return false; }
+    bool flipy() const { return true; }
+    int type() const { return MDL_OBJ; }
+
+    struct objmeshgroup : vertmeshgroup
+    {
+        void parsevert(char *s, vector<vec> &out)
+        {
+            vec &v = out.add(vec(0, 0, 0));
+            while(isalpha(*s)) s++;
+            loopi(3)
+            {
+                v[i] = strtod(s, &s);
+                while(isspace(*s)) s++;
+                if(!*s) break;
+            }
+        }
+
+        bool load(const char *filename, float smooth)
+        {
+            int len = strlen(filename);
+            if(len < 4 || strcasecmp(&filename[len-4], ".obj")) return false;
+
+            stream *file = openfile(filename, "rb");
+            if(!file) return false;
+
+            name = newstring(filename);
+
+            numframes = 1;
+
+            vector<vec> attrib[3];
+            char buf[512];
+
+            hashtable<ivec, int> verthash;
+            vector<vert> verts;
+            vector<tcvert> tcverts;
+            vector<tri> tris;
+
+            #define STARTMESH do { \
+                vertmesh &m = *new vertmesh; \
+                m.group = this; \
+                m.name = meshname[0] ? newstring(meshname) : NULL; \
+                meshes.add(&m); \
+                curmesh = &m; \
+                verthash.clear(); \
+                verts.setsize(0); \
+                tcverts.setsize(0); \
+                tris.setsize(0); \
+            } while(0)
+
+            #define FLUSHMESH do { \
+                curmesh->numverts = verts.length(); \
+                if(verts.length()) \
+                { \
+                    curmesh->verts = new vert[verts.length()]; \
+                    memcpy(curmesh->verts, verts.getbuf(), verts.length()*sizeof(vert)); \
+                    curmesh->tcverts = new tcvert[verts.length()]; \
+                    memcpy(curmesh->tcverts, tcverts.getbuf(), tcverts.length()*sizeof(tcvert)); \
+                } \
+                curmesh->numtris = tris.length(); \
+                if(tris.length()) \
+                { \
+                    curmesh->tris = new tri[tris.length()]; \
+                    memcpy(curmesh->tris, tris.getbuf(), tris.length()*sizeof(tri)); \
+                } \
+                if(attrib[2].empty()) \
+                { \
+                    if(smooth <= 1) curmesh->smoothnorms(smooth); \
+                    else curmesh->buildnorms(); \
+                } \
+            } while(0)
+
+            string meshname = "";
+            vertmesh *curmesh = NULL;
+            while(file->getline(buf, sizeof(buf)))
+            {
+                char *c = buf;
+                while(isspace(*c)) c++;
+                switch(*c)
+                {
+                    case '#': continue;
+                    case 'v':
+                        if(isspace(c[1])) parsevert(c, attrib[0]);
+                        else if(c[1]=='t') parsevert(c, attrib[1]);
+                        else if(c[1]=='n') parsevert(c, attrib[2]);
+                        break;
+                    case 'g':
+                    {
+                        while(isalpha(*c)) c++;
+                        while(isspace(*c)) c++;
+                        char *name = c;
+                        size_t namelen = strlen(name);
+                        while(namelen > 0 && isspace(name[namelen-1])) namelen--;
+                        copystring(meshname, name, min(namelen+1, sizeof(meshname)));
+
+                        if(curmesh) FLUSHMESH;
+                        curmesh = NULL;
+                        break;
+                    }
+                    case 'f':
+                    {
+                        if(!curmesh) STARTMESH;
+                        int v0 = -1, v1 = -1;
+                        while(isalpha(*c)) c++;
+                        for(;;)
+                        {
+                            while(isspace(*c)) c++;
+                            if(!*c) break; 
+                            ivec vkey(-1, -1, -1);
+                            loopi(3)
+                            {
+                                vkey[i] = strtol(c, &c, 10);
+                                if(vkey[i] < 0) vkey[i] = attrib[i].length() + vkey[i];
+                                else vkey[i]--;
+                                if(!attrib[i].inrange(vkey[i])) vkey[i] = -1;
+                                if(*c!='/') break;
+                                c++;
+                            }
+                            int *index = verthash.access(vkey);
+                            if(!index)
+                            {
+                                index = &verthash[vkey];
+                                *index = verts.length();
+                                vert &v = verts.add();
+                                v.pos = vkey.x < 0 ? vec(0, 0, 0) : attrib[0][vkey.x];
+                                v.pos = vec(v.pos.z, -v.pos.x, v.pos.y);
+                                v.norm = vkey.z < 0 ? vec(0, 0, 0) : attrib[2][vkey.z];
+                                v.norm = vec(v.norm.z, -v.norm.x, v.norm.y);
+                                tcvert &tcv = tcverts.add();
+                                tcv.tc = vkey.y < 0 ? vec2(0, 0) : vec2(attrib[1][vkey.y].x, 1-attrib[1][vkey.y].y);
+                            }
+                            if(v0 < 0) v0 = *index;
+                            else if(v1 < 0) v1 = *index;
+                            else
+                            {
+                                tri &t = tris.add();
+                                t.vert[0] = ushort(*index);
+                                t.vert[1] = ushort(v1);
+                                t.vert[2] = ushort(v0);
+                                v1 = *index;
+                            }
+                        }
+                        break;
+                    }
+                }
+            }
+
+            if(curmesh) FLUSHMESH;
+
+            delete file;
+
+            return true;
+        }
+    };
+
+    meshgroup *loadmeshes(const char *name, va_list args)
+    {
+        objmeshgroup *group = new objmeshgroup;
+        if(!group->load(name, va_arg(args, double))) { delete group; return NULL; }
+        return group;
+    }
+
+    bool loaddefaultparts()
+    {
+        part &mdl = addpart();
+        const char *pname = parentdir(name);
+        defformatstring(name1, "packages/models/%s/tris.obj", name);
+        mdl.meshes = sharemeshes(path(name1), 2.0);
+        if(!mdl.meshes)
+        {
+            defformatstring(name2, "packages/models/%s/tris.obj", pname);    // try obj in parent folder (vert sharing)
+            mdl.meshes = sharemeshes(path(name2), 2.0);
+            if(!mdl.meshes) return false;
+        }
+        Texture *tex, *masks;
+        loadskin(name, pname, tex, masks);
+        mdl.initskins(tex, masks);
+        if(tex==notexture) conoutf(CON_ERROR, "could not load model skin for %s", name1);
+        return true;
+    }
+};
+
+vertcommands<obj> objcommands;
+
diff --git a/src/engine/octa.cpp b/src/engine/octa.cpp
new file mode 100644 (file)
index 0000000..e4f0901
--- /dev/null
@@ -0,0 +1,1880 @@
+// core world management routines
+
+#include "engine.h"
+
+cube *worldroot = newcubes(F_SOLID);
+int allocnodes = 0;
+
+cubeext *growcubeext(cubeext *old, int maxverts)
+{
+    cubeext *ext = (cubeext *)new uchar[sizeof(cubeext) + maxverts*sizeof(vertinfo)];
+    if(old)
+    {
+        ext->va = old->va;
+        ext->ents = old->ents;
+        ext->tjoints = old->tjoints;
+    }
+    else
+    {
+        ext->va = NULL;
+        ext->ents = NULL;
+        ext->tjoints = -1;
+    }
+    ext->maxverts = maxverts;
+    return ext;
+}
+
+void setcubeext(cube &c, cubeext *ext)
+{
+    cubeext *old = c.ext;
+    if(old == ext) return;
+    c.ext = ext;
+    if(old) delete[] (uchar *)old;
+}
+  
+cubeext *newcubeext(cube &c, int maxverts, bool init)
+{
+    if(c.ext && c.ext->maxverts >= maxverts) return c.ext;
+    cubeext *ext = growcubeext(c.ext, maxverts);
+    if(init)
+    {
+        if(c.ext)
+        {
+            memcpy(ext->surfaces, c.ext->surfaces, sizeof(ext->surfaces));
+            memcpy(ext->verts(), c.ext->verts(), c.ext->maxverts*sizeof(vertinfo));
+        }
+        else memset(ext->surfaces, 0, sizeof(ext->surfaces)); 
+    }
+    setcubeext(c, ext);
+    return ext;
+}
+
+cube *newcubes(uint face, int mat)
+{
+    cube *c = new cube[8];
+    loopi(8)
+    {
+        c->children = NULL;
+        c->ext = NULL;
+        c->visible = 0;
+        c->merged = 0;
+        setfaces(*c, face);
+        loopl(6) c->texture[l] = DEFAULT_GEOM;
+        c->material = mat;
+        c++;
+    }
+    allocnodes++;
+    return c-8;
+}
+
+int familysize(const cube &c)
+{
+    int size = 1;
+    if(c.children) loopi(8) size += familysize(c.children[i]);
+    return size;
+}
+
+void freeocta(cube *c)
+{
+    if(!c) return;
+    loopi(8) discardchildren(c[i]);
+    delete[] c;
+    allocnodes--;
+}
+
+void freecubeext(cube &c)
+{
+    if(c.ext)
+    {
+        delete[] (uchar *)c.ext;
+        c.ext = NULL;
+    }
+}
+
+void discardchildren(cube &c, bool fixtex, int depth)
+{
+    c.material = MAT_AIR;
+    c.visible = 0;
+    c.merged = 0;
+    if(c.ext)
+    {
+        if(c.ext->va) destroyva(c.ext->va);
+        c.ext->va = NULL;
+        c.ext->tjoints = -1;
+        freeoctaentities(c);
+        freecubeext(c);
+    }
+    if(c.children)
+    {
+        uint filled = F_EMPTY;
+        loopi(8) 
+        {
+            discardchildren(c.children[i], fixtex, depth+1);
+            filled |= c.children[i].faces[0];
+        }
+        if(fixtex) 
+        {
+            loopi(6) c.texture[i] = getmippedtexture(c, i);
+            if(depth > 0 && filled != F_EMPTY) c.faces[0] = F_SOLID;
+        }
+        DELETEA(c.children);
+        allocnodes--;
+    }
+}
+
+void getcubevector(cube &c, int d, int x, int y, int z, ivec &p)
+{
+    ivec v(d, x, y, z);
+
+    loopi(3)
+        p[i] = edgeget(cubeedge(c, i, v[R[i]], v[C[i]]), v[D[i]]);
+}
+
+void setcubevector(cube &c, int d, int x, int y, int z, const ivec &p)
+{
+    ivec v(d, x, y, z);
+
+    loopi(3)
+        edgeset(cubeedge(c, i, v[R[i]], v[C[i]]), v[D[i]], p[i]);
+}
+
+static inline void getcubevector(cube &c, int i, ivec &p)
+{
+    p.x = edgeget(cubeedge(c, 0, (i>>R[0])&1, (i>>C[0])&1), (i>>D[0])&1);
+    p.y = edgeget(cubeedge(c, 1, (i>>R[1])&1, (i>>C[1])&1), (i>>D[1])&1);
+    p.z = edgeget(cubeedge(c, 2, (i>>R[2])&1, (i>>C[2])&1), (i>>D[2])&1);
+}
+
+static inline void setcubevector(cube &c, int i, const ivec &p)
+{
+    edgeset(cubeedge(c, 0, (i>>R[0])&1, (i>>C[0])&1), (i>>D[0])&1, p.x);
+    edgeset(cubeedge(c, 1, (i>>R[1])&1, (i>>C[1])&1), (i>>D[1])&1, p.y);
+    edgeset(cubeedge(c, 2, (i>>R[2])&1, (i>>C[2])&1), (i>>D[2])&1, p.z);
+}
+
+void optiface(uchar *p, cube &c)
+{
+    uint f = *(uint *)p;
+    if(((f>>4)&0x0F0F0F0FU) == (f&0x0F0F0F0FU)) emptyfaces(c);
+}
+
+void printcube()
+{
+    cube &c = lookupcube(lu); // assume this is cube being pointed at
+    conoutf(CON_DEBUG, "= %p = (%d, %d, %d) @ %d", (void *)&c, lu.x, lu.y, lu.z, lusize);
+    conoutf(CON_DEBUG, " x  %.8x", c.faces[0]);
+    conoutf(CON_DEBUG, " y  %.8x", c.faces[1]);
+    conoutf(CON_DEBUG, " z  %.8x", c.faces[2]);
+}
+
+COMMAND(printcube, "");
+
+bool isvalidcube(const cube &c)
+{
+    clipplanes p;
+    genclipplanes(c, ivec(0, 0, 0), 256, p);
+    loopi(8) // test that cube is convex
+    {
+        vec v = p.v[i];
+        loopj(p.size) if(p.p[j].dist(v)>1e-3f) return false;
+    }
+    return true;
+}
+
+void validatec(cube *c, int size)
+{
+    loopi(8)
+    {
+        if(c[i].children)
+        {
+            if(size<=1)
+            {
+                solidfaces(c[i]);
+                discardchildren(c[i], true);
+            }
+            else validatec(c[i].children, size>>1);
+        }
+        else if(size > 0x1000)
+        {
+            subdividecube(c[i], true, false);
+            validatec(c[i].children, size>>1);
+        }
+        else
+        {
+            loopj(3)
+            {
+                uint f = c[i].faces[j], e0 = f&0x0F0F0F0FU, e1 = (f>>4)&0x0F0F0F0FU;
+                if(e0 == e1 || ((e1+0x07070707U)|(e1-e0))&0xF0F0F0F0U)
+                {
+                    emptyfaces(c[i]);
+                    break;
+                }
+            }
+        }
+    }
+}
+
+ivec lu;
+int lusize;
+cube &lookupcube(const ivec &to, int tsize, ivec &ro, int &rsize)
+{
+    int tx = clamp(to.x, 0, worldsize-1),
+        ty = clamp(to.y, 0, worldsize-1),
+        tz = clamp(to.z, 0, worldsize-1);
+    int scale = worldscale-1, csize = abs(tsize);
+    cube *c = &worldroot[octastep(tx, ty, tz, scale)];
+    if(!(csize>>scale)) do
+    {
+        if(!c->children)
+        {
+            if(tsize > 0) do
+            {
+                subdividecube(*c);
+                scale--;
+                c = &c->children[octastep(tx, ty, tz, scale)];
+            } while(!(csize>>scale));
+            break;
+        }
+        scale--;
+        c = &c->children[octastep(tx, ty, tz, scale)];
+    } while(!(csize>>scale));
+    ro = ivec(tx, ty, tz).mask(~0U<<scale);
+    rsize = 1<<scale;
+    return *c;
+}
+
+int lookupmaterial(const vec &v)
+{
+    ivec o(v);
+    if(!insideworld(o)) return MAT_AIR;
+    int scale = worldscale-1;
+    cube *c = &worldroot[octastep(o.x, o.y, o.z, scale)];
+    while(c->children)
+    {
+        scale--;
+        c = &c->children[octastep(o.x, o.y, o.z, scale)];
+    }
+    return c->material;
+}
+
+const cube *neighbourstack[32];
+int neighbourdepth = -1;
+
+const cube &neighbourcube(const cube &c, int orient, const ivec &co, int size, ivec &ro, int &rsize)
+{
+    ivec n = co;
+    int dim = dimension(orient);
+    uint diff = n[dim];
+    if(dimcoord(orient)) n[dim] += size; else n[dim] -= size;
+    diff ^= n[dim];
+    if(diff >= uint(worldsize)) { ro = n; rsize = size; return c; }
+    int scale = worldscale;
+    const cube *nc = worldroot;
+    if(neighbourdepth >= 0)
+    {
+        scale -= neighbourdepth + 1;
+        diff >>= scale;
+        do { scale++; diff >>= 1; } while(diff);
+        nc = neighbourstack[worldscale - scale];
+    }
+    scale--;
+    nc = &nc[octastep(n.x, n.y, n.z, scale)];
+    if(!(size>>scale) && nc->children) do
+    {
+        scale--;
+        nc = &nc->children[octastep(n.x, n.y, n.z, scale)];
+    } while(!(size>>scale) && nc->children);
+    ro = n.mask(~0U<<scale);
+    rsize = 1<<scale;
+    return *nc;
+}
+
+////////// (re)mip //////////
+
+int getmippedtexture(const cube &p, int orient)
+{
+    cube *c = p.children;
+    int d = dimension(orient), dc = dimcoord(orient), texs[4] = { -1, -1, -1, -1 }, numtexs = 0;
+    loop(x, 2) loop(y, 2)
+    {
+        int n = octaindex(d, x, y, dc);
+        if(isempty(c[n]))
+        {
+            n = oppositeocta(d, n);
+            if(isempty(c[n]))
+                continue;
+        }
+        int tex = c[n].texture[orient];
+        if(tex > DEFAULT_SKY) loopi(numtexs) if(texs[i] == tex) return tex;
+        texs[numtexs++] = tex;
+    }
+    loopirev(numtexs) if(!i || texs[i] > DEFAULT_SKY) return texs[i];
+    return DEFAULT_GEOM;
+}
+
+void forcemip(cube &c, bool fixtex)
+{
+    cube *ch = c.children;
+    emptyfaces(c);
+
+    loopi(8) loopj(8)
+    {
+        int n = i^(j==3 ? 4 : (j==4 ? 3 : j));
+        if(!isempty(ch[n])) // breadth first search for cube near vert
+        {
+            ivec v;
+            getcubevector(ch[n], i, v);
+            // adjust vert to parent size
+            setcubevector(c, i, ivec(n, v, 8).shr(1));
+            break;
+        }
+    }
+
+    if(fixtex) loopj(6)
+        c.texture[j] = getmippedtexture(c, j);
+}
+
+static int midedge(const ivec &a, const ivec &b, int xd, int yd, bool &perfect)
+{
+    int ax = a[xd], ay = a[yd], bx = b[xd], by = b[yd];
+    if(ay==by) return ay;
+    if(ax==bx) { perfect = false; return ay; }
+    bool crossx = (ax<8 && bx>8) || (ax>8 && bx<8);
+    bool crossy = (ay<8 && by>8) || (ay>8 && by<8);
+    if(crossy && !crossx) { midedge(a,b,yd,xd,perfect); return 8; } // to test perfection
+    if(ax<=8 && bx<=8) return ax>bx ? ay : by;
+    if(ax>=8 && bx>=8) return ax<bx ? ay : by;
+    int risex = (by-ay)*(8-ax)*256;
+    int s = risex/(bx-ax);
+    int y = s/256 + ay;
+    if(((abs(s)&0xFF)!=0) || // ie: rounding error
+        (crossy && y!=8) ||
+        (y<0 || y>16)) perfect = false;
+    return crossy ? 8 : min(max(y, 0), 16);
+}
+
+static inline bool crosscenter(const ivec &a, const ivec &b, int xd, int yd)
+{
+    int ax = a[xd], ay = a[yd], bx = b[xd], by = b[yd];
+    return (((ax <= 8 && bx <= 8) || (ax >= 8 && bx >= 8)) &&
+            ((ay <= 8 && by <= 8) || (ay >= 8 && by >= 8))) ||
+           (ax + bx == 16 && ay + by == 16);
+}
+
+bool subdividecube(cube &c, bool fullcheck, bool brighten)
+{
+    if(c.children) return true;
+    if(c.ext) memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces));
+       if(isempty(c) || isentirelysolid(c))
+    {
+               c.children = newcubes(isempty(c) ? F_EMPTY : F_SOLID, c.material);
+        loopi(8)
+        {
+            loopl(6) c.children[i].texture[l] = c.texture[l];
+            if(brighten && !isempty(c)) brightencube(c.children[i]);
+        }
+        return true;
+    }
+    cube *ch = c.children = newcubes(F_SOLID, c.material);
+    bool perfect = true;
+    ivec v[8];
+    loopi(8)
+    {
+        getcubevector(c, i, v[i]);
+        v[i].mul(2);
+    }
+
+    loopj(6)
+    {
+        int d = dimension(j), z = dimcoord(j);
+        const ivec &v00 = v[octaindex(d, 0, 0, z)],
+                   &v10 = v[octaindex(d, 1, 0, z)],
+                   &v01 = v[octaindex(d, 0, 1, z)],
+                   &v11 = v[octaindex(d, 1, 1, z)];
+        int e[3][3];
+        // corners   
+        e[0][0] = v00[d];
+        e[0][2] = v01[d];
+        e[2][0] = v10[d];
+        e[2][2] = v11[d];
+        // edges
+        e[0][1] = midedge(v00, v01, C[d], d, perfect); 
+        e[1][0] = midedge(v00, v10, R[d], d, perfect);
+        e[1][2] = midedge(v11, v01, R[d], d, perfect);
+        e[2][1] = midedge(v11, v10, C[d], d, perfect); 
+        // center
+        bool p1 = perfect, p2 = perfect;
+        int c1 = midedge(v00, v11, R[d], d, p1);
+        int c2 = midedge(v01, v10, R[d], d, p2);
+        if(z ? c1 > c2 : c1 < c2)
+        {
+            e[1][1] = c1;
+            perfect = p1 && (c1 == c2 || crosscenter(v00, v11, C[d], R[d]));
+        }
+        else
+        {
+            e[1][1] = c2;
+            perfect = p2 && (c1 == c2 || crosscenter(v01, v10, C[d], R[d]));
+        }    
+
+        loopi(8)
+        {
+            ch[i].texture[j] = c.texture[j];
+            int rd = (i>>R[d])&1, cd = (i>>C[d])&1, dd = (i>>D[d])&1;
+            edgeset(cubeedge(ch[i], d, 0, 0), z, clamp(e[rd][cd] - dd*8, 0, 8));
+            edgeset(cubeedge(ch[i], d, 1, 0), z, clamp(e[1+rd][cd] - dd*8, 0, 8));
+            edgeset(cubeedge(ch[i], d, 0, 1), z, clamp(e[rd][1+cd] - dd*8, 0, 8));
+            edgeset(cubeedge(ch[i], d, 1, 1), z, clamp(e[1+rd][1+cd] - dd*8, 0, 8));
+        }
+    }
+
+    validatec(ch);
+    if(fullcheck) loopi(8) if(!isvalidcube(ch[i])) // not so good...
+    {
+        emptyfaces(ch[i]);
+        perfect=false;
+    }
+    if(brighten) loopi(8) if(!isempty(ch[i])) brightencube(ch[i]);
+    return perfect;
+}
+
+bool crushededge(uchar e, int dc) { return dc ? e==0 : e==0x88; }
+
+int visibleorient(const cube &c, int orient)
+{
+    loopi(2)
+    {
+        int a = faceedgesidx[orient][i*2 + 0];
+        int b = faceedgesidx[orient][i*2 + 1];
+        loopj(2)
+        {
+            if(crushededge(c.edges[a],j) &&
+               crushededge(c.edges[b],j) &&
+                touchingface(c, orient)) return ((a>>2)<<1) + j;
+        }
+    }
+    return orient;
+}
+
+VAR(mipvis, 0, 0, 1);
+
+static int remipprogress = 0, remiptotal = 0;
+
+bool remip(cube &c, const ivec &co, int size)
+{
+    cube *ch = c.children;
+    if(!ch)
+    {
+        if(size<<1 <= 0x1000) return true;
+        subdividecube(c);
+        ch = c.children;
+    }
+    else if((remipprogress++&0xFFF)==1) renderprogress(float(remipprogress)/remiptotal, "remipping...");
+
+    bool perfect = true;
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        if(!remip(ch[i], o, size>>1)) perfect = false;
+    }
+
+    solidfaces(c); // so texmip is more consistent
+    loopj(6)
+        c.texture[j] = getmippedtexture(c, j); // parents get child texs regardless
+
+    if(!perfect) return false;
+    if(size<<1 > 0x1000) return false;
+
+    ushort mat = MAT_AIR;
+    loopi(8)
+    {
+        mat = ch[i].material;
+        if((mat&MATF_CLIP) == MAT_NOCLIP || mat&MAT_ALPHA)
+        {
+            if(i > 0) return false;
+            while(++i < 8) if(ch[i].material != mat) return false;
+            break;
+        }
+        else if(!isentirelysolid(ch[i]))
+        {
+            while(++i < 8)
+            {
+                int omat = ch[i].material;
+                if(isentirelysolid(ch[i]) ? (omat&MATF_CLIP) == MAT_NOCLIP || omat&MAT_ALPHA : mat != omat) return false;
+            }
+            break;
+        }
+    }
+
+    cube n = c;
+    n.ext = NULL;
+    forcemip(n);
+    n.children = NULL;
+    if(!subdividecube(n, false, false))
+        { freeocta(n.children); return false; }
+
+    cube *nh = n.children;
+    uchar vis[6] = {0, 0, 0, 0, 0, 0};
+    loopi(8)
+    {
+        if(ch[i].faces[0] != nh[i].faces[0] ||
+           ch[i].faces[1] != nh[i].faces[1] ||
+           ch[i].faces[2] != nh[i].faces[2])
+            { freeocta(nh); return false; }
+
+        if(isempty(ch[i]) && isempty(nh[i])) continue;
+
+        ivec o(i, co, size);
+        loop(orient, 6)
+            if(visibleface(ch[i], orient, o, size, MAT_AIR, (mat&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA))
+            {
+                if(ch[i].texture[orient] != n.texture[orient]) { freeocta(nh); return false; }
+                vis[orient] |= 1<<i;
+            }
+    }
+    if(mipvis) loop(orient, 6)
+    {
+        int mask = 0;
+        loop(x, 2) loop(y, 2) mask |= 1<<octaindex(dimension(orient), x, y, dimcoord(orient));
+        if(vis[orient]&mask && (vis[orient]&mask)!=mask) { freeocta(nh); return false; }
+    }
+
+    freeocta(nh);
+    discardchildren(c);
+    loopi(3) c.faces[i] = n.faces[i];
+    c.material = mat;
+    loopi(6) if(vis[i]) c.visible |= 1<<i;
+    if(c.visible) c.visible |= 0x40;
+    brightencube(c);
+    return true;
+}
+
+void mpremip(bool local)
+{
+    extern selinfo sel;
+    if(local) game::edittrigger(sel, EDIT_REMIP);
+    remipprogress = 1;
+    remiptotal = allocnodes;
+    loopi(8)
+    {
+        ivec o(i, ivec(0, 0, 0), worldsize>>1);
+        remip(worldroot[i], o, worldsize>>2);
+    }
+    calcmerges();
+    if(!local) allchanged();
+}
+
+void remip_()
+{
+    mpremip(true);
+    allchanged();
+}
+
+COMMANDN(remip, remip_, "");
+
+static inline int edgeval(cube &c, const ivec &p, int dim, int coord)
+{
+    return edgeget(cubeedge(c, dim, p[R[dim]]>>3, p[C[dim]]>>3), coord);
+}
+
+void genvertp(cube &c, ivec &p1, ivec &p2, ivec &p3, plane &pl, bool solid = false)
+{
+    int dim = 0;
+    if(p1.y==p2.y && p2.y==p3.y) dim = 1;
+    else if(p1.z==p2.z && p2.z==p3.z) dim = 2;
+
+    int coord = p1[dim];
+    ivec v1(p1), v2(p2), v3(p3);
+    v1[dim] = solid ? coord*8 : edgeval(c, p1, dim, coord);
+    v2[dim] = solid ? coord*8 : edgeval(c, p2, dim, coord);
+    v3[dim] = solid ? coord*8 : edgeval(c, p3, dim, coord);
+
+    pl.toplane(vec(v1), vec(v2), vec(v3));
+}
+
+static bool threeplaneintersect(plane &pl1, plane &pl2, plane &pl3, vec &dest)
+{
+    vec &t1 = dest, t2, t3, t4;
+    t1.cross(pl1, pl2); t4 = t1; t1.mul(pl3.offset);
+    t2.cross(pl3, pl1);          t2.mul(pl2.offset);
+    t3.cross(pl2, pl3);          t3.mul(pl1.offset);
+    t1.add(t2);
+    t1.add(t3);
+    t1.mul(-1);
+    float d = t4.dot(pl3);
+    if(d==0) return false;
+    t1.div(d);
+    return true;
+}
+
+static void genedgespanvert(ivec &p, cube &c, vec &v)
+{
+    ivec p1(8-p.x, p.y, p.z);
+    ivec p2(p.x, 8-p.y, p.z);
+    ivec p3(p.x, p.y, 8-p.z);
+
+    plane plane1, plane2, plane3;
+    genvertp(c, p, p1, p2, plane1);
+    genvertp(c, p, p2, p3, plane2);
+    genvertp(c, p, p3, p1, plane3);
+    if(plane1==plane2) genvertp(c, p, p1, p2, plane1, true);
+    if(plane1==plane3) genvertp(c, p, p1, p2, plane1, true);
+    if(plane2==plane3) genvertp(c, p, p2, p3, plane2, true);
+
+    ASSERT(threeplaneintersect(plane1, plane2, plane3, v));
+    //ASSERT(v.x>=0 && v.x<=8);
+    //ASSERT(v.y>=0 && v.y<=8);
+    //ASSERT(v.z>=0 && v.z<=8);
+    v.x = max(0.0f, min(8.0f, v.x));
+    v.y = max(0.0f, min(8.0f, v.y));
+    v.z = max(0.0f, min(8.0f, v.z));
+}
+
+void edgespan2vectorcube(cube &c)
+{
+    if(isentirelysolid(c) || isempty(c)) return;
+    cube o = c;
+    loop(x, 2) loop(y, 2) loop(z, 2)
+    {
+        ivec p(8*x, 8*y, 8*z);
+        vec v;
+        genedgespanvert(p, o, v);
+
+        edgeset(cubeedge(c, 0, y, z), x, int(v.x+0.49f));
+        edgeset(cubeedge(c, 1, z, x), y, int(v.y+0.49f));
+        edgeset(cubeedge(c, 2, x, y), z, int(v.z+0.49f));
+    }
+}
+
+const ivec cubecoords[8] = // verts of bounding cube
+{
+#define GENCUBEVERT(n, x, y, z) ivec(x, y, z),
+    GENCUBEVERTS(0, 8, 0, 8, 0, 8)
+#undef GENCUBEVERT 
+};
+
+template<class T>
+static inline void gencubevert(const cube &c, int i, T &v)
+{
+    switch(i)
+    {
+        default:
+#define GENCUBEVERT(n, x, y, z) \
+        case n: \
+            v = T(edgeget(cubeedge(c, 0, y, z), x), \
+                  edgeget(cubeedge(c, 1, z, x), y), \
+                  edgeget(cubeedge(c, 2, x, y), z)); \
+            break;
+        GENCUBEVERTS(0, 1, 0, 1, 0, 1)
+#undef GENCUBEVERT
+    }
+}
+
+void genfaceverts(const cube &c, int orient, ivec v[4])
+{
+    switch(orient)
+    {
+        default:
+#define GENFACEORIENT(o, v0, v1, v2, v3) \
+        case o: v0 v1 v2 v3 break;
+#define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \
+            v[n] = ivec(edgeget(cubeedge(c, 0, y, z), x), \
+                        edgeget(cubeedge(c, 1, z, x), y), \
+                        edgeget(cubeedge(c, 2, x, y), z));
+        GENFACEVERTS(0, 1, 0, 1, 0, 1, , , , , , )
+    #undef GENFACEORIENT
+    #undef GENFACEVERT
+    }
+}
+
+const ivec facecoords[6][4] =
+{
+#define GENFACEORIENT(o, v0, v1, v2, v3) \
+    { v0, v1, v2, v3 },
+#define GENFACEVERT(o, n, x,y,z, xv,yv,zv) \
+        ivec(x,y,z)
+    GENFACEVERTS(0, 8, 0, 8, 0, 8, , , , , , )
+#undef GENFACEORIENT
+#undef GENFACEVERT
+};
+
+const uchar fv[6][4] = // indexes for cubecoords, per each vert of a face orientation
+{
+    { 2, 1, 6, 5 },
+    { 3, 4, 7, 0 },
+    { 4, 5, 6, 7 },
+    { 1, 2, 3, 0 },
+    { 6, 1, 0, 7 },
+    { 5, 4, 3, 2 },
+};
+
+const uchar fvmasks[64] = // mask of verts used given a mask of visible face orientations
+{
+    0x00, 0x66, 0x99, 0xFF, 0xF0, 0xF6, 0xF9, 0xFF,
+    0x0F, 0x6F, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+    0xC3, 0xE7, 0xDB, 0xFF, 0xF3, 0xF7, 0xFB, 0xFF,
+    0xCF, 0xEF, 0xDF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+    0x3C, 0x7E, 0xBD, 0xFF, 0xFC, 0xFE, 0xFD, 0xFF,
+    0x3F, 0x7F, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+};
+
+const uchar faceedgesidx[6][4] = // ordered edges surrounding each orient
+{//0..1 = row edges, 2..3 = column edges
+    { 4,  5,  8, 10 },
+    { 6,  7,  9, 11 },
+    { 8,  9,  0, 2 },
+    { 10, 11, 1, 3 },
+    { 0,  1,  4, 6 },
+    { 2,  3,  5, 7 },
+};
+
+bool flataxisface(const cube &c, int orient)
+{
+    uint face = c.faces[dimension(orient)];
+    if(dimcoord(orient)) face >>= 4;
+    return (face&0x0F0F0F0F) == 0x01010101*(face&0x0F);
+}
+
+bool collideface(const cube &c, int orient)
+{
+    if(flataxisface(c, orient))
+    {
+        uchar r1 = c.edges[faceedgesidx[orient][0]], r2 = c.edges[faceedgesidx[orient][1]];
+        if(uchar((r1>>4)|(r2&0xF0)) == uchar((r1&0x0F)|(r2<<4))) return false;
+        uchar c1 = c.edges[faceedgesidx[orient][2]], c2 = c.edges[faceedgesidx[orient][3]];
+        if(uchar((c1>>4)|(c2&0xF0)) == uchar((c1&0x0F)|(c2<<4))) return false;
+    }
+    return true;
+}
+
+bool touchingface(const cube &c, int orient)
+{
+    uint face = c.faces[dimension(orient)];
+    return dimcoord(orient) ? (face&0xF0F0F0F0)==0x80808080 : (face&0x0F0F0F0F)==0;
+}
+
+bool notouchingface(const cube &c, int orient)
+{
+    uint face = c.faces[dimension(orient)];
+    return dimcoord(orient) ? (face&0x80808080)==0 : ((0x88888888-face)&0x08080808) == 0;
+}   
+
+int faceconvexity(const ivec v[4])
+{
+    ivec n;
+    n.cross(ivec(v[1]).sub(v[0]), ivec(v[2]).sub(v[0]));
+    return ivec(v[0]).sub(v[3]).dot(n);
+    // 1 if convex, -1 if concave, 0 if flat
+}
+
+int faceconvexity(const vertinfo *verts, int numverts, int size)
+{
+    if(numverts < 4) return 0;
+    ivec v0 = verts[0].getxyz(),
+         e1 = verts[1].getxyz().sub(v0),
+         e2 = verts[2].getxyz().sub(v0),
+         n;
+    if(size >= (8<<5))
+    {
+        if(size >= (8<<10)) n.cross(e1.shr(10), e2.shr(10));
+        else n.cross(e1, e2).shr(10);
+    }
+    else n.cross(e1, e2);
+    return verts[3].getxyz().sub(v0).dot(n);
+}
+
+int faceconvexity(const ivec v[4], int &vis)
+{
+    ivec e1, e2, e3, n;
+    n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+    int convex = (e3 = v[0]).sub(v[3]).dot(n);
+    if(!convex)
+    {
+        if(ivec().cross(e3, e2).iszero()) { if(!n.iszero()) vis = 1; }
+        else if(n.iszero()) { vis = 2; }
+        return 0;
+    }
+    return convex;
+} 
+
+int faceconvexity(const cube &c, int orient)
+{
+    if(flataxisface(c, orient)) return 0;
+    ivec v[4];
+    genfaceverts(c, orient, v); 
+    return faceconvexity(v);
+}
+
+int faceorder(const cube &c, int orient) // gets above 'fv' so that each face is convex
+{
+    return faceconvexity(c, orient)<0 ? 1 : 0;
+}
+
+static inline void faceedges(const cube &c, int orient, uchar edges[4])
+{
+    loopk(4) edges[k] = c.edges[faceedgesidx[orient][k]];
+}
+
+uint faceedges(const cube &c, int orient)
+{
+    union { uchar edges[4]; uint face; } u;
+    faceedges(c, orient, u.edges);
+    return u.face;
+}
+
+static inline int genfacevecs(const cube &cu, int orient, const ivec &pos, int size, bool solid, ivec2 *fvecs, const ivec *v = NULL)
+{
+    int i = 0;
+    if(solid)
+    {
+        switch(orient)
+        {
+        #define GENFACEORIENT(orient, v0, v1, v2, v3) \
+            case orient: \
+            { \
+                if(dimcoord(orient)) { v0 v1 v2 v3 } else { v3 v2 v1 v0 } \
+                break; \
+            }
+        #define GENFACEVERT(orient, vert, xv,yv,zv, x,y,z) \
+            { ivec2 &f = fvecs[i]; x ((xv)<<3); y ((yv)<<3); z ((zv)<<3); i++; }
+            GENFACEVERTS(pos.x, pos.x+size, pos.y, pos.y+size, pos.z, pos.z+size, f.x = , f.x = , f.y = , f.y = , (void), (void))
+        #undef GENFACEVERT
+        }
+        return 4;
+    }
+    ivec buf[4];
+    if(!v) { genfaceverts(cu, orient, buf); v = buf; }
+    ivec2 prev(INT_MAX, INT_MAX);
+    switch(orient)
+    {
+    #define GENFACEVERT(orient, vert, sx,sy,sz, dx,dy,dz) \
+        { \
+            const ivec &e = v[vert]; \
+            ivec ef; \
+            ef.dx = e.sx; ef.dy = e.sy; ef.dz = e.sz; \
+            if(ef.z == dimcoord(orient)*8) \
+            { \
+                ivec2 &f = fvecs[i]; \
+                ivec pf; \
+                pf.dx = pos.sx; pf.dy = pos.sy; pf.dz = pos.sz; \
+                f = ivec2(ef.x*size + (pf.x<<3), ef.y*size + (pf.y<<3)); \
+                if(f != prev) { prev = f; i++; } \
+            } \
+        } 
+        GENFACEVERTS(x, x, y, y, z, z, x, x, y, y, z, z)
+    #undef GENFACEORIENT
+    #undef GENFACEVERT
+    }
+    if(fvecs[0] == prev) i--;
+    return i;
+}
+
+static inline int clipfacevecy(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 &r)
+{
+    if(dir.x >= 0)
+    {
+        if(cx <= o.x || cx >= o.x+dir.x) return 0;
+    }
+    else if(cx <= o.x+dir.x || cx >= o.x) return 0;
+
+    int t = (o.y-cy) + (cx-o.x)*dir.y/dir.x;
+    if(t <= 0 || t >= size) return 0;
+
+    r.x = cx;
+    r.y = cy + t;
+    return 1;
+}
+
+static inline int clipfacevecx(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 &r)
+{
+    if(dir.y >= 0)
+    {
+        if(cy <= o.y || cy >= o.y+dir.y) return 0;
+    }
+    else if(cy <= o.y+dir.y || cy >= o.y) return 0;
+
+    int t = (o.x-cx) + (cy-o.y)*dir.x/dir.y;
+    if(t <= 0 || t >= size) return 0;
+
+    r.x = cx + t;
+    r.y = cy;
+    return 1;
+}
+
+static inline int clipfacevec(const ivec2 &o, const ivec2 &dir, int cx, int cy, int size, ivec2 *rvecs)
+{
+    int r = 0;
+
+    if(o.x >= cx && o.x <= cx+size &&
+       o.y >= cy && o.y <= cy+size &&
+       ((o.x != cx && o.x != cx+size) || (o.y != cy && o.y != cy+size)))
+    {
+        rvecs[0].x = o.x;
+        rvecs[0].y = o.y;
+        r++;
+    }
+
+    r += clipfacevecx(o, dir, cx, cy, size, rvecs[r]);
+    r += clipfacevecx(o, dir, cx, cy+size, size, rvecs[r]);
+    r += clipfacevecy(o, dir, cx, cy, size, rvecs[r]);
+    r += clipfacevecy(o, dir, cx+size, cy, size, rvecs[r]);
+
+    ASSERT(r <= 2);
+    return r;
+}
+
+static inline bool insideface(const ivec2 *p, int nump, const ivec2 *o, int numo)
+{
+    int bounds = 0;
+    ivec2 prev = o[numo-1];
+    loopi(numo)
+    {
+        const ivec2 &cur = o[i];
+        ivec2 dir(cur.x-prev.x, cur.y-prev.y);
+        int offset = dir.x*prev.y - dir.y*prev.x;
+        loopj(nump) if(dir.x*p[j].y - dir.y*p[j].x > offset) return false;
+        bounds++;
+        prev = cur;
+    }
+    return bounds>=3;
+}
+
+static inline int clipfacevecs(const ivec2 *o, int numo, int cx, int cy, int size, ivec2 *rvecs)
+{
+    cx <<= 3;
+    cy <<= 3;
+    size <<= 3;
+
+    int r = 0;
+    ivec2 prev = o[numo-1];
+    loopi(numo)
+    {
+        const ivec2 &cur = o[i];
+        r += clipfacevec(prev, ivec2(cur.x-prev.x, cur.y-prev.y), cx, cy, size, &rvecs[r]);
+        prev = cur;
+    }
+    ivec2 corner[4] = {ivec2(cx, cy), ivec2(cx+size, cy), ivec2(cx+size, cy+size), ivec2(cx, cy+size)};
+    loopi(4) if(insideface(&corner[i], 1, o, numo)) rvecs[r++] = corner[i];
+    ASSERT(r <= 8);
+    return r;
+}
+
+bool collapsedface(const cube &c, int orient)
+{
+    int e0 = c.edges[faceedgesidx[orient][0]], e1 = c.edges[faceedgesidx[orient][1]],
+        e2 = c.edges[faceedgesidx[orient][2]], e3 = c.edges[faceedgesidx[orient][3]],
+        face = dimension(orient)*4,
+        f0 = c.edges[face+0], f1 = c.edges[face+1],
+        f2 = c.edges[face+2], f3 = c.edges[face+3];
+    if(dimcoord(orient)) { f0 >>= 4; f1 >>= 4; f2 >>= 4; f3 >>= 4; }
+    else { f0 &= 0xF; f1 &= 0xF; f2 &= 0xF; f3 &= 0xF; }
+    ivec v0(e0&0xF, e2&0xF, f0),
+         v1(e0>>4, e3&0xF, f1),
+         v2(e1>>4, e3>>4, f3),
+         v3(e1&0xF, e2>>4, f2);
+    return ivec().cross(v1.sub(v0), v2.sub(v0)).iszero() &&
+           ivec().cross(v2, v3.sub(v0)).iszero();
+}
+
+static inline bool occludesface(const cube &c, int orient, const ivec &o, int size, const ivec &vo, int vsize, ushort vmat, ushort nmat, ushort matmask, const ivec2 *vf, int numv)
+{
+    int dim = dimension(orient);
+    if(!c.children)
+    {
+         if(nmat != MAT_AIR && (c.material&matmask) == nmat)
+         {
+            ivec2 nf[8];
+            return clipfacevecs(vf, numv, o[C[dim]], o[R[dim]], size, nf) < 3;
+         }
+         if(isentirelysolid(c)) return true;
+         if(vmat != MAT_AIR && ((c.material&matmask) == vmat || (isliquid(vmat) && isclipped(c.material&MATF_VOLUME)))) return true;
+         if(touchingface(c, orient) && faceedges(c, orient) == F_SOLID) return true;
+         ivec2 cf[8];
+         int numc = clipfacevecs(vf, numv, o[C[dim]], o[R[dim]], size, cf);
+         if(numc < 3) return true;
+         if(isempty(c) || notouchingface(c, orient)) return false;
+         ivec2 of[4];
+         int numo = genfacevecs(c, orient, o, size, false, of);
+         return numo >= 3 && insideface(cf, numc, of, numo);
+    }
+
+    size >>= 1;
+    int coord = dimcoord(orient);
+    loopi(8) if(octacoord(dim, i) == coord)
+    {
+        if(!occludesface(c.children[i], orient, ivec(i, o, size), size, vo, vsize, vmat, nmat, matmask, vf, numv)) return false;
+    }
+
+    return true;
+}
+
+bool visibleface(const cube &c, int orient, const ivec &co, int size, ushort mat, ushort nmat, ushort matmask)
+{
+    if(mat != MAT_AIR)
+    {
+        if(faceedges(c, orient)==F_SOLID && touchingface(c, orient)) return false;
+    }
+    else
+    {
+        if(collapsedface(c, orient)) return false;
+        if(!touchingface(c, orient)) return true;
+    }
+
+    ivec no;
+    int nsize;
+    const cube &o = neighbourcube(c, orient, co, size, no, nsize);
+    if(&o==&c) return false;
+
+    int opp = opposite(orient);
+    if(nsize > size || (nsize == size && !o.children))
+    {
+        if(nmat != MAT_AIR && (o.material&matmask) == nmat) return true;
+        if(isentirelysolid(o)) return false;
+        if(mat != MAT_AIR && ((o.material&matmask) == mat || (isliquid(mat) && (o.material&MATF_VOLUME) == MAT_GLASS))) return false;
+        if(isempty(o) || notouchingface(o, opp)) return true;
+        if(touchingface(o, opp) && faceedges(o, opp) == F_SOLID) return false;
+
+        ivec vo = ivec(co).mask(0xFFF);
+        no.mask(0xFFF);
+        ivec2 cf[4], of[4];
+        int numc = genfacevecs(c, orient, vo, size, mat != MAT_AIR, cf),
+            numo = genfacevecs(o, opp, no, nsize, false, of);
+        return numo < 3 || !insideface(cf, numc, of, numo);
+    }
+
+
+    ivec vo = ivec(co).mask(0xFFF);
+    no.mask(0xFFF);
+    ivec2 cf[4];
+    int numc = genfacevecs(c, orient, vo, size, mat != MAT_AIR, cf);
+    return !occludesface(o, opp, no, nsize, vo, size, mat, nmat, matmask, cf, numc);
+}
+
+int classifyface(const cube &c, int orient, const ivec &co, int size)
+{
+    if(collapsedface(c, orient)) return 0;
+    int vismask = (c.material&MATF_CLIP) == MAT_NOCLIP ? 1 : 3;
+    if(!touchingface(c, orient)) return vismask;
+
+    ivec no;
+    int nsize;
+    const cube &o = neighbourcube(c, orient, co, size, no, nsize);
+    if(&o==&c) return 0;
+
+    int vis = 0, opp = opposite(orient);
+    if(nsize > size || (nsize == size && !o.children))
+    {
+        if((~c.material & o.material) & MAT_ALPHA) vis |= 1;
+        if((o.material&MATF_CLIP) == MAT_NOCLIP) vis |= vismask&2;
+        if(vis == vismask || isentirelysolid(o)) return vis;
+        if(isempty(o) || notouchingface(o, opp)) return vismask;
+        if(touchingface(o, opp) && faceedges(o, opp) == F_SOLID) return vis;
+
+        ivec vo = ivec(co).mask(0xFFF);
+        no.mask(0xFFF);
+        ivec2 cf[4], of[4];
+        int numc = genfacevecs(c, orient, vo, size, false, cf),
+            numo = genfacevecs(o, opp, no, nsize, false, of);
+        if(numo < 3 || !insideface(cf, numc, of, numo)) return vismask;
+        return vis;
+    }
+
+    ivec vo = ivec(co).mask(0xFFF);
+    no.mask(0xFFF);
+    ivec2 cf[4];
+    int numc = genfacevecs(c, orient, vo, size, false, cf);
+    if(!occludesface(o, opp, no, nsize, vo, size, MAT_AIR, (c.material&MAT_ALPHA)^MAT_ALPHA, MAT_ALPHA, cf, numc)) vis |= 1;
+    if(vismask&2 && !occludesface(o, opp, no, nsize, vo, size, MAT_AIR, MAT_NOCLIP, MATF_CLIP, cf, numc)) vis |= 2;
+    return vis; 
+}
+
+// more expensive version that checks both triangles of a face independently
+int visibletris(const cube &c, int orient, const ivec &co, int size, ushort nmat, ushort matmask)
+{
+    int vis = 3, touching = 0xF;
+    ivec v[4], e1, e2, e3, n;
+    genfaceverts(c, orient, v);
+    n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+    int convex = (e3 = v[0]).sub(v[3]).dot(n);
+    if(!convex)
+    {
+        if(ivec().cross(e3, e2).iszero() || v[1] == v[3]) { if(n.iszero()) return 0; vis = 1; touching = 0xF&~(1<<3); }
+        else if(n.iszero()) { vis = 2; touching = 0xF&~(1<<1); }
+    }
+
+    int dim = dimension(orient), coord = dimcoord(orient);
+    if(v[0][dim] != coord*8) touching &= ~(1<<0);
+    if(v[1][dim] != coord*8) touching &= ~(1<<1);
+    if(v[2][dim] != coord*8) touching &= ~(1<<2);
+    if(v[3][dim] != coord*8) touching &= ~(1<<3);
+    static const int notouchmasks[2][16] = // mask of triangles not touching
+    { // order 0: flat or convex
+       // 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15
+        { 3, 3, 3, 3, 3, 3, 3, 2, 3, 3, 3, 3, 3, 1, 3, 0 },
+      // order 1: concave
+        { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 2, 0 },
+    };
+    int order = convex < 0 ? 1 : 0, notouch = notouchmasks[order][touching];
+    if((vis&notouch)==vis) return vis;
+
+    ivec no;
+    int nsize;
+    const cube &o = neighbourcube(c, orient, co, size, no, nsize);
+    if(&o==&c) return 0;
+    
+    if((c.material&matmask) == nmat) nmat = MAT_AIR;
+
+    ivec vo = ivec(co).mask(0xFFF);
+    no.mask(0xFFF);
+    ivec2 cf[4], of[4];
+    int opp = opposite(orient), numo = 0, numc;
+    if(nsize > size || (nsize == size && !o.children))
+    {
+        if(isempty(o) || notouchingface(o, opp)) return vis;
+        if(nmat != MAT_AIR && (o.material&matmask) == nmat) return vis;
+        if(isentirelysolid(o) || (touchingface(o, opp) && faceedges(o, opp) == F_SOLID)) return vis&notouch;
+
+        numc = genfacevecs(c, orient, vo, size, false, cf, v);
+        numo = genfacevecs(o, opp, no, nsize, false, of);
+        if(numo < 3) return vis;
+        if(insideface(cf, numc, of, numo)) return vis&notouch;
+    }
+    else
+    {
+        numc = genfacevecs(c, orient, vo, size, false, cf, v);
+        if(occludesface(o, opp, no, nsize, vo, size, MAT_AIR, nmat, matmask, cf, numc)) return vis&notouch;
+    }
+    if(vis != 3 || notouch) return vis;
+
+    static const int triverts[2][2][2][3] =
+    { // order
+        { // coord
+            { { 1, 2, 3 }, { 0, 1, 3 } }, // verts
+            { { 0, 1, 2 }, { 0, 2, 3 } }
+        },
+        { // coord
+            { { 0, 1, 2 }, { 3, 0, 2 } }, // verts
+            { { 1, 2, 3 }, { 1, 3, 0 } }
+        }
+    };
+
+    do
+    {
+        loopi(2)
+        {
+            const int *verts = triverts[order][coord][i];
+            ivec2 tf[3] = { cf[verts[0]], cf[verts[1]], cf[verts[2]] };
+            if(numo > 0) { if(!insideface(tf, 3, of, numo)) continue; }
+            else if(!occludesface(o, opp, no, nsize, vo, size, MAT_AIR, nmat, matmask, tf, 3)) continue;
+            return vis & ~(1<<i);
+        }
+        vis |= 4;
+    } while(++order <= 1);
+
+    return 3;
+}
+
+void calcvert(const cube &c, const ivec &co, int size, ivec &v, int i, bool solid)
+{
+    if(solid) v = cubecoords[i]; else gencubevert(c, i, v);
+    // avoid overflow
+    if(size>=8) v.mul(size/8);
+    else v.div(8/size);
+    v.add(ivec(co).shl(3));
+}
+
+void calcvert(const cube &c, const ivec &co, int size, vec &v, int i, bool solid)
+{
+    if(solid) v = vec(cubecoords[i]); else gencubevert(c, i, v);
+    v.mul(size/8.0f).add(vec(co));
+}
+
+int genclipplane(const cube &c, int orient, vec *v, plane *clip)
+{
+    int planes = 0, convex = faceconvexity(c, orient), order = convex < 0 ? 1 : 0;
+    const vec &v0 = v[fv[orient][order]], &v1 = v[fv[orient][order+1]], &v2 = v[fv[orient][order+2]], &v3 = v[fv[orient][(order+3)&3]];
+    if(v0==v2) return 0;
+    if(v0!=v1 && v1!=v2) clip[planes++].toplane(v0, v1, v2);
+    if(v0!=v3 && v2!=v3 && (!planes || convex)) clip[planes++].toplane(v0, v2, v3);
+    return planes;
+}
+
+void genclipplanes(const cube &c, const ivec &co, int size, clipplanes &p, bool collide)
+{
+    // generate tight bounding box
+    calcvert(c, co, size, p.v[0], 0);
+    vec mx = p.v[0], mn = p.v[0];
+    for(int i = 1; i < 8; i++)
+    {
+        calcvert(c, co, size, p.v[i], i);
+        mx.max(p.v[i]);
+        mn.min(p.v[i]);
+    }
+
+    p.r = mx.sub(mn).mul(0.5f);
+    p.o = mn.add(p.r);
+
+    p.size = 0;
+    p.visible = 0;
+    if(collide || (c.visible&0xC0) == 0x40)
+    {
+        loopi(6) if(c.visible&(1<<i))
+        {
+            int vis;
+            if(flataxisface(c, i)) p.visible |= 1<<i;
+            else if((vis = visibletris(c, i, co, size, MAT_NOCLIP, MATF_CLIP)))
+            {
+                int convex = faceconvexity(c, i), order = vis&4 || convex < 0 ? 1 : 0;
+                const vec &v0 = p.v[fv[i][order]], &v1 = p.v[fv[i][order+1]], &v2 = p.v[fv[i][order+2]], &v3 = p.v[fv[i][(order+3)&3]];
+                if(vis&1) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v1, v2); }
+                if(vis&2 && (!(vis&1) || convex)) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v2, v3); }
+            }
+        }
+    }
+    else if(c.visible&0x80)
+    {
+        int vis;
+        loopi(6) if((vis = visibletris(c, i, co, size)))
+        {
+            if(flataxisface(c, i)) p.visible |= 1<<i;
+            else
+            {
+                int convex = faceconvexity(c, i), order = vis&4 || convex < 0 ? 1 : 0;
+                const vec &v0 = p.v[fv[i][order]], &v1 = p.v[fv[i][order+1]], &v2 = p.v[fv[i][order+2]], &v3 = p.v[fv[i][(order+3)&3]];
+                if(vis&1) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v1, v2); }
+                if(vis&2 && (!(vis&1) || convex)) { p.side[p.size] = i; p.p[p.size++].toplane(v0, v2, v3); }
+            }
+        }
+    }
+}
+
+static inline bool mergefacecmp(const facebounds &x, const facebounds &y)
+{
+    if(x.v2 < y.v2) return true;
+    if(x.v2 > y.v2) return false;
+    if(x.u1 < y.u1) return true;
+    if(x.u1 > y.u1) return false;
+    return false;
+}
+
+static int mergefacev(int orient, facebounds *m, int sz, facebounds &n)
+{
+    for(int i = sz-1; i >= 0; --i)
+    {
+        if(m[i].v2 < n.v1) break;
+        if(m[i].v2 == n.v1 && m[i].u1 == n.u1 && m[i].u2 == n.u2)
+        {
+            n.v1 = m[i].v1;
+            memmove(&m[i], &m[i+1], (sz - (i+1)) * sizeof(facebounds));
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int mergefaceu(int orient, facebounds &m, facebounds &n)
+{
+    if(m.v1 == n.v1 && m.v2 == n.v2 && m.u2 == n.u1)
+    {
+        n.u1 = m.u1;
+        return 1;
+    }
+    return 0;
+}
+
+static int mergeface(int orient, facebounds *m, int sz, facebounds &n)
+{
+    for(bool merged = false; sz; merged = true)
+    {
+        int vmerged = mergefacev(orient, m, sz, n);
+        sz -= vmerged;
+        if(!vmerged && merged) break;
+        if(!sz) break;
+        int umerged = mergefaceu(orient, m[sz-1], n);
+        sz -= umerged;
+        if(!umerged) break;
+    }
+    m[sz++] = n;
+    return sz;
+}
+
+int mergefaces(int orient, facebounds *m, int sz)
+{
+    quicksort(m, sz, mergefacecmp);
+
+    int nsz = 0;
+    loopi(sz) nsz = mergeface(orient, m, nsz, m[i]);
+    return nsz;
+}
+
+struct cfkey
+{
+    uchar orient;
+    ushort material, tex;
+    ivec n;
+    int offset;
+};
+
+static inline bool htcmp(const cfkey &x, const cfkey &y)
+{
+    return x.orient == y.orient && x.tex == y.tex && x.n == y.n && x.offset == y.offset && x.material==y.material;
+}
+
+static inline uint hthash(const cfkey &k)
+{
+    return hthash(k.n)^k.offset^k.tex^k.orient^k.material;
+}
+
+void mincubeface(const cube &cu, int orient, const ivec &o, int size, const facebounds &orig, facebounds &cf, ushort nmat, ushort matmask)
+{
+    int dim = dimension(orient);
+    if(cu.children)
+    {
+        size >>= 1;
+        int coord = dimcoord(orient);
+        loopi(8) if(octacoord(dim, i) == coord)
+            mincubeface(cu.children[i], orient, ivec(i, o, size), size, orig, cf, nmat, matmask);
+        return;
+    }
+    int c = C[dim], r = R[dim];
+    ushort uco = (o[c]&0xFFF)<<3, vco = (o[r]&0xFFF)<<3;
+    ushort uc1 = uco, vc1 = vco, uc2 = ushort(size<<3)+uco, vc2 = ushort(size<<3)+vco;
+    uc1 = max(uc1, orig.u1);
+    uc2 = min(uc2, orig.u2);
+    vc1 = max(vc1, orig.v1);
+    vc2 = min(vc2, orig.v2);
+    if(!isempty(cu) && touchingface(cu, orient) && !(nmat!=MAT_AIR && (cu.material&matmask)==nmat))
+    {
+        uchar r1 = cu.edges[faceedgesidx[orient][0]], r2 = cu.edges[faceedgesidx[orient][1]],
+              c1 = cu.edges[faceedgesidx[orient][2]], c2 = cu.edges[faceedgesidx[orient][3]];
+        ushort u1 = max(c1&0xF, c2&0xF)*size+uco, u2 = min(c1>>4, c2>>4)*size+uco,
+               v1 = max(r1&0xF, r2&0xF)*size+vco, v2 = min(r1>>4, r2>>4)*size+vco;
+        u1 = max(u1, orig.u1);
+        u2 = min(u2, orig.u2);
+        v1 = max(v1, orig.v1);
+        v2 = min(v2, orig.v2);
+        if(v2-v1==vc2-vc1)
+        {
+            if(u2-u1==uc2-uc1) return;
+            if(u1==uc1) uc1 = u2;
+            if(u2==uc2) uc2 = u1;
+        }
+        else if(u2-u1==uc2-uc1)
+        {
+            if(v1==vc1) vc1 = v2;
+            if(v2==vc2) vc2 = v1;
+        }
+    }
+    if(uc1==uc2 || vc1==vc2) return;
+    cf.u1 = min(cf.u1, uc1);
+    cf.u2 = max(cf.u2, uc2);
+    cf.v1 = min(cf.v1, vc1);
+    cf.v2 = max(cf.v2, vc2);
+}
+
+bool mincubeface(const cube &cu, int orient, const ivec &co, int size, facebounds &orig)
+{
+    ivec no;
+    int nsize;
+    const cube &nc = neighbourcube(cu, orient, co, size, no, nsize);
+    facebounds mincf;
+    mincf.u1 = orig.u2;
+    mincf.u2 = orig.u1;
+    mincf.v1 = orig.v2;
+    mincf.v2 = orig.v1;
+    mincubeface(nc, opposite(orient), no, nsize, orig, mincf, cu.material&MAT_ALPHA ? MAT_AIR : MAT_ALPHA, MAT_ALPHA);
+    bool smaller = false;
+    if(mincf.u1 > orig.u1) { orig.u1 = mincf.u1; smaller = true; }
+    if(mincf.u2 < orig.u2) { orig.u2 = mincf.u2; smaller = true; }
+    if(mincf.v1 > orig.v1) { orig.v1 = mincf.v1; smaller = true; }
+    if(mincf.v2 < orig.v2) { orig.v2 = mincf.v2; smaller = true; }
+    return smaller;
+}
+
+VAR(maxmerge, 0, 6, 12);
+VAR(minface, 0, 4, 12);
+
+struct pvert
+{
+    ushort x, y;
+
+    pvert() {}
+    pvert(ushort x, ushort y) : x(x), y(y) {}
+
+    bool operator==(const pvert &o) const { return x == o.x && y == o.y; }
+    bool operator!=(const pvert &o) const { return x != o.x || y != o.y; }
+};
+
+struct pedge
+{
+    pvert from, to;
+
+    pedge() {}
+    pedge(const pvert &from, const pvert &to) : from(from), to(to) {}
+
+    bool operator==(const pedge &o) const { return from == o.from && to == o.to; }
+    bool operator!=(const pedge &o) const { return from != o.from || to != o.to; }
+};
+
+static inline uint hthash(const pedge &x) { return uint(x.from.x)^(uint(x.from.y)<<8); }
+static inline bool htcmp(const pedge &x, const pedge &y) { return x == y; }
+
+struct poly
+{
+    cube *c;
+    int numverts;
+    bool merged;
+    pvert verts[MAXFACEVERTS];
+};
+
+bool clippoly(poly &p, const facebounds &b)
+{
+    pvert verts1[MAXFACEVERTS+4], verts2[MAXFACEVERTS+4];
+    int numverts1 = 0, numverts2 = 0, px = p.verts[p.numverts-1].x, py = p.verts[p.numverts-1].y; 
+    loopi(p.numverts)
+    {
+        int x = p.verts[i].x, y = p.verts[i].y;
+        if(x < b.u1) 
+        {
+            if(px > b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px));     
+            if(px > b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px));      
+        }
+        else if(x > b.u2)
+        {
+            if(px < b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px)); 
+            if(px < b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px));
+        }
+        else    
+        {
+            if(px < b.u1)
+            {
+                if(x > b.u1) verts1[numverts1++] = pvert(b.u1, y + ((y - py)*(b.u1 - x))/(x - px));
+            }
+            else if(px > b.u2 && x < b.u2) verts1[numverts1++] = pvert(b.u2, y + ((y - py)*(b.u2 - x))/(x - px));
+            verts1[numverts1++] = pvert(x, y);
+        }
+        px = x;
+        py = y;
+    }
+    if(numverts1 < 3) return false;
+    px = verts1[numverts1-1].x;
+    py = verts1[numverts1-1].y;
+    loopi(numverts1)
+    {
+        int x = verts1[i].x, y = verts1[i].y;
+        if(y < b.v1)
+        {
+            if(py > b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2);
+            if(py > b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1);
+        }
+        else if(y > b.v2)
+        {
+            if(py < b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1);
+            if(py < b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2);
+        }
+        else
+        {
+            if(py < b.v1)
+            {
+                if(y > b.v1) verts2[numverts2++] = pvert(x + ((x - px)*(b.v1 - y))/(y - py), b.v1);
+            }
+            else if(py > b.v2 && y < b.v2) verts2[numverts2++] = pvert(x + ((x - px)*(b.v2 - y))/(y - py), b.v2);
+            verts2[numverts2++] = pvert(x, y);
+        }
+        px = x;
+        py = y;
+    }
+    if(numverts2 < 3) return false;
+    if(numverts2 > MAXFACEVERTS) return false;
+    memcpy(p.verts, verts2, numverts2*sizeof(pvert));
+    p.numverts = numverts2;
+    return true;
+} 
+
+bool genpoly(cube &cu, int orient, const ivec &o, int size, int vis, ivec &n, int &offset, poly &p)
+{
+    int dim = dimension(orient), coord = dimcoord(orient);
+    ivec v[4];
+    genfaceverts(cu, orient, v);
+    if(flataxisface(cu, orient))
+    {
+         n = ivec(0, 0, 0);
+         n[dim] = coord ? 1 : -1;
+    }
+    else
+    {
+        if(faceconvexity(v)) return false;
+        n.cross(ivec(v[1]).sub(v[0]), ivec(v[2]).sub(v[0]));
+        if(n.iszero()) n.cross(ivec(v[2]).sub(v[0]), ivec(v[3]).sub(v[0]));
+        reduceslope(n);
+    }
+
+    ivec po = ivec(o).mask(0xFFF).shl(3);
+    loopk(4) v[k].mul(size).add(po);
+    offset = -n.dot(v[3]);
+    
+    int r = R[dim], c = C[dim], order = vis&4 ? 1 : 0;
+    p.numverts = 0;
+    if(coord)
+    {
+        const ivec &v0 = v[order]; p.verts[p.numverts++] = pvert(v0[c], v0[r]);
+        if(vis&1) { const ivec &v1 = v[order+1]; p.verts[p.numverts++] = pvert(v1[c], v1[r]); }
+        const ivec &v2 = v[order+2]; p.verts[p.numverts++] = pvert(v2[c], v2[r]);
+        if(vis&2) { const ivec &v3 = v[(order+3)&3]; p.verts[p.numverts++] = pvert(v3[c], v3[r]); }
+    }
+    else
+    {
+        if(vis&2) { const ivec &v3 = v[(order+3)&3]; p.verts[p.numverts++] = pvert(v3[c], v3[r]); }
+        const ivec &v2 = v[order+2]; p.verts[p.numverts++] = pvert(v2[c], v2[r]);
+        if(vis&1) { const ivec &v1 = v[order+1]; p.verts[p.numverts++] = pvert(v1[c], v1[r]); }
+        const ivec &v0 = v[order]; p.verts[p.numverts++] = pvert(v0[c], v0[r]);
+    }
+
+    if(faceedges(cu, orient)!=F_SOLID)
+    {
+        int px = int(p.verts[p.numverts-2].x) - int(p.verts[p.numverts-3].x), py = int(p.verts[p.numverts-2].y) - int(p.verts[p.numverts-3].y),
+            cx = int(p.verts[p.numverts-1].x) - int(p.verts[p.numverts-2].x), cy = int(p.verts[p.numverts-1].y) - int(p.verts[p.numverts-2].y),
+            dir = px*cy - py*cx;
+        if(dir > 0) return false;
+        if(!dir) { if(p.numverts < 4) return false; p.verts[p.numverts-2] = p.verts[p.numverts-1]; p.numverts--; }
+        px = cx; py = cy;
+        cx = int(p.verts[0].x) - int(p.verts[p.numverts-1].x); cy = int(p.verts[0].y) - int(p.verts[p.numverts-1].y); 
+        dir = px*cy - py*cx;
+        if(dir > 0) return false;
+        if(!dir) { if(p.numverts < 4) return false; p.numverts--; }
+        px = cx; py = cy;
+        cx = int(p.verts[1].x) - int(p.verts[0].x); cy = int(p.verts[1].y) - int(p.verts[0].y);
+        dir = px*cy - py*cx;
+        if(dir > 0) return false;
+        if(!dir) { if(p.numverts < 4) return false; p.verts[0] = p.verts[p.numverts-1]; p.numverts--; }
+        px = cx; py = cy;
+        cx = int(p.verts[2].x) - int(p.verts[1].x); cy = int(p.verts[2].y) - int(p.verts[1].y);
+        dir = px*cy - py*cx;
+        if(dir > 0) return false;
+        if(!dir) { if(p.numverts < 4) return false; p.verts[1] = p.verts[2]; p.verts[2] = p.verts[3]; p.numverts--; } 
+    }
+
+    p.c = &cu;
+    p.merged = false;
+
+    if(minface && size >= 1<<minface && touchingface(cu, orient))
+    {
+        facebounds b;
+        b.u1 = b.u2 = p.verts[0].x;
+        b.v1 = b.v2 = p.verts[0].y;
+        for(int i = 1; i < p.numverts; i++)
+        {
+            const pvert &v = p.verts[i];
+            b.u1 = min(b.u1, v.x);
+            b.u2 = max(b.u2, v.x);
+            b.v1 = min(b.v1, v.y);
+            b.v2 = max(b.v2, v.y);
+        }
+        if(mincubeface(cu, orient, o, size, b) && clippoly(p, b))
+            p.merged = true;
+    }
+
+    return true;
+}
+
+struct plink : pedge
+{
+    int polys[2];
+
+    plink() { clear(); }
+    plink(const pedge &p) : pedge(p) { clear(); }
+
+    void clear() { polys[0] = polys[1] = -1; }
+};
+
+bool mergepolys(int orient, hashset<plink> &links, vector<plink *> &queue, int owner, poly &p, poly &q, const pedge &e)
+{
+    int pe = -1, qe = -1;
+    loopi(p.numverts) if(p.verts[i] == e.from) { pe = i; break; }
+    loopi(q.numverts) if(q.verts[i] == e.to) { qe = i; break; }
+    if(pe < 0 || qe < 0) return false;
+    if(p.verts[(pe+1)%p.numverts] != e.to || q.verts[(qe+1)%q.numverts] != e.from) return false;
+    /*
+     *  c----d
+     *  |    |
+     *  F----T
+     *  |  P |
+     *  b----a
+     */
+    pvert verts[2*MAXFACEVERTS];
+    int numverts = 0, index = pe+2; // starts at A = T+1, ends at F = T+p.numverts
+    loopi(p.numverts-1)
+    {
+        if(index >= p.numverts) index -= p.numverts;
+        verts[numverts++] = p.verts[index++];
+    }
+    index = qe+2; // starts at C = T+2 = F+1, ends at T = T+q.numverts
+    int px = int(verts[numverts-1].x) - int(verts[numverts-2].x), py = int(verts[numverts-1].y) - int(verts[numverts-2].y);
+    loopi(q.numverts-1)
+    {
+        if(index >= q.numverts) index -= q.numverts;
+        pvert &src = q.verts[index++];
+        int cx = int(src.x) - int(verts[numverts-1].x), cy = int(src.y) - int(verts[numverts-1].y),
+            dir = px*cy - py*cx;
+        if(dir > 0) return false;
+        if(!dir) numverts--;
+        verts[numverts++] = src;
+        px = cx;
+        py = cy;
+    }
+    int cx = int(verts[0].x) - int(verts[numverts-1].x), cy = int(verts[0].y) - int(verts[numverts-1].y),
+        dir = px*cy - py*cx;
+    if(dir > 0) return false;
+    if(!dir) numverts--;
+
+    if(numverts > MAXFACEVERTS) return false;
+
+    q.merged = true;
+    q.numverts = 0;
+
+    p.merged = true;
+    p.numverts = numverts;
+    memcpy(p.verts, verts, numverts*sizeof(pvert));
+
+    int prev = p.numverts-1;
+    loopj(p.numverts)
+    {
+        pedge e(p.verts[prev], p.verts[j]);
+        int order = e.from.x > e.to.x || (e.from.x == e.to.x && e.from.y > e.to.y) ? 1 : 0;
+        if(order) swap(e.from, e.to);
+        plink &l = links.access(e, e);
+        bool shouldqueue = l.polys[order] < 0 && l.polys[order^1] >= 0;
+        l.polys[order] = owner;
+        if(shouldqueue) queue.add(&l);
+        prev = j;
+    }
+
+    return true;
+}
+
+void addmerge(cube &cu, int orient, const ivec &co, const ivec &n, int offset, poly &p)
+{
+    cu.merged |= 1<<orient;
+    if(!p.numverts)
+    {
+        if(cu.ext) cu.ext->surfaces[orient] = ambientsurface;
+        return;
+    }
+    surfaceinfo surf = brightsurface;
+    vertinfo verts[MAXFACEVERTS];
+    surf.numverts |= p.numverts;
+    int dim = dimension(orient), coord = dimcoord(orient), c = C[dim], r = R[dim];
+    loopk(p.numverts)
+    {
+        pvert &src = p.verts[coord ? k : p.numverts-1-k];
+        vertinfo &dst = verts[k];
+        ivec v;
+        v[c] = src.x;
+        v[r] = src.y;
+        v[dim] = -(offset + n[c]*src.x + n[r]*src.y)/n[dim];
+        dst.set(v);
+    }
+    if(cu.ext)
+    {
+        const surfaceinfo &oldsurf = cu.ext->surfaces[orient];
+        int numverts = oldsurf.numverts&MAXFACEVERTS;
+        if(numverts == p.numverts)
+        {
+            ivec v0 = verts[0].getxyz();
+            const vertinfo *oldverts = cu.ext->verts() + oldsurf.verts;
+            loopj(numverts) if(v0 == oldverts[j].getxyz()) 
+            { 
+                for(int k = 1; k < numverts; k++)
+                {
+                    if(++j >= numverts) j = 0; 
+                    if(verts[k].getxyz() != oldverts[j].getxyz()) goto nomatch;
+                }
+                return;
+            }
+        nomatch:;
+        }
+    }     
+    setsurface(cu, orient, surf, verts, p.numverts);
+}
+
+static inline void clearmerge(cube &c, int orient)
+{
+    if(c.merged&(1<<orient))
+    {
+        c.merged &= ~(1<<orient);
+        if(c.ext) c.ext->surfaces[orient] = brightsurface;
+    }
+}
+
+void addmerges(int orient, const ivec &co, const ivec &n, int offset, vector<poly> &polys)
+{
+    loopv(polys)
+    {
+        poly &p = polys[i];
+        if(p.merged) addmerge(*p.c, orient, co, n, offset, p);
+        else clearmerge(*p.c, orient);
+    }
+}
+
+void mergepolys(int orient, const ivec &co, const ivec &n, int offset, vector<poly> &polys)
+{
+    if(polys.length() <= 1) { addmerges(orient, co, n, offset, polys); return; }
+    hashset<plink> links(polys.length() <= 32 ? 128 : 1024);
+    vector<plink *> queue;
+    loopv(polys)
+    {
+        poly &p = polys[i];
+        int prev = p.numverts-1;
+        loopj(p.numverts)
+        {
+            pedge e(p.verts[prev], p.verts[j]);
+            int order = e.from.x > e.to.x || (e.from.x == e.to.x && e.from.y > e.to.y) ? 1 : 0;
+            if(order) swap(e.from, e.to);
+            plink &l = links.access(e, e);
+            l.polys[order] = i;
+            if(l.polys[0] >= 0 && l.polys[1] >= 0) queue.add(&l);
+            prev = j;
+        }
+    }
+    vector<plink *> nextqueue;
+    while(queue.length())
+    {
+        loopv(queue)
+        {
+            plink &l = *queue[i];
+            if(l.polys[0] >= 0 && l.polys[1] >= 0)
+                mergepolys(orient, links, nextqueue, l.polys[0], polys[l.polys[0]], polys[l.polys[1]], l);
+        }
+        queue.setsize(0);
+        queue.move(nextqueue);
+    }
+    addmerges(orient, co, n, offset, polys);
+}
+
+static int genmergeprogress = 0;
+
+struct cfpolys
+{
+    vector<poly> polys;
+};
+
+static hashtable<cfkey, cfpolys> cpolys;
+
+void genmerges(cube *c = worldroot, const ivec &o = ivec(0, 0, 0), int size = worldsize>>1)
+{
+    if((genmergeprogress++&0xFFF)==0) renderprogress(float(genmergeprogress)/allocnodes, "merging faces...");
+    neighbourstack[++neighbourdepth] = c;
+    loopi(8)
+    {
+        ivec co(i, o, size);
+        int vis;
+        if(c[i].children) genmerges(c[i].children, co, size>>1);
+        else if(!isempty(c[i])) loopj(6) if((vis = visibletris(c[i], j, co, size)))
+        {
+            cfkey k;
+            poly p;
+            if(size < 1<<maxmerge && c != worldroot)
+            {
+                if(genpoly(c[i], j, co, size, vis, k.n, k.offset, p)) 
+                {
+                    k.orient = j;
+                    k.tex = c[i].texture[j];
+                    k.material = c[i].material&MAT_ALPHA;
+                    cpolys[k].polys.add(p);
+                    continue;
+                }
+            }
+            else if(minface && size >= 1<<minface && touchingface(c[i], j))
+            {
+                if(genpoly(c[i], j, co, size, vis, k.n, k.offset, p) && p.merged)
+                {
+                    addmerge(c[i], j, co, k.n, k.offset, p);
+                    continue;
+                }
+            } 
+            clearmerge(c[i], j);
+        }
+        if((size == 1<<maxmerge || c == worldroot) && cpolys.numelems)
+        {
+            enumeratekt(cpolys, cfkey, key, cfpolys, val,
+            {
+                mergepolys(key.orient, co, key.n, key.offset, val.polys);
+            });
+            cpolys.clear();
+        }
+    }
+    --neighbourdepth;
+}
+
+int calcmergedsize(int orient, const ivec &co, int size, const vertinfo *verts, int numverts)
+{
+    ushort x1 = verts[0].x, y1 = verts[0].y, z1 = verts[0].z, 
+           x2 = x1, y2 = y1, z2 = z1;
+    for(int i = 1; i < numverts; i++)
+    {
+        const vertinfo &v = verts[i];
+        x1 = min(x1, v.x);
+        x2 = max(x2, v.x);
+        y1 = min(y1, v.y);
+        y2 = max(y2, v.y);
+        z1 = min(z1, v.z);
+        z2 = max(z2, v.z);
+    }
+    int bits = 0;
+    while(1<<bits < size) ++bits;
+    bits += 3;
+    ivec mo(co);
+    mo.mask(0xFFF);
+    mo.shl(3);
+    while(bits<15)
+    {
+        mo.mask(~((1<<bits)-1));
+        if(mo.x <= x1 && mo.x + (1<<bits) >= x2 &&
+           mo.y <= y1 && mo.y + (1<<bits) >= y2 &&
+           mo.z <= z1 && mo.z + (1<<bits) >= z2)
+            break;
+        bits++;
+    }
+    return bits-3;
+}
+
+static void invalidatemerges(cube &c)
+{
+    if(c.merged)
+    {
+        brightencube(c);
+        c.merged = 0;
+    }
+    if(c.ext)
+    {
+        if(c.ext->va)
+        {
+            if(!(c.ext->va->hasmerges&(MERGE_PART | MERGE_ORIGIN))) return;
+            destroyva(c.ext->va);
+            c.ext->va = NULL;
+        }
+        if(c.ext->tjoints >= 0) c.ext->tjoints = -1;
+    }
+    if(c.children) loopi(8) invalidatemerges(c.children[i]);
+}
+
+static int invalidatedmerges = 0;
+
+void invalidatemerges(cube &c, const ivec &co, int size, bool msg)
+{
+    if(msg && invalidatedmerges!=totalmillis)
+    {
+        renderprogress(0, "invalidating merged surfaces...");
+        invalidatedmerges = totalmillis;
+    }
+    invalidatemerges(c);
+}
+
+void calcmerges()
+{
+    genmergeprogress = 0;
+    genmerges();
+}
+
diff --git a/src/engine/octa.h b/src/engine/octa.h
new file mode 100644 (file)
index 0000000..6b7bfca
--- /dev/null
@@ -0,0 +1,340 @@
+// 6-directional octree heightfield map format
+
+struct elementset
+{
+    ushort texture, lmid, envmap;
+    uchar dim, layer;
+    ushort length[2], minvert[2], maxvert[2];
+};
+
+enum
+{
+    EMID_NONE = 0,
+    EMID_CUSTOM,
+    EMID_SKY,
+    EMID_RESERVED
+};
+
+struct materialsurface
+{
+    ivec o;
+    ushort csize, rsize;
+    ushort material, skip;
+    uchar orient, visible;
+    union
+    {
+        short index;
+        short depth;
+    };
+    union
+    {
+        entity *light;
+        ushort envmap;
+        uchar ends;
+    };
+};
+
+struct vertinfo
+{
+    ushort x, y, z, u, v, norm;
+
+    void setxyz(ushort a, ushort b, ushort c) { x = a; y = b; z = c; }
+    void setxyz(const ivec &v) { setxyz(v.x, v.y, v.z); }
+    void set(ushort a, ushort b, ushort c, ushort s = 0, ushort t = 0, ushort n = 0) { setxyz(a, b, c); u = s; v = t; norm = n; }
+    void set(const ivec &v, ushort s = 0, ushort t = 0, ushort n = 0) { set(v.x, v.y, v.z, s, t, n); }
+    ivec getxyz() const { return ivec(x, y, z); }
+};
+
+enum
+{
+    LAYER_TOP    = (1<<5),
+    LAYER_BOTTOM = (1<<6),
+    LAYER_DUP    = (1<<7),
+
+    LAYER_BLEND  = LAYER_TOP|LAYER_BOTTOM,
+    
+    MAXFACEVERTS = 15
+};
+
+enum { LMID_AMBIENT = 0, LMID_AMBIENT1, LMID_BRIGHT, LMID_BRIGHT1, LMID_DARK, LMID_DARK1, LMID_RESERVED };
+
+struct surfaceinfo
+{
+    uchar lmid[2];
+    uchar verts, numverts;
+
+    int totalverts() const { return numverts&LAYER_DUP ? (numverts&MAXFACEVERTS)*2 : numverts&MAXFACEVERTS; }
+    bool used() const { return lmid[0] != LMID_AMBIENT || lmid[1] != LMID_AMBIENT || numverts&~LAYER_TOP; }
+    void clear() { lmid[0] = LMID_AMBIENT; lmid[1] = LMID_AMBIENT; numverts = (numverts&MAXFACEVERTS) | LAYER_TOP; }
+    void brighten() { lmid[0] = LMID_BRIGHT; lmid[1] = LMID_AMBIENT; numverts = (numverts&MAXFACEVERTS) | LAYER_TOP; }
+};
+
+static const surfaceinfo ambientsurface = {{LMID_AMBIENT, LMID_AMBIENT}, 0, LAYER_TOP};
+static const surfaceinfo brightsurface = {{LMID_BRIGHT, LMID_AMBIENT}, 0, LAYER_TOP};
+static const surfaceinfo brightbottomsurface = {{LMID_AMBIENT, LMID_BRIGHT}, 0, LAYER_BOTTOM};
+
+struct grasstri
+{
+    vec v[4];
+    int numv;
+    vec4 tcu, tcv;
+    plane surface;
+    vec center;
+    float radius;
+    float minz, maxz;
+    ushort texture, lmid;
+};
+
+struct occludequery
+{
+    void *owner;
+    GLuint id;
+    int fragments;
+};
+
+struct vtxarray;
+
+struct octaentities
+{
+    vector<int> mapmodels;
+    vector<int> other;
+    occludequery *query;
+    octaentities *next, *rnext;
+    int distance;
+    ivec o;
+    int size;
+    ivec bbmin, bbmax;
+
+    octaentities(const ivec &o, int size) : query(0), o(o), size(size), bbmin(o), bbmax(o)
+    {
+        bbmin.add(size);
+    }
+};
+
+enum
+{
+    OCCLUDE_NOTHING = 0,
+    OCCLUDE_GEOM,
+    OCCLUDE_BB,
+    OCCLUDE_PARENT
+};
+
+enum
+{
+    MERGE_ORIGIN = 1<<0,
+    MERGE_PART   = 1<<1,
+    MERGE_USE    = 1<<2
+};
+
+struct vtxarray
+{
+    vtxarray *parent;
+    vector<vtxarray *> children;
+    vtxarray *next, *rnext; // linked list of visible VOBs
+    vertex *vdata;           // vertex data
+    ushort voffset;          // offset into vertex data
+    ushort *edata, *skydata; // vertex indices
+    GLuint vbuf, ebuf, skybuf; // VBOs
+    ushort minvert, maxvert; // DRE info
+    elementset *eslist;      // List of element indices sets (range) per texture
+    materialsurface *matbuf; // buffer of material surfaces
+    int verts, tris, texs, blendtris, blends, alphabacktris, alphaback, alphafronttris, alphafront, alphatris, texmask, sky, explicitsky, skyfaces, skyclip, matsurfs, distance;
+    double skyarea;
+    ivec o;
+    int size;                // location and size of cube.
+    ivec geommin, geommax;   // BB of geom
+    ivec shadowmapmin, shadowmapmax; // BB of shadowmapped surfaces
+    ivec matmin, matmax;     // BB of any materials
+    ivec bbmin, bbmax;       // BB of everything including children
+    uchar curvfc, occluded;
+    occludequery *query;
+    vector<octaentities *> mapmodels;
+    vector<grasstri> grasstris;
+    int hasmerges, mergelevel;
+    uint dynlightmask;
+    bool shadowed;
+};
+
+struct cube;
+
+struct clipplanes
+{
+    vec o, r, v[8];
+    plane p[12];
+    uchar side[12];
+    uchar size, visible;
+    const cube *owner;
+    int version;
+};
+
+struct facebounds
+{
+    ushort u1, u2, v1, v2;
+
+    bool empty() const { return u1 >= u2 || v1 >= v2; }
+};
+
+struct tjoint
+{
+    int next;
+    ushort offset;
+    uchar edge;
+};
+
+struct cubeext
+{
+    vtxarray *va;            // vertex array for children, or NULL
+    octaentities *ents;      // map entities inside cube
+    surfaceinfo surfaces[6]; // render info for each surface
+    int tjoints;             // linked list of t-joints
+    uchar maxverts;          // allocated space for verts
+
+    vertinfo *verts() { return (vertinfo *)(this+1); }
+};  
+
+struct cube
+{
+    cube *children;          // points to 8 cube structures which are its children, or NULL. -Z first, then -Y, -X
+    cubeext *ext;            // extended info for the cube
+    union
+    {
+        uchar edges[12];     // edges of the cube, each uchar is 2 4bit values denoting the range.
+                             // see documentation jpgs for more info.
+        uint faces[3];       // 4 edges of each dimension together representing 2 perpendicular faces
+    };
+    ushort texture[6];       // one for each face. same order as orient.
+    ushort material;         // empty-space material
+    uchar merged;            // merged faces of the cube
+    union
+    {
+        uchar escaped;       // mask of which children have escaped merges
+        uchar visible;       // visibility info for faces
+    };
+};
+
+struct block3
+{
+    ivec o, s;
+    int grid, orient;
+    block3() {}
+    block3(const selinfo &sel) : o(sel.o), s(sel.s), grid(sel.grid), orient(sel.orient) {}
+    cube *c()           { return (cube *)(this+1); }
+    int size()    const { return s.x*s.y*s.z; }
+};
+
+struct editinfo
+{
+    block3 *copy;
+    editinfo() : copy(NULL) {}
+};
+
+struct undoent   { int i; entity e; };
+struct undoblock // undo header, all data sits in payload
+{
+    undoblock *prev, *next;
+    int size, timestamp, numents; // if numents is 0, is a cube undo record, otherwise an entity undo record
+
+    block3 *block() { return (block3 *)(this + 1); }
+    uchar *gridmap()
+    {
+        block3 *ub = block();
+        return (uchar *)(ub->c() + ub->size());
+    }
+    undoent *ents() { return (undoent *)(this + 1); }
+};
+
+extern cube *worldroot;             // the world data. only a ptr to 8 cubes (ie: like cube.children above)
+extern int wtris, wverts, vtris, vverts, glde, gbatches, rplanes;
+extern int allocnodes, allocva, selchildcount, selchildmat;
+
+const uint F_EMPTY = 0;             // all edges in the range (0,0)
+const uint F_SOLID = 0x80808080;    // all edges in the range (0,8)
+
+#define isempty(c) ((c).faces[0]==F_EMPTY)
+#define isentirelysolid(c) ((c).faces[0]==F_SOLID && (c).faces[1]==F_SOLID && (c).faces[2]==F_SOLID)
+#define setfaces(c, face) { (c).faces[0] = (c).faces[1] = (c).faces[2] = face; }
+#define solidfaces(c) setfaces(c, F_SOLID)
+#define emptyfaces(c) setfaces(c, F_EMPTY)
+
+#define edgemake(a, b) ((b)<<4|a)
+#define edgeget(edge, coord) ((coord) ? (edge)>>4 : (edge)&0xF)
+#define edgeset(edge, coord, val) ((edge) = ((coord) ? ((edge)&0xF)|((val)<<4) : ((edge)&0xF0)|(val)))
+
+#define cubeedge(c, d, x, y) ((c).edges[(((d)<<2)+((y)<<1)+(x))])
+
+#define octadim(d)          (1<<(d))                    // creates mask for bit of given dimension
+#define octacoord(d, i)     (((i)&octadim(d))>>(d))
+#define oppositeocta(d, i)  ((i)^octadim(D[d]))
+#define octaindex(d,x,y,z)  (((z)<<D[d])+((y)<<C[d])+((x)<<R[d]))
+#define octastep(x, y, z, scale) (((((z)>>(scale))&1)<<2) | ((((y)>>(scale))&1)<<1) | (((x)>>(scale))&1))
+
+static inline uchar octaboxoverlap(const ivec &o, int size, const ivec &bbmin, const ivec &bbmax)
+{
+    uchar p = 0xFF; // bitmask of possible collisions with octants. 0 bit = 0 octant, etc
+    ivec mid = ivec(o).add(size);
+    if(mid.z <= bbmin.z)      p &= 0xF0; // not in a -ve Z octant
+    else if(mid.z >= bbmax.z) p &= 0x0F; // not in a +ve Z octant
+    if(mid.y <= bbmin.y)      p &= 0xCC; // not in a -ve Y octant
+    else if(mid.y >= bbmax.y) p &= 0x33; // etc..
+    if(mid.x <= bbmin.x)      p &= 0xAA;
+    else if(mid.x >= bbmax.x) p &= 0x55;
+    return p;
+}
+
+#define loopoctabox(o, size, bbmin, bbmax) uchar possible = octaboxoverlap(o, size, bbmin, bbmax); loopi(8) if(possible&(1<<i))
+#define loopoctaboxsize(o, size, bborigin, bbsize) uchar possible = octaboxoverlap(o, size, bborigin, ivec(bborigin).add(bbsize)); loopi(8) if(possible&(1<<i))
+
+enum
+{
+    O_LEFT = 0,
+    O_RIGHT,
+    O_BACK,
+    O_FRONT,
+    O_BOTTOM,
+    O_TOP
+};
+
+#define dimension(orient) ((orient)>>1)
+#define dimcoord(orient)  ((orient)&1)
+#define opposite(orient)  ((orient)^1)
+
+enum
+{
+    VFC_FULL_VISIBLE = 0,
+    VFC_PART_VISIBLE,
+    VFC_FOGGED,
+    VFC_NOT_VISIBLE,
+    PVS_FULL_VISIBLE,
+    PVS_PART_VISIBLE,
+    PVS_FOGGED
+};
+
+#define GENCUBEVERTS(x0,x1, y0,y1, z0,z1) \
+    GENCUBEVERT(0, x1, y1, z0) \
+    GENCUBEVERT(1, x0, y1, z0) \
+    GENCUBEVERT(2, x0, y1, z1) \
+    GENCUBEVERT(3, x1, y1, z1) \
+    GENCUBEVERT(4, x1, y0, z1) \
+    GENCUBEVERT(5, x0, y0, z1) \
+    GENCUBEVERT(6, x0, y0, z0) \
+    GENCUBEVERT(7, x1, y0, z0)
+
+#define GENFACEVERTX(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv)
+#define GENFACEVERTSX(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEORIENT(0, GENFACEVERTX(0,0, x0,y1,z1, d0,r1,c1), GENFACEVERTX(0,1, x0,y1,z0, d0,r1,c0), GENFACEVERTX(0,2, x0,y0,z0, d0,r0,c0), GENFACEVERTX(0,3, x0,y0,z1, d0,r0,c1)) \
+    GENFACEORIENT(1, GENFACEVERTX(1,0, x1,y1,z1, d1,r1,c1), GENFACEVERTX(1,1, x1,y0,z1, d1,r0,c1), GENFACEVERTX(1,2, x1,y0,z0, d1,r0,c0), GENFACEVERTX(1,3, x1,y1,z0, d1,r1,c0))
+#define GENFACEVERTY(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv)
+#define GENFACEVERTSY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEORIENT(2, GENFACEVERTY(2,0, x1,y0,z1, c1,d0,r1), GENFACEVERTY(2,1, x0,y0,z1, c0,d0,r1), GENFACEVERTY(2,2, x0,y0,z0, c0,d0,r0), GENFACEVERTY(2,3, x1,y0,z0, c1,d0,r0)) \
+    GENFACEORIENT(3, GENFACEVERTY(3,0, x0,y1,z0, c0,d1,r0), GENFACEVERTY(3,1, x0,y1,z1, c0,d1,r1), GENFACEVERTY(3,2, x1,y1,z1, c1,d1,r1), GENFACEVERTY(3,3, x1,y1,z0, c1,d1,r0))
+#define GENFACEVERTZ(o,n, x,y,z, xv,yv,zv) GENFACEVERT(o,n, x,y,z, xv,yv,zv)
+#define GENFACEVERTSZ(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEORIENT(4, GENFACEVERTZ(4,0, x0,y0,z0, r0,c0,d0), GENFACEVERTZ(4,1, x0,y1,z0, r0,c1,d0), GENFACEVERTZ(4,2, x1,y1,z0, r1,c1,d0), GENFACEVERTZ(4,3, x1,y0,z0, r1,c0,d0)) \
+    GENFACEORIENT(5, GENFACEVERTZ(5,0, x0,y0,z1, r0,c0,d1), GENFACEVERTZ(5,1, x1,y0,z1, r1,c0,d1), GENFACEVERTZ(5,2, x1,y1,z1, r1,c1,d1), GENFACEVERTZ(5,3, x0,y1,z1, r0,c1,d1))
+#define GENFACEVERTSXY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEVERTSX(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEVERTSY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1)
+#define GENFACEVERTS(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEVERTSXY(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1) \
+    GENFACEVERTSZ(x0,x1, y0,y1, z0,z1, c0,c1, r0,r1, d0,d1)
+
diff --git a/src/engine/octaedit.cpp b/src/engine/octaedit.cpp
new file mode 100644 (file)
index 0000000..5baa6f0
--- /dev/null
@@ -0,0 +1,2977 @@
+#include "engine.h"
+
+extern int outline;
+
+bool boxoutline = false;
+
+void boxs(int orient, vec o, const vec &s, float size) 
+{
+    int d = dimension(orient), dc = dimcoord(orient);
+    float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+    o[D[d]] += dc * s[D[d]] + f;
+
+    vec r(0, 0, 0), c(0, 0, 0);
+    r[R[d]] = s[R[d]];
+    c[C[d]] = s[C[d]];
+
+    vec v1 = o, v2 = vec(o).add(r), v3 = vec(o).add(r).add(c), v4 = vec(o).add(c);
+
+    r[R[d]] = 0.5f*size;
+    c[C[d]] = 0.5f*size;
+
+    gle::defvertex();
+    gle::begin(GL_TRIANGLE_STRIP);
+    gle::attrib(vec(v1).sub(r).sub(c));
+        gle::attrib(vec(v1).add(r).add(c));
+    gle::attrib(vec(v2).add(r).sub(c));
+        gle::attrib(vec(v2).sub(r).add(c));
+    gle::attrib(vec(v3).add(r).add(c));
+        gle::attrib(vec(v3).sub(r).sub(c));
+    gle::attrib(vec(v4).sub(r).add(c));
+        gle::attrib(vec(v4).add(r).sub(c));
+    gle::attrib(vec(v1).sub(r).sub(c));
+        gle::attrib(vec(v1).add(r).add(c));
+    xtraverts += gle::end();
+}
+
+void boxs(int orient, vec o, const vec &s)
+{
+    int d = dimension(orient), dc = dimcoord(orient);
+    float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+    o[D[d]] += dc * s[D[d]] + f;
+
+    gle::defvertex();
+    gle::begin(GL_LINE_LOOP);
+
+    gle::attrib(o); o[R[d]] += s[R[d]];
+    gle::attrib(o); o[C[d]] += s[C[d]];
+    gle::attrib(o); o[R[d]] -= s[R[d]];
+    gle::attrib(o);
+
+    xtraverts += gle::end();
+}
+
+void boxs3D(const vec &o, vec s, int g)
+{
+    s.mul(g);
+    loopi(6)
+        boxs(i, o, s);
+}
+
+void boxsgrid(int orient, vec o, vec s, int g)
+{
+    int d = dimension(orient), dc = dimcoord(orient);
+    float ox = o[R[d]],
+          oy = o[C[d]],
+          xs = s[R[d]],
+          ys = s[C[d]],
+          f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0;
+
+    o[D[d]] += dc * s[D[d]]*g + f;
+
+    gle::defvertex();
+    gle::begin(GL_LINES);
+    loop(x, xs)
+    {
+        o[R[d]] += g;
+        gle::attrib(o);
+        o[C[d]] += ys*g;
+        gle::attrib(o);
+        o[C[d]] = oy;
+    }
+    loop(y, ys)
+    {
+        o[C[d]] += g;
+        o[R[d]] = ox;
+        gle::attrib(o);
+        o[R[d]] += xs*g;
+        gle::attrib(o);
+    }
+    xtraverts += gle::end();
+}
+
+selinfo sel, lastsel, savedsel;
+
+int orient = 0;
+int gridsize = 8;
+ivec cor, lastcor;
+ivec cur, lastcur;
+
+extern int entediting;
+bool editmode = false;
+bool havesel = false;
+bool hmapsel = false;
+int horient  = 0;
+
+extern int entmoving;
+
+VARF(dragging, 0, 0, 1,
+    if(!dragging || cor[0]<0) return;
+    lastcur = cur;
+    lastcor = cor;
+    sel.grid = gridsize;
+    sel.orient = orient;
+);
+
+int moving = 0;
+ICOMMAND(moving, "b", (int *n),
+{
+    if(*n >= 0)
+    {
+        if(!*n || (moving<=1 && !pointinsel(sel, vec(cur).add(1)))) moving = 0;
+        else if(!moving) moving = 1;
+    }
+    intret(moving);
+});
+
+VARF(gridpower, 0, 3, 12,
+{
+    if(dragging) return;
+    gridsize = 1<<gridpower;
+    if(gridsize>=worldsize) gridsize = worldsize/2;
+    cancelsel();
+});
+
+VAR(passthroughsel, 0, 0, 1);
+VAR(editing, 1, 0, 0);
+VAR(selectcorners, 0, 0, 1);
+VARF(hmapedit, 0, 0, 1, horient = sel.orient);
+
+void forcenextundo() { lastsel.orient = -1; }
+
+extern void hmapcancel();
+
+void cubecancel()
+{
+    havesel = false;
+    moving = dragging = hmapedit = passthroughsel = 0;
+    forcenextundo();
+    hmapcancel();
+}
+
+void cancelsel()
+{
+    cubecancel();
+    entcancel();
+}
+
+void toggleedit(bool force)
+{
+    if(!force)
+    {
+        if(!isconnected()) return;
+        if(player->state!=CS_ALIVE && player->state!=CS_DEAD && player->state!=CS_EDITING) return; // do not allow dead players to edit to avoid state confusion
+        if(!game::allowedittoggle()) return;         // not in most multiplayer modes
+    }
+    if(!(editmode = !editmode))
+    {
+        player->state = player->editstate;
+        player->o.z -= player->eyeheight;       // entinmap wants feet pos
+        entinmap(player);                       // find spawn closest to current floating pos
+    }
+    else
+    {
+        game::resetgamestate();
+        player->editstate = player->state;
+        player->state = CS_EDITING;
+    }
+    cancelsel();
+    stoppaintblendmap();
+    keyrepeat(editmode);
+    editing = entediting = editmode;
+    extern int fullbright;
+    if(fullbright) { initlights(); lightents(); }
+    if(!force) game::edittoggled(editmode);
+}
+
+VARP(editinview, 0, 1, 1);
+
+bool noedit(bool view, bool msg)
+{
+    if(!editmode) { if(msg) conoutf(CON_ERROR, "operation only allowed in edit mode"); return true; }
+    if(view || haveselent()) return false;
+    float r = 1.0f;
+    vec o(sel.o), s(sel.s);
+    s.mul(float(sel.grid) / 2.0f);
+    o.add(s);
+    r = float(max(s.x, max(s.y, s.z)));
+    bool viewable = (isvisiblesphere(r, o) != VFC_NOT_VISIBLE);
+    if(viewable || !editinview) return false;
+    if(msg) conoutf(CON_ERROR, "selection not in view");
+    return true;
+}
+
+void reorient()
+{
+    sel.cx = 0;
+    sel.cy = 0;
+    sel.cxs = sel.s[R[dimension(orient)]]*2;
+    sel.cys = sel.s[C[dimension(orient)]]*2;
+    sel.orient = orient;
+}
+
+void selextend()
+{
+    if(noedit(true)) return;
+    loopi(3)
+    {
+        if(cur[i]<sel.o[i])
+        {
+            sel.s[i] += (sel.o[i]-cur[i])/sel.grid;
+            sel.o[i] = cur[i];
+        }
+        else if(cur[i]>=sel.o[i]+sel.s[i]*sel.grid)
+        {
+            sel.s[i] = (cur[i]-sel.o[i])/sel.grid+1;
+        }
+    }
+}
+
+ICOMMAND(edittoggle, "", (), toggleedit(false));
+COMMAND(entcancel, "");
+COMMAND(cubecancel, "");
+COMMAND(cancelsel, "");
+COMMAND(reorient, "");
+COMMAND(selextend, "");
+
+ICOMMAND(selmoved, "", (), { if(noedit(true)) return; intret(sel.o != savedsel.o ? 1 : 0); });
+ICOMMAND(selsave, "", (), { if(noedit(true)) return; savedsel = sel; });
+ICOMMAND(selrestore, "", (), { if(noedit(true)) return; sel = savedsel; });
+ICOMMAND(selswap, "", (), { if(noedit(true)) return; swap(sel, savedsel); });
+
+ICOMMAND(getselpos, "", (),
+{
+    if(noedit(true)) return;
+    defformatstring(pos, "%s %s %s", floatstr(sel.o.x), floatstr(sel.o.y), floatstr(sel.o.z));
+    result(pos);
+});
+
+void setselpos(int *x, int *y, int *z)
+{
+    if(noedit(moving!=0)) return;
+    havesel = true;
+    sel.o = ivec(*x, *y, *z).mask(~(gridsize-1));
+}
+COMMAND(setselpos, "iii");
+
+void movesel(int *dir, int *dim)
+{
+    if(noedit(moving!=0)) return;
+    if(*dim < 0 || *dim > 2) return;
+    sel.o[*dim] += *dir * sel.grid;
+}
+COMMAND(movesel, "ii");
+
+///////// selection support /////////////
+
+cube &blockcube(int x, int y, int z, const block3 &b, int rgrid) // looks up a world cube, based on coordinates mapped by the block
+{
+    int dim = dimension(b.orient), dc = dimcoord(b.orient);
+    ivec s(dim, x*b.grid, y*b.grid, dc*(b.s[dim]-1)*b.grid);
+    s.add(b.o);
+    if(dc) s[dim] -= z*b.grid; else s[dim] += z*b.grid;
+    return lookupcube(s, rgrid);
+}
+
+#define loopxy(b)        loop(y,(b).s[C[dimension((b).orient)]]) loop(x,(b).s[R[dimension((b).orient)]])
+#define loopxyz(b, r, f) { loop(z,(b).s[D[dimension((b).orient)]]) loopxy((b)) { cube &c = blockcube(x,y,z,b,r); f; } }
+#define loopselxyz(f)    { if(local) makeundo(); loopxyz(sel, sel.grid, f); changed(sel); }
+#define selcube(x, y, z) blockcube(x, y, z, sel, sel.grid)
+
+////////////// cursor ///////////////
+
+int selchildcount = 0, selchildmat = -1;
+
+ICOMMAND(havesel, "", (), intret(havesel ? selchildcount : 0));
+
+void countselchild(cube *c, const ivec &cor, int size)
+{
+    ivec ss = ivec(sel.s).mul(sel.grid);
+    loopoctaboxsize(cor, size, sel.o, ss)
+    {
+        ivec o(i, cor, size);
+        if(c[i].children) countselchild(c[i].children, o, size/2);
+        else 
+        {
+            selchildcount++;
+            if(c[i].material != MAT_AIR && selchildmat != MAT_AIR)
+            {
+                if(selchildmat < 0) selchildmat = c[i].material;
+                else if(selchildmat != c[i].material) selchildmat = MAT_AIR;
+            }
+        }
+    }
+}
+
+void normalizelookupcube(const ivec &o)
+{
+    if(lusize>gridsize)
+    {
+        lu.x += (o.x-lu.x)/gridsize*gridsize;
+        lu.y += (o.y-lu.y)/gridsize*gridsize;
+        lu.z += (o.z-lu.z)/gridsize*gridsize;
+    }
+    else if(gridsize>lusize)
+    {
+        lu.x &= ~(gridsize-1);
+        lu.y &= ~(gridsize-1);
+        lu.z &= ~(gridsize-1);
+    }
+    lusize = gridsize;
+}
+
+void updateselection()
+{
+    sel.o.x = min(lastcur.x, cur.x);
+    sel.o.y = min(lastcur.y, cur.y);
+    sel.o.z = min(lastcur.z, cur.z);
+    sel.s.x = abs(lastcur.x-cur.x)/sel.grid+1;
+    sel.s.y = abs(lastcur.y-cur.y)/sel.grid+1;
+    sel.s.z = abs(lastcur.z-cur.z)/sel.grid+1;
+}
+
+bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first)
+{
+    plane pl(d, off);
+    float dist = 0.0f;
+    if(!pl.rayintersect(player->o, ray, dist))
+        return false;
+
+    dest = vec(ray).mul(dist).add(player->o);
+    if(first) handle = vec(dest).sub(o);
+    dest.sub(handle);
+    return true;
+}
+
+inline bool isheightmap(int orient, int d, bool empty, cube *c);
+extern void entdrag(const vec &ray);
+extern bool hoveringonent(int ent, int orient);
+extern void renderentselection(const vec &o, const vec &ray, bool entmoving);
+extern float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent);
+
+VAR(gridlookup, 0, 0, 1);
+VAR(passthroughcube, 0, 1, 1);
+VAR(passthroughent, 0, 1, 1);
+VARF(passthrough, 0, 0, 1, { passthroughsel = passthrough; entcancel(); });
+
+void rendereditcursor()
+{
+    int d   = dimension(sel.orient),
+        od  = dimension(orient),
+        odc = dimcoord(orient);
+
+    bool hidecursor = g3d_windowhit(true, false) || blendpaintmode, hovering = false;
+    hmapsel = false;
+
+    if(moving)
+    {
+        static vec dest, handle;
+        if(editmoveplane(vec(sel.o), camdir, od, sel.o[D[od]]+odc*sel.grid*sel.s[D[od]], handle, dest, moving==1))
+        {
+            if(moving==1)
+            {
+                dest.add(handle);
+                handle = vec(ivec(handle).mask(~(sel.grid-1)));
+                dest.sub(handle);
+                moving = 2;
+            }
+            ivec o = ivec(dest).mask(~(sel.grid-1));
+            sel.o[R[od]] = o[R[od]];
+            sel.o[C[od]] = o[C[od]];
+        }
+    }
+    else
+    if(entmoving)
+    {
+        entdrag(camdir);
+    }
+    else
+    {
+        ivec w;
+        float sdist = 0, wdist = 0, t;
+        int entorient = 0, ent = -1;
+
+        wdist = rayent(player->o, camdir, 1e16f,
+                       (editmode && showmat ? RAY_EDITMAT : 0)   // select cubes first
+                       | (!dragging && entediting && (!passthrough || !passthroughent) ? RAY_ENTS : 0)
+                       | RAY_SKIPFIRST
+                       | (passthroughcube || passthrough ? RAY_PASS : 0), gridsize, entorient, ent);
+
+        if((havesel || dragging) && !passthroughsel && !hmapedit)     // now try selecting the selection
+            if(rayboxintersect(vec(sel.o), vec(sel.s).mul(sel.grid), player->o, camdir, sdist, orient))
+            {   // and choose the nearest of the two
+                if(sdist < wdist)
+                {
+                    wdist = sdist;
+                    ent   = -1;
+                }
+            }
+
+        if((hovering = hoveringonent(hidecursor ? -1 : ent, entorient)))
+        {
+           if(!havesel)
+           {
+               selchildcount = 0;
+               selchildmat = -1;
+               sel.s = ivec(0, 0, 0);
+           }
+        }
+        else
+        {
+            vec w = vec(camdir).mul(wdist+0.05f).add(player->o);
+            if(!insideworld(w))
+            {
+                loopi(3) wdist = min(wdist, ((camdir[i] > 0 ? worldsize : 0) - player->o[i]) / camdir[i]);
+                w = vec(camdir).mul(wdist-0.05f).add(player->o);
+                if(!insideworld(w))
+                {
+                    wdist = 0;
+                    loopi(3) w[i] = clamp(player->o[i], 0.0f, float(worldsize));
+                }
+            }
+            cube *c = &lookupcube(ivec(w));
+            if(gridlookup && !dragging && !moving && !havesel && hmapedit!=1) gridsize = lusize;
+            int mag = lusize / gridsize;
+            normalizelookupcube(ivec(w));
+            if(sdist == 0 || sdist > wdist) rayboxintersect(vec(lu), vec(gridsize), player->o, camdir, t=0, orient); // just getting orient
+            cur = lu;
+            cor = ivec(vec(w).mul(2).div(gridsize));
+            od = dimension(orient);
+            d = dimension(sel.orient);
+
+            if(hmapedit==1 && dimcoord(horient) == (camdir[dimension(horient)]<0))
+            {
+                hmapsel = isheightmap(horient, dimension(horient), false, c);
+                if(hmapsel)
+                    od = dimension(orient = horient);
+            }
+
+            if(dragging)
+            {
+                updateselection();
+                sel.cx   = min(cor[R[d]], lastcor[R[d]]);
+                sel.cy   = min(cor[C[d]], lastcor[C[d]]);
+                sel.cxs  = max(cor[R[d]], lastcor[R[d]]);
+                sel.cys  = max(cor[C[d]], lastcor[C[d]]);
+
+                if(!selectcorners)
+                {
+                    sel.cx &= ~1;
+                    sel.cy &= ~1;
+                    sel.cxs &= ~1;
+                    sel.cys &= ~1;
+                    sel.cxs -= sel.cx-2;
+                    sel.cys -= sel.cy-2;
+                }
+                else
+                {
+                    sel.cxs -= sel.cx-1;
+                    sel.cys -= sel.cy-1;
+                }
+
+                sel.cx  &= 1;
+                sel.cy  &= 1;
+                havesel = true;
+            }
+            else if(!havesel)
+            {
+                sel.o = lu;
+                sel.s.x = sel.s.y = sel.s.z = 1;
+                sel.cx = sel.cy = 0;
+                sel.cxs = sel.cys = 2;
+                sel.grid = gridsize;
+                sel.orient = orient;
+                d = od;
+            }
+
+            sel.corner = (cor[R[d]]-(lu[R[d]]*2)/gridsize)+(cor[C[d]]-(lu[C[d]]*2)/gridsize)*2;
+            selchildcount = 0;
+            selchildmat = -1;
+            countselchild(worldroot, ivec(0, 0, 0), worldsize/2);
+            if(mag>=1 && selchildcount==1) 
+            {
+                selchildmat = c->material;
+                if(mag>1) selchildcount = -mag;
+            }
+        }
+    }
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_ONE, GL_ONE);
+
+    // cursors
+
+    notextureshader->set();
+
+    renderentselection(player->o, camdir, entmoving!=0);
+
+    boxoutline = outline!=0;
+
+    enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+    if(!moving && !hovering && !hidecursor)
+    {
+        if(hmapedit==1)
+            gle::colorub(0, hmapsel ? 255 : 40, 0);
+        else
+            gle::colorub(120,120,120);
+        boxs(orient, vec(lu), vec(lusize));
+    }
+
+    // selections
+    if(havesel || moving)
+    {
+        d = dimension(sel.orient);
+        gle::colorub(50,50,50);   // grid
+        boxsgrid(sel.orient, vec(sel.o), vec(sel.s), sel.grid);
+        gle::colorub(200,0,0);    // 0 reference
+        boxs3D(vec(sel.o).sub(0.5f*min(gridsize*0.25f, 2.0f)), vec(min(gridsize*0.25f, 2.0f)), 1);
+        gle::colorub(200,200,200);// 2D selection box
+        vec co(sel.o.v), cs(sel.s.v);
+        co[R[d]] += 0.5f*(sel.cx*gridsize);
+        co[C[d]] += 0.5f*(sel.cy*gridsize);
+        cs[R[d]]  = 0.5f*(sel.cxs*gridsize);
+        cs[C[d]]  = 0.5f*(sel.cys*gridsize);
+        cs[D[d]] *= gridsize;
+        boxs(sel.orient, co, cs);
+        if(hmapedit==1)         // 3D selection box
+            gle::colorub(0,120,0);
+        else
+            gle::colorub(0,0,120);
+        boxs3D(vec(sel.o), vec(sel.s), sel.grid);
+    }
+
+    disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+    boxoutline = false;
+
+    glDisable(GL_BLEND);
+}
+
+void tryedit()
+{
+    extern int hidehud;
+    if(!editmode || hidehud || mainmenu) return;
+    if(blendpaintmode) trypaintblendmap();
+}
+
+//////////// ready changes to vertex arrays ////////////
+
+static bool haschanged = false;
+
+void readychanges(const ivec &bbmin, const ivec &bbmax, cube *c, const ivec &cor, int size)
+{
+    loopoctabox(cor, size, bbmin, bbmax)
+    {
+        ivec o(i, cor, size);
+        if(c[i].ext)
+        {
+            if(c[i].ext->va)             // removes va s so that octarender will recreate
+            {
+                int hasmerges = c[i].ext->va->hasmerges;
+                destroyva(c[i].ext->va);
+                c[i].ext->va = NULL;
+                if(hasmerges) invalidatemerges(c[i], o, size, true);
+            }
+            freeoctaentities(c[i]);
+            c[i].ext->tjoints = -1;
+        }
+        if(c[i].children)
+        {
+            if(size<=1)
+            {
+                solidfaces(c[i]);
+                discardchildren(c[i], true);
+                brightencube(c[i]);
+            }
+            else readychanges(bbmin, bbmax, c[i].children, o, size/2);
+        }
+        else brightencube(c[i]);
+    }
+}
+
+void commitchanges(bool force)
+{
+    if(!force && !haschanged) return;
+    haschanged = false;
+
+    int oldlen = valist.length();
+    resetclipplanes();
+    entitiesinoctanodes();
+    inbetweenframes = false;
+    octarender();
+    inbetweenframes = true;
+    setupmaterials(oldlen);
+    invalidatepostfx();
+    updatevabbs();
+    resetblobs();
+}
+
+void changed(const block3 &sel, bool commit = true)
+{
+    if(sel.s.iszero()) return;
+    readychanges(ivec(sel.o).sub(1), ivec(sel.s).mul(sel.grid).add(sel.o).add(1), worldroot, ivec(0, 0, 0), worldsize/2);
+    haschanged = true;
+
+    if(commit) commitchanges();
+}
+
+//////////// copy and undo /////////////
+static inline void copycube(const cube &src, cube &dst)
+{
+    dst = src;
+    dst.visible = 0;
+    dst.merged = 0;
+    dst.ext = NULL; // src cube is responsible for va destruction
+    if(src.children)
+    {
+        dst.children = newcubes(F_EMPTY);
+        loopi(8) copycube(src.children[i], dst.children[i]);
+    }
+}
+
+static inline void pastecube(const cube &src, cube &dst)
+{
+    discardchildren(dst);
+    copycube(src, dst);
+}
+
+void blockcopy(const block3 &s, int rgrid, block3 *b)
+{
+    *b = s;
+    cube *q = b->c();
+    loopxyz(s, rgrid, copycube(c, *q++));
+}
+
+block3 *blockcopy(const block3 &s, int rgrid)
+{
+    int bsize = sizeof(block3)+sizeof(cube)*s.size();
+    if(bsize <= 0 || bsize > (100<<20)) return NULL;
+    block3 *b = (block3 *)new (false) uchar[bsize];
+    if(b) blockcopy(s, rgrid, b);
+    return b;
+}
+
+void freeblock(block3 *b, bool alloced = true)
+{
+    cube *q = b->c();
+    loopi(b->size()) discardchildren(*q++);
+    if(alloced) delete[] b;
+}
+
+void selgridmap(selinfo &sel, uchar *g)                           // generates a map of the cube sizes at each grid point
+{
+    loopxyz(sel, -sel.grid, (*g++ = bitscan(lusize), (void)c));
+}
+
+void freeundo(undoblock *u)
+{
+    if(!u->numents) freeblock(u->block(), false);
+    delete[] (uchar *)u;
+}
+
+void pasteundoblock(block3 *b, uchar *g)
+{
+    cube *s = b->c();
+    loopxyz(*b, 1<<min(int(*g++), worldscale-1), pastecube(*s++, c));
+}
+
+void pasteundo(undoblock *u)
+{
+    if(u->numents) pasteundoents(u);
+    else pasteundoblock(u->block(), u->gridmap());
+}
+
+static inline int undosize(undoblock *u)
+{
+    if(u->numents) return u->numents*sizeof(undoent);
+    else
+    {
+        block3 *b = u->block();
+        cube *q = b->c();
+        int size = b->size(), total = size;
+        loopj(size) total += familysize(*q++)*sizeof(cube);
+        return total;
+    }
+}
+
+struct undolist
+{
+    undoblock *first, *last;
+
+    undolist() : first(NULL), last(NULL) {}
+
+    bool empty() { return !first; }
+
+    void add(undoblock *u)
+    {
+        u->next = NULL;
+        u->prev = last;
+        if(!first) first = last = u;
+        else
+        {
+            last->next = u;
+            last = u;
+        }
+    }
+
+    undoblock *popfirst()
+    {
+        undoblock *u = first;
+        first = first->next;
+        if(first) first->prev = NULL;
+        else last = NULL;
+        return u;
+    }
+
+    undoblock *poplast()
+    {
+        undoblock *u = last;
+        last = last->prev;
+        if(last) last->next = NULL;
+        else first = NULL;
+        return u;
+    }
+};
+
+undolist undos, redos;
+VARP(undomegs, 0, 8, 100);                              // bounded by n megs
+int totalundos = 0;
+
+void pruneundos(int maxremain)                          // bound memory
+{
+    while(totalundos > maxremain && !undos.empty())
+    {
+        undoblock *u = undos.popfirst();
+        totalundos -= u->size;
+        freeundo(u);
+    }
+    //conoutf(CON_DEBUG, "undo: %d of %d(%%%d)", totalundos, undomegs<<20, totalundos*100/(undomegs<<20));
+    while(!redos.empty())
+    {
+        undoblock *u = redos.popfirst();
+        totalundos -= u->size;
+        freeundo(u);
+    }
+}
+
+void clearundos() { pruneundos(0); }
+
+COMMAND(clearundos, "");
+
+undoblock *newundocube(selinfo &s)
+{
+    int ssize = s.size(),
+        selgridsize = ssize,
+        blocksize = sizeof(block3)+ssize*sizeof(cube);
+    if(blocksize <= 0 || blocksize > (undomegs<<20)) return NULL;
+    undoblock *u = (undoblock *)new (false) uchar[sizeof(undoblock) + blocksize + selgridsize];
+    if(!u) return NULL;
+    u->numents = 0;
+    block3 *b = u->block();
+    blockcopy(s, -s.grid, b);
+    uchar *g = u->gridmap();
+    selgridmap(s, g);
+    return u;
+}
+
+void addundo(undoblock *u)
+{
+    u->size = undosize(u);
+    u->timestamp = totalmillis;
+    undos.add(u);
+    totalundos += u->size;
+    pruneundos(undomegs<<20);
+}
+
+VARP(nompedit, 0, 1, 1);
+
+void makeundo(selinfo &s)
+{
+    undoblock *u = newundocube(s);
+    if(u) addundo(u);
+}
+
+void makeundo()                        // stores state of selected cubes before editing
+{
+    if(lastsel==sel || sel.s.iszero()) return;
+    lastsel=sel;
+    makeundo(sel);
+}
+
+static inline int countblock(cube *c, int n = 8)
+{
+    int r = 0;
+    loopi(n) if(c[i].children) r += countblock(c[i].children); else ++r;
+    return r;
+}
+
+static int countblock(block3 *b) { return countblock(b->c(), b->size()); }
+
+void swapundo(undolist &a, undolist &b, int op)
+{
+    if(noedit()) return;
+    if(a.empty()) { conoutf(CON_WARN, "nothing more to %s", op == EDIT_REDO ? "redo" : "undo"); return; }
+    int ts = a.last->timestamp;
+    if(multiplayer(false))
+    {
+        int n = 0, ops = 0;
+        for(undoblock *u = a.last; u && ts==u->timestamp; u = u->prev)
+        {
+            ++ops;
+            n += u->numents ? u->numents : countblock(u->block());
+            if(ops > 10 || n > 2500)
+            {
+                conoutf(CON_WARN, "undo too big for multiplayer");
+                if(nompedit) { multiplayer(); return; }
+                op = -1;
+                break;
+            }
+        }
+    }
+    selinfo l = sel;
+    while(!a.empty() && ts==a.last->timestamp)
+    {
+        if(op >= 0) game::edittrigger(sel, op);
+        undoblock *u = a.poplast(), *r;
+        if(u->numents) r = copyundoents(u);
+        else
+        {
+            block3 *ub = u->block();
+            l.o = ub->o;
+            l.s = ub->s;
+            l.grid = ub->grid;
+            l.orient = ub->orient;
+            r = newundocube(l);
+        }
+        if(r)
+        {
+            r->size = u->size;
+            r->timestamp = totalmillis;
+            b.add(r);
+        }
+        pasteundo(u);
+        if(!u->numents) changed(*u->block(), false);
+        freeundo(u);
+    }
+    commitchanges();
+    if(!hmapsel)
+    {
+        sel = l;
+        reorient();
+    }
+    forcenextundo();
+}
+
+void editundo() { swapundo(undos, redos, EDIT_UNDO); }
+void editredo() { swapundo(redos, undos, EDIT_REDO); }
+
+// guard against subdivision
+#define protectsel(f) { undoblock *_u = newundocube(sel); f; if(_u) { pasteundo(_u); freeundo(_u); } }
+
+vector<editinfo *> editinfos;
+editinfo *localedit = NULL;
+
+template<class B>
+static void packcube(cube &c, B &buf)
+{
+    if(c.children)
+    {
+        buf.put(0xFF);
+        loopi(8) packcube(c.children[i], buf);
+    }
+    else
+    {
+        cube data = c;
+        lilswap(data.texture, 6);
+        buf.put(c.material&0xFF);
+        buf.put(c.material>>8);
+        buf.put(data.edges, sizeof(data.edges));
+        buf.put((uchar *)data.texture, sizeof(data.texture));
+    }
+}
+
+template<class B>
+static bool packblock(block3 &b, B &buf)
+{
+    if(b.size() <= 0 || b.size() > (1<<20)) return false;
+    block3 hdr = b;
+    lilswap(hdr.o.v, 3);
+    lilswap(hdr.s.v, 3);
+    lilswap(&hdr.grid, 1);
+    lilswap(&hdr.orient, 1);
+    buf.put((const uchar *)&hdr, sizeof(hdr));
+    cube *c = b.c();
+    loopi(b.size()) packcube(c[i], buf);
+    return true;
+}
+
+struct vslothdr
+{
+    ushort index;
+    ushort slot;
+};
+
+static void packvslots(cube &c, vector<uchar> &buf, vector<ushort> &used)
+{
+    if(c.children)
+    {
+        loopi(8) packvslots(c.children[i], buf, used);
+    }
+    else loopi(6)
+    {
+        ushort index = c.texture[i];
+        if(vslots.inrange(index) && vslots[index]->changed && used.find(index) < 0)
+        {
+            used.add(index);
+            VSlot &vs = *vslots[index];
+            vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr));
+            hdr.index = index;
+            hdr.slot = vs.slot->index;
+            lilswap(&hdr.index, 2);
+            packvslot(buf, vs);
+        }
+    }
+}
+
+static void packvslots(block3 &b, vector<uchar> &buf)
+{
+    vector<ushort> used;
+    cube *c = b.c();
+    loopi(b.size()) packvslots(c[i], buf, used);
+    memset(buf.pad(sizeof(vslothdr)), 0, sizeof(vslothdr));
+}
+
+template<class B>
+static void unpackcube(cube &c, B &buf)
+{
+    int mat = buf.get();
+    if(mat == 0xFF)
+    {
+        c.children = newcubes(F_EMPTY);
+        loopi(8) unpackcube(c.children[i], buf);
+    }
+    else
+    {
+        c.material = mat | (buf.get()<<8);
+        buf.get(c.edges, sizeof(c.edges));
+        buf.get((uchar *)c.texture, sizeof(c.texture));
+        lilswap(c.texture, 6);
+    }
+}
+
+template<class B>
+static bool unpackblock(block3 *&b, B &buf)
+{
+    if(b) { freeblock(b); b = NULL; }
+    block3 hdr;
+    if(buf.get((uchar *)&hdr, sizeof(hdr)) < int(sizeof(hdr))) return false;
+    lilswap(hdr.o.v, 3);
+    lilswap(hdr.s.v, 3);
+    lilswap(&hdr.grid, 1);
+    lilswap(&hdr.orient, 1);
+    if(hdr.size() > (1<<20) || hdr.grid <= 0 || hdr.grid > (1<<12)) return false;
+    b = (block3 *)new (false) uchar[sizeof(block3)+hdr.size()*sizeof(cube)];
+    if(!b) return false;
+    *b = hdr;
+    cube *c = b->c();
+    memset(c, 0, b->size()*sizeof(cube));
+    loopi(b->size()) unpackcube(c[i], buf);
+    return true;
+}
+
+struct vslotmap
+{   
+    int index;
+    VSlot *vslot;
+
+    vslotmap() {}
+    vslotmap(int index, VSlot *vslot) : index(index), vslot(vslot) {}
+};
+static vector<vslotmap> unpackingvslots;
+
+static void unpackvslots(cube &c, ucharbuf &buf)
+{
+    if(c.children)
+    {
+        loopi(8) unpackvslots(c.children[i], buf);
+    }
+    else loopi(6)
+    {
+        ushort tex = c.texture[i];
+        loopvj(unpackingvslots) if(unpackingvslots[j].index == tex) { c.texture[i] = unpackingvslots[j].vslot->index; break; }
+    }
+}
+
+static void unpackvslots(block3 &b, ucharbuf &buf)
+{
+    while(buf.remaining() >= int(sizeof(vslothdr)))
+    {
+        vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr));
+        lilswap(&hdr.index, 2);
+        if(!hdr.index) break;
+        VSlot &vs = *lookupslot(hdr.slot, false).variants;
+        VSlot ds;
+        if(!unpackvslot(buf, ds, false)) break;
+        if(vs.index < 0 || vs.index == DEFAULT_SKY) continue;
+        VSlot *edit = editvslot(vs, ds);
+        unpackingvslots.add(vslotmap(hdr.index, edit ? edit : &vs));
+    }
+
+    cube *c = b.c();
+    loopi(b.size()) unpackvslots(c[i], buf);
+
+    unpackingvslots.setsize(0);
+}
+
+static bool compresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen)
+{
+    uLongf len = compressBound(inlen);
+    if(len > (1<<20)) return false;
+    outbuf = new (false) uchar[len];
+    if(!outbuf || compress2((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen, Z_BEST_COMPRESSION) != Z_OK || len > (1<<16))
+    {
+        delete[] outbuf;
+        outbuf = NULL;
+        return false;
+    }
+    outlen = len;
+    return true;
+}
+
+static bool uncompresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen)
+{
+    if(compressBound(outlen) > (1<<20)) return false;
+    uLongf len = outlen;
+    outbuf = new (false) uchar[len];
+    if(!outbuf || uncompress((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen) != Z_OK)
+    {
+        delete[] outbuf;
+        outbuf = NULL;
+        return false;
+    }
+    outlen = len;
+    return true;
+}
+
+bool packeditinfo(editinfo *e, int &inlen, uchar *&outbuf, int &outlen)
+{
+    vector<uchar> buf;
+    if(!e || !e->copy || !packblock(*e->copy, buf)) return false;
+    packvslots(*e->copy, buf);
+    inlen = buf.length();
+    return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen);
+}
+
+bool unpackeditinfo(editinfo *&e, const uchar *inbuf, int inlen, int outlen)
+{
+    if(e && e->copy) { freeblock(e->copy); e->copy = NULL; }
+    uchar *outbuf = NULL;
+    if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false;
+    ucharbuf buf(outbuf, outlen);
+    if(!e) e = editinfos.add(new editinfo);
+    if(!unpackblock(e->copy, buf))
+    {
+        delete[] outbuf;
+        return false;
+    }
+    unpackvslots(*e->copy, buf);
+    delete[] outbuf;
+    return true;
+}
+
+void freeeditinfo(editinfo *&e)
+{
+    if(!e) return;
+    editinfos.removeobj(e);
+    if(e->copy) freeblock(e->copy);
+    delete e;
+    e = NULL;
+}
+
+bool packundo(undoblock *u, int &inlen, uchar *&outbuf, int &outlen)
+{
+    vector<uchar> buf;
+    buf.reserve(512);
+    *(ushort *)buf.pad(2) = lilswap(ushort(u->numents));
+    if(u->numents)
+    {
+        undoent *ue = u->ents();
+        loopi(u->numents)
+        {
+            *(ushort *)buf.pad(2) = lilswap(ushort(ue[i].i));
+            entity &e = *(entity *)buf.pad(sizeof(entity));
+            e = ue[i].e;
+            lilswap(&e.o.x, 3);
+            lilswap(&e.attr1, 5);
+        }
+    }
+    else
+    {
+        block3 &b = *u->block();
+        if(!packblock(b, buf)) return false;
+        buf.put(u->gridmap(), b.size());
+        packvslots(b, buf);
+    }
+    inlen = buf.length();
+    return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen);
+}
+
+bool unpackundo(const uchar *inbuf, int inlen, int outlen)
+{
+    uchar *outbuf = NULL;
+    if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false;
+    ucharbuf buf(outbuf, outlen);
+    if(buf.remaining() < 2)
+    {
+        delete[] outbuf;
+        return false;
+    }
+    int numents = lilswap(*(const ushort *)buf.pad(2));
+    if(numents)
+    {
+        if(buf.remaining() < numents*int(2 + sizeof(entity)))
+        {
+            delete[] outbuf;
+            return false;
+        }
+        loopi(numents)
+        {
+            int idx = lilswap(*(const ushort *)buf.pad(2));
+            entity &e = *(entity *)buf.pad(sizeof(entity));
+            lilswap(&e.o.x, 3);
+            lilswap(&e.attr1, 5);
+            pasteundoent(idx, e);
+        }
+    }
+    else
+    {
+        block3 *b = NULL;
+        if(!unpackblock(b, buf) || b->grid >= worldsize || buf.remaining() < b->size())
+        {
+            freeblock(b);
+            delete[] outbuf;
+            return false;
+        }
+        uchar *g = buf.pad(b->size());
+        unpackvslots(*b, buf);
+        pasteundoblock(b, g);
+        changed(*b, false);
+        freeblock(b);
+    }
+    delete[] outbuf;
+    commitchanges();
+    return true;
+}
+
+bool packundo(int op, int &inlen, uchar *&outbuf, int &outlen)
+{
+    switch(op)
+    {
+        case EDIT_UNDO: return !undos.empty() && packundo(undos.last, inlen, outbuf, outlen);
+        case EDIT_REDO: return !redos.empty() && packundo(redos.last, inlen, outbuf, outlen);
+        default: return false;
+    }
+}
+
+struct prefabheader
+{
+    char magic[4];
+    int version;
+};
+
+struct prefab : editinfo
+{
+    char *name;
+    GLuint ebo, vbo;
+    int numtris, numverts;
+
+    prefab() : name(NULL), ebo(0), vbo(0), numtris(0), numverts(0) {}
+    ~prefab() { DELETEA(name); if(copy) freeblock(copy); }
+
+    void cleanup()
+    {
+        if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; }
+        if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+        numtris = numverts = 0;
+    }
+};
+
+static hashnameset<prefab> prefabs;
+
+void cleanupprefabs()
+{
+    enumerate(prefabs, prefab, p, p.cleanup());
+}
+
+void delprefab(char *name)
+{
+    prefab *p = prefabs.access(name);
+    if(p)
+    {
+        p->cleanup();
+        prefabs.remove(name);
+        conoutf("deleted prefab %s", name);
+    }
+}
+COMMAND(delprefab, "s");
+
+void saveprefab(char *name)
+{
+    if(!name[0] || noedit(true) || (nompedit && multiplayer())) return;
+    prefab *b = prefabs.access(name);
+    if(!b)
+    {
+        b = &prefabs[name];
+        b->name = newstring(name);
+    }
+    if(b->copy) freeblock(b->copy);
+    protectsel(b->copy = blockcopy(block3(sel), sel.grid));
+    changed(sel);
+    defformatstring(filename, strpbrk(name, "/\\") ? "packages/%s.obr" : "packages/prefab/%s.obr", name);
+    path(filename);
+    stream *f = opengzfile(filename, "wb");
+    if(!f) { conoutf(CON_ERROR, "could not write prefab to %s", filename); return; }
+    prefabheader hdr;
+    memcpy(hdr.magic, "OEBR", 4);
+    hdr.version = 0;
+    lilswap(&hdr.version, 1);
+    f->write(&hdr, sizeof(hdr));
+    streambuf<uchar> s(f);
+    if(!packblock(*b->copy, s)) { delete f; conoutf(CON_ERROR, "could not pack prefab %s", filename); return; }
+    delete f;
+    conoutf("wrote prefab file %s", filename);
+}
+COMMAND(saveprefab, "s");
+
+void pasteblock(block3 &b, selinfo &sel, bool local)
+{
+    sel.s = b.s;
+    int o = sel.orient;
+    sel.orient = b.orient;
+    cube *s = b.c();
+    loopselxyz(if(!isempty(*s) || s->children || s->material != MAT_AIR) pastecube(*s, c); s++); // 'transparent'. old opaque by 'delcube; paste'
+    sel.orient = o;
+}
+
+bool prefabloaded(const char *name)
+{
+    return prefabs.access(name) != NULL;
+}
+
+prefab *loadprefab(const char *name, bool msg = true)
+{
+   prefab *b = prefabs.access(name);
+   if(b) return b;
+
+   defformatstring(filename, strpbrk(name, "/\\") ? "packages/%s.obr" : "packages/prefab/%s.obr", name);
+   path(filename);
+   stream *f = opengzfile(filename, "rb");
+   if(!f) { if(msg) conoutf(CON_ERROR, "could not read prefab %s", filename); return NULL; }
+   prefabheader hdr;
+   if(f->read(&hdr, sizeof(hdr)) != sizeof(prefabheader) || memcmp(hdr.magic, "OEBR", 4)) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s has malformatted header", filename); return NULL; }
+   lilswap(&hdr.version, 1);
+   if(hdr.version != 0) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s uses unsupported version", filename); return NULL; }
+   streambuf<uchar> s(f);
+   block3 *copy = NULL;
+   if(!unpackblock(copy, s)) { delete f; if(msg) conoutf(CON_ERROR, "could not unpack prefab %s", filename); return NULL; }
+   delete f;
+
+   b = &prefabs[name];
+   b->name = newstring(name);
+   b->copy = copy;
+
+   return b;
+}
+
+void pasteprefab(char *name)
+{
+    if(!name[0] || noedit() || (nompedit && multiplayer())) return;
+    prefab *b = loadprefab(name, true);
+    if(b) pasteblock(*b->copy, sel, true);
+}
+COMMAND(pasteprefab, "s");
+
+struct prefabmesh
+{
+    struct vertex { vec pos; bvec4 norm; };
+
+    static const int SIZE = 1<<9;
+    int table[SIZE];
+    vector<vertex> verts;
+    vector<int> chain;
+    vector<ushort> tris;
+
+    prefabmesh() { memset(table, -1, sizeof(table)); }
+
+    int addvert(const vertex &v)
+    {
+        uint h = hthash(v.pos)&(SIZE-1);
+        for(int i = table[h]; i>=0; i = chain[i])
+        {
+            const vertex &c = verts[i];
+            if(c.pos==v.pos && c.norm==v.norm) return i;
+        }
+        if(verts.length() >= USHRT_MAX) return -1;
+        verts.add(v);
+        chain.add(table[h]);
+        return table[h] = verts.length()-1;
+    }
+
+    int addvert(const vec &pos, const bvec &norm)
+    {
+        vertex vtx;
+        vtx.pos = pos;
+        vtx.norm = norm;
+        return addvert(vtx);
+   }
+
+    void setup(prefab &p)
+    {
+        if(tris.empty()) return;
+
+        p.cleanup();
+
+        loopv(verts) verts[i].norm.flip();
+        if(!p.vbo) glGenBuffers_(1, &p.vbo);
+        gle::bindvbo(p.vbo);
+        glBufferData_(GL_ARRAY_BUFFER, verts.length()*sizeof(vertex), verts.getbuf(), GL_STATIC_DRAW);
+        gle::clearvbo();
+        p.numverts = verts.length();
+
+        if(!p.ebo) glGenBuffers_(1, &p.ebo);
+        gle::bindebo(p.ebo);
+        glBufferData_(GL_ELEMENT_ARRAY_BUFFER, tris.length()*sizeof(ushort), tris.getbuf(), GL_STATIC_DRAW);
+        gle::clearebo();
+        p.numtris = tris.length()/3;
+    }
+
+};
+
+static void genprefabmesh(prefabmesh &r, cube &c, const ivec &co, int size)
+{
+    if(c.children)
+    {
+        neighbourstack[++neighbourdepth] = c.children;
+        loopi(8)
+        {
+            ivec o(i, co, size/2);
+            genprefabmesh(r, c.children[i], o, size/2);
+        }
+        --neighbourdepth;
+    }
+    else if(!isempty(c))
+    {
+        int vis; 
+        loopi(6) if((vis = visibletris(c, i, co, size)))
+        {
+            ivec v[4];
+            genfaceverts(c, i, v);
+            int convex = 0;
+            if(!flataxisface(c, i)) convex = faceconvexity(v);
+            int order = vis&4 || convex < 0 ? 1 : 0, numverts = 0;
+            vec vo(co), pos[4], norm[4];
+            pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+            if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+            pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+            if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+            guessnormals(pos, numverts, norm);
+            int index[4];
+            loopj(numverts) index[j] = r.addvert(pos[j], bvec(norm[j]));
+            loopj(numverts-2) if(index[0]!=index[j+1] && index[j+1]!=index[j+2] && index[j+2]!=index[0])
+            {
+                r.tris.add(index[0]);
+                r.tris.add(index[j+1]);
+                r.tris.add(index[j+2]);
+            }
+        }
+    }
+}
+
+void genprefabmesh(prefab &p)
+{
+    block3 b = *p.copy;
+    b.o = ivec(0, 0, 0);
+
+    cube *oldworldroot = worldroot;
+    int oldworldscale = worldscale, oldworldsize = worldsize;
+
+    worldroot = newcubes();
+    worldscale = 1;
+    worldsize = 2;
+    while(worldsize < max(max(b.s.x, b.s.y), b.s.z)*b.grid)
+    {
+        worldscale++;
+        worldsize *= 2;
+    }
+
+    cube *s = p.copy->c();
+    loopxyz(b, b.grid, if(!isempty(*s) || s->children) pastecube(*s, c); s++);
+
+    prefabmesh r;
+    neighbourstack[++neighbourdepth] = worldroot;
+    loopi(8) genprefabmesh(r, worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2);
+    --neighbourdepth;
+    r.setup(p);
+
+    freeocta(worldroot);
+
+    worldroot = oldworldroot;
+    worldscale = oldworldscale;
+    worldsize = oldworldsize;
+
+    useshaderbyname("prefab");
+}
+
+extern int outlinecolour;
+
+static void renderprefab(prefab &p, const vec &o, float yaw, float pitch, float roll, float size, const vec &color)
+{
+    if(!p.numtris)
+    {
+        genprefabmesh(p);
+        if(!p.numtris) return;
+    }
+
+    block3 &b = *p.copy;
+
+    matrix4 m;
+    m.identity();
+    m.settranslation(o);
+    if(yaw) m.rotate_around_z(yaw*RAD);
+    if(pitch) m.rotate_around_x(pitch*RAD);
+    if(roll) m.rotate_around_y(-roll*RAD);
+    matrix3 w(m);
+    if(size > 0 && size != 1) m.scale(size);
+    m.translate(vec(b.s).mul(-b.grid*0.5f));
+
+    gle::bindvbo(p.vbo);
+    gle::bindebo(p.ebo);
+    gle::enablevertex();
+    gle::enablenormal();
+    prefabmesh::vertex *v = (prefabmesh::vertex *)0;
+    gle::vertexpointer(sizeof(prefabmesh::vertex), v->pos.v);
+    gle::normalpointer(sizeof(prefabmesh::vertex), v->norm.v, GL_BYTE);
+
+    matrix4 pm;
+    pm.mul(camprojmatrix, m);
+    GLOBALPARAM(prefabmatrix, pm);
+    GLOBALPARAM(prefabworld, w);
+    SETSHADER(prefab);
+    gle::color(color);
+    glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0);
+
+    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+    pm.mul(camprojmatrix, m);
+    GLOBALPARAM(prefabmatrix, pm);
+    SETSHADER(prefab);
+    gle::color(vec::hexcolor(outlinecolour));
+    glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0);
+
+    disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+    gle::disablevertex();
+    gle::disablenormal();
+    gle::clearebo();
+    gle::clearvbo();
+}
+
+void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size, const vec &color)
+{
+    prefab *p = loadprefab(name, false);
+    if(p) renderprefab(*p, o, yaw, pitch, roll, size, color);
+}
+
+void previewprefab(const char *name, const vec &color)
+{
+    prefab *p = loadprefab(name, false);
+    if(p)
+    {
+        block3 &b = *p->copy;
+        float yaw;
+        vec o = calcmodelpreviewpos(vec(b.s).mul(b.grid*0.5f), yaw);
+        renderprefab(*p, o, yaw, 0, 0, 1, color);
+    }
+}
+
+void mpcopy(editinfo *&e, selinfo &sel, bool local)
+{
+    if(local) game::edittrigger(sel, EDIT_COPY);
+    if(e==NULL) e = editinfos.add(new editinfo);
+    if(e->copy) freeblock(e->copy);
+    e->copy = NULL;
+    protectsel(e->copy = blockcopy(block3(sel), sel.grid));
+    changed(sel);
+}
+
+void mppaste(editinfo *&e, selinfo &sel, bool local)
+{
+    if(e==NULL) return;
+    if(local) game::edittrigger(sel, EDIT_PASTE);
+    if(e->copy) pasteblock(*e->copy, sel, local);
+}
+
+void copy()
+{
+    if(noedit(true)) return;
+    mpcopy(localedit, sel, true);
+}
+
+void pastehilite()
+{
+    if(!localedit) return;
+       sel.s = localedit->copy->s;
+    reorient();
+    havesel = true;
+}
+
+void paste()
+{
+    if(noedit(true)) return;
+    mppaste(localedit, sel, true);
+}
+
+COMMAND(copy, "");
+COMMAND(pastehilite, "");
+COMMAND(paste, "");
+COMMANDN(undo, editundo, "");
+COMMANDN(redo, editredo, "");
+
+static vector<int *> editingvslots;
+struct vslotref
+{
+    vslotref(int &index) { editingvslots.add(&index); }
+    ~vslotref() { editingvslots.pop(); }
+};
+#define editingvslot(...) vslotref vslotrefs[] = { __VA_ARGS__ }; (void)vslotrefs;
+
+void compacteditvslots()
+{
+    loopv(editingvslots) if(*editingvslots[i]) compactvslot(*editingvslots[i]);
+    loopv(unpackingvslots) compactvslot(*unpackingvslots[i].vslot);
+    loopv(editinfos)
+    {
+        editinfo *e = editinfos[i];
+        compactvslots(e->copy->c(), e->copy->size());
+    }
+    for(undoblock *u = undos.first; u; u = u->next)
+        if(!u->numents)
+            compactvslots(u->block()->c(), u->block()->size());
+    for(undoblock *u = redos.first; u; u = u->next)
+        if(!u->numents)
+            compactvslots(u->block()->c(), u->block()->size());
+}
+
+///////////// height maps ////////////////
+
+#define MAXBRUSH    64
+#define MAXBRUSHC   63
+#define MAXBRUSH2   32
+int brush[MAXBRUSH][MAXBRUSH];
+VAR(brushx, 0, MAXBRUSH2, MAXBRUSH);
+VAR(brushy, 0, MAXBRUSH2, MAXBRUSH);
+bool paintbrush = 0;
+int brushmaxx = 0, brushminx = MAXBRUSH;
+int brushmaxy = 0, brushminy = MAXBRUSH;
+
+void clearbrush()
+{
+    memset(brush, 0, sizeof brush);
+    brushmaxx = brushmaxy = 0;
+    brushminx = brushminy = MAXBRUSH;
+    paintbrush = false;
+}
+
+void brushvert(int *x, int *y, int *v)
+{
+    *x += MAXBRUSH2 - brushx + 1; // +1 for automatic padding
+    *y += MAXBRUSH2 - brushy + 1;
+    if(*x<0 || *y<0 || *x>=MAXBRUSH || *y>=MAXBRUSH) return;
+    brush[*x][*y] = clamp(*v, 0, 8);
+    paintbrush = paintbrush || (brush[*x][*y] > 0);
+    brushmaxx = min(MAXBRUSH-1, max(brushmaxx, *x+1));
+    brushmaxy = min(MAXBRUSH-1, max(brushmaxy, *y+1));
+    brushminx = max(0,          min(brushminx, *x-1));
+    brushminy = max(0,          min(brushminy, *y-1));
+}
+
+vector<int> htextures;
+
+COMMAND(clearbrush, "");
+COMMAND(brushvert, "iii");
+void hmapcancel() { htextures.setsize(0); }
+COMMAND(hmapcancel, "");
+ICOMMAND(hmapselect, "", (),
+    int t = lookupcube(cur).texture[orient];
+    int i = htextures.find(t);
+    if(i<0)
+        htextures.add(t);
+    else
+        htextures.remove(i);
+);
+
+inline bool isheightmap(int o, int d, bool empty, cube *c)
+{
+    return havesel ||
+           (empty && isempty(*c)) ||
+           htextures.empty() ||
+           htextures.find(c->texture[o]) >= 0;
+}
+
+namespace hmap
+{
+#   define PAINTED     1
+#   define NOTHMAP     2
+#   define MAPPED      16
+    uchar  flags[MAXBRUSH][MAXBRUSH];
+    cube   *cmap[MAXBRUSHC][MAXBRUSHC][4];
+    int    mapz[MAXBRUSHC][MAXBRUSHC];
+    int    map [MAXBRUSH][MAXBRUSH];
+
+    selinfo changes;
+    bool selecting;
+    int d, dc, dr, dcr, biasup, br, hws, fg;
+    int gx, gy, gz, mx, my, mz, nx, ny, nz, bmx, bmy, bnx, bny;
+    uint fs;
+    selinfo hundo;
+
+    cube *getcube(ivec t, int f)
+    {
+        t[d] += dcr*f*gridsize;
+        if(t[d] > nz || t[d] < mz) return NULL;
+        cube *c = &lookupcube(t, gridsize);
+        if(c->children) forcemip(*c, false);
+        discardchildren(*c, true);
+        if(!isheightmap(sel.orient, d, true, c)) return NULL;
+        if     (t.x < changes.o.x) changes.o.x = t.x;
+        else if(t.x > changes.s.x) changes.s.x = t.x;
+        if     (t.y < changes.o.y) changes.o.y = t.y;
+        else if(t.y > changes.s.y) changes.s.y = t.y;
+        if     (t.z < changes.o.z) changes.o.z = t.z;
+        else if(t.z > changes.s.z) changes.s.z = t.z;
+        return c;
+    }
+
+    uint getface(cube *c, int d)
+    {
+        return  0x0f0f0f0f & ((dc ? c->faces[d] : 0x88888888 - c->faces[d]) >> fs);
+    }
+
+    void pushside(cube &c, int d, int x, int y, int z)
+    {
+        ivec a;
+        getcubevector(c, d, x, y, z, a);
+        a[R[d]] = 8 - a[R[d]];
+        setcubevector(c, d, x, y, z, a);
+    }
+
+    void addpoint(int x, int y, int z, int v)
+    {
+        if(!(flags[x][y] & MAPPED))
+          map[x][y] = v + (z*8);
+        flags[x][y] |= MAPPED;
+    }
+
+    void select(int x, int y, int z)
+    {
+        if((NOTHMAP & flags[x][y]) || (PAINTED & flags[x][y])) return;
+        ivec t(d, x+gx, y+gy, dc ? z : hws-z);
+        t.shl(gridpower);
+
+        // selections may damage; must makeundo before
+        hundo.o = t;
+        hundo.o[D[d]] -= dcr*gridsize*2;
+        makeundo(hundo);
+
+        cube **c = cmap[x][y];
+        loopk(4) c[k] = NULL;
+        c[1] = getcube(t, 0);
+        if(!c[1] || !isempty(*c[1]))
+        {   // try up
+            c[2] = c[1];
+            c[1] = getcube(t, 1);
+            if(!c[1] || isempty(*c[1])) { c[0] = c[1]; c[1] = c[2]; c[2] = NULL; }
+            else { z++; t[d]+=fg; }
+        }
+        else // drop down
+        {
+            z--;
+            t[d]-= fg;
+            c[0] = c[1];
+            c[1] = getcube(t, 0);
+        }
+
+        if(!c[1] || isempty(*c[1])) { flags[x][y] |= NOTHMAP; return; }
+
+        flags[x][y] |= PAINTED;
+        mapz [x][y]  = z;
+
+        if(!c[0]) c[0] = getcube(t, 1);
+        if(!c[2]) c[2] = getcube(t, -1);
+        c[3] = getcube(t, -2);
+        c[2] = !c[2] || isempty(*c[2]) ? NULL : c[2];
+        c[3] = !c[3] || isempty(*c[3]) ? NULL : c[3];
+
+        uint face = getface(c[1], d);
+        if(face == 0x08080808 && (!c[0] || !isempty(*c[0]))) { flags[x][y] |= NOTHMAP; return; }
+        if(c[1]->faces[R[d]] == F_SOLID)   // was single
+            face += 0x08080808;
+        else                               // was pair
+            face += c[2] ? getface(c[2], d) : 0x08080808;
+        face += 0x08080808;                // c[3]
+        uchar *f = (uchar*)&face;
+        addpoint(x,   y,   z, f[0]);
+        addpoint(x+1, y,   z, f[1]);
+        addpoint(x,   y+1, z, f[2]);
+        addpoint(x+1, y+1, z, f[3]);
+
+        if(selecting) // continue to adjacent cubes
+        {
+            if(x>bmx) select(x-1, y, z);
+            if(x<bnx) select(x+1, y, z);
+            if(y>bmy) select(x, y-1, z);
+            if(y<bny) select(x, y+1, z);
+        }
+    }
+
+    void ripple(int x, int y, int z, bool force)
+    {
+        if(force) select(x, y, z);
+        if((NOTHMAP & flags[x][y]) || !(PAINTED & flags[x][y])) return;
+
+        bool changed = false;
+        int *o[4], best, par, q = 0;
+        loopi(2) loopj(2) o[i+j*2] = &map[x+i][y+j];
+        #define pullhmap(I, LT, GT, M, N, A) do { \
+            best = I; \
+            loopi(4) if(*o[i] LT best) best = *o[q = i] - M; \
+            par = (best&(~7)) + N; \
+            /* dual layer for extra smoothness */ \
+            if(*o[q^3] GT par && !(*o[q^1] LT par || *o[q^2] LT par)) { \
+                if(*o[q^3] GT par A 8 || *o[q^1] != par || *o[q^2] != par) { \
+                    *o[q^3] = (*o[q^3] GT par A 8 ? par A 8 : *o[q^3]); \
+                    *o[q^1] = *o[q^2] = par; \
+                    changed = true; \
+                } \
+            /* single layer */ \
+            } else { \
+                loopj(4) if(*o[j] GT par) { \
+                    *o[j] = par; \
+                    changed = true; \
+                } \
+            } \
+        } while(0)
+
+        if(biasup)
+            pullhmap(0, >, <, 1, 0, -);
+        else
+            pullhmap(worldsize*8, <, >, 0, 8, +);
+
+        cube **c  = cmap[x][y];
+        int e[2][2];
+        int notempty = 0;
+
+        loopk(4) if(c[k]) {
+            loopi(2) loopj(2) {
+                e[i][j] = min(8, map[x+i][y+j] - (mapz[x][y]+3-k)*8);
+                notempty |= e[i][j] > 0;
+            }
+            if(notempty)
+            {
+                c[k]->texture[sel.orient] = c[1]->texture[sel.orient];
+                solidfaces(*c[k]);
+                loopi(2) loopj(2)
+                {
+                    int f = e[i][j];
+                    if(f<0 || (f==0 && e[1-i][j]==0 && e[i][1-j]==0))
+                    {
+                        f=0;
+                        pushside(*c[k], d, i, j, 0);
+                        pushside(*c[k], d, i, j, 1);
+                    }
+                    edgeset(cubeedge(*c[k], d, i, j), dc, dc ? f : 8-f);
+                }
+            }
+            else
+                emptyfaces(*c[k]);
+        }
+
+        if(!changed) return;
+        if(x>mx) ripple(x-1, y, mapz[x][y], true);
+        if(x<nx) ripple(x+1, y, mapz[x][y], true);
+        if(y>my) ripple(x, y-1, mapz[x][y], true);
+        if(y<ny) ripple(x, y+1, mapz[x][y], true);
+
+#define DIAGONAL_RIPPLE(a,b,exp) if(exp) { \
+            if(flags[x a][ y] & PAINTED) \
+                ripple(x a, y b, mapz[x a][y], true); \
+            else if(flags[x][y b] & PAINTED) \
+                ripple(x a, y b, mapz[x][y b], true); \
+        }
+
+        DIAGONAL_RIPPLE(-1, -1, (x>mx && y>my)); // do diagonals because adjacents
+        DIAGONAL_RIPPLE(-1, +1, (x>mx && y<ny)); //    won't unless changed
+        DIAGONAL_RIPPLE(+1, +1, (x<nx && y<ny));
+        DIAGONAL_RIPPLE(+1, -1, (x<nx && y>my));
+    }
+
+#define loopbrush(i) for(int x=bmx; x<=bnx+i; x++) for(int y=bmy; y<=bny+i; y++)
+
+    void paint()
+    {
+        loopbrush(1)
+            map[x][y] -= dr * brush[x][y];
+    }
+
+    void smooth()
+    {
+        int sum, div;
+        loopbrush(-2)
+        {
+            sum = 0;
+            div = 9;
+            loopi(3) loopj(3)
+                if(flags[x+i][y+j] & MAPPED)
+                    sum += map[x+i][y+j];
+                else div--;
+            if(div)
+                map[x+1][y+1] = sum / div;
+        }
+    }
+
+    void rippleandset()
+    {
+        loopbrush(0)
+            ripple(x, y, gz, false);
+    }
+
+    void run(int dir, int mode)
+    {
+        d  = dimension(sel.orient);
+        dc = dimcoord(sel.orient);
+        dcr= dc ? 1 : -1;
+        dr = dir>0 ? 1 : -1;
+        br = dir>0 ? 0x08080808 : 0;
+     //   biasup = mode == dir<0;
+        biasup = dir<0;
+        bool paintme = paintbrush;
+        int cx = (sel.corner&1 ? 0 : -1);
+        int cy = (sel.corner&2 ? 0 : -1);
+        hws= (worldsize>>gridpower);
+        gx = (cur[R[d]] >> gridpower) + cx - MAXBRUSH2;
+        gy = (cur[C[d]] >> gridpower) + cy - MAXBRUSH2;
+        gz = (cur[D[d]] >> gridpower);
+        fs = dc ? 4 : 0;
+        fg = dc ? gridsize : -gridsize;
+        mx = max(0, -gx); // ripple range
+        my = max(0, -gy);
+        nx = min(MAXBRUSH-1, hws-gx) - 1;
+        ny = min(MAXBRUSH-1, hws-gy) - 1;
+        if(havesel)
+        {   // selection range
+            bmx = mx = max(mx, (sel.o[R[d]]>>gridpower)-gx);
+            bmy = my = max(my, (sel.o[C[d]]>>gridpower)-gy);
+            bnx = nx = min(nx, (sel.s[R[d]]+(sel.o[R[d]]>>gridpower))-gx-1);
+            bny = ny = min(ny, (sel.s[C[d]]+(sel.o[C[d]]>>gridpower))-gy-1);
+        }
+        if(havesel && mode<0) // -ve means smooth selection
+            paintme = false;
+        else
+        {   // brush range
+            bmx = max(mx, brushminx);
+            bmy = max(my, brushminy);
+            bnx = min(nx, brushmaxx-1);
+            bny = min(ny, brushmaxy-1);
+        }
+        nz = worldsize-gridsize;
+        mz = 0;
+        hundo.s = ivec(d,1,1,5);
+        hundo.orient = sel.orient;
+        hundo.grid = gridsize;
+        forcenextundo();
+
+        changes.grid = gridsize;
+        changes.s = changes.o = cur;
+        memset(map, 0, sizeof map);
+        memset(flags, 0, sizeof flags);
+
+        selecting = true;
+        select(clamp(MAXBRUSH2-cx, bmx, bnx),
+               clamp(MAXBRUSH2-cy, bmy, bny),
+               dc ? gz : hws - gz);
+        selecting = false;
+        if(paintme)
+            paint();
+        else
+            smooth();
+        rippleandset();                       // pull up points to cubify, and set
+        changes.s.sub(changes.o).shr(gridpower).add(1);
+        changed(changes);
+    }
+}
+
+void edithmap(int dir, int mode) {
+    if((nompedit && multiplayer()) || !hmapsel) return;
+    hmap::run(dir, mode);
+}
+
+///////////// main cube edit ////////////////
+
+int bounded(int n) { return n<0 ? 0 : (n>8 ? 8 : n); }
+
+void pushedge(uchar &edge, int dir, int dc)
+{
+    int ne = bounded(edgeget(edge, dc)+dir);
+    edgeset(edge, dc, ne);
+    int oe = edgeget(edge, 1-dc);
+    if((dir<0 && dc && oe>ne) || (dir>0 && dc==0 && oe<ne)) edgeset(edge, 1-dc, ne);
+}
+
+void linkedpush(cube &c, int d, int x, int y, int dc, int dir)
+{
+    ivec v, p;
+    getcubevector(c, d, x, y, dc, v);
+
+    loopi(2) loopj(2)
+    {
+        getcubevector(c, d, i, j, dc, p);
+        if(v==p)
+            pushedge(cubeedge(c, d, i, j), dir, dc);
+    }
+}
+
+static ushort getmaterial(cube &c)
+{
+    if(c.children)
+    {
+        ushort mat = getmaterial(c.children[7]);
+        loopi(7) if(mat != getmaterial(c.children[i])) return MAT_AIR;
+        return mat;
+    }
+    return c.material;
+}
+
+VAR(invalidcubeguard, 0, 1, 1);
+
+void mpeditface(int dir, int mode, selinfo &sel, bool local)
+{
+    if(mode==1 && (sel.cx || sel.cy || sel.cxs&1 || sel.cys&1)) mode = 0;
+    int d = dimension(sel.orient);
+    int dc = dimcoord(sel.orient);
+    int seldir = dc ? -dir : dir;
+
+    if(local)
+        game::edittrigger(sel, EDIT_FACE, dir, mode);
+
+    if(mode==1)
+    {
+        int h = sel.o[d]+dc*sel.grid;
+        if(((dir>0) == dc && h<=0) || ((dir<0) == dc && h>=worldsize)) return;
+        if(dir<0) sel.o[d] += sel.grid * seldir;
+    }
+
+    if(dc) sel.o[d] += sel.us(d)-sel.grid;
+    sel.s[d] = 1;
+
+    loopselxyz(
+        if(c.children) solidfaces(c);
+        ushort mat = getmaterial(c);
+        discardchildren(c, true);
+        c.material = mat;
+        if(mode==1) // fill command
+        {
+            if(dir<0)
+            {
+                solidfaces(c);
+                cube &o = blockcube(x, y, 1, sel, -sel.grid);
+                loopi(6)
+                    c.texture[i] = o.children ? DEFAULT_GEOM : o.texture[i];
+            }
+            else
+                emptyfaces(c);
+        }
+        else
+        {
+            uint bak = c.faces[d];
+            uchar *p = (uchar *)&c.faces[d];
+
+            if(mode==2)
+                linkedpush(c, d, sel.corner&1, sel.corner>>1, dc, seldir); // corner command
+            else
+            {
+                loop(mx,2) loop(my,2)                                       // pull/push edges command
+                {
+                    if(x==0 && mx==0 && sel.cx) continue;
+                    if(y==0 && my==0 && sel.cy) continue;
+                    if(x==sel.s[R[d]]-1 && mx==1 && (sel.cx+sel.cxs)&1) continue;
+                    if(y==sel.s[C[d]]-1 && my==1 && (sel.cy+sel.cys)&1) continue;
+                    if(p[mx+my*2] != ((uchar *)&bak)[mx+my*2]) continue;
+
+                    linkedpush(c, d, mx, my, dc, seldir);
+                }
+            }
+
+            optiface(p, c);
+            if(invalidcubeguard==1 && !isvalidcube(c))
+            {
+                uint newbak = c.faces[d];
+                uchar *m = (uchar *)&bak;
+                uchar *n = (uchar *)&newbak;
+                loopk(4) if(n[k] != m[k]) // tries to find partial edit that is valid
+                {
+                    c.faces[d] = bak;
+                    c.edges[d*4+k] = n[k];
+                    if(isvalidcube(c))
+                        m[k] = n[k];
+                }
+                c.faces[d] = bak;
+            }
+        }
+    );
+    if (mode==1 && dir>0)
+        sel.o[d] += sel.grid * seldir;
+}
+
+void editface(int *dir, int *mode)
+{
+    if(noedit(moving!=0)) return;
+    if(hmapedit!=1)
+        mpeditface(*dir, *mode, sel, true);
+    else
+        edithmap(*dir, *mode);
+}
+
+VAR(selectionsurf, 0, 0, 1);
+
+void pushsel(int *dir)
+{
+    if(noedit(moving!=0)) return;
+    int d = dimension(orient);
+    int s = dimcoord(orient) ? -*dir : *dir;
+    sel.o[d] += s*sel.grid;
+    if(selectionsurf==1)
+    {
+        player->o[d] += s*sel.grid;
+        player->resetinterp();
+    }
+}
+
+void mpdelcube(selinfo &sel, bool local)
+{
+    if(local) game::edittrigger(sel, EDIT_DELCUBE);
+    loopselxyz(discardchildren(c, true); emptyfaces(c));
+}
+
+void delcube()
+{
+    if(noedit(true)) return;
+    mpdelcube(sel, true);
+}
+
+COMMAND(pushsel, "i");
+COMMAND(editface, "ii");
+COMMAND(delcube, "");
+
+/////////// texture editing //////////////////
+
+int curtexindex = -1, lasttex = 0, lasttexmillis = -1;
+int texpaneltimer = 0;
+vector<ushort> texmru;
+
+void tofronttex()                                       // maintain most recently used of the texture lists when applying texture
+{
+    int c = curtexindex;
+    if(texmru.inrange(c))
+    {
+        texmru.insert(0, texmru.remove(c));
+        curtexindex = -1;
+    }
+}
+
+selinfo repsel;
+int reptex = -1;
+
+static vector<vslotmap> remappedvslots;
+
+VAR(usevdelta, 1, 0, 0);
+
+static VSlot *remapvslot(int index, bool delta, const VSlot &ds)
+{
+    loopv(remappedvslots) if(remappedvslots[i].index == index) return remappedvslots[i].vslot;
+    VSlot &vs = lookupvslot(index, false);
+    if(vs.index < 0 || vs.index == DEFAULT_SKY) return NULL;
+    VSlot *edit = NULL;
+    if(delta)
+    {
+        VSlot ms;
+        mergevslot(ms, vs, ds);
+        edit = ms.changed ? editvslot(vs, ms) : vs.slot->variants;
+    }
+    else edit = ds.changed ? editvslot(vs, ds) : vs.slot->variants;
+    if(!edit) edit = &vs;
+    remappedvslots.add(vslotmap(vs.index, edit));
+    return edit;
+}
+
+static void remapvslots(cube &c, bool delta, const VSlot &ds, int orient, bool &findrep, VSlot *&findedit)
+{
+    if(c.children)
+    {
+        loopi(8) remapvslots(c.children[i], delta, ds, orient, findrep, findedit);
+        return;
+    }
+    static VSlot ms;
+    if(orient<0) loopi(6)
+    {
+        VSlot *edit = remapvslot(c.texture[i], delta, ds);
+        if(edit)
+        {
+            c.texture[i] = edit->index;
+            if(!findedit) findedit = edit;
+        }
+    }
+    else
+    {
+        int i = visibleorient(c, orient);
+        VSlot *edit = remapvslot(c.texture[i], delta, ds);
+        if(edit)
+        {
+            if(findrep)
+            {
+                if(reptex < 0) reptex = c.texture[i];
+                else if(reptex != c.texture[i]) findrep = false;
+            }
+            c.texture[i] = edit->index;
+            if(!findedit) findedit = edit;
+        }
+    }
+}
+
+void edittexcube(cube &c, int tex, int orient, bool &findrep)
+{
+    if(orient<0) loopi(6) c.texture[i] = tex;
+    else
+    {
+        int i = visibleorient(c, orient);
+        if(findrep)
+        {
+            if(reptex < 0) reptex = c.texture[i];
+            else if(reptex != c.texture[i]) findrep = false;
+        }
+        c.texture[i] = tex;
+    }
+    if(c.children) loopi(8) edittexcube(c.children[i], tex, orient, findrep);
+}
+
+VAR(allfaces, 0, 0, 1);
+
+void mpeditvslot(int delta, VSlot &ds, int allfaces, selinfo &sel, bool local)
+{
+    if(local)
+    {
+        game::edittrigger(sel, EDIT_VSLOT, delta, allfaces, 0, &ds);
+        if(!(lastsel==sel)) tofronttex();
+        if(allfaces || !(repsel == sel)) reptex = -1;
+        repsel = sel;
+    }
+    bool findrep = local && !allfaces && reptex < 0;
+    VSlot *findedit = NULL;
+    loopselxyz(remapvslots(c, delta != 0, ds, allfaces ? -1 : sel.orient, findrep, findedit));
+    remappedvslots.setsize(0);
+    if(local && findedit)
+    {
+        lasttex = findedit->index;
+        lasttexmillis = totalmillis;
+        curtexindex = texmru.find(lasttex);
+        if(curtexindex < 0)
+        {
+            curtexindex = texmru.length();
+            texmru.add(lasttex);
+        }
+    }
+}
+
+bool mpeditvslot(int delta, int allfaces, selinfo &sel, ucharbuf &buf)
+{
+    VSlot ds;
+    if(!unpackvslot(buf, ds, delta != 0)) return false;
+    editingvslot(ds.layer);
+    mpeditvslot(delta, ds, allfaces, sel, false);
+    return true;
+}
+
+void vdelta(char *body)
+{
+    if(noedit()) return;
+    usevdelta++;
+    execute(body);
+    usevdelta--;
+}
+COMMAND(vdelta, "s");
+
+void vrotate(int *n)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_ROTATION;
+    ds.rotation = usevdelta ? *n : clamp(*n, 0, 7);
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vrotate, "i");
+ICOMMAND(getvrotate, "i", (int *tex), intret(lookupvslot(*tex, false).rotation));
+
+void voffset(int *x, int *y)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_OFFSET;
+    ds.offset = usevdelta ? ivec2(*x, *y) : ivec2(*x, *y).max(0);
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(voffset, "ii");
+ICOMMAND(getvoffset, "i", (int *tex),
+{
+    VSlot &vslot = lookupvslot(*tex, false);
+    defformatstring(str, "%d %d", vslot.offset.x, vslot.offset.y);
+    result(str);
+});
+
+void vscroll(float *s, float *t)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_SCROLL;
+    ds.scroll = vec2(*s, *t).div(1000);
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vscroll, "ff");
+ICOMMAND(getvscroll, "i", (int *tex),
+{
+    VSlot &vslot = lookupvslot(*tex, false);
+    defformatstring(str, "%s %s", floatstr(vslot.scroll.x), floatstr(vslot.scroll.y));
+    result(str);
+});
+
+void vscale(float *scale)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_SCALE;
+    ds.scale = *scale <= 0 ? 1 : (usevdelta ? *scale : clamp(*scale, 1/8.0f, 8.0f));
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vscale, "f");
+ICOMMAND(getvscale, "i", (int *tex), floatret(lookupvslot(*tex, false).scale));
+
+void vlayer(int *n)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_LAYER;
+    if(vslots.inrange(*n))
+    {
+        ds.layer = *n;
+        if(vslots[ds.layer]->changed && nompedit && multiplayer()) return;
+    }
+    editingvslot(ds.layer);
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vlayer, "i");
+ICOMMAND(getvlayer, "i", (int *tex), intret(lookupvslot(*tex, false).layer));
+
+void valpha(float *front, float *back)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_ALPHA;
+    ds.alphafront = clamp(*front, 0.0f, 1.0f);
+    ds.alphaback = clamp(*back, 0.0f, 1.0f);
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(valpha, "ff");
+ICOMMAND(getvalpha, "i", (int *tex),
+{
+    VSlot &vslot = lookupvslot(*tex, false);
+    defformatstring(str, "%s %s", floatstr(vslot.alphafront), floatstr(vslot.alphaback));
+    result(str);
+});
+
+void vcolor(float *r, float *g, float *b)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_COLOR;
+    ds.colorscale = vec(clamp(*r, 0.0f, 1.0f), clamp(*g, 0.0f, 1.0f), clamp(*b, 0.0f, 1.0f));
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vcolor, "fff");
+ICOMMAND(getvcolor, "i", (int *tex),
+{
+    VSlot &vslot = lookupvslot(*tex, false);
+    defformatstring(str, "%s %s %s", floatstr(vslot.colorscale.r), floatstr(vslot.colorscale.g), floatstr(vslot.colorscale.b));
+    result(str);
+});
+
+void vreset()
+{
+    if(noedit()) return;
+    VSlot ds;
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vreset, "");
+
+void vshaderparam(const char *name, float *x, float *y, float *z, float *w)
+{
+    if(noedit()) return;
+    VSlot ds;
+    ds.changed = 1<<VSLOT_SHPARAM;
+    if(name[0])
+    {
+        SlotShaderParam p = { getshaderparamname(name), -1, {*x, *y, *z, *w} };
+        ds.params.add(p);
+    }
+    mpeditvslot(usevdelta, ds, allfaces, sel, true);
+}
+COMMAND(vshaderparam, "sffff");
+ICOMMAND(getvshaderparam, "is", (int *tex, const char *name),
+{
+    VSlot &vslot = lookupvslot(*tex, false);
+    loopv(vslot.params)
+    {
+        SlotShaderParam &p = vslot.params[i];
+        if(!strcmp(p.name, name))
+        {
+            defformatstring(str, "%s %s %s %s", floatstr(p.val[0]), floatstr(p.val[1]), floatstr(p.val[2]), floatstr(p.val[3]));
+            result(str);
+            return;
+        }
+    }
+});
+ICOMMAND(getvshaderparamnames, "i", (int *tex),
+{
+    VSlot &vslot = lookupvslot(*tex, false);
+    vector<char> str;
+    loopv(vslot.params)
+    {
+        SlotShaderParam &p = vslot.params[i];
+        if(i) str.put(' ');
+        str.put(p.name, strlen(p.name));
+    }
+    str.add('\0');
+    stringret(newstring(str.getbuf(), str.length()-1));
+});
+
+void mpedittex(int tex, int allfaces, selinfo &sel, bool local)
+{
+    if(local)
+    {
+        game::edittrigger(sel, EDIT_TEX, tex, allfaces);
+        if(allfaces || !(repsel == sel)) reptex = -1;
+        repsel = sel;
+    }
+    bool findrep = local && !allfaces && reptex < 0;
+    loopselxyz(edittexcube(c, tex, allfaces ? -1 : sel.orient, findrep));
+}
+
+static int unpacktex(int &tex, ucharbuf &buf, bool insert = true)
+{
+    if(tex < 0x10000) return true;
+    VSlot ds;
+    if(!unpackvslot(buf, ds, false)) return false;
+    VSlot &vs = *lookupslot(tex & 0xFFFF, false).variants;
+    if(vs.index < 0 || vs.index == DEFAULT_SKY) return false;
+    VSlot *edit = insert ? editvslot(vs, ds) : findvslot(*vs.slot, vs, ds);
+    if(!edit) return false;
+    tex = edit->index;
+    return true;
+}
+
+int shouldpacktex(int index)
+{
+    if(vslots.inrange(index))
+    {
+        VSlot &vs = *vslots[index];
+        if(vs.changed) return 0x10000 + vs.slot->index;
+    }
+    return 0;
+}
+
+
+bool mpedittex(int tex, int allfaces, selinfo &sel, ucharbuf &buf)
+{
+    if(!unpacktex(tex, buf)) return false;
+    mpedittex(tex, allfaces, sel, false);
+    return true;
+}
+
+void filltexlist()
+{
+    if(texmru.length()!=vslots.length())
+    {
+        loopvrev(texmru) if(texmru[i]>=vslots.length())
+        {
+            if(curtexindex > i) curtexindex--;
+            else if(curtexindex == i) curtexindex = -1;
+            texmru.remove(i);
+        }
+        loopv(vslots) if(texmru.find(i)<0) texmru.add(i);
+    }
+}
+
+void compactmruvslots()
+{
+    remappedvslots.setsize(0);
+    loopvrev(texmru)
+    {
+        if(vslots.inrange(texmru[i]))
+        {
+            VSlot &vs = *vslots[texmru[i]];
+            if(vs.index >= 0)
+            {
+                texmru[i] = vs.index;
+                continue;
+            }
+        }
+        if(curtexindex > i) curtexindex--;
+        else if(curtexindex == i) curtexindex = -1;
+        texmru.remove(i);
+    }
+    if(vslots.inrange(lasttex))
+    {
+        VSlot &vs = *vslots[lasttex];
+        lasttex = vs.index >= 0 ? vs.index : 0;
+    }
+    else lasttex = 0;
+    reptex = vslots.inrange(reptex) ? vslots[reptex]->index : -1;
+}
+
+void edittex(int i, bool save = true)
+{
+    lasttex = i;
+    lasttexmillis = totalmillis;
+    if(save)
+    {
+        loopvj(texmru) if(texmru[j]==lasttex) { curtexindex = j; break; }
+    }
+    mpedittex(i, allfaces, sel, true);
+}
+
+void edittex_(int *dir)
+{
+    if(noedit()) return;
+    filltexlist();
+    if(texmru.empty()) return;
+    texpaneltimer = 5000;
+    if(!(lastsel==sel)) tofronttex();
+    curtexindex = clamp(curtexindex<0 ? 0 : curtexindex+*dir, 0, texmru.length()-1);
+    edittex(texmru[curtexindex], false);
+}
+
+void gettex()
+{
+    if(noedit(true)) return;
+    filltexlist();
+    int tex = -1;
+    loopxyz(sel, sel.grid, tex = c.texture[sel.orient]);
+    loopv(texmru) if(texmru[i]==tex)
+    {
+        curtexindex = i;
+        tofronttex();
+        return;
+    }
+}
+
+void getcurtex()
+{
+    if(noedit(true)) return;
+    filltexlist();
+    int index = curtexindex < 0 ? 0 : curtexindex;
+    if(!texmru.inrange(index)) return;
+    intret(texmru[index]);
+}
+
+void getseltex()
+{
+    if(noedit(true)) return;
+    cube &c = lookupcube(sel.o, -sel.grid);
+    if(c.children || isempty(c)) return;
+    intret(c.texture[sel.orient]);
+}
+
+void gettexname(int *tex, int *subslot)
+{
+    if(noedit(true) || *tex<0) return;
+    VSlot &vslot = lookupvslot(*tex, false);
+    Slot &slot = *vslot.slot;
+    if(!slot.sts.inrange(*subslot)) return;
+    result(slot.sts[*subslot].name);
+}
+
+void getslottex(int *idx)
+{
+    if(*idx < 0 || !slots.inrange(*idx)) { intret(-1); return; }
+    Slot &slot = lookupslot(*idx, false);
+    intret(slot.variants->index);
+}
+
+COMMANDN(edittex, edittex_, "i");
+COMMAND(gettex, "");
+COMMAND(getcurtex, "");
+COMMAND(getseltex, "");
+ICOMMAND(getreptex, "", (), { if(!noedit()) intret(vslots.inrange(reptex) ? reptex : -1); });
+COMMAND(gettexname, "ii");
+ICOMMAND(numvslots, "", (), intret(vslots.length()));
+ICOMMAND(numslots, "", (), intret(slots.length()));
+COMMAND(getslottex, "i");
+ICOMMAND(texloaded, "i", (int *tex), intret(slots.inrange(*tex) && slots[*tex]->loaded ? 1 : 0));
+
+void replacetexcube(cube &c, int oldtex, int newtex)
+{
+    loopi(6) if(c.texture[i] == oldtex) c.texture[i] = newtex;
+    if(c.children) loopi(8) replacetexcube(c.children[i], oldtex, newtex);
+}
+
+void mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, bool local)
+{
+    if(local) game::edittrigger(sel, EDIT_REPLACE, oldtex, newtex, insel ? 1 : 0);
+    if(insel)
+    {
+        loopselxyz(replacetexcube(c, oldtex, newtex));
+    }
+    else
+    {
+        loopi(8) replacetexcube(worldroot[i], oldtex, newtex);
+    }
+    allchanged();
+}
+
+bool mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, ucharbuf &buf)
+{
+    if(!unpacktex(oldtex, buf, false)) return false;
+    editingvslot(oldtex);
+    if(!unpacktex(newtex, buf)) return false;
+    mpreplacetex(oldtex, newtex, insel, sel, false);
+    return true;
+}
+
+void replace(bool insel)
+{
+    if(noedit()) return;
+    if(reptex < 0) { conoutf(CON_ERROR, "can only replace after a texture edit"); return; }
+    mpreplacetex(reptex, lasttex, insel, sel, true);
+}
+
+ICOMMAND(replace, "", (), replace(false));
+ICOMMAND(replacesel, "", (), replace(true));
+
+////////// flip and rotate ///////////////
+uint dflip(uint face) { return face==F_EMPTY ? face : 0x88888888 - (((face&0xF0F0F0F0)>>4) | ((face&0x0F0F0F0F)<<4)); }
+uint cflip(uint face) { return ((face&0xFF00FF00)>>8) | ((face&0x00FF00FF)<<8); }
+uint rflip(uint face) { return ((face&0xFFFF0000)>>16)| ((face&0x0000FFFF)<<16); }
+uint mflip(uint face) { return (face&0xFF0000FF) | ((face&0x00FF0000)>>8) | ((face&0x0000FF00)<<8); }
+
+void flipcube(cube &c, int d)
+{
+    swap(c.texture[d*2], c.texture[d*2+1]);
+    c.faces[D[d]] = dflip(c.faces[D[d]]);
+    c.faces[C[d]] = cflip(c.faces[C[d]]);
+    c.faces[R[d]] = rflip(c.faces[R[d]]);
+    if(c.children)
+    {
+        loopi(8) if(i&octadim(d)) swap(c.children[i], c.children[i-octadim(d)]);
+        loopi(8) flipcube(c.children[i], d);
+    }
+}
+
+void rotatequad(cube &a, cube &b, cube &c, cube &d)
+{
+    cube t = a; a = b; b = c; c = d; d = t;
+}
+
+void rotatecube(cube &c, int d)   // rotates cube clockwise. see pics in cvs for help.
+{
+    c.faces[D[d]] = cflip (mflip(c.faces[D[d]]));
+    c.faces[C[d]] = dflip (mflip(c.faces[C[d]]));
+    c.faces[R[d]] = rflip (mflip(c.faces[R[d]]));
+    swap(c.faces[R[d]], c.faces[C[d]]);
+
+    swap(c.texture[2*R[d]], c.texture[2*C[d]+1]);
+    swap(c.texture[2*C[d]], c.texture[2*R[d]+1]);
+    swap(c.texture[2*C[d]], c.texture[2*C[d]+1]);
+
+    if(c.children)
+    {
+        int row = octadim(R[d]);
+        int col = octadim(C[d]);
+        for(int i=0; i<=octadim(d); i+=octadim(d)) rotatequad
+        (
+            c.children[i+row],
+            c.children[i],
+            c.children[i+col],
+            c.children[i+col+row]
+        );
+        loopi(8) rotatecube(c.children[i], d);
+    }
+}
+
+void mpflip(selinfo &sel, bool local)
+{
+    if(local) 
+    { 
+        game::edittrigger(sel, EDIT_FLIP);
+        makeundo();
+    }
+    int zs = sel.s[dimension(sel.orient)];
+    loopxy(sel)
+    {
+        loop(z,zs) flipcube(selcube(x, y, z), dimension(sel.orient));
+        loop(z,zs/2)
+        {
+            cube &a = selcube(x, y, z);
+            cube &b = selcube(x, y, zs-z-1);
+            swap(a, b);
+        }
+    }
+    changed(sel);
+}
+
+void flip()
+{
+    if(noedit()) return;
+    mpflip(sel, true);
+}
+
+void mprotate(int cw, selinfo &sel, bool local)
+{
+    if(local) game::edittrigger(sel, EDIT_ROTATE, cw);
+    int d = dimension(sel.orient);
+    if(!dimcoord(sel.orient)) cw = -cw;
+    int m = sel.s[C[d]] < sel.s[R[d]] ? C[d] : R[d];
+    int ss = sel.s[m] = max(sel.s[R[d]], sel.s[C[d]]);
+    if(local) makeundo();
+    loop(z,sel.s[D[d]]) loopi(cw>0 ? 1 : 3)
+    {
+        loopxy(sel) rotatecube(selcube(x,y,z), d);
+        loop(y,ss/2) loop(x,ss-1-y*2) rotatequad
+        (
+            selcube(ss-1-y, x+y, z),
+            selcube(x+y, y, z),
+            selcube(y, ss-1-x-y, z),
+            selcube(ss-1-x-y, ss-1-y, z)
+        );
+    }
+    changed(sel);
+}
+
+void rotate(int *cw)
+{
+    if(noedit()) return;
+    mprotate(*cw, sel, true);
+}
+
+COMMAND(flip, "");
+COMMAND(rotate, "i");
+
+enum { EDITMATF_EMPTY = 0x10000, EDITMATF_NOTEMPTY = 0x20000, EDITMATF_SOLID = 0x30000, EDITMATF_NOTSOLID = 0x40000 };
+static const struct { const char *name; int filter; } editmatfilters[] = 
+{ 
+    { "empty", EDITMATF_EMPTY },
+    { "notempty", EDITMATF_NOTEMPTY },
+    { "solid", EDITMATF_SOLID },
+    { "notsolid", EDITMATF_NOTSOLID }
+};
+
+void setmat(cube &c, ushort mat, ushort matmask, ushort filtermat, ushort filtermask, int filtergeom)
+{
+    if(c.children)
+        loopi(8) setmat(c.children[i], mat, matmask, filtermat, filtermask, filtergeom);
+    else if((c.material&filtermask) == filtermat)
+    {
+        switch(filtergeom)
+        {
+            case EDITMATF_EMPTY: if(isempty(c)) break; return;
+            case EDITMATF_NOTEMPTY: if(!isempty(c)) break; return;
+            case EDITMATF_SOLID: if(isentirelysolid(c)) break; return;
+            case EDITMATF_NOTSOLID: if(!isentirelysolid(c)) break; return;
+        }
+        if(mat!=MAT_AIR)
+        {
+            c.material &= matmask;
+            c.material |= mat;
+        }
+        else c.material = MAT_AIR;
+    }
+}
+
+void mpeditmat(int matid, int filter, selinfo &sel, bool local)
+{
+    if(local) game::edittrigger(sel, EDIT_MAT, matid, filter);
+
+    ushort filtermat = 0, filtermask = 0, matmask;
+    int filtergeom = 0;
+    if(filter >= 0)
+    {
+        filtermat = filter&0xFFFF;
+        filtermask = filtermat&(MATF_VOLUME|MATF_INDEX) ? MATF_VOLUME|MATF_INDEX : (filtermat&MATF_CLIP ? MATF_CLIP : filtermat);
+        filtergeom = filter&~0xFFFF;
+    }
+    if(matid < 0)
+    {
+        matid = 0;
+        matmask = filtermask;
+        if(isclipped(filtermat&MATF_VOLUME)) matmask &= ~MATF_CLIP;
+        if(isdeadly(filtermat&MATF_VOLUME)) matmask &= ~MAT_DEATH;
+    }
+    else
+    {
+        matmask = matid&(MATF_VOLUME|MATF_INDEX) ? 0 : (matid&MATF_CLIP ? ~MATF_CLIP : ~matid);
+        if(isclipped(matid&MATF_VOLUME)) matid |= MAT_CLIP;
+        if(isdeadly(matid&MATF_VOLUME)) matid |= MAT_DEATH;
+    }
+    loopselxyz(setmat(c, matid, matmask, filtermat, filtermask, filtergeom));
+}
+
+void editmat(char *name, char *filtername)
+{
+    if(noedit()) return;
+    int filter = -1;
+    if(filtername[0])
+    {
+        loopi(sizeof(editmatfilters)/sizeof(editmatfilters[0])) if(!strcmp(editmatfilters[i].name, filtername)) { filter = editmatfilters[i].filter; break; }
+        if(filter < 0) filter = findmaterial(filtername);
+        if(filter < 0)
+        {
+            conoutf(CON_ERROR, "unknown material \"%s\"", filtername); 
+            return; 
+        }
+    }
+    int id = -1;
+    if(name[0] || filter < 0)
+    {
+        id = findmaterial(name);
+        if(id<0) { conoutf(CON_ERROR, "unknown material \"%s\"", name); return; }
+    }
+    mpeditmat(id, filter, sel, true);
+}
+
+COMMAND(editmat, "ss");
+
+extern int menudistance, menuautoclose;
+
+VARP(texguiwidth, 1, 15, 1000);
+VARP(texguiheight, 1, 8, 1000);
+FVARP(texguiscale, 0.1f, 1.5f, 10.0f);
+VARP(texguitime, 0, 15, 1000);
+VARP(texguiname, 0, 1, 1);
+
+static int lastthumbnail = 0;
+
+VARP(texgui2d, 0, 1, 1);
+VAR(texguinum, 1, -1, 0);
+
+struct texturegui : g3d_callback
+{
+    bool menuon;
+    vec menupos;
+    int menustart, menutab;
+
+    texturegui() : menustart(-1) {}
+
+    void gui(g3d_gui &g, bool firstpass)
+    {
+        int origtab = menutab, numtabs = max((slots.length() + texguiwidth*texguiheight - 1)/(texguiwidth*texguiheight), 1);
+        if(!firstpass) texguinum = -1;
+        g.start(menustart, 0.04f, &menutab);
+        bool oldautotab = g.allowautotab(false);
+        loopi(numtabs)
+        {
+            g.tab(!i ? "Textures" : NULL, 0xFFDD88);
+            if(i+1 != origtab) continue; //don't load textures on non-visible tabs!
+            Slot *rollover = NULL;
+            loop(h, texguiheight)
+            {
+                g.pushlist();
+                loop(w, texguiwidth)
+                {
+                    extern VSlot dummyvslot;
+                    int ti = (i*texguiheight+h)*texguiwidth+w;
+                    if(ti<slots.length())
+                    {
+                        Slot &slot = lookupslot(ti, false);
+                        VSlot &vslot = *slot.variants;
+                        if(slot.sts.empty()) continue;
+                        else if(!slot.loaded && !slot.thumbnail)
+                        {
+                            if(totalmillis-lastthumbnail<texguitime)
+                            {
+                                g.texture(dummyvslot, texguiscale, false); //create an empty space
+                                continue;
+                            }
+                            loadthumbnail(slot);
+                            lastthumbnail = totalmillis;
+                        }
+                        int ret = g.texture(vslot, texguiscale, true);
+                        if(ret&G3D_ROLLOVER) { rollover = &slot; texguinum = ti; }
+                        if(ret&G3D_UP && (slot.loaded || slot.thumbnail!=notexture))
+                        {
+                            edittex(vslot.index);
+                            hudshader->set();
+                        }
+                    }
+                    else
+                    {
+                        g.texture(dummyvslot, texguiscale, false); //create an empty space
+                    }
+                }
+                g.poplist();
+            }
+            if(texguiname)
+            {
+                if(rollover)
+                {
+                    defformatstring(name, "%d \f7:\fc %s", texguinum, rollover->sts[0].name);
+                    g.title(name, 0xFFDD88);
+                }
+                else g.space(1);
+            }
+        }
+        g.allowautotab(oldautotab);
+        g.end();
+    }
+
+    void showtextures(bool on)
+    {
+        if(on == menuon) return;
+        if((menuon = on))
+        {
+            if(menustart <= lasttexmillis)
+                menutab = 1+clamp(lookupvslot(lasttex, false).slot->index, 0, slots.length()-1)/(texguiwidth*texguiheight);
+            menupos = menuinfrontofplayer();
+            menustart = starttime();
+        }
+        else texguinum = -1;
+    }
+
+    void show()
+    {
+        if(!menuon) return;
+        filltexlist();
+        extern int usegui2d;
+        if(!editmode || ((!texgui2d || !usegui2d) && camera1->o.dist(menupos) > menuautoclose)) { menuon = false; texguinum = -1; }
+        else g3d_addgui(this, menupos, texgui2d ? GUI_2D : 0);
+    }
+} gui;
+
+void g3d_texturemenu()
+{
+    gui.show();
+}
+
+void showtexgui(int *n)
+{
+    if(!editmode) { conoutf(CON_ERROR, "operation only allowed in edit mode"); return; }
+    gui.showtextures(*n==0 ? !gui.menuon : *n==1);
+}
+
+// 0/noargs = toggle, 1 = on, other = off - will autoclose if too far away or exit editmode
+COMMAND(showtexgui, "i");
+
+bool cleartexgui()
+{
+    if(!gui.menuon) return false;
+    gui.showtextures(false);
+    return true;
+}
+ICOMMAND(cleartexgui, "", (), intret(cleartexgui() ? 1 : 0));
+
+void rendertexturepanel(int w, int h)
+{
+    if((texpaneltimer -= curtime)>0 && editmode)
+    {
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+        pushhudmatrix();
+        hudmatrix.scale(h/1800.0f, h/1800.0f, 1);
+        flushhudmatrix(false);
+        SETSHADER(hudrgb);
+
+        int y = 50, gap = 10;
+
+        gle::defvertex(2);
+        gle::deftexcoord0();
+
+        loopi(7)
+        {
+            int s = (i == 3 ? 285 : 220), ti = curtexindex+i-3;
+            if(texmru.inrange(ti))
+            {
+                VSlot &vslot = lookupvslot(texmru[ti]), *layer = NULL;
+                Slot &slot = *vslot.slot;
+                Texture *tex = slot.sts.empty() ? notexture : slot.sts[0].t, *glowtex = NULL, *layertex = NULL;
+                if(slot.texmask&(1<<TEX_GLOW))
+                {
+                    loopvj(slot.sts) if(slot.sts[j].type==TEX_GLOW) { glowtex = slot.sts[j].t; break; }
+                }
+                if(vslot.layer)
+                {
+                    layer = &lookupvslot(vslot.layer);
+                    layertex = layer->slot->sts.empty() ? notexture : layer->slot->sts[0].t;
+                }
+                float sx = min(1.0f, tex->xs/(float)tex->ys), sy = min(1.0f, tex->ys/(float)tex->xs);
+                int x = w*1800/h-s-50, r = s;
+                vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) };
+                float xoff = vslot.offset.x, yoff = vslot.offset.y;
+                if(vslot.rotation)
+                {
+                    const texrotation &r = texrotations[vslot.rotation];
+                    if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); }
+                    if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; }
+                    if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; }
+                }
+                loopk(4) { tc[k].x = tc[k].x/sx - xoff/tex->xs; tc[k].y = tc[k].y/sy - yoff/tex->ys; }
+                glBindTexture(GL_TEXTURE_2D, tex->id);
+                loopj(glowtex ? 3 : 2)
+                {
+                    if(j < 2) gle::color(vec(vslot.colorscale).mul(j), texpaneltimer/1000.0f);
+                    else
+                    {
+                        glBindTexture(GL_TEXTURE_2D, glowtex->id);
+                        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+                        gle::color(vslot.glowcolor, texpaneltimer/1000.0f);
+                    }
+                    gle::begin(GL_TRIANGLE_STRIP);
+                    gle::attribf(x,   y);   gle::attrib(tc[0]);
+                    gle::attribf(x+r, y);   gle::attrib(tc[1]);
+                    gle::attribf(x,   y+r); gle::attrib(tc[3]);
+                    gle::attribf(x+r, y+r); gle::attrib(tc[2]);
+                    xtraverts += gle::end();
+                    if(j==1 && layertex)
+                    {
+                        gle::color(layer->colorscale, texpaneltimer/1000.0f);
+                        glBindTexture(GL_TEXTURE_2D, layertex->id);
+                        gle::begin(GL_TRIANGLE_STRIP);
+                        gle::attribf(x+r/2, y+r/2); gle::attrib(tc[0]);
+                        gle::attribf(x+r,   y+r/2); gle::attrib(tc[1]);
+                        gle::attribf(x+r/2, y+r);   gle::attrib(tc[3]);
+                        gle::attribf(x+r,   y+r);   gle::attrib(tc[2]);
+                        xtraverts += gle::end();
+                    }
+                    if(!j)
+                    {
+                        r -= 10;
+                        x += 5;
+                        y += 5;
+                    }
+                    else if(j == 2) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                }
+            }
+            y += s+gap;
+        }
+
+        pophudmatrix(true, false);
+        hudshader->set();
+    }
+}
diff --git a/src/engine/octarender.cpp b/src/engine/octarender.cpp
new file mode 100644 (file)
index 0000000..faf609f
--- /dev/null
@@ -0,0 +1,1803 @@
+// octarender.cpp: fill vertex arrays with different cube surfaces.
+
+#include "engine.h"
+
+struct vboinfo
+{
+    int uses;
+};
+
+hashtable<GLuint, vboinfo> vbos;
+
+VAR(printvbo, 0, 0, 1);
+VARFN(vbosize, maxvbosize, 0, 1<<14, 1<<16, allchanged());
+
+enum
+{
+    VBO_VBUF = 0,
+    VBO_EBUF,
+    VBO_SKYBUF,
+    NUMVBO
+};
+
+static vector<uchar> vbodata[NUMVBO];
+static vector<vtxarray *> vbovas[NUMVBO];
+static int vbosize[NUMVBO];
+
+void destroyvbo(GLuint vbo)
+{
+    vboinfo *exists = vbos.access(vbo);
+    if(!exists) return;
+    vboinfo &vbi = *exists;
+    if(vbi.uses <= 0) return;
+    vbi.uses--;
+    if(!vbi.uses) 
+    {
+        glDeleteBuffers_(1, &vbo);
+        vbos.remove(vbo);
+    }
+}
+
+void genvbo(int type, void *buf, int len, vtxarray **vas, int numva)
+{
+    gle::disable();
+
+    GLuint vbo;
+    glGenBuffers_(1, &vbo);
+    GLenum target = type==VBO_VBUF ? GL_ARRAY_BUFFER : GL_ELEMENT_ARRAY_BUFFER;
+    glBindBuffer_(target, vbo);
+    glBufferData_(target, len, buf, GL_STATIC_DRAW);
+    glBindBuffer_(target, 0);
+
+    vboinfo &vbi = vbos[vbo]; 
+    vbi.uses = numva;
+    if(printvbo) conoutf(CON_DEBUG, "vbo %d: type %d, size %d, %d uses", vbo, type, len, numva);
+
+    loopi(numva)
+    {
+        vtxarray *va = vas[i];
+        switch(type)
+        {
+            case VBO_VBUF: 
+                va->vbuf = vbo; 
+                break;
+            case VBO_EBUF: 
+                va->ebuf = vbo; 
+                break;
+            case VBO_SKYBUF: 
+                va->skybuf = vbo; 
+                break;
+        }
+    }
+}
+
+bool readva(vtxarray *va, ushort *&edata, vertex *&vdata)
+{
+    if(!va->vbuf || !va->ebuf) return false;
+
+    edata = new ushort[3*va->tris];
+    vdata = new vertex[va->verts];
+
+    gle::bindebo(va->ebuf);
+    glGetBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, (size_t)va->edata, 3*va->tris*sizeof(ushort), edata);
+    gle::clearebo();
+
+    gle::bindvbo(va->vbuf);
+    glGetBufferSubData_(GL_ARRAY_BUFFER, va->voffset*sizeof(vertex), va->verts*sizeof(vertex), vdata);
+    gle::clearvbo();
+    return true;
+}
+
+void flushvbo(int type = -1)
+{
+    if(type < 0)
+    {
+        loopi(NUMVBO) flushvbo(i);
+        return;
+    }
+
+    vector<uchar> &data = vbodata[type];
+    if(data.empty()) return;
+    vector<vtxarray *> &vas = vbovas[type];
+    genvbo(type, data.getbuf(), data.length(), vas.getbuf(), vas.length());
+    data.setsize(0);
+    vas.setsize(0);
+    vbosize[type] = 0;
+}
+
+uchar *addvbo(vtxarray *va, int type, int numelems, int elemsize)
+{
+    vbosize[type] += numelems;
+
+    vector<uchar> &data = vbodata[type];
+    vector<vtxarray *> &vas = vbovas[type];
+
+    vas.add(va);
+
+    int len = numelems*elemsize;
+    uchar *buf = data.reserve(len).buf;
+    data.advance(len);
+    return buf; 
+}
+struct verthash
+{
+    static const int SIZE = 1<<13;
+    int table[SIZE];
+    vector<vertex> verts;
+    vector<int> chain;
+
+    verthash() { clearverts(); }
+
+    void clearverts() 
+    { 
+        memset(table, -1, sizeof(table));
+        chain.setsize(0); 
+        verts.setsize(0);
+    }
+
+    int addvert(const vertex &v)
+    {
+        uint h = hthash(v.pos)&(SIZE-1);
+        for(int i = table[h]; i>=0; i = chain[i])
+        {
+            const vertex &c = verts[i];
+            if(c.pos==v.pos && c.tc==v.tc && c.norm==v.norm && c.tangent==v.tangent && (v.lm.iszero() || c.lm==v.lm))
+                return i;
+        }
+        if(verts.length() >= USHRT_MAX) return -1;
+        verts.add(v);
+        chain.add(table[h]);
+        return table[h] = verts.length()-1;
+    }
+
+    int addvert(const vec &pos, const vec2 &tc = vec2(0, 0), const svec2 &lm = svec2(0, 0), const bvec &norm = bvec(128, 128, 128), const bvec4 &tangent = bvec4(128, 128, 128, 128))
+    {
+        vertex vtx;
+        vtx.pos = pos;
+        vtx.tc = tc;
+        vtx.lm = lm;
+        vtx.norm = norm;
+        vtx.tangent = tangent;
+        return addvert(vtx);
+    } 
+};
+
+enum
+{
+    NO_ALPHA = 0,
+    ALPHA_BACK,
+    ALPHA_FRONT
+};
+
+struct sortkey
+{
+     ushort tex, lmid, envmap;
+     uchar dim, layer, alpha;
+
+     sortkey() {}
+     sortkey(ushort tex, ushort lmid, uchar dim, uchar layer = LAYER_TOP, ushort envmap = EMID_NONE, uchar alpha = NO_ALPHA)
+      : tex(tex), lmid(lmid), envmap(envmap), dim(dim), layer(layer), alpha(alpha)
+     {}
+
+     bool operator==(const sortkey &o) const { return tex==o.tex && lmid==o.lmid && envmap==o.envmap && dim==o.dim && layer==o.layer && alpha==o.alpha; }
+};
+
+struct sortval
+{
+     int unlit;
+     vector<ushort> tris[2];
+
+     sortval() : unlit(0) {}
+};
+
+static inline bool htcmp(const sortkey &x, const sortkey &y)
+{
+    return x == y;
+}
+
+static inline uint hthash(const sortkey &k)
+{
+    return k.tex + k.lmid*9741;
+}
+
+struct vacollect : verthash
+{
+    ivec origin;
+    int size;
+    hashtable<sortkey, sortval> indices;
+    vector<sortkey> texs;
+    vector<grasstri> grasstris;
+    vector<materialsurface> matsurfs;
+    vector<octaentities *> mapmodels;
+    vector<ushort> skyindices, explicitskyindices;
+    vector<facebounds> skyfaces[6];
+    int worldtris, skytris, skymask, skyclip, skyarea;
+
+    void clear()
+    {
+        clearverts();
+        worldtris = skytris = 0;
+        skymask = 0;
+        skyclip = INT_MAX;
+        skyarea = 0;
+        indices.clear();
+        skyindices.setsize(0);
+        explicitskyindices.setsize(0);
+        matsurfs.setsize(0);
+        mapmodels.setsize(0);
+        grasstris.setsize(0);
+        texs.setsize(0);
+        loopi(6) skyfaces[i].setsize(0);
+    }
+
+    void remapunlit(vector<sortkey> &remap)
+    {
+        uint lastlmid[8] = { LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT }, 
+             firstlmid[8] = { LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT, LMID_AMBIENT };
+        int firstlit[8] = { -1, -1, -1, -1, -1, -1, -1, -1 };
+        loopv(texs)
+        {
+            sortkey &k = texs[i];
+            if(k.lmid>=LMID_RESERVED) 
+            {
+                LightMapTexture &lmtex = lightmaptexs[k.lmid];
+                int type = lmtex.type&LM_TYPE;
+                if(k.layer==LAYER_BLEND) type += 2;
+                else if(k.alpha) type += 4 + 2*(k.alpha-1);
+                lastlmid[type] = lmtex.unlitx>=0 ? k.lmid : LMID_AMBIENT;
+                if(firstlmid[type]==LMID_AMBIENT && lastlmid[type]!=LMID_AMBIENT)
+                {
+                    firstlit[type] = i;
+                    firstlmid[type] = lastlmid[type];
+                }
+            }
+            else if(k.lmid==LMID_AMBIENT)
+            {
+                Shader *s = lookupvslot(k.tex, false).slot->shader;
+                int type = s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE;
+                if(k.layer==LAYER_BLEND) type += 2;
+                else if(k.alpha) type += 4 + 2*(k.alpha-1);
+                if(lastlmid[type]!=LMID_AMBIENT)
+                {
+                    sortval &t = indices[k];
+                    if(t.unlit<=0) t.unlit = lastlmid[type];
+                }
+            }
+        }
+        loopj(2)
+        {
+            int offset = 2*j;
+            if(firstlmid[offset]==LMID_AMBIENT && firstlmid[offset+1]==LMID_AMBIENT) continue;
+            loopi(max(firstlit[offset], firstlit[offset+1]))
+            {
+                sortkey &k = texs[i];
+                if((j ? k.layer!=LAYER_BLEND : k.layer==LAYER_BLEND) || k.alpha) continue;
+                if(k.lmid!=LMID_AMBIENT) continue;
+                Shader *s = lookupvslot(k.tex, false).slot->shader;
+                int type = offset + (s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE);
+                if(firstlmid[type]==LMID_AMBIENT) continue;
+                indices[k].unlit = firstlmid[type];
+            }
+        }  
+        loopj(2)
+        {
+            int offset = 4 + 2*j;
+            if(firstlmid[offset]==LMID_AMBIENT && firstlmid[offset+1]==LMID_AMBIENT) continue;
+            loopi(max(firstlit[offset], firstlit[offset+1]))
+            {
+                sortkey &k = texs[i];
+                if(k.alpha != j+1) continue;
+                if(k.lmid!=LMID_AMBIENT) continue;
+                Shader *s = lookupvslot(k.tex, false).slot->shader;
+                int type = offset + (s->type&SHADER_NORMALSLMS ? LM_BUMPMAP0 : LM_DIFFUSE);
+                if(firstlmid[type]==LMID_AMBIENT) continue;
+                indices[k].unlit = firstlmid[type];
+            }
+        } 
+        loopv(remap)
+        {
+            sortkey &k = remap[i];
+            sortval &t = indices[k];
+            if(t.unlit<=0) continue; 
+            LightMapTexture &lm = lightmaptexs[t.unlit];
+            svec2 lmtc(short(ceil((lm.unlitx + 0.5f) * SHRT_MAX/lm.w)), 
+                       short(ceil((lm.unlity + 0.5f) * SHRT_MAX/lm.h)));
+            loopl(2) loopvj(t.tris[l])
+            {
+                vertex &vtx = verts[t.tris[l][j]];
+                if(vtx.lm.iszero()) vtx.lm = lmtc;
+                else if(vtx.lm != lmtc)
+                {
+                    vertex vtx2 = vtx;
+                    vtx2.lm = lmtc;
+                    t.tris[l][j] = addvert(vtx2);
+                }
+            }
+            sortval *dst = indices.access(sortkey(k.tex, t.unlit, k.dim, k.layer, k.envmap, k.alpha));
+            if(dst) loopl(2) loopvj(t.tris[l]) dst->tris[l].add(t.tris[l][j]);
+        }
+    }
+                    
+    void optimize()
+    {
+        vector<sortkey> remap;
+        enumeratekt(indices, sortkey, k, sortval, t,
+            loopl(2) if(t.tris[l].length() && t.unlit<=0)
+            {
+                if(k.lmid>=LMID_RESERVED && lightmaptexs[k.lmid].unlitx>=0)
+                {
+                    sortkey ukey(k.tex, LMID_AMBIENT, k.dim, k.layer, k.envmap, k.alpha);
+                    sortval *uval = indices.access(ukey);
+                    if(uval && uval->unlit<=0)
+                    {
+                        if(uval->unlit<0) texs.removeobj(ukey);
+                        else remap.add(ukey);
+                        uval->unlit = k.lmid;
+                    }
+                }
+                else if(k.lmid==LMID_AMBIENT)
+                {
+                    remap.add(k);
+                    t.unlit = -1;
+                }
+                texs.add(k);
+                break;
+            }
+        );
+        texs.sort(texsort);
+
+        remapunlit(remap);
+
+        matsurfs.shrink(optimizematsurfs(matsurfs.getbuf(), matsurfs.length()));
+    }
+
+    static inline bool texsort(const sortkey &x, const sortkey &y)
+    {
+        if(x.alpha < y.alpha) return true;
+        if(x.alpha > y.alpha) return false;
+        if(x.layer < y.layer) return true;
+        if(x.layer > y.layer) return false;
+        if(x.tex == y.tex) 
+        {
+            if(x.lmid < y.lmid) return true;
+            if(x.lmid > y.lmid) return false;
+            if(x.envmap < y.envmap) return true;
+            if(x.envmap > y.envmap) return false;
+            if(x.dim < y.dim) return true;
+            if(x.dim > y.dim) return false;
+            return false;
+        }
+        VSlot &xs = lookupvslot(x.tex, false), &ys = lookupvslot(y.tex, false);
+        if(xs.slot->shader < ys.slot->shader) return true;
+        if(xs.slot->shader > ys.slot->shader) return false;
+        if(xs.slot->params.length() < ys.slot->params.length()) return true;
+        if(xs.slot->params.length() > ys.slot->params.length()) return false;
+        if(x.tex < y.tex) return true;
+        else return false;
+    }
+
+#define GENVERTS(type, ptr, body) do \
+    { \
+        type *f = (type *)ptr; \
+        loopv(verts) \
+        { \
+            const vertex &v = verts[i]; \
+            body; \
+            f++; \
+        } \
+    } while(0)
+
+    void genverts(void *buf)
+    {
+        GENVERTS(vertex, buf, { *f = v; f->norm.flip(); f->tangent.flip(); });
+    }
+
+    void setupdata(vtxarray *va)
+    {
+        va->verts = verts.length();
+        va->tris = worldtris/3;
+        va->vbuf = 0;
+        va->vdata = 0;
+        va->minvert = 0;
+        va->maxvert = va->verts-1;
+        va->voffset = 0;
+        if(va->verts)
+        {
+            if(vbosize[VBO_VBUF] + verts.length() > maxvbosize || 
+               vbosize[VBO_EBUF] + worldtris > USHRT_MAX ||
+               vbosize[VBO_SKYBUF] + skytris > USHRT_MAX) 
+                flushvbo();
+
+            va->voffset = vbosize[VBO_VBUF];
+            uchar *vdata = addvbo(va, VBO_VBUF, va->verts, sizeof(vertex));
+            genverts(vdata);
+            va->minvert += va->voffset;
+            va->maxvert += va->voffset;
+        }
+
+        va->matbuf = NULL;
+        va->matsurfs = matsurfs.length();
+        if(va->matsurfs) 
+        {
+            va->matbuf = new materialsurface[matsurfs.length()];
+            memcpy(va->matbuf, matsurfs.getbuf(), matsurfs.length()*sizeof(materialsurface));
+        }
+
+        va->skybuf = 0;
+        va->skydata = 0;
+        va->sky = skyindices.length();
+        va->explicitsky = explicitskyindices.length();
+        if(va->sky + va->explicitsky)
+        {
+            va->skydata += vbosize[VBO_SKYBUF];
+            ushort *skydata = (ushort *)addvbo(va, VBO_SKYBUF, va->sky+va->explicitsky, sizeof(ushort));
+            memcpy(skydata, skyindices.getbuf(), va->sky*sizeof(ushort));
+            memcpy(skydata+va->sky, explicitskyindices.getbuf(), va->explicitsky*sizeof(ushort));
+            if(va->voffset) loopi(va->sky+va->explicitsky) skydata[i] += va->voffset; 
+        }
+
+        va->eslist = NULL;
+        va->texs = texs.length();
+        va->blendtris = 0;
+        va->blends = 0;
+        va->alphabacktris = 0;
+        va->alphaback = 0;
+        va->alphafronttris = 0;
+        va->alphafront = 0;
+        va->ebuf = 0;
+        va->edata = 0;
+        va->texmask = 0;
+        if(va->texs)
+        {
+            va->eslist = new elementset[va->texs];
+            va->edata += vbosize[VBO_EBUF];
+            ushort *edata = (ushort *)addvbo(va, VBO_EBUF, worldtris, sizeof(ushort)), *curbuf = edata;
+            loopv(texs)
+            {
+                const sortkey &k = texs[i];
+                const sortval &t = indices[k];
+                elementset &e = va->eslist[i];
+                e.texture = k.tex;
+                e.lmid = t.unlit>0 ? t.unlit : k.lmid;
+                e.dim = k.dim;
+                e.layer = k.layer;
+                e.envmap = k.envmap;
+                ushort *startbuf = curbuf;
+                loopl(2) 
+                {
+                    e.minvert[l] = USHRT_MAX;
+                    e.maxvert[l] = 0;
+
+                    if(t.tris[l].length())
+                    {
+                        memcpy(curbuf, t.tris[l].getbuf(), t.tris[l].length() * sizeof(ushort));
+
+                        loopvj(t.tris[l])
+                        {
+                            curbuf[j] += va->voffset;
+                            e.minvert[l] = min(e.minvert[l], curbuf[j]);
+                            e.maxvert[l] = max(e.maxvert[l], curbuf[j]);
+                        }
+
+                        curbuf += t.tris[l].length();
+                    }
+                    e.length[l] = curbuf-startbuf;
+                }
+                if(k.layer==LAYER_BLEND) { va->texs--; va->tris -= e.length[1]/3; va->blends++; va->blendtris += e.length[1]/3; }
+                else if(k.alpha==ALPHA_BACK) { va->texs--; va->tris -= e.length[1]/3; va->alphaback++; va->alphabacktris += e.length[1]/3; }
+                else if(k.alpha==ALPHA_FRONT) { va->texs--; va->tris -= e.length[1]/3; va->alphafront++; va->alphafronttris += e.length[1]/3; } 
+
+                Slot &slot = *lookupvslot(k.tex, false).slot;
+                loopvj(slot.sts) va->texmask |= 1<<slot.sts[j].type;
+                if(slot.shader->type&SHADER_ENVMAP) va->texmask |= 1<<TEX_ENVMAP;
+            }
+        }
+
+        va->alphatris = va->alphabacktris + va->alphafronttris;
+
+        if(grasstris.length())
+        {
+            va->grasstris.move(grasstris);
+            useshaderbyname("grass");
+        }
+
+        if(mapmodels.length()) va->mapmodels.put(mapmodels.getbuf(), mapmodels.length());
+    }
+
+    bool emptyva()
+    {
+        return verts.empty() && matsurfs.empty() && skyindices.empty() && explicitskyindices.empty() && grasstris.empty() && mapmodels.empty();
+    }            
+} vc;
+
+int recalcprogress = 0;
+#define progress(s)     if((recalcprogress++&0xFFF)==0) renderprogress(recalcprogress/(float)allocnodes, s);
+
+vector<tjoint> tjoints;
+
+vec shadowmapmin, shadowmapmax;
+
+int calcshadowmask(vec *pos, int numpos)
+{
+    extern vec shadowdir;
+    int mask = 0, used = 1;
+    vec pe = vec(pos[1]).sub(pos[0]);
+    loopk(numpos-2)
+    {
+        vec e = vec(pos[k+2]).sub(pos[0]);
+        if(vec().cross(pe, e).dot(shadowdir)>0)
+        {
+            mask |= 1<<k;
+            used |= 6<<k;
+        }
+        pe = e;
+    }
+    if(!mask) return 0;
+    loopk(numpos) if(used&(1<<k))
+    {
+        const vec &v = pos[k];
+        shadowmapmin.min(v);
+        shadowmapmax.max(v);
+    }
+    return mask;
+}
+
+VARFP(filltjoints, 0, 1, 1, allchanged());
+
+void reduceslope(ivec &n)
+{
+    int mindim = -1, minval = 64;
+    loopi(3) if(n[i])
+    {
+        int val = abs(n[i]);
+        if(mindim < 0 || val < minval)
+        {
+            mindim = i;
+            minval = val;
+        }
+    }
+    if(!(n[R[mindim]]%minval) && !(n[C[mindim]]%minval)) n.div(minval);
+    while(!((n.x|n.y|n.z)&1)) n.shr(1);
+}
+
+// [rotation][dimension]
+extern const vec orientation_tangent[8][3] =
+{
+    { vec(0,  1,  0), vec( 1, 0,  0), vec( 1,  0, 0) },
+    { vec(0,  0, -1), vec( 0, 0, -1), vec( 0,  1, 0) },
+    { vec(0, -1,  0), vec(-1, 0,  0), vec(-1,  0, 0) },
+    { vec(0,  0,  1), vec( 0, 0,  1), vec( 0, -1, 0) },
+    { vec(0, -1,  0), vec(-1, 0,  0), vec(-1,  0, 0) },
+    { vec(0,  1,  0), vec( 1, 0,  0), vec( 1,  0, 0) },
+    { vec(0,  0, -1), vec( 0, 0, -1), vec( 0,  1, 0) },
+    { vec(0,  0,  1), vec( 0, 0,  1), vec( 0, -1, 0) },
+};
+extern const vec orientation_bitangent[8][3] =
+{
+    { vec(0,  0, -1), vec( 0, 0, -1), vec( 0,  1, 0) },
+    { vec(0, -1,  0), vec(-1, 0,  0), vec(-1,  0, 0) },
+    { vec(0,  0,  1), vec( 0, 0,  1), vec( 0, -1, 0) },
+    { vec(0,  1,  0), vec( 1, 0,  0), vec( 1,  0, 0) },
+    { vec(0,  0, -1), vec( 0, 0, -1), vec( 0,  1, 0) },
+    { vec(0,  0,  1), vec( 0, 0,  1), vec( 0, -1, 0) },
+    { vec(0,  1,  0), vec( 1, 0,  0), vec( 1,  0, 0) },
+    { vec(0, -1,  0), vec(-1, 0,  0), vec(-1,  0, 0) },
+};
+
+void addtris(const sortkey &key, int orient, vertex *verts, int *index, int numverts, int convex, int shadowmask, int tj)
+{
+    int &total = key.tex==DEFAULT_SKY ? vc.skytris : vc.worldtris;
+    int edge = orient*(MAXFACEVERTS+1);
+    loopi(numverts-2) if(index[0]!=index[i+1] && index[i+1]!=index[i+2] && index[i+2]!=index[0])
+    {
+        vector<ushort> &idxs = key.tex==DEFAULT_SKY ? vc.explicitskyindices : vc.indices[key].tris[(shadowmask>>i)&1];
+        int left = index[0], mid = index[i+1], right = index[i+2], start = left, i0 = left, i1 = -1;
+        loopk(4)
+        {
+            int i2 = -1, ctj = -1, cedge = -1;
+            switch(k)
+            {
+            case 1: i1 = i2 = mid; cedge = edge+i+1; break;
+            case 2: if(i1 != mid || i0 == left) { i0 = i1; i1 = right; } i2 = right; if(i+1 == numverts-2) cedge = edge+i+2; break;
+            case 3: if(i0 == start) { i0 = i1; i1 = left; } i2 = left; // fall-through 
+            default: if(!i) cedge = edge; break;
+            }
+            if(i1 != i2)
+            {
+                if(total + 3 > USHRT_MAX) return;
+                total += 3;
+                idxs.add(i0);
+                idxs.add(i1);
+                idxs.add(i2);
+                i1 = i2;
+            }
+            if(cedge >= 0)
+            {
+                for(ctj = tj;;)
+                {
+                    if(ctj < 0) break;
+                    if(tjoints[ctj].edge < cedge) { ctj = tjoints[ctj].next; continue; }
+                    if(tjoints[ctj].edge != cedge) ctj = -1;
+                    break;
+                }
+            }
+            if(ctj >= 0)
+            {
+                int e1 = cedge%(MAXFACEVERTS+1), e2 = (e1+1)%numverts;
+                vertex &v1 = verts[e1], &v2 = verts[e2];
+                ivec d(vec(v2.pos).sub(v1.pos).mul(8));
+                int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2);
+                if(d[axis] < 0) d.neg();
+                reduceslope(d);
+                int origin = int(min(v1.pos[axis], v2.pos[axis])*8)&~0x7FFF,
+                    offset1 = (int(v1.pos[axis]*8) - origin) / d[axis],
+                    offset2 = (int(v2.pos[axis]*8) - origin) / d[axis];
+                vec o = vec(v1.pos).sub(vec(d).mul(offset1/8.0f));
+                float doffset = 1.0f / (offset2 - offset1);
+    
+                if(i1 < 0) for(;;)
+                {
+                    tjoint &t = tjoints[ctj];
+                    if(t.next < 0 || tjoints[t.next].edge != cedge) break;
+                    ctj = t.next;
+                } 
+                while(ctj >= 0)
+                {
+                    tjoint &t = tjoints[ctj];
+                    if(t.edge != cedge) break;
+                    float offset = (t.offset - offset1) * doffset;
+                    vertex vt;
+                    vt.pos = vec(d).mul(t.offset/8.0f).add(o);
+                    vt.tc.lerp(v1.tc, v2.tc, offset);
+                    vt.lm.x = short(v1.lm.x + (v2.lm.x-v1.lm.x)*offset),
+                    vt.lm.y = short(v1.lm.y + (v2.lm.y-v1.lm.y)*offset);
+                    vt.norm.lerp(v1.norm, v2.norm, offset);
+                    vt.tangent.lerp(v1.tangent, v2.tangent, offset);
+                    int i2 = vc.addvert(vt);
+                    if(i2 < 0) return;
+                    if(i1 >= 0)
+                    {
+                        if(total + 3 > USHRT_MAX) return;
+                        total += 3;
+                        idxs.add(i0);
+                        idxs.add(i1);
+                        idxs.add(i2);
+                        i1 = i2;
+                    }
+                    else start = i0 = i2;
+                    ctj = t.next;
+                }
+            }
+        }
+    }
+}
+
+void addgrasstri(int face, vertex *verts, int numv, ushort texture, ushort lmid)
+{
+    grasstri &g = vc.grasstris.add();
+    int i1, i2, i3, i4;
+    if(numv <= 3 && face%2) { i1 = face+1; i2 = face+2; i3 = i4 = 0; }
+    else { i1 = 0; i2 = face+1; i3 = face+2; i4 = numv > 3 ? face+3 : i3; } 
+    g.v[0] = verts[i1].pos;
+    g.v[1] = verts[i2].pos;
+    g.v[2] = verts[i3].pos;
+    g.v[3] = verts[i4].pos;
+    g.numv = numv;
+
+    g.surface.toplane(g.v[0], g.v[1], g.v[2]);
+    if(g.surface.z <= 0) { vc.grasstris.pop(); return; }
+
+    g.minz = min(min(g.v[0].z, g.v[1].z), min(g.v[2].z, g.v[3].z));
+    g.maxz = max(max(g.v[0].z, g.v[1].z), max(g.v[2].z, g.v[3].z));
+
+    g.center = vec(0, 0, 0);
+    loopk(numv) g.center.add(g.v[k]);
+    g.center.div(numv);
+    g.radius = 0;
+    loopk(numv) g.radius = max(g.radius, g.v[k].dist(g.center));
+
+    vec area, bx, by;
+    area.cross(vec(g.v[1]).sub(g.v[0]), vec(g.v[2]).sub(g.v[0]));
+    float scale;
+    int px, py;
+
+    if(fabs(area.x) >= fabs(area.y) && fabs(area.x) >= fabs(area.z))
+        scale = 1/area.x, px = 1, py = 2;
+    else if(fabs(area.y) >= fabs(area.x) && fabs(area.y) >= fabs(area.z))
+        scale = -1/area.y, px = 0, py = 2;
+    else
+        scale = 1/area.z, px = 0, py = 1;
+
+    bx.x = (g.v[2][py] - g.v[0][py])*scale;
+    bx.y = (g.v[2][px] - g.v[0][px])*scale;
+    bx.z = bx.x*g.v[2][px] - bx.y*g.v[2][py];
+
+    by.x = (g.v[2][py] - g.v[1][py])*scale;
+    by.y = (g.v[2][px] - g.v[1][px])*scale;
+    by.z = by.x*g.v[1][px] - by.y*g.v[1][py] - 1;
+    by.sub(bx);
+
+    float tc1u = verts[i1].lm.x,
+          tc1v = verts[i1].lm.y,
+          tc2u = verts[i2].lm.x - verts[i1].lm.x,
+          tc2v = verts[i2].lm.y - verts[i1].lm.y,
+          tc3u = verts[i3].lm.x - verts[i1].lm.x,
+          tc3v = verts[i3].lm.y - verts[i1].lm.y;
+        
+    g.tcu = vec4(0, 0, 0, tc1u - (bx.z*tc2u + by.z*tc3u));
+    g.tcu[px] = bx.x*tc2u + by.x*tc3u;
+    g.tcu[py] = -(bx.y*tc2u + by.y*tc3u);
+
+    g.tcv = vec4(0, 0, 0, tc1v - (bx.z*tc2v + by.z*tc3v));
+    g.tcv[px] = bx.x*tc2v + by.x*tc3v;
+    g.tcv[py] = -(bx.y*tc2v + by.y*tc3v);
+
+    g.texture = texture;
+    g.lmid = lmid;
+}
+
+static inline void calctexgen(VSlot &vslot, int dim, vec4 &sgen, vec4 &tgen)
+{
+    Texture *tex = vslot.slot->sts.empty() ? notexture : vslot.slot->sts[0].t;
+    const texrotation &r = texrotations[vslot.rotation];
+    float k = TEX_SCALE/vslot.scale,
+          xs = r.flipx ? -tex->xs : tex->xs,
+          ys = r.flipy ? -tex->ys : tex->ys,
+          sk = k/xs, tk = k/ys,
+          soff = -(r.swapxy ? vslot.offset.y : vslot.offset.x)/xs,
+          toff = -(r.swapxy ? vslot.offset.x : vslot.offset.y)/ys;
+    static const int si[] = { 1, 0, 0 }, ti[] = { 2, 2, 1 };
+    int sdim = si[dim], tdim = ti[dim];
+    sgen = vec4(0, 0, 0, soff); 
+    tgen = vec4(0, 0, 0, toff);
+    if(r.swapxy)
+    {
+        sgen[tdim] = (dim <= 1 ? -sk : sk);
+        tgen[sdim] = tk;
+    }
+    else
+    {
+        sgen[sdim] = sk;
+        tgen[tdim] = (dim <= 1 ? -tk : tk);
+    }
+}
+
+ushort encodenormal(const vec &n)
+{               
+    if(n.iszero()) return 0;
+    int yaw = int(-atan2(n.x, n.y)/RAD), pitch = int(asin(n.z)/RAD);
+    return ushort(clamp(pitch + 90, 0, 180)*360 + (yaw < 0 ? yaw%360 + 360 : yaw%360) + 1);
+}
+
+vec decodenormal(ushort norm)
+{
+    if(!norm) return vec(0, 0, 1);
+    norm--;
+    const vec2 &yaw = sincos360[norm%360], &pitch = sincos360[norm/360+270];
+    return vec(-yaw.y*pitch.x, yaw.x*pitch.x, pitch.y);
+}
+
+void guessnormals(const vec *pos, int numverts, vec *normals)
+{
+    vec n1, n2;
+    n1.cross(pos[0], pos[1], pos[2]);
+    if(numverts != 4)
+    {
+        n1.normalize();
+        loopk(numverts) normals[k] = n1;
+        return;
+    }
+    n2.cross(pos[0], pos[2], pos[3]);
+    if(n1.iszero())
+    {
+        n2.normalize();
+        loopk(4) normals[k] = n2;
+        return;
+    }
+    else n1.normalize();
+    if(n2.iszero())
+    {
+        loopk(4) normals[k] = n1;
+        return;
+    }
+    else n2.normalize();
+    vec avg = vec(n1).add(n2).normalize();
+    normals[0] = avg;
+    normals[1] = n1;
+    normals[2] = avg;
+    normals[3] = n2;
+}
+
+void addcubeverts(VSlot &vslot, int orient, int size, vec *pos, int convex, ushort texture, ushort lmid, vertinfo *vinfo, int numverts, int tj = -1, ushort envmap = EMID_NONE, int grassy = 0, bool alpha = false, int layer = LAYER_TOP)
+{
+    int dim = dimension(orient);
+    int shadowmask = texture==DEFAULT_SKY || alpha ? 0 : calcshadowmask(pos, numverts);
+
+    LightMap *lm = NULL;
+    LightMapTexture *lmtex = NULL;
+    if(lightmaps.inrange(lmid-LMID_RESERVED))
+    {
+        lm = &lightmaps[lmid-LMID_RESERVED];
+        if((lm->type&LM_TYPE)==LM_DIFFUSE ||
+            ((lm->type&LM_TYPE)==LM_BUMPMAP0 &&
+                lightmaps.inrange(lmid+1-LMID_RESERVED) &&
+                (lightmaps[lmid+1-LMID_RESERVED].type&LM_TYPE)==LM_BUMPMAP1))
+            lmtex = &lightmaptexs[lm->tex];
+        else lm = NULL;
+    }
+
+    vec4 sgen, tgen;
+    calctexgen(vslot, dim, sgen, tgen);
+    vertex verts[MAXFACEVERTS];
+    int index[MAXFACEVERTS];
+    loopk(numverts)
+    {
+        vertex &v = verts[k];
+        v.pos = pos[k];
+        v.tc = vec2(sgen.dot(v.pos), tgen.dot(v.pos));
+        if(lmtex) 
+        { 
+            v.lm = svec2(short(ceil((lm->offsetx + vinfo[k].u*(float(LM_PACKW)/float(USHRT_MAX+1)) + 0.5f) * float(SHRT_MAX)/lmtex->w)), 
+                         short(ceil((lm->offsety + vinfo[k].v*(float(LM_PACKH)/float(USHRT_MAX+1)) + 0.5f) * float(SHRT_MAX)/lmtex->h)));
+        }
+        else v.lm = svec2(0, 0);
+        if(vinfo && vinfo[k].norm)
+        {
+            vec n = decodenormal(vinfo[k].norm), t = orientation_tangent[vslot.rotation][dim];
+            t.project(n).normalize();
+            v.norm = bvec(n);
+            v.tangent = bvec4(bvec(t), orientation_bitangent[vslot.rotation][dim].scalartriple(n, t) < 0 ? 0 : 255);
+        }
+        else
+        {
+            v.norm = vinfo && vinfo[k].norm && envmap != EMID_NONE ? bvec(decodenormal(vinfo[k].norm)) : bvec(128, 128, 255);
+            v.tangent = bvec4(255, 128, 128, 255);
+        }
+        index[k] = vc.addvert(v);
+        if(index[k] < 0) return;
+    }
+
+    if(texture == DEFAULT_SKY)
+    {
+        loopk(numverts) vc.skyclip = min(vc.skyclip, int(pos[k].z*8)>>3);
+        vc.skymask |= 0x3F&~(1<<orient);
+    }
+
+    if(lmid >= LMID_RESERVED) lmid = lm ? lm->tex : LMID_AMBIENT;
+
+    sortkey key(texture, lmid, !vslot.scroll.iszero() ? dim : 3, layer == LAYER_BLEND ? LAYER_BLEND : LAYER_TOP, envmap, alpha ? (vslot.alphaback ? ALPHA_BACK : (vslot.alphafront ? ALPHA_FRONT : NO_ALPHA)) : NO_ALPHA);
+    addtris(key, orient, verts, index, numverts, convex, shadowmask, tj);
+
+    if(grassy) 
+    {
+        for(int i = 0; i < numverts-2; i += 2)
+        {
+            int faces = 0;
+            if(index[0]!=index[i+1] && index[i+1]!=index[i+2] && index[i+2]!=index[0]) faces |= 1;
+            if(i+3 < numverts && index[0]!=index[i+2] && index[i+2]!=index[i+3] && index[i+3]!=index[0]) faces |= 2;
+            if(grassy > 1 && faces==3) addgrasstri(i, verts, 4, texture, lmid);
+            else 
+            {
+                if(faces&1) addgrasstri(i, verts, 3, texture, lmid);
+                if(faces&2) addgrasstri(i+1, verts, 3, texture, lmid);
+            }
+        }
+    }
+}
+
+struct edgegroup
+{
+    ivec slope, origin;
+    int axis;
+};
+
+static uint hthash(const edgegroup &g)
+{
+    return g.slope.x^(g.slope.y<<2)^(g.slope.z<<4)^g.origin.x^g.origin.y^g.origin.z;
+}
+
+static bool htcmp(const edgegroup &x, const edgegroup &y) 
+{ 
+    return x.slope==y.slope && x.origin==y.origin;
+}
+
+enum
+{
+    CE_START = 1<<0,
+    CE_END   = 1<<1,
+    CE_FLIP  = 1<<2,
+    CE_DUP   = 1<<3
+};
+
+struct cubeedge
+{
+    cube *c;
+    int next, offset;
+    ushort size;
+    uchar index, flags;
+};
+
+vector<cubeedge> cubeedges;
+hashtable<edgegroup, int> edgegroups(1<<13);
+
+void gencubeedges(cube &c, const ivec &co, int size)
+{
+    ivec pos[MAXFACEVERTS];
+    int vis;
+    loopi(6) if((vis = visibletris(c, i, co, size)))
+    {
+        int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0;
+        if(numverts)
+        {
+            vertinfo *verts = c.ext->verts() + c.ext->surfaces[i].verts;
+            ivec vo = ivec(co).mask(~0xFFF).shl(3);
+            loopj(numverts)
+            {
+                vertinfo &v = verts[j];
+                pos[j] = ivec(v.x, v.y, v.z).add(vo);
+            }
+        }
+        else if(c.merged&(1<<i)) continue;
+        else
+        {
+            ivec v[4];
+            genfaceverts(c, i, v);
+            int order = vis&4 || (!flataxisface(c, i) && faceconvexity(v) < 0) ? 1 : 0;
+            ivec vo = ivec(co).shl(3);
+            pos[numverts++] = v[order].mul(size).add(vo);
+            if(vis&1) pos[numverts++] = v[order+1].mul(size).add(vo);
+            pos[numverts++] = v[order+2].mul(size).add(vo);
+            if(vis&2) pos[numverts++] = v[(order+3)&3].mul(size).add(vo);
+        }
+        loopj(numverts)
+        {
+            int e1 = j, e2 = j+1 < numverts ? j+1 : 0;
+            ivec d = pos[e2];
+            d.sub(pos[e1]);
+            if(d.iszero()) continue;
+            int axis = abs(d.x) > abs(d.y) ? (abs(d.x) > abs(d.z) ? 0 : 2) : (abs(d.y) > abs(d.z) ? 1 : 2);
+            if(d[axis] < 0)
+            {
+                d.neg();
+                swap(e1, e2);
+            }
+            reduceslope(d);
+
+            int t1 = pos[e1][axis]/d[axis],
+                t2 = pos[e2][axis]/d[axis];
+            edgegroup g;
+            g.origin = ivec(pos[e1]).sub(ivec(d).mul(t1));
+            g.slope = d;
+            g.axis = axis;
+            cubeedge ce;
+            ce.c = &c;
+            ce.offset = t1;
+            ce.size = t2 - t1;
+            ce.index = i*(MAXFACEVERTS+1)+j;
+            ce.flags = CE_START | CE_END | (e1!=j ? CE_FLIP : 0);
+            ce.next = -1;
+
+            bool insert = true;
+            int *exists = edgegroups.access(g);
+            if(exists)
+            {
+                int prev = -1, cur = *exists;
+                while(cur >= 0)
+                {
+                    cubeedge &p = cubeedges[cur];
+                    if(ce.offset <= p.offset+p.size)
+                    {
+                        if(ce.offset < p.offset) break;
+                        if(p.flags&CE_DUP ?
+                            ce.offset+ce.size <= p.offset+p.size :
+                            ce.offset==p.offset && ce.size==p.size)
+                        {
+                            p.flags |= CE_DUP;
+                            insert = false;
+                            break;
+                        }
+                        if(ce.offset == p.offset+p.size) ce.flags &= ~CE_START;
+                    }
+                    prev = cur;
+                    cur = p.next;
+                }
+                if(insert)
+                {
+                    ce.next = cur;
+                    while(cur >= 0)
+                    {
+                        cubeedge &p = cubeedges[cur];
+                        if(ce.offset+ce.size==p.offset) { ce.flags &= ~CE_END; break; }
+                        cur = p.next;
+                    }
+                    if(prev>=0) cubeedges[prev].next = cubeedges.length();
+                    else *exists = cubeedges.length();
+                }
+            }
+            else edgegroups[g] = cubeedges.length();
+
+            if(insert) cubeedges.add(ce);
+        }
+    }
+}
+
+void gencubeedges(cube *c = worldroot, const ivec &co = ivec(0, 0, 0), int size = worldsize>>1)
+{
+    progress("fixing t-joints...");
+    neighbourstack[++neighbourdepth] = c;
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        if(c[i].ext) c[i].ext->tjoints = -1;
+        if(c[i].children) gencubeedges(c[i].children, o, size>>1);
+        else if(!isempty(c[i])) gencubeedges(c[i], o, size);
+    }
+    --neighbourdepth;
+}
+
+void gencubeverts(cube &c, const ivec &co, int size, int csi)
+{
+    if(!(c.visible&0xC0)) return;
+
+    int vismask = ~c.merged & 0x3F;
+    if(!(c.visible&0x80)) vismask &= c.visible;
+    if(!vismask) return;
+    
+    int tj = filltjoints && c.ext ? c.ext->tjoints : -1, vis;
+    loopi(6) if(vismask&(1<<i) && (vis = visibletris(c, i, co, size)))
+    {
+        vec pos[MAXFACEVERTS];
+        vertinfo *verts = NULL;
+        int numverts = c.ext ? c.ext->surfaces[i].numverts&MAXFACEVERTS : 0, convex = 0;
+        if(numverts)
+        {
+            verts = c.ext->verts() + c.ext->surfaces[i].verts;
+            vec vo(ivec(co).mask(~0xFFF));
+            loopj(numverts) pos[j] = vec(verts[j].getxyz()).mul(1.0f/8).add(vo);
+            if(!flataxisface(c, i)) convex = faceconvexity(verts, numverts, size);
+        }
+        else
+        {
+            ivec v[4];
+            genfaceverts(c, i, v);
+            if(!flataxisface(c, i)) convex = faceconvexity(v);
+            int order = vis&4 || convex < 0 ? 1 : 0;
+            vec vo(co);
+            pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo);
+            if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo);
+            pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo);
+            if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo);
+        }
+
+        VSlot &vslot = lookupvslot(c.texture[i], true),
+              *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, true) : NULL;
+        ushort envmap = vslot.slot->shader->type&SHADER_ENVMAP ? (vslot.slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size)) : EMID_NONE,
+               envmap2 = layer && layer->slot->shader->type&SHADER_ENVMAP ? (layer->slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size)) : EMID_NONE;
+        while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next;
+        int hastj = tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1) ? tj : -1;
+        int grassy = vslot.slot->autograss && i!=O_BOTTOM ? (vis!=3 || convex ? 1 : 2) : 0;
+        if(!c.ext)
+            addcubeverts(vslot, i, size, pos, convex, c.texture[i], LMID_AMBIENT, NULL, numverts, hastj, envmap, grassy, (c.material&MAT_ALPHA)!=0);
+        else
+        { 
+            const surfaceinfo &surf = c.ext->surfaces[i];
+            if(!surf.numverts || surf.numverts&LAYER_TOP)
+                addcubeverts(vslot, i, size, pos, convex, c.texture[i], surf.lmid[0], verts, numverts, hastj, envmap, grassy, (c.material&MAT_ALPHA)!=0, LAYER_TOP|(surf.numverts&LAYER_BLEND));
+            if(surf.numverts&LAYER_BOTTOM)
+                addcubeverts(layer ? *layer : vslot, i, size, pos, convex, vslot.layer, surf.lmid[1], surf.numverts&LAYER_DUP ? verts + numverts : verts, numverts, hastj, envmap2);
+        }
+    }
+}
+
+static inline bool skyoccluded(cube &c, int orient)
+{
+    return touchingface(c, orient) && faceedges(c, orient) == F_SOLID;
+}
+
+static int dummyskyfaces[6];
+static inline int hasskyfaces(cube &c, const ivec &co, int size, int faces[6] = dummyskyfaces)
+{
+    int numfaces = 0;
+    if(isempty(c) || c.material&MAT_ALPHA)
+    {
+        if(co.x == 0) faces[numfaces++] = O_LEFT;
+        if(co.x + size == worldsize) faces[numfaces++] = O_RIGHT;
+        if(co.y == 0) faces[numfaces++] = O_BACK;
+        if(co.y + size == worldsize) faces[numfaces++] = O_FRONT;
+        if(co.z == 0) faces[numfaces++] = O_BOTTOM;
+        if(co.z + size == worldsize) faces[numfaces++] = O_TOP;
+    }
+    else if(!isentirelysolid(c))
+    {
+        if(co.x == 0 && !skyoccluded(c, O_LEFT)) faces[numfaces++] = O_LEFT;
+        if(co.x + size == worldsize && !skyoccluded(c, O_RIGHT)) faces[numfaces++] = O_RIGHT;
+        if(co.y == 0 && !skyoccluded(c, O_BACK)) faces[numfaces++] = O_BACK;
+        if(co.y + size == worldsize && !skyoccluded(c, O_FRONT)) faces[numfaces++] = O_FRONT;
+        if(co.z == 0 && !skyoccluded(c, O_BOTTOM)) faces[numfaces++] = O_BOTTOM;
+        if(co.z + size == worldsize && !skyoccluded(c, O_TOP)) faces[numfaces++] = O_TOP;
+    }
+    return numfaces;
+}
+
+void minskyface(cube &cu, int orient, const ivec &co, int size, facebounds &orig)
+{   
+    facebounds mincf;
+    mincf.u1 = orig.u2;
+    mincf.u2 = orig.u1;
+    mincf.v1 = orig.v2;
+    mincf.v2 = orig.v1;
+    mincubeface(cu, orient, co, size, orig, mincf, MAT_ALPHA, MAT_ALPHA);
+    orig.u1 = max(mincf.u1, orig.u1);
+    orig.u2 = min(mincf.u2, orig.u2);
+    orig.v1 = max(mincf.v1, orig.v1);
+    orig.v2 = min(mincf.v2, orig.v2);
+}  
+
+void genskyfaces(cube &c, const ivec &o, int size)
+{
+    int faces[6], numfaces = hasskyfaces(c, o, size, faces);
+    if(!numfaces) return;
+
+    loopi(numfaces)
+    {
+        int orient = faces[i], dim = dimension(orient);
+        facebounds m;
+        m.u1 = (o[C[dim]]&0xFFF)<<3; 
+        m.u2 = m.u1 + (size<<3);
+        m.v1 = (o[R[dim]]&0xFFF)<<3;
+        m.v2 = m.v1 + (size<<3);
+        minskyface(c, orient, o, size, m);
+        if(m.u1 >= m.u2 || m.v1 >= m.v2) continue;
+        vc.skyarea += (int(m.u2-m.u1)*int(m.v2-m.v1) + (1<<(2*3))-1)>>(2*3);
+        vc.skyfaces[orient].add(m);
+    }
+}
+
+void addskyverts(const ivec &o, int size)
+{
+    loopi(6)
+    {
+        int dim = dimension(i), c = C[dim], r = R[dim];
+        vector<facebounds> &sf = vc.skyfaces[i]; 
+        if(sf.empty()) continue;
+        vc.skymask |= 0x3F&~(1<<opposite(i));
+        sf.setsize(mergefaces(i, sf.getbuf(), sf.length()));
+        loopvj(sf)
+        {
+            facebounds &m = sf[j];
+            int index[4];
+            loopk(4)
+            {
+                const ivec &coords = facecoords[opposite(i)][k];
+                vec v;
+                v[dim] = o[dim];
+                if(coords[dim]) v[dim] += size;
+                v[c] = (o[c]&~0xFFF) + (coords[c] ? m.u2 : m.u1)/8.0f;
+                v[r] = (o[r]&~0xFFF) + (coords[r] ? m.v2 : m.v1)/8.0f;
+                index[k] = vc.addvert(v);
+                if(index[k] < 0) goto nextskyface;
+                vc.skyclip = min(vc.skyclip, int(v.z*8)>>3);
+            }
+            if(vc.skytris + 6 > USHRT_MAX) break;
+            vc.skytris += 6;
+            vc.skyindices.add(index[0]);
+            vc.skyindices.add(index[1]);
+            vc.skyindices.add(index[2]);
+
+            vc.skyindices.add(index[0]);
+            vc.skyindices.add(index[2]);
+            vc.skyindices.add(index[3]);
+        nextskyface:;
+        }
+    }
+}
+                    
+////////// Vertex Arrays //////////////
+
+int allocva = 0;
+int wtris = 0, wverts = 0, vtris = 0, vverts = 0, glde = 0, gbatches = 0;
+vector<vtxarray *> valist, varoot;
+
+vtxarray *newva(const ivec &co, int size)
+{
+    vc.optimize();
+
+    vtxarray *va = new vtxarray;
+    va->parent = NULL;
+    va->o = co;
+    va->size = size;
+    va->skyarea = vc.skyarea;
+    va->skyfaces = vc.skymask;
+    va->skyclip = vc.skyclip < INT_MAX ? vc.skyclip : INT_MAX;
+    va->curvfc = VFC_NOT_VISIBLE;
+    va->occluded = OCCLUDE_NOTHING;
+    va->query = NULL;
+    va->bbmin = ivec(-1, -1, -1);
+    va->bbmax = ivec(-1, -1, -1);
+    va->hasmerges = 0;
+    va->mergelevel = -1;
+
+    vc.setupdata(va);
+
+    wverts += va->verts;
+    wtris  += va->tris + va->blends + va->alphatris;
+    allocva++;
+    valist.add(va);
+
+    return va;
+}
+
+void destroyva(vtxarray *va, bool reparent)
+{
+    wverts -= va->verts;
+    wtris -= va->tris + va->blends + va->alphatris;
+    allocva--;
+    valist.removeobj(va);
+    if(!va->parent) varoot.removeobj(va);
+    if(reparent)
+    {
+        if(va->parent) va->parent->children.removeobj(va);
+        loopv(va->children)
+        {
+            vtxarray *child = va->children[i];
+            child->parent = va->parent;
+            if(child->parent) child->parent->children.add(child);
+        }
+    }
+    if(va->vbuf) destroyvbo(va->vbuf);
+    if(va->ebuf) destroyvbo(va->ebuf);
+    if(va->skybuf) destroyvbo(va->skybuf);
+    if(va->eslist) delete[] va->eslist;
+    if(va->matbuf) delete[] va->matbuf;
+    delete va;
+}
+
+void clearvas(cube *c)
+{
+    loopi(8)
+    {
+        if(c[i].ext)
+        {
+            if(c[i].ext->va) destroyva(c[i].ext->va, false);
+            c[i].ext->va = NULL;
+            c[i].ext->tjoints = -1;
+        }
+        if(c[i].children) clearvas(c[i].children);
+    }
+}
+
+void updatevabb(vtxarray *va, bool force)
+{
+    if(!force && va->bbmin.x >= 0) return;
+
+    va->bbmin = va->geommin;
+    va->bbmax = va->geommax;
+    va->bbmin.min(va->matmin);
+    va->bbmax.max(va->matmax);
+    loopv(va->children)
+    {
+        vtxarray *child = va->children[i];
+        updatevabb(child, force);
+        va->bbmin.min(child->bbmin);
+        va->bbmax.max(child->bbmax);
+    }
+    loopv(va->mapmodels)
+    {
+        octaentities *oe = va->mapmodels[i];
+        va->bbmin.min(oe->bbmin);
+        va->bbmax.max(oe->bbmax);
+    }
+    va->bbmin.max(va->o);
+    va->bbmax.min(ivec(va->o).add(va->size));
+
+    if(va->skyfaces)
+    {
+        va->skyfaces |= 0x80;
+        if(va->sky) loop(dim, 3) if(va->skyfaces&(3<<(2*dim)))
+        {
+            int r = R[dim], c = C[dim];
+            if((va->skyfaces&(1<<(2*dim)) && va->o[dim] < va->bbmin[dim]) ||
+               (va->skyfaces&(2<<(2*dim)) && va->o[dim]+va->size > va->bbmax[dim]) ||
+               va->o[r] < va->bbmin[r] || va->o[r]+va->size > va->bbmax[r] ||
+               va->o[c] < va->bbmin[c] || va->o[c]+va->size > va->bbmax[c])
+            {
+                va->skyfaces &= ~0x80;
+                break;
+            }
+        }
+    }
+}
+
+void updatevabbs(bool force)
+{
+    loopv(varoot) updatevabb(varoot[i], force);
+}
+
+struct mergedface
+{   
+    uchar orient, lmid, numverts;
+    ushort mat, tex, envmap;
+    vertinfo *verts;
+    int tjoints;
+};  
+
+#define MAXMERGELEVEL 12
+static int vahasmerges = 0, vamergemax = 0;
+static vector<mergedface> vamerges[MAXMERGELEVEL+1];
+
+int genmergedfaces(cube &c, const ivec &co, int size, int minlevel = -1)
+{
+    if(!c.ext || isempty(c)) return -1;
+    int tj = c.ext->tjoints, maxlevel = -1;
+    loopi(6) if(c.merged&(1<<i)) 
+    {
+        surfaceinfo &surf = c.ext->surfaces[i];
+        int numverts = surf.numverts&MAXFACEVERTS;
+        if(!numverts) 
+        {
+            if(minlevel < 0) vahasmerges |= MERGE_PART;
+            continue;
+        }
+        mergedface mf;
+        mf.orient = i;
+        mf.mat = c.material;
+        mf.tex = c.texture[i];
+        mf.envmap = EMID_NONE;
+        mf.lmid = surf.lmid[0];
+        mf.numverts = surf.numverts;
+        mf.verts = c.ext->verts() + surf.verts; 
+        mf.tjoints = -1;
+        int level = calcmergedsize(i, co, size, mf.verts, mf.numverts&MAXFACEVERTS);
+        if(level > minlevel)
+        {
+            maxlevel = max(maxlevel, level);
+
+            while(tj >= 0 && tjoints[tj].edge < i*(MAXFACEVERTS+1)) tj = tjoints[tj].next;
+            if(tj >= 0 && tjoints[tj].edge < (i+1)*(MAXFACEVERTS+1)) mf.tjoints = tj;
+
+            VSlot &vslot = lookupvslot(mf.tex, true),
+                  *layer = vslot.layer && !(c.material&MAT_ALPHA) ? &lookupvslot(vslot.layer, true) : NULL;
+            if(vslot.slot->shader->type&SHADER_ENVMAP)
+                mf.envmap = vslot.slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size);
+            ushort envmap2 = layer && layer->slot->shader->type&SHADER_ENVMAP ? (layer->slot->texmask&(1<<TEX_ENVMAP) ? EMID_CUSTOM : closestenvmap(i, co, size)) : EMID_NONE;
+
+            if(surf.numverts&LAYER_TOP) vamerges[level].add(mf); 
+            if(surf.numverts&LAYER_BOTTOM)
+            {
+                mf.tex = vslot.layer;
+                mf.envmap = envmap2;
+                mf.lmid = surf.lmid[1];
+                mf.numverts &= ~LAYER_TOP;
+                if(surf.numverts&LAYER_DUP) mf.verts += numverts;
+                vamerges[level].add(mf);
+            }
+        }
+    }
+    if(maxlevel >= 0)
+    {
+        vamergemax = max(vamergemax, maxlevel);
+        vahasmerges |= MERGE_ORIGIN;
+    }
+    return maxlevel;
+}
+
+int findmergedfaces(cube &c, const ivec &co, int size, int csi, int minlevel)
+{
+    if(c.ext && c.ext->va && !(c.ext->va->hasmerges&MERGE_ORIGIN)) return c.ext->va->mergelevel;
+    else if(c.children)
+    {
+        int maxlevel = -1;
+        loopi(8)
+        {
+            ivec o(i, co, size/2); 
+            int level = findmergedfaces(c.children[i], o, size/2, csi-1, minlevel);
+            maxlevel = max(maxlevel, level);
+        }
+        return maxlevel;
+    }
+    else if(c.ext && c.merged) return genmergedfaces(c, co, size, minlevel);
+    else return -1;
+}
+
+void addmergedverts(int level, const ivec &o)
+{
+    vector<mergedface> &mfl = vamerges[level];
+    if(mfl.empty()) return;
+    vec vo(ivec(o).mask(~0xFFF));
+    vec pos[MAXFACEVERTS];
+    loopv(mfl)
+    {
+        mergedface &mf = mfl[i];
+        int numverts = mf.numverts&MAXFACEVERTS;
+        loopi(numverts)
+        {
+            vertinfo &v = mf.verts[i];
+            pos[i] = vec(v.x, v.y, v.z).mul(1.0f/8).add(vo);
+        }
+        VSlot &vslot = lookupvslot(mf.tex, true);
+        int grassy = vslot.slot->autograss && mf.orient!=O_BOTTOM && mf.numverts&LAYER_TOP ? 2 : 0;
+        addcubeverts(vslot, mf.orient, 1<<level, pos, 0, mf.tex, mf.lmid, mf.verts, numverts, mf.tjoints, mf.envmap, grassy, (mf.mat&MAT_ALPHA)!=0, mf.numverts&LAYER_BLEND);
+        vahasmerges |= MERGE_USE;
+    }
+    mfl.setsize(0);
+}
+
+void rendercube(cube &c, const ivec &co, int size, int csi, int &maxlevel)  // creates vertices and indices ready to be put into a va
+{
+    //if(size<=16) return;
+    if(c.ext && c.ext->va) 
+    {
+        maxlevel = max(maxlevel, c.ext->va->mergelevel);
+        return;                            // don't re-render
+    }
+
+    if(c.children)
+    {
+        neighbourstack[++neighbourdepth] = c.children;
+        c.escaped = 0;
+        loopi(8)
+        {
+            ivec o(i, co, size/2);
+            int level = -1;
+            rendercube(c.children[i], o, size/2, csi-1, level);
+            if(level >= csi) 
+                c.escaped |= 1<<i;
+            maxlevel = max(maxlevel, level);   
+        }
+        --neighbourdepth;
+
+        if(csi <= MAXMERGELEVEL && vamerges[csi].length()) addmergedverts(csi, co);
+
+        if(c.ext)
+        {
+            if(c.ext->ents && c.ext->ents->mapmodels.length()) vc.mapmodels.add(c.ext->ents);
+        }
+        return;
+    }
+    
+    genskyfaces(c, co, size);
+
+    if(!isempty(c)) 
+    {
+        gencubeverts(c, co, size, csi);
+        if(c.merged) maxlevel = max(maxlevel, genmergedfaces(c, co, size));
+    }
+    if(c.material != MAT_AIR) genmatsurfs(c, co, size, vc.matsurfs);
+
+    if(c.ext)
+    {
+        if(c.ext->ents && c.ext->ents->mapmodels.length()) vc.mapmodels.add(c.ext->ents);
+    }
+
+    if(csi <= MAXMERGELEVEL && vamerges[csi].length()) addmergedverts(csi, co);
+}
+
+void calcgeombb(const ivec &co, int size, ivec &bbmin, ivec &bbmax)
+{
+    vec vmin(co), vmax = vmin;
+    vmin.add(size);
+
+    loopv(vc.verts)
+    {
+        const vec &v = vc.verts[i].pos;
+        vmin.min(v);
+        vmax.max(v);
+    }
+
+    bbmin = ivec(vmin.mul(8)).shr(3);
+    bbmax = ivec(vmax.mul(8)).add(7).shr(3);
+}
+
+void calcmatbb(const ivec &co, int size, ivec &bbmin, ivec &bbmax)
+{
+    bbmax = co;
+    (bbmin = bbmax).add(size);
+    loopv(vc.matsurfs)
+    {
+        materialsurface &m = vc.matsurfs[i];
+        switch(m.material&MATF_VOLUME)
+        {
+            case MAT_WATER:
+            case MAT_GLASS:
+            case MAT_LAVA:
+                break;
+
+            default:
+                continue;
+        }
+
+        int dim = dimension(m.orient),
+            r = R[dim],
+            c = C[dim];
+        bbmin[dim] = min(bbmin[dim], m.o[dim]);
+        bbmax[dim] = max(bbmax[dim], m.o[dim]);
+
+        bbmin[r] = min(bbmin[r], m.o[r]);
+        bbmax[r] = max(bbmax[r], m.o[r] + m.rsize);
+
+        bbmin[c] = min(bbmin[c], m.o[c]);
+        bbmax[c] = max(bbmax[c], m.o[c] + m.csize);
+    }
+}
+
+void setva(cube &c, const ivec &co, int size, int csi)
+{
+    ASSERT(size <= 0x1000);
+
+    int vamergeoffset[MAXMERGELEVEL+1];
+    loopi(MAXMERGELEVEL+1) vamergeoffset[i] = vamerges[i].length();
+
+    vc.origin = co;
+    vc.size = size;
+
+    shadowmapmin = vec(co).add(size);
+    shadowmapmax = vec(co);
+
+    int maxlevel = -1;
+    rendercube(c, co, size, csi, maxlevel);
+
+    ivec bbmin, bbmax;
+
+    calcgeombb(co, size, bbmin, bbmax);
+
+    addskyverts(co, size);
+
+    if(size == min(0x1000, worldsize/2) || !vc.emptyva())
+    {
+        vtxarray *va = newva(co, size);
+        ext(c).va = va;
+        va->geommin = bbmin;
+        va->geommax = bbmax;
+        calcmatbb(co, size, va->matmin, va->matmax);
+        va->shadowmapmin = ivec(shadowmapmin.mul(8)).shr(3);
+        va->shadowmapmax = ivec(shadowmapmax.mul(8)).add(7).shr(3);
+        va->hasmerges = vahasmerges;
+        va->mergelevel = vamergemax;
+    }
+    else
+    {
+        loopi(MAXMERGELEVEL+1) vamerges[i].setsize(vamergeoffset[i]);
+    }
+
+    vc.clear();
+}
+
+static inline int setcubevisibility(cube &c, const ivec &co, int size)
+{
+    int numvis = 0, vismask = 0, collidemask = 0, checkmask = 0;
+    loopi(6)
+    {
+        int facemask = classifyface(c, i, co, size);
+        if(facemask&1) 
+        {
+            vismask |= 1<<i;
+            if(c.merged&(1<<i))
+            {
+                if(c.ext && c.ext->surfaces[i].numverts&MAXFACEVERTS) numvis++;
+            }
+            else 
+            {
+                numvis++;
+                if(c.texture[i] != DEFAULT_SKY && !(c.ext && c.ext->surfaces[i].numverts&MAXFACEVERTS)) checkmask |= 1<<i;
+            }
+        } 
+        if(facemask&2 && collideface(c, i)) collidemask |= 1<<i;
+    }
+    c.visible = collidemask | (vismask ? (vismask != collidemask ? (checkmask ? 0x80|0x40 : 0x80) : 0x40) : 0);
+    return numvis;
+}
+
+VARF(vafacemax, 64, 384, 256*256, allchanged());
+VARF(vafacemin, 0, 96, 256*256, allchanged());
+VARF(vacubesize, 32, 128, 0x1000, allchanged());
+
+int updateva(cube *c, const ivec &co, int size, int csi)
+{
+    progress("recalculating geometry...");
+    int ccount = 0, cmergemax = vamergemax, chasmerges = vahasmerges;
+    neighbourstack[++neighbourdepth] = c;
+    loopi(8)                                    // counting number of semi-solid/solid children cubes
+    {
+        int count = 0, childpos = varoot.length();
+        ivec o(i, co, size);
+        vamergemax = 0;
+        vahasmerges = 0;
+        if(c[i].ext && c[i].ext->va) 
+        {
+            varoot.add(c[i].ext->va);
+            if(c[i].ext->va->hasmerges&MERGE_ORIGIN) findmergedfaces(c[i], o, size, csi, csi);
+        }
+        else
+        {
+            if(c[i].children) count += updateva(c[i].children, o, size/2, csi-1);
+            else 
+            {
+                if(!isempty(c[i])) count += setcubevisibility(c[i], o, size);
+                count += hasskyfaces(c[i], o, size);
+            }
+            int tcount = count + (csi <= MAXMERGELEVEL ? vamerges[csi].length() : 0);
+            if(tcount > vafacemax || (tcount >= vafacemin && size >= vacubesize) || size == min(0x1000, worldsize/2)) 
+            {
+                loadprogress = clamp(recalcprogress/float(allocnodes), 0.0f, 1.0f);
+                setva(c[i], o, size, csi);
+                if(c[i].ext && c[i].ext->va)
+                {
+                    while(varoot.length() > childpos)
+                    {
+                        vtxarray *child = varoot.pop();
+                        c[i].ext->va->children.add(child);
+                        child->parent = c[i].ext->va;
+                    }
+                    varoot.add(c[i].ext->va);
+                    if(vamergemax > size)
+                    {
+                        cmergemax = max(cmergemax, vamergemax);
+                        chasmerges |= vahasmerges&~MERGE_USE;
+                    }
+                    continue;
+                }
+                else count = 0;
+            }
+        }
+        if(csi+1 <= MAXMERGELEVEL && vamerges[csi].length()) vamerges[csi+1].move(vamerges[csi]);
+        cmergemax = max(cmergemax, vamergemax);
+        chasmerges |= vahasmerges;
+        ccount += count;
+    }
+    --neighbourdepth;
+    vamergemax = cmergemax;
+    vahasmerges = chasmerges;
+
+    return ccount;
+}
+
+void addtjoint(const edgegroup &g, const cubeedge &e, int offset)
+{
+    int vcoord = (g.slope[g.axis]*offset + g.origin[g.axis]) & 0x7FFF;
+    tjoint &tj = tjoints.add();
+    tj.offset = vcoord / g.slope[g.axis];
+    tj.edge = e.index;
+
+    int prev = -1, cur = ext(*e.c).tjoints;
+    while(cur >= 0)
+    {
+        tjoint &o = tjoints[cur];
+        if(tj.edge < o.edge || (tj.edge==o.edge && (e.flags&CE_FLIP ? tj.offset > o.offset : tj.offset < o.offset))) break;
+        prev = cur;
+        cur = o.next;
+    }
+
+    tj.next = cur;
+    if(prev < 0) e.c->ext->tjoints = tjoints.length()-1;
+    else tjoints[prev].next = tjoints.length()-1; 
+}
+
+void findtjoints(int cur, const edgegroup &g)
+{
+    int active = -1;
+    while(cur >= 0)
+    {
+        cubeedge &e = cubeedges[cur];
+        int prevactive = -1, curactive = active;
+        while(curactive >= 0)
+        {
+            cubeedge &a = cubeedges[curactive];
+            if(a.offset+a.size <= e.offset)
+            {
+                if(prevactive >= 0) cubeedges[prevactive].next = a.next;
+                else active = a.next;
+            }
+            else
+            {
+                prevactive = curactive;
+                if(!(a.flags&CE_DUP))
+                {
+                    if(e.flags&CE_START && e.offset > a.offset && e.offset < a.offset+a.size)
+                        addtjoint(g, a, e.offset);
+                    if(e.flags&CE_END && e.offset+e.size > a.offset && e.offset+e.size < a.offset+a.size)
+                        addtjoint(g, a, e.offset+e.size);
+                }
+                if(!(e.flags&CE_DUP))
+                {
+                    if(a.flags&CE_START && a.offset > e.offset && a.offset < e.offset+e.size)
+                        addtjoint(g, e, a.offset);
+                    if(a.flags&CE_END && a.offset+a.size > e.offset && a.offset+a.size < e.offset+e.size)
+                        addtjoint(g, e, a.offset+a.size);
+                }
+            }
+            curactive = a.next;
+        }
+        int next = e.next;
+        e.next = active;
+        active = cur;
+        cur = next;
+    }
+}
+
+void findtjoints()
+{
+    recalcprogress = 0;
+    gencubeedges();
+    tjoints.setsize(0);
+    enumeratekt(edgegroups, edgegroup, g, int, e, findtjoints(e, g));
+    cubeedges.setsize(0);
+    edgegroups.clear();
+}
+
+void octarender()                               // creates va s for all leaf cubes that don't already have them
+{
+    int csi = 0;
+    while(1<<csi < worldsize) csi++;
+
+    recalcprogress = 0;
+    varoot.setsize(0);
+    updateva(worldroot, ivec(0, 0, 0), worldsize/2, csi-1);
+    loadprogress = 0;
+    flushvbo();
+
+    explicitsky = 0;
+    skyarea = 0;
+    loopv(valist)
+    {
+        vtxarray *va = valist[i];
+        explicitsky += va->explicitsky;
+        skyarea += va->skyarea;
+    }
+
+    visibleva = NULL;
+}
+
+void precachetextures()
+{
+    vector<int> texs;
+    loopv(valist)
+    {
+        vtxarray *va = valist[i];
+        loopj(va->texs + va->blends) if(texs.find(va->eslist[j].texture) < 0) texs.add(va->eslist[j].texture);
+    }
+    loopv(texs)
+    {
+        loadprogress = float(i+1)/texs.length();
+        lookupvslot(texs[i]);
+    }
+    loadprogress = 0;
+}
+
+void allchanged(bool load)
+{
+    renderprogress(0, "clearing vertex arrays...");
+    clearvas(worldroot);
+    resetqueries();
+    resetclipplanes();
+    if(load)
+    {
+        setupsky();
+        initenvmaps();
+    }
+    guessshadowdir();
+    entitiesinoctanodes();
+    tjoints.setsize(0);
+    if(filltjoints) findtjoints();
+    octarender();
+    if(load) precachetextures();
+    setupmaterials();
+    invalidatepostfx();
+    updatevabbs(true);
+    resetblobs();
+    lightents();
+    if(load) 
+    {
+        seedparticles();
+        drawtextures();
+    }
+}
+
+void recalc()
+{
+    allchanged(true);
+}
+
+COMMAND(recalc, "");
+
diff --git a/src/engine/physics.cpp b/src/engine/physics.cpp
new file mode 100644 (file)
index 0000000..6e78863
--- /dev/null
@@ -0,0 +1,2057 @@
+// physics.cpp: no physics books were hurt nor consulted in the construction of this code.
+// All physics computations and constants were invented on the fly and simply tweaked until
+// they "felt right", and have no basis in reality. Collision detection is simplistic but
+// very robust (uses discrete steps at fixed fps).
+
+#include "engine.h"
+#include "mpr.h"
+
+const int MAXCLIPPLANES = 1024;
+static clipplanes clipcache[MAXCLIPPLANES];
+static int clipcacheversion = -2;
+
+static inline clipplanes &getclipplanes(const cube &c, const ivec &o, int size, bool collide = true, int offset = 0)
+{
+    clipplanes &p = clipcache[int(&c - worldroot)&(MAXCLIPPLANES-1)];
+    if(p.owner != &c || p.version != clipcacheversion+offset) 
+    {
+        p.owner = &c;
+        p.version = clipcacheversion+offset;
+        genclipplanes(c, o, size, p, collide);
+    }
+    return p;
+}
+
+void resetclipplanes()
+{
+    clipcacheversion += 2;
+    if(!clipcacheversion)
+    {
+        memclear(clipcache);
+        clipcacheversion = 2;
+    }
+}
+
+/////////////////////////  ray - cube collision ///////////////////////////////////////////////
+
+#define INTERSECTPLANES(setentry, exit) \
+    float enterdist = -1e16f, exitdist = 1e16f; \
+    loopi(p.size) \
+    { \
+        float pdist = p.p[i].dist(v), facing = ray.dot(p.p[i]); \
+        if(facing < 0) \
+        { \
+            pdist /= -facing; \
+            if(pdist > enterdist) \
+            { \
+                if(pdist > exitdist) exit; \
+                enterdist = pdist; \
+                setentry; \
+            } \
+        } \
+        else if(facing > 0) \
+        { \
+            pdist /= -facing; \
+            if(pdist < exitdist) \
+            { \
+                if(pdist < enterdist) exit; \
+                exitdist = pdist; \
+            } \
+        } \
+        else if(pdist > 0) exit; \
+    }
+
+#define INTERSECTBOX(setentry, exit) \
+    loop(i, 3) \
+    { \
+        if(ray[i]) \
+        { \
+            float prad = fabs(p.r[i] * invray[i]), pdist = (p.o[i] - v[i]) * invray[i], pmin = pdist - prad, pmax = pdist + prad; \
+            if(pmin > enterdist) \
+            { \
+                if(pmin > exitdist) exit; \
+                enterdist = pmin; \
+                setentry; \
+            } \
+            if(pmax < exitdist) \
+            { \
+                if(pmax < enterdist) exit; \
+                exitdist = pmax; \
+            } \
+         } \
+         else if(v[i] < p.o[i]-p.r[i] || v[i] > p.o[i]+p.r[i]) exit; \
+    }
+
+vec hitsurface;
+
+static inline bool raycubeintersect(const clipplanes &p, const cube &c, const vec &v, const vec &ray, const vec &invray, float &dist)
+{
+    int entry = -1, bbentry = -1;
+    INTERSECTPLANES(entry = i, return false);
+    INTERSECTBOX(bbentry = i, return false);
+    if(exitdist < 0) return false;
+    dist = max(enterdist+0.1f, 0.0f);
+    if(bbentry>=0) { hitsurface = vec(0, 0, 0); hitsurface[bbentry] = ray[bbentry]>0 ? -1 : 1; }
+    else hitsurface = p.p[entry];
+    return true;
+}
+
+extern void entselectionbox(const entity &e, vec &eo, vec &es);
+float hitentdist;
+int hitent, hitorient;
+
+static float disttoent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+    vec eo, es;
+    int orient = -1;
+    float dist = radius, f = 0.0f;
+    const vector<extentity *> &ents = entities::getents();
+
+    #define entintersect(mask, type, func) {\
+        if((mode&(mask))==(mask)) loopv(oc->type) \
+        { \
+            extentity &e = *ents[oc->type[i]]; \
+            if(!(e.flags&EF_OCTA) || &e==t) continue; \
+            func; \
+            if(f<dist && f>0 && vec(ray).mul(f).add(o).insidebb(oc->o, oc->size)) \
+            { \
+                hitentdist = dist = f; \
+                hitent = oc->type[i]; \
+                hitorient = orient; \
+            } \
+        } \
+    }
+
+    entintersect(RAY_POLY, mapmodels,
+        if(!mmintersect(e, o, ray, radius, mode, f)) continue;
+    );
+
+    entintersect(RAY_ENTS, other,
+        entselectionbox(e, eo, es);
+        if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
+    );
+
+    entintersect(RAY_ENTS, mapmodels,
+        entselectionbox(e, eo, es);
+        if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
+    );
+
+    return dist;
+}
+
+static float disttooutsideent(const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+    vec eo, es;
+    int orient;
+    float dist = radius, f = 0.0f;
+    const vector<extentity *> &ents = entities::getents();
+    loopv(outsideents)
+    {
+        extentity &e = *ents[outsideents[i]];
+        if(!(e.flags&EF_OCTA) || &e==t) continue;
+        entselectionbox(e, eo, es);
+        if(!rayboxintersect(eo, es, o, ray, f, orient)) continue;
+        if(f<dist && f>0)
+        {
+            hitentdist = dist = f;
+            hitent = outsideents[i];
+            hitorient = orient;
+        }
+    }
+    return dist;
+}
+
+// optimized shadow version
+static float shadowent(octaentities *oc, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+    float dist = radius, f = 0.0f;
+    const vector<extentity *> &ents = entities::getents();
+    loopv(oc->mapmodels)
+    {
+        extentity &e = *ents[oc->mapmodels[i]];
+        if(!(e.flags&EF_OCTA) || &e==t) continue;
+        if(!mmintersect(e, o, ray, radius, mode, f)) continue;
+        if(f>0 && f<dist) dist = f;
+    }
+    return dist;
+}
+
+#define INITRAYCUBE \
+    float dist = 0, dent = radius > 0 ? radius : 1e16f; \
+    vec v(o), invray(ray.x ? 1/ray.x : 1e16f, ray.y ? 1/ray.y : 1e16f, ray.z ? 1/ray.z : 1e16f); \
+    cube *levels[20]; \
+    levels[worldscale] = worldroot; \
+    int lshift = worldscale, elvl = mode&RAY_BB ? worldscale : 0; \
+    ivec lsizemask(invray.x>0 ? 1 : 0, invray.y>0 ? 1 : 0, invray.z>0 ? 1 : 0); \
+
+#define CHECKINSIDEWORLD \
+    if(!insideworld(o)) \
+    { \
+        float disttoworld = 0, exitworld = 1e16f; \
+        loopi(3) \
+        { \
+            float c = v[i]; \
+            if(c<0 || c>=worldsize) \
+            { \
+                float d = ((invray[i]>0?0:worldsize)-c)*invray[i]; \
+                if(d<0) return (radius>0?radius:-1); \
+                disttoworld = max(disttoworld, 0.1f + d); \
+            } \
+            float e = ((invray[i]>0?worldsize:0)-c)*invray[i]; \
+            exitworld = min(exitworld, e); \
+        } \
+        if(disttoworld > exitworld) return (radius>0?radius:-1); \
+        v.add(vec(ray).mul(disttoworld)); \
+        dist += disttoworld; \
+    }
+
+#define DOWNOCTREE(disttoent, earlyexit) \
+        cube *lc = levels[lshift]; \
+        for(;;) \
+        { \
+            lshift--; \
+            lc += octastep(x, y, z, lshift); \
+            if(lc->ext && lc->ext->ents && lshift < elvl) \
+            { \
+                float edist = disttoent(lc->ext->ents, o, ray, dent, mode, t); \
+                if(edist < dent) \
+                { \
+                    earlyexit return min(edist, dist); \
+                    elvl = lshift; \
+                    dent = min(dent, edist); \
+                } \
+            } \
+            if(lc->children==NULL) break; \
+            lc = lc->children; \
+            levels[lshift] = lc; \
+        }
+
+#define FINDCLOSEST(xclosest, yclosest, zclosest) \
+        float dx = (lo.x+(lsizemask.x<<lshift)-v.x)*invray.x, \
+              dy = (lo.y+(lsizemask.y<<lshift)-v.y)*invray.y, \
+              dz = (lo.z+(lsizemask.z<<lshift)-v.z)*invray.z; \
+        float disttonext = dx; \
+        xclosest; \
+        if(dy < disttonext) { disttonext = dy; yclosest; } \
+        if(dz < disttonext) { disttonext = dz; zclosest; } \
+        disttonext += 0.1f; \
+        v.add(vec(ray).mul(disttonext)); \
+        dist += disttonext;
+
+#define UPOCTREE(exitworld) \
+        x = int(v.x); \
+        y = int(v.y); \
+        z = int(v.z); \
+        uint diff = uint(lo.x^x)|uint(lo.y^y)|uint(lo.z^z); \
+        if(diff >= uint(worldsize)) exitworld; \
+        diff >>= lshift; \
+        if(!diff) exitworld; \
+        do \
+        { \
+            lshift++; \
+            diff >>= 1; \
+        } while(diff);
+
+float raycube(const vec &o, const vec &ray, float radius, int mode, int size, extentity *t)
+{
+    if(ray.iszero()) return 0;
+
+    INITRAYCUBE;
+    CHECKINSIDEWORLD;
+
+    int closest = -1, x = int(v.x), y = int(v.y), z = int(v.z);
+    for(;;)
+    {
+        DOWNOCTREE(disttoent, if(mode&RAY_SHADOW));
+
+        int lsize = 1<<lshift;
+
+        cube &c = *lc;
+        if((dist>0 || !(mode&RAY_SKIPFIRST)) &&
+           (((mode&RAY_CLIPMAT) && isclipped(c.material&MATF_VOLUME)) ||
+            ((mode&RAY_EDITMAT) && c.material != MAT_AIR) ||
+            (!(mode&RAY_PASS) && lsize==size && !isempty(c)) ||
+            isentirelysolid(c) ||
+            dent < dist))
+        {
+            if(closest >= 0) { hitsurface = vec(0, 0, 0); hitsurface[closest] = ray[closest]>0 ? -1 : 1; }
+            return min(dent, dist);
+        }
+
+        ivec lo(x&(~0U<<lshift), y&(~0U<<lshift), z&(~0U<<lshift));
+
+        if(!isempty(c))
+        {
+            const clipplanes &p = getclipplanes(c, lo, lsize, false, 1);
+            float f = 0;
+            if(raycubeintersect(p, c, v, ray, invray, f) && (dist+f>0 || !(mode&RAY_SKIPFIRST)))
+                return min(dent, dist+f);
+        }
+
+        FINDCLOSEST(closest = 0, closest = 1, closest = 2);
+
+        if(radius>0 && dist>=radius) return min(dent, dist);
+
+        UPOCTREE(return min(dent, radius>0 ? radius : dist));
+    }
+}
+
+// optimized version for lightmap shadowing... every cycle here counts!!!
+float shadowray(const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+    INITRAYCUBE;
+    CHECKINSIDEWORLD;
+
+    int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z);
+    for(;;)
+    {
+        DOWNOCTREE(shadowent, );
+
+        cube &c = *lc;
+        ivec lo(x&(~0U<<lshift), y&(~0U<<lshift), z&(~0U<<lshift));
+
+        if(!isempty(c) && !(c.material&MAT_ALPHA))
+        {
+            if(isentirelysolid(c))
+            {
+                if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+                {
+                    if(mode&RAY_SKYTEX) return radius;
+                }
+                else return dist;
+            }
+            else
+            {
+                const clipplanes &p = getclipplanes(c, lo, 1<<lshift, false, 1);
+                INTERSECTPLANES(side = p.side[i], goto nextcube);
+                INTERSECTBOX(side = (i<<1) + 1 - lsizemask[i], goto nextcube);
+                if(exitdist >= 0)
+                {
+                    if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+                    {
+                        if(mode&RAY_SKYTEX) return radius;
+                    }
+                    else return dist+max(enterdist+0.1f, 0.0f);
+                }
+            }
+        }
+
+    nextcube:
+        FINDCLOSEST(side = O_RIGHT - lsizemask.x, side = O_FRONT - lsizemask.y, side = O_TOP - lsizemask.z);
+
+        if(dist>=radius) return dist;
+
+        UPOCTREE(return radius);
+    }
+}
+
+// thread safe version
+
+struct ShadowRayCache
+{
+    clipplanes clipcache[MAXCLIPPLANES];
+    int version;
+
+    ShadowRayCache() : version(-1) {}
+};
+
+ShadowRayCache *newshadowraycache() { return new ShadowRayCache; }
+
+void freeshadowraycache(ShadowRayCache *&cache) { delete cache; cache = NULL; }
+
+void resetshadowraycache(ShadowRayCache *cache) 
+{ 
+    cache->version++;
+    if(!cache->version)
+    {
+        memclear(cache->clipcache);
+        cache->version = 1;
+    }
+}
+
+float shadowray(ShadowRayCache *cache, const vec &o, const vec &ray, float radius, int mode, extentity *t)
+{
+    INITRAYCUBE;
+    CHECKINSIDEWORLD;
+
+    int side = O_BOTTOM, x = int(v.x), y = int(v.y), z = int(v.z);
+    for(;;)
+    {
+        DOWNOCTREE(shadowent, );
+
+        cube &c = *lc;
+        ivec lo(x&(~0U<<lshift), y&(~0U<<lshift), z&(~0U<<lshift));
+
+        if(!isempty(c) && !(c.material&MAT_ALPHA))
+        {
+            if(isentirelysolid(c))
+            {
+                if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+                {
+                    if(mode&RAY_SKYTEX) return radius;
+                }
+                else return dist;
+            }
+            else
+            {
+                clipplanes &p = cache->clipcache[int(&c - worldroot)&(MAXCLIPPLANES-1)];
+                if(p.owner != &c || p.version != cache->version) { p.owner = &c; p.version = cache->version; genclipplanes(c, lo, 1<<lshift, p, false); }
+                INTERSECTPLANES(side = p.side[i], goto nextcube);
+                INTERSECTBOX(side = (i<<1) + 1 - lsizemask[i], goto nextcube);
+                if(exitdist >= 0)
+                {
+                    if(c.texture[side]==DEFAULT_SKY && mode&RAY_SKIPSKY)
+                    {
+                        if(mode&RAY_SKYTEX) return radius;
+                    }
+                    else return dist+max(enterdist+0.1f, 0.0f);
+                }
+            }
+        }
+
+    nextcube:
+        FINDCLOSEST(side = O_RIGHT - lsizemask.x, side = O_FRONT - lsizemask.y, side = O_TOP - lsizemask.z);
+
+        if(dist>=radius) return dist;
+
+        UPOCTREE(return radius);
+    }
+}
+
+float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent)
+{
+    hitent = -1;
+    hitentdist = radius;
+    hitorient = -1;
+    float dist = raycube(o, ray, radius, mode, size);
+    if((mode&RAY_ENTS) == RAY_ENTS)
+    {
+        float dent = disttooutsideent(o, ray, dist < 0 ? 1e16f : dist, mode, NULL);
+        if(dent < 1e15f && (dist < 0 || dent < dist)) dist = dent;
+    }
+    orient = hitorient;
+    ent = hitentdist == dist ? hitent : -1;
+    return dist;
+}
+
+float raycubepos(const vec &o, const vec &ray, vec &hitpos, float radius, int mode, int size)
+{
+    hitpos = ray;
+    float dist = raycube(o, ray, radius, mode, size);
+    if(radius>0 && dist>=radius) dist = radius;
+    hitpos.mul(dist).add(o);
+    return dist;
+}
+
+bool raycubelos(const vec &o, const vec &dest, vec &hitpos)
+{
+    vec ray(dest);
+    ray.sub(o);
+    float mag = ray.magnitude();
+    ray.mul(1/mag);
+    float distance = raycubepos(o, ray, hitpos, mag, RAY_CLIPMAT|RAY_POLY);
+    return distance >= mag;
+}
+
+float rayfloor(const vec &o, vec &floor, int mode, float radius)
+{
+    if(o.z<=0) return -1;
+    hitsurface = vec(0, 0, 1);
+    float dist = raycube(o, vec(0, 0, -1), radius, mode);
+    if(dist<0 || (radius>0 && dist>=radius)) return dist;
+    floor = hitsurface;
+    return dist;
+}
+
+/////////////////////////  entity collision  ///////////////////////////////////////////////
+
+// info about collisions
+int collideinside; // whether an internal collision happened
+physent *collideplayer; // whether the collection hit a player
+vec collidewall; // just the normal vectors.
+
+const float STAIRHEIGHT = 4.1f;
+const float FLOORZ = 0.867f;
+const float SLOPEZ = 0.5f;
+const float WALLZ = 0.2f;
+extern const float JUMPVEL = 125.0f;
+extern const float GRAVITY = 200.0f;
+
+bool ellipseboxcollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
+{
+    float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye),
+          above = (d->o.z-d->eyeheight) - (o.z+center.z+hi);
+    if(below>=0 || above>=0) return false;
+
+    vec yo(d->o);
+    yo.sub(o);
+    yo.rotate_around_z(-yaw*RAD);
+    yo.sub(center);
+
+    float dx = clamp(yo.x, -xr, xr) - yo.x, dy = clamp(yo.y, -yr, yr) - yo.y,
+          dist = sqrtf(dx*dx + dy*dy) - d->radius;
+    if(dist < 0)
+    {
+        int sx = yo.x <= -xr ? -1 : (yo.x >= xr ? 1 : 0),
+            sy = yo.y <= -yr ? -1 : (yo.y >= yr ? 1 : 0);
+        if(dist > (yo.z < 0 ? below : above) && (sx || sy))
+        {
+            vec ydir(dir);
+            ydir.rotate_around_z(-yaw*RAD);
+            if(sx*yo.x - xr > sy*yo.y - yr)
+            {
+                if(dir.iszero() || sx*ydir.x < -1e-6f)
+                {
+                    collidewall = vec(sx, 0, 0);
+                    collidewall.rotate_around_z(yaw*RAD);
+                    return true;
+                }
+            }
+            else if(dir.iszero() || sy*ydir.y < -1e-6f)
+            {
+                collidewall = vec(0, sy, 0);
+                collidewall.rotate_around_z(yaw*RAD);
+                return true;
+            }
+        }
+        if(yo.z < 0)
+        {
+            if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->eyeheight+d->aboveeye)/4.0f)))
+            {
+                collidewall = vec(0, 0, -1);
+                return true;
+            }
+        }
+        else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->eyeheight+d->aboveeye)/3.0f)))
+        {
+            collidewall = vec(0, 0, 1);
+            return true;
+        }
+        collideinside++;
+    }
+    return false;
+}
+
+bool ellipsecollide(physent *d, const vec &dir, const vec &o, const vec &center, float yaw, float xr, float yr, float hi, float lo)
+{
+    float below = (o.z+center.z-lo) - (d->o.z+d->aboveeye),
+          above = (d->o.z-d->eyeheight) - (o.z+center.z+hi);
+    if(below>=0 || above>=0) return false;
+    vec yo(center);
+    yo.rotate_around_z(yaw*RAD);
+    yo.add(o);
+    float x = yo.x - d->o.x, y = yo.y - d->o.y;
+    float angle = atan2f(y, x), dangle = angle-(d->yaw+90)*RAD, eangle = angle-(yaw+90)*RAD;
+    float dx = d->xradius*cosf(dangle), dy = d->yradius*sinf(dangle);
+    float ex = xr*cosf(eangle), ey = yr*sinf(eangle);
+    float dist = sqrtf(x*x + y*y) - sqrtf(dx*dx + dy*dy) - sqrtf(ex*ex + ey*ey);
+    if(dist < 0)
+    {
+        if(dist > (d->o.z < yo.z ? below : above) && (dir.iszero() || x*dir.x + y*dir.y > 0))
+        {
+            collidewall = vec(-x, -y, 0);
+            if(!collidewall.iszero()) collidewall.normalize();
+            return true;
+        }
+        if(d->o.z < yo.z)
+        {
+            if(dir.iszero() || (dir.z > 0 && (d->type>=ENT_INANIMATE || below >= d->zmargin-(d->eyeheight+d->aboveeye)/4.0f)))
+            {
+                collidewall = vec(0, 0, -1);
+                return true;
+            }
+        }
+        else if(dir.iszero() || (dir.z < 0 && (d->type>=ENT_INANIMATE || above >= d->zmargin-(d->eyeheight+d->aboveeye)/3.0f)))
+        {
+            collidewall = vec(0, 0, 1);
+            return true;
+        }
+        collideinside++;
+    }
+    return false;
+}
+
+#define DYNENTCACHESIZE 1024
+
+static uint dynentframe = 0;
+
+static struct dynentcacheentry
+{
+    int x, y;
+    uint frame;
+    vector<physent *> dynents;
+} dynentcache[DYNENTCACHESIZE];
+
+void cleardynentcache()
+{
+    dynentframe++;
+    if(!dynentframe || dynentframe == 1) loopi(DYNENTCACHESIZE) dynentcache[i].frame = 0;
+    if(!dynentframe) dynentframe = 1;
+}
+
+VARF(dynentsize, 4, 7, 12, cleardynentcache());
+
+#define DYNENTHASH(x, y) (((((x)^(y))<<5) + (((x)^(y))>>5)) & (DYNENTCACHESIZE - 1))
+
+const vector<physent *> &checkdynentcache(int x, int y)
+{
+    dynentcacheentry &dec = dynentcache[DYNENTHASH(x, y)];
+    if(dec.x == x && dec.y == y && dec.frame == dynentframe) return dec.dynents;
+    dec.x = x;
+    dec.y = y;
+    dec.frame = dynentframe;
+    dec.dynents.shrink(0);
+    int numdyns = game::numdynents(), dsize = 1<<dynentsize, dx = x<<dynentsize, dy = y<<dynentsize;
+    loopi(numdyns)
+    {
+        dynent *d = game::iterdynents(i);
+        if(d->state != CS_ALIVE ||
+           d->o.x+d->radius <= dx || d->o.x-d->radius >= dx+dsize ||
+           d->o.y+d->radius <= dy || d->o.y-d->radius >= dy+dsize)
+            continue;
+        dec.dynents.add(d);
+    }
+    return dec.dynents;
+}
+
+#define loopdynentcache(curx, cury, o, radius) \
+    for(int curx = max(int(o.x-radius), 0)>>dynentsize, endx = min(int(o.x+radius), worldsize-1)>>dynentsize; curx <= endx; curx++) \
+    for(int cury = max(int(o.y-radius), 0)>>dynentsize, endy = min(int(o.y+radius), worldsize-1)>>dynentsize; cury <= endy; cury++)
+
+void updatedynentcache(physent *d)
+{
+    loopdynentcache(x, y, d->o, d->radius)
+    {
+        dynentcacheentry &dec = dynentcache[DYNENTHASH(x, y)];
+        if(dec.x != x || dec.y != y || dec.frame != dynentframe || dec.dynents.find(d) >= 0) continue;
+        dec.dynents.add(d);
+    }
+}
+
+bool overlapsdynent(const vec &o, float radius)
+{
+    loopdynentcache(x, y, o, radius)
+    {
+        const vector<physent *> &dynents = checkdynentcache(x, y);
+        loopv(dynents)
+        {
+            physent *d = dynents[i];
+            if(o.dist(d->o)-d->radius < radius) return true;
+        }
+    }
+    return false;
+}
+
+template<class E, class O>
+static inline bool plcollide(physent *d, const vec &dir, physent *o)
+{
+    E entvol(d);
+    O obvol(o);
+    vec cp;
+    if(mpr::collide(entvol, obvol, NULL, NULL, &cp))
+    {
+        vec wn = vec(cp).sub(obvol.center());
+        collidewall = obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
+        if(!collidewall.iszero()) return true;
+        collideinside++;
+    }
+    return false;
+}
+
+static inline bool plcollide(physent *d, const vec &dir, physent *o)
+{
+    switch(d->collidetype)
+    {
+        case COLLIDE_ELLIPSE:
+        case COLLIDE_ELLIPSE_PRECISE:
+            if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight);
+            else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight);
+        case COLLIDE_OBB:
+            if(o->collidetype == COLLIDE_OBB) return plcollide<mpr::EntOBB, mpr::EntOBB>(d, dir, o);
+            else return plcollide<mpr::EntOBB, mpr::EntCylinder>(d, dir, o);
+        default: return false;
+    }
+}
+
+bool plcollide(physent *d, const vec &dir, bool insideplayercol)    // collide with player or monster
+{
+    if(d->type==ENT_CAMERA || d->state!=CS_ALIVE) return false;
+    int lastinside = collideinside;
+    physent *insideplayer = NULL;
+    loopdynentcache(x, y, d->o, d->radius)
+    {
+        const vector<physent *> &dynents = checkdynentcache(x, y);
+        loopv(dynents)
+        {
+            physent *o = dynents[i];
+            if(o==d || d->o.reject(o->o, d->radius+o->radius)) continue;
+            if(plcollide(d, dir, o))
+            {
+                collideplayer = o;
+                game::dynentcollide(d, o, collidewall);
+                return true;
+            }
+            if(collideinside > lastinside)
+            {
+                lastinside = collideinside;
+                insideplayer = o;
+            }
+        }
+    }
+    if(insideplayer && insideplayercol)
+    {
+        collideplayer = insideplayer;
+        game::dynentcollide(d, insideplayer, vec(0, 0, 0));
+        return true;
+    }
+    return false;
+}
+
+void rotatebb(vec &center, vec &radius, int yaw)
+{
+    if(yaw < 0) yaw = 360 + yaw%360;
+    else if(yaw >= 360) yaw %= 360;
+    const vec2 &rot = sincos360[yaw];
+    vec2 oldcenter(center), oldradius(radius);
+    center.x = oldcenter.x*rot.x - oldcenter.y*rot.y;
+    center.y = oldcenter.y*rot.x + oldcenter.x*rot.y;
+    radius.x = fabs(oldradius.x*rot.x) + fabs(oldradius.y*rot.y);
+    radius.y = fabs(oldradius.y*rot.x) + fabs(oldradius.x*rot.y);
+}
+
+template<class E, class M>
+static inline bool mmcollide(physent *d, const vec &dir, const extentity &e, const vec &center, const vec &radius, float yaw)
+{
+    E entvol(d);
+    M mdlvol(e.o, center, radius, yaw);
+    vec cp;
+    if(mpr::collide(entvol, mdlvol, NULL, NULL, &cp))
+    {
+        vec wn = vec(cp).sub(mdlvol.center());
+        collidewall = mdlvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir);
+        if(!collidewall.iszero()) return true;
+        collideinside++;
+    }
+    return false;
+}
+
+bool mmcollide(physent *d, const vec &dir, octaentities &oc)               // collide with a mapmodel
+{
+    const vector<extentity *> &ents = entities::getents();
+    loopv(oc.mapmodels)
+    {
+        extentity &e = *ents[oc.mapmodels[i]];
+        if(e.flags&EF_NOCOLLIDE) continue;
+        model *m = loadmapmodel(e.attr2);
+        if(!m || !m->collide) continue;
+
+        vec center, radius;
+        float rejectradius = m->collisionbox(center, radius);
+        if(d->o.reject(e.o, d->radius + rejectradius)) continue;
+
+        float yaw = e.attr1;
+        switch(d->collidetype)
+        {
+            case COLLIDE_ELLIPSE:
+            case COLLIDE_ELLIPSE_PRECISE:
+                if(m->ellipsecollide)
+                {
+                    if(ellipsecollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true;
+                }
+                else if(ellipseboxcollide(d, dir, e.o, center, yaw, radius.x, radius.y, radius.z, radius.z)) return true;
+                break;
+            case COLLIDE_OBB:
+                if(m->ellipsecollide)
+                {
+                    if(mmcollide<mpr::EntOBB, mpr::ModelEllipse>(d, dir, e, center, radius, yaw)) return true;
+                }
+                else if(mmcollide<mpr::EntOBB, mpr::ModelOBB>(d, dir, e, center, radius, yaw)) return true;
+                break;
+            default: continue;
+        }
+    }
+    return false;
+}
+
+template<class E>
+static bool fuzzycollidesolid(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with solid cube geometry
+{
+    int crad = size/2;
+    if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad ||
+       d->o.z + d->aboveeye < co.z || d->o.z - d->eyeheight > co.z + size)
+        return false;
+
+    E entvol(d);
+    collidewall = vec(0, 0, 0);
+    float bestdist = -1e10f;
+    int visible = isentirelysolid(c) ? c.visible : 0xFF;
+    #define CHECKSIDE(side, distval, dotval, margin, normal) if(visible&(1<<side)) do \
+    { \
+        float dist = distval; \
+        if(dist > 0) return false; \
+        if(dist <= bestdist) continue; \
+        if(!dir.iszero()) \
+        { \
+            if(dotval >= -cutoff*dir.magnitude()) continue; \
+            if(d->type<ENT_CAMERA && dotval < 0 && dist < margin) continue; \
+        } \
+        collidewall = normal; \
+        bestdist = dist; \
+    } while(0)
+    CHECKSIDE(O_LEFT, co.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, d->o.x - d->radius - (co.x + size), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, co.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, d->o.y - d->radius - (co.y + size), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, co.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, d->o.z - d->eyeheight - (co.z + size), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    if(collidewall.iszero())
+    {
+        collideinside++;
+        return false;
+    }
+    return true;
+}
+
+template<class E>
+static inline bool clampcollide(const clipplanes &p, const E &entvol, const plane &w, const vec &pw)
+{
+    if(w.x && (w.y || w.z) && fabs(pw.x - p.o.x) > p.r.x)
+    {
+        vec c = entvol.center();
+        float fv = pw.x < p.o.x ? p.o.x-p.r.x : p.o.x+p.r.x, fdist = (w.x*fv + w.y*c.y + w.z*c.z + w.offset) / (w.y*w.y + w.z*w.z);
+        vec fdir(fv - c.x, -w.y*fdist, -w.z*fdist);
+        if((pw.y-c.y-fdir.y)*w.y + (pw.z-c.z-fdir.z)*w.z >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true;
+    }
+    if(w.y && (w.x || w.z) && fabs(pw.y - p.o.y) > p.r.y)
+    {
+        vec c = entvol.center();
+        float fv = pw.y < p.o.y ? p.o.y-p.r.y : p.o.y+p.r.y, fdist = (w.x*c.x + w.y*fv + w.z*c.z + w.offset) / (w.x*w.x + w.z*w.z);
+        vec fdir(-w.x*fdist, fv - c.y, -w.z*fdist);
+        if((pw.x-c.x-fdir.x)*w.x + (pw.z-c.z-fdir.z)*w.z >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true;
+    }
+    if(w.z && (w.x || w.y) && fabs(pw.z - p.o.z) > p.r.z)
+    {
+        vec c = entvol.center();
+        float fv = pw.z < p.o.z ? p.o.z-p.r.z : p.o.z+p.r.z, fdist = (w.x*c.x + w.y*c.y + w.z*fv + w.offset) / (w.x*w.x + w.y*w.y);
+        vec fdir(-w.x*fdist, -w.y*fdist, fv - c.z);
+        if((pw.x-c.x-fdir.x)*w.x + (pw.y-c.y-fdir.y)*w.y >= 0 && entvol.supportpoint(fdir).squaredist(c) < fdir.squaredlen()) return true;
+    }
+    return false;
+}
+    
+template<class E>
+static bool fuzzycollideplanes(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with deformed cube geometry
+{
+    const clipplanes &p = getclipplanes(c, co, size);
+
+    if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius ||
+       d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->eyeheight > p.o.z + p.r.z)
+        return false;
+
+    collidewall = vec(0, 0, 0);
+    float bestdist = -1e10f;
+    int visible = p.visible;
+    CHECKSIDE(O_LEFT, p.o.x - p.r.x - (d->o.x + d->radius), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, d->o.x - d->radius - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, p.o.y - p.r.y - (d->o.y + d->radius), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, d->o.y - d->radius - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - (d->o.z + d->aboveeye), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, d->o.z - d->eyeheight - (p.o.z + p.r.z), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    E entvol(d);
+    int bestplane = -1;
+    loopi(p.size)
+    {
+        const plane &w = p.p[i];
+        vec pw = entvol.supportpoint(vec(w).neg());
+        float dist = w.dist(pw);
+        if(dist >= 0) return false;
+        if(dist <= bestdist) continue;
+        bestplane = -1;
+        bestdist = dist;
+        if(!dir.iszero())
+        {
+            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
+            if(d->type<ENT_CAMERA &&
+                dist < (dir.z*w.z < 0 ?
+                    d->zmargin-(d->eyeheight+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
+                    ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
+                continue;
+        }
+        if(clampcollide(p, entvol, w, pw)) continue;
+        bestplane = i;
+    }
+    if(bestplane >= 0) collidewall = p.p[bestplane];
+    else if(collidewall.iszero())
+    {
+        collideinside++;
+        return false;
+    }
+    return true;
+}
+
+template<class E>
+static bool cubecollidesolid(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with solid cube geometry
+{
+    int crad = size/2;
+    if(fabs(d->o.x - co.x - crad) > d->radius + crad || fabs(d->o.y - co.y - crad) > d->radius + crad ||
+       d->o.z + d->aboveeye < co.z || d->o.z - d->eyeheight > co.z + size)
+        return false;
+
+    E entvol(d);
+    bool collided = mpr::collide(mpr::SolidCube(co, size), entvol);
+    if(!collided) return false;
+
+    collidewall = vec(0, 0, 0);
+    float bestdist = -1e10f;
+    int visible = isentirelysolid(c) ? c.visible : 0xFF;
+    CHECKSIDE(O_LEFT, co.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, entvol.left() - (co.x + size), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, co.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, entvol.back() - (co.y + size), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, co.z - entvol.top(), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, entvol.bottom() - (co.z + size), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    if(collidewall.iszero())
+    {
+        collideinside++;
+        return false;
+    }
+    return true;
+}
+
+template<class E>
+static bool cubecollideplanes(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size) // collide with deformed cube geometry
+{
+    const clipplanes &p = getclipplanes(c, co, size);
+
+    if(fabs(d->o.x - p.o.x) > p.r.x + d->radius || fabs(d->o.y - p.o.y) > p.r.y + d->radius ||
+       d->o.z + d->aboveeye < p.o.z - p.r.z || d->o.z - d->eyeheight > p.o.z + p.r.z)
+        return false;
+
+    E entvol(d);
+    bool collided = mpr::collide(mpr::CubePlanes(p), entvol);
+    if(!collided) return false;
+
+    collidewall = vec(0, 0, 0);
+    float bestdist = -1e10f;
+    int visible = p.visible;
+    CHECKSIDE(O_LEFT, p.o.x - p.r.x - entvol.right(), -dir.x, -d->radius, vec(-1, 0, 0));
+    CHECKSIDE(O_RIGHT, entvol.left() - (p.o.x + p.r.x), dir.x, -d->radius, vec(1, 0, 0));
+    CHECKSIDE(O_BACK, p.o.y - p.r.y - entvol.front(), -dir.y, -d->radius, vec(0, -1, 0));
+    CHECKSIDE(O_FRONT, entvol.back() - (p.o.y + p.r.y), dir.y, -d->radius, vec(0, 1, 0));
+    CHECKSIDE(O_BOTTOM, p.o.z - p.r.z - entvol.top(), -dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/4.0f, vec(0, 0, -1));
+    CHECKSIDE(O_TOP, entvol.bottom() - (p.o.z + p.r.z), dir.z, d->zmargin-(d->eyeheight+d->aboveeye)/3.0f, vec(0, 0, 1));
+
+    int bestplane = -1;
+    loopi(p.size)
+    {
+        const plane &w = p.p[i];
+        vec pw = entvol.supportpoint(vec(w).neg());
+        float dist = w.dist(pw);
+        if(dist <= bestdist) continue;
+        bestplane = -1;
+        bestdist = dist;
+        if(!dir.iszero())
+        {
+            if(w.dot(dir) >= -cutoff*dir.magnitude()) continue;
+            if(d->type<ENT_CAMERA &&
+                dist < (dir.z*w.z < 0 ?
+                    d->zmargin-(d->eyeheight+d->aboveeye)/(dir.z < 0 ? 3.0f : 4.0f) :
+                    ((dir.x*w.x < 0 || dir.y*w.y < 0) ? -d->radius : 0)))
+                continue;
+        }
+        if(clampcollide(p, entvol, w, pw)) continue;
+        bestplane = i;
+    }
+    if(bestplane >= 0) collidewall = p.p[bestplane];
+    else if(collidewall.iszero())
+    {
+        collideinside++;
+        return false;
+    }
+    return true;
+}
+
+static inline bool cubecollide(physent *d, const vec &dir, float cutoff, const cube &c, const ivec &co, int size, bool solid)
+{
+    switch(d->collidetype)
+    {
+    case COLLIDE_OBB:
+        if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntOBB>(d, dir, cutoff, c, co, size);
+        else return cubecollideplanes<mpr::EntOBB>(d, dir, cutoff, c, co, size);
+    case COLLIDE_ELLIPSE:
+        if(isentirelysolid(c) || solid) return fuzzycollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+        else return fuzzycollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+    case COLLIDE_ELLIPSE_PRECISE:
+        if(isentirelysolid(c) || solid) return cubecollidesolid<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+        else return cubecollideplanes<mpr::EntCapsule>(d, dir, cutoff, c, co, size);
+    default: return false;
+    }
+}
+
+static inline bool octacollide(physent *d, const vec &dir, float cutoff, const ivec &bo, const ivec &bs, const cube *c, const ivec &cor, int size) // collide with octants
+{
+    loopoctabox(cor, size, bo, bs)
+    {
+        if(c[i].ext && c[i].ext->ents) if(mmcollide(d, dir, *c[i].ext->ents)) return true;
+        ivec o(i, cor, size);
+        if(c[i].children)
+        {
+            if(octacollide(d, dir, cutoff, bo, bs, c[i].children, o, size>>1)) return true;
+        }
+        else
+        {
+            bool solid = false;
+            switch(c[i].material&MATF_CLIP)
+            {
+                case MAT_NOCLIP: continue;
+                case MAT_GAMECLIP: if(d->type==ENT_AI) solid = true; break;
+                case MAT_CLIP: if(isclipped(c[i].material&MATF_VOLUME) || d->type<ENT_CAMERA) solid = true; break;
+            }
+            if(!solid && isempty(c[i])) continue;
+            if(cubecollide(d, dir, cutoff, c[i], o, size, solid)) return true;
+        }
+    }
+    return false;
+}
+
+static inline bool octacollide(physent *d, const vec &dir, float cutoff, const ivec &bo, const ivec &bs)
+{
+    int diff = (bo.x^bs.x) | (bo.y^bs.y) | (bo.z^bs.z),
+        scale = worldscale-1;
+    if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|bs.x|bs.y|bs.z) >= uint(worldsize))
+       return octacollide(d, dir, cutoff, bo, bs, worldroot, ivec(0, 0, 0), worldsize>>1);
+    const cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
+    if(c->ext && c->ext->ents && mmcollide(d, dir, *c->ext->ents)) return true;
+    scale--;
+    while(c->children && !(diff&(1<<scale)))
+    {
+        c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
+        if(c->ext && c->ext->ents && mmcollide(d, dir, *c->ext->ents)) return true;
+        scale--;
+    }
+    if(c->children) return octacollide(d, dir, cutoff, bo, bs, c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale);
+    bool solid = false;
+    switch(c->material&MATF_CLIP)
+    {
+        case MAT_NOCLIP: return false;
+        case MAT_GAMECLIP: if(d->type==ENT_AI) solid = true; break;
+        case MAT_CLIP: if(isclipped(c->material&MATF_VOLUME) || d->type<ENT_CAMERA) solid = true; break;
+    }
+    if(!solid && isempty(*c)) return false;
+    int csize = 2<<scale, cmask = ~(csize-1);
+    return cubecollide(d, dir, cutoff, *c, ivec(bo).mask(cmask), csize, solid);
+}
+
+// all collision happens here
+bool collide(physent *d, const vec &dir, float cutoff, bool playercol, bool insideplayercol)
+{
+    collideinside = 0;
+    collideplayer = NULL;
+    collidewall = vec(0, 0, 0);
+    ivec bo(int(d->o.x-d->radius), int(d->o.y-d->radius), int(d->o.z-d->eyeheight)),
+         bs(int(d->o.x+d->radius), int(d->o.y+d->radius), int(d->o.z+d->aboveeye));
+    bs.add(1);  // guard space for rounding errors
+    return octacollide(d, dir, cutoff, bo, bs) || (playercol && plcollide(d, dir, insideplayercol));
+}
+
+void recalcdir(physent *d, const vec &oldvel, vec &dir)
+{
+    float speed = oldvel.magnitude();
+    if(speed > 1e-6f)
+    {
+        float step = dir.magnitude();
+        dir = d->vel;
+        dir.add(d->falling);
+        dir.mul(step/speed);
+    }
+}
+
+void slideagainst(physent *d, vec &dir, const vec &obstacle, bool foundfloor, bool slidecollide)
+{
+    vec wall(obstacle);
+    if(foundfloor ? wall.z > 0 : slidecollide)
+    {
+        wall.z = 0;
+        if(!wall.iszero()) wall.normalize();
+    }
+    vec oldvel(d->vel);
+    oldvel.add(d->falling);
+    d->vel.project(wall);
+    d->falling.project(wall);
+    recalcdir(d, oldvel, dir);
+}
+
+void switchfloor(physent *d, vec &dir, const vec &floor)
+{
+    if(floor.z >= FLOORZ) d->falling = vec(0, 0, 0);
+
+    vec oldvel(d->vel);
+    oldvel.add(d->falling);
+    if(dir.dot(floor) >= 0)
+    {
+        if(d->physstate < PHYS_SLIDE || fabs(dir.dot(d->floor)) > 0.01f*dir.magnitude()) return;
+        d->vel.projectxy(floor, 0.0f);
+    }
+    else d->vel.projectxy(floor);
+    d->falling.project(floor);
+    recalcdir(d, oldvel, dir);
+}
+
+bool trystepup(physent *d, vec &dir, const vec &obstacle, float maxstep, const vec &floor)
+{
+    vec old(d->o), stairdir = (obstacle.z >= 0 && obstacle.z < SLOPEZ ? vec(-obstacle.x, -obstacle.y, 0) : vec(dir.x, dir.y, 0)).rescale(1);
+    bool cansmooth = true;
+    /* check if there is space atop the stair to move to */
+    if(d->physstate != PHYS_STEP_UP)
+    {
+        vec checkdir = stairdir;
+        checkdir.mul(0.1f);
+        checkdir.z += maxstep + 0.1f;
+        d->o.add(checkdir);
+        if(collide(d))
+        {
+            d->o = old;
+            if(!collide(d, vec(0, 0, -1), SLOPEZ)) return false;
+            cansmooth = false;
+        }
+    }
+
+    if(cansmooth)
+    {
+        vec checkdir = stairdir;
+        checkdir.z += 1;
+        checkdir.mul(maxstep);
+        d->o = old;
+        d->o.add(checkdir);
+        int scale = 2;
+        if(collide(d, checkdir))
+        {
+            if(!collide(d, vec(0, 0, -1), SLOPEZ))
+            {
+                d->o = old;
+                return false;
+            }
+            d->o.add(checkdir);
+            if(collide(d, vec(0, 0, -1), SLOPEZ)) scale = 1;
+        }
+        if(scale != 1)
+        {
+            d->o = old;
+            d->o.sub(checkdir.mul(vec(2, 2, 1)));
+            if(!collide(d, vec(0, 0, -1), SLOPEZ)) scale = 1;
+        }
+
+        d->o = old;
+        vec smoothdir(dir.x, dir.y, 0);
+        float magxy = smoothdir.magnitude();
+        if(magxy > 1e-9f)
+        {
+            if(magxy > scale*dir.z)
+            {
+                smoothdir.mul(1/magxy);
+                smoothdir.z = 1.0f/scale;
+                smoothdir.mul(dir.magnitude()/smoothdir.magnitude());
+            }
+            else smoothdir.z = dir.z;
+            d->o.add(smoothdir);
+            d->o.z += maxstep + 0.1f;
+            if(!collide(d, smoothdir))
+            {
+                d->o.z -= maxstep + 0.1f;
+                if(d->physstate == PHYS_FALL || d->floor != floor)
+                {
+                    d->timeinair = 0;
+                    d->floor = floor;
+                    switchfloor(d, dir, d->floor);
+                }
+                d->physstate = PHYS_STEP_UP;
+                return true;
+            }
+        }
+    }
+
+    /* try stepping up */
+    d->o = old;
+    d->o.z += dir.magnitude();
+    if(!collide(d, vec(0, 0, 1)))
+    {
+        if(d->physstate == PHYS_FALL || d->floor != floor)
+        {
+            d->timeinair = 0;
+            d->floor = floor;
+            switchfloor(d, dir, d->floor);
+        }
+        if(cansmooth) d->physstate = PHYS_STEP_UP;
+        return true;
+    }
+    d->o = old;
+    return false;
+}
+
+bool trystepdown(physent *d, vec &dir, float step, float xy, float z, bool init = false)
+{
+    vec stepdir(dir.x, dir.y, 0);
+    stepdir.z = -stepdir.magnitude2()*z/xy;
+    if(!stepdir.z) return false;
+    stepdir.normalize();
+
+    vec old(d->o);
+    d->o.add(vec(stepdir).mul(STAIRHEIGHT/fabs(stepdir.z))).z -= STAIRHEIGHT;
+    d->zmargin = -STAIRHEIGHT;
+    if(collide(d, vec(0, 0, -1), SLOPEZ))
+    {
+        d->o = old;
+        d->o.add(vec(stepdir).mul(step));
+        d->zmargin = 0;
+        if(!collide(d, vec(0, 0, -1)))
+        {
+            vec stepfloor(stepdir);
+            stepfloor.mul(-stepfloor.z).z += 1;
+            stepfloor.normalize();
+            if(d->physstate >= PHYS_SLOPE && d->floor != stepfloor)
+            {
+                // prevent alternating step-down/step-up states if player would keep bumping into the same floor 
+                vec stepped(d->o);
+                d->o.z -= 0.5f;
+                d->zmargin = -0.5f;
+                if(collide(d, stepdir) && collidewall == d->floor)
+                {
+                    d->o = old;
+                    if(!init) { d->o.x += dir.x; d->o.y += dir.y; if(dir.z <= 0 || collide(d, dir)) d->o.z += dir.z; }
+                    d->zmargin = 0;
+                    d->physstate = PHYS_STEP_DOWN;
+                    d->timeinair = 0;
+                    return true;
+                }
+                d->o = init ? old : stepped;
+                d->zmargin = 0;
+            }
+            else if(init) d->o = old;
+            switchfloor(d, dir, stepfloor);
+            d->floor = stepfloor;
+            d->physstate = PHYS_STEP_DOWN;
+            d->timeinair = 0;
+            return true;
+        }
+    }
+    d->o = old;
+    d->zmargin = 0;
+    return false;
+}
+
+bool trystepdown(physent *d, vec &dir, bool init = false)
+{
+    if((!d->move && !d->strafe) || !game::allowmove(d)) return false;
+    vec old(d->o);
+    d->o.z -= STAIRHEIGHT;
+    d->zmargin = -STAIRHEIGHT;
+    if(!collide(d, vec(0, 0, -1), SLOPEZ))
+    {
+        d->o = old;
+        d->zmargin = 0;
+        return false;
+    }
+    d->o = old;
+    d->zmargin = 0;
+    float step = dir.magnitude();
+#if 1
+    // weaker check, just enough to avoid hopping up slopes
+    if(trystepdown(d, dir, step, 4, 1, init)) return true;
+#else
+    if(trystepdown(d, dir, step, 2, 1, init)) return true;
+    if(trystepdown(d, dir, step, 1, 1, init)) return true;
+    if(trystepdown(d, dir, step, 1, 2, init)) return true;
+#endif
+    return false;
+}
+
+void falling(physent *d, vec &dir, const vec &floor)
+{
+    if(floor.z > 0.0f && floor.z < SLOPEZ)
+    {
+        if(floor.z >= WALLZ) switchfloor(d, dir, floor);
+        d->timeinair = 0;
+        d->physstate = PHYS_SLIDE;
+        d->floor = floor;
+    }
+    else if(d->physstate < PHYS_SLOPE || dir.dot(d->floor) > 0.01f*dir.magnitude() || (floor.z != 0.0f && floor.z != 1.0f) || !trystepdown(d, dir, true))
+        d->physstate = PHYS_FALL;
+}
+
+void landing(physent *d, vec &dir, const vec &floor, bool collided)
+{
+#if 0
+    if(d->physstate == PHYS_FALL)
+    {
+        d->timeinair = 0;
+        if(dir.z < 0.0f) dir.z = d->vel.z = 0.0f;
+    }
+#endif
+    switchfloor(d, dir, floor);
+    d->timeinair = 0;
+    if((d->physstate!=PHYS_STEP_UP && d->physstate!=PHYS_STEP_DOWN) || !collided)
+        d->physstate = floor.z >= FLOORZ ? PHYS_FLOOR : PHYS_SLOPE;
+    d->floor = floor;
+}
+
+bool findfloor(physent *d, bool collided, const vec &obstacle, bool &slide, vec &floor)
+{
+    bool found = false;
+    vec moved(d->o);
+    d->o.z -= 0.1f;
+    if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? SLOPEZ : FLOORZ))
+    {
+        floor = collidewall;
+        found = true;
+    }
+    else if(collided && obstacle.z >= SLOPEZ)
+    {
+        floor = obstacle;
+        found = true;
+        slide = false;
+    }
+    else if(d->physstate == PHYS_STEP_UP || d->physstate == PHYS_SLIDE)
+    {
+        if(collide(d, vec(0, 0, -1)) && collidewall.z > 0.0f)
+        {
+            floor = collidewall;
+            if(floor.z >= SLOPEZ) found = true;
+        }
+    }
+    else if(d->physstate >= PHYS_SLOPE && d->floor.z < 1.0f)
+    {
+        if(collide(d, vec(d->floor).neg(), 0.95f) || collide(d, vec(0, 0, -1)))
+        {
+            floor = collidewall;
+            if(floor.z >= SLOPEZ && floor.z < 1.0f) found = true;
+        }
+    }
+    if(collided && (!found || obstacle.z > floor.z))
+    {
+        floor = obstacle;
+        slide = !found && (floor.z < WALLZ || floor.z >= SLOPEZ);
+    }
+    d->o = moved;
+    return found;
+}
+
+bool move(physent *d, vec &dir)
+{
+    vec old(d->o);
+    bool collided = false, slidecollide = false;
+    vec obstacle;
+    d->o.add(dir);
+    if(collide(d, dir) || ((d->type==ENT_AI || d->type==ENT_INANIMATE) && collide(d, vec(0, 0, 0), 0, false)))
+    {
+        obstacle = collidewall;
+        /* check to see if there is an obstacle that would prevent this one from being used as a floor (or ceiling bump) */
+        if(d->type==ENT_PLAYER && ((collidewall.z>=SLOPEZ && dir.z<0) || (collidewall.z<=-SLOPEZ && dir.z>0)) && (dir.x || dir.y) && collide(d, vec(dir.x, dir.y, 0)))
+        {
+            if(collidewall.dot(dir) >= 0) slidecollide = true;
+            obstacle = collidewall;
+        }
+        d->o = old;
+        d->o.z -= STAIRHEIGHT;
+        d->zmargin = -STAIRHEIGHT;
+        if(d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR || (collide(d, vec(0, 0, -1), SLOPEZ) && (d->physstate==PHYS_STEP_UP || d->physstate==PHYS_STEP_DOWN || collidewall.z>=FLOORZ)))
+        {
+            d->o = old;
+            d->zmargin = 0;
+            if(trystepup(d, dir, obstacle, STAIRHEIGHT, d->physstate == PHYS_SLOPE || d->physstate == PHYS_FLOOR ? d->floor : vec(collidewall))) return true;
+        }
+        else
+        {
+            d->o = old;
+            d->zmargin = 0;
+        }
+        /* can't step over the obstacle, so just slide against it */
+        collided = true;
+    }
+    else if(d->physstate == PHYS_STEP_UP)
+    {
+        if(collide(d, vec(0, 0, -1), SLOPEZ))
+        {
+            d->o = old;
+            if(trystepup(d, dir, vec(0, 0, 1), STAIRHEIGHT, vec(collidewall))) return true;
+            d->o.add(dir);
+        }
+    }
+    else if(d->physstate == PHYS_STEP_DOWN && dir.dot(d->floor) <= 1e-6f)
+    {
+        vec moved(d->o);
+        d->o = old;
+        if(trystepdown(d, dir)) return true;
+        d->o = moved;
+    }
+    vec floor(0, 0, 0);
+    bool slide = collided,
+         found = findfloor(d, collided, obstacle, slide, floor);
+    if(slide || (!collided && floor.z > 0 && floor.z < WALLZ))
+    {
+        slideagainst(d, dir, slide ? obstacle : floor, found, slidecollide);
+        //if(d->type == ENT_AI || d->type == ENT_INANIMATE)
+        d->blocked = true;
+    }
+    if(found) landing(d, dir, floor, collided);
+    else falling(d, dir, floor);
+    return !collided;
+}
+
+bool bounce(physent *d, float secs, float elasticity, float waterfric, float grav)
+{
+    // make sure bouncers don't start inside geometry
+    if(d->physstate!=PHYS_BOUNCE && collide(d, vec(0, 0, 0), 0, false)) return true;
+    int mat = lookupmaterial(vec(d->o.x, d->o.y, d->o.z + (d->aboveeye - d->eyeheight)/2));
+    bool water = isliquid(mat);
+    if(water)
+    {
+        d->vel.z -= grav*GRAVITY/16*secs;
+        d->vel.mul(max(1.0f - secs/waterfric, 0.0f));
+    }
+    else d->vel.z -= grav*GRAVITY*secs;
+    vec old(d->o);
+    loopi(2)
+    {
+        vec dir(d->vel);
+        dir.mul(secs);
+        d->o.add(dir);
+        if(!collide(d, dir, 0, true, true))
+        {
+            if(collideinside)
+            {
+                d->o = old;
+                d->vel.mul(-elasticity);
+            }
+            break;
+        }
+        else if(collideplayer) break;
+        d->o = old;
+        game::bounced(d, collidewall);
+        float c = collidewall.dot(d->vel),
+              k = 1.0f + (1.0f-elasticity)*c/d->vel.magnitude();
+        d->vel.mul(k);
+        d->vel.sub(vec(collidewall).mul(elasticity*2.0f*c));
+    }
+    if(d->physstate!=PHYS_BOUNCE)
+    {
+        // make sure bouncers don't start inside geometry
+        if(d->o == old) return !collideplayer;
+        d->physstate = PHYS_BOUNCE;
+    }
+    return collideplayer!=NULL;
+}
+
+void avoidcollision(physent *d, const vec &dir, physent *obstacle, float space)
+{
+    float rad = obstacle->radius+d->radius;
+    vec bbmin(obstacle->o);
+    bbmin.x -= rad;
+    bbmin.y -= rad;
+    bbmin.z -= obstacle->eyeheight+d->aboveeye;
+    bbmin.sub(space);
+    vec bbmax(obstacle->o);
+    bbmax.x += rad;
+    bbmax.y += rad;
+    bbmax.z += obstacle->aboveeye+d->eyeheight;
+    bbmax.add(space);
+
+    loopi(3) if(d->o[i] <= bbmin[i] || d->o[i] >= bbmax[i]) return;
+
+    float mindist = 1e16f;
+    loopi(3) if(dir[i] != 0)
+    {
+        float dist = ((dir[i] > 0 ? bbmax[i] : bbmin[i]) - d->o[i]) / dir[i];
+        mindist = min(mindist, dist);
+    }
+    if(mindist >= 0.0f && mindist < 1e15f) d->o.add(vec(dir).mul(mindist));
+}
+
+bool movecamera(physent *pl, const vec &dir, float dist, float stepdist)
+{
+    int steps = (int)ceil(dist/stepdist);
+    if(steps <= 0) return true;
+
+    vec d(dir);
+    d.mul(dist/steps);
+    loopi(steps)
+    {
+        vec oldpos(pl->o);
+        pl->o.add(d);
+        if(collide(pl, vec(0, 0, 0), 0, false))
+        {
+            pl->o = oldpos;
+            return false;
+        }
+    }
+    return true;
+}
+
+bool droptofloor(vec &o, float radius, float height)
+{
+    static struct dropent : physent
+    {
+        dropent() 
+        { 
+            type = ENT_BOUNCE; 
+            vel = vec(0, 0, -1);
+        }
+    } d;
+    d.o = o;
+    if(!insideworld(d.o)) 
+    {
+        if(d.o.z < worldsize) return false;
+        d.o.z = worldsize - 1e-3f;
+        if(!insideworld(d.o)) return false;
+    }
+    vec v(0.0001f, 0.0001f, -1);
+    v.normalize();
+    if(raycube(d.o, v, worldsize) >= worldsize) return false;
+    d.radius = d.xradius = d.yradius = radius;
+    d.eyeheight = height;
+    d.aboveeye = radius;
+    if(!movecamera(&d, d.vel, worldsize, 1))
+    {
+        o = d.o;
+        return true;
+    }
+    return false;
+}
+
+float dropheight(entity &e)
+{
+    switch(e.type)
+    {
+        case ET_PARTICLES:
+        case ET_MAPMODEL: return 0.0f;
+        default:
+            if(e.type >= ET_GAMESPECIFIC) return entities::dropheight(e);
+            return 4.0f;
+    }
+}
+
+void dropenttofloor(entity *e)
+{
+    droptofloor(e->o, 1.0f, dropheight(*e));
+}
+
+void phystest()
+{
+    static const char * const states[] = {"float", "fall", "slide", "slope", "floor", "step up", "step down", "bounce"};
+    printf ("PHYS(pl): %s, air %d, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)\n", states[player->physstate], player->timeinair, player->floor.x, player->floor.y, player->floor.z, player->vel.x, player->vel.y, player->vel.z, player->falling.x, player->falling.y, player->falling.z);
+    printf ("PHYS(cam): %s, air %d, floor: (%f, %f, %f), vel: (%f, %f, %f), g: (%f, %f, %f)\n", states[camera1->physstate], camera1->timeinair, camera1->floor.x, camera1->floor.y, camera1->floor.z, camera1->vel.x, camera1->vel.y, camera1->vel.z, camera1->falling.x, camera1->falling.y, camera1->falling.z);
+}
+
+COMMAND(phystest, "");
+
+void vecfromyawpitch(float yaw, float pitch, int move, int strafe, vec &m)
+{
+    if(move)
+    {
+        m.x = move*-sinf(RAD*yaw);
+        m.y = move*cosf(RAD*yaw);
+    }
+    else m.x = m.y = 0;
+
+    if(pitch)
+    {
+        m.x *= cosf(RAD*pitch);
+        m.y *= cosf(RAD*pitch);
+        m.z = move*sinf(RAD*pitch);
+    }
+    else m.z = 0;
+
+    if(strafe)
+    {
+        m.x += strafe*cosf(RAD*yaw);
+        m.y += strafe*sinf(RAD*yaw);
+    }
+}
+
+void vectoyawpitch(const vec &v, float &yaw, float &pitch)
+{
+    if(v.iszero()) yaw = pitch = 0;
+    else
+    {
+        yaw = -atan2(v.x, v.y)/RAD;
+        pitch = asin(v.z/v.magnitude())/RAD;
+    }
+}
+
+#define PHYSFRAMETIME 5
+
+VARP(maxroll, 0, 0, 20);
+FVAR(straferoll, 0, 0.033f, 90);
+FVAR(faderoll, 0, 0.95f, 1);
+VAR(floatspeed, 1, 100, 10000);
+
+void modifyvelocity(physent *pl, bool local, bool water, bool floating, int curtime)
+{
+    bool allowmove = game::allowmove(pl);
+    if(floating)
+    {
+        if(pl->jumping && allowmove)
+        {
+            pl->jumping = false;
+            pl->vel.z = max(pl->vel.z, JUMPVEL);
+        }
+    }
+    else if(pl->physstate >= PHYS_SLOPE || water)
+    {
+        if(water && !pl->inwater) pl->vel.div(8);
+        if(pl->jumping && allowmove)
+        {
+            pl->jumping = false;
+
+            pl->vel.z = max(pl->vel.z, JUMPVEL); // physics impulse upwards
+            if(water) { pl->vel.x /= 8.0f; pl->vel.y /= 8.0f; } // dampen velocity change even harder, gives correct water feel
+
+            game::physicstrigger(pl, local, 1, 0);
+        }
+    }
+    if(!floating && pl->physstate == PHYS_FALL) pl->timeinair = min(pl->timeinair + curtime, 1000);
+
+    vec m(0.0f, 0.0f, 0.0f);
+    if((pl->move || pl->strafe) && allowmove)
+    {
+        vecfromyawpitch(pl->yaw, floating || water || pl->type==ENT_CAMERA ? pl->pitch : 0, pl->move, pl->strafe, m);
+
+        if(!floating && pl->physstate >= PHYS_SLOPE)
+        {
+            /* move up or down slopes in air
+             * but only move up slopes in water
+             */
+            float dz = -(m.x*pl->floor.x + m.y*pl->floor.y)/pl->floor.z;
+            m.z = water ? max(m.z, dz) : dz;
+        }
+
+        m.normalize();
+    }
+
+    vec d(m);
+    d.mul(pl->maxspeed);
+    if(pl->type==ENT_PLAYER)
+    {
+        if(floating)
+        {
+            if(pl==player) d.mul(floatspeed/100.0f);
+        }
+        else if(!water && allowmove) d.mul((pl->move && !pl->strafe ? 1.3f : 1.0f) * (pl->physstate < PHYS_SLOPE ? 1.3f : 1.0f));
+    }
+    float fric = water && !floating ? 20.0f : (pl->physstate >= PHYS_SLOPE || floating ? 6.0f : 30.0f);
+    pl->vel.lerp(d, pl->vel, pow(1 - 1/fric, curtime/20.0f));
+// old fps friction
+//    float friction = water && !floating ? 20.0f : (pl->physstate >= PHYS_SLOPE || floating ? 6.0f : 30.0f);
+//    float fpsfric = min(curtime/(20.0f*friction), 1.0f);
+//    pl->vel.lerp(pl->vel, d, fpsfric);
+}
+
+void modifygravity(physent *pl, bool water, int curtime)
+{
+    float secs = curtime/1000.0f;
+    vec g(0, 0, 0);
+    if(pl->physstate == PHYS_FALL) g.z -= GRAVITY*secs;
+    else if(pl->floor.z > 0 && pl->floor.z < FLOORZ)
+    {
+        g.z = -1;
+        g.project(pl->floor);
+        g.normalize();
+        g.mul(GRAVITY*secs);
+    }
+    if(!water || !game::allowmove(pl) || (!pl->move && !pl->strafe)) pl->falling.add(g);
+
+    if(water || pl->physstate >= PHYS_SLOPE)
+    {
+        float fric = water ? 2.0f : 6.0f,
+              c = water ? 1.0f : clamp((pl->floor.z - SLOPEZ)/(FLOORZ-SLOPEZ), 0.0f, 1.0f);
+        pl->falling.mul(pow(1 - c/fric, curtime/20.0f));
+// old fps friction
+//        float friction = water ? 2.0f : 6.0f,
+//              fpsfric = friction/curtime*20.0f,
+//              c = water ? 1.0f : clamp((pl->floor.z - SLOPEZ)/(FLOORZ-SLOPEZ), 0.0f, 1.0f);
+//        pl->falling.mul(1 - c/fpsfric);
+    }
+}
+
+// main physics routine, moves a player/monster for a curtime step
+// moveres indicated the physics precision (which is lower for monsters and multiplayer prediction)
+// local is false for multiplayer prediction
+
+bool moveplayer(physent *pl, int moveres, bool local, int curtime)
+{
+    int material = lookupmaterial(vec(pl->o.x, pl->o.y, pl->o.z + (3*pl->aboveeye - pl->eyeheight)/4));
+    bool water = isliquid(material&MATF_VOLUME);
+    bool floating = pl->type==ENT_PLAYER && (pl->state==CS_EDITING || pl->state==CS_SPECTATOR);
+    float secs = curtime/1000.f;
+
+    // apply gravity
+    if(!floating) modifygravity(pl, water, curtime);
+    // apply any player generated changes in velocity
+    modifyvelocity(pl, local, water, floating, curtime);
+
+    vec d(pl->vel);
+    if(!floating && water) d.mul(0.5f);
+    d.add(pl->falling);
+    d.mul(secs);
+
+    pl->blocked = false;
+
+    if(floating)                // just apply velocity
+    {
+        if(pl->physstate != PHYS_FLOAT)
+        {
+            pl->physstate = PHYS_FLOAT;
+            pl->timeinair = 0;
+            pl->falling = vec(0, 0, 0);
+        }
+        pl->o.add(d);
+    }
+    else                        // apply velocity with collision
+    {
+        const float f = 1.0f/moveres;
+        const int timeinair = pl->timeinair;
+        int collisions = 0;
+
+        d.mul(f);
+        loopi(moveres) if(!move(pl, d) && ++collisions<5) i--; // discrete steps collision detection & sliding
+        if(timeinair > 800 && !pl->timeinair && !water) // if we land after long time must have been a high jump, make thud sound
+        {
+            game::physicstrigger(pl, local, -1, 0);
+        }
+    }
+
+    if(pl->state==CS_ALIVE) updatedynentcache(pl);
+
+    // automatically apply smooth roll when strafing
+
+    if(pl->strafe && maxroll) pl->roll = clamp(pl->roll - pow(clamp(1.0f + pl->strafe*pl->roll/maxroll, 0.0f, 1.0f), 0.33f)*pl->strafe*curtime*straferoll, -maxroll, maxroll);
+    else pl->roll *= curtime == PHYSFRAMETIME ? faderoll : pow(faderoll, curtime/float(PHYSFRAMETIME));
+
+    // play sounds on water transitions
+
+    if(pl->inwater && !water)
+    {
+        material = lookupmaterial(vec(pl->o.x, pl->o.y, pl->o.z + (pl->aboveeye - pl->eyeheight)/2));
+        water = isliquid(material&MATF_VOLUME);
+    }
+    if(!pl->inwater && water) game::physicstrigger(pl, local, 0, -1, material&MATF_VOLUME);
+    else if(pl->inwater && !water) game::physicstrigger(pl, local, 0, 1, pl->inwater);
+    pl->inwater = water ? material&MATF_VOLUME : MAT_AIR;
+
+    if(pl->state==CS_ALIVE && (pl->o.z < 0 || material&MAT_DEATH)) game::suicide(pl);
+
+    return true;
+}
+
+int physsteps = 0, physframetime = PHYSFRAMETIME, lastphysframe = 0;
+
+void physicsframe()          // optimally schedule physics frames inside the graphics frames
+{
+    int diff = lastmillis - lastphysframe;
+    if(diff <= 0) physsteps = 0;
+    else
+    {
+        physframetime = clamp(game::scaletime(PHYSFRAMETIME)/100, 1, PHYSFRAMETIME);
+        physsteps = (diff + physframetime - 1)/physframetime;
+        lastphysframe += physsteps * physframetime;
+    }
+    cleardynentcache();
+}
+
+VAR(physinterp, 0, 1, 1);
+
+void interppos(physent *pl)
+{
+    pl->o = pl->newpos;
+
+    int diff = lastphysframe - lastmillis;
+    if(diff <= 0 || !physinterp) return;
+
+    vec deltapos(pl->deltapos);
+    deltapos.mul(min(diff, physframetime)/float(physframetime));
+    pl->o.add(deltapos);
+}
+
+void moveplayer(physent *pl, int moveres, bool local)
+{
+    if(physsteps <= 0)
+    {
+        if(local) interppos(pl);
+        return;
+    }
+
+    if(local) pl->o = pl->newpos;
+    loopi(physsteps-1) moveplayer(pl, moveres, local, physframetime);
+    if(local) pl->deltapos = pl->o;
+    moveplayer(pl, moveres, local, physframetime);
+    if(local)
+    {
+        pl->newpos = pl->o;
+        pl->deltapos.sub(pl->newpos);
+        interppos(pl);
+    }
+}
+
+bool bounce(physent *d, float elasticity, float waterfric, float grav)
+{
+    if(physsteps <= 0)
+    {
+        interppos(d);
+        return false;
+    }
+
+    d->o = d->newpos;
+    bool hitplayer = false;
+    loopi(physsteps-1)
+    {
+        if(bounce(d, physframetime/1000.0f, elasticity, waterfric, grav)) hitplayer = true;
+    }
+    d->deltapos = d->o;
+    if(bounce(d, physframetime/1000.0f, elasticity, waterfric, grav)) hitplayer = true;
+    d->newpos = d->o;
+    d->deltapos.sub(d->newpos);
+    interppos(d);
+    return hitplayer;
+}
+
+void updatephysstate(physent *d)
+{
+    if(d->physstate == PHYS_FALL) return;
+    d->timeinair = 0;
+    vec old(d->o);
+    /* Attempt to reconstruct the floor state.
+     * May be inaccurate since movement collisions are not considered.
+     * If good floor is not found, just keep the old floor and hope it's correct enough.
+     */
+    switch(d->physstate)
+    {
+        case PHYS_SLOPE:
+        case PHYS_FLOOR:
+        case PHYS_STEP_DOWN:
+            d->o.z -= 0.15f;
+            if(collide(d, vec(0, 0, -1), d->physstate == PHYS_SLOPE || d->physstate == PHYS_STEP_DOWN ? SLOPEZ : FLOORZ))
+                d->floor = collidewall;
+            break;
+
+        case PHYS_STEP_UP:
+            d->o.z -= STAIRHEIGHT+0.15f;
+            if(collide(d, vec(0, 0, -1), SLOPEZ))
+                d->floor = collidewall;
+            break;
+
+        case PHYS_SLIDE:
+            d->o.z -= 0.15f;
+            if(collide(d, vec(0, 0, -1)) && collidewall.z < SLOPEZ)
+                d->floor = collidewall;
+            break;
+    }
+    if(d->physstate > PHYS_FALL && d->floor.z <= 0) d->floor = vec(0, 0, 1);
+    d->o = old;
+}
+
+const float PLATFORMMARGIN = 0.2f;
+const float PLATFORMBORDER = 10.0f;
+
+struct platforment
+{
+    physent *d;
+    int stacks, chains;
+
+    platforment() {}
+    platforment(physent *d) : d(d), stacks(-1), chains(-1) {}
+
+    bool operator==(const physent *o) const { return d == o; }
+};
+
+struct platformcollision
+{
+    platforment *ent;
+    int next;
+
+    platformcollision() {}
+    platformcollision(platforment *ent, int next) : ent(ent), next(next) {}
+};
+
+template<class E, class O>
+static inline bool platformcollide(physent *d, const vec &dir, physent *o, float margin)
+{
+    E entvol(d);
+    O obvol(o, margin);
+    vec cp;
+    if(mpr::collide(entvol, obvol, NULL, NULL, &cp))
+    {
+        vec wn = vec(cp).sub(obvol.center());
+        return !obvol.contactface(wn, dir.iszero() ? vec(wn).neg() : dir).iszero();
+    }
+    return false;
+}
+
+bool platformcollide(physent *d, physent *o, const vec &dir, float margin = 0)
+{
+    if(d->collidetype == COLLIDE_OBB)
+    {
+        if(o->collidetype == COLLIDE_OBB) return platformcollide<mpr::EntOBB, mpr::EntOBB>(d, dir, o, margin);
+        else return platformcollide<mpr::EntOBB, mpr::EntCylinder>(d, dir, o, margin);
+
+    }
+    else if(o->collidetype == COLLIDE_OBB) return ellipseboxcollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight + margin);
+    else return ellipsecollide(d, dir, o->o, vec(0, 0, 0), o->yaw, o->xradius, o->yradius, o->aboveeye, o->eyeheight + margin);
+}
+
+bool moveplatform(physent *p, const vec &dir)
+{
+    if(!insideworld(p->newpos)) return false;
+
+    vec oldpos(p->o);
+    (p->o = p->newpos).add(dir);
+    if(collide(p, dir, 0, dir.z<=0))
+    {
+        p->o = oldpos;
+        return false;
+    }
+    p->o = oldpos;
+
+    static vector<platforment> ents;
+    ents.setsize(0);
+    for(int x = int(max(p->o.x-p->radius-PLATFORMBORDER, 0.0f))>>dynentsize, ex = int(min(p->o.x+p->radius+PLATFORMBORDER, worldsize-1.0f))>>dynentsize; x <= ex; x++)
+    for(int y = int(max(p->o.y-p->radius-PLATFORMBORDER, 0.0f))>>dynentsize, ey = int(min(p->o.y+p->radius+PLATFORMBORDER, worldsize-1.0f))>>dynentsize; y <= ey; y++)
+    {
+        const vector<physent *> &dynents = checkdynentcache(x, y);
+        loopv(dynents)
+        {
+            physent *d = dynents[i];
+            if(p==d || d->o.z-d->eyeheight < p->o.z+p->aboveeye || p->o.reject(d->o, p->radius+PLATFORMBORDER+d->radius) || ents.find(d) >= 0) continue;
+            ents.add(d);
+        }
+    }
+    static vector<platforment *> passengers, colliders;
+    passengers.setsize(0);
+    colliders.setsize(0);
+    static vector<platformcollision> collisions;
+    collisions.setsize(0);
+    // build up collision DAG of colliders to be pushed off, and DAG of stacked passengers
+    loopv(ents)
+    {
+        platforment &ent = ents[i];
+        physent *d = ent.d;
+        // check if the dynent is on top of the platform
+        if(platformcollide(p, d, vec(0, 0, 1), PLATFORMMARGIN)) passengers.add(&ent);
+        vec doldpos(d->o);
+        (d->o = d->newpos).add(dir);
+        if(collide(d, dir, 0, false)) colliders.add(&ent);
+        d->o = doldpos;
+        loopvj(ents)
+        {
+            platforment &o = ents[j];
+            if(platformcollide(d, o.d, dir))
+            {
+                collisions.add(platformcollision(&ent, o.chains));
+                o.chains = collisions.length() - 1;
+            }
+            if(d->o.z < o.d->o.z && platformcollide(d, o.d, vec(0, 0, 1), PLATFORMMARGIN))
+            {
+                collisions.add(platformcollision(&o, ent.stacks));
+                ent.stacks = collisions.length() - 1;
+            }
+        }
+    }
+    loopv(colliders) // propagate collisions
+    {
+        platforment *ent = colliders[i];
+        for(int n = ent->chains; n>=0; n = collisions[n].next)
+        {
+            platforment *o = collisions[n].ent;
+            if(colliders.find(o)<0) colliders.add(o);
+        }
+    }
+    if(dir.z>0)
+    {
+        loopv(passengers) // if any stacked passengers collide, stop the platform
+        {
+            platforment *ent = passengers[i];
+            if(colliders.find(ent)>=0) return false;
+            for(int n = ent->stacks; n>=0; n = collisions[n].next)
+            {
+                platforment *o = collisions[n].ent;
+                if(passengers.find(o)<0) passengers.add(o);
+            }
+        }
+        loopv(passengers)
+        {
+            physent *d = passengers[i]->d;
+            d->o.add(dir);
+            d->newpos.add(dir);
+            if(dir.x || dir.y) updatedynentcache(d);
+        }
+    }
+    else loopv(passengers) // move any stacked passengers who aren't colliding with non-passengers
+    {
+        platforment *ent = passengers[i];
+        if(colliders.find(ent)>=0) continue;
+
+        physent *d = ent->d;
+        d->o.add(dir);
+        d->newpos.add(dir);
+        if(dir.x || dir.y) updatedynentcache(d);
+
+        for(int n = ent->stacks; n>=0; n = collisions[n].next)
+        {
+            platforment *o = collisions[n].ent;
+            if(passengers.find(o)<0) passengers.add(o);
+        }
+    }
+
+    p->o.add(dir);
+    p->newpos.add(dir);
+    if(dir.x || dir.y) updatedynentcache(p);
+
+    return true;
+}
+
+#define dir(name,v,d,s,os) ICOMMAND(name, "D", (int *down), { player->s = *down!=0; player->v = player->s ? d : (player->os ? -(d) : 0); });
+
+dir(backward, move,   -1, k_down,  k_up);
+dir(forward,  move,    1, k_up,    k_down);
+dir(left,     strafe,  1, k_left,  k_right);
+dir(right,    strafe, -1, k_right, k_left);
+
+ICOMMAND(jump,   "D", (int *down), { if(!*down || game::canjump()) player->jumping = *down!=0; });
+ICOMMAND(attack, "D", (int *down), { game::doattack(*down!=0); });
+
+bool entinmap(dynent *d, bool avoidplayers)        // brute force but effective way to find a free spawn spot in the map
+{
+    d->o.z += d->eyeheight; // pos specified is at feet
+    vec orig = d->o;
+    loopi(100)              // try max 100 times
+    {
+        if(i)
+        {
+            d->o = orig;
+            d->o.x += (rnd(21)-10)*i/5;  // increasing distance
+            d->o.y += (rnd(21)-10)*i/5;
+            d->o.z += (rnd(21)-10)*i/5;
+        }
+
+        if(!collide(d) && !collideinside)
+        {
+            if(collideplayer)
+            {
+                if(!avoidplayers) continue;
+                d->o = orig;
+                d->resetinterp();
+                return false;
+            }
+
+            d->resetinterp();
+            return true;
+        }
+    }
+    // leave ent at original pos, possibly stuck
+    d->o = orig;
+    d->resetinterp();
+    conoutf(CON_WARN, "can't find entity spawn spot! (%.1f, %.1f, %.1f)", d->o.x, d->o.y, d->o.z);
+    return false;
+}
+
diff --git a/src/engine/pvs.cpp b/src/engine/pvs.cpp
new file mode 100644 (file)
index 0000000..0f29bf5
--- /dev/null
@@ -0,0 +1,1315 @@
+#include "engine.h"
+
+enum
+{
+    PVS_HIDE_GEOM = 1<<0,
+    PVS_HIDE_BB   = 1<<1
+};
+
+struct pvsnode
+{
+    bvec edges;
+    uchar flags;
+    uint children;
+};
+
+static vector<pvsnode> origpvsnodes;
+
+static bool mergepvsnodes(pvsnode &p, pvsnode *children)
+{
+    loopi(7) if(children[i].flags!=children[7].flags) return false;
+    bvec bbs[4];
+    loop(x, 2) loop(y, 2)
+    {
+        const bvec &lo = children[octaindex(2, x, y, 0)].edges,
+                   &hi = children[octaindex(2, x, y, 1)].edges;
+        if(lo.x!=0xFF && (lo.x&0x11 || lo.y&0x11 || lo.z&0x11)) return false;
+        if(hi.x!=0xFF && (hi.x&0x11 || hi.y&0x11 || hi.z&0x11)) return false;
+
+#define MERGEBBS(res, coord, row, col) \
+        if(lo.coord==0xFF) \
+        { \
+            if(hi.coord!=0xFF) \
+            { \
+                res.coord = ((hi.coord&~0x11)>>1) + 0x44; \
+                res.row = hi.row; \
+                res.col = hi.col; \
+            } \
+        } \
+        else if(hi.coord==0xFF) \
+        { \
+            res.coord = (lo.coord&0xEE)>>1; \
+            res.row = lo.row; \
+            res.col = lo.col; \
+        } \
+        else if(lo.row!=hi.row || lo.col!=hi.col || (lo.coord&0xF0)!=0x80 || (hi.coord&0xF)!=0) return false; \
+        else \
+        { \
+            res.coord = ((lo.coord&~0xF1)>>1) | (((hi.coord&~0x1F)>>1) + 0x40); \
+            res.row = lo.row; \
+            res.col = lo.col; \
+        }
+
+        bvec &res = bbs[x + 2*y];
+        MERGEBBS(res, z, x, y);
+        res.x = lo.x;
+        res.y = lo.y;
+    }
+    loop(x, 2)
+    {
+        bvec &lo = bbs[x], &hi = bbs[x+2];
+        MERGEBBS(lo, y, x, z);
+    }
+    bvec &lo = bbs[0], &hi = bbs[1];
+    MERGEBBS(p.edges, x, y, z);
+
+    return true;
+}
+
+static void genpvsnodes(cube *c, int parent = 0, const ivec &co = ivec(0, 0, 0), int size = worldsize/2)
+{
+    int index = origpvsnodes.length();
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        pvsnode &n = origpvsnodes.add();
+        n.flags = 0;
+        n.children = 0;
+        if(c[i].children || isempty(c[i]) || c[i].material&MAT_ALPHA) memset(n.edges.v, 0xFF, 3);
+        else loopk(3)
+        {
+            uint face = c[i].faces[k];
+            if(face==F_SOLID) n.edges[k] = 0x80;
+            else
+            {
+                uchar low = max(max(face&0xF, (face>>8)&0xF), max((face>>16)&0xF, (face>>24)&0xF)),
+                      high = min(min((face>>4)&0xF, (face>>12)&0xF), min((face>>20)&0xF, (face>>28)&0xF));
+                if(size<8)
+                {
+                    if(low&((8/size)-1)) { low += 8/size - (low&((8/size)-1)); }
+                    if(high&((8/size)-1)) high &= ~(8/size-1);
+                }
+                if(low >= high) { memset(n.edges.v, 0xFF, 3); break; }
+                n.edges[k] = low | (high<<4);
+            }
+        }
+    }
+    int branches = 0;
+    loopi(8) if(c[i].children)
+    {
+        ivec o(i, co, size);
+        genpvsnodes(c[i].children, index+i, o, size>>1);
+        if(origpvsnodes[index+i].children) branches++;
+    }
+    if(!branches && mergepvsnodes(origpvsnodes[parent], &origpvsnodes[index])) origpvsnodes.setsize(index);
+    else origpvsnodes[parent].children = index;
+}
+
+struct shaftplane
+{
+    float r, c, offset;
+    uchar rnear, cnear, rfar, cfar;
+};
+
+struct shaftbb
+{
+    union
+    {
+        ushort v[6];
+        struct { usvec min, max; };
+    };
+
+    shaftbb() {}
+    shaftbb(const ivec &o, int size)
+    {
+        min.x = o.x;
+        min.y = o.y;
+        min.z = o.z;
+        max.x = o.x + size;
+        max.y = o.y + size;
+        max.z = o.z + size;
+    }
+    shaftbb(const ivec &o, int size, const bvec &edges)
+    {
+        min.x = o.x + (size*(edges.x&0xF))/8;
+        min.y = o.y + (size*(edges.y&0xF))/8;
+        min.z = o.z + (size*(edges.z&0xF))/8;
+        max.x = o.x + (size*(edges.x>>4))/8;
+        max.y = o.y + (size*(edges.y>>4))/8;
+        max.z = o.z + (size*(edges.z>>4))/8;
+    }
+
+    ushort &operator[](int i) { return v[i]; }
+    ushort operator[](int i) const { return v[i]; }
+
+    bool contains(const shaftbb &o) const
+    {
+        return min.x<=o.min.x && min.y<=o.min.y && min.z<=o.min.z &&
+               max.x>=o.max.x && max.y>=o.max.y && max.z>=o.max.z;
+    }
+
+    bool outside(const ivec &o, int size) const
+    {
+        return o.x>=max.x || o.y>=max.y || o.z>=max.z ||
+               o.x+size<=min.x || o.y+size<=min.y || o.z+size<=min.z;
+    }
+
+    bool outside(const shaftbb &o) const
+    {
+        return o.min.x>max.x || o.min.y>max.y || o.min.z>max.z ||
+               o.max.x<min.x || o.max.y<min.y || o.max.z<min.z;
+    }
+
+    bool notinside(const shaftbb &o) const
+    {
+        return o.min.x<min.x || o.min.y<min.y || o.min.z<min.z ||
+               o.max.x>max.x || o.max.y>max.y || o.max.z>max.z;
+    }
+};
+
+struct shaft
+{
+    shaftbb bounds;
+    shaftplane planes[8];
+    int numplanes;
+
+    shaft(const shaftbb &from, const shaftbb &to)
+    {
+        calcshaft(from, to);
+    }
+
+    void calcshaft(const shaftbb &from, const shaftbb &to)
+    {
+        uchar match = 0, color = 0;
+        loopi(3)
+        {
+            if(to.min[i] < from.min[i]) { color |= 1<<i; bounds.min[i] = 0; }
+            else if(to.min[i] > from.min[i]) bounds.min[i] = to.min[i]+1;
+            else { match |= 1<<i; bounds.min[i] = to.min[i]; }
+
+            if(to.max[i] > from.max[i]) { color |= 8<<i; bounds.max[i] = USHRT_MAX; }
+            else if(to.max[i] < from.max[i]) bounds.max[i] = to.max[i]-1;
+            else { match |= 8<<i; bounds.max[i] = to.max[i]; }
+        }
+        numplanes = 0;
+        loopi(5) if(!(match&(1<<i))) for(int j = i+1; j<6; j++) if(!(match&(1<<j)) && i+3!=j && ((color>>i)^(color>>j))&1)
+        {
+            int r = i%3, c = j%3, d = (r+1)%3;
+            if(d==c) d = (c+1)%3;
+            shaftplane &p = planes[numplanes++];
+            p.r = from[j] - to[j];
+            if(i<3 ? p.r >= 0 : p.r < 0)
+            {
+                p.r = -p.r;
+                p.c = from[i] - to[i];
+            }
+            else p.c = to[i] - from[i];
+            p.offset = -(from[i]*p.r + from[j]*p.c);
+            p.rnear = p.r >= 0 ? r : 3+r;
+            p.cnear = p.c >= 0 ? c : 3+c;
+            p.rfar = p.r < 0 ? r : 3+r;
+            p.cfar = p.c < 0 ? c : 3+c;
+        }
+    }
+
+    bool outside(const shaftbb &o) const
+    {
+        if(bounds.outside(o)) return true;
+
+        for(const shaftplane *p = planes; p < &planes[numplanes]; p++)
+        {
+            if(o[p->rnear]*p->r + o[p->cnear]*p->c + p->offset > 0) return true;
+        }
+        return false;
+    }
+
+    bool inside(const shaftbb &o) const
+    {
+        if(bounds.notinside(o)) return false;
+
+        for(const shaftplane *p = planes; p < &planes[numplanes]; p++)
+        {
+            if(o[p->rfar]*p->r + o[p->cfar]*p->c + p->offset > 0) return false;
+        }
+        return true;
+    }
+};
+
+struct pvsdata
+{
+    int offset, len;
+
+    pvsdata() {}
+    pvsdata(int offset, int len) : offset(offset), len(len) {}
+};
+
+static vector<uchar> pvsbuf;
+
+static inline uint hthash(const pvsdata &k)
+{
+    uint h = 5381;
+    loopi(k.len) h = ((h<<5)+h)^pvsbuf[k.offset+i];
+    return h;
+}
+
+static inline bool htcmp(const pvsdata &x, const pvsdata &y)
+{
+    return x.len==y.len && !memcmp(&pvsbuf[x.offset], &pvsbuf[y.offset], x.len);
+}
+
+static SDL_mutex *pvsmutex = NULL;
+static hashtable<pvsdata, int> pvscompress;
+static vector<pvsdata> pvs;
+
+static SDL_mutex *viewcellmutex = NULL;
+struct viewcellrequest
+{
+    int *result;
+    ivec o;
+    int size;
+};
+static vector<viewcellrequest> viewcellrequests;
+
+static bool genpvs_canceled = false;
+static int numviewcells = 0;
+
+VAR(maxpvsblocker, 1, 512, 1<<16);
+VAR(pvsleafsize, 1, 64, 1024);
+
+#define MAXWATERPVS 32
+
+static struct
+{
+    int height;
+    vector<materialsurface *> matsurfs;
+} waterplanes[MAXWATERPVS];
+static vector<materialsurface *> waterfalls;
+uint numwaterplanes = 0;
+
+struct pvsworker
+{
+    pvsworker() : thread(NULL), pvsnodes(new pvsnode[origpvsnodes.length()])
+    {
+    }
+    ~pvsworker()
+    {
+        delete[] pvsnodes;
+    }
+
+    SDL_Thread *thread;
+    pvsnode *pvsnodes;
+
+    shaftbb viewcellbb;
+
+    pvsnode *levels[32];
+    int curlevel;
+    ivec origin;
+
+    void resetlevels()
+    {
+        curlevel = worldscale;
+        levels[curlevel] = &pvsnodes[0];
+        origin = ivec(0, 0, 0);
+    }
+
+    int hasvoxel(const ivec &p, int coord, int dir, int ocoord = 0, int odir = 0, int *omin = NULL)
+    {
+        uint diff = (origin.x^p.x)|(origin.y^p.y)|(origin.z^p.z);
+        if(diff >= uint(worldsize)) return 0;
+        diff >>= curlevel;
+        while(diff)
+        {
+            curlevel++;
+            diff >>= 1;
+        }
+
+        pvsnode *cur = levels[curlevel];
+        while(cur->children && !(cur->flags&PVS_HIDE_BB))
+        {
+            cur = &pvsnodes[cur->children];
+            curlevel--;
+            cur += ((p.z>>(curlevel-2))&4) | ((p.y>>(curlevel-1))&2) | ((p.x>>curlevel)&1);
+            levels[curlevel] = cur;
+        }
+
+        origin = ivec(p.x&(~0U<<curlevel), p.y&(~0U<<curlevel), p.z&(~0U<<curlevel));
+
+        if(cur->flags&PVS_HIDE_BB || cur->edges==bvec(0x80, 0x80, 0x80))
+        {
+            if(omin)
+            {
+                int step = origin[ocoord] + (odir<<curlevel) - p[ocoord] + odir - 1;
+                if(odir ? step < *omin : step > *omin) *omin = step;
+            }
+            return origin[coord] + (dir<<curlevel) - p[coord] + dir - 1;
+        }
+
+        if(cur->edges.x==0xFF) return 0;
+        ivec bbp(p);
+        bbp.sub(origin);
+        ivec bbmin, bbmax;
+        bbmin.x = ((cur->edges.x&0xF)<<curlevel)/8;
+        if(bbp.x < bbmin.x) return 0;
+        bbmax.x = ((cur->edges.x>>4)<<curlevel)/8;
+        if(bbp.x >= bbmax.x) return 0;
+        bbmin.y = ((cur->edges.y&0xF)<<curlevel)/8;
+        if(bbp.y < bbmin.y) return 0;
+        bbmax.y = ((cur->edges.y>>4)<<curlevel)/8;
+        if(bbp.y >= bbmax.y) return 0;
+        bbmin.z = ((cur->edges.z&0xF)<<curlevel)/8;
+        if(bbp.z < bbmin.z) return 0;
+        bbmax.z = ((cur->edges.z>>4)<<curlevel)/8;
+        if(bbp.z >= bbmax.z) return 0;
+
+        if(omin)
+        {
+            int step = (odir ? bbmax[ocoord] : bbmin[ocoord]) - bbp[ocoord] + (odir - 1);
+            if(odir ? step < *omin : step > *omin) *omin = step;
+        }
+        return (dir ? bbmax[coord] : bbmin[coord]) - bbp[coord] + (dir - 1);
+    }
+
+    void hidepvs(pvsnode &p)
+    {
+        if(p.children)
+        {
+            pvsnode *children = &pvsnodes[p.children];
+            loopi(8) hidepvs(children[i]);
+            p.flags |= PVS_HIDE_BB;
+            return;
+        }
+        p.flags |= PVS_HIDE_BB;
+        if(p.edges.x!=0xFF) p.flags |= PVS_HIDE_GEOM;
+    }
+
+    void shaftcullpvs(shaft &s, pvsnode &p, const ivec &co = ivec(0, 0, 0), int size = worldsize)
+    {
+        if(p.flags&PVS_HIDE_BB) return;
+        shaftbb bb(co, size);
+        if(s.outside(bb)) return;
+        if(s.inside(bb)) { hidepvs(p); return; }
+        if(p.children)
+        {
+            pvsnode *children = &pvsnodes[p.children];
+            uchar flags = 0xFF;
+            loopi(8)
+            {
+                ivec o(i, co, size>>1);
+                shaftcullpvs(s, children[i], o, size>>1);
+                flags &= children[i].flags;
+            }
+            if(flags & PVS_HIDE_BB) p.flags |= PVS_HIDE_BB;
+            return;
+        }
+        if(p.edges.x==0xFF) return;
+        shaftbb geom(co, size, p.edges);
+        if(s.inside(geom)) p.flags |= PVS_HIDE_GEOM;
+    }
+
+    queue<shaftbb, 32> prevblockers;
+
+    struct cullorder
+    {
+        int index, dist;
+
+        cullorder() {}
+        cullorder(int index, int dist) : index(index), dist(dist) {}
+    };
+
+    void cullpvs(pvsnode &p, const ivec &co = ivec(0, 0, 0), int size = worldsize)
+    {
+        if(p.flags&(PVS_HIDE_BB | PVS_HIDE_GEOM) || genpvs_canceled) return;
+        if(p.children && !(p.flags&PVS_HIDE_BB))
+        {
+            pvsnode *children = &pvsnodes[p.children];
+            int csize = size>>1;
+            ivec dmin = ivec(co).add(csize>>1).sub(ivec(viewcellbb.min).add(ivec(viewcellbb.max)).shr(1)), dmax = ivec(dmin).add(csize);
+            dmin.mul(dmin);
+            dmax.mul(dmax);
+            ivec diff = ivec(dmax).sub(dmin);
+            cullorder order[8];
+            int dir = 0;
+            if(diff.x < 0) { diff.x = -diff.x; dir |= 1; }
+            if(diff.y < 0) { diff.y = -diff.y; dir |= 2; }
+            if(diff.z < 0) { diff.z = -diff.z; dir |= 4; }
+            order[0] = cullorder(0, 0);
+            order[7] = cullorder(7, diff.x + diff.y + diff.z);
+            order[1] = cullorder(1, diff.x);
+            order[2] = cullorder(2, diff.y);
+            order[3] = cullorder(4, diff.z);
+            if(order[2].dist < order[1].dist) swap(order[1], order[2]);
+            if(order[3].dist < order[2].dist) swap(order[2], order[3]);
+            if(order[2].dist < order[1].dist) swap(order[1], order[2]);
+            cullorder dxy(order[1].index|order[2].index, order[1].dist+order[2].dist),
+                      dxz(order[1].index|order[3].index, order[1].dist+order[3].dist),
+                      dyz(order[2].index|order[3].index, order[2].dist+order[3].dist);
+            int j;
+            for(j = 4; j > 0 && dxy.dist < order[j-1].dist; --j) order[j] = order[j-1];
+            order[j] = dxy;
+            for(j = 5; j > 0 && dxz.dist < order[j-1].dist; --j) order[j] = order[j-1];
+            order[j] = dxz;
+            for(j = 6; j > 0 && dyz.dist < order[j-1].dist; --j) order[j] = order[j-1];
+            order[j] = dyz;
+            loopi(8)
+            {
+                int index = order[i].index^dir;
+                ivec o(index, co, csize);
+                cullpvs(children[index], o, csize);
+            }
+            if(!(p.flags & PVS_HIDE_BB)) return;
+        }
+        bvec edges = p.children ? bvec(0x80, 0x80, 0x80) : p.edges;
+        if(edges.x==0xFF) return;
+        shaftbb geom(co, size, edges);
+        ivec diff = ivec(geom.max).sub(ivec(viewcellbb.min)).abs();
+        cullorder order[3] = { cullorder(0, diff.x), cullorder(1, diff.y), cullorder(2, diff.z) };
+        if(order[1].dist > order[0].dist) swap(order[0], order[1]);
+        if(order[2].dist > order[1].dist) swap(order[1], order[2]);
+        if(order[1].dist > order[0].dist) swap(order[0], order[1]);
+        loopi(6)
+        {
+            int dim = order[i >= 3 ? i-3 : i].index, dc = (i >= 3) != (geom.max[dim] <= viewcellbb.min[dim]) ? 1 : 0, r = R[dim], c = C[dim];
+            int ccenter = geom.min[c];
+            if(geom.min[r]==geom.max[r] || geom.min[c]==geom.max[c]) continue;
+            while(ccenter < geom.max[c])
+            {
+                ivec rmin;
+                rmin[dim] = geom[dim + 3*dc] + (dc ? -1 : 0);
+                rmin[r] = geom.min[r];
+                rmin[c] = ccenter;
+                ivec rmax = rmin;
+                rmax[r] = geom.max[r] - 1;
+                int rcenter = (rmin[r] + rmax[r])/2;
+                resetlevels();
+                for(int minstep = -1, maxstep = 1; (minstep || maxstep) && rmax[r] - rmin[r] < maxpvsblocker;)
+                {
+                    if(minstep) minstep = hasvoxel(rmin, r, 0);
+                    if(maxstep) maxstep = hasvoxel(rmax, r, 1);
+                    rmin[r] += minstep;
+                    rmax[r] += maxstep;
+                }
+                rmin[r] = rcenter + (rmin[r] - rcenter)/2;
+                rmax[r] = rcenter + (rmax[r] - rcenter)/2;
+                if(rmin[r]>=geom.min[r] && rmax[r]<geom.max[r]) { rmin[r] = geom.min[r]; rmax[r] = geom.max[r] - 1; }
+                ivec cmin = rmin, cmax = rmin;
+                if(rmin[r]>=geom.min[r] && rmax[r]<geom.max[r])
+                {
+                    cmin[c] = geom.min[c];
+                    cmax[c] = geom.max[c]-1;
+                }
+                int cminstep = -1, cmaxstep = 1;
+                for(; (cminstep || cmaxstep) && cmax[c] - cmin[c] < maxpvsblocker;)
+                {
+                    if(cminstep)
+                    {
+                        cmin[c] += cminstep; cminstep = INT_MIN;
+                        cmin[r] = rmin[r];
+                        resetlevels();
+                        for(int rstep = 1; rstep && cmin[r] <= rmax[r];)
+                        {
+                            rstep = hasvoxel(cmin, r, 1, c, 0, &cminstep);
+                            cmin[r] += rstep;
+                        }
+                        if(cmin[r] <= rmax[r]) cminstep = 0;
+                    }
+                    if(cmaxstep)
+                    {
+                        cmax[c] += cmaxstep; cmaxstep = INT_MAX;
+                        cmax[r] = rmin[r];
+                        resetlevels();
+                        for(int rstep = 1; rstep && cmax[r] <= rmax[r];)
+                        {
+                            rstep = hasvoxel(cmax, r, 1, c, 1, &cmaxstep);
+                            cmax[r] += rstep;
+                        }
+                        if(cmax[r] <= rmax[r]) cmaxstep = 0;
+                    }
+                }
+                if(!cminstep) cmin[c]++;
+                if(!cmaxstep) cmax[c]--;
+                ivec emin = rmin, emax = rmax;
+                if(cmin[c]>=geom.min[c] && cmax[c]<geom.max[c])
+                {
+                    if(emin[r]>geom.min[r]) emin[r] = geom.min[r];
+                    if(emax[r]<geom.max[r]-1) emax[r] = geom.max[r]-1;
+                }
+                int rminstep = -1, rmaxstep = 1;
+                for(; (rminstep || rmaxstep) && emax[r] - emin[r] < maxpvsblocker;)
+                {
+                    if(rminstep)
+                    {
+                        emin[r] += -1; rminstep = INT_MIN;
+                        emin[c] = cmin[c];
+                        resetlevels();
+                        for(int cstep = 1; cstep && emin[c] <= cmax[c];)
+                        {
+                            cstep = hasvoxel(emin, c, 1, r, 0, &rminstep);
+                            emin[c] += cstep;
+                        }
+                        if(emin[c] <= cmax[c]) rminstep = 0;
+                    }
+                    if(rmaxstep)
+                    {
+                        emax[r] += 1; rmaxstep = INT_MAX;
+                        emax[c] = cmin[c];
+                        resetlevels();
+                        for(int cstep = 1; cstep && emax[c] <= cmax[c];)
+                        {
+                            cstep = hasvoxel(emax, c, 1, r, 1, &rmaxstep);
+                            emax[c] += cstep;
+                        }
+                        if(emax[c] <= cmax[c]) rmaxstep = 0;
+                    }
+                }
+                if(!rminstep) emin[r]++;
+                if(!rmaxstep) emax[r]--;
+                shaftbb bb;
+                bb.min[dim] = rmin[dim];
+                bb.max[dim] = rmin[dim]+1;
+                bb.min[r] = emin[r];
+                bb.max[r] = emax[r]+1;
+                bb.min[c] = cmin[c];
+                bb.max[c] = cmax[c]+1;
+                if(bb.min[dim] >= viewcellbb.max[dim] || bb.max[dim] <= viewcellbb.min[dim])
+                {
+                    int ddir = bb.min[dim] >= viewcellbb.max[dim] ? 1 : -1,
+                        dval = ddir>0 ? USHRT_MAX-1 : 0,
+                        dlimit = maxpvsblocker,
+                        numsides = 0;
+                    loopj(4)
+                    {
+                        ivec dmax;
+                        int odim = j < 2 ? c : r;
+                        if(j&1)
+                        {
+                            if(bb.max[odim] >= viewcellbb.max[odim]) continue;
+                            dmax[odim] = bb.max[odim]-1;
+                        }
+                        else
+                        {
+                            if(bb.min[odim] <= viewcellbb.min[odim]) continue;
+                            dmax[odim] = bb.min[odim];
+                        }
+                        numsides++;
+                        dmax[dim] = bb.min[dim];
+                        int stepdim = j < 2 ? r : c, stepstart = bb.min[stepdim], stepend = bb.max[stepdim];
+                        int dstep = ddir;
+                        for(; dstep && ddir*(dmax[dim] - (int)bb.min[dim]) < dlimit;)
+                        {
+                            dmax[dim] += dstep; dstep = ddir > 0 ? INT_MAX : INT_MIN;
+                            dmax[stepdim] = stepstart;
+                            resetlevels();
+                            for(int step = 1; step && dmax[stepdim] < stepend;)
+                            {
+                                step = hasvoxel(dmax, stepdim, 1, dim, (ddir+1)/2, &dstep);
+                                dmax[stepdim] += step;
+                            }
+                            if(dmax[stepdim] < stepend) dstep = 0;
+                        }
+                        dlimit = min(dlimit, ddir*(dmax[dim] - (int)bb.min[dim]));
+                        if(!dstep) dmax[dim] -= ddir;
+                        if(ddir>0) dval = min(dval, dmax[dim]);
+                        else dval = max(dval, dmax[dim]);
+                    }
+                    if(numsides>0)
+                    {
+                        if(ddir>0) bb.max[dim] = dval+1;
+                        else bb.min[dim] = dval;
+                    }
+                    //printf("(%d,%d,%d) x %d,%d,%d, side %d, ccenter = %d, origin = (%d,%d,%d), size = %d\n", bb.min.x, bb.min.y, bb.min.z, bb.max.x-bb.min.x, bb.max.y-bb.min.y, bb.max.z-bb.min.z, i, ccenter, co.x, co.y, co.z, size);
+                }
+                bool dup = false;
+                loopvj(prevblockers)
+                {
+                    if(prevblockers[j].contains(bb)) { dup = true; break; }
+                }
+                if(!dup)
+                {
+                    shaft s(viewcellbb, bb);
+                    shaftcullpvs(s, pvsnodes[0]);
+                    prevblockers.add(bb);
+                }
+                if(bb.contains(geom)) return;
+                ccenter = cmax[c] + 1;
+            }
+        }
+    }
+
+    bool compresspvs(pvsnode &p, int size, int threshold)
+    {
+        if(!p.children) return true;
+        if(p.flags&PVS_HIDE_BB) { p.children = 0; return true; }
+        pvsnode *children = &pvsnodes[p.children];
+        bool canreduce = true;
+        loopi(8)
+        {
+            if(!compresspvs(children[i], size/2, threshold)) canreduce = false;
+        }
+        if(canreduce)
+        {
+            int hide = children[7].flags&PVS_HIDE_BB;
+            loopi(7) if((children[i].flags&PVS_HIDE_BB)!=hide) canreduce = false;
+            if(canreduce) 
+            {
+                p.flags = (p.flags & ~PVS_HIDE_BB) | hide;
+                p.children = 0;
+                return true;
+            }
+        }
+        if(size <= threshold)
+        {
+            p.children = 0;
+            return true;
+        }
+        return false;
+    }
+    
+    vector<uchar> outbuf;
+
+    bool serializepvs(pvsnode &p, int storage = -1)
+    {
+        if(!p.children)
+        {
+            outbuf.add(0xFF);
+            loopi(8) outbuf.add(p.flags&PVS_HIDE_BB ? 0xFF : 0);
+            return true;
+        }
+        int index = outbuf.length();
+        pvsnode *children = &pvsnodes[p.children];
+        int i = 0;
+        uchar leafvalues = 0;
+        if(storage>=0)
+        {
+            for(; i < 8; i++)
+            {   
+                pvsnode &child = children[i];
+                if(child.flags&PVS_HIDE_BB) leafvalues |= 1<<i;
+                else if(child.children) break;
+            }
+            if(i==8) { outbuf[storage] = leafvalues; return false; }
+            // if offset won't fit, just mark the space as a visible to avoid problems
+            int offset = (index - storage + 8)/9;
+            if(offset>255) { outbuf[storage] = 0; return false; }
+            outbuf[storage] = uchar(offset);
+        }
+        outbuf.add(0);
+        loopj(8) outbuf.add(leafvalues&(1<<j) ? 0xFF : 0);
+        uchar leafmask = (1<<i)-1;
+        for(; i < 8; i++)
+        {
+            pvsnode &child = children[i];
+            if(child.children) { if(!serializepvs(child, index+1+i)) leafmask |= 1<<i; }
+            else { leafmask |= 1<<i; outbuf[index+1+i] = child.flags&PVS_HIDE_BB ? 0xFF : 0; }
+        }
+        outbuf[index] = leafmask;
+        return true;
+    }
+
+    bool materialoccluded(pvsnode &p, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax)
+    {
+        pvsnode *children = &pvsnodes[p.children];
+        loopoctabox(co, size, bbmin, bbmax)
+        {
+            ivec o(i, co, size);
+            if(children[i].flags & PVS_HIDE_BB) continue;
+            if(!children[i].children || !materialoccluded(children[i], o, size/2, bbmin, bbmax)) return false;
+        }
+        return true;
+    }
+
+    bool materialoccluded(vector<materialsurface *> &matsurfs)
+    {
+        if(pvsnodes[0].flags & PVS_HIDE_BB) return true;
+        if(!pvsnodes[0].children) return false;
+        loopv(matsurfs)
+        {
+            materialsurface &m = *matsurfs[i];
+            ivec bbmin(m.o), bbmax(m.o);
+            int dim = dimension(m.orient);
+            bbmin[dim] += dimcoord(m.orient) ? -2 : 2;
+            bbmax[C[dim]] += m.csize;
+            bbmax[R[dim]] += m.rsize;
+            if(!materialoccluded(pvsnodes[0], ivec(0, 0, 0), worldsize/2, bbmin, bbmax)) return false;
+        }
+        return true;
+    }
+
+    int wateroccluded, waterbytes;
+
+    void calcpvs(const ivec &co, int size)
+    {
+        loopk(3)
+        {
+            viewcellbb.min[k] = co[k];
+            viewcellbb.max[k] = co[k]+size;
+        }
+        memcpy(pvsnodes, origpvsnodes.getbuf(), origpvsnodes.length()*sizeof(pvsnode));
+        prevblockers.clear();
+        cullpvs(pvsnodes[0]);
+
+        wateroccluded = 0;
+        loopi(numwaterplanes)
+        {
+            if(waterplanes[i].height < 0)
+            {
+                if(waterfalls.length() && materialoccluded(waterfalls)) wateroccluded |= 1<<i;
+            }
+            else if(waterplanes[i].matsurfs.length() && materialoccluded(waterplanes[i].matsurfs)) wateroccluded |= 1<<i;
+        }
+        waterbytes = 0;
+        loopi(4) if(wateroccluded&(0xFF<<(i*8))) waterbytes = i+1;
+
+        compresspvs(pvsnodes[0], worldsize, pvsleafsize);
+        outbuf.setsize(0);
+        serializepvs(pvsnodes[0]);
+    }
+
+    uchar *testviewcell(const ivec &co, int size, int *waterpvs = NULL, int *len = NULL)
+    {
+        calcpvs(co, size);
+
+        uchar *buf = new uchar[outbuf.length()];
+        memcpy(buf, outbuf.getbuf(), outbuf.length());
+        if(waterpvs) *waterpvs = wateroccluded;
+        if(len) *len = outbuf.length();
+        return buf;
+    }
+
+    int genviewcell(const ivec &co, int size)
+    {
+        calcpvs(co, size);
+
+        if(pvsmutex) SDL_LockMutex(pvsmutex);
+        numviewcells++;
+        pvsdata key(pvsbuf.length(), waterbytes + outbuf.length());
+        loopi(waterbytes) pvsbuf.add((wateroccluded>>(i*8))&0xFF);
+        pvsbuf.put(outbuf.getbuf(), outbuf.length());
+        int *val = pvscompress.access(key);
+        if(val) pvsbuf.setsize(key.offset);
+        else
+        {
+            val = &pvscompress[key];
+            *val = pvs.length();
+            pvs.add(key);
+        }
+        if(pvsmutex) SDL_UnlockMutex(pvsmutex);
+        return *val;
+    }
+
+    static int run(void *data)
+    {
+        pvsworker *w = (pvsworker *)data;
+        SDL_LockMutex(viewcellmutex);
+        while(viewcellrequests.length())
+        {
+            viewcellrequest req = viewcellrequests.pop();
+            SDL_UnlockMutex(viewcellmutex);
+            int result = w->genviewcell(req.o, req.size);
+            SDL_LockMutex(viewcellmutex);
+            *req.result = result;
+        }
+        SDL_UnlockMutex(viewcellmutex);
+        return 0;
+    }
+};
+
+struct viewcellnode
+{
+    uchar leafmask;
+    union viewcellchild
+    {
+        int pvs;
+        viewcellnode *node;
+    } children[8];
+
+    viewcellnode() : leafmask(0xFF)
+    {
+        loopi(8) children[i].pvs = -1;
+    }
+    ~viewcellnode()
+    {
+        loopi(8) if(!(leafmask&(1<<i))) delete children[i].node;
+    }
+};
+
+VARP(pvsthreads, 0, 0, 16);
+static vector<pvsworker *> pvsworkers;
+
+static volatile bool check_genpvs_progress = false;
+
+static Uint32 genpvs_timer(Uint32 interval, void *param)
+{
+    check_genpvs_progress = true;
+    return interval;
+}
+
+static int totalviewcells = 0;
+
+static void show_genpvs_progress(int unique = pvs.length(), int processed = numviewcells)
+{
+    float bar1 = float(processed) / float(totalviewcells>0 ? totalviewcells : 1);
+
+    defformatstring(text1, "%d%% - %d of %d view cells (%d unique)", int(bar1 * 100), processed, totalviewcells, unique);
+
+    renderprogress(bar1, text1);
+
+    if(interceptkey(SDLK_ESCAPE)) genpvs_canceled = true;
+    check_genpvs_progress = false;
+}
+
+static shaftbb pvsbounds;
+
+static void calcpvsbounds()
+{
+    loopk(3) pvsbounds.min[k] = USHRT_MAX;
+    loopk(3) pvsbounds.max[k] = 0;
+    loopv(valist)
+    {
+        vtxarray *va = valist[i];
+        loopk(3)
+        {
+            if(va->geommin[k]>va->geommax[k]) continue;
+            pvsbounds.min[k] = min(pvsbounds.min[k], (ushort)va->geommin[k]);
+            pvsbounds.max[k] = max(pvsbounds.max[k], (ushort)va->geommax[k]);
+        }
+    }
+}
+
+static inline bool isallclip(cube *c)
+{
+    loopi(8)
+    {
+        cube &h = c[i];
+        if(h.children ? !isallclip(h.children) : (!isentirelysolid(h) && (h.material&MATF_CLIP)!=MAT_CLIP))
+            return false;
+    }
+    return true;
+}
+   
+static int countviewcells(cube *c, const ivec &co, int size, int threshold)
+{
+    int count = 0;
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        if(pvsbounds.outside(o, size)) continue;
+        cube &h = c[i];
+        if(h.children)
+        {
+            if(size>threshold)
+            {
+                count += countviewcells(h.children, o, size>>1, threshold);
+                continue;
+            }
+            if(isallclip(h.children)) continue;
+        }
+        else if(isentirelysolid(h) || (h.material&MATF_CLIP)==MAT_CLIP) continue;
+        count++;
+    }
+    return count;
+}
+
+static void genviewcells(viewcellnode &p, cube *c, const ivec &co, int size, int threshold)
+{
+    if(genpvs_canceled) return;
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        if(pvsbounds.outside(o, size)) continue;
+        cube &h = c[i];
+        if(h.children)
+        {
+            if(size>threshold)
+            {
+                p.leafmask &= ~(1<<i);
+                p.children[i].node = new viewcellnode;
+                genviewcells(*p.children[i].node, h.children, o, size>>1, threshold);
+                continue;
+            }
+            if(isallclip(h.children)) continue;
+        }
+        else if(isentirelysolid(h) || (h.material&MATF_CLIP)==MAT_CLIP) continue;
+        if(pvsworkers.length())
+        {
+            if(genpvs_canceled) return;
+            p.children[i].pvs = pvsworkers[0]->genviewcell(o, size);
+            if(check_genpvs_progress) show_genpvs_progress();
+        }
+        else
+        {
+            viewcellrequest &req = viewcellrequests.add();
+            req.result = &p.children[i].pvs;
+            req.o = o;
+            req.size = size;
+        }
+    }
+}
+
+static viewcellnode *viewcells = NULL;
+static int lockedwaterplanes[MAXWATERPVS];
+static uchar *curpvs = NULL, *lockedpvs = NULL;
+static int curwaterpvs = 0, lockedwaterpvs = 0;
+
+static inline pvsdata *lookupviewcell(const vec &p)
+{
+    uint x = uint(floor(p.x)), y = uint(floor(p.y)), z = uint(floor(p.z));
+    if(!viewcells || (x|y|z)>=uint(worldsize)) return NULL;
+    viewcellnode *vc = viewcells;
+    for(int scale = worldscale-1; scale>=0; scale--)
+    {
+        int i = octastep(x, y, z, scale);
+        if(vc->leafmask&(1<<i))
+        {
+            return vc->children[i].pvs>=0 ? &pvs[vc->children[i].pvs] : NULL;
+        }
+        vc = vc->children[i].node;
+    }
+    return NULL;
+}
+
+static void lockpvs_(bool lock)
+{
+    if(lockedpvs) DELETEA(lockedpvs);
+    if(!lock) return;
+    pvsdata *d = lookupviewcell(camera1->o);
+    if(!d) return;
+    int wbytes = d->len%9, len = d->len - wbytes;
+    lockedpvs = new uchar[len];
+    memcpy(lockedpvs, &pvsbuf[d->offset + wbytes], len);
+    lockedwaterpvs = 0;
+    loopi(wbytes) lockedwaterpvs |= pvsbuf[d->offset + i] << (i*8);
+    loopi(MAXWATERPVS) lockedwaterplanes[i] = waterplanes[i].height;
+    conoutf("locked view cell at %.1f, %.1f, %.1f", camera1->o.x, camera1->o.y, camera1->o.z);
+}
+
+VARF(lockpvs, 0, 0, 1, lockpvs_(lockpvs!=0));
+
+VARN(pvs, usepvs, 0, 1, 1);
+VARN(waterpvs, usewaterpvs, 0, 1, 1);
+
+void setviewcell(const vec &p)
+{
+    if(!usepvs) curpvs = NULL;
+    else if(lockedpvs) 
+    {
+        curpvs = lockedpvs;
+        curwaterpvs = lockedwaterpvs;
+    }
+    else
+    {
+        pvsdata *d = lookupviewcell(p);
+        curpvs = d ? &pvsbuf[d->offset] : NULL;
+        curwaterpvs = 0;
+        if(d)
+        {
+            loopi(d->len%9) curwaterpvs |= *curpvs++ << (i*8);
+        }
+    }
+    if(!usepvs || !usewaterpvs) curwaterpvs = 0;
+}
+
+void clearpvs()
+{
+    DELETEP(viewcells);
+    pvs.setsize(0);
+    pvsbuf.setsize(0);
+    curpvs = NULL;
+    numwaterplanes = 0;
+    lockpvs = 0;
+    lockpvs_(false);
+}
+
+COMMAND(clearpvs, "");
+
+static void findwaterplanes()
+{
+    loopi(MAXWATERPVS)
+    {
+        waterplanes[i].height = -1;
+        waterplanes[i].matsurfs.setsize(0);
+    }
+    waterfalls.setsize(0);
+    numwaterplanes = 0;
+    loopv(valist)
+    {
+        vtxarray *va = valist[i];
+        loopj(va->matsurfs)
+        {
+            materialsurface &m = va->matbuf[j];
+            if((m.material&MATF_VOLUME)!=MAT_WATER || m.orient==O_BOTTOM) { j += m.skip; continue; }
+            if(m.orient!=O_TOP)
+            {
+                waterfalls.add(&m);
+                continue;
+            }
+            loopk(numwaterplanes) if(waterplanes[k].height == m.o.z)
+            {
+                waterplanes[k].matsurfs.add(&m);
+                goto nextmatsurf;
+            }
+            if(numwaterplanes < MAXWATERPVS)
+            {
+                waterplanes[numwaterplanes].height = m.o.z;
+                waterplanes[numwaterplanes].matsurfs.add(&m);
+                numwaterplanes++;
+            }
+        nextmatsurf:;
+        }
+    }
+    if(waterfalls.length() > 0 && numwaterplanes < MAXWATERPVS) numwaterplanes++;
+}
+
+void testpvs(int *vcsize)
+{
+    lockpvs_(false);
+
+    uint oldnumwaterplanes = numwaterplanes;
+    int oldwaterplanes[MAXWATERPVS];
+    loopi(numwaterplanes) oldwaterplanes[i] = waterplanes[i].height;
+
+    findwaterplanes();
+
+    pvsnode &root = origpvsnodes.add();
+    memset(root.edges.v, 0xFF, 3);
+    root.flags = 0;
+    root.children = 0;
+    genpvsnodes(worldroot);
+
+    genpvs_canceled = false;
+    check_genpvs_progress = false;
+
+    int size = *vcsize>0 ? *vcsize : 32;
+    for(int mask = 1; mask < size; mask <<= 1) size &= ~mask;
+
+    ivec o = ivec(camera1->o).mask(~(size-1));
+    pvsworker w;
+    int len;
+    lockedpvs = w.testviewcell(o, size, &lockedwaterpvs, &len);
+    loopi(MAXWATERPVS) lockedwaterplanes[i] = waterplanes[i].height;
+    lockpvs = 1;
+    conoutf("generated test view cell of size %d at %.1f, %.1f, %.1f (%d B)", size, camera1->o.x, camera1->o.y, camera1->o.z, len);
+
+    origpvsnodes.setsize(0);
+    numwaterplanes = oldnumwaterplanes;
+    loopi(numwaterplanes) waterplanes[i].height = oldwaterplanes[i];
+}
+
+COMMAND(testpvs, "i");
+
+void genpvs(int *viewcellsize)
+{
+    if(worldsize > 1<<15)
+    {
+        conoutf(CON_ERROR, "map is too large for PVS");
+        return;
+    }
+
+    renderbackground("generating PVS (esc to abort)");
+    genpvs_canceled = false;
+    Uint32 start = SDL_GetTicks();
+
+    renderprogress(0, "finding view cells");
+
+    clearpvs();
+    calcpvsbounds();
+    findwaterplanes();
+
+    pvsnode &root = origpvsnodes.add();
+    memset(root.edges.v, 0xFF, 3);
+    root.flags = 0;
+    root.children = 0;
+    genpvsnodes(worldroot);
+
+    totalviewcells = countviewcells(worldroot, ivec(0, 0, 0), worldsize>>1, *viewcellsize>0 ? *viewcellsize : 32);
+    numviewcells = 0;
+    genpvs_canceled = false;
+    check_genpvs_progress = false;
+    SDL_TimerID timer = 0;
+    int numthreads = pvsthreads > 0 ? pvsthreads : numcpus;
+    if(numthreads<=1) 
+    {
+        pvsworkers.add(new pvsworker);
+        timer = SDL_AddTimer(500, genpvs_timer, NULL);
+    }
+    viewcells = new viewcellnode;
+    genviewcells(*viewcells, worldroot, ivec(0, 0, 0), worldsize>>1, *viewcellsize>0 ? *viewcellsize : 32);
+    if(numthreads<=1)
+    {
+        SDL_RemoveTimer(timer);
+    }
+    else
+    {
+        renderprogress(0, "creating threads");
+        if(!pvsmutex) pvsmutex = SDL_CreateMutex();
+        if(!viewcellmutex) viewcellmutex = SDL_CreateMutex();
+        loopi(numthreads)
+        {
+            pvsworker *w = pvsworkers.add(new pvsworker);
+            w->thread = SDL_CreateThread(pvsworker::run, "pvs worker", w);
+        }
+        show_genpvs_progress(0, 0);
+        while(!genpvs_canceled)
+        {
+            SDL_Delay(500);
+            SDL_LockMutex(viewcellmutex);
+            int unique = pvs.length(), processed = numviewcells, remaining = viewcellrequests.length();
+            SDL_UnlockMutex(viewcellmutex);
+            show_genpvs_progress(unique, processed);
+            if(!remaining) break;
+        }        
+        SDL_LockMutex(viewcellmutex);
+        viewcellrequests.setsize(0);
+        SDL_UnlockMutex(viewcellmutex);
+        loopv(pvsworkers) SDL_WaitThread(pvsworkers[i]->thread, NULL);
+    }
+    pvsworkers.deletecontents();
+
+    origpvsnodes.setsize(0);
+    pvscompress.clear();
+
+    Uint32 end = SDL_GetTicks();
+    if(genpvs_canceled) 
+    {
+        clearpvs();
+        conoutf("genpvs aborted");
+    }
+    else conoutf("generated %d unique view cells totaling %.1f kB and averaging %d B (%.1f seconds)", 
+            pvs.length(), pvsbuf.length()/1024.0f, pvsbuf.length()/max(pvs.length(), 1), (end - start) / 1000.0f);
+}
+
+COMMAND(genpvs, "i");
+
+void pvsstats()
+{
+    conoutf("%d unique view cells totaling %.1f kB and averaging %d B",          
+        pvs.length(), pvsbuf.length()/1024.0f, pvsbuf.length()/max(pvs.length(), 1));
+}
+
+COMMAND(pvsstats, "");
+
+static inline bool pvsoccluded(uchar *buf, const ivec &co, int size, const ivec &bbmin, const ivec &bbmax)
+{
+    uchar leafmask = buf[0];
+    loopoctabox(co, size, bbmin, bbmax)
+    {
+        ivec o(i, co, size);
+        if(leafmask&(1<<i))
+        {
+            uchar leafvalues = buf[1+i];
+            if(!leafvalues || (leafvalues!=0xFF && octaboxoverlap(o, size>>1, bbmin, bbmax)&~leafvalues))
+                return false;
+        }
+        else if(!pvsoccluded(buf+9*buf[1+i], o, size>>1, bbmin, bbmax)) return false;
+    }
+    return true;
+}
+
+static inline bool pvsoccluded(uchar *buf, const ivec &bbmin, const ivec &bbmax)
+{
+    int diff = (bbmin.x^bbmax.x) | (bbmin.y^bbmax.y) | (bbmin.z^bbmax.z);
+    if(diff&~((1<<worldscale)-1)) return false;
+    int scale = worldscale-1;
+    while(!(diff&(1<<scale)))
+    {
+        int i = octastep(bbmin.x, bbmin.y, bbmin.z, scale);
+        scale--;
+        uchar leafmask = buf[0];
+        if(leafmask&(1<<i))
+        {
+            uchar leafvalues = buf[1+i];
+            return leafvalues && (leafvalues==0xFF || !(octaboxoverlap(ivec(bbmin).mask(~((2<<scale)-1)), 1<<scale, bbmin, bbmax)&~leafvalues));
+        }
+        buf += 9*buf[1+i];
+    }
+    return pvsoccluded(buf, ivec(bbmin).mask(~((2<<scale)-1)), 1<<scale, bbmin, bbmax);
+}
+
+bool pvsoccluded(const ivec &bbmin, const ivec &bbmax)
+{
+    return curpvs!=NULL && pvsoccluded(curpvs, bbmin, bbmax);
+}
+
+bool pvsoccludedsphere(const vec &center, float radius)
+{
+    if(curpvs==NULL) return false;
+    ivec bbmin(vec(center).sub(radius)), bbmax(vec(center).add(radius+1));
+    return pvsoccluded(curpvs, bbmin, bbmax);
+}
+
+bool waterpvsoccluded(int height)
+{
+    if(!curwaterpvs) return false;
+    if(lockedpvs)
+    {
+        loopi(MAXWATERPVS) if(lockedwaterplanes[i]==height) return (curwaterpvs&(1<<i))!=0;
+    }
+    else
+    {
+        loopi(numwaterplanes) if(waterplanes[i].height==height) return (curwaterpvs&(1<<i))!=0;
+    }
+    return false;
+}
+
+void saveviewcells(stream *f, viewcellnode &p)
+{
+    f->putchar(p.leafmask);
+    loopi(8)
+    {
+        if(p.leafmask&(1<<i)) f->putlil<int>(p.children[i].pvs);
+        else saveviewcells(f, *p.children[i].node);
+    }
+}
+
+void savepvs(stream *f)
+{
+    uint totallen = pvsbuf.length() | (numwaterplanes>0 ? 0x80000000U : 0);
+    f->putlil<uint>(totallen);
+    if(numwaterplanes>0)
+    {
+        f->putlil<uint>(numwaterplanes);
+        loopi(numwaterplanes)
+        {
+            f->putlil<int>(waterplanes[i].height);
+            if(waterplanes[i].height < 0) break;
+        }
+    }
+    loopv(pvs) f->putlil<ushort>(pvs[i].len);
+    f->write(pvsbuf.getbuf(), pvsbuf.length());
+    saveviewcells(f, *viewcells);
+}
+
+viewcellnode *loadviewcells(stream *f)
+{
+    viewcellnode *p = new viewcellnode;
+    p->leafmask = f->getchar();
+    loopi(8)
+    {
+        if(p->leafmask&(1<<i)) p->children[i].pvs = f->getlil<int>();
+        else p->children[i].node = loadviewcells(f);
+    }
+    return p;
+}
+
+void loadpvs(stream *f, int numpvs)
+{
+    uint totallen = f->getlil<uint>();
+    if(totallen & 0x80000000U)
+    {
+        totallen &= ~0x80000000U;
+        numwaterplanes = f->getlil<uint>();
+        loopi(numwaterplanes) waterplanes[i].height = f->getlil<int>();
+    }
+    int offset = 0;
+    loopi(numpvs)
+    {
+        ushort len = f->getlil<ushort>();
+        pvs.add(pvsdata(offset, len));
+        offset += len;
+    }
+    f->read(pvsbuf.reserve(totallen).buf, totallen);
+    pvsbuf.advance(totallen);
+    viewcells = loadviewcells(f);
+}
+
+int getnumviewcells() { return pvs.length(); }
+
diff --git a/src/engine/ragdoll.h b/src/engine/ragdoll.h
new file mode 100644 (file)
index 0000000..9f6f641
--- /dev/null
@@ -0,0 +1,534 @@
+struct ragdollskel
+{
+    struct vert
+    {
+        vec pos;
+        float radius, weight;
+    };
+
+    struct tri
+    {
+        int vert[3];
+
+        bool shareverts(const tri &t) const
+        {
+            loopi(3) loopj(3) if(vert[i] == t.vert[j]) return true;
+            return false;
+        }
+    };
+
+    struct distlimit
+    {
+        int vert[2];
+        float mindist, maxdist;
+    }; 
+
+    struct rotlimit
+    {
+        int tri[2];
+        float maxangle;
+        matrix3 middle;
+    };
+
+    struct rotfriction
+    {
+        int tri[2];
+        matrix3 middle;
+    };
+
+    struct joint
+    {
+        int bone, tri, vert[3];
+        float weight;
+        matrix4x3 orient;
+    };
+
+    struct reljoint
+    {
+        int bone, parent;
+    };
+
+    bool loaded, animjoints;
+    int eye;
+    vector<vert> verts;
+    vector<tri> tris;
+    vector<distlimit> distlimits;
+    vector<rotlimit> rotlimits;
+    vector<rotfriction> rotfrictions;
+    vector<joint> joints;
+    vector<reljoint> reljoints;
+
+    ragdollskel() : loaded(false), animjoints(false), eye(-1) {}
+
+    void setupjoints()
+    {
+        loopv(verts) verts[i].weight = 0;
+        loopv(joints)
+        {
+            joint &j = joints[i];
+            j.weight = 0;
+            vec pos(0, 0, 0);
+            loopk(3) if(j.vert[k]>=0)
+            {
+                pos.add(verts[j.vert[k]].pos);
+                j.weight++;
+                verts[j.vert[k]].weight++;
+            }
+            if(j.weight) j.weight = 1/j.weight;
+            pos.mul(j.weight);
+
+            tri &t = tris[j.tri];
+            matrix4x3 &m = j.orient;
+            const vec &v1 = verts[t.vert[0]].pos,
+                      &v2 = verts[t.vert[1]].pos,
+                      &v3 = verts[t.vert[2]].pos;
+            m.a = vec(v2).sub(v1).normalize();
+            m.c.cross(m.a, vec(v3).sub(v1)).normalize();
+            m.b.cross(m.c, m.a);
+            m.d = pos;
+            m.transpose();
+        }
+        loopv(verts) if(verts[i].weight) verts[i].weight = 1/verts[i].weight;
+        reljoints.shrink(0);
+    }
+
+    void setuprotfrictions()
+    {
+        rotfrictions.shrink(0);
+        loopv(tris) for(int j = i+1; j < tris.length(); j++) if(tris[i].shareverts(tris[j]))
+        {
+            rotfriction &r = rotfrictions.add();
+            r.tri[0] = i;
+            r.tri[1] = j;
+        }
+    }
+
+    void setup()
+    {
+        setupjoints();
+        setuprotfrictions();
+        
+        loaded = true;
+    } 
+
+    void addreljoint(int bone, int parent)
+    {
+        reljoint &r = reljoints.add();
+        r.bone = bone;
+        r.parent = parent;
+    }
+};
+
+struct ragdolldata
+{
+    struct vert
+    {
+        vec oldpos, pos, newpos;
+        float weight;
+        bool collided, stuck;
+
+        vert() : pos(0, 0, 0), newpos(0, 0, 0), weight(0), collided(false), stuck(true) {}
+    };
+
+    ragdollskel *skel;
+    int millis, collidemillis, collisions, floating, lastmove, unsticks;
+    vec offset, center;
+    float radius, timestep, scale;
+    vert *verts;
+    matrix3 *tris;
+    matrix4x3 *animjoints;
+    dualquat *reljoints;
+
+    ragdolldata(ragdollskel *skel, float scale = 1)
+        : skel(skel),
+          millis(lastmillis),
+          collidemillis(0),
+          collisions(0),
+          floating(0),
+          lastmove(lastmillis),
+          unsticks(INT_MAX),
+          radius(0),
+          timestep(0),
+          scale(scale),
+          verts(new vert[skel->verts.length()]), 
+          tris(new matrix3[skel->tris.length()]),
+          animjoints(!skel->animjoints || skel->joints.empty() ? NULL : new matrix4x3[skel->joints.length()]),
+          reljoints(skel->reljoints.empty() ? NULL : new dualquat[skel->reljoints.length()])
+    {
+    }
+
+    ~ragdolldata()
+    {
+        delete[] verts;
+        delete[] tris;
+        if(animjoints) delete[] animjoints;
+        if(reljoints) delete[] reljoints;
+    }
+
+    void calcanimjoint(int i, const matrix4x3 &anim)
+    {
+        if(!animjoints) return;
+        ragdollskel::joint &j = skel->joints[i];
+        vec pos(0, 0, 0);
+        loopk(3) if(j.vert[k]>=0) pos.add(verts[j.vert[k]].pos);
+        pos.mul(j.weight);
+
+        ragdollskel::tri &t = skel->tris[j.tri];
+        matrix4x3 m;
+        const vec &v1 = verts[t.vert[0]].pos,
+                  &v2 = verts[t.vert[1]].pos,
+                  &v3 = verts[t.vert[2]].pos;
+        m.a = vec(v2).sub(v1).normalize();
+        m.c.cross(m.a, vec(v3).sub(v1)).normalize();
+        m.b.cross(m.c, m.a);
+        m.d = pos;
+        animjoints[i].transposemul(m, anim);
+    }
+
+    void calctris()
+    {
+        loopv(skel->tris)
+        {
+            ragdollskel::tri &t = skel->tris[i];
+            matrix3 &m = tris[i];
+            const vec &v1 = verts[t.vert[0]].pos,
+                      &v2 = verts[t.vert[1]].pos,
+                      &v3 = verts[t.vert[2]].pos;
+            m.a = vec(v2).sub(v1).normalize();
+            m.c.cross(m.a, vec(v3).sub(v1)).normalize();
+            m.b.cross(m.c, m.a);
+        }
+    }
+
+    void calcboundsphere()
+    {
+        center = vec(0, 0, 0);
+        loopv(skel->verts) center.add(verts[i].pos);
+        center.div(skel->verts.length());
+        radius = 0;
+        loopv(skel->verts) radius = max(radius, verts[i].pos.dist(center));
+    }
+
+    void init(dynent *d)
+    {
+        extern int ragdolltimestepmin;
+        float ts = ragdolltimestepmin/1000.0f;
+        loopv(skel->verts) (verts[i].oldpos = verts[i].pos).sub(vec(d->vel).add(d->falling).mul(ts));
+        timestep = ts;
+
+        calctris();
+        calcboundsphere();
+        offset = d->o;
+        offset.sub(skel->eye >= 0 ? verts[skel->eye].pos : center);
+        offset.z += (d->eyeheight + d->aboveeye)/2;
+    }
+
+    void move(dynent *pl, float ts);
+    void updatepos();
+    void constrain();
+    void constraindist();
+    void applyrotlimit(ragdollskel::tri &t1, ragdollskel::tri &t2, float angle, const vec &axis);
+    void constrainrot();
+    void calcrotfriction();
+    void applyrotfriction(float ts);
+    void tryunstick(float speed);
+
+    static inline bool collidevert(const vec &pos, const vec &dir, float radius)
+    {
+        static struct vertent : physent
+        {
+            vertent()
+            {
+                type = ENT_BOUNCE;
+                radius = xradius = yradius = eyeheight = aboveeye = 1;
+            }
+        } v;
+        v.o = pos;
+        if(v.radius != radius) v.radius = v.xradius = v.yradius = v.eyeheight = v.aboveeye = radius;
+        return collide(&v, dir, 0, false);
+    }
+};
+
+/*
+    seed particle position = avg(modelview * base2anim * spherepos)  
+    mapped transform = invert(curtri) * origtrig 
+    parented transform = parent{invert(curtri) * origtrig} * (invert(parent{base2anim}) * base2anim)
+*/
+
+void ragdolldata::constraindist()
+{
+    float invscale = 1.0f/scale;
+    loopv(skel->distlimits)
+    {
+        ragdollskel::distlimit &d = skel->distlimits[i];
+        vert &v1 = verts[d.vert[0]], &v2 = verts[d.vert[1]];
+        vec dir = vec(v2.pos).sub(v1.pos);
+        float dist = dir.magnitude()*invscale, cdist;
+        if(dist < d.mindist) cdist = d.mindist;
+        else if(dist > d.maxdist) cdist = d.maxdist;
+        else continue;
+        if(dist > 1e-4f) dir.mul(cdist*0.5f/dist);
+        else dir = vec(0, 0, cdist*0.5f/invscale);
+        vec center = vec(v1.pos).add(v2.pos).mul(0.5f);
+        v1.newpos.add(vec(center).sub(dir));
+        v1.weight++;
+        v2.newpos.add(vec(center).add(dir));
+        v2.weight++;
+    }
+}
+
+inline void ragdolldata::applyrotlimit(ragdollskel::tri &t1, ragdollskel::tri &t2, float angle, const vec &axis)
+{
+    vert &v1a = verts[t1.vert[0]], &v1b = verts[t1.vert[1]], &v1c = verts[t1.vert[2]],
+         &v2a = verts[t2.vert[0]], &v2b = verts[t2.vert[1]], &v2c = verts[t2.vert[2]];
+    vec m1 = vec(v1a.pos).add(v1b.pos).add(v1c.pos).div(3),
+        m2 = vec(v2a.pos).add(v2b.pos).add(v2c.pos).div(3),
+        q1a, q1b, q1c, q2a, q2b, q2c;
+    float w1 = q1a.cross(axis, vec(v1a.pos).sub(m1)).magnitude() +
+               q1b.cross(axis, vec(v1b.pos).sub(m1)).magnitude() +
+               q1c.cross(axis, vec(v1c.pos).sub(m1)).magnitude(),
+          w2 = q2a.cross(axis, vec(v2a.pos).sub(m2)).magnitude() +
+               q2b.cross(axis, vec(v2b.pos).sub(m2)).magnitude() +
+               q2c.cross(axis, vec(v2c.pos).sub(m2)).magnitude();
+    angle /= w1 + w2 + 1e-9f;
+    float a1 = angle*w2, a2 = -angle*w1,
+          s1 = sinf(a1), s2 = sinf(a2);
+    vec c1 = vec(axis).mul(1 - cosf(a1)), c2 = vec(axis).mul(1 - cosf(a2));
+    v1a.newpos.add(vec().cross(c1, q1a).madd(q1a, s1).add(v1a.pos));
+    v1a.weight++;
+    v1b.newpos.add(vec().cross(c1, q1b).madd(q1b, s1).add(v1b.pos));
+    v1b.weight++;
+    v1c.newpos.add(vec().cross(c1, q1c).madd(q1c, s1).add(v1c.pos));
+    v1c.weight++;
+    v2a.newpos.add(vec().cross(c2, q2a).madd(q2a, s2).add(v2a.pos));
+    v2a.weight++;
+    v2b.newpos.add(vec().cross(c2, q2b).madd(q2b, s2).add(v2b.pos));
+    v2b.weight++;
+    v2c.newpos.add(vec().cross(c2, q2c).madd(q2c, s2).add(v2c.pos));
+    v2c.weight++;
+}
+    
+void ragdolldata::constrainrot()
+{
+    loopv(skel->rotlimits)
+    {
+        ragdollskel::rotlimit &r = skel->rotlimits[i];
+        matrix3 rot;
+        rot.mul(tris[r.tri[0]], r.middle);
+        rot.multranspose(tris[r.tri[1]]);
+
+        vec axis;
+        float angle;
+        if(!rot.calcangleaxis(angle, axis)) continue;
+        angle = r.maxangle - fabs(angle);
+        if(angle >= 0) continue; 
+        angle += 1e-3f;
+
+        applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis);
+    }
+}
+
+VAR(ragdolltimestepmin, 1, 5, 50);
+VAR(ragdolltimestepmax, 1, 10, 50);
+FVAR(ragdollrotfric, 0, 0.85f, 1);
+FVAR(ragdollrotfricstop, 0, 0.1f, 1);
+
+void ragdolldata::calcrotfriction()
+{
+    loopv(skel->rotfrictions)
+    {
+        ragdollskel::rotfriction &r = skel->rotfrictions[i];
+        r.middle.transposemul(tris[r.tri[0]], tris[r.tri[1]]);
+    }
+}
+
+void ragdolldata::applyrotfriction(float ts)
+{
+    calctris();
+    float stopangle = 2*M_PI*ts*ragdollrotfricstop, rotfric = 1.0f - pow(ragdollrotfric, ts*1000.0f/ragdolltimestepmin);
+    loopv(skel->rotfrictions)
+    {
+        ragdollskel::rotfriction &r = skel->rotfrictions[i];
+        matrix3 rot;
+        rot.mul(tris[r.tri[0]], r.middle);
+        rot.multranspose(tris[r.tri[1]]);
+
+        vec axis;
+        float angle;
+        if(rot.calcangleaxis(angle, axis))
+        {
+            angle *= -(fabs(angle) >= stopangle ? rotfric : 1.0f);
+            applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis);
+        }
+    }
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        if(v.weight) v.pos = v.newpos.div(v.weight);
+        v.newpos = vec(0, 0, 0);
+        v.weight = 0;
+    }
+}
+
+void ragdolldata::tryunstick(float speed)
+{
+    vec unstuck(0, 0, 0);
+    int stuck = 0;
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        if(v.stuck)
+        {
+            if(collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius)) { stuck++; continue; }
+            v.stuck = false;
+        }
+        unstuck.add(v.pos);
+    }
+    unsticks = 0;
+    if(!stuck || stuck >= skel->verts.length()) return;
+    unstuck.div(skel->verts.length() - stuck);
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        if(v.stuck)
+        {
+            v.pos.add(vec(unstuck).sub(v.pos).rescale(speed));
+            unsticks++;
+        }
+    }
+}
+
+void ragdolldata::updatepos()
+{
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        if(v.weight)
+        {
+            v.newpos.div(v.weight);
+            if(!collidevert(v.newpos, vec(v.newpos).sub(v.pos), skel->verts[i].radius)) v.pos = v.newpos;
+            else
+            {
+                vec dir = vec(v.newpos).sub(v.oldpos);
+                if(dir.dot(collidewall) < 0) v.oldpos = vec(v.pos).sub(dir.reflect(collidewall));
+                v.collided = true;
+            }
+        }
+        v.newpos = vec(0, 0, 0);
+        v.weight = 0;
+    }
+}
+
+VAR(ragdollconstrain, 1, 5, 100);
+
+void ragdolldata::constrain()
+{
+    loopi(ragdollconstrain)
+    {
+        constraindist();
+        updatepos();
+
+        calctris();
+        constrainrot();
+        updatepos();
+    }
+}
+
+FVAR(ragdollbodyfric, 0, 0.95f, 1);
+FVAR(ragdollbodyfricscale, 0, 2, 10);
+FVAR(ragdollwaterfric, 0, 0.85f, 1);
+FVAR(ragdollgroundfric, 0, 0.8f, 1);
+FVAR(ragdollairfric, 0, 0.996f, 1);
+FVAR(ragdollunstick, 0, 10, 1e3f);
+VAR(ragdollexpireoffset, 0, 1500, 30000);
+VAR(ragdollwaterexpireoffset, 0, 3000, 30000);
+
+void ragdolldata::move(dynent *pl, float ts)
+{
+    extern const float GRAVITY;
+    if(collidemillis && lastmillis > collidemillis) return;
+
+    int material = lookupmaterial(vec(center.x, center.y, center.z + radius/2));
+    bool water = isliquid(material&MATF_VOLUME);
+    if(!pl->inwater && water) game::physicstrigger(pl, true, 0, -1, material&MATF_VOLUME);
+    else if(pl->inwater && !water)
+    {
+        material = lookupmaterial(center);
+        water = isliquid(material&MATF_VOLUME);
+        if(!water) game::physicstrigger(pl, true, 0, 1, pl->inwater);
+    }
+    pl->inwater = water ? material&MATF_VOLUME : MAT_AIR;
+   
+    calcrotfriction(); 
+       float tsfric = timestep ? ts/timestep : 1,
+                 airfric = ragdollairfric + min((ragdollbodyfricscale*collisions)/skel->verts.length(), 1.0f)*(ragdollbodyfric - ragdollairfric);
+    collisions = 0;
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        vec dpos = vec(v.pos).sub(v.oldpos);
+        dpos.z -= GRAVITY*ts*ts;
+        if(water) dpos.z += 0.25f*sinf(detrnd(size_t(this)+i, 360)*RAD + lastmillis/10000.0f*M_PI)*ts;
+        dpos.mul(pow((water ? ragdollwaterfric : 1.0f) * (v.collided ? ragdollgroundfric : airfric), ts*1000.0f/ragdolltimestepmin)*tsfric);
+        v.oldpos = v.pos;
+        v.pos.add(dpos);
+    }
+    applyrotfriction(ts);
+    loopv(skel->verts)
+    {
+        vert &v = verts[i];
+        if(v.pos.z < 0) { v.pos.z = 0; v.oldpos = v.pos; collisions++; }
+        vec dir = vec(v.pos).sub(v.oldpos);
+        v.collided = collidevert(v.pos, dir, skel->verts[i].radius);
+        if(v.collided)
+        {
+            v.pos = v.oldpos;
+            v.oldpos.sub(dir.reflect(collidewall));
+            collisions++;
+        }   
+    }
+
+    if(unsticks && ragdollunstick) tryunstick(ts*ragdollunstick);
+    timestep = ts;
+    if(collisions)
+    {
+        floating = 0;
+        if(!collidemillis) collidemillis = lastmillis + (water ? ragdollwaterexpireoffset : ragdollexpireoffset);
+    }
+    else if(++floating > 1 && lastmillis < collidemillis) collidemillis = 0;
+
+    constrain();
+    calctris();
+    calcboundsphere();
+}    
+
+FVAR(ragdolleyesmooth, 0, 0.5f, 1);
+VAR(ragdolleyesmoothmillis, 1, 250, 10000);
+
+void moveragdoll(dynent *d)
+{
+    if(!curtime || !d->ragdoll) return;
+
+    if(!d->ragdoll->collidemillis || lastmillis < d->ragdoll->collidemillis)
+    {
+        int lastmove = d->ragdoll->lastmove;
+        while(d->ragdoll->lastmove + (lastmove == d->ragdoll->lastmove ? ragdolltimestepmin : ragdolltimestepmax) <= lastmillis)
+        {
+            int timestep = min(ragdolltimestepmax, lastmillis - d->ragdoll->lastmove);
+            d->ragdoll->move(d, timestep/1000.0f);
+            d->ragdoll->lastmove += timestep;
+        }
+    }
+
+    vec eye = d->ragdoll->skel->eye >= 0 ? d->ragdoll->verts[d->ragdoll->skel->eye].pos : d->ragdoll->center;
+    eye.add(d->ragdoll->offset);
+    float k = pow(ragdolleyesmooth, float(curtime)/ragdolleyesmoothmillis);
+    d->o.mul(k).add(eye.mul(1-k));
+}
+
+void cleanragdoll(dynent *d)
+{
+    DELETEP(d->ragdoll);
+}
+
diff --git a/src/engine/rendergl.cpp b/src/engine/rendergl.cpp
new file mode 100644 (file)
index 0000000..6049951
--- /dev/null
@@ -0,0 +1,2388 @@
+// rendergl.cpp: core opengl rendering stuff
+
+#include "engine.h"
+
+bool hasVAO = false, hasFBO = false, hasAFBO = false, hasDS = false, hasTF = false, hasTRG = false, hasTSW = false, hasS3TC = false, hasFXT1 = false, hasLATC = false, hasRGTC = false, hasAF = false, hasFBB = false, hasUBO = false, hasMBR = false;
+
+VAR(glversion, 1, 0, 0);
+VAR(glslversion, 1, 0, 0);
+VAR(glcompat, 1, 0, 0);
+
+// OpenGL 1.3
+#ifdef WIN32
+PFNGLACTIVETEXTUREPROC       glActiveTexture_       = NULL;
+
+PFNGLBLENDEQUATIONEXTPROC glBlendEquation_ = NULL;
+PFNGLBLENDCOLOREXTPROC    glBlendColor_    = NULL;
+
+PFNGLTEXIMAGE3DPROC        glTexImage3D_        = NULL;
+PFNGLTEXSUBIMAGE3DPROC     glTexSubImage3D_     = NULL;
+PFNGLCOPYTEXSUBIMAGE3DPROC glCopyTexSubImage3D_ = NULL;
+
+PFNGLCOMPRESSEDTEXIMAGE3DPROC    glCompressedTexImage3D_    = NULL;
+PFNGLCOMPRESSEDTEXIMAGE2DPROC    glCompressedTexImage2D_    = NULL;
+PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC glCompressedTexSubImage3D_ = NULL;
+PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC glCompressedTexSubImage2D_ = NULL;
+PFNGLGETCOMPRESSEDTEXIMAGEPROC   glGetCompressedTexImage_   = NULL;
+
+PFNGLDRAWRANGEELEMENTSPROC glDrawRangeElements_ = NULL;
+#endif
+
+// OpenGL 2.0
+#ifndef __APPLE__
+PFNGLMULTIDRAWARRAYSPROC   glMultiDrawArrays_   = NULL;
+PFNGLMULTIDRAWELEMENTSPROC glMultiDrawElements_ = NULL;
+
+PFNGLBLENDFUNCSEPARATEPROC     glBlendFuncSeparate_     = NULL;
+PFNGLBLENDEQUATIONSEPARATEPROC glBlendEquationSeparate_ = NULL;
+PFNGLSTENCILOPSEPARATEPROC     glStencilOpSeparate_     = NULL;
+PFNGLSTENCILFUNCSEPARATEPROC   glStencilFuncSeparate_   = NULL;
+PFNGLSTENCILMASKSEPARATEPROC   glStencilMaskSeparate_   = NULL;
+
+PFNGLGENBUFFERSPROC       glGenBuffers_       = NULL;
+PFNGLBINDBUFFERPROC       glBindBuffer_       = NULL;
+PFNGLMAPBUFFERPROC        glMapBuffer_        = NULL;
+PFNGLUNMAPBUFFERPROC      glUnmapBuffer_      = NULL;
+PFNGLBUFFERDATAPROC       glBufferData_       = NULL;
+PFNGLBUFFERSUBDATAPROC    glBufferSubData_    = NULL;
+PFNGLDELETEBUFFERSPROC    glDeleteBuffers_    = NULL;
+PFNGLGETBUFFERSUBDATAPROC glGetBufferSubData_ = NULL;
+
+PFNGLGENQUERIESPROC        glGenQueries_        = NULL;
+PFNGLDELETEQUERIESPROC     glDeleteQueries_     = NULL;
+PFNGLBEGINQUERYPROC        glBeginQuery_        = NULL;
+PFNGLENDQUERYPROC          glEndQuery_          = NULL;
+PFNGLGETQUERYIVPROC        glGetQueryiv_        = NULL;
+PFNGLGETQUERYOBJECTIVPROC  glGetQueryObjectiv_  = NULL;
+PFNGLGETQUERYOBJECTUIVPROC glGetQueryObjectuiv_ = NULL;
+
+PFNGLCREATEPROGRAMPROC            glCreateProgram_            = NULL;
+PFNGLDELETEPROGRAMPROC            glDeleteProgram_            = NULL;
+PFNGLUSEPROGRAMPROC               glUseProgram_               = NULL;
+PFNGLCREATESHADERPROC             glCreateShader_             = NULL;
+PFNGLDELETESHADERPROC             glDeleteShader_             = NULL;
+PFNGLSHADERSOURCEPROC             glShaderSource_             = NULL;
+PFNGLCOMPILESHADERPROC            glCompileShader_            = NULL;
+PFNGLGETSHADERIVPROC              glGetShaderiv_              = NULL;
+PFNGLGETPROGRAMIVPROC             glGetProgramiv_             = NULL;
+PFNGLATTACHSHADERPROC             glAttachShader_             = NULL;
+PFNGLGETPROGRAMINFOLOGPROC        glGetProgramInfoLog_        = NULL;
+PFNGLGETSHADERINFOLOGPROC         glGetShaderInfoLog_         = NULL;
+PFNGLLINKPROGRAMPROC              glLinkProgram_              = NULL;
+PFNGLGETUNIFORMLOCATIONPROC       glGetUniformLocation_       = NULL;
+PFNGLUNIFORM1FPROC                glUniform1f_                = NULL;
+PFNGLUNIFORM2FPROC                glUniform2f_                = NULL;
+PFNGLUNIFORM3FPROC                glUniform3f_                = NULL;
+PFNGLUNIFORM4FPROC                glUniform4f_                = NULL;
+PFNGLUNIFORM1FVPROC               glUniform1fv_               = NULL;
+PFNGLUNIFORM2FVPROC               glUniform2fv_               = NULL;
+PFNGLUNIFORM3FVPROC               glUniform3fv_               = NULL;
+PFNGLUNIFORM4FVPROC               glUniform4fv_               = NULL;
+PFNGLUNIFORM1IPROC                glUniform1i_                = NULL;
+PFNGLUNIFORM2IPROC                glUniform2i_                = NULL;
+PFNGLUNIFORM3IPROC                glUniform3i_                = NULL;
+PFNGLUNIFORM4IPROC                glUniform4i_                = NULL;
+PFNGLUNIFORM1IVPROC               glUniform1iv_               = NULL;
+PFNGLUNIFORM2IVPROC               glUniform2iv_               = NULL;
+PFNGLUNIFORM3IVPROC               glUniform3iv_               = NULL;
+PFNGLUNIFORM4IVPROC               glUniform4iv_               = NULL;
+PFNGLUNIFORMMATRIX2FVPROC         glUniformMatrix2fv_         = NULL;
+PFNGLUNIFORMMATRIX3FVPROC         glUniformMatrix3fv_         = NULL;
+PFNGLUNIFORMMATRIX4FVPROC         glUniformMatrix4fv_         = NULL;
+PFNGLBINDATTRIBLOCATIONPROC       glBindAttribLocation_       = NULL;
+PFNGLGETACTIVEUNIFORMPROC         glGetActiveUniform_         = NULL;
+PFNGLENABLEVERTEXATTRIBARRAYPROC  glEnableVertexAttribArray_  = NULL;
+PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray_ = NULL;
+
+PFNGLVERTEXATTRIB1FPROC           glVertexAttrib1f_           = NULL;
+PFNGLVERTEXATTRIB1FVPROC          glVertexAttrib1fv_          = NULL;
+PFNGLVERTEXATTRIB1SPROC           glVertexAttrib1s_           = NULL;
+PFNGLVERTEXATTRIB1SVPROC          glVertexAttrib1sv_          = NULL;
+PFNGLVERTEXATTRIB2FPROC           glVertexAttrib2f_           = NULL;
+PFNGLVERTEXATTRIB2FVPROC          glVertexAttrib2fv_          = NULL;
+PFNGLVERTEXATTRIB2SPROC           glVertexAttrib2s_           = NULL;
+PFNGLVERTEXATTRIB2SVPROC          glVertexAttrib2sv_          = NULL;
+PFNGLVERTEXATTRIB3FPROC           glVertexAttrib3f_           = NULL;
+PFNGLVERTEXATTRIB3FVPROC          glVertexAttrib3fv_          = NULL;
+PFNGLVERTEXATTRIB3SPROC           glVertexAttrib3s_           = NULL;
+PFNGLVERTEXATTRIB3SVPROC          glVertexAttrib3sv_          = NULL;
+PFNGLVERTEXATTRIB4FPROC           glVertexAttrib4f_           = NULL;
+PFNGLVERTEXATTRIB4FVPROC          glVertexAttrib4fv_          = NULL;
+PFNGLVERTEXATTRIB4SPROC           glVertexAttrib4s_           = NULL;
+PFNGLVERTEXATTRIB4SVPROC          glVertexAttrib4sv_          = NULL;
+PFNGLVERTEXATTRIB4BVPROC          glVertexAttrib4bv_          = NULL;
+PFNGLVERTEXATTRIB4IVPROC          glVertexAttrib4iv_          = NULL;
+PFNGLVERTEXATTRIB4UBVPROC         glVertexAttrib4ubv_         = NULL;
+PFNGLVERTEXATTRIB4UIVPROC         glVertexAttrib4uiv_         = NULL;
+PFNGLVERTEXATTRIB4USVPROC         glVertexAttrib4usv_         = NULL;
+PFNGLVERTEXATTRIB4NBVPROC         glVertexAttrib4Nbv_         = NULL;
+PFNGLVERTEXATTRIB4NIVPROC         glVertexAttrib4Niv_         = NULL;
+PFNGLVERTEXATTRIB4NUBPROC         glVertexAttrib4Nub_         = NULL;
+PFNGLVERTEXATTRIB4NUBVPROC        glVertexAttrib4Nubv_        = NULL;
+PFNGLVERTEXATTRIB4NUIVPROC        glVertexAttrib4Nuiv_        = NULL;
+PFNGLVERTEXATTRIB4NUSVPROC        glVertexAttrib4Nusv_        = NULL;
+PFNGLVERTEXATTRIBPOINTERPROC      glVertexAttribPointer_      = NULL;
+
+PFNGLDRAWBUFFERSPROC glDrawBuffers_ = NULL;
+#endif
+
+// OpenGL 3.0
+PFNGLGETSTRINGIPROC           glGetStringi_           = NULL;
+PFNGLBINDFRAGDATALOCATIONPROC glBindFragDataLocation_ = NULL;
+
+// GL_EXT_framebuffer_object
+PFNGLBINDRENDERBUFFERPROC        glBindRenderbuffer_        = NULL;
+PFNGLDELETERENDERBUFFERSPROC     glDeleteRenderbuffers_     = NULL;
+PFNGLGENFRAMEBUFFERSPROC         glGenRenderbuffers_        = NULL;
+PFNGLRENDERBUFFERSTORAGEPROC     glRenderbufferStorage_     = NULL;
+PFNGLCHECKFRAMEBUFFERSTATUSPROC  glCheckFramebufferStatus_  = NULL;
+PFNGLBINDFRAMEBUFFERPROC         glBindFramebuffer_         = NULL;
+PFNGLDELETEFRAMEBUFFERSPROC      glDeleteFramebuffers_      = NULL;
+PFNGLGENFRAMEBUFFERSPROC         glGenFramebuffers_         = NULL;
+PFNGLFRAMEBUFFERTEXTURE2DPROC    glFramebufferTexture2D_    = NULL;
+PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer_ = NULL;
+PFNGLGENERATEMIPMAPPROC          glGenerateMipmap_          = NULL;
+
+// GL_EXT_framebuffer_blit
+PFNGLBLITFRAMEBUFFERPROC         glBlitFramebuffer_         = NULL;
+
+// GL_ARB_uniform_buffer_object
+PFNGLGETUNIFORMINDICESPROC       glGetUniformIndices_       = NULL;
+PFNGLGETACTIVEUNIFORMSIVPROC     glGetActiveUniformsiv_     = NULL;
+PFNGLGETUNIFORMBLOCKINDEXPROC    glGetUniformBlockIndex_    = NULL;
+PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv_ = NULL;
+PFNGLUNIFORMBLOCKBINDINGPROC     glUniformBlockBinding_     = NULL;
+PFNGLBINDBUFFERBASEPROC          glBindBufferBase_          = NULL;
+PFNGLBINDBUFFERRANGEPROC         glBindBufferRange_         = NULL;
+
+// GL_ARB_map_buffer_range
+PFNGLMAPBUFFERRANGEPROC         glMapBufferRange_         = NULL;
+PFNGLFLUSHMAPPEDBUFFERRANGEPROC glFlushMappedBufferRange_ = NULL;
+
+// GL_ARB_vertex_array_object
+PFNGLBINDVERTEXARRAYPROC    glBindVertexArray_    = NULL;
+PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays_ = NULL;
+PFNGLGENVERTEXARRAYSPROC    glGenVertexArrays_    = NULL;
+PFNGLISVERTEXARRAYPROC      glIsVertexArray_      = NULL;
+
+void *getprocaddress(const char *name)
+{
+    return SDL_GL_GetProcAddress(name);
+}
+
+VARP(ati_skybox_bug, 0, 0, 1);
+VAR(ati_minmax_bug, 0, 0, 1);
+VAR(ati_cubemap_bug, 0, 0, 1);
+VAR(intel_vertexarray_bug, 0, 0, 1);
+VAR(intel_mapbufferrange_bug, 0, 0, 1);
+VAR(mesa_swap_bug, 0, 0, 1);
+VAR(minimizetcusage, 1, 0, 0);
+VAR(useubo, 1, 0, 0);
+VAR(usetexcompress, 1, 0, 0);
+VAR(rtscissor, 0, 1, 1);
+VAR(blurtile, 0, 1, 1);
+VAR(rtsharefb, 0, 1, 1);
+
+VAR(dbgexts, 0, 0, 1);
+
+hashset<const char *> glexts;
+
+void parseglexts()
+{
+    if(glversion >= 300)
+    {
+        GLint numexts = 0;
+        glGetIntegerv(GL_NUM_EXTENSIONS, &numexts);
+        loopi(numexts)
+        {
+            const char *ext = (const char *)glGetStringi_(GL_EXTENSIONS, i);
+            glexts.add(newstring(ext));
+        }
+    }
+    else
+    {
+        const char *exts = (const char *)glGetString(GL_EXTENSIONS);
+        for(;;)
+        {
+            while(*exts == ' ') exts++;
+            if(!*exts) break;
+            const char *ext = exts;
+            while(*exts && *exts != ' ') exts++;
+            if(exts > ext) glexts.add(newstring(ext, size_t(exts-ext)));
+        }
+    }
+}
+
+bool hasext(const char *ext)
+{
+    return glexts.access(ext)!=NULL;
+}
+
+void gl_checkextensions()
+{
+    const char *vendor = (const char *)glGetString(GL_VENDOR);
+    const char *renderer = (const char *)glGetString(GL_RENDERER);
+    const char *version = (const char *)glGetString(GL_VERSION);
+    conoutf(CON_INIT, "Renderer: %s (%s)", renderer, vendor);
+    conoutf(CON_INIT, "Driver: %s", version);
+
+    bool mesa = false, intel = false, ati = false, nvidia = false;
+    if(strstr(renderer, "Mesa") || strstr(version, "Mesa"))
+    {
+        mesa = true;
+        if(strstr(renderer, "Intel")) intel = true;
+    }
+    else if(strstr(vendor, "NVIDIA"))
+        nvidia = true;
+    else if(strstr(vendor, "ATI") || strstr(vendor, "Advanced Micro Devices"))
+        ati = true;
+    else if(strstr(vendor, "Intel"))
+        intel = true;
+
+    uint glmajorversion, glminorversion;
+    if(sscanf(version, " %u.%u", &glmajorversion, &glminorversion) != 2) glversion = 100;
+    else glversion = glmajorversion*100 + glminorversion*10;
+
+    if(glversion < 200) fatal("OpenGL 2.0 or greater is required!");
+
+#ifdef WIN32
+    glActiveTexture_ =            (PFNGLACTIVETEXTUREPROC)            getprocaddress("glActiveTexture");
+
+    glBlendEquation_ =            (PFNGLBLENDEQUATIONPROC)            getprocaddress("glBlendEquation");
+    glBlendColor_ =               (PFNGLBLENDCOLORPROC)               getprocaddress("glBlendColor");
+
+    glTexImage3D_ =               (PFNGLTEXIMAGE3DPROC)               getprocaddress("glTexImage3D");
+    glTexSubImage3D_ =            (PFNGLTEXSUBIMAGE3DPROC)            getprocaddress("glTexSubImage3D");
+    glCopyTexSubImage3D_ =        (PFNGLCOPYTEXSUBIMAGE3DPROC)        getprocaddress("glCopyTexSubImage3D");
+
+    glCompressedTexImage3D_ =     (PFNGLCOMPRESSEDTEXIMAGE3DPROC)     getprocaddress("glCompressedTexImage3D");
+    glCompressedTexImage2D_ =     (PFNGLCOMPRESSEDTEXIMAGE2DPROC)     getprocaddress("glCompressedTexImage2D");
+    glCompressedTexSubImage3D_ =  (PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)  getprocaddress("glCompressedTexSubImage3D");
+    glCompressedTexSubImage2D_ =  (PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)  getprocaddress("glCompressedTexSubImage2D");
+    glGetCompressedTexImage_ =    (PFNGLGETCOMPRESSEDTEXIMAGEPROC)    getprocaddress("glGetCompressedTexImage");
+
+    glDrawRangeElements_ =        (PFNGLDRAWRANGEELEMENTSPROC)        getprocaddress("glDrawRangeElements");
+#endif
+
+#ifndef __APPLE__
+    glMultiDrawArrays_ =          (PFNGLMULTIDRAWARRAYSPROC)          getprocaddress("glMultiDrawArrays");
+    glMultiDrawElements_ =        (PFNGLMULTIDRAWELEMENTSPROC)        getprocaddress("glMultiDrawElements");
+
+    glBlendFuncSeparate_ =        (PFNGLBLENDFUNCSEPARATEPROC)        getprocaddress("glBlendFuncSeparate");
+    glBlendEquationSeparate_ =    (PFNGLBLENDEQUATIONSEPARATEPROC)    getprocaddress("glBlendEquationSeparate");
+    glStencilOpSeparate_ =        (PFNGLSTENCILOPSEPARATEPROC)        getprocaddress("glStencilOpSeparate");
+    glStencilFuncSeparate_ =      (PFNGLSTENCILFUNCSEPARATEPROC)      getprocaddress("glStencilFuncSeparate");
+    glStencilMaskSeparate_ =      (PFNGLSTENCILMASKSEPARATEPROC)      getprocaddress("glStencilMaskSeparate");
+
+    glGenBuffers_ =               (PFNGLGENBUFFERSPROC)               getprocaddress("glGenBuffers");
+    glBindBuffer_ =               (PFNGLBINDBUFFERPROC)               getprocaddress("glBindBuffer");
+    glMapBuffer_ =                (PFNGLMAPBUFFERPROC)                getprocaddress("glMapBuffer");
+    glUnmapBuffer_ =              (PFNGLUNMAPBUFFERPROC)              getprocaddress("glUnmapBuffer");
+    glBufferData_ =               (PFNGLBUFFERDATAPROC)               getprocaddress("glBufferData");
+    glBufferSubData_ =            (PFNGLBUFFERSUBDATAPROC)            getprocaddress("glBufferSubData");
+    glDeleteBuffers_ =            (PFNGLDELETEBUFFERSPROC)            getprocaddress("glDeleteBuffers");
+    glGetBufferSubData_ =         (PFNGLGETBUFFERSUBDATAPROC)         getprocaddress("glGetBufferSubData");
+
+    glGetQueryiv_ =               (PFNGLGETQUERYIVPROC)               getprocaddress("glGetQueryiv");
+    glGenQueries_ =               (PFNGLGENQUERIESPROC)               getprocaddress("glGenQueries");
+    glDeleteQueries_ =            (PFNGLDELETEQUERIESPROC)            getprocaddress("glDeleteQueries");
+    glBeginQuery_ =               (PFNGLBEGINQUERYPROC)               getprocaddress("glBeginQuery");
+    glEndQuery_ =                 (PFNGLENDQUERYPROC)                 getprocaddress("glEndQuery");
+    glGetQueryObjectiv_ =         (PFNGLGETQUERYOBJECTIVPROC)         getprocaddress("glGetQueryObjectiv");
+    glGetQueryObjectuiv_ =        (PFNGLGETQUERYOBJECTUIVPROC)        getprocaddress("glGetQueryObjectuiv");
+
+    glCreateProgram_ =            (PFNGLCREATEPROGRAMPROC)            getprocaddress("glCreateProgram");
+    glDeleteProgram_ =            (PFNGLDELETEPROGRAMPROC)            getprocaddress("glDeleteProgram");
+    glUseProgram_ =               (PFNGLUSEPROGRAMPROC)               getprocaddress("glUseProgram");
+    glCreateShader_ =             (PFNGLCREATESHADERPROC)             getprocaddress("glCreateShader");
+    glDeleteShader_ =             (PFNGLDELETESHADERPROC)             getprocaddress("glDeleteShader");
+    glShaderSource_ =             (PFNGLSHADERSOURCEPROC)             getprocaddress("glShaderSource");
+    glCompileShader_ =            (PFNGLCOMPILESHADERPROC)            getprocaddress("glCompileShader");
+    glGetShaderiv_ =              (PFNGLGETSHADERIVPROC)              getprocaddress("glGetShaderiv");
+    glGetProgramiv_ =             (PFNGLGETPROGRAMIVPROC)             getprocaddress("glGetProgramiv");
+    glAttachShader_ =             (PFNGLATTACHSHADERPROC)             getprocaddress("glAttachShader");
+    glGetProgramInfoLog_ =        (PFNGLGETPROGRAMINFOLOGPROC)        getprocaddress("glGetProgramInfoLog");
+    glGetShaderInfoLog_ =         (PFNGLGETSHADERINFOLOGPROC)         getprocaddress("glGetShaderInfoLog");
+    glLinkProgram_ =              (PFNGLLINKPROGRAMPROC)              getprocaddress("glLinkProgram");
+    glGetUniformLocation_ =       (PFNGLGETUNIFORMLOCATIONPROC)       getprocaddress("glGetUniformLocation");
+    glUniform1f_ =                (PFNGLUNIFORM1FPROC)                getprocaddress("glUniform1f");
+    glUniform2f_ =                (PFNGLUNIFORM2FPROC)                getprocaddress("glUniform2f");
+    glUniform3f_ =                (PFNGLUNIFORM3FPROC)                getprocaddress("glUniform3f");
+    glUniform4f_ =                (PFNGLUNIFORM4FPROC)                getprocaddress("glUniform4f");
+    glUniform1fv_ =               (PFNGLUNIFORM1FVPROC)               getprocaddress("glUniform1fv");
+    glUniform2fv_ =               (PFNGLUNIFORM2FVPROC)               getprocaddress("glUniform2fv");
+    glUniform3fv_ =               (PFNGLUNIFORM3FVPROC)               getprocaddress("glUniform3fv");
+    glUniform4fv_ =               (PFNGLUNIFORM4FVPROC)               getprocaddress("glUniform4fv");
+    glUniform1i_ =                (PFNGLUNIFORM1IPROC)                getprocaddress("glUniform1i");
+    glUniform2i_ =                (PFNGLUNIFORM2IPROC)                getprocaddress("glUniform2i");
+    glUniform3i_ =                (PFNGLUNIFORM3IPROC)                getprocaddress("glUniform3i");
+    glUniform4i_ =                (PFNGLUNIFORM4IPROC)                getprocaddress("glUniform4i");
+    glUniform1iv_ =               (PFNGLUNIFORM1IVPROC)               getprocaddress("glUniform1iv");
+    glUniform2iv_ =               (PFNGLUNIFORM2IVPROC)               getprocaddress("glUniform2iv");
+    glUniform3iv_ =               (PFNGLUNIFORM3IVPROC)               getprocaddress("glUniform3iv");
+    glUniform4iv_ =               (PFNGLUNIFORM4IVPROC)               getprocaddress("glUniform4iv");
+    glUniformMatrix2fv_ =         (PFNGLUNIFORMMATRIX2FVPROC)         getprocaddress("glUniformMatrix2fv");
+    glUniformMatrix3fv_ =         (PFNGLUNIFORMMATRIX3FVPROC)         getprocaddress("glUniformMatrix3fv");
+    glUniformMatrix4fv_ =         (PFNGLUNIFORMMATRIX4FVPROC)         getprocaddress("glUniformMatrix4fv");
+    glBindAttribLocation_ =       (PFNGLBINDATTRIBLOCATIONPROC)       getprocaddress("glBindAttribLocation");
+    glGetActiveUniform_ =         (PFNGLGETACTIVEUNIFORMPROC)         getprocaddress("glGetActiveUniform");
+    glEnableVertexAttribArray_ =  (PFNGLENABLEVERTEXATTRIBARRAYPROC)  getprocaddress("glEnableVertexAttribArray");
+    glDisableVertexAttribArray_ = (PFNGLDISABLEVERTEXATTRIBARRAYPROC) getprocaddress("glDisableVertexAttribArray");
+
+    glVertexAttrib1f_ =           (PFNGLVERTEXATTRIB1FPROC)           getprocaddress("glVertexAttrib1f");
+    glVertexAttrib1fv_ =          (PFNGLVERTEXATTRIB1FVPROC)          getprocaddress("glVertexAttrib1fv");
+    glVertexAttrib1s_ =           (PFNGLVERTEXATTRIB1SPROC)           getprocaddress("glVertexAttrib1s");
+    glVertexAttrib1sv_ =          (PFNGLVERTEXATTRIB1SVPROC)          getprocaddress("glVertexAttrib1sv");
+    glVertexAttrib2f_ =           (PFNGLVERTEXATTRIB2FPROC)           getprocaddress("glVertexAttrib2f");
+    glVertexAttrib2fv_ =          (PFNGLVERTEXATTRIB2FVPROC)          getprocaddress("glVertexAttrib2fv");
+    glVertexAttrib2s_ =           (PFNGLVERTEXATTRIB2SPROC)           getprocaddress("glVertexAttrib2s");
+    glVertexAttrib2sv_ =          (PFNGLVERTEXATTRIB2SVPROC)          getprocaddress("glVertexAttrib2sv");
+    glVertexAttrib3f_ =           (PFNGLVERTEXATTRIB3FPROC)           getprocaddress("glVertexAttrib3f");
+    glVertexAttrib3fv_ =          (PFNGLVERTEXATTRIB3FVPROC)          getprocaddress("glVertexAttrib3fv");
+    glVertexAttrib3s_ =           (PFNGLVERTEXATTRIB3SPROC)           getprocaddress("glVertexAttrib3s");
+    glVertexAttrib3sv_ =          (PFNGLVERTEXATTRIB3SVPROC)          getprocaddress("glVertexAttrib3sv");
+    glVertexAttrib4f_ =           (PFNGLVERTEXATTRIB4FPROC)           getprocaddress("glVertexAttrib4f");
+    glVertexAttrib4fv_ =          (PFNGLVERTEXATTRIB4FVPROC)          getprocaddress("glVertexAttrib4fv");
+    glVertexAttrib4s_ =           (PFNGLVERTEXATTRIB4SPROC)           getprocaddress("glVertexAttrib4s");
+    glVertexAttrib4sv_ =          (PFNGLVERTEXATTRIB4SVPROC)          getprocaddress("glVertexAttrib4sv");
+    glVertexAttrib4bv_ =          (PFNGLVERTEXATTRIB4BVPROC)          getprocaddress("glVertexAttrib4bv");
+    glVertexAttrib4iv_ =          (PFNGLVERTEXATTRIB4IVPROC)          getprocaddress("glVertexAttrib4iv");
+    glVertexAttrib4ubv_ =         (PFNGLVERTEXATTRIB4UBVPROC)         getprocaddress("glVertexAttrib4ubv");
+    glVertexAttrib4uiv_ =         (PFNGLVERTEXATTRIB4UIVPROC)         getprocaddress("glVertexAttrib4uiv");
+    glVertexAttrib4usv_ =         (PFNGLVERTEXATTRIB4USVPROC)         getprocaddress("glVertexAttrib4usv");
+    glVertexAttrib4Nbv_ =         (PFNGLVERTEXATTRIB4NBVPROC)         getprocaddress("glVertexAttrib4Nbv");
+    glVertexAttrib4Niv_ =         (PFNGLVERTEXATTRIB4NIVPROC)         getprocaddress("glVertexAttrib4Niv");
+    glVertexAttrib4Nub_ =         (PFNGLVERTEXATTRIB4NUBPROC)         getprocaddress("glVertexAttrib4Nub");
+    glVertexAttrib4Nubv_ =        (PFNGLVERTEXATTRIB4NUBVPROC)        getprocaddress("glVertexAttrib4Nubv");
+    glVertexAttrib4Nuiv_ =        (PFNGLVERTEXATTRIB4NUIVPROC)        getprocaddress("glVertexAttrib4Nuiv");
+    glVertexAttrib4Nusv_ =        (PFNGLVERTEXATTRIB4NUSVPROC)        getprocaddress("glVertexAttrib4Nusv");
+    glVertexAttribPointer_ =      (PFNGLVERTEXATTRIBPOINTERPROC)      getprocaddress("glVertexAttribPointer");
+
+    glDrawBuffers_ =              (PFNGLDRAWBUFFERSPROC)              getprocaddress("glDrawBuffers");
+#endif
+
+    if(glversion >= 300)
+    {
+        glGetStringi_ =            (PFNGLGETSTRINGIPROC)          getprocaddress("glGetStringi");
+        glBindFragDataLocation_ =  (PFNGLBINDFRAGDATALOCATIONPROC)getprocaddress("glBindFragDataLocation");
+    }
+
+    const char *glslstr = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
+    uint glslmajorversion, glslminorversion;
+    if(glslstr && sscanf(glslstr, " %u.%u", &glslmajorversion, &glslminorversion) == 2) glslversion = glslmajorversion*100 + glslminorversion;
+
+    if(glslversion < 120) fatal("GLSL 1.20 or greater is required!");
+
+    parseglexts();
+
+    GLint val;
+    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &val);
+    hwtexsize = val;
+    glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &val);
+    hwcubetexsize = val;
+
+    if(glversion >= 300 || hasext("GL_ARB_texture_float") || hasext("GL_ATI_texture_float"))
+    {
+        hasTF = true;
+        if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_float extension.");
+        shadowmap = 1;
+        extern int smoothshadowmappeel;
+        smoothshadowmappeel = 1;
+    }
+
+    if(glversion >= 300 || hasext("GL_ARB_texture_rg"))
+    {
+        hasTRG = true;
+        if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_rg extension.");
+    }
+
+    if(glversion >= 300 || hasext("GL_ARB_framebuffer_object"))
+    {
+        glBindRenderbuffer_        = (PFNGLBINDRENDERBUFFERPROC)       getprocaddress("glBindRenderbuffer");
+        glDeleteRenderbuffers_     = (PFNGLDELETERENDERBUFFERSPROC)    getprocaddress("glDeleteRenderbuffers");
+        glGenRenderbuffers_        = (PFNGLGENFRAMEBUFFERSPROC)        getprocaddress("glGenRenderbuffers");
+        glRenderbufferStorage_     = (PFNGLRENDERBUFFERSTORAGEPROC)    getprocaddress("glRenderbufferStorage");
+        glCheckFramebufferStatus_  = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) getprocaddress("glCheckFramebufferStatus");
+        glBindFramebuffer_         = (PFNGLBINDFRAMEBUFFERPROC)        getprocaddress("glBindFramebuffer");
+        glDeleteFramebuffers_      = (PFNGLDELETEFRAMEBUFFERSPROC)     getprocaddress("glDeleteFramebuffers");
+        glGenFramebuffers_         = (PFNGLGENFRAMEBUFFERSPROC)        getprocaddress("glGenFramebuffers");
+        glFramebufferTexture2D_    = (PFNGLFRAMEBUFFERTEXTURE2DPROC)   getprocaddress("glFramebufferTexture2D");
+        glFramebufferRenderbuffer_ = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)getprocaddress("glFramebufferRenderbuffer");
+        glGenerateMipmap_          = (PFNGLGENERATEMIPMAPPROC)         getprocaddress("glGenerateMipmap");
+        glBlitFramebuffer_         = (PFNGLBLITFRAMEBUFFERPROC)        getprocaddress("glBlitFramebuffer");
+        hasAFBO = hasFBO = hasFBB = hasDS = true;
+        if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_framebuffer_object extension.");
+    }
+    else if(hasext("GL_EXT_framebuffer_object"))
+    {
+        glBindRenderbuffer_        = (PFNGLBINDRENDERBUFFERPROC)       getprocaddress("glBindRenderbufferEXT");
+        glDeleteRenderbuffers_     = (PFNGLDELETERENDERBUFFERSPROC)    getprocaddress("glDeleteRenderbuffersEXT");
+        glGenRenderbuffers_        = (PFNGLGENFRAMEBUFFERSPROC)        getprocaddress("glGenRenderbuffersEXT");
+        glRenderbufferStorage_     = (PFNGLRENDERBUFFERSTORAGEPROC)    getprocaddress("glRenderbufferStorageEXT");
+        glCheckFramebufferStatus_  = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) getprocaddress("glCheckFramebufferStatusEXT");
+        glBindFramebuffer_         = (PFNGLBINDFRAMEBUFFERPROC)        getprocaddress("glBindFramebufferEXT");
+        glDeleteFramebuffers_      = (PFNGLDELETEFRAMEBUFFERSPROC)     getprocaddress("glDeleteFramebuffersEXT");
+        glGenFramebuffers_         = (PFNGLGENFRAMEBUFFERSPROC)        getprocaddress("glGenFramebuffersEXT");
+        glFramebufferTexture2D_    = (PFNGLFRAMEBUFFERTEXTURE2DPROC)   getprocaddress("glFramebufferTexture2DEXT");
+        glFramebufferRenderbuffer_ = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)getprocaddress("glFramebufferRenderbufferEXT");
+        glGenerateMipmap_          = (PFNGLGENERATEMIPMAPPROC)         getprocaddress("glGenerateMipmapEXT");
+        hasFBO = true;
+        if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_framebuffer_object extension.");
+
+        if(hasext("GL_EXT_framebuffer_blit"))
+        {
+            glBlitFramebuffer_     = (PFNGLBLITFRAMEBUFFERPROC)        getprocaddress("glBlitFramebufferEXT");
+            hasFBB = true;
+            if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_framebuffer_blit extension.");
+        }
+
+        if(hasext("GL_EXT_packed_depth_stencil") || hasext("GL_NV_packed_depth_stencil"))
+        {
+            hasDS = true;
+            if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_packed_depth_stencil extension.");
+        }
+    }
+    else fatal("Framebuffer object support is required!");
+
+    extern int fpdepthfx;
+    if(ati)
+    {
+        //conoutf(CON_WARN, "WARNING: ATI cards may show garbage in skybox. (use \"/ati_skybox_bug 1\" to fix)");
+
+        minimizetcusage = 1;
+        if(hasTF && hasTRG) fpdepthfx = 1;
+        // On Catalyst 10.2, issuing an occlusion query on the first draw using a given cubemap texture causes a nasty crash
+        ati_cubemap_bug = 1;
+    }
+    else if(nvidia)
+    {
+        reservevpparams = 10;
+        rtsharefb = 0; // work-around for strange driver stalls involving when using many FBOs
+        extern int filltjoints;
+        if(glversion < 300 && !hasext("GL_EXT_gpu_shader4")) filltjoints = 0; // DX9 or less NV cards seem to not cause many sparklies
+
+        if(hasTF && hasTRG) fpdepthfx = 1;
+    }
+    else
+    {
+        if(intel)
+        {
+#ifdef WIN32
+            intel_vertexarray_bug = 1;
+            // MapBufferRange is buggy on older Intel drivers on Windows
+            if(glversion <= 310) intel_mapbufferrange_bug = 1;
+#endif
+        }
+
+        reservevpparams = 20;
+
+        if(mesa) mesa_swap_bug = 1;
+    }
+
+    if(glversion >= 300 || hasext("GL_ARB_map_buffer_range"))
+    {
+        glMapBufferRange_         = (PFNGLMAPBUFFERRANGEPROC)        getprocaddress("glMapBufferRange");
+        glFlushMappedBufferRange_ = (PFNGLFLUSHMAPPEDBUFFERRANGEPROC)getprocaddress("glFlushMappedBufferRange");
+        hasMBR = true;
+        if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_map_buffer_range.");
+    }
+
+    if(glversion >= 310 || hasext("GL_ARB_uniform_buffer_object"))
+    {
+        glGetUniformIndices_       = (PFNGLGETUNIFORMINDICESPROC)      getprocaddress("glGetUniformIndices");
+        glGetActiveUniformsiv_     = (PFNGLGETACTIVEUNIFORMSIVPROC)    getprocaddress("glGetActiveUniformsiv");
+        glGetUniformBlockIndex_    = (PFNGLGETUNIFORMBLOCKINDEXPROC)   getprocaddress("glGetUniformBlockIndex");
+        glGetActiveUniformBlockiv_ = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC)getprocaddress("glGetActiveUniformBlockiv");
+        glUniformBlockBinding_     = (PFNGLUNIFORMBLOCKBINDINGPROC)    getprocaddress("glUniformBlockBinding");
+        glBindBufferBase_          = (PFNGLBINDBUFFERBASEPROC)         getprocaddress("glBindBufferBase");
+        glBindBufferRange_         = (PFNGLBINDBUFFERRANGEPROC)        getprocaddress("glBindBufferRange");
+
+        useubo = 1;
+        hasUBO = true;
+        if(glversion < 310 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_uniform_buffer_object extension.");
+    }
+
+    if(glversion >= 300 || hasext("GL_ARB_vertex_array_object"))
+    {
+        glBindVertexArray_ =    (PFNGLBINDVERTEXARRAYPROC)   getprocaddress("glBindVertexArray");
+        glDeleteVertexArrays_ = (PFNGLDELETEVERTEXARRAYSPROC)getprocaddress("glDeleteVertexArrays");
+        glGenVertexArrays_ =    (PFNGLGENVERTEXARRAYSPROC)   getprocaddress("glGenVertexArrays");
+        glIsVertexArray_ =      (PFNGLISVERTEXARRAYPROC)     getprocaddress("glIsVertexArray");
+        hasVAO = true;
+        if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_vertex_array_object extension.");
+    }
+    else if(hasext("GL_APPLE_vertex_array_object"))
+    {
+        glBindVertexArray_ =    (PFNGLBINDVERTEXARRAYPROC)   getprocaddress("glBindVertexArrayAPPLE");
+        glDeleteVertexArrays_ = (PFNGLDELETEVERTEXARRAYSPROC)getprocaddress("glDeleteVertexArraysAPPLE");
+        glGenVertexArrays_ =    (PFNGLGENVERTEXARRAYSPROC)   getprocaddress("glGenVertexArraysAPPLE");
+        glIsVertexArray_ =      (PFNGLISVERTEXARRAYPROC)     getprocaddress("glIsVertexArrayAPPLE");
+        hasVAO = true;
+        if(dbgexts) conoutf(CON_INIT, "Using GL_APPLE_vertex_array_object extension.");
+    }
+
+    if(glversion >= 330 || hasext("GL_ARB_texture_swizzle") || hasext("GL_EXT_texture_swizzle"))
+    {
+        hasTSW = true;
+        if(glversion < 330 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_swizzle extension.");
+    }
+
+    if(hasext("GL_EXT_texture_compression_s3tc"))
+    {
+        hasS3TC = true;
+#ifdef __APPLE__
+        usetexcompress = 1;
+#else
+        if(!mesa) usetexcompress = 2;
+#endif
+        if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_s3tc extension.");
+    }
+    else if(hasext("GL_EXT_texture_compression_dxt1") && hasext("GL_ANGLE_texture_compression_dxt3") && hasext("GL_ANGLE_texture_compression_dxt5"))
+    {
+        hasS3TC = true;
+        if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_dxt1 extension.");
+    }
+    if(hasext("GL_3DFX_texture_compression_FXT1"))
+    {
+        hasFXT1 = true;
+        if(mesa) usetexcompress = max(usetexcompress, 1);
+        if(dbgexts) conoutf(CON_INIT, "Using GL_3DFX_texture_compression_FXT1.");
+    }
+    if(hasext("GL_EXT_texture_compression_latc"))
+    {
+        hasLATC = true;
+        if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_compression_latc extension.");
+    }
+    if(glversion >= 300 || hasext("GL_ARB_texture_compression_rgtc") || hasext("GL_EXT_texture_compression_rgtc"))
+    {
+        hasRGTC = true;
+        if(glversion < 300 && dbgexts) conoutf(CON_INIT, "Using GL_ARB_texture_compression_rgtc extension.");
+    }
+
+    if(hasext("GL_EXT_texture_filter_anisotropic"))
+    {
+        GLint val;
+        glGetIntegerv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &val);
+        hwmaxaniso = val;
+        hasAF = true;
+        if(dbgexts) conoutf(CON_INIT, "Using GL_EXT_texture_filter_anisotropic extension.");
+    }
+
+    if(glversion >= 300 || hasext("GL_EXT_gpu_shader4"))
+    {
+        // on DX10 or above class cards (i.e. GF8 or RadeonHD) enable expensive features
+        extern int grass, glare, maxdynlights, depthfxsize, blurdepthfx, texcompress;
+        grass = 1;
+        waterfallrefract = 1;
+        glare = 1;
+        maxdynlights = MAXDYNLIGHTS;
+        depthfxsize = 10;
+        blurdepthfx = 0;
+        texcompress = max(texcompress, 1024 + 1);
+    }
+}
+
+void glext(char *ext)
+{
+    intret(hasext(ext) ? 1 : 0);
+}
+COMMAND(glext, "s");
+
+void gl_resize()
+{
+    glViewport(0, 0, screenw, screenh);
+}
+
+void gl_init()
+{
+    glClearColor(0, 0, 0, 0);
+    glClearDepth(1);
+    glDepthFunc(GL_LESS);
+    glDisable(GL_DEPTH_TEST);
+    
+    glEnable(GL_LINE_SMOOTH);
+    //glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
+
+    glFrontFace(GL_CW);
+    glCullFace(GL_BACK);
+    glDisable(GL_CULL_FACE);
+
+    gle::setup();
+
+    setupshaders();
+
+    setuptexcompress();
+
+    gl_resize();
+}
+
+VAR(wireframe, 0, 0, 1);
+
+ICOMMAND(getcamyaw, "", (), floatret(camera1->yaw));
+ICOMMAND(getcampitch, "", (), floatret(camera1->pitch));
+ICOMMAND(getcamroll, "", (), floatret(camera1->roll));
+ICOMMAND(getcampos, "", (), 
+{
+    defformatstring(pos, "%s %s %s", floatstr(camera1->o.x), floatstr(camera1->o.y), floatstr(camera1->o.z));
+    result(pos);
+});
+
+vec worldpos, camdir, camright, camup;
+
+void setcammatrix()
+{
+    // move from RH to Z-up LH quake style worldspace
+    cammatrix = viewmatrix;
+    cammatrix.rotate_around_y(camera1->roll*RAD);
+    cammatrix.rotate_around_x(camera1->pitch*-RAD);
+    cammatrix.rotate_around_z(camera1->yaw*-RAD);
+    cammatrix.translate(vec(camera1->o).neg());
+
+    cammatrix.transposedtransformnormal(vec(viewmatrix.b), camdir);
+    cammatrix.transposedtransformnormal(vec(viewmatrix.a).neg(), camright);
+    cammatrix.transposedtransformnormal(vec(viewmatrix.c), camup);
+
+    if(!drawtex)
+    {
+        if(raycubepos(camera1->o, camdir, worldpos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1)
+            worldpos = vec(camdir).mul(2*worldsize).add(camera1->o); // if nothing is hit, just far away in the view direction
+    }
+}
+
+void setcamprojmatrix(bool init = true, bool flush = false)
+{
+    if(init)
+    {   
+        setcammatrix();
+    }
+
+    camprojmatrix.muld(projmatrix, cammatrix);
+
+    if(init)
+    {
+        invcammatrix.invert(cammatrix);
+        invcamprojmatrix.invert(camprojmatrix);
+    }
+
+    GLOBALPARAM(camprojmatrix, camprojmatrix);
+
+    if(fogging)
+    {
+        vec fogplane(cammatrix.c);
+        fogplane.x /= projmatrix.a.x;
+        fogplane.y /= projmatrix.b.y;
+        fogplane.z /= projmatrix.c.w;
+        GLOBALPARAMF(fogplane, fogplane.x, fogplane.y, 0, fogplane.z);               
+    }
+    else
+    {
+        vec2 lineardepthscale = projmatrix.lineardepthscale();
+        GLOBALPARAMF(fogplane, 0, 0, lineardepthscale.x, lineardepthscale.y);
+    }
+
+    if(flush && Shader::lastshader) Shader::lastshader->flushparams();
+}
+
+matrix4 hudmatrix, hudmatrixstack[64];
+int hudmatrixpos = 0;
+
+void resethudmatrix()
+{
+    hudmatrixpos = 0;
+    GLOBALPARAM(hudmatrix, hudmatrix);
+}
+
+void pushhudmatrix()
+{
+    if(hudmatrixpos >= 0 && hudmatrixpos < int(sizeof(hudmatrixstack)/sizeof(hudmatrixstack[0]))) hudmatrixstack[hudmatrixpos] = hudmatrix;
+    ++hudmatrixpos;
+}
+
+void flushhudmatrix(bool flushparams)
+{
+    GLOBALPARAM(hudmatrix, hudmatrix);
+    if(flushparams && Shader::lastshader) Shader::lastshader->flushparams();
+}
+
+void pophudmatrix(bool flush, bool flushparams)
+{
+    --hudmatrixpos;
+    if(hudmatrixpos >= 0 && hudmatrixpos < int(sizeof(hudmatrixstack)/sizeof(hudmatrixstack[0])))
+    {
+        hudmatrix = hudmatrixstack[hudmatrixpos];
+        if(flush) flushhudmatrix(flushparams);
+    }
+}
+
+void pushhudscale(float sx, float sy)
+{
+    if(!sy) sy = sx;
+    pushhudmatrix();
+    hudmatrix.scale(sx, sy, 1);
+    flushhudmatrix();
+}
+
+void pushhudtranslate(float tx, float ty, float sx, float sy)
+{
+    if(!sy) sy = sx;
+    pushhudmatrix();
+    hudmatrix.translate(tx, ty, 0);
+    if(sy) hudmatrix.scale(sx, sy, 1);
+    flushhudmatrix();
+}
+
+float curfov = 100, curavatarfov = 65, fovy, aspect;
+int farplane;
+VARP(zoominvel, 0, 250, 5000);
+VARP(zoomoutvel, 0, 100, 5000);
+VARP(zoomfov, 10, 35, 60);
+VARP(fov, 10, 100, 150);
+VAR(avatarzoomfov, 10, 25, 60);
+VAR(avatarfov, 10, 65, 150);
+FVAR(avatardepth, 0, 0.5f, 1);
+FVARNP(aspect, forceaspect, 0, 0, 1e3f);
+
+static float zoomprogress = 0;
+VAR(zoom, -1, 0, 1);
+
+void disablezoom()
+{
+    zoom = 0;
+    zoomprogress = 0;
+}
+
+void computezoom()
+{
+    if(!zoom) { zoomprogress = 0; curfov = fov; curavatarfov = avatarfov; return; }
+    if(zoom > 0) zoomprogress = zoominvel ? min(zoomprogress + float(elapsedtime) / zoominvel, 1.0f) : 1;
+    else
+    {
+        zoomprogress = zoomoutvel ? max(zoomprogress - float(elapsedtime) / zoomoutvel, 0.0f) : 0;
+        if(zoomprogress <= 0) zoom = 0;
+    }
+    curfov = zoomfov*zoomprogress + fov*(1 - zoomprogress);
+    curavatarfov = avatarzoomfov*zoomprogress + avatarfov*(1 - zoomprogress);
+}
+
+FVARP(zoomsens, 1e-3f, 1, 1000);
+FVARP(zoomaccel, 0, 0, 1000);
+VARP(zoomautosens, 0, 1, 1);
+FVARP(sensitivity, 1e-3f, 3, 1000);
+FVARP(sensitivityscale, 1e-3f, 1, 1000);
+VARP(invmouse, 0, 0, 1);
+FVARP(mouseaccel, 0, 0, 1000);
+VAR(thirdperson, 0, 0, 2);
+FVAR(thirdpersondistance, 0, 20, 50);
+FVAR(thirdpersonup, -25, 0, 25);
+FVAR(thirdpersonside, -25, 0, 25);
+physent *camera1 = NULL;
+bool detachedcamera = false;
+bool isthirdperson() { return player!=camera1 || detachedcamera || reflecting; }
+
+void fixcamerarange()
+{
+    const float MAXPITCH = 90.0f;
+    if(camera1->pitch>MAXPITCH) camera1->pitch = MAXPITCH;
+    if(camera1->pitch<-MAXPITCH) camera1->pitch = -MAXPITCH;
+    while(camera1->yaw<0.0f) camera1->yaw += 360.0f;
+    while(camera1->yaw>=360.0f) camera1->yaw -= 360.0f;
+}
+
+void mousemove(int dx, int dy)
+{
+    if(!game::allowmouselook()) return;
+    float cursens = sensitivity, curaccel = mouseaccel;
+    if(zoom)
+    {
+        if(zoomautosens) 
+        {
+            cursens = float(sensitivity*zoomfov)/fov;
+            curaccel = float(mouseaccel*zoomfov)/fov;
+        }
+        else 
+        {
+            cursens = zoomsens;
+            curaccel = zoomaccel;
+        }
+    }
+    if(curaccel && curtime && (dx || dy)) cursens += curaccel * sqrtf(dx*dx + dy*dy)/curtime;
+    cursens /= 33.0f*sensitivityscale;
+    camera1->yaw += dx*cursens;
+    camera1->pitch -= dy*cursens*(invmouse ? -1 : 1);
+    fixcamerarange();
+    if(camera1!=player && !detachedcamera)
+    {
+        player->yaw = camera1->yaw;
+        player->pitch = camera1->pitch;
+    }
+}
+
+void recomputecamera()
+{
+    game::setupcamera();
+    computezoom();
+
+    bool allowthirdperson = game::allowthirdperson();
+    bool shoulddetach = (allowthirdperson && thirdperson > 1) || game::detachcamera();
+    if((!allowthirdperson || !thirdperson) && !shoulddetach)
+    {
+        camera1 = player;
+        detachedcamera = false;
+    }
+    else
+    {
+        static physent tempcamera;
+        camera1 = &tempcamera;
+        if(detachedcamera && shoulddetach) camera1->o = player->o;
+        else
+        {
+            *camera1 = *player;
+            detachedcamera = shoulddetach;
+        }
+        camera1->reset();
+        camera1->type = ENT_CAMERA;
+        camera1->move = -1;
+        camera1->eyeheight = camera1->aboveeye = camera1->radius = camera1->xradius = camera1->yradius = 2;
+       
+        matrix3 orient;
+        orient.identity();
+        orient.rotate_around_z(camera1->yaw*RAD);
+        orient.rotate_around_x(camera1->pitch*RAD);
+        orient.rotate_around_y(camera1->roll*-RAD);
+        vec dir = vec(orient.b).neg(), side = vec(orient.a).neg(), up = orient.c;
+
+        if(game::collidecamera()) 
+        {
+            movecamera(camera1, dir, thirdpersondistance, 1);
+            movecamera(camera1, dir, clamp(thirdpersondistance - camera1->o.dist(player->o), 0.0f, 1.0f), 0.1f);
+            if(thirdpersonup)
+            {
+                vec pos = camera1->o;
+                float dist = fabs(thirdpersonup);
+                if(thirdpersonup < 0) up.neg();
+                movecamera(camera1, up, dist, 1);
+                movecamera(camera1, up, clamp(dist - camera1->o.dist(pos), 0.0f, 1.0f), 0.1f);
+            }
+            if(thirdpersonside)
+            {
+                vec pos = camera1->o;
+                float dist = fabs(thirdpersonside);
+                if(thirdpersonside < 0) side.neg();
+                movecamera(camera1, side, dist, 1);
+                movecamera(camera1, side, clamp(dist - camera1->o.dist(pos), 0.0f, 1.0f), 0.1f);
+            }
+        }
+        else 
+        {
+            camera1->o.add(vec(dir).mul(thirdpersondistance));
+            if(thirdpersonup) camera1->o.add(vec(up).mul(thirdpersonup));
+            if(thirdpersonside) camera1->o.add(vec(side).mul(thirdpersonside));
+        }
+    }
+
+    setviewcell(camera1->o);
+}
+
+extern const matrix4 viewmatrix(vec(-1, 0, 0), vec(0, 0, 1), vec(0, -1, 0));
+matrix4 cammatrix, projmatrix, camprojmatrix, invcammatrix, invcamprojmatrix;
+
+FVAR(nearplane, 0.01f, 0.54f, 2.0f);
+
+vec calcavatarpos(const vec &pos, float dist)
+{
+    vec eyepos;
+    cammatrix.transform(pos, eyepos);
+    GLdouble ydist = nearplane * tan(curavatarfov/2*RAD), xdist = ydist * aspect;
+    vec4 scrpos;
+    scrpos.x = eyepos.x*nearplane/xdist;
+    scrpos.y = eyepos.y*nearplane/ydist;
+    scrpos.z = (eyepos.z*(farplane + nearplane) - 2*nearplane*farplane) / (farplane - nearplane);
+    scrpos.w = -eyepos.z;
+
+    vec worldpos = invcamprojmatrix.perspectivetransform(scrpos);
+    vec dir = vec(worldpos).sub(camera1->o).rescale(dist);
+    return dir.add(camera1->o);
+}
+
+VAR(reflectclip, 0, 6, 64);
+VAR(reflectclipavatar, -64, 0, 64);
+
+matrix4 clipmatrix, noclipmatrix;
+
+void renderavatar()
+{
+    if(isthirdperson()) return;
+
+    matrix4 oldprojmatrix = projmatrix;
+    projmatrix.perspective(curavatarfov, aspect, nearplane, farplane);
+    projmatrix.scalez(avatardepth);
+    setcamprojmatrix(false);
+
+    game::renderavatar();
+
+    projmatrix = oldprojmatrix;
+    setcamprojmatrix(false);
+}
+
+FVAR(polygonoffsetfactor, -1e4f, -3.0f, 1e4f);
+FVAR(polygonoffsetunits, -1e4f, -3.0f, 1e4f);
+FVAR(depthoffset, -1e4f, 0.01f, 1e4f);
+
+matrix4 nooffsetmatrix;
+
+void enablepolygonoffset(GLenum type)
+{
+    if(!depthoffset)
+    {
+        glPolygonOffset(polygonoffsetfactor, polygonoffsetunits);
+        glEnable(type);
+        return;
+    }
+    
+    bool clipped = reflectz < 1e15f && reflectclip;
+
+    nooffsetmatrix = projmatrix;
+    projmatrix.d.z += depthoffset * (clipped ? noclipmatrix.c.z : projmatrix.c.z);
+    setcamprojmatrix(false, true);
+}
+
+void disablepolygonoffset(GLenum type)
+{
+    if(!depthoffset)
+    {
+        glDisable(type);
+        return;
+    }
+    
+    projmatrix = nooffsetmatrix;
+    setcamprojmatrix(false, true);
+}
+
+void calcspherescissor(const vec &center, float size, float &sx1, float &sy1, float &sx2, float &sy2)
+{
+    vec worldpos(center), e;
+    if(reflecting) worldpos.z = 2*reflectz - worldpos.z; 
+    cammatrix.transform(worldpos, e); 
+    if(e.z > 2*size) { sx1 = sy1 = 1; sx2 = sy2 = -1; return; }
+    float zzrr = e.z*e.z - size*size,
+          dx = e.x*e.x + zzrr, dy = e.y*e.y + zzrr,
+          focaldist = 1.0f/tan(fovy*0.5f*RAD);
+    sx1 = sy1 = -1;
+    sx2 = sy2 = 1;
+    #define CHECKPLANE(c, dir, focaldist, low, high) \
+    do { \
+        float nzc = (cz*cz + 1) / (cz dir drt) - cz, \
+              pz = (d##c)/(nzc*e.c - e.z); \
+        if(pz > 0) \
+        { \
+            float c = (focaldist)*nzc, \
+                  pc = pz*nzc; \
+            if(pc < e.c) low = c; \
+            else if(pc > e.c) high = c; \
+        } \
+    } while(0)
+    if(dx > 0)
+    {
+        float cz = e.x/e.z, drt = sqrtf(dx)/size;
+        CHECKPLANE(x, -, focaldist/aspect, sx1, sx2);
+        CHECKPLANE(x, +, focaldist/aspect, sx1, sx2);
+    }
+    if(dy > 0)
+    {
+        float cz = e.y/e.z, drt = sqrtf(dy)/size;
+        CHECKPLANE(y, -, focaldist, sy1, sy2);
+        CHECKPLANE(y, +, focaldist, sy1, sy2);
+    }
+}
+
+static int scissoring = 0;
+static GLint oldscissor[4];
+
+int pushscissor(float sx1, float sy1, float sx2, float sy2)
+{
+    scissoring = 0;
+
+    if(sx1 <= -1 && sy1 <= -1 && sx2 >= 1 && sy2 >= 1) return 0;
+
+    sx1 = max(sx1, -1.0f);
+    sy1 = max(sy1, -1.0f);
+    sx2 = min(sx2, 1.0f);
+    sy2 = min(sy2, 1.0f);
+
+    GLint viewport[4];
+    glGetIntegerv(GL_VIEWPORT, viewport);
+    int sx = viewport[0] + int(floor((sx1+1)*0.5f*viewport[2])),
+        sy = viewport[1] + int(floor((sy1+1)*0.5f*viewport[3])),
+        sw = viewport[0] + int(ceil((sx2+1)*0.5f*viewport[2])) - sx,
+        sh = viewport[1] + int(ceil((sy2+1)*0.5f*viewport[3])) - sy;
+    if(sw <= 0 || sh <= 0) return 0;
+
+    if(glIsEnabled(GL_SCISSOR_TEST))
+    {
+        glGetIntegerv(GL_SCISSOR_BOX, oldscissor);
+        sw += sx;
+        sh += sy;
+        sx = max(sx, int(oldscissor[0]));
+        sy = max(sy, int(oldscissor[1]));
+        sw = min(sw, int(oldscissor[0] + oldscissor[2])) - sx;
+        sh = min(sh, int(oldscissor[1] + oldscissor[3])) - sy;
+        if(sw <= 0 || sh <= 0) return 0;
+        scissoring = 2;
+    }
+    else scissoring = 1;
+
+    glScissor(sx, sy, sw, sh);
+    if(scissoring<=1) glEnable(GL_SCISSOR_TEST);
+    
+    return scissoring;
+}
+
+void popscissor()
+{
+    if(scissoring>1) glScissor(oldscissor[0], oldscissor[1], oldscissor[2], oldscissor[3]);
+    else if(scissoring) glDisable(GL_SCISSOR_TEST);
+    scissoring = 0;
+}
+
+static GLuint screenquadvbo = 0;
+
+static void setupscreenquad()
+{
+    if(!screenquadvbo)
+    {
+        glGenBuffers_(1, &screenquadvbo);
+        gle::bindvbo(screenquadvbo);
+        vec2 verts[4] = { vec2(1, -1), vec2(-1, -1), vec2(1, 1), vec2(-1, 1) };
+        glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+        gle::clearvbo();
+    }
+}
+
+static void cleanupscreenquad()
+{
+    if(screenquadvbo) { glDeleteBuffers_(1, &screenquadvbo); screenquadvbo = 0; }
+}
+
+void screenquad()
+{
+    setupscreenquad();
+    gle::bindvbo(screenquadvbo);
+    gle::enablevertex();
+    gle::vertexpointer(sizeof(vec2), (const vec2 *)0, GL_FLOAT, 2);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+    gle::disablevertex();
+    gle::clearvbo();
+}
+
+static LocalShaderParam screentexcoord[2] = { LocalShaderParam("screentexcoord0"), LocalShaderParam("screentexcoord1") };
+
+static inline void setscreentexcoord(int i, float w, float h, float x = 0, float y = 0)
+{
+    screentexcoord[i].setf(w*0.5f, h*0.5f, x + w*0.5f, y + fabs(h)*0.5f);
+}
+
+void screenquad(float sw, float sh)
+{
+    setscreentexcoord(0, sw, sh);
+    screenquad();
+}
+
+void screenquadflipped(float sw, float sh)
+{
+    setscreentexcoord(0, sw, -sh);
+    screenquad();
+}
+
+void screenquad(float sw, float sh, float sw2, float sh2)
+{
+    setscreentexcoord(0, sw, sh);
+    setscreentexcoord(1, sw2, sh2);
+    screenquad();
+}
+
+void screenquadoffset(float x, float y, float w, float h)
+{
+    setscreentexcoord(0, w, h, x, y);
+    screenquad();
+}
+
+void screenquadoffset(float x, float y, float w, float h, float x2, float y2, float w2, float h2)
+{
+    setscreentexcoord(0, w, h, x, y);
+    setscreentexcoord(1, w2, h2, x2, y2);
+    screenquad();
+}
+
+#define HUDQUAD(x1, y1, x2, y2, sx1, sy1, sx2, sy2) { \
+    gle::defvertex(2); \
+    gle::deftexcoord0(); \
+    gle::begin(GL_TRIANGLE_STRIP); \
+    gle::attribf(x2, y1); gle::attribf(sx2, sy1); \
+    gle::attribf(x1, y1); gle::attribf(sx1, sy1); \
+    gle::attribf(x2, y2); gle::attribf(sx2, sy2); \
+    gle::attribf(x1, y2); gle::attribf(sx1, sy2); \
+    gle::end(); \
+}
+
+void hudquad(float x, float y, float w, float h, float tx, float ty, float tw, float th)
+{
+    HUDQUAD(x, y, x+w, y+h, tx, ty, tx+tw, ty+th);
+}
+
+VARR(fog, 16, 4000, 1000024);
+bvec fogcolor(0x80, 0x99, 0xB3);
+HVARFR(fogcolour, 0, 0x8099B3, 0xFFFFFF,
+{
+    fogcolor = bvec((fogcolour>>16)&0xFF, (fogcolour>>8)&0xFF, fogcolour&0xFF);
+});
+
+static float findsurface(int fogmat, const vec &v, int &abovemat)
+{
+    fogmat &= MATF_VOLUME;
+    ivec o(v), co;
+    int csize;
+    do
+    {
+        cube &c = lookupcube(o, 0, co, csize);
+        int mat = c.material&MATF_VOLUME;
+        if(mat != fogmat)
+        {
+            abovemat = isliquid(mat) ? c.material : MAT_AIR;
+            return o.z;
+        }
+        o.z = co.z + csize;
+    }
+    while(o.z < worldsize);
+    abovemat = MAT_AIR;
+    return worldsize;
+}
+
+static void blendfog(int fogmat, float blend, float logblend, float &start, float &end, vec &fogc)
+{
+    switch(fogmat&MATF_VOLUME)
+    {
+        case MAT_WATER:
+        {
+            const bvec &wcol = getwatercolor(fogmat);
+            int wfog = getwaterfog(fogmat);
+            fogc.madd(wcol.tocolor(), blend);
+            end += logblend*min(fog, max(wfog*4, 32));
+            break;
+        }
+
+        case MAT_LAVA:
+        {
+            const bvec &lcol = getlavacolor(fogmat);
+            int lfog = getlavafog(fogmat);
+            fogc.madd(lcol.tocolor(), blend);
+            end += logblend*min(fog, max(lfog*4, 32));
+            break;
+        }
+
+        default:
+            fogc.madd(fogcolor.tocolor(), blend);
+            start += logblend*(fog+64)/8;
+            end += logblend*fog;
+            break;
+    }
+}
+
+vec oldfogcolor(0, 0, 0), curfogcolor(0, 0, 0);
+float oldfogstart = 0, oldfogend = 1000000, curfogstart = 0, curfogend = 1000000;
+
+void setfogcolor(const vec &v)
+{
+    GLOBALPARAM(fogcolor, v);
+}
+
+void zerofogcolor()
+{
+    setfogcolor(vec(0, 0, 0));
+}
+
+void resetfogcolor()
+{
+    setfogcolor(curfogcolor);
+}
+
+void pushfogcolor(const vec &v)
+{
+    oldfogcolor = curfogcolor;
+    curfogcolor = v;
+    resetfogcolor();
+}
+
+void popfogcolor()
+{
+    curfogcolor = oldfogcolor;
+    resetfogcolor();
+}
+
+void setfogdist(float start, float end)
+{
+    GLOBALPARAMF(fogparams, 1/(end - start), end/(end - start));
+}
+
+void clearfogdist()
+{
+    setfogdist(0, 1000000);
+}
+
+void resetfogdist()
+{
+    setfogdist(curfogstart, curfogend);
+}
+
+void pushfogdist(float start, float end)
+{
+    oldfogstart = curfogstart;
+    oldfogend = curfogend;
+    curfogstart = start;
+    curfogend = end;
+    resetfogdist();
+}
+
+void popfogdist()
+{
+    curfogstart = oldfogstart;
+    curfogend = oldfogend;
+    resetfogdist();
+}
+
+static void resetfog()
+{
+    resetfogcolor();
+    resetfogdist();
+
+    glClearColor(curfogcolor.r, curfogcolor.g, curfogcolor.b, 1.0f);
+}
+
+static void setfog(int fogmat, float below = 1, int abovemat = MAT_AIR)
+{
+    float logscale = 256, logblend = log(1 + (logscale - 1)*below) / log(logscale);
+
+    curfogstart = curfogend = 0;
+    curfogcolor = vec(0, 0, 0);
+    blendfog(fogmat, below, logblend, curfogstart, curfogend, curfogcolor);
+    if(below < 1) blendfog(abovemat, 1-below, 1-logblend, curfogstart, curfogend, curfogcolor);
+
+    resetfog();
+}
+
+static void setnofog(const vec &color = vec(0, 0, 0))
+{
+    curfogstart = 0;
+    curfogend = 1000000;
+    curfogcolor = color;
+
+    resetfog();
+}
+
+static void blendfogoverlay(int fogmat, float blend, vec &overlay)
+{
+    float maxc;
+    switch(fogmat&MATF_VOLUME)
+    {
+        case MAT_WATER:
+        {
+            const bvec &wcol = getwatercolor(fogmat);
+            maxc = max(wcol.r, max(wcol.g, wcol.b));
+            overlay.madd(vec(wcol.r, wcol.g, wcol.b).div(min(32.0f + maxc*7.0f/8.0f, 255.0f)).max(0.4f), blend);
+            break;
+        }
+
+        case MAT_LAVA:
+        {
+            const bvec &lcol = getlavacolor(fogmat);
+            maxc = max(lcol.r, max(lcol.g, lcol.b));
+            overlay.madd(vec(lcol.r, lcol.g, lcol.b).div(min(32.0f + maxc*7.0f/8.0f, 255.0f)).max(0.4f), blend);
+            break;
+        }
+
+        default:
+            overlay.add(blend);
+            break;
+    }
+}
+
+void drawfogoverlay(int fogmat, float fogblend, int abovemat)
+{
+    SETSHADER(fogoverlay);
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+    vec overlay(0, 0, 0);
+    blendfogoverlay(fogmat, fogblend, overlay);
+    blendfogoverlay(abovemat, 1-fogblend, overlay);
+
+    gle::color(overlay);
+    screenquad();
+
+    glDisable(GL_BLEND);
+}
+
+bool renderedgame = false;
+
+void rendergame(bool mainpass)
+{
+    game::rendergame(mainpass);
+    if(!shadowmapping) renderedgame = true;
+}
+
+VARP(skyboxglare, 0, 1, 1);
+
+void drawglare()
+{
+    glaring = true;
+    refracting = -1;
+
+    pushfogcolor(vec(0, 0, 0));
+
+    glClearColor(0, 0, 0, 1);
+    glClear((skyboxglare && !shouldclearskyboxglare() ? 0 : GL_COLOR_BUFFER_BIT) | GL_DEPTH_BUFFER_BIT);
+
+    rendergeom();
+
+    if(skyboxglare) drawskybox(farplane, false);
+
+    renderreflectedmapmodels();
+    rendergame();
+    renderavatar();
+
+    renderwater();
+    rendermaterials();
+    renderalphageom();
+    renderparticles();
+
+    popfogcolor();
+
+    refracting = 0;
+    glaring = false;
+}
+
+VARP(reflectmms, 0, 1, 1);
+VARR(refractsky, 0, 0, 1);
+
+matrix4 noreflectmatrix;
+
+void drawreflection(float z, bool refract, int fogdepth, const bvec &col)
+{
+    reflectz = z < 0 ? 1e16f : z;
+    reflecting = !refract;
+    refracting = refract ? (z < 0 || camera1->o.z >= z ? -1 : 1) : 0;
+    fading = waterrefract && waterfade && z>=0;
+    fogging = refracting<0 && z>=0;
+    refractfog = fogdepth;
+
+    if(fogging)
+    {
+        pushfogdist(camera1->o.z - z, camera1->o.z - (z - max(refractfog, 1)));
+        pushfogcolor(col.tocolor());
+    }
+    else
+    {
+        vec color(0, 0, 0);
+        float start = 0, end = 0;
+        blendfog(MAT_AIR, 1, 1, start, end, color);
+        pushfogdist(start, end);
+        pushfogcolor(color);
+    }
+
+    if(fading)
+    {
+        float scale = fogging ? -0.25f : 0.25f, offset = 2*fabs(scale) - scale*z;
+        GLOBALPARAMF(waterfadeparams, scale, offset, -scale, offset + camera1->o.z*scale);
+    }
+
+    if(reflecting)
+    {
+        noreflectmatrix = cammatrix;
+        cammatrix.reflectz(z);
+
+        glFrontFace(GL_CCW);
+    }
+
+    if(reflectclip && z>=0)
+    {
+        float zoffset = reflectclip/4.0f, zclip;
+        if(refracting<0)
+        {
+            zclip = z+zoffset;
+            if(camera1->o.z<=zclip) zclip = z;
+        }
+        else
+        {
+            zclip = z-zoffset;
+            if(camera1->o.z>=zclip && camera1->o.z<=z+4.0f) zclip = z;
+            if(reflecting) zclip = 2*z - zclip;
+        }
+        plane clipplane;
+        invcammatrix.transposedtransform(plane(0, 0, refracting>0 ? 1 : -1, refracting>0 ? -zclip : zclip), clipplane);
+        clipmatrix.clip(clipplane, projmatrix);
+        noclipmatrix = projmatrix;
+        projmatrix = clipmatrix;
+    }
+
+    setcamprojmatrix(false, true);
+
+    renderreflectedgeom(refracting<0 && z>=0 && caustics, fogging);
+
+    if(reflecting || refracting>0 || (refracting<0 && refractsky) || z<0)
+    {
+        if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+        if(reflectclip && z>=0)
+        {
+            projmatrix = noclipmatrix;
+            setcamprojmatrix(false, true);
+        }
+        drawskybox(farplane, false);
+        if(reflectclip && z>=0)
+        {
+            projmatrix = clipmatrix;
+            setcamprojmatrix(false, true);
+        }
+        if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+    }
+    else if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+    renderdecals();
+
+    if(reflectmms) renderreflectedmapmodels();
+    rendergame();
+
+    if(refracting && z>=0 && !isthirdperson() && fabs(camera1->o.z-z) <= 0.5f*(player->eyeheight + player->aboveeye))
+    {   
+        matrix4 oldprojmatrix = projmatrix, avatarproj;
+        avatarproj.perspective(curavatarfov, aspect, nearplane, farplane);
+        if(reflectclip)
+        {
+            matrix4 avatarclip;
+            plane clipplane;
+            invcammatrix.transposedtransform(plane(0, 0, refracting, reflectclipavatar/4.0f - refracting*z), clipplane);
+            avatarclip.clip(clipplane, avatarproj);
+            projmatrix = avatarclip;
+        }
+        else projmatrix = avatarproj;
+        setcamprojmatrix(false, true);
+        game::renderavatar();
+        projmatrix = oldprojmatrix;
+        setcamprojmatrix(false, true);
+    }
+
+    if(refracting) rendergrass();
+    rendermaterials();
+    renderalphageom(fogging);
+    renderparticles();
+
+    if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+    if(reflectclip && z>=0) projmatrix = noclipmatrix; 
+
+    if(reflecting)
+    {
+        cammatrix = noreflectmatrix;
+
+        glFrontFace(GL_CW);
+    }
+
+    popfogdist();
+    popfogcolor();
+    
+    reflectz = 1e16f;
+    refracting = 0;
+    reflecting = fading = fogging = false;
+
+    setcamprojmatrix(false, true);
+}
+
+int drawtex = 0;
+
+void drawcubemap(int size, const vec &o, float yaw, float pitch, const cubemapside &side, bool onlysky)
+{
+    drawtex = DRAWTEX_ENVMAP;
+
+    physent *oldcamera = camera1;
+    static physent cmcamera;
+    cmcamera = *player;
+    cmcamera.reset();
+    cmcamera.type = ENT_CAMERA;
+    cmcamera.o = o;
+    cmcamera.yaw = yaw;
+    cmcamera.pitch = pitch;
+    cmcamera.roll = 0;
+    camera1 = &cmcamera;
+    setviewcell(camera1->o);
+   
+    int fogmat = lookupmaterial(o)&(MATF_VOLUME|MATF_INDEX);
+
+    setfog(fogmat);
+
+    int farplane = worldsize*2;
+
+    projmatrix.perspective(90.0f, 1.0f, nearplane, farplane);
+    if(!side.flipx || !side.flipy) projmatrix.scalexy(!side.flipx ? -1 : 1, !side.flipy ? -1 : 1);
+    if(side.swapxy)
+    {
+        swap(projmatrix.a.x, projmatrix.a.y);
+        swap(projmatrix.b.x, projmatrix.b.y);
+        swap(projmatrix.c.x, projmatrix.c.y);
+        swap(projmatrix.d.x, projmatrix.d.y);
+    }
+    setcamprojmatrix();
+
+    xtravertsva = xtraverts = glde = gbatches = 0;
+
+    visiblecubes();
+
+    if(onlysky) drawskybox(farplane, false, true);
+    else
+    {
+        glClear(GL_DEPTH_BUFFER_BIT);
+
+        glEnable(GL_CULL_FACE);
+        glEnable(GL_DEPTH_TEST);
+
+        if(limitsky()) drawskybox(farplane, true);
+
+        rendergeom();
+
+        if(!limitsky()) drawskybox(farplane, false);
+
+//      queryreflections();
+
+        rendermapmodels();
+        renderalphageom();
+
+//      drawreflections();
+
+//      renderwater();
+//      rendermaterials();
+
+        glDisable(GL_DEPTH_TEST);
+        glDisable(GL_CULL_FACE);
+    }
+
+    camera1 = oldcamera;
+    drawtex = 0;
+}
+
+VAR(modelpreviewfov, 10, 20, 100);
+VAR(modelpreviewpitch, -90, -15, 90);
+
+namespace modelpreview
+{
+    physent *oldcamera;
+    physent camera;
+
+    float oldaspect, oldfovy, oldfov;
+    int oldfarplane;
+    matrix4 oldprojmatrix;
+
+    void start(int x, int y, int w, int h, bool background)
+    {
+        drawtex = DRAWTEX_MODELPREVIEW;
+
+        glViewport(x, y, w, h);
+        glScissor(x, y, w, h);
+        glEnable(GL_SCISSOR_TEST);
+
+        oldcamera = camera1;
+        camera = *camera1;
+        camera.reset();
+        camera.type = ENT_CAMERA;
+        camera.o = vec(0, 0, 0);
+        camera.yaw = 0;
+        camera.pitch = modelpreviewpitch;
+        camera.roll = 0;
+        camera1 = &camera;
+
+        oldaspect = aspect;
+        oldfovy = fovy;
+        oldfov = curfov;
+        oldfarplane = farplane;
+        oldprojmatrix = projmatrix;
+
+        aspect = w/float(h);
+        fovy = modelpreviewfov;
+        curfov = 2*atan2(tan(fovy/2*RAD), 1/aspect)/RAD;
+        farplane = 1024;
+
+        clearfogdist();
+        zerofogcolor();
+        glClearColor(0, 0, 0, 1);
+
+        glClear((background ? GL_COLOR_BUFFER_BIT : 0) | GL_DEPTH_BUFFER_BIT);
+
+        projmatrix.perspective(fovy, aspect, nearplane, farplane);
+        setcamprojmatrix();
+
+        glEnable(GL_CULL_FACE);
+        glEnable(GL_DEPTH_TEST);
+    }
+
+    void end()
+    {
+        glDisable(GL_CULL_FACE);
+        glDisable(GL_DEPTH_TEST);
+
+        resetfogdist();
+        resetfogcolor();
+        glClearColor(curfogcolor.r, curfogcolor.g, curfogcolor.b, 1);
+
+        aspect = oldaspect;
+        fovy = oldfovy;
+        curfov = oldfov;
+        farplane = oldfarplane;
+
+        camera1 = oldcamera;
+        drawtex = 0;
+
+        glDisable(GL_SCISSOR_TEST);
+        glViewport(0, 0, screenw, screenh);
+
+        projmatrix = oldprojmatrix;
+        setcamprojmatrix();
+    }
+}
+
+vec calcmodelpreviewpos(const vec &radius, float &yaw)
+{
+    yaw = fmod(lastmillis/10000.0f*360.0f, 360.0f);
+    float dist = 1.15f*max(radius.magnitude2()/aspect, radius.magnitude())/sinf(fovy/2*RAD);
+    return vec(0, dist, 0).rotate_around_x(camera1->pitch*RAD);
+}
+
+GLuint minimaptex = 0;
+vec minimapcenter(0, 0, 0), minimapradius(0, 0, 0), minimapscale(0, 0, 0);
+
+void clearminimap()
+{
+    if(minimaptex) { glDeleteTextures(1, &minimaptex); minimaptex = 0; }
+}
+
+VARR(minimapheight, 0, 0, 2<<16);
+bvec minimapcolor(0, 0, 0);
+HVARFR(minimapcolour, 0, 0, 0xFFFFFF,
+{
+    minimapcolor = bvec((minimapcolour>>16)&0xFF, (minimapcolour>>8)&0xFF, minimapcolour&0xFF);
+});
+VARR(minimapclip, 0, 0, 1);
+VARFP(minimapsize, 7, 8, 10, { if(minimaptex) drawminimap(); });
+
+void bindminimap()
+{
+    glBindTexture(GL_TEXTURE_2D, minimaptex);
+}
+
+void clipminimap(ivec &bbmin, ivec &bbmax, cube *c = worldroot, const ivec &co = ivec(0, 0, 0), int size = worldsize>>1)
+{
+    loopi(8)
+    {
+        ivec o(i, co, size);
+        if(c[i].children) clipminimap(bbmin, bbmax, c[i].children, o, size>>1);
+        else if(!isentirelysolid(c[i]) && (c[i].material&MATF_CLIP)!=MAT_CLIP)
+        {
+            loopk(3) bbmin[k] = min(bbmin[k], o[k]);
+            loopk(3) bbmax[k] = max(bbmax[k], o[k] + size);
+        }
+    }
+}
+
+void drawminimap()
+{
+    if(!game::needminimap()) { clearminimap(); return; }
+
+    renderprogress(0, "generating mini-map...", 0, !renderedframe);
+
+    int size = 1<<minimapsize, sizelimit = min(hwtexsize, min(screenw, screenh));
+    while(size > sizelimit) size /= 2;
+    if(!minimaptex) glGenTextures(1, &minimaptex);
+
+    ivec bbmin(worldsize, worldsize, worldsize), bbmax(0, 0, 0);
+    loopv(valist)
+    {
+        vtxarray *va = valist[i];
+        loopk(3)
+        {
+            if(va->geommin[k]>va->geommax[k]) continue;
+            bbmin[k] = min(bbmin[k], va->geommin[k]);
+            bbmax[k] = max(bbmax[k], va->geommax[k]);
+        }
+    }
+    if(minimapclip)
+    {
+        ivec clipmin(worldsize, worldsize, worldsize), clipmax(0, 0, 0);
+        clipminimap(clipmin, clipmax);
+        loopk(2) bbmin[k] = max(bbmin[k], clipmin[k]);
+        loopk(2) bbmax[k] = min(bbmax[k], clipmax[k]); 
+    }
+    minimapradius = vec(bbmax).sub(vec(bbmin)).mul(0.5f); 
+    minimapcenter = vec(bbmin).add(minimapradius);
+    minimapradius.x = minimapradius.y = max(minimapradius.x, minimapradius.y);
+    minimapscale = vec((0.5f - 1.0f/size)/minimapradius.x, (0.5f - 1.0f/size)/minimapradius.y, 1.0f);
+
+    drawtex = DRAWTEX_MINIMAP;
+
+    physent *oldcamera = camera1;
+    static physent cmcamera;
+    cmcamera = *player;
+    cmcamera.reset();
+    cmcamera.type = ENT_CAMERA;
+    cmcamera.o = vec(minimapcenter.x, minimapcenter.y, max(minimapcenter.z + minimapradius.z + 1, float(minimapheight)));
+    cmcamera.yaw = 0;
+    cmcamera.pitch = -90;
+    cmcamera.roll = 0;
+    camera1 = &cmcamera;
+    setviewcell(vec(-1, -1, -1));
+
+    projmatrix.ortho(-minimapradius.x, minimapradius.x, -minimapradius.y, minimapradius.y, 0, camera1->o.z + 1);
+    projmatrix.a.mul(-1);
+    setcamprojmatrix();
+
+    setnofog(minimapcolor.tocolor());
+
+    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
+
+    glViewport(0, 0, size, size);
+
+    glEnable(GL_CULL_FACE);
+    glEnable(GL_DEPTH_TEST);
+
+    glFrontFace(GL_CCW);
+
+    xtravertsva = xtraverts = glde = gbatches = 0;
+
+    visiblecubes(false);
+    queryreflections();
+    drawreflections();
+
+    loopi(minimapheight > 0 && minimapheight < minimapcenter.z + minimapradius.z ? 2 : 1)
+    {
+        if(i)
+        {
+            glClear(GL_DEPTH_BUFFER_BIT);
+            camera1->o.z = minimapheight;
+            setcamprojmatrix();
+        }
+        rendergeom();
+        rendermapmodels();
+        renderwater();
+        rendermaterials();
+        renderalphageom();
+    }
+
+    glFrontFace(GL_CW);
+
+    glDisable(GL_DEPTH_TEST);
+    glDisable(GL_CULL_FACE);
+
+    glViewport(0, 0, screenw, screenh);
+
+    camera1 = oldcamera;
+    drawtex = 0;
+
+    glBindTexture(GL_TEXTURE_2D, minimaptex);
+    glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5, 0, 0, size, size, 0);
+    setuptexparameters(minimaptex, NULL, 3, 1, GL_RGB5, GL_TEXTURE_2D);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+    GLfloat border[4] = { minimapcolor.x/255.0f, minimapcolor.y/255.0f, minimapcolor.z/255.0f, 1.0f };
+    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
+    glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+bool deferdrawtextures = false;
+
+void drawtextures()
+{
+    if(minimized) { deferdrawtextures = true; return; }
+    deferdrawtextures = false;
+    genenvmaps();
+    drawminimap();
+}
+
+GLuint motiontex = 0;
+int motionw = 0, motionh = 0, lastmotion = 0;
+
+void cleanupmotionblur()
+{
+    if(motiontex) { glDeleteTextures(1, &motiontex); motiontex = 0; }
+    motionw = motionh = 0;
+    lastmotion = 0;
+}
+
+VARFP(motionblur, 0, 0, 1, { if(!motionblur) cleanupmotionblur(); });
+VARP(motionblurmillis, 1, 5, 1000);
+FVARP(motionblurscale, 0, 0.5f, 1);
+
+void addmotionblur()
+{
+    if(!motionblur || max(screenw, screenh) > hwtexsize) return;
+
+    if(game::ispaused()) { lastmotion = 0; return; }
+
+    if(!motiontex || motionw != screenw || motionh != screenh)
+    {
+        if(!motiontex) glGenTextures(1, &motiontex);
+        motionw = screenw;
+        motionh = screenh;
+        lastmotion = 0;
+        createtexture(motiontex, motionw, motionh, NULL, 3, 0, GL_RGB);
+    }
+
+    glBindTexture(GL_TEXTURE_2D, motiontex);
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+    SETSHADER(screenrect);
+
+    gle::colorf(1, 1, 1, lastmotion ? pow(motionblurscale, max(float(lastmillis - lastmotion)/motionblurmillis, 1.0f)) : 0);
+    screenquad(1, 1);
+
+    glDisable(GL_BLEND);
+
+    if(lastmillis - lastmotion >= motionblurmillis)
+    {
+        lastmotion = lastmillis - lastmillis%motionblurmillis;
+
+        glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
+    }
+}
+
+bool dopostfx = false;
+
+void invalidatepostfx()
+{
+    dopostfx = false;
+}
+
+int xtraverts, xtravertsva;
+
+void gl_drawframe()
+{
+    if(deferdrawtextures) drawtextures();
+
+    updatedynlights();
+
+    int w = screenw, h = screenh;
+    aspect = forceaspect ? forceaspect : w/float(h);
+    fovy = 2*atan2(tan(curfov/2*RAD), aspect)/RAD;
+    
+    int fogmat = lookupmaterial(camera1->o)&(MATF_VOLUME|MATF_INDEX), abovemat = MAT_AIR;
+    float fogblend = 1.0f, causticspass = 0.0f;
+    if(isliquid(fogmat&MATF_VOLUME))
+    {
+        float z = findsurface(fogmat, camera1->o, abovemat) - WATER_OFFSET;
+        if(camera1->o.z < z + 1) fogblend = min(z + 1 - camera1->o.z, 1.0f);
+        else fogmat = abovemat;
+        if(caustics && (fogmat&MATF_VOLUME)==MAT_WATER && camera1->o.z < z)
+            causticspass = min(z - camera1->o.z, 1.0f);
+    }
+    else fogmat = MAT_AIR;    
+    setfog(fogmat, fogblend, abovemat);
+    if(fogmat!=MAT_AIR)
+    {
+        float blend = abovemat==MAT_AIR ? fogblend : 1.0f;
+        fovy += blend*sinf(lastmillis/1000.0)*2.0f;
+        aspect += blend*sinf(lastmillis/1000.0+M_PI)*0.1f;
+    }
+
+    farplane = worldsize*2;
+
+    projmatrix.perspective(fovy, aspect, nearplane, farplane);
+    setcamprojmatrix();
+
+    glEnable(GL_CULL_FACE);
+    glEnable(GL_DEPTH_TEST);
+
+    xtravertsva = xtraverts = glde = gbatches = 0;
+
+    visiblecubes();
+    
+    glClear(GL_DEPTH_BUFFER_BIT|(wireframe && editmode ? GL_COLOR_BUFFER_BIT : 0));
+
+    if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); 
+
+    if(limitsky()) drawskybox(farplane, true);
+
+    rendergeom(causticspass);
+
+    extern int outline;
+    if(!wireframe && editmode && outline) renderoutline();
+
+    queryreflections();
+
+    generategrass();
+
+    if(!limitsky()) drawskybox(farplane, false);
+
+    renderdecals(true);
+
+    rendermapmodels();
+    rendergame(true);
+    renderavatar();
+
+    if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+    drawglaretex();
+    drawdepthfxtex();
+    drawreflections();
+
+    if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+
+    renderwater();
+    rendergrass();
+
+    rendermaterials();
+    renderalphageom();
+
+    if(wireframe && editmode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+    renderparticles(true);
+
+    glDisable(GL_CULL_FACE);
+    glDisable(GL_DEPTH_TEST);
+
+    addmotionblur();
+    addglare();
+    if(isliquid(fogmat&MATF_VOLUME)) drawfogoverlay(fogmat, fogblend, abovemat);
+    renderpostfx();
+
+    gl_drawhud();
+
+    renderedgame = false;
+}
+
+void gl_drawmainmenu()
+{
+    xtravertsva = xtraverts = glde = gbatches = 0;
+
+    renderbackground(NULL, NULL, NULL, NULL, true, true);
+    renderpostfx();
+
+    gl_drawhud();
+}
+
+VARNP(damagecompass, usedamagecompass, 0, 1, 1);
+VARP(damagecompassfade, 1, 1000, 10000);
+VARP(damagecompasssize, 1, 30, 100);
+VARP(damagecompassalpha, 1, 25, 100);
+VARP(damagecompassmin, 1, 25, 1000);
+VARP(damagecompassmax, 1, 200, 1000);
+
+float damagedirs[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+
+void damagecompass(int n, const vec &loc)
+{
+    if(!usedamagecompass || minimized) return;
+    vec delta(loc);
+    delta.sub(camera1->o); 
+    float yaw = 0, pitch;
+    if(delta.magnitude() > 4)
+    {
+        vectoyawpitch(delta, yaw, pitch);
+        yaw -= camera1->yaw;
+    }
+    if(yaw >= 360) yaw = fmod(yaw, 360);
+    else if(yaw < 0) yaw = 360 - fmod(-yaw, 360);
+    int dir = (int(yaw+22.5f)%360)/45;
+    damagedirs[dir] += max(n, damagecompassmin)/float(damagecompassmax);
+    if(damagedirs[dir]>1) damagedirs[dir] = 1;
+}
+
+void drawdamagecompass(int w, int h)
+{
+    hudnotextureshader->set();
+
+    int dirs = 0;
+    float size = damagecompasssize/100.0f*min(h, w)/2.0f;
+    loopi(8) if(damagedirs[i]>0)
+    {
+        if(!dirs)
+        {
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            gle::colorf(1, 0, 0, damagecompassalpha/100.0f);
+            gle::defvertex();
+            gle::begin(GL_TRIANGLES);
+        }
+        dirs++;
+
+        float logscale = 32,
+              scale = log(1 + (logscale - 1)*damagedirs[i]) / log(logscale),
+              offset = -size/2.0f-min(h, w)/4.0f;
+        matrix4x3 m;
+        m.identity();
+        m.settranslation(w/2, h/2, 0);
+        m.rotate_around_z(i*45*RAD);
+        m.translate(0, offset, 0);
+        m.scale(size*scale);
+
+        gle::attrib(m.transform(vec2(1, 1)));
+        gle::attrib(m.transform(vec2(-1, 1)));
+        gle::attrib(m.transform(vec2(0, 0)));
+
+        // fade in log space so short blips don't disappear too quickly
+        scale -= float(curtime)/damagecompassfade;
+        damagedirs[i] = scale > 0 ? (pow(logscale, scale) - 1) / (logscale - 1) : 0;
+    }
+    if(dirs) gle::end();
+}
+
+int damageblendmillis = 0;
+
+VARFP(damagescreen, 0, 1, 1, { if(!damagescreen) damageblendmillis = 0; });
+VARP(damagescreenfactor, 1, 7, 100);
+VARP(damagescreenalpha, 1, 45, 100);
+VARP(damagescreenfade, 0, 125, 1000);
+VARP(damagescreenmin, 1, 10, 1000);
+VARP(damagescreenmax, 1, 100, 1000);
+
+void damageblend(int n)
+{
+    if(!damagescreen || minimized) return;
+    if(lastmillis > damageblendmillis) damageblendmillis = lastmillis;
+    damageblendmillis += clamp(n, damagescreenmin, damagescreenmax)*damagescreenfactor;
+}
+
+void drawdamagescreen(int w, int h)
+{
+    if(lastmillis >= damageblendmillis) return;
+
+    hudshader->set();
+
+    static Texture *damagetex = NULL;
+    if(!damagetex) damagetex = textureload("packages/hud/damage.png", 3);
+
+    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+    glBindTexture(GL_TEXTURE_2D, damagetex->id);
+    float fade = damagescreenalpha/100.0f;
+    if(damageblendmillis - lastmillis < damagescreenfade)
+        fade *= float(damageblendmillis - lastmillis)/damagescreenfade;
+    gle::colorf(fade, fade, fade, fade);
+
+    hudquad(0, 0, w, h);
+}
+
+void cleardamagescreen()
+{
+    damageblendmillis = 0;
+    loopi(8) damagedirs[i] = 0;
+}
+
+VAR(hidestats, 0, 0, 1);
+VAR(hidehud, 0, 0, 1);
+
+VARP(crosshairsize, 0, 15, 50);
+VARP(cursorsize, 0, 30, 50);
+VARP(crosshairfx, 0, 1, 1);
+VARP(crosshaircolors, 0, 1, 1);
+
+#define MAXCROSSHAIRS 4
+static Texture *crosshairs[MAXCROSSHAIRS] = { NULL, NULL, NULL, NULL };
+
+void loadcrosshair(const char *name, int i)
+{
+    if(i < 0 || i >= MAXCROSSHAIRS) return;
+       crosshairs[i] = name ? textureload(name, 3, true) : notexture;
+    if(crosshairs[i] == notexture) 
+    {
+        name = game::defaultcrosshair(i);
+        if(!name) name = "data/crosshair.png";
+        crosshairs[i] = textureload(name, 3, true);
+    }
+}
+
+void loadcrosshair_(const char *name, int *i)
+{
+       loadcrosshair(name, *i);
+}
+
+COMMANDN(loadcrosshair, loadcrosshair_, "si");
+
+ICOMMAND(getcrosshair, "i", (int *i), 
+{
+    const char *name = "";
+    if(*i >= 0 && *i < MAXCROSSHAIRS)
+    {
+        name = crosshairs[*i] ? crosshairs[*i]->name : game::defaultcrosshair(*i);
+        if(!name) name = "data/crosshair.png";
+    }
+    result(name);
+});
+void writecrosshairs(stream *f)
+{
+    loopi(MAXCROSSHAIRS) if(crosshairs[i] && crosshairs[i]!=notexture)
+        f->printf("loadcrosshair %s %d\n", escapestring(crosshairs[i]->name), i);
+    f->printf("\n");
+}
+
+void drawcrosshair(int w, int h)
+{
+    bool windowhit = g3d_windowhit(true, false);
+    if(!windowhit && (hidehud || mainmenu)) return; //(hidehud || player->state==CS_SPECTATOR || player->state==CS_DEAD)) return;
+
+    vec color(1, 1, 1);
+    float cx = 0.5f, cy = 0.5f, chsize;
+    Texture *crosshair;
+    if(windowhit)
+    {
+        static Texture *cursor = NULL;
+        if(!cursor) cursor = textureload("data/guicursor.png", 3, true);
+        crosshair = cursor;
+        chsize = cursorsize*w/900.0f;
+        g3d_cursorpos(cx, cy);
+    }
+    else
+    { 
+        int index = game::selectcrosshair(color);
+        if(index < 0) return;
+        if(!crosshairfx) index = 0;
+        if(!crosshairfx || !crosshaircolors) color = vec(1, 1, 1);
+        crosshair = crosshairs[index];
+        if(!crosshair) 
+        {
+            loadcrosshair(NULL, index);
+            crosshair = crosshairs[index];
+        }
+        chsize = crosshairsize*w/900.0f;
+    }
+    if(crosshair->type&Texture::ALPHA) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    else glBlendFunc(GL_ONE, GL_ONE);
+    float x = cx*w - (windowhit ? 0 : chsize/2.0f);
+    float y = cy*h - (windowhit ? 0 : chsize/2.0f);
+    glBindTexture(GL_TEXTURE_2D, crosshair->id);
+
+    hudshader->set();
+    gle::color(color);
+    hudquad(x, y, chsize, chsize);
+}
+
+VARP(wallclock, 0, 0, 1);
+VARP(wallclock24, 0, 0, 1);
+VARP(wallclocksecs, 0, 0, 1);
+
+static time_t walltime = 0;
+
+VARP(showfps, 0, 1, 1);
+VARP(showfpsrange, 0, 0, 1);
+VAR(showeditstats, 0, 0, 1);
+VAR(statrate, 1, 200, 1000);
+
+FVARP(conscale, 1e-3f, 0.33f, 1e3f);
+
+void gl_drawhud()
+{
+    g3d_render();
+
+    int w = screenw, h = screenh;
+    if(forceaspect) w = int(ceil(h*forceaspect));
+
+    if(editmode && !hidehud && !mainmenu)
+    {
+        glEnable(GL_DEPTH_TEST);
+        glDepthMask(GL_FALSE);
+
+        renderblendbrush();
+
+        rendereditcursor();
+
+        glDepthMask(GL_TRUE);
+        glDisable(GL_DEPTH_TEST);
+    }
+
+    gettextres(w, h);
+
+    hudmatrix.ortho(0, w, h, 0, -1, 1);
+    resethudmatrix();
+    
+    gle::colorf(1, 1, 1);
+
+    extern int debugsm;
+    if(debugsm)
+    {
+        extern void viewshadowmap();
+        viewshadowmap();
+    }
+
+    extern int debugglare;
+    if(debugglare)
+    {
+        extern void viewglaretex();
+        viewglaretex();
+    }
+
+    extern int debugdepthfx;
+    if(debugdepthfx)
+    {
+        extern void viewdepthfxtex();
+        viewdepthfxtex();
+    }
+
+    glEnable(GL_BLEND);
+   
+    extern void debugparticles();
+    debugparticles();
+    if(!mainmenu)
+    {
+        drawdamagescreen(w, h);
+        drawdamagecompass(w, h);
+    }
+
+    hudshader->set();
+
+    int conw = int(w/conscale), conh = int(h/conscale), abovehud = conh - FONTH, limitgui = abovehud;
+    if(!hidehud && !mainmenu)
+    {
+        if(!hidestats)
+        {
+            pushhudmatrix();
+            hudmatrix.scale(conscale, conscale, 1);
+            flushhudmatrix();
+
+            int roffset = 0;
+            if(showfps)
+            {
+                static int lastfps = 0, prevfps[3] = { 0, 0, 0 }, curfps[3] = { 0, 0, 0 };
+                if(totalmillis - lastfps >= statrate)
+                {
+                    memcpy(prevfps, curfps, sizeof(prevfps));
+                    lastfps = totalmillis - (totalmillis%statrate);
+                }
+                int nextfps[3];
+                getfps(nextfps[0], nextfps[1], nextfps[2]);
+                loopi(3) if(prevfps[i]==curfps[i]) curfps[i] = nextfps[i];
+                if(showfpsrange) draw_textf("fps %d+%d-%d", conw-7*FONTH, conh-FONTH*3/2, curfps[0], curfps[1], curfps[2]);
+                else draw_textf("fps %d", conw-5*FONTH, conh-FONTH*3/2, curfps[0]);
+                roffset += FONTH;
+            }
+
+            if(wallclock)
+            {
+                if(!walltime) { walltime = time(NULL); walltime -= totalmillis/1000; if(!walltime) walltime++; }
+                time_t walloffset = walltime + totalmillis/1000;
+                struct tm *localvals = localtime(&walloffset);
+                static string buf;
+                if(localvals && strftime(buf, sizeof(buf), wallclocksecs ? (wallclock24 ? "%H:%M:%S" : "%I:%M:%S%p") : (wallclock24 ? "%H:%M" : "%I:%M%p"), localvals))
+                {
+                    // hack because not all platforms (windows) support %P lowercase option
+                    // also strip leading 0 from 12 hour time
+                    char *dst = buf;
+                    const char *src = &buf[!wallclock24 && buf[0]=='0' ? 1 : 0];
+                    while(*src) *dst++ = tolower(*src++);
+                    *dst++ = '\0'; 
+                    draw_text(buf, conw-5*FONTH, conh-FONTH*3/2-roffset);
+                    roffset += FONTH;
+                }
+            }
+                       
+            if(editmode || showeditstats)
+            {
+                static int laststats = 0, prevstats[8] = { 0, 0, 0, 0, 0, 0, 0 }, curstats[8] = { 0, 0, 0, 0, 0, 0, 0 };
+                if(totalmillis - laststats >= statrate)
+                {
+                    memcpy(prevstats, curstats, sizeof(prevstats));
+                    laststats = totalmillis - (totalmillis%statrate);
+                }
+                int nextstats[8] =
+                {
+                    vtris*100/max(wtris, 1),
+                    vverts*100/max(wverts, 1),
+                    xtraverts/1024,
+                    xtravertsva/1024,
+                    glde,
+                    gbatches,
+                    getnumqueries(),
+                    rplanes
+                };
+                loopi(8) if(prevstats[i]==curstats[i]) curstats[i] = nextstats[i];
+
+                abovehud -= 2*FONTH;
+                draw_textf("wtr:%dk(%d%%) wvt:%dk(%d%%) evt:%dk eva:%dk", FONTH/2, abovehud, wtris/1024, curstats[0], wverts/1024, curstats[1], curstats[2], curstats[3]);
+                draw_textf("ond:%d va:%d gl:%d(%d) oq:%d lm:%d rp:%d pvs:%d", FONTH/2, abovehud+FONTH, allocnodes*8, allocva, curstats[4], curstats[5], curstats[6], lightmaps.length(), curstats[7], getnumviewcells());
+                limitgui = abovehud;
+            }
+
+            if(editmode)
+            {
+                abovehud -= FONTH;
+                draw_textf("cube %s%d%s", FONTH/2, abovehud, selchildcount<0 ? "1/" : "", abs(selchildcount), showmat && selchildmat > 0 ? getmaterialdesc(selchildmat, ": ") : "");
+
+                if(char *editinfo = execidentstr("edithud"))
+                {
+                    if(editinfo[0])
+                    {
+                        int tw, th;
+                        text_bounds(editinfo, tw, th);
+                        th += FONTH-1; th -= th%FONTH;
+                        abovehud -= max(th, FONTH);
+                        draw_text(editinfo, FONTH/2, abovehud);
+                    }
+                    DELETEA(editinfo);
+                }
+            }
+            else if(char *gameinfo = execidentstr("gamehud"))
+            {
+                if(gameinfo[0])
+                {
+                    int tw, th;
+                    text_bounds(gameinfo, tw, th);
+                    th += FONTH-1; th -= th%FONTH;
+                    roffset += max(th, FONTH);
+                    draw_text(gameinfo, conw-max(5*FONTH, 2*FONTH+tw), conh-FONTH/2-roffset);
+                }
+                DELETEA(gameinfo);
+            }
+
+            pophudmatrix();
+        }
+
+        if(hidestats || (!editmode && !showeditstats))
+        {
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            game::gameplayhud(w, h);
+            limitgui = abovehud = min(abovehud, int(conh*game::abovegameplayhud(w, h)));
+        }
+
+        rendertexturepanel(w, h);
+    }
+
+    glDisable(GL_BLEND);
+
+    g3d_limitscale((2*limitgui - conh) / float(conh));
+    g3d_render2d();
+
+    glEnable(GL_BLEND);
+
+    hudmatrix.ortho(0, w, h, 0, -1, 1);
+    resethudmatrix();
+
+    pushhudmatrix();
+    hudmatrix.scale(conscale, conscale, 1);
+    flushhudmatrix();
+    abovehud -= rendercommand(FONTH/2, abovehud - FONTH/2, conw-FONTH);
+    extern int fullconsole;
+    if(!hidehud || fullconsole) renderconsole(conw, conh, abovehud - FONTH/2);
+    pophudmatrix();
+
+    drawcrosshair(w, h);
+
+    glDisable(GL_BLEND);
+}
+
+void cleanupgl()
+{
+    cleanupmotionblur();
+
+    clearminimap();
+
+    cleanupscreenquad();
+
+    gle::cleanup();
+}
+
+
diff --git a/src/engine/rendermodel.cpp b/src/engine/rendermodel.cpp
new file mode 100644 (file)
index 0000000..dc05e69
--- /dev/null
@@ -0,0 +1,1142 @@
+#include "engine.h"
+
+VAR(oqdynent, 0, 1, 1);
+VAR(animationinterpolationtime, 0, 150, 1000);
+
+model *loadingmodel = NULL;
+
+#include "ragdoll.h"
+#include "animmodel.h"
+#include "vertmodel.h"
+#include "skelmodel.h"
+
+static model *(__cdecl *modeltypes[NUMMODELTYPES])(const char *);
+
+static int addmodeltype(int type, model *(__cdecl *loader)(const char *))
+{
+    modeltypes[type] = loader;
+    return type;
+}
+
+#define MODELTYPE(modeltype, modelclass) \
+static model *__loadmodel__##modelclass(const char *filename) \
+{ \
+    return new modelclass(filename); \
+} \
+UNUSED static int __dummy__##modelclass = addmodeltype((modeltype), __loadmodel__##modelclass);
+#include "md2.h"
+#include "md3.h"
+#include "md5.h"
+#include "obj.h"
+#include "smd.h"
+#include "iqm.h"
+
+MODELTYPE(MDL_MD2, md2);
+MODELTYPE(MDL_MD3, md3);
+MODELTYPE(MDL_MD5, md5);
+MODELTYPE(MDL_OBJ, obj);
+MODELTYPE(MDL_SMD, smd);
+MODELTYPE(MDL_IQM, iqm);
+
+#define checkmdl if(!loadingmodel) { conoutf(CON_ERROR, "not loading a model"); return; }
+
+void mdlcullface(int *cullface)
+{
+    checkmdl;
+    loadingmodel->setcullface(*cullface!=0);
+}
+
+COMMAND(mdlcullface, "i");
+
+void mdlcollide(int *collide)
+{
+    checkmdl;
+    loadingmodel->collide = *collide!=0;
+}
+
+COMMAND(mdlcollide, "i");
+
+void mdlellipsecollide(int *collide)
+{
+    checkmdl;
+    loadingmodel->ellipsecollide = *collide!=0;
+}   
+    
+COMMAND(mdlellipsecollide, "i");
+
+void mdlspec(int *percent)
+{
+    checkmdl;
+    float spec = 1.0f; 
+    if(*percent>0) spec = *percent/100.0f;
+    else if(*percent<0) spec = 0.0f;
+    loadingmodel->setspec(spec);
+}
+
+COMMAND(mdlspec, "i");
+
+void mdlambient(int *percent)
+{
+    checkmdl;
+    float ambient = 0.3f;
+    if(*percent>0) ambient = *percent/100.0f;
+    else if(*percent<0) ambient = 0.0f;
+    loadingmodel->setambient(ambient);
+}
+
+COMMAND(mdlambient, "i");
+
+void mdlalphatest(float *cutoff)
+{   
+    checkmdl;
+    loadingmodel->setalphatest(max(0.0f, min(1.0f, *cutoff)));
+}
+
+COMMAND(mdlalphatest, "f");
+
+void mdlalphablend(int *blend)
+{   
+    checkmdl;
+    loadingmodel->setalphablend(*blend!=0);
+}
+
+COMMAND(mdlalphablend, "i");
+
+void mdlalphadepth(int *depth)
+{
+    checkmdl;
+    loadingmodel->alphadepth = *depth!=0;
+}
+
+COMMAND(mdlalphadepth, "i");
+
+void mdldepthoffset(int *offset)
+{
+    checkmdl;
+    loadingmodel->depthoffset = *offset!=0;
+}
+
+COMMAND(mdldepthoffset, "i");
+
+void mdlglow(int *percent, int *delta, float *pulse)
+{
+    checkmdl;
+    float glow = 3.0f, glowdelta = *delta/100.0f, glowpulse = *pulse > 0 ? *pulse/1000.0f : 0;
+    if(*percent>0) glow = *percent/100.0f;
+    else if(*percent<0) glow = 0.0f;
+    glowdelta -= glow;
+    loadingmodel->setglow(glow, glowdelta, glowpulse);
+}
+
+COMMAND(mdlglow, "iif");
+
+void mdlglare(float *specglare, float *glowglare)
+{
+    checkmdl;
+    loadingmodel->setglare(*specglare, *glowglare);
+}
+
+COMMAND(mdlglare, "ff");
+
+void mdlenvmap(float *envmapmax, float *envmapmin, char *envmap)
+{
+    checkmdl;
+    loadingmodel->setenvmap(*envmapmin, *envmapmax, envmap[0] ? cubemapload(envmap) : NULL);
+}
+
+COMMAND(mdlenvmap, "ffs");
+
+void mdlfullbright(float *fullbright)
+{
+    checkmdl;
+    loadingmodel->setfullbright(*fullbright);
+}
+
+COMMAND(mdlfullbright, "f");
+
+void mdlshader(char *shader)
+{
+    checkmdl;
+    loadingmodel->setshader(lookupshaderbyname(shader));
+}
+
+COMMAND(mdlshader, "s");
+
+void mdlspin(float *yaw, float *pitch)
+{
+    checkmdl;
+    loadingmodel->spinyaw = *yaw;
+    loadingmodel->spinpitch = *pitch;
+}
+
+COMMAND(mdlspin, "ff");
+
+void mdlscale(int *percent)
+{
+    checkmdl;
+    float scale = 1.0f;
+    if(*percent>0) scale = *percent/100.0f;
+    loadingmodel->scale = scale;
+}  
+
+COMMAND(mdlscale, "i");
+
+void mdltrans(float *x, float *y, float *z)
+{
+    checkmdl;
+    loadingmodel->translate = vec(*x, *y, *z);
+} 
+
+COMMAND(mdltrans, "fff");
+
+void mdlyaw(float *angle)
+{
+    checkmdl;
+    loadingmodel->offsetyaw = *angle;
+}
+
+COMMAND(mdlyaw, "f");
+
+void mdlpitch(float *angle)
+{
+    checkmdl;
+    loadingmodel->offsetpitch = *angle;
+}
+
+COMMAND(mdlpitch, "f");
+
+void mdlshadow(int *shadow)
+{
+    checkmdl;
+    loadingmodel->shadow = *shadow!=0;
+}
+
+COMMAND(mdlshadow, "i");
+
+void mdlbb(float *rad, float *h, float *eyeheight)
+{
+    checkmdl;
+    loadingmodel->collidexyradius = *rad;
+    loadingmodel->collideheight = *h;
+    loadingmodel->eyeheight = *eyeheight; 
+}
+
+COMMAND(mdlbb, "fff");
+
+void mdlextendbb(float *x, float *y, float *z)
+{
+    checkmdl;
+    loadingmodel->bbextend = vec(*x, *y, *z);
+}
+
+COMMAND(mdlextendbb, "fff");
+
+void mdlname()
+{
+    checkmdl;
+    result(loadingmodel->name);
+}
+
+COMMAND(mdlname, "");
+
+#define checkragdoll \
+    checkmdl; \
+    if(!loadingmodel->skeletal()) { conoutf(CON_ERROR, "not loading a skeletal model"); return; } \
+    skelmodel *m = (skelmodel *)loadingmodel; \
+    if(m->parts.empty()) return; \
+    skelmodel::skelmeshgroup *meshes = (skelmodel::skelmeshgroup *)m->parts.last()->meshes; \
+    if(!meshes) return; \
+    skelmodel::skeleton *skel = meshes->skel; \
+    if(!skel->ragdoll) skel->ragdoll = new ragdollskel; \
+    ragdollskel *ragdoll = skel->ragdoll; \
+    if(ragdoll->loaded) return;
+    
+
+void rdvert(float *x, float *y, float *z, float *radius)
+{
+    checkragdoll;
+    ragdollskel::vert &v = ragdoll->verts.add();
+    v.pos = vec(*x, *y, *z);
+    v.radius = *radius > 0 ? *radius : 1;
+}
+COMMAND(rdvert, "ffff");
+
+void rdeye(int *v)
+{
+    checkragdoll;
+    ragdoll->eye = *v;
+}
+COMMAND(rdeye, "i");
+
+void rdtri(int *v1, int *v2, int *v3)
+{
+    checkragdoll;
+    ragdollskel::tri &t = ragdoll->tris.add();
+    t.vert[0] = *v1;
+    t.vert[1] = *v2;
+    t.vert[2] = *v3;
+}
+COMMAND(rdtri, "iii");
+
+void rdjoint(int *n, int *t, int *v1, int *v2, int *v3)
+{
+    checkragdoll;
+    if(*n < 0 || *n >= skel->numbones) return;
+    ragdollskel::joint &j = ragdoll->joints.add();
+    j.bone = *n;
+    j.tri = *t;
+    j.vert[0] = *v1;
+    j.vert[1] = *v2;
+    j.vert[2] = *v3;
+}
+COMMAND(rdjoint, "iibbb");
+   
+void rdlimitdist(int *v1, int *v2, float *mindist, float *maxdist)
+{
+    checkragdoll;
+    ragdollskel::distlimit &d = ragdoll->distlimits.add();
+    d.vert[0] = *v1;
+    d.vert[1] = *v2;
+    d.mindist = *mindist;
+    d.maxdist = max(*maxdist, *mindist);
+}
+COMMAND(rdlimitdist, "iiff");
+
+void rdlimitrot(int *t1, int *t2, float *maxangle, float *qx, float *qy, float *qz, float *qw)
+{
+    checkragdoll;
+    ragdollskel::rotlimit &r = ragdoll->rotlimits.add();
+    r.tri[0] = *t1;
+    r.tri[1] = *t2;
+    r.maxangle = *maxangle * RAD;
+    r.middle = matrix3(quat(*qx, *qy, *qz, *qw));
+}
+COMMAND(rdlimitrot, "iifffff");
+
+void rdanimjoints(int *on)
+{
+    checkragdoll;
+    ragdoll->animjoints = *on!=0;
+}
+COMMAND(rdanimjoints, "i");
+
+// mapmodels
+
+vector<mapmodelinfo> mapmodels;
+
+void mmodel(char *name)
+{
+    mapmodelinfo &mmi = mapmodels.add();
+    copystring(mmi.name, name);
+    mmi.m = NULL;
+}
+
+void mapmodelcompat(int *rad, int *h, int *tex, char *name, char *shadow)
+{
+    mmodel(name);
+}
+
+void mapmodelreset(int *n) 
+{ 
+    if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return;
+    mapmodels.shrink(clamp(*n, 0, mapmodels.length())); 
+}
+
+mapmodelinfo *getmminfo(int i) { return mapmodels.inrange(i) ? &mapmodels[i] : 0; }
+const char *mapmodelname(int i) { return mapmodels.inrange(i) ? mapmodels[i].name : NULL; }
+
+COMMAND(mmodel, "s");
+COMMANDN(mapmodel, mapmodelcompat, "iiiss");
+COMMAND(mapmodelreset, "i");
+ICOMMAND(mapmodelname, "i", (int *index), { result(mapmodels.inrange(*index) ? mapmodels[*index].name : ""); });
+ICOMMAND(mapmodelloaded, "i", (int *index), { intret(mapmodels.inrange(*index) && mapmodels[*index].m ? 1 : 0); });
+ICOMMAND(nummapmodels, "", (), { intret(mapmodels.length()); });
+ICOMMAND(mapmodelfind, "s", (char *name), { int found = -1; loopv(mapmodels) if(strstr(mapmodels[i].name, name)) { found = i; break; } intret(found); });
+
+// model registry
+
+hashnameset<model *> models;
+vector<const char *> preloadmodels;
+
+void preloadmodel(const char *name)
+{
+    if(!name || !name[0] || models.access(name)) return;
+    preloadmodels.add(newstring(name));
+}
+
+void flushpreloadedmodels(bool msg)
+{
+    loopv(preloadmodels)
+    {
+        loadprogress = float(i+1)/preloadmodels.length();
+        model *m = loadmodel(preloadmodels[i], -1, msg);
+        if(!m) { if(msg) conoutf(CON_WARN, "could not load model: %s", preloadmodels[i]); }
+        else
+        {
+            m->preloadmeshes();
+        }
+    }
+    preloadmodels.deletearrays();
+    loadprogress = 0;
+}
+
+void preloadusedmapmodels(bool msg, bool bih)
+{
+    vector<extentity *> &ents = entities::getents();
+    vector<int> mapmodels;
+    loopv(ents)
+    {
+        extentity &e = *ents[i];
+        if(e.type==ET_MAPMODEL && e.attr2 >= 0 && mapmodels.find(e.attr2) < 0) mapmodels.add(e.attr2);
+    }
+
+    loopv(mapmodels)
+    {
+        loadprogress = float(i+1)/mapmodels.length();
+        int mmindex = mapmodels[i];
+        mapmodelinfo *mmi = getmminfo(mmindex);
+        if(!mmi) { if(msg) conoutf(CON_WARN, "could not find map model: %d", mmindex); }
+        else if(mmi->name[0] && !loadmodel(NULL, mmindex, msg)) { if(msg) conoutf(CON_WARN, "could not load model: %s", mmi->name); }
+        else if(mmi->m)
+        {
+            if(bih) mmi->m->preloadBIH();
+            mmi->m->preloadmeshes();
+        }
+    }
+    loadprogress = 0;
+}
+
+bool modelloaded(const char *name)
+{
+    return models.find(name, NULL) != NULL;
+}
+
+model *loadmodel(const char *name, int i, bool msg)
+{
+    if(!name)
+    {
+        if(!mapmodels.inrange(i)) return NULL;
+        mapmodelinfo &mmi = mapmodels[i];
+        if(mmi.m) return mmi.m;
+        name = mmi.name;
+    }
+    model **mm = models.access(name);
+    model *m;
+    if(mm) m = *mm;
+    else
+    { 
+        if(!name[0] || loadingmodel || lightmapping > 1) return NULL;
+        if(msg)
+        {
+            defformatstring(filename, "packages/models/%s", name);
+            renderprogress(loadprogress, filename);
+        }
+        loopi(NUMMODELTYPES)
+        {
+            m = modeltypes[i](name);
+            if(!m) continue;
+            loadingmodel = m;
+            if(m->load()) break;
+            DELETEP(m);
+        }
+        loadingmodel = NULL;
+        if(!m) return NULL;
+        models.access(m->name, m);
+        m->preloadshaders();
+    }
+    if(mapmodels.inrange(i) && !mapmodels[i].m) mapmodels[i].m = m;
+    return m;
+}
+
+void preloadmodelshaders(bool force)
+{
+    if(initing) return;
+    enumerate(models, model *, m, m->preloadshaders(force));
+}
+
+void clear_mdls()
+{
+    enumerate(models, model *, m, delete m);
+}
+
+void cleanupmodels()
+{
+    enumerate(models, model *, m, m->cleanup());
+}
+
+void clearmodel(char *name)
+{
+    model **m = models.access(name);
+    if(!m) { conoutf(CON_WARN, "model %s is not loaded", name); return; }
+    loopv(mapmodels) if(mapmodels[i].m==*m) mapmodels[i].m = NULL;
+    models.remove(name);
+    (*m)->cleanup();
+    delete *m;
+    conoutf("cleared model %s", name);
+}
+
+COMMAND(clearmodel, "s");
+
+bool modeloccluded(const vec &center, float radius)
+{
+    ivec bbmin(vec(center).sub(radius)), bbmax(ivec(center).add(radius+1));
+    return pvsoccluded(bbmin, bbmax) || bboccluded(bbmin, bbmax);
+}
+
+VAR(showboundingbox, 0, 0, 2);
+
+void render2dbox(vec &o, float x, float y, float z)
+{
+    gle::begin(GL_LINE_LOOP);
+    gle::attribf(o.x, o.y, o.z);
+    gle::attribf(o.x, o.y, o.z+z);
+    gle::attribf(o.x+x, o.y+y, o.z+z);
+    gle::attribf(o.x+x, o.y+y, o.z);
+    xtraverts += gle::end();
+}
+
+void render3dbox(vec &o, float tofloor, float toceil, float xradius, float yradius)
+{
+    if(yradius<=0) yradius = xradius;
+    vec c = o;
+    c.sub(vec(xradius, yradius, tofloor));
+    float xsz = xradius*2, ysz = yradius*2;
+    float h = tofloor+toceil;
+    gle::colorf(1, 1, 1);
+    gle::defvertex();
+    render2dbox(c, xsz, 0, h);
+    render2dbox(c, 0, ysz, h);
+    c.add(vec(xsz, ysz, 0));
+    render2dbox(c, -xsz, 0, h);
+    render2dbox(c, 0, -ysz, h);
+}
+
+void renderellipse(vec &o, float xradius, float yradius, float yaw)
+{
+    gle::colorf(0.5f, 0.5f, 0.5f);
+    gle::defvertex();
+    gle::begin(GL_LINE_LOOP);
+    loopi(15)
+    {
+        const vec2 &sc = sincos360[i*(360/15)];
+        gle::attrib(vec(xradius*sc.x, yradius*sc.y, 0).rotate_around_z((yaw+90)*RAD).add(o));
+    }
+    xtraverts += gle::end();
+}
+
+struct batchedmodel
+{
+    vec pos, color, dir;
+    int anim;
+    float yaw, pitch, transparent;
+    int basetime, basetime2, flags;
+    dynent *d;
+    int attached;
+    occludequery *query;
+};  
+struct modelbatch
+{
+    model *m;
+    int flags;
+    vector<batchedmodel> batched;
+};  
+static vector<modelbatch *> batches;
+static vector<modelattach> modelattached;
+static int numbatches = -1;
+static occludequery *modelquery = NULL;
+
+void startmodelbatches()
+{
+    numbatches = 0;
+    modelattached.setsize(0);
+}
+
+modelbatch &addbatchedmodel(model *m)
+{
+    modelbatch *b = NULL;
+    if(m->batch>=0 && m->batch<numbatches && batches[m->batch]->m==m) b = batches[m->batch];
+    else
+    {
+        if(numbatches<batches.length())
+        {
+            b = batches[numbatches];
+            b->batched.setsize(0);
+        }
+        else b = batches.add(new modelbatch);
+        b->m = m;
+        b->flags = 0;
+        m->batch = numbatches++;
+    }
+    return *b;
+}
+
+void renderbatchedmodel(model *m, batchedmodel &b)
+{
+    modelattach *a = NULL;
+    if(b.attached>=0) a = &modelattached[b.attached];
+
+    int anim = b.anim;
+    if(shadowmapping)
+    {
+        anim |= ANIM_NOSKIN; 
+        GLOBALPARAMF(shadowintensity, b.transparent);
+    }
+    else 
+    {
+        if(b.flags&MDL_FULLBRIGHT) anim |= ANIM_FULLBRIGHT;
+        if(b.flags&MDL_GHOST) anim |= ANIM_GHOST;
+    }
+
+    m->render(anim, b.basetime, b.basetime2, b.pos, b.yaw, b.pitch, b.d, a, b.color, b.dir, b.transparent);
+}
+
+struct transparentmodel
+{
+    model *m;
+    batchedmodel *batched;
+    float dist;
+};
+
+static inline bool sorttransparentmodels(const transparentmodel &x, const transparentmodel &y)
+{
+    return x.dist < y.dist;
+}
+
+void endmodelbatches()
+{
+    vector<transparentmodel> transparent;
+    loopi(numbatches)
+    {
+        modelbatch &b = *batches[i];
+        if(b.batched.empty()) continue;
+        if(b.flags&(MDL_SHADOW|MDL_DYNSHADOW))
+        {
+            vec center, bbradius;
+            b.m->boundbox(center, bbradius);
+            loopvj(b.batched)
+            {
+                batchedmodel &bm = b.batched[j];
+                if(bm.flags&(MDL_SHADOW|MDL_DYNSHADOW))
+                    renderblob(bm.flags&MDL_DYNSHADOW ? BLOB_DYNAMIC : BLOB_STATIC, bm.d && bm.d->ragdoll ? bm.d->ragdoll->center : bm.pos, bm.d ? bm.d->radius : max(bbradius.x, bbradius.y), bm.transparent);
+            }
+            flushblobs();
+        }
+        bool rendered = false;
+        occludequery *query = NULL;
+        if(b.flags&MDL_GHOST)
+        {
+            loopvj(b.batched)
+            {
+                batchedmodel &bm = b.batched[j];
+                if((bm.flags&(MDL_CULL_VFC|MDL_GHOST))!=MDL_GHOST || bm.query) continue;
+                if(!rendered) { b.m->startrender(); rendered = true; }
+                renderbatchedmodel(b.m, bm);
+            }
+            if(rendered) 
+            {
+                b.m->endrender();
+                rendered = false;
+            }
+        }
+        loopvj(b.batched) 
+        {
+            batchedmodel &bm = b.batched[j];
+            if(bm.flags&(MDL_CULL_VFC|MDL_GHOST)) continue;
+            if(bm.query!=query)
+            {
+                if(query) endquery(query);
+                query = bm.query;
+                if(query) startquery(query);
+            }
+            if(bm.transparent < 1 && (!query || query->owner==bm.d) && !shadowmapping)
+            {
+                transparentmodel &tm = transparent.add();
+                tm.m = b.m;
+                tm.batched = &bm;
+                tm.dist = camera1->o.dist(bm.d && bm.d->ragdoll ? bm.d->ragdoll->center : bm.pos);
+                continue;
+            }
+            if(!rendered) { b.m->startrender(); rendered = true; }
+            renderbatchedmodel(b.m, bm);
+        }
+        if(query) endquery(query);
+        if(rendered) b.m->endrender();
+    }
+    if(transparent.length())
+    {
+        transparent.sort(sorttransparentmodels);
+        model *lastmodel = NULL;
+        occludequery *query = NULL;
+        loopv(transparent)
+        {
+            transparentmodel &tm = transparent[i];
+            if(lastmodel!=tm.m)
+            {
+                if(lastmodel) lastmodel->endrender();
+                (lastmodel = tm.m)->startrender();
+            }
+            if(query!=tm.batched->query)
+            {
+                if(query) endquery(query);
+                query = tm.batched->query;
+                if(query) startquery(query);
+            }
+            renderbatchedmodel(tm.m, *tm.batched);
+        }
+        if(query) endquery(query);
+        if(lastmodel) lastmodel->endrender();
+    }
+    numbatches = -1;
+}
+
+void startmodelquery(occludequery *query)
+{
+    modelquery = query;
+}
+
+void endmodelquery()
+{
+    int querybatches = 0;
+    loopi(numbatches)
+    {
+        modelbatch &b = *batches[i];
+        if(b.batched.empty() || b.batched.last().query!=modelquery) continue;
+        querybatches++;
+    }
+    if(querybatches<=1)
+    {
+        if(!querybatches) modelquery->fragments = 0;
+        modelquery = NULL;
+        return;
+    }
+    int minattached = modelattached.length();
+    startquery(modelquery);
+    loopi(numbatches)
+    {
+        modelbatch &b = *batches[i];
+        if(b.batched.empty() || b.batched.last().query!=modelquery) continue;
+        b.m->startrender();
+        do
+        {
+            batchedmodel &bm = b.batched.pop();
+            if(bm.attached>=0) minattached = min(minattached, bm.attached);
+            renderbatchedmodel(b.m, bm);
+        }
+        while(b.batched.length() && b.batched.last().query==modelquery);
+        b.m->endrender();
+    }
+    endquery(modelquery);
+    modelquery = NULL;
+    modelattached.setsize(minattached);
+}
+
+VAR(maxmodelradiusdistance, 10, 200, 1000);
+
+static inline void enablecullmodelquery()
+{
+    startbb();
+}
+
+static inline void rendercullmodelquery(model *m, dynent *d, const vec &center, float radius)
+{
+    if(fabs(camera1->o.x-center.x) < radius+1 &&
+       fabs(camera1->o.y-center.y) < radius+1 &&
+       fabs(camera1->o.z-center.z) < radius+1)
+    {
+        d->query = NULL;
+        return;
+    }
+    d->query = newquery(d);
+    if(!d->query) return;
+    startquery(d->query);
+    int br = int(radius*2)+1;
+    drawbb(ivec(int(center.x-radius), int(center.y-radius), int(center.z-radius)), ivec(br, br, br));
+    endquery(d->query);
+}
+
+static inline void disablecullmodelquery()
+{
+    endbb();
+}
+
+static inline int cullmodel(model *m, const vec &center, float radius, int flags, dynent *d = NULL, bool shadow = false)
+{
+    if(flags&MDL_CULL_DIST && center.dist(camera1->o)/radius>maxmodelradiusdistance) return MDL_CULL_DIST;
+    if(flags&MDL_CULL_VFC)
+    {
+        if(reflecting || refracting)
+        {
+            if(reflecting || refracting>0)
+            {
+                if(center.z+radius<=reflectz) return MDL_CULL_VFC;
+            }
+            else
+            {
+                if(fogging && center.z+radius<reflectz-refractfog) return MDL_CULL_VFC;
+                if(!shadow && center.z-radius>=reflectz) return MDL_CULL_VFC;
+            }
+            if(center.dist(camera1->o)-radius>reflectdist) return MDL_CULL_VFC;
+        }
+        if(isfoggedsphere(radius, center)) return MDL_CULL_VFC;
+        if(shadowmapping && !isshadowmapcaster(center, radius)) return MDL_CULL_VFC;
+    }
+    if(shadowmapping)
+    {
+        if(d)
+        {
+            if(flags&MDL_CULL_OCCLUDED && d->occluded>=OCCLUDE_PARENT) return MDL_CULL_OCCLUDED;
+            if(flags&MDL_CULL_QUERY && d->occluded+1>=OCCLUDE_BB && d->query && d->query->owner==d && checkquery(d->query)) return MDL_CULL_QUERY;
+        }
+        if(!addshadowmapcaster(center, radius, radius)) return MDL_CULL_VFC;
+    }
+    else if(flags&MDL_CULL_OCCLUDED && modeloccluded(center, radius))
+    {
+        if(!reflecting && !refracting && d) d->occluded = OCCLUDE_PARENT;
+        return MDL_CULL_OCCLUDED;
+    }
+    else if(flags&MDL_CULL_QUERY && d->query && d->query->owner==d && checkquery(d->query))
+    {
+        if(!reflecting && !refracting && d->occluded<OCCLUDE_BB) d->occluded++;
+        return MDL_CULL_QUERY;
+    }
+    return 0;
+}
+
+void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, float yaw, float pitch, int flags, dynent *d, modelattach *a, int basetime, int basetime2, float trans)
+{
+    if(shadowmapping && !(flags&(MDL_SHADOW|MDL_DYNSHADOW))) return;
+    model *m = loadmodel(mdl); 
+    if(!m) return;
+    vec center(0, 0, 0), bbradius(0, 0, 0);
+    float radius = 0;
+    bool shadow = !shadowmap && !glaring && (flags&(MDL_SHADOW|MDL_DYNSHADOW)) && showblobs;
+
+    if(flags&(MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED|MDL_CULL_QUERY|MDL_SHADOW|MDL_DYNSHADOW))
+    {
+        if(flags&MDL_CULL_QUERY)
+        {
+            if(!oqfrags || !oqdynent || !d) flags &= ~MDL_CULL_QUERY;
+        }
+
+        m->boundbox(center, bbradius);
+        radius = bbradius.magnitude();
+        if(d && d->ragdoll)
+        {
+            radius = max(radius, d->ragdoll->radius);
+            center = d->ragdoll->center;
+        }
+        else
+        {
+            center.rotate_around_z(yaw*RAD);
+            center.add(o);
+        }
+
+        int culled = cullmodel(m, center, radius, flags, d, shadow);
+        if(culled)
+        {
+            if(culled&(MDL_CULL_OCCLUDED|MDL_CULL_QUERY) && flags&MDL_CULL_QUERY && !reflecting && !refracting)
+            {
+                enablecullmodelquery();
+                rendercullmodelquery(m, d, center, radius);
+                disablecullmodelquery();
+            }
+            return;
+        }
+
+        if(reflecting || refracting || shadowmapping) flags &= ~MDL_CULL_QUERY;
+    }
+
+    if(flags&MDL_NORENDER) anim |= ANIM_NORENDER;
+    else if(showboundingbox && !shadowmapping && !reflecting && !refracting && editmode)
+    {
+        notextureshader->set();
+        if(d && showboundingbox==1) 
+        {
+            render3dbox(d->o, d->eyeheight, d->aboveeye, d->radius);
+            renderellipse(d->o, d->xradius, d->yradius, d->yaw);
+        }
+        else
+        {
+            vec center, radius;
+            if(showboundingbox==1) m->collisionbox(center, radius);
+            else m->boundbox(center, radius);
+            rotatebb(center, radius, int(yaw));
+            center.add(o);
+            render3dbox(center, radius.z, radius.z, radius.x, radius.y);
+        }
+    }
+
+    vec lightcolor(1, 1, 1), lightdir(0, 0, 1);
+    if(!shadowmapping)
+    {
+        vec pos = o;
+        if(d) 
+        {
+            if(!reflecting && !refracting) d->occluded = OCCLUDE_NOTHING;
+            if(!light) light = &d->light;
+            if(flags&MDL_LIGHT && light->millis!=lastmillis)
+            {
+                if(d->ragdoll)
+                {
+                    pos = d->ragdoll->center;
+                    pos.z += radius/2;
+                }
+                else if(d->type < ENT_CAMERA) pos.z += 0.75f*(d->eyeheight + d->aboveeye);
+                lightreaching(pos, light->color, light->dir, (flags&MDL_LIGHT_FAST)!=0);
+                dynlightreaching(pos, light->color, light->dir, (flags&MDL_HUD)!=0);
+                game::lighteffects(d, light->color, light->dir);
+                light->millis = lastmillis;
+            }
+        }
+        else if(flags&MDL_LIGHT)
+        {
+            if(!light) 
+            {
+                lightreaching(pos, lightcolor, lightdir, (flags&MDL_LIGHT_FAST)!=0);
+                dynlightreaching(pos, lightcolor, lightdir, (flags&MDL_HUD)!=0);
+            }
+            else if(light->millis!=lastmillis)
+            {
+                lightreaching(pos, light->color, light->dir, (flags&MDL_LIGHT_FAST)!=0);
+                dynlightreaching(pos, light->color, light->dir, (flags&MDL_HUD)!=0);
+                light->millis = lastmillis;
+            }
+        }
+        if(light) { lightcolor = light->color; lightdir = light->dir; }
+        if(flags&MDL_DYNLIGHT) dynlightreaching(pos, lightcolor, lightdir, (flags&MDL_HUD)!=0);
+    }
+
+    if(a) for(int i = 0; a[i].tag; i++)
+    {
+        if(a[i].name) a[i].m = loadmodel(a[i].name);
+        //if(a[i].m && a[i].m->type()!=m->type()) a[i].m = NULL;
+    }
+
+    if(numbatches>=0)
+    {
+        modelbatch &mb = addbatchedmodel(m);
+        batchedmodel &b = mb.batched.add();
+        b.query = modelquery;
+        b.pos = o;
+        b.color = lightcolor;
+        b.dir = lightdir;
+        b.anim = anim;
+        b.yaw = yaw;
+        b.pitch = pitch;
+        b.basetime = basetime;
+        b.basetime2 = basetime2;
+        b.transparent = trans;
+        b.flags = flags & ~(MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED);
+        if(!shadow || reflecting || refracting>0) 
+        {
+            b.flags &= ~(MDL_SHADOW|MDL_DYNSHADOW);
+            if((flags&MDL_CULL_VFC) && refracting<0 && center.z-radius>=reflectz) b.flags |= MDL_CULL_VFC;
+        }
+        mb.flags |= b.flags;
+        b.d = d;
+        b.attached = a ? modelattached.length() : -1;
+        if(a) for(int i = 0;; i++) { modelattached.add(a[i]); if(!a[i].tag) break; }
+        if(flags&MDL_CULL_QUERY) d->query = b.query = newquery(d);
+        return;
+    }
+
+    if(shadow && !reflecting && refracting<=0)
+    {
+        renderblob(flags&MDL_DYNSHADOW ? BLOB_DYNAMIC : BLOB_STATIC, d && d->ragdoll ? center : o, d ? d->radius : max(bbradius.x, bbradius.y), trans);
+        flushblobs();
+        if((flags&MDL_CULL_VFC) && refracting<0 && center.z-radius>=reflectz) return;
+    }
+
+    m->startrender();
+
+    if(shadowmapping)
+    {
+        anim |= ANIM_NOSKIN;
+        GLOBALPARAMF(shadowintensity, trans);
+    }
+    else 
+    {
+        if(flags&MDL_FULLBRIGHT) anim |= ANIM_FULLBRIGHT;
+        if(flags&MDL_GHOST) anim |= ANIM_GHOST;
+    }
+
+    if(flags&MDL_CULL_QUERY)
+    {
+        d->query = newquery(d);
+        if(d->query) startquery(d->query);
+    }
+
+    m->render(anim, basetime, basetime2, o, yaw, pitch, d, a, lightcolor, lightdir, trans);
+
+    if(flags&MDL_CULL_QUERY && d->query) endquery(d->query);
+
+    m->endrender();
+}
+
+void abovemodel(vec &o, const char *mdl)
+{
+    model *m = loadmodel(mdl);
+    if(!m) return;
+    o.z += m->above();
+}
+
+bool matchanim(const char *name, const char *pattern)
+{
+    for(;; pattern++)
+    {
+        const char *s = name;
+        char c;
+        for(;; pattern++)
+        {
+            c = *pattern;
+            if(!c || c=='|') break;
+            else if(c=='*') 
+            {
+                if(!*s || iscubespace(*s)) break;
+                do s++; while(*s && !iscubespace(*s));
+            }
+            else if(c!=*s) break;
+            else s++;
+        }
+        if(!*s && (!c || c=='|')) return true;
+        pattern = strchr(pattern, '|');
+        if(!pattern) break;
+    }
+    return false;
+}
+
+void findanims(const char *pattern, vector<int> &anims)
+{
+    loopi(sizeof(animnames)/sizeof(animnames[0])) if(matchanim(animnames[i], pattern)) anims.add(i);
+}
+
+ICOMMAND(findanims, "s", (char *name),
+{
+    vector<int> anims;
+    findanims(name, anims);
+    vector<char> buf;
+    string num;
+    loopv(anims)
+    {
+        formatstring(num, "%d", anims[i]);
+        if(i > 0) buf.add(' ');
+        buf.put(num, strlen(num));
+    }
+    buf.add('\0');
+    result(buf.getbuf());
+});
+
+void loadskin(const char *dir, const char *altdir, Texture *&skin, Texture *&masks) // model skin sharing
+{
+#define ifnoload(tex, path) if((tex = textureload(path, 0, true, false))==notexture)
+#define tryload(tex, prefix, cmd, name) \
+    ifnoload(tex, makerelpath(mdir, name ".jpg", prefix, cmd)) \
+    { \
+        ifnoload(tex, makerelpath(mdir, name ".png", prefix, cmd)) \
+        { \
+            ifnoload(tex, makerelpath(maltdir, name ".jpg", prefix, cmd)) \
+            { \
+                ifnoload(tex, makerelpath(maltdir, name ".png", prefix, cmd)) return; \
+            } \
+        } \
+    }
+   
+    defformatstring(mdir, "packages/models/%s", dir);
+    defformatstring(maltdir, "packages/models/%s", altdir);
+    masks = notexture;
+    tryload(skin, NULL, NULL, "skin");
+    tryload(masks, NULL, NULL, "masks");
+}
+
+// convenient function that covers the usual anims for players/monsters/npcs
+
+VAR(animoverride, -1, 0, NUMANIMS-1);
+VAR(testanims, 0, 0, 1);
+VAR(testpitch, -90, 0, 90);
+
+void renderclient(dynent *d, const char *mdlname, modelattach *attachments, int hold, int attack, int attackdelay, int lastaction, int lastpain, float fade, bool ragdoll)
+{
+    int anim = hold ? hold : ANIM_IDLE|ANIM_LOOP;
+    float yaw = testanims && d==player ? 0 : d->yaw+90,
+          pitch = testpitch && d==player ? testpitch : d->pitch;
+    vec o = d->feetpos();
+    int basetime = 0;
+    if(animoverride) anim = (animoverride<0 ? ANIM_ALL : animoverride)|ANIM_LOOP;
+    else if(d->state==CS_DEAD)
+    {
+        anim = ANIM_DYING|ANIM_NOPITCH;
+        basetime = lastpain;
+        if(ragdoll)
+        {
+            if(!d->ragdoll || d->ragdoll->millis < basetime) 
+            {
+                DELETEP(d->ragdoll);
+                anim |= ANIM_RAGDOLL;
+            }
+        }
+        else if(lastmillis-basetime>1000) anim = ANIM_DEAD|ANIM_LOOP|ANIM_NOPITCH;
+    }
+    else if(d->state==CS_EDITING || d->state==CS_SPECTATOR) anim = ANIM_EDIT|ANIM_LOOP;
+    else if(d->state==CS_LAGGED)                            anim = ANIM_LAG|ANIM_LOOP;
+    else
+    {
+        if(lastmillis-lastpain < 300) 
+        { 
+            anim = ANIM_PAIN;
+            basetime = lastpain;
+        }
+        else if(lastpain < lastaction && (attack < 0 || (d->type != ENT_AI && lastmillis-lastaction < attackdelay)))
+        { 
+            anim = attack < 0 ? -attack : attack; 
+            basetime = lastaction; 
+        }
+
+        if(d->inwater && d->physstate<=PHYS_FALL) anim |= (((game::allowmove(d) && (d->move || d->strafe)) || d->vel.z+d->falling.z>0 ? ANIM_SWIM : ANIM_SINK)|ANIM_LOOP)<<ANIM_SECONDARY;
+        else if(d->timeinair>100) anim |= (ANIM_JUMP|ANIM_END)<<ANIM_SECONDARY;
+        else if(game::allowmove(d) && (d->move || d->strafe)) 
+        {
+            if(d->move>0) anim |= (ANIM_FORWARD|ANIM_LOOP)<<ANIM_SECONDARY;
+            else if(d->strafe)
+            {
+                if(d->move<0) anim |= ((d->strafe>0 ? ANIM_RIGHT : ANIM_LEFT)|ANIM_REVERSE|ANIM_LOOP)<<ANIM_SECONDARY;
+                else anim |= ((d->strafe>0 ? ANIM_LEFT : ANIM_RIGHT)|ANIM_LOOP)<<ANIM_SECONDARY;
+            }
+            else if(d->move<0) anim |= (ANIM_BACKWARD|ANIM_LOOP)<<ANIM_SECONDARY;
+        }
+        
+        if((anim&ANIM_INDEX)==ANIM_IDLE && (anim>>ANIM_SECONDARY)&ANIM_INDEX) anim >>= ANIM_SECONDARY;
+    }
+    if(d->ragdoll && (!ragdoll || (anim&ANIM_INDEX)!=ANIM_DYING)) DELETEP(d->ragdoll);
+    if(!((anim>>ANIM_SECONDARY)&ANIM_INDEX)) anim |= (ANIM_IDLE|ANIM_LOOP)<<ANIM_SECONDARY;
+    int flags = MDL_LIGHT;
+    if(d!=player && !(anim&ANIM_RAGDOLL)) flags |= MDL_CULL_VFC | MDL_CULL_OCCLUDED | MDL_CULL_QUERY;
+    if(d->type==ENT_PLAYER) flags |= MDL_FULLBRIGHT;
+    else flags |= MDL_CULL_DIST;
+    if(d->state==CS_LAGGED) fade = min(fade, 0.3f);
+    else flags |= MDL_DYNSHADOW;
+    if(drawtex == DRAWTEX_MODELPREVIEW) flags &= ~(MDL_LIGHT | MDL_FULLBRIGHT | MDL_CULL_VFC | MDL_CULL_OCCLUDED | MDL_CULL_QUERY | MDL_CULL_DIST | MDL_DYNSHADOW);
+    rendermodel(NULL, mdlname, anim, o, yaw, pitch, flags, d, attachments, basetime, 0, fade);
+}
+
+void setbbfrommodel(dynent *d, const char *mdl)
+{
+    model *m = loadmodel(mdl); 
+    if(!m) return;
+    vec center, radius;
+    m->collisionbox(center, radius);
+    if(d->type==ENT_INANIMATE && !m->ellipsecollide)
+        d->collidetype = COLLIDE_OBB;
+    d->xradius   = radius.x + fabs(center.x);
+    d->yradius   = radius.y + fabs(center.y);
+    d->radius    = d->collidetype==COLLIDE_OBB ? sqrtf(d->xradius*d->xradius + d->yradius*d->yradius) : max(d->xradius, d->yradius);
+    d->eyeheight = (center.z-radius.z) + radius.z*2*m->eyeheight;
+    d->aboveeye  = radius.z*2*(1.0f-m->eyeheight);
+    if (d->aboveeye + d->eyeheight <= 0.5f)
+    {
+        float zrad = (0.5f - (d->aboveeye + d->eyeheight)) / 2;
+        d->aboveeye += zrad;
+        d->eyeheight += zrad;
+    }
+}
+
diff --git a/src/engine/renderparticles.cpp b/src/engine/renderparticles.cpp
new file mode 100644 (file)
index 0000000..17350cf
--- /dev/null
@@ -0,0 +1,1551 @@
+// renderparticles.cpp
+
+#include "engine.h"
+#include "rendertarget.h"
+
+Shader *particleshader = NULL, *particlenotextureshader = NULL;
+
+VARP(particlesize, 20, 100, 500);
+    
+// Check canemitparticles() to limit the rate that paricles can be emitted for models/sparklies
+// Automatically stops particles being emitted when paused or in reflective drawing
+VARP(emitmillis, 1, 17, 1000);
+static int lastemitframe = 0, emitoffset = 0;
+static bool canemit = false, regenemitters = false, canstep = false;
+
+static bool canemitparticles()
+{
+    if(reflecting || refracting) return false;
+    return canemit || emitoffset;
+}
+
+VARP(showparticles, 0, 1, 1);
+VAR(cullparticles, 0, 1, 1);
+VAR(replayparticles, 0, 1, 1);
+VARN(seedparticles, seedmillis, 0, 3000, 10000);
+VAR(dbgpcull, 0, 0, 1);
+VAR(dbgpseed, 0, 0, 1);
+
+struct particleemitter
+{
+    extentity *ent;
+    vec bbmin, bbmax;
+    vec center;
+    float radius;
+    ivec cullmin, cullmax;
+    int maxfade, lastemit, lastcull;
+
+    particleemitter(extentity *ent)
+        : ent(ent), bbmin(ent->o), bbmax(ent->o), maxfade(-1), lastemit(0), lastcull(0)
+    {}
+
+    void finalize()
+    {
+        center = vec(bbmin).add(bbmax).mul(0.5f);
+        radius = bbmin.dist(bbmax)/2;
+        cullmin = ivec(int(floor(bbmin.x)), int(floor(bbmin.y)), int(floor(bbmin.z)));
+        cullmax = ivec(int(ceil(bbmax.x)), int(ceil(bbmax.y)), int(ceil(bbmax.z)));
+        if(dbgpseed) conoutf(CON_DEBUG, "radius: %f, maxfade: %d", radius, maxfade);
+    }
+    
+    void extendbb(const vec &o, float size = 0)
+    {
+        bbmin.x = min(bbmin.x, o.x - size);
+        bbmin.y = min(bbmin.y, o.y - size);
+        bbmin.z = min(bbmin.z, o.z - size);
+        bbmax.x = max(bbmax.x, o.x + size);
+        bbmax.y = max(bbmax.y, o.y + size);
+        bbmax.z = max(bbmax.z, o.z + size);
+    }
+
+    void extendbb(float z, float size = 0)
+    {
+        bbmin.z = min(bbmin.z, z - size);
+        bbmax.z = max(bbmax.z, z + size);
+    }
+};
+
+static vector<particleemitter> emitters;
+static particleemitter *seedemitter = NULL;
+
+void clearparticleemitters()
+{
+    emitters.setsize(0);
+    regenemitters = true;
+}
+
+void addparticleemitters()
+{
+    emitters.setsize(0);
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        extentity &e = *ents[i];
+        if(e.type != ET_PARTICLES) continue;
+        emitters.add(particleemitter(&e));
+    }
+    regenemitters = false;
+}
+
+enum
+{
+    PT_PART = 0,
+    PT_TAPE,
+    PT_TRAIL,
+    PT_TEXT,
+    PT_TEXTICON,
+    PT_METER,
+    PT_METERVS,
+    PT_FIREBALL,
+    PT_LIGHTNING,
+    PT_FLARE,
+
+    PT_MOD    = 1<<8,
+    PT_RND4   = 1<<9,
+    PT_LERP   = 1<<10, // use very sparingly - order of blending issues
+    PT_TRACK  = 1<<11,
+    PT_GLARE  = 1<<12,
+    PT_SOFT   = 1<<13,
+    PT_HFLIP  = 1<<14,
+    PT_VFLIP  = 1<<15,
+    PT_ROT    = 1<<16,
+    PT_CULL   = 1<<17,
+    PT_FEW    = 1<<18,
+    PT_ICON   = 1<<19,
+    PT_NOTEX  = 1<<20,
+    PT_SHADER = 1<<21,
+    PT_FLIP  = PT_HFLIP | PT_VFLIP | PT_ROT
+};
+
+const char *partnames[] = { "part", "tape", "trail", "text", "texticon", "meter", "metervs", "fireball", "lightning", "flare" };
+
+struct particle
+{
+    vec o, d;
+    int gravity, fade, millis;
+    bvec color;
+    uchar flags;
+    float size;
+    union
+    {
+        const char *text;
+        float val;
+        physent *owner;
+        struct
+        {
+            uchar color2[3];
+            uchar progress;
+        };
+    }; 
+};
+
+struct partvert
+{
+    vec pos;
+    bvec4 color;
+    vec2 tc;
+};
+
+#define COLLIDERADIUS 8.0f
+#define COLLIDEERROR 1.0f
+
+struct partrenderer
+{
+    Texture *tex;
+    const char *texname;
+    int texclamp;
+    uint type;
+    int collide;
+    string info;
+   
+    partrenderer(const char *texname, int texclamp, int type, int collide = 0)
+        : tex(NULL), texname(texname), texclamp(texclamp), type(type), collide(collide)
+    {
+    }
+    partrenderer(int type, int collide = 0)
+        : tex(NULL), texname(NULL), texclamp(0), type(type), collide(collide)
+    {
+    }
+    virtual ~partrenderer()
+    {
+    }
+
+    virtual void init(int n) { }
+    virtual void reset() = 0;
+    virtual void resettracked(physent *owner) { }   
+    virtual particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) = 0;    
+    virtual int adddepthfx(vec &bbmin, vec &bbmax) { return 0; }
+    virtual void update() { }
+    virtual void render() = 0;
+    virtual bool haswork() = 0;
+    virtual int count() = 0; //for debug
+    virtual void cleanup() {}
+
+    virtual void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+    {
+    }
+
+    //blend = 0 => remove it
+    void calc(particle *p, int &blend, int &ts, vec &o, vec &d, bool step = true)
+    {
+        o = p->o;
+        d = p->d;
+        if(type&PT_TRACK && p->owner) game::particletrack(p->owner, o, d);
+        if(p->fade <= 5) 
+        {
+            ts = 1;
+            blend = 255;
+        }
+        else
+        {
+            ts = lastmillis-p->millis;
+            blend = max(255 - (ts<<8)/p->fade, 0);
+            if(p->gravity)
+            {
+                if(ts > p->fade) ts = p->fade;
+                float t = ts;
+                o.add(vec(d).mul(t/5000.0f));
+                o.z -= t*t/(2.0f * 5000.0f * p->gravity);
+            }
+            if(collide && o.z < p->val && step)
+            {
+                if(collide >= 0)
+                {
+                    vec surface;
+                    float floorz = rayfloor(vec(o.x, o.y, p->val), surface, RAY_CLIPMAT, COLLIDERADIUS);
+                    float collidez = floorz<0 ? o.z-COLLIDERADIUS : p->val - floorz;
+                    if(o.z >= collidez+COLLIDEERROR) 
+                        p->val = collidez+COLLIDEERROR;
+                    else 
+                    {
+                        adddecal(collide, vec(o.x, o.y, collidez), vec(p->o).sub(o).normalize(), 2*p->size, p->color, type&PT_RND4 ? (p->flags>>5)&3 : 0);
+                        blend = 0;
+                    }
+                }
+                else blend = 0;
+            }
+        }
+    }
+
+    const char *debuginfo()
+    {
+        formatstring(info, "%d\t%s(", count(), partnames[type&0xFF]);
+        if(type&PT_GLARE) concatstring(info, "g,");
+        if(type&PT_SOFT) concatstring(info, "s,");
+        if(type&PT_LERP) concatstring(info, "l,");
+        if(type&PT_MOD) concatstring(info, "m,");
+        if(type&PT_RND4) concatstring(info, "r,");
+        if(type&PT_FLIP) concatstring(info, "f,");
+        if(collide) concatstring(info, "c,");
+        int len = strlen(info);
+        info[len-1] = info[len-1] == ',' ? ')' : '\0';
+        if(texname)
+        {   
+            const char *title = strrchr(texname, '/');
+            if(title) concformatstring(info, ": %s", title+1);
+        }
+        return info;
+    }
+};
+
+struct listparticle : particle
+{   
+    listparticle *next;
+};
+
+VARP(outlinemeters, 0, 0, 1);
+
+struct listrenderer : partrenderer
+{
+    static listparticle *parempty;
+    listparticle *list;
+
+    listrenderer(const char *texname, int texclamp, int type, int collide = 0) 
+        : partrenderer(texname, texclamp, type, collide), list(NULL)
+    {
+    }
+    listrenderer(int type, int collide = 0)
+        : partrenderer(type, collide), list(NULL)
+    {
+    }
+
+    virtual ~listrenderer()
+    {
+    }
+
+    virtual void killpart(listparticle *p)
+    {
+    }
+
+    void reset()  
+    {
+        if(!list) return;
+        listparticle *p = list;
+        for(;;)
+        {
+            killpart(p);
+            if(p->next) p = p->next;
+            else break;
+        }
+        p->next = parempty;
+        parempty = list;
+        list = NULL;
+    }
+    
+    void resettracked(physent *owner) 
+    {
+        if(!(type&PT_TRACK)) return;
+        for(listparticle **prev = &list, *cur = list; cur; cur = *prev)
+        {
+            if(!owner || cur->owner==owner) 
+            {
+                *prev = cur->next;
+                cur->next = parempty;
+                parempty = cur;
+            }
+            else prev = &cur->next;
+        }
+    }
+    
+    particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity) 
+    {
+        if(!parempty)
+        {
+            listparticle *ps = new listparticle[256];
+            loopi(255) ps[i].next = &ps[i+1];
+            ps[255].next = parempty;
+            parempty = ps;
+        }
+        listparticle *p = parempty;
+        parempty = p->next;
+        p->next = list;
+        list = p;
+        p->o = o;
+        p->d = d;
+        p->gravity = gravity;
+        p->fade = fade;
+        p->millis = lastmillis + emitoffset;
+        p->color = bvec(color>>16, (color>>8)&0xFF, color&0xFF);
+        p->size = size;
+        p->owner = NULL;
+        p->flags = 0;
+        return p;
+    }
+    
+    int count() 
+    {
+        int num = 0;
+        listparticle *lp;
+        for(lp = list; lp; lp = lp->next) num++;
+        return num;
+    }
+    
+    bool haswork() 
+    {
+        return (list != NULL);
+    }
+    
+    virtual void startrender() = 0;
+    virtual void endrender() = 0;
+    virtual void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) = 0;
+
+    void render() 
+    {
+        startrender();
+        if(texname)
+        {
+            if(!tex) tex = textureload(texname, texclamp);
+            glBindTexture(GL_TEXTURE_2D, tex->id);
+        }
+        
+        for(listparticle **prev = &list, *p = list; p; p = *prev)
+        {   
+            vec o, d;
+            int blend, ts;
+            calc(p, blend, ts, o, d, canstep);
+            if(blend > 0) 
+            {
+                renderpart(p, o, d, blend, ts);
+
+                if(p->fade > 5 || !canstep) 
+                {
+                    prev = &p->next;
+                    continue;
+                }
+            }
+            //remove
+            *prev = p->next;
+            p->next = parempty;
+            killpart(p);
+            parempty = p;
+        }
+       
+        endrender();
+    }
+};
+
+listparticle *listrenderer::parempty = NULL;
+
+struct meterrenderer : listrenderer
+{
+    meterrenderer(int type)
+        : listrenderer(type|PT_NOTEX|PT_LERP)
+    {}
+
+    void startrender()
+    {
+        glDisable(GL_BLEND);
+        gle::defvertex();
+    }
+
+    void endrender()
+    {
+        glEnable(GL_BLEND);
+    }
+
+    void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+    {
+        int basetype = type&0xFF;
+        float scale = FONTH*p->size/80.0f, right = 8, left = p->progress/100.0f*right;
+        matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o);
+        m.scale(scale);
+        m.translate(-right/2.0f, 0, 0);
+
+        if(outlinemeters)
+        {
+            gle::colorf(0, 0.8f, 0);
+            gle::begin(GL_TRIANGLE_STRIP);
+            loopk(10)
+            {
+                const vec2 &sc = sincos360[k*(180/(10-1))];
+                float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x;
+                gle::attrib(m.transform(vec2(-c, s)));
+                gle::attrib(m.transform(vec2(right + c, s)));
+            }
+            gle::end();
+        }
+
+        if(basetype==PT_METERVS) gle::colorub(p->color2[0], p->color2[1], p->color2[2]);
+        else gle::colorf(0, 0, 0);
+        gle::begin(GL_TRIANGLE_STRIP);
+        loopk(10)
+        {
+            const vec2 &sc = sincos360[k*(180/(10-1))];
+            float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x;
+            gle::attrib(m.transform(vec2(left + c, s)));
+            gle::attrib(m.transform(vec2(right + c, s)));
+        }
+        gle::end();
+
+        if(outlinemeters)
+        {
+            gle::colorf(0, 0.8f, 0);
+            gle::begin(GL_TRIANGLE_FAN);
+            loopk(10)
+            {
+                const vec2 &sc = sincos360[k*(180/(10-1))];
+                float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x;
+                gle::attrib(m.transform(vec2(left + c, s)));
+            }
+            gle::end();
+        }
+
+        gle::color(p->color);
+        gle::begin(GL_TRIANGLE_STRIP);
+        loopk(10)
+        {
+            const vec2 &sc = sincos360[k*(180/(10-1))];
+            float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x;
+            gle::attrib(m.transform(vec2(-c, s)));
+            gle::attrib(m.transform(vec2(left + c, s)));
+        }
+        gle::end();
+    }
+};
+static meterrenderer meters(PT_METER), metervs(PT_METERVS);
+
+struct textrenderer : listrenderer
+{
+    textrenderer(int type)
+        : listrenderer(type)
+    {}
+
+    void startrender()
+    {
+    }
+
+    void endrender()
+    {
+    }
+
+    void killpart(listparticle *p)
+    {
+        if(p->text && p->flags&1) delete[] p->text;
+    }
+
+    void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+    {
+        float scale = p->size/80.0f, xoff = -(text_width(p->text) + ((p->flags>>1)&7)*FONTH)/2, yoff = 0;
+
+        matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o);
+        m.scale(scale);
+        m.translate(xoff, yoff, 50);
+
+        textmatrix = &m;
+        draw_text(p->text, 0, 0, p->color.r, p->color.g, p->color.b, blend);
+        textmatrix = NULL;
+    } 
+};
+static textrenderer texts(PT_TEXT|PT_LERP);
+
+struct texticonrenderer : listrenderer
+{
+    texticonrenderer(const char *texname, int type)
+        : listrenderer(texname, 3, type)
+    {}
+
+    void startrender()
+    {
+        gle::defvertex();
+        gle::deftexcoord0();
+        gle::defcolor(4, GL_UNSIGNED_BYTE);
+        gle::begin(GL_QUADS);
+    }
+
+    void endrender()
+    {
+        gle::end();
+    }
+
+    void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts)
+    {
+        float scale = p->size/80.0f, xoff = p->val, yoff = 0;
+
+        matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o);
+        m.scale(scale);
+        m.translate(xoff, yoff, 50);
+
+        float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3);
+
+        gle::attrib(m.transform(vec2(0, 0)));
+            gle::attrib(tx, ty);
+            gle::attrib(p->color, blend);
+        gle::attrib(m.transform(vec2(FONTH, 0)));
+            gle::attrib(tx + 0.25f, ty);
+            gle::attrib(p->color, blend);
+        gle::attrib(m.transform(vec2(FONTH, FONTH)));
+            gle::attrib(tx + 0.25f, ty + 0.25f);
+            gle::attrib(p->color, blend);
+        gle::attrib(m.transform(vec2(0, FONTH)));
+            gle::attrib(tx, ty + 0.25f);
+            gle::attrib(p->color, blend);
+    }
+};
+static texticonrenderer texticons("packages/hud/items.png", PT_TEXTICON|PT_LERP);
+
+template<int T>
+static inline void modifyblend(const vec &o, int &blend)
+{
+    blend = min(blend<<2, 255);
+}
+
+template<>
+inline void modifyblend<PT_TAPE>(const vec &o, int &blend)
+{
+}
+
+template<int T>
+static inline void genpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs)
+{
+    vec udir = vec(camup).sub(camright).mul(size);
+    vec vdir = vec(camup).add(camright).mul(size);
+    vs[0].pos = vec(o.x + udir.x, o.y + udir.y, o.z + udir.z);
+    vs[1].pos = vec(o.x + vdir.x, o.y + vdir.y, o.z + vdir.z);
+    vs[2].pos = vec(o.x - udir.x, o.y - udir.y, o.z - udir.z);
+    vs[3].pos = vec(o.x - vdir.x, o.y - vdir.y, o.z - vdir.z);
+}
+
+template<>
+inline void genpos<PT_TAPE>(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs)
+{
+    vec dir1 = d, dir2 = d, c;
+    dir1.sub(o);
+    dir2.sub(camera1->o);
+    c.cross(dir2, dir1).normalize().mul(size);
+    vs[0].pos = vec(d.x-c.x, d.y-c.y, d.z-c.z);
+    vs[1].pos = vec(o.x-c.x, o.y-c.y, o.z-c.z);
+    vs[2].pos = vec(o.x+c.x, o.y+c.y, o.z+c.z);
+    vs[3].pos = vec(d.x+c.x, d.y+c.y, d.z+c.z);
+}
+
+template<>
+inline void genpos<PT_TRAIL>(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs)
+{
+    vec e = d;
+    if(grav) e.z -= float(ts)/grav;
+    e.div(-75.0f).add(o);
+    genpos<PT_TAPE>(o, e, size, ts, grav, vs);
+}
+
+template<int T>
+static inline void genrotpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot)
+{
+    genpos<T>(o, d, size, grav, ts, vs);
+}
+
+#define ROTCOEFFS(n) { \
+    vec(-1,  1, 0).rotate_around_z(n*2*M_PI/32.0f), \
+    vec( 1,  1, 0).rotate_around_z(n*2*M_PI/32.0f), \
+    vec( 1, -1, 0).rotate_around_z(n*2*M_PI/32.0f), \
+    vec(-1, -1, 0).rotate_around_z(n*2*M_PI/32.0f) \
+}
+static const vec rotcoeffs[32][4] =
+{
+    ROTCOEFFS(0),  ROTCOEFFS(1),  ROTCOEFFS(2),  ROTCOEFFS(3),  ROTCOEFFS(4),  ROTCOEFFS(5),  ROTCOEFFS(6),  ROTCOEFFS(7),
+    ROTCOEFFS(8),  ROTCOEFFS(9),  ROTCOEFFS(10), ROTCOEFFS(11), ROTCOEFFS(12), ROTCOEFFS(13), ROTCOEFFS(14), ROTCOEFFS(15),
+    ROTCOEFFS(16), ROTCOEFFS(17), ROTCOEFFS(18), ROTCOEFFS(19), ROTCOEFFS(20), ROTCOEFFS(21), ROTCOEFFS(22), ROTCOEFFS(7),
+    ROTCOEFFS(24), ROTCOEFFS(25), ROTCOEFFS(26), ROTCOEFFS(27), ROTCOEFFS(28), ROTCOEFFS(29), ROTCOEFFS(30), ROTCOEFFS(31),
+};
+
+template<>
+inline void genrotpos<PT_PART>(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot)
+{
+    const vec *coeffs = rotcoeffs[rot];
+    (vs[0].pos = o).add(vec(camright).mul(coeffs[0].x*size)).add(vec(camup).mul(coeffs[0].y*size));
+    (vs[1].pos = o).add(vec(camright).mul(coeffs[1].x*size)).add(vec(camup).mul(coeffs[1].y*size));
+    (vs[2].pos = o).add(vec(camright).mul(coeffs[2].x*size)).add(vec(camup).mul(coeffs[2].y*size));
+    (vs[3].pos = o).add(vec(camright).mul(coeffs[3].x*size)).add(vec(camup).mul(coeffs[3].y*size));
+}
+
+template<int T>
+static inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav)
+{
+    if(grav)
+    {
+        vec end(o);
+        float t = fade;
+        end.add(vec(d).mul(t/5000.0f));
+        end.z -= t*t/(2.0f * 5000.0f * grav);
+        pe.extendbb(end, size);
+
+        float tpeak = d.z*grav;
+        if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size);
+    }
+}
+
+template<>
+inline void seedpos<PT_TAPE>(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav)
+{
+    pe.extendbb(d, size);
+}
+
+template<>
+inline void seedpos<PT_TRAIL>(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav)
+{
+    vec e = d;
+    if(grav) e.z -= float(fade)/grav;
+    e.div(-75.0f).add(o);
+    pe.extendbb(e, size); 
+}
+
+template<int T>
+struct varenderer : partrenderer
+{
+    partvert *verts;
+    particle *parts;
+    int maxparts, numparts, lastupdate, rndmask;
+    GLuint vbo;
+
+    varenderer(const char *texname, int type, int collide = 0) 
+        : partrenderer(texname, 3, type, collide),
+          verts(NULL), parts(NULL), maxparts(0), numparts(0), lastupdate(-1), rndmask(0), vbo(0)
+    {
+        if(type & PT_HFLIP) rndmask |= 0x01;
+        if(type & PT_VFLIP) rndmask |= 0x02;
+        if(type & PT_ROT) rndmask |= 0x1F<<2;
+        if(type & PT_RND4) rndmask |= 0x03<<5;
+    }
+
+    void cleanup()
+    {
+        if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+    }
+    
+    void init(int n)
+    {
+        DELETEA(parts);
+        DELETEA(verts);
+        parts = new particle[n];
+        verts = new partvert[n*4];
+        maxparts = n;
+        numparts = 0;
+        lastupdate = -1;
+    }
+        
+    void reset() 
+    {
+        numparts = 0;
+        lastupdate = -1;
+    }
+    
+    void resettracked(physent *owner) 
+    {
+        if(!(type&PT_TRACK)) return;
+        loopi(numparts)
+        {
+            particle *p = parts+i;
+            if(!owner || (p->owner == owner)) p->fade = -1;
+        }
+        lastupdate = -1;
+    }
+    
+    int count() 
+    {
+        return numparts;
+    }
+    
+    bool haswork() 
+    {
+        return (numparts > 0);
+    }
+
+    particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity) 
+    {
+        particle *p = parts + (numparts < maxparts ? numparts++ : rnd(maxparts)); //next free slot, or kill a random kitten
+        p->o = o;
+        p->d = d;
+        p->gravity = gravity;
+        p->fade = fade;
+        p->millis = lastmillis + emitoffset;
+        p->color = bvec(color>>16, (color>>8)&0xFF, color&0xFF);
+        p->size = size;
+        p->owner = NULL;
+        p->flags = 0x80 | (rndmask ? rnd(0x80) & rndmask : 0);
+        lastupdate = -1;
+        return p;
+    }
+    void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity)
+    {
+        pe.maxfade = max(pe.maxfade, fade);
+        size *= SQRT2;
+        pe.extendbb(o, size);
+
+        seedpos<T>(pe, o, d, fade, size, gravity);
+        if(!gravity) return;
+
+        vec end(o);
+        float t = fade;
+        end.add(vec(d).mul(t/5000.0f));
+        end.z -= t*t/(2.0f * 5000.0f * gravity);
+        pe.extendbb(end, size);
+
+        float tpeak = d.z*gravity;
+        if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size);
+    }
+    void genverts(particle *p, partvert *vs, bool regen)
+    {
+        vec o, d;
+        int blend, ts;
+
+        calc(p, blend, ts, o, d);
+        if(blend <= 1 || p->fade <= 5) p->fade = -1; //mark to remove on next pass (i.e. after render)
+
+        modifyblend<T>(o, blend);
+
+        if(regen)
+        {
+            p->flags &= ~0x80;
+
+            #define SETTEXCOORDS(u1c, u2c, v1c, v2c, body) \
+            { \
+                float u1 = u1c, u2 = u2c, v1 = v1c, v2 = v2c; \
+                body; \
+                vs[0].tc = vec2(u1, v1); \
+                vs[1].tc = vec2(u2, v1); \
+                vs[2].tc = vec2(u2, v2); \
+                vs[3].tc = vec2(u1, v2); \
+            }
+            if(type&PT_RND4)
+            {
+                float tx = 0.5f*((p->flags>>5)&1), ty = 0.5f*((p->flags>>6)&1);
+                SETTEXCOORDS(tx, tx + 0.5f, ty, ty + 0.5f,
+                {
+                    if(p->flags&0x01) swap(u1, u2);
+                    if(p->flags&0x02) swap(v1, v2);
+                });
+            } 
+            else if(type&PT_ICON)
+            {
+                float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3);
+                SETTEXCOORDS(tx, tx + 0.25f, ty, ty + 0.25f, {});
+            }
+            else SETTEXCOORDS(0, 1, 0, 1, {});
+
+            #define SETCOLOR(r, g, b, a) \
+            do { \
+                bvec4 col(r, g, b, a); \
+                loopi(4) vs[i].color = col; \
+            } while(0)
+            #define SETMODCOLOR SETCOLOR((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8, 255)
+            if(type&PT_MOD) SETMODCOLOR;
+            else SETCOLOR(p->color.r, p->color.g, p->color.b, blend);
+        }
+        else if(type&PT_MOD) SETMODCOLOR;
+        else loopi(4) vs[i].color.a = blend;
+
+        if(type&PT_ROT) genrotpos<T>(o, d, p->size, ts, p->gravity, vs, (p->flags>>2)&0x1F);
+        else genpos<T>(o, d, p->size, ts, p->gravity, vs);
+    }
+
+    void genverts()
+    {
+        loopi(numparts)
+        {
+            particle *p = &parts[i];
+            partvert *vs = &verts[i*4];
+            if(p->fade < 0)
+            {
+                do 
+                {
+                    --numparts; 
+                    if(numparts <= i) return;
+                }
+                while(parts[numparts].fade < 0);
+                *p = parts[numparts];
+                genverts(p, vs, true);
+            }
+            else genverts(p, vs, (p->flags&0x80)!=0);
+        }
+    }
+   
+    void update()
+    {
+        if(lastmillis == lastupdate && vbo) return;
+        lastupdate = lastmillis;
+
+        genverts();
+
+        if(!vbo) glGenBuffers_(1, &vbo);
+        gle::bindvbo(vbo);
+        glBufferData_(GL_ARRAY_BUFFER, maxparts*4*sizeof(partvert), NULL, GL_STREAM_DRAW);
+        glBufferSubData_(GL_ARRAY_BUFFER, 0, numparts*4*sizeof(partvert), verts);
+        gle::clearvbo();
+    }
+    void render()
+    {   
+        if(!tex) tex = textureload(texname, texclamp);
+        glBindTexture(GL_TEXTURE_2D, tex->id);
+
+        gle::bindvbo(vbo);
+        const partvert *ptr = 0;
+        gle::vertexpointer(sizeof(partvert), ptr->pos.v);
+        gle::texcoord0pointer(sizeof(partvert), ptr->tc.v);
+        gle::colorpointer(sizeof(partvert), ptr->color.v);
+        gle::enablevertex();
+        gle::enabletexcoord0();
+        gle::enablecolor();
+        gle::enablequads();
+
+        gle::drawquads(0, numparts);
+
+        gle::disablequads();
+        gle::disablevertex();
+        gle::disabletexcoord0();
+        gle::disablecolor();
+        gle::clearvbo();
+    }
+};
+typedef varenderer<PT_PART> quadrenderer;
+typedef varenderer<PT_TAPE> taperenderer;
+typedef varenderer<PT_TRAIL> trailrenderer;
+
+#include "depthfx.h"
+#include "explosion.h"
+#include "lensflare.h"
+#include "lightning.h"
+
+struct softquadrenderer : quadrenderer
+{
+    softquadrenderer(const char *texname, int type, int collide = 0)
+        : quadrenderer(texname, type|PT_SOFT, collide)
+    {
+    }
+
+    int adddepthfx(vec &bbmin, vec &bbmax)
+    {
+        if(!depthfxtex.highprecision() && !depthfxtex.emulatehighprecision()) return 0;
+        int numsoft = 0;
+        loopi(numparts)
+        {
+            particle &p = parts[i];
+            float radius = p.size*SQRT2;
+            vec o, d;
+            int blend, ts;
+            calc(&p, blend, ts, o, d, false);
+            if(!isfoggedsphere(radius, p.o) && (depthfxscissor!=2 || depthfxtex.addscissorbox(p.o, radius))) 
+            {
+                numsoft++;
+                loopk(3)
+                {
+                    bbmin[k] = min(bbmin[k], o[k] - radius);
+                    bbmax[k] = max(bbmax[k], o[k] + radius);
+                }
+            }
+        }
+        return numsoft;
+    }
+};
+
+static partrenderer *parts[] = 
+{
+    new quadrenderer("<grey>packages/particles/blood.png", PT_PART|PT_FLIP|PT_MOD|PT_RND4, DECAL_BLOOD), // blood spats (note: rgb is inverted) 
+    new trailrenderer("packages/particles/base.png", PT_TRAIL|PT_LERP),                            // water, entity
+    new quadrenderer("<grey>packages/particles/smoke.png", PT_PART|PT_FLIP|PT_LERP),               // smoke
+    new quadrenderer("<grey>packages/particles/steam.png", PT_PART|PT_FLIP),                       // steam
+    new quadrenderer("<grey>packages/particles/flames.png", PT_PART|PT_HFLIP|PT_RND4|PT_GLARE),    // flame on - no flipping please, they have orientation
+    new quadrenderer("packages/particles/ball1.png", PT_PART|PT_FEW|PT_GLARE),                     // fireball1
+    new quadrenderer("packages/particles/ball2.png", PT_PART|PT_FEW|PT_GLARE),                     // fireball2
+    new quadrenderer("packages/particles/ball3.png", PT_PART|PT_FEW|PT_GLARE),                     // fireball3
+    new taperenderer("packages/particles/flare.jpg", PT_TAPE|PT_GLARE),                            // streak
+    &lightnings,                                                                                   // lightning
+    &fireballs,                                                                                    // explosion fireball
+    &bluefireballs,                                                                                // bluish explosion fireball
+    new quadrenderer("packages/particles/spark.png", PT_PART|PT_FLIP|PT_GLARE),                    // sparks
+    new quadrenderer("packages/particles/base.png",  PT_PART|PT_FLIP|PT_GLARE),                    // edit mode entities
+    new quadrenderer("<grey>packages/particles/snow.png", PT_PART|PT_FLIP|PT_RND4, -1),            // colliding snow
+    new quadrenderer("packages/particles/muzzleflash1.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash
+    new quadrenderer("packages/particles/muzzleflash2.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash
+    new quadrenderer("packages/particles/muzzleflash3.jpg", PT_PART|PT_FEW|PT_FLIP|PT_GLARE|PT_TRACK), // muzzle flash
+    new quadrenderer("packages/hud/items.png", PT_PART|PT_FEW|PT_ICON),                            // hud icon
+    new quadrenderer("<colorify:1/1/1>packages/hud/items.png", PT_PART|PT_FEW|PT_ICON),            // grey hud icon
+    &texts,                                                                                        // text
+    &texticons,                                                                                    // text icons
+    &meters,                                                                                       // meter
+    &metervs,                                                                                      // meter vs.
+    &flares                                                                                        // lens flares - must be done last
+};
+
+void finddepthfxranges()
+{
+    depthfxmin = vec(1e16f, 1e16f, 1e16f);
+    depthfxmax = vec(0, 0, 0);
+    numdepthfxranges = fireballs.finddepthfxranges(depthfxowners, depthfxranges, 0, MAXDFXRANGES, depthfxmin, depthfxmax);
+    numdepthfxranges = bluefireballs.finddepthfxranges(depthfxowners, depthfxranges, numdepthfxranges, MAXDFXRANGES, depthfxmin, depthfxmax);
+    loopk(3)
+    {
+        depthfxmin[k] -= depthfxmargin;
+        depthfxmax[k] += depthfxmargin;
+    }
+    if(depthfxparts)
+    {
+        loopi(sizeof(parts)/sizeof(parts[0]))
+        {
+            partrenderer *p = parts[i];
+            if(p->type&PT_SOFT && p->adddepthfx(depthfxmin, depthfxmax))
+            {
+                if(!numdepthfxranges)
+                {
+                    numdepthfxranges = 1;
+                    depthfxowners[0] = NULL;
+                    depthfxranges[0] = 0;
+                }
+            }
+        }
+    }              
+    if(depthfxscissor<2 && numdepthfxranges>0) depthfxtex.addscissorbox(depthfxmin, depthfxmax);
+}
+VARFP(maxparticles, 10, 4000, 40000, initparticles());
+VARFP(fewparticles, 10, 100, 40000, initparticles());
+
+void initparticles() 
+{
+    if(!particleshader) particleshader = lookupshaderbyname("particle");
+    if(!particlenotextureshader) particlenotextureshader = lookupshaderbyname("particlenotexture");
+    loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->init(parts[i]->type&PT_FEW ? min(fewparticles, maxparticles) : maxparticles);
+}
+
+void clearparticles()
+{   
+    loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->reset();
+    clearparticleemitters();
+}   
+
+void cleanupparticles()
+{
+    loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->cleanup();
+}
+
+void removetrackedparticles(physent *owner)
+{
+    loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->resettracked(owner);
+}
+
+VARP(particleglare, 0, 2, 100);
+
+VARN(debugparticles, dbgparts, 0, 0, 1);
+
+void debugparticles()
+{
+    if(!dbgparts) return;
+    int n = sizeof(parts)/sizeof(parts[0]);
+    pushhudmatrix();
+    hudmatrix.ortho(0, FONTH*n*2*screenw/float(screenh), FONTH*n*2, 0, -1, 1); //squeeze into top-left corner        
+    flushhudmatrix();
+    hudshader->set();
+    loopi(n) draw_text(parts[i]->info, FONTH, (i+n/2)*FONTH);
+    pophudmatrix();
+}
+
+void renderparticles(bool mainpass)
+{
+    canstep = mainpass;
+    //want to debug BEFORE the lastpass render (that would delete particles)
+    if(dbgparts && mainpass) loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->debuginfo();
+
+    if(glaring && !particleglare) return;
+    
+    loopi(sizeof(parts)/sizeof(parts[0])) 
+    {
+        if(glaring && !(parts[i]->type&PT_GLARE)) continue;
+        parts[i]->update();
+    }
+    
+    bool rendered = false;
+    uint lastflags = PT_LERP|PT_SHADER,
+         flagmask = PT_LERP|PT_MOD|PT_SHADER|PT_NOTEX;
+   
+    if(binddepthfxtex()) flagmask |= PT_SOFT;
+
+    loopi(sizeof(parts)/sizeof(parts[0]))
+    {
+        partrenderer *p = parts[i];
+        if(glaring && !(p->type&PT_GLARE)) continue;
+        if(!p->haswork()) continue;
+    
+        if(!rendered)
+        {
+            rendered = true;
+            glDepthMask(GL_FALSE);
+            glEnable(GL_BLEND);
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);             
+
+            if(glaring) GLOBALPARAMF(colorscale, particleglare, particleglare, particleglare, 1);
+            else GLOBALPARAMF(colorscale, 1, 1, 1, 1);
+        }
+        
+        uint flags = p->type & flagmask, changedbits = (flags ^ lastflags);
+        if(changedbits)
+        {
+            if(changedbits&PT_LERP)
+            {
+                if(flags&PT_LERP) resetfogcolor();
+                else zerofogcolor();
+            }
+            if(changedbits&(PT_LERP|PT_MOD))
+            {
+                if(flags&PT_LERP) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+                else if(flags&PT_MOD) glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR);
+                else glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+            }
+            if(!(flags&PT_SHADER))
+            {
+                if(changedbits&(PT_SOFT|PT_SHADER|PT_NOTEX|PT_LERP))
+                {
+                    if(flags&PT_SOFT)
+                    {
+                        if(!depthfxtex.highprecision()) SETSHADER(particlesoft8);
+                        else SETSHADER(particlesoft);
+
+                        binddepthfxparams(depthfxpartblend);
+                    }
+                    else if(flags&PT_NOTEX) particlenotextureshader->set();
+                    else particleshader->set();
+                }
+            }
+            lastflags = flags;        
+        }
+        p->render();
+    }
+
+    if(rendered)
+    {
+        if(lastflags&(PT_LERP|PT_MOD)) glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+        if(!(lastflags&PT_LERP)) resetfogcolor();
+        glDisable(GL_BLEND);
+        glDepthMask(GL_TRUE);
+    }
+}
+
+static int addedparticles = 0;
+
+static inline particle *newparticle(const vec &o, const vec &d, int fade, int type, int color, float size, int gravity = 0)
+{
+    static particle dummy;
+    if(seedemitter) 
+    {
+        parts[type]->seedemitter(*seedemitter, o, d, fade, size, gravity);
+        return &dummy;
+    }
+    if(fade + emitoffset < 0) return &dummy;
+    addedparticles++;
+    return parts[type]->addpart(o, d, fade, color, size, gravity);
+}
+
+VARP(maxparticledistance, 256, 1024, 4096);
+
+static void splash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity)
+{
+    if(camera1->o.dist(p) > maxparticledistance && !seedemitter) return;
+    float collidez = parts[type]->collide ? p.z - raycube(p, vec(0, 0, -1), COLLIDERADIUS, RAY_CLIPMAT) + (parts[type]->collide >= 0 ? COLLIDEERROR : 0) : -1; 
+    int fmin = 1;
+    int fmax = fade*3;
+    loopi(num)
+    {
+        int x, y, z;
+        do
+        {
+            x = rnd(radius*2)-radius;
+            y = rnd(radius*2)-radius;
+            z = rnd(radius*2)-radius;
+        }
+        while(x*x+y*y+z*z>radius*radius);
+       vec tmp = vec((float)x, (float)y, (float)z);
+        int f = (num < 10) ? (fmin + rnd(fmax)) : (fmax - (i*(fmax-fmin))/(num-1)); //help deallocater by using fade distribution rather than random
+        newparticle(p, tmp, f, type, color, size, gravity)->val = collidez;
+    }
+}
+
+static void regularsplash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity, int delay = 0) 
+{
+    if(!canemitparticles() || (delay > 0 && rnd(delay) != 0)) return;
+    splash(type, color, radius, num, fade, p, size, gravity);
+}
+
+bool canaddparticles()
+{
+    return !renderedgame && !shadowmapping && !minimized;
+}
+
+void regular_particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity, int delay) 
+{
+    if(!canaddparticles()) return;
+    regularsplash(type, color, radius, num, fade, p, size, gravity, delay);
+}
+
+void particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity) 
+{
+    if(!canaddparticles()) return;
+    splash(type, color, radius, num, fade, p, size, gravity);
+}
+
+VARP(maxtrail, 1, 500, 10000);
+
+void particle_trail(int type, int fade, const vec &s, const vec &e, int color, float size, int gravity)
+{
+    if(!canaddparticles()) return;
+    vec v;
+    float d = e.dist(s, v);
+    int steps = clamp(int(d*2), 1, maxtrail);
+    v.div(steps);
+    vec p = s;
+    loopi(steps)
+    {
+        p.add(v);
+        vec tmp = vec(float(rnd(11)-5), float(rnd(11)-5), float(rnd(11)-5));
+        newparticle(p, tmp, rnd(fade)+fade, type, color, size, gravity);
+    }
+}
+
+VARP(particletext, 0, 1, 1);
+VARP(maxparticletextdistance, 0, 128, 10000);
+
+void particle_text(const vec &s, const char *t, int type, int fade, int color, float size, int gravity, int icons)
+{
+    if(!canaddparticles()) return;
+    if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return;
+    particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+    p->text = t;
+    p->flags = icons<<1;
+}
+
+void particle_textcopy(const vec &s, const char *t, int type, int fade, int color, float size, int gravity)
+{
+    if(!canaddparticles()) return;
+    if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return;
+    particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+    p->text = newstring(t);
+    p->flags = 1;
+}
+
+void particle_texticon(const vec &s, int ix, int iy, float offset, int type, int fade, int color, float size, int gravity)
+{
+    if(!canaddparticles()) return;
+    if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return;
+    particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+    p->flags |= ix | (iy<<2);
+    p->val = offset;
+}
+
+void particle_icon(const vec &s, int ix, int iy, int type, int fade, int color, float size, int gravity)
+{
+    if(!canaddparticles()) return;
+    particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity);
+    p->flags |= ix | (iy<<2);
+}
+
+void particle_meter(const vec &s, float val, int type, int fade, int color, int color2, float size)
+{
+    if(!canaddparticles()) return;
+    particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size);
+    p->color2[0] = color2>>16;
+    p->color2[1] = (color2>>8)&0xFF;
+    p->color2[2] = color2&0xFF;
+    p->progress = clamp(int(val*100), 0, 100);
+}
+
+void particle_flare(const vec &p, const vec &dest, int fade, int type, int color, float size, physent *owner)
+{
+    if(!canaddparticles()) return;
+    newparticle(p, dest, fade, type, color, size)->owner = owner;
+}
+
+void particle_fireball(const vec &dest, float maxsize, int type, int fade, int color, float size)
+{
+    if(!canaddparticles()) return;
+    float growth = maxsize - size;
+    if(fade < 0) fade = int(growth*20);
+    newparticle(dest, vec(0, 0, 1), fade, type, color, size)->val = growth;
+}
+
+//dir = 0..6 where 0=up
+static inline vec offsetvec(vec o, int dir, int dist) 
+{
+    vec v = vec(o);    
+    v[(2+dir)%3] += (dir>2)?(-dist):dist;
+    return v;
+}
+
+//converts a 16bit color to 24bit
+static inline int colorfromattr(int attr) 
+{
+    return (((attr&0xF)<<4) | ((attr&0xF0)<<8) | ((attr&0xF00)<<12)) + 0x0F0F0F;
+}
+
+/* Experiments in shapes...
+ * dir: (where dir%3 is similar to offsetvec with 0=up)
+ * 0..2 circle
+ * 3.. 5 cylinder shell
+ * 6..11 cone shell
+ * 12..14 plane volume
+ * 15..20 line volume, i.e. wall
+ * 21 sphere
+ * 24..26 flat plane
+ * +32 to inverse direction
+ */
+void regularshape(int type, int radius, int color, int dir, int num, int fade, const vec &p, float size, int gravity, int vel = 200)
+{
+    if(!canemitparticles()) return;
+    
+    int basetype = parts[type]->type&0xFF;
+    bool flare = (basetype == PT_TAPE) || (basetype == PT_LIGHTNING),
+         inv = (dir&0x20)!=0, taper = (dir&0x40)!=0 && !seedemitter;
+    dir &= 0x1F;
+    loopi(num)
+    {
+        vec to, from;
+        if(dir < 12) 
+        { 
+            const vec2 &sc = sincos360[rnd(360)];
+            to[dir%3] = sc.y*radius;
+            to[(dir+1)%3] = sc.x*radius;
+            to[(dir+2)%3] = 0.0;
+            to.add(p);
+            if(dir < 3) //circle
+                from = p;
+            else if(dir < 6) //cylinder
+            {
+                from = to;
+                to[(dir+2)%3] += radius;
+                from[(dir+2)%3] -= radius;
+            }
+            else //cone
+            {
+                from = p;
+                to[(dir+2)%3] += (dir < 9)?radius:(-radius);
+            }
+        }
+        else if(dir < 15) //plane
+        { 
+            to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+            to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+            to[(dir+2)%3] = radius;
+            to.add(p);
+            from = to;
+            from[(dir+2)%3] -= 2*radius;
+        }
+        else if(dir < 21) //line
+        {
+            if(dir < 18) 
+            {
+                to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+                to[(dir+1)%3] = 0.0;
+            } 
+            else 
+            {
+                to[dir%3] = 0.0;
+                to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0;
+            }
+            to[(dir+2)%3] = 0.0;
+            to.add(p);
+            from = to;
+            to[(dir+2)%3] += radius;  
+        } 
+        else if(dir < 24) //sphere
+        {   
+            to = vec(2*M_PI*float(rnd(1000))/1000.0, M_PI*float(rnd(1000)-500)/1000.0).mul(radius); 
+            to.add(p);
+            from = p;
+        }
+        else if(dir < 27) // flat plane
+        {
+            to[dir%3] = float(rndscale(2*radius)-radius);
+            to[(dir+1)%3] = float(rndscale(2*radius)-radius);
+            to[(dir+2)%3] = 0.0;
+            to.add(p);
+            from = to; 
+        }
+        else from = to = p; 
+
+        if(inv) swap(from, to);
+
+        if(taper)
+        {
+            float dist = clamp(from.dist2(camera1->o)/maxparticledistance, 0.0f, 1.0f);
+            if(dist > 0.2f)
+            {
+                dist = 1 - (dist - 0.2f)/0.8f;
+                if(rnd(0x10000) > dist*dist*0xFFFF) continue;
+            }
+        }
+        if(flare)
+            newparticle(from, to, rnd(fade*3)+1, type, color, size, gravity);
+        else 
+        {  
+            vec d = vec(to).sub(from).rescale(vel); //velocity
+            particle *n = newparticle(from, d, rnd(fade*3)+1, type, color, size, gravity);
+            if(parts[type]->collide)
+                n->val = from.z - raycube(from, vec(0, 0, -1), parts[type]->collide >= 0 ? COLLIDERADIUS : max(from.z, 0.0f), RAY_CLIPMAT) + (parts[type]->collide >= 0 ? COLLIDEERROR : 0);
+        }
+    }
+}
+
+static void regularflame(int type, const vec &p, float radius, float height, int color, int density = 3, float scale = 2.0f, float speed = 200.0f, float fade = 600.0f, int gravity = -15) 
+{
+    if(!canemitparticles()) return;
+    
+    float size = scale * min(radius, height);
+    vec v(0, 0, min(1.0f, height)*speed);
+    loopi(density)
+    {
+        vec s = p;        
+        s.x += rndscale(radius*2.0f)-radius;
+        s.y += rndscale(radius*2.0f)-radius;
+        newparticle(s, v, rnd(max(int(fade*height), 1))+1, type, color, size, gravity);
+    }
+}
+
+void regular_particle_flame(int type, const vec &p, float radius, float height, int color, int density, float scale, float speed, float fade, int gravity)
+{
+    if(!canaddparticles()) return;
+    regularflame(type, p, radius, height, color, density, scale, speed, fade, gravity);
+}
+
+static void makeparticles(entity &e) 
+{
+    switch(e.attr1)
+    {
+        case 0: //fire and smoke -  <radius> <height> <rgb> - 0 values default to compat for old maps
+        {
+            //regularsplash(PART_FIREBALL1, 0xFFC8C8, 150, 1, 40, e.o, 4.8f);
+            //regularsplash(PART_SMOKE, 0x897661, 50, 1, 200,  vec(e.o.x, e.o.y, e.o.z+3.0f), 2.4f, -20, 3);
+            float radius = e.attr2 ? float(e.attr2)/100.0f : 1.5f,
+                  height = e.attr3 ? float(e.attr3)/100.0f : radius/3;
+            regularflame(PART_FLAME, e.o, radius, height, e.attr4 ? colorfromattr(e.attr4) : 0x903020, 3, 2.0f);
+            regularflame(PART_SMOKE, vec(e.o.x, e.o.y, e.o.z + 4.0f*min(radius, height)), radius, height, 0x303020, 1, 4.0f, 100.0f, 2000.0f, -20);
+            break;
+        }
+        case 1: //steam vent - <dir>
+            regularsplash(PART_STEAM, 0x897661, 50, 1, 200, offsetvec(e.o, e.attr2, rnd(10)), 2.4f, -20);
+            break;
+        case 2: //water fountain - <dir>
+        {
+            int color;
+            if(e.attr3 > 0) color = colorfromattr(e.attr3);
+            else
+            {
+                int mat = MAT_WATER + clamp(-e.attr3, 0, 3); 
+                const bvec &wfcol = getwaterfallcolor(mat);
+                color = (int(wfcol[0])<<16) | (int(wfcol[1])<<8) | int(wfcol[2]);
+                if(!color) 
+                {
+                    const bvec &wcol = getwatercolor(mat);
+                    color = (int(wcol[0])<<16) | (int(wcol[1])<<8) | int(wcol[2]);
+                }
+            }
+            regularsplash(PART_WATER, color, 150, 4, 200, offsetvec(e.o, e.attr2, rnd(10)), 0.6f, 2);
+            break;
+        }
+        case 3: //fire ball - <size> <rgb>
+            newparticle(e.o, vec(0, 0, 1), 1, PART_EXPLOSION, colorfromattr(e.attr3), 4.0f)->val = 1+e.attr2;
+            break;
+        case 4:  //tape - <dir> <length> <rgb>
+        case 7:  //lightning 
+        case 9:  //steam
+        case 10: //water
+        case 13: //snow
+        {
+            static const int typemap[]   = { PART_STREAK, -1, -1, PART_LIGHTNING, -1, PART_STEAM, PART_WATER, -1, -1, PART_SNOW };
+            static const float sizemap[] = { 0.28f, 0.0f, 0.0f, 1.0f, 0.0f, 2.4f, 0.60f, 0.0f, 0.0f, 0.5f };
+            static const int gravmap[] = { 0, 0, 0, 0, 0, -20, 2, 0, 0, 20 };
+            int type = typemap[e.attr1-4];
+            float size = sizemap[e.attr1-4];
+            int gravity = gravmap[e.attr1-4];
+            if(e.attr2 >= 256) regularshape(type, max(1+e.attr3, 1), colorfromattr(e.attr4), e.attr2-256, 5, e.attr5 > 0 ? min(int(e.attr5), 10000) : 200, e.o, size, gravity);
+            else newparticle(e.o, offsetvec(e.o, e.attr2, max(1+e.attr3, 0)), 1, type, colorfromattr(e.attr4), size, gravity);
+            break;
+        }
+        case 5: //meter, metervs - <percent> <rgb> <rgb2>
+        case 6:
+        {
+            particle *p = newparticle(e.o, vec(0, 0, 1), 1, e.attr1==5 ? PART_METER : PART_METER_VS, colorfromattr(e.attr3), 2.0f);
+            int color2 = colorfromattr(e.attr4);
+            p->color2[0] = color2>>16;
+            p->color2[1] = (color2>>8)&0xFF;
+            p->color2[2] = color2&0xFF;
+            p->progress = clamp(int(e.attr2), 0, 100);
+            break;
+        }
+        case 11: // flame <radius> <height> <rgb> - radius=100, height=100 is the classic size
+            regularflame(PART_FLAME, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 3, 2.0f);
+            break;
+        case 12: // smoke plume <radius> <height> <rgb>
+            regularflame(PART_SMOKE, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 1, 4.0f, 100.0f, 2000.0f, -20);
+            break;
+        case 32: //lens flares - plain/sparkle/sun/sparklesun <red> <green> <blue>
+        case 33:
+        case 34:
+        case 35:
+            flares.addflare(e.o, e.attr2, e.attr3, e.attr4, (e.attr1&0x02)!=0, (e.attr1&0x01)!=0);
+            break;
+        default:
+            if(!editmode)
+            {
+                defformatstring(ds, "particles %d?", e.attr1);
+                particle_textcopy(e.o, ds, PART_TEXT, 1, 0x6496FF, 2.0f);
+            }
+            break;
+    }
+}
+
+bool printparticles(extentity &e, char *buf, int len)
+{
+    switch(e.attr1)
+    {
+        case 0: case 4: case 7: case 8: case 9: case 10: case 11: case 12: case 13: 
+            nformatstring(buf, len, "%s %d %d %d 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+            return true;
+        case 3:
+            nformatstring(buf, len, "%s %d %d 0x%.3hX %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+            return true;
+        case 5: case 6:
+            nformatstring(buf, len, "%s %d %d 0x%.3hX 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+            return true; 
+    }
+    return false;
+}
+
+void seedparticles()
+{
+    renderprogress(0, "seeding particles");
+    addparticleemitters();
+    canemit = true;
+    loopv(emitters)
+    {
+        particleemitter &pe = emitters[i];
+        extentity &e = *pe.ent;
+        seedemitter = &pe;
+        for(int millis = 0; millis < seedmillis; millis += min(emitmillis, seedmillis/10))
+            makeparticles(e);    
+        seedemitter = NULL;
+        pe.lastemit = -seedmillis;
+        pe.finalize();
+    }
+}
+
+void updateparticles()
+{
+    if(regenemitters) addparticleemitters();
+
+    if(minimized) { canemit = false; return; }
+
+    if(lastmillis - lastemitframe >= emitmillis)
+    {
+        canemit = true;
+        lastemitframe = lastmillis - (lastmillis%emitmillis);
+    }
+    else canemit = false;
+   
+    flares.makelightflares();
+
+    if(!editmode || showparticles) 
+    {
+        int emitted = 0, replayed = 0;
+        addedparticles = 0;
+        loopv(emitters)
+        {
+            particleemitter &pe = emitters[i];
+            extentity &e = *pe.ent;
+            if(e.o.dist(camera1->o) > maxparticledistance) { pe.lastemit = lastmillis; continue; } 
+            if(cullparticles && pe.maxfade >= 0)
+            {
+                if(isfoggedsphere(pe.radius, pe.center)) { pe.lastcull = lastmillis; continue; }
+                if(pvsoccluded(pe.cullmin, pe.cullmax)) { pe.lastcull = lastmillis; continue; }
+            }
+            makeparticles(e);
+            emitted++;
+            if(replayparticles && pe.maxfade > 5 && pe.lastcull > pe.lastemit)
+            {
+                for(emitoffset = max(pe.lastemit + emitmillis - lastmillis, -pe.maxfade); emitoffset < 0; emitoffset += emitmillis)
+                {
+                    makeparticles(e);
+                    replayed++;
+                }
+                emitoffset = 0;
+            } 
+            pe.lastemit = lastmillis;
+        }
+        if(dbgpcull && (canemit || replayed) && addedparticles) conoutf(CON_DEBUG, "%d emitters, %d particles", emitted, addedparticles);
+    }
+    if(editmode) // show sparkly thingies for map entities in edit mode
+    {
+        const vector<extentity *> &ents = entities::getents();
+        // note: order matters in this case as particles of the same type are drawn in the reverse order that they are added
+        loopv(entgroup)
+        {
+            entity &e = *ents[entgroup[i]];
+            particle_textcopy(e.o, entname(e), PART_TEXT, 1, 0xFF4B19, 2.0f);
+        }
+        loopv(ents)
+        {
+            entity &e = *ents[i];
+            if(e.type==ET_EMPTY) continue;
+            particle_textcopy(e.o, entname(e), PART_TEXT, 1, 0x1EC850, 2.0f);
+            regular_particle_splash(PART_EDIT, 2, 40, e.o, 0x3232FF, 0.32f*particlesize/100.0f);
+        }
+    }
+}
diff --git a/src/engine/rendersky.cpp b/src/engine/rendersky.cpp
new file mode 100644 (file)
index 0000000..32ca947
--- /dev/null
@@ -0,0 +1,774 @@
+#include "engine.h"
+
+Texture *sky[6] = { 0, 0, 0, 0, 0, 0 }, *clouds[6] = { 0, 0, 0, 0, 0, 0 };
+
+void loadsky(const char *basename, Texture *texs[6])
+{
+    const char *wildcard = strchr(basename, '*');
+    loopi(6)
+    {
+        const char *side = cubemapsides[i].name;
+        string name;
+        copystring(name, makerelpath("packages", basename));
+        if(wildcard)
+        {
+            char *chop = strchr(name, '*');
+            if(chop) { *chop = '\0'; concatstring(name, side); concatstring(name, wildcard+1); }
+            texs[i] = textureload(name, 3, true, false); 
+        }
+        else
+        {
+            defformatstring(ext, "_%s.jpg", side);
+            concatstring(name, ext);
+            if((texs[i] = textureload(name, 3, true, false))==notexture)
+            {
+                strcpy(name+strlen(name)-3, "png");
+                texs[i] = textureload(name, 3, true, false);
+            }
+        }
+        if(texs[i]==notexture) conoutf(CON_ERROR, "could not load side %s of sky texture %s", side, basename);
+    }
+}
+
+Texture *cloudoverlay = NULL;
+
+Texture *loadskyoverlay(const char *basename)
+{
+    const char *ext = strrchr(basename, '.'); 
+    string name;
+    copystring(name, makerelpath("packages", basename));
+    Texture *t = notexture;
+    if(ext) t = textureload(name, 0, true, false);
+    else
+    {
+        concatstring(name, ".jpg");
+        if((t = textureload(name, 0, true, false)) == notexture)
+        {
+            strcpy(name+strlen(name)-3, "png");
+            t = textureload(name, 0, true, false);
+        }
+    }
+    if(t==notexture) conoutf(CON_ERROR, "could not load sky overlay texture %s", basename);
+    return t;
+}
+
+SVARFR(skybox, "", { if(skybox[0]) loadsky(skybox, sky); }); 
+HVARR(skyboxcolour, 0, 0xFFFFFF, 0xFFFFFF);
+FVARR(spinsky, -720, 0, 720);
+VARR(yawsky, 0, 0, 360);
+SVARFR(cloudbox, "", { if(cloudbox[0]) loadsky(cloudbox, clouds); });
+HVARR(cloudboxcolour, 0, 0xFFFFFF, 0xFFFFFF);
+FVARR(cloudboxalpha, 0, 1, 1);
+FVARR(spinclouds, -720, 0, 720);
+VARR(yawclouds, 0, 0, 360);
+FVARR(cloudclip, 0, 0.5f, 1);
+SVARFR(cloudlayer, "", { if(cloudlayer[0]) cloudoverlay = loadskyoverlay(cloudlayer); });
+FVARR(cloudoffsetx, 0, 0, 1);
+FVARR(cloudoffsety, 0, 0, 1);
+FVARR(cloudscrollx, -16, 0, 16);
+FVARR(cloudscrolly, -16, 0, 16);
+FVARR(cloudscale, 0.001, 1, 64);
+FVARR(spincloudlayer, -720, 0, 720);
+VARR(yawcloudlayer, 0, 0, 360);
+FVARR(cloudheight, -1, 0.2f, 1);
+FVARR(cloudfade, 0, 0.2f, 1);
+FVARR(cloudalpha, 0, 1, 1);
+VARR(cloudsubdiv, 4, 16, 64);
+HVARR(cloudcolour, 0, 0xFFFFFF, 0xFFFFFF);
+
+void drawenvboxface(float s0, float t0, int x0, int y0, int z0,
+                    float s1, float t1, int x1, int y1, int z1,
+                    float s2, float t2, int x2, int y2, int z2,
+                    float s3, float t3, int x3, int y3, int z3,
+                    Texture *tex)
+{
+    glBindTexture(GL_TEXTURE_2D, (tex ? tex : notexture)->id);
+    gle::begin(GL_TRIANGLE_STRIP);
+    gle::attribf(x3, y3, z3); gle::attribf(s3, t3);
+    gle::attribf(x2, y2, z2); gle::attribf(s2, t2);
+    gle::attribf(x0, y0, z0); gle::attribf(s0, t0);
+    gle::attribf(x1, y1, z1); gle::attribf(s1, t1);
+    xtraverts += gle::end();
+}
+
+void drawenvbox(int w, float z1clip = 0.0f, float z2clip = 1.0f, int faces = 0x3F, Texture **sky = NULL)
+{
+    if(z1clip >= z2clip) return;
+
+    float v1 = 1-z1clip, v2 = 1-z2clip;
+    int z1 = int(ceil(2*w*(z1clip-0.5f))), z2 = int(ceil(2*w*(z2clip-0.5f)));
+
+    gle::defvertex();
+    gle::deftexcoord0();
+
+    if(faces&0x01)
+        drawenvboxface(0.0f, v2,  -w, -w, z2,
+                       1.0f, v2,  -w,  w, z2,
+                       1.0f, v1,  -w,  w, z1,
+                       0.0f, v1,  -w, -w, z1, sky[0]);
+
+    if(faces&0x02)
+        drawenvboxface(1.0f, v1, w, -w, z1,
+                       0.0f, v1, w,  w, z1,
+                       0.0f, v2, w,  w, z2,
+                       1.0f, v2, w, -w, z2, sky[1]);
+
+    if(faces&0x04)
+        drawenvboxface(1.0f, v1, -w, -w, z1,
+                       0.0f, v1,  w, -w, z1,
+                       0.0f, v2,  w, -w, z2,
+                       1.0f, v2, -w, -w, z2, sky[2]);
+
+    if(faces&0x08)
+        drawenvboxface(1.0f, v1,  w,  w, z1,
+                       0.0f, v1, -w,  w, z1,
+                       0.0f, v2, -w,  w, z2,
+                       1.0f, v2,  w,  w, z2, sky[3]);
+
+    if(z1clip <= 0 && faces&0x10)
+        drawenvboxface(0.0f, 1.0f, -w,  w,  -w,
+                       0.0f, 0.0f,  w,  w,  -w,
+                       1.0f, 0.0f,  w, -w,  -w,
+                       1.0f, 1.0f, -w, -w,  -w, sky[4]);
+
+    if(z2clip >= 1 && faces&0x20)
+        drawenvboxface(0.0f, 1.0f,  w,  w, w,
+                       0.0f, 0.0f, -w,  w, w,
+                       1.0f, 0.0f, -w, -w, w,
+                       1.0f, 1.0f,  w, -w, w, sky[5]);
+}
+
+void drawenvoverlay(int w, Texture *overlay = NULL, float tx = 0, float ty = 0)
+{
+    float z = w*cloudheight, tsz = 0.5f*(1-cloudfade)/cloudscale, psz = w*(1-cloudfade);
+    glBindTexture(GL_TEXTURE_2D, overlay ? overlay->id : notexture->id);
+    vec color = vec::hexcolor(cloudcolour);
+    gle::color(color, cloudalpha);
+    gle::defvertex();
+    gle::deftexcoord0();
+    gle::begin(GL_TRIANGLE_FAN);
+    loopi(cloudsubdiv+1)
+    {
+        vec p(1, 1, 0);
+        p.rotate_around_z((-2.0f*M_PI*i)/cloudsubdiv);
+        gle::attribf(p.x*psz, p.y*psz, z);
+            gle::attribf(tx + p.x*tsz, ty + p.y*tsz);
+    }
+    xtraverts += gle::end();
+    float tsz2 = 0.5f/cloudscale;
+    gle::defvertex();
+    gle::deftexcoord0();
+    gle::defcolor(4);
+    gle::begin(GL_TRIANGLE_STRIP);
+    loopi(cloudsubdiv+1)
+    {
+        vec p(1, 1, 0);
+        p.rotate_around_z((-2.0f*M_PI*i)/cloudsubdiv);
+        gle::attribf(p.x*psz, p.y*psz, z);
+            gle::attribf(tx + p.x*tsz, ty + p.y*tsz);
+            gle::attrib(color, cloudalpha);
+        gle::attribf(p.x*w, p.y*w, z);
+            gle::attribf(tx + p.x*tsz2, ty + p.y*tsz2);
+            gle::attrib(color, 0.0f);
+    }
+    xtraverts += gle::end();
+}
+
+FVARR(fogdomeheight, -1, -0.5f, 1);
+FVARR(fogdomemin, 0, 0, 1);
+FVARR(fogdomemax, 0, 0, 1);
+VARR(fogdomecap, 0, 1, 1);
+FVARR(fogdomeclip, 0, 1, 1);
+bvec fogdomecolor(0, 0, 0);
+HVARFR(fogdomecolour, 0, 0, 0xFFFFFF,
+{
+    fogdomecolor = bvec((fogdomecolour>>16)&0xFF, (fogdomecolour>>8)&0xFF, fogdomecolour&0xFF);
+});
+VARR(fogdomeclouds, 0, 1, 1);
+
+namespace fogdome
+{
+    struct vert
+    {
+        vec pos;
+        bvec4 color;
+    
+       vert() {}
+       vert(const vec &pos, const bvec &fcolor, float alpha) : pos(pos), color(fcolor, uchar(alpha*255))
+       {
+       }
+        vert(const vert &v0, const vert &v1) : pos(vec(v0.pos).add(v1.pos).normalize()), color(v0.color)
+       {
+            if(v0.pos.z != v1.pos.z) color.a += uchar((v1.color.a - v0.color.a) * (pos.z - v0.pos.z) / (v1.pos.z - v0.pos.z));
+       }
+    } *verts = NULL;
+    GLushort *indices = NULL;
+    int numverts = 0, numindices = 0, capindices = 0;
+    GLuint vbuf = 0, ebuf = 0;
+    bvec lastcolor(0, 0, 0);
+    float lastminalpha = 0, lastmaxalpha = 0, lastcapsize = -1, lastclipz = 1;
+    
+    void subdivide(int depth, int face);
+    
+    void genface(int depth, int i1, int i2, int i3)
+    {
+        int face = numindices; numindices += 3;
+        indices[face]   = i3;
+        indices[face+1] = i2;
+        indices[face+2] = i1;
+        subdivide(depth, face);
+    }
+    
+    void subdivide(int depth, int face)
+    {
+        if(depth-- <= 0) return;
+        int idx[6];
+        loopi(3) idx[i] = indices[face+2-i];
+        loopi(3)
+        {
+            int curvert = numverts++;
+            verts[curvert] = vert(verts[idx[i]], verts[idx[(i+1)%3]]); //push on to unit sphere
+            idx[3+i] = curvert;
+            indices[face+2-i] = curvert;
+        }
+        subdivide(depth, face);
+        loopi(3) genface(depth, idx[i], idx[3+i], idx[3+(i+2)%3]);
+    }
+    
+    int sortcap(GLushort x, GLushort y)
+    {
+        const vec &xv = verts[x].pos, &yv = verts[y].pos;
+        return xv.y < 0 ? yv.y >= 0 || xv.x < yv.x : yv.y >= 0 && xv.x > yv.x;
+    }
+    
+    void init(const bvec &color, float minalpha = 0.0f, float maxalpha = 1.0f, float capsize = -1, float clipz = 1, int hres = 16, int depth = 2)
+    {
+        const int tris = hres << (2*depth);
+        numverts = numindices = capindices = 0;
+        verts = new vert[tris+1 + (capsize >= 0 ? 1 : 0)];
+        indices = new GLushort[(tris + (capsize >= 0 ? hres<<depth : 0))*3];
+        if(clipz >= 1)
+        {
+            verts[numverts++] = vert(vec(0.0f, 0.0f, 1.0f), color, minalpha); //build initial 'hres' sided pyramid
+            loopi(hres) verts[numverts++] = vert(vec(sincos360[(360*i)/hres], 0.0f), color, maxalpha);
+            loopi(hres) genface(depth, 0, i+1, 1+(i+1)%hres);
+        }
+        else if(clipz <= 0)
+        {
+            loopi(hres<<depth) verts[numverts++] = vert(vec(sincos360[(360*i)/(hres<<depth)], 0.0f), color, maxalpha);
+        }
+        else
+        {
+            float clipxy = sqrtf(1 - clipz*clipz);
+            const vec2 &scm = sincos360[180/hres];
+            loopi(hres)
+            {
+                const vec2 &sc = sincos360[(360*i)/hres];
+                verts[numverts++] = vert(vec(sc.x*clipxy, sc.y*clipxy, clipz), color, minalpha);
+                verts[numverts++] = vert(vec(sc.x, sc.y, 0.0f), color, maxalpha);
+                verts[numverts++] = vert(vec(sc.x*scm.x - sc.y*scm.y, sc.y*scm.x + sc.x*scm.y, 0.0f), color, maxalpha);
+            }
+            loopi(hres)
+            {
+                genface(depth-1, 3*i, 3*i+1, 3*i+2);
+                genface(depth-1, 3*i, 3*i+2, 3*((i+1)%hres));
+                genface(depth-1, 3*i+2, 3*((i+1)%hres)+1, 3*((i+1)%hres));
+            }
+        }
+    
+        if(capsize >= 0)
+        {
+            GLushort *cap = &indices[numindices];
+            int capverts = 0;
+            loopi(numverts) if(!verts[i].pos.z) cap[capverts++] = i;
+            verts[numverts++] = vert(vec(0.0f, 0.0f, -capsize), color, maxalpha);
+            quicksort(cap, capverts, sortcap); 
+            loopi(capverts)
+            {
+                int n = capverts-1-i;
+                cap[n*3] = cap[n];
+                cap[n*3+1] = cap[(n+1)%capverts];
+                cap[n*3+2] = numverts-1;
+                capindices += 3;
+            }
+        }
+    
+        if(!vbuf) glGenBuffers_(1, &vbuf);
+        gle::bindvbo(vbuf);
+        glBufferData_(GL_ARRAY_BUFFER, numverts*sizeof(vert), verts, GL_STATIC_DRAW);
+        DELETEA(verts);
+    
+        if(!ebuf) glGenBuffers_(1, &ebuf);
+        gle::bindebo(ebuf);
+        glBufferData_(GL_ELEMENT_ARRAY_BUFFER, (numindices + capindices)*sizeof(GLushort), indices, GL_STATIC_DRAW);
+        DELETEA(indices);
+    }
+    
+    void cleanup()
+    {
+       numverts = numindices = 0;
+        if(vbuf) { glDeleteBuffers_(1, &vbuf); vbuf = 0; }
+        if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+    }
+    
+    void draw()
+    {
+        float capsize = fogdomecap && fogdomeheight < 1 ? (1 + fogdomeheight) / (1 - fogdomeheight) : -1;
+        bvec color = fogdomecolour ? fogdomecolor : fogcolor;
+        if(!numverts || lastcolor != color || lastminalpha != fogdomemin || lastmaxalpha != fogdomemax || lastcapsize != capsize || lastclipz != fogdomeclip)
+        {
+            init(color, min(fogdomemin, fogdomemax), fogdomemax, capsize, fogdomeclip);
+            lastcolor = color;
+            lastminalpha = fogdomemin;
+            lastmaxalpha = fogdomemax;
+            lastcapsize = capsize;
+            lastclipz = fogdomeclip;
+        }
+    
+        gle::bindvbo(vbuf);
+        gle::bindebo(ebuf);
+
+        gle::vertexpointer(sizeof(vert), &verts->pos);
+        gle::colorpointer(sizeof(vert), &verts->color);
+        gle::enablevertex();
+        gle::enablecolor();
+    
+        glDrawRangeElements_(GL_TRIANGLES, 0, numverts-1, numindices + fogdomecap*capindices, GL_UNSIGNED_SHORT, indices);
+        xtraverts += numverts;
+        glde++;
+
+        gle::disablevertex();
+        gle::disablecolor();
+    
+        gle::clearvbo();
+        gle::clearebo();
+    }
+}
+
+static void drawfogdome(int farplane)
+{
+    SETSHADER(skyfog);
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+    matrix4 skymatrix = cammatrix, skyprojmatrix;
+    skymatrix.settranslation(vec(cammatrix.c).mul(farplane*fogdomeheight*0.5f));
+    skymatrix.scale(farplane/2, farplane/2, farplane*(0.5f - fogdomeheight*0.5f));
+    skyprojmatrix.mul(projmatrix, skymatrix);
+    LOCALPARAM(skymatrix, skyprojmatrix);
+
+    fogdome::draw();
+
+    glDisable(GL_BLEND);
+}
+
+void cleanupsky()
+{
+    fogdome::cleanup();
+}
+
+extern int atmo;
+
+void preloadatmoshaders(bool force = false)
+{
+    static bool needatmo = false;
+    if(force) needatmo = true;
+    if(!atmo || !needatmo) return;
+
+    useshaderbyname("atmosphere");
+    useshaderbyname("atmosphereglare");
+}
+
+void setupsky()
+{
+    preloadatmoshaders(true);
+}
+
+VARFR(atmo, 0, 0, 1, preloadatmoshaders());
+FVARR(atmoplanetsize, 1e-3f, 1, 1e3f);
+FVARR(atmoheight, 1e-3f, 1, 1e3f);
+FVARR(atmobright, 0, 1, 16);
+bvec atmosunlightcolor(0, 0, 0);
+HVARFR(atmosunlight, 0, 0, 0xFFFFFF,
+{
+    if(atmosunlight <= 255) atmosunlight |= (atmosunlight<<8) | (atmosunlight<<16);
+    atmosunlightcolor = bvec((atmosunlight>>16)&0xFF, (atmosunlight>>8)&0xFF, atmosunlight&0xFF);
+});
+FVARR(atmosunlightscale, 0, 1, 16);
+bvec atmosundiskcolor(0, 0, 0);
+HVARFR(atmosundisk, 0, 0, 0xFFFFFF,
+{
+    if(atmosundisk <= 255) atmosundisk |= (atmosundisk<<8) | (atmosundisk<<16);
+    atmosundiskcolor = bvec((atmosundisk>>16)&0xFF, (atmosundisk>>8)&0xFF, atmosundisk&0xFF);
+});
+FVARR(atmosundisksize, 0, 12, 90);
+FVARR(atmosundiskcorona, 0, 0.4f, 1);
+FVARR(atmosundiskbright, 0, 1, 16);
+FVARR(atmohaze, 0, 0.1f, 16);
+FVARR(atmodensity, 0, 1, 16);
+FVARR(atmoozone, 0, 1, 16);
+FVARR(atmoalpha, 0, 1, 1);
+
+static void drawatmosphere(int w, float z1clip = 0.0f, float z2clip = 1.0f, int faces = 0x3F)
+{
+    if(z1clip >= z2clip) return;
+
+    if(glaring) SETSHADER(atmosphereglare);
+    else SETSHADER(atmosphere);
+
+    matrix4 skymatrix = cammatrix, skyprojmatrix;
+    skymatrix.settranslation(0, 0, 0);
+    skyprojmatrix.mul(projmatrix, skymatrix);
+    LOCALPARAM(skymatrix, skyprojmatrix);
+
+    // optical depth scales for 3 different shells of atmosphere - air, haze, ozone
+    const float earthradius = 6371e3f, earthairheight = 8.4e3f, earthhazeheight = 1.25e3f, earthozoneheight = 50e3f;
+    float planetradius = earthradius*atmoplanetsize;
+    vec atmoshells = vec(earthairheight, earthhazeheight, earthozoneheight).mul(atmoheight).add(planetradius).square().sub(planetradius*planetradius);
+    LOCALPARAM(opticaldepthparams, vec4(atmoshells, planetradius));
+
+    // Henyey-Greenstein approximation, 1/(4pi) * (1 - g^2)/(1 + g^2 - 2gcos)]^1.5
+    // Hoffman-Preetham variation uses (1-g)^2 instead of 1-g^2 which avoids excessive glare
+    // clamp values near 0 angle to avoid spotlight artifact inside sundisk
+    float gm = max(0.95f - 0.2f*atmohaze, 0.65f), miescale = pow((1-gm)*(1-gm)/(4*M_PI), -2.0f/3.0f);
+    LOCALPARAMF(mieparams, miescale*(1 + gm*gm), miescale*-2*gm, 1 - (1 - cosf(0.5f*atmosundisksize*(1 - atmosundiskcorona)*RAD)));
+
+    static const vec lambda(680e-9f, 550e-9f, 450e-9f),
+                     k(0.686f, 0.678f, 0.666f),
+                     ozone(3.426f, 8.298f, 0.356f);
+    vec betar = vec(lambda).square().square().recip().mul(1.241e-30f/M_LN2 * atmodensity),
+        betam = vec(lambda).recip().square().mul(k).mul(9.072e-17f/M_LN2 * atmohaze),
+        betao = vec(ozone).mul(1.5e-7f/M_LN2 * atmoozone);
+    LOCALPARAM(betarayleigh, betar);
+    LOCALPARAM(betamie, betam);
+    LOCALPARAM(betaozone, betao);
+
+    // extinction in direction of sun
+    float sunoffset = sunlightdir.z*planetradius;
+    vec sundepth = vec(atmoshells).add(sunoffset*sunoffset).sqrt().sub(sunoffset);
+    vec sunweight = vec(betar).mul(sundepth.x).madd(betam, sundepth.y).madd(betao, sundepth.z - sundepth.x);
+    vec sunextinction = vec(sunweight).neg().exp2();
+    vec suncolor = atmosunlight ? atmosunlightcolor.tocolor().mul(atmosunlightscale) : sunlightcolor.tocolor().mul(sunlightscale);
+    // assume sunlight color is gamma encoded, so decode to linear light, then apply extinction
+    vec sunscale = vec(suncolor).square().mul(atmobright * 16).mul(sunextinction);
+    float maxsunweight = max(max(sunweight.x, sunweight.y), sunweight.z);
+    if(maxsunweight > 127) sunweight.mul(127/maxsunweight);
+    sunweight.add(1e-4f);
+    LOCALPARAM(sunweight, sunweight);
+    LOCALPARAM(sunlight, vec4(sunscale, atmoalpha));
+    LOCALPARAM(sundir, sunlightdir);
+
+    // invert extinction at zenith to get an approximation of how bright the sun disk should be
+    vec zenithdepth = vec(atmoshells).add(planetradius*planetradius).sqrt().sub(planetradius);
+    vec zenithweight = vec(betar).mul(zenithdepth.x).madd(betam, zenithdepth.y).madd(betao, zenithdepth.z - zenithdepth.x);
+    vec zenithextinction = vec(zenithweight).sub(sunweight).exp2();
+    vec diskcolor = (atmosundisk ? atmosundiskcolor.tocolor() : suncolor).square().mul(zenithextinction).mul(atmosundiskbright * (glaring ? 1 : 1.5f)).min(1);
+    LOCALPARAM(sundiskcolor, diskcolor);
+
+    // convert from view cosine into mu^2 for limb darkening, where mu = sqrt(1 - sin^2) and sin^2 = 1 - cos^2, thus mu^2 = 1 - (1 - cos^2*scale)
+    // convert corona offset into scale for mu^2, where sin = (1-corona) and thus mu^2 = 1 - (1-corona^2) 
+    float sundiskscale = sinf(0.5f*atmosundisksize*RAD);
+    float coronamu = 1 - (1-atmosundiskcorona)*(1-atmosundiskcorona);
+    if(sundiskscale > 0) LOCALPARAMF(sundiskparams, 1.0f/(sundiskscale*sundiskscale), 1.0f/max(coronamu, 1e-3f));
+    else LOCALPARAMF(sundiskparams, 0, 0);
+
+    float z1 = 2*w*(z1clip-0.5f), z2 = ceil(2*w*(z2clip-0.5f));
+
+    gle::defvertex();
+
+    if(glaring)
+    {
+        if(sundiskscale > 0 && sunlightdir.z*w + sundiskscale > z1 && sunlightdir.z*w - sundiskscale < z2)
+        {
+            gle::begin(GL_TRIANGLE_FAN);
+            vec spoke;
+            spoke.orthogonal(sunlightdir);
+            spoke.rescale(2*sundiskscale);
+            loopi(4) gle::attrib(vec(spoke).rotate(-2*M_PI*i/4.0f, sunlightdir).add(sunlightdir).mul(w));
+            xtraverts += gle::end();
+        }
+        return;
+    }
+
+    gle::begin(GL_QUADS);
+
+    if(faces&0x01)
+    {
+        gle::attribf(-w, -w, z1);
+        gle::attribf(-w,  w, z1);
+        gle::attribf(-w,  w, z2);
+        gle::attribf(-w, -w, z2);
+    }
+
+    if(faces&0x02)
+    {
+        gle::attribf(w, -w, z2);
+        gle::attribf(w,  w, z2);
+        gle::attribf(w,  w, z1);
+        gle::attribf(w, -w, z1);
+    }
+
+    if(faces&0x04)
+    {
+        gle::attribf(-w, -w, z2);
+        gle::attribf( w, -w, z2);
+        gle::attribf( w, -w, z1);
+        gle::attribf(-w, -w, z1);
+    }
+
+    if(faces&0x08)
+    {
+        gle::attribf( w, w, z2);
+        gle::attribf(-w, w, z2);
+        gle::attribf(-w, w, z1);
+        gle::attribf( w, w, z1);
+    }
+
+    if(z1clip <= 0 && faces&0x10)
+    {
+        gle::attribf(-w, -w, -w);
+        gle::attribf( w, -w, -w);
+        gle::attribf( w,  w, -w);
+        gle::attribf(-w,  w, -w);
+    }
+
+    if(z2clip >= 1 && faces&0x20)
+    {
+        gle::attribf( w, -w, w);
+        gle::attribf(-w, -w, w);
+        gle::attribf(-w,  w, w);
+        gle::attribf( w,  w, w);
+    }
+
+    xtraverts += gle::end();
+}
+
+VARP(sparklyfix, 0, 0, 1);
+VAR(showsky, 0, 1, 1); 
+VAR(clipsky, 0, 1, 1);
+
+bool drawskylimits(bool explicitonly)
+{
+    nocolorshader->set();
+
+    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+    bool rendered = rendersky(explicitonly);
+    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+    return rendered;
+}
+
+void drawskyoutline()
+{
+    notextureshader->set();
+
+    glDepthMask(GL_FALSE);
+    extern int wireframe;
+    if(!wireframe)
+    {
+        enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    }
+    gle::colorf(0.5f, 0.0f, 0.5f);
+    rendersky(true);
+    if(!wireframe) 
+    {
+        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+        disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+    }
+    glDepthMask(GL_TRUE);
+}
+
+VAR(clampsky, 0, 1, 1);
+
+static int yawskyfaces(int faces, int yaw, float spin = 0)
+{
+    if(spin || yaw%90) return faces&0x0F ? faces | 0x0F : faces;
+    static const int faceidxs[3][4] =
+    {
+        { 3, 2, 0, 1 },
+        { 1, 0, 3, 2 },
+        { 2, 3, 1, 0 }
+    };
+    yaw /= 90;
+    if(yaw < 1 || yaw > 3) return faces;
+    const int *idxs = faceidxs[yaw - 1];
+    return (faces & ~0x0F) | (((faces>>idxs[0])&1)<<0) | (((faces>>idxs[1])&1)<<1) | (((faces>>idxs[2])&1)<<2) | (((faces>>idxs[3])&1)<<3);
+}
+
+void drawskybox(int farplane, bool limited, bool force)
+{
+    extern int renderedskyfaces, renderedskyclip; // , renderedsky, renderedexplicitsky;
+    bool alwaysrender = editmode || !insideworld(camera1->o) || reflecting || force,
+         explicitonly = false;
+    if(limited)
+    {
+        explicitonly = alwaysrender || !sparklyfix || refracting; 
+        if(!drawskylimits(explicitonly) && !alwaysrender) return;
+        extern int ati_skybox_bug;
+        if(!alwaysrender && !renderedskyfaces && !ati_skybox_bug) explicitonly = false;
+    }
+    else if(!alwaysrender)
+    {
+        renderedskyfaces = 0;
+        renderedskyclip = INT_MAX;
+        for(vtxarray *va = visibleva; va; va = va->next)
+        {
+            if(va->occluded >= OCCLUDE_BB && va->skyfaces&0x80) continue;
+            renderedskyfaces |= va->skyfaces&0x3F;
+            if(!(va->skyfaces&0x1F) || camera1->o.z < va->skyclip) renderedskyclip = min(renderedskyclip, va->skyclip);
+            else renderedskyclip = 0;
+        }
+        if(!renderedskyfaces) return;
+    }
+    
+    if(alwaysrender)
+    {
+        renderedskyfaces = 0x3F;
+        renderedskyclip = 0;
+    }
+
+    float skyclip = clipsky ? max(renderedskyclip-1, 0) : 0, topclip = 1;
+    if(reflectz<worldsize)
+    {
+        if(refracting<0) topclip = 0.5f + 0.5f*(reflectz-camera1->o.z)/float(worldsize);
+        else if(reflectz>skyclip) skyclip = reflectz;
+    }
+    if(skyclip) skyclip = 0.5f + 0.5f*(skyclip-camera1->o.z)/float(worldsize); 
+
+    if(limited) 
+    {
+        if(explicitonly) glDisable(GL_DEPTH_TEST);
+        else glDepthFunc(GL_GEQUAL);
+    }
+    else glDepthFunc(GL_LEQUAL);
+
+    glDepthMask(GL_FALSE);
+
+    if(clampsky) glDepthRange(1, 1);
+
+    if(!atmo || (skybox[0] && atmoalpha < 1))
+    {
+        if(glaring) SETSHADER(skyboxglare);
+        else SETSHADER(skybox);
+
+        gle::color(vec::hexcolor(skyboxcolour));
+
+        matrix4 skymatrix = cammatrix, skyprojmatrix;
+        skymatrix.settranslation(0, 0, 0);
+        skymatrix.rotate_around_z((spinsky*lastmillis/1000.0f+yawsky)*-RAD);
+        skyprojmatrix.mul(projmatrix, skymatrix);
+        LOCALPARAM(skymatrix, skyprojmatrix);
+
+        drawenvbox(farplane/2, skyclip, topclip, yawskyfaces(renderedskyfaces, yawsky, spinsky), sky);
+    }
+
+    if(atmo)
+    {
+        if(atmoalpha < 1)
+        {
+            if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+            glEnable(GL_BLEND);
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        }
+
+        drawatmosphere(farplane/2, skyclip, topclip, renderedskyfaces);
+
+        if(atmoalpha < 1) glDisable(GL_BLEND);
+    }
+
+    if(!glaring)
+    {
+        if(fogdomemax && !fogdomeclouds)
+        {
+            if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+            drawfogdome(farplane);
+        }
+
+        if(cloudbox[0])
+        {
+            SETSHADER(skybox);
+
+            if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+            glEnable(GL_BLEND);
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+            gle::color(vec::hexcolor(cloudboxcolour), cloudboxalpha);
+
+            matrix4 skymatrix = cammatrix, skyprojmatrix;
+            skymatrix.settranslation(0, 0, 0);
+            skymatrix.rotate_around_z((spinclouds*lastmillis/1000.0f+yawclouds)*-RAD);
+            skyprojmatrix.mul(projmatrix, skymatrix);
+            LOCALPARAM(skymatrix, skyprojmatrix);
+
+            drawenvbox(farplane/2, skyclip ? skyclip : cloudclip, topclip, yawskyfaces(renderedskyfaces, yawclouds, spinclouds), clouds);
+
+            glDisable(GL_BLEND);
+        }
+
+        if(cloudlayer[0] && cloudheight && renderedskyfaces&(cloudheight < 0 ? 0x1F : 0x2F))
+        {
+            SETSHADER(skybox);
+
+            if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+            glDisable(GL_CULL_FACE);
+
+            glEnable(GL_BLEND);
+            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+            matrix4 skymatrix = cammatrix, skyprojmatrix;
+            skymatrix.settranslation(0, 0, 0);
+            skymatrix.rotate_around_z((spincloudlayer*lastmillis/1000.0f+yawcloudlayer)*-RAD);
+            skyprojmatrix.mul(projmatrix, skymatrix);
+            LOCALPARAM(skymatrix, skyprojmatrix);
+
+            drawenvoverlay(farplane/2, cloudoverlay, cloudoffsetx + cloudscrollx * lastmillis/1000.0f, cloudoffsety + cloudscrolly * lastmillis/1000.0f);
+
+            glDisable(GL_BLEND);
+
+            glEnable(GL_CULL_FACE);
+        }
+
+           if(fogdomemax && fogdomeclouds)
+           {
+            if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+            drawfogdome(farplane);
+           }
+    }
+
+    if(clampsky) glDepthRange(0, 1);
+
+    glDepthMask(GL_TRUE);
+
+    if(limited)
+    {
+        if(explicitonly) glEnable(GL_DEPTH_TEST);
+        else glDepthFunc(GL_LESS);
+        if(!reflecting && !refracting && !drawtex && editmode && showsky) drawskyoutline();
+    }
+    else glDepthFunc(GL_LESS);
+}
+
+VARNR(skytexture, useskytexture, 0, 1, 1);
+
+int explicitsky = 0;
+double skyarea = 0;
+
+bool limitsky()
+{
+    return (explicitsky && (useskytexture || editmode)) || (sparklyfix && skyarea / (double(worldsize)*double(worldsize)*6) < 0.9);
+}
+
+bool shouldrenderskyenvmap()
+{
+    return atmo != 0;
+}
+
+bool shouldclearskyboxglare()
+{
+    return atmo && (!skybox[0] || atmoalpha >= 1);
+}
+
diff --git a/src/engine/rendertarget.h b/src/engine/rendertarget.h
new file mode 100644 (file)
index 0000000..3c93f18
--- /dev/null
@@ -0,0 +1,464 @@
+extern int rtsharefb, rtscissor, blurtile;
+
+struct rendertarget
+{
+    int texw, texh, vieww, viewh;
+    GLenum colorfmt, depthfmt;
+    GLuint rendertex, renderfb, renderdb, blurtex, blurfb, blurdb;
+    int blursize, blurysize;
+    float blursigma;
+    float blurweights[MAXBLURRADIUS+1], bluroffsets[MAXBLURRADIUS+1];
+    float bluryweights[MAXBLURRADIUS+1], bluryoffsets[MAXBLURRADIUS+1];
+
+    float scissorx1, scissory1, scissorx2, scissory2;
+#define BLURTILES 32
+#define BLURTILEMASK (0xFFFFFFFFU>>(32-BLURTILES))
+    uint blurtiles[BLURTILES+1];
+
+    bool initialized;
+
+    rendertarget() : texw(0), texh(0), vieww(0), viewh(0), colorfmt(GL_FALSE), depthfmt(GL_FALSE), rendertex(0), renderfb(0), renderdb(0), blurtex(0), blurfb(0), blurdb(0), blursize(0), blurysize(0), blursigma(0), initialized(false)
+    {
+    }
+
+    virtual ~rendertarget() {}
+
+    virtual GLenum attachment() const
+    {
+        return GL_COLOR_ATTACHMENT0;
+    }
+
+    virtual const GLenum *colorformats() const
+    {
+        static const GLenum colorfmts[] = { GL_RGB, GL_RGB8, GL_FALSE };
+        return colorfmts;
+    }
+
+    virtual const GLenum *depthformats() const
+    {
+        static const GLenum depthfmts[] = { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT32, GL_FALSE };
+        return depthfmts;
+    }
+
+    virtual bool depthtest() const { return true; }
+
+    void cleanup(bool fullclean = false)
+    {
+        if(renderfb) { glDeleteFramebuffers_(1, &renderfb); renderfb = 0; }
+        if(renderdb) { glDeleteRenderbuffers_(1, &renderdb); renderdb = 0; }
+        if(rendertex) { glDeleteTextures(1, &rendertex); rendertex = 0; }
+        texw = texh = 0;
+        cleanupblur();
+
+        if(fullclean) colorfmt = depthfmt = GL_FALSE;
+    }
+
+    void cleanupblur()
+    {
+        if(blurfb) { glDeleteFramebuffers_(1, &blurfb); blurfb = 0; }
+        if(blurtex) { glDeleteTextures(1, &blurtex); blurtex = 0; }
+        if(blurdb) { glDeleteRenderbuffers_(1, &blurdb); blurdb = 0; }
+        blursize = blurysize = 0;
+        blursigma = 0.0f;
+    }
+
+    void setupblur()
+    {
+        if(!blurtex) glGenTextures(1, &blurtex);
+        createtexture(blurtex, texw, texh, NULL, 3, 1, colorfmt);
+
+        if(!swaptexs() || rtsharefb) return;
+        if(!blurfb) glGenFramebuffers_(1, &blurfb);
+        glBindFramebuffer_(GL_FRAMEBUFFER, blurfb);
+        glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, blurtex, 0);
+        if(depthtest())
+        {
+            if(!blurdb) glGenRenderbuffers_(1, &blurdb);
+            glGenRenderbuffers_(1, &blurdb);
+            glBindRenderbuffer_(GL_RENDERBUFFER, blurdb);
+            glRenderbufferStorage_(GL_RENDERBUFFER, depthfmt, texw, texh);
+            glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, blurdb);
+        }
+        glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+    }
+
+    void setup(int w, int h)
+    {
+        if(!renderfb) glGenFramebuffers_(1, &renderfb);
+        glBindFramebuffer_(GL_FRAMEBUFFER, renderfb);
+        if(!rendertex) glGenTextures(1, &rendertex);
+
+        GLenum attach = attachment();
+        if(attach == GL_DEPTH_ATTACHMENT)
+        {
+            glDrawBuffer(GL_NONE);
+            glReadBuffer(GL_NONE);
+        }
+
+        const GLenum *colorfmts = colorformats();
+        int find = 0;
+        do
+        {
+            createtexture(rendertex, w, h, NULL, 3, filter() ? 1 : 0, colorfmt ? colorfmt : colorfmts[find]);
+            glFramebufferTexture2D_(GL_FRAMEBUFFER, attach, GL_TEXTURE_2D, rendertex, 0);
+            if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+        }
+        while(!colorfmt && colorfmts[++find]);
+        if(!colorfmt) colorfmt = colorfmts[find];
+
+        if(attach != GL_DEPTH_ATTACHMENT && depthtest())
+        {
+            if(!renderdb) { glGenRenderbuffers_(1, &renderdb); depthfmt = GL_FALSE; }
+            if(!depthfmt) glBindRenderbuffer_(GL_RENDERBUFFER, renderdb);
+            const GLenum *depthfmts = depthformats();
+            find = 0;
+            do
+            {
+                if(!depthfmt) glRenderbufferStorage_(GL_RENDERBUFFER, depthfmts[find], w, h);
+                glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderdb);
+                if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+            }
+            while(!depthfmt && depthfmts[++find]);
+            if(!depthfmt) depthfmt = depthfmts[find];
+        }
+        glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+
+        texw = w;
+        texh = h;
+        initialized = false;
+    }
+
+    bool addblurtiles(float x1, float y1, float x2, float y2, float blurmargin = 0)
+    {
+        if(x1 >= 1 || y1 >= 1 || x2 <= -1 || y2 <= -1) return false;
+
+        scissorx1 = min(scissorx1, max(x1, -1.0f));
+        scissory1 = min(scissory1, max(y1, -1.0f));
+        scissorx2 = max(scissorx2, min(x2, 1.0f));
+        scissory2 = max(scissory2, min(y2, 1.0f));
+
+        float blurerror = 2.0f*float(2*blursize + blurmargin);
+        int tx1 = max(0, min(BLURTILES - 1, int((x1-blurerror/vieww + 1)/2 * BLURTILES))),
+            ty1 = max(0, min(BLURTILES - 1, int((y1-blurerror/viewh + 1)/2 * BLURTILES))),
+            tx2 = max(0, min(BLURTILES - 1, int((x2+blurerror/vieww + 1)/2 * BLURTILES))),
+            ty2 = max(0, min(BLURTILES - 1, int((y2+blurerror/viewh + 1)/2 * BLURTILES)));
+
+        uint mask = (BLURTILEMASK>>(BLURTILES - (tx2+1))) & (BLURTILEMASK<<tx1);
+        for(int y = ty1; y <= ty2; y++) blurtiles[y] |= mask;
+        return true;
+    }
+
+    bool checkblurtiles(float x1, float y1, float x2, float y2, float blurmargin = 0)
+    {
+        float blurerror = 2.0f*float(2*blursize + blurmargin);
+        if(x2+blurerror/vieww < scissorx1 || y2+blurerror/viewh < scissory1 || 
+           x1-blurerror/vieww > scissorx2 || y1-blurerror/viewh > scissory2) 
+            return false;
+
+        if(!blurtile) return true;
+
+        int tx1 = max(0, min(BLURTILES - 1, int((x1 + 1)/2 * BLURTILES))),
+            ty1 = max(0, min(BLURTILES - 1, int((y1 + 1)/2 * BLURTILES))),
+            tx2 = max(0, min(BLURTILES - 1, int((x2 + 1)/2 * BLURTILES))),
+            ty2 = max(0, min(BLURTILES - 1, int((y2 + 1)/2 * BLURTILES)));
+
+        uint mask = (BLURTILEMASK>>(BLURTILES - (tx2+1))) & (BLURTILEMASK<<tx1);
+        for(int y = ty1; y <= ty2; y++) if(blurtiles[y] & mask) return true;
+
+        return false;
+    }
+
+    void rendertiles()
+    {
+        float wscale = vieww/float(texw), hscale = viewh/float(texh);
+        if(blurtile && scissorx1 < scissorx2 && scissory1 < scissory2)
+        {
+            uint tiles[sizeof(blurtiles)/sizeof(uint)];
+            memcpy(tiles, blurtiles, sizeof(blurtiles));
+
+            LOCALPARAMF(screentexcoord0, wscale*0.5f, hscale*0.5f, wscale*0.5f, hscale*0.5f);
+            gle::defvertex(2);
+            gle::begin(GL_QUADS);
+            float tsz = 1.0f/BLURTILES;
+            loop(y, BLURTILES+1)
+            {
+                uint mask = tiles[y];
+                int x = 0;
+                while(mask)
+                {
+                    while(!(mask&0xFF)) { mask >>= 8; x += 8; }
+                    while(!(mask&1)) { mask >>= 1; x++; }
+                    int xstart = x;
+                    do { mask >>= 1; x++; } while(mask&1);
+                    uint strip = (BLURTILEMASK>>(BLURTILES - x)) & (BLURTILEMASK<<xstart);
+                    int yend = y;
+                    do { tiles[yend] &= ~strip; yend++; } while((tiles[yend] & strip) == strip);
+                    float tx = xstart*tsz,
+                          ty = y*tsz,
+                          tw = (x-xstart)*tsz,
+                          th = (yend-y)*tsz,
+                          vx = 2*tx - 1, vy = 2*ty - 1, vw = tw*2, vh = th*2;
+                    gle::attribf(vx,    vy);
+                    gle::attribf(vx+vw, vy);
+                    gle::attribf(vx+vw, vy+vh);
+                    gle::attribf(vx,    vy+vh);
+                }
+            }
+            gle::end();
+        }
+        else
+        {
+            screenquad(wscale, hscale);
+        }
+    }
+
+    void blur(int wantsblursize, float wantsblursigma, int wantsblurysize, int x, int y, int w, int h, bool scissor)
+    {
+        if(!blurtex) setupblur();
+        if(blursize!=wantsblursize || blurysize != wantsblurysize || (wantsblursize && blursigma!=wantsblursigma))
+        {
+            setupblurkernel(wantsblursize, wantsblursigma, blurweights, bluroffsets);
+            if(wantsblurysize != wantsblursize) setupblurkernel(wantsblurysize, wantsblursigma, bluryweights, bluryoffsets);
+            blursize = wantsblursize;
+            blursigma = wantsblursigma;
+            blurysize = wantsblurysize;
+        }
+
+        glDisable(GL_DEPTH_TEST);
+        glDisable(GL_CULL_FACE);
+
+        if(scissor)
+        {
+            glScissor(x, y, w, h);
+            glEnable(GL_SCISSOR_TEST);
+        }
+
+        loopi(2)
+        {
+            if(i && blurysize != blursize) setblurshader(i, texh, blurysize, bluryweights, bluryoffsets);
+            else setblurshader(i, i ? texh : texw, blursize, blurweights, bluroffsets);
+
+            if(!swaptexs() || rtsharefb) glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, i ? rendertex : blurtex, 0);
+            else glBindFramebuffer_(GL_FRAMEBUFFER, i ? renderfb : blurfb);
+            glBindTexture(GL_TEXTURE_2D, i ? blurtex : rendertex);
+
+            rendertiles();
+        }
+
+        if(scissor) glDisable(GL_SCISSOR_TEST);
+            
+        glEnable(GL_DEPTH_TEST);
+        glEnable(GL_CULL_FACE);
+    }
+
+    virtual bool swaptexs() const { return false; }
+
+    virtual bool dorender() { return true; }
+
+    virtual bool shouldrender() { return true; }
+
+    virtual void doblur(int blursize, float blursigma, int blurysize)
+    { 
+        int sx, sy, sw, sh;
+        bool scissoring = rtscissor && scissorblur(sx, sy, sw, sh) && sw > 0 && sh > 0;
+        if(!scissoring) { sx = sy = 0; sw = vieww; sh = viewh; }
+        blur(blursize, blursigma, blurysize, sx, sy, sw, sh, scissoring);
+    }
+
+    virtual bool scissorrender(int &x, int &y, int &w, int &h)
+    {
+        if(scissorx1 >= scissorx2 || scissory1 >= scissory2) 
+        {
+            if(vieww < texw || viewh < texh)
+            {
+                x = y = 0;
+                w = vieww;
+                h = viewh;
+                return true;
+            }
+            return false;
+        }
+        x = max(int(floor((scissorx1+1)/2*vieww)) - 2*blursize, 0);
+        y = max(int(floor((scissory1+1)/2*viewh)) - 2*blursize, 0);
+        w = min(int(ceil((scissorx2+1)/2*vieww)) + 2*blursize, vieww) - x;
+        h = min(int(ceil((scissory2+1)/2*viewh)) + 2*blursize, viewh) - y;
+        return true;
+    }
+
+    virtual bool scissorblur(int &x, int &y, int &w, int &h)
+    {
+        if(scissorx1 >= scissorx2 || scissory1 >= scissory2)
+        {
+            if(vieww < texw || viewh < texh)
+            {
+                x = y = 0;
+                w = vieww;
+                h = viewh;
+                return true;
+            }
+            return false;
+        }
+        x = max(int(floor((scissorx1+1)/2*vieww)), 0);
+        y = max(int(floor((scissory1+1)/2*viewh)), 0);
+        w = min(int(ceil((scissorx2+1)/2*vieww)), vieww) - x;
+        h = min(int(ceil((scissory2+1)/2*viewh)), viewh) - y;
+        return true;
+    }
+
+    virtual void doclear() {}
+
+    virtual bool screenrect() const { return false; }
+    virtual bool filter() const { return true; }
+
+    void render(int w, int h, int blursize = 0, float blursigma = 0, int blurysize = 0)
+    {
+        w = min(w, hwtexsize);
+        h = min(h, hwtexsize);
+        if(screenrect())
+        {
+            if(w > screenw) w = screenw;
+            if(h > screenh) h = screenh;
+        }
+        vieww = w;
+        viewh = h;
+        if(w!=texw || h!=texh || (swaptexs() && !rtsharefb ? !blurfb : blurfb)) cleanup();
+        
+        if(!filter())
+        {
+            if(blurtex) cleanupblur();
+            blursize = blurysize = 0;
+        }
+            
+        if(!rendertex) setup(w, h);
+   
+        scissorx2 = scissory2 = -1;
+        scissorx1 = scissory1 = 1;
+        memset(blurtiles, 0, sizeof(blurtiles));
+        if(!shouldrender()) return;
+
+        if(blursize && !blurtex) setupblur();
+        if(swaptexs() && blursize)
+        {
+            swap(rendertex, blurtex);
+            if(!rtsharefb)
+            {
+                swap(renderfb, blurfb);
+                swap(renderdb, blurdb);
+            }
+        }
+        glBindFramebuffer_(GL_FRAMEBUFFER, renderfb);
+        if(swaptexs() && blursize && rtsharefb)
+            glFramebufferTexture2D_(GL_FRAMEBUFFER, attachment(), GL_TEXTURE_2D, rendertex, 0);
+        glViewport(0, 0, vieww, viewh);
+
+        doclear();
+
+        int sx, sy, sw, sh;
+        bool scissoring = rtscissor && scissorrender(sx, sy, sw, sh) && sw > 0 && sh > 0;
+        if(scissoring)
+        {
+            glScissor(sx, sy, sw, sh);
+            glEnable(GL_SCISSOR_TEST);
+        }
+        else
+        {
+            sx = sy = 0;
+            sw = vieww;
+            sh = viewh;
+        }
+
+        if(!depthtest()) glDisable(GL_DEPTH_TEST);
+
+        bool succeeded = dorender();
+
+        if(!depthtest()) glEnable(GL_DEPTH_TEST);
+
+        if(scissoring) glDisable(GL_SCISSOR_TEST);
+
+        if(succeeded)
+        {
+            initialized = true;
+
+            if(blursize) doblur(blursize, blursigma, blurysize ? blurysize : blursize);
+        }
+
+        glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+        glViewport(0, 0, screenw, screenh);
+    }
+
+    virtual void dodebug(int w, int h) {}
+    virtual bool flipdebug() const { return true; }
+
+    void debugscissor(int w, int h, bool lines = false)
+    {
+        if(!rtscissor || scissorx1 >= scissorx2 || scissory1 >= scissory2) return;
+        int sx = int(0.5f*(scissorx1 + 1)*w),
+            sy = int(0.5f*(scissory1 + 1)*h),
+            sw = int(0.5f*(scissorx2 - scissorx1)*w),
+            sh = int(0.5f*(scissory2 - scissory1)*h);
+        if(flipdebug()) { sy = h - sy; sh = -sh; }
+        gle::defvertex(2);
+        gle::begin(lines ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
+        gle::attribf(sx,      sy);
+        gle::attribf(sx + sw, sy);
+        if(lines) gle::attribf(sx + sw, sy + sh);
+        gle::attribf(sx,      sy + sh);
+        if(!lines) gle::attribf(sx + sw, sy + sh);
+        gle::end();
+    }
+
+    void debugblurtiles(int w, int h, bool lines = false)
+    {
+        if(!blurtile) return;
+        float vxsz = float(w)/BLURTILES, vysz = float(h)/BLURTILES;
+        gle::defvertex(2);
+        loop(y, BLURTILES+1)
+        {
+            uint mask = blurtiles[y];
+            int x = 0;
+            while(mask)
+            {
+                while(!(mask&0xFF)) { mask >>= 8; x += 8; }
+                while(!(mask&1)) { mask >>= 1; x++; }
+                    int xstart = x;
+                do { mask >>= 1; x++; } while(mask&1);
+                uint strip = (BLURTILEMASK>>(BLURTILES - x)) & (BLURTILEMASK<<xstart);
+                int yend = y;
+                do { blurtiles[yend] &= ~strip; yend++; } while((blurtiles[yend] & strip) == strip);
+                float vx = xstart*vxsz,
+                      vy = y*vysz,
+                      vw = (x-xstart)*vxsz,
+                      vh = (yend-y)*vysz;
+                if(flipdebug()) { vy = h - vy; vh = -vh; }
+                loopi(lines ? 1 : 2)
+                {
+                    if(!lines) gle::colorf(1, 1, i ? 1.0f : 0.5f);
+                    gle::begin(lines || i ? GL_LINE_LOOP : GL_TRIANGLE_STRIP);
+                    gle::attribf(vx,    vy);
+                    gle::attribf(vx+vw, vy);
+                    if(lines || i) gle::attribf(vx+vw, vy+vh);
+                    gle::attribf(vx,    vy+vh);
+                    if(!lines && !i) gle::attribf(vx+vw, vy+vh);
+                    gle::end();
+                }
+            }
+        }
+    }
+
+    void debug()
+    {
+        if(!rendertex) return;
+        int w = min(screenw, screenh)/2, h = (w*screenh)/screenw;
+        hudshader->set(); 
+        gle::colorf(1, 1, 1);
+        glBindTexture(GL_TEXTURE_2D, rendertex);
+        float tx1 = 0, tx2 = 1, ty1 = 0, ty2 = 1;
+        if(flipdebug()) swap(ty1, ty2);
+        hudquad(0, 0, w, h, tx1, ty1, tx2-tx1, ty2-ty1);
+        hudnotextureshader->set();
+        dodebug(w, h);
+    }
+};
+
diff --git a/src/engine/rendertext.cpp b/src/engine/rendertext.cpp
new file mode 100644 (file)
index 0000000..44ad136
--- /dev/null
@@ -0,0 +1,392 @@
+#include "engine.h"
+
+static hashnameset<font> fonts;
+static font *fontdef = NULL;
+static int fontdeftex = 0;
+
+font *curfont = NULL;
+int curfonttex = 0;
+
+void newfont(char *name, char *tex, int *defaultw, int *defaulth)
+{
+    font *f = &fonts[name];
+    if(!f->name) f->name = newstring(name);
+    f->texs.shrink(0);
+    f->texs.add(textureload(tex));
+    f->chars.shrink(0);
+    f->charoffset = '!';
+    f->defaultw = *defaultw;
+    f->defaulth = *defaulth;
+    f->scale = f->defaulth;
+
+    fontdef = f;
+    fontdeftex = 0;
+}
+
+void fontoffset(char *c)
+{
+    if(!fontdef) return;
+    
+    fontdef->charoffset = c[0];
+}
+
+void fontscale(int *scale)
+{
+    if(!fontdef) return;
+
+    fontdef->scale = *scale > 0 ? *scale : fontdef->defaulth; 
+}
+
+void fonttex(char *s)
+{
+    if(!fontdef) return;
+
+    Texture *t = textureload(s);
+    loopv(fontdef->texs) if(fontdef->texs[i] == t) { fontdeftex = i; return; }
+    fontdeftex = fontdef->texs.length();
+    fontdef->texs.add(t);
+}
+
+void fontchar(int *x, int *y, int *w, int *h, int *offsetx, int *offsety, int *advance)
+{
+    if(!fontdef) return;
+
+    font::charinfo &c = fontdef->chars.add();
+    c.x = *x;
+    c.y = *y;
+    c.w = *w ? *w : fontdef->defaultw;
+    c.h = *h ? *h : fontdef->defaulth;
+    c.offsetx = *offsetx;
+    c.offsety = *offsety;
+    c.advance = *advance ? *advance : c.offsetx + c.w;
+    c.tex = fontdeftex;
+}
+
+void fontskip(int *n)
+{
+    if(!fontdef) return;
+    loopi(max(*n, 1))
+    {
+        font::charinfo &c = fontdef->chars.add();
+        c.x = c.y = c.w = c.h = c.offsetx = c.offsety = c.advance = c.tex = 0;
+    }
+}
+
+COMMANDN(font, newfont, "ssii");
+COMMAND(fontoffset, "s");
+COMMAND(fontscale, "i");
+COMMAND(fonttex, "s");
+COMMAND(fontchar, "iiiiiii");
+COMMAND(fontskip, "i");
+
+void fontalias(const char *dst, const char *src)
+{
+    font *s = fonts.access(src);
+    if(!s) return;
+    font *d = &fonts[dst];
+    if(!d->name) d->name = newstring(dst);
+    d->texs = s->texs;
+    d->chars = s->chars;
+    d->charoffset = s->charoffset;
+    d->defaultw = s->defaultw;
+    d->defaulth = s->defaulth;
+    d->scale = s->scale;
+
+    fontdef = d;
+    fontdeftex = d->texs.length()-1;
+}
+
+COMMAND(fontalias, "ss");
+
+bool setfont(const char *name)
+{
+    font *f = fonts.access(name);
+    if(!f) return false;
+    curfont = f;
+    return true;
+}
+
+static vector<font *> fontstack;
+
+void pushfont()
+{
+    fontstack.add(curfont);
+}
+
+bool popfont()
+{
+    if(fontstack.empty()) return false;
+    curfont = fontstack.pop();
+    return true;
+}
+
+void gettextres(int &w, int &h)
+{
+    if(w < MINRESW || h < MINRESH)
+    {
+        if(MINRESW > w*MINRESH/h)
+        {
+            h = h*MINRESW/w;
+            w = MINRESW;
+        }
+        else
+        {
+            w = w*MINRESH/h;
+            h = MINRESH;
+        }
+    }
+}
+
+float text_widthf(const char *str) 
+{
+    float width, height;
+    text_boundsf(str, width, height);
+    return width;
+}
+
+#define FONTTAB (4*FONTW)
+#define TEXTTAB(x) ((int((x)/FONTTAB)+1.0f)*FONTTAB)
+
+void tabify(const char *str, int *numtabs)
+{
+    int tw = max(*numtabs, 0)*FONTTAB-1, tabs = 0;
+    for(float w = text_widthf(str); w <= tw; w = TEXTTAB(w)) ++tabs;
+    int len = strlen(str);
+    char *tstr = newstring(len + tabs);
+    memcpy(tstr, str, len);
+    memset(&tstr[len], '\t', tabs);
+    tstr[len+tabs] = '\0';
+    stringret(tstr);
+}
+
+COMMAND(tabify, "si");
+    
+void draw_textf(const char *fstr, int left, int top, ...)
+{
+    defvformatstring(str, top, fstr);
+    draw_text(str, left, top);
+}
+
+const matrix4x3 *textmatrix = NULL;
+
+static float draw_char(Texture *&tex, int c, float x, float y, float scale)
+{
+    font::charinfo &info = curfont->chars[c-curfont->charoffset];
+    if(tex != curfont->texs[info.tex])
+    {
+        xtraverts += gle::end();
+        tex = curfont->texs[info.tex];
+        glBindTexture(GL_TEXTURE_2D, tex->id);
+    }
+
+    float x1 = x + scale*info.offsetx,
+          y1 = y + scale*info.offsety,
+          x2 = x + scale*(info.offsetx + info.w),
+          y2 = y + scale*(info.offsety + info.h),
+          tx1 = info.x / float(tex->xs),
+          ty1 = info.y / float(tex->ys),
+          tx2 = (info.x + info.w) / float(tex->xs),
+          ty2 = (info.y + info.h) / float(tex->ys);
+
+    if(textmatrix)
+    {
+        gle::attrib(textmatrix->transform(vec2(x1, y1))); gle::attribf(tx1, ty1);
+        gle::attrib(textmatrix->transform(vec2(x2, y1))); gle::attribf(tx2, ty1);
+        gle::attrib(textmatrix->transform(vec2(x2, y2))); gle::attribf(tx2, ty2);
+        gle::attrib(textmatrix->transform(vec2(x1, y2))); gle::attribf(tx1, ty2);
+    }
+    else
+    {
+        gle::attribf(x1, y1); gle::attribf(tx1, ty1);
+        gle::attribf(x2, y1); gle::attribf(tx2, ty1);
+        gle::attribf(x2, y2); gle::attribf(tx2, ty2);
+        gle::attribf(x1, y2); gle::attribf(tx1, ty2);
+    }
+
+    return scale*info.advance;
+}
+
+//stack[sp] is current color index
+static void text_color(char c, char *stack, int size, int &sp, bvec color, int a) 
+{
+    if(c=='s') // save color
+    {   
+        c = stack[sp];
+        if(sp<size-1) stack[++sp] = c;
+    }
+    else
+    {
+        xtraverts += gle::end();
+        if(c=='r') { if(sp > 0) --sp; c = stack[sp]; } // restore color
+        else stack[sp] = c;
+        switch(c)
+        {
+            case '0': color = bvec( 64, 255, 128); break;   // green: player talk
+            case '1': color = bvec( 96, 160, 255); break;   // blue: "echo" command
+            case '2': color = bvec(255, 192,  64); break;   // yellow: gameplay messages 
+            case '3': color = bvec(255,  64,  64); break;   // red: important errors
+            case '4': color = bvec(128, 128, 128); break;   // gray
+            case '5': color = bvec(192,  64, 192); break;   // magenta
+            case '6': color = bvec(255, 128,   0); break;   // orange
+            case '7': color = bvec(255, 255, 255); break;   // white
+            case '8': color = bvec( 96, 240, 255); break;   // cyan
+            // provided color: everything else
+        }
+        gle::color(color, a);
+    } 
+}
+
+#define TEXTSKELETON \
+    float y = 0, x = 0, scale = curfont->scale/float(curfont->defaulth);\
+    int i;\
+    for(i = 0; str[i]; i++)\
+    {\
+        TEXTINDEX(i)\
+        int c = uchar(str[i]);\
+        if(c=='\t')      { x = TEXTTAB(x); TEXTWHITE(i) }\
+        else if(c==' ')  { x += scale*curfont->defaultw; TEXTWHITE(i) }\
+        else if(c=='\n') { TEXTLINE(i) x = 0; y += FONTH; }\
+        else if(c=='\f') { if(str[i+1]) { i++; TEXTCOLOR(i) }}\
+        else if(curfont->chars.inrange(c-curfont->charoffset))\
+        {\
+            float cw = scale*curfont->chars[c-curfont->charoffset].advance;\
+            if(cw <= 0) continue;\
+            if(maxwidth != -1)\
+            {\
+                int j = i;\
+                float w = cw;\
+                for(; str[i+1]; i++)\
+                {\
+                    int c = uchar(str[i+1]);\
+                    if(c=='\f') { if(str[i+2]) i++; continue; }\
+                    if(i-j > 16) break;\
+                    if(!curfont->chars.inrange(c-curfont->charoffset)) break;\
+                    float cw = scale*curfont->chars[c-curfont->charoffset].advance;\
+                    if(cw <= 0 || w + cw > maxwidth) break;\
+                    w += cw;\
+                }\
+                if(x + w > maxwidth && j!=0) { TEXTLINE(j-1) x = 0; y += FONTH; }\
+                TEXTWORD\
+            }\
+            else\
+            { TEXTCHAR(i) }\
+        }\
+    }
+
+//all the chars are guaranteed to be either drawable or color commands
+#define TEXTWORDSKELETON \
+                for(; j <= i; j++)\
+                {\
+                    TEXTINDEX(j)\
+                    int c = uchar(str[j]);\
+                    if(c=='\f') { if(str[j+1]) { j++; TEXTCOLOR(j) }}\
+                    else { float cw = scale*curfont->chars[c-curfont->charoffset].advance; TEXTCHAR(j) }\
+                }
+
+#define TEXTEND(cursor) if(cursor >= i) { do { TEXTINDEX(cursor); } while(0); }
+
+int text_visible(const char *str, float hitx, float hity, int maxwidth)
+{
+    #define TEXTINDEX(idx)
+    #define TEXTWHITE(idx) if(y+FONTH > hity && x >= hitx) return idx;
+    #define TEXTLINE(idx) if(y+FONTH > hity) return idx;
+    #define TEXTCOLOR(idx)
+    #define TEXTCHAR(idx) x += cw; TEXTWHITE(idx)
+    #define TEXTWORD TEXTWORDSKELETON
+    TEXTSKELETON
+    #undef TEXTINDEX
+    #undef TEXTWHITE
+    #undef TEXTLINE
+    #undef TEXTCOLOR
+    #undef TEXTCHAR
+    #undef TEXTWORD
+    return i;
+}
+
+//inverse of text_visible
+void text_posf(const char *str, int cursor, float &cx, float &cy, int maxwidth) 
+{
+    #define TEXTINDEX(idx) if(idx == cursor) { cx = x; cy = y; break; }
+    #define TEXTWHITE(idx)
+    #define TEXTLINE(idx)
+    #define TEXTCOLOR(idx)
+    #define TEXTCHAR(idx) x += cw;
+    #define TEXTWORD TEXTWORDSKELETON if(i >= cursor) break;
+    cx = cy = 0;
+    TEXTSKELETON
+    TEXTEND(cursor)
+    #undef TEXTINDEX
+    #undef TEXTWHITE
+    #undef TEXTLINE
+    #undef TEXTCOLOR
+    #undef TEXTCHAR
+    #undef TEXTWORD
+}
+
+void text_boundsf(const char *str, float &width, float &height, int maxwidth)
+{
+    #define TEXTINDEX(idx)
+    #define TEXTWHITE(idx)
+    #define TEXTLINE(idx) if(x > width) width = x;
+    #define TEXTCOLOR(idx)
+    #define TEXTCHAR(idx) x += cw;
+    #define TEXTWORD x += w;
+    width = 0;
+    TEXTSKELETON
+    height = y + FONTH;
+    TEXTLINE(_)
+    #undef TEXTINDEX
+    #undef TEXTWHITE
+    #undef TEXTLINE
+    #undef TEXTCOLOR
+    #undef TEXTCHAR
+    #undef TEXTWORD
+}
+
+void draw_text(const char *str, int left, int top, int r, int g, int b, int a, int cursor, int maxwidth) 
+{
+    #define TEXTINDEX(idx) if(idx == cursor) { cx = x; cy = y; }
+    #define TEXTWHITE(idx)
+    #define TEXTLINE(idx) 
+    #define TEXTCOLOR(idx) if(usecolor) text_color(str[idx], colorstack, sizeof(colorstack), colorpos, color, a);
+    #define TEXTCHAR(idx) draw_char(tex, c, left+x, top+y, scale); x += cw;
+    #define TEXTWORD TEXTWORDSKELETON
+    char colorstack[10];
+    colorstack[0] = 'c'; //indicate user color
+    bvec color(r, g, b);
+    int colorpos = 0;
+    float cx = -FONTW, cy = 0;
+    bool usecolor = true;
+    if(a < 0) { usecolor = false; a = -a; }
+    Texture *tex = curfont->texs[0];
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    glBindTexture(GL_TEXTURE_2D, tex->id);
+    gle::color(color, a);
+    gle::defvertex(textmatrix ? 3 : 2);
+    gle::deftexcoord0();
+    gle::begin(GL_QUADS);
+    TEXTSKELETON
+    TEXTEND(cursor)
+    xtraverts += gle::end();
+    if(cursor >= 0 && (totalmillis/250)&1)
+    {
+        gle::color(color, a);
+        if(maxwidth != -1 && cx >= maxwidth) { cx = 0; cy += FONTH; }
+        draw_char(tex, '_', left+cx, top+cy, scale);
+        xtraverts += gle::end();
+    }
+    #undef TEXTINDEX
+    #undef TEXTWHITE
+    #undef TEXTLINE
+    #undef TEXTCOLOR
+    #undef TEXTCHAR
+    #undef TEXTWORD
+}
+
+void reloadfonts()
+{
+    enumerate(fonts, font, f,
+        loopv(f.texs) if(!reloadtexture(*f.texs[i])) fatal("failed to reload font texture");
+    );
+}
+
diff --git a/src/engine/renderva.cpp b/src/engine/renderva.cpp
new file mode 100644 (file)
index 0000000..cf9d376
--- /dev/null
@@ -0,0 +1,1896 @@
+// renderva.cpp: handles the occlusion and rendering of vertex arrays
+
+#include "engine.h"
+
+static inline void drawtris(GLsizei numindices, const GLvoid *indices, ushort minvert, ushort maxvert)
+{
+    glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, numindices, GL_UNSIGNED_SHORT, indices);
+    glde++;
+}
+
+static inline void drawvatris(vtxarray *va, GLsizei numindices, const GLvoid *indices)
+{
+    drawtris(numindices, indices, va->minvert, va->maxvert);
+}
+
+///////// view frustrum culling ///////////////////////
+
+plane vfcP[5];  // perpindictular vectors to view frustrum bounding planes
+float vfcDfog;  // far plane culling distance (fog limit).
+float vfcDnear[5], vfcDfar[5];
+
+vtxarray *visibleva;
+
+bool isfoggedsphere(float rad, const vec &cv)
+{
+    loopi(4) if(vfcP[i].dist(cv) < -rad) return true;
+    float dist = vfcP[4].dist(cv);
+    return dist < -rad || dist > vfcDfog + rad;
+}
+
+int isvisiblesphere(float rad, const vec &cv)
+{
+    int v = VFC_FULL_VISIBLE;
+    float dist;
+
+    loopi(5)
+    {
+        dist = vfcP[i].dist(cv);
+        if(dist < -rad) return VFC_NOT_VISIBLE;
+        if(dist < rad) v = VFC_PART_VISIBLE;
+    }
+
+    dist -= vfcDfog;
+    if(dist > rad) return VFC_FOGGED;  //VFC_NOT_VISIBLE;    // culling when fog is closer than size of world results in HOM
+    if(dist > -rad) v = VFC_PART_VISIBLE;
+
+    return v;
+}
+
+static inline int ishiddencube(const ivec &o, int size)
+{
+    loopi(5) if(o.dist(vfcP[i]) < -vfcDfar[i]*size) return true;
+    return false;
+}
+
+static inline int isfoggedcube(const ivec &o, int size)
+{
+    loopi(4) if(o.dist(vfcP[i]) < -vfcDfar[i]*size) return true;
+    float dist = o.dist(vfcP[4]);
+    return dist < -vfcDfar[4]*size || dist > vfcDfog - vfcDnear[4]*size;
+}
+
+int isvisiblecube(const ivec &o, int size)
+{
+    int v = VFC_FULL_VISIBLE;
+    float dist;
+
+    loopi(5)
+    {
+        dist = o.dist(vfcP[i]);
+        if(dist < -vfcDfar[i]*size) return VFC_NOT_VISIBLE;
+        if(dist < -vfcDnear[i]*size) v = VFC_PART_VISIBLE;
+    }
+
+    dist -= vfcDfog;
+    if(dist > -vfcDnear[4]*size) return VFC_FOGGED;
+    if(dist > -vfcDfar[4]*size) v = VFC_PART_VISIBLE;
+
+    return v;
+}
+
+float vadist(vtxarray *va, const vec &p)
+{
+    return p.dist_to_bb(va->bbmin, va->bbmax);
+}
+
+#define VASORTSIZE 64
+
+static vtxarray *vasort[VASORTSIZE];
+
+void addvisibleva(vtxarray *va)
+{
+    float dist = vadist(va, camera1->o);
+    va->distance = int(dist); /*cv.dist(camera1->o) - va->size*SQRT3/2*/
+
+    int hash = clamp(int(dist*VASORTSIZE/worldsize), 0, VASORTSIZE-1);
+    vtxarray **prev = &vasort[hash], *cur = vasort[hash];
+
+    while(cur && va->distance >= cur->distance)
+    {
+        prev = &cur->next;
+        cur = cur->next;
+    }
+
+    va->next = *prev;
+    *prev = va;
+}
+
+void sortvisiblevas()
+{
+    visibleva = NULL; 
+    vtxarray **last = &visibleva;
+    loopi(VASORTSIZE) if(vasort[i])
+    {
+        vtxarray *va = vasort[i];
+        *last = va;
+        while(va->next) va = va->next;
+        last = &va->next;
+    }
+}
+
+void findvisiblevas(vector<vtxarray *> &vas, bool resetocclude = false)
+{
+    loopv(vas)
+    {
+        vtxarray &v = *vas[i];
+        int prevvfc = resetocclude ? VFC_NOT_VISIBLE : v.curvfc;
+        v.curvfc = isvisiblecube(v.o, v.size);
+        if(v.curvfc!=VFC_NOT_VISIBLE) 
+        {
+            if(pvsoccluded(v.o, v.size))
+            {
+                v.curvfc += PVS_FULL_VISIBLE - VFC_FULL_VISIBLE;
+                continue;
+            }
+            addvisibleva(&v);
+            if(v.children.length()) findvisiblevas(v.children, prevvfc>=VFC_NOT_VISIBLE);
+            if(prevvfc>=VFC_NOT_VISIBLE)
+            {
+                v.occluded = !v.texs ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+                v.query = NULL;
+            }
+        }
+    }
+}
+
+void calcvfcD()
+{
+    loopi(5)
+    {
+        plane &p = vfcP[i];
+        vfcDnear[i] = vfcDfar[i] = 0;
+        loopk(3) if(p[k] > 0) vfcDfar[i] += p[k];
+        else vfcDnear[i] += p[k];
+    }
+} 
+
+void setvfcP(float z, const vec &bbmin, const vec &bbmax)
+{
+    vec4 px = camprojmatrix.rowx(), py = camprojmatrix.rowy(), pz = camprojmatrix.rowz(), pw = camprojmatrix.roww();
+    vfcP[0] = plane(vec4(pw).mul(-bbmin.x).add(px)).normalize(); // left plane
+    vfcP[1] = plane(vec4(pw).mul(bbmax.x).sub(px)).normalize(); // right plane
+    vfcP[2] = plane(vec4(pw).mul(-bbmin.y).add(py)).normalize(); // bottom plane
+    vfcP[3] = plane(vec4(pw).mul(bbmax.y).sub(py)).normalize(); // top plane
+    vfcP[4] = plane(vec4(pw).add(pz)).normalize(); // near/far planes
+    if(z >= 0) loopi(5) vfcP[i].reflectz(z);
+
+    vfcDfog = fog;
+    calcvfcD();
+}
+
+plane oldvfcP[5];
+
+void savevfcP()
+{
+    memcpy(oldvfcP, vfcP, sizeof(vfcP));
+}
+
+void restorevfcP()
+{
+    memcpy(vfcP, oldvfcP, sizeof(vfcP));
+    calcvfcD();
+}
+
+void visiblecubes(bool cull)
+{
+    memclear(vasort);
+
+    if(cull)
+    {
+        setvfcP();
+        findvisiblevas(varoot);
+        sortvisiblevas();
+    }
+    else
+    {
+        memclear(vfcP);
+        vfcDfog = 1000000;
+        memclear(vfcDnear);
+        memclear(vfcDfar);
+        visibleva = NULL;
+        loopv(valist)
+        {
+            vtxarray *va = valist[i];
+            va->distance = 0;
+            va->curvfc = VFC_FULL_VISIBLE;
+            va->occluded = !va->texs ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+            va->query = NULL;
+            va->next = visibleva;
+            visibleva = va;
+        }
+    }
+}
+
+static inline bool insideva(const vtxarray *va, const vec &v, int margin = 2)
+{
+    int size = va->size + margin;
+    return v.x>=va->o.x-margin && v.y>=va->o.y-margin && v.z>=va->o.z-margin && 
+           v.x<=va->o.x+size && v.y<=va->o.y+size && v.z<=va->o.z+size;
+}
+
+///////// occlusion queries /////////////
+
+#define MAXQUERY 2048
+#define MAXQUERYFRAMES 2
+
+struct queryframe
+{
+    int cur, max;
+    occludequery queries[MAXQUERY];
+
+    queryframe() : cur(0), max(0) {}
+
+    void flip() { loopi(cur) queries[i].owner = NULL; cur = 0; }
+
+    occludequery *newquery(void *owner)
+    {
+        if(cur >= max)
+        {
+            if(max >= MAXQUERY) return NULL;
+            glGenQueries_(1, &queries[max++].id);
+        }
+        occludequery *query = &queries[cur++];
+        query->owner = owner;
+        query->fragments = -1;
+        return query;
+    }
+
+    void reset() { loopi(max) queries[i].owner = NULL; }
+
+    void cleanup()
+    {
+        loopi(max)
+        {
+            glDeleteQueries_(1, &queries[i].id);
+            queries[i].owner = NULL;
+        }
+        cur = max = 0;
+    }
+};
+
+static queryframe queryframes[MAXQUERYFRAMES];
+static uint flipquery = 0;
+
+int getnumqueries()
+{
+    return queryframes[flipquery].cur;
+}
+
+void flipqueries()
+{
+    flipquery = (flipquery + 1) % MAXQUERYFRAMES;
+    queryframes[flipquery].flip();
+}
+
+occludequery *newquery(void *owner)
+{
+    return queryframes[flipquery].newquery(owner);
+}
+
+void resetqueries()
+{
+    loopi(MAXQUERYFRAMES) queryframes[i].reset();
+}
+
+void clearqueries()
+{
+    loopi(MAXQUERYFRAMES) queryframes[i].cleanup();
+}
+
+VAR(oqfrags, 0, 8, 64);
+VAR(oqwait, 0, 1, 1);
+
+void startquery(occludequery *query)
+{
+    glBeginQuery_(GL_SAMPLES_PASSED, query->id);
+}
+
+void endquery(occludequery *query)
+{
+    glEndQuery_(GL_SAMPLES_PASSED);
+}
+
+bool checkquery(occludequery *query, bool nowait)
+{
+    GLuint fragments;
+    if(query->fragments >= 0) fragments = query->fragments;
+    else
+    {
+        if(nowait || !oqwait)
+        {
+            GLint avail;
+            glGetQueryObjectiv_(query->id, GL_QUERY_RESULT_AVAILABLE, &avail);
+            if(!avail) return false;
+        }
+        glGetQueryObjectuiv_(query->id, GL_QUERY_RESULT, &fragments);
+        query->fragments = fragments;
+    }
+    return fragments < uint(oqfrags);
+}
+
+static GLuint bbvbo = 0, bbebo = 0;
+
+static void setupbb()
+{
+    if(!bbvbo)
+    {
+        glGenBuffers_(1, &bbvbo);
+        gle::bindvbo(bbvbo);
+        vec verts[8];
+        loopi(8) verts[i] = vec(i&1, (i>>1)&1, (i>>2)&1);
+        glBufferData_(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+        gle::clearvbo();
+    }
+    if(!bbebo)
+    {
+        glGenBuffers_(1, &bbebo);
+        gle::bindebo(bbebo);
+        GLushort tris[3*2*6];
+        #define GENFACEORIENT(orient, v0, v1, v2, v3) do { \
+            int offset = orient*3*2; \
+            tris[offset + 0] = v0; \
+            tris[offset + 1] = v1; \
+            tris[offset + 2] = v2; \
+            tris[offset + 3] = v0; \
+            tris[offset + 4] = v2; \
+            tris[offset + 5] = v3; \
+        } while(0);
+        #define GENFACEVERT(orient, vert, ox,oy,oz, rx,ry,rz) (ox | oy | oz)
+        GENFACEVERTS(0, 1, 0, 2, 0, 4, , , , , , )
+        #undef GENFACEORIENT
+        #undef GENFACEVERT
+        glBufferData_(GL_ELEMENT_ARRAY_BUFFER, sizeof(tris), tris, GL_STATIC_DRAW);
+        gle::clearebo();
+    }
+}
+
+static void cleanupbb()
+{
+    if(bbvbo) { glDeleteBuffers_(1, &bbvbo); bbvbo = 0; }
+    if(bbebo) { glDeleteBuffers_(1, &bbebo); bbebo = 0; }
+}
+
+void startbb(bool mask)
+{
+    setupbb();
+    gle::bindvbo(bbvbo);
+    gle::bindebo(bbebo);
+    gle::vertexpointer(sizeof(vec), (const vec *)0);
+    gle::enablevertex();
+    SETSHADER(bbquery);
+    if(mask)
+    {
+        glDepthMask(GL_FALSE);
+        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+    }
+}
+
+void endbb(bool mask)
+{
+    gle::disablevertex();
+    gle::clearvbo();
+    gle::clearebo();
+    if(mask)
+    {
+        glDepthMask(GL_TRUE);
+        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+    }
+}
+
+void drawbb(const ivec &bo, const ivec &br)
+{
+    LOCALPARAMF(bborigin, bo.x, bo.y, bo.z);
+    LOCALPARAMF(bbsize, br.x, br.y, br.z);
+    glDrawRangeElements_(GL_TRIANGLES, 0, 8-1, 3*2*6, GL_UNSIGNED_SHORT, (ushort *)0);
+    xtraverts += 8;
+}
+
+extern int octaentsize;
+
+static octaentities *visiblemms, **lastvisiblemms;
+
+static inline bool insideoe(const octaentities *oe, const vec &v, int margin = 1)
+{
+    return v.x>=oe->bbmin.x-margin && v.y>=oe->bbmin.y-margin && v.z>=oe->bbmin.z-margin &&
+           v.x<=oe->bbmax.x+margin && v.y<=oe->bbmax.y+margin && v.z<=oe->bbmax.z+margin;
+}
+
+void findvisiblemms(const vector<extentity *> &ents, bool doquery)
+{
+    visiblemms = NULL;
+    lastvisiblemms = &visiblemms;
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(va->mapmodels.empty() || va->curvfc >= VFC_FOGGED || va->occluded >= OCCLUDE_BB) continue;
+        loopv(va->mapmodels)
+        {
+            octaentities *oe = va->mapmodels[i];
+            if(isfoggedcube(oe->o, oe->size) || pvsoccluded(oe->bbmin, oe->bbmax)) continue;
+
+            bool occluded = doquery && oe->query && oe->query->owner == oe && checkquery(oe->query);
+            if(occluded)
+            {
+                oe->distance = -1;
+
+                oe->next = NULL;
+                *lastvisiblemms = oe;
+                lastvisiblemms = &oe->next;
+            }
+            else
+            {
+                int visible = 0;
+                loopv(oe->mapmodels)
+                {
+                    extentity &e = *ents[oe->mapmodels[i]];
+                    if(e.flags&EF_NOVIS) continue;
+                    e.flags |= EF_RENDER;
+                    ++visible;
+                }
+                if(!visible) continue;
+
+                oe->distance = int(camera1->o.dist_to_bb(oe->o, oe->size));
+
+                octaentities **prev = &visiblemms, *cur = visiblemms;
+                while(cur && cur->distance >= 0 && oe->distance > cur->distance)
+                {
+                    prev = &cur->next;
+                    cur = cur->next;
+                }
+
+                if(*prev == NULL) lastvisiblemms = &oe->next;
+                oe->next = *prev;
+                *prev = oe;
+            }
+        }
+    }
+}
+
+VAR(oqmm, 0, 4, 8);
+
+void rendermapmodel(extentity &e)
+{
+    int anim = ANIM_MAPMODEL|ANIM_LOOP, basetime = 0;
+    if(e.flags&EF_ANIM) entities::animatemapmodel(e, anim, basetime);
+    mapmodelinfo *mmi = getmminfo(e.attr2);
+    if(mmi) rendermodel(&e.light, mmi->name, anim, e.o, e.attr1, 0, MDL_CULL_VFC | MDL_CULL_DIST | MDL_DYNLIGHT, NULL, NULL, basetime);
+}
+
+vtxarray *reflectedva;
+
+void renderreflectedmapmodels()
+{
+    const vector<extentity *> &ents = entities::getents();
+
+    octaentities *mms = visiblemms;
+    if(reflecting)
+    {
+        octaentities **lastmms = &mms;
+        for(vtxarray *va = reflectedva; va; va = va->rnext)
+        {
+            if(va->mapmodels.empty() || va->distance > reflectdist) continue;
+            loopv(va->mapmodels) 
+            {
+                octaentities *oe = va->mapmodels[i];
+                *lastmms = oe;
+                lastmms = &oe->rnext;
+            }
+        }
+        *lastmms = NULL;
+    }
+    for(octaentities *oe = mms; oe; oe = reflecting ? oe->rnext : oe->next) if(reflecting || oe->distance >= 0)
+    {
+        if(reflecting || refracting>0 ? oe->bbmax.z <= reflectz : oe->bbmin.z >= reflectz) continue;
+        if(isfoggedcube(oe->o, oe->size)) continue;
+        loopv(oe->mapmodels)
+        {
+           extentity &e = *ents[oe->mapmodels[i]];
+           if(e.flags&(EF_NOVIS | EF_RENDER)) continue;
+           e.flags |= EF_RENDER;
+        }
+    }
+    if(mms)
+    {
+        startmodelbatches();
+        for(octaentities *oe = mms; oe; oe = reflecting ? oe->rnext : oe->next)
+        {
+            loopv(oe->mapmodels)
+            {
+                extentity &e = *ents[oe->mapmodels[i]];
+                if(!(e.flags&EF_RENDER)) continue;
+                rendermapmodel(e);
+                e.flags &= ~EF_RENDER;
+            }
+        }
+        endmodelbatches();
+    }
+}
+
+void rendermapmodels()
+{
+    static int skipoq = 0;
+    bool doquery = !drawtex && oqfrags && oqmm;
+    const vector<extentity *> &ents = entities::getents();
+    findvisiblemms(ents, doquery);
+
+    startmodelbatches();
+    for(octaentities *oe = visiblemms; oe; oe = oe->next) if(oe->distance>=0)
+    {
+        bool rendered = false;
+        loopv(oe->mapmodels)
+        {
+            extentity &e = *ents[oe->mapmodels[i]];
+            if(!(e.flags&EF_RENDER)) continue;
+            if(!rendered)
+            {
+                rendered = true;
+                oe->query = doquery && oe->distance>0 && !(++skipoq%oqmm) ? newquery(oe) : NULL;
+                if(oe->query) startmodelquery(oe->query);
+            }        
+            rendermapmodel(e);
+            e.flags &= ~EF_RENDER;
+        }
+        if(rendered && oe->query) endmodelquery();
+    }
+    endmodelbatches();
+
+    bool queried = true;
+    for(octaentities *oe = visiblemms; oe; oe = oe->next) if(oe->distance<0)
+    {
+        oe->query = doquery && !insideoe(oe, camera1->o) ? newquery(oe) : NULL;
+        if(!oe->query) continue;
+        if(queried)
+        {
+            startbb();
+            queried = false;
+        }
+        startquery(oe->query);
+        drawbb(oe->bbmin, ivec(oe->bbmax).sub(oe->bbmin));
+        endquery(oe->query);
+    }
+    if(!queried)
+    {
+        endbb();
+    }
+}
+
+static inline bool bbinsideva(const ivec &bo, const ivec &br, vtxarray *va)
+{
+    return bo.x >= va->bbmin.x && bo.y >= va->bbmin.y && bo.z >= va->bbmin.z &&
+        br.x <= va->bbmax.x && br.y <= va->bbmax.y && br.z <= va->bbmax.z; 
+}
+
+static inline bool bboccluded(const ivec &bo, const ivec &br, cube *c, const ivec &o, int size)
+{
+    loopoctabox(o, size, bo, br)
+    {
+        ivec co(i, o, size);
+        if(c[i].ext && c[i].ext->va)
+        {
+            vtxarray *va = c[i].ext->va;
+            if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) continue;
+        }
+        if(c[i].children && bboccluded(bo, br, c[i].children, co, size>>1)) continue;
+        return false;
+    }
+    return true;
+}
+
+bool bboccluded(const ivec &bo, const ivec &br)
+{
+    int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z);
+    if(diff&~((1<<worldscale)-1)) return false;
+    int scale = worldscale-1;
+    if(diff&(1<<scale)) return bboccluded(bo, br, worldroot, ivec(0, 0, 0), 1<<scale);
+    cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
+    if(c->ext && c->ext->va)
+    {
+        vtxarray *va = c->ext->va;
+        if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) return true;
+    }
+    scale--;
+    while(c->children && !(diff&(1<<scale)))
+    {
+        c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
+        if(c->ext && c->ext->va)
+        {
+            vtxarray *va = c->ext->va;
+            if(va->curvfc >= VFC_FOGGED || (va->occluded >= OCCLUDE_BB && bbinsideva(bo, br, va))) return true;
+        }
+        scale--;
+    }
+    if(c->children) return bboccluded(bo, br, c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale);
+    return false;
+}
+
+VAR(outline, 0, 0, 1);
+HVARP(outlinecolour, 0, 0, 0xFFFFFF);
+VAR(dtoutline, 0, 1, 1);
+
+void renderoutline()
+{
+    notextureshader->set();
+
+    gle::enablevertex();
+
+    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    gle::color(vec::hexcolor(outlinecolour));
+
+    enablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+    if(!dtoutline) glDisable(GL_DEPTH_TEST);
+
+    vtxarray *prev = NULL;
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(va->occluded >= OCCLUDE_BB) continue;
+        if(!va->alphaback && !va->alphafront && (!va->texs || va->occluded >= OCCLUDE_GEOM)) continue;
+
+        if(!prev || va->vbuf != prev->vbuf)
+        {
+            gle::bindvbo(va->vbuf);
+            gle::bindebo(va->ebuf);
+            const vertex *ptr = 0;
+            gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+        }
+
+        if(va->texs && va->occluded < OCCLUDE_GEOM)
+        {
+            drawvatris(va, 3*va->tris, va->edata);
+            xtravertsva += va->verts;
+        }
+        if(va->alphatris)
+        {
+            drawvatris(va, 3*va->alphatris, &va->edata[3*(va->tris + va->blendtris)]);
+            xtravertsva += 3*va->alphatris;
+        }
+        
+        prev = va;
+    }
+
+    if(!dtoutline) glEnable(GL_DEPTH_TEST);
+
+    disablepolygonoffset(GL_POLYGON_OFFSET_LINE);
+
+    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+    gle::clearvbo();
+    gle::clearebo();
+    gle::disablevertex();
+}
+
+HVAR(blendbrushcolor, 0, 0x0000C0, 0xFFFFFF);
+
+void renderblendbrush(GLuint tex, float x, float y, float w, float h)
+{
+    SETSHADER(blendbrush);
+
+    gle::enablevertex();
+
+    glDepthFunc(GL_LEQUAL);
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+    glBindTexture(GL_TEXTURE_2D, tex);
+    gle::color(vec::hexcolor(blendbrushcolor), 0.25f);
+
+    LOCALPARAMF(texgenS, 1.0f/w, 0, 0, -x/w);
+    LOCALPARAMF(texgenT, 0, 1.0f/h, 0, -y/h);
+
+    vtxarray *prev = NULL;
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(!va->texs || va->occluded >= OCCLUDE_GEOM) continue;
+        if(va->o.x + va->size <= x || va->o.y + va->size <= y || va->o.x >= x + w || va->o.y >= y + h) continue;
+
+        if(!prev || va->vbuf != prev->vbuf)
+        {
+            gle::bindvbo(va->vbuf);
+            gle::bindebo(va->ebuf);
+            const vertex *ptr = 0;
+            gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+        }
+
+        drawvatris(va, 3*va->tris, va->edata);
+        xtravertsva += va->verts;
+
+        prev = va;
+    }
+
+    glDisable(GL_BLEND);
+
+    glDepthFunc(GL_LESS);
+
+    gle::clearvbo();
+    gle::clearebo();
+    gle::disablevertex();
+}
+void rendershadowmapreceivers()
+{
+    SETSHADER(shadowmapreceiver);
+
+    gle::enablevertex();
+
+    glCullFace(GL_FRONT);
+    glDepthMask(GL_FALSE);
+    glDepthFunc(GL_GREATER);
+
+    extern int ati_minmax_bug;
+    if(!ati_minmax_bug) glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE);
+
+    glEnable(GL_BLEND);
+    glBlendEquation_(GL_MAX);
+    glBlendFunc(GL_ONE, GL_ONE);
+    vtxarray *prev = NULL;
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(!va->texs || va->curvfc >= VFC_FOGGED || !isshadowmapreceiver(va)) continue;
+
+        if(!prev || va->vbuf != prev->vbuf)
+        {
+            gle::bindvbo(va->vbuf);
+            gle::bindebo(va->ebuf);
+            const vertex *ptr = 0;
+            gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+        }
+
+        drawvatris(va, 3*va->tris, va->edata);
+        xtravertsva += va->verts;
+
+        prev = va;
+    }
+
+    glDisable(GL_BLEND);
+    glBlendEquation_(GL_FUNC_ADD);
+
+    glCullFace(GL_BACK);
+    glDepthMask(GL_TRUE);
+    glDepthFunc(GL_LESS);
+    
+    if(!ati_minmax_bug) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+    gle::clearvbo();
+    gle::clearebo();
+    gle::disablevertex();
+}
+
+void renderdepthobstacles(const vec &bbmin, const vec &bbmax, float scale, float *ranges, int numranges)
+{
+    float scales[4] = { 0, 0, 0, 0 }, offsets[4] = { 0, 0, 0, 0 };
+    if(numranges < 0)
+    {
+        SETSHADER(depthfxsplitworld);
+
+        loopi(-numranges)
+        {
+            if(!i) scales[i] = 1.0f/scale;
+            else scales[i] = scales[i-1]*256;
+        }
+    }
+    else
+    {
+        SETSHADER(depthfxworld);
+
+        if(!numranges) loopi(4) scales[i] = 1.0f/scale;
+        else loopi(numranges) 
+        {
+            scales[i] = 1.0f/scale;
+            offsets[i] = -ranges[i]/scale;
+        }
+    }
+    LOCALPARAMF(depthscale, scales[0], scales[1], scales[2], scales[3]);
+    LOCALPARAMF(depthoffsets, offsets[0], offsets[1], offsets[2], offsets[3]);
+
+    gle::enablevertex();
+
+    vtxarray *prev = NULL;
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(!va->texs || va->occluded >= OCCLUDE_GEOM || 
+           va->o.x > bbmax.x || va->o.y > bbmax.y || va->o.z > bbmax.z ||
+           va->o.x + va->size < bbmin.x || va->o.y + va->size < bbmin.y || va->o.z + va->size < bbmin.z)
+           continue;
+
+        if(!prev || va->vbuf != prev->vbuf)
+        {
+            gle::bindvbo(va->vbuf);
+            gle::bindebo(va->ebuf);
+            const vertex *ptr = 0;
+            gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+        }
+
+        drawvatris(va, 3*va->tris, va->edata);
+        xtravertsva += va->verts;
+        if(va->alphatris > 0)
+        {
+            drawvatris(va, 3*va->alphatris, va->edata + 3*(va->tris + va->blendtris));
+            xtravertsva += 3*va->alphatris;
+        }
+
+        prev = va;
+    }
+
+    gle::clearvbo();
+    gle::clearebo();
+    gle::disablevertex();
+}
+
+VAR(oqdist, 0, 256, 1024);
+VAR(zpass, 0, 1, 1);
+VAR(envpass, 0, 1, 1);
+
+struct renderstate
+{
+    bool colormask, depthmask, blending;
+    int alphaing;
+    GLuint vbuf;
+    bool vattribs, vquery;
+    vec colorscale, lightcolor;
+    float alphascale;
+    GLuint textures[8];
+    Slot *slot, *texgenslot;
+    VSlot *vslot, *texgenvslot;
+    vec2 texgenscroll;
+    int texgendim;
+    int visibledynlights;
+    uint dynlightmask;
+
+    renderstate() : colormask(true), depthmask(true), blending(false), alphaing(0), vbuf(0), vattribs(false), vquery(false), colorscale(1, 1, 1), alphascale(0), slot(NULL), texgenslot(NULL), vslot(NULL), texgenvslot(NULL), texgenscroll(0, 0), texgendim(-1), visibledynlights(0), dynlightmask(0)
+    {
+        loopk(8) textures[k] = 0;
+    }
+};
+
+static inline void disablevbuf(renderstate &cur)
+{
+    gle::clearvbo();
+    gle::clearebo();
+    cur.vbuf = 0;
+}
+
+static inline void enablevquery(renderstate &cur)
+{
+    if(cur.colormask) { cur.colormask = false; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); }
+    if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); }
+    startbb(false);
+    cur.vquery = true;
+}
+
+static inline void disablevquery(renderstate &cur)
+{
+    endbb(false);
+    cur.vquery = false;
+}
+
+static void renderquery(renderstate &cur, occludequery *query, vtxarray *va, bool full = true)
+{
+    if(!cur.vquery) enablevquery(cur);
+
+    startquery(query);
+
+    if(full) drawbb(ivec(va->bbmin).sub(1), ivec(va->bbmax).sub(va->bbmin).add(2));
+    else drawbb(va->geommin, ivec(va->geommax).sub(va->geommin));
+
+    endquery(query);
+}
+
+enum
+{
+    RENDERPASS_LIGHTMAP = 0,
+    RENDERPASS_Z,
+    RENDERPASS_CAUSTICS,
+    RENDERPASS_FOG,
+    RENDERPASS_LIGHTMAP_BLEND
+};
+
+struct geombatch
+{
+    const elementset &es;
+    VSlot &vslot;
+    ushort *edata;
+    vtxarray *va;
+    int next, batch;
+
+    geombatch(const elementset &es, ushort *edata, vtxarray *va)
+      : es(es), vslot(lookupvslot(es.texture)), edata(edata), va(va),
+        next(-1), batch(-1)
+    {}
+
+    int compare(const geombatch &b) const
+    {
+        if(va->vbuf < b.va->vbuf) return -1;
+        if(va->vbuf > b.va->vbuf) return 1;
+        if(va->dynlightmask < b.va->dynlightmask) return -1;
+        if(va->dynlightmask > b.va->dynlightmask) return 1;
+        if(vslot.slot->shader < b.vslot.slot->shader) return -1;
+        if(vslot.slot->shader > b.vslot.slot->shader) return 1;
+        if(vslot.slot->params.length() < b.vslot.slot->params.length()) return -1;
+        if(vslot.slot->params.length() > b.vslot.slot->params.length()) return 1;
+        if(es.texture < b.es.texture) return -1;
+        if(es.texture > b.es.texture) return 1;
+        if(es.lmid < b.es.lmid) return -1;
+        if(es.lmid > b.es.lmid) return 1;
+        if(es.envmap < b.es.envmap) return -1;
+        if(es.envmap > b.es.envmap) return 1;
+        if(es.dim < b.es.dim) return -1;
+        if(es.dim > b.es.dim) return 1;
+        return 0;
+    }
+};
+
+static vector<geombatch> geombatches;
+static int firstbatch = -1, numbatches = 0;
+
+static void mergetexs(renderstate &cur, vtxarray *va, elementset *texs = NULL, int numtexs = 0, ushort *edata = NULL)
+{
+    if(!texs) 
+    { 
+        texs = va->eslist; 
+        numtexs = va->texs; 
+        edata = va->edata;
+        if(cur.alphaing)
+        {
+            texs += va->texs + va->blends;
+            edata += 3*(va->tris + va->blendtris);
+            numtexs = va->alphaback;
+            if(cur.alphaing > 1) numtexs += va->alphafront;
+        }
+    }
+
+    if(firstbatch < 0)
+    {
+        firstbatch = geombatches.length();
+        numbatches = numtexs;
+        loopi(numtexs-1) 
+        {
+            geombatches.add(geombatch(texs[i], edata, va)).next = i+1;
+            edata += texs[i].length[1];
+        }
+        geombatches.add(geombatch(texs[numtexs-1], edata, va));
+        return;
+    }
+    
+    int prevbatch = -1, curbatch = firstbatch, curtex = 0;
+    do
+    {
+        geombatch &b = geombatches.add(geombatch(texs[curtex], edata, va));
+        edata += texs[curtex].length[1];
+        int dir = -1;
+        while(curbatch >= 0)
+        {
+            dir = b.compare(geombatches[curbatch]);
+            if(dir <= 0) break;
+            prevbatch = curbatch;
+            curbatch = geombatches[curbatch].next;
+        }
+        if(!dir)
+        {
+            int last = curbatch, next;
+            for(;;)
+            {
+                next = geombatches[last].batch;
+                if(next < 0) break;
+                last = next;
+            }
+            if(last==curbatch)
+            {
+                b.batch = curbatch;
+                b.next = geombatches[curbatch].next;
+                if(prevbatch < 0) firstbatch = geombatches.length()-1;
+                else geombatches[prevbatch].next = geombatches.length()-1;
+                curbatch = geombatches.length()-1;
+            }
+            else
+            {
+                b.batch = next;
+                geombatches[last].batch = geombatches.length()-1;
+            }    
+        }
+        else 
+        {
+            numbatches++;
+            b.next = curbatch;
+            if(prevbatch < 0) firstbatch = geombatches.length()-1;
+            else geombatches[prevbatch].next = geombatches.length()-1;
+            prevbatch = geombatches.length()-1;
+        }
+    }
+    while(++curtex < numtexs);
+}
+
+static inline void enablevattribs(renderstate &cur, bool all = true)
+{
+    gle::enablevertex();
+    if(all)
+    {
+        gle::enabletexcoord0();
+        gle::enabletexcoord1();
+        gle::enablenormal();
+        gle::enabletangent();
+    }
+    cur.vattribs = true;
+}
+
+static inline void disablevattribs(renderstate &cur, bool all = true)
+{
+    gle::disablevertex();
+    if(all)
+    {
+        gle::disabletexcoord0();
+        gle::disabletexcoord1();
+        gle::disablenormal();
+        gle::disabletangent();
+    }
+    cur.vattribs = false;
+}
+
+static void changevbuf(renderstate &cur, int pass, vtxarray *va)
+{
+    gle::bindvbo(va->vbuf);
+    gle::bindebo(va->ebuf);
+    cur.vbuf = va->vbuf;
+
+    vertex *vdata = (vertex *)0;
+    gle::vertexpointer(sizeof(vertex), vdata->pos.v);
+
+    if(pass==RENDERPASS_LIGHTMAP)
+    {
+        gle::normalpointer(sizeof(vertex), vdata->norm.v, GL_BYTE);
+        gle::texcoord0pointer(sizeof(vertex), vdata->tc.v);
+        gle::texcoord1pointer(sizeof(vertex), vdata->lm.v, GL_SHORT);
+        gle::tangentpointer(sizeof(vertex), vdata->tangent.v, GL_BYTE);
+    }
+}
+
+static void changebatchtmus(renderstate &cur, int pass, geombatch &b)
+{
+    bool changed = false;
+    extern bool brightengeom;
+    extern int fullbright;
+    int lmid = brightengeom && (b.es.lmid < LMID_RESERVED || (fullbright && editmode)) ? LMID_BRIGHT : b.es.lmid; 
+    if(cur.textures[1]!=lightmaptexs[lmid].id)
+    {
+        glActiveTexture_(GL_TEXTURE1);
+        glBindTexture(GL_TEXTURE_2D, cur.textures[1] = lightmaptexs[lmid].id);
+        changed = true;
+    }
+    int tmu = 2;
+    if(b.vslot.slot->shader->type&SHADER_NORMALSLMS)
+    {
+        if(cur.textures[tmu]!=lightmaptexs[lmid+1].id)
+        {
+            glActiveTexture_(GL_TEXTURE0+tmu);
+            glBindTexture(GL_TEXTURE_2D, cur.textures[tmu] = lightmaptexs[lmid+1].id);
+            changed = true;
+        }
+        tmu++;
+    }
+    if(b.vslot.slot->shader->type&SHADER_ENVMAP && b.es.envmap!=EMID_CUSTOM)
+    {
+        GLuint emtex = lookupenvmap(b.es.envmap);
+        if(cur.textures[tmu]!=emtex)
+        {
+            glActiveTexture_(GL_TEXTURE0+tmu);
+            glBindTexture(GL_TEXTURE_CUBE_MAP, cur.textures[tmu] = emtex);
+            changed = true;
+        }
+    }
+    if(changed) glActiveTexture_(GL_TEXTURE0);
+
+    if(cur.dynlightmask != b.va->dynlightmask)
+    {
+        cur.visibledynlights = setdynlights(b.va);
+        cur.dynlightmask = b.va->dynlightmask;
+    }
+}
+
+static void changeslottmus(renderstate &cur, int pass, Slot &slot, VSlot &vslot)
+{
+    if(pass==RENDERPASS_LIGHTMAP)
+    {
+        GLuint diffusetex = slot.sts.empty() ? notexture->id : slot.sts[0].t->id;
+        if(cur.textures[0]!=diffusetex)
+            glBindTexture(GL_TEXTURE_2D, cur.textures[0] = diffusetex);
+    }
+
+    if(cur.alphaing)
+    {
+        float alpha = cur.alphaing > 1 ? vslot.alphafront : vslot.alphaback;
+        if(cur.colorscale != vslot.colorscale || cur.alphascale != alpha) 
+        {
+            cur.colorscale = vslot.colorscale;
+            cur.alphascale = alpha;
+            GLOBALPARAMF(colorparams, 2*alpha*vslot.colorscale.x, 2*alpha*vslot.colorscale.y, 2*alpha*vslot.colorscale.z, alpha);
+            setfogcolor(vec(curfogcolor).mul(alpha));
+        }
+    }
+    else if(cur.colorscale != vslot.colorscale)
+    {
+        cur.colorscale = vslot.colorscale;
+        GLOBALPARAMF(colorparams, 2*vslot.colorscale.x, 2*vslot.colorscale.y, 2*vslot.colorscale.z, 1);
+    }
+    int tmu = 2, envmaptmu = -1;
+    if(slot.shader->type&SHADER_NORMALSLMS) tmu++;
+    if(slot.shader->type&SHADER_ENVMAP) envmaptmu = tmu++;
+    loopvj(slot.sts)
+    {
+        Slot::Tex &t = slot.sts[j];
+        if(t.type==TEX_DIFFUSE || t.combined>=0) continue;
+        if(t.type==TEX_ENVMAP)
+        {
+            if(envmaptmu>=0 && t.t && cur.textures[envmaptmu]!=t.t->id)
+            {
+                glActiveTexture_(GL_TEXTURE0+envmaptmu);
+                glBindTexture(GL_TEXTURE_CUBE_MAP, cur.textures[envmaptmu] = t.t->id);
+            }
+        }
+        else 
+        {
+            if(cur.textures[tmu]!=t.t->id)
+            {
+                glActiveTexture_(GL_TEXTURE0+tmu);
+                glBindTexture(GL_TEXTURE_2D, cur.textures[tmu] = t.t->id);
+            }
+            if(++tmu >= 8) break;
+        }
+    }
+    glActiveTexture_(GL_TEXTURE0);
+
+    cur.slot = &slot;
+    cur.vslot = &vslot;
+}
+
+static void changeshader(renderstate &cur, Shader *s, Slot &slot, VSlot &vslot, bool shadowed)
+{
+    if(glaring)
+    {
+        static Shader *noglareshader = NULL, *noglareblendshader = NULL, *noglarealphashader = NULL;
+        Shader *fallback;
+        if(cur.blending) { if(!noglareblendshader) noglareblendshader = lookupshaderbyname("noglareblendworld"); fallback = noglareblendshader; }
+        else if(cur.alphaing) { if(!noglarealphashader) noglarealphashader = lookupshaderbyname("noglarealphaworld"); fallback = noglarealphashader; }
+        else { if(!noglareshader) noglareshader = lookupshaderbyname("noglareworld"); fallback = noglareshader; }
+        if(s->hasoption(4)) s->setvariant(cur.visibledynlights, 4, slot, vslot, fallback);
+        else s->setvariant(cur.blending ? 1 : 0, 4, slot, vslot, fallback);
+    }
+    else if(fading && !cur.blending && !cur.alphaing)
+    {
+        if(shadowed) s->setvariant(cur.visibledynlights, 3, slot, vslot);
+        else s->setvariant(cur.visibledynlights, 2, slot, vslot);
+    }
+    else if(shadowed) s->setvariant(cur.visibledynlights, 1, slot, vslot);
+    else if(!cur.visibledynlights) s->set(slot, vslot);
+    else s->setvariant(cur.visibledynlights-1, 0, slot, vslot);
+}
+
+static void changetexgen(renderstate &cur, int dim, Slot &slot, VSlot &vslot)
+{
+    if(cur.texgenslot != &slot || cur.texgenvslot != &vslot)
+    {
+        Texture *curtex = !cur.texgenslot || cur.texgenslot->sts.empty() ? notexture : cur.texgenslot->sts[0].t,
+                *tex = slot.sts.empty() ? notexture : slot.sts[0].t;
+        if(!cur.texgenvslot || slot.sts.empty() ||
+            (curtex->xs != tex->xs || curtex->ys != tex->ys ||
+             cur.texgenvslot->rotation != vslot.rotation || cur.texgenvslot->scale != vslot.scale ||
+             cur.texgenvslot->offset != vslot.offset || cur.texgenvslot->scroll != vslot.scroll))
+        {
+            const texrotation &r = texrotations[vslot.rotation];
+            float xs = r.flipx ? -tex->xs : tex->xs,
+                  ys = r.flipy ? -tex->ys : tex->ys;
+            vec2 scroll(vslot.scroll);
+            if(r.swapxy) swap(scroll.x, scroll.y);
+            scroll.x *= lastmillis*tex->xs/xs;
+            scroll.y *= lastmillis*tex->ys/ys;
+            if(cur.texgenscroll != scroll)
+            {
+                cur.texgenscroll = scroll;
+                cur.texgendim = -1;
+            }
+        }
+        cur.texgenslot = &slot;
+        cur.texgenvslot = &vslot;
+    }
+
+    if(cur.texgendim == dim) return;
+    GLOBALPARAM(texgenscroll, cur.texgenscroll);
+    cur.texgendim = dim;
+}
+
+static void renderbatch(renderstate &cur, int pass, geombatch &b)
+{
+    geombatch *shadowed = NULL;
+    int rendered = -1;
+    for(geombatch *curbatch = &b;; curbatch = &geombatches[curbatch->batch])
+    {
+        ushort len = curbatch->es.length[curbatch->va->shadowed ? 0 : 1];
+        if(len) 
+        {
+            if(rendered < 0)
+            {
+                changeshader(cur, b.vslot.slot->shader, *b.vslot.slot, b.vslot, false);
+                rendered = 0;
+                gbatches++;
+            }
+            ushort minvert = curbatch->es.minvert[0], maxvert = curbatch->es.maxvert[0];
+            if(!curbatch->va->shadowed) { minvert = min(minvert, curbatch->es.minvert[1]); maxvert = max(maxvert, curbatch->es.maxvert[1]); } 
+            drawtris(len, curbatch->edata, minvert, maxvert); 
+            vtris += len/3;
+        }
+        if(curbatch->es.length[1] > len && !shadowed) shadowed = curbatch;
+        if(curbatch->batch < 0) break;
+    }
+    if(shadowed) for(geombatch *curbatch = shadowed;; curbatch = &geombatches[curbatch->batch])
+    {
+        if(curbatch->va->shadowed && curbatch->es.length[1] > curbatch->es.length[0])
+        {
+            if(rendered < 1)
+            {
+                changeshader(cur, b.vslot.slot->shader, *b.vslot.slot, b.vslot, true);
+                rendered = 1;
+                gbatches++;
+            }
+            ushort len = curbatch->es.length[1] - curbatch->es.length[0];
+            drawtris(len, curbatch->edata + curbatch->es.length[0], curbatch->es.minvert[1], curbatch->es.maxvert[1]);
+            vtris += len/3;
+        }
+        if(curbatch->batch < 0) break;
+    }
+}
+
+static void resetbatches()
+{
+    geombatches.setsize(0);
+    firstbatch = -1;
+    numbatches = 0;
+}
+
+static void renderbatches(renderstate &cur, int pass)
+{
+    cur.slot = NULL;
+    cur.vslot = NULL;
+    int curbatch = firstbatch;
+    if(curbatch >= 0)
+    {
+        if(cur.alphaing)
+        {
+            if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); }
+        }
+        else if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+        if(!cur.colormask) { cur.colormask = true; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, cur.alphaing ? GL_FALSE : GL_TRUE); }
+        if(!cur.vattribs)
+        {
+            if(cur.vquery) disablevquery(cur);
+            enablevattribs(cur);
+        }
+    }        
+    while(curbatch >= 0)
+    {
+        geombatch &b = geombatches[curbatch];
+        curbatch = b.next;
+
+        if(cur.vbuf != b.va->vbuf) changevbuf(cur, pass, b.va);
+        if(cur.vslot != &b.vslot) 
+        {
+            changeslottmus(cur, pass, *b.vslot.slot, b.vslot);
+            if(cur.texgendim != b.es.dim || (cur.texgendim <= 2 && cur.texgenvslot != &b.vslot)) changetexgen(cur, b.es.dim, *b.vslot.slot, b.vslot);
+        }
+        else if(cur.texgendim != b.es.dim) changetexgen(cur, b.es.dim, *b.vslot.slot, b.vslot);
+        if(pass == RENDERPASS_LIGHTMAP) changebatchtmus(cur, pass, b);
+
+        renderbatch(cur, pass, b);
+    }
+
+    resetbatches();
+}
+
+void renderzpass(renderstate &cur, vtxarray *va)
+{
+    if(!cur.vattribs)
+    {   
+        if(cur.vquery) disablevquery(cur);
+        enablevattribs(cur, false);
+    }
+    if(cur.vbuf!=va->vbuf) changevbuf(cur, RENDERPASS_Z, va);
+    if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+    if(cur.colormask) { cur.colormask = false; glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); }
+    int firsttex = 0, numtris = va->tris;
+    ushort *edata = va->edata;
+    if(cur.alphaing)
+    {
+        firsttex += va->texs + va->blends;
+        edata += 3*(va->tris + va->blendtris);
+        numtris = va->alphatris;
+        xtravertsva += 3*numtris;
+    }
+    else xtravertsva += va->verts;
+    nocolorshader->set();
+    drawvatris(va, 3*numtris, edata);
+}
+
+vector<vtxarray *> foggedvas;
+
+#define startvaquery(va, flush) \
+    do { \
+        if(va->query) \
+        { \
+            flush; \
+            startquery(va->query); \
+        } \
+    } while(0)
+
+
+#define endvaquery(va, flush) \
+    do { \
+        if(va->query) \
+        { \
+            flush; \
+            endquery(va->query); \
+        } \
+    } while(0)
+
+void renderfoggedvas(renderstate &cur, bool doquery = false)
+{
+    static Shader *fogshader = NULL;
+    if(!fogshader) fogshader = lookupshaderbyname("fogworld");
+    if(fading) fogshader->setvariant(0, 2);
+    else fogshader->set();
+
+    if(!cur.vattribs) enablevattribs(cur, false);
+
+    loopv(foggedvas)
+    {
+        vtxarray *va = foggedvas[i];
+        if(cur.vbuf!=va->vbuf) changevbuf(cur, RENDERPASS_FOG, va);
+
+        if(doquery) startvaquery(va, );
+        drawvatris(va, 3*va->tris, va->edata);
+        vtris += va->tris;
+        if(doquery) endvaquery(va, );
+    }
+
+    foggedvas.setsize(0);
+}
+
+VAR(batchgeom, 0, 1, 1);
+
+void renderva(renderstate &cur, vtxarray *va, int pass = RENDERPASS_LIGHTMAP, bool fogpass = false, bool doquery = false)
+{
+    switch(pass)
+    {
+        case RENDERPASS_LIGHTMAP:
+            if(!cur.alphaing) vverts += va->verts;
+            va->shadowed = false;
+            va->dynlightmask = 0;
+            if(fogpass ? va->geommax.z<=reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED)
+            {
+                if(!cur.alphaing && !cur.blending) foggedvas.add(va);
+                break;
+            }
+            if(!drawtex && !glaring && !cur.alphaing)
+            {
+                va->shadowed = isshadowmapreceiver(va);
+                calcdynlightmask(va);
+            }
+            if(doquery) startvaquery(va, { if(geombatches.length()) renderbatches(cur, pass); });
+            mergetexs(cur, va);
+            if(doquery) endvaquery(va, { if(geombatches.length()) renderbatches(cur, pass); });
+            else if(!batchgeom && geombatches.length()) renderbatches(cur, pass);
+            break;
+
+        case RENDERPASS_LIGHTMAP_BLEND:
+        {
+            if(doquery) startvaquery(va, { if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); });
+            mergetexs(cur, va, &va->eslist[va->texs], va->blends, va->edata + 3*va->tris);
+            if(doquery) endvaquery(va, { if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP); });
+            else if(!batchgeom && geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+            break;
+        }
+
+        case RENDERPASS_FOG:
+            if(cur.vbuf!=va->vbuf) changevbuf(cur, pass, va);
+            drawvatris(va, 3*va->tris, va->edata);
+            xtravertsva += va->verts;
+            break;
+
+        case RENDERPASS_CAUSTICS:
+            if(cur.vbuf!=va->vbuf) changevbuf(cur, pass, va);
+            drawvatris(va, 3*va->tris, va->edata);
+            xtravertsva += va->verts;
+            break;
+        case RENDERPASS_Z:
+            if(doquery) startvaquery(va, );
+            renderzpass(cur, va);
+            if(doquery) endvaquery(va, );
+            break;
+    }
+}
+
+#define NUMCAUSTICS 32
+
+static Texture *caustictex[NUMCAUSTICS] = { NULL };
+
+void loadcaustics(bool force)
+{
+    static bool needcaustics = false;
+    if(force) needcaustics = true;
+    if(!caustics || !needcaustics) return;
+    useshaderbyname("caustic");
+    if(caustictex[0]) return;
+    loopi(NUMCAUSTICS)
+    {
+        defformatstring(name, "<grey><noswizzle>packages/caustics/caust%.2d.png", i);
+        caustictex[i] = textureload(name);
+    }
+}
+
+void cleanupva()
+{
+    clearvas(worldroot);
+    clearqueries();
+    cleanupbb();
+    cleanupgrass();
+    loopi(NUMCAUSTICS) caustictex[i] = NULL;
+}
+
+VARR(causticscale, 0, 50, 10000);
+VARR(causticmillis, 0, 75, 1000);
+FVARR(causticcontrast, 0, 0.6f, 1);
+VARFP(caustics, 0, 1, 1, loadcaustics());
+
+void setupcaustics(float blend)
+{
+    if(!caustictex[0]) loadcaustics(true);
+
+    vec s = vec(0.011f, 0, 0.0066f).mul(100.0f/causticscale), t = vec(0, 0.011f, 0.0066f).mul(100.0f/causticscale);
+    int tex = (lastmillis/causticmillis)%NUMCAUSTICS;
+    float frac = float(lastmillis%causticmillis)/causticmillis;
+    loopi(2)
+    {
+        glActiveTexture_(GL_TEXTURE0+i);
+        glBindTexture(GL_TEXTURE_2D, caustictex[(tex+i)%NUMCAUSTICS]->id);
+    }
+    glActiveTexture_(GL_TEXTURE0);
+    SETSHADER(caustic);
+    LOCALPARAM(texgenS, s);
+    LOCALPARAM(texgenT, t);
+    blend *= causticcontrast;
+    LOCALPARAMF(frameblend, blend*(1-frac), blend*frac, blend, 1 - blend);
+}
+
+void setupgeom(renderstate &cur)
+{
+    GLOBALPARAMF(colorparams, 2, 2, 2, 1);
+    GLOBALPARAM(camera, camera1->o);
+    GLOBALPARAMF(ambient, ambientcolor.x/255.0f, ambientcolor.y/255.0f, ambientcolor.z/255.0f);
+    GLOBALPARAMF(millis, lastmillis/1000.0f);
+
+    glActiveTexture_(GL_TEXTURE0);
+}
+
+void cleanupgeom(renderstate &cur)
+{
+    if(cur.vattribs) disablevattribs(cur);
+    if(cur.vbuf) disablevbuf(cur);
+}
+
+#define FIRSTVA (reflecting ? reflectedva : visibleva)
+#define NEXTVA (reflecting ? va->rnext : va->next)
+
+static void rendergeommultipass(renderstate &cur, int pass, bool fogpass)
+{
+    if(cur.vbuf) disablevbuf(cur);
+    if(!cur.vattribs) enablevattribs(cur, false);
+    cur.texgendim = -1;
+    for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+    {
+        if(!va->texs) continue;
+        if(refracting)
+        {    
+            if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_GEOM) continue;
+            if(ishiddencube(va->o, va->size)) continue;
+        }
+        else if(reflecting)
+        {
+            if(va->geommax.z <= reflectz) continue;
+        }
+        else if(va->occluded >= OCCLUDE_GEOM) continue;
+        if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue;
+        renderva(cur, va, pass, fogpass);
+    }
+    if(geombatches.length()) renderbatches(cur, pass);
+}
+
+VAR(oqgeom, 0, 1, 1);
+
+void rendergeom(float causticspass, bool fogpass)
+{
+    if(causticspass && (!causticscale || !causticmillis)) causticspass = 0;
+
+    bool mainpass = !reflecting && !refracting && !drawtex && !glaring,
+         doOQ = oqfrags && oqgeom && mainpass,
+         doZP = doOQ && zpass,
+         doSM = shadowmap && !drawtex && !glaring;
+    renderstate cur;
+    if(mainpass)
+    {
+        flipqueries();
+        vtris = vverts = 0;
+    }
+    if(!doZP) 
+    {
+        if(shadowmap && mainpass) rendershadowmap();
+        setupgeom(cur);
+        if(doSM) pushshadowmap();
+    }
+
+    finddynlights();
+
+    resetbatches();
+
+    int blends = 0;
+    for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+    {
+        if(!va->texs) continue;
+        if(refracting)
+        {
+            if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_GEOM) continue;
+            if(ishiddencube(va->o, va->size)) continue;
+        }
+        else if(reflecting)
+        {
+            if(va->geommax.z <= reflectz) continue;
+        }
+        else if(doOQ && (zpass || va->distance > oqdist) && !insideva(va, camera1->o))
+        {
+            if(va->parent && va->parent->occluded >= OCCLUDE_BB)
+            {
+                va->query = NULL;
+                va->occluded = OCCLUDE_PARENT;
+                continue;
+            }
+            va->occluded = va->query && va->query->owner == va && checkquery(va->query) ? min(va->occluded+1, int(OCCLUDE_BB)) : OCCLUDE_NOTHING;
+            va->query = newquery(va);
+            if((!va->query && zpass) || !va->occluded)
+                va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+            if(va->occluded >= OCCLUDE_GEOM)
+            {
+                if(va->query) 
+                {
+                    if(!zpass && geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+                    if(cur.vattribs) disablevattribs(cur, !doZP);
+                    if(cur.vbuf) disablevbuf(cur);
+                    renderquery(cur, va->query, va);
+                }
+                continue;
+            }
+        }
+        else
+        {
+            va->query = NULL;
+            va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+            if(va->occluded >= OCCLUDE_GEOM) continue;
+        }
+
+        if(!doZP) blends += va->blends;
+        renderva(cur, va, doZP ? RENDERPASS_Z : RENDERPASS_LIGHTMAP, fogpass, doOQ);
+    }
+
+    if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+
+    if(cur.vquery) disablevquery(cur);
+    if(cur.vattribs) disablevattribs(cur, !doZP);
+    if(cur.vbuf) disablevbuf(cur);
+
+    if(!cur.colormask) { cur.colormask = true; glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); }
+    if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+   
+    bool multipassing = false;
+
+    if(doZP)
+    {
+               glFlush();
+
+        if(shadowmap && mainpass) rendershadowmap();
+        setupgeom(cur);
+        if(doSM) pushshadowmap();
+
+        if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); }
+        cur.texgendim = -1;
+
+        for(vtxarray *va = visibleva; va; va = va->next)
+        {
+            if(!va->texs || va->occluded >= OCCLUDE_GEOM) continue;
+            blends += va->blends;
+            renderva(cur, va, RENDERPASS_LIGHTMAP, fogpass);
+        }
+        if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+        for(vtxarray *va = visibleva; va; va = va->next)
+        {
+            if(!va->texs || va->occluded < OCCLUDE_GEOM) continue;
+            else if((va->parent && va->parent->occluded >= OCCLUDE_BB) ||
+                    (va->query && checkquery(va->query)))
+            {
+                va->occluded = OCCLUDE_BB;
+                continue;
+            }
+            else
+            {
+                va->occluded = pvsoccluded(va->geommin, va->geommax) ? OCCLUDE_GEOM : OCCLUDE_NOTHING;
+                if(va->occluded >= OCCLUDE_GEOM) continue;
+            }
+
+            blends += va->blends;
+            renderva(cur, va, RENDERPASS_LIGHTMAP, fogpass);
+        }
+        if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+    }
+
+    if(blends)
+    {
+        if(cur.vbuf) disablevbuf(cur);
+
+        if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); }
+        glDepthMask(GL_FALSE);
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+
+        cur.texgendim = -1;
+        cur.blending = true;
+        for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+        {
+            if(!va->blends) continue;
+            if(refracting)
+            {
+                if(refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) continue;
+                if(ishiddencube(va->o, va->size)) continue;
+                if(va->occluded >= OCCLUDE_GEOM) continue;
+            }
+            else if(reflecting)
+            {
+                if(va->geommax.z <= reflectz) continue;
+            }
+            else if(va->occluded >= OCCLUDE_GEOM) continue;
+            if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue;
+            renderva(cur, va, RENDERPASS_LIGHTMAP_BLEND, fogpass);
+        }
+        if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+        cur.blending = false;
+
+        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+        glDisable(GL_BLEND);
+        glDepthMask(GL_TRUE);
+    }
+
+    if(doSM) popshadowmap();
+
+    if(cur.vattribs) disablevattribs(cur);
+
+    if(foggedvas.length()) renderfoggedvas(cur, doOQ && !zpass);
+
+    if(causticspass)
+    {
+        if(!multipassing) { multipassing = true; glDepthFunc(GL_LEQUAL); }
+        glDepthMask(GL_FALSE);
+        glEnable(GL_BLEND);
+
+        setupcaustics(causticspass);
+        glBlendFunc(GL_ZERO, GL_SRC_COLOR);
+        if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+        rendergeommultipass(cur, RENDERPASS_CAUSTICS, fogpass);
+        if(fading) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+
+        glDisable(GL_BLEND);
+        glDepthMask(GL_TRUE);
+    }
+
+    if(multipassing) glDepthFunc(GL_LESS);
+
+    cleanupgeom(cur);
+}
+
+void renderalphageom(bool fogpass)
+{
+    static vector<vtxarray *> alphavas;
+    alphavas.setsize(0);
+    bool hasback = false;
+    for(vtxarray *va = FIRSTVA; va; va = NEXTVA)
+    {
+        if(!va->alphatris) continue;
+        if(refracting)
+        {
+            if((refracting < 0 ? va->geommin.z > reflectz : va->geommax.z <= reflectz) || va->occluded >= OCCLUDE_BB) continue;
+            if(ishiddencube(va->o, va->size)) continue;
+            if(va->occluded >= OCCLUDE_GEOM && pvsoccluded(va->geommin, va->geommax)) continue;
+        }
+        else if(reflecting)
+        {
+            if(va->geommax.z <= reflectz) continue;
+        }
+        else 
+        {
+            if(va->occluded >= OCCLUDE_BB) continue;
+            if(va->occluded >= OCCLUDE_GEOM && pvsoccluded(va->geommin, va->geommax)) continue;
+        }
+        if(fogpass ? va->geommax.z <= reflectz-refractfog || !refractfog : va->curvfc==VFC_FOGGED) continue;
+        alphavas.add(va);
+        if(va->alphabacktris) hasback = true;
+    }
+    if(alphavas.empty()) return;
+
+    resetbatches();
+
+    renderstate cur;
+    cur.alphaing = 1;
+
+    loop(front, 2) if(front || hasback)
+    {
+        cur.alphaing = front+1;
+        if(!front) glCullFace(GL_FRONT);
+        cur.vbuf = 0;
+        cur.texgendim = -1;
+        loopv(alphavas) renderva(cur, alphavas[i], RENDERPASS_Z);
+        if(cur.depthmask) { cur.depthmask = false; glDepthMask(GL_FALSE); }
+        cur.colormask = true;
+        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
+   
+        if(cur.vattribs) disablevattribs(cur, false);
+        if(cur.vbuf) disablevbuf(cur);
+
+        setupgeom(cur);
+
+        glDepthFunc(GL_LEQUAL);
+        glEnable(GL_BLEND);
+        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+        cur.vbuf = 0;
+        cur.texgendim = -1;
+        cur.colorscale = vec(1, 1, 1);
+        cur.alphascale = -1;
+        loopv(alphavas) if(front || alphavas[i]->alphabacktris) renderva(cur, alphavas[i], RENDERPASS_LIGHTMAP, fogpass);
+        if(geombatches.length()) renderbatches(cur, RENDERPASS_LIGHTMAP);
+
+        cleanupgeom(cur);
+
+        resetfogcolor();
+        if(!cur.depthmask) { cur.depthmask = true; glDepthMask(GL_TRUE); }
+        glDisable(GL_BLEND);
+        glDepthFunc(GL_LESS);
+        if(!front) glCullFace(GL_BACK);
+    }
+
+    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, fading ? GL_FALSE : GL_TRUE);
+}
+void findreflectedvas(vector<vtxarray *> &vas, int prevvfc = VFC_PART_VISIBLE)
+{
+    loopv(vas)
+    {
+        vtxarray *va = vas[i];
+        if(prevvfc >= VFC_NOT_VISIBLE) va->curvfc = prevvfc;
+        if(va->curvfc == VFC_FOGGED || va->curvfc == PVS_FOGGED || va->o.z+va->size <= reflectz || isfoggedcube(va->o, va->size)) continue;
+        bool render = true;
+        if(va->curvfc == VFC_FULL_VISIBLE)
+        {
+            if(va->occluded >= OCCLUDE_BB) continue;
+            if(va->occluded >= OCCLUDE_GEOM) render = false;
+        }
+        else if(va->curvfc == PVS_FULL_VISIBLE) continue;
+        if(render)
+        {
+            if(va->curvfc >= VFC_NOT_VISIBLE) va->distance = (int)vadist(va, camera1->o);
+            vtxarray **vprev = &reflectedva, *vcur = reflectedva;
+            while(vcur && va->distance > vcur->distance)
+            {
+                vprev = &vcur->rnext;
+                vcur = vcur->rnext;
+            }
+            va->rnext = *vprev;
+            *vprev = va;
+        }
+        if(va->children.length()) findreflectedvas(va->children, va->curvfc);
+    }
+}
+
+void renderreflectedgeom(bool causticspass, bool fogpass)
+{
+    if(reflecting)
+    {
+        reflectedva = NULL;
+        findreflectedvas(varoot);
+        rendergeom(causticspass ? 1 : 0, fogpass);
+    }
+    else rendergeom(causticspass ? 1 : 0, fogpass);
+}                
+
+static vtxarray *prevskyva = NULL;
+
+void renderskyva(vtxarray *va, bool explicitonly = false)
+{
+    if(!prevskyva || va->vbuf != prevskyva->vbuf)
+    {
+        gle::bindvbo(va->vbuf);
+        gle::bindebo(va->skybuf);
+        const vertex *ptr = 0;
+        gle::vertexpointer(sizeof(vertex), ptr->pos.v);
+        if(!prevskyva) gle::enablevertex();
+    }
+
+    drawvatris(va, explicitonly ? va->explicitsky : va->sky+va->explicitsky, explicitonly ? va->skydata+va->sky : va->skydata);
+
+    if(!explicitonly) xtraverts += va->sky/3;
+    xtraverts += va->explicitsky/3;
+
+    prevskyva = va;
+}
+
+int renderedsky = 0, renderedexplicitsky = 0, renderedskyfaces = 0, renderedskyclip = INT_MAX;
+
+static inline void updateskystats(vtxarray *va)
+{
+    renderedsky += va->sky;
+    renderedexplicitsky += va->explicitsky;
+    renderedskyfaces |= va->skyfaces&0x3F;
+    if(!(va->skyfaces&0x1F) || camera1->o.z < va->skyclip) renderedskyclip = min(renderedskyclip, va->skyclip);
+    else renderedskyclip = 0;
+}
+
+void renderreflectedskyvas(vector<vtxarray *> &vas, int prevvfc = VFC_PART_VISIBLE)
+{
+    loopv(vas)
+    {
+        vtxarray *va = vas[i];
+        if(prevvfc >= VFC_NOT_VISIBLE) va->curvfc = prevvfc;
+        if((va->curvfc == VFC_FULL_VISIBLE && va->occluded >= OCCLUDE_BB) || va->curvfc==PVS_FULL_VISIBLE) continue;
+        if(va->o.z+va->size <= reflectz || ishiddencube(va->o, va->size)) continue;
+        if(va->sky+va->explicitsky) 
+        {
+            updateskystats(va);
+            renderskyva(va);
+        }
+        if(va->children.length()) renderreflectedskyvas(va->children, va->curvfc);
+    }
+}
+
+bool rendersky(bool explicitonly)
+{
+    prevskyva = NULL;
+    renderedsky = renderedexplicitsky = renderedskyfaces = 0;
+    renderedskyclip = INT_MAX;
+
+    if(reflecting)
+    {
+        renderreflectedskyvas(varoot);
+    }
+    else for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if((va->occluded >= OCCLUDE_BB && va->skyfaces&0x80) || !(va->sky+va->explicitsky)) continue;
+
+        // count possibly visible sky even if not actually rendered
+        updateskystats(va);
+        if(explicitonly && !va->explicitsky) continue;
+        renderskyva(va, explicitonly);
+    }
+
+    if(prevskyva)
+    {
+        gle::disablevertex(); 
+        gle::clearvbo();
+        gle::clearebo();
+    }
+
+    return renderedsky+renderedexplicitsky > 0;
+}
+
diff --git a/src/engine/server.cpp b/src/engine/server.cpp
new file mode 100644 (file)
index 0000000..e76c67d
--- /dev/null
@@ -0,0 +1,1154 @@
+// server.cpp: little more than enhanced multicaster
+// runs dedicated or as client coroutine
+
+#include "engine.h"
+
+#define LOGSTRLEN 512
+
+static FILE *logfile = NULL;
+
+void closelogfile()
+{
+    if(logfile)
+    {
+        fclose(logfile);
+        logfile = NULL;
+    }
+}
+
+FILE *getlogfile()
+{
+#ifdef WIN32
+    return logfile;
+#else
+    return logfile ? logfile : stdout;
+#endif
+}
+
+void setlogfile(const char *fname)
+{
+    closelogfile();
+    if(fname && fname[0])
+    {
+        fname = findfile(fname, "w");
+        if(fname) logfile = fopen(fname, "w");
+    }
+    FILE *f = getlogfile();
+    if(f) setvbuf(f, NULL, _IOLBF, BUFSIZ);
+}
+
+void logoutf(const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    logoutfv(fmt, args);
+    va_end(args);
+}
+
+
+static void writelog(FILE *file, const char *buf)
+{
+    static uchar ubuf[512];
+    size_t len = strlen(buf), carry = 0;
+    while(carry < len)
+    {
+        size_t numu = encodeutf8(ubuf, sizeof(ubuf)-1, &((const uchar *)buf)[carry], len - carry, &carry);
+        if(carry >= len) ubuf[numu++] = '\n';
+        fwrite(ubuf, 1, numu, file);
+    }
+}
+
+static void writelogv(FILE *file, const char *fmt, va_list args)
+{
+    static char buf[LOGSTRLEN];
+    vformatstring(buf, fmt, args, sizeof(buf));
+    writelog(file, buf);
+}
+#ifdef STANDALONE
+void fatal(const char *fmt, ...) 
+{ 
+    void cleanupserver();
+    cleanupserver(); 
+       defvformatstring(msg,fmt,fmt);
+       if(logfile) logoutf("%s", msg);
+#ifdef WIN32
+       MessageBox(NULL, msg, "Cube 2: Sauerbraten fatal error", MB_OK|MB_SYSTEMMODAL);
+#else
+    fprintf(stderr, "server error: %s\n", msg);
+#endif
+    closelogfile();
+    exit(EXIT_FAILURE); 
+}
+
+void conoutfv(int type, const char *fmt, va_list args)
+{
+    logoutfv(fmt, args);
+}
+#endif
+
+#define DEFAULTCLIENTS 8
+
+enum { ST_EMPTY, ST_LOCAL, ST_TCPIP };
+
+struct client                   // server side version of "dynent" type
+{
+    int type;
+    int num;
+    ENetPeer *peer;
+    string hostname;
+    void *info;
+};
+
+vector<client *> clients;
+
+ENetHost *serverhost = NULL;
+int laststatus = 0; 
+ENetSocket pongsock = ENET_SOCKET_NULL, lansock = ENET_SOCKET_NULL;
+
+int localclients = 0, nonlocalclients = 0;
+
+bool hasnonlocalclients() { return nonlocalclients!=0; }
+bool haslocalclients() { return localclients!=0; }
+
+client &addclient(int type)
+{
+    client *c = NULL;
+    loopv(clients) if(clients[i]->type==ST_EMPTY)
+    {
+        c = clients[i];
+        break;
+    }
+    if(!c)
+    {
+        c = new client;
+        c->num = clients.length();
+        clients.add(c);
+    }
+    c->info = server::newclientinfo();
+    c->type = type;
+    switch(type)
+    {
+        case ST_TCPIP: nonlocalclients++; break;
+        case ST_LOCAL: localclients++; break;
+    }
+    return *c;
+}
+
+void delclient(client *c)
+{
+    if(!c) return;
+    switch(c->type)
+    {
+        case ST_TCPIP: nonlocalclients--; if(c->peer) c->peer->data = NULL; break;
+        case ST_LOCAL: localclients--; break;
+        case ST_EMPTY: return;
+    }
+    c->type = ST_EMPTY;
+    c->peer = NULL;
+    if(c->info)
+    {
+        server::deleteclientinfo(c->info);
+        c->info = NULL;
+    }
+}
+
+void cleanupserver()
+{
+    if(serverhost) enet_host_destroy(serverhost);
+    serverhost = NULL;
+
+    if(pongsock != ENET_SOCKET_NULL) enet_socket_destroy(pongsock);
+    if(lansock != ENET_SOCKET_NULL) enet_socket_destroy(lansock);
+    pongsock = lansock = ENET_SOCKET_NULL;
+}
+
+VARF(maxclients, 0, DEFAULTCLIENTS, MAXCLIENTS, { if(!maxclients) maxclients = DEFAULTCLIENTS; });
+VARF(maxdupclients, 0, 0, MAXCLIENTS, { if(serverhost) serverhost->duplicatePeers = maxdupclients ? maxdupclients : MAXCLIENTS; });
+
+void process(ENetPacket *packet, int sender, int chan);
+//void disconnect_client(int n, int reason);
+
+int getservermtu() { return serverhost ? serverhost->mtu : -1; }
+void *getclientinfo(int i) { return !clients.inrange(i) || clients[i]->type==ST_EMPTY ? NULL : clients[i]->info; }
+ENetPeer *getclientpeer(int i) { return clients.inrange(i) && clients[i]->type==ST_TCPIP ? clients[i]->peer : NULL; }
+int getnumclients()        { return clients.length(); }
+uint getclientip(int n)    { return clients.inrange(n) && clients[n]->type==ST_TCPIP ? clients[n]->peer->address.host : 0; }
+
+void sendpacket(int n, int chan, ENetPacket *packet, int exclude)
+{
+    if(n<0)
+    {
+        server::recordpacket(chan, packet->data, packet->dataLength);
+        loopv(clients) if(i!=exclude && server::allowbroadcast(i)) sendpacket(i, chan, packet);
+        return;
+    }
+    switch(clients[n]->type)
+    {
+        case ST_TCPIP:
+        {
+            enet_peer_send(clients[n]->peer, chan, packet);
+            break;
+        }
+
+#ifndef STANDALONE
+        case ST_LOCAL:
+            localservertoclient(chan, packet);
+            break;
+#endif
+    }
+}
+
+ENetPacket *sendf(int cn, int chan, const char *format, ...)
+{
+    int exclude = -1;
+    bool reliable = false;
+    if(*format=='r') { reliable = true; ++format; }
+    packetbuf p(MAXTRANS, reliable ? ENET_PACKET_FLAG_RELIABLE : 0);
+    va_list args;
+    va_start(args, format);
+    while(*format) switch(*format++)
+    {
+        case 'x':
+            exclude = va_arg(args, int);
+            break;
+
+        case 'v':
+        {
+            int n = va_arg(args, int);
+            int *v = va_arg(args, int *);
+            loopi(n) putint(p, v[i]);
+            break;
+        }
+
+        case 'i': 
+        {
+            int n = isdigit(*format) ? *format++-'0' : 1;
+            loopi(n) putint(p, va_arg(args, int));
+            break;
+        }
+        case 'f':
+        {
+            int n = isdigit(*format) ? *format++-'0' : 1;
+            loopi(n) putfloat(p, (float)va_arg(args, double));
+            break;
+        }
+        case 's': sendstring(va_arg(args, const char *), p); break;
+        case 'm':
+        {
+            int n = va_arg(args, int);
+            p.put(va_arg(args, uchar *), n);
+            break;
+        }
+    }
+    va_end(args);
+    ENetPacket *packet = p.finalize();
+    sendpacket(cn, chan, packet, exclude);
+    return packet->referenceCount > 0 ? packet : NULL;
+}
+
+ENetPacket *sendfile(int cn, int chan, stream *file, const char *format, ...)
+{
+    if(cn < 0)
+    {
+#ifdef STANDALONE
+        return NULL;
+#endif
+    }
+    else if(!clients.inrange(cn)) return NULL;
+
+    int len = (int)min(file->size(), stream::offset(INT_MAX));
+    if(len <= 0 || len > 16<<20) return NULL;
+
+    packetbuf p(MAXTRANS+len, ENET_PACKET_FLAG_RELIABLE);
+    va_list args;
+    va_start(args, format);
+    while(*format) switch(*format++)
+    {
+        case 'i':
+        {
+            int n = isdigit(*format) ? *format++-'0' : 1;
+            loopi(n) putint(p, va_arg(args, int));
+            break;
+        }
+        case 's': sendstring(va_arg(args, const char *), p); break;
+        case 'l': putint(p, len); break;
+    }
+    va_end(args);
+
+    file->seek(0, SEEK_SET);
+    file->read(p.subbuf(len).buf, len);
+
+    ENetPacket *packet = p.finalize();
+    if(cn >= 0) sendpacket(cn, chan, packet, -1);
+#ifndef STANDALONE
+    else sendclientpacket(packet, chan);
+#endif
+    return packet->referenceCount > 0 ? packet : NULL;
+}
+
+const char *disconnectreason(int reason)
+{
+    switch(reason)
+    {
+        case DISC_EOP: return "end of packet";
+        case DISC_LOCAL: return "server is in local mode";
+        case DISC_KICK: return "kicked/banned";
+        case DISC_MSGERR: return "message error";
+        case DISC_IPBAN: return "ip is banned";
+        case DISC_PRIVATE: return "server is in private mode";
+        case DISC_MAXCLIENTS: return "server FULL";
+        case DISC_TIMEOUT: return "connection timed out";
+        case DISC_OVERFLOW: return "overflow";
+        case DISC_PASSWORD: return "invalid password";
+        default: return NULL;
+    }
+}
+
+void disconnect_client(int n, int reason)
+{
+    if(!clients.inrange(n) || clients[n]->type!=ST_TCPIP) return;
+    enet_peer_disconnect(clients[n]->peer, reason);
+    server::clientdisconnect(n);
+    delclient(clients[n]);
+    const char *msg = disconnectreason(reason);
+    string s;
+    if(msg) formatstring(s, "client (%s) disconnected because: %s", clients[n]->hostname, msg);
+    else formatstring(s, "client (%s) disconnected", clients[n]->hostname);
+    logoutf("%s", s);
+    server::sendservmsg(s);
+}
+
+void kicknonlocalclients(int reason)
+{
+    loopv(clients) if(clients[i]->type==ST_TCPIP) disconnect_client(i, reason);
+}
+
+void process(ENetPacket *packet, int sender, int chan)   // sender may be -1
+{
+    packetbuf p(packet);
+    server::parsepacket(sender, chan, p);
+    if(p.overread()) { disconnect_client(sender, DISC_EOP); return; }
+}
+
+void localclienttoserver(int chan, ENetPacket *packet)
+{
+    client *c = NULL;
+    loopv(clients) if(clients[i]->type==ST_LOCAL) { c = clients[i]; break; }
+    if(c) process(packet, c->num, chan);
+}
+
+#ifdef STANDALONE
+bool resolverwait(const char *name, ENetAddress *address)
+{
+    return enet_address_set_host(address, name) >= 0;
+}
+
+int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &remoteaddress)
+{
+    return enet_socket_connect(sock, &remoteaddress);
+}
+#endif
+
+ENetSocket mastersock = ENET_SOCKET_NULL;
+ENetAddress masteraddress = { ENET_HOST_ANY, ENET_PORT_ANY }, serveraddress = { ENET_HOST_ANY, ENET_PORT_ANY };
+int lastupdatemaster = 0, lastconnectmaster = 0, masterconnecting = 0, masterconnected = 0;
+vector<char> masterout, masterin;
+int masteroutpos = 0, masterinpos = 0;
+VARN(updatemaster, allowupdatemaster, 0, 1, 1);
+
+void disconnectmaster()
+{
+    if(mastersock != ENET_SOCKET_NULL) 
+    {
+        server::masterdisconnected();
+        enet_socket_destroy(mastersock);
+        mastersock = ENET_SOCKET_NULL;
+    }
+
+    masterout.setsize(0);
+    masterin.setsize(0);
+    masteroutpos = masterinpos = 0;
+
+    masteraddress.host = ENET_HOST_ANY;
+    masteraddress.port = ENET_PORT_ANY;
+
+    lastupdatemaster = masterconnecting = masterconnected = 0;
+}
+
+SVARF(mastername, server::defaultmaster(), disconnectmaster());
+VARF(masterport, 1, server::masterport(), 0xFFFF, disconnectmaster());
+
+ENetSocket connectmaster(bool wait)
+{
+    if(!mastername[0]) return ENET_SOCKET_NULL;
+    if(masteraddress.host == ENET_HOST_ANY)
+    {
+        if(isdedicatedserver()) logoutf("looking up %s...", mastername);
+        masteraddress.port = masterport;
+        if(!resolverwait(mastername, &masteraddress)) return ENET_SOCKET_NULL;
+    }
+    ENetSocket sock = enet_socket_create(ENET_SOCKET_TYPE_STREAM);
+    if(sock == ENET_SOCKET_NULL)
+    {
+        if(isdedicatedserver()) logoutf("could not open master server socket");
+        return ENET_SOCKET_NULL;
+    }
+    if(wait || serveraddress.host == ENET_HOST_ANY || !enet_socket_bind(sock, &serveraddress))
+    {
+        enet_socket_set_option(sock, ENET_SOCKOPT_NONBLOCK, 1);
+        if(wait)
+        {
+            if(!connectwithtimeout(sock, mastername, masteraddress)) return sock;
+        }
+        else if(!enet_socket_connect(sock, &masteraddress)) return sock;
+    }
+    enet_socket_destroy(sock);
+    if(isdedicatedserver()) logoutf("could not connect to master server");
+    return ENET_SOCKET_NULL;
+}
+
+bool requestmaster(const char *req)
+{
+    if(mastersock == ENET_SOCKET_NULL)
+    {
+        mastersock = connectmaster(false);
+        if(mastersock == ENET_SOCKET_NULL) return false;
+        lastconnectmaster = masterconnecting = totalmillis ? totalmillis : 1;
+    }
+
+    if(masterout.length() >= 4096) return false;
+
+    masterout.put(req, strlen(req));
+    return true;
+}
+
+bool requestmasterf(const char *fmt, ...)
+{
+    defvformatstring(req, fmt, fmt);
+    return requestmaster(req);
+}
+
+void processmasterinput()
+{
+    if(masterinpos >= masterin.length()) return;
+
+    char *input = &masterin[masterinpos], *end = (char *)memchr(input, '\n', masterin.length() - masterinpos);
+    while(end)
+    {
+        *end = '\0';
+
+        const char *args = input;
+        while(args < end && !iscubespace(*args)) args++;
+        int cmdlen = args - input;
+        while(args < end && iscubespace(*args)) args++;
+
+        if(matchstring(input, cmdlen, "failreg"))
+            conoutf(CON_ERROR, "master server registration failed: %s", args);
+        else if(matchstring(input, cmdlen, "succreg"))
+            conoutf("master server registration succeeded");
+        else server::processmasterinput(input, cmdlen, args);
+
+        end++;
+        masterinpos = end - masterin.getbuf();
+        input = end;
+        end = (char *)memchr(input, '\n', masterin.length() - masterinpos);
+    } 
+
+    if(masterinpos >= masterin.length())
+    {
+        masterin.setsize(0);
+        masterinpos = 0;
+    }
+}
+
+void flushmasteroutput()
+{
+    if(masterconnecting && totalmillis - masterconnecting >= 60000)
+    {
+        logoutf("could not connect to master server");
+        disconnectmaster();
+    }
+    if(masterout.empty() || !masterconnected) return;
+
+    ENetBuffer buf;
+    buf.data = &masterout[masteroutpos];
+    buf.dataLength = masterout.length() - masteroutpos;
+    int sent = enet_socket_send(mastersock, NULL, &buf, 1);
+    if(sent >= 0)
+    {
+        masteroutpos += sent;
+        if(masteroutpos >= masterout.length())
+        {
+            masterout.setsize(0);
+            masteroutpos = 0;
+        }
+    }
+    else disconnectmaster();
+}
+
+void flushmasterinput()
+{
+    if(masterin.length() >= masterin.capacity())
+        masterin.reserve(4096);
+
+    ENetBuffer buf;
+    buf.data = masterin.getbuf() + masterin.length();
+    buf.dataLength = masterin.capacity() - masterin.length();
+    int recv = enet_socket_receive(mastersock, NULL, &buf, 1);
+    if(recv > 0)
+    {
+        masterin.advance(recv);
+        processmasterinput();
+    }
+    else disconnectmaster();
+}
+
+static ENetAddress pongaddr;
+
+void sendserverinforeply(ucharbuf &p)
+{
+    ENetBuffer buf;
+    buf.data = p.buf;
+    buf.dataLength = p.length();
+    enet_socket_send(pongsock, &pongaddr, &buf, 1);
+}
+
+#define MAXPINGDATA 32
+
+void checkserversockets()        // reply all server info requests
+{
+    static ENetSocketSet readset, writeset;
+    ENET_SOCKETSET_EMPTY(readset);
+    ENET_SOCKETSET_EMPTY(writeset);
+    ENetSocket maxsock = pongsock;
+    ENET_SOCKETSET_ADD(readset, pongsock);
+    if(mastersock != ENET_SOCKET_NULL)
+    {
+        maxsock = max(maxsock, mastersock);
+        ENET_SOCKETSET_ADD(readset, mastersock);
+        if(!masterconnected) ENET_SOCKETSET_ADD(writeset, mastersock);
+    }
+    if(lansock != ENET_SOCKET_NULL)
+    {
+        maxsock = max(maxsock, lansock);
+        ENET_SOCKETSET_ADD(readset, lansock);
+    }
+    if(enet_socketset_select(maxsock, &readset, &writeset, 0) <= 0) return;
+
+    ENetBuffer buf;
+    uchar pong[MAXTRANS];
+    loopi(2)
+    {
+        ENetSocket sock = i ? lansock : pongsock;
+        if(sock == ENET_SOCKET_NULL || !ENET_SOCKETSET_CHECK(readset, sock)) continue;
+
+        buf.data = pong;
+        buf.dataLength = sizeof(pong);
+        int len = enet_socket_receive(sock, &pongaddr, &buf, 1);
+        if(len < 0 || len > MAXPINGDATA) continue;
+        ucharbuf req(pong, len), p(pong, sizeof(pong));
+        p.len += len;
+        server::serverinforeply(req, p);
+    }
+
+    if(mastersock != ENET_SOCKET_NULL)
+    {
+        if(!masterconnected)
+        {
+            if(ENET_SOCKETSET_CHECK(readset, mastersock) || ENET_SOCKETSET_CHECK(writeset, mastersock)) 
+            { 
+                int error = 0;
+                if(enet_socket_get_option(mastersock, ENET_SOCKOPT_ERROR, &error) < 0 || error)
+                {
+                    logoutf("could not connect to master server");
+                    disconnectmaster();
+                }
+                else
+                {
+                    masterconnecting = 0; 
+                    masterconnected = totalmillis ? totalmillis : 1; 
+                    server::masterconnected(); 
+                }
+            }
+        }
+        if(mastersock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(readset, mastersock)) flushmasterinput();
+    }
+}
+
+VAR(serveruprate, 0, 0, INT_MAX);
+SVAR(serverip, "");
+VARF(serverport, 0, server::serverport(), 0xFFFF-1, { if(!serverport) serverport = server::serverport(); });
+
+#ifdef STANDALONE
+int curtime = 0, lastmillis = 0, elapsedtime = 0, totalmillis = 0;
+#endif
+
+void updatemasterserver()
+{
+    if(!masterconnected && lastconnectmaster && totalmillis-lastconnectmaster <= 5*60*1000) return;
+    if(mastername[0] && allowupdatemaster) requestmasterf("regserv %d\n", serverport);
+    lastupdatemaster = totalmillis ? totalmillis : 1;
+}
+
+uint totalsecs = 0;
+
+void updatetime()
+{
+    static int lastsec = 0;
+    if(totalmillis - lastsec >= 1000) 
+    {
+        int cursecs = (totalmillis - lastsec) / 1000;
+        totalsecs += cursecs;
+        lastsec += cursecs * 1000;
+    }
+}
+
+void serverslice(bool dedicated, uint timeout)   // main server update, called from main loop in sp, or from below in dedicated server
+{
+    if(!serverhost) 
+    {
+        server::serverupdate();
+        server::sendpackets();
+        return;
+    }
+       
+    // below is network only
+
+    if(dedicated) 
+    {
+        int millis = (int)enet_time_get();
+        elapsedtime = millis - totalmillis;
+        static int timeerr = 0;
+        int scaledtime = server::scaletime(elapsedtime) + timeerr;
+        curtime = scaledtime/100;
+        timeerr = scaledtime%100;
+        if(server::ispaused()) curtime = 0;
+        lastmillis += curtime;
+        totalmillis = millis;
+        updatetime();
+    }
+    server::serverupdate();
+
+    flushmasteroutput();
+    checkserversockets();
+
+    if(!lastupdatemaster || totalmillis-lastupdatemaster>60*60*1000)       // send alive signal to masterserver every hour of uptime
+        updatemasterserver();
+    
+    if(totalmillis-laststatus>60*1000)   // display bandwidth stats, useful for server ops
+    {
+        laststatus = totalmillis;     
+        if(nonlocalclients || serverhost->totalSentData || serverhost->totalReceivedData) logoutf("status: %d remote clients, %.1f send, %.1f rec (K/sec)", nonlocalclients, serverhost->totalSentData/60.0f/1024, serverhost->totalReceivedData/60.0f/1024);
+        serverhost->totalSentData = serverhost->totalReceivedData = 0;
+    }
+
+    ENetEvent event;
+    bool serviced = false;
+    while(!serviced)
+    {
+        if(enet_host_check_events(serverhost, &event) <= 0)
+        {
+            if(enet_host_service(serverhost, &event, timeout) <= 0) break;
+            serviced = true;
+        }
+        switch(event.type)
+        {
+            case ENET_EVENT_TYPE_CONNECT:
+            {
+                client &c = addclient(ST_TCPIP);
+                c.peer = event.peer;
+                c.peer->data = &c;
+                string hn;
+                copystring(c.hostname, (enet_address_get_host_ip(&c.peer->address, hn, sizeof(hn))==0) ? hn : "unknown");
+                logoutf("client connected (%s)", c.hostname);
+                int reason = server::clientconnect(c.num, c.peer->address.host);
+                if(reason) disconnect_client(c.num, reason);
+                break;
+            }
+            case ENET_EVENT_TYPE_RECEIVE:
+            {
+                client *c = (client *)event.peer->data;
+                if(c) process(event.packet, c->num, event.channelID);
+                if(event.packet->referenceCount==0) enet_packet_destroy(event.packet);
+                break;
+            }
+            case ENET_EVENT_TYPE_DISCONNECT: 
+            {
+                client *c = (client *)event.peer->data;
+                if(!c) break;
+                logoutf("disconnected client (%s)", c->hostname);
+                server::clientdisconnect(c->num);
+                delclient(c);
+                break;
+            }
+            default:
+                break;
+        }
+    }
+    if(server::sendpackets()) enet_host_flush(serverhost);
+}
+
+void flushserver(bool force)
+{
+    if(server::sendpackets(force) && serverhost) enet_host_flush(serverhost);
+}
+
+#ifndef STANDALONE
+void localdisconnect(bool cleanup)
+{
+    bool disconnected = false;
+    loopv(clients) if(clients[i]->type==ST_LOCAL) 
+    {
+        server::localdisconnect(i);
+        delclient(clients[i]);
+        disconnected = true;
+    }
+    if(!disconnected) return;
+    game::gamedisconnect(cleanup);
+    mainmenu = 1;
+}
+
+void localconnect()
+{
+    if(initing) return;
+    client &c = addclient(ST_LOCAL);
+    copystring(c.hostname, "local");
+    game::gameconnect(false);
+    server::localconnect(c.num);
+}
+#endif
+
+#ifdef WIN32
+#include "shellapi.h"
+
+#define IDI_ICON1 1
+
+static string apptip = "";
+static HINSTANCE appinstance = NULL;
+static ATOM wndclass = 0;
+static HWND appwindow = NULL, conwindow = NULL;
+static HICON appicon = NULL;
+static HMENU appmenu = NULL;
+static HANDLE outhandle = NULL;
+static const int MAXLOGLINES = 200;
+struct logline { int len; char buf[LOGSTRLEN]; };
+static queue<logline, MAXLOGLINES> loglines;
+
+static void cleanupsystemtray()
+{
+    NOTIFYICONDATA nid;
+    memset(&nid, 0, sizeof(nid));
+    nid.cbSize = sizeof(nid);
+    nid.hWnd = appwindow;
+    nid.uID = IDI_ICON1;
+    Shell_NotifyIcon(NIM_DELETE, &nid);
+}
+
+static bool setupsystemtray(UINT uCallbackMessage)
+{
+       NOTIFYICONDATA nid;
+       memset(&nid, 0, sizeof(nid));
+       nid.cbSize = sizeof(nid);
+       nid.hWnd = appwindow;
+       nid.uID = IDI_ICON1;
+       nid.uCallbackMessage = uCallbackMessage;
+       nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
+       nid.hIcon = appicon;
+       strcpy(nid.szTip, apptip);
+       if(Shell_NotifyIcon(NIM_ADD, &nid) != TRUE)
+        return false;
+    atexit(cleanupsystemtray);
+    return true;
+}
+
+#if 0
+static bool modifysystemtray()
+{
+       NOTIFYICONDATA nid;
+       memset(&nid, 0, sizeof(nid));
+       nid.cbSize = sizeof(nid);
+       nid.hWnd = appwindow;
+       nid.uID = IDI_ICON1;
+       nid.uFlags = NIF_TIP;
+       strcpy(nid.szTip, apptip);
+       return Shell_NotifyIcon(NIM_MODIFY, &nid) == TRUE;
+}
+#endif
+
+static void cleanupwindow()
+{
+       if(!appinstance) return;
+       if(appmenu)
+       {
+               DestroyMenu(appmenu);
+               appmenu = NULL;
+       }
+       if(wndclass)
+       {
+               UnregisterClass(MAKEINTATOM(wndclass), appinstance);
+               wndclass = 0;
+       }
+}
+
+static BOOL WINAPI consolehandler(DWORD dwCtrlType)
+{
+    switch(dwCtrlType)
+    {
+        case CTRL_C_EVENT:
+        case CTRL_BREAK_EVENT:
+        case CTRL_CLOSE_EVENT:
+            exit(EXIT_SUCCESS);
+            return TRUE;
+    }
+    return FALSE;
+}
+
+static void writeline(logline &line)
+{
+    static uchar ubuf[512];
+    size_t len = strlen(line.buf), carry = 0;
+    while(carry < len)
+    {
+        size_t numu = encodeutf8(ubuf, sizeof(ubuf), &((uchar *)line.buf)[carry], len - carry, &carry);
+        DWORD written = 0;
+        WriteConsole(outhandle, ubuf, numu, &written, NULL);
+    }     
+}
+
+static void setupconsole()
+{
+       if(conwindow) return;
+    if(!AllocConsole()) return;
+       SetConsoleCtrlHandler(consolehandler, TRUE);
+       conwindow = GetConsoleWindow();
+    SetConsoleTitle(apptip);
+       //SendMessage(conwindow, WM_SETICON, ICON_SMALL, (LPARAM)appicon);
+       SendMessage(conwindow, WM_SETICON, ICON_BIG, (LPARAM)appicon);
+    outhandle = GetStdHandle(STD_OUTPUT_HANDLE);
+    CONSOLE_SCREEN_BUFFER_INFO coninfo;
+    GetConsoleScreenBufferInfo(outhandle, &coninfo);
+    coninfo.dwSize.Y = MAXLOGLINES;
+    SetConsoleScreenBufferSize(outhandle, coninfo.dwSize);
+    SetConsoleCP(CP_UTF8);
+    SetConsoleOutputCP(CP_UTF8);
+    loopv(loglines) writeline(loglines[i]);
+}
+
+enum
+{
+       MENU_OPENCONSOLE = 0,
+       MENU_SHOWCONSOLE,
+       MENU_HIDECONSOLE,
+       MENU_EXIT
+};
+
+static LRESULT CALLBACK handlemessages(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+       switch(uMsg)
+       {
+               case WM_APP:
+                       SetForegroundWindow(hWnd);
+                       switch(lParam)
+                       {
+                               //case WM_MOUSEMOVE:
+                               //      break;
+                               case WM_LBUTTONUP:
+                               case WM_RBUTTONUP:
+                               {
+                                       POINT pos;
+                                       GetCursorPos(&pos);
+                                       TrackPopupMenu(appmenu, TPM_CENTERALIGN|TPM_BOTTOMALIGN|TPM_RIGHTBUTTON, pos.x, pos.y, 0, hWnd, NULL);
+                                       PostMessage(hWnd, WM_NULL, 0, 0);
+                                       break;
+                               }
+                       }
+                       return 0;
+               case WM_COMMAND:
+                       switch(LOWORD(wParam))
+                       {
+                case MENU_OPENCONSOLE:
+                                       setupconsole();
+                                       if(conwindow) ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_HIDECONSOLE, "Hide Console");
+                    break;
+                               case MENU_SHOWCONSOLE:
+                                       ShowWindow(conwindow, SW_SHOWNORMAL);
+                                       ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_HIDECONSOLE, "Hide Console"); 
+                                       break;
+                               case MENU_HIDECONSOLE:
+                                       ShowWindow(conwindow, SW_HIDE);
+                                       ModifyMenu(appmenu, 0, MF_BYPOSITION|MF_STRING, MENU_SHOWCONSOLE, "Show Console");
+                                       break;
+                               case MENU_EXIT:
+                                       PostMessage(hWnd, WM_CLOSE, 0, 0);
+                                       break;
+                       }
+                       return 0;
+               case WM_CLOSE:
+                       PostQuitMessage(0);
+                       return 0;
+       }
+       return DefWindowProc(hWnd, uMsg, wParam, lParam);
+}
+
+static void setupwindow(const char *title)
+{
+       copystring(apptip, title);
+       //appinstance = GetModuleHandle(NULL);
+       if(!appinstance) fatal("failed getting application instance");
+       appicon = LoadIcon(appinstance, MAKEINTRESOURCE(IDI_ICON1));//(HICON)LoadImage(appinstance, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE);
+       if(!appicon) fatal("failed loading icon");
+
+       appmenu = CreatePopupMenu();
+       if(!appmenu) fatal("failed creating popup menu");
+    AppendMenu(appmenu, MF_STRING, MENU_OPENCONSOLE, "Open Console");
+    AppendMenu(appmenu, MF_SEPARATOR, 0, NULL);
+       AppendMenu(appmenu, MF_STRING, MENU_EXIT, "Exit");
+       //SetMenuDefaultItem(appmenu, 0, FALSE);
+
+       WNDCLASS wc;
+       memset(&wc, 0, sizeof(wc));
+       wc.hCursor = NULL; //LoadCursor(NULL, IDC_ARROW);
+       wc.hIcon = appicon;
+       wc.lpszMenuName = NULL;
+       wc.lpszClassName = title;
+       wc.style = 0;
+       wc.hInstance = appinstance;
+       wc.lpfnWndProc = handlemessages;
+       wc.cbWndExtra = 0;
+       wc.cbClsExtra = 0;
+       wndclass = RegisterClass(&wc);
+       if(!wndclass) fatal("failed registering window class");
+       
+       appwindow = CreateWindow(MAKEINTATOM(wndclass), title, 0, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HWND_MESSAGE, NULL, appinstance, NULL);
+       if(!appwindow) fatal("failed creating window");
+
+       atexit(cleanupwindow);
+
+    if(!setupsystemtray(WM_APP)) fatal("failed adding to system tray");
+}
+
+static char *parsecommandline(const char *src, vector<char *> &args)
+{
+    char *buf = new char[strlen(src) + 1], *dst = buf;
+    for(;;)
+    {
+        while(isspace(*src)) src++;
+        if(!*src) break;
+        args.add(dst);
+               for(bool quoted = false; *src && (quoted || !isspace(*src)); src++)
+        {
+            if(*src != '"') *dst++ = *src;
+                       else if(dst > buf && src[-1] == '\\') dst[-1] = '"';
+                       else quoted = !quoted;
+               }
+               *dst++ = '\0';
+    }
+    args.add(NULL);
+    return buf;
+}
+                
+
+int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int sw)
+{
+    vector<char *> args;
+    char *buf = parsecommandline(GetCommandLine(), args);
+       appinstance = hInst;
+#ifdef STANDALONE
+    int standalonemain(int argc, char **argv);
+    int status = standalonemain(args.length()-1, args.getbuf());
+    #define main standalonemain
+#else
+    SDL_SetMainReady();
+    int status = SDL_main(args.length()-1, args.getbuf());
+#endif
+    delete[] buf;
+    exit(status);
+    return 0;
+}
+
+void logoutfv(const char *fmt, va_list args)
+{
+    if(appwindow)
+    {
+        logline &line = loglines.add();
+        vformatstring(line.buf, fmt, args, sizeof(line.buf));
+        if(logfile) writelog(logfile, line.buf);
+        line.len = min(strlen(line.buf), sizeof(line.buf)-2);
+        line.buf[line.len++] = '\n';
+        line.buf[line.len] = '\0';
+        if(outhandle) writeline(line);
+    }
+    else if(logfile) writelogv(logfile, fmt, args);
+}
+
+#else
+
+void logoutfv(const char *fmt, va_list args)
+{
+    FILE *f = getlogfile();
+    if(f) writelogv(f, fmt, args);
+}
+
+#endif
+
+static bool dedicatedserver = false;
+
+bool isdedicatedserver() { return dedicatedserver; }
+
+void rundedicatedserver()
+{
+    dedicatedserver = true;
+    logoutf("dedicated server started, waiting for clients...");
+#ifdef WIN32
+    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
+       for(;;)
+       {
+               MSG msg;
+               while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
+               {
+                       if(msg.message == WM_QUIT) exit(EXIT_SUCCESS);
+                       TranslateMessage(&msg);
+                       DispatchMessage(&msg);
+               }
+               serverslice(true, 5);
+       }
+#else
+    for(;;) serverslice(true, 5);
+#endif
+    dedicatedserver = false;
+}
+
+bool servererror(bool dedicated, const char *desc)
+{
+#ifndef STANDALONE
+    if(!dedicated)
+    {
+        conoutf(CON_ERROR, "%s", desc);
+        cleanupserver();
+    }
+    else
+#endif
+        fatal("%s", desc);
+    return false;
+}
+  
+bool setuplistenserver(bool dedicated)
+{
+    ENetAddress address = { ENET_HOST_ANY, enet_uint16(serverport <= 0 ? server::serverport() : serverport) };
+    if(*serverip)
+    {
+        if(enet_address_set_host(&address, serverip)<0) conoutf(CON_WARN, "WARNING: server ip not resolved");
+        else serveraddress.host = address.host;
+    }
+    serverhost = enet_host_create(&address, min(maxclients + server::reserveclients(), MAXCLIENTS), server::numchannels(), 0, serveruprate);
+    if(!serverhost) return servererror(dedicated, "could not create server host");
+    serverhost->duplicatePeers = maxdupclients ? maxdupclients : MAXCLIENTS;
+    address.port = server::serverinfoport(serverport > 0 ? serverport : -1);
+    pongsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+    if(pongsock != ENET_SOCKET_NULL && enet_socket_bind(pongsock, &address) < 0)
+    {
+        enet_socket_destroy(pongsock);
+        pongsock = ENET_SOCKET_NULL;
+    }
+    if(pongsock == ENET_SOCKET_NULL) return servererror(dedicated, "could not create server info socket");
+    else enet_socket_set_option(pongsock, ENET_SOCKOPT_NONBLOCK, 1);
+    address.port = server::laninfoport();
+    lansock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+    if(lansock != ENET_SOCKET_NULL && (enet_socket_set_option(lansock, ENET_SOCKOPT_REUSEADDR, 1) < 0 || enet_socket_bind(lansock, &address) < 0))
+    {
+        enet_socket_destroy(lansock);
+        lansock = ENET_SOCKET_NULL;
+    }
+    if(lansock == ENET_SOCKET_NULL) conoutf(CON_WARN, "WARNING: could not create LAN server info socket");
+    else enet_socket_set_option(lansock, ENET_SOCKOPT_NONBLOCK, 1);
+    return true;
+}
+
+void initserver(bool listen, bool dedicated)
+{
+    if(dedicated) 
+    {
+#ifdef WIN32
+        setupwindow("Cube 2: Sauerbraten server");
+#endif
+    }
+    
+    execfile("server-init.cfg", false);
+
+    if(listen) setuplistenserver(dedicated);
+
+    server::serverinit();
+
+    if(listen)
+    {
+        dedicatedserver = dedicated;
+        updatemasterserver();
+        if(dedicated) rundedicatedserver(); // never returns
+#ifndef STANDALONE
+        else conoutf("listen server started");
+#endif
+    }
+}
+
+#ifndef STANDALONE
+void startlistenserver(int *usemaster)
+{
+    if(serverhost) { conoutf(CON_ERROR, "listen server is already running"); return; }
+
+    allowupdatemaster = *usemaster>0 ? 1 : 0;
+    if(!setuplistenserver(false)) return;
+    
+    updatemasterserver();
+   
+    conoutf("listen server started for %d clients%s", maxclients, allowupdatemaster ? " and listed with master server" : ""); 
+}
+COMMAND(startlistenserver, "i");
+
+void stoplistenserver()
+{
+    if(!serverhost) { conoutf(CON_ERROR, "listen server is not running"); return; }
+
+    kicknonlocalclients();
+    enet_host_flush(serverhost);
+    cleanupserver();
+
+    conoutf("listen server stopped");
+}
+COMMAND(stoplistenserver, "");
+#endif
+
+bool serveroption(char *opt)
+{
+    switch(opt[1])
+    {
+        case 'u': setvar("serveruprate", atoi(opt+2)); return true;
+        case 'c': setvar("maxclients", atoi(opt+2)); return true;
+        case 'i': setsvar("serverip", opt+2); return true;
+        case 'j': setvar("serverport", atoi(opt+2)); return true; 
+        case 'm': setsvar("mastername", opt+2); setvar("updatemaster", mastername[0] ? 1 : 0); return true;
+#ifdef STANDALONE
+        case 'q': logoutf("Using home directory: %s", opt); sethomedir(opt+2); return true;
+        case 'k': logoutf("Adding package directory: %s", opt); addpackagedir(opt+2); return true;
+        case 'g': logoutf("Setting log file: %s", opt); setlogfile(opt+2); return true;
+#endif
+        default: return false;
+    }
+}
+
+vector<const char *> gameargs;
+
+#ifdef STANDALONE
+int main(int argc, char **argv)
+{   
+    setlogfile(NULL);
+    if(enet_initialize()<0) fatal("Unable to initialise network module");
+    atexit(enet_deinitialize);
+    enet_time_set(0);
+    for(int i = 1; i<argc; i++) if(argv[i][0]!='-' || !serveroption(argv[i])) gameargs.add(argv[i]);
+    game::parseoptions(gameargs);
+    initserver(true, true);
+    return EXIT_SUCCESS;
+}
+#endif
diff --git a/src/engine/serverbrowser.cpp b/src/engine/serverbrowser.cpp
new file mode 100644 (file)
index 0000000..281b0dc
--- /dev/null
@@ -0,0 +1,751 @@
+// serverbrowser.cpp: eihrul's concurrent resolver, and server browser window management
+
+#include "engine.h"
+
+struct resolverthread
+{
+    SDL_Thread *thread;
+    const char *query;
+    int starttime;
+};
+
+struct resolverresult
+{
+    const char *query;
+    ENetAddress address;
+};
+
+vector<resolverthread> resolverthreads;
+vector<const char *> resolverqueries;
+vector<resolverresult> resolverresults;
+SDL_mutex *resolvermutex;
+SDL_cond *querycond, *resultcond;
+
+#define RESOLVERTHREADS 2
+#define RESOLVERLIMIT 3000
+
+int resolverloop(void * data)
+{
+    resolverthread *rt = (resolverthread *)data;
+    SDL_LockMutex(resolvermutex);
+    SDL_Thread *thread = rt->thread;
+    SDL_UnlockMutex(resolvermutex);
+    if(!thread || SDL_GetThreadID(thread) != SDL_ThreadID())
+        return 0;
+    while(thread == rt->thread)
+    {
+        SDL_LockMutex(resolvermutex);
+        while(resolverqueries.empty()) SDL_CondWait(querycond, resolvermutex);
+        rt->query = resolverqueries.pop();
+        rt->starttime = totalmillis;
+        SDL_UnlockMutex(resolvermutex);
+
+        ENetAddress address = { ENET_HOST_ANY, ENET_PORT_ANY };
+        enet_address_set_host(&address, rt->query);
+
+        SDL_LockMutex(resolvermutex);
+        if(rt->query && thread == rt->thread)
+        {
+            resolverresult &rr = resolverresults.add();
+            rr.query = rt->query;
+            rr.address = address;
+            rt->query = NULL;
+            rt->starttime = 0;
+            SDL_CondSignal(resultcond);
+        }
+        SDL_UnlockMutex(resolvermutex);
+    }
+    return 0;
+}
+
+void resolverinit()
+{
+    resolvermutex = SDL_CreateMutex();
+    querycond = SDL_CreateCond();
+    resultcond = SDL_CreateCond();
+
+    SDL_LockMutex(resolvermutex);
+    loopi(RESOLVERTHREADS)
+    {
+        resolverthread &rt = resolverthreads.add();
+        rt.query = NULL;
+        rt.starttime = 0;
+        rt.thread = SDL_CreateThread(resolverloop, "resolver", &rt);
+    }
+    SDL_UnlockMutex(resolvermutex);
+}
+
+void resolverstop(resolverthread &rt)
+{
+    SDL_LockMutex(resolvermutex);
+    if(rt.query)
+    {
+#if SDL_VERSION_ATLEAST(2, 0, 2)
+        SDL_DetachThread(rt.thread);
+#endif
+        rt.thread = SDL_CreateThread(resolverloop, "resolver", &rt);
+    }
+    rt.query = NULL;
+    rt.starttime = 0;
+    SDL_UnlockMutex(resolvermutex);
+} 
+
+void resolverclear()
+{
+    if(resolverthreads.empty()) return;
+
+    SDL_LockMutex(resolvermutex);
+    resolverqueries.shrink(0);
+    resolverresults.shrink(0);
+    loopv(resolverthreads)
+    {
+        resolverthread &rt = resolverthreads[i];
+        resolverstop(rt);
+    }
+    SDL_UnlockMutex(resolvermutex);
+}
+
+void resolverquery(const char *name)
+{
+    if(resolverthreads.empty()) resolverinit();
+
+    SDL_LockMutex(resolvermutex);
+    resolverqueries.add(name);
+    SDL_CondSignal(querycond);
+    SDL_UnlockMutex(resolvermutex);
+}
+
+bool resolvercheck(const char **name, ENetAddress *address)
+{
+    bool resolved = false;
+    SDL_LockMutex(resolvermutex);
+    if(!resolverresults.empty())
+    {
+        resolverresult &rr = resolverresults.pop();
+        *name = rr.query;
+        address->host = rr.address.host;
+        resolved = true;
+    }
+    else loopv(resolverthreads)
+    {
+        resolverthread &rt = resolverthreads[i];
+        if(rt.query && totalmillis - rt.starttime > RESOLVERLIMIT)        
+        {
+            resolverstop(rt);
+            *name = rt.query;
+            resolved = true;
+        }    
+    }
+    SDL_UnlockMutex(resolvermutex);
+    return resolved;
+}
+
+bool resolverwait(const char *name, ENetAddress *address)
+{
+    if(resolverthreads.empty()) resolverinit();
+
+    defformatstring(text, "resolving %s... (esc to abort)", name);
+    renderprogress(0, text);
+
+    SDL_LockMutex(resolvermutex);
+    resolverqueries.add(name);
+    SDL_CondSignal(querycond);
+    int starttime = SDL_GetTicks(), timeout = 0;
+    bool resolved = false;
+    for(;;) 
+    {
+        SDL_CondWaitTimeout(resultcond, resolvermutex, 250);
+        loopv(resolverresults) if(resolverresults[i].query == name) 
+        {
+            address->host = resolverresults[i].address.host;
+            resolverresults.remove(i);
+            resolved = true;
+            break;
+        }
+        if(resolved) break;
+    
+        timeout = SDL_GetTicks() - starttime;
+        renderprogress(min(float(timeout)/RESOLVERLIMIT, 1.0f), text);
+        if(interceptkey(SDLK_ESCAPE)) timeout = RESOLVERLIMIT + 1;
+        if(timeout > RESOLVERLIMIT) break;    
+    }
+    if(!resolved && timeout > RESOLVERLIMIT)
+    {
+        loopv(resolverthreads)
+        {
+            resolverthread &rt = resolverthreads[i];
+            if(rt.query == name) { resolverstop(rt); break; }
+        }
+    }
+    SDL_UnlockMutex(resolvermutex);
+    return resolved && address->host != ENET_HOST_ANY;
+}
+
+#define CONNLIMIT 20000
+
+int connectwithtimeout(ENetSocket sock, const char *hostname, const ENetAddress &address)
+{
+    defformatstring(text, "connecting to %s... (esc to abort)", hostname);
+    renderprogress(0, text);
+
+    ENetSocketSet readset, writeset;
+    if(!enet_socket_connect(sock, &address)) for(int starttime = SDL_GetTicks(), timeout = 0; timeout <= CONNLIMIT;)
+    {
+        ENET_SOCKETSET_EMPTY(readset);
+        ENET_SOCKETSET_EMPTY(writeset);
+        ENET_SOCKETSET_ADD(readset, sock);
+        ENET_SOCKETSET_ADD(writeset, sock);
+        int result = enet_socketset_select(sock, &readset, &writeset, 250);
+        if(result < 0) break;
+        else if(result > 0)
+        {
+            if(ENET_SOCKETSET_CHECK(readset, sock) || ENET_SOCKETSET_CHECK(writeset, sock))
+            {
+                int error = 0;
+                if(enet_socket_get_option(sock, ENET_SOCKOPT_ERROR, &error) < 0 || error) break;
+                return 0;
+            }
+        }
+        timeout = SDL_GetTicks() - starttime;
+        renderprogress(min(float(timeout)/CONNLIMIT, 1.0f), text);
+        if(interceptkey(SDLK_ESCAPE)) break;
+    }
+
+    return -1;
+}
+struct pingattempts
+{
+    enum { MAXATTEMPTS = 2 };
+
+    int offset, attempts[MAXATTEMPTS];
+
+    pingattempts() : offset(0) { clearattempts(); }
+
+    void clearattempts() { memset(attempts, 0, sizeof(attempts)); }
+
+    void setoffset() { offset = 1 + rnd(0xFFFFFF); } 
+
+    int encodeping(int millis)
+    {
+        millis += offset;
+        return millis ? millis : 1;
+    }
+
+    int decodeping(int val)
+    {
+        return val - offset;
+    }
+
+    int addattempt(int millis)
+    {
+        int val = encodeping(millis);
+        loopk(MAXATTEMPTS-1) attempts[k+1] = attempts[k];
+        attempts[0] = val;
+        return val;
+    }
+
+    bool checkattempt(int val, bool del = true)
+    {
+        if(val) loopk(MAXATTEMPTS) if(attempts[k] == val)
+        {
+            if(del) attempts[k] = 0;
+            return true;
+        }
+        return false;
+    }
+
+};
+
+enum { UNRESOLVED = 0, RESOLVING, RESOLVED };
+
+struct serverinfo : pingattempts
+{
+    enum 
+    { 
+        WAITING = INT_MAX,
+
+        MAXPINGS = 3
+    };
+
+    string name, map, sdesc;
+    int port, numplayers, resolved, ping, lastping, nextping;
+    int pings[MAXPINGS];
+    vector<int> attr;
+    ENetAddress address;
+    bool keep;
+    const char *password;
+
+    serverinfo()
+        : port(-1), numplayers(0), resolved(UNRESOLVED), keep(false), password(NULL)
+    {
+        name[0] = map[0] = sdesc[0] = '\0';
+        clearpings();
+        setoffset();
+    }
+
+    ~serverinfo()
+    {
+        DELETEA(password);
+    }
+
+    void clearpings()
+    {
+        ping = WAITING;
+        loopk(MAXPINGS) pings[k] = WAITING;
+        nextping = 0;
+        lastping = -1;
+        clearattempts();
+    }
+
+    void cleanup()
+    {
+        clearpings();
+        attr.setsize(0);
+        numplayers = 0;
+    }
+
+    void reset()
+    {
+        lastping = -1;
+    }
+
+    void checkdecay(int decay)
+    {
+        if(lastping >= 0 && totalmillis - lastping >= decay)
+            cleanup();
+        if(lastping < 0) lastping = totalmillis;
+    }
+
+    void calcping()
+    {
+        int numpings = 0, totalpings = 0;
+        loopk(MAXPINGS) if(pings[k] != WAITING) { totalpings += pings[k]; numpings++; }
+        ping = numpings ? totalpings/numpings : WAITING;
+    }
+
+    void addping(int rtt, int millis)
+    {
+        if(millis >= lastping) lastping = -1;
+        pings[nextping] = rtt;
+        nextping = (nextping+1)%MAXPINGS;
+        calcping();
+    }
+
+    static bool compare(serverinfo *a, serverinfo *b)
+    {
+        bool ac = server::servercompatible(a->name, a->sdesc, a->map, a->ping, a->attr, a->numplayers),
+             bc = server::servercompatible(b->name, b->sdesc, b->map, b->ping, b->attr, b->numplayers);
+        if(ac > bc) return true;
+        if(bc > ac) return false;
+        if(a->keep > b->keep) return true;
+        if(a->keep < b->keep) return false;
+        if(a->numplayers < b->numplayers) return false;
+        if(a->numplayers > b->numplayers) return true;
+        if(a->ping > b->ping) return false;
+        if(a->ping < b->ping) return true;
+        int cmp = strcmp(a->name, b->name);
+        if(cmp != 0) return cmp < 0;
+        if(a->port < b->port) return true;
+        if(a->port > b->port) return false;
+        return false;
+    }
+};
+
+vector<serverinfo *> servers;
+ENetSocket pingsock = ENET_SOCKET_NULL;
+int lastinfo = 0;
+
+static serverinfo *newserver(const char *name, int port, uint ip = ENET_HOST_ANY)
+{
+    serverinfo *si = new serverinfo;
+    si->address.host = ip;
+    si->address.port = server::serverinfoport(port);
+    if(ip!=ENET_HOST_ANY) si->resolved = RESOLVED;
+
+    si->port = port;
+    if(name) copystring(si->name, name);
+    else if(ip==ENET_HOST_ANY || enet_address_get_host_ip(&si->address, si->name, sizeof(si->name)) < 0)
+    {
+        delete si;
+        return NULL;
+
+    }
+
+    servers.add(si);
+
+    return si;
+}
+
+void addserver(const char *name, int port, const char *password, bool keep)
+{
+    if(port <= 0) port = server::serverport();
+    loopv(servers)
+    {
+        serverinfo *s = servers[i];
+        if(strcmp(s->name, name) || s->port != port) continue;
+        if(password && (!s->password || strcmp(s->password, password)))
+        {
+            DELETEA(s->password);
+            s->password = newstring(password);
+        }
+        if(keep && !s->keep) s->keep = true;
+        return;
+    }
+    serverinfo *s = newserver(name, port);
+    if(!s) return;
+    if(password) s->password = newstring(password);
+    s->keep = keep;
+}
+
+VARP(searchlan, 0, 0, 1);
+VARP(servpingrate, 1000, 5000, 60000);
+VARP(servpingdecay, 1000, 15000, 60000);
+VARP(maxservpings, 0, 10, 1000);
+
+pingattempts lanpings;
+
+template<size_t N> static inline void buildping(ENetBuffer &buf, uchar (&ping)[N], pingattempts &a)
+{
+    ucharbuf p(ping, N);
+    putint(p, a.addattempt(totalmillis));
+    buf.data = ping;
+    buf.dataLength = p.length();
+}
+
+void pingservers()
+{
+    if(pingsock == ENET_SOCKET_NULL) 
+    {
+        pingsock = enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+        if(pingsock == ENET_SOCKET_NULL)
+        {
+            lastinfo = totalmillis;
+            return;
+        }
+        enet_socket_set_option(pingsock, ENET_SOCKOPT_NONBLOCK, 1);
+        enet_socket_set_option(pingsock, ENET_SOCKOPT_BROADCAST, 1);
+
+        lanpings.setoffset();
+    }
+
+    ENetBuffer buf;
+    uchar ping[MAXTRANS];
+
+    static int lastping = 0;
+    if(lastping >= servers.length()) lastping = 0;
+    loopi(maxservpings ? min(servers.length(), maxservpings) : servers.length())
+    {
+        serverinfo &si = *servers[lastping];
+        if(++lastping >= servers.length()) lastping = 0;
+        if(si.address.host == ENET_HOST_ANY) continue;
+        buildping(buf, ping, si);
+        enet_socket_send(pingsock, &si.address, &buf, 1);
+        
+        si.checkdecay(servpingdecay);
+    }
+    if(searchlan)
+    {
+        ENetAddress address;
+        address.host = ENET_HOST_BROADCAST;
+        address.port = server::laninfoport();
+        buildping(buf, ping, lanpings);
+        enet_socket_send(pingsock, &address, &buf, 1);
+    }
+    lastinfo = totalmillis;
+}
+  
+void checkresolver()
+{
+    int resolving = 0;
+    loopv(servers)
+    {
+        serverinfo &si = *servers[i];
+        if(si.resolved == RESOLVED) continue;
+        if(si.address.host == ENET_HOST_ANY)
+        {
+            if(si.resolved == UNRESOLVED) { si.resolved = RESOLVING; resolverquery(si.name); }
+            resolving++;
+        }
+    }
+    if(!resolving) return;
+
+    const char *name = NULL;
+    for(;;)
+    {
+        ENetAddress addr = { ENET_HOST_ANY, ENET_PORT_ANY };
+        if(!resolvercheck(&name, &addr)) break;
+        loopv(servers)
+        {
+            serverinfo &si = *servers[i];
+            if(name == si.name)
+            {
+                si.resolved = RESOLVED; 
+                si.address.host = addr.host;
+                break;
+            }
+        }
+    }
+}
+
+static int lastreset = 0;
+
+void checkpings()
+{
+    if(pingsock==ENET_SOCKET_NULL) return;
+    enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE;
+    ENetBuffer buf;
+    ENetAddress addr;
+    uchar ping[MAXTRANS];
+    char text[MAXTRANS];
+    buf.data = ping; 
+    buf.dataLength = sizeof(ping);
+    while(enet_socket_wait(pingsock, &events, 0) >= 0 && events)
+    {
+        int len = enet_socket_receive(pingsock, &addr, &buf, 1);
+        if(len <= 0) return;  
+        ucharbuf p(ping, len);
+        int millis = getint(p);
+        serverinfo *si = NULL;
+        loopv(servers) if(addr.host == servers[i]->address.host && addr.port == servers[i]->address.port) { si = servers[i]; break; }
+        if(si)
+        {
+            if(!si->checkattempt(millis)) continue;
+            millis = si->decodeping(millis);
+        }
+        else if(!searchlan || !lanpings.checkattempt(millis, false)) continue;
+        else
+        {
+            si = newserver(NULL, server::serverport(addr.port), addr.host); 
+            millis = lanpings.decodeping(millis);
+        }
+        int rtt = clamp(totalmillis - millis, 0, min(servpingdecay, totalmillis));
+        if(millis >= lastreset && rtt < servpingdecay) si->addping(rtt, millis);
+        si->numplayers = getint(p);
+        int numattr = getint(p);
+        si->attr.setsize(0);
+        loopj(numattr) { int attr = getint(p); if(p.overread()) break; si->attr.add(attr); }
+        getstring(text, p);
+        filtertext(si->map, text, false);
+        getstring(text, p);
+        filtertext(si->sdesc, text, true, true);
+    }
+}
+
+void sortservers()
+{
+    servers.sort(serverinfo::compare);
+}
+COMMAND(sortservers, "");
+
+VARP(autosortservers, 0, 1, 1);
+VARP(autoupdateservers, 0, 1, 1);
+
+void refreshservers()
+{
+    static int lastrefresh = 0;
+    if(lastrefresh==totalmillis) return;
+    if(totalmillis - lastrefresh > 1000) 
+    {
+        loopv(servers) servers[i]->reset();
+        lastreset = totalmillis;
+    }
+    lastrefresh = totalmillis;
+
+    checkresolver();
+    checkpings();
+    if(totalmillis - lastinfo >= servpingrate/(maxservpings ? max(1, (servers.length() + maxservpings - 1) / maxservpings) : 1)) pingservers();
+    if(autosortservers) sortservers();
+}
+
+serverinfo *selectedserver = NULL;
+
+const char *showservers(g3d_gui *cgui, uint *header, int pagemin, int pagemax)
+{
+    refreshservers();
+    if(servers.empty())
+    {
+        if(header) execute(header);
+        return NULL;
+    }
+    serverinfo *sc = NULL;
+    for(int start = 0; start < servers.length();)
+    {
+        if(start > 0) cgui->tab();
+        if(header) execute(header);
+        int end = servers.length();
+        cgui->pushlist();
+        loopi(10)
+        {
+            if(!game::serverinfostartcolumn(cgui, i)) break;
+            for(int j = start; j < end; j++)
+            {
+                if(!i && j+1 - start >= pagemin && (j+1 - start >= pagemax || cgui->shouldtab())) { end = j; break; }
+                serverinfo &si = *servers[j];
+                const char *sdesc = si.sdesc;
+                if(si.address.host == ENET_HOST_ANY) sdesc = "[unknown host]";
+                else if(si.ping == serverinfo::WAITING) sdesc = "[waiting for response]";
+                if(game::serverinfoentry(cgui, i, si.name, si.port, sdesc, si.map, sdesc == si.sdesc ? si.ping : -1, si.attr, si.numplayers))
+                    sc = &si;
+            }
+            game::serverinfoendcolumn(cgui, i);
+        }
+        cgui->poplist();
+        start = end;
+    }
+    if(selectedserver || !sc) return NULL;
+    selectedserver = sc;
+    return "connectselected";
+}
+
+void connectselected()
+{
+    if(!selectedserver) return;
+    connectserv(selectedserver->name, selectedserver->port, selectedserver->password);
+    selectedserver = NULL;
+}
+
+COMMAND(connectselected, "");
+
+void clearservers(bool full = false)
+{
+    resolverclear();
+    if(full) servers.deletecontents();
+    else loopvrev(servers) if(!servers[i]->keep) delete servers.remove(i);
+    selectedserver = NULL;
+}
+
+#define RETRIEVELIMIT 20000
+
+void retrieveservers(vector<char> &data)
+{
+    ENetSocket sock = connectmaster(true);
+    if(sock == ENET_SOCKET_NULL) return;
+
+    extern char *mastername;
+    defformatstring(text, "retrieving servers from %s... (esc to abort)", mastername);
+    renderprogress(0, text);
+
+    int starttime = SDL_GetTicks(), timeout = 0;
+    const char *req = "list\n";
+    int reqlen = strlen(req);
+    ENetBuffer buf;
+    while(reqlen > 0)
+    {
+        enet_uint32 events = ENET_SOCKET_WAIT_SEND;
+        if(enet_socket_wait(sock, &events, 250) >= 0 && events) 
+        {
+            buf.data = (void *)req;
+            buf.dataLength = reqlen;
+            int sent = enet_socket_send(sock, NULL, &buf, 1);
+            if(sent < 0) break;
+            req += sent;
+            reqlen -= sent;
+            if(reqlen <= 0) break;
+        }
+        timeout = SDL_GetTicks() - starttime;
+        renderprogress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text);
+        if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1;
+        if(timeout > RETRIEVELIMIT) break;
+    }
+
+    if(reqlen <= 0) for(;;)
+    {
+        enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE;
+        if(enet_socket_wait(sock, &events, 250) >= 0 && events)
+        {
+            if(data.length() >= data.capacity()) data.reserve(4096);
+            buf.data = data.getbuf() + data.length();
+            buf.dataLength = data.capacity() - data.length();
+            int recv = enet_socket_receive(sock, NULL, &buf, 1);
+            if(recv <= 0) break;
+            data.advance(recv);
+        }
+        timeout = SDL_GetTicks() - starttime;
+        renderprogress(min(float(timeout)/RETRIEVELIMIT, 1.0f), text);
+        if(interceptkey(SDLK_ESCAPE)) timeout = RETRIEVELIMIT + 1;
+        if(timeout > RETRIEVELIMIT) break;
+    }
+
+    if(data.length()) data.add('\0');
+    enet_socket_destroy(sock);
+}
+
+bool updatedservers = false;
+
+void updatefrommaster()
+{
+    vector<char> data;
+    retrieveservers(data);
+    if(data.empty()) conoutf(CON_ERROR, "master server not replying");
+    else
+    {
+        clearservers();
+        char *line = data.getbuf();
+        while(char *end = (char *)memchr(line, '\n', data.length() - (line - data.getbuf())))
+        {
+            *end = '\0';
+
+            const char *args = line;
+            while(args < end && !iscubespace(*args)) args++;
+            int cmdlen = args - line;
+            while(args < end && iscubespace(*args)) args++;
+
+            if(matchstring(line, cmdlen, "addserver"))
+            {
+                string ip;
+                int port;
+                if(sscanf(args, "%100s %d", ip, &port) == 2) addserver(ip, port);
+            }
+            else if(matchstring(line, cmdlen, "echo")) conoutf("\f1%s", args);
+
+            line = end + 1;
+        }
+    }
+    refreshservers();
+    updatedservers = true;
+}
+
+void initservers()
+{
+    selectedserver = NULL;
+    if(autoupdateservers && !updatedservers) updatefrommaster();
+}
+
+ICOMMAND(addserver, "sis", (const char *name, int *port, const char *password), addserver(name, *port, password[0] ? password : NULL));
+ICOMMAND(keepserver, "sis", (const char *name, int *port, const char *password), addserver(name, *port, password[0] ? password : NULL, true));
+ICOMMAND(clearservers, "i", (int *full), clearservers(*full!=0));
+COMMAND(updatefrommaster, "");
+COMMAND(initservers, "");
+
+void writeservercfg()
+{
+    if(!game::savedservers()) return;
+    stream *f = openutf8file(path(game::savedservers(), true), "w");
+    if(!f) return;
+    int kept = 0;
+    loopv(servers)
+    {
+        serverinfo *s = servers[i];
+        if(s->keep)
+        {
+            if(!kept) f->printf("// servers that should never be cleared from the server list\n\n");
+            if(s->password) f->printf("keepserver %s %d %s\n", escapeid(s->name), s->port, escapestring(s->password));
+            else f->printf("keepserver %s %d\n", escapeid(s->name), s->port);
+            kept++;
+        }
+    }
+    if(kept) f->printf("\n");
+    f->printf("// servers connected to are added here automatically\n\n");
+    loopv(servers) 
+    {
+        serverinfo *s = servers[i];
+        if(!s->keep) 
+        {
+            if(s->password) f->printf("addserver %s %d %s\n", escapeid(s->name), s->port, escapestring(s->password));
+            else f->printf("addserver %s %d\n", escapeid(s->name), s->port);
+        }
+    }
+    delete f;
+}
+
diff --git a/src/engine/shader.cpp b/src/engine/shader.cpp
new file mode 100644 (file)
index 0000000..da17d4f
--- /dev/null
@@ -0,0 +1,1522 @@
+// shader.cpp: OpenGL GLSL shader management
+
+#include "engine.h"
+
+Shader *Shader::lastshader = NULL;
+
+Shader *nullshader = NULL, *hudshader = NULL, *hudnotextureshader = NULL, *textureshader = NULL, *notextureshader = NULL, *nocolorshader = NULL, *foggedshader = NULL, *foggednotextureshader = NULL, *stdworldshader = NULL;
+
+static hashnameset<GlobalShaderParamState> globalparams(256);
+static hashtable<const char *, int> localparams(256);
+static hashnameset<Shader> shaders(256);
+static Shader *slotshader = NULL;
+static vector<SlotShaderParam> slotparams;
+static bool standardshaders = false, forceshaders = true, loadedshaders = false;
+
+VAR(reservevpparams, 1, 16, 0);
+VAR(maxvsuniforms, 1, 0, 0);
+VAR(maxfsuniforms, 1, 0, 0);
+VAR(maxvaryings, 1, 0, 0);
+VAR(dbgshader, 0, 0, 2);
+
+void loadshaders()
+{
+    standardshaders = true;
+    execfile("data/glsl.cfg");
+    standardshaders = false;
+
+    nullshader = lookupshaderbyname("null");
+    hudshader = lookupshaderbyname("hud");
+    hudnotextureshader = lookupshaderbyname("hudnotexture");
+    stdworldshader = lookupshaderbyname("stdworld");
+    if(!nullshader || !hudshader || !hudnotextureshader || !stdworldshader) fatal("cannot find shader definitions");
+
+    dummyslot.shader = stdworldshader;
+
+    textureshader = lookupshaderbyname("texture");
+    notextureshader = lookupshaderbyname("notexture");
+    nocolorshader = lookupshaderbyname("nocolor");
+    foggedshader = lookupshaderbyname("fogged");
+    foggednotextureshader = lookupshaderbyname("foggednotexture");
+    
+    nullshader->set();
+
+    loadedshaders = true;
+}
+
+Shader *lookupshaderbyname(const char *name) 
+{ 
+    Shader *s = shaders.access(name);
+    return s && s->loaded() ? s : NULL;
+}
+
+Shader *generateshader(const char *name, const char *fmt, ...)
+{
+    if(!loadedshaders) return NULL; 
+    Shader *s = name ? lookupshaderbyname(name) : NULL;
+    if(!s)
+    {
+        defvformatstring(cmd, fmt, fmt);
+        bool wasstandard = standardshaders;
+        standardshaders = true;
+        execute(cmd); 
+        standardshaders = wasstandard;
+        s = name ? lookupshaderbyname(name) : NULL;
+        if(!s) s = nullshader;
+    }
+    return s;
+}
+
+static void showglslinfo(GLenum type, GLuint obj, const char *name, const char **parts = NULL, int numparts = 0)
+{
+    GLint length = 0;
+    if(type) glGetShaderiv_(obj, GL_INFO_LOG_LENGTH, &length);
+    else glGetProgramiv_(obj, GL_INFO_LOG_LENGTH, &length);
+    if(length > 1)
+    {
+        conoutf(CON_ERROR, "GLSL ERROR (%s:%s)", type == GL_VERTEX_SHADER ? "VS" : (type == GL_FRAGMENT_SHADER ? "FS" : "PROG"), name);
+        FILE *l = getlogfile();
+        if(l)
+        {
+            GLchar *log = new GLchar[length];
+            if(type) glGetShaderInfoLog_(obj, length, &length, log);
+            else glGetProgramInfoLog_(obj, length, &length, log);
+            fprintf(l, "%s\n", log);
+            bool partlines = log[0] != '0';
+            int line = 0;
+            loopi(numparts)
+            {
+                const char *part = parts[i];
+                int startline = line;
+                while(*part)
+                {
+                    const char *next = strchr(part, '\n');
+                    if(++line > 1000) goto done;
+                    if(partlines) fprintf(l, "%d(%d): ", i, line - startline); else fprintf(l, "%d: ", line);
+                    fwrite(part, 1, next ? next - part + 1 : strlen(part), l);
+                    if(!next) { fputc('\n', l); break; }
+                    part = next + 1;
+                }
+            }
+        done:
+            delete[] log;
+        }
+    }
+}
+
+static void compileglslshader(GLenum type, GLuint &obj, const char *def, const char *name, bool msg = true) 
+{
+    const char *source = def + strspn(def, " \t\r\n");
+    const char *parts[16];
+    int numparts = 0;
+    static const struct { int version; const char * const header; } glslversions[] =
+    {
+        { 330, "#version 330\n" },
+        { 150, "#version 150\n" },
+        { 130, "#version 130\n" },
+        { 120, "#version 120\n" }
+    };
+    loopi(sizeof(glslversions)/sizeof(glslversions[0])) if(glslversion >= glslversions[i].version)
+    {
+        parts[numparts++] = glslversions[i].header;
+        break;
+    }
+    if(glslversion >= 130)
+    {
+        if(type == GL_VERTEX_SHADER) parts[numparts++] =
+            "#define attribute in\n"
+            "#define varying out\n";
+        else if(type == GL_FRAGMENT_SHADER)
+        {
+            parts[numparts++] = "#define varying in\n";
+            if(glslversion < 150) parts[numparts++] = "precision highp float;\n";
+            if(glversion >= 300) parts[numparts++] =
+                "out vec4 cube2_FragColor;\n"
+                "#define gl_FragColor cube2_FragColor\n";
+        }
+        parts[numparts++] =
+            "#define texture2D(sampler, coords) texture(sampler, coords)\n"
+            "#define texture2DProj(sampler, coords) textureProj(sampler, coords)\n"
+            "#define textureCube(sampler, coords) texture(sampler, coords)\n";
+    }
+    parts[numparts++] = source;
+
+    obj = glCreateShader_(type);
+    glShaderSource_(obj, numparts, (const GLchar **)parts, NULL);
+    glCompileShader_(obj);
+    GLint success;
+    glGetShaderiv_(obj, GL_COMPILE_STATUS, &success);
+    if(!success) 
+    {
+        if(msg) showglslinfo(type, obj, name, parts, numparts);
+        glDeleteShader_(obj);
+        obj = 0;
+    }
+    else if(dbgshader > 1 && msg) showglslinfo(type, obj, name, parts, numparts);
+}  
+
+VAR(dbgubo, 0, 0, 1);
+
+static void bindglsluniform(Shader &s, UniformLoc &u)
+{
+    u.loc = glGetUniformLocation_(s.program, u.name);
+    if(!u.blockname || !hasUBO) return;
+    GLuint bidx = glGetUniformBlockIndex_(s.program, u.blockname);
+    GLuint uidx = GL_INVALID_INDEX;
+    glGetUniformIndices_(s.program, 1, &u.name, &uidx);
+    if(bidx != GL_INVALID_INDEX && uidx != GL_INVALID_INDEX)
+    {
+        GLint sizeval = 0, offsetval = 0, strideval = 0;
+        glGetActiveUniformBlockiv_(s.program, bidx, GL_UNIFORM_BLOCK_DATA_SIZE, &sizeval);
+        if(sizeval <= 0) return;
+        glGetActiveUniformsiv_(s.program, 1, &uidx, GL_UNIFORM_OFFSET, &offsetval);
+        if(u.stride > 0)
+        {
+            glGetActiveUniformsiv_(s.program, 1, &uidx, GL_UNIFORM_ARRAY_STRIDE, &strideval);
+            if(strideval > u.stride) return;
+        }
+        u.offset = offsetval;
+        u.size = sizeval;
+        glUniformBlockBinding_(s.program, bidx, u.binding);
+        if(dbgubo) conoutf(CON_DEBUG, "UBO: %s:%s:%d, offset: %d, size: %d, stride: %d", u.name, u.blockname, u.binding, offsetval, sizeval, strideval);
+    }
+}
+
+static void linkglslprogram(Shader &s, bool msg = true)
+{
+    s.program = s.vsobj && s.psobj ? glCreateProgram_() : 0;
+    GLint success = 0;
+    if(s.program)
+    {
+        glAttachShader_(s.program, s.vsobj);
+        glAttachShader_(s.program, s.psobj);
+        uint attribs = 0;
+        loopv(s.attriblocs)
+        {
+            AttribLoc &a = s.attriblocs[i];
+            glBindAttribLocation_(s.program, a.loc, a.name);
+            attribs |= 1<<a.loc;
+        }
+        loopi(gle::MAXATTRIBS) if(!(attribs&(1<<i))) glBindAttribLocation_(s.program, i, gle::attribnames[i]);
+        if(glversion >= 300)
+        {
+            glBindFragDataLocation_(s.program, 0, "cube2_FragColor");
+        }
+        glLinkProgram_(s.program);
+        glGetProgramiv_(s.program, GL_LINK_STATUS, &success);
+    }
+    if(success)
+    {
+        glUseProgram_(s.program);
+        loopi(8)
+        {
+            static const char * const texnames[8] = { "tex0", "tex1", "tex2", "tex3", "tex4", "tex5", "tex6", "tex7" };
+            GLint loc = glGetUniformLocation_(s.program, texnames[i]);
+            if(loc != -1) glUniform1i_(loc, i);
+        }
+        loopv(s.defaultparams)
+        {
+            SlotShaderParamState &param = s.defaultparams[i];
+            param.loc = glGetUniformLocation_(s.program, param.name);
+        }
+        loopv(s.uniformlocs) bindglsluniform(s, s.uniformlocs[i]);
+        glUseProgram_(0);
+    }
+    else if(s.program)
+    {
+        if(msg) showglslinfo(GL_FALSE, s.program, s.name);
+        glDeleteProgram_(s.program);
+        s.program = 0;
+    }
+}
+
+int getlocalparam(const char *name)
+{
+    return localparams.access(name, int(localparams.numelems));
+}
+
+static int addlocalparam(Shader &s, const char *name, int loc, int size, GLenum format)
+{
+    int idx = getlocalparam(name);
+    if(idx >= s.localparamremap.length())
+    {
+        int n = idx + 1 - s.localparamremap.length();
+        memset(s.localparamremap.pad(n), 0xFF, n);
+    }
+    s.localparamremap[idx] = s.localparams.length();
+
+    LocalShaderParamState &l = s.localparams.add();
+    l.name = name;
+    l.loc = loc;
+    l.size = size;
+    l.format = format;
+    return idx;
+}
+
+GlobalShaderParamState *getglobalparam(const char *name)
+{
+    GlobalShaderParamState *param = globalparams.access(name);
+    if(!param)
+    {
+        param = &globalparams[name];
+        param->name = name;
+        memset(param->buf, -1, sizeof(param->buf));
+        param->version = -1;
+    }
+    return param;
+}
+
+static GlobalShaderParamUse *addglobalparam(Shader &s, GlobalShaderParamState *param, int loc, int size, GLenum format)
+{
+    GlobalShaderParamUse &g = s.globalparams.add();
+    g.param = param;
+    g.version = -2;
+    g.loc = loc;
+    g.size = size;
+    g.format = format;
+    return &g;
+}
+
+static void setglsluniformformat(Shader &s, const char *name, GLenum format, int size)
+{
+    switch(format)
+    {
+        case GL_FLOAT:
+        case GL_FLOAT_VEC2:
+        case GL_FLOAT_VEC3:
+        case GL_FLOAT_VEC4:
+        case GL_INT:
+        case GL_INT_VEC2:
+        case GL_INT_VEC3:
+        case GL_INT_VEC4:
+        case GL_BOOL:
+        case GL_BOOL_VEC2:
+        case GL_BOOL_VEC3:
+        case GL_BOOL_VEC4:
+        case GL_FLOAT_MAT2:
+        case GL_FLOAT_MAT3:
+        case GL_FLOAT_MAT4:
+            break;
+        default:
+            return;
+    }
+    if(!strncmp(name, "gl_", 3)) return;
+
+    int loc = glGetUniformLocation_(s.program, name);
+    if(loc < 0) return;
+    loopvj(s.defaultparams) if(s.defaultparams[j].loc == loc)
+    {
+        s.defaultparams[j].format = format;
+        return;
+    }
+    loopvj(s.uniformlocs) if(s.uniformlocs[j].loc == loc) return;
+    loopvj(s.globalparams) if(s.globalparams[j].loc == loc) return;
+    loopvj(s.localparams) if(s.localparams[j].loc == loc) return;
+
+    name = getshaderparamname(name);
+    GlobalShaderParamState *param = globalparams.access(name);
+    if(param) addglobalparam(s, param, loc, size, format);
+    else addlocalparam(s, name, loc, size, format);
+}
+
+static void allocglslactiveuniforms(Shader &s)
+{
+    GLint numactive = 0;
+    glGetProgramiv_(s.program, GL_ACTIVE_UNIFORMS, &numactive);
+    string name;
+    loopi(numactive)
+    {
+        GLsizei namelen = 0;
+        GLint size = 0;
+        GLenum format = GL_FLOAT_VEC4;
+        name[0] = '\0';
+        glGetActiveUniform_(s.program, i, sizeof(name)-1, &namelen, &size, &format, name);
+        if(namelen <= 0 || size <= 0) continue;
+        name[clamp(int(namelen), 0, (int)sizeof(name)-2)] = '\0';
+        char *brak = strchr(name, '[');
+        if(brak) *brak = '\0';
+        setglsluniformformat(s, name, format, size);
+    }
+}
+
+void Shader::allocparams(Slot *slot)
+{
+    if(slot)
+    {
+#define UNIFORMTEX(name, tmu) \
+        { \
+            loc = glGetUniformLocation_(program, name); \
+            int val = tmu; \
+            if(loc != -1) glUniform1i_(loc, val); \
+        }
+        int loc, tmu = 2;
+        if(type & SHADER_NORMALSLMS)
+        {
+            UNIFORMTEX("lmcolor", 1);
+            UNIFORMTEX("lmdir", 2);
+            tmu++;
+        }
+        else UNIFORMTEX("lightmap", 1);
+        if(type & SHADER_ENVMAP) UNIFORMTEX("envmap", tmu++);
+        UNIFORMTEX("shadowmap", 7);
+        int stex = 0;
+        loopv(slot->sts)
+        {
+            Slot::Tex &t = slot->sts[i];
+            switch(t.type)
+            {
+                case TEX_DIFFUSE: UNIFORMTEX("diffusemap", 0); break;
+                case TEX_NORMAL: UNIFORMTEX("normalmap", tmu++); break;
+                case TEX_GLOW: UNIFORMTEX("glowmap", tmu++); break;
+                case TEX_DECAL: UNIFORMTEX("decal", tmu++); break;
+                case TEX_SPEC: if(t.combined<0) UNIFORMTEX("specmap", tmu++); break;
+                case TEX_DEPTH: if(t.combined<0) UNIFORMTEX("depthmap", tmu++); break;
+                case TEX_ALPHA: if(t.combined<0) UNIFORMTEX("alphamap", tmu++); break;
+                case TEX_UNKNOWN:
+                {
+                    defformatstring(sname, "stex%d", stex++);
+                    UNIFORMTEX(sname, tmu++);
+                    break;
+                }
+            }
+        }
+    }
+    allocglslactiveuniforms(*this);
+}
+
+int GlobalShaderParamState::nextversion = 0;
+
+void GlobalShaderParamState::resetversions()
+{
+    enumerate(shaders, Shader, s,
+    {
+        loopv(s.globalparams)
+        {
+            GlobalShaderParamUse &u = s.globalparams[i];
+            if(u.version != u.param->version) u.version = -2;
+        }
+    });
+    nextversion = 0;
+    enumerate(globalparams, GlobalShaderParamState, g, { g.version = ++nextversion; });
+    enumerate(shaders, Shader, s,
+    {
+        loopv(s.globalparams)
+        {
+            GlobalShaderParamUse &u = s.globalparams[i];
+            if(u.version >= 0) u.version = u.param->version;
+        }
+    });
+}
+
+static float *findslotparam(Slot &s, const char *name, float *noval = NULL)
+{
+    loopv(s.params)
+    {
+        SlotShaderParam &param = s.params[i];
+        if(name == param.name) return param.val;
+    }
+    loopv(s.shader->defaultparams)
+    {
+        SlotShaderParamState &param = s.shader->defaultparams[i];
+        if(name == param.name) return param.val;
+    }
+    return noval;
+}
+
+static float *findslotparam(VSlot &s, const char *name, float *noval = NULL)
+{
+    loopv(s.params)
+    {
+        SlotShaderParam &param = s.params[i];
+        if(name == param.name) return param.val;
+    }
+    return findslotparam(*s.slot, name, noval);
+}
+
+static inline void setslotparam(SlotShaderParamState &l, const float *val)
+{
+    switch(l.format)
+    {
+        case GL_BOOL:
+        case GL_FLOAT:      glUniform1fv_(l.loc, 1, val); break;
+        case GL_BOOL_VEC2:
+        case GL_FLOAT_VEC2: glUniform2fv_(l.loc, 1, val); break;
+        case GL_BOOL_VEC3:
+        case GL_FLOAT_VEC3: glUniform3fv_(l.loc, 1, val); break;
+        case GL_BOOL_VEC4:
+        case GL_FLOAT_VEC4: glUniform4fv_(l.loc, 1, val); break;
+        case GL_INT:      glUniform1i_(l.loc, int(val[0])); break;
+        case GL_INT_VEC2: glUniform2i_(l.loc, int(val[0]), int(val[1])); break;
+        case GL_INT_VEC3: glUniform3i_(l.loc, int(val[0]), int(val[1]), int(val[2])); break;
+        case GL_INT_VEC4: glUniform4i_(l.loc, int(val[0]), int(val[1]), int(val[2]), int(val[3])); break;
+    }
+}
+
+#define SETSLOTPARAM(l, mask, i, val) do { \
+    if(!(mask&(1<<i))) { \
+        mask |= 1<<i; \
+        setslotparam(l, val); \
+    } \
+} while(0)
+
+#define SETSLOTPARAMS(slotparams) \
+    loopv(slotparams) \
+    { \
+        SlotShaderParam &p = slotparams[i]; \
+        if(!defaultparams.inrange(p.loc)) continue; \
+        SlotShaderParamState &l = defaultparams[p.loc]; \
+        SETSLOTPARAM(l, unimask, p.loc, p.val); \
+    }
+#define SETDEFAULTPARAMS \
+    loopv(defaultparams) \
+    { \
+        SlotShaderParamState &l = defaultparams[i]; \
+        SETSLOTPARAM(l, unimask, i, l.val); \
+    }
+
+void Shader::setslotparams(Slot &slot)
+{
+    uint unimask = 0;
+    SETSLOTPARAMS(slot.params)
+    SETDEFAULTPARAMS
+}
+
+void Shader::setslotparams(Slot &slot, VSlot &vslot)
+{
+    uint unimask = 0;
+    if(vslot.slot == &slot)
+    {
+        SETSLOTPARAMS(vslot.params)
+        SETSLOTPARAMS(slot.params)
+        SETDEFAULTPARAMS
+    }
+    else
+    {
+        SETSLOTPARAMS(slot.params)
+        SETDEFAULTPARAMS
+    }
+}
+
+void Shader::bindprograms()
+{
+    if(this == lastshader || type&(SHADER_DEFERRED|SHADER_INVALID)) return;
+    glUseProgram_(program);
+    lastshader = this;
+}
+
+bool Shader::compile()
+{
+    if(!vsstr) vsobj = !reusevs || reusevs->invalid() ? 0 : reusevs->vsobj;
+    else compileglslshader(GL_VERTEX_SHADER,   vsobj, vsstr, name, dbgshader || !variantshader);
+    if(!psstr) psobj = !reuseps || reuseps->invalid() ? 0 : reuseps->psobj;
+    else compileglslshader(GL_FRAGMENT_SHADER, psobj, psstr, name, dbgshader || !variantshader);
+    linkglslprogram(*this, !variantshader);
+    return program!=0;
+}
+
+void Shader::cleanup(bool invalid)
+{
+    detailshader = NULL;
+    used = false;
+    if(vsobj) { if(!reusevs) glDeleteShader_(vsobj); vsobj = 0; }
+    if(psobj) { if(!reuseps) glDeleteShader_(psobj); psobj = 0; }
+    if(program) { glDeleteProgram_(program); program = 0; }
+    localparams.setsize(0);
+    localparamremap.setsize(0);
+    globalparams.setsize(0);
+    if(standard || invalid)
+    {
+        type = SHADER_INVALID;
+        DELETEA(vsstr);
+        DELETEA(psstr);
+        DELETEA(defer);
+        variants.setsize(0);
+        DELETEA(variantrows);
+        defaultparams.setsize(0);
+        attriblocs.setsize(0);
+        uniformlocs.setsize(0);
+        altshader = NULL;
+        loopi(MAXSHADERDETAIL) fastshader[i] = this;
+        reusevs = reuseps = NULL;
+    }
+    else loopv(defaultparams) defaultparams[i].loc = -1;
+    owner = NULL;
+}
+
+bool Shader::isnull(const Shader *s) { return !s; }
+
+static void genattriblocs(Shader &s, const char *vs, const char *ps, Shader *reusevs, Shader *reuseps)
+{
+    static int len = strlen("//:attrib");
+    string name;
+    int loc;
+    if(reusevs) s.attriblocs = reusevs->attriblocs;
+    else while((vs = strstr(vs, "//:attrib")))
+    {
+        if(sscanf(vs, "//:attrib %100s %d", name, &loc) == 2)
+            s.attriblocs.add(AttribLoc(getshaderparamname(name), loc));
+        vs += len;
+    }
+}
+
+static void genuniformlocs(Shader &s, const char *vs, const char *ps, Shader *reusevs, Shader *reuseps)
+{
+    static int len = strlen("//:uniform");
+    string name, blockname;
+    int binding, stride;
+    if(reusevs) s.uniformlocs = reusevs->uniformlocs;
+    else while((vs = strstr(vs, "//:uniform")))
+    {
+        int numargs = sscanf(vs, "//:uniform %100s %100s %d %d", name, blockname, &binding, &stride);
+        if(numargs >= 3) s.uniformlocs.add(UniformLoc(getshaderparamname(name), getshaderparamname(blockname), binding, numargs >= 4 ? stride : 0));
+        else if(numargs >= 1) s.uniformlocs.add(UniformLoc(getshaderparamname(name)));
+        vs += len;
+    }
+}
+
+Shader *newshader(int type, const char *name, const char *vs, const char *ps, Shader *variant = NULL, int row = 0)
+{
+    if(Shader::lastshader)
+    {
+        glUseProgram_(0);
+        Shader::lastshader = NULL;
+    }
+
+    Shader *exists = shaders.access(name); 
+    char *rname = exists ? exists->name : newstring(name);
+    Shader &s = shaders[rname];
+    s.name = rname;
+    s.vsstr = newstring(vs);
+    s.psstr = newstring(ps);
+    DELETEA(s.defer);
+    s.type = type;
+    s.variantshader = variant;
+    s.standard = standardshaders;
+    if(forceshaders) s.forced = true;
+    s.reusevs = s.reuseps = NULL;
+    if(variant)
+    {
+        int row = 0, col = 0;
+        if(!vs[0] || sscanf(vs, "%d , %d", &row, &col) >= 1)
+        {
+            DELETEA(s.vsstr);
+            s.reusevs = !vs[0] ? variant : variant->getvariant(col, row);
+        }
+        row = col = 0;
+        if(!ps[0] || sscanf(ps, "%d , %d", &row, &col) >= 1)
+        {
+            DELETEA(s.psstr);
+            s.reuseps = !ps[0] ? variant : variant->getvariant(col, row);
+        }
+    }
+    if(variant) loopv(variant->defaultparams) s.defaultparams.add(variant->defaultparams[i]);
+    else loopv(slotparams) s.defaultparams.add(slotparams[i]);
+    s.attriblocs.setsize(0);
+    s.uniformlocs.setsize(0);
+    genattriblocs(s, vs, ps, s.reusevs, s.reuseps);
+    genuniformlocs(s, vs, ps, s.reusevs, s.reuseps);
+    if(!s.compile())
+    {
+        s.cleanup(true);
+        if(variant) shaders.remove(rname);
+        return NULL;
+    }
+    if(variant) variant->addvariant(row, &s);
+    s.fixdetailshader();
+    return &s;
+}
+
+void setupshaders()
+{
+    GLint val;
+    glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS, &val);
+    maxvsuniforms = val/4;
+    glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, &val);
+    maxfsuniforms = val/4;
+    if(glversion < 300)
+    {
+        glGetIntegerv(GL_MAX_VARYING_COMPONENTS, &val);
+        maxvaryings = val;
+    }
+
+    standardshaders = true;
+    nullshader = newshader(0, "<init>null",
+        "attribute vec4 vvertex;\n"
+        "void main(void) {\n"
+        "    gl_Position = vvertex;\n"
+        "}\n",
+        "void main(void) {\n"
+        "    gl_FragColor = vec4(1.0, 0.0, 1.0, 0.0);\n"
+        "}\n");
+    hudshader = newshader(0, "<init>hud",
+        "attribute vec4 vvertex, vcolor;\n"
+        "attribute vec2 vtexcoord0;\n"
+        "uniform mat4 hudmatrix;\n"
+        "varying vec2 texcoord0;\n"
+        "varying vec4 color;\n"
+        "void main(void) {\n"
+        "    gl_Position = hudmatrix * vvertex;\n"
+        "    texcoord0 = vtexcoord0;\n"
+        "    color = vcolor;\n"
+        "}\n",
+        "varying vec2 texcoord0;\n"
+        "varying vec4 color;\n"
+        "uniform sampler2D tex0;\n"
+        "void main(void) {\n"
+        "    gl_FragColor = color * texture2D(tex0, texcoord0);\n"
+        "}\n");
+    hudnotextureshader = newshader(0, "<init>hudnotexture",
+        "attribute vec4 vvertex, vcolor;\n"
+        "uniform mat4 hudmatrix;\n"
+        "varying vec4 color;\n"
+        "void main(void) {\n"
+        "    gl_Position = hudmatrix * vvertex;\n"
+        "    color = vcolor;\n"
+        "}\n",
+        "varying vec4 color;\n"
+        "void main(void) {\n"
+        "    gl_FragColor = color;\n"
+        "}\n");
+    standardshaders = false;
+
+    if(!nullshader || !hudshader || !hudnotextureshader) fatal("failed to setup shaders");
+
+    dummyslot.shader = nullshader;
+}
+
+static const char *findglslmain(const char *s)
+{
+    const char *main = strstr(s, "main");
+    if(!main) return NULL;
+    for(; main >= s; main--) switch(*main) { case '\r': case '\n': case ';': return main + 1; }
+    return s;
+}
+
+static void gengenericvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 0)
+{
+    int rowoffset = 0;
+    bool vschanged = false, pschanged = false;
+    vector<char> vsv, psv;
+    vsv.put(vs, strlen(vs)+1);
+    psv.put(ps, strlen(ps)+1);
+
+    static const int len = strlen("//:variant"), olen = strlen("override");
+    for(char *vspragma = vsv.getbuf();; vschanged = true)
+    {
+        vspragma = strstr(vspragma, "//:variant");
+        if(!vspragma) break;
+        if(sscanf(vspragma + len, "row %d", &rowoffset) == 1) continue;
+        memset(vspragma, ' ', len);
+        vspragma += len;
+        if(!strncmp(vspragma, "override", olen))
+        { 
+            memset(vspragma, ' ', olen);
+            vspragma += olen;
+            char *end = vspragma + strcspn(vspragma, "\n\r");
+            end += strspn(end, "\n\r");
+            int endlen = strcspn(end, "\n\r");
+            memset(end, ' ', endlen);
+        }
+    }
+    for(char *pspragma = psv.getbuf();; pschanged = true)
+    {
+        pspragma = strstr(pspragma, "//:variant");
+        if(!pspragma) break;
+        if(sscanf(pspragma + len, "row %d", &rowoffset) == 1) continue;
+        memset(pspragma, ' ', len);
+        pspragma += len;
+        if(!strncmp(pspragma, "override", olen))
+        { 
+            memset(pspragma, ' ', olen);
+            pspragma += olen;
+            char *end = pspragma + strcspn(pspragma, "\n\r");
+            end += strspn(end, "\n\r");
+            int endlen = strcspn(end, "\n\r");
+            memset(end, ' ', endlen);
+        }
+    }
+    row += rowoffset; 
+    if(row < 0 || row >= MAXVARIANTROWS) return;
+    int col = s.numvariants(row);
+    defformatstring(varname, "<variant:%d,%d>%s", col, row, sname);
+    string reuse;
+    if(col) formatstring(reuse, "%d", row);
+    else copystring(reuse, "");
+    newshader(s.type, varname, vschanged ? vsv.getbuf() : reuse, pschanged ? psv.getbuf() : reuse, &s, row);
+}
+
+static bool genwatervariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 2)
+{
+    if(!strstr(vs, "//:water") && !strstr(ps, "//:water")) return false;
+
+    vector<char> vsw, psw;
+
+    const char *vsmain = findglslmain(vs), *vsend = strrchr(vs, '}');
+    if(!vsmain || !vsend) return false;
+    vsw.put(vs, vsmain - vs);
+    const char *fadeparams = "\nuniform vec4 waterfadeparams;\nvarying float fadedepth;\n";
+    vsw.put(fadeparams, strlen(fadeparams));
+    vsw.put(vsmain, vsend - vsmain);
+    const char *fadedef = "\nfadedepth = vvertex.z*waterfadeparams.x + waterfadeparams.y;\n";
+    vsw.put(fadedef, strlen(fadedef));
+    vsw.put(vsend, strlen(vsend)+1);
+
+    const char *psmain = findglslmain(ps), *psend = strrchr(ps, '}');
+    if(!psmain || !psend) return false;
+    psw.put(ps, psmain - ps);
+    const char *fadeinterp = "\nvarying float fadedepth;\n";
+    psw.put(fadeinterp, strlen(fadeinterp));
+    psw.put(psmain, psend - psmain);
+    const char *fade = "\ngl_FragColor.a = fadedepth;\n";
+    psw.put(fade, strlen(fade));
+    psw.put(psend, strlen(psend)+1);
+
+    defformatstring(name, "<water>%s", sname);
+    Shader *variant = newshader(s.type, name, vsw.getbuf(), psw.getbuf(), &s, row);
+    return variant!=NULL;
+}
+
+bool minimizedynlighttcusage() { return glversion < 300 && maxvaryings < 48; }
+
+static void gendynlightvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 0)
+{
+    int numlights = minimizedynlighttcusage() ? 1 : MAXDYNLIGHTS;
+
+    const char *vspragma = strstr(vs, "//:dynlight"), *pspragma = strstr(ps, "//:dynlight");
+    if(!vspragma || !pspragma) return;
+
+    string pslight;
+    vspragma += strcspn(vspragma, "\n");
+    if(*vspragma) vspragma++;
+    
+    if(sscanf(pspragma, "//:dynlight %100s", pslight)!=1) return;
+
+    pspragma += strcspn(pspragma, "\n"); 
+    if(*pspragma) pspragma++;
+
+    const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps);
+    if(vsmain > vspragma) vsmain = vs;
+    if(psmain > pspragma) psmain = ps;
+
+    vector<char> vsdl, psdl;
+    loopi(MAXDYNLIGHTS)
+    {
+        vsdl.setsize(0);
+        psdl.setsize(0);
+        if(vsmain >= vs) vsdl.put(vs, vsmain - vs);
+        if(psmain >= ps) psdl.put(ps, psmain - ps);
+
+        defformatstring(pos, "uniform vec4 dynlightpos[%d];\n", i+1);
+        vsdl.put(pos, strlen(pos));
+        psdl.put(pos, strlen(pos));
+        defformatstring(color, "uniform vec3 dynlightcolor[%d];\n", i+1);
+        psdl.put(color, strlen(color));
+
+        loopk(min(i+1, numlights))
+        {
+            defformatstring(dir, "%sdynlight%ddir%s", !k ? "varying vec3 " : " ", k, k==i || k+1==numlights ? ";\n" : ",");
+            vsdl.put(dir, strlen(dir));
+            psdl.put(dir, strlen(dir));
+        }
+            
+        vsdl.put(vsmain, vspragma-vsmain);
+        psdl.put(psmain, pspragma-psmain);
+
+        loopk(i+1)
+        {
+            defformatstring(tc, 
+                k<numlights ? 
+                    "dynlight%ddir = vvertex.xyz*dynlightpos[%d].w + dynlightpos[%d].xyz;\n" :
+                    "vec3 dynlight%ddir = dynlight0dir*dynlightpos[%d].w + dynlightpos[%d].xyz;\n",   
+                k, k, k);
+            if(k < numlights) vsdl.put(tc, strlen(tc));
+            else psdl.put(tc, strlen(tc));
+
+            defformatstring(dl, 
+                "%s.rgb += dynlightcolor[%d] * (1.0 - clamp(dot(dynlight%ddir, dynlight%ddir), 0.0, 1.0));\n",
+                pslight, k, k, k);
+            psdl.put(dl, strlen(dl));
+        }
+
+        vsdl.put(vspragma, strlen(vspragma)+1);
+        psdl.put(pspragma, strlen(pspragma)+1);
+
+        defformatstring(name, "<dynlight %d>%s", i+1, sname);
+        Shader *variant = newshader(s.type, name, vsdl.getbuf(), psdl.getbuf(), &s, row); 
+        if(!variant) return;
+        if(row < 4) genwatervariant(s, name, vsdl.getbuf(), psdl.getbuf(), row+2);
+    }
+}
+
+static void genshadowmapvariant(Shader &s, const char *sname, const char *vs, const char *ps, int row = 1)
+{
+    const char *vspragma = strstr(vs, "//:shadowmap"), *pspragma = strstr(ps, "//:shadowmap");
+    if(!vspragma || !pspragma) return;
+
+    string pslight;
+    vspragma += strcspn(vspragma, "\n");
+    if(*vspragma) vspragma++;
+
+    if(sscanf(pspragma, "//:shadowmap %100s", pslight)!=1) return;
+
+    pspragma += strcspn(pspragma, "\n");
+    if(*pspragma) pspragma++;
+
+    const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps);
+    if(vsmain > vspragma) vsmain = vs;
+    if(psmain > pspragma) psmain = ps;
+
+    vector<char> vssm, pssm;
+    if(vsmain >= vs) vssm.put(vs, vsmain - vs);
+    if(psmain >= ps) pssm.put(ps, psmain - ps);
+
+    const char *vsdecl =
+        "uniform mat4 shadowmapproject;\n"
+        "varying vec3 shadowmaptc;\n";
+    vssm.put(vsdecl, strlen(vsdecl));
+
+    const char *psdecl =
+        "uniform sampler2D shadowmap;\n"
+        "uniform vec4 shadowmapambient;\n"
+        "varying vec3 shadowmaptc;\n";
+    pssm.put(psdecl, strlen(psdecl));
+
+    vssm.put(vsmain, vspragma-vsmain);
+    pssm.put(psmain, pspragma-psmain);
+
+    extern int smoothshadowmappeel;
+    const char *tcgen =
+        "shadowmaptc = vec3(shadowmapproject * vvertex);\n";
+    vssm.put(tcgen, strlen(tcgen));
+    const char *sm =
+        smoothshadowmappeel ? 
+            "vec4 smvals = texture2D(shadowmap, shadowmaptc.xy);\n"
+            "vec2 smdiff = clamp(smvals.xz - shadowmaptc.zz*smvals.y, 0.0, 1.0);\n"
+            "float shadowed = clamp((smdiff.x > 0.0 ? smvals.w : 0.0) - 8.0*smdiff.y, 0.0, 1.0);\n" :
+
+            "vec4 smvals = texture2D(shadowmap, shadowmaptc.xy);\n"
+            "float smtest = shadowmaptc.z*smvals.y;\n"
+            "float shadowed = smtest < smvals.x && smtest > smvals.z ? smvals.w : 0.0;\n";
+    pssm.put(sm, strlen(sm));
+    defformatstring(smlight, 
+        "%s.rgb -= shadowed*clamp(%s.rgb - shadowmapambient.rgb, 0.0, 1.0);\n",
+        pslight, pslight);
+    pssm.put(smlight, strlen(smlight));
+
+    vssm.put(vspragma, strlen(vspragma)+1);
+    pssm.put(pspragma, strlen(pspragma)+1);
+
+    defformatstring(name, "<shadowmap>%s", sname);
+    Shader *variant = newshader(s.type, name, vssm.getbuf(), pssm.getbuf(), &s, row);
+    if(!variant) return;
+    genwatervariant(s, name, vssm.getbuf(), pssm.getbuf(), row+2);
+
+    if(strstr(vs, "//:dynlight")) gendynlightvariant(s, name, vssm.getbuf(), pssm.getbuf(), row);
+}
+
+static void genfogshader(vector<char> &vsbuf, vector<char> &psbuf, const char *vs, const char *ps)
+{
+    const char *vspragma = strstr(vs, "//:fog"), *pspragma = strstr(ps, "//:fog");
+    if(!vspragma && !pspragma) return;
+    static const int pragmalen = strlen("//:fog");
+    const char *vsmain = findglslmain(vs), *vsend = strrchr(vs, '}');
+    if(vsmain && vsend)
+    {   
+        vsbuf.put(vs, vsmain - vs);
+        const char *fogparams = "\nuniform vec4 fogplane;\nvarying float fogcoord;\n";
+        vsbuf.put(fogparams, strlen(fogparams));
+        vsbuf.put(vsmain, vsend - vsmain);
+        const char *vsfog = "\nfogcoord = dot(fogplane, gl_Position);\n";
+        vsbuf.put(vsfog, strlen(vsfog));
+        vsbuf.put(vsend, strlen(vsend)+1);
+    }
+    const char *psmain = findglslmain(ps), *psend = strrchr(ps, '}');
+    if(psmain && psend)
+    {
+        psbuf.put(ps, psmain - ps);
+        const char *fogparams =
+            "\nuniform vec3 fogcolor;\n"
+            "uniform vec2 fogparams;\n"
+            "varying float fogcoord;\n";
+        psbuf.put(fogparams, strlen(fogparams));
+        psbuf.put(psmain, psend - psmain);
+        const char *psdef = "\n#define FOG_COLOR ";
+        const char *psfog = 
+            pspragma && !strncmp(pspragma+pragmalen, "rgba", 4) ? 
+                "\ngl_FragColor = mix((FOG_COLOR), gl_FragColor, clamp(fogcoord*fogparams.x + fogparams.y, 0.0, 1.0));\n" :
+                "\ngl_FragColor.rgb = mix((FOG_COLOR).rgb, gl_FragColor.rgb, clamp(fogcoord*fogparams.x + fogparams.y, 0.0, 1.0));\n";
+        int clen = 0;
+        if(pspragma)
+        {
+            pspragma += pragmalen;
+            while(iscubealpha(*pspragma)) pspragma++;
+            while(*pspragma && !iscubespace(*pspragma)) pspragma++;
+            pspragma += strspn(pspragma, " \t\v\f");
+            clen = strcspn(pspragma, "\r\n");
+        }
+        if(clen <= 0) { pspragma = "fogcolor"; clen = strlen(pspragma); }
+        psbuf.put(psdef, strlen(psdef));
+        psbuf.put(pspragma, clen);
+        psbuf.put(psfog, strlen(psfog));
+        psbuf.put(psend, strlen(psend)+1);
+    }
+}
+
+static void genuniformdefs(vector<char> &vsbuf, vector<char> &psbuf, const char *vs, const char *ps, Shader *variant = NULL)
+{
+    if(variant ? variant->defaultparams.empty() : slotparams.empty()) return;
+    const char *vsmain = findglslmain(vs), *psmain = findglslmain(ps);
+    if(!vsmain || !psmain) return;
+    vsbuf.put(vs, vsmain - vs);
+    psbuf.put(ps, psmain - ps);
+    if(variant) loopv(variant->defaultparams)
+    {
+        defformatstring(uni, "\nuniform vec4 %s;\n", variant->defaultparams[i].name);
+        vsbuf.put(uni, strlen(uni));
+        psbuf.put(uni, strlen(uni));
+    }
+    else loopv(slotparams)
+    {
+        defformatstring(uni, "\nuniform vec4 %s;\n", slotparams[i].name);
+        vsbuf.put(uni, strlen(uni));
+        psbuf.put(uni, strlen(uni));
+    }
+    vsbuf.put(vsmain, strlen(vsmain)+1);
+    psbuf.put(psmain, strlen(psmain)+1);
+}
+
+VAR(defershaders, 0, 1, 1);
+
+void defershader(int *type, const char *name, const char *contents)
+{
+    Shader *exists = shaders.access(name);
+    if(exists && !exists->invalid()) return;
+    if(!defershaders) { execute(contents); return; }
+    char *rname = exists ? exists->name : newstring(name);
+    Shader &s = shaders[rname];
+    s.name = rname;
+    DELETEA(s.defer);
+    s.defer = newstring(contents);
+    s.type = SHADER_DEFERRED | *type;
+    s.standard = standardshaders;
+}
+
+void Shader::force()
+{
+    if(!deferred() || !defer) return;
+        
+    char *cmd = defer;
+    defer = NULL;
+    bool wasstandard = standardshaders, wasforcing = forceshaders;
+    int oldflags = identflags;
+    standardshaders = standard;
+    forceshaders = false;
+    identflags &= ~IDF_PERSIST;
+    slotparams.shrink(0);
+    execute(cmd);
+    identflags = oldflags;
+    forceshaders = wasforcing;
+    standardshaders = wasstandard;
+    delete[] cmd;
+
+    if(deferred())
+    {
+        DELETEA(defer);
+        type = SHADER_INVALID;
+    }
+}
+
+void fixshaderdetail()
+{
+    // must null out separately because fixdetailshader can recursively set it
+    enumerate(shaders, Shader, s, { if(!s.forced) s.detailshader = NULL; });
+    enumerate(shaders, Shader, s, { if(s.forced) s.fixdetailshader(); }); 
+    linkslotshaders();
+}
+
+int Shader::uniformlocversion()
+{
+    static int version = 0;
+    if(++version >= 0) return version;
+    version = 0;
+    enumerate(shaders, Shader, s, { loopvj(s.uniformlocs) s.uniformlocs[j].version = -1; });
+    return version;
+}
+
+VARFP(shaderdetail, 0, MAXSHADERDETAIL, MAXSHADERDETAIL, fixshaderdetail());
+
+void Shader::fixdetailshader(bool shouldforce, bool recurse)
+{
+    Shader *alt = this;
+    detailshader = NULL;
+    do
+    {
+        Shader *cur = shaderdetail < MAXSHADERDETAIL ? alt->fastshader[shaderdetail] : alt;
+        if(cur->deferred() && shouldforce) cur->force();
+        if(!cur->invalid())
+        {
+            if(cur->deferred()) break;
+            detailshader = cur;
+            break;
+        }
+        alt = alt->altshader;
+    } while(alt && alt!=this);
+
+    if(recurse && detailshader) loopv(detailshader->variants) detailshader->variants[i]->fixdetailshader(shouldforce, false);
+}
+
+Shader *useshaderbyname(const char *name)
+{
+    Shader *s = shaders.access(name);
+    if(!s) return NULL;
+    if(!s->detailshader) s->fixdetailshader(); 
+    s->forced = true;
+    return s;
+}
+
+void shader(int *type, char *name, char *vs, char *ps)
+{
+    if(lookupshaderbyname(name)) return;
+   
+    defformatstring(info, "shader %s", name);
+    renderprogress(loadprogress, info);
+
+    vector<char> vsbuf, psbuf, vsbak, psbak;
+#define GENSHADER(cond, body) \
+    if(cond) \
+    { \
+        if(vsbuf.length()) { vsbak.setsize(0); vsbak.put(vs, strlen(vs)+1); vs = vsbak.getbuf(); vsbuf.setsize(0); } \
+        if(psbuf.length()) { psbak.setsize(0); psbak.put(ps, strlen(ps)+1); ps = psbak.getbuf(); psbuf.setsize(0); } \
+        body; \
+        if(vsbuf.length()) vs = vsbuf.getbuf(); \
+        if(psbuf.length()) ps = psbuf.getbuf(); \
+    }
+    GENSHADER(slotparams.length(), genuniformdefs(vsbuf, psbuf, vs, ps));
+    GENSHADER(strstr(vs, "//:fog") || strstr(ps, "//:fog"), genfogshader(vsbuf, psbuf, vs, ps)); 
+    Shader *s = newshader(*type, name, vs, ps);
+    if(s)
+    {
+        if(strstr(vs, "//:water")) genwatervariant(*s, s->name, vs, ps);
+        if(strstr(vs, "//:shadowmap")) genshadowmapvariant(*s, s->name, vs, ps);
+        if(strstr(vs, "//:dynlight")) gendynlightvariant(*s, s->name, vs, ps);
+    }
+    slotparams.shrink(0);
+}
+
+void variantshader(int *type, char *name, int *row, char *vs, char *ps)
+{
+    if(*row < 0)
+    {
+        shader(type, name, vs, ps);
+        return;
+    }
+    else if(*row >= MAXVARIANTROWS) return;
+
+    Shader *s = lookupshaderbyname(name);
+    if(!s) return;
+
+    defformatstring(varname, "<variant:%d,%d>%s", s->numvariants(*row), *row, name);
+    //defformatstring(info, "shader %s", varname);
+    //renderprogress(loadprogress, info);
+    vector<char> vsbuf, psbuf, vsbak, psbak;
+    GENSHADER(s->defaultparams.length(), genuniformdefs(vsbuf, psbuf, vs, ps, s));
+    GENSHADER(strstr(vs, "//:fog") || strstr(ps, "//:fog"), genfogshader(vsbuf, psbuf, vs, ps));
+    Shader *v = newshader(*type, varname, vs, ps, s, *row);
+    if(v)
+    {
+        if(strstr(vs, "//:dynlight")) gendynlightvariant(*s, varname, vs, ps, *row);
+        if(strstr(ps, "//:variant") || strstr(vs, "//:variant")) gengenericvariant(*s, varname, vs, ps, *row);
+    }
+}
+
+void setshader(char *name)
+{
+    slotparams.shrink(0);
+    Shader *s = shaders.access(name);
+    if(!s)
+    {
+        conoutf(CON_ERROR, "no such shader: %s", name);
+    }
+    else slotshader = s;
+}
+
+void resetslotshader()
+{
+    slotshader = NULL;
+    slotparams.shrink(0);
+}
+
+void setslotshader(Slot &s)
+{
+    s.shader = slotshader;
+    if(!s.shader)
+    {
+        s.shader = stdworldshader;
+        return;
+    }
+    loopv(slotparams) s.params.add(slotparams[i]);
+}
+
+static void linkslotshaderparams(vector<SlotShaderParam> &params, Shader *sh, bool load)
+{
+    if(sh) loopv(params)
+    {
+        int loc = -1;
+        SlotShaderParam &param = params[i];
+        loopv(sh->defaultparams)
+        {
+            SlotShaderParamState &dparam = sh->defaultparams[i];
+            if(dparam.name==param.name)
+            {
+                if(memcmp(param.val, dparam.val, sizeof(param.val))) loc = i;
+                break;
+            }
+        }
+        param.loc = loc;
+    }
+    else if(load) loopv(params) params[i].loc = -1;
+}
+
+void linkslotshader(Slot &s, bool load)
+{
+    if(!s.shader) return;
+
+    if(load && !s.shader->detailshader) s.shader->fixdetailshader();
+
+    linkslotshaderparams(s.params, s.shader->detailshader, load);
+}
+
+void linkvslotshader(VSlot &s, bool load)
+{
+    if(!s.slot->shader) return;
+
+    Shader *sh = s.slot->shader->detailshader;
+    linkslotshaderparams(s.params, sh, load);
+
+    if(!sh) return;
+
+    if(s.slot->texmask&(1<<TEX_GLOW))
+    {
+        static const char *paramname = getshaderparamname("glowcolor");
+        const float *param = findslotparam(s, paramname);
+        if(param) s.glowcolor = vec(param).clamp(0, 1);
+    }
+}
+
+void altshader(char *origname, char *altname)
+{
+    Shader *orig = shaders.access(origname), *alt = shaders.access(altname);
+    if(!orig || !alt) return;
+    orig->altshader = alt;
+    orig->fixdetailshader(false);
+}
+
+void fastshader(char *nice, char *fast, int *detail)
+{
+    Shader *ns = shaders.access(nice), *fs = shaders.access(fast);
+    if(!ns || !fs) return;
+    loopi(min(*detail+1, MAXSHADERDETAIL)) ns->fastshader[i] = fs;
+    ns->fixdetailshader(false);
+}
+
+COMMAND(shader, "isss");
+COMMAND(variantshader, "isiss");
+COMMAND(setshader, "s");
+COMMAND(altshader, "ss");
+COMMAND(fastshader, "ssi");
+COMMAND(defershader, "iss");
+ICOMMAND(forceshader, "s", (const char *name), useshaderbyname(name));
+
+ICOMMAND(isshaderdefined, "s", (char *name), intret(lookupshaderbyname(name) ? 1 : 0));
+
+static hashset<const char *> shaderparamnames(256);
+
+const char *getshaderparamname(const char *name, bool insert)
+{
+    const char *exists = shaderparamnames.find(name, NULL);
+    if(exists || !insert) return exists;
+    return shaderparamnames.add(newstring(name));
+}
+
+void addslotparam(const char *name, float x, float y, float z, float w)
+{
+    if(name) name = getshaderparamname(name);
+    loopv(slotparams)
+    {
+        SlotShaderParam &param = slotparams[i];
+        if(param.name==name)
+        {
+            param.val[0] = x;
+            param.val[1] = y;
+            param.val[2] = z;
+            param.val[3] = w;
+            return;
+        }
+    }
+    SlotShaderParam param = {name, -1, {x, y, z, w}};
+    slotparams.add(param);
+}
+
+ICOMMAND(setuniformparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w));
+ICOMMAND(setshaderparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w));
+ICOMMAND(defuniformparam, "sffff", (char *name, float *x, float *y, float *z, float *w), addslotparam(name, *x, *y, *z, *w));
+
+#define NUMPOSTFXBINDS 10
+
+struct postfxtex
+{
+    GLuint id;
+    int scale, used;
+
+    postfxtex() : id(0), scale(0), used(-1) {}
+};
+vector<postfxtex> postfxtexs;
+int postfxbinds[NUMPOSTFXBINDS];
+GLuint postfxfb = 0;
+int postfxw = 0, postfxh = 0;
+
+struct postfxpass
+{
+    Shader *shader;
+    vec4 params;
+    uint inputs, freeinputs;
+    int outputbind, outputscale;
+
+    postfxpass() : shader(NULL), inputs(1), freeinputs(1), outputbind(0), outputscale(0) {}
+};
+vector<postfxpass> postfxpasses;
+
+static int allocatepostfxtex(int scale)
+{
+    loopv(postfxtexs)
+    {
+        postfxtex &t = postfxtexs[i];
+        if(t.scale==scale && t.used < 0) return i; 
+    }
+    postfxtex &t = postfxtexs.add();
+    t.scale = scale;
+    glGenTextures(1, &t.id);
+    createtexture(t.id, max(screenw>>scale, 1), max(screenh>>scale, 1), NULL, 3, 1, GL_RGB);
+    return postfxtexs.length()-1;
+}
+
+void cleanuppostfx(bool fullclean)
+{
+    if(fullclean && postfxfb)
+    {
+        glDeleteFramebuffers_(1, &postfxfb);
+        postfxfb = 0;
+    }
+
+    loopv(postfxtexs) glDeleteTextures(1, &postfxtexs[i].id);
+    postfxtexs.shrink(0);
+
+    postfxw = 0;
+    postfxh = 0;
+}
+
+void renderpostfx()
+{
+    if(postfxpasses.empty()) return;
+
+    if(postfxw != screenw || postfxh != screenh) 
+    {
+        cleanuppostfx(false);
+        postfxw = screenw;
+        postfxh = screenh;
+    }
+
+    int binds[NUMPOSTFXBINDS];
+    loopi(NUMPOSTFXBINDS) binds[i] = -1;
+    loopv(postfxtexs) postfxtexs[i].used = -1;
+
+    binds[0] = allocatepostfxtex(0);
+    postfxtexs[binds[0]].used = 0;
+    glBindTexture(GL_TEXTURE_2D, postfxtexs[binds[0]].id);
+    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, screenw, screenh);
+
+    if(postfxpasses.length() > 1)
+    {
+        if(!postfxfb) glGenFramebuffers_(1, &postfxfb);
+        glBindFramebuffer_(GL_FRAMEBUFFER, postfxfb);
+    }
+
+    GLOBALPARAMF(millis, lastmillis/1000.0f);
+
+    loopv(postfxpasses)
+    {
+        postfxpass &p = postfxpasses[i];
+
+        int tex = -1;
+        if(!postfxpasses.inrange(i+1))
+        {
+            if(postfxpasses.length() > 1) glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+        }
+        else
+        {
+            tex = allocatepostfxtex(p.outputscale);
+            glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, postfxtexs[tex].id, 0);
+        }
+
+        int w = tex >= 0 ? max(screenw>>postfxtexs[tex].scale, 1) : screenw, 
+            h = tex >= 0 ? max(screenh>>postfxtexs[tex].scale, 1) : screenh;
+        glViewport(0, 0, w, h);
+        p.shader->set();
+        LOCALPARAM(params, p.params);
+        int tw = w, th = h, tmu = 0;
+        loopj(NUMPOSTFXBINDS) if(p.inputs&(1<<j) && binds[j] >= 0)
+        {
+            if(!tmu)
+            {
+                tw = max(screenw>>postfxtexs[binds[j]].scale, 1);
+                th = max(screenh>>postfxtexs[binds[j]].scale, 1);
+            }
+            else glActiveTexture_(GL_TEXTURE0 + tmu);
+            glBindTexture(GL_TEXTURE_2D, postfxtexs[binds[j]].id);
+            ++tmu;
+        }
+        if(tmu) glActiveTexture_(GL_TEXTURE0);
+        LOCALPARAMF(postfxscale, 1.0f/tw, 1.0f/th);
+        screenquad(1, 1);
+
+        loopj(NUMPOSTFXBINDS) if(p.freeinputs&(1<<j) && binds[j] >= 0)
+        {
+            postfxtexs[binds[j]].used = -1;
+            binds[j] = -1;
+        }
+        if(tex >= 0)
+        {
+            if(binds[p.outputbind] >= 0) postfxtexs[binds[p.outputbind]].used = -1;
+            binds[p.outputbind] = tex;
+            postfxtexs[tex].used = p.outputbind;
+        }
+    }
+}
+
+static bool addpostfx(const char *name, int outputbind, int outputscale, uint inputs, uint freeinputs, const vec4 &params)
+{
+    if(!*name) return false;
+    Shader *s = useshaderbyname(name);
+    if(!s)
+    {
+        conoutf(CON_ERROR, "no such postfx shader: %s", name);
+        return false;
+    }
+    postfxpass &p = postfxpasses.add();
+    p.shader = s;
+    p.outputbind = outputbind;
+    p.outputscale = outputscale;
+    p.inputs = inputs;
+    p.freeinputs = freeinputs;
+    p.params = params;
+    return true;
+}
+
+void clearpostfx()
+{
+    postfxpasses.shrink(0);
+    cleanuppostfx(false);
+}
+
+COMMAND(clearpostfx, "");
+
+ICOMMAND(addpostfx, "siisffff", (char *name, int *bind, int *scale, char *inputs, float *x, float *y, float *z, float *w),
+{
+    int inputmask = inputs[0] ? 0 : 1;
+    int freemask = inputs[0] ? 0 : 1;
+    bool freeinputs = true;
+    for(; *inputs; inputs++) if(isdigit(*inputs)) 
+    {
+        inputmask |= 1<<(*inputs-'0');
+        if(freeinputs) freemask |= 1<<(*inputs-'0');
+    }
+    else if(*inputs=='+') freeinputs = false;
+    else if(*inputs=='-') freeinputs = true;
+    inputmask &= (1<<NUMPOSTFXBINDS)-1;
+    freemask &= (1<<NUMPOSTFXBINDS)-1;
+    addpostfx(name, clamp(*bind, 0, NUMPOSTFXBINDS-1), max(*scale, 0), inputmask, freemask, vec4(*x, *y, *z, *w));
+});
+
+ICOMMAND(setpostfx, "sffff", (char *name, float *x, float *y, float *z, float *w),
+{
+    clearpostfx();
+    if(name[0]) addpostfx(name, 0, 0, 1, 1, vec4(*x, *y, *z, *w));
+});
+
+void cleanupshaders()
+{
+    cleanuppostfx(true);
+
+    loadedshaders = false;
+    nullshader = hudshader = hudnotextureshader = textureshader = notextureshader = nocolorshader = foggedshader = foggednotextureshader = stdworldshader = NULL;
+    enumerate(shaders, Shader, s, s.cleanup());
+    Shader::lastshader = NULL;
+    glUseProgram_(0);
+}
+
+void reloadshaders()
+{
+    identflags &= ~IDF_PERSIST;
+    loadshaders();
+    identflags |= IDF_PERSIST;
+    linkslotshaders();
+    enumerate(shaders, Shader, s, 
+    {
+        if(!s.standard && !(s.type&(SHADER_DEFERRED|SHADER_INVALID)) && !s.variantshader) 
+        {
+            defformatstring(info, "shader %s", s.name);
+            renderprogress(0.0, info);
+            if(!s.compile()) s.cleanup(true);
+            loopv(s.variants)
+            {
+                Shader *v = s.variants[i];
+                if((v->reusevs && v->reusevs->invalid()) || 
+                   (v->reuseps && v->reuseps->invalid()) ||
+                   !v->compile())
+                    v->cleanup(true);
+            }
+        }
+        if(s.forced && !s.detailshader) s.fixdetailshader();
+    });
+}
+
+void setupblurkernel(int radius, float sigma, float *weights, float *offsets)
+{
+    if(radius<1 || radius>MAXBLURRADIUS) return;
+    sigma *= 2*radius;
+    float total = 1.0f/sigma;
+    weights[0] = total;
+    offsets[0] = 0;
+    // rely on bilinear filtering to sample 2 pixels at once
+    // transforms a*X + b*Y into (u+v)*[X*u/(u+v) + Y*(1 - u/(u+v))]
+    loopi(radius)
+    {
+        float weight1 = exp(-((2*i)*(2*i)) / (2*sigma*sigma)) / sigma,
+              weight2 = exp(-((2*i+1)*(2*i+1)) / (2*sigma*sigma)) / sigma,
+              scale = weight1 + weight2,
+              offset = 2*i+1 + weight2 / scale;
+        weights[i+1] = scale;
+        offsets[i+1] = offset;
+        total += 2*scale;
+    }
+    loopi(radius+1) weights[i] /= total;
+    for(int i = radius+1; i <= MAXBLURRADIUS; i++) weights[i] = offsets[i] = 0;
+}
+
+void setblurshader(int pass, int size, int radius, float *weights, float *offsets)
+{
+    if(radius<1 || radius>MAXBLURRADIUS) return; 
+    static Shader *blurshader[7][2] = { { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL }, { NULL, NULL } };
+    Shader *&s = blurshader[radius-1][pass];
+    if(!s)
+    {
+        defformatstring(name, "blur%c%d", 'x'+pass, radius);
+        s = lookupshaderbyname(name);
+    }
+    s->set();
+    LOCALPARAMV(weights, weights, 8);
+    float scaledoffsets[8];
+    loopk(8) scaledoffsets[k] = offsets[k]/size;
+    LOCALPARAMV(offsets, scaledoffsets, 8);
+}
+
diff --git a/src/engine/shadowmap.cpp b/src/engine/shadowmap.cpp
new file mode 100644 (file)
index 0000000..4dafbd8
--- /dev/null
@@ -0,0 +1,329 @@
+#include "engine.h"
+#include "rendertarget.h"
+
+VARP(shadowmap, 0, 0, 1);
+
+extern void cleanshadowmap();
+VARFP(shadowmapsize, 7, 9, 11, cleanshadowmap());
+VARP(shadowmapradius, 64, 96, 256);
+VAR(shadowmapheight, 0, 32, 128);
+VARP(shadowmapdist, 128, 256, 512);
+VARFP(fpshadowmap, 0, 0, 1, cleanshadowmap());
+VARFP(shadowmapprecision, 0, 0, 1, cleanshadowmap());
+bvec shadowmapambientcolor(0, 0, 0);
+HVARFR(shadowmapambient, 0, 0, 0xFFFFFF,
+{
+    if(shadowmapambient <= 255) shadowmapambient |= (shadowmapambient<<8) | (shadowmapambient<<16);
+    shadowmapambientcolor = bvec((shadowmapambient>>16)&0xFF, (shadowmapambient>>8)&0xFF, shadowmapambient&0xFF);
+});
+VARP(shadowmapintensity, 0, 40, 100);
+
+VARP(blurshadowmap, 0, 1, 3);
+VARP(blursmsigma, 1, 100, 200);
+
+#define SHADOWSKEW 0.7071068f
+
+vec shadowoffset(0, 0, 0), shadowfocus(0, 0, 0), shadowdir(0, SHADOWSKEW, 1);
+VAR(shadowmapcasters, 1, 0, 0);
+float shadowmapmaxz = 0;
+
+void setshadowdir(int angle)
+{
+    shadowdir = vec(0, SHADOWSKEW, 1);
+    shadowdir.rotate_around_z(angle*RAD);
+}
+
+VARFR(shadowmapangle, 0, 0, 360, setshadowdir(shadowmapangle));
+
+void guessshadowdir()
+{
+    if(shadowmapangle) return;
+    vec dir;
+    if(!sunlightcolor.iszero()) dir = sunlightdir;
+    else
+    {
+        vec lightpos(0, 0, 0), casterpos(0, 0, 0);
+        int numlights = 0, numcasters = 0;
+        const vector<extentity *> &ents = entities::getents();
+        loopv(ents)
+        {
+            extentity &e = *ents[i];
+            switch(e.type)
+            {
+                case ET_LIGHT:
+                    if(!e.attr1) { lightpos.add(e.o); numlights++; }
+                    break;
+
+                case ET_MAPMODEL:
+                    casterpos.add(e.o);
+                    numcasters++;
+                    break;
+
+                default:
+                    if(e.type<ET_GAMESPECIFIC) break;
+                    casterpos.add(e.o);
+                    numcasters++;
+                    break;
+            }
+        }
+        if(!numlights || !numcasters) return;
+        lightpos.div(numlights);
+        casterpos.div(numcasters);
+        dir = vec(lightpos).sub(casterpos);
+    }
+    dir.z = 0;
+    if(dir.iszero()) return;
+    dir.normalize();
+    dir.mul(SHADOWSKEW);
+    dir.z = 1;
+    shadowdir = dir;
+}
+
+bool shadowmapping = false;
+
+matrix4 shadowmatrix;
+
+VARP(shadowmapbias, 0, 5, 1024);
+VARP(shadowmappeelbias, 0, 20, 1024);
+VAR(smdepthpeel, 0, 1, 1);
+VAR(smoothshadowmappeel, 1, 0, 0);
+
+static struct shadowmaptexture : rendertarget
+{
+    const GLenum *colorformats() const
+    {
+        static const GLenum rgbafmts[] = { GL_RGBA16F, GL_RGBA16, GL_RGBA, GL_RGBA8, GL_FALSE };
+        return &rgbafmts[fpshadowmap && hasTF ? 0 : (shadowmapprecision ? 1 : 2)];
+    }
+
+    bool swaptexs() const { return true; }
+
+    bool scissorblur(int &x, int &y, int &w, int &h)
+    {
+        x = max(int(floor((scissorx1+1)/2*vieww)) - 2*blursize, 2);
+        y = max(int(floor((scissory1+1)/2*viewh)) - 2*blursize, 2);
+        w = min(int(ceil((scissorx2+1)/2*vieww)) + 2*blursize, vieww-2) - x;
+        h = min(int(ceil((scissory2+1)/2*viewh)) + 2*blursize, viewh-2) - y;
+        return true;
+    }
+
+    bool scissorrender(int &x, int &y, int &w, int &h)
+    {
+        x = y = 2;
+        w = vieww - 2*2;
+        h = viewh - 2*2;
+        return true;
+    }
+
+    void doclear()
+    {
+        glClearColor(0, 0, 0, 0);
+        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
+    }
+
+    bool dorender()
+    {
+        vec skewdir(shadowdir);
+        skewdir.rotate_around_z(-camera1->yaw*RAD);
+
+        vec dir;
+        vecfromyawpitch(camera1->yaw, camera1->pitch, 1, 0, dir);
+        dir.z = 0;
+        dir.mul(shadowmapradius);
+
+        vec dirx, diry;
+        vecfromyawpitch(camera1->yaw, 0, 0, 1, dirx);
+        vecfromyawpitch(camera1->yaw, 0, 1, 0, diry);
+        shadowoffset.x = -fmod(dirx.dot(camera1->o) - skewdir.x*camera1->o.z, 2.0f*shadowmapradius/vieww);
+        shadowoffset.y = -fmod(diry.dot(camera1->o) - skewdir.y*camera1->o.z, 2.0f*shadowmapradius/viewh);
+
+        shadowmatrix.ortho(-shadowmapradius, shadowmapradius, -shadowmapradius, shadowmapradius, -shadowmapdist, shadowmapdist);
+        shadowmatrix.mul(matrix3(vec(1, 0, 0), vec(0, 1, 0), vec(skewdir.x, skewdir.y, 1)));
+        shadowmatrix.translate(skewdir.x*shadowmapheight + shadowoffset.x, skewdir.y*shadowmapheight + shadowoffset.y + dir.magnitude(), -shadowmapheight);
+        shadowmatrix.rotate_around_z((camera1->yaw+180)*-RAD);
+        shadowmatrix.translate(vec(camera1->o).neg());
+        GLOBALPARAM(shadowmatrix, shadowmatrix);
+
+        shadowfocus = camera1->o;
+        shadowfocus.add(dir);
+        shadowfocus.add(vec(shadowdir).mul(shadowmapheight));
+        shadowfocus.add(dirx.mul(shadowoffset.x));
+        shadowfocus.add(diry.mul(shadowoffset.y));
+
+        gle::colorf(0, 0, 0);
+
+        GLOBALPARAMF(shadowmapbias, -shadowmapbias/float(shadowmapdist), 1 - (shadowmapbias + (smoothshadowmappeel ? 0 : shadowmappeelbias))/float(shadowmapdist));
+
+        shadowmapcasters = 0;
+        shadowmapmaxz = shadowfocus.z - shadowmapdist;
+        shadowmapping = true;
+        rendergame();
+        shadowmapping = false;
+        shadowmapmaxz = min(shadowmapmaxz, shadowfocus.z);
+
+        if(shadowmapcasters && smdepthpeel) 
+        {
+            int sx, sy, sw, sh;
+            bool scissoring = rtscissor && scissorblur(sx, sy, sw, sh) && sw > 0 && sh > 0;
+            if(scissoring) glScissor(sx, sy, sw, sh);
+            if(!rtscissor || scissoring) rendershadowmapreceivers();
+        }
+
+        return shadowmapcasters>0;
+    }
+
+    bool flipdebug() const { return false; }
+
+    void dodebug(int w, int h)
+    {
+        if(shadowmapcasters)
+        {
+            glColorMask(GL_TRUE, GL_FALSE, GL_FALSE, GL_FALSE);
+            debugscissor(w, h);
+            glColorMask(GL_FALSE, GL_FALSE, GL_TRUE, GL_FALSE);
+            debugblurtiles(w, h);
+            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+        }
+    }
+} shadowmaptex;
+
+void cleanshadowmap()
+{
+    shadowmaptex.cleanup(true);
+}
+
+void calcshadowmapbb(const vec &o, float xyrad, float zrad, float &x1, float &y1, float &x2, float &y2)
+{
+    vec skewdir(shadowdir);
+    skewdir.rotate_around_z(-camera1->yaw*RAD);
+
+    vec ro(o);
+    ro.sub(camera1->o);
+    ro.rotate_around_z(-(camera1->yaw+180)*RAD);
+    ro.x += ro.z * skewdir.x + shadowoffset.x;
+    ro.y += ro.z * skewdir.y + shadowmapradius * cosf(camera1->pitch*RAD) + shadowoffset.y;
+
+    vec high(ro), low(ro);
+    high.x += zrad * skewdir.x;
+    high.y += zrad * skewdir.y;
+    low.x -= zrad * skewdir.x;
+    low.y -= zrad * skewdir.y;
+
+    x1 = (min(high.x, low.x) - xyrad) / shadowmapradius;
+    y1 = (min(high.y, low.y) - xyrad) / shadowmapradius;
+    x2 = (max(high.x, low.x) + xyrad) / shadowmapradius;
+    y2 = (max(high.y, low.y) + xyrad) / shadowmapradius;
+}
+
+bool addshadowmapcaster(const vec &o, float xyrad, float zrad)
+{
+    if(o.z + zrad <= shadowfocus.z - shadowmapdist || o.z - zrad >= shadowfocus.z) return false;
+
+    shadowmapmaxz = max(shadowmapmaxz, o.z + zrad);
+
+    float x1, y1, x2, y2;
+    calcshadowmapbb(o, xyrad, zrad, x1, y1, x2, y2);
+
+    if(!shadowmaptex.addblurtiles(x1, y1, x2, y2, 2)) return false;
+
+    shadowmapcasters++;
+    return true;
+}
+
+bool isshadowmapreceiver(vtxarray *va)
+{
+    if(!shadowmap || !shadowmapcasters) return false;
+
+    if(va->shadowmapmax.z <= shadowfocus.z - shadowmapdist || va->shadowmapmin.z >= shadowmapmaxz) return false;
+
+    float xyrad = SQRT2*0.5f*max(va->shadowmapmax.x-va->shadowmapmin.x, va->shadowmapmax.y-va->shadowmapmin.y),
+          zrad = 0.5f*(va->shadowmapmax.z-va->shadowmapmin.z),
+          x1, y1, x2, y2;
+    if(xyrad<0 || zrad<0) return false;
+
+    vec center = vec(va->shadowmapmin).add(vec(va->shadowmapmax)).mul(0.5f);
+    calcshadowmapbb(center, xyrad, zrad, x1, y1, x2, y2);
+
+    return shadowmaptex.checkblurtiles(x1, y1, x2, y2, 2);
+
+#if 0
+    // cheaper inexact test
+    float dz = va->o.z + va->size/2 - shadowfocus.z;
+    float cx = shadowfocus.x + dz*shadowdir.x, cy = shadowfocus.y + dz*shadowdir.y;
+    float skew = va->size/2*SHADOWSKEW;
+    if(!shadowmap || !shadowmaptex ||
+       va->o.z + va->size <= shadowfocus.z - shadowmapdist || va->o.z >= shadowmapmaxz ||
+       va->o.x + va->size <= cx - shadowmapradius-skew || va->o.x >= cx + shadowmapradius+skew || 
+       va->o.y + va->size <= cy - shadowmapradius-skew || va->o.y >= cy + shadowmapradius+skew) 
+        return false;
+    return true;
+#endif
+}
+
+bool isshadowmapcaster(const vec &o, float rad)
+{
+    // cheaper inexact test
+    float dz = o.z - shadowfocus.z;
+    float cx = shadowfocus.x + dz*shadowdir.x, cy = shadowfocus.y + dz*shadowdir.y;
+    float skew = rad*SHADOWSKEW;
+    if(!shadowmapping ||
+       o.z + rad <= shadowfocus.z - shadowmapdist || o.z - rad >= shadowfocus.z ||
+       o.x + rad <= cx - shadowmapradius-skew || o.x - rad >= cx + shadowmapradius+skew ||
+       o.y + rad <= cy - shadowmapradius-skew || o.y - rad >= cy + shadowmapradius+skew)
+        return false;
+    return true;
+}
+
+void pushshadowmap()
+{
+    if(!shadowmap || !shadowmaptex.rendertex) return;
+
+    glActiveTexture_(GL_TEXTURE7);
+    glBindTexture(GL_TEXTURE_2D, shadowmaptex.rendertex);
+
+    matrix4 m = shadowmatrix;
+    m.projective(-1, 1-shadowmapbias/float(shadowmapdist));
+    GLOBALPARAM(shadowmapproject, m);
+
+    glActiveTexture_(GL_TEXTURE0);
+
+    float r, g, b;
+       if(!shadowmapambient)
+       {
+               if(skylightcolor[0] || skylightcolor[1] || skylightcolor[2])
+               {
+                       r = max(25.0f, 0.4f*ambientcolor[0] + 0.6f*max(ambientcolor[0], skylightcolor[0]));
+                       g = max(25.0f, 0.4f*ambientcolor[1] + 0.6f*max(ambientcolor[1], skylightcolor[1]));
+                       b = max(25.0f, 0.4f*ambientcolor[2] + 0.6f*max(ambientcolor[2], skylightcolor[2]));
+               }
+               else 
+        {
+            r = max(25.0f, 2.0f*ambientcolor[0]);
+            g = max(25.0f, 2.0f*ambientcolor[1]);
+            b = max(25.0f, 2.0f*ambientcolor[2]);
+        }
+       }
+    else { r = shadowmapambientcolor[0]; g = shadowmapambientcolor[1]; b = shadowmapambientcolor[2]; }
+    GLOBALPARAMF(shadowmapambient, r/255.0f, g/255.0f, b/255.0f);
+}
+
+void popshadowmap()
+{
+    if(!shadowmap || !shadowmaptex.rendertex) return;
+}
+
+void rendershadowmap()
+{
+    if(!shadowmap) return;
+
+    shadowmaptex.render(1<<shadowmapsize, 1<<shadowmapsize, blurshadowmap, blursmsigma/100.0f);
+}
+
+VAR(debugsm, 0, 0, 1);
+
+void viewshadowmap()
+{
+    if(!shadowmap) return;
+    shadowmaptex.debug();
+}
+
diff --git a/src/engine/skelmodel.h b/src/engine/skelmodel.h
new file mode 100644 (file)
index 0000000..6d36b35
--- /dev/null
@@ -0,0 +1,1861 @@
+VARP(gpuskel, 0, 1, 1);
+
+VAR(maxskelanimdata, 1, 192, 0);
+VAR(testtags, 0, 0, 1);
+
+#define BONEMASK_NOT  0x8000
+#define BONEMASK_END  0xFFFF
+#define BONEMASK_BONE 0x7FFF
+
+struct skelmodel : animmodel
+{
+    struct vert { vec pos, norm; vec2 tc; int blend, interpindex; };
+    struct vvert { vec pos; vec2 tc; };
+    struct vvertn : vvert { vec norm; };
+    struct vvertbump : vvert { squat tangent; };
+    struct vvertw { uchar weights[4]; uchar bones[4]; };
+    struct vvertnw : vvertn, vvertw {};
+    struct vvertbumpw : vvertbump, vvertw {};
+    struct bumpvert { quat tangent; };
+    struct tri { ushort vert[3]; };
+
+    struct blendcombo
+    {
+        int uses, interpindex;
+        float weights[4];
+        uchar bones[4], interpbones[4];
+
+        blendcombo() : uses(1)
+        {
+        }
+
+        bool operator==(const blendcombo &c) const
+        {
+            loopk(4) if(bones[k] != c.bones[k]) return false;
+            loopk(4) if(weights[k] != c.weights[k]) return false;
+            return true;
+        }
+
+        int size() const
+        {
+            int i = 1;
+            while(i < 4 && weights[i]) i++;
+            return i;
+        }
+
+        static bool sortcmp(const blendcombo &x, const blendcombo &y)
+        {
+            loopi(4)
+            {
+                if(x.weights[i])
+                {
+                    if(!y.weights[i]) return true;
+                }
+                else if(y.weights[i]) return false;
+                else break;
+            }
+            return false;
+        }
+
+        int addweight(int sorted, float weight, int bone)
+        {
+            if(weight <= 1e-3f) return sorted;
+            loopk(sorted) if(weight > weights[k])
+            {
+                for(int l = min(sorted-1, 2); l >= k; l--)
+                {
+                    weights[l+1] = weights[l];
+                    bones[l+1] = bones[l];
+                }
+                weights[k] = weight;
+                bones[k] = bone;
+                return sorted<4 ? sorted+1 : sorted;
+            }
+            if(sorted>=4) return sorted;
+            weights[sorted] = weight;
+            bones[sorted] = bone;
+            return sorted+1;
+        }
+        
+        void finalize(int sorted)
+        {
+            loopj(4-sorted) { weights[sorted+j] = 0; bones[sorted+j] = 0; }
+            if(sorted <= 0) return;
+            float total = 0;
+            loopj(sorted) total += weights[j];
+            total = 1.0f/total;
+            loopj(sorted) weights[j] *= total;
+        }
+
+        void serialize(vvertw &v)
+        {
+            if(interpindex >= 0)
+            {
+                v.weights[0] = 255;
+                loopk(3) v.weights[k+1] = 0;
+                v.bones[0] = 2*interpindex;
+                loopk(3) v.bones[k+1] = v.bones[0];
+            }
+            else
+            {
+                int total = 0;
+                loopk(4) total += (v.weights[k] = uchar(0.5f + weights[k]*255));
+                while(total > 255)
+                {
+                    loopk(4) if(v.weights[k] > 0 && total > 255) { v.weights[k]--; total--; } 
+                }
+                while(total < 255)
+                {
+                    loopk(4) if(v.weights[k] < 255 && total < 255) { v.weights[k]++; total++; }
+                }
+                loopk(4) v.bones[k] = 2*interpbones[k];
+            }
+        }
+    };
+
+
+    struct animcacheentry
+    {
+        animstate as[MAXANIMPARTS];
+        float pitch;
+        int millis;
+        uchar *partmask;
+        ragdolldata *ragdoll;
+
+        animcacheentry() : ragdoll(NULL)
+        {
+            loopk(MAXANIMPARTS) as[k].cur.fr1 = as[k].prev.fr1 = -1;
+        }
+
+        bool operator==(const animcacheentry &c) const
+        {
+            loopi(MAXANIMPARTS) if(as[i]!=c.as[i]) return false;
+            return pitch==c.pitch && partmask==c.partmask && ragdoll==c.ragdoll && (!ragdoll || min(millis, c.millis) >= ragdoll->lastmove);
+        }
+    };
+
+    struct vbocacheentry : animcacheentry
+    {
+        GLuint vbuf;
+        int owner;
+
+        vbocacheentry() : vbuf(0), owner(-1) {}
+    };
+    
+    struct skelcacheentry : animcacheentry
+    {
+        dualquat *bdata;
+        int version;
+        bool dirty;
+        skelcacheentry() : bdata(NULL), version(-1), dirty(false) {}
+        
+        void nextversion()
+        {
+            version = Shader::uniformlocversion();
+            dirty = true;
+        } 
+    };
+
+    struct blendcacheentry : skelcacheentry
+    {
+        int owner;
+
+        blendcacheentry() : owner(-1) {}
+    };
+
+    struct skelmeshgroup;
+
+    struct skelmesh : mesh
+    {
+        vert *verts;
+        bumpvert *bumpverts;
+        tri *tris;
+        int numverts, numtris, maxweights;
+
+        int voffset, eoffset, elen;
+        ushort minvert, maxvert;
+
+        skelmesh() : verts(NULL), bumpverts(NULL), tris(NULL), numverts(0), numtris(0), maxweights(0)
+        {
+        }
+
+        virtual ~skelmesh()
+        {
+            DELETEA(verts);
+            DELETEA(bumpverts);
+            DELETEA(tris);
+        }
+
+        int addblendcombo(const blendcombo &c)
+        {
+            maxweights = max(maxweights, c.size());
+            return ((skelmeshgroup *)group)->addblendcombo(c);
+        }
+
+        void smoothnorms(float limit = 0, bool areaweight = true)
+        {
+            mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight);
+        }
+
+        void buildnorms(bool areaweight = true)
+        {
+            mesh::buildnorms(verts, numverts, tris, numtris, areaweight);
+        }
+
+        void calctangents(bool areaweight = true)
+        {
+            if(bumpverts) return;
+            bumpverts = new bumpvert[numverts];
+            mesh::calctangents(bumpverts, verts, verts, numverts, tris, numtris, areaweight);
+        }
+
+        void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+        {
+            loopj(numverts)
+            {
+                vec v = m.transform(verts[j].pos);
+                loopi(3)
+                {
+                    bbmin[i] = min(bbmin[i], v[i]);
+                    bbmax[i] = max(bbmax[i], v[i]);
+                }
+            }
+        }
+
+        void genBIH(BIH::mesh &m)
+        {
+            m.tris = (const BIH::tri *)tris;
+            m.numtris = numtris;
+            m.pos = (const uchar *)&verts->pos;
+            m.posstride = sizeof(vert);
+            m.tc = (const uchar *)&verts->tc;
+            m.tcstride = sizeof(vert);
+        }
+
+        static inline void assignvert(vvertn &vv, int j, vert &v, blendcombo &c)
+        {
+            vv.pos = v.pos;
+            vv.norm = v.norm;
+            vv.tc = v.tc;
+        }
+
+        inline void assignvert(vvertbump &vv, int j, vert &v, blendcombo &c)
+        {
+            vv.pos = v.pos;
+            vv.tc = v.tc;
+            vv.tangent = bumpverts[j].tangent;
+        }
+
+        static inline void assignvert(vvertnw &vv, int j, vert &v, blendcombo &c)
+        {
+            vv.pos = v.pos;
+            vv.norm = v.norm;
+            vv.tc = v.tc;
+            c.serialize(vv);
+        }
+
+        inline void assignvert(vvertbumpw &vv, int j, vert &v, blendcombo &c)
+        {
+            vv.pos = v.pos;
+            vv.tc = v.tc;
+            vv.tangent = bumpverts[j].tangent;
+            c.serialize(vv);
+        }
+
+        template<class T>
+        int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts)
+        {
+            voffset = offset;
+            eoffset = idxs.length();
+            loopi(numverts)
+            {
+                vert &v = verts[i];
+                assignvert(vverts.add(), i, v, ((skelmeshgroup *)group)->blendcombos[v.blend]);
+            }
+            loopi(numtris) loopj(3) idxs.add(voffset + tris[i].vert[j]);
+            elen = idxs.length()-eoffset;
+            minvert = voffset;
+            maxvert = voffset + numverts-1;
+            return numverts;
+        }
+
+        template<class T>
+        int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts, int *htdata, int htlen)
+        {
+            voffset = offset;
+            eoffset = idxs.length();
+            minvert = 0xFFFF;
+            loopi(numtris)
+            {
+                tri &t = tris[i];
+                loopj(3)
+                {
+                    int index = t.vert[j];
+                    vert &v = verts[index];
+                    T vv;
+                    assignvert(vv, index, v, ((skelmeshgroup *)group)->blendcombos[v.blend]);
+                    int htidx = hthash(v.pos)&(htlen-1);
+                    loopk(htlen)
+                    {
+                        int &vidx = htdata[(htidx+k)&(htlen-1)];
+                        if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; }
+                        else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; }
+                    }
+                }
+            }
+            elen = idxs.length()-eoffset;
+            minvert = min(minvert, ushort(voffset));
+            maxvert = max(minvert, ushort(vverts.length()-1));
+            return vverts.length()-voffset;
+        }
+
+        int genvbo(vector<ushort> &idxs, int offset)
+        {
+            loopi(numverts) verts[i].interpindex = ((skelmeshgroup *)group)->remapblend(verts[i].blend);
+            
+            voffset = offset;
+            eoffset = idxs.length();
+            loopi(numtris)
+            {
+                tri &t = tris[i];
+                loopj(3) idxs.add(voffset+t.vert[j]);
+            }
+            minvert = voffset;
+            maxvert = voffset + numverts-1;
+            elen = idxs.length()-eoffset;
+            return numverts;
+        }
+
+        template<class T>
+        static inline void fillvert(T &vv, int j, vert &v)
+        {
+            vv.tc = v.tc;
+        }
+
+        template<class T>
+        void fillverts(T *vdata)
+        {
+            vdata += voffset;
+            loopi(numverts) fillvert(vdata[i], i, verts[i]);
+        }
+
+        void interpverts(const dualquat * RESTRICT bdata1, const dualquat * RESTRICT bdata2, bool tangents, void * RESTRICT vdata, skin &s)
+        {
+            const int blendoffset = ((skelmeshgroup *)group)->skel->numgpubones;
+            bdata2 -= blendoffset;
+
+            #define IPLOOP(type, dosetup, dotransform) \
+                loopi(numverts) \
+                { \
+                    const vert &src = verts[i]; \
+                    type &dst = ((type * RESTRICT)vdata)[i]; \
+                    dosetup; \
+                    const dualquat &b = (src.interpindex < blendoffset ? bdata1 : bdata2)[src.interpindex]; \
+                    dst.pos = b.transform(src.pos); \
+                    dotransform; \
+                }
+
+            if(tangents)
+            {
+                IPLOOP(vvertbump, bumpvert &bsrc = bumpverts[i],
+                {   
+                    quat q = b.transform(bsrc.tangent);
+                    fixqtangent(q, bsrc.tangent.w);
+                    dst.tangent = q;
+                });
+            }
+            else
+            {
+                IPLOOP(vvertn, ,
+                {
+                    dst.norm = b.transformnormal(src.norm);
+                });
+            }
+
+            #undef IPLOOP
+        }
+
+        void setshader(Shader *s)
+        {
+            skelmeshgroup *g = (skelmeshgroup *)group;
+            if(glaring)
+            {
+                if(!g->skel->usegpuskel) s->setvariant(0, 1);
+                else s->setvariant(min(maxweights, g->vweights), 1);
+            }
+            else if(!g->skel->usegpuskel) s->set();
+            else s->setvariant(min(maxweights, g->vweights)-1, 0);
+        }
+
+        void render(const animstate *as, skin &s, vbocacheentry &vc)
+        {
+            if(!Shader::lastshader) return;
+            glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((skelmeshgroup *)group)->edata[eoffset]);
+            glde++;
+            xtravertsva += numverts;
+        }
+    };
+
+       
+    struct tag
+    {
+        char *name;
+        int bone;
+        matrix4x3 matrix;
+
+        tag() : name(NULL) {}
+        ~tag() { DELETEA(name); }
+    };
+
+    struct skelanimspec
+    {
+        char *name;
+        int frame, range;
+
+        skelanimspec() : name(NULL), frame(0), range(0) {}
+        ~skelanimspec()
+        {
+            DELETEA(name);
+        }
+    };
+
+    struct boneinfo
+    {
+        const char *name;
+        int parent, children, next, group, scheduled, interpindex, interpparent, ragdollindex, correctindex;
+        float pitchscale, pitchoffset, pitchmin, pitchmax;
+        dualquat base, invbase;
+
+        boneinfo() : name(NULL), parent(-1), children(-1), next(-1), group(INT_MAX), scheduled(-1), interpindex(-1), interpparent(-1), ragdollindex(-1), correctindex(-1), pitchscale(0), pitchoffset(0), pitchmin(0), pitchmax(0) {}
+        ~boneinfo()
+        {
+            DELETEA(name);
+        }
+    };
+
+    struct antipode
+    {
+        int parent, child;
+
+        antipode(int parent, int child) : parent(parent), child(child) {}
+    };
+
+    struct pitchdep
+    {
+        int bone, parent;
+        dualquat pose;
+    };
+
+    struct pitchtarget
+    {
+        int bone, frame, corrects, deps;
+        float pitchmin, pitchmax, deviated;
+        dualquat pose;
+    };
+
+    struct pitchcorrect
+    {
+        int bone, target, parent;
+        float pitchmin, pitchmax, pitchscale, pitchangle, pitchtotal;
+
+        pitchcorrect() : parent(-1), pitchangle(0), pitchtotal(0) {}
+    };
+
+    struct skeleton
+    {
+        char *name;
+        int shared;
+        vector<skelmeshgroup *> users;
+        boneinfo *bones;
+        int numbones, numinterpbones, numgpubones, numframes;
+        dualquat *framebones;
+        vector<skelanimspec> skelanims;
+        vector<tag> tags;
+        vector<antipode> antipodes;
+        ragdollskel *ragdoll;
+        vector<pitchdep> pitchdeps;
+        vector<pitchtarget> pitchtargets;
+        vector<pitchcorrect> pitchcorrects;
+
+        bool usegpuskel;
+        vector<skelcacheentry> skelcache;
+        hashtable<GLuint, int> blendoffsets;
+
+        skeleton() : name(NULL), shared(0), bones(NULL), numbones(0), numinterpbones(0), numgpubones(0), numframes(0), framebones(NULL), ragdoll(NULL), usegpuskel(false), blendoffsets(32)
+        {
+        }
+
+        ~skeleton()
+        {
+            DELETEA(name);
+            DELETEA(bones);
+            DELETEA(framebones);
+            DELETEP(ragdoll);
+            loopv(skelcache)
+            {
+                DELETEA(skelcache[i].bdata);
+            }
+        }
+
+        skelanimspec *findskelanim(const char *name, char sep = '\0')
+        {
+            int len = sep ? strlen(name) : 0;
+            loopv(skelanims)
+            {
+                if(skelanims[i].name)
+                {
+                    if(sep)
+                    {
+                        const char *end = strchr(skelanims[i].name, ':');
+                        if(end && end - skelanims[i].name == len && !memcmp(name, skelanims[i].name, len)) return &skelanims[i];
+                    }
+                    if(!strcmp(name, skelanims[i].name)) return &skelanims[i];
+                }
+            }
+            return NULL;
+        }
+
+        skelanimspec &addskelanim(const char *name)
+        {
+            skelanimspec &sa = skelanims.add();
+            sa.name = name ? newstring(name) : NULL;
+            return sa;
+        }
+
+        int findbone(const char *name)
+        {
+            loopi(numbones) if(bones[i].name && !strcmp(bones[i].name, name)) return i;
+            return -1;
+        }
+
+        int findtag(const char *name)
+        {
+            loopv(tags) if(!strcmp(tags[i].name, name)) return i;
+            return -1;
+        }
+
+        bool addtag(const char *name, int bone, const matrix4x3 &matrix)
+        {
+            int idx = findtag(name);
+            if(idx >= 0)
+            {
+                if(!testtags) return false;
+                tag &t = tags[idx];
+                t.bone = bone;
+                t.matrix = matrix;
+            }
+            else
+            {
+                tag &t = tags.add();
+                t.name = newstring(name);
+                t.bone = bone;
+                t.matrix = matrix;
+            }
+            return true;
+        }
+
+        void calcantipodes()
+        {
+            antipodes.shrink(0);
+            vector<int> schedule;
+            loopi(numbones) 
+            {
+                if(bones[i].group >= numbones) 
+                {
+                    bones[i].scheduled = schedule.length();
+                    schedule.add(i);
+                }
+                else bones[i].scheduled = -1;
+            }
+            loopv(schedule)
+            {
+                int bone = schedule[i];
+                const boneinfo &info = bones[bone];
+                loopj(numbones) if(abs(bones[j].group) == bone && bones[j].scheduled < 0)
+                {
+                    antipodes.add(antipode(info.interpindex, bones[j].interpindex));
+                    bones[j].scheduled = schedule.length();
+                    schedule.add(j);
+                }
+                if(i + 1 == schedule.length())
+                {
+                    int conflict = INT_MAX;
+                    loopj(numbones) if(bones[j].group < numbones && bones[j].scheduled < 0) conflict = min(conflict, abs(bones[j].group));
+                    if(conflict < numbones)
+                    {
+                        bones[conflict].scheduled = schedule.length();
+                        schedule.add(conflict);
+                    }
+                }
+            }
+        }
+
+        void remapbones()
+        {
+            loopi(numbones) 
+            {
+                boneinfo &info = bones[i];
+                info.interpindex = -1;
+                info.ragdollindex = -1;
+            }
+            numgpubones = 0;
+            loopv(users)
+            {
+                skelmeshgroup *group = users[i];
+                loopvj(group->blendcombos)
+                {
+                    blendcombo &c = group->blendcombos[j];
+                    loopk(4) 
+                    {
+                        if(!c.weights[k]) { c.interpbones[k] = k > 0 ? c.interpbones[k-1] : 0; continue; } 
+                        boneinfo &info = bones[c.bones[k]];
+                        if(info.interpindex < 0) info.interpindex = numgpubones++;
+                        c.interpbones[k] = info.interpindex;
+                        if(info.group < 0) continue;
+                        loopl(4)
+                        {
+                            if(!c.weights[l]) break;
+                            if(l == k) continue;
+                            int parent = c.bones[l];
+                            if(info.parent == parent || (info.parent >= 0 && info.parent == bones[parent].parent)) { info.group = -info.parent; break; }
+                            if(info.group <= parent) continue;
+                            int child = c.bones[k];
+                            while(parent > child) parent = bones[parent].parent;
+                            if(parent != child) info.group = c.bones[l];
+                        }
+                    }
+                }
+            }
+            numinterpbones = numgpubones;
+            loopv(tags)
+            {
+                boneinfo &info = bones[tags[i].bone];
+                if(info.interpindex < 0) info.interpindex = numinterpbones++;
+            }
+            if(ragdoll)
+            {
+                loopv(ragdoll->joints) 
+                {
+                    boneinfo &info = bones[ragdoll->joints[i].bone];
+                    if(info.interpindex < 0) info.interpindex = numinterpbones++;
+                    info.ragdollindex = i;
+                }
+            }
+            loopi(numbones)
+            {
+                boneinfo &info = bones[i];
+                if(info.interpindex < 0) continue;
+                for(int parent = info.parent; parent >= 0 && bones[parent].interpindex < 0; parent = bones[parent].parent)
+                    bones[parent].interpindex = numinterpbones++;
+            }
+            loopi(numbones)
+            {
+                boneinfo &info = bones[i];
+                if(info.interpindex < 0) continue;
+                info.interpparent = info.parent >= 0 ? bones[info.parent].interpindex : -1;
+            }
+            if(ragdoll)
+            {
+                loopi(numbones)
+                {
+                    boneinfo &info = bones[i];
+                    if(info.interpindex < 0 || info.ragdollindex >= 0) continue;
+                    for(int parent = info.parent; parent >= 0; parent = bones[parent].parent)
+                    {
+                        if(bones[parent].ragdollindex >= 0) { ragdoll->addreljoint(i, bones[parent].ragdollindex); break; }
+                    }
+                }
+            }
+            calcantipodes();
+        }
+
+
+        void addpitchdep(int bone, int frame)
+        {
+            for(; bone >= 0; bone = bones[bone].parent)
+            {
+                int pos = pitchdeps.length();
+                loopvj(pitchdeps) if(bone <= pitchdeps[j].bone)
+                { 
+                    if(bone == pitchdeps[j].bone) goto nextbone;
+                    pos = j;
+                    break;
+                }
+                {
+                    pitchdep d;
+                    d.bone = bone;
+                    d.parent = -1;
+                    d.pose = framebones[frame*numbones + bone];
+                    pitchdeps.insert(pos, d);
+                }
+            nextbone:;
+            }
+        }
+
+        int findpitchdep(int bone)
+        {
+            loopv(pitchdeps) if(bone <= pitchdeps[i].bone) return bone == pitchdeps[i].bone ? i : -1;
+            return -1;
+        }
+
+        int findpitchcorrect(int bone)
+        {
+            loopv(pitchcorrects) if(bone <= pitchcorrects[i].bone) return bone == pitchcorrects[i].bone ? i : -1;
+            return -1;
+        }
+
+        void initpitchdeps()
+        {
+            pitchdeps.setsize(0);
+            if(pitchtargets.empty()) return;
+            loopv(pitchtargets)
+            {
+                pitchtarget &t = pitchtargets[i];
+                t.deps = -1;
+                addpitchdep(t.bone, t.frame);
+            }
+            loopv(pitchdeps)
+            {
+                pitchdep &d = pitchdeps[i];
+                int parent = bones[d.bone].parent;
+                if(parent >= 0) 
+                {
+                    int j = findpitchdep(parent);
+                    if(j >= 0)
+                    {
+                        d.parent = j;
+                        d.pose.mul(pitchdeps[j].pose, dualquat(d.pose));
+                    }
+                }
+            }
+            loopv(pitchtargets)
+            {
+                pitchtarget &t = pitchtargets[i];
+                int j = findpitchdep(t.bone);
+                if(j >= 0)
+                {
+                    t.deps = j;
+                    t.pose = pitchdeps[j].pose;
+                }    
+                t.corrects = -1;
+                for(int parent = t.bone; parent >= 0; parent = bones[parent].parent)
+                {
+                    t.corrects = findpitchcorrect(parent);
+                    if(t.corrects >= 0) break;
+                }
+            }
+            loopv(pitchcorrects)
+            {
+                pitchcorrect &c = pitchcorrects[i];
+                bones[c.bone].correctindex = i;
+                c.parent = -1;
+                for(int parent = c.bone;;)
+                {
+                    parent = bones[parent].parent;
+                    if(parent < 0) break;
+                    c.parent = findpitchcorrect(parent);
+                    if(c.parent >= 0) break;
+                }
+            }
+        }
+
+        void optimize()
+        {
+            cleanup();
+            if(ragdoll) ragdoll->setup();
+            remapbones();
+            initpitchdeps();
+        }
+
+        void expandbonemask(uchar *expansion, int bone, int val)
+        {
+            expansion[bone] = val;
+            bone = bones[bone].children;
+            while(bone>=0) { expandbonemask(expansion, bone, val); bone = bones[bone].next; }
+        }
+
+        void applybonemask(ushort *mask, uchar *partmask, int partindex)
+        {
+            if(!mask || *mask==BONEMASK_END) return;
+            uchar *expansion = new uchar[numbones];
+            memset(expansion, *mask&BONEMASK_NOT ? 1 : 0, numbones);
+            while(*mask!=BONEMASK_END)
+            {
+                expandbonemask(expansion, *mask&BONEMASK_BONE, *mask&BONEMASK_NOT ? 0 : 1);
+                mask++;
+            }
+            loopi(numbones) if(expansion[i]) partmask[i] = partindex;
+            delete[] expansion;
+        }
+
+        void linkchildren()
+        {
+            loopi(numbones)
+            {
+                boneinfo &b = bones[i];
+                b.children = -1;
+                if(b.parent<0) b.next = -1;
+                else
+                {
+                    b.next = bones[b.parent].children;
+                    bones[b.parent].children = i;
+                }
+            }
+        }
+
+        int availgpubones() const { return min(maxvsuniforms - reservevpparams - 10, maxskelanimdata) / 2; }
+        bool gpuaccelerate() const { return numframes && gpuskel && numgpubones<=availgpubones(); }
+
+        float calcdeviation(const vec &axis, const vec &forward, const dualquat &pose1, const dualquat &pose2)
+        {
+            vec forward1 = pose1.transformnormal(forward).project(axis).normalize(),
+                forward2 = pose2.transformnormal(forward).project(axis).normalize(),
+                daxis = vec().cross(forward1, forward2);
+            float dx = clamp(forward1.dot(forward2), -1.0f, 1.0f), dy = clamp(daxis.magnitude(), -1.0f, 1.0f);
+            if(daxis.dot(axis) < 0) dy = -dy;
+            return atan2f(dy, dx)/RAD;
+        }
+
+        void calcpitchcorrects(float pitch, const vec &axis, const vec &forward)
+        {
+            loopv(pitchtargets)
+            {
+                pitchtarget &t = pitchtargets[i];
+                t.deviated = calcdeviation(axis, forward, t.pose, pitchdeps[t.deps].pose);
+            }
+            loopv(pitchcorrects)
+            {
+                pitchcorrect &c = pitchcorrects[i];
+                c.pitchangle = c.pitchtotal = 0;
+            }
+            loopvj(pitchtargets)
+            {
+                pitchtarget &t = pitchtargets[j];
+                float tpitch = pitch - t.deviated;
+                for(int parent = t.corrects; parent >= 0; parent = pitchcorrects[parent].parent)
+                    tpitch -= pitchcorrects[parent].pitchangle;
+                if(t.pitchmin || t.pitchmax) tpitch = clamp(tpitch, t.pitchmin, t.pitchmax);
+                loopv(pitchcorrects)
+                {
+                    pitchcorrect &c = pitchcorrects[i];
+                    if(c.target != j) continue;
+                    float total = c.parent >= 0 ? pitchcorrects[c.parent].pitchtotal : 0, 
+                          avail = tpitch - total, 
+                          used = tpitch*c.pitchscale;
+                    if(c.pitchmin || c.pitchmax)
+                    {
+                        if(used < 0) used = clamp(c.pitchmin, used, 0.0f);
+                        else used = clamp(c.pitchmax, 0.0f, used);
+                    }
+                    if(used < 0) used = clamp(avail, used, 0.0f);
+                    else used = clamp(avail, 0.0f, used);
+                    c.pitchangle = used;
+                    c.pitchtotal = used + total;
+                }
+            }
+        }
+
+        #define INTERPBONE(bone) \
+            const animstate &s = as[partmask[bone]]; \
+            const framedata &f = partframes[partmask[bone]]; \
+            dualquat d; \
+            (d = f.fr1[bone]).mul((1-s.cur.t)*s.interp); \
+            d.accumulate(f.fr2[bone], s.cur.t*s.interp); \
+            if(s.interp<1) \
+            { \
+                d.accumulate(f.pfr1[bone], (1-s.prev.t)*(1-s.interp)); \
+                d.accumulate(f.pfr2[bone], s.prev.t*(1-s.interp)); \
+            }
+
+        void interpbones(const animstate *as, float pitch, const vec &axis, const vec &forward, int numanimparts, const uchar *partmask, skelcacheentry &sc)
+        {
+            if(!sc.bdata) sc.bdata = new dualquat[numinterpbones];
+            sc.nextversion();
+            struct framedata
+            {
+                const dualquat *fr1, *fr2, *pfr1, *pfr2;
+            } partframes[MAXANIMPARTS];
+            loopi(numanimparts)
+            {
+                partframes[i].fr1 = &framebones[as[i].cur.fr1*numbones];
+                partframes[i].fr2 = &framebones[as[i].cur.fr2*numbones];
+                if(as[i].interp<1)
+                {
+                    partframes[i].pfr1 = &framebones[as[i].prev.fr1*numbones];
+                    partframes[i].pfr2 = &framebones[as[i].prev.fr2*numbones];
+                }
+            }
+            loopv(pitchdeps)
+            {
+                pitchdep &p = pitchdeps[i];
+                INTERPBONE(p.bone);
+                d.normalize();
+                if(p.parent >= 0) p.pose.mul(pitchdeps[p.parent].pose, d);
+                else p.pose = d;
+            }
+            calcpitchcorrects(pitch, axis, forward);
+            loopi(numbones) if(bones[i].interpindex>=0)
+            {
+                INTERPBONE(i);
+                const boneinfo &b = bones[i];
+                d.normalize();
+                if(b.interpparent<0) sc.bdata[b.interpindex] = d;
+                else sc.bdata[b.interpindex].mul(sc.bdata[b.interpparent], d);
+                float angle;
+                if(b.pitchscale) { angle = b.pitchscale*pitch + b.pitchoffset; if(b.pitchmin || b.pitchmax) angle = clamp(angle, b.pitchmin, b.pitchmax); }
+                else if(b.correctindex >= 0) angle = pitchcorrects[b.correctindex].pitchangle;
+                else continue;
+                if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH))
+                    angle *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp);
+                sc.bdata[b.interpindex].mulorient(quat(axis, angle*RAD), b.base);
+            }
+            loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
+        }
+
+        void initragdoll(ragdolldata &d, skelcacheentry &sc, part *p)
+        {
+            const dualquat *bdata = sc.bdata;
+            loopv(ragdoll->joints)
+            {
+                const ragdollskel::joint &j = ragdoll->joints[i];
+                const boneinfo &b = bones[j.bone];
+                const dualquat &q = bdata[b.interpindex];
+                loopk(3) if(j.vert[k] >= 0)
+                {
+                    ragdollskel::vert &v = ragdoll->verts[j.vert[k]];
+                    ragdolldata::vert &dv = d.verts[j.vert[k]];
+                    dv.pos.add(q.transform(v.pos).mul(v.weight));
+                }
+            }
+            if(ragdoll->animjoints) loopv(ragdoll->joints)
+            {
+                const ragdollskel::joint &j = ragdoll->joints[i];
+                const boneinfo &b = bones[j.bone];
+                const dualquat &q = bdata[b.interpindex];
+                d.calcanimjoint(i, matrix4x3(q));
+            }
+            loopv(ragdoll->verts)
+            {
+                ragdolldata::vert &dv = d.verts[i];
+                matrixstack[matrixpos].transform(vec(dv.pos).add(p->translate).mul(p->model->scale), dv.pos);
+            }
+            loopv(ragdoll->reljoints)
+            {
+                const ragdollskel::reljoint &r = ragdoll->reljoints[i];
+                const ragdollskel::joint &j = ragdoll->joints[r.parent];
+                const boneinfo &br = bones[r.bone], &bj = bones[j.bone];
+                d.reljoints[i].mul(dualquat(bdata[bj.interpindex]).invert(), bdata[br.interpindex]);
+            }
+        }
+
+        void genragdollbones(ragdolldata &d, skelcacheentry &sc, part *p)
+        {
+            if(!sc.bdata) sc.bdata = new dualquat[numinterpbones];
+            sc.nextversion();
+            loopv(ragdoll->joints)
+            {
+                const ragdollskel::joint &j = ragdoll->joints[i];
+                const boneinfo &b = bones[j.bone];
+                vec pos(0, 0, 0);
+                loopk(3) if(j.vert[k]>=0) pos.add(d.verts[j.vert[k]].pos);
+                pos.mul(j.weight/p->model->scale).sub(p->translate);
+                matrix4x3 m;
+                m.mul(d.tris[j.tri], pos, d.animjoints ? d.animjoints[i] : j.orient);
+                sc.bdata[b.interpindex] = dualquat(m);
+            }
+            loopv(ragdoll->reljoints)
+            {
+                const ragdollskel::reljoint &r = ragdoll->reljoints[i];
+                const ragdollskel::joint &j = ragdoll->joints[r.parent];
+                const boneinfo &br = bones[r.bone], &bj = bones[j.bone];
+                sc.bdata[br.interpindex].mul(sc.bdata[bj.interpindex], d.reljoints[i]);
+            }
+            loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
+        }
+
+        void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
+        {
+            matrix4x3 t;
+            t.mul(bones[tags[i].bone].base, tags[i].matrix);
+            t.posttranslate(p->translate, p->model->scale);
+            n.mul(m, t);
+        }
+
+        void calctags(part *p, skelcacheentry *sc = NULL)
+        {
+            loopv(p->links)
+            {
+                linkedpart &l = p->links[i];
+                tag &t = tags[l.tag];
+                dualquat q;
+                if(sc) q.mul(sc->bdata[bones[t.bone].interpindex], bones[t.bone].base);
+                else q = bones[t.bone].base;
+                matrix4x3 m;
+                m.mul(q, t.matrix);
+                m.d.add(p->translate).mul(p->model->scale);
+                l.matrix = m;
+            }
+        }
+
+        void cleanup(bool full = true)
+        {
+            loopv(skelcache)
+            {
+                skelcacheentry &sc = skelcache[i];
+                loopj(MAXANIMPARTS) sc.as[j].cur.fr1 = -1;
+                DELETEA(sc.bdata);
+            }
+            skelcache.setsize(0);
+            blendoffsets.clear();
+            if(full) loopv(users) users[i]->cleanup();
+        }
+
+        bool canpreload() { return !numframes || gpuaccelerate(); }
+
+        void preload()
+        {
+            if(!numframes) return;
+            if(skelcache.empty())
+            {
+                usegpuskel = gpuaccelerate();
+            }
+        }
+
+        skelcacheentry &checkskelcache(part *p, const animstate *as, float pitch, const vec &axis, const vec &forward, ragdolldata *rdata)
+        {
+            if(skelcache.empty()) 
+            {
+                usegpuskel = gpuaccelerate();
+            }
+
+            int numanimparts = ((skelpart *)as->owner)->numanimparts;
+            uchar *partmask = ((skelpart *)as->owner)->partmask;
+            skelcacheentry *sc = NULL;
+            bool match = false;
+            loopv(skelcache)
+            {
+                skelcacheentry &c = skelcache[i];
+                loopj(numanimparts) if(c.as[j]!=as[j]) goto mismatch;
+                if(c.pitch != pitch || c.partmask != partmask || c.ragdoll != rdata || (rdata && c.millis < rdata->lastmove)) goto mismatch;
+                match = true;
+                sc = &c;
+                break;
+            mismatch:
+                if(c.millis < lastmillis) { sc = &c; break; }
+            }
+            if(!sc) sc = &skelcache.add();
+            if(!match)
+            {
+                loopi(numanimparts) sc->as[i] = as[i];
+                sc->pitch = pitch;
+                sc->partmask = partmask;
+                sc->ragdoll = rdata;
+                if(rdata) genragdollbones(*rdata, *sc, p);
+                else interpbones(as, pitch, axis, forward, numanimparts, partmask, *sc);
+            }
+            sc->millis = lastmillis;
+            return *sc;
+        }
+
+        int getblendoffset(UniformLoc &u)
+        {
+            int &offset = blendoffsets.access(Shader::lastshader->program, -1);
+            if(offset < 0)
+            {
+                defformatstring(offsetname, "%s[%d]", u.name, 2*numgpubones); 
+                offset = glGetUniformLocation_(Shader::lastshader->program, offsetname);
+            }
+            return offset;
+        }
+            
+        void setglslbones(UniformLoc &u, skelcacheentry &sc, skelcacheentry &bc, int count)
+        {
+            if(u.version == bc.version && u.data == bc.bdata) return;
+            glUniform4fv_(u.loc, 2*numgpubones, sc.bdata[0].real.v);
+            if(count > 0) 
+            {
+                int offset = getblendoffset(u);
+                if(offset >= 0) glUniform4fv_(offset, 2*count, bc.bdata[0].real.v);
+            }
+            u.version = bc.version;
+            u.data = bc.bdata;
+        }
+        
+        void setgpubones(skelcacheentry &sc, blendcacheentry *bc, int count)
+        {
+            if(!Shader::lastshader) return;
+            if(Shader::lastshader->uniformlocs.length() < 1) return;
+            UniformLoc &u = Shader::lastshader->uniformlocs[0];
+            setglslbones(u, sc, bc ? *bc : sc, count);
+        }
+    
+        bool shouldcleanup() const
+        {
+            return numframes && (skelcache.empty() || gpuaccelerate()!=usegpuskel);
+        }
+    };
+
+    struct skelmeshgroup : meshgroup
+    {
+        skeleton *skel;
+
+        vector<blendcombo> blendcombos;
+        int numblends[4];
+
+        static const int MAXBLENDCACHE = 16;
+        blendcacheentry blendcache[MAXBLENDCACHE];
+
+        static const int MAXVBOCACHE = 16;
+        vbocacheentry vbocache[MAXVBOCACHE];
+        ushort *edata;
+        GLuint ebuf;
+        bool vtangents;
+        int vlen, vertsize, vblends, vweights;
+        uchar *vdata;
+
+        skelmeshgroup() : skel(NULL), edata(NULL), ebuf(0), vtangents(false), vlen(0), vertsize(0), vblends(0), vweights(0), vdata(NULL)
+        {
+            memset(numblends, 0, sizeof(numblends));
+        }
+
+        virtual ~skelmeshgroup()
+        {
+            if(skel)
+            {
+                if(skel->shared) skel->users.removeobj(this);
+                else DELETEP(skel);
+            }
+            if(ebuf) glDeleteBuffers_(1, &ebuf);
+            loopi(MAXBLENDCACHE)
+            {
+                DELETEA(blendcache[i].bdata);
+            }
+            loopi(MAXVBOCACHE)
+            {
+                if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf);
+            }
+            DELETEA(vdata);
+        }
+
+        void shareskeleton(char *name)
+        {
+            if(!name)
+            {
+                skel = new skeleton;
+                skel->users.add(this);
+                return;
+            }
+
+            static hashnameset<skeleton *> skeletons;
+            if(skeletons.access(name)) skel = skeletons[name];
+            else
+            {
+                skel = new skeleton;
+                skel->name = newstring(name);
+                skeletons.add(skel);
+            }
+            skel->users.add(this);
+            skel->shared++;
+        }
+
+        int findtag(const char *name)
+        {
+            return skel->findtag(name);
+        }
+
+        void *animkey() { return skel; }
+        int totalframes() const { return max(skel->numframes, 1); }
+
+        virtual skelanimspec *loadanim(const char *filename) { return NULL; }
+
+        void genvbo(bool tangents, vbocacheentry &vc)
+        {
+            if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf);
+            if(ebuf) return;
+
+            vector<ushort> idxs;
+
+            if(tangents) loopv(meshes) ((skelmesh *)meshes[i])->calctangents();
+
+            vtangents = tangents;
+            vlen = 0;
+            vblends = 0;
+            if(skel->numframes && !skel->usegpuskel)
+            {
+                vweights = 1;
+                loopv(blendcombos)
+                {
+                    blendcombo &c = blendcombos[i];
+                    c.interpindex = c.weights[1] ? skel->numgpubones + vblends++ : -1;
+                }
+
+                vertsize = tangents ? sizeof(vvertbump) : sizeof(vvertn);
+                loopv(meshes) vlen += ((skelmesh *)meshes[i])->genvbo(idxs, vlen);
+                DELETEA(vdata);
+                vdata = new uchar[vlen*vertsize];
+                #define FILLVDATA(type) do { \
+                    loopv(meshes) ((skelmesh *)meshes[i])->fillverts((type *)vdata); \
+                } while(0)
+                if(tangents) FILLVDATA(vvertbump);
+                else FILLVDATA(vvertn);
+                #undef FILLVDATA
+            }
+            else
+            {
+                if(skel->numframes)
+                {
+                    vweights = 4;
+                    int availbones = skel->availgpubones() - skel->numgpubones;
+                    while(vweights > 1 && availbones >= numblends[vweights-1]) availbones -= numblends[--vweights];
+                    loopv(blendcombos)
+                    {
+                        blendcombo &c = blendcombos[i];
+                        c.interpindex = c.size() > vweights ? skel->numgpubones + vblends++ : -1;
+                    }
+                }
+                else
+                {
+                    vweights = 0;
+                    loopv(blendcombos) blendcombos[i].interpindex = -1;
+                }
+
+                gle::bindvbo(vc.vbuf);
+                #define GENVBO(type, args) do { \
+                    vertsize = sizeof(type); \
+                    vector<type> vverts; \
+                    loopv(meshes) vlen += ((skelmesh *)meshes[i])->genvbo args; \
+                    glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \
+                } while(0)
+                #define GENVBOANIM(type) GENVBO(type, (idxs, vlen, vverts))
+                #define GENVBOSTAT(type) GENVBO(type, (idxs, vlen, vverts, htdata, htlen))
+                if(skel->numframes)
+                {
+                    if(tangents) GENVBOANIM(vvertbumpw);
+                    else GENVBOANIM(vvertnw);
+                }
+                else 
+                {
+                    int numverts = 0, htlen = 128;
+                    loopv(meshes) numverts += ((skelmesh *)meshes[i])->numverts;
+                    while(htlen < numverts) htlen *= 2;
+                    if(numverts*4 > htlen*3) htlen *= 2;  
+                    int *htdata = new int[htlen];
+                    memset(htdata, -1, htlen*sizeof(int));
+                    if(tangents) GENVBOSTAT(vvertbump);
+                    else GENVBOSTAT(vvertn);
+                    delete[] htdata;
+                }
+                #undef GENVBO
+                #undef GENVBOANIM
+                #undef GENVBOSTAT
+                gle::clearvbo();
+            }
+
+            glGenBuffers_(1, &ebuf);
+            gle::bindebo(ebuf);
+            glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW);
+            gle::clearebo();
+        }
+
+        void bindvbo(const animstate *as, vbocacheentry &vc, skelcacheentry *sc = NULL, blendcacheentry *bc = NULL)
+        {
+            vvert *vverts = 0;
+            bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize);
+            if(as->cur.anim&ANIM_NOSKIN)
+            {
+                if(enabletc) disabletc();
+                if(enablenormals) disablenormals();
+                if(enabletangents) disabletangents();
+            }
+            else
+            {
+                if(vtangents)
+                {
+                    if(enablenormals) disablenormals();
+                    vvertbump *vvertbumps = 0;
+                    bindtangents(&vvertbumps->tangent, vertsize);
+                }
+                else
+                {
+                    if(enabletangents) disabletangents();
+                    vvertn *vvertns = 0;
+                    bindnormals(&vvertns->norm, vertsize);
+                }
+
+                bindtc(&vverts->tc, vertsize);
+            }
+            if(!sc || !skel->usegpuskel)
+            {
+                if(enablebones) disablebones();
+            }
+            else
+            {
+                if(vtangents) 
+                {
+                    vvertbumpw *vvertbumpws = 0;
+                    bindbones(vvertbumpws->weights, vvertbumpws->bones, vertsize);
+                }
+                else
+                {
+                    vvertnw *vvertnws = 0;
+                    bindbones(vvertnws->weights, vvertnws->bones, vertsize);
+                }
+            }
+        }
+
+        void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
+        {
+            skel->concattagtransform(p, i, m, n);
+        }
+
+        int addblendcombo(const blendcombo &c)
+        {
+            loopv(blendcombos) if(blendcombos[i]==c)
+            {
+                blendcombos[i].uses += c.uses;
+                return i;
+            }
+            numblends[c.size()-1]++;
+            blendcombo &a = blendcombos.add(c);
+            return a.interpindex = blendcombos.length()-1; 
+        }
+
+        void sortblendcombos()
+        {
+            blendcombos.sort(blendcombo::sortcmp);
+            int *remap = new int[blendcombos.length()];
+            loopv(blendcombos) remap[blendcombos[i].interpindex] = i;
+            loopv(meshes)
+            {
+                skelmesh *m = (skelmesh *)meshes[i];
+                loopj(m->numverts)
+                {
+                    vert &v = m->verts[j];
+                    v.blend = remap[v.blend];
+                }
+            }
+            delete[] remap;
+        }
+
+        int remapblend(int blend)
+        {
+            const blendcombo &c = blendcombos[blend];
+            return c.weights[1] ? c.interpindex : c.interpbones[0];
+        }
+
+        static inline void blendbones(dualquat &d, const dualquat *bdata, const blendcombo &c)
+        {
+            d = bdata[c.interpbones[0]];
+            d.mul(c.weights[0]);
+            d.accumulate(bdata[c.interpbones[1]], c.weights[1]);
+            if(c.weights[2])
+            {
+                d.accumulate(bdata[c.interpbones[2]], c.weights[2]);
+                if(c.weights[3]) d.accumulate(bdata[c.interpbones[3]], c.weights[3]);
+            }
+        }
+
+        void blendbones(const skelcacheentry &sc, blendcacheentry &bc)
+        {
+            bc.nextversion();
+            if(!bc.bdata) bc.bdata = new dualquat[vblends];
+            dualquat *dst = bc.bdata - skel->numgpubones;
+            bool normalize = !skel->usegpuskel || vweights<=1;
+            loopv(blendcombos)
+            {
+                const blendcombo &c = blendcombos[i];
+                if(c.interpindex<0) break;
+                dualquat &d = dst[c.interpindex];
+                blendbones(d, sc.bdata, c);
+                if(normalize) d.normalize();
+            }
+        }
+
+        void cleanup()
+        {
+            loopi(MAXBLENDCACHE)
+            {
+                blendcacheentry &c = blendcache[i];
+                DELETEA(c.bdata);
+                c.owner = -1;
+            }
+            loopi(MAXVBOCACHE)
+            {
+                vbocacheentry &c = vbocache[i];
+                if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; }
+                c.owner = -1;
+            }
+            if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+            if(skel) skel->cleanup(false);
+        }
+
+        #define SEARCHCACHE(cachesize, cacheentry, cache, reusecheck) \
+            loopi(cachesize) \
+            { \
+                cacheentry &c = cache[i]; \
+                if(c.owner==owner) \
+                { \
+                     if(c==sc) return c; \
+                     else c.owner = -1; \
+                     break; \
+                } \
+            } \
+            loopi(cachesize-1) \
+            { \
+                cacheentry &c = cache[i]; \
+                if(reusecheck c.owner < 0 || c.millis < lastmillis) \
+                    return c; \
+            } \
+            return cache[cachesize-1];
+
+        vbocacheentry &checkvbocache(skelcacheentry &sc, int owner)
+        {
+            SEARCHCACHE(MAXVBOCACHE, vbocacheentry, vbocache, !c.vbuf || );
+        }
+
+        blendcacheentry &checkblendcache(skelcacheentry &sc, int owner)
+        {
+            SEARCHCACHE(MAXBLENDCACHE, blendcacheentry, blendcache, )
+        }
+
+        void preload(part *p)
+        {
+            if(!skel->canpreload()) return;
+            bool tangents = false;
+            loopv(p->skins) if(p->skins[i].tangents()) tangents = true;
+            if(skel->shouldcleanup()) skel->cleanup();
+            else if(tangents!=vtangents) cleanup();
+            skel->preload();
+            if(!vbocache->vbuf) genvbo(tangents, *vbocache);
+        }
+
+        void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p)
+        {
+            bool tangents = false;
+            loopv(p->skins) if(p->skins[i].tangents()) tangents = true;
+            if(skel->shouldcleanup()) { skel->cleanup(); disablevbo(); }
+            else if(tangents!=vtangents) { cleanup(); disablevbo(); }
+
+            if(!skel->numframes)
+            {
+                if(!(as->cur.anim&ANIM_NORENDER))
+                {
+                    if(!vbocache->vbuf) genvbo(tangents, *vbocache);
+                    bindvbo(as, *vbocache);
+                    loopv(meshes) 
+                    {
+                        skelmesh *m = (skelmesh *)meshes[i];
+                        p->skins[i].bind(m, as);
+                        m->render(as, p->skins[i], *vbocache);
+                    }
+                }
+                skel->calctags(p);
+                return;
+            }
+
+            skelcacheentry &sc = skel->checkskelcache(p, as, pitch, axis, forward, as->cur.anim&ANIM_RAGDOLL || !d || !d->ragdoll || d->ragdoll->skel != skel->ragdoll ? NULL : d->ragdoll);
+            if(!(as->cur.anim&ANIM_NORENDER))
+            {
+                int owner = &sc-&skel->skelcache[0];
+                vbocacheentry &vc = skel->usegpuskel ? *vbocache : checkvbocache(sc, owner);
+                vc.millis = lastmillis;
+                if(!vc.vbuf) genvbo(tangents, vc);
+                blendcacheentry *bc = NULL;
+                if(vblends)
+                {
+                    bc = &checkblendcache(sc, owner);
+                    bc->millis = lastmillis;
+                    if(bc->owner!=owner)
+                    {
+                        bc->owner = owner;
+                        *(animcacheentry *)bc = sc;
+                        blendbones(sc, *bc);
+                    }
+                }
+                if(!skel->usegpuskel && vc.owner!=owner)
+                { 
+                    vc.owner = owner;
+                    (animcacheentry &)vc = sc;
+                    loopv(meshes)
+                    {
+                        skelmesh &m = *(skelmesh *)meshes[i];
+                        m.interpverts(sc.bdata, bc ? bc->bdata : NULL, tangents, vdata + m.voffset*vertsize, p->skins[i]);
+                    }
+                    gle::bindvbo(vc.vbuf);
+                    glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW);
+                }
+
+                bindvbo(as, vc, &sc, bc);
+                loopv(meshes) 
+                {
+                    skelmesh *m = (skelmesh *)meshes[i];
+                    p->skins[i].bind(m, as);
+                    if(skel->usegpuskel) skel->setgpubones(sc, bc, vblends);
+                    m->render(as, p->skins[i], vc);
+                }
+            }
+
+            skel->calctags(p, &sc);
+
+            if(as->cur.anim&ANIM_RAGDOLL && skel->ragdoll && !d->ragdoll)
+            {
+                d->ragdoll = new ragdolldata(skel->ragdoll, p->model->scale);
+                skel->initragdoll(*d->ragdoll, sc, p);
+                d->ragdoll->init(d);
+            }
+        }
+    };
+
+    struct animpartmask
+    {
+        animpartmask *next;
+        int numbones;
+        uchar bones[1];
+    };
+
+    struct skelpart : part
+    {
+        animpartmask *buildingpartmask;
+
+        uchar *partmask;
+        
+        skelpart(animmodel *model, int index = 0) : part(model, index), buildingpartmask(NULL), partmask(NULL)
+        {
+        }
+
+        virtual ~skelpart()
+        {
+            DELETEA(buildingpartmask);
+        }
+
+        uchar *sharepartmask(animpartmask *o)
+        {
+            static animpartmask *partmasks = NULL;
+            animpartmask *p = partmasks;
+            for(; p; p = p->next) if(p->numbones==o->numbones && !memcmp(p->bones, o->bones, p->numbones))
+            {
+                delete[] (uchar *)o;
+                return p->bones;
+            }
+
+            o->next = p;
+            partmasks = o;
+            return o->bones;
+        }
+
+        animpartmask *newpartmask()
+        {
+            animpartmask *p = (animpartmask *)new uchar[sizeof(animpartmask) + ((skelmeshgroup *)meshes)->skel->numbones-1];
+            p->numbones = ((skelmeshgroup *)meshes)->skel->numbones;
+            memset(p->bones, 0, p->numbones);
+            return p;
+        }
+
+        void initanimparts()
+        {
+            DELETEA(buildingpartmask);
+            buildingpartmask = newpartmask();
+        }
+
+        bool addanimpart(ushort *bonemask)
+        {
+            if(!buildingpartmask || numanimparts>=MAXANIMPARTS) return false;
+            ((skelmeshgroup *)meshes)->skel->applybonemask(bonemask, buildingpartmask->bones, numanimparts);
+            numanimparts++;
+            return true;
+        }
+
+        void endanimparts()
+        {
+            if(buildingpartmask)
+            {
+                partmask = sharepartmask(buildingpartmask);
+                buildingpartmask = NULL;
+            }
+
+            ((skelmeshgroup *)meshes)->skel->optimize();
+        }
+
+        void loaded()
+        {
+            endanimparts();
+            part::loaded();
+        }
+    };
+
+    skelmodel(const char *name) : animmodel(name)
+    {
+    }
+
+    int linktype(animmodel *m) const
+    {
+        return type()==m->type() &&
+            ((skelmeshgroup *)parts[0]->meshes)->skel == ((skelmeshgroup *)m->parts[0]->meshes)->skel ? 
+                LINK_REUSE : 
+                LINK_TAG;
+    }
+    
+    bool skeletal() const { return true; }
+
+    skelpart &addpart()
+    {
+        flushpart();
+        skelpart *p = new skelpart(this, parts.length());
+        parts.add(p);
+        return *p;
+    }
+};
+
+struct skeladjustment
+{
+    float yaw, pitch, roll;
+    vec translate;
+
+    skeladjustment(float yaw, float pitch, float roll, const vec &translate) : yaw(yaw), pitch(pitch), roll(roll), translate(translate) {}
+
+    void adjust(dualquat &dq)
+    {
+        if(yaw) dq.mulorient(quat(vec(0, 0, 1), yaw*RAD));
+        if(pitch) dq.mulorient(quat(vec(0, -1, 0), pitch*RAD));
+        if(roll) dq.mulorient(quat(vec(-1, 0, 0), roll*RAD));
+        if(!translate.iszero()) dq.translate(translate);
+    }
+};
+
+template<class MDL> struct skelloader : modelloader<MDL, skelmodel>
+{
+    static vector<skeladjustment> adjustments;
+
+    skelloader(const char *name) : modelloader<MDL, skelmodel>(name) {}
+
+    void flushpart()
+    {
+        adjustments.setsize(0);
+    }
+};
+
+template<class MDL> vector<skeladjustment> skelloader<MDL>::adjustments;
+
+template<class MDL> struct skelcommands : modelcommands<MDL, struct MDL::skelmesh>
+{
+    typedef modelcommands<MDL, struct MDL::skelmesh> commands;
+    typedef struct MDL::skeleton skeleton;
+    typedef struct MDL::skelmeshgroup meshgroup;
+    typedef struct MDL::skelpart part;
+    typedef struct MDL::skin skin;
+    typedef struct MDL::boneinfo boneinfo;
+    typedef struct MDL::skelanimspec animspec;
+    typedef struct MDL::pitchdep pitchdep;
+    typedef struct MDL::pitchtarget pitchtarget;
+    typedef struct MDL::pitchcorrect pitchcorrect;
+
+    static void loadpart(char *meshfile, char *skelname, float *smooth)
+    {
+        if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        defformatstring(filename, "%s/%s", MDL::dir, meshfile);
+        part &mdl = MDL::loading->addpart();
+        mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+        mdl.meshes = MDL::loading->sharemeshes(path(filename), skelname[0] ? skelname : NULL, double(*smooth > 0 ? cos(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2));
+        if(!mdl.meshes) conoutf(CON_ERROR, "could not load %s", filename);
+        else
+        {
+            mdl.initanimparts();
+            mdl.initskins();
+        }
+    }
+   
+    static void settag(char *name, char *tagname, float *tx, float *ty, float *tz, float *rx, float *ry, float *rz)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        part &mdl = *(part *)MDL::loading->parts.last();
+        int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
+        if(i >= 0)
+        {
+            float cx = *rx ? cosf(*rx/2*RAD) : 1, sx = *rx ? sinf(*rx/2*RAD) : 0,
+                  cy = *ry ? cosf(*ry/2*RAD) : 1, sy = *ry ? sinf(*ry/2*RAD) : 0,
+                  cz = *rz ? cosf(*rz/2*RAD) : 1, sz = *rz ? sinf(*rz/2*RAD) : 0;
+            matrix4x3 m(matrix3(quat(sx*cy*cz - cx*sy*sz, cx*sy*cz + sx*cy*sz, cx*cy*sz - sx*sy*cz, cx*cy*cz + sx*sy*sz)),
+                        vec(*tx, *ty, *tz));
+            ((meshgroup *)mdl.meshes)->skel->addtag(tagname, i, m);
+            return;
+        }
+        conoutf(CON_ERROR, "could not find bone %s for tag %s", name, tagname);
+    }
+
+    static void setpitch(char *name, float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        part &mdl = *(part *)MDL::loading->parts.last();
+    
+        if(name[0])
+        {
+            int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
+            if(i>=0)
+            {
+                boneinfo &b = ((meshgroup *)mdl.meshes)->skel->bones[i];
+                b.pitchscale = *pitchscale;
+                b.pitchoffset = *pitchoffset;
+                if(*pitchmin || *pitchmax)
+                {
+                    b.pitchmin = *pitchmin;
+                    b.pitchmax = *pitchmax;
+                }
+                else
+                {
+                    b.pitchmin = -360*fabs(b.pitchscale) + b.pitchoffset;
+                    b.pitchmax = 360*fabs(b.pitchscale) + b.pitchoffset;
+                }
+                return;
+            }
+            conoutf(CON_ERROR, "could not find bone %s to pitch", name);
+            return;
+        }
+    
+        mdl.pitchscale = *pitchscale;
+        mdl.pitchoffset = *pitchoffset;
+        if(*pitchmin || *pitchmax)
+        {
+            mdl.pitchmin = *pitchmin;
+            mdl.pitchmax = *pitchmax;
+        }
+        else
+        {
+            mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+            mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+        }
+    }
+
+    static void setpitchtarget(char *name, char *animfile, int *frameoffset, float *pitchmin, float *pitchmax)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        part &mdl = *(part *)MDL::loading->parts.last();
+        if(!mdl.meshes) return;
+        defformatstring(filename, "%s/%s", MDL::dir, animfile);
+        animspec *sa = ((meshgroup *)mdl.meshes)->loadanim(path(filename));
+        if(!sa) { conoutf(CON_ERROR, "could not load %s anim file %s", MDL::formatname(), filename); return; }
+        skeleton *skel = ((meshgroup *)mdl.meshes)->skel;
+        int bone = skel ? skel->findbone(name) : -1;
+        if(bone < 0)
+        {
+            conoutf(CON_ERROR, "could not find bone %s to pitch target", name);
+            return;
+        }
+        loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == bone) return;
+        pitchtarget &t = skel->pitchtargets.add();
+        t.bone = bone;
+        t.frame = sa->frame + clamp(*frameoffset, 0, sa->range-1);
+        t.pitchmin = *pitchmin;
+        t.pitchmax = *pitchmax;
+    }
+
+    static void setpitchcorrect(char *name, char *targetname, float *scale, float *pitchmin, float *pitchmax)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        part &mdl = *(part *)MDL::loading->parts.last();
+        if(!mdl.meshes) return;
+        skeleton *skel = ((meshgroup *)mdl.meshes)->skel;
+        int bone = skel ? skel->findbone(name) : -1;
+        if(bone < 0)
+        {
+            conoutf(CON_ERROR, "could not find bone %s to pitch correct", name);
+            return;
+        }
+        if(skel->findpitchcorrect(bone) >= 0) return;
+        int targetbone = skel->findbone(targetname), target = -1;
+        if(targetbone >= 0) loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == targetbone) { target = i; break; }
+        if(target < 0)
+        {
+            conoutf(CON_ERROR, "could not find pitch target %s to pitch correct %s", targetname, name);
+            return;
+        }
+        pitchcorrect c;
+        c.bone = bone;
+        c.target = target;
+        c.pitchmin = *pitchmin;
+        c.pitchmax = *pitchmax;
+        c.pitchscale = *scale;
+        int pos = skel->pitchcorrects.length();
+        loopv(skel->pitchcorrects) if(bone <= skel->pitchcorrects[i].bone) { pos = i; break; }
+        skel->pitchcorrects.insert(pos, c); 
+    }
+
+    static void setanim(char *anim, char *animfile, float *speed, int *priority, int *startoffset, int *endoffset)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+    
+        vector<int> anims;
+        findanims(anim, anims);
+        if(anims.empty()) conoutf(CON_ERROR, "could not find animation %s", anim);
+        else
+        {
+            part *p = (part *)MDL::loading->parts.last();
+            if(!p->meshes) return;
+            defformatstring(filename, "%s/%s", MDL::dir, animfile);
+            animspec *sa = ((meshgroup *)p->meshes)->loadanim(path(filename));
+            if(!sa) conoutf(CON_ERROR, "could not load %s anim file %s", MDL::formatname(), filename);
+            else loopv(anims)
+            {
+                int start = sa->frame, end = sa->range;
+                if(*startoffset > 0) start += min(*startoffset, end-1);
+                else if(*startoffset < 0) start += max(end + *startoffset, 0);
+                end -= start - sa->frame;
+                if(*endoffset > 0) end = min(end, *endoffset);
+                else if(*endoffset < 0) end = max(end + *endoffset, 1); 
+                MDL::loading->parts.last()->setanim(p->numanimparts-1, anims[i], start, end, *speed, *priority);
+            }
+        }
+    }
+    
+    static void setanimpart(char *maskstr)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+    
+        part *p = (part *)MDL::loading->parts.last();
+    
+        vector<char *> bonestrs;
+        explodelist(maskstr, bonestrs);
+        vector<ushort> bonemask;
+        loopv(bonestrs)
+        {
+            char *bonestr = bonestrs[i];
+            int bone = p->meshes ? ((meshgroup *)p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr+1 : bonestr) : -1;
+            if(bone<0) { conoutf(CON_ERROR, "could not find bone %s for anim part mask [%s]", bonestr, maskstr); bonestrs.deletearrays(); return; }
+            bonemask.add(bone | (bonestr[0]=='!' ? BONEMASK_NOT : 0));
+        }
+        bonestrs.deletearrays();
+        bonemask.sort();
+        if(bonemask.length()) bonemask.add(BONEMASK_END);
+    
+        if(!p->addanimpart(bonemask.getbuf())) conoutf(CON_ERROR, "too many animation parts");
+    }
+
+    static void setadjust(char *name, float *yaw, float *pitch, float *roll, float *tx, float *ty, float *tz)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        part &mdl = *(part *)MDL::loading->parts.last();
+
+        if(!name[0]) return;
+        int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
+        if(i < 0) {  conoutf(CON_ERROR, "could not find bone %s to adjust", name); return; }
+        while(!MDL::adjustments.inrange(i)) MDL::adjustments.add(skeladjustment(0, 0, 0, vec(0, 0, 0)));
+        MDL::adjustments[i] = skeladjustment(*yaw, *pitch, *roll, vec(*tx/4, *ty/4, *tz/4));
+    }
+    
+    skelcommands()
+    {
+        if(MDL::multiparted()) this->modelcommand(loadpart, "load", "ssf");
+        this->modelcommand(settag, "tag", "ssffffff");
+        this->modelcommand(setpitch, "pitch", "sffff");
+        this->modelcommand(setpitchtarget, "pitchtarget", "ssiff");
+        this->modelcommand(setpitchcorrect, "pitchcorrect", "ssfff");
+        if(MDL::animated())
+        {
+            this->modelcommand(setanim, "anim", "ssfiii");
+            this->modelcommand(setanimpart, "animpart", "s");
+            this->modelcommand(setadjust, "adjust", "sffffff");
+        }
+    }
+};
+
diff --git a/src/engine/smd.h b/src/engine/smd.h
new file mode 100644 (file)
index 0000000..771ec9a
--- /dev/null
@@ -0,0 +1,447 @@
+struct smd;
+
+struct smdbone
+{
+    string name;
+    int parent;
+    smdbone() : parent(-1) { name[0] = '\0'; }
+};
+
+struct smd : skelloader<smd>
+{
+    smd(const char *name) : skelloader(name) {}
+
+    static const char *formatname() { return "smd"; }
+    int type() const { return MDL_SMD; }
+
+    struct smdmesh : skelmesh
+    {
+    };
+
+    struct smdmeshgroup : skelmeshgroup
+    {
+        smdmeshgroup() 
+        {
+        }
+
+        bool skipcomment(char *&curbuf)
+        {
+            while(*curbuf && isspace(*curbuf)) curbuf++;
+            switch(*curbuf)
+            {
+                case '#':
+                case ';':
+                case '\r':
+                case '\n':
+                case '\0':
+                    return true;
+                case '/':
+                    if(curbuf[1] == '/') return true;
+                    break;
+            }
+            return false;
+        }
+
+        void skipsection(stream *f, char *buf, size_t bufsize)
+        {
+            while(f->getline(buf, bufsize))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                if(!strncmp(curbuf, "end", 3)) break;
+            }
+        }
+
+        void readname(char *&curbuf, char *name, size_t namesize)
+        {
+            char *curname = name;
+            while(*curbuf && isspace(*curbuf)) curbuf++;
+            bool allowspace = false;
+            if(*curbuf == '"') { curbuf++; allowspace = true; }
+            while(*curbuf)
+            {
+                char c = *curbuf++;
+                if(c == '"') break;      
+                if(isspace(c) && !allowspace) break;
+                if(curname < &name[namesize-1]) *curname++ = c;
+            } 
+            *curname = '\0';
+        }
+
+        void readnodes(stream *f, char *buf, size_t bufsize, vector<smdbone> &bones)
+        {
+            while(f->getline(buf, bufsize))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                if(!strncmp(curbuf, "end", 3)) break;
+                int id = strtol(curbuf, &curbuf, 10);
+                string name;
+                readname(curbuf, name, sizeof(name));
+                int parent = strtol(curbuf, &curbuf, 10);
+                if(id < 0 || id > 255 || parent > 255 || !name[0]) continue; 
+                while(!bones.inrange(id)) bones.add();
+                smdbone &bone = bones[id];
+                copystring(bone.name, name);
+                bone.parent = parent;
+            }
+        }
+
+        void readmaterial(char *&curbuf, char *name, size_t namesize)
+        {
+            char *curname = name;
+            while(*curbuf && isspace(*curbuf)) curbuf++;
+            while(*curbuf)
+            {
+                char c = *curbuf++;
+                if(isspace(c)) break;
+                if(c == '.')
+                {
+                    while(*curbuf && !isspace(*curbuf)) curbuf++;
+                    break;
+                }
+                if(curname < &name[namesize-1]) *curname++ = c;
+            }
+            *curname = '\0';
+        }
+
+        struct smdmeshdata
+        {
+            smdmesh *mesh;
+            vector<vert> verts;
+            vector<tri> tris;
+
+            void finalize()
+            {
+                if(verts.empty() || tris.empty()) return;
+                vert *mverts = new vert[mesh->numverts + verts.length()];
+                if(mesh->numverts) 
+                {
+                    memcpy(mverts, mesh->verts, mesh->numverts*sizeof(vert));
+                    delete[] mesh->verts;
+                }
+                memcpy(&mverts[mesh->numverts], verts.getbuf(), verts.length()*sizeof(vert));
+                mesh->numverts += verts.length();
+                mesh->verts = mverts;
+                tri *mtris = new tri[mesh->numtris + tris.length()];
+                if(mesh->numtris) 
+                {
+                    memcpy(mtris, mesh->tris, mesh->numtris*sizeof(tri));
+                    delete[] mesh->tris;
+                }
+                memcpy(&mtris[mesh->numtris], tris.getbuf(), tris.length()*sizeof(tri));
+                mesh->numtris += tris.length();
+                mesh->tris = mtris;
+            }
+        };
+
+        struct smdvertkey : vert
+        {
+            smdmeshdata *mesh;
+            
+            smdvertkey(smdmeshdata *mesh) : mesh(mesh) {}
+        };
+     
+        void readtriangles(stream *f, char *buf, size_t bufsize)
+        {
+            smdmeshdata *curmesh = NULL;
+            hashtable<const char *, smdmeshdata> materials(1<<6);
+            hashset<int> verts(1<<12); 
+            while(f->getline(buf, bufsize))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                if(!strncmp(curbuf, "end", 3)) break;
+                string material;
+                readmaterial(curbuf, material, sizeof(material)); 
+                if(!curmesh || strcmp(curmesh->mesh->name, material))
+                {
+                    curmesh = materials.access(material);
+                    if(!curmesh)
+                    {
+                        smdmesh *m = new smdmesh;
+                        m->group = this;
+                        m->name = newstring(material);
+                        meshes.add(m);
+                        curmesh = &materials[m->name];
+                        curmesh->mesh = m;
+                    }
+                }
+                tri curtri;
+                loopi(3)                        
+                {
+                    char *curbuf;
+                    do
+                    {
+                        if(!f->getline(buf, bufsize)) goto endsection;
+                        curbuf = buf;
+                    } while(skipcomment(curbuf));
+                    smdvertkey key(curmesh);     
+                    int parent = -1, numlinks = 0, len = 0;
+                    if(sscanf(curbuf, " %d %f %f %f %f %f %f %f %f %d%n", &parent, &key.pos.x, &key.pos.y, &key.pos.z, &key.norm.x, &key.norm.y, &key.norm.z, &key.tc.x, &key.tc.y, &numlinks, &len) < 9) goto endsection;    
+                    curbuf += len;
+                    key.pos.y = -key.pos.y;
+                    key.norm.y = -key.norm.y;
+                    key.tc.y = 1 - key.tc.y;
+                    blendcombo c;
+                    int sorted = 0;
+                    float pweight = 0, tweight = 0;
+                    for(; numlinks > 0; numlinks--)
+                    {
+                        int bone = -1, len = 0;
+                        float weight = 0;
+                        if(sscanf(curbuf, " %d %f%n", &bone, &weight, &len) < 2) break;
+                        curbuf += len;
+                        tweight += weight;
+                        if(bone == parent) pweight += weight;                       
+                        else sorted = c.addweight(sorted, weight, bone);
+                    }
+                    if(tweight < 1) pweight += 1 - tweight;
+                    if(pweight > 0) sorted = c.addweight(sorted, pweight, parent);
+                    c.finalize(sorted);
+                    key.blend = curmesh->mesh->addblendcombo(c);
+                    int index = verts.access(key, curmesh->verts.length());
+                    if(index == curmesh->verts.length()) curmesh->verts.add(key);
+                    curtri.vert[2-i] = index;
+                }
+                curmesh->tris.add(curtri);
+            }
+        endsection:
+            enumerate(materials, smdmeshdata, data, data.finalize());
+        }
+
+        void readskeleton(stream *f, char *buf, size_t bufsize)
+        {
+            int frame = -1;
+            while(f->getline(buf, bufsize))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                if(sscanf(curbuf, " time %d", &frame) == 1) continue;
+                else if(!strncmp(curbuf, "end", 3)) break;
+                else if(frame != 0) continue;
+                int bone;
+                vec pos, rot;
+                if(sscanf(curbuf, " %d %f %f %f %f %f %f", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7)
+                    continue;
+                if(bone < 0 || bone >= skel->numbones)
+                    continue;
+                rot.x = -rot.x;
+                rot.z = -rot.z;
+                float cx = cosf(rot.x/2), sx = sinf(rot.x/2),
+                      cy = cosf(rot.y/2), sy = sinf(rot.y/2),
+                      cz = cosf(rot.z/2), sz = sinf(rot.z/2);
+                pos.y = -pos.y;
+                dualquat dq(quat(sx*cy*cz - cx*sy*sz,
+                                 cx*sy*cz + sx*cy*sz,
+                                 cx*cy*sz - sx*sy*cz,
+                                 cx*cy*cz + sx*sy*sz),
+                            pos);
+                boneinfo &b = skel->bones[bone];
+                if(b.parent < 0) b.base = dq;
+                else b.base.mul(skel->bones[b.parent].base, dq);
+                (b.invbase = b.base).invert();
+            }
+        }
+
+        bool loadmesh(const char *filename)
+        {
+            stream *f = openfile(filename, "r");
+            if(!f) return false;
+            
+            char buf[512];
+            int version = -1;
+            while(f->getline(buf, sizeof(buf)))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                if(sscanf(curbuf, " version %d", &version) == 1)
+                {
+                    if(version != 1) { delete f; return false; }
+                }
+                else if(!strncmp(curbuf, "nodes", 5))
+                {
+                    if(skel->numbones > 0) { skipsection(f, buf, sizeof(buf)); continue; }
+                    vector<smdbone> bones;
+                    readnodes(f, buf, sizeof(buf), bones); 
+                    if(bones.empty()) continue;
+                    skel->numbones = bones.length();
+                    skel->bones = new boneinfo[skel->numbones];
+                    loopv(bones)
+                    {
+                        boneinfo &dst = skel->bones[i];
+                        smdbone &src = bones[i];
+                        dst.name = newstring(src.name);
+                        dst.parent = src.parent;
+                    }
+                    skel->linkchildren();
+                }
+                else if(!strncmp(curbuf, "triangles", 9))
+                    readtriangles(f, buf, sizeof(buf));
+                else if(!strncmp(curbuf, "skeleton", 8))
+                {
+                    if(skel->shared > 1) skipsection(f, buf, sizeof(buf));
+                    else readskeleton(f, buf, sizeof(buf));
+                }
+                else if(!strncmp(curbuf, "vertexanimation", 15))
+                    skipsection(f, buf, sizeof(buf));
+            }
+
+            sortblendcombos();
+
+            delete f;
+            return true;
+        }
+
+        int readframes(stream *f, char *buf, size_t bufsize, vector<dualquat> &animbones)
+        {
+            int frame = -1, numframes = 0, lastbone = skel->numbones;
+            while(f->getline(buf, bufsize))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                int nextframe = -1;
+                if(sscanf(curbuf, " time %d", &nextframe) == 1)
+                {
+                    for(; lastbone < skel->numbones; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone];
+                    if(nextframe >= numframes)
+                    {
+                        databuf<dualquat> framebones = animbones.reserve(skel->numbones * (nextframe + 1 - numframes));
+                        loopi(nextframe - numframes) framebones.put(animbones.getbuf(), skel->numbones);
+                        animbones.addbuf(framebones);
+                        animbones.advance(skel->numbones);
+                        numframes = nextframe + 1;
+                    }
+                    frame = nextframe;
+                    lastbone = 0;
+                    continue;
+                }
+                else if(!strncmp(curbuf, "end", 3)) break;
+                int bone;
+                vec pos, rot;
+                if(sscanf(curbuf, " %d %f %f %f %f %f %f", &bone, &pos.x, &pos.y, &pos.z, &rot.x, &rot.y, &rot.z) != 7)
+                    continue;
+                if(bone < 0 || bone >= skel->numbones)
+                    continue;
+                for(; lastbone < bone; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone];
+                lastbone++;
+                float cx = cosf(rot.x/2), sx = sinf(rot.x/2),
+                      cy = cosf(rot.y/2), sy = sinf(rot.y/2),
+                      cz = cosf(rot.z/2), sz = sinf(rot.z/2);
+                pos.y = -pos.y;
+                dualquat dq(quat(-(sx*cy*cz - cx*sy*sz),
+                                 cx*sy*cz + sx*cy*sz,
+                                 -(cx*cy*sz - sx*sy*cz),
+                                 cx*cy*cz + sx*sy*sz),
+                            pos);
+                if(adjustments.inrange(bone)) adjustments[bone].adjust(dq);
+                dq.mul(skel->bones[bone].invbase);
+                dualquat &dst = animbones[frame*skel->numbones + bone];
+                if(skel->bones[bone].parent < 0) dst = dq;
+                else dst.mul(skel->bones[skel->bones[bone].parent].base, dq);
+                dst.fixantipodal(skel->numframes > 0 ? skel->framebones[bone] : animbones[bone]);
+            }
+            for(; lastbone < skel->numbones; lastbone++) animbones[frame*skel->numbones + lastbone] = animbones[lastbone];
+            return numframes;
+        }
+
+        skelanimspec *loadanim(const char *filename)
+        {
+            skelanimspec *sa = skel->findskelanim(filename);
+            if(sa || skel->numbones <= 0) return sa;
+
+            stream *f = openfile(filename, "r");
+            if(!f) return NULL;
+
+            char buf[512];
+            int version = -1;
+            vector<dualquat> animbones;
+            while(f->getline(buf, sizeof(buf)))
+            {
+                char *curbuf = buf;
+                if(skipcomment(curbuf)) continue;
+                if(sscanf(curbuf, " version %d", &version) == 1)
+                {
+                    if(version != 1) { delete f; return NULL; }
+                }
+                else if(!strncmp(curbuf, "nodes", 5))
+                {
+                    vector<smdbone> bones;
+                    readnodes(f, buf, sizeof(buf), bones);
+                    if(bones.length() != skel->numbones) { delete f; return NULL; }
+                }
+                else if(!strncmp(curbuf, "triangles", 9))
+                    skipsection(f, buf, sizeof(buf));
+                else if(!strncmp(curbuf, "skeleton", 8))
+                    readframes(f, buf, sizeof(buf), animbones);
+                else if(!strncmp(curbuf, "vertexanimation", 15))
+                    skipsection(f, buf, sizeof(buf));
+            }
+            int numframes = animbones.length() / skel->numbones;
+            dualquat *framebones = new dualquat[(skel->numframes+numframes)*skel->numbones];             
+            if(skel->framebones)
+            {
+                memcpy(framebones, skel->framebones, skel->numframes*skel->numbones*sizeof(dualquat));
+                delete[] skel->framebones;
+            }
+            memcpy(&framebones[skel->numframes*skel->numbones], animbones.getbuf(), numframes*skel->numbones*sizeof(dualquat));
+            skel->framebones = framebones;
+            sa = &skel->addskelanim(filename);
+            sa->frame = skel->numframes;
+            sa->range = numframes;
+            skel->numframes += numframes;
+
+            delete f;
+
+            return sa;
+        }
+
+        bool load(const char *meshfile)
+        {
+            name = newstring(meshfile);
+
+            if(!loadmesh(meshfile)) return false;
+            
+            return true;
+        }
+    };            
+
+    meshgroup *loadmeshes(const char *name, va_list args)
+    {
+        smdmeshgroup *group = new smdmeshgroup;
+        group->shareskeleton(va_arg(args, char *));
+        if(!group->load(name)) { delete group; return NULL; }
+        return group;
+    }
+
+    bool loaddefaultparts()
+    {
+        skelpart &mdl = addpart();
+        mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+        adjustments.setsize(0);
+        const char *fname = name + strlen(name);
+        do --fname; while(fname >= name && *fname!='/' && *fname!='\\');
+        fname++;
+        defformatstring(meshname, "packages/models/%s/%s.smd", name, fname);
+        mdl.meshes = sharemeshes(path(meshname), NULL);
+        if(!mdl.meshes) return false;
+        mdl.initanimparts();
+        mdl.initskins();
+        return true;
+    }
+};
+
+static inline uint hthash(const smd::smdmeshgroup::smdvertkey &k)
+{
+    return hthash(k.pos);
+}
+
+static inline bool htcmp(const smd::smdmeshgroup::smdvertkey &k, int index)
+{
+    if(!k.mesh->verts.inrange(index)) return false;
+    const smd::vert &v = k.mesh->verts[index];
+    return k.pos == v.pos && k.norm == v.norm && k.tc == v.tc && k.blend == v.blend;
+}
+
+skelcommands<smd> smdcommands;
+
diff --git a/src/engine/sound.cpp b/src/engine/sound.cpp
new file mode 100644 (file)
index 0000000..38ff025
--- /dev/null
@@ -0,0 +1,991 @@
+// sound.cpp: basic positional sound using sdl_mixer
+
+#include "engine.h"
+#include "SDL_mixer.h"
+
+bool nosound = true;
+
+struct soundsample
+{
+    char *name;
+    Mix_Chunk *chunk;
+
+    soundsample() : name(NULL), chunk(NULL) {}
+    ~soundsample() { DELETEA(name); }
+
+    void cleanup() { if(chunk) { Mix_FreeChunk(chunk); chunk = NULL; } }
+    bool load(bool msg = false);
+};
+
+struct soundslot
+{
+    soundsample *sample;
+    int volume;
+};
+
+struct soundconfig
+{
+    int slots, numslots;
+    int maxuses;
+
+    bool hasslot(const soundslot *p, const vector<soundslot> &v) const
+    {
+        return p >= v.getbuf() + slots && p < v.getbuf() + slots+numslots && slots+numslots < v.length(); 
+    }
+
+    int chooseslot(int flags) const
+    {
+        if(flags&SND_NO_ALT || numslots <= 1) return slots;
+        if(flags&SND_USE_ALT) return slots + 1 + rnd(numslots - 1);
+        return slots + rnd(numslots);
+    }
+};
+
+struct soundchannel
+{ 
+    int id;
+    bool inuse;
+    vec loc; 
+    soundslot *slot;
+    extentity *ent; 
+    int radius, volume, pan, flags;
+    bool dirty;
+
+    soundchannel(int id) : id(id) { reset(); }
+
+    bool hasloc() const { return loc.x >= -1e15f; }
+    void clearloc() { loc = vec(-1e16f, -1e16f, -1e16f); }
+
+    void reset()
+    {
+        inuse = false;
+        clearloc();
+        slot = NULL;
+        ent = NULL;
+        radius = 0;
+        volume = -1;
+        pan = -1;
+        flags = 0;
+        dirty = false;
+    }
+};
+vector<soundchannel> channels;
+int maxchannels = 0;
+
+soundchannel &newchannel(int n, soundslot *slot, const vec *loc = NULL, extentity *ent = NULL, int flags = 0, int radius = 0)
+{
+    if(ent)
+    {
+        loc = &ent->o;
+        ent->flags |= EF_SOUND;
+    }
+    while(!channels.inrange(n)) channels.add(channels.length());
+    soundchannel &chan = channels[n];
+    chan.reset();
+    chan.inuse = true;
+    if(loc) chan.loc = *loc;
+    chan.slot = slot;
+    chan.ent = ent;
+    chan.flags = 0;
+    chan.radius = radius;
+    return chan;
+}
+
+void freechannel(int n)
+{
+    if(!channels.inrange(n) || !channels[n].inuse) return;
+    soundchannel &chan = channels[n];
+    chan.inuse = false;
+    if(chan.ent) chan.ent->flags &= ~EF_SOUND;
+}
+
+void syncchannel(soundchannel &chan)
+{
+    if(!chan.dirty) return;
+    if(!Mix_FadingChannel(chan.id)) Mix_Volume(chan.id, chan.volume);
+    Mix_SetPanning(chan.id, 255-chan.pan, chan.pan);
+    chan.dirty = false;
+}
+
+void stopchannels()
+{
+    loopv(channels)
+    {
+        soundchannel &chan = channels[i];
+        if(!chan.inuse) continue;
+        Mix_HaltChannel(i);
+        freechannel(i);
+    }
+}
+
+void setmusicvol(int musicvol);
+extern int musicvol;
+static int curvol = 0;
+VARFP(soundvol, 0, 255, 255,
+{
+    if(!soundvol) { stopchannels(); setmusicvol(0); }
+    else if(!curvol) setmusicvol(musicvol);
+    curvol = soundvol;
+});
+VARFP(musicvol, 0, 128, 255, setmusicvol(soundvol ? musicvol : 0));
+
+char *musicfile = NULL, *musicdonecmd = NULL;
+
+Mix_Music *music = NULL;
+SDL_RWops *musicrw = NULL;
+stream *musicstream = NULL;
+
+void setmusicvol(int musicvol)
+{
+    if(nosound) return;
+    if(music) Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255);
+}
+
+void stopmusic()
+{
+    if(nosound) return;
+    DELETEA(musicfile);
+    DELETEA(musicdonecmd);
+    if(music)
+    {
+        Mix_HaltMusic();
+        Mix_FreeMusic(music);
+        music = NULL;
+    }
+    if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; }
+    DELETEP(musicstream);
+}
+
+#ifdef WIN32
+#define AUDIODRIVER "directsound winmm"
+#else
+#define AUDIODRIVER ""
+#endif
+bool shouldinitaudio = true;
+SVARF(audiodriver, AUDIODRIVER, { shouldinitaudio = true; initwarning("sound configuration", INIT_RESET, CHANGE_SOUND); });
+VARF(usesound, 0, 1, 1, { shouldinitaudio = true; initwarning("sound configuration", INIT_RESET, CHANGE_SOUND); });
+VARF(soundchans, 1, 32, 128, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+VARF(soundfreq, 0, MIX_DEFAULT_FREQUENCY, 48000, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+VARF(soundbufferlen, 128, 1024, 4096, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
+
+bool initaudio()
+{
+    static string fallback = "";
+    static bool initfallback = true;
+    static bool restorefallback = false;
+    if(initfallback)
+    {
+        initfallback = false;
+        if(char *env = SDL_getenv("SDL_AUDIODRIVER")) copystring(fallback, env);
+    }
+    if(!fallback[0] && audiodriver[0])
+    {
+        vector<char*> drivers;
+        explodelist(audiodriver, drivers);
+        loopv(drivers)
+        {
+            restorefallback = true;
+            SDL_setenv("SDL_AUDIODRIVER", drivers[i], 1);
+            if(SDL_InitSubSystem(SDL_INIT_AUDIO) >= 0)
+            {
+                drivers.deletearrays();
+                return true;
+            }
+        }
+        drivers.deletearrays();
+    }
+    if(restorefallback)
+    {
+        restorefallback = false;
+    #ifdef WIN32
+        SDL_setenv("SDL_AUDIODRIVER", fallback, 1);
+    #else
+        unsetenv("SDL_AUDIODRIVER");
+    #endif
+    }
+    if(SDL_InitSubSystem(SDL_INIT_AUDIO) >= 0) return true;
+    conoutf(CON_ERROR, "sound init failed: %s", SDL_GetError());
+    return false;
+}
+
+void initsound()
+{
+    SDL_version version;
+    SDL_GetVersion(&version);
+    if(version.major == 2 && version.minor == 0 && version.patch == 6)
+    {
+        nosound = true;
+        if(usesound) conoutf(CON_ERROR, "audio is broken in SDL 2.0.6");
+        return;
+    }
+
+    if(shouldinitaudio)
+    {
+        shouldinitaudio = false;
+        if(SDL_WasInit(SDL_INIT_AUDIO)) SDL_QuitSubSystem(SDL_INIT_AUDIO);
+        if(!usesound || !initaudio())
+        {
+            nosound = true;
+            return;
+        }
+    }
+
+    if(Mix_OpenAudio(soundfreq, MIX_DEFAULT_FORMAT, 2, soundbufferlen)<0)
+    {
+        nosound = true;
+        conoutf(CON_ERROR, "sound init failed (SDL_mixer): %s", Mix_GetError());
+        return;
+    }
+       Mix_AllocateChannels(soundchans);       
+    maxchannels = soundchans;
+    nosound = false;
+}
+
+void musicdone()
+{
+    if(music) { Mix_HaltMusic(); Mix_FreeMusic(music); music = NULL; }
+    if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; }
+    DELETEP(musicstream);
+    DELETEA(musicfile);
+    if(!musicdonecmd) return;
+    char *cmd = musicdonecmd;
+    musicdonecmd = NULL;
+    execute(cmd);
+    delete[] cmd;
+}
+
+Mix_Music *loadmusic(const char *name)
+{
+    if(!musicstream) musicstream = openzipfile(name, "rb");
+    if(musicstream)
+    {
+        if(!musicrw) musicrw = musicstream->rwops();
+        if(!musicrw) DELETEP(musicstream);
+    }
+    if(musicrw) music = Mix_LoadMUSType_RW(musicrw, MUS_NONE, 0);
+    else music = Mix_LoadMUS(findfile(name, "rb")); 
+    if(!music)
+    {
+        if(musicrw) { SDL_FreeRW(musicrw); musicrw = NULL; }
+        DELETEP(musicstream);
+    }
+    return music;
+}
+void startmusic(char *name, char *cmd)
+{
+    if(nosound) return;
+    stopmusic();
+    if(soundvol && musicvol && *name)
+    {
+        defformatstring(file, "packages/%s", name);
+        path(file);
+        if(loadmusic(file))
+        {
+            DELETEA(musicfile);
+            DELETEA(musicdonecmd);
+            musicfile = newstring(file);
+            if(cmd[0]) musicdonecmd = newstring(cmd);
+            Mix_PlayMusic(music, cmd[0] ? 0 : -1);
+            Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255);
+            intret(1);
+        }
+        else
+        {
+            conoutf(CON_ERROR, "could not play music: %s", file);
+            intret(0); 
+        }
+    }
+}
+
+COMMANDN(music, startmusic, "ss");
+
+static Mix_Chunk *loadwav(const char *name)
+{
+    Mix_Chunk *c = NULL;
+    stream *z = openzipfile(name, "rb");
+    if(z)
+    {
+        SDL_RWops *rw = z->rwops();
+        if(rw)
+        {
+            c = Mix_LoadWAV_RW(rw, 0);
+            SDL_FreeRW(rw);
+        }
+        delete z;
+    }
+    if(!c) c = Mix_LoadWAV(findfile(name, "rb"));
+    return c;
+}
+
+template<class T> static void scalewav(T* dst, T* src, size_t len, int scale)
+{
+    len /= sizeof(T);
+    const T* end = src + len;
+    if(scale==2) for(; src < end; src++, dst += scale)
+    {
+        T s = src[0];
+        dst[0] = s;
+        dst[1] = s;
+    }
+    else if(scale==4) for(; src < end; src++, dst += scale)
+    {
+        T s = src[0];
+        dst[0] = s;
+        dst[1] = s;
+        dst[2] = s;
+        dst[3] = s;
+    }
+    else for(; src < end; src++)
+    {
+        T s = src[0];
+        loopi(scale) *dst++ = s;
+    }
+}
+
+static Mix_Chunk *loadwavscaled(const char *name)
+{
+    int mixerfreq = 0;
+    Uint16 mixerformat = 0;
+    int mixerchannels = 0;
+    if(!Mix_QuerySpec(&mixerfreq, &mixerformat, &mixerchannels)) return NULL;
+
+    SDL_AudioSpec spec;
+    Uint8 *audiobuf = NULL;
+    Uint32 audiolen = 0;
+    stream *z = openzipfile(name, "rb");
+    if(z)
+    {
+        SDL_RWops *rw = z->rwops();
+        if(rw)
+        {
+            SDL_LoadWAV_RW(rw, 0, &spec, &audiobuf, &audiolen);
+            SDL_FreeRW(rw);
+        }
+        delete z;
+    }
+    if(!audiobuf) SDL_LoadWAV(findfile(name, "rb"), &spec, &audiobuf, &audiolen);
+    if(!audiobuf) return NULL;
+    int samplesize = ((spec.format&0xFF)/8) * spec.channels;
+    int scale = mixerfreq / spec.freq;
+    if(scale >= 2)
+    {
+        Uint8 *scalebuf = (Uint8*)SDL_malloc(audiolen * scale);
+        if(scalebuf)
+        {
+            switch(samplesize)
+            {
+                case 1: scalewav((uchar*)scalebuf, (uchar*)audiobuf, audiolen, scale); break;
+                case 2: scalewav((ushort*)scalebuf, (ushort*)audiobuf, audiolen, scale); break;
+                case 4: scalewav((uint*)scalebuf, (uint*)audiobuf, audiolen, scale); break;
+                case 8: scalewav((ullong*)scalebuf, (ullong*)audiobuf, audiolen, scale); break;
+                default: SDL_free(scalebuf); scalebuf = NULL; break;
+            }
+            if(scalebuf)
+            {
+                SDL_free(audiobuf);
+                audiobuf = scalebuf;
+                audiolen *= scale;
+                spec.freq *= scale;
+            }
+        }
+    }
+    if(spec.freq != mixerfreq || spec.format != mixerformat || spec.channels != mixerchannels)
+    {
+        SDL_AudioCVT cvt;
+        if(SDL_BuildAudioCVT(&cvt, spec.format, spec.channels, spec.freq, mixerformat, mixerchannels, mixerfreq) < 0)
+        {
+            SDL_free(audiobuf);
+            return NULL;
+        }
+        if(cvt.filters[0])
+        {
+            cvt.len = audiolen & ~(samplesize-1);
+            cvt.buf = (Uint8*)SDL_malloc(cvt.len * cvt.len_mult);
+            if(!cvt.buf) { SDL_free(audiobuf); return NULL; }
+            SDL_memcpy(cvt.buf, audiobuf, cvt.len);
+            SDL_free(audiobuf);
+            if(SDL_ConvertAudio(&cvt) < 0) { SDL_free(cvt.buf); return NULL; }
+            audiobuf = cvt.buf;
+            audiolen = cvt.len_cvt;
+        }
+    }
+    Mix_Chunk *c = Mix_QuickLoad_RAW(audiobuf, audiolen);
+    if(!c) { SDL_free(audiobuf); return NULL; }
+    c->allocated = 1;
+    return c;
+}
+
+VARFP(fixwav, 0, 1, 1, initwarning("sound configuration", INIT_LOAD, CHANGE_SOUND));
+
+bool soundsample::load(bool msg)
+{
+    if(chunk) return true;
+    if(!name[0]) return false;
+
+    static const char * const exts[] = { "", ".wav", ".ogg" };
+    string filename;
+    loopi(sizeof(exts)/sizeof(exts[0]))
+    {
+        formatstring(filename, "packages/sounds/%s%s", name, exts[i]);
+        if(msg && !i) renderprogress(0, filename);
+        path(filename);
+        if(fixwav)
+        {
+            size_t len = strlen(filename);
+            if(len >= 4 && !strcasecmp(filename + len - 4, ".wav"))
+            {
+                chunk = loadwavscaled(filename);
+                if(chunk) return true;
+            }
+        }
+        chunk = loadwav(filename);
+        if(chunk) return true;
+    }
+
+    conoutf(CON_ERROR, "failed to load sample: packages/sounds/%s", name);
+    return false;
+}
+
+static hashnameset<soundsample> samples;
+
+static void cleanupsamples()
+{
+    enumerate(samples, soundsample, s, s.cleanup());
+}
+
+static struct soundtype
+{
+    vector<soundslot> slots;
+    vector<soundconfig> configs;
+
+    int findsound(const char *name, int vol)
+    {
+        loopv(configs)
+        {
+            soundconfig &s = configs[i];
+            loopj(s.numslots)
+            {
+                soundslot &c = slots[s.slots+j];
+                if(!strcmp(c.sample->name, name) && (!vol || c.volume==vol)) return i;
+            }
+        }
+        return -1;
+    }
+
+    int addslot(const char *name, int vol)
+    {
+        soundsample *s = samples.access(name);
+        if(!s)
+        {
+            char *n = newstring(name);
+            s = &samples[n];
+            s->name = n;
+            s->chunk = NULL;
+        }
+        soundslot *oldslots = slots.getbuf();
+        int oldlen = slots.length();
+        soundslot &slot = slots.add();
+        // soundslots.add() may relocate slot pointers
+        if(slots.getbuf() != oldslots) loopv(channels)
+        {
+            soundchannel &chan = channels[i];
+            if(chan.inuse && chan.slot >= oldslots && chan.slot < &oldslots[oldlen])
+                chan.slot = &slots[chan.slot - oldslots];
+        }
+        slot.sample = s;
+        slot.volume = vol ? vol : 100;
+        return oldlen;
+    }
+
+    int addsound(const char *name, int vol, int maxuses = 0)
+    {
+        soundconfig &s = configs.add();
+        s.slots = addslot(name, vol);
+        s.numslots = 1;
+        s.maxuses = maxuses;
+        return configs.length()-1;
+    }
+
+    void addalt(const char *name, int vol)
+    {
+        if(configs.empty()) return;
+        addslot(name, vol);
+        configs.last().numslots++;
+    }
+
+    void clear()
+    {
+        slots.setsize(0);
+        configs.setsize(0);
+    }
+
+    void reset()
+    {
+        loopv(channels)
+        {
+            soundchannel &chan = channels[i];
+            if(chan.inuse && slots.inbuf(chan.slot))
+            {
+                Mix_HaltChannel(i);
+                freechannel(i);
+            }
+        }
+        clear();
+    }
+
+    void preloadsound(int n)
+    {
+        if(nosound || !configs.inrange(n)) return;
+        soundconfig &config = configs[n];
+        loopk(config.numslots) slots[config.slots+k].sample->load(true);
+    }
+
+    bool playing(const soundchannel &chan, const soundconfig &config) const
+    {
+        return chan.inuse && config.hasslot(chan.slot, slots);
+    }
+} gamesounds, mapsounds;
+
+void registersound(char *name, int *vol) { intret(gamesounds.addsound(name, *vol, 0)); }
+COMMAND(registersound, "si");
+
+void mapsound(char *name, int *vol, int *maxuses) { intret(mapsounds.addsound(name, *vol, *maxuses < 0 ? 0 : max(1, *maxuses))); }
+COMMAND(mapsound, "sii");
+
+void altsound(char *name, int *vol) { gamesounds.addalt(name, *vol); }
+COMMAND(altsound, "si");
+
+void altmapsound(char *name, int *vol) { mapsounds.addalt(name, *vol); }
+COMMAND(altmapsound, "si");
+
+ICOMMAND(numsounds, "", (), intret(gamesounds.configs.length()));
+ICOMMAND(nummapsounds, "", (), intret(mapsounds.configs.length()));
+
+void soundreset()
+{
+    gamesounds.reset();
+}
+COMMAND(soundreset, "");
+
+void mapsoundreset()
+{
+    mapsounds.reset();
+}
+COMMAND(mapsoundreset, "");
+
+void resetchannels()
+{
+    loopv(channels) if(channels[i].inuse) freechannel(i);
+    channels.shrink(0);
+}
+
+void clear_sound()
+{
+    closemumble();
+    if(nosound) return;
+    stopmusic();
+
+    cleanupsamples();
+    gamesounds.clear();
+    mapsounds.clear();
+    samples.clear();
+    Mix_CloseAudio();
+    resetchannels();
+}
+
+void stopmapsounds()
+{
+    loopv(channels) if(channels[i].inuse && channels[i].ent)
+    {
+        Mix_HaltChannel(i);
+        freechannel(i);
+    }
+}
+
+void clearmapsounds()
+{
+    stopmapsounds();
+    mapsounds.clear();
+}
+
+void stopmapsound(extentity *e)
+{
+    loopv(channels)
+    {
+        soundchannel &chan = channels[i];
+        if(chan.inuse && chan.ent == e)
+        {
+            Mix_HaltChannel(i);
+            freechannel(i);
+        }
+    }
+}
+
+void checkmapsounds()
+{
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        extentity &e = *ents[i];
+        if(e.type!=ET_SOUND) continue;
+        if(camera1->o.dist(e.o) < e.attr2)
+        {
+            if(!(e.flags&EF_SOUND)) playsound(e.attr1, NULL, &e, SND_MAP, -1);
+        }
+        else if(e.flags&EF_SOUND) stopmapsound(&e);
+    }
+}
+
+VAR(stereo, 0, 1, 1);
+
+bool updatechannel(soundchannel &chan)
+{
+    if(!chan.slot) return false;
+    int vol = soundvol, pan = 255/2;
+    if(chan.hasloc())
+    {
+        vec v;
+        float dist = chan.loc.dist(camera1->o, v);
+        int rad = 0;
+        if(chan.ent)
+        {
+            rad = chan.ent->attr2;
+            if(chan.ent->attr3)
+            {
+                rad -= chan.ent->attr3;
+                dist -= chan.ent->attr3;
+            }
+        }
+        else if(chan.radius > 0) rad = chan.radius;
+        if(rad > 0) vol -= int(clamp(dist/rad, 0.0f, 1.0f)*soundvol); // simple mono distance attenuation
+        if(stereo && (v.x != 0 || v.y != 0) && dist>0)
+        {
+            v.rotate_around_z(-camera1->yaw*RAD);
+            pan = int(255.9f*(0.5f - 0.5f*v.x/v.magnitude2())); // range is from 0 (left) to 255 (right)
+        }
+    }
+    vol = (vol*MIX_MAX_VOLUME*chan.slot->volume)/255/255;
+    vol = min(vol, MIX_MAX_VOLUME);
+    if(vol == chan.volume && pan == chan.pan) return false;
+    chan.volume = vol;
+    chan.pan = pan;
+    chan.dirty = true;
+    return true;
+}  
+
+void reclaimchannels()
+{
+    loopv(channels)
+    {
+        soundchannel &chan = channels[i];
+        if(chan.inuse && !Mix_Playing(i)) freechannel(i);
+    }
+}
+
+void syncchannels()
+{
+    loopv(channels)
+    {
+        soundchannel &chan = channels[i];
+        if(chan.inuse && chan.hasloc() && updatechannel(chan)) syncchannel(chan);
+    }
+}
+
+VARP(minimizedsounds, 0, 0, 1);
+
+void updatesounds()
+{
+    updatemumble();
+    if(nosound) return;
+    if(minimized && !minimizedsounds) stopsounds();
+    else
+    {
+        reclaimchannels();
+        if(mainmenu) stopmapsounds();
+        else checkmapsounds();
+        syncchannels();
+    }
+    if(music)
+    {
+        if(!Mix_PlayingMusic()) musicdone();
+        else if(Mix_PausedMusic()) Mix_ResumeMusic();
+    }
+}
+
+VARP(maxsoundsatonce, 0, 7, 100);
+
+VAR(dbgsound, 0, 0, 1);
+
+void preloadsound(int n)
+{
+    gamesounds.preloadsound(n);
+}
+
+void preloadmapsound(int n)
+{
+    mapsounds.preloadsound(n);
+}
+
+void preloadmapsounds()
+{
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        extentity &e = *ents[i];
+        if(e.type==ET_SOUND) mapsounds.preloadsound(e.attr1);
+    }
+}
+int playsound(int n, const vec *loc, extentity *ent, int flags, int loops, int fade, int chanid, int radius, int expire)
+{
+    if(nosound || !soundvol || (minimized && !minimizedsounds)) return -1;
+
+    soundtype &sounds = ent || flags&SND_MAP ? mapsounds : gamesounds;
+    if(!sounds.configs.inrange(n)) { conoutf(CON_WARN, "unregistered sound: %d", n); return -1; }
+    soundconfig &config = sounds.configs[n];
+
+    if(loc)
+    {
+        // cull sounds that are unlikely to be heard
+        int maxrad = game::maxsoundradius(n);
+        if(radius <= 0 || maxrad < radius) radius = maxrad;
+        if(camera1->o.dist(*loc) > 1.5f*radius)
+        {
+            if(channels.inrange(chanid) && sounds.playing(channels[chanid], config))
+            {
+                Mix_HaltChannel(chanid);
+                freechannel(chanid);
+            }
+            return -1;    
+        }
+    }
+
+    if(chanid < 0)
+    {
+        if(config.maxuses)
+        {
+            int uses = 0;
+            loopv(channels) if(sounds.playing(channels[i], config) && ++uses >= config.maxuses) return -1;
+        }
+
+        // avoid bursts of sounds with heavy packetloss and in sp
+        static int soundsatonce = 0, lastsoundmillis = 0;
+        if(totalmillis == lastsoundmillis) soundsatonce++; else soundsatonce = 1;
+        lastsoundmillis = totalmillis;
+        if(maxsoundsatonce && soundsatonce > maxsoundsatonce) return -1;
+    }
+
+    if(channels.inrange(chanid))
+    {
+        soundchannel &chan = channels[chanid];
+        if(sounds.playing(chan, config))
+        {
+            if(loc) chan.loc = *loc;
+            else if(chan.hasloc()) chan.clearloc();
+            return chanid;
+        }
+    }
+    if(fade < 0) return -1;
+
+    soundslot &slot = sounds.slots[config.chooseslot(flags)];
+    if(!slot.sample->chunk && !slot.sample->load()) return -1;
+
+    if(dbgsound) conoutf(CON_DEBUG, "sound: %s", slot.sample->name);
+    chanid = -1;
+    loopv(channels) if(!channels[i].inuse) { chanid = i; break; }
+    if(chanid < 0 && channels.length() < maxchannels) chanid = channels.length();
+    if(chanid < 0) loopv(channels) if(!channels[i].volume) { chanid = i; break; }
+    if(chanid < 0) return -1;
+
+    soundchannel &chan = newchannel(chanid, &slot, loc, ent, flags, radius);
+    updatechannel(chan);
+    int playing = -1;
+    if(fade) 
+    {
+        Mix_Volume(chanid, chan.volume);
+        playing = expire >= 0 ? Mix_FadeInChannelTimed(chanid, slot.sample->chunk, loops, fade, expire) : Mix_FadeInChannel(chanid, slot.sample->chunk, loops, fade);
+    }
+    else playing = expire >= 0 ? Mix_PlayChannelTimed(chanid, slot.sample->chunk, loops, expire) : Mix_PlayChannel(chanid, slot.sample->chunk, loops);
+    if(playing >= 0) syncchannel(chan); 
+    else freechannel(chanid);
+    return playing;
+}
+
+void stopsounds()
+{
+    loopv(channels) if(channels[i].inuse)
+    {
+        Mix_HaltChannel(i);
+        freechannel(i);
+    }
+}
+
+bool stopsound(int n, int chanid, int fade)
+{
+    if(!gamesounds.configs.inrange(n) || !channels.inrange(chanid) || !channels[chanid].inuse || !gamesounds.playing(channels[chanid], gamesounds.configs[n])) return false;
+    if(dbgsound) conoutf(CON_DEBUG, "stopsound: %s", channels[chanid].slot->sample->name);
+    if(!fade || !Mix_FadeOutChannel(chanid, fade))
+    {
+        Mix_HaltChannel(chanid);
+        freechannel(chanid);
+    }
+    return true;
+}
+
+int playsoundname(const char *s, const vec *loc, int vol, int flags, int loops, int fade, int chanid, int radius, int expire) 
+{ 
+    if(!vol) vol = 100;
+    int id = gamesounds.findsound(s, vol);
+    if(id < 0) id = gamesounds.addsound(s, vol);
+    return playsound(id, loc, NULL, flags, loops, fade, chanid, radius, expire);
+}
+
+ICOMMAND(sound, "i", (int *n), playsound(*n));
+
+void resetsound()
+{
+    clearchanges(CHANGE_SOUND);
+    if(!nosound) 
+    {
+        cleanupsamples();
+        if(music)
+        {
+            Mix_HaltMusic();
+            Mix_FreeMusic(music);
+        }
+        if(musicstream) musicstream->seek(0, SEEK_SET);
+        Mix_CloseAudio();
+    }
+    initsound();
+    resetchannels();
+    if(nosound)
+    {
+        DELETEA(musicfile);
+        DELETEA(musicdonecmd);
+        music = NULL;
+        cleanupsamples();
+        return;
+    }
+    if(music && loadmusic(musicfile))
+    {
+        Mix_PlayMusic(music, musicdonecmd ? 0 : -1);
+        Mix_VolumeMusic((musicvol*MIX_MAX_VOLUME)/255);
+    }
+    else
+    {
+        DELETEA(musicfile);
+        DELETEA(musicdonecmd);
+    }
+}
+
+COMMAND(resetsound, "");
+
+#ifdef WIN32
+
+#include <wchar.h>
+
+#else
+
+#include <unistd.h>
+
+#ifdef _POSIX_SHARED_MEMORY_OBJECTS
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <wchar.h>
+#endif
+
+#endif
+
+#if defined(WIN32) || defined(_POSIX_SHARED_MEMORY_OBJECTS)
+struct MumbleInfo
+{
+    int version, timestamp;
+    vec pos, front, top;
+    wchar_t name[256];
+};
+#endif
+
+#ifdef WIN32
+static HANDLE mumblelink = NULL;
+static MumbleInfo *mumbleinfo = NULL;
+#define VALID_MUMBLELINK (mumblelink && mumbleinfo)
+#elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
+static int mumblelink = -1;
+static MumbleInfo *mumbleinfo = (MumbleInfo *)-1; 
+#define VALID_MUMBLELINK (mumblelink >= 0 && mumbleinfo != (MumbleInfo *)-1)
+#endif
+
+#ifdef VALID_MUMBLELINK
+VARFP(mumble, 0, 1, 1, { if(mumble) initmumble(); else closemumble(); });
+#else
+VARFP(mumble, 0, 0, 1, { if(mumble) initmumble(); else closemumble(); });
+#endif
+
+void initmumble()
+{
+    if(!mumble) return;
+#ifdef VALID_MUMBLELINK
+    if(VALID_MUMBLELINK) return;
+
+    #ifdef WIN32
+        mumblelink = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "MumbleLink");
+        if(mumblelink)
+        {
+            mumbleinfo = (MumbleInfo *)MapViewOfFile(mumblelink, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(MumbleInfo));
+            if(mumbleinfo) wcsncpy(mumbleinfo->name, L"Sauerbraten", 256);
+        }
+    #elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
+        defformatstring(shmname, "/MumbleLink.%d", getuid());
+        mumblelink = shm_open(shmname, O_RDWR, 0);
+        if(mumblelink >= 0)
+        {
+            mumbleinfo = (MumbleInfo *)mmap(NULL, sizeof(MumbleInfo), PROT_READ|PROT_WRITE, MAP_SHARED, mumblelink, 0);
+            if(mumbleinfo != (MumbleInfo *)-1) wcsncpy(mumbleinfo->name, L"Sauerbraten", 256);
+        }
+    #endif
+    if(!VALID_MUMBLELINK) closemumble();
+#else
+    conoutf(CON_ERROR, "Mumble positional audio is not available on this platform.");
+#endif
+}
+
+void closemumble()
+{
+#ifdef WIN32
+    if(mumbleinfo) { UnmapViewOfFile(mumbleinfo); mumbleinfo = NULL; }
+    if(mumblelink) { CloseHandle(mumblelink); mumblelink = NULL; }
+#elif defined(_POSIX_SHARED_MEMORY_OBJECTS)
+    if(mumbleinfo != (MumbleInfo *)-1) { munmap(mumbleinfo, sizeof(MumbleInfo)); mumbleinfo = (MumbleInfo *)-1; } 
+    if(mumblelink >= 0) { close(mumblelink); mumblelink = -1; }
+#endif
+}
+
+static inline vec mumblevec(const vec &v, bool pos = false)
+{
+    // change from X left, Z up, Y forward to X right, Y up, Z forward
+    // 8 cube units = 1 meter
+    vec m(-v.x, v.z, v.y);
+    if(pos) m.div(8);
+    return m;
+}
+
+void updatemumble()
+{
+#ifdef VALID_MUMBLELINK
+    if(!VALID_MUMBLELINK) return;
+
+    static int timestamp = 0;
+
+    mumbleinfo->version = 1;
+    mumbleinfo->timestamp = ++timestamp;
+
+    mumbleinfo->pos = mumblevec(player->o, true);
+    mumbleinfo->front = mumblevec(vec(RAD*player->yaw, RAD*player->pitch));
+    mumbleinfo->top = mumblevec(vec(RAD*player->yaw, RAD*(player->pitch+90)));
+#endif
+}
+
diff --git a/src/engine/textedit.h b/src/engine/textedit.h
new file mode 100644 (file)
index 0000000..c273661
--- /dev/null
@@ -0,0 +1,770 @@
+
+struct editline
+{
+    enum { CHUNKSIZE = 256 };
+
+    char *text;
+    int len, maxlen;
+
+    editline() : text(NULL), len(0), maxlen(0) {}
+    editline(const char *init) : text(NULL), len(0), maxlen(0)
+    {
+        set(init);
+    }
+
+    bool empty() { return len <= 0; }
+
+    void clear()
+    {
+        DELETEA(text);
+        len = maxlen = 0;
+    }
+
+    bool grow(int total, const char *fmt = "", ...)
+    {
+        if(total + 1 <= maxlen) return false;
+        maxlen = (total + CHUNKSIZE) - total%CHUNKSIZE;
+        char *newtext = new char[maxlen];
+        if(fmt)
+        {
+            va_list args;
+            va_start(args, fmt);
+            vformatstring(newtext, fmt, args, maxlen);
+            va_end(args);
+        }
+        else newtext[0] = '\0';
+        DELETEA(text);
+        text = newtext;
+        return true;
+    }
+
+    void set(const char *str, int slen = -1)
+    {
+        if(slen < 0)
+        {
+            slen = strlen(str);
+            if(!grow(slen, "%s", str)) memcpy(text, str, slen + 1);
+        }
+        else
+        {
+            grow(slen);
+            memcpy(text, str, slen);
+            text[slen] = '\0';
+        }
+        len = slen;
+    }
+
+    void prepend(const char *str)
+    {
+        int slen = strlen(str);
+        if(!grow(slen + len, "%s%s", str, text ? text : ""))
+        {
+            memmove(&text[slen], text, len + 1);
+            memcpy(text, str, slen + 1);
+        }
+        len += slen;
+    }
+
+    void append(const char *str)
+    {
+        int slen = strlen(str);
+        if(!grow(len + slen, "%s%s", text ? text : "", str)) memcpy(&text[len], str, slen + 1);
+        len += slen;
+    }
+
+    bool read(stream *f, int chop = -1)
+    {
+        if(chop < 0) chop = INT_MAX; else chop++;
+        set("");
+        while(len + 1 < chop && f->getline(&text[len], min(maxlen, chop) - len))
+        {
+            len += strlen(&text[len]);
+            if(len > 0 && text[len-1] == '\n')
+            {
+                text[--len] = '\0';
+                return true;
+            }
+            if(len + 1 >= maxlen && len + 1 < chop) grow(len + CHUNKSIZE, "%s", text);
+        }
+        if(len + 1 >= chop)
+        {
+            char buf[CHUNKSIZE];
+            while(f->getline(buf, sizeof(buf)))
+            {
+                int blen = strlen(buf);
+                if(blen > 0 && buf[blen-1] == '\n') return true;
+            }
+        }
+        return len > 0;
+    }
+
+    void del(int start, int count)
+    {
+        if(!text) return;
+        if(start < 0) { count += start; start = 0; } 
+        if(count <= 0 || start >= len) return;
+        if(start + count > len) count = len - start - 1;
+        memmove(&text[start], &text[start+count], len + 1 - (start + count));
+        len -= count;
+    }
+
+    void chop(int newlen)
+    {
+        if(!text) return;
+        len = clamp(newlen, 0, len);
+        text[len] = '\0';
+    }
+
+    void insert(char *str, int start, int count = 0)
+    {
+        if(count <= 0) count = strlen(str);
+        start = clamp(start, 0, len);
+        grow(len + count, "%s", text ? text : "");
+        memmove(&text[start + count], &text[start], len - start + 1);
+        memcpy(&text[start], str, count);
+        len += count;
+    }
+
+    void combinelines(vector<editline> &src)
+    {
+        if(src.empty()) set("");
+        else loopv(src)
+        {
+            if(i) append("\n");
+            if(!i) set(src[i].text, src[i].len);
+            else insert(src[i].text, len, src[i].len);
+        }
+    }
+};
+        
+struct editor 
+{
+    int mode; //editor mode - 1= keep while focused, 2= keep while used in gui, 3= keep forever (i.e. until mode changes)
+    bool active, rendered;
+    const char *name;
+    const char *filename;
+
+    int cx, cy; // cursor position - ensured to be valid after a region() or currentline()
+    int mx, my; // selection mark, mx=-1 if following cursor - avoid direct access, instead use region()
+    int maxx, maxy; // maxy=-1 if unlimited lines, 1 if single line editor
+    
+    int scrolly; // vertical scroll offset
+    
+    bool linewrap;
+    int pixelwidth; // required for up/down/hit/draw/bounds
+    int pixelheight; // -1 for variable sized, i.e. from bounds()
+    
+    vector<editline> lines; // MUST always contain at least one line!
+        
+    editor(const char *name, int mode, const char *initval) : 
+        mode(mode), active(true), rendered(false), name(newstring(name)), filename(NULL),
+        cx(0), cy(0), mx(-1), maxx(-1), maxy(-1), scrolly(0), linewrap(false), pixelwidth(-1), pixelheight(-1)
+    {
+        //printf("editor %08x '%s'\n", this, name);
+        lines.add().set(initval ? initval : "");
+    }
+    
+    ~editor()
+    {
+        //printf("~editor %08x '%s'\n", this, name);
+        DELETEA(name);
+        DELETEA(filename);
+        clear(NULL);
+    }
+        
+    void clear(const char *init = "")
+    {
+        cx = cy = 0;
+        mark(false);
+        loopv(lines) lines[i].clear();
+        lines.shrink(0);
+        if(init) lines.add().set(init);
+    }
+    
+    void setfile(const char *fname)
+    {
+        DELETEA(filename); 
+        if(fname) filename = newstring(fname);
+    }
+    
+    void load()
+    {
+        if(!filename) return;
+        clear(NULL);
+        stream *file = openutf8file(filename, "r");
+        if(file) 
+        {
+            while(lines.add().read(file, maxx) && (maxy < 0 || lines.length() <= maxy));
+            lines.pop().clear();
+            delete file;
+        }
+        if(lines.empty()) lines.add().set("");
+    }
+    
+    void save()
+    {
+        if(!filename) return;
+        stream *file = openutf8file(filename, "w");
+        if(!file) return;
+        loopv(lines) file->putline(lines[i].text);
+        delete file;
+    }
+   
+    void mark(bool enable) 
+    {
+        mx = (enable) ? cx : -1;
+        my = cy;
+    }
+        
+    void selectall()
+    {
+        mx = my = INT_MAX;
+        cx = cy = 0;
+    }
+    
+    // constrain results to within buffer - s=start, e=end, return true if a selection range
+    // also ensures that cy is always within lines[] and cx is valid
+    bool region(int &sx, int &sy, int &ex, int &ey)
+    {
+        int n = lines.length(); 
+        ASSERT(n != 0);
+        if(cy < 0) cy = 0; else if(cy >= n) cy = n-1;
+        int len = lines[cy].len;
+        if(cx < 0) cx = 0; else if(cx > len) cx = len;
+        if(mx >= 0) 
+        {
+            if(my < 0) my = 0; else if(my >= n) my = n-1;
+            len = lines[my].len;
+            if(mx > len) mx = len;
+        }
+        sx = (mx >= 0) ? mx : cx;
+        sy = (mx >= 0) ? my : cy;
+        ex = cx;
+        ey = cy;
+        if(sy > ey) { swap(sy, ey); swap(sx, ex); }
+        else if(sy==ey && sx > ex) swap(sx, ex);        
+        return (sx != ex) || (sy != ey);
+    }
+   
+    bool region() { int sx, sy, ex, ey; return region(sx, sy, ex, ey); }
+
+    // also ensures that cy is always within lines[] and cx is valid
+    editline &currentline()
+    {
+        int n = lines.length(); 
+        ASSERT(n != 0);
+        if(cy < 0) cy = 0; else if(cy >= n) cy = n-1;
+        if(cx < 0) cx = 0; else if(cx > lines[cy].len) cx = lines[cy].len;
+        return lines[cy];
+    }
+
+    void copyselectionto(editor *b)
+    {
+        if(b==this) return;
+
+        b->clear(NULL);
+        int sx, sy, ex, ey;
+        region(sx, sy, ex, ey);
+        loopi(1+ey-sy)
+        {
+            if(b->maxy != -1 && b->lines.length() >= b->maxy) break;
+            int y = sy+i;
+            char *line = lines[y].text;
+            int len = lines[y].len;
+            if(y == sy && y == ey)
+            {
+                line += sx;
+                len = ex - sx;
+            }
+            else if(y == sy) line += sx;
+            else if(y == ey) len = ex;
+            b->lines.add().set(line, len);
+        }
+        if(b->lines.empty()) b->lines.add().set("");
+    }
+
+    char *tostring()
+    {
+        int len = 0;
+        loopv(lines) len += lines[i].len + 1;
+        char *str = newstring(len);
+        int offset = 0;
+        loopv(lines)
+        {
+            editline &l = lines[i];
+            memcpy(&str[offset], l.text, l.len);
+            offset += l.len;
+            str[offset++] = '\n';
+        }
+        str[offset] = '\0';
+        return str;
+    }
+
+    char *selectiontostring()
+    {
+        vector<char> buf;
+        int sx, sy, ex, ey;
+        region(sx, sy, ex, ey);
+        loopi(1+ey-sy)
+        {
+            int y = sy+i;
+            char *line = lines[y].text;
+            int len = lines[y].len;
+            if(y == sy && y == ey)
+            {
+                line += sx;
+                len = ex - sx;
+            }
+            else if(y == sy) line += sx;
+            else if(y == ey) len = ex;
+            buf.put(line, len);
+            buf.add('\n');
+        }
+        buf.add('\0');
+        return newstring(buf.getbuf(), buf.length()-1);
+    }
+
+    void removelines(int start, int count)
+    {
+        loopi(count) lines[start+i].clear();
+        lines.remove(start, count);
+    }
+            
+    bool del() // removes the current selection (if any)
+    {
+        int sx, sy, ex, ey;
+        if(!region(sx, sy, ex, ey)) 
+        { 
+            mark(false); 
+            return false; 
+        }
+        if(sy == ey) 
+        {
+            if(sx == 0 && ex == lines[ey].len) removelines(sy, 1);
+            else lines[sy].del(sx, ex - sx);
+        }
+        else
+        {
+            if(ey > sy+1) { removelines(sy+1, ey-(sy+1)); ey = sy+1; }
+            if(ex == lines[ey].len) removelines(ey, 1); else lines[ey].del(0, ex);
+            if(sx == 0) removelines(sy, 1); else lines[sy].del(sx, lines[sy].len - sx);
+        }
+        if(lines.empty()) lines.add().set("");
+        mark(false);
+        cx = sx;
+        cy = sy;
+        editline &current = currentline();
+        if(cx >= current.len && cy < lines.length() - 1)
+        {
+            current.append(lines[cy+1].text);
+            removelines(cy + 1, 1);
+        }
+        return true;
+    }
+        
+    void insert(char ch) 
+    {
+        del();
+        editline &current = currentline();
+        if(ch == '\n') 
+        {
+            if(maxy == -1 || cy < maxy-1)
+            {
+                editline newline(&current.text[cx]);
+                current.chop(cx);
+                cy = min(lines.length(), cy+1);
+                lines.insert(cy, newline);
+            }
+            else current.chop(cx);
+            cx = 0;
+        } 
+        else
+        {
+            int len = current.len;
+            if(maxx >= 0 && len > maxx-1) len = maxx-1;
+            if(cx <= len) current.insert(&ch, cx++, 1);
+        }
+    }
+
+    void insert(const char *s)
+    {
+        while(*s) insert(*s++);
+    }
+
+    void insertallfrom(editor *b) 
+    {   
+        if(b==this) return;
+
+        del();
+        
+        if(b->lines.length() == 1 || maxy == 1) 
+        {
+            editline &current = currentline();
+            char *str = b->lines[0].text;            
+            int slen = b->lines[0].len;
+            if(maxx >= 0 && b->lines[0].len + cx > maxx) slen = maxx-cx;
+            if(slen > 0) 
+            {
+                int len = current.len;
+                if(maxx >= 0 && slen + cx + len > maxx) len = max(0, maxx-(cx+slen));
+                current.insert(str, cx, slen); 
+                cx += slen;
+            }
+        } 
+        else 
+        {
+            loopv(b->lines) 
+            {   
+                if(!i) 
+                {
+                    lines[cy++].append(b->lines[i].text);
+                }
+                else if(i >= b->lines.length())
+                {
+                    cx = b->lines[i].len;
+                    lines[cy].prepend(b->lines[i].text);
+                }
+                else if(maxy < 0 || lines.length() < maxy) lines.insert(cy++, editline(b->lines[i].text));
+            }
+        }
+    }
+
+    void key(int code)
+    {
+        switch(code) 
+        {
+            case SDLK_UP:
+                if(linewrap) 
+                {
+                    int x, y; 
+                    char *str = currentline().text;
+                    text_pos(str, cx+1, x, y, pixelwidth);
+                    if(y > 0) { cx = text_visible(str, x, y-FONTH, pixelwidth); break; }
+                }
+                cy--;
+                break;
+            case SDLK_DOWN:
+                if(linewrap) 
+                {
+                    int x, y, width, height;
+                    char *str = currentline().text;
+                    text_pos(str, cx, x, y, pixelwidth);
+                    text_bounds(str, width, height, pixelwidth);
+                    y += FONTH;
+                    if(y < height) { cx = text_visible(str, x, y, pixelwidth); break; }
+                }
+                cy++;
+                break;
+            case -4:
+                cy--;
+                break;
+            case -5:
+                cy++;
+                break;
+            case SDLK_PAGEUP:
+                cy-=pixelheight/FONTH;
+                break;
+            case SDLK_PAGEDOWN:
+                cy+=pixelheight/FONTH;
+                break;
+            case SDLK_HOME:
+                cx = cy = 0;
+                break;
+            case SDLK_END:
+                cx = cy = INT_MAX;
+                break;
+            case SDLK_LEFT:
+                cx--;
+                break;
+            case SDLK_RIGHT:
+                cx++;
+                break;
+            case SDLK_DELETE:
+                if(!del())
+                {
+                    editline &current = currentline();
+                    if(cx < current.len) current.del(cx, 1);
+                    else if(cy < lines.length()-1)
+                    {   //combine with next line
+                        current.append(lines[cy+1].text);
+                        removelines(cy+1, 1);
+                    }
+                }
+                break;
+            case SDLK_BACKSPACE:
+                if(!del())
+                {
+                    editline &current = currentline();
+                    if(cx > 0) current.del(--cx, 1); 
+                    else if(cy > 0)
+                    {   //combine with previous line
+                        cx = lines[cy-1].len;
+                        lines[cy-1].append(current.text);
+                        removelines(cy--, 1);
+                    }
+                }
+                break;
+            case SDLK_LSHIFT:
+            case SDLK_RSHIFT:
+                break;
+            case SDLK_RETURN:    
+                insert('\n');
+                break;
+            case SDLK_TAB:
+                insert('\t');
+                break;
+        }
+    }
+
+    void input(const char *str, int len)
+    {
+        loopi(len) insert(str[i]);
+    }
+
+    void hit(int hitx, int hity, bool dragged)
+    {
+        int maxwidth = linewrap?pixelwidth:-1;
+        int h = 0;
+        for(int i = scrolly; i < lines.length(); i++)
+        {
+            int width, height;
+            text_bounds(lines[i].text, width, height, maxwidth);
+            if(h + height > pixelheight) break;
+            
+            if(hity >= h && hity <= h+height) 
+            {
+                int x = text_visible(lines[i].text, hitx, hity-h, maxwidth); 
+                if(dragged) { mx = x; my = i; } else { cx = x; cy = i; };
+                break;
+            }
+           h+=height;
+        }
+    }
+
+    int limitscrolly()
+    {
+        int maxwidth = linewrap?pixelwidth:-1;
+        int slines = lines.length();
+        for(int ph = pixelheight; slines > 0 && ph > 0;)
+        {
+            int width, height;
+            text_bounds(lines[slines-1].text, width, height, maxwidth);
+            if(height > ph) break;
+            ph -= height;
+            slines--;
+        }
+        return slines;
+    }
+
+    void draw(int x, int y, int color, bool hit)
+    {
+        int maxwidth = linewrap?pixelwidth:-1;
+        
+        int sx, sy, ex, ey;
+        bool selection = region(sx, sy, ex, ey);
+        
+        // fix scrolly so that <cx, cy> is always on screen
+        if(cy < scrolly) scrolly = cy;
+        else 
+        {
+            if(scrolly < 0) scrolly = 0;
+            int h = 0;
+            for(int i = cy; i >= scrolly; i--)
+            {
+                int width, height;
+                text_bounds(lines[i].text, width, height, maxwidth);
+                if(h + height > pixelheight) { scrolly = i+1; break; }
+                h += height;
+            }
+        }
+        
+        if(selection)
+        {
+            // convert from cursor coords into pixel coords
+            int psx, psy, pex, pey;
+            text_pos(lines[sy].text, sx, psx, psy, maxwidth);
+            text_pos(lines[ey].text, ex, pex, pey, maxwidth);
+            int maxy = lines.length();
+            int h = 0;
+            for(int i = scrolly; i < maxy; i++)
+            {
+                int width, height;
+                text_bounds(lines[i].text, width, height, maxwidth);                
+                if(h + height > pixelheight) { maxy = i; break; }
+                if(i == sy) psy += h;
+                if(i == ey) { pey += h; break; }
+                h += height;
+            }
+            maxy--;
+            
+            if(ey >= scrolly && sy <= maxy) 
+            {
+                // crop top/bottom within window
+                if(sy < scrolly) { sy = scrolly; psy = 0; psx = 0; }
+                if(ey > maxy) { ey = maxy; pey = pixelheight - FONTH; pex = pixelwidth; }
+
+                hudnotextureshader->set();
+                gle::colorub(0xA0, 0x80, 0x80);
+                gle::defvertex(2);
+                gle::begin(GL_QUADS);
+                if(psy == pey) 
+                {
+                    gle::attribf(x+psx, y+psy);
+                    gle::attribf(x+pex, y+psy);
+                    gle::attribf(x+pex, y+pey+FONTH);
+                    gle::attribf(x+psx, y+pey+FONTH);
+                } 
+                else 
+                {   gle::attribf(x+psx,        y+psy);
+                    gle::attribf(x+psx,        y+psy+FONTH);
+                    gle::attribf(x+pixelwidth, y+psy+FONTH);
+                    gle::attribf(x+pixelwidth, y+psy);
+                    if(pey-psy > FONTH) 
+                    {
+                        gle::attribf(x,            y+psy+FONTH);
+                        gle::attribf(x+pixelwidth, y+psy+FONTH);
+                        gle::attribf(x+pixelwidth, y+pey);
+                        gle::attribf(x,            y+pey);
+                    }
+                    gle::attribf(x,     y+pey);
+                    gle::attribf(x,     y+pey+FONTH);
+                    gle::attribf(x+pex, y+pey+FONTH);
+                    gle::attribf(x+pex, y+pey);
+                }
+                gle::end();
+                hudshader->set();
+            }
+        }
+    
+        int h = 0;
+        for(int i = scrolly; i < lines.length(); i++)
+        {
+            int width, height;
+            text_bounds(lines[i].text, width, height, maxwidth);
+            if(h + height > pixelheight) break;
+            
+            draw_text(lines[i].text, x, y+h, color>>16, (color>>8)&0xFF, color&0xFF, 0xFF, hit&&(cy==i)?cx:-1, maxwidth);
+            if(linewrap && height > FONTH) // line wrap indicator
+            {   
+                hudnotextureshader->set();
+                gle::colorub(0x80, 0xA0, 0x80);
+                gle::defvertex(2);
+                gle::begin(GL_TRIANGLE_STRIP);
+                gle::attribf(x,         y+h+FONTH);
+                gle::attribf(x,         y+h+height);
+                gle::attribf(x-FONTW/2, y+h+FONTH);
+                gle::attribf(x-FONTW/2, y+h+height);
+                gle::end();
+                hudshader->set();
+            }
+            h+=height;
+        }
+    }
+};
+
+// a 'stack' where the last is the current focused editor
+static vector <editor*> editors;
+
+static editor *currentfocus() { return editors.length() ? editors.last() : NULL; }
+
+static void readyeditors() 
+{
+    loopv(editors) editors[i]->active = (editors[i]->mode==EDITORFOREVER);
+}
+
+static void flusheditors() 
+{
+    loopvrev(editors) if(!editors[i]->active) 
+    {
+        editor *e = editors.remove(i);
+        DELETEP(e);
+    }
+}
+
+static editor *useeditor(const char *name, int mode, bool focus, const char *initval = NULL) 
+{
+    loopv(editors) if(strcmp(editors[i]->name, name) == 0) 
+    {
+        editor *e = editors[i];
+        if(focus) { editors.add(e); editors.remove(i); } // re-position as last
+        e->active = true;
+        return e;
+    }
+    editor *e = new editor(name, mode, initval);
+    if(focus) editors.add(e); else editors.insert(0, e); 
+    return e;
+}
+
+
+#define TEXTCOMMAND(f, s, d, body) ICOMMAND(f, s, d,\
+    editor *top = currentfocus();\
+    if(!top || identflags&IDF_OVERRIDDEN) return;\
+    body\
+)
+
+ICOMMAND(textlist, "", (), // @DEBUG return list of all the editors
+    vector<char> s;
+    loopv(editors)
+    {   
+        if(i > 0) s.put(", ", 2);
+        s.put(editors[i]->name, strlen(editors[i]->name));
+    }
+    s.add('\0');
+    result(s.getbuf());
+);
+TEXTCOMMAND(textshow, "", (), // @DEBUG return the start of the buffer
+    editline line;
+    line.combinelines(top->lines);
+    result(line.text);
+    line.clear();
+);
+ICOMMAND(textfocus, "si", (char *name, int *mode), // focus on a (or create a persistent) specific editor, else returns current name
+    if(*name) useeditor(name, *mode<=0 ? EDITORFOREVER : *mode, true);
+    else if(editors.length() > 0) result(editors.last()->name);
+);
+TEXTCOMMAND(textprev, "", (), editors.insert(0, top); editors.pop();); // return to the previous editor
+TEXTCOMMAND(textmode, "i", (int *m), // (1= keep while focused, 2= keep while used in gui, 3= keep forever (i.e. until mode changes)) topmost editor, return current setting if no args
+    if(*m) top->mode = *m;
+    else intret(top->mode);
+);
+TEXTCOMMAND(textsave, "s", (char *file),  // saves the topmost (filename is optional)
+    if(*file) top->setfile(path(file, true)); 
+    top->save();
+);  
+TEXTCOMMAND(textload, "s", (char *file), // loads into the topmost editor, returns filename if no args
+    if(*file)
+    {
+        top->setfile(path(file, true));
+        top->load();
+    }
+    else if(top->filename) result(top->filename);
+);
+TEXTCOMMAND(textinit, "sss", (char *name, char *file, char *initval), // loads into named editor if no file assigned and editor has been rendered
+{
+    editor *e = NULL;
+    loopv(editors) if(!strcmp(editors[i]->name, name)) { e = editors[i]; break; }
+    if(e && e->rendered && !e->filename && *file && (e->lines.empty() || (e->lines.length() == 1 && !strcmp(e->lines[0].text, initval))))
+    {
+        e->setfile(path(file, true));
+        e->load();
+    }
+});
+#define PASTEBUFFER "#pastebuffer"
+
+TEXTCOMMAND(textcopy, "", (), editor *b = useeditor(PASTEBUFFER, EDITORFOREVER, false); top->copyselectionto(b););
+TEXTCOMMAND(textpaste, "", (), editor *b = useeditor(PASTEBUFFER, EDITORFOREVER, false); top->insertallfrom(b););
+TEXTCOMMAND(textmark, "i", (int *m),  // (1=mark, 2=unmark), return current mark setting if no args
+    if(*m) top->mark(*m==1);
+    else intret(top->region() ? 1 : 2);
+);
+TEXTCOMMAND(textselectall, "", (), top->selectall(););
+TEXTCOMMAND(textclear, "", (), top->clear(););
+TEXTCOMMAND(textcurrentline, "",  (), result(top->currentline().text););
+
+TEXTCOMMAND(textexec, "i", (int *selected), // execute script commands from the buffer (0=all, 1=selected region only)
+    char *script = *selected ? top->selectiontostring() : top->tostring();
+    execute(script);
+    delete[] script;
+);
+
diff --git a/src/engine/texture.cpp b/src/engine/texture.cpp
new file mode 100644 (file)
index 0000000..964d39d
--- /dev/null
@@ -0,0 +1,3644 @@
+// texture.cpp: texture slot management
+
+#include "engine.h"
+#include "SDL_image.h"
+
+#ifndef SDL_IMAGE_VERSION_ATLEAST
+#define SDL_IMAGE_VERSION_ATLEAST(X, Y, Z) \
+    (SDL_VERSIONNUM(SDL_IMAGE_MAJOR_VERSION, SDL_IMAGE_MINOR_VERSION, SDL_IMAGE_PATCHLEVEL) >= SDL_VERSIONNUM(X, Y, Z))
+#endif
+
+template<int BPP> static void halvetexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst)
+{
+    for(uchar *yend = &src[sh*stride]; src < yend;)
+    {
+        for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += 2*BPP, dst += BPP)
+        {
+            loopi(BPP) dst[i] = (uint(xsrc[i]) + uint(xsrc[i+BPP]) + uint(xsrc[stride+i]) + uint(xsrc[stride+i+BPP]))>>2;
+        }
+        src += 2*stride;
+    }
+}
+
+template<int BPP> static void shifttexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst, uint dw, uint dh)
+{
+    uint wfrac = sw/dw, hfrac = sh/dh, wshift = 0, hshift = 0;
+    while(dw<<wshift < sw) wshift++;
+    while(dh<<hshift < sh) hshift++;
+    uint tshift = wshift + hshift;
+    for(uchar *yend = &src[sh*stride]; src < yend;)
+    {
+        for(uchar *xend = &src[sw*BPP], *xsrc = src; xsrc < xend; xsrc += wfrac*BPP, dst += BPP)
+        {
+            uint t[BPP] = {0};
+            for(uchar *ycur = xsrc, *xend = &ycur[wfrac*BPP], *yend = &src[hfrac*stride];
+                ycur < yend;
+                ycur += stride, xend += stride)
+            {
+                for(uchar *xcur = ycur; xcur < xend; xcur += BPP)
+                    loopi(BPP) t[i] += xcur[i];
+            }
+            loopi(BPP) dst[i] = t[i] >> tshift;
+        }
+        src += hfrac*stride;
+    }
+}
+
+template<int BPP> static void scaletexture(uchar * RESTRICT src, uint sw, uint sh, uint stride, uchar * RESTRICT dst, uint dw, uint dh)
+{
+    uint wfrac = (sw<<12)/dw, hfrac = (sh<<12)/dh, darea = dw*dh, sarea = sw*sh;
+    int over, under;
+    for(over = 0; (darea>>over) > sarea; over++);
+    for(under = 0; (darea<<under) < sarea; under++);
+    uint cscale = clamp(under, over - 12, 12),
+         ascale = clamp(12 + under - over, 0, 24),
+         dscale = ascale + 12 - cscale,
+         area = ((ullong)darea<<ascale)/sarea;
+    dw *= wfrac;
+    dh *= hfrac;
+    for(uint y = 0; y < dh; y += hfrac)
+    {
+        const uint yn = y + hfrac - 1, yi = y>>12, h = (yn>>12) - yi, ylow = ((yn|(-int(h)>>24))&0xFFFU) + 1 - (y&0xFFFU), yhigh = (yn&0xFFFU) + 1;
+        const uchar *ysrc = &src[yi*stride];
+        for(uint x = 0; x < dw; x += wfrac, dst += BPP)
+        {
+            const uint xn = x + wfrac - 1, xi = x>>12, w = (xn>>12) - xi, xlow = ((w+0xFFFU)&0x1000U) - (x&0xFFFU), xhigh = (xn&0xFFFU) + 1;
+            const uchar *xsrc = &ysrc[xi*BPP], *xend = &xsrc[w*BPP];
+            uint t[BPP] = {0};
+            for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP)
+                loopi(BPP) t[i] += xcur[i];
+            loopi(BPP) t[i] = (ylow*(t[i] + ((xsrc[i]*xlow + xend[i]*xhigh)>>12)))>>cscale;
+            if(h)
+            {
+                xsrc += stride;
+                xend += stride;
+                for(uint hcur = h; --hcur; xsrc += stride, xend += stride)
+                {
+                    uint c[BPP] = {0};
+                    for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP)
+                        loopi(BPP) c[i] += xcur[i];
+                    loopi(BPP) t[i] += ((c[i]<<12) + xsrc[i]*xlow + xend[i]*xhigh)>>cscale;
+                }
+                uint c[BPP] = {0};
+                for(const uchar *xcur = &xsrc[BPP]; xcur < xend; xcur += BPP)
+                    loopi(BPP) c[i] += xcur[i];
+                loopi(BPP) t[i] += (yhigh*(c[i] + ((xsrc[i]*xlow + xend[i]*xhigh)>>12)))>>cscale;
+            }
+            loopi(BPP) dst[i] = (t[i] * area)>>dscale;
+        }
+    }
+}
+
+static void scaletexture(uchar * RESTRICT src, uint sw, uint sh, uint bpp, uint pitch, uchar * RESTRICT dst, uint dw, uint dh)
+{
+    if(sw == dw*2 && sh == dh*2)
+    {
+        switch(bpp)
+        {
+            case 1: return halvetexture<1>(src, sw, sh, pitch, dst);
+            case 2: return halvetexture<2>(src, sw, sh, pitch, dst);
+            case 3: return halvetexture<3>(src, sw, sh, pitch, dst);
+            case 4: return halvetexture<4>(src, sw, sh, pitch, dst);
+        }
+    }
+    else if(sw < dw || sh < dh || sw&(sw-1) || sh&(sh-1) || dw&(dw-1) || dh&(dh-1))
+    {
+        switch(bpp)
+        {
+            case 1: return scaletexture<1>(src, sw, sh, pitch, dst, dw, dh);
+            case 2: return scaletexture<2>(src, sw, sh, pitch, dst, dw, dh);
+            case 3: return scaletexture<3>(src, sw, sh, pitch, dst, dw, dh);
+            case 4: return scaletexture<4>(src, sw, sh, pitch, dst, dw, dh);
+        }
+    }
+    else
+    {
+        switch(bpp)
+        {
+            case 1: return shifttexture<1>(src, sw, sh, pitch, dst, dw, dh);
+            case 2: return shifttexture<2>(src, sw, sh, pitch, dst, dw, dh);
+            case 3: return shifttexture<3>(src, sw, sh, pitch, dst, dw, dh);
+            case 4: return shifttexture<4>(src, sw, sh, pitch, dst, dw, dh);
+        }
+    }
+}
+
+static void reorientnormals(uchar * RESTRICT src, int sw, int sh, int bpp, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy)
+{
+    int stridex = bpp, stridey = bpp;
+    if(swapxy) stridex *= sh; else stridey *= sw;
+    if(flipx) { dst += (sw-1)*stridex; stridex = -stridex; }
+    if(flipy) { dst += (sh-1)*stridey; stridey = -stridey; }
+    uchar *srcrow = src;
+    loopi(sh)
+    {
+        for(uchar *curdst = dst, *src = srcrow, *end = &srcrow[sw*bpp]; src < end;)
+        {
+            uchar nx = *src++, ny = *src++;
+            if(flipx) nx = 255-nx;
+            if(flipy) ny = 255-ny;
+            if(swapxy) swap(nx, ny);
+            curdst[0] = nx;
+            curdst[1] = ny;
+            curdst[2] = *src++;
+            if(bpp > 3) curdst[3] = *src++;
+            curdst += stridex;
+        }
+        srcrow += stride;
+        dst += stridey;
+    }
+}
+
+template<int BPP>
+static inline void reorienttexture(uchar * RESTRICT src, int sw, int sh, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy)
+{
+    int stridex = BPP, stridey = BPP;
+    if(swapxy) stridex *= sh; else stridey *= sw;
+    if(flipx) { dst += (sw-1)*stridex; stridex = -stridex; }
+    if(flipy) { dst += (sh-1)*stridey; stridey = -stridey; }
+    uchar *srcrow = src;
+    loopi(sh)
+    {
+        for(uchar *curdst = dst, *src = srcrow, *end = &srcrow[sw*BPP]; src < end;)
+        {
+            loopk(BPP) curdst[k] = *src++;
+            curdst += stridex;
+        }
+        srcrow += stride;
+        dst += stridey;
+    }
+}
+
+static void reorienttexture(uchar * RESTRICT src, int sw, int sh, int bpp, int stride, uchar * RESTRICT dst, bool flipx, bool flipy, bool swapxy)
+{
+    switch(bpp)
+    {
+        case 1: return reorienttexture<1>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+        case 2: return reorienttexture<2>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+        case 3: return reorienttexture<3>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+        case 4: return reorienttexture<4>(src, sw, sh, stride, dst, flipx, flipy, swapxy);
+    }
+}
+
+static void reorients3tc(GLenum format, int blocksize, int w, int h, uchar *src, uchar *dst, bool flipx, bool flipy, bool swapxy, bool normals = false)
+{
+    int bx1 = 0, by1 = 0, bx2 = min(w, 4), by2 = min(h, 4), bw = (w+3)/4, bh = (h+3)/4, stridex = blocksize, stridey = blocksize;
+    if(swapxy) stridex *= bw; else stridey *= bh;
+    if(flipx) { dst += (bw-1)*stridex; stridex = -stridex; bx1 += 4-bx2; bx2 = 4; }
+    if(flipy) { dst += (bh-1)*stridey; stridey = -stridey; by1 += 4-by2; by2 = 4; }
+    loopi(bh)
+    {
+        for(uchar *curdst = dst, *end = &src[bw*blocksize]; src < end; src += blocksize, curdst += stridex)
+        {
+            if(format == GL_COMPRESSED_RGBA_S3TC_DXT3_EXT)
+            {
+                ullong salpha = lilswap(*(const ullong *)src), dalpha = 0;
+                uint xmask = flipx ? 15 : 0, ymask = flipy ? 15 : 0, xshift = 2, yshift = 4;
+                if(swapxy) swap(xshift, yshift);
+                for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+                {
+                    dalpha |= ((salpha&15) << (((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+                    salpha >>= 4;
+                }
+                *(ullong *)curdst = lilswap(dalpha);
+                src += 8;
+                curdst += 8;
+            }
+            else if(format == GL_COMPRESSED_RGBA_S3TC_DXT5_EXT)
+            {
+                uchar alpha1 = src[0], alpha2 = src[1];
+                ullong salpha = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16), dalpha = 0;
+                uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2;
+                if(swapxy) swap(xshift, yshift);
+                for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+                {
+                    dalpha |= ((salpha&7) << (3*((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+                    salpha >>= 3;
+                }
+                curdst[0] = alpha1;
+                curdst[1] = alpha2;
+                *(ushort *)&curdst[2] = lilswap(ushort(dalpha));
+                *(ushort *)&curdst[4] = lilswap(ushort(dalpha>>16));
+                *(ushort *)&curdst[6] = lilswap(ushort(dalpha>>32));
+                src += 8;
+                curdst += 8;
+            }
+
+            ushort color1 = lilswap(*(const ushort *)src), color2 = lilswap(*(const ushort *)&src[2]);
+            uint sbits = lilswap(*(const uint *)&src[4]);
+            if(normals)
+            {
+                ushort ncolor1 = color1, ncolor2 = color2;
+                if(flipx) 
+                { 
+                    ncolor1 = (ncolor1 & ~0xF800) | (0xF800 - (ncolor1 & 0xF800)); 
+                    ncolor2 = (ncolor2 & ~0xF800) | (0xF800 - (ncolor2 & 0xF800));
+                }
+                if(flipy)
+                {
+                    ncolor1 = (ncolor1 & ~0x7E0) | (0x7E0 - (ncolor1 & 0x7E0));
+                    ncolor2 = (ncolor2 & ~0x7E0) | (0x7E0 - (ncolor2 & 0x7E0));
+                }
+                if(swapxy)
+                {
+                    ncolor1 = (ncolor1 & 0x1F) | (((((ncolor1 >> 11) & 0x1F) * 0x3F) / 0x1F) << 5) | (((((ncolor1 >> 5) & 0x3F) * 0x1F) / 0x3F) << 11);
+                    ncolor2 = (ncolor2 & 0x1F) | (((((ncolor2 >> 11) & 0x1F) * 0x3F) / 0x1F) << 5) | (((((ncolor2 >> 5) & 0x3F) * 0x1F) / 0x3F) << 11);
+                }
+                if(color1 <= color2 && ncolor1 > ncolor2) { color1 = ncolor2; color2 = ncolor1; }
+                else { color1 = ncolor1; color2 = ncolor2; }
+            }
+            uint dbits = 0, xmask = flipx ? 3 : 0, ymask = flipy ? 3 : 0, xshift = 1, yshift = 3;
+            if(swapxy) swap(xshift, yshift);
+            for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+            {
+                dbits |= ((sbits&3) << (((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+                sbits >>= 2;
+            }
+            *(ushort *)curdst = lilswap(color1);
+            *(ushort *)&curdst[2] = lilswap(color2);
+            *(uint *)&curdst[4] = lilswap(dbits);
+
+            if(blocksize > 8) { src -= 8; curdst -= 8; }
+        }
+        dst += stridey;
+    }
+}
+
+static void reorientrgtc(GLenum format, int blocksize, int w, int h, uchar *src, uchar *dst, bool flipx, bool flipy, bool swapxy)
+{
+    int bx1 = 0, by1 = 0, bx2 = min(w, 4), by2 = min(h, 4), bw = (w+3)/4, bh = (h+3)/4, stridex = blocksize, stridey = blocksize;
+    if(swapxy) stridex *= bw; else stridey *= bh;
+    if(flipx) { dst += (bw-1)*stridex; stridex = -stridex; bx1 += 4-bx2; bx2 = 4; }
+    if(flipy) { dst += (bh-1)*stridey; stridey = -stridey; by1 += 4-by2; by2 = 4; }
+    stridex -= blocksize;
+    loopi(bh)
+    {
+        for(uchar *curdst = dst, *end = &src[bw*blocksize]; src < end; curdst += stridex)
+        {
+            loopj(blocksize/8)
+            {
+                uchar val1 = src[0], val2 = src[1];
+                ullong sval = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4] )<< 16), dval = 0;
+                uint xmask = flipx ? 7 : 0, ymask = flipy ? 7 : 0, xshift = 0, yshift = 2;
+                if(swapxy) swap(xshift, yshift);
+                for(int y = by1; y < by2; y++) for(int x = bx1; x < bx2; x++)
+                {
+                    dval |= ((sval&7) << (3*((xmask^x)<<xshift) + ((ymask^y)<<yshift)));
+                    sval >>= 3;
+                }
+                curdst[0] = val1;
+                curdst[1] = val2;
+                *(ushort *)&curdst[2] = lilswap(ushort(dval));
+                *(ushort *)&curdst[4] = lilswap(ushort(dval>>16));
+                *(ushort *)&curdst[6] = lilswap(ushort(dval>>32));
+                src += 8;
+                curdst += 8;
+            }
+        }
+        dst += stridey;
+    }
+}
+
+#define writetex(t, body) do \
+    { \
+        uchar *dstrow = t.data; \
+        loop(y, t.h) \
+        { \
+            for(uchar *dst = dstrow, *end = &dstrow[t.w*t.bpp]; dst < end; dst += t.bpp) \
+            { \
+                body; \
+            } \
+            dstrow += t.pitch; \
+        } \
+    } while(0)
+
+#define readwritetex(t, s, body) do \
+    { \
+        uchar *dstrow = t.data, *srcrow = s.data; \
+        loop(y, t.h) \
+        { \
+            for(uchar *dst = dstrow, *src = srcrow, *end = &srcrow[s.w*s.bpp]; src < end; dst += t.bpp, src += s.bpp) \
+            { \
+                body; \
+            } \
+            dstrow += t.pitch; \
+            srcrow += s.pitch; \
+        } \
+    } while(0)
+
+#define read2writetex(t, s1, src1, s2, src2, body) do \
+    { \
+        uchar *dstrow = t.data, *src1row = s1.data, *src2row = s2.data; \
+        loop(y, t.h) \
+        { \
+            for(uchar *dst = dstrow, *end = &dstrow[t.w*t.bpp], *src1 = src1row, *src2 = src2row; dst < end; dst += t.bpp, src1 += s1.bpp, src2 += s2.bpp) \
+            { \
+                body; \
+            } \
+            dstrow += t.pitch; \
+            src1row += s1.pitch; \
+            src2row += s2.pitch; \
+        } \
+    } while(0)
+
+#define readwritergbtex(t, s, body) \
+    { \
+        if(t.bpp >= 3) readwritetex(t, s, body); \
+        else \
+        { \
+            ImageData rgb(t.w, t.h, 3); \
+            read2writetex(rgb, t, orig, s, src, { dst[0] = dst[1] = dst[2] = orig[0]; body; }); \
+            t.replace(rgb); \
+        } \
+    }
+
+void forcergbimage(ImageData &s)
+{
+    if(s.bpp >= 3) return;
+    ImageData d(s.w, s.h, 3);
+    readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; });
+    s.replace(d);
+}
+
+#define readwritergbatex(t, s, body) \
+    { \
+        if(t.bpp >= 4) { readwritetex(t, s, body); } \
+        else \
+        { \
+            ImageData rgba(t.w, t.h, 4); \
+            if(t.bpp==3) read2writetex(rgba, t, orig, s, src, { dst[0] = orig[0]; dst[1] = orig[1]; dst[2] = orig[2]; body; }); \
+            else read2writetex(rgba, t, orig, s, src, { dst[0] = dst[1] = dst[2] = orig[0]; body; }); \
+            t.replace(rgba); \
+        } \
+    }
+
+void forcergbaimage(ImageData &s)
+{
+    if(s.bpp >= 4) return;
+    ImageData d(s.w, s.h, 4);
+    if(s.bpp==3) readwritetex(d, s, { dst[0] = src[0]; dst[1] = src[1]; dst[2] = src[2]; });
+    else readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; });
+    s.replace(d);
+}
+
+void swizzleimage(ImageData &s)
+{
+    if(s.bpp==2)
+    {
+        ImageData d(s.w, s.h, 4);
+        readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; dst[3] = src[1]; });
+        s.replace(d);
+    }
+    else if(s.bpp==1)
+    {
+        ImageData d(s.w, s.h, 3);
+        readwritetex(d, s, { dst[0] = dst[1] = dst[2] = src[0]; });
+        s.replace(d);
+    }
+}
+
+void texreorient(ImageData &s, bool flipx, bool flipy, bool swapxy, int type = TEX_DIFFUSE)
+{
+    ImageData d(swapxy ? s.h : s.w, swapxy ? s.w : s.h, s.bpp, s.levels, s.align, s.compressed);
+    switch(s.compressed)
+    {
+    case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+    case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+    case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+    case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+        {
+            uchar *dst = d.data, *src = s.data;
+            loopi(s.levels)
+            {
+                reorients3tc(s.compressed, s.bpp, max(s.w>>i, 1), max(s.h>>i, 1), src, dst, flipx, flipy, swapxy, type==TEX_NORMAL);
+                src += s.calclevelsize(i);
+                dst += d.calclevelsize(i);
+            }
+            break;
+        }
+    case GL_COMPRESSED_RED_RGTC1:
+    case GL_COMPRESSED_RG_RGTC2:
+    case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+    case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+        {
+            uchar *dst = d.data, *src = s.data;
+            loopi(s.levels)
+            {
+                reorientrgtc(s.compressed, s.bpp, max(s.w>>i, 1), max(s.h>>i, 1), src, dst, flipx, flipy, swapxy);
+                src += s.calclevelsize(i);
+                dst += d.calclevelsize(i);
+            }
+            break;
+        }
+    default:
+        if(type==TEX_NORMAL && s.bpp >= 3) reorientnormals(s.data, s.w, s.h, s.bpp, s.pitch, d.data, flipx, flipy, swapxy);
+        else reorienttexture(s.data, s.w, s.h, s.bpp, s.pitch, d.data, flipx, flipy, swapxy);
+        break;
+    }
+    s.replace(d);
+}
+
+extern const texrotation texrotations[8] =
+{
+    { false, false, false }, // 0: default
+    { false,  true,  true }, // 1: 90 degrees
+    {  true,  true, false }, // 2: 180 degrees
+    {  true, false,  true }, // 3: 270 degrees
+    {  true, false, false }, // 4: flip X
+    { false,  true, false }, // 5: flip Y
+    { false, false,  true }, // 6: transpose
+    {  true,  true,  true }, // 7: flipped transpose
+};
+
+void texrotate(ImageData &s, int numrots, int type = TEX_DIFFUSE)
+{
+    if(numrots>=1 && numrots<=7)
+    {
+        const texrotation &r = texrotations[numrots];
+        texreorient(s, r.flipx, r.flipy, r.swapxy, type);
+    }
+}
+
+void texoffset(ImageData &s, int xoffset, int yoffset)
+{
+    xoffset = max(xoffset, 0);
+    xoffset %= s.w;
+    yoffset = max(yoffset, 0);
+    yoffset %= s.h;
+    if(!xoffset && !yoffset) return;
+    ImageData d(s.w, s.h, s.bpp);
+    uchar *src = s.data;
+    loop(y, s.h)
+    {
+        uchar *dst = (uchar *)d.data+((y+yoffset)%d.h)*d.pitch;
+        memcpy(dst+xoffset*s.bpp, src, (s.w-xoffset)*s.bpp);
+        memcpy(dst, src+(s.w-xoffset)*s.bpp, xoffset*s.bpp);
+        src += s.pitch;
+    }
+    s.replace(d);
+}
+
+void texmad(ImageData &s, const vec &mul, const vec &add)
+{
+    if(s.bpp < 3 && (mul.x != mul.y || mul.y != mul.z || add.x != add.y || add.y != add.z))
+        swizzleimage(s);
+    int maxk = min(int(s.bpp), 3);
+    writetex(s,
+        loopk(maxk) dst[k] = uchar(clamp(dst[k]*mul[k] + 255*add[k], 0.0f, 255.0f));
+    );
+}
+
+void texcolorify(ImageData &s, const vec &color, vec weights)
+{
+    if(s.bpp < 3) return;
+    if(weights.iszero()) weights = vec(0.21f, 0.72f, 0.07f);
+    writetex(s,
+        float lum = dst[0]*weights.x + dst[1]*weights.y + dst[2]*weights.z;
+        loopk(3) dst[k] = uchar(clamp(lum*color[k], 0.0f, 255.0f));
+    );
+}
+
+void texcolormask(ImageData &s, const vec &color1, const vec &color2)
+{
+    if(s.bpp < 4) return;
+    ImageData d(s.w, s.h, 3);
+    readwritetex(d, s,
+        vec color;
+        color.lerp(color2, color1, src[3]/255.0f);
+        loopk(3) dst[k] = uchar(clamp(color[k]*src[k], 0.0f, 255.0f));
+    );
+    s.replace(d);
+}
+    
+void texdup(ImageData &s, int srcchan, int dstchan)
+{
+    if(srcchan==dstchan || max(srcchan, dstchan) >= s.bpp) return;
+    writetex(s, dst[dstchan] = dst[srcchan]);
+}
+
+void texmix(ImageData &s, int c1, int c2, int c3, int c4)
+{
+    int numchans = c1 < 0 ? 0 : (c2 < 0 ? 1 : (c3 < 0 ? 2 : (c4 < 0 ? 3 : 4)));
+    if(numchans <= 0) return;
+    ImageData d(s.w, s.h, numchans);
+    readwritetex(d, s,
+        switch(numchans)
+        {
+            case 4: dst[3] = src[c4];
+            case 3: dst[2] = src[c3];
+            case 2: dst[1] = src[c2];
+            case 1: dst[0] = src[c1];
+        }
+    );
+    s.replace(d);
+}
+
+void texgrey(ImageData &s)
+{
+    if(s.bpp <= 2) return;
+    ImageData d(s.w, s.h, s.bpp >= 4 ? 2 : 1);
+    if(s.bpp >= 4)
+    {
+        readwritetex(d, s,
+            dst[0] = src[0];
+            dst[1] = src[3];
+        );
+    }
+    else 
+    {
+        readwritetex(d, s, dst[0] = src[0]);
+    }
+    s.replace(d);
+}
+
+void texpremul(ImageData &s)
+{
+    switch(s.bpp)
+    {
+        case 2: 
+            writetex(s, 
+                dst[0] = uchar((uint(dst[0])*uint(dst[1]))/255);
+            ); 
+            break;
+        case 4: 
+            writetex(s,
+                uint alpha = dst[3];
+                dst[0] = uchar((uint(dst[0])*alpha)/255);
+                dst[1] = uchar((uint(dst[1])*alpha)/255);
+                dst[2] = uchar((uint(dst[2])*alpha)/255);
+            );
+            break;
+    }
+}
+
+void texagrad(ImageData &s, float x2, float y2, float x1, float y1)
+{
+    if(s.bpp != 2 && s.bpp != 4) return;
+    y1 = 1 - y1;
+    y2 = 1 - y2;
+    float minx = 1, miny = 1, maxx = 1, maxy = 1;
+    if(x1 != x2)
+    {
+        minx = (0 - x1) / (x2 - x1);
+        maxx = (1 - x1) / (x2 - x1);
+    }
+    if(y1 != y2)
+    {
+        miny = (0 - y1) / (y2 - y1);
+        maxy = (1 - y1) / (y2 - y1);
+    }
+    float dx = (maxx - minx)/max(s.w-1, 1),                  
+          dy = (maxy - miny)/max(s.h-1, 1),
+          cury = miny;
+    for(uchar *dstrow = s.data + s.bpp - 1, *endrow = dstrow + s.h*s.pitch; dstrow < endrow; dstrow += s.pitch)
+    {
+        float curx = minx;
+        for(uchar *dst = dstrow, *end = &dstrow[s.w*s.bpp]; dst < end; dst += s.bpp)
+        {
+            dst[0] = uchar(dst[0]*clamp(curx, 0.0f, 1.0f)*clamp(cury, 0.0f, 1.0f));
+            curx += dx;
+        }
+        cury += dy;
+    }
+}
+
+VAR(hwtexsize, 1, 0, 0);
+VAR(hwcubetexsize, 1, 0, 0);
+VAR(hwmaxaniso, 1, 0, 0);
+VARFP(maxtexsize, 0, 0, 1<<12, initwarning("texture quality", INIT_LOAD));
+VARFP(reducefilter, 0, 1, 1, initwarning("texture quality", INIT_LOAD));
+VARFP(texreduce, 0, 0, 12, initwarning("texture quality", INIT_LOAD));
+VARFP(texcompress, 0, 1<<10, 1<<12, initwarning("texture quality", INIT_LOAD));
+VARFP(texcompressquality, -1, -1, 1, setuptexcompress());
+VARFP(trilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD));
+VARFP(bilinear, 0, 1, 1, initwarning("texture filtering", INIT_LOAD));
+VARFP(aniso, 0, 0, 16, initwarning("texture filtering", INIT_LOAD));
+
+extern int usetexcompress;
+
+void setuptexcompress()
+{
+    if(!usetexcompress) return;
+
+    GLenum hint = GL_DONT_CARE;
+    switch(texcompressquality)
+    {
+        case 1: hint = GL_NICEST; break;
+        case 0: hint = GL_FASTEST; break;
+    }
+    glHint(GL_TEXTURE_COMPRESSION_HINT, hint);
+}
+
+GLenum compressedformat(GLenum format, int w, int h, int force = 0)
+{
+    if(usetexcompress && texcompress && force >= 0 && (force || max(w, h) >= texcompress)) switch(format)
+    {
+        case GL_RGB5:
+        case GL_RGB8:
+        case GL_RGB: return usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB;
+        case GL_RGB5_A1: return usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGBA;
+        case GL_RGBA: return usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA;
+        case GL_RED:
+        case GL_R8: return hasRGTC ? (usetexcompress > 1 ? GL_COMPRESSED_RED_RGTC1 : GL_COMPRESSED_RED) : (usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB);
+        case GL_RG:
+        case GL_RG8: return hasRGTC ? (usetexcompress > 1 ? GL_COMPRESSED_RG_RGTC2 : GL_COMPRESSED_RG) : (usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA);
+        case GL_LUMINANCE:
+        case GL_LUMINANCE8: return hasLATC ? (usetexcompress > 1 ? GL_COMPRESSED_LUMINANCE_LATC1_EXT : GL_COMPRESSED_LUMINANCE) : (usetexcompress > 1 ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_COMPRESSED_RGB);
+        case GL_LUMINANCE_ALPHA:
+        case GL_LUMINANCE8_ALPHA8: return hasLATC ? (usetexcompress > 1 ? GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT : GL_COMPRESSED_LUMINANCE_ALPHA) : (usetexcompress > 1 ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_COMPRESSED_RGBA);
+    }
+    return format;
+}
+
+GLenum uncompressedformat(GLenum format)
+{
+    switch(format)
+    {
+        case GL_COMPRESSED_ALPHA:
+            return GL_ALPHA;
+        case GL_COMPRESSED_LUMINANCE:
+        case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+            return GL_LUMINANCE;
+        case GL_COMPRESSED_LUMINANCE_ALPHA:
+        case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+            return GL_LUMINANCE_ALPHA;
+        case GL_COMPRESSED_RED:
+        case GL_COMPRESSED_RED_RGTC1:
+            return GL_RED;
+        case GL_COMPRESSED_RG:
+        case GL_COMPRESSED_RG_RGTC2:
+            return GL_RG;
+        case GL_COMPRESSED_RGB:
+        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+            return GL_RGB;
+        case GL_COMPRESSED_RGBA:
+        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+            return GL_RGBA;
+    }
+    return GL_FALSE;
+}
+
+int formatsize(GLenum format)
+{
+    switch(format)
+    {
+        case GL_RED:
+        case GL_LUMINANCE:
+        case GL_ALPHA: return 1;
+        case GL_RG:
+        case GL_LUMINANCE_ALPHA: return 2;
+        case GL_RGB: return 3;
+        case GL_RGBA: return 4;
+        default: return 4;
+    }
+}
+
+VARFP(usenp2, 0, 0, 1, initwarning("texture quality", INIT_LOAD));
+
+void resizetexture(int w, int h, bool mipmap, bool canreduce, GLenum target, int compress, int &tw, int &th)
+{
+    int hwlimit = target==GL_TEXTURE_CUBE_MAP ? hwcubetexsize : hwtexsize,
+        sizelimit = mipmap && maxtexsize ? min(maxtexsize, hwlimit) : hwlimit;
+    if(compress > 0 && !usetexcompress)
+    {
+        w = max(w/compress, 1);
+        h = max(h/compress, 1);
+    }
+    if(canreduce && texreduce)
+    {
+        w = max(w>>texreduce, 1);
+        h = max(h>>texreduce, 1);
+    }
+    w = min(w, sizelimit);
+    h = min(h, sizelimit);
+    if(!usenp2 && (w&(w-1) || h&(h-1)))
+    {
+        tw = th = 1;
+        while(tw < w) tw *= 2;
+        while(th < h) th *= 2;
+        if(w < tw - tw/4) tw /= 2;
+        if(h < th - th/4) th /= 2;
+    }
+    else
+    {
+        tw = w;
+        th = h;
+    }
+}
+
+static GLuint mipmapfbo[2] = { 0, 0 };
+
+void cleanupmipmaps()
+{
+    if(mipmapfbo[0]) { glDeleteFramebuffers_(2, mipmapfbo); memset(mipmapfbo, 0, sizeof(mipmapfbo)); }
+}
+
+VARFP(gpumipmap, 0, 0, 1, cleanupmipmaps());
+
+void uploadtexture(int tnum, GLenum target, GLenum internal, int tw, int th, GLenum format, GLenum type, void *pixels, int pw, int ph, int pitch, bool mipmap)
+{
+    int bpp = formatsize(format), row = 0, rowalign = 0;
+    if(!pitch) pitch = pw*bpp; 
+    uchar *buf = NULL;
+    if(pw!=tw || ph!=th) 
+    {
+        buf = new uchar[tw*th*bpp];
+        scaletexture((uchar *)pixels, pw, ph, bpp, pitch, buf, tw, th);
+    }
+    else if(tw*bpp != pitch)
+    {
+        row = pitch/bpp;
+        rowalign = texalign(pixels, pitch, 1);
+        while(rowalign > 0 && ((row*bpp + rowalign - 1)/rowalign)*rowalign != pitch) rowalign >>= 1;
+        if(!rowalign)
+        {
+            row = 0;
+            buf = new uchar[tw*th*bpp];
+            loopi(th) memcpy(&buf[i*tw*bpp], &((uchar *)pixels)[i*pitch], tw*bpp);
+        }
+    }
+    bool shouldgpumipmap = pixels && mipmap && max(tw, th) > 1 && gpumipmap && hasFBB && !uncompressedformat(internal);
+    for(int level = 0, align = 0, mw = tw, mh = th;; level++)
+    {
+        uchar *src = buf ? buf : (uchar *)pixels;
+        if(buf) pitch = mw*bpp;
+        int srcalign = row > 0 ? rowalign : texalign(src, pitch, 1);
+        if(align != srcalign) glPixelStorei(GL_UNPACK_ALIGNMENT, align = srcalign);
+        if(row > 0) glPixelStorei(GL_UNPACK_ROW_LENGTH, row);
+        glTexImage2D(target, level, internal, mw, mh, 0, format, type, src);
+        if(row > 0) glPixelStorei(GL_UNPACK_ROW_LENGTH, row = 0);
+        if(!mipmap || shouldgpumipmap || max(mw, mh) <= 1) break;
+        int srcw = mw, srch = mh;
+        if(mw > 1) mw /= 2;
+        if(mh > 1) mh /= 2;
+        if(src)
+        {
+            if(!buf) buf = new uchar[mw*mh*bpp];
+            scaletexture(src, srcw, srch, bpp, pitch, buf, mw, mh);
+        }
+    }
+    if(buf) delete[] buf;
+    if(shouldgpumipmap)
+    {
+        GLint fbo = 0;
+        if(!inbetweenframes || drawtex) glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
+        for(int level = 1, mw = tw, mh = th; max(mw, mh) > 1; level++)
+        {
+            if(mw > 1) mw /= 2;
+            if(mh > 1) mh /= 2;
+            glTexImage2D(target, level, internal, mw, mh, 0, format, type, NULL);
+        }
+        if(!mipmapfbo[0]) glGenFramebuffers_(2, mipmapfbo);
+        glBindFramebuffer_(GL_READ_FRAMEBUFFER, mipmapfbo[0]);
+        glBindFramebuffer_(GL_DRAW_FRAMEBUFFER, mipmapfbo[1]);
+        for(int level = 1, mw = tw, mh = th; max(mw, mh) > 1; level++)
+        {
+            int srcw = mw, srch = mh;
+            if(mw > 1) mw /= 2;
+            if(mh > 1) mh /= 2;
+            glFramebufferTexture2D_(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, tnum, level - 1);
+            glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, tnum, level);
+            glBlitFramebuffer_(0, 0, srcw, srch, 0, 0, mw, mh, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+        }
+        glFramebufferTexture2D_(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0, 0);
+        glFramebufferTexture2D_(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, 0, 0);
+        glBindFramebuffer_(GL_FRAMEBUFFER, fbo);
+    }
+}
+
+void uploadcompressedtexture(GLenum target, GLenum subtarget, GLenum format, int w, int h, uchar *data, int align, int blocksize, int levels, bool mipmap)
+{
+    int hwlimit = target==GL_TEXTURE_CUBE_MAP ? hwcubetexsize : hwtexsize,
+        sizelimit = levels > 1 && maxtexsize ? min(maxtexsize, hwlimit) : hwlimit;
+    int level = 0;
+    loopi(levels)
+    {
+        int size = ((w + align-1)/align) * ((h + align-1)/align) * blocksize;
+        if(w <= sizelimit && h <= sizelimit)
+        {
+            glCompressedTexImage2D_(subtarget, level, format, w, h, 0, size, data);
+            level++;
+            if(!mipmap) break;
+        }
+        if(max(w, h) <= 1) break;
+        if(w > 1) w /= 2;
+        if(h > 1) h /= 2;
+        data += size;
+    }
+}
+
+GLenum textarget(GLenum subtarget)
+{
+    switch(subtarget)
+    {
+        case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
+        case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
+        case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
+        case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
+        case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
+        case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
+            return GL_TEXTURE_CUBE_MAP;
+    }
+    return subtarget;
+}
+
+const GLint *swizzlemask(GLenum format)
+{
+    static const GLint luminance[4] = { GL_RED, GL_RED, GL_RED, GL_ONE };
+    static const GLint luminancealpha[4] = { GL_RED, GL_RED, GL_RED, GL_GREEN };
+    switch(format)
+    {
+        case GL_RED: return luminance;
+        case GL_RG: return luminancealpha;
+    }
+    return NULL;
+}
+    
+void setuptexparameters(int tnum, void *pixels, int clamp, int filter, GLenum format, GLenum target, bool swizzle)
+{
+    glBindTexture(target, tnum);
+    glTexParameteri(target, GL_TEXTURE_WRAP_S, clamp&1 ? GL_CLAMP_TO_EDGE : (clamp&0x100 ? GL_MIRRORED_REPEAT : GL_REPEAT));
+    glTexParameteri(target, GL_TEXTURE_WRAP_T, clamp&2 ? GL_CLAMP_TO_EDGE : (clamp&0x200 ? GL_MIRRORED_REPEAT : GL_REPEAT));
+    if(target==GL_TEXTURE_2D && hasAF && min(aniso, hwmaxaniso) > 0 && filter > 1) glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, min(aniso, hwmaxaniso));
+    glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter && bilinear ? GL_LINEAR : GL_NEAREST);
+    glTexParameteri(target, GL_TEXTURE_MIN_FILTER,
+        filter > 1 ?
+            (trilinear ?
+                (bilinear ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR) :
+                (bilinear ? GL_LINEAR_MIPMAP_NEAREST : GL_NEAREST_MIPMAP_NEAREST)) :
+            (filter && bilinear ? GL_LINEAR : GL_NEAREST));
+    if(swizzle && hasTRG && hasTSW)
+    {
+        const GLint *mask = swizzlemask(format);
+        if(mask) glTexParameteriv(target, GL_TEXTURE_SWIZZLE_RGBA, mask);
+    }
+}
+
+static GLenum textype(GLenum &component, GLenum &format)
+{
+    GLenum type = GL_UNSIGNED_BYTE;
+    switch(component)
+    {
+        case GL_R16F:
+        case GL_R32F:
+            if(!format) format = GL_RED;
+            type = GL_FLOAT;
+            break;
+
+        case GL_RG16F:
+        case GL_RG32F:
+            if(!format) format = GL_RG;
+            type = GL_FLOAT;
+            break;
+
+        case GL_RGB16F:
+        case GL_RGB32F:
+            if(!format) format = GL_RGB;
+            type = GL_FLOAT;
+            break;
+
+        case GL_RGBA16F:
+        case GL_RGBA32F:
+            if(!format) format = GL_RGBA;
+            type = GL_FLOAT;
+            break;
+
+        case GL_DEPTH_COMPONENT16:
+        case GL_DEPTH_COMPONENT24:
+        case GL_DEPTH_COMPONENT32:
+            if(!format) format = GL_DEPTH_COMPONENT;
+            break;
+
+        case GL_RGB5:
+        case GL_RGB8:
+        case GL_RGB10:
+        case GL_RGB16:
+        case GL_COMPRESSED_RGB:
+        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+            if(!format) format = GL_RGB;
+            break;
+
+        case GL_RGB5_A1:
+        case GL_RGBA8:
+        case GL_RGB10_A2:
+        case GL_RGBA16:
+        case GL_COMPRESSED_RGBA:
+        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+            if(!format) format = GL_RGBA;
+            break;
+
+        case GL_DEPTH_STENCIL:
+        case GL_DEPTH24_STENCIL8:
+            if(!format) format = GL_DEPTH_STENCIL;
+            type = GL_UNSIGNED_INT_24_8;
+            break;
+
+        case GL_R8:
+        case GL_R16:
+        case GL_COMPRESSED_RED:
+        case GL_COMPRESSED_RED_RGTC1:
+            if(!format) format = GL_RED;
+            break;
+
+        case GL_RG8:
+        case GL_RG16:
+        case GL_COMPRESSED_RG:
+        case GL_COMPRESSED_RG_RGTC2:
+            if(!format) format = GL_RG;
+            break;
+
+        case GL_LUMINANCE8:
+        case GL_LUMINANCE16:
+        case GL_COMPRESSED_LUMINANCE:
+        case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+            if(!format) format = GL_LUMINANCE;
+            break;
+
+        case GL_LUMINANCE8_ALPHA8:
+        case GL_LUMINANCE16_ALPHA16:
+        case GL_COMPRESSED_LUMINANCE_ALPHA:
+        case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+            if(!format) format = GL_LUMINANCE_ALPHA;
+            break;
+
+        case GL_ALPHA8:
+        case GL_ALPHA16:
+        case GL_COMPRESSED_ALPHA:
+            if(!format) format = GL_ALPHA;
+            break;
+    }
+    if(!format) format = component;
+    return type;
+}
+
+void createtexture(int tnum, int w, int h, void *pixels, int clamp, int filter, GLenum component, GLenum subtarget, int pw, int ph, int pitch, bool resize, GLenum format, bool swizzle)
+{
+    GLenum target = textarget(subtarget), type = textype(component, format);
+    if(filter >= 0 && clamp >= 0) setuptexparameters(tnum, pixels, clamp, filter, format, target, swizzle);
+    if(!pw) pw = w;
+    if(!ph) ph = h;
+    int tw = w, th = h;
+    bool mipmap = filter > 1 && pixels;
+    if(resize && pixels) 
+    {
+        resizetexture(w, h, mipmap, false, target, 0, tw, th);
+        if(mipmap) component = compressedformat(component, tw, th);
+    }
+    uploadtexture(tnum, subtarget, component, tw, th, format, type, pixels, pw, ph, pitch, mipmap); 
+}
+
+void createcompressedtexture(int tnum, int w, int h, uchar *data, int align, int blocksize, int levels, int clamp, int filter, GLenum format, GLenum subtarget, bool swizzle = false)
+{
+    GLenum target = textarget(subtarget);
+    if(filter >= 0 && clamp >= 0) setuptexparameters(tnum, data, clamp, filter, format, target);
+    uploadcompressedtexture(target, subtarget, format, w, h, data, align, blocksize, levels, filter > 1); 
+}
+
+hashnameset<Texture> textures;
+
+Texture *notexture = NULL; // used as default, ensured to be loaded
+
+static GLenum texformat(int bpp, bool swizzle = false)
+{
+    switch(bpp)
+    {
+        case 1: return hasTRG && (hasTSW || !glcompat || !swizzle) ? GL_RED : GL_LUMINANCE;
+        case 2: return hasTRG && (hasTSW || !glcompat || !swizzle) ? GL_RG : GL_LUMINANCE_ALPHA;
+        case 3: return GL_RGB;
+        case 4: return GL_RGBA;
+        default: return 0;
+    }
+}
+
+static bool alphaformat(GLenum format)
+{
+    switch(format)
+    {
+        case GL_ALPHA:
+        case GL_LUMINANCE_ALPHA:
+        case GL_RG:
+        case GL_RGBA:
+            return true;
+        default:
+            return false;
+    }
+}
+
+int texalign(const void *data, int w, int bpp)
+{
+    int stride = w*bpp;
+    if(stride&1) return 1;
+    if(stride&2) return 2;
+    return 4;
+}
+    
+static Texture *newtexture(Texture *t, const char *rname, ImageData &s, int clamp = 0, bool mipit = true, bool canreduce = false, bool transient = false, int compress = 0)
+{
+    if(!t)
+    {
+        char *key = newstring(rname);
+        t = &textures[key];
+        t->name = key;
+    }
+
+    t->clamp = clamp;
+    t->mipmap = mipit;
+    t->type = Texture::IMAGE;
+    if(transient) t->type |= Texture::TRANSIENT;
+    if(clamp&0x300) t->type |= Texture::MIRROR;
+    if(!s.data)
+    {
+        t->type |= Texture::STUB;
+        t->w = t->h = t->xs = t->ys = t->bpp = 0;
+        return t;
+    }
+
+    bool swizzle = !(clamp&0x10000);
+    GLenum format;
+    if(s.compressed)
+    {
+        format = uncompressedformat(s.compressed);
+        t->bpp = formatsize(format);
+        t->type |= Texture::COMPRESSED;
+    }
+    else 
+    {
+        format = texformat(s.bpp, swizzle);
+        t->bpp = s.bpp;
+        if(swizzle && hasTRG && !hasTSW && swizzlemask(format))
+        {
+            swizzleimage(s);   
+            format = texformat(s.bpp, swizzle);
+            t->bpp = s.bpp;
+        }
+    }
+    if(alphaformat(format)) t->type |= Texture::ALPHA;
+    t->w = t->xs = s.w;
+    t->h = t->ys = s.h;
+
+    int filter = !canreduce || reducefilter ? (mipit ? 2 : 1) : 0;
+    glGenTextures(1, &t->id);
+    if(s.compressed)
+    {
+        uchar *data = s.data;
+        int levels = s.levels, level = 0;
+        if(canreduce && texreduce) loopi(min(texreduce, s.levels-1))
+        {
+            data += s.calclevelsize(level++);
+            levels--;
+            if(t->w > 1) t->w /= 2;
+            if(t->h > 1) t->h /= 2;
+        } 
+        int sizelimit = mipit && maxtexsize ? min(maxtexsize, hwtexsize) : hwtexsize;
+        while(t->w > sizelimit || t->h > sizelimit)
+        {
+            data += s.calclevelsize(level++);
+            levels--;
+            if(t->w > 1) t->w /= 2;
+            if(t->h > 1) t->h /= 2;
+        }
+        createcompressedtexture(t->id, t->w, t->h, data, s.align, s.bpp, levels, clamp, filter, s.compressed, GL_TEXTURE_2D, swizzle);
+    }
+    else
+    {
+        resizetexture(t->w, t->h, mipit, canreduce, GL_TEXTURE_2D, compress, t->w, t->h);
+        GLenum component = compressedformat(format, t->w, t->h, compress);
+        createtexture(t->id, t->w, t->h, s.data, clamp, filter, component, GL_TEXTURE_2D, t->xs, t->ys, s.pitch, false, format, swizzle);
+    }
+    return t;
+}
+
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+#define RGBAMASKS 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff
+#define RGBMASKS  0xff0000, 0x00ff00, 0x0000ff, 0
+#else
+#define RGBAMASKS 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000
+#define RGBMASKS  0x0000ff, 0x00ff00, 0xff0000, 0
+#endif
+
+SDL_Surface *wrapsurface(void *data, int width, int height, int bpp)
+{
+    switch(bpp)
+    {
+        case 3: return SDL_CreateRGBSurfaceFrom(data, width, height, 8*bpp, bpp*width, RGBMASKS);
+        case 4: return SDL_CreateRGBSurfaceFrom(data, width, height, 8*bpp, bpp*width, RGBAMASKS);
+    }
+    return NULL;
+}
+
+SDL_Surface *creatergbsurface(SDL_Surface *os)
+{
+    SDL_Surface *ns = SDL_CreateRGBSurface(SDL_SWSURFACE, os->w, os->h, 24, RGBMASKS);
+    if(ns) SDL_BlitSurface(os, NULL, ns, NULL);
+    SDL_FreeSurface(os);
+    return ns;
+}
+
+SDL_Surface *creatergbasurface(SDL_Surface *os)
+{
+    SDL_Surface *ns = SDL_CreateRGBSurface(SDL_SWSURFACE, os->w, os->h, 32, RGBAMASKS);
+    if(ns) 
+    {
+        SDL_SetSurfaceBlendMode(os, SDL_BLENDMODE_NONE);
+        SDL_BlitSurface(os, NULL, ns, NULL);
+    }
+    SDL_FreeSurface(os);
+    return ns;
+}
+
+bool checkgrayscale(SDL_Surface *s)
+{
+    // gray scale images have 256 levels, no colorkey, and the palette is a ramp
+    if(s->format->palette)
+    {
+        if(s->format->palette->ncolors != 256 || SDL_GetColorKey(s, NULL) >= 0) return false;
+        const SDL_Color *colors = s->format->palette->colors;
+        loopi(256) if(colors[i].r != i || colors[i].g != i || colors[i].b != i) return false;
+    }
+    return true;
+}
+
+SDL_Surface *fixsurfaceformat(SDL_Surface *s)
+{
+    if(!s) return NULL;
+    if(!s->pixels || min(s->w, s->h) <= 0 || s->format->BytesPerPixel <= 0)
+    { 
+        SDL_FreeSurface(s); 
+        return NULL; 
+    }
+    static const uint rgbmasks[] = { RGBMASKS }, rgbamasks[] = { RGBAMASKS };
+    switch(s->format->BytesPerPixel)
+    {
+        case 1:
+            if(!checkgrayscale(s)) return SDL_GetColorKey(s, NULL) >= 0 ? creatergbasurface(s) : creatergbsurface(s);
+            break;
+        case 3:
+            if(s->format->Rmask != rgbmasks[0] || s->format->Gmask != rgbmasks[1] || s->format->Bmask != rgbmasks[2]) 
+                return creatergbsurface(s);
+            break;
+        case 4:
+            if(s->format->Rmask != rgbamasks[0] || s->format->Gmask != rgbamasks[1] || s->format->Bmask != rgbamasks[2] || s->format->Amask != rgbamasks[3])
+                return s->format->Amask ? creatergbasurface(s) : creatergbsurface(s);
+            break;
+    }
+    return s;
+}
+
+void texflip(ImageData &s)
+{
+    ImageData d(s.w, s.h, s.bpp);
+    uchar *dst = d.data, *src = &s.data[s.pitch*s.h];
+    loopi(s.h)
+    {
+        src -= s.pitch;
+        memcpy(dst, src, s.bpp*s.w);
+        dst += d.pitch;
+    }
+    s.replace(d);
+}
+
+void texnormal(ImageData &s, int emphasis)    
+{
+    ImageData d(s.w, s.h, 3);
+    uchar *src = s.data, *dst = d.data;
+    loop(y, s.h) loop(x, s.w)
+    {
+        vec normal(0.0f, 0.0f, 255.0f/emphasis);
+        normal.x += src[y*s.pitch + ((x+s.w-1)%s.w)*s.bpp];
+        normal.x -= src[y*s.pitch + ((x+1)%s.w)*s.bpp];
+        normal.y += src[((y+s.h-1)%s.h)*s.pitch + x*s.bpp];
+        normal.y -= src[((y+1)%s.h)*s.pitch + x*s.bpp];
+        normal.normalize();
+        *dst++ = uchar(127.5f + normal.x*127.5f);
+        *dst++ = uchar(127.5f + normal.y*127.5f);
+        *dst++ = uchar(127.5f + normal.z*127.5f);
+    }
+    s.replace(d);
+}
+
+template<int n, int bpp, bool normals>
+static void blurtexture(int w, int h, uchar *dst, const uchar *src, int margin)
+{
+    static const int weights3x3[9] =
+    {
+        0x10, 0x20, 0x10,
+        0x20, 0x40, 0x20,
+        0x10, 0x20, 0x10
+    };
+    static const int weights5x5[25] =
+    {
+        0x05, 0x05, 0x09, 0x05, 0x05,
+        0x05, 0x0A, 0x14, 0x0A, 0x05,
+        0x09, 0x14, 0x28, 0x14, 0x09,
+        0x05, 0x0A, 0x14, 0x0A, 0x05,
+        0x05, 0x05, 0x09, 0x05, 0x05
+    };
+    const int *mat = n > 1 ? weights5x5 : weights3x3;
+    int mstride = 2*n + 1,
+        mstartoffset = n*(mstride + 1),
+        stride = bpp*w,
+        startoffset = n*bpp,
+        nextoffset1 = stride + mstride*bpp,
+        nextoffset2 = stride - mstride*bpp;
+    src += margin*(stride + bpp);
+    for(int y = margin; y < h-margin; y++)
+    {
+        for(int x = margin; x < w-margin; x++)
+        {
+            int dr = 0, dg = 0, db = 0;
+            const uchar *p = src - startoffset;
+            const int *m = mat + mstartoffset;
+            for(int t = y; t >= y-n; t--, p -= nextoffset1, m -= mstride)
+            {
+                if(t < 0) p += stride;
+                int a = 0;
+                if(n > 1) { a += m[-2]; if(x >= 2) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; }
+                a += m[-1]; if(x >= 1) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp;
+                int cr = p[0], cg = p[1], cb = p[2]; a += m[0]; dr += cr * a; dg += cg * a; db += cb * a; p += bpp;
+                if(x+1 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[1]; dg += cg * m[1]; db += cb * m[1]; p += bpp;
+                if(n > 1) { if(x+2 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[2]; dg += cg * m[2]; db += cb * m[2]; p += bpp; }
+            }
+            p = src - startoffset + stride;
+            m = mat + mstartoffset + mstride;
+            for(int t = y+1; t <= y+n; t++, p += nextoffset2, m += mstride)
+            {
+                if(t >= h) p -= stride;
+                int a = 0;
+                if(n > 1) { a += m[-2]; if(x >= 2) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp; }
+                a += m[-1]; if(x >= 1) { dr += p[0] * a; dg += p[1] * a; db += p[2] * a; a = 0; } p += bpp;
+                int cr = p[0], cg = p[1], cb = p[2]; a += m[0]; dr += cr * a; dg += cg * a; db += cb * a; p += bpp;
+                if(x+1 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[1]; dg += cg * m[1]; db += cb * m[1]; p += bpp;
+                if(n > 1) { if(x+2 < w) { cr = p[0]; cg = p[1]; cb = p[2]; } dr += cr * m[2]; dg += cg * m[2]; db += cb * m[2]; p += bpp; }
+            }
+            if(normals)
+            {
+                vec v(dr-0x7F80, dg-0x7F80, db-0x7F80);
+                float mag = 127.5f/v.magnitude();
+                dst[0] = uchar(v.x*mag + 127.5f);
+                dst[1] = uchar(v.y*mag + 127.5f);
+                dst[2] = uchar(v.z*mag + 127.5f);
+            }
+            else 
+            {
+                dst[0] = dr>>8;
+                dst[1] = dg>>8;
+                dst[2] = db>>8;
+            }
+            if(bpp > 3) dst[3] = src[3];
+            dst += bpp;
+            src += bpp;
+        }
+        src += 2*margin*bpp;
+    }
+}
+
+void blurtexture(int n, int bpp, int w, int h, uchar *dst, const uchar *src, int margin)
+{
+    switch((clamp(n, 1, 2)<<4) | bpp)
+    {
+        case 0x13: blurtexture<1, 3, false>(w, h, dst, src, margin); break;
+        case 0x23: blurtexture<2, 3, false>(w, h, dst, src, margin); break;
+        case 0x14: blurtexture<1, 4, false>(w, h, dst, src, margin); break;
+        case 0x24: blurtexture<2, 4, false>(w, h, dst, src, margin); break;
+    }
+}
+
+void blurnormals(int n, int w, int h, bvec *dst, const bvec *src, int margin)
+{
+    switch(clamp(n, 1, 2))
+    {
+        case 1: blurtexture<1, 3, true>(w, h, dst->v, src->v, margin); break;
+        case 2: blurtexture<2, 3, true>(w, h, dst->v, src->v, margin); break;
+    }
+}
+void texblur(ImageData &s, int n, int r)
+{
+    if(s.bpp < 3) return;
+    loopi(r)
+    {
+        ImageData d(s.w, s.h, s.bpp);
+        blurtexture(n, s.bpp, s.w, s.h, d.data, s.data);
+        s.replace(d);
+    }
+}
+
+void scaleimage(ImageData &s, int w, int h)
+{
+    ImageData d(w, h, s.bpp);
+    scaletexture(s.data, s.w, s.h, s.bpp, s.pitch, d.data, w, h);
+    s.replace(d);
+}
+
+bool canloadsurface(const char *name)
+{
+    stream *f = openfile(name, "rb");
+    if(!f) return false;
+    delete f;
+    return true;
+}
+
+SDL_Surface *loadsurface(const char *name)
+{
+    SDL_Surface *s = NULL;
+    stream *z = openzipfile(name, "rb");
+    if(z)
+    {
+        SDL_RWops *rw = z->rwops();
+        if(rw) 
+        {
+            char *ext = (char *)strrchr(name, '.');
+            if(ext) ++ext;
+            s = IMG_LoadTyped_RW(rw, 0, ext);
+            SDL_FreeRW(rw);
+        }
+        delete z;
+    }
+    if(!s) s = IMG_Load(findfile(name, "rb"));
+    return fixsurfaceformat(s);
+}
+   
+static vec parsevec(const char *arg)
+{
+    vec v(0, 0, 0);
+    int i = 0;
+    for(; arg[0] && (!i || arg[0]=='/') && i<3; arg += strcspn(arg, "/,><"), i++)
+    {
+        if(i) arg++;
+        v[i] = atof(arg);
+    }
+    if(i==1) v.y = v.z = v.x;
+    return v;
+}
+
+VAR(usedds, 0, 1, 1);
+VAR(dbgdds, 0, 0, 1);
+VAR(scaledds, 0, 2, 4);
+
+static bool texturedata(ImageData &d, const char *tname, Slot::Tex *tex = NULL, bool msg = true, int *compress = NULL, int *wrap = NULL)
+{
+    const char *cmds = NULL, *file = tname;
+
+    if(!tname)
+    {
+        if(!tex) return false;
+        if(tex->name[0]=='<') 
+        {
+            cmds = tex->name;
+            file = strrchr(tex->name, '>');
+            if(!file) { if(msg) conoutf(CON_ERROR, "could not load texture packages/%s", tex->name); return false; }
+            file++;
+        }
+        else file = tex->name;
+        
+        static string pname;
+        formatstring(pname, "packages/%s", file);
+        file = path(pname);
+    }
+    else if(tname[0]=='<') 
+    {
+        cmds = tname;
+        file = strrchr(tname, '>');
+        if(!file) { if(msg) conoutf(CON_ERROR, "could not load texture %s", tname); return false; }
+        file++;
+    }
+
+    int flen = strlen(file);
+    bool raw = !usedds || !compress, dds = false, guess = false;
+    for(const char *pcmds = cmds; pcmds;)
+    {
+        #define PARSETEXCOMMANDS(cmds) \
+            const char *cmd = NULL, *end = NULL, *arg[4] = { NULL, NULL, NULL, NULL }; \
+            cmd = &cmds[1]; \
+            end = strchr(cmd, '>'); \
+            if(!end) break; \
+            cmds = strchr(cmd, '<'); \
+            size_t len = strcspn(cmd, ":,><"); \
+            loopi(4) \
+            { \
+                arg[i] = strchr(i ? arg[i-1] : cmd, i ? ',' : ':'); \
+                if(!arg[i] || arg[i] >= end) arg[i] = ""; \
+                else arg[i]++; \
+            }
+        PARSETEXCOMMANDS(pcmds);
+        if(matchstring(cmd, len, "dds")) dds = true;
+        else if(matchstring(cmd, len, "thumbnail"))
+        {
+            raw = true;
+            guess = flen >= 4 && !strchr(file+flen-4, '.');
+        }
+        else if(matchstring(cmd, len, "stub")) return canloadsurface(file);
+    }
+
+    if(msg) renderprogress(loadprogress, file);
+
+    if(flen >= 4 && (!strcasecmp(file + flen - 4, ".dds") || (dds && !raw)))
+    {
+        string dfile;
+        copystring(dfile, file);
+        memcpy(dfile + flen - 4, ".dds", 4);
+        if(!loaddds(dfile, d, raw ? 1 : (dds ? 0 : -1)) && (!dds || raw))
+        {
+            if(msg) conoutf(CON_ERROR, "could not load texture %s", dfile);
+            return false;
+        }
+        if(d.data && !d.compressed && !dds && compress) *compress = scaledds;
+    }
+        
+    if(!d.data)
+    {
+        SDL_Surface *s = NULL;
+        if(guess)
+        {
+            static const char *exts[] = {".jpg", ".png"};
+            string ext;
+            loopi(sizeof(exts)/sizeof(exts[0]))
+            {
+                copystring(ext, file);
+                concatstring(ext, exts[i]);
+                s = loadsurface(ext);
+                if(s) break;
+            }
+        }
+        else s = loadsurface(file);
+        if(!s) { if(msg) conoutf(CON_ERROR, "could not load texture %s", file); return false; }
+        int bpp = s->format->BitsPerPixel;
+        if(bpp%8 || !texformat(bpp/8)) { SDL_FreeSurface(s); conoutf(CON_ERROR, "texture must be 8, 16, 24, or 32 bpp: %s", file); return false; }
+        if(max(s->w, s->h) > (1<<12)) { SDL_FreeSurface(s); conoutf(CON_ERROR, "texture size exceeded %dx%d pixels: %s", 1<<12, 1<<12, file); return false; }
+        d.wrap(s);
+    }
+
+    while(cmds)
+    {
+        PARSETEXCOMMANDS(cmds);
+        if(d.compressed) goto compressed;
+        if(matchstring(cmd, len, "mad")) texmad(d, parsevec(arg[0]), parsevec(arg[1])); 
+        else if(matchstring(cmd, len, "colorify")) texcolorify(d, parsevec(arg[0]), parsevec(arg[1]));
+        else if(matchstring(cmd, len, "colormask")) texcolormask(d, parsevec(arg[0]), *arg[1] ? parsevec(arg[1]) : vec(1, 1, 1));
+        else if(matchstring(cmd, len, "normal")) 
+        {
+            int emphasis = atoi(arg[0]);
+            texnormal(d, emphasis > 0 ? emphasis : 3);
+        }
+        else if(matchstring(cmd, len, "dup")) texdup(d, atoi(arg[0]), atoi(arg[1]));
+        else if(matchstring(cmd, len, "offset")) texoffset(d, atoi(arg[0]), atoi(arg[1]));
+        else if(matchstring(cmd, len, "rotate")) texrotate(d, atoi(arg[0]), tex ? tex->type : 0);
+        else if(matchstring(cmd, len, "reorient")) texreorient(d, atoi(arg[0])>0, atoi(arg[1])>0, atoi(arg[2])>0, tex ? tex->type : TEX_DIFFUSE);
+        else if(matchstring(cmd, len, "mix")) texmix(d, *arg[0] ? atoi(arg[0]) : -1, *arg[1] ? atoi(arg[1]) : -1, *arg[2] ? atoi(arg[2]) : -1, *arg[3] ? atoi(arg[3]) : -1);
+        else if(matchstring(cmd, len, "grey")) texgrey(d);
+        else if(matchstring(cmd, len, "blur"))
+        {
+            int emphasis = atoi(arg[0]), repeat = atoi(arg[1]);
+            texblur(d, emphasis > 0 ? clamp(emphasis, 1, 2) : 1, repeat > 0 ? repeat : 1);
+        }
+        else if(matchstring(cmd, len, "premul")) texpremul(d);
+        else if(matchstring(cmd, len, "agrad")) texagrad(d, atof(arg[0]), atof(arg[1]), atof(arg[2]), atof(arg[3]));
+        else if(matchstring(cmd, len, "compress") || matchstring(cmd, len, "dds")) 
+        { 
+            int scale = atoi(arg[0]);
+            if(scale <= 0) scale = scaledds;
+            if(compress) *compress = scale;
+        }
+        else if(matchstring(cmd, len, "nocompress"))
+        {
+            if(compress) *compress = -1;
+        }
+        else if(matchstring(cmd, len, "thumbnail"))
+        {
+            int w = atoi(arg[0]), h = atoi(arg[1]);
+            if(w <= 0 || w > (1<<12)) w = 64;
+            if(h <= 0 || h > (1<<12)) h = w;
+            if(d.w > w || d.h > h) scaleimage(d, w, h);
+        }
+        else
+    compressed:
+        if(matchstring(cmd, len, "mirror"))
+        {
+            if(wrap) *wrap |= 0x300;
+        }
+        else if(matchstring(cmd, len, "noswizzle"))
+        {
+            if(wrap) *wrap |= 0x10000;
+        }
+    }
+
+    return true;
+}
+
+uchar *loadalphamask(Texture *t)
+{
+    if(t->alphamask) return t->alphamask;
+    if(!(t->type&Texture::ALPHA)) return NULL;
+    ImageData s;
+    if(!texturedata(s, t->name, NULL, false) || !s.data || s.compressed) return NULL;
+    t->alphamask = new uchar[s.h * ((s.w+7)/8)];
+    uchar *srcrow = s.data, *dst = t->alphamask-1;
+    loop(y, s.h)
+    {
+        uchar *src = srcrow+s.bpp-1;
+        loop(x, s.w)
+        {
+            int offset = x%8;
+            if(!offset) *++dst = 0;
+            if(*src) *dst |= 1<<offset;
+            src += s.bpp;
+        }
+        srcrow += s.pitch;
+    }
+    return t->alphamask;
+}
+
+Texture *textureload(const char *name, int clamp, bool mipit, bool msg)
+{
+    string tname;
+    copystring(tname, name);
+    Texture *t = textures.access(path(tname));
+    if(t) return t;
+    int compress = 0;
+    ImageData s;
+    if(texturedata(s, tname, NULL, msg, &compress, &clamp)) return newtexture(NULL, tname, s, clamp, mipit, false, false, compress);
+    return notexture;
+}
+
+bool settexture(const char *name, int clamp)
+{
+    Texture *t = textureload(name, clamp, true, false);
+    glBindTexture(GL_TEXTURE_2D, t->id);
+    return t != notexture;
+}
+
+vector<VSlot *> vslots;
+vector<Slot *> slots;
+MSlot materialslots[(MATF_VOLUME|MATF_INDEX)+1];
+Slot dummyslot;
+VSlot dummyvslot(&dummyslot);
+
+void texturereset(int *n)
+{
+    if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return;
+    resetslotshader();
+    int limit = clamp(*n, 0, slots.length());
+    for(int i = limit; i < slots.length(); i++) 
+    {
+        Slot *s = slots[i];
+        for(VSlot *vs = s->variants; vs; vs = vs->next) vs->slot = &dummyslot;
+        delete s;
+    }
+    slots.setsize(limit);
+    while(vslots.length())
+    {
+        VSlot *vs = vslots.last();
+        if(vs->slot != &dummyslot || vs->changed) break;
+        delete vslots.pop();
+    }
+}
+
+COMMAND(texturereset, "i");
+
+void materialreset()
+{
+    if(!(identflags&IDF_OVERRIDDEN) && !game::allowedittoggle()) return;
+    loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].reset();
+}
+
+COMMAND(materialreset, "");
+
+static int compactedvslots = 0, compactvslotsprogress = 0, clonedvslots = 0;
+static bool markingvslots = false;
+
+void clearslots()
+{
+    resetslotshader();
+    slots.deletecontents();
+    vslots.deletecontents();
+    loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].reset();
+    clonedvslots = 0;
+}
+
+static void assignvslot(VSlot &vs);
+
+static inline void assignvslotlayer(VSlot &vs)
+{
+    if(vs.layer && vslots.inrange(vs.layer))
+    {
+        VSlot &layer = *vslots[vs.layer];
+        if(layer.index < 0) assignvslot(layer);
+    }
+}
+
+static void assignvslot(VSlot &vs)
+{
+    vs.index = compactedvslots++;
+    assignvslotlayer(vs);
+}
+
+void compactvslot(int &index)
+{
+    if(vslots.inrange(index))
+    {
+        VSlot &vs = *vslots[index];
+        if(vs.index < 0) assignvslot(vs);
+        if(!markingvslots) index = vs.index;
+    }
+}
+
+void compactvslot(VSlot &vs)
+{
+    if(vs.index < 0) assignvslot(vs);
+}
+
+void compactvslots(cube *c, int n)
+{
+    if((compactvslotsprogress++&0xFFF)==0) renderprogress(min(float(compactvslotsprogress)/allocnodes, 1.0f), markingvslots ? "marking slots..." : "compacting slots...");
+    loopi(n)
+    {
+        if(c[i].children) compactvslots(c[i].children);
+        else loopj(6) if(vslots.inrange(c[i].texture[j]))
+        {
+            VSlot &vs = *vslots[c[i].texture[j]];
+            if(vs.index < 0) assignvslot(vs);
+            if(!markingvslots) c[i].texture[j] = vs.index;
+        }
+    }
+}
+
+int compactvslots()
+{
+    clonedvslots = 0;
+    markingvslots = false;
+    compactedvslots = 0;
+    compactvslotsprogress = 0;
+    loopv(vslots) vslots[i]->index = -1;
+    loopv(slots) slots[i]->variants->index = compactedvslots++;
+    loopv(slots) assignvslotlayer(*slots[i]->variants);
+    loopv(vslots)
+    {
+        VSlot &vs = *vslots[i];
+        if(!vs.changed && vs.index < 0) { markingvslots = true; break; }
+    }
+    compactvslots(worldroot);
+    int total = compactedvslots;
+    compacteditvslots();
+    loopv(vslots)
+    {
+        VSlot *vs = vslots[i];
+        if(vs->changed) continue;
+        while(vs->next)
+        {
+            if(vs->next->index < 0) vs->next = vs->next->next;
+            else vs = vs->next;
+        }
+    }
+    if(markingvslots)
+    {
+        markingvslots = false;
+        compactedvslots = 0;
+        compactvslotsprogress = 0;
+        int lastdiscard = 0;
+        loopv(vslots)
+        {
+            VSlot &vs = *vslots[i];
+            if(vs.changed || (vs.index < 0 && !vs.next)) vs.index = -1;
+            else
+            {
+                while(lastdiscard < i)
+                {
+                    VSlot &ds = *vslots[lastdiscard++];
+                    if(!ds.changed && ds.index < 0) ds.index = compactedvslots++;
+                } 
+                vs.index = compactedvslots++;
+            }
+        }
+        compactvslots(worldroot);
+        total = compactedvslots;
+        compacteditvslots();
+    }
+    compactmruvslots();
+    loopv(vslots)
+    {
+        VSlot &vs = *vslots[i];
+        if(vs.index >= 0 && vs.layer && vslots.inrange(vs.layer)) vs.layer = vslots[vs.layer]->index;
+    }
+    loopv(vslots) 
+    {
+        while(vslots[i]->index >= 0 && vslots[i]->index != i)     
+            swap(vslots[i], vslots[vslots[i]->index]); 
+    }
+    for(int i = compactedvslots; i < vslots.length(); i++) delete vslots[i];
+    vslots.setsize(compactedvslots);
+    return total;
+}
+
+ICOMMAND(compactvslots, "", (),
+{
+    extern int nompedit;
+    if(nompedit && multiplayer()) return;
+    compactvslots();
+    allchanged();
+});
+
+static Slot &loadslot(Slot &s, bool forceload);
+
+static void clampvslotoffset(VSlot &dst, Slot *slot = NULL)
+{
+    if(!slot) slot = dst.slot;
+    if(slot && slot->sts.inrange(0))
+    {
+        if(!slot->loaded) loadslot(*slot, false);
+        Texture *t = slot->sts[0].t;
+        int xs = t->xs, ys = t->ys;
+        if(t->type & Texture::MIRROR) { xs *= 2; ys *= 2; }
+        if(texrotations[dst.rotation].swapxy) swap(xs, ys);
+        dst.offset.x %= xs; if(dst.offset.x < 0) dst.offset.x += xs;
+        dst.offset.y %= ys; if(dst.offset.y < 0) dst.offset.y += ys;
+    }
+    else dst.offset.max(0);
+}
+
+static void propagatevslot(VSlot &dst, const VSlot &src, int diff, bool edit = false)
+{
+    if(diff & (1<<VSLOT_SHPARAM)) loopv(src.params) dst.params.add(src.params[i]);
+    if(diff & (1<<VSLOT_SCALE)) dst.scale = src.scale;
+    if(diff & (1<<VSLOT_ROTATION)) 
+    {
+        dst.rotation = src.rotation;
+        if(edit && !dst.offset.iszero()) clampvslotoffset(dst);
+    }
+    if(diff & (1<<VSLOT_OFFSET))
+    {
+        dst.offset = src.offset;
+        if(edit) clampvslotoffset(dst);
+    }
+    if(diff & (1<<VSLOT_SCROLL)) dst.scroll = src.scroll;
+    if(diff & (1<<VSLOT_LAYER)) dst.layer = src.layer;
+    if(diff & (1<<VSLOT_ALPHA))
+    {
+        dst.alphafront = src.alphafront;
+        dst.alphaback = src.alphaback;
+    }
+    if(diff & (1<<VSLOT_COLOR)) dst.colorscale = src.colorscale;
+}
+
+static void propagatevslot(VSlot *root, int changed)
+{
+    for(VSlot *vs = root->next; vs; vs = vs->next)
+    {
+        int diff = changed & ~vs->changed;
+        if(diff) propagatevslot(*vs, *root, diff);
+    }
+}
+
+static void mergevslot(VSlot &dst, const VSlot &src, int diff, Slot *slot = NULL)
+{
+    if(diff & (1<<VSLOT_SHPARAM)) loopv(src.params) 
+    {
+        const SlotShaderParam &sp = src.params[i];
+        loopvj(dst.params)
+        {
+            SlotShaderParam &dp = dst.params[j];
+            if(sp.name == dp.name)
+            {
+                memcpy(dp.val, sp.val, sizeof(dp.val));
+                goto nextparam;
+            }
+        }
+        dst.params.add(sp);
+    nextparam:;
+    }
+    if(diff & (1<<VSLOT_SCALE)) 
+    {
+        dst.scale = clamp(dst.scale*src.scale, 1/8.0f, 8.0f);
+    }
+    if(diff & (1<<VSLOT_ROTATION)) 
+    {
+        dst.rotation = clamp(dst.rotation + src.rotation, 0, 7);
+        if(!dst.offset.iszero()) clampvslotoffset(dst, slot);
+    }
+    if(diff & (1<<VSLOT_OFFSET))
+    {
+        dst.offset.add(src.offset);
+        clampvslotoffset(dst, slot);
+    }
+    if(diff & (1<<VSLOT_SCROLL)) dst.scroll.add(src.scroll);
+    if(diff & (1<<VSLOT_LAYER)) dst.layer = src.layer;
+    if(diff & (1<<VSLOT_ALPHA))
+    {
+        dst.alphafront = src.alphafront;
+        dst.alphaback = src.alphaback;
+    }
+    if(diff & (1<<VSLOT_COLOR)) dst.colorscale.mul(src.colorscale);
+}
+
+void mergevslot(VSlot &dst, const VSlot &src, const VSlot &delta)
+{
+    dst.changed = src.changed | delta.changed;
+    propagatevslot(dst, src, (1<<VSLOT_NUM)-1);
+    mergevslot(dst, delta, delta.changed, src.slot);
+}
+
+static VSlot *reassignvslot(Slot &owner, VSlot *vs)
+{
+    owner.variants = vs;
+    while(vs)
+    {
+        vs->slot = &owner;
+        vs->linked = false;
+        vs = vs->next;
+    }
+    return owner.variants;
+}
+
+static VSlot *emptyvslot(Slot &owner)
+{
+    int offset = 0;
+    loopvrev(slots) if(slots[i]->variants) { offset = slots[i]->variants->index + 1; break; }
+    for(int i = offset; i < vslots.length(); i++) if(!vslots[i]->changed) return reassignvslot(owner, vslots[i]);
+    return vslots.add(new VSlot(&owner, vslots.length()));
+}
+
+static bool comparevslot(const VSlot &dst, const VSlot &src, int diff)
+{
+    if(diff & (1<<VSLOT_SHPARAM)) 
+    {
+        if(src.params.length() != dst.params.length()) return false;
+        loopv(src.params) 
+        {
+            const SlotShaderParam &sp = src.params[i], &dp = dst.params[i];
+            if(sp.name != dp.name || memcmp(sp.val, dp.val, sizeof(sp.val))) return false;
+        }
+    }
+    if(diff & (1<<VSLOT_SCALE) && dst.scale != src.scale) return false;
+    if(diff & (1<<VSLOT_ROTATION) && dst.rotation != src.rotation) return false;
+    if(diff & (1<<VSLOT_OFFSET) && dst.offset != src.offset) return false;
+    if(diff & (1<<VSLOT_SCROLL) && dst.scroll != src.scroll) return false;
+    if(diff & (1<<VSLOT_LAYER) && dst.layer != src.layer) return false;
+    if(diff & (1<<VSLOT_ALPHA) && (dst.alphafront != src.alphafront || dst.alphaback != src.alphaback)) return false;
+    if(diff & (1<<VSLOT_COLOR) && dst.colorscale != src.colorscale) return false;
+    return true;
+}
+
+void packvslot(vector<uchar> &buf, const VSlot &src)
+{
+    if(src.changed & (1<<VSLOT_SHPARAM))
+    {
+        loopv(src.params)
+        {
+            const SlotShaderParam &p = src.params[i];
+            buf.put(VSLOT_SHPARAM);
+            sendstring(p.name, buf);
+            loopj(4) putfloat(buf, p.val[j]);
+        }
+    }
+    if(src.changed & (1<<VSLOT_SCALE))
+    {
+        buf.put(VSLOT_SCALE);
+        putfloat(buf, src.scale);
+    }
+    if(src.changed & (1<<VSLOT_ROTATION))
+    {
+        buf.put(VSLOT_ROTATION);
+        putint(buf, src.rotation);
+    }
+    if(src.changed & (1<<VSLOT_OFFSET))
+    {
+        buf.put(VSLOT_OFFSET);
+        putint(buf, src.offset.x);
+        putint(buf, src.offset.y);
+    }
+    if(src.changed & (1<<VSLOT_SCROLL))
+    {
+        buf.put(VSLOT_SCROLL);
+        putfloat(buf, src.scroll.x);
+        putfloat(buf, src.scroll.y);
+    }
+    if(src.changed & (1<<VSLOT_LAYER))
+    {
+        buf.put(VSLOT_LAYER);
+        putuint(buf, vslots.inrange(src.layer) && !vslots[src.layer]->changed ? src.layer : 0);
+    }
+    if(src.changed & (1<<VSLOT_ALPHA))
+    {
+        buf.put(VSLOT_ALPHA);
+        putfloat(buf, src.alphafront);
+        putfloat(buf, src.alphaback);
+    }
+    if(src.changed & (1<<VSLOT_COLOR))
+    {
+        buf.put(VSLOT_COLOR);
+        putfloat(buf, src.colorscale.r);
+        putfloat(buf, src.colorscale.g);
+        putfloat(buf, src.colorscale.b);
+    }
+    buf.put(0xFF);
+}
+
+void packvslot(vector<uchar> &buf, int index)
+{
+    if(vslots.inrange(index)) packvslot(buf, *vslots[index]);
+    else buf.put(0xFF);
+}
+
+void packvslot(vector<uchar> &buf, const VSlot *vs)
+{
+    if(vs) packvslot(buf, *vs);
+    else buf.put(0xFF);
+}
+
+bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta)
+{
+    while(buf.remaining())
+    {
+        int changed = buf.get();
+        if(changed >= 0x80) break;
+        switch(changed)
+        {
+            case VSLOT_SHPARAM:
+            {
+                string name;
+                getstring(name, buf);
+                SlotShaderParam p = { name[0] ? getshaderparamname(name) : NULL, -1, { 0, 0, 0, 0 } };
+                loopi(4) p.val[i] = getfloat(buf);
+                if(p.name) dst.params.add(p);
+                break;
+            }
+            case VSLOT_SCALE:
+                dst.scale = getfloat(buf);
+                if(dst.scale <= 0) dst.scale = 1;
+                else if(!delta) dst.scale = clamp(dst.scale, 1/8.0f, 8.0f);
+                break;
+            case VSLOT_ROTATION:
+                dst.rotation = getint(buf);
+                if(!delta) dst.rotation = clamp(dst.rotation, 0, 7);
+                break;
+            case VSLOT_OFFSET:
+                dst.offset.x = getint(buf);
+                dst.offset.y = getint(buf);
+                if(!delta) dst.offset.max(0);
+                break;
+            case VSLOT_SCROLL:
+                dst.scroll.x = getfloat(buf);
+                dst.scroll.y = getfloat(buf);
+                break;
+            case VSLOT_LAYER:
+            {
+                int tex = getuint(buf);
+                dst.layer = vslots.inrange(tex) ? tex : 0;
+                break;
+            }
+            case VSLOT_ALPHA:
+                dst.alphafront = clamp(getfloat(buf), 0.0f, 1.0f);
+                dst.alphaback = clamp(getfloat(buf), 0.0f, 1.0f);
+                break;
+            case VSLOT_COLOR:
+                dst.colorscale.r = clamp(getfloat(buf), 0.0f, 2.0f);
+                dst.colorscale.g = clamp(getfloat(buf), 0.0f, 2.0f);
+                dst.colorscale.b = clamp(getfloat(buf), 0.0f, 2.0f);
+                break;
+            default:
+                return false;
+        }
+        dst.changed |= 1<<changed;
+    }
+    if(buf.overread()) return false;
+    return true;
+}
+
+VSlot *findvslot(Slot &slot, const VSlot &src, const VSlot &delta)
+{
+    for(VSlot *dst = slot.variants; dst; dst = dst->next)
+    {
+        if((!dst->changed || dst->changed == (src.changed | delta.changed)) &&
+           comparevslot(*dst, src, src.changed & ~delta.changed) &&
+           comparevslot(*dst, delta, delta.changed))
+            return dst;
+    }
+    return NULL;
+}
+
+static VSlot *clonevslot(const VSlot &src, const VSlot &delta)
+{
+    VSlot *dst = vslots.add(new VSlot(src.slot, vslots.length()));
+    dst->changed = src.changed | delta.changed;
+    propagatevslot(*dst, src, ((1<<VSLOT_NUM)-1) & ~delta.changed);
+    propagatevslot(*dst, delta, delta.changed, true);
+    return dst;
+}
+
+VARP(autocompactvslots, 0, 256, 0x10000);
+
+VSlot *editvslot(const VSlot &src, const VSlot &delta)
+{
+    VSlot *exists = findvslot(*src.slot, src, delta);
+    if(exists) return exists;
+    if(vslots.length()>=0x10000)
+    {
+        compactvslots();
+        allchanged();
+        if(vslots.length()>=0x10000) return NULL;
+    }
+    if(autocompactvslots && ++clonedvslots >= autocompactvslots)
+    {
+        compactvslots();
+        allchanged();
+    }
+    return clonevslot(src, delta);
+}
+
+static void fixinsidefaces(cube *c, const ivec &o, int size, int tex)
+{
+    loopi(8) 
+    {
+        ivec co(i, o, size);
+        if(c[i].children) fixinsidefaces(c[i].children, co, size>>1, tex);
+        else loopj(6) if(!visibletris(c[i], j, co, size))
+            c[i].texture[j] = tex;
+    }
+}
+
+ICOMMAND(fixinsidefaces, "i", (int *tex),
+{
+    extern int nompedit;
+    if(noedit(true) || (nompedit && multiplayer())) return;
+    fixinsidefaces(worldroot, ivec(0, 0, 0), worldsize>>1, *tex && vslots.inrange(*tex) ? *tex : DEFAULT_GEOM);
+    allchanged();
+});
+
+const struct slottex
+{
+    const char *name;
+    int id;
+} slottexs[] =
+{
+    {"c", TEX_DIFFUSE},
+    {"u", TEX_UNKNOWN},
+    {"d", TEX_DECAL},
+    {"n", TEX_NORMAL},
+    {"g", TEX_GLOW},
+    {"s", TEX_SPEC},
+    {"z", TEX_DEPTH},
+    {"a", TEX_ALPHA},
+    {"e", TEX_ENVMAP}
+};
+
+int findslottex(const char *name)
+{
+    loopi(sizeof(slottexs)/sizeof(slottex))
+    {
+        if(!strcmp(slottexs[i].name, name)) return slottexs[i].id;
+    }
+    return -1;
+}
+
+void texture(char *type, char *name, int *rot, int *xoffset, int *yoffset, float *scale)
+{
+    if(slots.length()>=0x10000) return;
+    static int lastmatslot = -1;
+    int tnum = findslottex(type), matslot = findmaterial(type);
+    if(tnum<0) tnum = atoi(type);
+    if(tnum==TEX_DIFFUSE) lastmatslot = matslot;
+    else if(lastmatslot>=0) matslot = lastmatslot;
+    else if(slots.empty()) return;
+    Slot &s = matslot>=0 ? materialslots[matslot] : *(tnum!=TEX_DIFFUSE ? slots.last() : slots.add(new Slot(slots.length())));
+    s.loaded = false;
+    s.texmask |= 1<<tnum;
+    if(s.sts.length()>=8) conoutf(CON_WARN, "warning: too many textures in slot %d", slots.length()-1);
+    Slot::Tex &st = s.sts.add();
+    st.type = tnum;
+    st.combined = -1;
+    st.t = NULL;
+    copystring(st.name, name);
+    path(st.name);
+    if(tnum==TEX_DIFFUSE)
+    {
+        setslotshader(s);
+        VSlot &vs = matslot >= 0 ? materialslots[matslot] : *emptyvslot(s);
+        vs.reset();
+        vs.rotation = clamp(*rot, 0, 7);
+        vs.offset = ivec2(*xoffset, *yoffset).max(0);
+        vs.scale = *scale <= 0 ? 1 : *scale;
+        propagatevslot(&vs, (1<<VSLOT_NUM)-1);
+    }
+}
+
+COMMAND(texture, "ssiiif");
+
+void autograss(char *name)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    DELETEA(s.autograss);
+    s.autograss = name[0] ? newstring(makerelpath("packages", name, NULL, "<premul>")) : NULL;
+}
+COMMAND(autograss, "s");
+
+void texscroll(float *scrollS, float *scrollT)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->scroll = vec2(*scrollS, *scrollT).div(1000.0f);
+    propagatevslot(s.variants, 1<<VSLOT_SCROLL);
+}
+COMMAND(texscroll, "ff");
+
+void texoffset_(int *xoffset, int *yoffset)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->offset = ivec2(*xoffset, *yoffset).max(0);
+    propagatevslot(s.variants, 1<<VSLOT_OFFSET);
+}
+COMMANDN(texoffset, texoffset_, "ii");
+
+void texrotate_(int *rot)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->rotation = clamp(*rot, 0, 7);
+    propagatevslot(s.variants, 1<<VSLOT_ROTATION);
+}
+COMMANDN(texrotate, texrotate_, "i");
+
+void texscale(float *scale)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->scale = *scale <= 0 ? 1 : *scale;
+    propagatevslot(s.variants, 1<<VSLOT_SCALE);
+}
+COMMAND(texscale, "f");
+
+void texlayer(int *layer, char *name, int *mode, float *scale)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->layer = *layer < 0 ? max(slots.length()-1+*layer, 0) : *layer;
+    s.layermaskname = name[0] ? newstring(path(makerelpath("packages", name))) : NULL; 
+    s.layermaskmode = *mode;
+    s.layermaskscale = *scale <= 0 ? 1 : *scale;
+    propagatevslot(s.variants, 1<<VSLOT_LAYER);
+}
+COMMAND(texlayer, "isif");
+
+void texalpha(float *front, float *back)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->alphafront = clamp(*front, 0.0f, 1.0f);
+    s.variants->alphaback = clamp(*back, 0.0f, 1.0f);
+    propagatevslot(s.variants, 1<<VSLOT_ALPHA);
+}
+COMMAND(texalpha, "ff");
+
+void texcolor(float *r, float *g, float *b)
+{
+    if(slots.empty()) return;
+    Slot &s = *slots.last();
+    s.variants->colorscale = vec(clamp(*r, 0.0f, 1.0f), clamp(*g, 0.0f, 1.0f), clamp(*b, 0.0f, 1.0f));
+    propagatevslot(s.variants, 1<<VSLOT_COLOR);
+}
+COMMAND(texcolor, "fff");
+
+static int findtextype(Slot &s, int type, int last = -1)
+{
+    for(int i = last+1; i<s.sts.length(); i++) if((type&(1<<s.sts[i].type)) && s.sts[i].combined<0) return i;
+    return -1;
+}
+
+static void addglow(ImageData &c, ImageData &g, const vec &glowcolor)
+{
+    if(g.bpp < 3)
+    {
+        readwritergbtex(c, g,
+            loopk(3) dst[k] = clamp(int(dst[k]) + int(src[0]*glowcolor[k]), 0, 255);
+        );
+    }
+    else
+    {
+        readwritergbtex(c, g,
+            loopk(3) dst[k] = clamp(int(dst[k]) + int(src[k]*glowcolor[k]), 0, 255);
+        );
+    }
+}
+
+static void mergespec(ImageData &c, ImageData &s)
+{
+    if(s.bpp < 3)
+    {
+        readwritergbatex(c, s,
+            dst[3] = src[0];
+        );
+    }
+    else
+    {
+        readwritergbatex(c, s,
+            dst[3] = (int(src[0]) + int(src[1]) + int(src[2]))/3;
+        );
+    }
+}
+
+static void mergedepth(ImageData &c, ImageData &z)
+{
+    readwritergbatex(c, z,
+        dst[3] = src[0];
+    );
+}
+
+static void mergealpha(ImageData &c, ImageData &s)
+{
+    if(s.bpp < 3)
+    {
+        readwritergbatex(c, s,
+            dst[3] = src[0];
+        );
+    }
+    else if(s.bpp == 3)
+    {
+        readwritergbatex(c, s,
+            dst[3] = (int(src[0]) + int(src[1]) + int(src[2]))/3;
+        );
+    }
+    else
+    {
+        readwritergbatex(c, s,
+            dst[3] = src[3];
+        );
+    }
+}
+
+static void addname(vector<char> &key, Slot &slot, Slot::Tex &t, bool combined = false, const char *prefix = NULL)
+{
+    if(combined) key.add('&');
+    if(prefix) { while(*prefix) key.add(*prefix++); }
+    defformatstring(tname, "packages/%s", t.name);
+    for(const char *s = path(tname); *s; key.add(*s++));
+}
+
+static void texcombine(Slot &s, int index, Slot::Tex &t, bool forceload = false)
+{
+    vector<char> key; 
+    addname(key, s, t);
+    int texmask = 0;
+    if(!forceload) switch(t.type)
+    {
+        case TEX_DIFFUSE:
+        case TEX_NORMAL:
+        {
+            int i = findtextype(s, t.type==TEX_DIFFUSE ? (s.texmask&(1<<TEX_SPEC) ? 1<<TEX_SPEC : 1<<TEX_ALPHA) : (s.texmask&(1<<TEX_DEPTH) ? 1<<TEX_DEPTH : 1<<TEX_ALPHA));
+            if(i<0) break;
+            texmask |= 1<<s.sts[i].type;
+            s.sts[i].combined = index;
+            addname(key, s, s.sts[i], true);
+            break;
+        }
+    }
+    key.add('\0');
+    t.t = textures.access(key.getbuf());
+    if(t.t) return;
+    int compress = 0, wrap = 0;
+    ImageData ts;
+    if(!texturedata(ts, NULL, &t, true, &compress, &wrap)) { t.t = notexture; return; }
+    if(!ts.compressed) switch(t.type)
+    {
+        case TEX_DIFFUSE:
+        case TEX_NORMAL:
+            loopv(s.sts)
+            {
+                Slot::Tex &a = s.sts[i];
+                if(a.combined!=index) continue;
+                ImageData as;
+                if(!texturedata(as, NULL, &a)) continue;
+                //if(ts.bpp!=4) forcergbaimage(ts);
+                if(as.w!=ts.w || as.h!=ts.h) scaleimage(as, ts.w, ts.h);
+                switch(a.type)
+                {
+                    case TEX_SPEC: mergespec(ts, as); break;
+                    case TEX_DEPTH: mergedepth(ts, as); break;
+                    case TEX_ALPHA: mergealpha(ts, as); break;
+                }
+                break; // only one combination
+            }
+            break;
+    }
+    t.t = newtexture(NULL, key.getbuf(), ts, wrap, true, true, true, compress);
+}
+
+static Slot &loadslot(Slot &s, bool forceload)
+{
+    linkslotshader(s);
+    loopv(s.sts)
+    {
+        Slot::Tex &t = s.sts[i];
+        if(t.combined >= 0) continue;
+        switch(t.type)
+        {
+            case TEX_ENVMAP:
+                t.t = cubemapload(t.name);
+                break;
+
+            default:
+                texcombine(s, i, t, forceload);
+                break;
+        }
+    }
+    s.loaded = true;
+    return s;
+}
+
+MSlot &lookupmaterialslot(int index, bool load)
+{
+    if(materialslots[index].sts.empty() && index&MATF_INDEX) index &= ~MATF_INDEX;
+    MSlot &s = materialslots[index];
+    if(load && !s.linked)
+    {
+        if(!s.loaded) loadslot(s, true);
+        linkvslotshader(s);
+        s.linked = true;
+    }
+    return s;
+}
+
+Slot &lookupslot(int index, bool load)
+{
+    Slot &s = slots.inrange(index) ? *slots[index] : (slots.inrange(DEFAULT_GEOM) ? *slots[DEFAULT_GEOM] : dummyslot);
+    return s.loaded || !load ? s : loadslot(s, false);
+}
+
+VSlot &lookupvslot(int index, bool load)
+{
+    VSlot &s = vslots.inrange(index) && vslots[index]->slot ? *vslots[index] : (slots.inrange(DEFAULT_GEOM) && slots[DEFAULT_GEOM]->variants ? *slots[DEFAULT_GEOM]->variants : dummyvslot);
+    if(load && !s.linked)
+    {
+        if(!s.slot->loaded) loadslot(*s.slot, false);
+        linkvslotshader(s);
+        s.linked = true;
+    }
+    return s;
+}
+
+void linkslotshaders()
+{
+    loopv(slots) if(slots[i]->loaded) linkslotshader(*slots[i]);
+    loopv(vslots) if(vslots[i]->linked) linkvslotshader(*vslots[i]);
+    loopi((MATF_VOLUME|MATF_INDEX)+1) if(materialslots[i].loaded) 
+    {
+        linkslotshader(materialslots[i]);
+        linkvslotshader(materialslots[i]);
+    }
+}
+
+Texture *loadthumbnail(Slot &slot)
+{
+    if(slot.thumbnail) return slot.thumbnail;
+    if(!slot.variants)
+    {
+        slot.thumbnail = notexture;
+        return slot.thumbnail;
+    }
+    VSlot &vslot = *slot.variants;
+    linkslotshader(slot, false);
+    linkvslotshader(vslot, false);
+    vector<char> name;
+    if(vslot.colorscale == vec(1, 1, 1)) addname(name, slot, slot.sts[0], false, "<thumbnail>");
+    else
+    {
+        defformatstring(prefix, "<thumbnail:%.2f/%.2f/%.2f>", vslot.colorscale.x, vslot.colorscale.y, vslot.colorscale.z);
+        addname(name, slot, slot.sts[0], false, prefix);
+    }
+    int glow = -1;
+    if(slot.texmask&(1<<TEX_GLOW)) 
+    { 
+        loopvj(slot.sts) if(slot.sts[j].type==TEX_GLOW) { glow = j; break; } 
+        if(glow >= 0) 
+        {
+            defformatstring(prefix, "<glow:%.2f/%.2f/%.2f>", vslot.glowcolor.x, vslot.glowcolor.y, vslot.glowcolor.z); 
+            addname(name, slot, slot.sts[glow], true, prefix);
+        }
+    }
+    VSlot *layer = vslot.layer ? &lookupvslot(vslot.layer, false) : NULL;
+    if(layer) 
+    {
+        if(layer->colorscale == vec(1, 1, 1)) addname(name, *layer->slot, layer->slot->sts[0], true, "<layer>");
+        else
+        {
+            defformatstring(prefix, "<layer:%.2f/%.2f/%.2f>", vslot.colorscale.x, vslot.colorscale.y, vslot.colorscale.z);
+            addname(name, *layer->slot, layer->slot->sts[0], true, prefix);
+        }
+    }
+    name.add('\0');
+    Texture *t = textures.access(path(name.getbuf()));
+    if(t) slot.thumbnail = t;
+    else
+    {
+        ImageData s, g, l;
+        texturedata(s, NULL, &slot.sts[0], false);
+        if(glow >= 0) texturedata(g, NULL, &slot.sts[glow], false);
+        if(layer) texturedata(l, NULL, &layer->slot->sts[0], false);
+        if(!s.data) t = slot.thumbnail = notexture;
+        else
+        {
+            if(vslot.colorscale != vec(1, 1, 1)) texmad(s, vslot.colorscale, vec(0, 0, 0));
+            int xs = s.w, ys = s.h;
+            if(s.w > 64 || s.h > 64) scaleimage(s, min(s.w, 64), min(s.h, 64));
+            if(g.data)
+            {
+                if(g.w != s.w || g.h != s.h) scaleimage(g, s.w, s.h);
+                addglow(s, g, vslot.glowcolor);
+            }
+            if(l.data)
+            {
+                if(layer->colorscale != vec(1, 1, 1)) texmad(l, layer->colorscale, vec(0, 0, 0));
+                if(l.w != s.w/2 || l.h != s.h/2) scaleimage(l, s.w/2, s.h/2);
+                forcergbimage(s);
+                forcergbimage(l); 
+                uchar *dstrow = &s.data[s.pitch*l.h + s.bpp*l.w], *srcrow = l.data;
+                loop(y, l.h) 
+                {
+                    for(uchar *dst = dstrow, *src = srcrow, *end = &srcrow[l.w*l.bpp]; src < end; dst += s.bpp, src += l.bpp)
+                        loopk(3) dst[k] = src[k]; 
+                    dstrow += s.pitch;
+                    srcrow += l.pitch;
+                }
+            }
+            if(s.bpp < 3) forcergbimage(s);
+            t = newtexture(NULL, name.getbuf(), s, 0, false, false, true);
+            t->xs = xs;
+            t->ys = ys;
+            slot.thumbnail = t;
+        }
+    }
+    return t;
+}
+
+void loadlayermasks()
+{
+    loopv(slots)
+    {
+        Slot &slot = *slots[i];
+        if(slot.loaded && slot.layermaskname && !slot.layermask) 
+        {
+            slot.layermask = new ImageData;
+            texturedata(*slot.layermask, slot.layermaskname);
+            if(!slot.layermask->data) DELETEP(slot.layermask);
+        }
+    }
+}
+
+// environment mapped reflections
+
+void forcecubemapload(GLuint tex)
+{
+    extern int ati_cubemap_bug;
+    if(!ati_cubemap_bug || !tex) return;
+
+    SETSHADER(cubemap);
+    GLenum depthtest = glIsEnabled(GL_DEPTH_TEST), blend = glIsEnabled(GL_BLEND);
+    if(depthtest) glDisable(GL_DEPTH_TEST);
+    glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
+    if(!blend) glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    gle::defvertex(2);
+    gle::deftexcoord0(3);
+    gle::defcolor(4);
+    gle::begin(GL_LINES);
+    loopi(2)
+    {
+        gle::attribf(i*1e-3f, 0);
+        gle::attribf(0, 0, 1);
+        gle::attribf(1, 1, 1, 0);
+    }
+    gle::end();
+    if(!blend) glDisable(GL_BLEND);
+    if(depthtest) glEnable(GL_DEPTH_TEST);
+}
+
+extern const cubemapside cubemapsides[6] =
+{
+    { GL_TEXTURE_CUBE_MAP_NEGATIVE_X, "lf", true,  true,  true  },
+    { GL_TEXTURE_CUBE_MAP_POSITIVE_X, "rt", false, false, true  },
+    { GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, "ft", true,  false, false },
+    { GL_TEXTURE_CUBE_MAP_POSITIVE_Y, "bk", false, true,  false },
+    { GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, "dn", false, false, true  },
+    { GL_TEXTURE_CUBE_MAP_POSITIVE_Z, "up", false, false, true  },
+};
+
+VARFP(envmapsize, 4, 7, 10, setupmaterials());
+
+Texture *cubemaploadwildcard(Texture *t, const char *name, bool mipit, bool msg, bool transient = false)
+{
+    string tname;
+    if(!name) copystring(tname, t->name);
+    else
+    {
+        copystring(tname, name);
+        t = textures.access(path(tname));
+        if(t) 
+        {
+            if(!transient && t->type&Texture::TRANSIENT) t->type &= ~Texture::TRANSIENT;
+            return t;
+        }
+    }
+    char *wildcard = strchr(tname, '*');
+    ImageData surface[6];
+    string sname;
+    if(!wildcard) copystring(sname, tname);
+    int tsize = 0, compress = 0;
+    loopi(6)
+    {
+        if(wildcard)
+        {
+            copystring(sname, stringslice(tname, wildcard));
+            concatstring(sname, cubemapsides[i].name);
+            concatstring(sname, wildcard+1);
+        }
+        ImageData &s = surface[i];
+        texturedata(s, sname, NULL, msg, &compress);
+        if(!s.data) return NULL;
+        if(s.w != s.h)
+        {
+            if(msg) conoutf(CON_ERROR, "cubemap texture %s does not have square size", sname);
+            return NULL;
+        }
+        if(s.compressed ? s.compressed!=surface[0].compressed || s.w!=surface[0].w || s.h!=surface[0].h || s.levels!=surface[0].levels : surface[0].compressed || s.bpp!=surface[0].bpp)
+        {
+            if(msg) conoutf(CON_ERROR, "cubemap texture %s doesn't match other sides' format", sname);
+            return NULL;
+        }
+        tsize = max(tsize, max(s.w, s.h));
+    }
+    if(name)
+    {
+        char *key = newstring(tname);
+        t = &textures[key];
+        t->name = key;
+    }
+    t->type = Texture::CUBEMAP;
+    if(transient) t->type |= Texture::TRANSIENT;
+    GLenum format;
+    if(surface[0].compressed)
+    {
+        format = uncompressedformat(surface[0].compressed);
+        t->bpp = formatsize(format);
+        t->type |= Texture::COMPRESSED;
+    }
+    else 
+    {
+        format = texformat(surface[0].bpp, true);
+        t->bpp = surface[0].bpp;
+        if(hasTRG && !hasTSW && swizzlemask(format))
+        {
+            loopi(6) swizzleimage(surface[i]);
+            format = texformat(surface[0].bpp, true);
+            t->bpp = surface[0].bpp;
+        }
+    }
+    if(alphaformat(format)) t->type |= Texture::ALPHA;
+    t->mipmap = mipit;
+    t->clamp = 3;
+    t->xs = t->ys = tsize;
+    t->w = t->h = min(1<<envmapsize, tsize);
+    resizetexture(t->w, t->h, mipit, false, GL_TEXTURE_CUBE_MAP, compress, t->w, t->h);
+    GLenum component = format;
+    if(!surface[0].compressed)
+    {
+        component = compressedformat(format, t->w, t->h, compress);
+        switch(component)
+        {
+            case GL_RGB: component = GL_RGB5; break;
+        }
+    }
+    glGenTextures(1, &t->id);
+    loopi(6)
+    {
+        ImageData &s = surface[i];
+        const cubemapside &side = cubemapsides[i];
+        texreorient(s, side.flipx, side.flipy, side.swapxy);
+        if(s.compressed)
+        {
+            int w = s.w, h = s.h, levels = s.levels, level = 0;
+            uchar *data = s.data;
+            while(levels > 1 && (w > t->w || h > t->h))
+            {
+                data += s.calclevelsize(level++);
+                levels--;
+                if(w > 1) w /= 2;
+                if(h > 1) h /= 2;
+            }
+            createcompressedtexture(t->id, w, h, data, s.align, s.bpp, levels, i ? -1 : 3, mipit ? 2 : 1, s.compressed, side.target, true);
+        }
+        else
+        {
+            createtexture(t->id, t->w, t->h, s.data, i ? -1 : 3, mipit ? 2 : 1, component, side.target, s.w, s.h, s.pitch, false, format, true);
+        }
+    }
+    forcecubemapload(t->id);
+    return t;
+}
+
+Texture *cubemapload(const char *name, bool mipit, bool msg, bool transient)
+{
+    string pname;
+    copystring(pname, makerelpath("packages", name));
+    path(pname);
+    Texture *t = NULL;
+    if(!strchr(pname, '*'))
+    {
+        defformatstring(jpgname, "%s_*.jpg", pname);
+        t = cubemaploadwildcard(NULL, jpgname, mipit, false, transient);
+        if(!t)
+        {
+            defformatstring(pngname, "%s_*.png", pname);
+            t = cubemaploadwildcard(NULL, pngname, mipit, false, transient);
+            if(!t && msg) conoutf(CON_ERROR, "could not load envmap %s", name);
+        }
+    }
+    else t = cubemaploadwildcard(NULL, pname, mipit, msg, transient);
+    return t;
+}
+
+VARR(envmapradius, 0, 128, 10000);
+VARR(envmapbb, 0, 0, 1);
+
+struct envmap
+{
+    int radius, size, blur;
+    vec o;
+    GLuint tex;
+
+    envmap() : radius(-1), size(0), blur(0), o(0, 0, 0), tex(0) {}
+
+    void clear()
+    {
+        if(tex) { glDeleteTextures(1, &tex); tex = 0; }
+    }
+};  
+
+static vector<envmap> envmaps;
+static Texture *skyenvmap = NULL;
+
+void clearenvmaps()
+{
+    if(skyenvmap)
+    {
+        if(skyenvmap->type&Texture::TRANSIENT) cleanuptexture(skyenvmap);
+        skyenvmap = NULL;
+    }
+    loopv(envmaps) envmaps[i].clear();
+    envmaps.shrink(0);
+}
+
+VAR(aaenvmap, 0, 2, 4);
+
+GLuint genenvmap(const vec &o, int envmapsize, int blur, bool onlysky)
+{
+    int rendersize = 1<<(envmapsize+aaenvmap), sizelimit = min(hwcubetexsize, min(screenw, screenh));
+    if(maxtexsize) sizelimit = min(sizelimit, maxtexsize);
+    while(rendersize > sizelimit) rendersize /= 2;
+    int texsize = min(rendersize, 1<<envmapsize);
+    if(!aaenvmap) rendersize = texsize;
+    GLuint tex;
+    glGenTextures(1, &tex);
+    glViewport(0, 0, rendersize, rendersize);
+    float yaw = 0, pitch = 0;
+    uchar *pixels = new uchar[3*rendersize*rendersize*2];
+    glPixelStorei(GL_PACK_ALIGNMENT, texalign(pixels, rendersize, 3));
+    loopi(6)
+    {
+        const cubemapside &side = cubemapsides[i];
+        switch(side.target)
+        {
+            case GL_TEXTURE_CUBE_MAP_NEGATIVE_X: // lf
+                yaw = 90; pitch = 0; break;
+            case GL_TEXTURE_CUBE_MAP_POSITIVE_X: // rt
+                yaw = 270; pitch = 0; break;
+            case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y: // ft
+                yaw = 180; pitch = 0; break;
+            case GL_TEXTURE_CUBE_MAP_POSITIVE_Y: // bk
+                yaw = 0; pitch = 0; break;
+            case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: // dn
+                yaw = 270; pitch = -90; break;
+            case GL_TEXTURE_CUBE_MAP_POSITIVE_Z: // up
+                yaw = 270; pitch = 90; break;
+        }
+        glFrontFace((side.flipx==side.flipy)!=side.swapxy ? GL_CW : GL_CCW);
+        drawcubemap(rendersize, o, yaw, pitch, side, onlysky);
+        uchar *src = pixels, *dst = &pixels[3*rendersize*rendersize];
+        glReadPixels(0, 0, rendersize, rendersize, GL_RGB, GL_UNSIGNED_BYTE, src);
+        if(rendersize > texsize)
+        {
+            scaletexture(src, rendersize, rendersize, 3, 3*rendersize, dst, texsize, texsize);
+            swap(src, dst);
+        }
+        if(blur > 0)
+        {
+            blurtexture(blur, 3, texsize, texsize, dst, src);
+            swap(src, dst);
+        }
+        createtexture(tex, texsize, texsize, src, 3, 2, GL_RGB5, side.target);
+    }
+    glFrontFace(GL_CW);
+    delete[] pixels;
+    glViewport(0, 0, screenw, screenh);
+    clientkeepalive();
+    forcecubemapload(tex);
+    return tex;
+}
+
+void initenvmaps()
+{
+    clearenvmaps();
+    skyenvmap = NULL;
+    if(shouldrenderskyenvmap()) envmaps.add().size = 1;
+    else if(skybox[0]) skyenvmap = cubemapload(skybox, true, false, true);
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        const extentity &ent = *ents[i];
+        if(ent.type != ET_ENVMAP) continue;
+        envmap &em = envmaps.add();
+        em.radius = ent.attr1 ? clamp(int(ent.attr1), 0, 10000) : envmapradius;
+        em.size = ent.attr2 ? clamp(int(ent.attr2), 4, 9) : 0;
+        em.blur = ent.attr3 ? clamp(int(ent.attr3), 1, 2) : 0; 
+        em.o = ent.o;
+    }
+}
+
+void genenvmaps()
+{
+    if(envmaps.empty()) return;
+    renderprogress(0, "generating environment maps...");
+    int lastprogress = SDL_GetTicks();
+    loopv(envmaps)
+    {
+        envmap &em = envmaps[i];
+        em.tex = genenvmap(em.o, em.size ? min(em.size, envmapsize) : envmapsize, em.blur, em.radius < 0);
+        if(renderedframe) continue;
+        int millis = SDL_GetTicks();
+        if(millis - lastprogress >= 250)
+        {
+            renderprogress(float(i+1)/envmaps.length(), "generating environment maps...", 0, true);
+            lastprogress = millis;
+        }
+    }
+}
+
+ushort closestenvmap(const vec &o)
+{
+    ushort minemid = EMID_SKY;
+    float mindist = 1e16f;
+    loopv(envmaps)
+    {
+        envmap &em = envmaps[i];
+        float dist;
+        if(envmapbb)
+        {
+            if(!o.insidebb(vec(em.o).sub(em.radius), vec(em.o).add(em.radius))) continue;
+            dist = em.o.dist(o);
+        }
+        else
+        {
+            dist = em.o.dist(o);
+            if(dist > em.radius) continue;
+        }
+        if(dist < mindist)
+        {
+            minemid = EMID_RESERVED + i;
+            mindist = dist;
+        }
+    }
+    return minemid;
+}
+
+ushort closestenvmap(int orient, const ivec &co, int size)
+{
+    vec loc(co);
+    int dim = dimension(orient);
+    if(dimcoord(orient)) loc[dim] += size;
+    loc[R[dim]] += size/2;
+    loc[C[dim]] += size/2;
+    return closestenvmap(loc);
+}
+
+static inline GLuint lookupskyenvmap()
+{
+    return envmaps.length() && envmaps[0].radius < 0 ? envmaps[0].tex : (skyenvmap ? skyenvmap->id : 0);
+}
+
+GLuint lookupenvmap(Slot &slot)
+{
+    loopv(slot.sts) if(slot.sts[i].type==TEX_ENVMAP && slot.sts[i].t) return slot.sts[i].t->id;
+    return lookupskyenvmap();
+}
+
+GLuint lookupenvmap(ushort emid)
+{
+    if(emid==EMID_SKY || emid==EMID_CUSTOM) return skyenvmap ? skyenvmap->id : 0;
+    if(emid==EMID_NONE || !envmaps.inrange(emid-EMID_RESERVED)) return 0;
+    GLuint tex = envmaps[emid-EMID_RESERVED].tex;
+    return tex ? tex : lookupskyenvmap();
+}
+
+void cleanuptexture(Texture *t)
+{
+    DELETEA(t->alphamask);
+    if(t->id) { glDeleteTextures(1, &t->id); t->id = 0; }
+    if(t->type&Texture::TRANSIENT) textures.remove(t->name); 
+}
+
+void cleanuptextures()
+{
+    cleanupmipmaps();
+    clearenvmaps();
+    loopv(slots) slots[i]->cleanup();
+    loopv(vslots) vslots[i]->cleanup();
+    loopi((MATF_VOLUME|MATF_INDEX)+1) materialslots[i].cleanup();
+    enumerate(textures, Texture, tex, cleanuptexture(&tex));
+}
+
+bool reloadtexture(const char *name)
+{
+    Texture *t = textures.access(path(name, true));
+    if(t) return reloadtexture(*t);
+    return true;
+}
+
+bool reloadtexture(Texture &tex)
+{
+    if(tex.id) return true;
+    switch(tex.type&Texture::TYPE)
+    {
+        case Texture::IMAGE:
+        {
+            int compress = 0;
+            ImageData s;
+            if(!texturedata(s, tex.name, NULL, true, &compress) || !newtexture(&tex, NULL, s, tex.clamp, tex.mipmap, false, false, compress)) return false;
+            break;
+        }
+
+        case Texture::CUBEMAP:
+            if(!cubemaploadwildcard(&tex, NULL, tex.mipmap, true)) return false;
+            break;
+    }    
+    return true;
+}
+
+void reloadtex(char *name)
+{
+    Texture *t = textures.access(path(name, true));
+    if(!t) { conoutf(CON_ERROR, "texture %s is not loaded", name); return; }
+    if(t->type&Texture::TRANSIENT) { conoutf(CON_ERROR, "can't reload transient texture %s", name); return; }
+    DELETEA(t->alphamask);
+    Texture oldtex = *t;
+    t->id = 0;
+    if(!reloadtexture(*t))
+    {
+        if(t->id) glDeleteTextures(1, &t->id);
+        *t = oldtex;
+        conoutf(CON_ERROR, "failed to reload texture %s", name);
+    }
+}
+
+COMMAND(reloadtex, "s");
+
+void reloadtextures()
+{
+    int reloaded = 0;
+    enumerate(textures, Texture, tex, 
+    {
+        loadprogress = float(++reloaded)/textures.numelems;
+        reloadtexture(tex);
+    });
+    loadprogress = 0;
+}
+
+enum
+{
+    DDSD_CAPS                  = 0x00000001, 
+    DDSD_HEIGHT                = 0x00000002,
+    DDSD_WIDTH                 = 0x00000004, 
+    DDSD_PITCH                 = 0x00000008, 
+    DDSD_PIXELFORMAT           = 0x00001000, 
+    DDSD_MIPMAPCOUNT           = 0x00020000, 
+    DDSD_LINEARSIZE            = 0x00080000, 
+    DDSD_BACKBUFFERCOUNT       = 0x00800000, 
+    DDPF_ALPHAPIXELS           = 0x00000001, 
+    DDPF_FOURCC                = 0x00000004, 
+    DDPF_INDEXED               = 0x00000020, 
+    DDPF_ALPHA                 = 0x00000002,
+    DDPF_RGB                   = 0x00000040, 
+    DDPF_COMPRESSED            = 0x00000080,
+    DDPF_LUMINANCE             = 0x00020000,
+    DDSCAPS_COMPLEX            = 0x00000008, 
+    DDSCAPS_TEXTURE            = 0x00001000, 
+    DDSCAPS_MIPMAP             = 0x00400000, 
+    DDSCAPS2_CUBEMAP           = 0x00000200, 
+    DDSCAPS2_CUBEMAP_POSITIVEX = 0x00000400, 
+    DDSCAPS2_CUBEMAP_NEGATIVEX = 0x00000800, 
+    DDSCAPS2_CUBEMAP_POSITIVEY = 0x00001000, 
+    DDSCAPS2_CUBEMAP_NEGATIVEY = 0x00002000, 
+    DDSCAPS2_CUBEMAP_POSITIVEZ = 0x00004000, 
+    DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000, 
+    DDSCAPS2_VOLUME            = 0x00200000,
+    FOURCC_DXT1                = 0x31545844,
+    FOURCC_DXT2                = 0x32545844,
+    FOURCC_DXT3                = 0x33545844,
+    FOURCC_DXT4                = 0x34545844,
+    FOURCC_DXT5                = 0x35545844,
+    FOURCC_ATI1                = 0x31495441,
+    FOURCC_ATI2                = 0x32495441
+};
+
+struct DDCOLORKEY { uint dwColorSpaceLowValue, dwColorSpaceHighValue; };
+struct DDPIXELFORMAT
+{
+    uint dwSize, dwFlags, dwFourCC;
+    union { uint dwRGBBitCount, dwYUVBitCount, dwZBufferBitDepth, dwAlphaBitDepth, dwLuminanceBitCount, dwBumpBitCount, dwPrivateFormatBitCount; };
+    union { uint dwRBitMask, dwYBitMask, dwStencilBitDepth, dwLuminanceBitMask, dwBumpDuBitMask, dwOperations; };
+    union { uint dwGBitMask, dwUBitMask, dwZBitMask, dwBumpDvBitMask; struct { ushort wFlipMSTypes, wBltMSTypes; } MultiSampleCaps; };
+    union { uint dwBBitMask, dwVBitMask, dwStencilBitMask, dwBumpLuminanceBitMask; };
+    union { uint dwRGBAlphaBitMask, dwYUVAlphaBitMask, dwLuminanceAlphaBitMask, dwRGBZBitMask, dwYUVZBitMask; };
+
+};
+struct DDSCAPS2 { uint dwCaps, dwCaps2, dwCaps3, dwCaps4; };
+struct DDSURFACEDESC2
+{
+    uint dwSize, dwFlags, dwHeight, dwWidth; 
+    union { int lPitch; uint dwLinearSize; };
+    uint dwBackBufferCount; 
+    union { uint dwMipMapCount, dwRefreshRate, dwSrcVBHandle; };
+    uint dwAlphaBitDepth, dwReserved, lpSurface; 
+    union { DDCOLORKEY ddckCKDestOverlay; uint dwEmptyFaceColor; };
+    DDCOLORKEY ddckCKDestBlt, ddckCKSrcOverlay, ddckCKSrcBlt;     
+    union { DDPIXELFORMAT ddpfPixelFormat; uint dwFVF; };
+    DDSCAPS2 ddsCaps;  
+    uint dwTextureStage;   
+};
+
+#define DECODEDDS(name, dbpp, initblock, writeval, nextval) \
+static void name(ImageData &s) \
+{ \
+    ImageData d(s.w, s.h, dbpp); \
+    uchar *dst = d.data; \
+    const uchar *src = s.data; \
+    for(int by = 0; by < s.h; by += s.align) \
+    { \
+        for(int bx = 0; bx < s.w; bx += s.align, src += s.bpp) \
+        { \
+            int maxy = min(d.h - by, s.align), maxx = min(d.w - bx, s.align); \
+            initblock; \
+            loop(y, maxy) \
+            { \
+                int x; \
+                for(x = 0; x < maxx; ++x) \
+                { \
+                    writeval; \
+                    nextval; \
+                    dst += d.bpp; \
+                }  \
+                for(; x < s.align; ++x) { nextval; } \
+                dst += d.pitch - maxx*d.bpp; \
+            } \
+            dst += maxx*d.bpp - maxy*d.pitch; \
+        } \
+        dst += (s.align-1)*d.pitch; \
+    } \
+    s.replace(d); \
+}
+
+DECODEDDS(decodedxt1, s.compressed == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 4 : 3,
+    ushort color0 = lilswap(*(const ushort *)src);
+    ushort color1 = lilswap(*(const ushort *)&src[2]);
+    uint bits = lilswap(*(const uint *)&src[4]);
+    bvec4 rgba[4];
+    rgba[0] = bvec4(bvec::from565(color0), 0xFF);
+    rgba[1] = bvec4(bvec::from565(color1), 0xFF);
+    if(color0 > color1)
+    {
+        rgba[2].lerp(rgba[0], rgba[1], 2, 1, 3);
+        rgba[3].lerp(rgba[0], rgba[1], 1, 2, 3);
+    }
+    else
+    {
+        rgba[2].lerp(rgba[0], rgba[1], 1, 1, 2);
+        rgba[3] = bvec4(0, 0, 0, 0);
+    }
+,
+    memcpy(dst, rgba[bits&3].v, d.bpp);
+,
+    bits >>= 2;
+);
+
+DECODEDDS(decodedxt3, 4,
+    ullong alpha = lilswap(*(const ullong *)src);
+    ushort color0 = lilswap(*(const ushort *)&src[8]);
+    ushort color1 = lilswap(*(const ushort *)&src[10]);
+    uint bits = lilswap(*(const uint *)&src[12]);
+    bvec rgb[4];
+    rgb[0] = bvec::from565(color0);
+    rgb[1] = bvec::from565(color1);
+    rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3);
+    rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3);
+,
+    memcpy(dst, rgb[bits&3].v, 3);
+    dst[3] = ((alpha&0xF)*1088 + 32) >> 6;
+,
+    bits >>= 2;
+    alpha >>= 4;
+);
+
+static inline void decodealpha(uchar alpha0, uchar alpha1, uchar alpha[8])
+{
+    alpha[0] = alpha0;
+    alpha[1] = alpha1;
+    if(alpha0 > alpha1)
+    {
+        alpha[2] = (6*alpha0 + alpha1)/7;
+        alpha[3] = (5*alpha0 + 2*alpha1)/7;
+        alpha[4] = (4*alpha0 + 3*alpha1)/7;
+        alpha[5] = (3*alpha0 + 4*alpha1)/7;
+        alpha[6] = (2*alpha0 + 5*alpha1)/7;
+        alpha[7] = (alpha0 + 6*alpha1)/7;
+    }
+    else
+    {
+        alpha[2] = (4*alpha0 + alpha1)/5;
+        alpha[3] = (3*alpha0 + 2*alpha1)/5;
+        alpha[4] = (2*alpha0 + 3*alpha1)/5;
+        alpha[5] = (alpha0 + 4*alpha1)/5;
+        alpha[6] = 0;
+        alpha[7] = 0xFF;
+    }
+}
+
+DECODEDDS(decodedxt5, 4,
+    uchar alpha[8];
+    decodealpha(src[0], src[1], alpha);
+    ullong alphabits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+    ushort color0 = lilswap(*(const ushort *)&src[8]);
+    ushort color1 = lilswap(*(const ushort *)&src[10]);
+    uint bits = lilswap(*(const uint *)&src[12]);
+    bvec rgb[4];
+    rgb[0] = bvec::from565(color0);
+    rgb[1] = bvec::from565(color1);
+    rgb[2].lerp(rgb[0], rgb[1], 2, 1, 3);
+    rgb[3].lerp(rgb[0], rgb[1], 1, 2, 3);
+,
+    memcpy(dst, rgb[bits&3].v, 3);
+    dst[3] = alpha[alphabits&7];
+,
+    bits >>= 2;
+    alphabits >>= 3;
+);
+
+DECODEDDS(decodergtc1, 1,
+    uchar red[8];
+    decodealpha(src[0], src[1], red);
+    ullong redbits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+,
+    dst[0] = red[redbits&7];
+,
+    redbits >>= 3;
+);
+
+DECODEDDS(decodergtc2, 2,
+    uchar red[8];
+    decodealpha(src[0], src[1], red);
+    ullong redbits = lilswap(*(const ushort *)&src[2]) + ((ullong)lilswap(*(const uint *)&src[4]) << 16);
+    uchar green[8];
+    decodealpha(src[8], src[9], green);
+    ullong greenbits = lilswap(*(const ushort *)&src[10]) + ((ullong)lilswap(*(const uint *)&src[12]) << 16);
+,
+    dst[0] = red[redbits&7];
+    dst[1] = green[greenbits&7];
+,
+    redbits >>= 3;
+    greenbits >>= 3;
+);
+
+bool loaddds(const char *filename, ImageData &image, int force)
+{
+    stream *f = openfile(filename, "rb");
+    if(!f) return false;
+    GLenum format = GL_FALSE;
+    uchar magic[4];
+    if(f->read(magic, 4) != 4 || memcmp(magic, "DDS ", 4)) { delete f; return false; }
+    DDSURFACEDESC2 d;
+    if(f->read(&d, sizeof(d)) != sizeof(d)) { delete f; return false; }
+    lilswap((uint *)&d, sizeof(d)/sizeof(uint));
+    if(d.dwSize != sizeof(DDSURFACEDESC2) || d.ddpfPixelFormat.dwSize != sizeof(DDPIXELFORMAT)) { delete f; return false; }
+    bool supported = false;
+    if(d.ddpfPixelFormat.dwFlags & DDPF_FOURCC)
+    {
+        switch(d.ddpfPixelFormat.dwFourCC)
+        {
+            case FOURCC_DXT1:
+                if((supported = hasS3TC) || force) format = d.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS ? GL_COMPRESSED_RGBA_S3TC_DXT1_EXT : GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
+                break;
+            case FOURCC_DXT2:
+            case FOURCC_DXT3:
+                if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
+                break;
+            case FOURCC_DXT4:
+            case FOURCC_DXT5:
+                if((supported = hasS3TC) || force) format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+                break;
+            case FOURCC_ATI1:
+                if((supported = hasRGTC) || force) format = GL_COMPRESSED_RED_RGTC1;
+                else if((supported = hasLATC)) format = GL_COMPRESSED_LUMINANCE_LATC1_EXT;
+                break;
+            case FOURCC_ATI2:
+                if((supported = hasRGTC) || force) format = GL_COMPRESSED_RG_RGTC2;
+                else if((supported = hasLATC)) format = GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT;
+                break;
+        }
+    }
+    if(!format || (!supported && !force)) { delete f; return false; }
+    if(dbgdds) conoutf(CON_DEBUG, "%s: format 0x%X, %d x %d, %d mipmaps", filename, format, d.dwWidth, d.dwHeight, d.dwMipMapCount);
+    int bpp = 0;
+    switch(format)
+    {
+        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: 
+        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: bpp = 8; break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: 
+        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: bpp = 16; break;
+        case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+        case GL_COMPRESSED_RED_RGTC1: bpp = 8; break;
+        case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+        case GL_COMPRESSED_RG_RGTC2: bpp = 16; break;
+    }
+    image.setdata(NULL, d.dwWidth, d.dwHeight, bpp, !supported || force > 0 ? 1 : d.dwMipMapCount, 4, format);
+    size_t size = image.calcsize();
+    if(f->read(image.data, size) != size) { delete f; image.cleanup(); return false; }
+    delete f;
+    if(!supported || force > 0) switch(format)
+    {
+        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT:
+        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT:
+            decodedxt1(image);
+            break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT:
+            decodedxt3(image);
+            break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT:
+            decodedxt5(image);
+            break;
+        case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+        case GL_COMPRESSED_RED_RGTC1:
+            decodergtc1(image);
+            break;
+        case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+        case GL_COMPRESSED_RG_RGTC2:
+            decodergtc2(image);
+            break;
+    }
+    return true;
+}
+
+void gendds(char *infile, char *outfile)
+{
+    if(!hasS3TC || usetexcompress <= 1) { conoutf(CON_ERROR, "OpenGL driver does not support S3TC texture compression"); return; }
+
+    glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
+
+    defformatstring(cfile, "<compress>%s", infile);
+    extern void reloadtex(char *name);
+    Texture *t = textures.access(path(cfile));
+    if(t) reloadtex(cfile);
+    t = textureload(cfile);
+    if(t==notexture) { conoutf(CON_ERROR, "failed loading %s", infile); return; }
+
+    glBindTexture(GL_TEXTURE_2D, t->id);
+    GLint compressed = 0, format = 0, width = 0, height = 0; 
+    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed);
+    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format);
+    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
+    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
+
+    if(!compressed) { conoutf(CON_ERROR, "failed compressing %s", infile); return; }
+    int fourcc = 0;
+    switch(format)
+    {
+        case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: fourcc = FOURCC_DXT1; conoutf("compressed as DXT1"); break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: fourcc = FOURCC_DXT1; conoutf("compressed as DXT1a"); break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: fourcc = FOURCC_DXT3; conoutf("compressed as DXT3"); break;
+        case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: fourcc = FOURCC_DXT5; conoutf("compressed as DXT5"); break;
+        case GL_COMPRESSED_LUMINANCE_LATC1_EXT:
+        case GL_COMPRESSED_RED_RGTC1: fourcc = FOURCC_ATI1; conoutf("compressed as ATI1"); break;
+        case GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
+        case GL_COMPRESSED_RG_RGTC2: fourcc = FOURCC_ATI2; conoutf("compressed as ATI2"); break;
+        default:
+            conoutf(CON_ERROR, "failed compressing %s: unknown format: 0x%X", infile, format); break;
+            return;
+    }
+
+    if(!outfile[0])
+    {
+        static string buf;
+        copystring(buf, infile);
+        int len = strlen(buf);
+        if(len > 4 && buf[len-4]=='.') memcpy(&buf[len-4], ".dds", 4);
+        else concatstring(buf, ".dds");
+        outfile = buf;
+    }
+    
+    stream *f = openfile(path(outfile, true), "wb");
+    if(!f) { conoutf(CON_ERROR, "failed writing to %s", outfile); return; } 
+
+    int csize = 0;
+    for(int lw = width, lh = height, level = 0;;)
+    {
+        GLint size = 0;
+        glGetTexLevelParameteriv(GL_TEXTURE_2D, level++, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size);
+        csize += size;
+        if(max(lw, lh) <= 1) break;
+        if(lw > 1) lw /= 2;
+        if(lh > 1) lh /= 2;
+    }
+
+    DDSURFACEDESC2 d;
+    memset(&d, 0, sizeof(d));
+    d.dwSize = sizeof(DDSURFACEDESC2);
+    d.dwWidth = width;
+    d.dwHeight = height;
+    d.dwLinearSize = csize;
+    d.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE | DDSD_MIPMAPCOUNT;
+    d.ddsCaps.dwCaps = DDSCAPS_TEXTURE | DDSCAPS_COMPLEX | DDSCAPS_MIPMAP;
+    d.ddpfPixelFormat.dwSize = sizeof(DDPIXELFORMAT);
+    d.ddpfPixelFormat.dwFlags = DDPF_FOURCC | (alphaformat(uncompressedformat(format)) ? DDPF_ALPHAPIXELS : 0);
+    d.ddpfPixelFormat.dwFourCC = fourcc;
+   
+    uchar *data = new uchar[csize], *dst = data;
+    for(int lw = width, lh = height;;)
+    {
+        GLint size;
+        glGetTexLevelParameteriv(GL_TEXTURE_2D, d.dwMipMapCount, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &size);
+        glGetCompressedTexImage_(GL_TEXTURE_2D, d.dwMipMapCount++, dst);
+        dst += size;
+        if(max(lw, lh) <= 1) break;
+        if(lw > 1) lw /= 2;
+        if(lh > 1) lh /= 2;
+    }
+
+    lilswap((uint *)&d, sizeof(d)/sizeof(uint));
+
+    f->write("DDS ", 4);
+    f->write(&d, sizeof(d));
+    f->write(data, csize);
+    delete f;
+    
+    delete[] data;
+
+    conoutf("wrote DDS file %s", outfile);
+
+    setuptexcompress();
+}
+COMMAND(gendds, "ss");
+
+void writepngchunk(stream *f, const char *type, uchar *data = NULL, uint len = 0)
+{
+    f->putbig<uint>(len);
+    f->write(type, 4);
+    f->write(data, len);
+
+    uint crc = crc32(0, Z_NULL, 0);
+    crc = crc32(crc, (const Bytef *)type, 4);
+    if(data) crc = crc32(crc, data, len);
+    f->putbig<uint>(crc);
+}
+
+VARP(compresspng, 0, 9, 9);
+
+void savepng(const char *filename, ImageData &image, bool flip)
+{
+    uchar ctype = 0;
+    switch(image.bpp)
+    {
+        case 1: ctype = 0; break;
+        case 2: ctype = 4; break;
+        case 3: ctype = 2; break;
+        case 4: ctype = 6; break;
+        default: conoutf(CON_ERROR, "failed saving png to %s", filename); return;
+    }
+    stream *f = openfile(filename, "wb");
+    if(!f) { conoutf(CON_ERROR, "could not write to %s", filename); return; }
+
+    uchar signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
+    f->write(signature, sizeof(signature));
+
+    struct pngihdr
+    {
+        uint width, height;
+        uchar bitdepth, colortype, compress, filter, interlace;
+    } ihdr = { bigswap<uint>(image.w), bigswap<uint>(image.h), 8, ctype, 0, 0, 0 };
+    writepngchunk(f, "IHDR", (uchar *)&ihdr, 13);
+
+    stream::offset idat = f->tell();
+    uint len = 0;
+    f->write("\0\0\0\0IDAT", 8);
+    uint crc = crc32(0, Z_NULL, 0);
+    crc = crc32(crc, (const Bytef *)"IDAT", 4);
+
+    z_stream z;
+    z.zalloc = NULL;
+    z.zfree = NULL;
+    z.opaque = NULL;
+
+    if(deflateInit(&z, compresspng) != Z_OK)
+        goto error;
+
+    uchar buf[1<<12];
+    z.next_out = (Bytef *)buf;
+    z.avail_out = sizeof(buf);
+
+    loopi(image.h)
+    {
+        uchar filter = 0;
+        loopj(2)
+        {
+            z.next_in = j ? (Bytef *)image.data + (flip ? image.h-i-1 : i)*image.pitch : (Bytef *)&filter;
+            z.avail_in = j ? image.w*image.bpp : 1;
+            while(z.avail_in > 0)
+            {
+                if(deflate(&z, Z_NO_FLUSH) != Z_OK) goto cleanuperror;
+                #define FLUSHZ do { \
+                    int flush = sizeof(buf) - z.avail_out; \
+                    crc = crc32(crc, buf, flush); \
+                    len += flush; \
+                    f->write(buf, flush); \
+                    z.next_out = (Bytef *)buf; \
+                    z.avail_out = sizeof(buf); \
+                } while(0)
+                FLUSHZ;
+            }
+        }
+    }
+
+    for(;;)
+    {
+        int err = deflate(&z, Z_FINISH);
+        if(err != Z_OK && err != Z_STREAM_END) goto cleanuperror;
+        FLUSHZ;
+        if(err == Z_STREAM_END) break;
+    }
+
+    deflateEnd(&z);
+
+    f->seek(idat, SEEK_SET);
+    f->putbig<uint>(len);
+    f->seek(0, SEEK_END);
+    f->putbig<uint>(crc);
+
+    writepngchunk(f, "IEND");
+
+    delete f;
+    return;
+
+cleanuperror:
+    deflateEnd(&z);
+
+error:
+    delete f;
+
+    conoutf(CON_ERROR, "failed saving png to %s", filename);
+}
+
+struct tgaheader
+{
+    uchar  identsize;
+    uchar  cmaptype;
+    uchar  imagetype;
+    uchar  cmaporigin[2];
+    uchar  cmapsize[2];
+    uchar  cmapentrysize;
+    uchar  xorigin[2];
+    uchar  yorigin[2];
+    uchar  width[2];
+    uchar  height[2];
+    uchar  pixelsize;
+    uchar  descbyte;
+};
+
+VARP(compresstga, 0, 1, 1);
+
+void savetga(const char *filename, ImageData &image, bool flip)
+{
+    switch(image.bpp)
+    {
+        case 3: case 4: break;
+        default: conoutf(CON_ERROR, "failed saving tga to %s", filename); return;
+    }
+
+    stream *f = openfile(filename, "wb");
+    if(!f) { conoutf(CON_ERROR, "could not write to %s", filename); return; }
+
+    tgaheader hdr;
+    memset(&hdr, 0, sizeof(hdr));
+    hdr.pixelsize = image.bpp*8;
+    hdr.width[0] = image.w&0xFF;
+    hdr.width[1] = (image.w>>8)&0xFF;
+    hdr.height[0] = image.h&0xFF;
+    hdr.height[1] = (image.h>>8)&0xFF;
+    hdr.imagetype = compresstga ? 10 : 2;
+    f->write(&hdr, sizeof(hdr));
+
+    uchar buf[128*4];
+    loopi(image.h)
+    {
+        uchar *src = image.data + (flip ? i : image.h - i - 1)*image.pitch;
+        for(int remaining = image.w; remaining > 0;)
+        {
+            int raw = 1;
+            if(compresstga)
+            {
+                int run = 1;
+                for(uchar *scan = src; run < min(remaining, 128); run++)
+                {
+                    scan += image.bpp;
+                    if(src[0]!=scan[0] || src[1]!=scan[1] || src[2]!=scan[2] || (image.bpp==4 && src[3]!=scan[3])) break;
+                }
+                if(run > 1)
+                {
+                    f->putchar(0x80 | (run-1));
+                    f->putchar(src[2]); f->putchar(src[1]); f->putchar(src[0]);
+                    if(image.bpp==4) f->putchar(src[3]);
+                    src += run*image.bpp;
+                    remaining -= run;
+                    if(remaining <= 0) break;
+                }
+                for(uchar *scan = src; raw < min(remaining, 128); raw++)
+                {
+                    scan += image.bpp;
+                    if(src[0]==scan[0] && src[1]==scan[1] && src[2]==scan[2] && (image.bpp!=4 || src[3]==scan[3])) break;
+                }
+                f->putchar(raw - 1);
+            }
+            else raw = min(remaining, 128);
+            uchar *dst = buf;
+            loopj(raw)
+            {
+                dst[0] = src[2];
+                dst[1] = src[1];
+                dst[2] = src[0];
+                if(image.bpp==4) dst[3] = src[3];
+                dst += image.bpp;
+                src += image.bpp;
+            }
+            f->write(buf, raw*image.bpp);
+            remaining -= raw;
+        }
+    }
+
+    delete f;
+}
+
+enum
+{
+    IMG_BMP = 0,
+    IMG_TGA = 1,
+    IMG_PNG = 2,
+    IMG_JPG = 3,
+    NUMIMG
+};
+
+VARP(screenshotquality, 0, 97, 100);
+VARP(screenshotformat, 0, IMG_PNG, NUMIMG-1);
+
+const char *imageexts[NUMIMG] = { ".bmp", ".tga", ".png", ".jpg" };
+
+int guessimageformat(const char *filename, int format = IMG_BMP)
+{
+    int len = strlen(filename);
+    loopi(NUMIMG)
+    {
+        int extlen = strlen(imageexts[i]);
+        if(len >= extlen && !strcasecmp(&filename[len-extlen], imageexts[i])) return i;
+    }
+    return format;
+}
+
+void saveimage(const char *filename, int format, ImageData &image, bool flip = false)
+{
+    switch(format)
+    {
+        case IMG_PNG: savepng(filename, image, flip); break;
+        case IMG_TGA: savetga(filename, image, flip); break;
+        default:
+        {
+            ImageData flipped(image.w, image.h, image.bpp, image.data);
+            if(flip) texflip(flipped);
+            SDL_Surface *s = wrapsurface(flipped.data, flipped.w, flipped.h, flipped.bpp);
+            if(!s) break;
+            stream *f = openfile(filename, "wb");
+            if(f)
+            {
+                switch(format) {
+                    case IMG_JPG:
+#if SDL_IMAGE_VERSION_ATLEAST(2, 0, 2)
+                        IMG_SaveJPG_RW(s, f->rwops(), 1, screenshotquality);
+#else
+                        conoutf(CON_ERROR, "JPG screenshot support requires SDL_image 2.0.2");
+#endif
+                        break;
+                    default: SDL_SaveBMP_RW(s, f->rwops(), 1); break;
+                }
+                delete f;
+            }
+            SDL_FreeSurface(s);
+            break;
+        }
+    }
+}
+
+bool loadimage(const char *filename, ImageData &image)
+{
+    SDL_Surface *s = loadsurface(path(filename, true));
+    if(!s) return false;
+    image.wrap(s);
+    return true;
+}
+
+SVARP(screenshotdir, "screenshot");
+
+void screenshot(char *filename)
+{
+    static string buf;
+    int format = -1, dirlen = 0;
+    copystring(buf, screenshotdir);
+    if(screenshotdir[0])
+    {
+        dirlen = strlen(buf);
+        if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; }
+        const char *dir = findfile(buf, "w");
+        if(!fileexists(dir, "w")) createdir(dir);
+    }
+    if(filename[0])
+    {
+        concatstring(buf, filename);
+        format = guessimageformat(buf, -1);
+    }
+    else
+    {
+        string sstime;
+        time_t t = time(NULL);
+        size_t len = strftime(sstime, sizeof(sstime), "%Y-%m-%d_%H.%M.%S", localtime(&t));
+        sstime[min(len, sizeof(sstime)-1)] = '\0';
+        concatstring(buf, sstime);
+
+        const char *map = game::getclientmap(), *ssinfo = game::getscreenshotinfo();
+        if(map && map[0])
+        {
+            concatstring(buf, "_");
+            concatstring(buf, map);
+        }
+        if(ssinfo && ssinfo[0])
+        {
+            concatstring(buf, "_");
+            concatstring(buf, ssinfo);
+        }
+
+        for(char *s = &buf[dirlen]; *s; s++) if(iscubespace(*s) || *s == '/' || *s == '\\') *s = '-';
+    }
+    if(format < 0)
+    {
+        format = screenshotformat;
+        concatstring(buf, imageexts[format]);
+    }
+
+    ImageData image(screenw, screenh, 3);
+    glPixelStorei(GL_PACK_ALIGNMENT, texalign(image.data, screenw, 3));
+    glReadPixels(0, 0, screenw, screenh, GL_RGB, GL_UNSIGNED_BYTE, image.data);
+    saveimage(path(buf), format, image, true);
+}
+
+COMMAND(screenshot, "s");
+
+void flipnormalmapy(char *destfile, char *normalfile) // jpg/png /tga-> tga
+{
+    ImageData ns;
+    if(!loadimage(normalfile, ns)) return;
+    ImageData d(ns.w, ns.h, 3);
+    readwritetex(d, ns,
+        dst[0] = src[0];
+        dst[1] = 255 - src[1];
+        dst[2] = src[2];
+    );
+    saveimage(destfile, guessimageformat(destfile, IMG_TGA), d);
+}
+
+void mergenormalmaps(char *heightfile, char *normalfile) // jpg/png/tga + tga -> tga
+{
+    ImageData hs, ns;
+    if(!loadimage(heightfile, hs) || !loadimage(normalfile, ns) || hs.w != ns.w || hs.h != ns.h) return;
+    ImageData d(ns.w, ns.h, 3);
+    read2writetex(d, hs, srch, ns, srcn,
+        *(bvec *)dst = bvec(((bvec *)srcn)->tonormal().mul(2).add(((bvec *)srch)->tonormal()).normalize());
+    );
+    saveimage(normalfile, guessimageformat(normalfile, IMG_TGA), d);
+}
+
+COMMAND(flipnormalmapy, "ss");
+COMMAND(mergenormalmaps, "ss");
+
diff --git a/src/engine/texture.h b/src/engine/texture.h
new file mode 100644 (file)
index 0000000..17bce5f
--- /dev/null
@@ -0,0 +1,779 @@
+struct GlobalShaderParamState
+{
+    const char *name;
+    union
+    {
+        float fval[32];
+        int ival[32];
+        uint uval[32];
+        uchar buf[32*sizeof(float)];
+    };
+    int version;
+
+    static int nextversion;
+
+    void resetversions();
+
+    void changed()
+    {
+        if(++nextversion < 0) resetversions();
+        version = nextversion;
+    }
+};
+
+struct ShaderParamBinding
+{
+    int loc, size;
+    GLenum format;
+};
+
+struct GlobalShaderParamUse : ShaderParamBinding
+{
+
+    GlobalShaderParamState *param;
+    int version;
+
+    void flush()
+    {
+        if(version == param->version) return;
+        switch(format)
+        {
+            case GL_BOOL:
+            case GL_FLOAT:      glUniform1fv_(loc, size, param->fval); break;
+            case GL_BOOL_VEC2:
+            case GL_FLOAT_VEC2: glUniform2fv_(loc, size, param->fval); break;
+            case GL_BOOL_VEC3:
+            case GL_FLOAT_VEC3: glUniform3fv_(loc, size, param->fval); break;
+            case GL_BOOL_VEC4:
+            case GL_FLOAT_VEC4: glUniform4fv_(loc, size, param->fval); break;
+            case GL_INT:        glUniform1iv_(loc, size, param->ival); break;
+            case GL_INT_VEC2:   glUniform2iv_(loc, size, param->ival); break;
+            case GL_INT_VEC3:   glUniform3iv_(loc, size, param->ival); break;
+            case GL_INT_VEC4:   glUniform4iv_(loc, size, param->ival); break;
+            case GL_FLOAT_MAT2: glUniformMatrix2fv_(loc, 1, GL_FALSE, param->fval); break;
+            case GL_FLOAT_MAT3: glUniformMatrix3fv_(loc, 1, GL_FALSE, param->fval); break;
+            case GL_FLOAT_MAT4: glUniformMatrix4fv_(loc, 1, GL_FALSE, param->fval); break;
+        }
+        version = param->version;
+    }
+};
+
+struct LocalShaderParamState : ShaderParamBinding
+{
+    const char *name;
+};
+
+struct SlotShaderParam
+{
+    const char *name;
+    int loc;
+    float val[4];
+};
+
+struct SlotShaderParamState : LocalShaderParamState
+{
+    float val[4];
+
+    SlotShaderParamState() {}
+    SlotShaderParamState(const SlotShaderParam &p)
+    {
+        name = p.name;
+        loc = -1;
+        size = 1;
+        format = GL_FLOAT_VEC4;
+        memcpy(val, p.val, sizeof(val));
+    }
+};
+
+enum 
+{ 
+    SHADER_DEFAULT    = 0, 
+    SHADER_NORMALSLMS = 1<<0, 
+    SHADER_ENVMAP     = 1<<1,
+    SHADER_OPTION     = 1<<3,
+
+    SHADER_INVALID    = 1<<8,
+    SHADER_DEFERRED   = 1<<9
+};
+
+#define MAXSHADERDETAIL 3
+#define MAXVARIANTROWS 5
+
+extern int shaderdetail;
+
+struct Slot;
+struct VSlot;
+
+struct UniformLoc
+{
+    const char *name, *blockname;
+    int loc, version, binding, stride, offset, size;
+    void *data;
+    UniformLoc(const char *name = NULL, const char *blockname = NULL, int binding = -1, int stride = -1) : name(name), blockname(blockname), loc(-1), version(-1), binding(binding), stride(stride), offset(-1), size(-1), data(NULL) {}
+};
+
+struct AttribLoc
+{
+    const char *name;
+    int loc;
+    AttribLoc(const char *name = NULL, int loc = -1) : name(name), loc(loc) {}
+};
+
+struct Shader
+{
+    static Shader *lastshader;
+
+    char *name, *vsstr, *psstr, *defer;
+    int type;
+    GLuint program, vsobj, psobj;
+    vector<SlotShaderParamState> defaultparams;
+    vector<GlobalShaderParamUse> globalparams;
+    vector<LocalShaderParamState> localparams;
+    vector<uchar> localparamremap;
+    Shader *detailshader, *variantshader, *altshader, *fastshader[MAXSHADERDETAIL];
+    vector<Shader *> variants;
+    ushort *variantrows;
+    bool standard, forced, used;
+    Shader *reusevs, *reuseps;
+    vector<UniformLoc> uniformlocs;
+    vector<AttribLoc> attriblocs;
+    const void *owner;
+
+    Shader() : name(NULL), vsstr(NULL), psstr(NULL), defer(NULL), type(SHADER_DEFAULT), program(0), vsobj(0), psobj(0), detailshader(NULL), variantshader(NULL), altshader(NULL), variantrows(NULL), standard(false), forced(false), used(false), reusevs(NULL), reuseps(NULL), owner(NULL)
+    {
+        loopi(MAXSHADERDETAIL) fastshader[i] = this;
+    }
+
+    ~Shader()
+    {
+        DELETEA(name);
+        DELETEA(vsstr);
+        DELETEA(psstr);
+        DELETEA(defer);
+        DELETEA(variantrows);
+    }
+
+    void fixdetailshader(bool force = true, bool recurse = true);
+    void allocparams(Slot *slot = NULL);
+    void setslotparams(Slot &slot);
+    void setslotparams(Slot &slot, VSlot &vslot);
+    void bindprograms();
+
+    void flushparams(Slot *slot = NULL)
+    {
+        if(!used) { allocparams(slot); used = true; }
+        loopv(globalparams) globalparams[i].flush();
+    }
+
+    void force();
+
+    bool invalid() const { return (type&SHADER_INVALID)!=0; }
+    bool deferred() const { return (type&SHADER_DEFERRED)!=0; }
+    bool loaded() const { return detailshader!=NULL; }
+
+    static bool isnull(const Shader *s);
+
+    bool isnull() const { return isnull(this); }
+
+    int numvariants(int row) const
+    {
+        if(row < 0 || row >= MAXVARIANTROWS || !variantrows) return 0;
+        return variantrows[row+1] - variantrows[row];
+    }
+
+    Shader *getvariant(int col, int row) const
+    {
+        if(row < 0 || row >= MAXVARIANTROWS || col < 0 || !variantrows) return NULL;
+        int start = variantrows[row], end = variantrows[row+1];
+        return col < end - start ? variants[start + col] : NULL;
+    }
+
+    bool hasoption(int row)
+    {
+        if(detailshader)
+        {
+            Shader *s = getvariant(0, row);
+            if(s) return (s->type&SHADER_OPTION)!=0;
+        }
+        return false;
+    }
+
+    void addvariant(int row, Shader *s)
+    {
+        if(row < 0 || row >= MAXVARIANTROWS || variants.length() >= USHRT_MAX) return;
+        if(!variantrows) { variantrows = new ushort[MAXVARIANTROWS+1]; memset(variantrows, 0, (MAXVARIANTROWS+1)*sizeof(ushort)); }
+        variants.insert(variantrows[row+1], s);
+        for(int i = row+1; i <= MAXVARIANTROWS; ++i) ++variantrows[i];
+    }
+
+    void setvariant_(int col, int row, Shader *fallbackshader)
+    {
+        Shader *s = fallbackshader;
+        if(detailshader->variantrows)
+        {
+            int start = detailshader->variantrows[row], end = detailshader->variantrows[row+1];
+            for(col = min(start + col, end-1); col >= start; --col)
+            {
+                if(!detailshader->variants[col]->invalid()) { s = detailshader->variants[col]; break; }
+            }
+        }
+        if(lastshader!=s) s->bindprograms();
+    }
+
+    void setvariant(int col, int row, Shader *fallbackshader)
+    {
+        if(isnull() || !loaded()) return;
+        setvariant_(col, row, fallbackshader);
+        lastshader->flushparams();
+    }
+
+    void setvariant(int col, int row)
+    {
+        if(isnull() || !loaded()) return;
+        setvariant_(col, row, detailshader);
+        lastshader->flushparams();
+    }
+
+    void setvariant(int col, int row, Slot &slot, VSlot &vslot, Shader *fallbackshader)
+    {
+        if(isnull() || !loaded()) return;
+        setvariant_(col, row, fallbackshader);
+        lastshader->flushparams(&slot);
+        lastshader->setslotparams(slot, vslot);
+    }
+
+    void setvariant(int col, int row, Slot &slot, VSlot &vslot)
+    {
+        if(isnull() || !loaded()) return;
+        setvariant_(col, row, detailshader);
+        lastshader->flushparams(&slot);
+        lastshader->setslotparams(slot, vslot);
+    }
+
+    void set_()
+    {
+        if(lastshader!=detailshader) detailshader->bindprograms();
+    }
+    void set()
+    {
+        if(isnull() || !loaded()) return;
+        set_();
+        lastshader->flushparams();
+    }
+
+    void set(Slot &slot, VSlot &vslot)
+    {
+        if(isnull() || !loaded()) return;
+        set_();
+        lastshader->flushparams(&slot);
+        lastshader->setslotparams(slot, vslot);
+    }
+
+    bool compile();
+    void cleanup(bool invalid = false);
+    
+    static int uniformlocversion();
+};
+
+struct GlobalShaderParam
+{
+    const char *name;
+    GlobalShaderParamState *param;
+
+    GlobalShaderParam(const char *name) : name(name), param(NULL) {}
+
+    GlobalShaderParamState *resolve()
+    {
+        extern GlobalShaderParamState *getglobalparam(const char *name);
+        if(!param) param = getglobalparam(name);
+        param->changed();
+        return param;
+    }
+
+    void setf(float x = 0, float y = 0, float z = 0, float w = 0)
+    {
+        GlobalShaderParamState *g = resolve();
+        g->fval[0] = x;
+        g->fval[1] = y;
+        g->fval[2] = z;
+        g->fval[3] = w;
+    }
+    void set(const vec &v, float w = 0) { setf(v.x, v.y, v.z, w); }
+    void set(const vec2 &v, float z = 0, float w = 0) { setf(v.x, v.y, z, w); }
+    void set(const vec4 &v) { setf(v.x, v.y, v.z, v.w); }
+    void set(const plane &p) { setf(p.x, p.y, p.z, p.offset); }
+    void set(const matrix2 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); }
+    void set(const matrix3 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); }
+    void set(const matrix4 &m) { memcpy(resolve()->fval, m.a.v, sizeof(m)); }
+
+    template<class T>
+    void setv(const T *v, int n = 1) { memcpy(resolve()->buf, v, n*sizeof(T)); }
+
+    void seti(int x = 0, int y = 0, int z = 0, int w = 0)
+    {
+        GlobalShaderParamState *g = resolve();
+        g->ival[0] = x;
+        g->ival[1] = y;
+        g->ival[2] = z;
+        g->ival[3] = w;
+    }
+    void set(const ivec &v, int w = 0) { seti(v.x, v.y, v.z, w); }
+    void set(const ivec2 &v, int z = 0, int w = 0) { seti(v.x, v.y, z, w); }
+    void set(const ivec4 &v) { seti(v.x, v.y, v.z, v.w); }
+
+    void setu(uint x = 0, uint y = 0, uint z = 0, uint w = 0)
+    {
+        GlobalShaderParamState *g = resolve();
+        g->uval[0] = x;
+        g->uval[1] = y;
+        g->uval[2] = z;
+        g->uval[3] = w;
+    }
+
+    template<class T>
+    T *reserve(int n = 1) { return (T *)resolve()->buf; }
+};  
+
+struct LocalShaderParam
+{
+    const char *name;
+    int loc;
+
+    LocalShaderParam(const char *name) : name(name), loc(-1) {}
+    
+    LocalShaderParamState *resolve()
+    {
+        Shader *s = Shader::lastshader;
+        if(!s) return NULL;
+        if(!s->localparamremap.inrange(loc))
+        {
+            extern int getlocalparam(const char *name);
+            if(loc == -1) loc = getlocalparam(name);
+            if(!s->localparamremap.inrange(loc)) return NULL;
+        }
+        uchar remap = s->localparamremap[loc];
+        return s->localparams.inrange(remap) ? &s->localparams[remap] : NULL;
+    }
+
+    void setf(float x = 0, float y = 0, float z = 0, float w = 0)
+    {
+        ShaderParamBinding *b = resolve();
+        if(b) switch(b->format)
+        {
+            case GL_BOOL:
+            case GL_FLOAT:      glUniform1f_(b->loc, x); break;
+            case GL_BOOL_VEC2:
+            case GL_FLOAT_VEC2: glUniform2f_(b->loc, x, y); break;
+            case GL_BOOL_VEC3:
+            case GL_FLOAT_VEC3: glUniform3f_(b->loc, x, y, z); break;
+            case GL_BOOL_VEC4:
+            case GL_FLOAT_VEC4: glUniform4f_(b->loc, x, y, z, w); break;
+            case GL_INT:        glUniform1i_(b->loc, int(x)); break;
+            case GL_INT_VEC2:   glUniform2i_(b->loc, int(x), int(y)); break;
+            case GL_INT_VEC3:   glUniform3i_(b->loc, int(x), int(y), int(z)); break;
+            case GL_INT_VEC4:   glUniform4i_(b->loc, int(x), int(y), int(z), int(w)); break;
+        }
+    }
+    void set(const vec &v, float w = 0) { setf(v.x, v.y, v.z, w); }
+    void set(const vec2 &v, float z = 0, float w = 0) { setf(v.x, v.y, z, w); }
+    void set(const vec4 &v) { setf(v.x, v.y, v.z, v.w); }
+    void set(const plane &p) { setf(p.x, p.y, p.z, p.offset); }
+    void setv(const float *f, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform1fv_(b->loc, n, f); }
+    void setv(const vec *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform3fv_(b->loc, n, v->v); }
+    void setv(const vec2 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform2fv_(b->loc, n, v->v); }
+    void setv(const vec4 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4fv_(b->loc, n, v->v); }
+    void setv(const plane *p, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4fv_(b->loc, n, p->v); }
+    void setv(const matrix2 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix2fv_(b->loc, n, GL_FALSE, m->a.v); }
+    void setv(const matrix3 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix3fv_(b->loc, n, GL_FALSE, m->a.v); }
+    void setv(const matrix4 *m, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniformMatrix4fv_(b->loc, n, GL_FALSE, m->a.v); }
+    void set(const matrix2 &m) { setv(&m); }
+    void set(const matrix3 &m) { setv(&m); }
+    void set(const matrix4 &m) { setv(&m); }
+
+    template<class T>
+    void sett(T x, T y, T z, T w)
+    {
+        ShaderParamBinding *b = resolve();
+        if(b) switch(b->format)
+        {
+            case GL_FLOAT:      glUniform1f_(b->loc, x); break;
+            case GL_FLOAT_VEC2: glUniform2f_(b->loc, x, y); break;
+            case GL_FLOAT_VEC3: glUniform3f_(b->loc, x, y, z); break;
+            case GL_FLOAT_VEC4: glUniform4f_(b->loc, x, y, z, w); break;
+            case GL_BOOL:
+            case GL_INT:        glUniform1i_(b->loc, x); break;
+            case GL_BOOL_VEC2:
+            case GL_INT_VEC2:   glUniform2i_(b->loc, x, y); break;
+            case GL_BOOL_VEC3:
+            case GL_INT_VEC3:   glUniform3i_(b->loc, x, y, z); break;
+            case GL_BOOL_VEC4:
+            case GL_INT_VEC4:   glUniform4i_(b->loc, x, y, z, w); break;
+        }
+    }
+    void seti(int x = 0, int y = 0, int z = 0, int w = 0) { sett<int>(x, y, z, w); }
+    void set(const ivec &v, int w = 0) { seti(v.x, v.y, v.z, w); }
+    void set(const ivec2 &v, int z = 0, int w = 0) { seti(v.x, v.y, z, w); }
+    void set(const ivec4 &v) { seti(v.x, v.y, v.z, v.w); }
+    void setv(const int *i, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform1iv_(b->loc, n, i); }
+    void setv(const ivec *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform3iv_(b->loc, n, v->v); }
+    void setv(const ivec2 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform2iv_(b->loc, n, v->v); }
+    void setv(const ivec4 *v, int n = 1) { ShaderParamBinding *b = resolve(); if(b) glUniform4iv_(b->loc, n, v->v); }
+};
+
+#define LOCALPARAM(name, vals) do { static LocalShaderParam param( #name ); param.set(vals); } while(0)
+#define LOCALPARAMF(name, ...) do { static LocalShaderParam param( #name ); param.setf(__VA_ARGS__); } while(0)
+#define LOCALPARAMI(name, ...) do { static LocalShaderParam param( #name ); param.seti(__VA_ARGS__); } while(0)
+#define LOCALPARAMV(name, vals, num) do { static LocalShaderParam param( #name ); param.setv(vals, num); } while(0)
+#define GLOBALPARAM(name, vals) do { static GlobalShaderParam param( #name ); param.set(vals); } while(0)
+#define GLOBALPARAMF(name, ...) do { static GlobalShaderParam param( #name ); param.setf(__VA_ARGS__); } while(0)
+#define GLOBALPARAMI(name, ...) do { static GlobalShaderParam param( #name ); param.seti(__VA_ARGS__); } while(0)
+#define GLOBALPARAMV(name, vals, num) do { static GlobalShaderParam param( #name ); param.setv(vals, num); } while(0)
+
+#define SETSHADER(name, ...) \
+    do { \
+        static Shader *name##shader = NULL; \
+        if(!name##shader) name##shader = lookupshaderbyname(#name); \
+        name##shader->set(__VA_ARGS__); \
+    } while(0)
+#define SETVARIANT(name, ...) \
+    do { \
+        static Shader *name##shader = NULL; \
+        if(!name##shader) name##shader = lookupshaderbyname(#name); \
+        name##shader->setvariant(__VA_ARGS__); \
+    } while(0)
+
+struct ImageData
+{
+    int w, h, bpp, levels, align, pitch;
+    GLenum compressed;
+    uchar *data;
+    void *owner;
+    void (*freefunc)(void *);
+
+    ImageData()
+        : data(NULL), owner(NULL), freefunc(NULL)
+    {}
+
+    
+    ImageData(int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE) 
+    { 
+        setdata(NULL, nw, nh, nbpp, nlevels, nalign, ncompressed); 
+    }
+
+    ImageData(int nw, int nh, int nbpp, uchar *data)
+        : owner(NULL), freefunc(NULL)
+    { 
+        setdata(data, nw, nh, nbpp); 
+    }
+
+    ImageData(SDL_Surface *s) { wrap(s); }
+    ~ImageData() { cleanup(); }
+
+    void setdata(uchar *ndata, int nw, int nh, int nbpp, int nlevels = 1, int nalign = 0, GLenum ncompressed = GL_FALSE)
+    {
+        w = nw;
+        h = nh;
+        bpp = nbpp;
+        levels = nlevels;
+        align = nalign;
+        pitch = align ? 0 : w*bpp;
+        compressed = ncompressed;
+        data = ndata ? ndata : new uchar[calcsize()];
+        if(!ndata) { owner = this; freefunc = NULL; }
+    }
+  
+    int calclevelsize(int level) const { return ((max(w>>level, 1)+align-1)/align)*((max(h>>level, 1)+align-1)/align)*bpp; }
+    int calcsize() const
+    {
+        if(!align) return w*h*bpp;
+        int lw = w, lh = h,
+            size = 0;
+        loopi(levels)
+        {
+            if(lw<=0) lw = 1;
+            if(lh<=0) lh = 1;
+            size += ((lw+align-1)/align)*((lh+align-1)/align)*bpp;
+            if(lw*lh==1) break;
+            lw >>= 1;
+            lh >>= 1;
+        }
+        return size;
+    }
+
+    void disown()
+    {
+        data = NULL;
+        owner = NULL;
+        freefunc = NULL;
+    }
+
+    void cleanup()
+    {
+        if(owner==this) delete[] data;
+        else if(freefunc) (*freefunc)(owner);
+        disown();
+    }
+
+    void replace(ImageData &d)
+    {
+        cleanup();
+        *this = d;
+        if(owner == &d) owner = this;
+        d.disown();
+    }
+
+    void wrap(SDL_Surface *s)
+    {
+        setdata((uchar *)s->pixels, s->w, s->h, s->format->BytesPerPixel);
+        pitch = s->pitch;
+        owner = s;
+        freefunc = (void (*)(void *))SDL_FreeSurface;
+    }
+};
+
+// management of texture slots
+// each texture slot can have multiple texture frames, of which currently only the first is used
+// additional frames can be used for various shaders
+
+struct Texture
+{
+    enum
+    {
+        IMAGE      = 0,
+        CUBEMAP    = 1,
+        TYPE       = 0xFF,
+        
+        STUB       = 1<<8,
+        TRANSIENT  = 1<<9,
+        COMPRESSED = 1<<10, 
+        ALPHA      = 1<<11,
+        MIRROR     = 1<<12,
+        FLAGS      = 0xFF00
+    };
+
+    char *name;
+    int type, w, h, xs, ys, bpp, clamp;
+    bool mipmap, canreduce;
+    GLuint id;
+    uchar *alphamask;
+
+    Texture() : alphamask(NULL) {}
+};
+
+enum
+{
+    TEX_DIFFUSE = 0,
+    TEX_UNKNOWN,
+    TEX_DECAL,
+    TEX_NORMAL,
+    TEX_GLOW,
+    TEX_SPEC,
+    TEX_DEPTH,
+    TEX_ALPHA,
+    TEX_ENVMAP
+};
+
+enum 
+{ 
+    VSLOT_SHPARAM = 0, 
+    VSLOT_SCALE, 
+    VSLOT_ROTATION, 
+    VSLOT_OFFSET, 
+    VSLOT_SCROLL, 
+    VSLOT_LAYER, 
+    VSLOT_ALPHA,
+    VSLOT_COLOR,
+    VSLOT_NUM 
+};
+   
+struct VSlot
+{
+    Slot *slot;
+    VSlot *next;
+    int index, changed;
+    vector<SlotShaderParam> params;
+    bool linked;
+    float scale;
+    int rotation;
+    ivec2 offset;
+    vec2 scroll;
+    int layer;
+    float alphafront, alphaback;
+    vec colorscale;
+    vec glowcolor;
+
+    VSlot(Slot *slot = NULL, int index = -1) : slot(slot), next(NULL), index(index), changed(0)
+    { 
+        reset();
+        if(slot) addvariant(slot); 
+    }
+
+    void addvariant(Slot *slot);
+
+    void reset()
+    {
+        params.shrink(0);
+        linked = false;
+        scale = 1;
+        rotation = 0;
+        offset = ivec2(0, 0);
+        scroll = vec2(0, 0);
+        layer = 0;
+        alphafront = 0.5f;
+        alphaback = 0;
+        colorscale = vec(1, 1, 1);
+        glowcolor = vec(1, 1, 1);
+    }
+
+    void cleanup()
+    {
+        linked = false;
+    }
+};
+
+struct Slot
+{
+    struct Tex
+    {
+        int type;
+        Texture *t;
+        string name;
+        int combined;
+    };
+
+    int index;
+    vector<Tex> sts;
+    Shader *shader;
+    vector<SlotShaderParam> params;
+    VSlot *variants;
+    bool loaded;
+    uint texmask;
+    char *autograss;
+    Texture *grasstex, *thumbnail;
+    char *layermaskname;
+    int layermaskmode;
+    float layermaskscale;
+    ImageData *layermask;
+
+    Slot(int index = -1) : index(index), variants(NULL), autograss(NULL), layermaskname(NULL), layermask(NULL) { reset(); }
+    
+    void reset()
+    {
+        sts.shrink(0);
+        shader = NULL;
+        params.shrink(0);
+        loaded = false;
+        texmask = 0;
+        DELETEA(autograss);
+        grasstex = NULL;
+        thumbnail = NULL;
+        DELETEA(layermaskname);
+        layermaskmode = 0;
+        layermaskscale = 1;
+        if(layermask) DELETEP(layermask);
+    }
+
+    void cleanup()
+    {
+        loaded = false;
+        grasstex = NULL;
+        thumbnail = NULL;
+        loopv(sts) 
+        {
+            Tex &t = sts[i];
+            t.t = NULL;
+            t.combined = -1;
+        }
+    }
+};
+
+inline void VSlot::addvariant(Slot *slot)
+{
+    if(!slot->variants) slot->variants = this;
+    else
+    {
+        VSlot *prev = slot->variants;
+        while(prev->next) prev = prev->next;
+        prev->next = this;
+    }
+}
+
+struct MSlot : Slot, VSlot
+{
+    MSlot() : VSlot(this) {}
+
+    void reset()
+    {
+        Slot::reset();
+        VSlot::reset();
+    }
+
+    void cleanup()
+    {
+        Slot::cleanup();
+        VSlot::cleanup();
+    }
+};
+
+struct texrotation
+{
+    bool flipx, flipy, swapxy;
+};
+
+struct cubemapside
+{
+    GLenum target;
+    const char *name;
+    bool flipx, flipy, swapxy;
+};
+
+extern const texrotation texrotations[8];
+extern const cubemapside cubemapsides[6];
+extern Texture *notexture;
+extern Shader *nullshader, *hudshader, *hudnotextureshader, *textureshader, *notextureshader, *nocolorshader, *foggedshader, *foggednotextureshader, *stdworldshader;
+extern int reservevpparams, maxvsuniforms, maxfsuniforms;
+
+extern Shader *lookupshaderbyname(const char *name);
+extern Shader *useshaderbyname(const char *name);
+extern Shader *generateshader(const char *name, const char *cmd, ...);
+extern Texture *loadthumbnail(Slot &slot);
+extern void resetslotshader();
+extern void setslotshader(Slot &s);
+extern void linkslotshader(Slot &s, bool load = true);
+extern void linkvslotshader(VSlot &s, bool load = true);
+extern void linkslotshaders();
+extern const char *getshaderparamname(const char *name, bool insert = true);
+extern void setupshaders();
+extern void reloadshaders();
+extern void cleanupshaders();
+
+#define MAXDYNLIGHTS 5
+#define DYNLIGHTBITS 6
+#define DYNLIGHTMASK ((1<<DYNLIGHTBITS)-1)
+
+#define MAXBLURRADIUS 7
+
+extern void setupblurkernel(int radius, float sigma, float *weights, float *offsets);
+extern void setblurshader(int pass, int size, int radius, float *weights, float *offsets);
+
+extern void savepng(const char *filename, ImageData &image, bool flip = false);
+extern void savetga(const char *filename, ImageData &image, bool flip = false);
+extern bool loaddds(const char *filename, ImageData &image, int force = 0);
+extern bool loadimage(const char *filename, ImageData &image);
+
+extern MSlot &lookupmaterialslot(int slot, bool load = true);
+extern Slot &lookupslot(int slot, bool load = true);
+extern VSlot &lookupvslot(int slot, bool load = true);
+extern VSlot *findvslot(Slot &slot, const VSlot &src, const VSlot &delta);
+extern VSlot *editvslot(const VSlot &src, const VSlot &delta);
+extern void mergevslot(VSlot &dst, const VSlot &src, const VSlot &delta);
+extern void packvslot(vector<uchar> &buf, const VSlot &src);
+extern bool unpackvslot(ucharbuf &buf, VSlot &dst, bool delta);
+
+extern Slot dummyslot;
+extern VSlot dummyvslot;
+extern vector<Slot *> slots;
+extern vector<VSlot *> vslots;
+
diff --git a/src/engine/vertmodel.h b/src/engine/vertmodel.h
new file mode 100644 (file)
index 0000000..eb09001
--- /dev/null
@@ -0,0 +1,490 @@
+struct vertmodel : animmodel
+{
+    struct vert { vec pos, norm; };
+    struct vvert { vec pos; vec2 tc; };
+    struct vvertn : vvert { vec norm; };
+    struct vvertbump : vvert { squat tangent; };
+    struct tcvert { vec2 tc; };
+    struct bumpvert { vec4 tangent; };
+    struct tri { ushort vert[3]; };
+
+    struct vbocacheentry
+    {
+        GLuint vbuf;
+        animstate as;
+        int millis;
+        vbocacheentry() : vbuf(0) { as.cur.fr1 = as.prev.fr1 = -1; }
+    };
+
+    struct vertmesh : mesh
+    {
+        vert *verts;
+        tcvert *tcverts;
+        bumpvert *bumpverts;
+        tri *tris;
+        int numverts, numtris;
+
+        int voffset, eoffset, elen;
+        ushort minvert, maxvert;
+
+        vertmesh() : verts(0), tcverts(0), bumpverts(0), tris(0)
+        {
+        }
+
+        virtual ~vertmesh()
+        {
+            DELETEA(verts);
+            DELETEA(tcverts);
+            DELETEA(bumpverts);
+            DELETEA(tris);
+        }
+
+        void smoothnorms(float limit = 0, bool areaweight = true)
+        {
+            if(((vertmeshgroup *)group)->numframes == 1) mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight);
+            else buildnorms(areaweight);
+        }
+
+        void buildnorms(bool areaweight = true)
+        {
+            mesh::buildnorms(verts, numverts, tris, numtris, areaweight, ((vertmeshgroup *)group)->numframes);
+        }
+
+        void calctangents(bool areaweight = true)
+        {
+            if(bumpverts) return;
+            bumpverts = new bumpvert[((vertmeshgroup *)group)->numframes*numverts];
+            mesh::calctangents(bumpverts, verts, tcverts, numverts, tris, numtris, areaweight, ((vertmeshgroup *)group)->numframes);
+        }
+
+        void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
+        {
+            loopj(numverts)
+            {
+                vec v = m.transform(verts[j].pos);
+                loopi(3)
+                {
+                    bbmin[i] = min(bbmin[i], v[i]);
+                    bbmax[i] = max(bbmax[i], v[i]);
+                }
+            }
+        }
+
+        void genBIH(BIH::mesh &m)
+        {
+            m.tris = (const BIH::tri *)tris;
+            m.numtris = numtris;
+            m.pos = (const uchar *)&verts->pos;
+            m.posstride = sizeof(vert);
+            m.tc = (const uchar *)&tcverts->tc;
+            m.tcstride = sizeof(tcvert);
+        }
+
+        static inline void assignvert(vvertn &vv, int j, tcvert &tc, vert &v)
+        {
+            vv.pos = v.pos;
+            vv.norm = v.norm;
+            vv.tc = tc.tc;
+        }
+
+        inline void assignvert(vvertbump &vv, int j, tcvert &tc, vert &v)
+        {
+            vv.pos = v.pos;
+            vv.tc = tc.tc;
+            vv.tangent = bumpverts[j].tangent;
+        }
+
+        template<class T>
+        int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts, int *htdata, int htlen)
+        {
+            voffset = offset;
+            eoffset = idxs.length();
+            minvert = 0xFFFF;
+            loopi(numtris)
+            {
+                tri &t = tris[i];
+                loopj(3) 
+                {
+                    int index = t.vert[j];
+                    tcvert &tc = tcverts[index];
+                    vert &v = verts[index];
+                    T vv;
+                    assignvert(vv, index, tc, v);
+                    int htidx = hthash(v.pos)&(htlen-1);
+                    loopk(htlen)
+                    {
+                        int &vidx = htdata[(htidx+k)&(htlen-1)];
+                        if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; }
+                        else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; }
+                    }
+                }
+            }
+            minvert = min(minvert, ushort(voffset));
+            maxvert = max(minvert, ushort(vverts.length()-1));
+            elen = idxs.length()-eoffset;
+            return vverts.length()-voffset;
+        }
+
+        int genvbo(vector<ushort> &idxs, int offset)
+        {
+            voffset = offset;
+            eoffset = idxs.length();
+            loopi(numtris)
+            {
+                tri &t = tris[i];
+                loopj(3) idxs.add(voffset+t.vert[j]);
+            }
+            minvert = voffset;
+            maxvert = voffset + numverts-1;
+            elen = idxs.length()-eoffset;
+            return numverts;
+        }
+
+        template<class T>
+        static inline void fillvert(T &vv, int j, tcvert &tc, vert &v)
+        {
+            vv.tc = tc.tc;
+        }
+
+        template<class T>
+        void fillverts(T *vdata)
+        {
+            vdata += voffset;
+            loopi(numverts) fillvert(vdata[i], i, tcverts[i], verts[i]);
+        }
+
+        void interpverts(const animstate &as, bool tangents, void * RESTRICT vdata, skin &s)
+        {
+            const vert * RESTRICT vert1 = &verts[as.cur.fr1 * numverts],
+                       * RESTRICT vert2 = &verts[as.cur.fr2 * numverts],
+                       * RESTRICT pvert1 = as.interp<1 ? &verts[as.prev.fr1 * numverts] : NULL, 
+                       * RESTRICT pvert2 = as.interp<1 ? &verts[as.prev.fr2 * numverts] : NULL;
+            #define ipvert(attrib)   v.attrib.lerp(vert1[i].attrib, vert2[i].attrib, as.cur.t)
+            #define ipbvert(attrib, type)  v.attrib.lerp(bvert1[i].attrib, bvert2[i].attrib, as.cur.t)
+            #define ipvertp(attrib)  v.attrib.lerp(pvert1[i].attrib, pvert2[i].attrib, as.prev.t).lerp(vec().lerp(vert1[i].attrib, vert2[i].attrib, as.cur.t), as.interp)
+            #define ipbvertp(attrib, type) v.attrib.lerp(type().lerp(bpvert1[i].attrib, bpvert2[i].attrib, as.prev.t), type().lerp(bvert1[i].attrib, bvert2[i].attrib, as.cur.t), as.interp)
+            #define iploop(type, body) \
+                loopi(numverts) \
+                { \
+                    type &v = ((type * RESTRICT)vdata)[i]; \
+                    body; \
+                }
+            if(tangents)
+            {
+                const bumpvert * RESTRICT bvert1 = &bumpverts[as.cur.fr1 * numverts],
+                               * RESTRICT bvert2 = &bumpverts[as.cur.fr2 * numverts],
+                               * RESTRICT bpvert1 = as.interp<1 ? &bumpverts[as.prev.fr1 * numverts] : NULL, 
+                               * RESTRICT bpvert2 = as.interp<1 ? &bumpverts[as.prev.fr2 * numverts] : NULL;
+                if(as.interp<1) iploop(vvertbump, { ipvertp(pos); ipbvertp(tangent, vec4); })
+                else iploop(vvertbump, { ipvert(pos); ipbvert(tangent, vec4); })
+            }
+            else
+            {
+                if(as.interp<1) iploop(vvertn, { ipvertp(pos); ipvertp(norm); })
+                else iploop(vvertn, { ipvert(pos); ipvert(norm); })
+            }
+            #undef iploop
+            #undef ipvert
+            #undef ipbvert
+            #undef ipvertp
+            #undef ipbvertp
+        }
+
+        void render(const animstate *as, skin &s, vbocacheentry &vc)
+        {
+            if(!Shader::lastshader) return;
+            glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((vertmeshgroup *)group)->edata[eoffset]);
+            glde++;
+            xtravertsva += numverts;
+        }
+    };
+
+    struct tag
+    {
+        char *name;
+        matrix4x3 transform;
+
+        tag() : name(NULL) {}
+        ~tag() { DELETEA(name); }
+    };
+
+    struct vertmeshgroup : meshgroup
+    {
+        int numframes;
+        tag *tags;
+        int numtags;
+
+        static const int MAXVBOCACHE = 16;
+        vbocacheentry vbocache[MAXVBOCACHE];
+
+        ushort *edata;
+        GLuint ebuf;
+        bool vtangents;
+        int vlen, vertsize;
+        uchar *vdata;
+
+        vertmeshgroup() : numframes(0), tags(NULL), numtags(0), edata(NULL), ebuf(0), vtangents(false), vlen(0), vertsize(0), vdata(NULL) 
+        {
+        }
+
+        virtual ~vertmeshgroup()
+        {
+            DELETEA(tags);
+            if(ebuf) glDeleteBuffers_(1, &ebuf);
+            loopi(MAXVBOCACHE) 
+            {
+                if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf);
+            }
+            DELETEA(vdata);
+        }
+
+        int findtag(const char *name)
+        {
+            loopi(numtags) if(!strcmp(tags[i].name, name)) return i;
+            return -1;
+        }
+
+        int totalframes() const { return numframes; }
+
+        void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
+        {
+            n.mul(m, tags[numtags + i].transform);
+            n.posttranslate(m.transformnormal(p->translate), p->model->scale);
+        }
+
+        void calctagmatrix(part *p, int i, const animstate &as, matrix4 &matrix)
+        {
+            const matrix4x3 &tag1 = tags[as.cur.fr1*numtags + i].transform, 
+                            &tag2 = tags[as.cur.fr2*numtags + i].transform;
+            matrix4x3 tag;
+            tag.lerp(tag1, tag2, as.cur.t);
+            if(as.interp<1)
+            {
+                const matrix4x3 &tag1p = tags[as.prev.fr1*numtags + i].transform, 
+                                &tag2p = tags[as.prev.fr2*numtags + i].transform;
+                matrix4x3 tagp;
+                tagp.lerp(tag1p, tag2p, as.prev.t);
+                tag.lerp(tagp, tag, as.interp);
+            }
+            tag.d.add(p->translate).mul(p->model->scale);
+            matrix = matrix4(tag);
+        }
+
+        void genvbo(bool tangents, vbocacheentry &vc)
+        {
+            if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf);
+            if(ebuf) return;
+                
+            vector<ushort> idxs;
+
+            if(tangents) loopv(meshes) ((vertmesh *)meshes[i])->calctangents();
+
+            vtangents = tangents;
+            vertsize = tangents ? sizeof(vvertbump) : sizeof(vvertn);
+            vlen = 0;
+            if(numframes>1)
+            {
+                loopv(meshes) vlen += ((vertmesh *)meshes[i])->genvbo(idxs, vlen);
+                DELETEA(vdata);
+                vdata = new uchar[vlen*vertsize];
+                #define FILLVDATA(type) do { \
+                    loopv(meshes) ((vertmesh *)meshes[i])->fillverts((type *)vdata); \
+                } while(0)
+                if(tangents) FILLVDATA(vvertbump);
+                else FILLVDATA(vvertn);
+                #undef FILLVDATA
+            } 
+            else 
+            {
+                gle::bindvbo(vc.vbuf);
+                #define GENVBO(type) do { \
+                    vector<type> vverts; \
+                    loopv(meshes) vlen += ((vertmesh *)meshes[i])->genvbo(idxs, vlen, vverts, htdata, htlen); \
+                    glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \
+                } while(0)
+                int numverts = 0, htlen = 128;
+                loopv(meshes) numverts += ((vertmesh *)meshes[i])->numverts;
+                while(htlen < numverts) htlen *= 2;
+                if(numverts*4 > htlen*3) htlen *= 2; 
+                int *htdata = new int[htlen];
+                memset(htdata, -1, htlen*sizeof(int));
+                if(tangents) GENVBO(vvertbump);
+                else GENVBO(vvertn);
+                delete[] htdata;
+                #undef GENVBO
+                gle::clearvbo();
+            }
+
+            glGenBuffers_(1, &ebuf);
+            gle::bindebo(ebuf);
+            glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW);
+            gle::clearebo();
+        }
+
+        void bindvbo(const animstate *as, vbocacheentry &vc)
+        {
+            vvert *vverts = 0;
+            bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize);
+            if(as->cur.anim&ANIM_NOSKIN)
+            {
+                if(enabletc) disabletc();
+                if(enablenormals) disablenormals();
+                if(enabletangents) disabletangents();
+            }
+            else
+            {
+                if(vtangents)
+                {
+                    if(enablenormals) disablenormals();
+                    vvertbump *vvertbumps = 0;
+                    bindtangents(&vvertbumps->tangent, vertsize);
+                }
+                else
+                {
+                    if(enabletangents) disabletangents();
+                    vvertn *vvertns = 0;
+                    bindnormals(&vvertns->norm, vertsize);
+                }
+
+                bindtc(&vverts->tc, vertsize);
+            }
+            if(enablebones) disablebones();
+        }
+
+        void cleanup()
+        {
+            loopi(MAXVBOCACHE)
+            {
+                vbocacheentry &c = vbocache[i];
+                if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; }
+                c.as.cur.fr1 = -1;
+            }
+            if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
+        }
+
+        void preload(part *p)
+        {
+            if(numframes > 1) return;
+            bool tangents = p->tangents();
+            if(tangents!=vtangents) cleanup();
+            if(!vbocache->vbuf) genvbo(tangents, *vbocache);
+        }
+
+        void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p)
+        {
+            if(as->cur.anim&ANIM_NORENDER)
+            {
+                loopv(p->links) calctagmatrix(p, p->links[i].tag, *as, p->links[i].matrix);
+                return;
+            }
+
+            bool tangents = p->tangents();
+            if(tangents!=vtangents) { cleanup(); disablevbo(); }
+            vbocacheentry *vc = NULL;
+            if(numframes<=1) vc = vbocache;
+            else
+            {
+                loopi(MAXVBOCACHE)
+                {
+                    vbocacheentry &c = vbocache[i];
+                    if(!c.vbuf) continue;
+                    if(c.as==*as) { vc = &c; break; }
+                }
+                if(!vc) loopi(MAXVBOCACHE) { vc = &vbocache[i]; if(!vc->vbuf || vc->millis < lastmillis) break; }
+            }
+            if(!vc->vbuf) genvbo(tangents, *vc);
+            if(numframes>1)
+            {
+                if(vc->as!=*as)
+                {
+                    vc->as = *as;
+                    vc->millis = lastmillis;
+                    loopv(meshes) 
+                    {
+                        vertmesh &m = *(vertmesh *)meshes[i];
+                        m.interpverts(*as, tangents, vdata + m.voffset*vertsize, p->skins[i]);
+                    }
+                    gle::bindvbo(vc->vbuf);
+                    glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW);    
+                }
+                vc->millis = lastmillis;
+            }
+        
+            bindvbo(as, *vc);
+            loopv(meshes)
+            {
+                vertmesh *m = (vertmesh *)meshes[i];
+                p->skins[i].bind(m, as);
+                m->render(as, p->skins[i], *vc);
+            }
+            
+            loopv(p->links) calctagmatrix(p, p->links[i].tag, *as, p->links[i].matrix);
+        }
+    };
+
+    vertmodel(const char *name) : animmodel(name)
+    {
+    }
+};
+
+template<class MDL> struct vertloader : modelloader<MDL, vertmodel>
+{
+    vertloader(const char *name) : modelloader<MDL, vertmodel>(name) {}
+};
+
+template<class MDL> struct vertcommands : modelcommands<MDL, struct MDL::vertmesh>
+{
+    typedef struct MDL::part part;
+    typedef struct MDL::skin skin;
+
+    static void loadpart(char *model, float *smooth)
+    {
+        if(!MDL::loading) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        defformatstring(filename, "%s/%s", MDL::dir, model);
+        part &mdl = MDL::loading->addpart();
+        if(mdl.index) mdl.pitchscale = mdl.pitchoffset = mdl.pitchmin = mdl.pitchmax = 0;
+        mdl.meshes = MDL::loading->sharemeshes(path(filename), double(*smooth > 0 ? cos(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2));
+        if(!mdl.meshes) conoutf(CON_ERROR, "could not load %s", filename);
+        else mdl.initskins();
+    }
+    
+    static void setpitch(float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        part &mdl = *MDL::loading->parts.last();
+    
+        mdl.pitchscale = *pitchscale;
+        mdl.pitchoffset = *pitchoffset;
+        if(*pitchmin || *pitchmax)
+        {
+            mdl.pitchmin = *pitchmin;
+            mdl.pitchmax = *pitchmax;
+        }
+        else
+        {
+            mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+            mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset;
+        }
+    }
+
+    static void setanim(char *anim, int *frame, int *range, float *speed, int *priority)
+    {
+        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf(CON_ERROR, "not loading an %s", MDL::formatname()); return; }
+        vector<int> anims;
+        findanims(anim, anims);
+        if(anims.empty()) conoutf(CON_ERROR, "could not find animation %s", anim);
+        else loopv(anims)
+        {
+            MDL::loading->parts.last()->setanim(0, anims[i], *frame, *range, *speed, *priority);
+        }
+    }
+
+    vertcommands()
+    {
+        if(MDL::multiparted()) this->modelcommand(loadpart, "load", "sf"); 
+        this->modelcommand(setpitch, "pitch", "ffff");
+        if(MDL::animated()) this->modelcommand(setanim, "anim", "siiff");
+    }
+};
+
diff --git a/src/engine/water.cpp b/src/engine/water.cpp
new file mode 100644 (file)
index 0000000..07c24c2
--- /dev/null
@@ -0,0 +1,1061 @@
+#include "engine.h"
+
+VARFP(waterreflect, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+VARFP(waterrefract, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+VARFP(waterenvmap, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+VARFP(waterfallrefract, 0, 0, 1, { cleanreflections(); preloadwatershaders(); });
+
+/* vertex water */
+VARP(watersubdiv, 0, 2, 3);
+VARP(waterlod, 0, 1, 3);
+
+static int wx1, wy1, wx2, wy2, wz, wsize, wsubdiv;
+static float whoffset, whphase;
+
+static inline float vertwangle(int v1, int v2)
+{
+    static const float whscale = 59.0f/23.0f/(2*M_PI);
+    v1 &= wsize-1;
+    v2 &= wsize-1;
+    return v1*v2*whscale+whoffset;
+}
+
+static inline float vertwphase(float angle)
+{
+    float s = angle - int(angle) - 0.5f;
+    s *= 8 - fabs(s)*16;
+    return WATER_AMPLITUDE*s-WATER_OFFSET;
+}
+
+static inline void vertw(int v1, int v2, int v3)
+{
+    float h = vertwphase(vertwangle(v1, v2));
+    gle::attribf(v1, v2, v3+h);
+}
+
+static inline void vertwq(float v1, float v2, float v3)
+{
+    gle::attribf(v1, v2, v3+whphase);
+}
+
+static inline void vertwn(float v1, float v2, float v3)
+{
+    float h = -WATER_OFFSET;
+    gle::attribf(v1, v2, v3+h);
+}
+
+struct waterstrip
+{
+    int x1, y1, x2, y2, z;
+    ushort size, subdiv;
+
+    int numverts() const { return 2*((y2-y1)/subdiv + 1)*((x2-x1)/subdiv); }
+
+    void save()
+    {
+        x1 = wx1;
+        y1 = wy1;
+        x2 = wx2;
+        y2 = wy2;
+        z = wz;
+        size = wsize;
+        subdiv = wsubdiv;
+    }
+
+    void restore()
+    {
+        wx1 = x1;
+        wy1 = y1;
+        wx2 = x2;
+        wy2 = y2;
+        wz = z;
+        wsize = size;
+        wsubdiv = subdiv;
+    }
+};
+vector<waterstrip> waterstrips;
+
+void flushwaterstrips()
+{
+    if(gle::attribbuf.length()) xtraverts += gle::end();
+    gle::defvertex();
+    int numverts = 0;
+    loopv(waterstrips) numverts += waterstrips[i].numverts();
+    gle::begin(GL_TRIANGLE_STRIP, numverts);
+    loopv(waterstrips)
+    {
+        waterstrips[i].restore();
+        for(int x = wx1; x < wx2; x += wsubdiv)
+        {
+            for(int y = wy1; y <= wy2; y += wsubdiv)
+            {
+                vertw(x,         y, wz);
+                vertw(x+wsubdiv, y, wz);
+            }
+            x += wsubdiv;
+            if(x >= wx2) break;
+            for(int y = wy2; y >= wy1; y -= wsubdiv)
+            {
+                vertw(x,         y, wz);
+                vertw(x+wsubdiv, y, wz);
+            }
+        }
+        gle::multidraw();
+    }
+    waterstrips.setsize(0);
+    wsize = 0;
+    xtraverts += gle::end();
+}
+
+void flushwater(int mat = MAT_WATER, bool force = true)
+{
+    if(wsize)
+    {
+        if(wsubdiv >= wsize)
+        {
+            if(gle::attribbuf.empty()) { gle::defvertex(); gle::begin(GL_QUADS); }
+            vertwq(wx1, wy1, wz);
+            vertwq(wx2, wy1, wz);
+            vertwq(wx2, wy2, wz);
+            vertwq(wx1, wy2, wz);
+        }
+        else waterstrips.add().save();
+        wsize = 0;
+    }
+
+    if(force)
+    {
+        if(gle::attribbuf.length()) xtraverts += gle::end();
+        if(waterstrips.length()) flushwaterstrips();
+    }
+}
+
+void rendervertwater(int subdiv, int xo, int yo, int z, int size, int mat)
+{
+    if(wsize == size && wsubdiv == subdiv && wz == z)
+    {
+        if(wx2 == xo)
+        {
+            if(wy1 == yo && wy2 == yo + size) { wx2 += size; return; }
+        }
+        else if(wy2 == yo && wx1 == xo && wx2 == xo + size) { wy2 += size; return; }
+    }
+
+    flushwater(mat, false);
+
+    wx1 = xo;
+    wy1 = yo;
+    wx2 = xo + size,
+    wy2 = yo + size;
+    wz = z;
+    wsize = size;
+    wsubdiv = subdiv;
+
+    ASSERT((wx1 & (subdiv - 1)) == 0);
+    ASSERT((wy1 & (subdiv - 1)) == 0);
+}
+
+int calcwatersubdiv(int x, int y, int z, int size)
+{
+    float dist;
+    if(camera1->o.x >= x && camera1->o.x < x + size &&
+       camera1->o.y >= y && camera1->o.y < y + size)
+        dist = fabs(camera1->o.z - float(z));
+    else
+        dist = vec(x + size/2, y + size/2, z + size/2).dist(camera1->o) - size*1.42f/2;
+    int subdiv = watersubdiv + int(dist) / (32 << waterlod);
+    return subdiv >= 31 ? INT_MAX : 1<<subdiv;
+}
+
+int renderwaterlod(int x, int y, int z, int size, int mat)
+{
+    if(size <= (32 << waterlod))
+    {
+        int subdiv = calcwatersubdiv(x, y, z, size);
+        if(subdiv < size * 2) rendervertwater(min(subdiv, size), x, y, z, size, mat);
+        return subdiv;
+    }
+    else
+    {
+        int subdiv = calcwatersubdiv(x, y, z, size);
+        if(subdiv >= size)
+        {
+            if(subdiv < size * 2) rendervertwater(size, x, y, z, size, mat);
+            return subdiv;
+        }
+        int childsize = size / 2,
+            subdiv1 = renderwaterlod(x, y, z, childsize, mat),
+            subdiv2 = renderwaterlod(x + childsize, y, z, childsize, mat),
+            subdiv3 = renderwaterlod(x + childsize, y + childsize, z, childsize, mat),
+            subdiv4 = renderwaterlod(x, y + childsize, z, childsize, mat),
+            minsubdiv = subdiv1;
+        minsubdiv = min(minsubdiv, subdiv2);
+        minsubdiv = min(minsubdiv, subdiv3);
+        minsubdiv = min(minsubdiv, subdiv4);
+        if(minsubdiv < size * 2)
+        {
+            if(minsubdiv >= size) rendervertwater(size, x, y, z, size, mat);
+            else
+            {
+                if(subdiv1 >= size) rendervertwater(childsize, x, y, z, childsize, mat);
+                if(subdiv2 >= size) rendervertwater(childsize, x + childsize, y, z, childsize, mat);
+                if(subdiv3 >= size) rendervertwater(childsize, x + childsize, y + childsize, z, childsize, mat);
+                if(subdiv4 >= size) rendervertwater(childsize, x, y + childsize, z, childsize, mat);
+            } 
+        }
+        return minsubdiv;
+    }
+}
+
+void renderflatwater(int x, int y, int z, int rsize, int csize, int mat)
+{
+    if(gle::attribbuf.empty()) { gle::defvertex(); gle::begin(GL_QUADS); }
+    vertwn(x,       y,       z);
+    vertwn(x+rsize, y,       z);
+    vertwn(x+rsize, y+csize, z);
+    vertwn(x,       y+csize, z);
+}
+
+VARFP(vertwater, 0, 1, 1, allchanged());
+
+static inline void renderwater(const materialsurface &m, int mat = MAT_WATER)
+{
+    if(!vertwater || drawtex == DRAWTEX_MINIMAP) renderflatwater(m.o.x, m.o.y, m.o.z, m.rsize, m.csize, mat);
+    else if(renderwaterlod(m.o.x, m.o.y, m.o.z, m.csize, mat) >= int(m.csize) * 2)
+        rendervertwater(m.csize, m.o.x, m.o.y, m.o.z, m.csize, mat);
+}
+
+void setuplava(Texture *tex, float scale)
+{
+    float xk = TEX_SCALE/(tex->xs*scale);
+    float yk = TEX_SCALE/(tex->ys*scale);
+    float scroll = lastmillis/1000.0f;
+    LOCALPARAMF(lavatexgen, xk, yk, scroll, scroll);
+    gle::normal(vec(0, 0, 1));
+    whoffset = fmod(float(lastmillis/2000.0f/(2*M_PI)), 1.0f);
+    whphase = vertwphase(whoffset);
+}
+
+void renderlava(const materialsurface &m)
+{
+    renderwater(m, MAT_LAVA);
+}
+
+void flushlava()
+{
+    flushwater(MAT_LAVA);
+}
+
+/* reflective/refractive water */
+
+#define MAXREFLECTIONS 16
+
+struct Reflection
+{
+    GLuint tex, refracttex;
+    int material, height, depth, age;
+    bool init;
+    matrix4 projmat;
+    occludequery *query, *prevquery;
+    vector<materialsurface *> matsurfs;
+
+    Reflection() : tex(0), refracttex(0), material(-1), height(-1), depth(0), age(0), init(false), query(NULL), prevquery(NULL)
+    {}
+};
+
+VARP(reflectdist, 0, 2000, 10000);
+
+#define WATERVARS(name) \
+    bvec name##color(0x14, 0x46, 0x50), name##fallcolor(0, 0, 0); \
+    HVARFR(name##colour, 0, 0x144650, 0xFFFFFF, \
+    { \
+        if(!name##colour) name##colour = 0x144650; \
+        name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
+    }); \
+    VARR(name##fog, 0, 150, 10000); \
+    VARR(name##spec, 0, 150, 1000); \
+    HVARFR(name##fallcolour, 0, 0, 0xFFFFFF, \
+    { \
+        name##fallcolor = bvec((name##fallcolour>>16)&0xFF, (name##fallcolour>>8)&0xFF, name##fallcolour&0xFF); \
+    });
+
+WATERVARS(water)
+WATERVARS(water2)
+WATERVARS(water3)
+WATERVARS(water4)
+
+GETMATIDXVAR(water, colour, int)
+GETMATIDXVAR(water, color, const bvec &)
+GETMATIDXVAR(water, fallcolour, int)
+GETMATIDXVAR(water, fallcolor, const bvec &)
+GETMATIDXVAR(water, fog, int)
+GETMATIDXVAR(water, spec, int)
+
+#define LAVAVARS(name) \
+    bvec name##color(0xFF, 0x40, 0x00); \
+    HVARFR(name##colour, 0, 0xFF4000, 0xFFFFFF, \
+    { \
+        if(!name##colour) name##colour = 0xFF4000; \
+        name##color = bvec((name##colour>>16)&0xFF, (name##colour>>8)&0xFF, name##colour&0xFF); \
+    }); \
+    VARR(name##fog, 0, 50, 10000);
+
+LAVAVARS(lava)
+LAVAVARS(lava2)
+LAVAVARS(lava3)
+LAVAVARS(lava4)
+
+GETMATIDXVAR(lava, colour, int)
+GETMATIDXVAR(lava, color, const bvec &)
+GETMATIDXVAR(lava, fog, int)
+
+void setprojtexmatrix(Reflection &ref)
+{
+    if(ref.init)
+    {
+        ref.init = false;
+        (ref.projmat = camprojmatrix).projective();
+    }
+    
+    LOCALPARAM(watermatrix, ref.projmat);
+}
+
+Reflection reflections[MAXREFLECTIONS];
+Reflection waterfallrefraction;
+GLuint reflectionfb = 0, reflectiondb = 0;
+
+GLuint getwaterfalltex() { return waterfallrefraction.refracttex ? waterfallrefraction.refracttex : notexture->id; }
+
+VAR(oqwater, 0, 2, 2);
+VARFP(waterfade, 0, 1, 1, { cleanreflections(); preloadwatershaders(); });
+
+void preloadwatershaders(bool force)
+{
+    static bool needwater = false;
+    if(force) needwater = true;
+    if(!needwater) return;
+
+    useshaderbyname("waterglare");
+
+    if(waterenvmap && !waterreflect)
+        useshaderbyname(waterrefract ? (waterfade ? "waterenvfade" : "waterenvrefract") : "waterenv");
+    else useshaderbyname(waterrefract ? (waterfade ? "waterfade" : "waterrefract") : (waterreflect ? "waterreflect" : "water"));
+
+    useshaderbyname(waterrefract ? (waterfade ? "underwaterfade" : "underwaterrefract") : "underwater");
+
+    extern int waterfallenv;
+    useshaderbyname(waterfallenv ? "waterfallenv" : "waterfall");
+    if(waterfallrefract) useshaderbyname(waterfallenv ? "waterfallenvrefract" : "waterfallrefract");
+}
+
+void renderwater()
+{
+    if(editmode && showmat && !drawtex) return;
+    if(!rplanes) return;
+
+    glDisable(GL_CULL_FACE);
+
+    if(!glaring && drawtex != DRAWTEX_MINIMAP)
+    {
+        if(waterrefract)
+        {
+            if(waterfade)
+            {
+                glEnable(GL_BLEND);
+                glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            }
+        }
+        else
+        {
+            glDepthMask(GL_FALSE);
+            glEnable(GL_BLEND);
+            glBlendFunc(GL_ONE, GL_SRC_ALPHA);
+        }
+    }
+
+    GLOBALPARAM(camera, camera1->o);
+    GLOBALPARAMF(millis, lastmillis/1000.0f);
+
+    #define SETWATERSHADER(which, name) \
+    do { \
+        static Shader *name##shader = NULL; \
+        if(!name##shader) name##shader = lookupshaderbyname(#name); \
+        which##shader = name##shader; \
+    } while(0)
+
+    Shader *aboveshader = NULL;
+    if(glaring) SETWATERSHADER(above, waterglare);
+    else if(drawtex == DRAWTEX_MINIMAP) aboveshader = notextureshader;
+    else if(waterenvmap && !waterreflect)
+    {
+        if(waterrefract)
+        {
+            if(waterfade) SETWATERSHADER(above, waterenvfade);
+            else SETWATERSHADER(above, waterenvrefract);
+        }
+        else SETWATERSHADER(above, waterenv);
+    }
+    else if(waterrefract) 
+    {
+        if(waterfade) SETWATERSHADER(above, waterfade);
+        else SETWATERSHADER(above, waterrefract);
+    }
+    else if(waterreflect) SETWATERSHADER(above, waterreflect);
+    else SETWATERSHADER(above, water);
+
+    Shader *belowshader = NULL;
+    if(!glaring && drawtex != DRAWTEX_MINIMAP)
+    {
+        if(waterrefract)
+        {
+            if(waterfade) SETWATERSHADER(below, underwaterfade);
+            else SETWATERSHADER(below, underwaterrefract);
+        }
+        else SETWATERSHADER(below, underwater);
+    }
+
+    vec ambient(max(skylightcolor[0], ambientcolor[0]), max(skylightcolor[1], ambientcolor[1]), max(skylightcolor[2], ambientcolor[2]));
+    float offset = -WATER_OFFSET;
+    loopi(MAXREFLECTIONS)
+    {
+        Reflection &ref = reflections[i];
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
+        if(!glaring && oqfrags && oqwater && ref.query && ref.query->owner==&ref)
+        {
+            if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
+            {
+                if(checkquery(ref.query)) continue;
+            }
+        }
+
+        bool below = camera1->o.z < ref.height+offset;
+        if(below) 
+        {
+            if(!belowshader) continue;
+            belowshader->set();
+        }
+        else aboveshader->set();
+
+        if(!glaring && drawtex != DRAWTEX_MINIMAP)
+        {
+            if(waterreflect || waterrefract)
+            {
+                if(waterreflect || !waterenvmap) glBindTexture(GL_TEXTURE_2D, waterreflect ? ref.tex : ref.refracttex);
+                setprojtexmatrix(ref);
+            }
+
+            if(waterrefract)
+            {
+                glActiveTexture_(GL_TEXTURE3);
+                glBindTexture(GL_TEXTURE_2D, ref.refracttex);
+                if(waterfade) 
+                {
+                    float fadeheight = ref.height+offset+(below ? -2 : 2);
+                    LOCALPARAMF(waterheight, fadeheight);
+                }
+            }
+        }
+
+        MSlot &mslot = lookupmaterialslot(ref.material);
+        glActiveTexture_(GL_TEXTURE1);
+        glBindTexture(GL_TEXTURE_2D, mslot.sts.inrange(2) ? mslot.sts[2].t->id : notexture->id);
+        glActiveTexture_(GL_TEXTURE2);
+        glBindTexture(GL_TEXTURE_2D, mslot.sts.inrange(3) ? mslot.sts[3].t->id : notexture->id);
+        glActiveTexture_(GL_TEXTURE0);
+        if(!glaring && waterenvmap && !waterreflect && drawtex != DRAWTEX_MINIMAP)
+        {
+            glBindTexture(GL_TEXTURE_CUBE_MAP, lookupenvmap(mslot));
+        }
+
+        whoffset = fmod(float(lastmillis/600.0f/(2*M_PI)), 1.0f);
+        whphase = vertwphase(whoffset);
+
+        gle::color(getwatercolor(ref.material));
+        int wfog = getwaterfog(ref.material), wspec = getwaterspec(ref.material);
+
+        const entity *lastlight = (const entity *)-1;
+        int lastdepth = -1;
+        loopvj(ref.matsurfs)
+        {
+            materialsurface &m = *ref.matsurfs[j];
+
+            entity *light = (m.light && m.light->type==ET_LIGHT ? m.light : NULL);
+            if(light!=lastlight)
+            {
+                flushwater();
+                vec lightpos = light ? light->o : vec(worldsize/2, worldsize/2, worldsize);
+                float lightrad = light && light->attr1 ? light->attr1 : worldsize*8.0f;
+                vec lightcol = (light ? vec(light->attr2, light->attr3, light->attr4) : vec(ambient)).div(255.0f).mul(wspec/100.0f);
+                LOCALPARAM(lightpos, lightpos);
+                LOCALPARAM(lightcolor, lightcol);
+                LOCALPARAMF(lightradius, lightrad);
+                lastlight = light;
+            }
+
+            if(!glaring && !waterrefract && m.depth!=lastdepth)
+            {
+                flushwater();
+                float depth = !wfog ? 1.0f : min(0.75f*m.depth/wfog, 0.95f);
+                depth = max(depth, !below && (waterreflect || waterenvmap) ? 0.3f : 0.6f);
+                LOCALPARAMF(depth, depth, 1.0f-depth);
+                lastdepth = m.depth;
+            }
+
+            renderwater(m);
+        }
+        flushwater();
+    }
+
+    if(!glaring && drawtex != DRAWTEX_MINIMAP)
+    {
+        if(waterrefract)
+        {
+            if(waterfade) glDisable(GL_BLEND);
+        }
+        else
+        {
+            glDepthMask(GL_TRUE);
+            glDisable(GL_BLEND);
+        }
+    }
+
+    glEnable(GL_CULL_FACE);
+}
+
+void setupwaterfallrefract()
+{
+    glBindTexture(GL_TEXTURE_2D, waterfallrefraction.refracttex ? waterfallrefraction.refracttex : notexture->id);
+    setprojtexmatrix(waterfallrefraction);
+}
+
+void cleanreflection(Reflection &ref)
+{
+    ref.material = -1;
+    ref.height = -1;
+    ref.init = false;
+    ref.query = ref.prevquery = NULL;
+    ref.matsurfs.setsize(0);
+    if(ref.tex)
+    {
+        glDeleteTextures(1, &ref.tex);
+        ref.tex = 0;
+    }
+    if(ref.refracttex)
+    {
+        glDeleteTextures(1, &ref.refracttex);
+        ref.refracttex = 0;
+    }
+}
+
+void cleanreflections()
+{
+    loopi(MAXREFLECTIONS) cleanreflection(reflections[i]);
+    cleanreflection(waterfallrefraction);
+    if(reflectionfb)
+    {
+        glDeleteFramebuffers_(1, &reflectionfb);
+        reflectionfb = 0;
+    }
+    if(reflectiondb)
+    {
+        glDeleteRenderbuffers_(1, &reflectiondb);
+        reflectiondb = 0;
+    }
+}
+
+VARFP(reflectsize, 6, 8, 11, cleanreflections());
+
+void genwatertex(GLuint &tex, GLuint &fb, GLuint &db, bool refract = false)
+{
+    static const GLenum colorfmts[] = { GL_RGBA, GL_RGBA8, GL_RGB, GL_RGB8, GL_FALSE },
+                        depthfmts[] = { GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT32, GL_FALSE };
+    static GLenum reflectfmt = GL_FALSE, refractfmt = GL_FALSE, depthfmt = GL_FALSE;
+    static bool usingalpha = false;
+    bool needsalpha = refract && waterrefract && waterfade;
+    if(refract && usingalpha!=needsalpha)
+    {
+        usingalpha = needsalpha;
+        refractfmt = GL_FALSE;
+    }
+    int size = 1<<reflectsize;
+    while(size>hwtexsize) size /= 2;
+
+    glGenTextures(1, &tex);
+    char *buf = new char[size*size*4];
+    memset(buf, 0, size*size*4);
+
+    GLenum &colorfmt = refract ? refractfmt : reflectfmt;
+    if(colorfmt && fb && db)
+    {
+        createtexture(tex, size, size, buf, 3, 1, colorfmt);
+        delete[] buf;
+        return;
+    }
+
+    if(!fb) glGenFramebuffers_(1, &fb);
+    int find = needsalpha ? 0 : 2;
+    do
+    {
+        createtexture(tex, size, size, buf, 3, 1, colorfmt ? colorfmt : colorfmts[find]);
+        glBindFramebuffer_(GL_FRAMEBUFFER, fb);
+        glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+        if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+    }
+    while(!colorfmt && colorfmts[++find]);
+    if(!colorfmt) colorfmt = colorfmts[find];
+
+    delete[] buf;
+
+    if(!db) { glGenRenderbuffers_(1, &db); depthfmt = GL_FALSE; }
+    if(!depthfmt) glBindRenderbuffer_(GL_RENDERBUFFER, db);
+    find = 0;
+    do
+    {
+        if(!depthfmt) glRenderbufferStorage_(GL_RENDERBUFFER, depthfmts[find], size, size);
+        glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, db);
+        if(glCheckFramebufferStatus_(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) break;
+    }
+    while(!depthfmt && depthfmts[++find]);
+    if(!depthfmt)
+    {
+        glBindRenderbuffer_(GL_RENDERBUFFER, 0);
+        depthfmt = depthfmts[find];
+    }
+
+    glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+}
+
+void addwaterfallrefraction(materialsurface &m)
+{
+    Reflection &ref = waterfallrefraction;
+    if(ref.age>=0)
+    {
+        ref.age = -1;
+        ref.init = false;
+        ref.matsurfs.setsize(0);
+        ref.material = MAT_WATER;
+        ref.height = INT_MAX;
+    }
+    ref.matsurfs.add(&m);
+
+    if(!ref.refracttex) genwatertex(ref.refracttex, reflectionfb, reflectiondb);
+}
+
+void addreflection(materialsurface &m)
+{
+    int mat = m.material, height = m.o.z;
+    Reflection *ref = NULL, *oldest = NULL;
+    loopi(MAXREFLECTIONS)
+    {
+        Reflection &r = reflections[i];
+        if(r.height<0)
+        {
+            if(!ref) ref = &r;
+        }
+        else if(r.height==height && r.material==mat) 
+        {
+            r.matsurfs.add(&m);
+            r.depth = max(r.depth, int(m.depth));
+            if(r.age<0) return;
+            ref = &r;
+            break;
+        }
+        else if(!oldest || r.age>oldest->age) oldest = &r;
+    }
+    if(!ref)
+    {
+        if(!oldest || oldest->age<0) return;
+        ref = oldest;
+    }
+    if(ref->height!=height || ref->material!=mat) 
+    {
+        ref->material = mat;
+        ref->height = height;
+        ref->prevquery = NULL;
+    }
+    rplanes++;
+    ref->age = -1;
+    ref->init = false;
+    ref->matsurfs.setsize(0);
+    ref->matsurfs.add(&m);
+    ref->depth = m.depth;
+    if(drawtex == DRAWTEX_MINIMAP) return;
+
+    if(waterreflect && !ref->tex) genwatertex(ref->tex, reflectionfb, reflectiondb);
+    if(waterrefract && !ref->refracttex) genwatertex(ref->refracttex, reflectionfb, reflectiondb, true);
+}
+
+static void drawmaterialquery(const materialsurface &m, float offset, float border = 0, float reflect = -1)
+{
+    if(gle::attribbuf.empty())
+    {
+        gle::defvertex();
+        gle::begin(GL_QUADS);
+    }
+    float x = m.o.x, y = m.o.y, z = m.o.z, csize = m.csize + border, rsize = m.rsize + border;
+    if(reflect >= 0) z = 2*reflect - z;
+    switch(m.orient)
+    {
+#define GENFACEORIENT(orient, v0, v1, v2, v3) \
+        case orient: v0 v1 v2 v3 break;
+#define GENFACEVERT(orient, vert, mx,my,mz, sx,sy,sz) \
+            gle::attribf(mx sx, my sy, mz sz); 
+        GENFACEVERTS(x, x, y, y, z, z, - border, + csize, - border, + rsize, + offset, - offset)
+#undef GENFACEORIENT
+#undef GENFACEVERT
+    }
+}
+
+extern void drawreflection(float z, bool refract, int fogdepth = -1, const bvec &col = bvec(0, 0, 0));
+
+int rplanes = 0;
+
+void queryreflection(Reflection &ref, bool init)
+{
+    if(init)
+    {
+        nocolorshader->set();
+        glDepthMask(GL_FALSE);
+        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+        glDisable(GL_CULL_FACE);
+    }
+    startquery(ref.query);
+    loopvj(ref.matsurfs)
+    {
+        materialsurface &m = *ref.matsurfs[j];
+        float offset = 0.1f;
+        if(m.orient==O_TOP)
+        {
+            offset = WATER_OFFSET +
+                (vertwater ? WATER_AMPLITUDE*(camera1->pitch > 0 || m.depth < WATER_AMPLITUDE+0.5f ? -1 : 1) : 0);
+            if(fabs(m.o.z-offset - camera1->o.z) < 0.5f && m.depth > WATER_AMPLITUDE+1.5f)
+                offset += camera1->pitch > 0 ? -1 : 1;
+        }
+        drawmaterialquery(m, offset);
+    }
+    xtraverts += gle::end();
+    endquery(ref.query);
+}
+
+void queryreflections()
+{
+    rplanes = 0;
+
+    static int lastsize = 0;
+    int size = 1<<reflectsize;
+    while(size>hwtexsize) size /= 2;
+    if(size!=lastsize) { if(lastsize) cleanreflections(); lastsize = size; }
+
+    for(vtxarray *va = visibleva; va; va = va->next)
+    {
+        if(!va->matsurfs || va->occluded >= OCCLUDE_BB || va->curvfc >= VFC_FOGGED) continue;
+        int lastmat = -1;
+        loopi(va->matsurfs)
+        {
+            materialsurface &m = va->matbuf[i];
+            if(m.material != lastmat)
+            {
+                if((m.material&MATF_VOLUME) != MAT_WATER || m.orient == O_BOTTOM) { i += m.skip; continue; }
+                if(m.orient != O_TOP)
+                {
+                    if(!waterfallrefract || !getwaterfog(m.material)) { i += m.skip; continue; }
+                }
+                lastmat = m.material;
+            }
+            if(m.orient==O_TOP) addreflection(m);
+            else addwaterfallrefraction(m);
+        }
+    }
+  
+    loopi(MAXREFLECTIONS)
+    {
+        Reflection &ref = reflections[i];
+        ++ref.age;
+        if(ref.height>=0 && !ref.age && ref.matsurfs.length())
+        {
+            if(waterpvsoccluded(ref.height)) ref.matsurfs.setsize(0);
+        }
+    }
+    if(waterfallrefract)
+    {
+        Reflection &ref = waterfallrefraction;
+        ++ref.age;
+        if(ref.height>=0 && !ref.age && ref.matsurfs.length())
+        {
+            if(waterpvsoccluded(-1)) ref.matsurfs.setsize(0);
+        }
+    }
+
+    if((editmode && showmat && !drawtex) || !oqfrags || !oqwater || drawtex == DRAWTEX_MINIMAP) return;
+
+    int refs = 0;
+    if(waterreflect || waterrefract) loopi(MAXREFLECTIONS)
+    {
+        Reflection &ref = reflections[i];
+        ref.prevquery = oqwater > 1 ? ref.query : NULL;
+        ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL;
+        if(ref.query) queryreflection(ref, !refs++);
+    }
+    if(waterfallrefract)
+    {
+        Reflection &ref = waterfallrefraction;
+        ref.prevquery = oqwater > 1 ? ref.query : NULL;
+        ref.query = ref.height>=0 && !ref.age && ref.matsurfs.length() ? newquery(&ref) : NULL;
+        if(ref.query) queryreflection(ref, !refs++);
+    }
+
+    if(refs)
+    {
+        glDepthMask(GL_TRUE);
+        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+        glEnable(GL_CULL_FACE);
+    }
+
+       glFlush();
+}
+
+VARP(maxreflect, 1, 2, 8);
+
+int refracting = 0, refractfog = 0;
+bvec refractcolor(0, 0, 0);
+bool reflecting = false, fading = false, fogging = false;
+float reflectz = 1e16f;
+
+VAR(maskreflect, 0, 2, 16);
+
+void maskreflection(Reflection &ref, float offset, bool reflect, bool clear = false)
+{
+    const bvec &wcol = getwatercolor(ref.material);
+    vec color = wcol.tocolor();
+    if(!maskreflect)
+    {
+        if(clear) glClearColor(color.r, color.g, color.b, 1);
+        glClear(GL_DEPTH_BUFFER_BIT | (clear ? GL_COLOR_BUFFER_BIT : 0));
+        return;
+    }
+    glClearDepth(0);
+    glClear(GL_DEPTH_BUFFER_BIT);
+    glClearDepth(1);
+    glDepthRange(1, 1);
+    glDepthFunc(GL_ALWAYS);
+    glDisable(GL_CULL_FACE);
+    if(clear)
+    {
+        notextureshader->set();
+        gle::color(color);
+    }
+    else
+    {
+        nocolorshader->set();
+        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+    }
+    float reflectheight = reflect ? ref.height + offset : -1;
+    loopv(ref.matsurfs)
+    {
+        materialsurface &m = *ref.matsurfs[i];
+        drawmaterialquery(m, -offset, maskreflect, reflectheight);
+    }
+    xtraverts += gle::end();
+    if(!clear) glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+    glEnable(GL_CULL_FACE);
+    glDepthFunc(GL_LESS);
+    glDepthRange(0, 1);
+}
+
+VAR(reflectscissor, 0, 1, 1);
+VAR(reflectvfc, 0, 1, 1);
+
+static bool calcscissorbox(Reflection &ref, int size, vec &clipmin, vec &clipmax, int &sx, int &sy, int &sw, int &sh)
+{
+    materialsurface &m0 = *ref.matsurfs[0];
+    int dim = dimension(m0.orient), r = R[dim], c = C[dim];
+    ivec bbmin = m0.o, bbmax = bbmin;
+    bbmax[r] += m0.rsize;
+    bbmax[c] += m0.csize;
+    loopvj(ref.matsurfs)
+    {
+        materialsurface &m = *ref.matsurfs[j];
+        bbmin[r] = min(bbmin[r], m.o[r]);
+        bbmin[c] = min(bbmin[c], m.o[c]);
+        bbmax[r] = max(bbmax[r], m.o[r] + m.rsize);
+        bbmax[c] = max(bbmax[c], m.o[c] + m.csize);
+        bbmin[dim] = min(bbmin[dim], m.o[dim]);
+        bbmax[dim] = max(bbmax[dim], m.o[dim]);
+    }
+
+    vec4 v[8];
+    float sx1 = 1, sy1 = 1, sx2 = -1, sy2 = -1;
+    loopi(8)
+    {
+        vec4 &p = v[i];
+        camprojmatrix.transform(vec(i&1 ? bbmax.x : bbmin.x, i&2 ? bbmax.y : bbmin.y, (i&4 ? bbmax.z + WATER_AMPLITUDE : bbmin.z - WATER_AMPLITUDE) - WATER_OFFSET), p);
+        if(p.z >= -p.w)
+        {
+            float x = p.x / p.w, y = p.y / p.w;
+            sx1 = min(sx1, x);
+            sy1 = min(sy1, y);
+            sx2 = max(sx2, x);
+            sy2 = max(sy2, y);
+        }
+    }
+    if(sx1 >= sx2 || sy1 >= sy2) return false;
+    loopi(8)
+    {
+        const vec4 &p = v[i];
+        if(p.z >= -p.w) continue;    
+        loopj(3)
+        { 
+            const vec4 &o = v[i^(1<<j)];
+            if(o.z <= -o.w) continue;
+            float t = (p.z + p.w)/(p.z + p.w - o.z - o.w),
+                  w = p.w + t*(o.w - p.w),
+                  x = (p.x + t*(o.x - p.x))/w,
+                  y = (p.y + t*(o.y - p.y))/w;
+            sx1 = min(sx1, x);
+            sy1 = min(sy1, y);
+            sx2 = max(sx2, x);
+            sy2 = max(sy2, y);
+        }
+    }
+    if(sx1 <= -1 && sy1 <= -1 && sx2 >= 1 && sy2 >= 1) return false;
+    sx1 = max(sx1, -1.0f);
+    sy1 = max(sy1, -1.0f);
+    sx2 = min(sx2, 1.0f);
+    sy2 = min(sy2, 1.0f);
+    if(reflectvfc)
+    {
+        clipmin.x = clamp(clipmin.x, sx1, sx2);
+        clipmin.y = clamp(clipmin.y, sy1, sy2);
+        clipmax.x = clamp(clipmax.x, sx1, sx2);
+        clipmax.y = clamp(clipmax.y, sy1, sy2);
+    }
+    sx = int(floor((sx1+1)*0.5f*size));
+    sy = int(floor((sy1+1)*0.5f*size));
+    sw = max(int(ceil((sx2+1)*0.5f*size)) - sx, 0);
+    sh = max(int(ceil((sy2+1)*0.5f*size)) - sy, 0);
+    return true;
+}
+
+VARR(refractclear, 0, 0, 1);
+
+void drawreflections()
+{
+    if((editmode && showmat && !drawtex) || drawtex == DRAWTEX_MINIMAP) return;
+
+    static int lastdrawn = 0;
+    int refs = 0, n = lastdrawn;
+    float offset = -WATER_OFFSET;
+    int size = 1<<reflectsize;
+    while(size>hwtexsize) size /= 2;
+
+    if(waterreflect || waterrefract) loopi(MAXREFLECTIONS)
+    {
+        Reflection &ref = reflections[++n%MAXREFLECTIONS];
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) continue;
+        if(oqfrags && oqwater && ref.query && ref.query->owner==&ref)
+        { 
+            if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
+            {
+                if(checkquery(ref.query)) continue;
+            }
+        }
+
+        if(!refs) 
+        {
+            glViewport(0, 0, size, size);
+            glBindFramebuffer_(GL_FRAMEBUFFER, reflectionfb);
+        }
+        refs++;
+        ref.init = true;
+        lastdrawn = n;
+
+        vec clipmin(-1, -1, -1), clipmax(1, 1, 1);
+        int sx, sy, sw, sh;
+        bool scissor = reflectscissor && calcscissorbox(ref, size, clipmin, clipmax, sx, sy, sw, sh);
+        if(scissor) glScissor(sx, sy, sw, sh);
+        else
+        {
+            sx = sy = 0;
+            sw = sh = size;
+        }
+
+        const bvec &wcol = getwatercolor(ref.material);
+        int wfog = getwaterfog(ref.material);
+
+        if(waterreflect && ref.tex && camera1->o.z >= ref.height+offset)
+        {
+            glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.tex, 0);
+            if(scissor) glEnable(GL_SCISSOR_TEST);
+            maskreflection(ref, offset, true);
+            savevfcP();
+            setvfcP(ref.height+offset, clipmin, clipmax); 
+            drawreflection(ref.height+offset, false);
+            restorevfcP();
+            if(scissor) glDisable(GL_SCISSOR_TEST);
+        }
+
+        if(waterrefract && ref.refracttex)
+        {
+            glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.refracttex, 0);
+            if(scissor) glEnable(GL_SCISSOR_TEST);
+            maskreflection(ref, offset, false, refractclear || !wfog || (ref.depth>=10000 && camera1->o.z >= ref.height + offset));
+            if(wfog || waterfade)
+            {
+                savevfcP();
+                setvfcP(-1, clipmin, clipmax);
+                drawreflection(ref.height+offset, true, wfog, wcol);
+                restorevfcP();
+            }
+            if(scissor) glDisable(GL_SCISSOR_TEST);
+        }    
+
+        if(refs>=maxreflect) break;
+    }
+
+    if(waterfallrefract && waterfallrefraction.refracttex)
+    {
+        Reflection &ref = waterfallrefraction;
+
+        if(ref.height<0 || ref.age || ref.matsurfs.empty()) goto nowaterfall;
+        if(oqfrags && oqwater && ref.query && ref.query->owner==&ref)
+        {
+            if(!ref.prevquery || ref.prevquery->owner!=&ref || checkquery(ref.prevquery))
+            {
+                if(checkquery(ref.query)) goto nowaterfall;
+            }
+        }
+
+        if(!refs)
+        {
+            glViewport(0, 0, size, size);
+            glBindFramebuffer_(GL_FRAMEBUFFER, reflectionfb);
+        }
+        refs++;
+        ref.init = true;
+
+        vec clipmin(-1, -1, -1), clipmax(1, 1, 1);
+        int sx, sy, sw, sh;
+        bool scissor = reflectscissor && calcscissorbox(ref, size, clipmin, clipmax, sx, sy, sw, sh);
+        if(scissor) glScissor(sx, sy, sw, sh);
+        else
+        {
+            sx = sy = 0;
+            sw = sh = size;
+        }
+
+        glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ref.refracttex, 0);
+        if(scissor) glEnable(GL_SCISSOR_TEST);
+        maskreflection(ref, -0.1f, false);
+        savevfcP();
+        setvfcP(-1, clipmin, clipmax);
+        drawreflection(-1, true); 
+        restorevfcP();
+        if(scissor) glDisable(GL_SCISSOR_TEST);
+    }
+nowaterfall:
+
+    if(!refs) return;
+    glViewport(0, 0, screenw, screenh);
+    glBindFramebuffer_(GL_FRAMEBUFFER, 0);
+}
+
diff --git a/src/engine/world.cpp b/src/engine/world.cpp
new file mode 100644 (file)
index 0000000..fcf64c2
--- /dev/null
@@ -0,0 +1,1391 @@
+// world.cpp: core map management stuff
+
+#include "engine.h"
+
+VARR(mapversion, 1, MAPVERSION, 0);
+VARNR(mapscale, worldscale, 1, 0, 0);
+VARNR(mapsize, worldsize, 1, 0, 0);
+SVARR(maptitle, "Untitled Map by Unknown");
+
+VAR(octaentsize, 0, 64, 1024);
+VAR(entselradius, 0, 2, 10);
+
+static inline void mmboundbox(const entity &e, model *m, vec &center, vec &radius)
+{
+    m->boundbox(center, radius);
+    rotatebb(center, radius, e.attr1);
+}
+
+static inline void mmcollisionbox(const entity &e, model *m, vec &center, vec &radius)
+{
+    m->collisionbox(center, radius);
+    rotatebb(center, radius, e.attr1);
+}
+
+bool getentboundingbox(const extentity &e, ivec &o, ivec &r)
+{
+    switch(e.type)
+    {
+        case ET_EMPTY:
+            return false;
+        case ET_MAPMODEL:
+        {
+            model *m = loadmapmodel(e.attr2);
+            if(m)
+            {
+                vec center, radius;
+                mmboundbox(e, m, center, radius);
+                center.add(e.o);
+                radius.max(entselradius);
+                o = ivec(vec(center).sub(radius));
+                r = ivec(vec(center).add(radius).add(1));
+                break;
+            }
+        }
+        // invisible mapmodels use entselradius
+        default:
+            o = ivec(vec(e.o).sub(entselradius));
+            r = ivec(vec(e.o).add(entselradius+1));
+            break;
+    }
+    return true;
+}
+
+enum
+{
+    MODOE_ADD      = 1<<0,
+    MODOE_UPDATEBB = 1<<1,
+    MODOE_LIGHTENT = 1<<2
+};
+
+void modifyoctaentity(int flags, int id, extentity &e, cube *c, const ivec &cor, int size, const ivec &bo, const ivec &br, int leafsize, vtxarray *lastva = NULL)
+{
+    loopoctabox(cor, size, bo, br)
+    {
+        ivec o(i, cor, size);
+        vtxarray *va = c[i].ext && c[i].ext->va ? c[i].ext->va : lastva;
+        if(c[i].children != NULL && size > leafsize)
+            modifyoctaentity(flags, id, e, c[i].children, o, size>>1, bo, br, leafsize, va);
+        else if(flags&MODOE_ADD)
+        {
+            if(!c[i].ext || !c[i].ext->ents) ext(c[i]).ents = new octaentities(o, size);
+            octaentities &oe = *c[i].ext->ents;
+            switch(e.type)
+            {
+                case ET_MAPMODEL:
+                    if(loadmapmodel(e.attr2))
+                    {
+                        if(va)
+                        {
+                            va->bbmin.x = -1;
+                            if(oe.mapmodels.empty()) va->mapmodels.add(&oe);
+                        }
+                        oe.mapmodels.add(id);
+                        oe.bbmin.min(bo).max(oe.o);
+                        oe.bbmax.max(br).min(ivec(oe.o).add(oe.size));
+                        break;
+                    }
+                    // invisible mapmodel
+                default:
+                    oe.other.add(id);
+                    break;
+            }
+
+        }
+        else if(c[i].ext && c[i].ext->ents)
+        {
+            octaentities &oe = *c[i].ext->ents;
+            switch(e.type)
+            {
+                case ET_MAPMODEL:
+                    if(loadmapmodel(e.attr2))
+                    {
+                        oe.mapmodels.removeobj(id);
+                        if(va)
+                        {
+                            va->bbmin.x = -1;
+                            if(oe.mapmodels.empty()) va->mapmodels.removeobj(&oe);
+                        }
+                        oe.bbmin = oe.bbmax = oe.o;
+                        oe.bbmin.add(oe.size);
+                        loopvj(oe.mapmodels)
+                        {
+                            extentity &e = *entities::getents()[oe.mapmodels[j]];
+                            ivec eo, er;
+                            if(getentboundingbox(e, eo, er))
+                            {
+                                oe.bbmin.min(eo);
+                                oe.bbmax.max(er);
+                            }
+                        }
+                        oe.bbmin.max(oe.o);
+                        oe.bbmax.min(ivec(oe.o).add(oe.size));
+                        break;
+                    }
+                    // invisible mapmodel
+                default:
+                    oe.other.removeobj(id);
+                    break;
+            }
+            if(oe.mapmodels.empty() && oe.other.empty()) 
+                freeoctaentities(c[i]);
+        }
+        if(c[i].ext && c[i].ext->ents) c[i].ext->ents->query = NULL;
+        if(va && va!=lastva)
+        {
+            if(lastva)
+            {
+                if(va->bbmin.x < 0) lastva->bbmin.x = -1;
+            }
+            else if(flags&MODOE_UPDATEBB) updatevabb(va);
+        }
+    }
+}
+
+vector<int> outsideents;
+
+static bool modifyoctaent(int flags, int id, extentity &e)
+{
+    if(flags&MODOE_ADD ? e.flags&EF_OCTA : !(e.flags&EF_OCTA)) return false;
+
+    ivec o, r;
+    if(!getentboundingbox(e, o, r)) return false;
+
+    if(!insideworld(e.o)) 
+    {
+        int idx = outsideents.find(id);
+        if(flags&MODOE_ADD)
+        {
+            if(idx < 0) outsideents.add(id);
+        }
+        else if(idx >= 0) outsideents.removeunordered(idx);
+    }
+    else
+    {
+        int leafsize = octaentsize, limit = max(r.x - o.x, max(r.y - o.y, r.z - o.z));
+        while(leafsize < limit) leafsize *= 2;
+        int diff = ~(leafsize-1) & ((o.x^r.x)|(o.y^r.y)|(o.z^r.z));
+        if(diff && (limit > octaentsize/2 || diff < leafsize*2)) leafsize *= 2;
+        modifyoctaentity(flags, id, e, worldroot, ivec(0, 0, 0), worldsize>>1, o, r, leafsize);
+    }
+    e.flags ^= EF_OCTA;
+    if(e.type == ET_LIGHT) clearlightcache(id);
+    else if(e.type == ET_PARTICLES) clearparticleemitters();
+    else if(flags&MODOE_LIGHTENT) lightent(e);
+    return true;
+}
+
+static inline bool modifyoctaent(int flags, int id)
+{
+    vector<extentity *> &ents = entities::getents();
+    return ents.inrange(id) && modifyoctaent(flags, id, *ents[id]);
+}
+
+static inline void addentity(int id)    { modifyoctaent(MODOE_ADD|MODOE_UPDATEBB|MODOE_LIGHTENT, id); }
+static inline void removeentity(int id) { modifyoctaent(MODOE_UPDATEBB, id); }
+
+void freeoctaentities(cube &c)
+{
+    if(!c.ext) return;
+    if(entities::getents().length())
+    {
+        while(c.ext->ents && !c.ext->ents->mapmodels.empty()) removeentity(c.ext->ents->mapmodels.pop());
+        while(c.ext->ents && !c.ext->ents->other.empty())     removeentity(c.ext->ents->other.pop());
+    }
+    if(c.ext->ents)
+    {
+        delete c.ext->ents;
+        c.ext->ents = NULL;
+    }
+}
+
+void entitiesinoctanodes()
+{
+    vector<extentity *> &ents = entities::getents();
+    loopv(ents) modifyoctaent(MODOE_ADD, i, *ents[i]);
+}
+
+static inline void findents(octaentities &oe, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector<int> &found)
+{
+    vector<extentity *> &ents = entities::getents();
+    loopv(oe.other)
+    {
+        int id = oe.other[i];
+        extentity &e = *ents[id];
+        if(e.type >= low && e.type <= high && (e.spawned() || notspawned) && vec(e.o).sub(pos).mul(invradius).squaredlen() <= 1) found.add(id);
+    }
+}
+
+static inline void findents(cube *c, const ivec &o, int size, const ivec &bo, const ivec &br, int low, int high, bool notspawned, const vec &pos, const vec &invradius, vector<int> &found)
+{
+    loopoctabox(o, size, bo, br)
+    {
+        if(c[i].ext && c[i].ext->ents) findents(*c[i].ext->ents, low, high, notspawned, pos, invradius, found);
+        if(c[i].children && size > octaentsize) 
+        {
+            ivec co(i, o, size);
+            findents(c[i].children, co, size>>1, bo, br, low, high, notspawned, pos, invradius, found);
+        }
+    }
+}
+
+void findents(int low, int high, bool notspawned, const vec &pos, const vec &radius, vector<int> &found)
+{
+    vec invradius(1/radius.x, 1/radius.y, 1/radius.z);
+    ivec bo(vec(pos).sub(radius).sub(1)),
+         br(vec(pos).add(radius).add(1));
+    int diff = (bo.x^br.x) | (bo.y^br.y) | (bo.z^br.z) | octaentsize,
+        scale = worldscale-1;
+    if(diff&~((1<<scale)-1) || uint(bo.x|bo.y|bo.z|br.x|br.y|br.z) >= uint(worldsize))
+    {
+        findents(worldroot, ivec(0, 0, 0), 1<<scale, bo, br, low, high, notspawned, pos, invradius, found);
+        return;
+    }
+    cube *c = &worldroot[octastep(bo.x, bo.y, bo.z, scale)];
+    if(c->ext && c->ext->ents) findents(*c->ext->ents, low, high, notspawned, pos, invradius, found);
+    scale--;
+    while(c->children && !(diff&(1<<scale)))
+    {
+        c = &c->children[octastep(bo.x, bo.y, bo.z, scale)];
+        if(c->ext && c->ext->ents) findents(*c->ext->ents, low, high, notspawned, pos, invradius, found);
+        scale--;
+    }
+    if(c->children && 1<<scale >= octaentsize) findents(c->children, ivec(bo).mask(~((2<<scale)-1)), 1<<scale, bo, br, low, high, notspawned, pos, invradius, found);
+}
+
+char *entname(entity &e)
+{
+    static string fullentname;
+    copystring(fullentname, entities::entname(e.type));
+    const char *einfo = entities::entnameinfo(e);
+    if(*einfo)
+    {
+        concatstring(fullentname, ": ");
+        concatstring(fullentname, einfo);
+    }
+    return fullentname;
+}
+
+extern selinfo sel;
+extern bool havesel;
+int entlooplevel = 0;
+int efocus = -1, enthover = -1, entorient = -1, oldhover = -1;
+bool undonext = true;
+
+VARF(entediting, 0, 0, 1, { if(!entediting) { entcancel(); efocus = enthover = -1; } });
+
+bool noentedit()
+{
+    if(!editmode) { conoutf(CON_ERROR, "operation only allowed in edit mode"); return true; }
+    return !entediting;
+}
+
+bool pointinsel(const selinfo &sel, const vec &o)
+{
+    return(o.x <= sel.o.x+sel.s.x*sel.grid
+        && o.x >= sel.o.x
+        && o.y <= sel.o.y+sel.s.y*sel.grid
+        && o.y >= sel.o.y
+        && o.z <= sel.o.z+sel.s.z*sel.grid
+        && o.z >= sel.o.z);
+}
+
+vector<int> entgroup;
+
+bool haveselent()
+{
+    return entgroup.length() > 0;
+}
+
+void entcancel()
+{
+    entgroup.shrink(0);
+}
+
+void entadd(int id)
+{
+    undonext = true;
+    entgroup.add(id);
+}
+
+undoblock *newundoent()
+{
+    int numents = entgroup.length();
+    if(numents <= 0) return NULL;
+    undoblock *u = (undoblock *)new uchar[sizeof(undoblock) + numents*sizeof(undoent)];
+    u->numents = numents;
+    undoent *e = (undoent *)(u + 1);
+    loopv(entgroup)
+    {
+        e->i = entgroup[i];
+        e->e = *entities::getents()[entgroup[i]];
+        e++;
+    }
+    return u;
+}
+
+void makeundoent()
+{
+    if(!undonext) return;
+    undonext = false;
+    oldhover = enthover;
+    undoblock *u = newundoent();
+    if(u) addundo(u);
+}
+
+void detachentity(extentity &e)
+{
+    if(!e.attached) return;
+    e.attached->attached = NULL;
+    e.attached = NULL;
+}
+
+VAR(attachradius, 1, 100, 1000);
+
+void attachentity(extentity &e)
+{
+    switch(e.type)
+    {
+        case ET_SPOTLIGHT:
+            break;
+
+        default:
+            if(e.type<ET_GAMESPECIFIC || !entities::mayattach(e)) return;
+            break;
+    }
+
+    detachentity(e);
+
+    vector<extentity *> &ents = entities::getents();
+    int closest = -1;
+    float closedist = 1e10f;
+    loopv(ents)
+    {
+        extentity *a = ents[i];
+        if(a->attached) continue;
+        switch(e.type)
+        {
+            case ET_SPOTLIGHT: 
+                if(a->type!=ET_LIGHT) continue; 
+                break;
+
+            default:
+                if(e.type<ET_GAMESPECIFIC || !entities::attachent(e, *a)) continue;
+                break;
+        }
+        float dist = e.o.dist(a->o);
+        if(dist < closedist)
+        {
+            closest = i;
+            closedist = dist;
+        }
+    }
+    if(closedist>attachradius) return;
+    e.attached = ents[closest];
+    ents[closest]->attached = &e;
+}
+
+void attachentities()
+{
+    vector<extentity *> &ents = entities::getents();
+    loopv(ents) attachentity(*ents[i]);
+}
+
+// convenience macros implicitly define:
+// e         entity, currently edited ent
+// n         int,    index to currently edited ent
+#define addimplicit(f)    { if(entgroup.empty() && enthover>=0) { entadd(enthover); undonext = (enthover != oldhover); f; entgroup.drop(); } else f; }
+#define entfocusv(i, f, v){ int n = efocus = (i); if(n>=0) { extentity &e = *v[n]; f; } }
+#define entfocus(i, f)    entfocusv(i, f, entities::getents())
+#define enteditv(i, f, v) \
+{ \
+    entfocusv(i, \
+    { \
+        int oldtype = e.type; \
+        removeentity(n);  \
+        f; \
+        if(oldtype!=e.type) detachentity(e); \
+        if(e.type!=ET_EMPTY) { addentity(n); if(oldtype!=e.type) attachentity(e); } \
+        entities::editent(n, true); \
+    }, v); \
+}
+#define entedit(i, f)   enteditv(i, f, entities::getents())
+#define addgroup(exp)   { vector<extentity *> &ents = entities::getents(); loopv(ents) entfocusv(i, if(exp) entadd(n), ents); }
+#define setgroup(exp)   { entcancel(); addgroup(exp); }
+#define groupeditloop(f){ vector<extentity *> &ents = entities::getents(); entlooplevel++; int _ = efocus; loopv(entgroup) enteditv(entgroup[i], f, ents); efocus = _; entlooplevel--; }
+#define groupeditpure(f){ if(entlooplevel>0) { entedit(efocus, f); } else groupeditloop(f); }
+#define groupeditundo(f){ makeundoent(); groupeditpure(f); }
+#define groupedit(f)    { addimplicit(groupeditundo(f)); }
+
+vec getselpos()
+{
+    vector<extentity *> &ents = entities::getents();
+    if(entgroup.length() && ents.inrange(entgroup[0])) return ents[entgroup[0]]->o;
+    if(ents.inrange(enthover)) return ents[enthover]->o;
+    return vec(sel.o);
+}
+
+undoblock *copyundoents(undoblock *u)
+{
+    entcancel();
+    undoent *e = u->ents();
+    loopi(u->numents)
+        entadd(e[i].i);
+    undoblock *c = newundoent();
+       loopi(u->numents) if(e[i].e.type==ET_EMPTY)
+               entgroup.removeobj(e[i].i);
+    return c;
+}
+
+void pasteundoent(int idx, const entity &ue)
+{
+    if(idx < 0 || idx >= MAXENTS) return;
+    vector<extentity *> &ents = entities::getents();
+    while(ents.length() < idx) ents.add(entities::newentity())->type = ET_EMPTY;
+    int efocus = -1;
+    entedit(idx, (entity &)e = ue);
+}
+
+void pasteundoents(undoblock *u)
+{
+    undoent *ue = u->ents();
+    loopi(u->numents)
+        entedit(ue[i].i, (entity &)e = ue[i].e);
+}
+
+void entflip()
+{
+    if(noentedit()) return;
+    int d = dimension(sel.orient);
+    float mid = sel.s[d]*sel.grid/2+sel.o[d];
+    groupeditundo(e.o[d] -= (e.o[d]-mid)*2);
+}
+
+void entrotate(int *cw)
+{
+    if(noentedit()) return;
+    int d = dimension(sel.orient);
+    int dd = (*cw<0) == dimcoord(sel.orient) ? R[d] : C[d];
+    float mid = sel.s[dd]*sel.grid/2+sel.o[dd];
+    vec s(sel.o.v);
+    groupeditundo(
+        e.o[dd] -= (e.o[dd]-mid)*2;
+        e.o.sub(s);
+        swap(e.o[R[d]], e.o[C[d]]);
+        e.o.add(s);
+    );
+}
+
+void entselectionbox(const entity &e, vec &eo, vec &es) 
+{
+    model *m = NULL;
+    const char *mname = entities::entmodel(e);
+    if(mname && (m = loadmodel(mname)))
+    {   
+        m->collisionbox(eo, es);
+        if(es.x > es.y) es.y = es.x; else es.x = es.y; // square
+        es.z = (es.z + eo.z + 1 + entselradius)/2; // enclose ent radius box and model box
+        eo.x += e.o.x;
+        eo.y += e.o.y;
+        eo.z = e.o.z - entselradius + es.z;
+    } 
+    else if(e.type == ET_MAPMODEL && (m = loadmapmodel(e.attr2)))
+    {
+        mmcollisionbox(e, m, eo, es);
+        es.max(entselradius);
+        eo.add(e.o);
+    }   
+    else
+    {
+        es = vec(entselradius);
+        eo = e.o;
+    }    
+    eo.sub(es);
+    es.mul(2);
+}
+
+VAR(entselsnap, 0, 0, 1);
+VAR(entmovingshadow, 0, 1, 1);
+
+extern void boxs(int orient, vec o, const vec &s, float size);
+extern void boxs(int orient, vec o, const vec &s);
+extern void boxs3D(const vec &o, vec s, int g);
+extern bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first);
+
+int entmoving = 0;
+
+void entdrag(const vec &ray)
+{
+    if(noentedit() || !haveselent()) return;
+
+    float r = 0, c = 0;
+    static vec v, handle;
+    vec eo, es;
+    int d = dimension(entorient),
+        dc= dimcoord(entorient);
+
+    entfocus(entgroup.last(),        
+        entselectionbox(e, eo, es);
+
+        if(!editmoveplane(e.o, ray, d, eo[d] + (dc ? es[d] : 0), handle, v, entmoving==1))
+            return;        
+
+        ivec g(v);
+        int z = g[d]&(~(sel.grid-1));
+        g.add(sel.grid/2).mask(~(sel.grid-1));
+        g[d] = z;
+        
+        r = (entselsnap ? g[R[d]] : v[R[d]]) - e.o[R[d]];
+        c = (entselsnap ? g[C[d]] : v[C[d]]) - e.o[C[d]];       
+    );
+
+    if(entmoving==1) makeundoent();
+    groupeditpure(e.o[R[d]] += r; e.o[C[d]] += c);
+    entmoving = 2;
+}
+
+VAR(showentradius, 0, 1, 1);
+
+void renderentring(const extentity &e, float radius, int axis)
+{
+    if(radius <= 0) return;
+    gle::defvertex();
+    gle::begin(GL_LINE_LOOP);
+    loopi(15)
+    {
+        vec p(e.o);
+        const vec2 &sc = sincos360[i*(360/15)];
+        p[axis>=2 ? 1 : 0] += radius*sc.x;
+        p[axis>=1 ? 2 : 1] += radius*sc.y;
+        gle::attrib(p);
+    }
+    xtraverts += gle::end();
+}
+
+void renderentsphere(const extentity &e, float radius)
+{
+    if(radius <= 0) return;
+    loopk(3) renderentring(e, radius, k);
+}
+
+void renderentattachment(const extentity &e)
+{
+    if(!e.attached) return;
+    gle::defvertex();
+    gle::begin(GL_LINES);
+    gle::attrib(e.o);
+    gle::attrib(e.attached->o);
+    xtraverts += gle::end();
+}
+
+void renderentarrow(const extentity &e, const vec &dir, float radius)
+{
+    if(radius <= 0) return;
+    float arrowsize = min(radius/8, 0.5f);
+    vec target = vec(dir).mul(radius).add(e.o), arrowbase = vec(dir).mul(radius - arrowsize).add(e.o), spoke;
+    spoke.orthogonal(dir);
+    spoke.normalize();
+    spoke.mul(arrowsize);
+
+    gle::defvertex();
+
+    gle::begin(GL_LINES);
+    gle::attrib(e.o);
+    gle::attrib(target);
+    xtraverts += gle::end();
+
+    gle::begin(GL_TRIANGLE_FAN);
+    gle::attrib(target);
+    loopi(5) gle::attrib(vec(spoke).rotate(2*M_PI*i/4.0f, dir).add(arrowbase));
+    xtraverts += gle::end();
+}
+
+void renderentcone(const extentity &e, const vec &dir, float radius, float angle)
+{
+    if(radius <= 0) return;
+    vec spot = vec(dir).mul(radius*cosf(angle*RAD)).add(e.o), spoke;
+    spoke.orthogonal(dir);
+    spoke.normalize();
+    spoke.mul(radius*sinf(angle*RAD));
+
+    gle::defvertex();
+
+    gle::begin(GL_LINES);
+    loopi(8)
+    {
+        gle::attrib(e.o);
+        gle::attrib(vec(spoke).rotate(2*M_PI*i/8.0f, dir).add(spot));
+    }
+    xtraverts += gle::end();
+
+    gle::begin(GL_LINE_LOOP);
+    loopi(8) gle::attrib(vec(spoke).rotate(2*M_PI*i/8.0f, dir).add(spot));
+    xtraverts += gle::end();
+}
+
+void renderentradius(extentity &e, bool color)
+{
+    switch(e.type)
+    {
+        case ET_LIGHT:
+            if(color) gle::colorf(e.attr2/255.0f, e.attr3/255.0f, e.attr4/255.0f);
+            renderentsphere(e, e.attr1);
+            break;
+
+        case ET_SPOTLIGHT:
+            if(e.attached)
+            {
+                if(color) gle::colorf(0, 1, 1);
+                float radius = e.attached->attr1;
+                if(!radius) radius = 2*e.o.dist(e.attached->o);
+                vec dir = vec(e.o).sub(e.attached->o).normalize();
+                float angle = clamp(int(e.attr1), 1, 89);
+                renderentattachment(e);
+                renderentcone(*e.attached, dir, radius, angle); 
+            }
+            break;
+
+        case ET_SOUND:
+            if(color) gle::colorf(0, 1, 1);
+            renderentsphere(e, e.attr2);
+            break;
+
+        case ET_ENVMAP:
+        {
+            extern int envmapradius;
+            if(color) gle::colorf(0, 1, 1);
+            renderentsphere(e, e.attr1 ? max(0, min(10000, int(e.attr1))) : envmapradius);
+            break;
+        }
+
+        case ET_MAPMODEL:
+        case ET_PLAYERSTART:
+        {
+            if(color) gle::colorf(0, 1, 1);
+            entities::entradius(e, color);
+            vec dir;
+            vecfromyawpitch(e.attr1, 0, 1, 0, dir);
+            renderentarrow(e, dir, 4);
+            break;
+        }
+
+        default:
+            if(e.type>=ET_GAMESPECIFIC) 
+            {
+                if(color) gle::colorf(0, 1, 1);
+                entities::entradius(e, color);
+            }
+            break;
+    }
+}
+
+static void renderentbox(const vec &eo, vec es)
+{
+    es.add(eo);
+
+    // bottom quad
+    gle::attrib(eo.x, eo.y, eo.z); gle::attrib(es.x, eo.y, eo.z);
+    gle::attrib(es.x, eo.y, eo.z); gle::attrib(es.x, es.y, eo.z);
+    gle::attrib(es.x, es.y, eo.z); gle::attrib(eo.x, es.y, eo.z);
+    gle::attrib(eo.x, es.y, eo.z); gle::attrib(eo.x, eo.y, eo.z);
+
+    // top quad
+    gle::attrib(eo.x, eo.y, es.z); gle::attrib(es.x, eo.y, es.z);
+    gle::attrib(es.x, eo.y, es.z); gle::attrib(es.x, es.y, es.z);
+    gle::attrib(es.x, es.y, es.z); gle::attrib(eo.x, es.y, es.z);
+    gle::attrib(eo.x, es.y, es.z); gle::attrib(eo.x, eo.y, es.z);
+
+    // sides
+    gle::attrib(eo.x, eo.y, eo.z); gle::attrib(eo.x, eo.y, es.z);
+    gle::attrib(es.x, eo.y, eo.z); gle::attrib(es.x, eo.y, es.z);
+    gle::attrib(es.x, es.y, eo.z); gle::attrib(es.x, es.y, es.z);
+    gle::attrib(eo.x, es.y, eo.z); gle::attrib(eo.x, es.y, es.z);
+}
+
+void renderentselection(const vec &o, const vec &ray, bool entmoving)
+{   
+    if(noentedit()) return;
+    vec eo, es;
+
+    if(entgroup.length())
+    {
+        gle::colorub(0, 40, 0);
+        gle::defvertex();
+        gle::begin(GL_LINES, entgroup.length()*24);
+        loopv(entgroup) entfocus(entgroup[i],
+            entselectionbox(e, eo, es);
+            renderentbox(eo, es);
+        );
+        xtraverts += gle::end();
+    }
+
+    if(enthover >= 0)
+    {
+        gle::colorub(0, 40, 0);
+        entfocus(enthover, entselectionbox(e, eo, es)); // also ensures enthover is back in focus
+        boxs3D(eo, es, 1);
+        if(entmoving && entmovingshadow==1)
+        {
+            vec a, b;
+            gle::colorub(20, 20, 20);
+            (a = eo).x = eo.x - fmod(eo.x, worldsize); (b = es).x = a.x + worldsize; boxs3D(a, b, 1);  
+            (a = eo).y = eo.y - fmod(eo.y, worldsize); (b = es).y = a.x + worldsize; boxs3D(a, b, 1);  
+            (a = eo).z = eo.z - fmod(eo.z, worldsize); (b = es).z = a.x + worldsize; boxs3D(a, b, 1);
+        }
+        gle::colorub(150,0,0);
+        boxs(entorient, eo, es);
+        boxs(entorient, eo, es, clamp(0.015f*camera1->o.dist(eo)*tan(fovy*0.5f*RAD), 0.1f, 1.0f));
+    }
+
+    if(showentradius && (entgroup.length() || enthover >= 0))
+    {
+        glDepthFunc(GL_GREATER);
+        gle::colorf(0.25f, 0.25f, 0.25f);
+        loopv(entgroup) entfocus(entgroup[i], renderentradius(e, false));
+        if(enthover>=0) entfocus(enthover, renderentradius(e, false));
+        glDepthFunc(GL_LESS);
+        loopv(entgroup) entfocus(entgroup[i], renderentradius(e, true));
+        if(enthover>=0) entfocus(enthover, renderentradius(e, true));
+    }
+}
+
+bool enttoggle(int id)
+{
+    undonext = true;
+    int i = entgroup.find(id);
+    if(i < 0)
+        entadd(id);
+    else
+        entgroup.remove(i);
+    return i < 0;
+}
+
+bool hoveringonent(int ent, int orient)
+{
+    if(noentedit()) return false;
+    entorient = orient;
+    if((efocus = enthover = ent) >= 0)
+        return true;
+    efocus   = entgroup.empty() ? -1 : entgroup.last();
+    enthover = -1;
+    return false;
+}
+
+VAR(entitysurf, 0, 0, 1);
+
+ICOMMAND(entadd, "", (),
+{
+    if(enthover >= 0 && !noentedit())
+    {
+        if(entgroup.find(enthover) < 0) entadd(enthover);
+        if(entmoving > 1) entmoving = 1;
+    }
+});
+
+ICOMMAND(enttoggle, "", (),
+{
+    if(enthover < 0 || noentedit() || !enttoggle(enthover)) { entmoving = 0; intret(0); }
+    else { if(entmoving > 1) entmoving = 1; intret(1); }
+});
+
+ICOMMAND(entmoving, "b", (int *n),
+{
+    if(*n >= 0)
+    {
+        if(!*n || enthover < 0 || noentedit()) entmoving = 0;
+        else
+        {
+            if(entgroup.find(enthover) < 0) { entadd(enthover); entmoving = 1; }
+            else if(!entmoving) entmoving = 1;
+        }
+    }
+    intret(entmoving);
+});
+
+void entpush(int *dir)
+{
+    if(noentedit()) return;
+    int d = dimension(entorient);
+    int s = dimcoord(entorient) ? -*dir : *dir;
+    if(entmoving) 
+    {
+        groupeditpure(e.o[d] += float(s*sel.grid)); // editdrag supplies the undo
+    }
+    else 
+        groupedit(e.o[d] += float(s*sel.grid));
+    if(entitysurf==1)
+    {
+        player->o[d] += float(s*sel.grid);
+        player->resetinterp();
+    }
+}
+
+VAR(entautoviewdist, 0, 25, 100);
+void entautoview(int *dir) 
+{
+    if(!haveselent()) return;
+    static int s = 0;
+    vec v(player->o);
+    v.sub(worldpos);
+    v.normalize();
+    v.mul(entautoviewdist);
+    int t = s + *dir;
+    s = abs(t) % entgroup.length();
+    if(t<0 && s>0) s = entgroup.length() - s;
+    entfocus(entgroup[s],
+        v.add(e.o);
+        player->o = v;
+        player->resetinterp();
+    );
+}
+
+COMMAND(entautoview, "i");
+COMMAND(entflip, "");
+COMMAND(entrotate, "i");
+COMMAND(entpush, "i");
+
+void delent()
+{
+    if(noentedit()) return;
+    groupedit(e.type = ET_EMPTY;);
+    entcancel();
+}
+
+int findtype(char *what)
+{
+    for(int i = 0; *entities::entname(i); i++) if(strcmp(what, entities::entname(i))==0) return i;
+    conoutf(CON_ERROR, "unknown entity type \"%s\"", what);
+    return ET_EMPTY;
+}
+
+VAR(entdrop, 0, 2, 3);
+
+bool dropentity(entity &e, int drop = -1)
+{
+    vec radius(4.0f, 4.0f, 4.0f);
+    if(drop<0) drop = entdrop;
+    if(e.type == ET_MAPMODEL)
+    {
+        model *m = loadmapmodel(e.attr2);
+        if(m)
+        {
+            vec center;
+            mmboundbox(e, m, center, radius);
+            radius.x += fabs(center.x);
+            radius.y += fabs(center.y);
+        }
+        radius.z = 0.0f;
+    }
+    switch(drop)
+    {
+    case 1:
+        if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT)
+            dropenttofloor(&e);
+        break;
+    case 2:
+    case 3:
+        int cx = 0, cy = 0;
+        if(sel.cxs == 1 && sel.cys == 1)
+        {
+            cx = (sel.cx ? 1 : -1) * sel.grid / 2;
+            cy = (sel.cy ? 1 : -1) * sel.grid / 2;
+        }
+        e.o = vec(sel.o);
+        int d = dimension(sel.orient), dc = dimcoord(sel.orient);
+        e.o[R[d]] += sel.grid / 2 + cx;
+        e.o[C[d]] += sel.grid / 2 + cy;
+        if(!dc)
+            e.o[D[d]] -= radius[D[d]];
+        else
+            e.o[D[d]] += sel.grid + radius[D[d]];
+
+        if(drop == 3)
+            dropenttofloor(&e);
+        break;
+    }
+    return true;
+}
+
+void dropent()
+{
+    if(noentedit()) return;
+    groupedit(dropentity(e));
+}
+
+void attachent()
+{
+    if(noentedit()) return;
+    groupedit(attachentity(e));
+}
+
+COMMAND(attachent, "");
+
+VARP(entcamdir, 0, 1, 1);
+
+static int keepents = 0;
+
+extentity *newentity(bool local, const vec &o, int type, int v1, int v2, int v3, int v4, int v5, int &idx)
+{
+    vector<extentity *> &ents = entities::getents();
+    if(local)
+    {
+        idx = -1;
+        for(int i = keepents; i < ents.length(); i++) if(ents[i]->type == ET_EMPTY) { idx = i; break; }
+        if(idx < 0 && ents.length() >= MAXENTS) { conoutf(CON_ERROR, "too many entities"); return NULL; }
+    }
+    else while(ents.length() < idx) ents.add(entities::newentity())->type = ET_EMPTY;
+    extentity &e = *entities::newentity();
+    e.o = o;
+    e.attr1 = v1;
+    e.attr2 = v2;
+    e.attr3 = v3;
+    e.attr4 = v4;
+    e.attr5 = v5;
+    e.type = type;
+    e.reserved = 0;
+    e.light.color = vec(1, 1, 1);
+    e.light.dir = vec(0, 0, 1);
+    if(local)
+    {
+        if(entcamdir) switch(type)
+        {
+            case ET_MAPMODEL:
+            case ET_PLAYERSTART:
+                e.attr5 = e.attr4;
+                e.attr4 = e.attr3;
+                e.attr3 = e.attr2;
+                e.attr2 = e.attr1;
+                e.attr1 = (int)camera1->yaw;
+                break;
+        }
+        entities::fixentity(e);
+    }
+    if(ents.inrange(idx)) { entities::deleteentity(ents[idx]); ents[idx] = &e; }
+    else { idx = ents.length(); ents.add(&e); }
+    return &e;
+}
+
+void newentity(int type, int a1, int a2, int a3, int a4, int a5)
+{
+    int idx;
+    extentity *t = newentity(true, player->o, type, a1, a2, a3, a4, a5, idx);
+    if(!t) return;
+    dropentity(*t);
+    t->type = ET_EMPTY;
+    enttoggle(idx);
+    makeundoent();
+    entedit(idx, e.type = type);
+}
+
+void newent(char *what, int *a1, int *a2, int *a3, int *a4, int *a5)
+{
+    if(noentedit()) return;
+    int type = findtype(what);
+    if(type != ET_EMPTY)
+        newentity(type, *a1, *a2, *a3, *a4, *a5);
+}
+
+int entcopygrid;
+vector<entity> entcopybuf;
+
+void entcopy()
+{
+    if(noentedit()) return;
+    entcopygrid = sel.grid;
+    entcopybuf.shrink(0);
+    loopv(entgroup) 
+        entfocus(entgroup[i], entcopybuf.add(e).o.sub(vec(sel.o)));
+}
+
+void entpaste()
+{
+    if(noentedit()) return;
+    if(entcopybuf.length()==0) return;
+    entcancel();
+    float m = float(sel.grid)/float(entcopygrid);
+    loopv(entcopybuf)
+    {
+        entity &c = entcopybuf[i];
+        vec o(c.o);
+        o.mul(m).add(vec(sel.o));
+        int idx;
+        extentity *e = newentity(true, o, ET_EMPTY, c.attr1, c.attr2, c.attr3, c.attr4, c.attr5, idx);
+        if(!e) continue;
+        entadd(idx);
+        keepents = max(keepents, idx+1);
+    }
+    keepents = 0;
+    int j = 0;
+    groupeditundo(e.type = entcopybuf[j++].type;);
+}
+
+COMMAND(newent, "siiiii");
+COMMAND(delent, "");
+COMMAND(dropent, "");
+COMMAND(entcopy, "");
+COMMAND(entpaste, "");
+
+void entset(char *what, int *a1, int *a2, int *a3, int *a4, int *a5)
+{
+    if(noentedit()) return;
+    int type = findtype(what);
+    if(type != ET_EMPTY)
+        groupedit(e.type=type;
+                  e.attr1=*a1;
+                  e.attr2=*a2;
+                  e.attr3=*a3;
+                  e.attr4=*a4;
+                  e.attr5=*a5);
+}
+
+void printent(extentity &e, char *buf, int len)
+{
+    switch(e.type)
+    {
+        case ET_PARTICLES:
+            if(printparticles(e, buf, len)) return; 
+            break;
+        default:
+            if(e.type >= ET_GAMESPECIFIC && entities::printent(e, buf, len)) return;
+            break;
+    }
+    nformatstring(buf, len, "%s %d %d %d %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+}
+
+void nearestent()
+{
+    if(noentedit()) return;
+    int closest = -1;
+    float closedist = 1e16f;
+    vector<extentity *> &ents = entities::getents();
+    loopv(ents)
+    {
+        extentity &e = *ents[i];
+        if(e.type == ET_EMPTY) continue;
+        float dist = e.o.dist(player->o);
+        if(dist < closedist)
+        {
+            closest = i;
+            closedist = dist;
+        }
+    }
+    if(closest >= 0 && entgroup.find(closest) < 0) entadd(closest);
+}
+
+ICOMMAND(enthavesel,"",  (), addimplicit(intret(entgroup.length())));
+ICOMMAND(entselect, "e", (uint *body), if(!noentedit()) addgroup(e.type != ET_EMPTY && entgroup.find(n)<0 && executebool(body)));
+ICOMMAND(entloop,   "e", (uint *body), if(!noentedit()) addimplicit(groupeditloop(((void)e, execute(body)))));
+ICOMMAND(insel,     "",  (), entfocus(efocus, intret(pointinsel(sel, e.o))));
+ICOMMAND(entget,    "",  (), entfocus(efocus, string s; printent(e, s, sizeof(s)); result(s)));
+ICOMMAND(entindex,  "",  (), intret(efocus));
+COMMAND(entset, "siiiii");
+COMMAND(nearestent, "");
+
+void enttype(char *type, int *numargs)
+{
+    if(*numargs >= 1)
+    {
+        int typeidx = findtype(type);        
+        if(typeidx != ET_EMPTY) groupedit(e.type = typeidx);
+    }    
+    else entfocus(efocus,
+    {
+        result(entities::entname(e.type));
+    })
+}
+
+void entattr(int *attr, int *val, int *numargs)
+{
+    if(*numargs >= 2)
+    {
+        if(*attr >= 0 && *attr <= 4)
+            groupedit(
+                switch(*attr)
+                {
+                    case 0: e.attr1 = *val; break;
+                    case 1: e.attr2 = *val; break;
+                    case 2: e.attr3 = *val; break;
+                    case 3: e.attr4 = *val; break;
+                    case 4: e.attr5 = *val; break;
+                }
+            );        
+    }
+    else entfocus(efocus,
+    {
+        switch(*attr)
+        {
+            case 0: intret(e.attr1); break;
+            case 1: intret(e.attr2); break;
+            case 2: intret(e.attr3); break;
+            case 3: intret(e.attr4); break;
+            case 4: intret(e.attr5); break;
+        }
+    });
+}
+
+COMMAND(enttype, "sN");
+COMMAND(entattr, "iiN");
+
+int findentity(int type, int index, int attr1, int attr2)
+{
+    const vector<extentity *> &ents = entities::getents();
+    if(index > ents.length()) index = ents.length();
+    else for(int i = index; i<ents.length(); i++) 
+    {
+        extentity &e = *ents[i];
+        if(e.type==type && (attr1<0 || e.attr1==attr1) && (attr2<0 || e.attr2==attr2))
+            return i;
+    }
+    loopj(index)
+    {
+        extentity &e = *ents[j];
+        if(e.type==type && (attr1<0 || e.attr1==attr1) && (attr2<0 || e.attr2==attr2))
+            return j;
+    }
+    return -1;
+}
+
+struct spawninfo { const extentity *e; float weight; };
+
+// Compiles a vector of available playerstarts, each with a non-zero weight
+// which serves as a measure of its desirability for a spawning player.
+float gatherspawninfos(dynent *d, int tag, vector<spawninfo> &spawninfos)
+{
+    const vector<extentity *> &ents = entities::getents();
+    float total = 0.0f;
+    loopv(ents)
+    {
+        const extentity &e = *ents[i];
+        if(e.type != ET_PLAYERSTART || e.attr2 != tag) continue;
+        spawninfo &s = spawninfos.add();
+        s.e = &e;
+        s.weight = game::ratespawn(d, e);
+        total += s.weight;
+    }
+    return total;
+}
+
+// Randomly picks a weighted spawn from the provided vector and removes it.
+// The probability of a given spawn being picked is proportional to its weight.
+// If all weights are zero, the index is picked uniformly.
+static const extentity *poprandomspawn(vector<spawninfo> &spawninfos, float &total)
+{
+    if(spawninfos.empty()) return NULL;
+    int index = 0;
+    if(total > 0.0f)
+    {
+        float x = rndscale(total);
+        do x -= spawninfos[index].weight; while(x > 0 && ++index < spawninfos.length()-1);
+    }
+    else index = rnd(spawninfos.length());
+    spawninfo s = spawninfos.removeunordered(index);
+    total -= s.weight;
+    return s.e;
+}
+
+static inline bool tryspawn(dynent *d, const extentity &e)
+{
+    d->o = e.o;
+    d->yaw = e.attr1;
+    return entinmap(d, true);
+}
+
+void findplayerspawn(dynent *d, int forceent, int tag)
+{
+    const vector<extentity *> &ents = entities::getents();
+    d->pitch = 0;
+    d->roll = 0;
+    if(ents.inrange(forceent) && tryspawn(d, *ents[forceent])) return;
+    vector<spawninfo> spawninfos;
+    float total = gatherspawninfos(d, tag, spawninfos);
+    while(const extentity *e = poprandomspawn(spawninfos, total)) if(tryspawn(d, *e)) return;
+    d->o = vec(0.5f * worldsize).addz(1);
+    d->yaw = 0;
+    entinmap(d);
+}
+
+void splitocta(cube *c, int size)
+{
+    if(size <= 0x1000) return;
+    loopi(8)
+    {
+        if(!c[i].children) c[i].children = newcubes(isempty(c[i]) ? F_EMPTY : F_SOLID);
+        splitocta(c[i].children, size>>1);
+    }
+}
+
+void resetmap()
+{
+    clearoverrides();
+    clearmapsounds();
+    cleanreflections();
+    resetblendmap();
+    resetlightmaps();
+    clearpvs();
+    clearslots();
+    clearparticles();
+    cleardecals();
+    cleardamagescreen();
+    clearsleep();
+    cancelsel();
+    pruneundos();
+    clearmapcrc();
+
+    entities::clearents();
+    outsideents.setsize(0);
+}
+
+void startmap(const char *name)
+{
+    game::startmap(name);
+}
+
+bool emptymap(int scale, bool force, const char *mname, bool usecfg)    // main empty world creation routine
+{
+    if(!force && !editmode) 
+    {
+        conoutf(CON_ERROR, "newmap only allowed in edit mode");
+        return false;
+    }
+
+    resetmap();
+
+    setvar("mapscale", scale<10 ? 10 : (scale>16 ? 16 : scale), true, false);
+    setvar("mapsize", 1<<worldscale, true, false);
+    
+    texmru.shrink(0);
+    freeocta(worldroot);
+    worldroot = newcubes(F_EMPTY);
+    loopi(4) solidfaces(worldroot[i]);
+
+    if(worldsize > 0x1000) splitocta(worldroot, worldsize>>1);
+
+    clearmainmenu();
+
+    if(usecfg)
+    {
+        identflags |= IDF_OVERRIDDEN;
+        execfile("data/default_map_settings.cfg", false);
+        identflags &= ~IDF_OVERRIDDEN;
+    }
+
+    initlights();
+    allchanged(true);
+
+    startmap(mname);
+
+    return true;
+}
+
+bool enlargemap(bool force)
+{
+    if(!force && !editmode)
+    {
+        conoutf(CON_ERROR, "mapenlarge only allowed in edit mode");
+        return false;
+    }
+    if(worldsize >= 1<<16) return false;
+
+    while(outsideents.length()) removeentity(outsideents.pop());
+
+    worldscale++;
+    worldsize *= 2;
+    cube *c = newcubes(F_EMPTY);
+    c[0].children = worldroot;
+    loopi(3) solidfaces(c[i+1]);
+    worldroot = c;
+
+    if(worldsize > 0x1000) splitocta(worldroot, worldsize>>1);
+
+    enlargeblendmap();
+
+    allchanged();
+
+    return true;
+}
+
+static bool isallempty(cube &c)
+{
+    if(!c.children) return isempty(c);
+    loopi(8) if(!isallempty(c.children[i])) return false;
+    return true;
+}
+
+void shrinkmap()
+{
+    extern int nompedit;
+    if(noedit(true) || (nompedit && multiplayer())) return;
+    if(worldsize <= 1<<10) return;
+
+    int octant = -1;
+    loopi(8) if(!isallempty(worldroot[i]))
+    {
+        if(octant >= 0) return;
+        octant = i;
+    }
+    if(octant < 0) return;
+
+    while(outsideents.length()) removeentity(outsideents.pop());
+
+    if(!worldroot[octant].children) subdividecube(worldroot[octant], false, false);
+    cube *root = worldroot[octant].children;
+    worldroot[octant].children = NULL;
+    freeocta(worldroot);
+    worldroot = root; 
+    worldscale--;
+    worldsize /= 2; 
+
+    ivec offset(octant, ivec(0, 0, 0), worldsize);
+    vector<extentity *> &ents = entities::getents();
+    loopv(ents) ents[i]->o.sub(vec(offset));
+
+    shrinkblendmap(octant);
+    allchanged();
+
+    conoutf("shrunk map to size %d", worldscale);
+}
+
+void newmap(int *i) { bool force = !isconnected(); if(force) game::forceedit(""); if(emptymap(*i, force, NULL)) game::newmap(max(*i, 0)); }
+void mapenlarge() { if(enlargemap(false)) game::newmap(-1); }
+COMMAND(newmap, "i");
+COMMAND(mapenlarge, "");
+COMMAND(shrinkmap, "");
+
+void mapname()
+{
+    result(game::getclientmap());
+}
+
+COMMAND(mapname, "");
+
+void mpeditent(int i, const vec &o, int type, int attr1, int attr2, int attr3, int attr4, int attr5, bool local)
+{
+    if(i < 0 || i >= MAXENTS) return;
+    vector<extentity *> &ents = entities::getents();
+    if(ents.length()<=i)
+    {
+        extentity *e = newentity(local, o, type, attr1, attr2, attr3, attr4, attr5, i);
+        if(!e) return;
+        addentity(i);
+        attachentity(*e);
+    }
+    else
+    {
+        extentity &e = *ents[i];
+        removeentity(i);
+        int oldtype = e.type;
+        if(oldtype!=type) detachentity(e);
+        e.type = type;
+        e.o = o;
+        e.attr1 = attr1; e.attr2 = attr2; e.attr3 = attr3; e.attr4 = attr4; e.attr5 = attr5;
+        addentity(i);
+        if(oldtype!=type) attachentity(e);
+    }
+    entities::editent(i, local);
+}
+
+int getworldsize() { return worldsize; }
+int getmapversion() { return mapversion; }
+
diff --git a/src/engine/world.h b/src/engine/world.h
new file mode 100644 (file)
index 0000000..9c5be78
--- /dev/null
@@ -0,0 +1,59 @@
+
+enum                            // hardcoded texture numbers
+{
+    DEFAULT_SKY = 0,
+    DEFAULT_GEOM
+};
+
+#define MAPVERSION 33           // bump if map format changes, see worldio.cpp
+
+struct octaheader
+{
+    char magic[4];              // "OCTA"
+    int version;                // any >8bit quantity is little endian
+    int headersize;             // sizeof(header)
+    int worldsize;
+    int numents;
+    int numpvs;
+    int lightmaps;
+    int blendmap;
+    int numvars;
+    int numvslots;
+};
+    
+struct compatheader             // map file format header
+{
+    char magic[4];              // "OCTA"
+    int version;                // any >8bit quantity is little endian
+    int headersize;             // sizeof(header)
+    int worldsize;
+    int numents;
+    int numpvs;
+    int lightmaps;
+    int lightprecision, lighterror, lightlod;
+    uchar ambient;
+    uchar watercolour[3];
+    uchar blendmap;
+    uchar lerpangle, lerpsubdiv, lerpsubdivsize;
+    uchar bumperror;
+    uchar skylight[3];
+    uchar lavacolour[3];
+    uchar waterfallcolour[3];
+    uchar reserved[10];
+    char maptitle[128];
+};
+
+#define WATER_AMPLITUDE 0.4f
+#define WATER_OFFSET 1.1f
+
+enum 
+{ 
+    MATSURF_NOT_VISIBLE = 0,
+    MATSURF_VISIBLE,
+    MATSURF_EDIT_ONLY
+};
+
+#define TEX_SCALE 8.0f
+
+struct vertex { vec pos; bvec4 norm; vec2 tc; svec2 lm; bvec4 tangent; };
+
diff --git a/src/engine/worldio.cpp b/src/engine/worldio.cpp
new file mode 100644 (file)
index 0000000..514b45e
--- /dev/null
@@ -0,0 +1,1388 @@
+// worldio.cpp: loading & saving of maps and savegames
+
+#include "engine.h"
+
+void validmapname(char *dst, const char *src, const char *prefix = NULL, const char *alt = "untitled", size_t maxlen = 100)
+{
+    if(prefix) while(*prefix) *dst++ = *prefix++;
+    const char *start = dst;
+    if(src) loopi(maxlen)
+    {
+        char c = *src++;
+        if(iscubealnum(c) || c == '_' || c == '-' || c == '/' || c == '\\') *dst++ = c;
+        else break;
+    }
+    if(dst > start) *dst = '\0';
+    else if(dst != alt) copystring(dst, alt, maxlen);
+}
+
+void fixmapname(char *name)
+{
+    validmapname(name, name, NULL, "");
+}
+
+void getmapfilenames(const char *fname, const char *cname, char *pakname, char *mapname, char *cfgname)
+{
+    if(!cname) cname = fname;
+    string name;
+    validmapname(name, cname);
+    char *slash = strpbrk(name, "/\\");
+    if(slash)
+    {
+        copystring(pakname, name, slash-name+1);
+        copystring(cfgname, slash+1, MAXSTRLEN);
+    }
+    else
+    {
+        copystring(pakname, "base", MAXSTRLEN);
+        copystring(cfgname, name, MAXSTRLEN);
+    }
+    validmapname(mapname, fname, strpbrk(fname, "/\\") ? NULL : "base/");
+}
+
+static void fixent(entity &e, int version)
+{
+    if(version <= 10 && e.type >= 7) e.type++;
+    if(version <= 12 && e.type >= 8) e.type++;
+    if(version <= 14 && e.type >= ET_MAPMODEL && e.type <= 16)
+    {
+        if(e.type == 16) e.type = ET_MAPMODEL;
+        else e.type++;
+    }
+    if(version <= 20 && e.type >= ET_ENVMAP) e.type++;
+    if(version <= 21 && e.type >= ET_PARTICLES) e.type++;
+    if(version <= 22 && e.type >= ET_SOUND) e.type++;
+    if(version <= 23 && e.type >= ET_SPOTLIGHT) e.type++;
+    if(version <= 30 && (e.type == ET_MAPMODEL || e.type == ET_PLAYERSTART)) e.attr1 = (int(e.attr1)+180)%360;
+    if(version <= 31 && e.type == ET_MAPMODEL) { int yaw = (int(e.attr1)%360 + 360)%360 + 7; e.attr1 = yaw - yaw%15; }
+}
+
+bool loadents(const char *fname, vector<entity> &ents, uint *crc)
+{
+    string pakname, mapname, mcfgname, ogzname;
+    getmapfilenames(fname, NULL, pakname, mapname, mcfgname);
+    formatstring(ogzname, "packages/%s.ogz", mapname);
+    path(ogzname);
+    stream *f = opengzfile(ogzname, "rb");
+    if(!f) return false;
+    octaheader hdr;
+    if(f->read(&hdr, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    lilswap(&hdr.version, 6);
+    if(memcmp(hdr.magic, "OCTA", 4) || hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Cube 2: Sauerbraten", ogzname); delete f; return false; }
+    compatheader chdr;
+    if(hdr.version <= 28)
+    {
+        if(f->read(&chdr.lightprecision, sizeof(chdr) - 7*sizeof(int)) != sizeof(chdr) - 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    }
+    else
+    {
+        int extra = 0;
+        if(hdr.version <= 29) extra++;
+        if(f->read(&hdr.blendmap, sizeof(hdr) - (7+extra)*sizeof(int)) != sizeof(hdr) - (7+extra)*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    }
+
+    if(hdr.version <= 28)
+    {
+        lilswap(&chdr.lightprecision, 3);
+        hdr.blendmap = chdr.blendmap;
+        hdr.numvars = 0;
+        hdr.numvslots = 0;
+    }
+    else
+    {
+        lilswap(&hdr.blendmap, 2);
+        if(hdr.version <= 29) hdr.numvslots = 0;
+        else lilswap(&hdr.numvslots, 1);
+    }
+
+    loopi(hdr.numvars)
+    {
+        int type = f->getchar(), ilen = f->getlil<ushort>();
+        f->seek(ilen, SEEK_CUR);
+        switch(type)
+        {
+            case ID_VAR: f->getlil<int>(); break;
+            case ID_FVAR: f->getlil<float>(); break;
+            case ID_SVAR: { int slen = f->getlil<ushort>(); f->seek(slen, SEEK_CUR); break; }
+        }
+    }
+
+    string gametype;
+    copystring(gametype, "fps");
+    bool samegame = true;
+    int eif = 0;
+    if(hdr.version>=16)
+    {
+        int len = f->getchar();
+        f->read(gametype, len+1);
+    }
+    if(strcmp(gametype, game::gameident()))
+    {
+        samegame = false;
+        conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype);
+    }
+    if(hdr.version>=16)
+    {
+        eif = f->getlil<ushort>();
+        int extrasize = f->getlil<ushort>();
+        f->seek(extrasize, SEEK_CUR);
+    }
+
+    if(hdr.version<14)
+    {
+        f->seek(256, SEEK_CUR);
+    }
+    else
+    {
+        ushort nummru = f->getlil<ushort>();
+        f->seek(nummru*sizeof(ushort), SEEK_CUR);
+    }
+
+    loopi(min(hdr.numents, MAXENTS))
+    {
+        entity &e = ents.add();
+        f->read(&e, sizeof(entity));
+        lilswap(&e.o.x, 3);
+        lilswap(&e.attr1, 5);
+        fixent(e, hdr.version);
+        if(eif > 0) f->seek(eif, SEEK_CUR);
+        if(samegame)
+        {
+            entities::readent(e, NULL, hdr.version);
+        }
+        else if(e.type>=ET_GAMESPECIFIC || hdr.version<=14)
+        {
+            ents.pop();
+            continue;
+        }
+    }
+
+    if(crc)
+    {
+        f->seek(0, SEEK_END);
+        *crc = f->getcrc();
+    }
+    
+    delete f;
+
+    return true;
+}
+
+#ifndef STANDALONE
+string ogzname, bakname, cfgname, picname;
+
+VARP(savebak, 0, 2, 2);
+
+void setmapfilenames(const char *fname, const char *cname = NULL)
+{
+    string pakname, mapname, mcfgname;
+    getmapfilenames(fname, cname, pakname, mapname, mcfgname);
+
+    formatstring(ogzname, "packages/%s.ogz", mapname);
+    if(savebak==1) formatstring(bakname, "packages/%s.BAK", mapname);
+    else formatstring(bakname, "packages/%s_%d.BAK", mapname, totalmillis);
+    formatstring(cfgname, "packages/%s/%s.cfg", pakname, mcfgname);
+    formatstring(picname, "packages/%s.jpg", mapname);
+
+    path(ogzname);
+    path(bakname);
+    path(cfgname);
+    path(picname);
+}
+
+void mapcfgname()
+{
+    const char *mname = game::getclientmap();
+    string pakname, mapname, mcfgname;
+    getmapfilenames(mname, NULL, pakname, mapname, mcfgname);
+    defformatstring(cfgname, "packages/%s/%s.cfg", pakname, mcfgname);
+    path(cfgname);
+    result(cfgname);
+}
+
+COMMAND(mapcfgname, "");
+
+void backup(char *name, char *backupname)
+{   
+    string backupfile;
+    copystring(backupfile, findfile(backupname, "wb"));
+    remove(backupfile);
+    rename(findfile(name, "wb"), backupfile);
+}
+
+enum { OCTSAV_CHILDREN = 0, OCTSAV_EMPTY, OCTSAV_SOLID, OCTSAV_NORMAL, OCTSAV_LODCUBE };
+
+static int savemapprogress = 0;
+
+void savec(cube *c, const ivec &o, int size, stream *f, bool nolms)
+{
+    if((savemapprogress++&0xFFF)==0) renderprogress(float(savemapprogress)/allocnodes, "saving octree...");
+
+    loopi(8)
+    {
+        ivec co(i, o, size);
+        if(c[i].children)
+        {
+            f->putchar(OCTSAV_CHILDREN);
+            savec(c[i].children, co, size>>1, f, nolms);
+        }
+        else
+        {
+            int oflags = 0, surfmask = 0, totalverts = 0;
+            if(c[i].material!=MAT_AIR) oflags |= 0x40;
+            if(isempty(c[i])) f->putchar(oflags | OCTSAV_EMPTY);
+            else
+            {
+                if(!nolms)
+                {
+                    if(c[i].merged) oflags |= 0x80;
+                    if(c[i].ext) loopj(6) 
+                    {
+                        const surfaceinfo &surf = c[i].ext->surfaces[j];
+                        if(!surf.used()) continue;
+                        oflags |= 0x20; 
+                        surfmask |= 1<<j; 
+                        totalverts += surf.totalverts(); 
+                    }
+                }
+
+                if(isentirelysolid(c[i])) f->putchar(oflags | OCTSAV_SOLID);
+                else
+                {
+                    f->putchar(oflags | OCTSAV_NORMAL);
+                    f->write(c[i].edges, 12);
+                }
+            }
+    
+            loopj(6) f->putlil<ushort>(c[i].texture[j]);
+
+            if(oflags&0x40) f->putlil<ushort>(c[i].material);
+            if(oflags&0x80) f->putchar(c[i].merged);
+            if(oflags&0x20) 
+            {
+                f->putchar(surfmask);
+                f->putchar(totalverts);
+                loopj(6) if(surfmask&(1<<j))
+                {
+                    surfaceinfo surf = c[i].ext->surfaces[j];
+                    vertinfo *verts = c[i].ext->verts() + surf.verts;
+                    int layerverts = surf.numverts&MAXFACEVERTS, numverts = surf.totalverts(), 
+                        vertmask = 0, vertorder = 0, uvorder = 0,
+                        dim = dimension(j), vc = C[dim], vr = R[dim];
+                    if(numverts)
+                    {
+                        if(c[i].merged&(1<<j)) 
+                        {
+                            vertmask |= 0x04;
+                            if(layerverts == 4)
+                            {
+                                ivec v[4] = { verts[0].getxyz(), verts[1].getxyz(), verts[2].getxyz(), verts[3].getxyz() };
+                                loopk(4) 
+                                {
+                                    const ivec &v0 = v[k], &v1 = v[(k+1)&3], &v2 = v[(k+2)&3], &v3 = v[(k+3)&3];
+                                    if(v1[vc] == v0[vc] && v1[vr] == v2[vr] && v3[vc] == v2[vc] && v3[vr] == v0[vr])
+                                    {
+                                        vertmask |= 0x01;
+                                        vertorder = k;
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+                        else
+                        {
+                            int vis = visibletris(c[i], j, co, size);
+                            if(vis&4 || faceconvexity(c[i], j) < 0) vertmask |= 0x01;
+                            if(layerverts < 4 && vis&2) vertmask |= 0x02; 
+                        }
+                        bool matchnorm = true;
+                        loopk(numverts) 
+                        { 
+                            const vertinfo &v = verts[k]; 
+                            if(v.u || v.v) vertmask |= 0x40; 
+                            if(v.norm) { vertmask |= 0x80; if(v.norm != verts[0].norm) matchnorm = false; }
+                        }
+                        if(matchnorm) vertmask |= 0x08;
+                        if(vertmask&0x40 && layerverts == 4)
+                        {
+                            loopk(4)
+                            {
+                                const vertinfo &v0 = verts[k], &v1 = verts[(k+1)&3], &v2 = verts[(k+2)&3], &v3 = verts[(k+3)&3];
+                                if(v1.u == v0.u && v1.v == v2.v && v3.u == v2.u && v3.v == v0.v)
+                                {
+                                    if(surf.numverts&LAYER_DUP)
+                                    {
+                                        const vertinfo &b0 = verts[4+k], &b1 = verts[4+((k+1)&3)], &b2 = verts[4+((k+2)&3)], &b3 = verts[4+((k+3)&3)];
+                                        if(b1.u != b0.u || b1.v != b2.v || b3.u != b2.u || b3.v != b0.v)
+                                            continue;
+                                    }
+                                    uvorder = k;
+                                    vertmask |= 0x02 | (((k+4-vertorder)&3)<<4);
+                                    break;
+                                }
+                            } 
+                        }
+                    }
+                    surf.verts = vertmask;
+                    f->write(&surf, sizeof(surfaceinfo));
+                    bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0;
+                    if(layerverts == 4)
+                    {
+                        if(hasxyz && vertmask&0x01)
+                        {
+                            ivec v0 = verts[vertorder].getxyz(), v2 = verts[(vertorder+2)&3].getxyz();
+                            f->putlil<ushort>(v0[vc]); f->putlil<ushort>(v0[vr]);
+                            f->putlil<ushort>(v2[vc]); f->putlil<ushort>(v2[vr]);
+                            hasxyz = false;
+                        }
+                        if(hasuv && vertmask&0x02)
+                        {
+                            const vertinfo &v0 = verts[uvorder], &v2 = verts[(uvorder+2)&3];
+                            f->putlil<ushort>(v0.u); f->putlil<ushort>(v0.v);
+                            f->putlil<ushort>(v2.u); f->putlil<ushort>(v2.v);
+                            if(surf.numverts&LAYER_DUP)
+                            {
+                                const vertinfo &b0 = verts[4+uvorder], &b2 = verts[4+((uvorder+2)&3)];
+                                f->putlil<ushort>(b0.u); f->putlil<ushort>(b0.v);
+                                f->putlil<ushort>(b2.u); f->putlil<ushort>(b2.v);
+                            }
+                            hasuv = false;
+                        }
+                    } 
+                    if(hasnorm && vertmask&0x08) { f->putlil<ushort>(verts[0].norm); hasnorm = false; }
+                    if(hasxyz || hasuv || hasnorm) loopk(layerverts)
+                    {
+                        const vertinfo &v = verts[(k+vertorder)%layerverts];
+                        if(hasxyz) 
+                        { 
+                            ivec xyz = v.getxyz(); 
+                            f->putlil<ushort>(xyz[vc]); f->putlil<ushort>(xyz[vr]); 
+                        }
+                        if(hasuv) { f->putlil<ushort>(v.u); f->putlil<ushort>(v.v); }
+                        if(hasnorm) f->putlil<ushort>(v.norm); 
+                    }
+                    if(surf.numverts&LAYER_DUP) loopk(layerverts)
+                    {
+                        const vertinfo &v = verts[layerverts + (k+vertorder)%layerverts];
+                        if(hasuv) { f->putlil<ushort>(v.u); f->putlil<ushort>(v.v); }
+                    }
+                }
+            }
+        }
+    }
+}
+
+struct surfacecompat
+{
+    uchar texcoords[8];
+    uchar w, h;
+    ushort x, y;
+    uchar lmid, layer;
+};
+
+struct normalscompat
+{
+    bvec normals[4];
+};
+
+struct mergecompat
+{
+    ushort u1, u2, v1, v2;
+};
+
+cube *loadchildren(stream *f, const ivec &co, int size, bool &failed);
+
+void convertoldsurfaces(cube &c, const ivec &co, int size, surfacecompat *srcsurfs, int hassurfs, normalscompat *normals, int hasnorms, mergecompat *merges, int hasmerges)
+{
+    surfaceinfo dstsurfs[6];
+    vertinfo verts[6*2*MAXFACEVERTS];
+    int totalverts = 0, numsurfs = 6;
+    memset(dstsurfs, 0, sizeof(dstsurfs));
+    loopi(6) if((hassurfs|hasnorms|hasmerges)&(1<<i))
+    {
+        surfaceinfo &dst = dstsurfs[i];
+        vertinfo *curverts = NULL;
+        int numverts = 0;
+        surfacecompat *src = NULL, *blend = NULL;
+        if(hassurfs&(1<<i))
+        {
+            src = &srcsurfs[i];
+            if(src->layer&2) 
+            { 
+                blend = &srcsurfs[numsurfs++];
+                dst.lmid[0] = src->lmid;
+                dst.lmid[1] = blend->lmid;
+                dst.numverts |= LAYER_BLEND;
+                if(blend->lmid >= LMID_RESERVED && (src->x != blend->x || src->y != blend->y || src->w != blend->w || src->h != blend->h || memcmp(src->texcoords, blend->texcoords, sizeof(src->texcoords))))
+                    dst.numverts |= LAYER_DUP;
+            }
+            else if(src->layer == 1) { dst.lmid[1] = src->lmid; dst.numverts |= LAYER_BOTTOM; }
+            else { dst.lmid[0] = src->lmid; dst.numverts |= LAYER_TOP; } 
+        }
+        else dst.numverts |= LAYER_TOP;
+        bool uselms = hassurfs&(1<<i) && (dst.lmid[0] >= LMID_RESERVED || dst.lmid[1] >= LMID_RESERVED || dst.numverts&~LAYER_TOP),
+             usemerges = hasmerges&(1<<i) && merges[i].u1 < merges[i].u2 && merges[i].v1 < merges[i].v2,
+             usenorms = hasnorms&(1<<i) && normals[i].normals[0] != bvec(128, 128, 128);
+        if(uselms || usemerges || usenorms)
+        {
+            ivec v[4], pos[4], e1, e2, e3, n, vo = ivec(co).mask(0xFFF).shl(3);
+            genfaceverts(c, i, v); 
+            n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));
+            if(usemerges)
+            {
+                const mergecompat &m = merges[i];
+                int offset = -n.dot(v[0].mul(size).add(vo)),
+                    dim = dimension(i), vc = C[dim], vr = R[dim];
+                loopk(4)
+                {
+                    const ivec &coords = facecoords[i][k];
+                    int cc = coords[vc] ? m.u2 : m.u1,
+                        rc = coords[vr] ? m.v2 : m.v1,
+                        dc = n[dim] ? -(offset + n[vc]*cc + n[vr]*rc)/n[dim] : vo[dim];
+                    ivec &mv = pos[k];
+                    mv[vc] = cc;
+                    mv[vr] = rc;
+                    mv[dim] = dc;
+                }
+            }
+            else
+            {
+                int convex = (e3 = v[0]).sub(v[3]).dot(n), vis = 3;
+                if(!convex)
+                {
+                    if(ivec().cross(e3, e2).iszero()) { if(!n.iszero()) vis = 1; } 
+                    else if(n.iszero()) vis = 2;
+                }
+                int order = convex < 0 ? 1 : 0;
+                pos[0] = v[order].mul(size).add(vo);
+                pos[1] = vis&1 ? v[order+1].mul(size).add(vo) : pos[0];
+                pos[2] = v[order+2].mul(size).add(vo);
+                pos[3] = vis&2 ? v[(order+3)&3].mul(size).add(vo) : pos[0];
+            }
+            curverts = verts + totalverts;
+            loopk(4)
+            {
+                if(k > 0 && (pos[k] == pos[0] || pos[k] == pos[k-1])) continue;
+                vertinfo &dv = curverts[numverts++];
+                dv.setxyz(pos[k]);
+                if(uselms)
+                {
+                    float u = src->x + (src->texcoords[k*2] / 255.0f) * (src->w - 1),
+                          v = src->y + (src->texcoords[k*2+1] / 255.0f) * (src->h - 1);
+                    dv.u = ushort(floor(clamp((u) * float(USHRT_MAX+1)/LM_PACKW + 0.5f, 0.0f, float(USHRT_MAX))));
+                    dv.v = ushort(floor(clamp((v) * float(USHRT_MAX+1)/LM_PACKH + 0.5f, 0.0f, float(USHRT_MAX))));
+                }
+                else dv.u = dv.v = 0;
+                dv.norm = usenorms && normals[i].normals[k] != bvec(128, 128, 128) ? encodenormal(normals[i].normals[k].tonormal().normalize()) : 0;
+            }
+            dst.verts = totalverts;
+            dst.numverts |= numverts;
+            totalverts += numverts;
+            if(dst.numverts&LAYER_DUP) loopk(4)
+            {
+                if(k > 0 && (pos[k] == pos[0] || pos[k] == pos[k-1])) continue;
+                vertinfo &bv = verts[totalverts++];
+                bv.setxyz(pos[k]);
+                bv.u = ushort(floor(clamp((blend->x + (blend->texcoords[k*2] / 255.0f) * (blend->w - 1)) * float(USHRT_MAX+1)/LM_PACKW, 0.0f, float(USHRT_MAX))));
+                bv.v = ushort(floor(clamp((blend->y + (blend->texcoords[k*2+1] / 255.0f) * (blend->h - 1)) * float(USHRT_MAX+1)/LM_PACKH, 0.0f, float(USHRT_MAX))));
+                bv.norm = usenorms && normals[i].normals[k] != bvec(128, 128, 128) ? encodenormal(normals[i].normals[k].tonormal().normalize()) : 0;
+            }
+        }    
+    }
+    setsurfaces(c, dstsurfs, verts, totalverts);
+}
+
+static inline int convertoldmaterial(int mat)
+{
+    return ((mat&7)<<MATF_VOLUME_SHIFT) | (((mat>>3)&3)<<MATF_CLIP_SHIFT) | (((mat>>5)&7)<<MATF_FLAG_SHIFT);
+}
+void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed)
+{
+    bool haschildren = false;
+    int octsav = f->getchar();
+    switch(octsav&0x7)
+    {
+        case OCTSAV_CHILDREN:
+            c.children = loadchildren(f, co, size>>1, failed);
+            return;
+
+        case OCTSAV_LODCUBE: haschildren = true;    break;
+        case OCTSAV_EMPTY:  emptyfaces(c);          break;
+        case OCTSAV_SOLID:  solidfaces(c);          break;
+        case OCTSAV_NORMAL: f->read(c.edges, 12); break;
+        default: failed = true; return;
+    }
+    loopi(6) c.texture[i] = mapversion<14 ? f->getchar() : f->getlil<ushort>();
+    if(mapversion < 7) f->seek(3, SEEK_CUR);
+    else if(mapversion <= 31)
+    {
+        uchar mask = f->getchar();
+        if(mask & 0x80) 
+        {
+            int mat = f->getchar();
+            if(mapversion < 27)
+            {
+                static const ushort matconv[] = { MAT_AIR, MAT_WATER, MAT_CLIP, MAT_GLASS|MAT_CLIP, MAT_NOCLIP, MAT_LAVA|MAT_DEATH, MAT_GAMECLIP, MAT_DEATH };
+                c.material = size_t(mat) < sizeof(matconv)/sizeof(matconv[0]) ? matconv[mat] : MAT_AIR;
+            }
+            else c.material = convertoldmaterial(mat);
+        }
+        surfacecompat surfaces[12];
+        normalscompat normals[6];
+        mergecompat merges[6];
+        int hassurfs = 0, hasnorms = 0, hasmerges = 0;
+        if(mask & 0x3F)
+        {
+            int numsurfs = 6;
+            loopi(numsurfs)
+            {
+                if(i >= 6 || mask & (1 << i))
+                {
+                    f->read(&surfaces[i], sizeof(surfacecompat));
+                    lilswap(&surfaces[i].x, 2);
+                    if(mapversion < 10) ++surfaces[i].lmid;
+                    if(mapversion < 18)
+                    {
+                        if(surfaces[i].lmid >= LMID_AMBIENT1) ++surfaces[i].lmid;
+                        if(surfaces[i].lmid >= LMID_BRIGHT1) ++surfaces[i].lmid;
+                    }
+                    if(mapversion < 19)
+                    {
+                        if(surfaces[i].lmid >= LMID_DARK) surfaces[i].lmid += 2;
+                    }
+                    if(i < 6)
+                    {
+                        if(mask & 0x40) { hasnorms |= 1<<i; f->read(&normals[i], sizeof(normalscompat)); }
+                        if(surfaces[i].layer != 0 || surfaces[i].lmid != LMID_AMBIENT) 
+                            hassurfs |= 1<<i;
+                        if(surfaces[i].layer&2) numsurfs++;
+                    }
+                }
+            }
+        }
+        if(mapversion <= 8) edgespan2vectorcube(c);
+        if(mapversion <= 11)
+        {
+            swap(c.faces[0], c.faces[2]);
+            swap(c.texture[0], c.texture[4]);
+            swap(c.texture[1], c.texture[5]);
+            if(hassurfs&0x33)
+            {
+                swap(surfaces[0], surfaces[4]);
+                swap(surfaces[1], surfaces[5]);
+                hassurfs = (hassurfs&~0x33) | ((hassurfs&0x30)>>4) | ((hassurfs&0x03)<<4);
+            }
+        }
+        if(mapversion >= 20)
+        {
+            if(octsav&0x80)
+            {
+                int merged = f->getchar();
+                c.merged = merged&0x3F;
+                if(merged&0x80)
+                {
+                    int mask = f->getchar();
+                    if(mask)
+                    {
+                        hasmerges = mask&0x3F;
+                        loopi(6) if(mask&(1<<i))
+                        {
+                            mergecompat *m = &merges[i];
+                            f->read(m, sizeof(mergecompat));
+                            lilswap(&m->u1, 4);
+                            if(mapversion <= 25)
+                            {
+                                int uorigin = m->u1 & 0xE000, vorigin = m->v1 & 0xE000;
+                                m->u1 = (m->u1 - uorigin) << 2;
+                                m->u2 = (m->u2 - uorigin) << 2;
+                                m->v1 = (m->v1 - vorigin) << 2;
+                                m->v2 = (m->v2 - vorigin) << 2;
+                            }
+                        }
+                    }
+                }
+            }    
+        }                
+        if(hassurfs || hasnorms || hasmerges)
+            convertoldsurfaces(c, co, size, surfaces, hassurfs, normals, hasnorms, merges, hasmerges);
+    }
+    else
+    {
+        if(octsav&0x40) 
+        {
+            if(mapversion <= 32)
+            {
+                int mat = f->getchar();
+                c.material = convertoldmaterial(mat);
+            }
+            else c.material = f->getlil<ushort>();
+        }
+        if(octsav&0x80) c.merged = f->getchar();
+        if(octsav&0x20)
+        {
+            int surfmask, totalverts;
+            surfmask = f->getchar();
+            totalverts = max(f->getchar(), 0);
+            newcubeext(c, totalverts, false);
+            memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces));
+            memset(c.ext->verts(), 0, totalverts*sizeof(vertinfo));
+            int offset = 0;
+            loopi(6) if(surfmask&(1<<i)) 
+            {
+                surfaceinfo &surf = c.ext->surfaces[i];
+                f->read(&surf, sizeof(surfaceinfo));
+                int vertmask = surf.verts, numverts = surf.totalverts();
+                if(!numverts) { surf.verts = 0; continue; }
+                surf.verts = offset;
+                vertinfo *verts = c.ext->verts() + offset;
+                offset += numverts;
+                ivec v[4], n, vo = ivec(co).mask(0xFFF).shl(3);
+                int layerverts = surf.numverts&MAXFACEVERTS, dim = dimension(i), vc = C[dim], vr = R[dim], bias = 0;
+                genfaceverts(c, i, v);
+                bool hasxyz = (vertmask&0x04)!=0, hasuv = (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0;
+                if(hasxyz)
+                { 
+                    ivec e1, e2, e3;
+                    n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0]));   
+                    if(n.iszero()) n.cross(e2, (e3 = v[3]).sub(v[0]));
+                    bias = -n.dot(ivec(v[0]).mul(size).add(vo));
+                }
+                else
+                {
+                    int vis = layerverts < 4 ? (vertmask&0x02 ? 2 : 1) : 3, order = vertmask&0x01 ? 1 : 0, k = 0;
+                    verts[k++].setxyz(v[order].mul(size).add(vo));
+                    if(vis&1) verts[k++].setxyz(v[order+1].mul(size).add(vo));
+                    verts[k++].setxyz(v[order+2].mul(size).add(vo));
+                    if(vis&2) verts[k++].setxyz(v[(order+3)&3].mul(size).add(vo));
+                }
+                if(layerverts == 4)
+                {
+                    if(hasxyz && vertmask&0x01)
+                    {
+                        ushort c1 = f->getlil<ushort>(), r1 = f->getlil<ushort>(), c2 = f->getlil<ushort>(), r2 = f->getlil<ushort>();
+                        ivec xyz;
+                        xyz[vc] = c1; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+                        verts[0].setxyz(xyz);
+                        xyz[vc] = c1; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+                        verts[1].setxyz(xyz);
+                        xyz[vc] = c2; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+                        verts[2].setxyz(xyz);
+                        xyz[vc] = c2; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+                        verts[3].setxyz(xyz);
+                        hasxyz = false;
+                    }
+                    if(hasuv && vertmask&0x02)
+                    {
+                        int uvorder = (vertmask&0x30)>>4;
+                        vertinfo &v0 = verts[uvorder], &v1 = verts[(uvorder+1)&3], &v2 = verts[(uvorder+2)&3], &v3 = verts[(uvorder+3)&3]; 
+                        v0.u = f->getlil<ushort>(); v0.v = f->getlil<ushort>();
+                        v2.u = f->getlil<ushort>(); v2.v = f->getlil<ushort>();
+                        v1.u = v0.u; v1.v = v2.v;
+                        v3.u = v2.u; v3.v = v0.v;
+                        if(surf.numverts&LAYER_DUP)
+                        {
+                            vertinfo &b0 = verts[4+uvorder], &b1 = verts[4+((uvorder+1)&3)], &b2 = verts[4+((uvorder+2)&3)], &b3 = verts[4+((uvorder+3)&3)];
+                            b0.u = f->getlil<ushort>(); b0.v = f->getlil<ushort>();
+                            b2.u = f->getlil<ushort>(); b2.v = f->getlil<ushort>();
+                            b1.u = b0.u; b1.v = b2.v;
+                            b3.u = b2.u; b3.v = b0.v;
+                        }
+                        hasuv = false;
+                    } 
+                }
+                if(hasnorm && vertmask&0x08)
+                {
+                    ushort norm = f->getlil<ushort>();
+                    loopk(layerverts) verts[k].norm = norm;
+                    hasnorm = false;
+                }
+                if(hasxyz || hasuv || hasnorm) loopk(layerverts)
+                {
+                    vertinfo &v = verts[k];
+                    if(hasxyz)
+                    {
+                        ivec xyz;
+                        xyz[vc] = f->getlil<ushort>(); xyz[vr] = f->getlil<ushort>();
+                        xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim];
+                        v.setxyz(xyz);
+                    }
+                    if(hasuv) { v.u = f->getlil<ushort>(); v.v = f->getlil<ushort>(); }    
+                    if(hasnorm) v.norm = f->getlil<ushort>();
+                }
+                if(surf.numverts&LAYER_DUP) loopk(layerverts)
+                {
+                    vertinfo &v = verts[k+layerverts], &t = verts[k];
+                    v.setxyz(t.x, t.y, t.z);
+                    if(hasuv) { v.u = f->getlil<ushort>(); v.v = f->getlil<ushort>(); }
+                    v.norm = t.norm;
+                }
+            }
+        }    
+    }
+
+    c.children = (haschildren ? loadchildren(f, co, size>>1, failed) : NULL);
+}
+
+cube *loadchildren(stream *f, const ivec &co, int size, bool &failed)
+{
+    cube *c = newcubes();
+    loopi(8) 
+    {
+        loadc(f, c[i], ivec(i, co, size), size, failed);
+        if(failed) break;
+    }
+    return c;
+}
+
+VAR(dbgvars, 0, 0, 1);
+
+void savevslot(stream *f, VSlot &vs, int prev)
+{
+    f->putlil<int>(vs.changed);
+    f->putlil<int>(prev);
+    if(vs.changed & (1<<VSLOT_SHPARAM))
+    {
+        f->putlil<ushort>(vs.params.length());
+        loopv(vs.params)
+        {
+            SlotShaderParam &p = vs.params[i];
+            f->putlil<ushort>(strlen(p.name));
+            f->write(p.name, strlen(p.name));
+            loopk(4) f->putlil<float>(p.val[k]);
+        }
+    }
+    if(vs.changed & (1<<VSLOT_SCALE)) f->putlil<float>(vs.scale);
+    if(vs.changed & (1<<VSLOT_ROTATION)) f->putlil<int>(vs.rotation);
+    if(vs.changed & (1<<VSLOT_OFFSET))
+    {
+        f->putlil<int>(vs.offset.x);
+        f->putlil<int>(vs.offset.y);
+    }
+    if(vs.changed & (1<<VSLOT_SCROLL))
+    {
+        f->putlil<float>(vs.scroll.x);
+        f->putlil<float>(vs.scroll.y);
+    }
+    if(vs.changed & (1<<VSLOT_LAYER)) f->putlil<int>(vs.layer);
+    if(vs.changed & (1<<VSLOT_ALPHA))
+    {
+        f->putlil<float>(vs.alphafront);
+        f->putlil<float>(vs.alphaback);
+    }
+    if(vs.changed & (1<<VSLOT_COLOR)) 
+    {
+        loopk(3) f->putlil<float>(vs.colorscale[k]);
+    }
+}
+
+void savevslots(stream *f, int numvslots)
+{
+    if(vslots.empty()) return;
+    int *prev = new int[numvslots];
+    memset(prev, -1, numvslots*sizeof(int));
+    loopi(numvslots)
+    {
+        VSlot *vs = vslots[i];
+        if(vs->changed) continue;
+        for(;;)
+        {
+            VSlot *cur = vs;
+            do vs = vs->next; while(vs && vs->index >= numvslots);
+            if(!vs) break;
+            prev[vs->index] = cur->index;
+        } 
+    }
+    int lastroot = 0;
+    loopi(numvslots)
+    {
+        VSlot &vs = *vslots[i];
+        if(!vs.changed) continue;
+        if(lastroot < i) f->putlil<int>(-(i - lastroot));
+        savevslot(f, vs, prev[i]);
+        lastroot = i+1;
+    }
+    if(lastroot < numvslots) f->putlil<int>(-(numvslots - lastroot));
+    delete[] prev;
+}
+            
+void loadvslot(stream *f, VSlot &vs, int changed)
+{
+    vs.changed = changed;
+    if(vs.changed & (1<<VSLOT_SHPARAM))
+    {
+        int numparams = f->getlil<ushort>();
+        string name;
+        loopi(numparams)
+        {
+            SlotShaderParam &p = vs.params.add();
+            int nlen = f->getlil<ushort>();
+            f->read(name, min(nlen, MAXSTRLEN-1));
+            name[min(nlen, MAXSTRLEN-1)] = '\0';
+            if(nlen >= MAXSTRLEN) f->seek(nlen - (MAXSTRLEN-1), SEEK_CUR);
+            p.name = getshaderparamname(name);
+            p.loc = -1;
+            loopk(4) p.val[k] = f->getlil<float>();
+        }
+    }
+    if(vs.changed & (1<<VSLOT_SCALE)) vs.scale = f->getlil<float>();
+    if(vs.changed & (1<<VSLOT_ROTATION)) vs.rotation = clamp(f->getlil<int>(), 0, 7);
+    if(vs.changed & (1<<VSLOT_OFFSET))
+    {
+        vs.offset.x = f->getlil<int>();
+        vs.offset.y = f->getlil<int>();
+    }
+    if(vs.changed & (1<<VSLOT_SCROLL))
+    {
+        vs.scroll.x = f->getlil<float>();
+        vs.scroll.y = f->getlil<float>();
+    }
+    if(vs.changed & (1<<VSLOT_LAYER)) vs.layer = f->getlil<int>();
+    if(vs.changed & (1<<VSLOT_ALPHA))
+    {
+        vs.alphafront = f->getlil<float>();
+        vs.alphaback = f->getlil<float>();
+    }
+    if(vs.changed & (1<<VSLOT_COLOR)) 
+    {
+        loopk(3) vs.colorscale[k] = f->getlil<float>();
+    }
+}
+
+void loadvslots(stream *f, int numvslots)
+{
+    int *prev = new (false) int[numvslots];
+    if(!prev) return;
+    memset(prev, -1, numvslots*sizeof(int));
+    while(numvslots > 0)
+    {
+        int changed = f->getlil<int>();
+        if(changed < 0)
+        {
+            loopi(-changed) vslots.add(new VSlot(NULL, vslots.length()));
+            numvslots += changed;
+        }
+        else
+        {
+            prev[vslots.length()] = f->getlil<int>();
+            loadvslot(f, *vslots.add(new VSlot(NULL, vslots.length())), changed);    
+            numvslots--;
+        }
+    }
+    loopv(vslots) if(vslots.inrange(prev[i])) vslots[prev[i]]->next = vslots[i];
+    delete[] prev;
+}
+
+bool save_world(const char *mname, bool nolms)
+{
+    if(!*mname) mname = game::getclientmap();
+    setmapfilenames(mname);
+    if(savebak) backup(ogzname, bakname);
+    stream *f = opengzfile(ogzname, "wb");
+    if(!f) { conoutf(CON_WARN, "could not write map to %s", ogzname); return false; }
+
+    int numvslots = vslots.length();
+    if(!nolms && !multiplayer(false))
+    {
+        numvslots = compactvslots();
+        allchanged();
+    }
+
+    savemapprogress = 0;
+    renderprogress(0, "saving map...");
+
+    octaheader hdr;
+    memcpy(hdr.magic, "OCTA", 4);
+    hdr.version = MAPVERSION;
+    hdr.headersize = sizeof(hdr);
+    hdr.worldsize = worldsize;
+    hdr.numents = 0;
+    const vector<extentity *> &ents = entities::getents();
+    loopv(ents) if(ents[i]->type!=ET_EMPTY || nolms) hdr.numents++;
+    hdr.numpvs = nolms ? 0 : getnumviewcells();
+    hdr.lightmaps = nolms ? 0 : lightmaps.length();
+    hdr.blendmap = shouldsaveblendmap();
+    hdr.numvars = 0;
+    hdr.numvslots = numvslots;
+    enumerate(idents, ident, id, 
+    {
+        if((id.type == ID_VAR || id.type == ID_FVAR || id.type == ID_SVAR) && id.flags&IDF_OVERRIDE && !(id.flags&IDF_READONLY) && id.flags&IDF_OVERRIDDEN) hdr.numvars++;
+    });
+    lilswap(&hdr.version, 9);
+    f->write(&hdr, sizeof(hdr));
+   
+    enumerate(idents, ident, id, 
+    {
+        if((id.type!=ID_VAR && id.type!=ID_FVAR && id.type!=ID_SVAR) || !(id.flags&IDF_OVERRIDE) || id.flags&IDF_READONLY || !(id.flags&IDF_OVERRIDDEN)) continue;
+        f->putchar(id.type);
+        f->putlil<ushort>(strlen(id.name));
+        f->write(id.name, strlen(id.name));
+        switch(id.type)
+        {
+            case ID_VAR:
+                if(dbgvars) conoutf(CON_DEBUG, "wrote var %s: %d", id.name, *id.storage.i);
+                f->putlil<int>(*id.storage.i);
+                break;
+
+            case ID_FVAR:
+                if(dbgvars) conoutf(CON_DEBUG, "wrote fvar %s: %f", id.name, *id.storage.f);
+                f->putlil<float>(*id.storage.f);
+                break;
+
+            case ID_SVAR:
+                if(dbgvars) conoutf(CON_DEBUG, "wrote svar %s: %s", id.name, *id.storage.s);
+                f->putlil<ushort>(strlen(*id.storage.s));
+                f->write(*id.storage.s, strlen(*id.storage.s));
+                break;
+        }
+    });
+
+    if(dbgvars) conoutf(CON_DEBUG, "wrote %d vars", hdr.numvars);
+
+    f->putchar((int)strlen(game::gameident()));
+    f->write(game::gameident(), (int)strlen(game::gameident())+1);
+    f->putlil<ushort>(entities::extraentinfosize());
+    vector<char> extras;
+    game::writegamedata(extras);
+    f->putlil<ushort>(extras.length());
+    f->write(extras.getbuf(), extras.length());
+    
+    f->putlil<ushort>(texmru.length());
+    loopv(texmru) f->putlil<ushort>(texmru[i]);
+    char *ebuf = new char[entities::extraentinfosize()];
+    loopv(ents)
+    {
+        if(ents[i]->type!=ET_EMPTY || nolms)
+        {
+            entity tmp = *ents[i];
+            lilswap(&tmp.o.x, 3);
+            lilswap(&tmp.attr1, 5);
+            f->write(&tmp, sizeof(entity));
+            entities::writeent(*ents[i], ebuf);
+            if(entities::extraentinfosize()) f->write(ebuf, entities::extraentinfosize());
+        }
+    }
+    delete[] ebuf;
+
+    savevslots(f, numvslots);
+
+    renderprogress(0, "saving octree...");
+    savec(worldroot, ivec(0, 0, 0), worldsize>>1, f, nolms);
+
+    if(!nolms) 
+    {
+        if(lightmaps.length()) renderprogress(0, "saving lightmaps...");
+        loopv(lightmaps)
+        {
+            LightMap &lm = lightmaps[i];
+            f->putchar(lm.type | (lm.unlitx>=0 ? 0x80 : 0));
+            if(lm.unlitx>=0)
+            {
+                f->putlil<ushort>(ushort(lm.unlitx));
+                f->putlil<ushort>(ushort(lm.unlity));
+            }
+            f->write(lm.data, lm.bpp*LM_PACKW*LM_PACKH);
+            renderprogress(float(i+1)/lightmaps.length(), "saving lightmaps...");
+        }
+        if(getnumviewcells()>0) { renderprogress(0, "saving pvs..."); savepvs(f); }
+    }
+    if(shouldsaveblendmap()) { renderprogress(0, "saving blendmap..."); saveblendmap(f); }
+
+    delete f;
+    conoutf("wrote map file %s", ogzname);
+    return true;
+}
+
+static uint mapcrc = 0;
+
+uint getmapcrc() { return mapcrc; }
+void clearmapcrc() { mapcrc = 0; }
+
+bool load_world(const char *mname, const char *cname)        // still supports all map formats that have existed since the earliest cube betas!
+{
+    int loadingstart = SDL_GetTicks();
+    setmapfilenames(mname, cname);
+    stream *f = opengzfile(ogzname, "rb");
+    if(!f) { conoutf(CON_ERROR, "could not read map %s", ogzname); return false; }
+    octaheader hdr;
+    if(f->read(&hdr, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    lilswap(&hdr.version, 6);
+    if(memcmp(hdr.magic, "OCTA", 4) || hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Cube 2: Sauerbraten", ogzname); delete f; return false; }
+    compatheader chdr;
+    if(hdr.version <= 28)
+    {
+        if(f->read(&chdr.lightprecision, sizeof(chdr) - 7*sizeof(int)) != sizeof(chdr) - 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    }
+    else 
+    {
+        int extra = 0;
+        if(hdr.version <= 29) extra++; 
+        if(f->read(&hdr.blendmap, sizeof(hdr) - (7+extra)*sizeof(int)) != sizeof(hdr) - (7+extra)*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); delete f; return false; }
+    }
+
+    resetmap();
+
+    Texture *mapshot = textureload(picname, 3, true, false);
+    renderbackground("loading...", mapshot, mname, game::getmapinfo());
+
+    game::loadingmap(cname ? cname : mname);
+
+    setvar("mapversion", hdr.version, true, false);
+
+    if(hdr.version <= 28)
+    {
+        lilswap(&chdr.lightprecision, 3);
+        if(chdr.lightprecision) setvar("lightprecision", chdr.lightprecision);
+        if(chdr.lighterror) setvar("lighterror", chdr.lighterror);
+        if(chdr.bumperror) setvar("bumperror", chdr.bumperror);
+        setvar("lightlod", chdr.lightlod);
+        if(chdr.ambient) setvar("ambient", chdr.ambient);
+        setvar("skylight", (int(chdr.skylight[0])<<16) | (int(chdr.skylight[1])<<8) | int(chdr.skylight[2]));
+        setvar("watercolour", (int(chdr.watercolour[0])<<16) | (int(chdr.watercolour[1])<<8) | int(chdr.watercolour[2]), true);
+        setvar("waterfallcolour", (int(chdr.waterfallcolour[0])<<16) | (int(chdr.waterfallcolour[1])<<8) | int(chdr.waterfallcolour[2]));
+        setvar("lavacolour", (int(chdr.lavacolour[0])<<16) | (int(chdr.lavacolour[1])<<8) | int(chdr.lavacolour[2]));
+        setvar("fullbright", 0, true);
+        if(chdr.lerpsubdivsize || chdr.lerpangle) setvar("lerpangle", chdr.lerpangle);
+        if(chdr.lerpsubdivsize)
+        {
+            setvar("lerpsubdiv", chdr.lerpsubdiv);
+            setvar("lerpsubdivsize", chdr.lerpsubdivsize);
+        }
+        setsvar("maptitle", chdr.maptitle);
+        hdr.blendmap = chdr.blendmap;
+        hdr.numvars = 0; 
+        hdr.numvslots = 0;
+    }
+    else 
+    {
+        lilswap(&hdr.blendmap, 2);
+        if(hdr.version <= 29) hdr.numvslots = 0;
+        else lilswap(&hdr.numvslots, 1);
+    }
+
+    renderprogress(0, "clearing world...");
+
+    freeocta(worldroot);
+    worldroot = NULL;
+
+    int worldscale = 0;
+    while(1<<worldscale < hdr.worldsize) worldscale++;
+    setvar("mapsize", 1<<worldscale, true, false);
+    setvar("mapscale", worldscale, true, false);
+
+    renderprogress(0, "loading vars...");
+    loopi(hdr.numvars)
+    {
+        int type = f->getchar(), ilen = f->getlil<ushort>();
+        string name;
+        f->read(name, min(ilen, MAXSTRLEN-1));
+        name[min(ilen, MAXSTRLEN-1)] = '\0';
+        if(ilen >= MAXSTRLEN) f->seek(ilen - (MAXSTRLEN-1), SEEK_CUR);
+        ident *id = getident(name);
+        bool exists = id && id->type == type && id->flags&IDF_OVERRIDE;
+        switch(type)
+        {
+            case ID_VAR:
+            {
+                int val = f->getlil<int>();
+                if(exists && id->minval <= id->maxval) setvar(name, val);
+                if(dbgvars) conoutf(CON_DEBUG, "read var %s: %d", name, val);
+                break;
+            }
+            case ID_FVAR:
+            {
+                float val = f->getlil<float>();
+                if(exists && id->minvalf <= id->maxvalf) setfvar(name, val);
+                if(dbgvars) conoutf(CON_DEBUG, "read fvar %s: %f", name, val);
+                break;
+            }
+    
+            case ID_SVAR:
+            {
+                int slen = f->getlil<ushort>();
+                string val;
+                f->read(val, min(slen, MAXSTRLEN-1));
+                val[min(slen, MAXSTRLEN-1)] = '\0';
+                if(slen >= MAXSTRLEN) f->seek(slen - (MAXSTRLEN-1), SEEK_CUR);
+                if(exists) setsvar(name, val);
+                if(dbgvars) conoutf(CON_DEBUG, "read svar %s: %s", name, val);
+                break;
+            }
+        }
+    }
+    if(dbgvars) conoutf(CON_DEBUG, "read %d vars", hdr.numvars);
+
+    string gametype;
+    copystring(gametype, "fps");
+    bool samegame = true;
+    int eif = 0;
+    if(hdr.version>=16)
+    {
+        int len = f->getchar();
+        f->read(gametype, len+1);
+    }
+    if(strcmp(gametype, game::gameident())!=0)
+    {
+        samegame = false;
+        conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype);
+    }
+    if(hdr.version>=16)
+    {
+        eif = f->getlil<ushort>();
+        int extrasize = f->getlil<ushort>();
+        vector<char> extras;
+        f->read(extras.pad(extrasize), extrasize);
+        if(samegame) game::readgamedata(extras);
+    }
+   
+    texmru.shrink(0);
+    if(hdr.version<14)
+    {
+        uchar oldtl[256];
+        f->read(oldtl, sizeof(oldtl));
+        loopi(256) texmru.add(oldtl[i]);
+    }
+    else
+    {
+        ushort nummru = f->getlil<ushort>();
+        loopi(nummru) texmru.add(f->getlil<ushort>());
+    }
+
+    renderprogress(0, "loading entities...");
+
+    vector<extentity *> &ents = entities::getents();
+    int einfosize = entities::extraentinfosize();
+    char *ebuf = einfosize > 0 ? new char[einfosize] : NULL;
+    loopi(min(hdr.numents, MAXENTS))
+    {
+        extentity &e = *entities::newentity();
+        ents.add(&e);
+        f->read(&e, sizeof(entity));
+        lilswap(&e.o.x, 3);
+        lilswap(&e.attr1, 5);
+        fixent(e, hdr.version);
+        if(samegame)
+        {
+            if(einfosize > 0) f->read(ebuf, einfosize);
+            entities::readent(e, ebuf, mapversion);
+        }
+        else
+        {
+            if(eif > 0) f->seek(eif, SEEK_CUR);
+            if(e.type>=ET_GAMESPECIFIC || hdr.version<=14)
+            {
+                entities::deleteentity(ents.pop());
+                continue;
+            }
+        }
+        if(!insideworld(e.o))
+        {
+            if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT)
+            {
+                conoutf(CON_WARN, "warning: ent outside of world: enttype[%s] index %d (%f, %f, %f)", entities::entname(e.type), i, e.o.x, e.o.y, e.o.z);
+            }
+        }
+        if(hdr.version <= 14 && e.type == ET_MAPMODEL)
+        {
+            e.o.z += e.attr3;
+            if(e.attr4) conoutf(CON_WARN, "warning: mapmodel ent (index %d) uses texture slot %d", i, e.attr4);
+            e.attr3 = e.attr4 = 0;
+        }
+    }
+    if(ebuf) delete[] ebuf;
+
+    if(hdr.numents > MAXENTS) 
+    {
+        conoutf(CON_WARN, "warning: map has %d entities", hdr.numents);
+        f->seek((hdr.numents-MAXENTS)*(samegame ? sizeof(entity) + einfosize : eif), SEEK_CUR);
+    }
+
+    renderprogress(0, "loading slots...");
+    loadvslots(f, hdr.numvslots);
+
+    renderprogress(0, "loading octree...");
+    bool failed = false;
+    worldroot = loadchildren(f, ivec(0, 0, 0), hdr.worldsize>>1, failed);
+    if(failed) conoutf(CON_ERROR, "garbage in map");
+
+    renderprogress(0, "validating...");
+    validatec(worldroot, hdr.worldsize>>1);
+
+    if(!failed)
+    {
+        if(hdr.version >= 7) loopi(hdr.lightmaps)
+        {
+            renderprogress(i/(float)hdr.lightmaps, "loading lightmaps...");
+            LightMap &lm = lightmaps.add();
+            if(hdr.version >= 17)
+            {
+                int type = f->getchar();
+                lm.type = type&0x7F;
+                if(hdr.version >= 20 && type&0x80)
+                {
+                    lm.unlitx = f->getlil<ushort>();
+                    lm.unlity = f->getlil<ushort>();
+                }
+            }
+            if(lm.type&LM_ALPHA && (lm.type&LM_TYPE)!=LM_BUMPMAP1) lm.bpp = 4;
+            lm.data = new uchar[lm.bpp*LM_PACKW*LM_PACKH];
+            f->read(lm.data, lm.bpp * LM_PACKW * LM_PACKH);
+            lm.finalize();
+        }
+
+        if(hdr.version >= 25 && hdr.numpvs > 0) loadpvs(f, hdr.numpvs);
+        if(hdr.version >= 28 && hdr.blendmap) loadblendmap(f, hdr.blendmap);
+    }
+
+    mapcrc = f->getcrc();
+    delete f;
+
+    conoutf("read map %s (%.1f seconds)", ogzname, (SDL_GetTicks()-loadingstart)/1000.0f);
+
+    clearmainmenu();
+
+    identflags |= IDF_OVERRIDDEN;
+    execfile("data/default_map_settings.cfg", false);
+    execfile(cfgname, false);
+    identflags &= ~IDF_OVERRIDDEN;
+   
+    extern void fixlightmapnormals();
+    if(hdr.version <= 25) fixlightmapnormals();
+    extern void fixrotatedlightmaps();
+    if(hdr.version <= 31) fixrotatedlightmaps();
+
+    preloadusedmapmodels(true);
+
+    game::preload();
+    flushpreloadedmodels();
+
+    preloadmapsounds();
+
+    entitiesinoctanodes();
+    attachentities();
+    initlights();
+    allchanged(true);
+
+    renderbackground("loading...", mapshot, mname, game::getmapinfo());
+
+    if(maptitle[0] && strcmp(maptitle, "Untitled Map by Unknown")) conoutf(CON_ECHO, "%s", maptitle);
+
+    startmap(cname ? cname : mname);
+    
+    return true;
+}
+
+void savecurrentmap() { save_world(game::getclientmap()); }
+void savemap(char *mname) { save_world(mname); }
+
+COMMAND(savemap, "s");
+COMMAND(savecurrentmap, "");
+
+void writeobj(char *name)
+{
+    defformatstring(fname, "%s.obj", name);
+    stream *f = openfile(path(fname), "w"); 
+    if(!f) return;
+    f->printf("# obj file of Cube 2 level\n\n");
+    defformatstring(mtlname, "%s.mtl", name);
+    path(mtlname);
+    f->printf("mtllib %s\n\n", mtlname); 
+    vector<vec> verts;
+    vector<vec2> texcoords;
+    hashtable<vec, int> shareverts(1<<16);
+    hashtable<vec2, int> sharetc(1<<16);
+    hashtable<int, vector<ivec2> > mtls(1<<8);
+    vector<int> usedmtl;
+    vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f);
+    loopv(valist)
+    {
+        vtxarray &va = *valist[i];
+        ushort *edata = NULL;
+        vertex *vdata = NULL;
+        if(!readva(&va, edata, vdata)) continue;
+        ushort *idx = edata;
+        loopj(va.texs)
+        {
+            elementset &es = va.eslist[j];
+            if(usedmtl.find(es.texture) < 0) usedmtl.add(es.texture);
+            vector<ivec2> &keys = mtls[es.texture];
+            loopk(es.length[1])
+            {
+                int n = idx[k] - va.voffset;
+                const vertex &v = vdata[n];
+                const vec &pos = v.pos;
+                const vec2 &tc = v.tc;
+                ivec2 &key = keys.add();
+                key.x = shareverts.access(pos, verts.length());
+                if(key.x == verts.length()) 
+                {
+                    verts.add(pos);
+                    loopl(3)
+                    {
+                        bbmin[l] = min(bbmin[l], pos[l]);
+                        bbmax[l] = max(bbmax[l], pos[l]);
+                    }
+                }
+                key.y = sharetc.access(tc, texcoords.length());
+                if(key.y == texcoords.length()) texcoords.add(tc);
+            }
+            idx += es.length[1];
+        }
+        delete[] edata;
+        delete[] vdata;
+    }
+
+    vec center(-(bbmax.x + bbmin.x)/2, -(bbmax.y + bbmin.y)/2, -bbmin.z);
+    loopv(verts)
+    {
+        vec v = verts[i];
+        v.add(center);
+        if(v.y != floor(v.y)) f->printf("v %.3f ", -v.y); else f->printf("v %d ", int(-v.y));
+        if(v.z != floor(v.z)) f->printf("%.3f ", v.z); else f->printf("%d ", int(v.z));
+        if(v.x != floor(v.x)) f->printf("%.3f\n", v.x); else f->printf("%d\n", int(v.x));
+    } 
+    f->printf("\n");
+    loopv(texcoords)
+    {
+        const vec2 &tc = texcoords[i];
+        f->printf("vt %.6f %.6f\n", tc.x, 1-tc.y);  
+    }
+    f->printf("\n");
+
+    usedmtl.sort();
+    loopv(usedmtl)
+    {
+        vector<ivec2> &keys = mtls[usedmtl[i]];
+        f->printf("g slot%d\n", usedmtl[i]);
+        f->printf("usemtl slot%d\n\n", usedmtl[i]);
+        for(int i = 0; i < keys.length(); i += 3)
+        {
+            f->printf("f");
+            loopk(3) f->printf(" %d/%d", keys[i+2-k].x+1, keys[i+2-k].y+1);
+            f->printf("\n");
+        }
+        f->printf("\n");
+    }
+    delete f;
+
+    f = openfile(mtlname, "w");
+    if(!f) return;
+    f->printf("# mtl file of Cube 2 level\n\n");
+    loopv(usedmtl)
+    {
+        VSlot &vslot = lookupvslot(usedmtl[i], false);
+        f->printf("newmtl slot%d\n", usedmtl[i]);
+        f->printf("map_Kd %s\n", vslot.slot->sts.empty() ? notexture->name : path(makerelpath("packages", vslot.slot->sts[0].name)));
+        f->printf("\n");
+    } 
+    delete f;
+}  
+    
+COMMAND(writeobj, "s"); 
+
+#endif
+
diff --git a/src/fpsgame/ai.cpp b/src/fpsgame/ai.cpp
new file mode 100644 (file)
index 0000000..cfe3502
--- /dev/null
@@ -0,0 +1,1467 @@
+#include "game.h"
+
+extern int fog;
+
+namespace ai
+{
+    using namespace game;
+
+    avoidset obstacles;
+    int updatemillis = 0, iteration = 0, itermillis = 0, forcegun = -1;
+    vec aitarget(0, 0, 0);
+
+    VAR(aidebug, 0, 0, 6);
+    VAR(aiforcegun, -1, -1, NUMGUNS-1);
+
+    ICOMMAND(addbot, "s", (char *s), addmsg(N_ADDBOT, "ri", *s ? clamp(parseint(s), 1, 101) : -1));
+    ICOMMAND(delbot, "", (), addmsg(N_DELBOT, "r"));
+    ICOMMAND(botlimit, "i", (int *n), addmsg(N_BOTLIMIT, "ri", *n));
+    ICOMMAND(botbalance, "i", (int *n), addmsg(N_BOTBALANCE, "ri", *n));
+
+    float viewdist(int x)
+    {
+        return x <= 100 ? clamp((SIGHTMIN+(SIGHTMAX-SIGHTMIN))/100.f*float(x), float(SIGHTMIN), float(fog)) : float(fog);
+    }
+
+    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;
+    }
+
+    bool canmove(fpsent *d)
+    {
+        return d->state != CS_DEAD && !intermission;
+    }
+
+    float weapmindist(int weap)
+    {
+        return max(int(guns[weap].exprad), 2);
+    }
+
+    float weapmaxdist(int weap)
+    {
+        return guns[weap].range + 4;
+    }
+
+    bool weaprange(fpsent *d, int weap, float dist)
+    {
+        float mindist = weapmindist(weap), maxdist = weapmaxdist(weap);
+        return dist >= mindist*mindist && dist <= maxdist*maxdist;
+    }
+
+    bool targetable(fpsent *d, fpsent *e)
+    {
+        if(d == e || !canmove(d)) return false;
+        return e->state == CS_ALIVE && !isteam(d->team, e->team);
+    }
+
+    bool getsight(vec &o, float yaw, float pitch, vec &q, vec &v, float mdist, float fovx, float fovy)
+    {
+        float dist = o.dist(q);
+
+        if(dist <= mdist)
+        {
+            float x = fmod(fabs(asin((q.z-o.z)/dist)/RAD-pitch), 360);
+            float y = fmod(fabs(-atan2(q.x-o.x, q.y-o.y)/RAD-yaw), 360);
+            if(min(x, 360-x) <= fovx && min(y, 360-y) <= fovy) return raycubelos(o, q, v);
+        }
+        return false;
+    }
+
+    bool cansee(fpsent *d, vec &x, vec &y, vec &targ)
+    {
+        aistate &b = d->ai->getstate();
+        if(canmove(d) && b.type != AI_S_WAIT)
+            return getsight(x, d->yaw, d->pitch, y, targ, d->ai->views[2], d->ai->views[0], d->ai->views[1]);
+        return false;
+    }
+
+    bool canshoot(fpsent *d, fpsent *e)
+    {
+        if(weaprange(d, d->gunselect, e->o.squaredist(d->o)) && targetable(d, e))
+            return d->ammo[d->gunselect] > 0 && lastmillis - d->lastaction >= d->gunwait;
+        return false;
+    }
+
+    bool canshoot(fpsent *d)
+    {
+        return !d->ai->becareful && d->ammo[d->gunselect] > 0 && lastmillis - d->lastaction >= d->gunwait;
+    }
+
+       bool hastarget(fpsent *d, aistate &b, fpsent *e, float yaw, float pitch, float dist)
+       { // add margins of error
+        if(weaprange(d, d->gunselect, dist) || (d->skill <= 100 && !rnd(d->skill)))
+        {
+            if(d->gunselect == GUN_FIST) return true;
+                       float skew = clamp(float(lastmillis-d->ai->enemymillis)/float((d->skill*guns[d->gunselect].attackdelay/200.f)), 0.f, guns[d->gunselect].projspeed ? 0.25f : 1e16f),
+                offy = yaw-d->yaw, offp = pitch-d->pitch;
+            if(offy > 180) offy -= 360;
+            else if(offy < -180) offy += 360;
+            if(fabs(offy) <= d->ai->views[0]*skew && fabs(offp) <= d->ai->views[1]*skew) return true;
+        }
+        return false;
+       }
+
+    vec getaimpos(fpsent *d, fpsent *e)
+    {
+        vec o = e->o;
+        if(d->gunselect == GUN_RL) o.z += (e->aboveeye*0.2f)-(0.8f*d->eyeheight);
+        else if(d->gunselect != GUN_GL) o.z += (e->aboveeye-e->eyeheight)*0.5f;
+        if(d->skill <= 100)
+        {
+            if(lastmillis >= d->ai->lastaimrnd)
+            {
+                const int aiskew[NUMGUNS] = { 1, 10, 50, 5, 20, 1, 100, 10, 10, 10, 1, 1 };
+                #define rndaioffset(r) ((rnd(int(r*aiskew[d->gunselect]*2)+1)-(r*aiskew[d->gunselect]))*(1.f/float(max(d->skill, 1))))
+                loopk(3) d->ai->aimrnd[k] = rndaioffset(e->radius);
+                int dur = (d->skill+10)*10;
+                d->ai->lastaimrnd = lastmillis+dur+rnd(dur);
+            }
+            loopk(3) o[k] += d->ai->aimrnd[k];
+        }
+        return o;
+    }
+
+    void create(fpsent *d)
+    {
+        if(!d->ai) d->ai = new aiinfo;
+    }
+
+    void destroy(fpsent *d)
+    {
+        if(d->ai) DELETEP(d->ai);
+    }
+
+    void init(fpsent *d, int at, int ocn, int sk, int bn, int pm, const char *name, const char *team)
+    {
+        loadwaypoints();
+
+        fpsent *o = newclient(ocn);
+
+        d->aitype = at;
+
+        bool resetthisguy = false;
+        if(!d->name[0])
+        {
+            if(aidebug) conoutf(CON_DEBUG, "%s assigned to %s at skill %d", colorname(d, name), o ? colorname(o) : "?", sk);
+            else conoutf("\f0join:\f7 %s", colorname(d, name));
+            resetthisguy = true;
+        }
+        else
+        {
+            if(d->ownernum != ocn)
+            {
+                if(aidebug) conoutf(CON_DEBUG, "%s reassigned to %s", colorname(d, name), o ? colorname(o) : "?");
+                resetthisguy = true;
+            }
+            if(d->skill != sk && aidebug) conoutf(CON_DEBUG, "%s changed skill to %d", colorname(d, name), sk);
+        }
+
+        copystring(d->name, name, MAXNAMELEN+1);
+        copystring(d->team, team, MAXTEAMLEN+1);
+        d->ownernum = ocn;
+        d->plag = 0;
+        d->skill = sk;
+        d->playermodel = chooserandomplayermodel(pm);
+
+        if(resetthisguy) removeweapons(d);
+        if(d->ownernum >= 0 && player1->clientnum == d->ownernum)
+        {
+            create(d);
+            if(d->ai)
+            {
+                d->ai->views[0] = viewfieldx(d->skill);
+                d->ai->views[1] = viewfieldy(d->skill);
+                d->ai->views[2] = viewdist(d->skill);
+            }
+        }
+        else if(d->ai) destroy(d);
+    }
+
+    void update()
+    {
+        if(intermission) { loopv(players) if(players[i]->ai) players[i]->stopmoving(); }
+        else // fixed rate logic done out-of-sequence at 1 frame per second for each ai
+        {
+            if(totalmillis-updatemillis > 1000)
+            {
+                avoid();
+                forcegun = multiplayer(false) ? -1 : aiforcegun;
+                updatemillis = totalmillis;
+            }
+            if(!iteration && totalmillis-itermillis > 1000)
+            {
+                iteration = 1;
+                itermillis = totalmillis;
+            }
+            int count = 0;
+            loopv(players) if(players[i]->ai) think(players[i], ++count == iteration ? true : false);
+            if(++iteration > count) iteration = 0;
+        }
+    }
+
+    bool checkothers(vector<int> &targets, fpsent *d, int state, int targtype, int target, bool teams, int *members)
+    { // checks the states of other ai for a match
+        targets.setsize(0);
+        loopv(players)
+        {
+            fpsent *e = players[i];
+            if(targets.find(e->clientnum) >= 0) continue;
+            if(teams && d && !isteam(d->team, e->team)) continue;
+            if(members) (*members)++;
+            if(e == d || !e->ai || e->state != CS_ALIVE) continue;
+            aistate &b = e->ai->getstate();
+            if(state >= 0 && b.type != state) continue;
+            if(target >= 0 && b.target != target) continue;
+            if(targtype >=0 && b.targtype != targtype) continue;
+            targets.add(e->clientnum);
+        }
+        return !targets.empty();
+    }
+
+    bool makeroute(fpsent *d, aistate &b, int node, bool changed, int retries)
+    {
+        if(!iswaypoint(d->lastnode)) return false;
+               if(changed && d->ai->route.length() > 1 && d->ai->route[0] == node) return true;
+               if(route(d, d->lastnode, node, d->ai->route, obstacles, retries))
+               {
+                       b.override = false;
+                       return true;
+               }
+        // retry fails: 0 = first attempt, 1 = try ignoring obstacles, 2 = try ignoring prevnodes too
+               if(retries <= 1) return makeroute(d, b, node, false, retries+1);
+               return false;
+    }
+
+    bool makeroute(fpsent *d, aistate &b, const vec &pos, bool changed, int retries)
+    {
+        int node = closestwaypoint(pos, SIGHTMIN, true);
+        return makeroute(d, b, node, changed, retries);
+    }
+
+    bool randomnode(fpsent *d, aistate &b, const vec &pos, float guard, float wander)
+    {
+        static vector<int> candidates;
+        candidates.setsize(0);
+        findwaypointswithin(pos, guard, wander, candidates);
+
+        while(!candidates.empty())
+        {
+            int w = rnd(candidates.length()), n = candidates.removeunordered(w);
+            if(n != d->lastnode && !d->ai->hasprevnode(n) && !obstacles.find(n, d) && makeroute(d, b, n)) return true;
+        }
+        return false;
+    }
+
+    bool randomnode(fpsent *d, aistate &b, float guard, float wander)
+    {
+        return randomnode(d, b, d->feetpos(), guard, wander);
+    }
+
+    bool badhealth(fpsent *d)
+    {
+        if(d->skill <= 100) return d->health <= (111-d->skill)/4;
+        return false;
+    }
+
+    bool enemy(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, int pursue = 0)
+    {
+        fpsent *t = NULL;
+        vec dp = d->headpos();
+        float mindist = guard*guard, bestdist = 1e16f;
+        loopv(players)
+        {
+            fpsent *e = players[i];
+            if(e == d || !targetable(d, e)) continue;
+            vec ep = getaimpos(d, e);
+            float dist = ep.squaredist(dp);
+            if(dist < bestdist && (cansee(d, dp, ep) || dist <= mindist))
+            {
+                t = e;
+                bestdist = dist;
+            }
+        }
+        if(t && violence(d, b, t, pursue)) return true;
+        return false;
+    }
+
+    bool patrol(fpsent *d, aistate &b, const vec &pos, float guard, float wander, int walk, bool retry)
+    {
+        vec feet = d->feetpos();
+        if(walk == 2 || b.override || (walk && feet.squaredist(pos) <= guard*guard) || !makeroute(d, b, pos))
+        { // run away and back to keep ourselves busy
+            if(!b.override && randomnode(d, b, pos, guard, wander))
+            {
+                b.override = true;
+                return true;
+            }
+            else if(d->ai->route.empty())
+            {
+                if(!retry)
+                {
+                    b.override = false;
+                    return patrol(d, b, pos, guard, wander, walk, true);
+                }
+                b.override = false;
+                return false;
+            }
+        }
+        b.override = false;
+        return true;
+    }
+
+    bool defend(fpsent *d, aistate &b, const vec &pos, float guard, float wander, int walk)
+    {
+               bool hasenemy = enemy(d, b, pos, wander, d->gunselect == GUN_FIST ? 1 : 0);
+               if(!walk)
+               {
+                   if(d->feetpos().squaredist(pos) <= guard*guard)
+                   {
+                b.idle = hasenemy ? 2 : 1;
+                return true;
+                   }
+                   walk++;
+               }
+        return patrol(d, b, pos, guard, wander, walk);
+    }
+
+    bool violence(fpsent *d, aistate &b, fpsent *e, int pursue)
+    {
+        if(e && targetable(d, e))
+        {
+            if(pursue)
+            {
+                if((b.targtype != AI_T_AFFINITY || !(pursue%2)) && makeroute(d, b, e->lastnode))
+                    d->ai->switchstate(b, AI_S_PURSUE, AI_T_PLAYER, 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;
+        }
+        return false;
+    }
+
+    bool target(fpsent *d, aistate &b, int pursue = 0, bool force = false, float mindist = 0.f)
+    {
+        static vector<fpsent *> hastried; hastried.setsize(0);
+        vec dp = d->headpos();
+        while(true)
+        {
+            float dist = 1e16f;
+            fpsent *t = NULL;
+            loopv(players)
+            {
+                fpsent *e = players[i];
+                if(e == d || hastried.find(e) >= 0 || !targetable(d, e)) continue;
+                vec ep = getaimpos(d, e);
+                float v = ep.squaredist(dp);
+                if((!t || v < dist) && (mindist <= 0 || v <= mindist) && (force || cansee(d, dp, ep)))
+                {
+                    t = e;
+                    dist = v;
+                }
+            }
+            if(t)
+            {
+                if(violence(d, b, t, pursue)) return true;
+                hastried.add(t);
+            }
+            else break;
+        }
+        return false;
+    }
+
+    int isgoodammo(int gun) { return gun >= GUN_SG && gun <= GUN_GL; }
+
+    bool hasgoodammo(fpsent *d)
+    {
+        static const int goodguns[] = { GUN_CG, GUN_RL, GUN_SG, GUN_RIFLE };
+        loopi(sizeof(goodguns)/sizeof(goodguns[0])) if(d->hasammo(goodguns[0])) return true;
+        if(d->ammo[GUN_GL] > 5) return true;
+        return false;
+    }
+
+    void assist(fpsent *d, aistate &b, vector<interest> &interests, bool all, bool force)
+    {
+        loopv(players)
+        {
+            fpsent *e = players[i];
+            if(e == d || (!all && e->aitype != AI_NONE) || !isteam(d->team, e->team)) continue;
+            interest &n = interests.add();
+            n.state = AI_S_DEFEND;
+            n.node = e->lastnode;
+            n.target = e->clientnum;
+            n.targtype = AI_T_PLAYER;
+            n.score = e->o.squaredist(d->o)/(hasgoodammo(d) ? 1e8f : (force ? 1e4f : 1e2f));
+        }
+    }
+
+    static void tryitem(fpsent *d, extentity &e, int id, aistate &b, vector<interest> &interests, bool force = false)
+    {
+        float score = 0;
+        switch(e.type)
+        {
+            case I_HEALTH:
+                if(d->health < min(d->skill, 75)) score = 1e3f;
+                break;
+            case I_QUAD: score = 1e3f; break;
+            case I_BOOST: score = 1e2f; break;
+            case I_GREENARMOUR: case I_YELLOWARMOUR:
+            {
+                int atype = A_GREEN + e.type - I_GREENARMOUR;
+                if(atype > d->armourtype) score = atype == A_YELLOW ? 1e2f : 1e1f;
+                else if(d->armour < 50) score = 1e1f;
+                break;
+            }
+            default:
+            {
+                if(e.type >= I_SHELLS && e.type <= I_CARTRIDGES && !d->hasmaxammo(e.type))
+                {
+                    int gun = e.type - I_SHELLS + GUN_SG;
+                    // go get a weapon upgrade
+                    if(gun == d->ai->weappref) score = 1e8f;
+                    else if(isgoodammo(gun)) score = hasgoodammo(d) ? 1e2f : 1e4f;
+                }
+                break;
+            }
+        }
+        if(score != 0)
+        {
+            interest &n = interests.add();
+            n.state = AI_S_INTEREST;
+            n.node = closestwaypoint(e.o, SIGHTMIN, true);
+            n.target = id;
+            n.targtype = AI_T_ENTITY;
+            n.score = d->feetpos().squaredist(e.o)/(force ? -1 : score);
+        }
+    }
+
+    void items(fpsent *d, aistate &b, vector<interest> &interests, bool force = false)
+    {
+        loopv(entities::ents)
+        {
+            extentity &e = *(extentity *)entities::ents[i];
+            if(!e.spawned() || e.nopickup() || !d->canpickup(e.type)) continue;
+            tryitem(d, e, i, b, interests, force);
+        }
+    }
+
+    static vector<int> targets;
+
+    bool parseinterests(fpsent *d, aistate &b, vector<interest> &interests, bool override, bool ignore)
+    {
+        while(!interests.empty())
+        {
+            int q = interests.length()-1;
+            loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i;
+            interest n = interests.removeunordered(q);
+            bool proceed = true;
+            if(!ignore) switch(n.state)
+            {
+                case AI_S_DEFEND: // don't get into herds
+                {
+                    int members = 0;
+                    proceed = !checkothers(targets, d, n.state, n.targtype, n.target, true, &members) && members > 1;
+                    break;
+                }
+                default: break;
+            }
+            if(proceed && makeroute(d, b, n.node))
+            {
+                d->ai->switchstate(b, n.state, n.targtype, n.target);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    bool find(fpsent *d, aistate &b, bool override = false)
+    {
+        static vector<interest> interests;
+        interests.setsize(0);
+        if(!m_noitems)
+        {
+            if((!m_noammo && !hasgoodammo(d)) || d->health < min(d->skill - 15, 75))
+                items(d, b, interests);
+            else
+            {
+                static vector<int> nearby;
+                nearby.setsize(0);
+                findents(I_SHELLS, I_QUAD, false, d->feetpos(), vec(32, 32, 24), nearby);
+                loopv(nearby)
+                {
+                    int id = nearby[i];
+                    extentity &e = *(extentity *)entities::ents[id];
+                    if(d->canpickup(e.type)) tryitem(d, e, id, b, interests);
+                }
+            }
+        }
+        if(cmode) cmode->aifind(d, b, interests);
+        if(m_teammode) assist(d, b, interests);
+        return parseinterests(d, b, interests, override);
+    }
+
+    bool findassist(fpsent *d, aistate &b, bool override = false)
+    {
+        static vector<interest> interests;
+        interests.setsize(0);
+        assist(d, b, interests);
+        while(!interests.empty())
+        {
+            int q = interests.length()-1;
+            loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i;
+            interest n = interests.removeunordered(q);
+            bool proceed = true;
+            switch(n.state)
+            {
+                case AI_S_DEFEND: // don't get into herds
+                {
+                    int members = 0;
+                    proceed = !checkothers(targets, d, n.state, n.targtype, n.target, true, &members) && members > 1;
+                    break;
+                }
+                default: break;
+            }
+            if(proceed && makeroute(d, b, n.node))
+            {
+                d->ai->switchstate(b, n.state, n.targtype, n.target);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void damaged(fpsent *d, fpsent *e)
+    {
+        if(d->ai && canmove(d) && targetable(d, e)) // see if this ai is interested in a grudge
+        {
+            aistate &b = d->ai->getstate();
+            if(violence(d, b, e, d->gunselect == GUN_FIST ? 1 : 0)) return;
+        }
+        if(checkothers(targets, d, AI_S_DEFEND, AI_T_PLAYER, d->clientnum, true))
+        {
+            loopv(targets)
+            {
+                fpsent *t = getclient(targets[i]);
+                if(!t->ai || !canmove(t) || !targetable(t, e)) continue;
+                aistate &c = t->ai->getstate();
+                if(violence(t, c, e, d->gunselect == GUN_FIST ? 1 : 0)) return;
+            }
+        }
+    }
+
+    void findorientation(vec &o, float yaw, float pitch, vec &pos)
+    {
+        vec dir;
+        vecfromyawpitch(yaw, pitch, 1, 0, dir);
+        if(raycubepos(o, dir, pos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1)
+            pos = dir.mul(2*getworldsize()).add(o);
+    }
+
+    void setup(fpsent *d)
+    {
+        d->ai->clearsetup();
+        d->ai->reset(true);
+        d->ai->lastrun = lastmillis;
+        if(m_insta) d->ai->weappref = GUN_RIFLE;
+        else
+        {
+               if(forcegun >= 0 && forcegun < NUMGUNS) d->ai->weappref = forcegun;
+               else if(m_noammo) d->ai->weappref = -1;
+                       else d->ai->weappref = rnd(GUN_GL-GUN_SG+1)+GUN_SG;
+        }
+        vec dp = d->headpos();
+        findorientation(dp, d->yaw, d->pitch, d->ai->target);
+    }
+
+    void spawned(fpsent *d)
+    {
+        if(d->ai) setup(d);
+    }
+
+    void killed(fpsent *d, fpsent *e)
+    {
+        if(d->ai) d->ai->reset();
+    }
+
+    void itemspawned(int ent)
+    {
+        if(entities::ents.inrange(ent) && entities::ents[ent]->type >= I_SHELLS && entities::ents[ent]->type <= I_QUAD)
+        {
+            loopv(players) if(players[i] && players[i]->ai && players[i]->aitype == AI_BOT && players[i]->canpickup(entities::ents[ent]->type))
+            {
+                fpsent *d = players[i];
+                bool wantsitem = false;
+                switch(entities::ents[ent]->type)
+                {
+                    case I_BOOST: case I_HEALTH: wantsitem = badhealth(d); break;
+                    case I_GREENARMOUR: case I_YELLOWARMOUR: case I_QUAD: break;
+                    default:
+                    {
+                        itemstat &is = itemstats[entities::ents[ent]->type-I_SHELLS];
+                        wantsitem = isgoodammo(is.info) && d->ammo[is.info] <= (d->ai->weappref == is.info ? is.add : is.add/2);
+                        break;
+                    }
+                }
+                if(wantsitem)
+                {
+                    aistate &b = d->ai->getstate();
+                    if(b.targtype == AI_T_AFFINITY) continue;
+                    if(b.type == AI_S_INTEREST && b.targtype == AI_T_ENTITY)
+                    {
+                        if(entities::ents.inrange(b.target))
+                        {
+                            if(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;
+                    }
+                    d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent);
+                }
+            }
+        }
+    }
+
+    bool check(fpsent *d, aistate &b)
+    {
+        if(cmode && cmode->aicheck(d, b)) return true;
+        return false;
+    }
+
+    int dowait(fpsent *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(randomnode(d, b, SIGHTMIN, 1e16f))
+        {
+            d->ai->switchstate(b, AI_S_INTEREST, AI_T_NODE, d->ai->route[0]);
+            return 1;
+        }
+        return 0; // but don't pop the state
+    }
+
+    int dodefend(fpsent *d, aistate &b)
+    {
+        if(d->state == CS_ALIVE)
+        {
+            switch(b.targtype)
+            {
+                case AI_T_NODE:
+                    if(check(d, b)) return 1;
+                    if(iswaypoint(b.target)) return defend(d, b, waypoints[b.target].o) ? 1 : 0;
+                    break;
+                case AI_T_ENTITY:
+                    if(check(d, b)) return 1;
+                    if(entities::ents.inrange(b.target)) return defend(d, b, entities::ents[b.target]->o) ? 1 : 0;
+                    break;
+                case AI_T_AFFINITY:
+                    if(cmode) return cmode->aidefend(d, b) ? 1 : 0;
+                    break;
+                case AI_T_PLAYER:
+                {
+                    if(check(d, b)) return 1;
+                    fpsent *e = getclient(b.target);
+                    if(e && e->state == CS_ALIVE) return defend(d, b, e->feetpos()) ? 1 : 0;
+                    break;
+                }
+                default: break;
+            }
+        }
+        return 0;
+    }
+
+    int dointerest(fpsent *d, aistate &b)
+    {
+        if(d->state != CS_ALIVE) return 0;
+        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))
+                {
+                    extentity &e = *(extentity *)entities::ents[b.target];
+                    if(!e.spawned() || e.nopickup() || e.type < I_SHELLS || e.type > I_CARTRIDGES || d->hasmaxammo(e.type)) return 0;
+                    //if(d->feetpos().squaredist(e.o) <= CLOSEDIST*CLOSEDIST)
+                    //{
+                    //    b.idle = 1;
+                    //    return true;
+                    //}
+                    return makeroute(d, b, e.o) ? 1 : 0;
+                }
+                break;
+        }
+        return 0;
+    }
+
+    int dopursue(fpsent *d, aistate &b)
+    {
+        if(d->state == CS_ALIVE)
+        {
+            switch(b.targtype)
+            {
+                case AI_T_NODE:
+                {
+                    if(check(d, b)) return 1;
+                    if(iswaypoint(b.target))
+                        return defend(d, b, waypoints[b.target].o) ? 1 : 0;
+                    break;
+                }
+
+                case AI_T_AFFINITY:
+                {
+                    if(cmode) return cmode->aipursue(d, b) ? 1 : 0;
+                    break;
+                }
+
+                case AI_T_PLAYER:
+                {
+                    //if(check(d, b)) return 1;
+                    fpsent *e = getclient(b.target);
+                    if(e && e->state == CS_ALIVE)
+                    {
+                       float guard = SIGHTMIN, wander = guns[d->gunselect].range;
+                       if(d->gunselect == GUN_FIST) guard = 0.f;
+                       return patrol(d, b, e->feetpos(), guard, wander) ? 1 : 0;
+                    }
+                    break;
+                }
+                default: break;
+            }
+        }
+        return 0;
+    }
+
+    int closenode(fpsent *d)
+    {
+        vec pos = d->feetpos();
+        int node1 = -1, node2 = -1;
+        float mindist1 = CLOSEDIST*CLOSEDIST, mindist2 = CLOSEDIST*CLOSEDIST;
+        loopv(d->ai->route) if(iswaypoint(d->ai->route[i]))
+        {
+            vec epos = waypoints[d->ai->route[i]].o;
+            float dist = epos.squaredist(pos);
+            if(dist > FARDIST*FARDIST) continue;
+            int entid = obstacles.remap(d, d->ai->route[i], epos);
+            if(entid >= 0)
+            {
+                if(entid != i) dist = epos.squaredist(pos);
+                if(dist < mindist1) { node1 = i; mindist1 = dist; }
+            }
+            else if(dist < mindist2) { node2 = i; mindist2 = dist; }
+        }
+        return node1 >= 0 ? node1 : node2;
+    }
+
+    int wpspot(fpsent *d, int n, bool check = false)
+    {
+        if(iswaypoint(n)) loopk(2)
+        {
+            vec epos = waypoints[n].o;
+            int entid = obstacles.remap(d, n, epos, k!=0);
+            if(iswaypoint(entid))
+            {
+                d->ai->spot = epos;
+                d->ai->targnode = entid;
+                return !check || d->feetpos().squaredist(epos) > MINWPDIST*MINWPDIST ? 1 : 2;
+            }
+        }
+        return 0;
+    }
+
+    int randomlink(fpsent *d, int n)
+    {
+        if(iswaypoint(n) && waypoints[n].haslinks())
+        {
+            waypoint &w = waypoints[n];
+            static vector<int> linkmap; linkmap.setsize(0);
+            loopi(MAXWAYPOINTLINKS)
+            {
+                if(!w.links[i]) break;
+                if(iswaypoint(w.links[i]) && !d->ai->hasprevnode(w.links[i]) && d->ai->route.find(w.links[i]) < 0)
+                    linkmap.add(w.links[i]);
+            }
+            if(!linkmap.empty()) return linkmap[rnd(linkmap.length())];
+        }
+        return -1;
+    }
+
+    bool anynode(fpsent *d, aistate &b, int len = NUMPREVNODES)
+    {
+        if(iswaypoint(d->lastnode)) loopk(2)
+        {
+            d->ai->clear(k ? true : false);
+            int n = randomlink(d, d->lastnode);
+            if(wpspot(d, n))
+            {
+                d->ai->route.add(n);
+                d->ai->route.add(d->lastnode);
+                loopi(len)
+                {
+                    n = randomlink(d, n);
+                    if(iswaypoint(n)) d->ai->route.insert(0, n);
+                    else break;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    bool checkroute(fpsent *d, int n)
+    {
+        if(d->ai->route.empty() || !d->ai->route.inrange(n)) return false;
+        int last = d->ai->lastcheck ? lastmillis-d->ai->lastcheck : 0;
+        if(last < 500 || n < 3) return false; // route length is too short
+        d->ai->lastcheck = lastmillis;
+        int w = iswaypoint(d->lastnode) ? d->lastnode : d->ai->route[n], c = min(n-1, NUMPREVNODES);
+        loopj(c) // check ahead to see if we need to go around something
+        {
+            int p = n-j-1, v = d->ai->route[p];
+            if(d->ai->hasprevnode(v) || obstacles.find(v, d)) // something is in the way, try to remap around it
+            {
+                int m = p-1;
+                if(m < 3) return false; // route length is too short from this point
+                loopirev(m)
+                {
+                    int t = d->ai->route[i];
+                    if(!d->ai->hasprevnode(t) && !obstacles.find(t, d))
+                    {
+                        static vector<int> remap; remap.setsize(0);
+                        if(route(d, w, t, remap, obstacles))
+                        { // kill what we don't want and put the remap in
+                            while(d->ai->route.length() > i) d->ai->route.pop();
+                            loopvk(remap) d->ai->route.add(remap[k]);
+                            return true;
+                        }
+                        return false; // we failed
+                    }
+                }
+                return false;
+            }
+        }
+        return false;
+    }
+
+    bool hunt(fpsent *d, aistate &b)
+    {
+        if(!d->ai->route.empty())
+        {
+            int n = closenode(d);
+            if(d->ai->route.inrange(n) && checkroute(d, n)) n = closenode(d);
+            if(d->ai->route.inrange(n))
+            {
+                if(!n)
+                {
+                    switch(wpspot(d, d->ai->route[n], true))
+                    {
+                        case 2: d->ai->clear(false);
+                        case 1: return true; // not close enough to pop it yet
+                        case 0: default: break;
+                    }
+                }
+                else
+                {
+                    while(d->ai->route.length() > n+1) d->ai->route.pop(); // waka-waka-waka-waka
+                    int m = n-1; // next, please!
+                    if(d->ai->route.inrange(m) && wpspot(d, d->ai->route[m])) return true;
+                }
+            }
+        }
+        b.override = false;
+        return anynode(d, b);
+    }
+
+    void jumpto(fpsent *d, aistate &b, const vec &pos)
+    {
+               vec off = vec(pos).sub(d->feetpos()), dir(off.x, off.y, 0);
+        bool sequenced = d->ai->blockseq || d->ai->targseq, offground = d->timeinair && !d->inwater,
+            jump = !offground && lastmillis >= d->ai->jumpseed && (sequenced || off.z >= JUMPMIN || lastmillis >= d->ai->jumprand);
+               if(jump)
+               {
+                       vec old = d->o;
+                       d->o = vec(pos).add(vec(0, 0, d->eyeheight));
+                       if(collide(d, vec(0, 0, 1))) jump = false;
+                       d->o = old;
+                       if(jump)
+                       {
+                               float radius = 18*18;
+                               loopv(entities::ents) if(entities::ents[i]->type == JUMPPAD)
+                               {
+                                       fpsentity &e = *(fpsentity *)entities::ents[i];
+                                       if(e.o.squaredist(pos) <= radius) { jump = false; break; }
+                               }
+                       }
+               }
+               if(jump)
+               {
+                       d->jumping = true;
+                       int seed = (111-d->skill)*(d->inwater ? 3 : 5);
+                       d->ai->jumpseed = lastmillis+seed+rnd(seed);
+                       seed *= b.idle ? 50 : 25;
+                       d->ai->jumprand = lastmillis+seed+rnd(seed);
+               }
+    }
+
+    void fixfullrange(float &yaw, float &pitch, float &roll, bool full)
+    {
+        if(full)
+        {
+            while(pitch < -180.0f) pitch += 360.0f;
+            while(pitch >= 180.0f) pitch -= 360.0f;
+            while(roll < -180.0f) roll += 360.0f;
+            while(roll >= 180.0f) roll -= 360.0f;
+        }
+        else
+        {
+            if(pitch > 89.9f) pitch = 89.9f;
+            if(pitch < -89.9f) pitch = -89.9f;
+            if(roll > 89.9f) roll = 89.9f;
+            if(roll < -89.9f) roll = -89.9f;
+        }
+        while(yaw < 0.0f) yaw += 360.0f;
+        while(yaw >= 360.0f) yaw -= 360.0f;
+    }
+
+    void fixrange(float &yaw, float &pitch)
+    {
+        float r = 0.f;
+        fixfullrange(yaw, pitch, r, false);
+    }
+
+    void getyawpitch(const vec &from, const vec &pos, float &yaw, float &pitch)
+    {
+        float dist = from.dist(pos);
+        yaw = -atan2(pos.x-from.x, pos.y-from.y)/RAD;
+        pitch = asin((pos.z-from.z)/dist)/RAD;
+    }
+
+    void scaleyawpitch(float &yaw, float &pitch, float targyaw, float targpitch, float frame, float scale)
+    {
+        if(yaw < targyaw-180.0f) yaw += 360.0f;
+        if(yaw > targyaw+180.0f) yaw -= 360.0f;
+        float offyaw = fabs(targyaw-yaw)*frame, offpitch = fabs(targpitch-pitch)*frame*scale;
+        if(targyaw > yaw)
+        {
+            yaw += offyaw;
+            if(targyaw < yaw) yaw = targyaw;
+        }
+        else if(targyaw < yaw)
+        {
+            yaw -= offyaw;
+            if(targyaw > yaw) yaw = targyaw;
+        }
+        if(targpitch > pitch)
+        {
+            pitch += offpitch;
+            if(targpitch < pitch) pitch = targpitch;
+        }
+        else if(targpitch < pitch)
+        {
+            pitch -= offpitch;
+            if(targpitch > pitch) pitch = targpitch;
+        }
+        fixrange(yaw, pitch);
+    }
+
+    bool lockon(fpsent *d, fpsent *e, float maxdist)
+    {
+        if(d->gunselect == GUN_FIST && !d->blocked && !d->timeinair)
+        {
+            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;
+            if(zdist <= ddist && xydist >= ddist+4 && xydist <= mdist+ddist) return true;
+        }
+        return false;
+    }
+
+    int process(fpsent *d, aistate &b)
+    {
+        int result = 0, stupify = d->skill <= 10+rnd(15) ? rnd(d->skill*1000) : 0, skmod = 101-d->skill;
+        float frame = d->skill <= 100 ? float(lastmillis-d->ai->lastrun)/float(max(skmod,1)*10) : 1;
+        vec dp = d->headpos();
+
+        bool idle = b.idle == 1 || (stupify && stupify <= skmod);
+        d->ai->dontmove = false;
+        if(idle)
+        {
+            d->ai->lastaction = d->ai->lasthunt = lastmillis;
+            d->ai->dontmove = true;
+            d->ai->spot = vec(0, 0, 0);
+        }
+        else if(hunt(d, b))
+        {
+            getyawpitch(dp, vec(d->ai->spot).add(vec(0, 0, d->eyeheight)), d->ai->targyaw, d->ai->targpitch);
+            d->ai->lasthunt = lastmillis;
+        }
+        else
+        {
+            idle = d->ai->dontmove = true;
+            d->ai->spot = vec(0, 0, 0);
+        }
+
+               if(!d->ai->dontmove) jumpto(d, b, d->ai->spot);
+
+        fpsent *e = getclient(d->ai->enemy);
+        bool enemyok = e && targetable(d, e);
+        if(!enemyok || d->skill >= 50)
+        {
+            fpsent *f = (fpsent *)intersectclosest(dp, d->ai->target, d);
+            if(f)
+            {
+                if(targetable(d, f))
+                {
+                    if(!enemyok) violence(d, b, f, d->gunselect == GUN_FIST ? 1 : 0);
+                    enemyok = true;
+                    e = f;
+                }
+                else enemyok = false;
+            }
+            else if(!enemyok && target(d, b, d->gunselect == GUN_FIST ? 1 : 0, false, SIGHTMIN))
+                enemyok = (e = getclient(d->ai->enemy)) != NULL;
+        }
+        if(enemyok)
+        {
+            vec ep = getaimpos(d, e);
+            float yaw, pitch;
+            getyawpitch(dp, ep, yaw, pitch);
+            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 <= (d->gunselect == GUN_CG ? 300 : skmod)+30;
+            if(insight) d->ai->enemyseen = lastmillis;
+            if(idle || insight || hasseen || quick)
+            {
+                float sskew = insight || d->skill > 100 ? 1.5f : (hasseen ? 1.f : 0.5f);
+                if(insight && lockon(d, e, 16))
+                {
+                    d->ai->targyaw = yaw;
+                    d->ai->targpitch = pitch;
+                    if(!idle) frame *= 2;
+                    d->ai->becareful = false;
+                }
+                scaleyawpitch(d->yaw, d->pitch, yaw, pitch, frame, sskew);
+                if(insight || quick)
+                {
+                    if(canshoot(d, e) && hastarget(d, b, e, yaw, pitch, dp.squaredist(ep)))
+                    {
+                        d->attacking = true;
+                        d->ai->lastaction = lastmillis;
+                        result = 3;
+                    }
+                    else result = 2;
+                }
+                else result = 1;
+            }
+            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;
+            }
+        }
+        else
+        {
+            if(!enemyok)
+            {
+                d->ai->enemy = -1;
+                d->ai->enemyseen = d->ai->enemymillis = 0;
+            }
+            enemyok = false;
+            result = 0;
+        }
+
+        fixrange(d->ai->targyaw, d->ai->targpitch);
+        if(!result) scaleyawpitch(d->yaw, d->pitch, d->ai->targyaw, d->ai->targpitch, frame*0.25f, 1.f);
+
+        if(d->ai->becareful && d->physstate == PHYS_FALL)
+        {
+            float offyaw, offpitch;
+            vectoyawpitch(d->vel, offyaw, offpitch);
+            offyaw -= d->yaw; offpitch -= d->pitch;
+            if(fabs(offyaw)+fabs(offpitch) >= 135) d->ai->becareful = false;
+            else if(d->ai->becareful) d->ai->dontmove = true;
+        }
+        else d->ai->becareful = false;
+
+        if(d->ai->dontmove) d->move = d->strafe = 0;
+        else
+        { // our guys move one way.. but turn another?! :)
+            const struct aimdir { int move, strafe, offset; } aimdirs[8] =
+            {
+                {  1,  0,   0 },
+                {  1,  -1,  45 },
+                {  0,  -1,  90 },
+                { -1,  -1, 135 },
+                { -1,  0, 180 },
+                { -1, 1, 225 },
+                {  0, 1, 270 },
+                {  1, 1, 315 }
+            };
+            float yaw = d->ai->targyaw-d->yaw;
+            while(yaw < 0.0f) yaw += 360.0f;
+            while(yaw >= 360.0f) yaw -= 360.0f;
+            int r = clamp(((int)floor((yaw+22.5f)/45.0f))&7, 0, 7);
+            const aimdir &ad = aimdirs[r];
+            d->move = ad.move;
+            d->strafe = ad.strafe;
+        }
+        findorientation(dp, d->yaw, d->pitch, d->ai->target);
+        return result;
+    }
+
+    bool hasrange(fpsent *d, fpsent *e, int weap)
+    {
+        if(!e) return true;
+        if(targetable(d, e))
+        {
+            vec ep = getaimpos(d, e);
+            float dist = ep.squaredist(d->headpos());
+            if(weaprange(d, weap, dist)) return true;
+        }
+        return false;
+    }
+
+    bool request(fpsent *d, aistate &b)
+    {
+        fpsent *e = getclient(d->ai->enemy);
+        if(!d->hasammo(d->gunselect) || !hasrange(d, e, d->gunselect) || (d->gunselect != d->ai->weappref && (!isgoodammo(d->gunselect) || d->hasammo(d->ai->weappref))))
+        {
+            static const int gunprefs[] = { GUN_CG, GUN_RL, GUN_SG, GUN_RIFLE, GUN_GL, GUN_PISTOL, GUN_FIST };
+            int gun = -1;
+            if(d->hasammo(d->ai->weappref) && hasrange(d, e, d->ai->weappref)) gun = d->ai->weappref;
+            else
+            {
+                loopi(sizeof(gunprefs)/sizeof(gunprefs[0])) if(d->hasammo(gunprefs[i]) && hasrange(d, e, gunprefs[i]))
+                {
+                    gun = gunprefs[i];
+                    break;
+                }
+            }
+            if(gun >= 0 && gun != d->gunselect) gunselect(gun, d);
+        }
+        return process(d, b) >= 2;
+    }
+
+       void timeouts(fpsent *d, aistate &b)
+       {
+        if(d->blocked)
+        {
+            d->ai->blocktime += lastmillis-d->ai->lastrun;
+            if(d->ai->blocktime > (d->ai->blockseq+1)*1000)
+            {
+                d->ai->blockseq++;
+                switch(d->ai->blockseq)
+                {
+                    case 1: case 2: case 3:
+                        if(entities::ents.inrange(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode);
+                        d->ai->clear(false);
+                        break;
+                    case 4: d->ai->reset(true); break;
+                    case 5: d->ai->reset(false); break;
+                    case 6: default: suicide(d); return; break; // this is our last resort..
+                }
+            }
+        }
+        else d->ai->blocktime = d->ai->blockseq = 0;
+
+        if(d->ai->targnode == d->ai->targlast)
+        {
+            d->ai->targtime += lastmillis-d->ai->lastrun;
+            if(d->ai->targtime > (d->ai->targseq+1)*1000)
+            {
+                d->ai->targseq++;
+                switch(d->ai->targseq)
+                {
+                    case 1: case 2: case 3:
+                        if(entities::ents.inrange(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode);
+                        d->ai->clear(false);
+                        break;
+                    case 4: d->ai->reset(true); break;
+                    case 5: d->ai->reset(false); break;
+                    case 6: default: suicide(d); return; break; // this is our last resort..
+                }
+            }
+        }
+        else
+        {
+            d->ai->targtime = d->ai->targseq = 0;
+            d->ai->targlast = d->ai->targnode;
+        }
+
+        if(d->ai->lasthunt)
+        {
+            int millis = lastmillis-d->ai->lasthunt;
+            if(millis <= 1000) { d->ai->tryreset = false; d->ai->huntseq = 0; }
+            else if(millis > (d->ai->huntseq+1)*1000)
+            {
+                d->ai->huntseq++;
+                switch(d->ai->huntseq)
+                {
+                    case 1: d->ai->reset(true); break;
+                    case 2: d->ai->reset(false); break;
+                    case 3: default: suicide(d); return; break; // this is our last resort..
+                }
+            }
+        }
+       }
+
+    void logic(fpsent *d, aistate &b, bool run)
+    {
+        bool allowmove = canmove(d) && b.type != AI_S_WAIT;
+        if(d->state != CS_ALIVE || !allowmove) d->stopmoving();
+        if(d->state == CS_ALIVE)
+        {
+            if(allowmove)
+            {
+                if(!request(d, b)) target(d, b, d->gunselect == GUN_FIST ? 1 : 0, b.idle ? true : false);
+                shoot(d, d->ai->target);
+            }
+            if(!intermission)
+            {
+                if(d->ragdoll) cleanragdoll(d);
+                moveplayer(d, 10, true);
+                if(allowmove && !b.idle) timeouts(d, b);
+                if(d->quadmillis) entities::checkquad(curtime, d);
+                               entities::checkitems(d);
+                               if(cmode) cmode->checkitems(d);
+            }
+        }
+        else if(d->state == CS_DEAD)
+        {
+            if(d->ragdoll) moveragdoll(d);
+            else if(lastmillis-d->lastpain<2000)
+            {
+                d->move = d->strafe = 0;
+                moveplayer(d, 10, false);
+            }
+        }
+        d->attacking = d->jumping = false;
+    }
+
+       void avoid()
+    {
+        // guess as to the radius of ai and other critters relying on the avoid set for now
+        float guessradius = player1->radius;
+        obstacles.clear();
+        loopv(players)
+        {
+            dynent *d = players[i];
+            if(d->state != CS_ALIVE) continue;
+            obstacles.avoidnear(d, d->o.z + d->aboveeye + 1, d->feetpos(), guessradius + d->radius);
+        }
+               extern avoidset wpavoid;
+               obstacles.add(wpavoid);
+               avoidweapons(obstacles, guessradius);
+    }
+
+    void think(fpsent *d, bool run)
+    {
+        // the state stack works like a chain of commands, certain commands simply replace each other
+        // others spawn new commands to the stack the ai reads the top command from the stack and executes
+        // it or pops the stack and goes back along the history until it finds a suitable command to execute
+        bool cleannext = false;
+        if(d->ai->state.empty()) d->ai->addstate(AI_S_WAIT);
+        loopvrev(d->ai->state)
+        {
+            aistate &c = d->ai->state[i];
+            if(cleannext)
+            {
+                c.millis = lastmillis;
+                c.override = false;
+                cleannext = false;
+            }
+            if(d->state == CS_DEAD && d->respawned!=d->lifesequence && (!cmode || cmode->respawnwait(d, 250) <= 0) && lastmillis - d->lastpain >= 500)
+            {
+                addmsg(N_TRYSPAWN, "rc", d);
+                d->respawned = d->lifesequence;
+            }
+            else if(d->state == CS_ALIVE && run)
+            {
+                int result = 0;
+                c.idle = 0;
+                switch(c.type)
+                {
+                    case AI_S_WAIT: result = dowait(d, c); break;
+                    case AI_S_DEFEND: result = dodefend(d, c); break;
+                    case AI_S_PURSUE: result = dopursue(d, c); break;
+                    case AI_S_INTEREST: result = dointerest(d, c); break;
+                    default: result = 0; break;
+                }
+                if(result <= 0)
+                {
+                    if(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
+                    }
+                }
+            }
+            logic(d, c, run);
+            break;
+        }
+        if(d->ai->trywipe) d->ai->wipe();
+        d->ai->lastrun = lastmillis;
+    }
+
+    void drawroute(fpsent *d, float amt = 1.f)
+    {
+        int last = -1;
+        loopvrev(d->ai->route)
+        {
+            if(d->ai->route.inrange(last))
+            {
+                int index = d->ai->route[i], prev = d->ai->route[last];
+                if(iswaypoint(index) && iswaypoint(prev))
+                {
+                    waypoint &e = waypoints[index], &f = waypoints[prev];
+                    vec fr = f.o, dr = e.o;
+                    fr.z += amt; dr.z += amt;
+                    particle_flare(fr, dr, 1, PART_STREAK, 0xFFFFFF);
+                }
+            }
+            last = i;
+        }
+        if(aidebug >= 5)
+        {
+            vec pos = d->feetpos();
+            if(d->ai->spot != vec(0, 0, 0)) particle_flare(pos, d->ai->spot, 1, PART_LIGHTNING, 0x00FFFF);
+            if(iswaypoint(d->ai->targnode))
+                particle_flare(pos, waypoints[d->ai->targnode].o, 1, PART_LIGHTNING, 0xFF00FF);
+            if(iswaypoint(d->lastnode))
+                particle_flare(pos, waypoints[d->lastnode].o, 1, PART_LIGHTNING, 0xFFFF00);
+            loopi(NUMPREVNODES) if(iswaypoint(d->ai->prevnodes[i]))
+            {
+                particle_flare(pos, waypoints[d->ai->prevnodes[i]].o, 1, PART_LIGHTNING, 0x884400);
+                pos = waypoints[d->ai->prevnodes[i]].o;
+            }
+        }
+    }
+
+    VAR(showwaypoints, 0, 0, 1);
+    VAR(showwaypointsradius, 0, 200, 10000);
+
+    const char *stnames[AI_S_MAX] = {
+        "wait", "defend", "pursue", "interest"
+    }, *sttypes[AI_T_MAX+1] = {
+        "none", "node", "player", "affinity", "entity"
+    };
+    void render()
+    {
+        if(aidebug > 1)
+        {
+            int total = 0, alive = 0;
+            loopv(players) if(players[i]->ai) total++;
+            loopv(players) if(players[i]->state == CS_ALIVE && players[i]->ai)
+            {
+                fpsent *d = players[i];
+                vec pos = d->abovehead();
+                pos.z += 3;
+                alive++;
+                if(aidebug >= 4) drawroute(d, 4.f*(float(alive)/float(total)));
+                if(aidebug >= 3)
+                {
+                    defformatstring(q, "node: %d route: %d (%d)",
+                        d->lastnode,
+                        !d->ai->route.empty() ? d->ai->route[0] : -1,
+                        d->ai->route.length()
+                    );
+                    particle_textcopy(pos, q, PART_TEXT, 1);
+                    pos.z += 2;
+                }
+                bool top = true;
+                loopvrev(d->ai->state)
+                {
+                    aistate &b = d->ai->state[i];
+                    defformatstring(s, "%s%s (%d ms) %s:%d",
+                        top ? "\fg" : "\fy",
+                        stnames[b.type],
+                        lastmillis-b.millis,
+                        sttypes[b.targtype+1], b.target
+                    );
+                    particle_textcopy(pos, s, PART_TEXT, 1);
+                    pos.z += 2;
+                    if(top)
+                    {
+                        if(aidebug >= 3) top = false;
+                        else break;
+                    }
+                }
+                if(aidebug >= 3)
+                {
+                    if(d->ai->weappref >= 0 && d->ai->weappref < NUMGUNS)
+                    {
+                        particle_textcopy(pos, guns[d->ai->weappref].name, PART_TEXT, 1);
+                        pos.z += 2;
+                    }
+                    fpsent *e = getclient(d->ai->enemy);
+                    if(e)
+                    {
+                        particle_textcopy(pos, colorname(e), PART_TEXT, 1);
+                        pos.z += 2;
+                    }
+                }
+            }
+            if(aidebug >= 4)
+            {
+                int cur = 0;
+                loopv(obstacles.obstacles)
+                {
+                    const avoidset::obstacle &ob = obstacles.obstacles[i];
+                    int next = cur + ob.numwaypoints;
+                    for(; cur < next; cur++)
+                    {
+                        int ent = obstacles.waypoints[cur];
+                        if(iswaypoint(ent))
+                            regular_particle_splash(PART_EDIT, 2, 40, waypoints[ent].o, 0xFF6600, 1.5f);
+                    }
+                    cur = next;
+                }
+            }
+        }
+        if(showwaypoints || aidebug >= 6)
+        {
+            vector<int> close;
+            int len = waypoints.length();
+            if(showwaypointsradius)
+            {
+                findwaypointswithin(camera1->o, 0, showwaypointsradius, close);
+                len = close.length();
+            }
+            loopi(len)
+            {
+                waypoint &w = waypoints[showwaypointsradius ? close[i] : i];
+                loopj(MAXWAYPOINTLINKS)
+                {
+                     int link = w.links[j];
+                     if(!link) break;
+                     particle_flare(w.o, waypoints[link].o, 1, PART_STREAK, 0x0000FF);
+                }
+            }
+
+        }
+    }
+}
+
diff --git a/src/fpsgame/ai.h b/src/fpsgame/ai.h
new file mode 100644 (file)
index 0000000..43efde9
--- /dev/null
@@ -0,0 +1,317 @@
+struct fpsent;
+
+#define MAXBOTS 32
+
+enum { AI_NONE = 0, AI_BOT, AI_MAX };
+#define isaitype(a) (a >= 0 && a <= AI_MAX-1)
+
+namespace ai
+{
+    const int MAXWAYPOINTS = USHRT_MAX - 2;
+    const int MAXWAYPOINTLINKS = 6;
+    const int WAYPOINTRADIUS = 16;
+
+    const float MINWPDIST       = 4.f;     // is on top of
+    const float CLOSEDIST       = 32.f;    // is close
+    const float FARDIST         = 128.f;   // too far to remap close
+    const float JUMPMIN         = 4.f;     // decides to jump
+    const float JUMPMAX         = 32.f;    // max jump
+    const float SIGHTMIN        = 64.f;    // minimum line of sight
+    const float SIGHTMAX        = 1024.f;  // maximum line of sight
+    const float VIEWMIN         = 90.f;    // minimum field of view
+    const float VIEWMAX         = 180.f;   // maximum field of view
+
+    struct waypoint
+    {
+        vec o;
+        float curscore, estscore;
+               int weight;
+        ushort route, prev;
+        ushort links[MAXWAYPOINTLINKS];
+
+        waypoint() {}
+        waypoint(const vec &o, int weight = 0) : o(o), weight(weight), route(0) { memset(links, 0, sizeof(links)); }
+
+        int score() const { return int(curscore) + int(estscore); }
+
+        int find(int wp)
+               {
+                       loopi(MAXWAYPOINTLINKS) if(links[i] == wp) return i;
+                       return -1;
+               }
+
+               bool haslinks() { return links[0]!=0; }
+    };
+    extern vector<waypoint> waypoints;
+
+    static inline bool iswaypoint(int n)
+    {
+        return n > 0 && n < waypoints.length();
+    }
+
+    extern int showwaypoints, dropwaypoints;
+    extern int closestwaypoint(const vec &pos, float mindist, bool links, fpsent *d = NULL);
+    extern void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results);
+       extern void inferwaypoints(fpsent *d, const vec &o, const vec &v, float mindist = ai::CLOSEDIST);
+
+    struct avoidset
+    {
+        struct obstacle
+        {
+            void *owner;
+            int numwaypoints;
+            float above;
+
+            obstacle(void *owner, float above = -1) : owner(owner), numwaypoints(0), above(above) {}
+        };
+
+        vector<obstacle> obstacles;
+        vector<int> waypoints;
+
+        void clear()
+        {
+            obstacles.setsize(0);
+            waypoints.setsize(0);
+        }
+
+        void add(void *owner, float above)
+        {
+            obstacles.add(obstacle(owner, above));
+        }
+
+        void add(void *owner, float above, int wp)
+        {
+            if(obstacles.empty() || owner != obstacles.last().owner) add(owner, above);
+            obstacles.last().numwaypoints++;
+            waypoints.add(wp);
+        }
+
+               void add(avoidset &avoid)
+               {
+                       waypoints.put(avoid.waypoints.getbuf(), avoid.waypoints.length());
+                       loopv(avoid.obstacles)
+                       {
+                               obstacle &o = avoid.obstacles[i];
+                               if(obstacles.empty() || o.owner != obstacles.last().owner) add(o.owner, o.above);
+                               obstacles.last().numwaypoints += o.numwaypoints;
+                       }
+               }
+
+        void avoidnear(void *owner, float above, const vec &pos, float limit);
+
+        #define loopavoid(v, d, body) \
+            if(!(v).obstacles.empty()) \
+            { \
+                int cur = 0; \
+                loopv((v).obstacles) \
+                { \
+                    const ai::avoidset::obstacle &ob = (v).obstacles[i]; \
+                    int next = cur + ob.numwaypoints; \
+                    if(ob.owner != d) \
+                    { \
+                        for(; cur < next; cur++) \
+                        { \
+                            int wp = (v).waypoints[cur]; \
+                            body; \
+                        } \
+                    } \
+                    cur = next; \
+                } \
+            }
+
+        bool find(int n, fpsent *d) const
+        {
+            loopavoid(*this, d, { if(wp == n) return true; });
+            return false;
+        }
+
+        int remap(fpsent *d, int n, vec &pos, bool retry = false);
+    };
+
+    extern bool route(fpsent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries = 0);
+    extern void navigate();
+    extern void clearwaypoints(bool full = false);
+    extern void seedwaypoints();
+    extern void loadwaypoints(bool force = false, const char *mname = NULL);
+    extern void savewaypoints(bool force = false, const char *mname = NULL);
+
+    // ai state information for the owner client
+    enum
+    {
+        AI_S_WAIT = 0,      // waiting for next command
+        AI_S_DEFEND,        // defend goal target
+        AI_S_PURSUE,        // pursue goal target
+        AI_S_INTEREST,      // interest in goal entity
+        AI_S_MAX
+    };
+
+    enum
+    {
+        AI_T_NODE,
+        AI_T_PLAYER,
+        AI_T_AFFINITY,
+        AI_T_ENTITY,
+        AI_T_MAX
+    };
+
+    struct interest
+    {
+        int state, node, target, targtype;
+        float score;
+        interest() : state(-1), node(-1), target(-1), targtype(-1), score(0.f) {}
+        ~interest() {}
+    };
+
+    struct aistate
+    {
+        int type, millis, targtype, target, idle;
+        bool override;
+
+        aistate(int m, int t, int r = -1, int v = -1) : type(t), millis(m), targtype(r), target(v)
+        {
+            reset();
+        }
+        ~aistate() {}
+
+        void reset()
+        {
+            idle = 0;
+            override = false;
+        }
+    };
+
+    const int NUMPREVNODES = 6;
+
+    struct aiinfo
+    {
+        vector<aistate> state;
+        vector<int> route;
+        vec target, spot;
+        int enemy, enemyseen, enemymillis, weappref, prevnodes[NUMPREVNODES], targnode, targlast, targtime, targseq,
+            lastrun, lasthunt, lastaction, lastcheck, jumpseed, jumprand, blocktime, huntseq, blockseq, lastaimrnd;
+        float targyaw, targpitch, views[3], aimrnd[3];
+        bool dontmove, becareful, tryreset, trywipe;
+
+        aiinfo()
+        {
+            clearsetup();
+            reset();
+            loopk(3) views[k] = 0.f;
+        }
+        ~aiinfo() {}
+
+               void clearsetup()
+               {
+               weappref = GUN_PISTOL;
+            spot = target = vec(0, 0, 0);
+            lastaction = lasthunt = lastcheck = enemyseen = enemymillis = blocktime = huntseq = blockseq = targtime = targseq = lastaimrnd = 0;
+            lastrun = jumpseed = lastmillis;
+            jumprand = lastmillis+5000;
+            targnode = targlast = enemy = -1;
+               }
+
+               void clear(bool prev = false)
+               {
+            if(prev) memset(prevnodes, -1, sizeof(prevnodes));
+            route.setsize(0);
+               }
+
+        void wipe(bool prev = false)
+        {
+            clear(prev);
+            state.setsize(0);
+            addstate(AI_S_WAIT);
+            trywipe = false;
+        }
+
+        void clean(bool tryit = false)
+        {
+            if(!tryit) becareful = dontmove = false;
+            targyaw = rnd(360);
+            targpitch = 0.f;
+            tryreset = tryit;
+        }
+
+        void reset(bool tryit = false) { wipe(); clean(tryit); }
+
+        bool hasprevnode(int n) const
+        {
+            loopi(NUMPREVNODES) if(prevnodes[i] == n) return true;
+            return false;
+        }
+
+        void addprevnode(int n)
+        {
+            if(prevnodes[0] != n)
+            {
+                memmove(&prevnodes[1], prevnodes, sizeof(prevnodes) - sizeof(prevnodes[0]));
+                prevnodes[0] = n;
+            }
+        }
+
+        aistate &addstate(int t, int r = -1, int v = -1)
+        {
+            return state.add(aistate(lastmillis, t, r, v));
+        }
+
+        void removestate(int index = -1)
+        {
+            if(index < 0) state.pop();
+            else if(state.inrange(index)) state.remove(index);
+            if(!state.length()) addstate(AI_S_WAIT);
+        }
+
+        aistate &getstate(int idx = -1)
+        {
+            if(state.inrange(idx)) return state[idx];
+            return state.last();
+        }
+
+               aistate &switchstate(aistate &b, int t, int r = -1, int v = -1)
+               {
+                       if((b.type == t && b.targtype == r) || (b.type == AI_S_INTEREST && b.targtype == AI_T_NODE))
+                       {
+                               b.millis = lastmillis;
+                               b.target = v;
+                               b.reset();
+                               return b;
+                       }
+                       return addstate(t, r, v);
+               }
+    };
+
+       extern avoidset obstacles;
+    extern vec aitarget;
+
+    extern float viewdist(int x = 101);
+    extern float viewfieldx(int x = 101);
+    extern float viewfieldy(int x = 101);
+    extern bool targetable(fpsent *d, fpsent *e);
+    extern bool cansee(fpsent *d, vec &x, vec &y, vec &targ = aitarget);
+
+    extern void init(fpsent *d, int at, int on, int sk, int bn, int pm, const char *name, const char *team);
+    extern void update();
+    extern void avoid();
+    extern void think(fpsent *d, bool run);
+
+    extern bool badhealth(fpsent *d);
+    extern bool checkothers(vector<int> &targets, fpsent *d = NULL, int state = -1, int targtype = -1, int target = -1, bool teams = false, int *members = NULL);
+    extern bool makeroute(fpsent *d, aistate &b, int node, bool changed = true, int retries = 0);
+    extern bool makeroute(fpsent *d, aistate &b, const vec &pos, bool changed = true, int retries = 0);
+    extern bool randomnode(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX);
+    extern bool randomnode(fpsent *d, aistate &b, float guard = SIGHTMIN, float wander = SIGHTMAX);
+    extern bool violence(fpsent *d, aistate &b, fpsent *e, int pursue = 0);
+    extern bool patrol(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX, int walk = 1, bool retry = false);
+    extern bool defend(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX, int walk = 1);
+    extern void assist(fpsent *d, aistate &b, vector<interest> &interests, bool all = false, bool force = false);
+    extern bool parseinterests(fpsent *d, aistate &b, vector<interest> &interests, bool override = false, bool ignore = false);
+
+       extern void spawned(fpsent *d);
+       extern void damaged(fpsent *d, fpsent *e);
+       extern void killed(fpsent *d, fpsent *e);
+       extern void itemspawned(int ent);
+
+    extern void render();
+}
+
+
diff --git a/src/fpsgame/aiman.h b/src/fpsgame/aiman.h
new file mode 100644 (file)
index 0000000..a36118c
--- /dev/null
@@ -0,0 +1,276 @@
+// server-side ai manager
+namespace aiman
+{
+    bool dorefresh = false, botbalance = true;
+    VARN(serverbotlimit, botlimit, 0, 8, MAXBOTS);
+    VAR(serverbotbalance, 0, 1, 1);
+
+    void calcteams(vector<teamscore> &teams)
+    {
+        static const char * const defaults[2] = { "good", "evil" };
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue;
+            teamscore *t = NULL;
+            loopvj(teams) if(!strcmp(teams[j].team, ci->team)) { t = &teams[j]; break; }
+            if(t) t->score++;
+            else teams.add(teamscore(ci->team, 1));
+        }
+        teams.sort(teamscore::compare);
+        if(teams.length() < int(sizeof(defaults)/sizeof(defaults[0])))
+        {
+            loopi(sizeof(defaults)/sizeof(defaults[0])) if(teams.htfind(defaults[i]) < 0) teams.add(teamscore(defaults[i], 0));
+        }
+    }
+
+    void balanceteams()
+    {
+        vector<teamscore> teams;
+        calcteams(teams);
+        vector<clientinfo *> reassign;
+        loopv(bots) if(bots[i]) reassign.add(bots[i]);
+        while(reassign.length() && teams.length() && teams[0].score > teams.last().score + 1)
+        {
+            teamscore &t = teams.last();
+            clientinfo *bot = NULL;
+            loopv(reassign) if(reassign[i] && !strcmp(reassign[i]->team, teams[0].team))
+            {
+                bot = reassign.removeunordered(i);
+                teams[0].score--;
+                t.score++;
+                for(int j = teams.length() - 2; j >= 0; j--)
+                {
+                    if(teams[j].score >= teams[j+1].score) break;
+                    swap(teams[j], teams[j+1]);
+                }
+                break;
+            }
+            if(bot)
+            {
+                if(smode && bot->state.state==CS_ALIVE) smode->changeteam(bot, bot->team, t.team);
+                copystring(bot->team, t.team, MAXTEAMLEN+1);
+                sendf(-1, 1, "riisi", N_SETTEAM, bot->clientnum, bot->team, 0);
+            }
+            else teams.remove(0, 1);
+        }
+    }
+
+    const char *chooseteam()
+    {
+        vector<teamscore> teams;
+        calcteams(teams);
+        return teams.length() ? teams.last().team : "";
+    }
+
+    static inline bool validaiclient(clientinfo *ci)
+    {
+        return ci->clientnum >= 0 && ci->state.aitype == AI_NONE && (ci->state.state!=CS_SPECTATOR || ci->local || (ci->privilege && !ci->warned));
+    }
+
+       clientinfo *findaiclient(clientinfo *exclude = NULL)
+       {
+        clientinfo *least = NULL;
+               loopv(clients)
+               {
+                       clientinfo *ci = clients[i];
+                       if(!validaiclient(ci) || ci==exclude) continue;
+            if(!least || ci->bots.length() < least->bots.length()) least = ci;
+               }
+        return least;
+       }
+
+       bool addai(int skill, int limit)
+       {
+               int numai = 0, cn = -1, maxai = limit >= 0 ? min(limit, MAXBOTS) : MAXBOTS;
+               loopv(bots)
+        {
+            clientinfo *ci = bots[i];
+            if(!ci || ci->ownernum < 0) { if(cn < 0) cn = i; continue; }
+                       numai++;
+               }
+               if(numai >= maxai) return false;
+        if(bots.inrange(cn))
+        {
+            clientinfo *ci = bots[cn];
+            if(ci)
+            { // reuse a slot that was going to removed
+
+                clientinfo *owner = findaiclient();
+                ci->ownernum = owner ? owner->clientnum : -1;
+                if(owner) owner->bots.add(ci);
+                ci->aireinit = 2;
+                dorefresh = true;
+                return true;
+            }
+        }
+        else { cn = bots.length(); bots.add(NULL); }
+        const char *team = m_teammode ? chooseteam() : "";
+        if(!bots[cn]) bots[cn] = new clientinfo;
+        clientinfo *ci = bots[cn];
+               ci->clientnum = MAXCLIENTS + cn;
+               ci->state.aitype = AI_BOT;
+        clientinfo *owner = findaiclient();
+               ci->ownernum = owner ? owner->clientnum : -1;
+        if(owner) owner->bots.add(ci);
+        ci->state.skill = skill <= 0 ? rnd(50) + 51 : clamp(skill, 1, 101);
+           clients.add(ci);
+               ci->state.lasttimeplayed = lastmillis;
+               copystring(ci->name, "bot", MAXNAMELEN+1);
+               ci->state.state = CS_DEAD;
+        copystring(ci->team, team, MAXTEAMLEN+1);
+        ci->playermodel = rnd(128);
+               ci->aireinit = 2;
+               ci->connected = true;
+        dorefresh = true;
+               return true;
+       }
+
+       void deleteai(clientinfo *ci)
+       {
+        int cn = ci->clientnum - MAXCLIENTS;
+        if(!bots.inrange(cn)) return;
+        if(ci->ownernum >= 0 && !ci->aireinit && smode) smode->leavegame(ci, true);
+        sendf(-1, 1, "ri2", N_CDIS, ci->clientnum);
+        clientinfo *owner = (clientinfo *)getclientinfo(ci->ownernum);
+        if(owner) owner->bots.removeobj(ci);
+        clients.removeobj(ci);
+        DELETEP(bots[cn]);
+               dorefresh = true;
+       }
+
+       bool deleteai()
+       {
+        loopvrev(bots) if(bots[i] && bots[i]->ownernum >= 0)
+        {
+                       deleteai(bots[i]);
+                       return true;
+               }
+               return false;
+       }
+
+       void reinitai(clientinfo *ci)
+       {
+               if(ci->ownernum < 0) deleteai(ci);
+               else if(ci->aireinit >= 1)
+               {
+                       sendf(-1, 1, "ri6ss", N_INITAI, ci->clientnum, ci->ownernum, ci->state.aitype, ci->state.skill, ci->playermodel, ci->name, ci->team);
+                       if(ci->aireinit == 2)
+            {
+                ci->reassign();
+                if(ci->state.state==CS_ALIVE) sendspawn(ci);
+                else sendresume(ci);
+            }
+                       ci->aireinit = 0;
+               }
+       }
+
+       void shiftai(clientinfo *ci, clientinfo *owner = NULL)
+       {
+        if(ci->ownernum >= 0 && !ci->aireinit && smode) smode->leavegame(ci, true);
+        clientinfo *prevowner = (clientinfo *)getclientinfo(ci->ownernum);
+        if(prevowner) prevowner->bots.removeobj(ci);
+               if(!owner) { ci->aireinit = 0; ci->ownernum = -1; }
+               else if(ci->ownernum != owner->clientnum) { ci->aireinit = 2; ci->ownernum = owner->clientnum; owner->bots.add(ci); }
+        dorefresh = true;
+       }
+
+       void removeai(clientinfo *ci)
+       { // either schedules a removal, or someone else to assign to
+
+               loopvrev(ci->bots) shiftai(ci->bots[i], findaiclient(ci));
+       }
+
+       bool reassignai()
+       {
+        clientinfo *hi = NULL, *lo = NULL;
+               loopv(clients)
+               {
+                       clientinfo *ci = clients[i];
+                       if(!validaiclient(ci)) continue;
+            if(!lo || ci->bots.length() < lo->bots.length()) lo = ci;
+            if(!hi || ci->bots.length() > hi->bots.length()) hi = ci;
+               }
+               if(hi && lo && hi->bots.length() - lo->bots.length() > 1)
+               {
+                       loopvrev(hi->bots)
+                       {
+                               shiftai(hi->bots[i], lo);
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+
+       void checksetup()
+       {
+           if(m_teammode && botbalance) balanceteams();
+               loopvrev(bots) if(bots[i]) reinitai(bots[i]);
+       }
+
+       void clearai()
+       { // clear and remove all ai immediately
+        loopvrev(bots) if(bots[i]) deleteai(bots[i]);
+       }
+
+       void checkai()
+       {
+        if(!dorefresh) return;
+        dorefresh = false;
+        if(m_botmode && numclients(-1, false, true))
+               {
+                       checksetup();
+                       while(reassignai());
+               }
+               else clearai();
+       }
+
+       void reqadd(clientinfo *ci, int skill)
+       {
+        if(!ci->local && !ci->privilege) return;
+        if(!addai(skill, !ci->local && ci->privilege < PRIV_ADMIN ? botlimit : -1)) sendf(ci->clientnum, 1, "ris", N_SERVMSG, "failed to create or assign bot");
+       }
+
+       void reqdel(clientinfo *ci)
+       {
+        if(!ci->local && !ci->privilege) return;
+        if(!deleteai()) sendf(ci->clientnum, 1, "ris", N_SERVMSG, "failed to remove any bots");
+       }
+
+    void setbotlimit(clientinfo *ci, int limit)
+    {
+        if(ci && !ci->local && ci->privilege < PRIV_ADMIN) return;
+        botlimit = clamp(limit, 0, MAXBOTS);
+        dorefresh = true;
+        defformatstring(msg, "bot limit is now %d", botlimit);
+        sendservmsg(msg);
+    }
+
+    void setbotbalance(clientinfo *ci, bool balance)
+    {
+        if(ci && !ci->local && !ci->privilege) return;
+        botbalance = balance ? 1 : 0;
+        dorefresh = true;
+        defformatstring(msg, "bot team balancing is now %s", botbalance ? "enabled" : "disabled");
+        sendservmsg(msg);
+    }
+
+
+    void changemap()
+    {
+        dorefresh = true;
+        loopv(clients) if(clients[i]->local || clients[i]->privilege) return;
+        if(botbalance != (serverbotbalance != 0)) setbotbalance(NULL, serverbotbalance != 0);
+    }
+
+    void addclient(clientinfo *ci)
+    {
+        if(ci->state.aitype == AI_NONE) dorefresh = true;
+    }
+
+    void changeteam(clientinfo *ci)
+    {
+        if(ci->state.aitype == AI_NONE) dorefresh = true;
+    }
+}
diff --git a/src/fpsgame/client.cpp b/src/fpsgame/client.cpp
new file mode 100644 (file)
index 0000000..3717a58
--- /dev/null
@@ -0,0 +1,2164 @@
+#include "game.h"
+
+namespace game
+{
+    VARP(minradarscale, 0, 384, 10000);
+    VARP(maxradarscale, 1, 1024, 10000);
+    VARP(radarteammates, 0, 1, 1);
+    FVARP(minimapalpha, 0, 1, 1);
+
+    float calcradarscale()
+    {
+        return clamp(max(minimapradius.x, minimapradius.y)/3, float(minradarscale), float(maxradarscale));
+    }
+
+    void drawminimap(fpsent *d, float x, float y, float s)
+    {
+        vec pos = vec(d->o).sub(minimapcenter).mul(minimapscale).add(0.5f), dir;
+        vecfromyawpitch(camera1->yaw, 0, 1, 0, dir);
+        float scale = calcradarscale();
+        gle::defvertex(2);
+        gle::deftexcoord0();
+        gle::begin(GL_TRIANGLE_FAN);
+        loopi(16)
+        {
+            vec v = vec(0, -1, 0).rotate_around_z(i/16.0f*2*M_PI);
+            gle::attribf(x + 0.5f*s*(1.0f + v.x), y + 0.5f*s*(1.0f + v.y));
+            vec tc = vec(dir).rotate_around_z(i/16.0f*2*M_PI);
+            gle::attribf(pos.x + tc.x*scale*minimapscale.x, pos.y + tc.y*scale*minimapscale.y);
+        }
+        gle::end();
+    }
+
+    void drawradar(float x, float y, float s)
+    {
+        gle::defvertex(2);
+        gle::deftexcoord0();
+        gle::begin(GL_TRIANGLE_STRIP);
+        gle::attribf(x,   y);   gle::attribf(0, 0);
+        gle::attribf(x+s, y);   gle::attribf(1, 0);
+        gle::attribf(x,   y+s); gle::attribf(0, 1);
+        gle::attribf(x+s, y+s); gle::attribf(1, 1);
+        gle::end();
+    }
+
+    void drawteammate(fpsent *d, float x, float y, float s, fpsent *o, float scale)
+    {
+        vec dir = d->o;
+        dir.sub(o->o).div(scale);
+        float dist = dir.magnitude2(), maxdist = 1 - 0.05f - 0.05f;
+        if(dist >= maxdist) dir.mul(maxdist/dist);
+        dir.rotate_around_z(-camera1->yaw*RAD);
+        float bs = 0.06f*s,
+              bx = x + s*0.5f*(1.0f + dir.x),
+              by = y + s*0.5f*(1.0f + dir.y);
+        vec v(-0.5f, -0.5f, 0);
+        v.rotate_around_z((90+o->yaw-camera1->yaw)*RAD);
+        gle::attribf(bx + bs*v.x, by + bs*v.y); gle::attribf(0, 0);
+        gle::attribf(bx + bs*v.y, by - bs*v.x); gle::attribf(1, 0);
+        gle::attribf(bx - bs*v.x, by - bs*v.y); gle::attribf(1, 1);
+        gle::attribf(bx - bs*v.y, by + bs*v.x); gle::attribf(0, 1);
+    }
+
+    void drawteammates(fpsent *d, float x, float y, float s)
+    {
+        if(!radarteammates) return;
+        float scale = calcradarscale();
+        int alive = 0, dead = 0;
+        loopv(players) 
+        {
+            fpsent *o = players[i];
+            if(o != d && o->state == CS_ALIVE && isteam(o->team, d->team))
+            {
+                if(!alive++) 
+                {
+                    settexture(isteam(d->team, player1->team) ? "packages/hud/blip_blue_alive.png" : "packages/hud/blip_red_alive.png");
+                    gle::defvertex(2);
+                    gle::deftexcoord0();
+                    gle::begin(GL_QUADS);
+                }
+                drawteammate(d, x, y, s, o, scale);
+            }
+        }
+        if(alive) gle::end();
+        loopv(players) 
+        {
+            fpsent *o = players[i];
+            if(o != d && o->state == CS_DEAD && isteam(o->team, d->team))
+            {
+                if(!dead++) 
+                {
+                    settexture(isteam(d->team, player1->team) ? "packages/hud/blip_blue_dead.png" : "packages/hud/blip_red_dead.png");
+                    gle::defvertex(2);
+                    gle::deftexcoord0();
+                    gle::begin(GL_QUADS);
+                }
+                drawteammate(d, x, y, s, o, scale);
+            }
+        }
+        if(dead) gle::end();
+    }
+        
+    #include "capture.h"
+    #include "ctf.h"
+    #include "collect.h"
+
+    clientmode *cmode = NULL;
+    captureclientmode capturemode;
+    ctfclientmode ctfmode;
+    collectclientmode collectmode;
+
+    void setclientmode()
+    {
+        if(m_capture) cmode = &capturemode;
+        else if(m_ctf) cmode = &ctfmode;
+        else if(m_collect) cmode = &collectmode;
+        else cmode = NULL;
+    }
+
+    bool senditemstoserver = false, sendcrc = false; // after a map change, since server doesn't have map data
+    int lastping = 0;
+
+    bool connected = false, remote = false, demoplayback = false, gamepaused = false;
+    int sessionid = 0, mastermode = MM_OPEN, gamespeed = 100;
+    string servinfo = "", servauth = "", connectpass = "";
+
+    VARP(deadpush, 1, 2, 20);
+
+    void switchname(const char *name)
+    {
+        filtertext(player1->name, name, false, false, MAXNAMELEN);
+        if(!player1->name[0]) copystring(player1->name, "unnamed");
+        addmsg(N_SWITCHNAME, "rs", player1->name);
+    }
+    void printname()
+    {
+        conoutf("your name is: %s", colorname(player1));
+    }
+    ICOMMAND(name, "sN", (char *s, int *numargs),
+    {
+        if(*numargs > 0) switchname(s);
+        else if(!*numargs) printname();
+        else result(colorname(player1));
+    });
+    ICOMMAND(getname, "", (), result(player1->name));
+
+    void switchteam(const char *team)
+    {
+        if(player1->clientnum < 0) filtertext(player1->team, team, false, false, MAXTEAMLEN);
+        else addmsg(N_SWITCHTEAM, "rs", team);
+    }
+    void printteam()
+    {
+        conoutf("your team is: %s", player1->team);
+    }
+    ICOMMAND(team, "sN", (char *s, int *numargs),
+    {
+        if(*numargs > 0) switchteam(s);
+        else if(!*numargs) printteam();
+        else result(player1->team);
+    });
+    ICOMMAND(getteam, "", (), result(player1->team));
+
+    void switchplayermodel(int playermodel)
+    {
+        player1->playermodel = playermodel;
+        addmsg(N_SWITCHMODEL, "ri", player1->playermodel);
+    }
+
+    struct authkey
+    {
+        char *name, *key, *desc;
+        int lastauth;
+
+        authkey(const char *name, const char *key, const char *desc)
+            : name(newstring(name)), key(newstring(key)), desc(newstring(desc)),
+              lastauth(0)
+        {
+        }
+
+        ~authkey()
+        {
+            DELETEA(name);
+            DELETEA(key);
+            DELETEA(desc);
+        }
+    };
+    vector<authkey *> authkeys;
+
+    authkey *findauthkey(const char *desc = "")
+    {
+        loopv(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcasecmp(authkeys[i]->name, player1->name)) return authkeys[i];
+        loopv(authkeys) if(!strcmp(authkeys[i]->desc, desc)) return authkeys[i];
+        return NULL;
+    }
+
+    VARP(autoauth, 0, 1, 1);
+
+    void addauthkey(const char *name, const char *key, const char *desc)
+    {
+        loopvrev(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcmp(authkeys[i]->name, name)) delete authkeys.remove(i);
+        if(name[0] && key[0]) authkeys.add(new authkey(name, key, desc));
+    }
+    ICOMMAND(authkey, "sss", (char *name, char *key, char *desc), addauthkey(name, key, desc));
+
+    bool hasauthkey(const char *name, const char *desc)
+    {
+        if(!name[0] && !desc[0]) return authkeys.length() > 0;
+        loopvrev(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcmp(authkeys[i]->name, name)) return true;
+        return false;
+    }
+
+    ICOMMAND(hasauthkey, "ss", (char *name, char *desc), intret(hasauthkey(name, desc) ? 1 : 0));
+
+    void genauthkey(const char *secret)
+    {
+        if(!secret[0]) { conoutf(CON_ERROR, "you must specify a secret password"); return; }
+        vector<char> privkey, pubkey;
+        genprivkey(secret, privkey, pubkey);
+        conoutf("private key: %s", privkey.getbuf());
+        conoutf("public key: %s", pubkey.getbuf());
+        result(privkey.getbuf());
+    }
+    COMMAND(genauthkey, "s");
+
+    void getpubkey(const char *desc)
+    {
+        authkey *k = findauthkey(desc);
+        if(!k) { if(desc[0]) conoutf(CON_ERROR, "no authkey found: %s", desc); else conoutf(CON_ERROR, "no global authkey found"); return; }
+        vector<char> pubkey;
+        if(!calcpubkey(k->key, pubkey)) { conoutf(CON_ERROR, "failed calculating pubkey"); return; }
+        result(pubkey.getbuf());
+    }
+    COMMAND(getpubkey, "s");
+
+    void saveauthkeys()
+    {
+        stream *f = openfile("auth.cfg", "w");
+        if(!f) { conoutf(CON_ERROR, "failed to open auth.cfg for writing"); return; }
+        loopv(authkeys)
+        {
+            authkey *a = authkeys[i];
+            f->printf("authkey %s %s %s\n", escapestring(a->name), escapestring(a->key), escapestring(a->desc));
+        }
+        conoutf("saved authkeys to auth.cfg");
+        delete f;
+    }
+    COMMAND(saveauthkeys, "");
+
+    void sendmapinfo()
+    {
+        if(!connected) return;
+        sendcrc = true;
+        if(player1->state!=CS_SPECTATOR || player1->privilege || !remote) senditemstoserver = true;
+    }
+
+    void writeclientinfo(stream *f)
+    {
+        f->printf("name %s\n", escapestring(player1->name));
+    }
+
+    bool allowedittoggle()
+    {
+        if(editmode) return true;
+        if(isconnected() && multiplayer(false) && !m_edit)
+        {
+            conoutf(CON_ERROR, "editing in multiplayer requires coop edit mode (1)");
+            return false;
+        }
+        return execidentbool("allowedittoggle", true);
+    }
+
+    void edittoggled(bool on)
+    {
+        addmsg(N_EDITMODE, "ri", on ? 1 : 0);
+        if(player1->state==CS_DEAD) deathstate(player1, true);
+        else if(player1->state==CS_EDITING && player1->editstate==CS_DEAD) showscores(false);
+        disablezoom();
+        player1->suicided = player1->respawned = -2;
+    }
+
+    const char *getclientname(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d ? d->name : "";
+    }
+    ICOMMAND(getclientname, "i", (int *cn), result(getclientname(*cn)));
+
+    const char *getclientteam(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d ? d->team : "";
+    }
+    ICOMMAND(getclientteam, "i", (int *cn), result(getclientteam(*cn)));
+
+    int getclientmodel(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d ? d->playermodel : -1;
+    }
+    ICOMMAND(getclientmodel, "i", (int *cn), intret(getclientmodel(*cn)));
+
+    const char *getclienticon(int cn)
+    {
+        fpsent *d = getclient(cn);
+        if(!d || d->state==CS_SPECTATOR) return "spectator";
+        const playermodelinfo &mdl = getplayermodelinfo(d);
+        return m_teammode ? (isteam(player1->team, d->team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon;
+    }
+    ICOMMAND(getclienticon, "i", (int *cn), result(getclienticon(*cn)));
+
+    bool ismaster(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d && d->privilege >= PRIV_MASTER;
+    }
+    ICOMMAND(ismaster, "i", (int *cn), intret(ismaster(*cn) ? 1 : 0));
+
+    bool isauth(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d && d->privilege >= PRIV_AUTH;
+    }
+    ICOMMAND(isauth, "i", (int *cn), intret(isauth(*cn) ? 1 : 0));
+
+    bool isadmin(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d && d->privilege >= PRIV_ADMIN;
+    }
+    ICOMMAND(isadmin, "i", (int *cn), intret(isadmin(*cn) ? 1 : 0));
+
+    ICOMMAND(getmastermode, "", (), intret(mastermode));
+    ICOMMAND(mastermodename, "i", (int *mm), result(server::mastermodename(*mm, "")));
+
+    bool isspectator(int cn)
+    {
+        fpsent *d = getclient(cn);
+        return d && d->state==CS_SPECTATOR;
+    }
+    ICOMMAND(isspectator, "i", (int *cn), intret(isspectator(*cn) ? 1 : 0));
+
+    bool isai(int cn, int type)
+    {
+        fpsent *d = getclient(cn);
+        int aitype = type > 0 && type < AI_MAX ? type : AI_BOT;
+        return d && d->aitype==aitype;
+    }
+    ICOMMAND(isai, "ii", (int *cn, int *type), intret(isai(*cn, *type) ? 1 : 0));
+
+    int parseplayer(const char *arg)
+    {
+        char *end;
+        int n = strtol(arg, &end, 10);
+        if(*arg && !*end)
+        {
+            if(n!=player1->clientnum && !clients.inrange(n)) return -1;
+            return n;
+        }
+        // try case sensitive first
+        loopv(players)
+        {
+            fpsent *o = players[i];
+            if(!strcmp(arg, o->name)) return o->clientnum;
+        }
+        // nothing found, try case insensitive
+        loopv(players)
+        {
+            fpsent *o = players[i];
+            if(!strcasecmp(arg, o->name)) return o->clientnum;
+        }
+        return -1;
+    }
+    ICOMMAND(getclientnum, "s", (char *name), intret(name[0] ? parseplayer(name) : player1->clientnum));
+
+    void listclients(bool local, bool bots)
+    {
+        vector<char> buf;
+        string cn;
+        int numclients = 0;
+        if(local && connected)
+        {
+            formatstring(cn, "%d", player1->clientnum);
+            buf.put(cn, strlen(cn));
+            numclients++;
+        }
+        loopv(clients) if(clients[i] && (bots || clients[i]->aitype == AI_NONE))
+        {
+            formatstring(cn, "%d", clients[i]->clientnum);
+            if(numclients++) buf.add(' ');
+            buf.put(cn, strlen(cn));
+        }
+        buf.add('\0');
+        result(buf.getbuf());
+    }
+    ICOMMAND(listclients, "bb", (int *local, int *bots), listclients(*local>0, *bots!=0));
+
+    void clearbans()
+    {
+        addmsg(N_CLEARBANS, "r");
+    }
+    COMMAND(clearbans, "");
+
+    void kick(const char *victim, const char *reason)
+    {
+        int vn = parseplayer(victim);
+        if(vn>=0 && vn!=player1->clientnum) addmsg(N_KICK, "ris", vn, reason);
+    }
+    COMMAND(kick, "ss");
+
+    void authkick(const char *desc, const char *victim, const char *reason)
+    {
+        authkey *a = findauthkey(desc);
+        int vn = parseplayer(victim);
+        if(a && vn>=0 && vn!=player1->clientnum) 
+        {
+            a->lastauth = lastmillis;
+            addmsg(N_AUTHKICK, "rssis", a->desc, a->name, vn, reason);
+        }
+    }
+    ICOMMAND(authkick, "ss", (const char *victim, const char *reason), authkick("", victim, reason));
+    ICOMMAND(sauthkick, "ss", (const char *victim, const char *reason), if(servauth[0]) authkick(servauth, victim, reason));
+    ICOMMAND(dauthkick, "sss", (const char *desc, const char *victim, const char *reason), if(desc[0]) authkick(desc, victim, reason));
+
+    vector<int> ignores;
+
+    void ignore(int cn)
+    {
+        fpsent *d = getclient(cn);
+        if(!d || d == player1) return;
+        conoutf("ignoring %s", d->name);
+        if(ignores.find(cn) < 0) ignores.add(cn);
+    }
+
+    void unignore(int cn)
+    {
+        if(ignores.find(cn) < 0) return;
+        fpsent *d = getclient(cn);
+        if(d) conoutf("stopped ignoring %s", d->name);
+        ignores.removeobj(cn);
+    }
+
+    bool isignored(int cn) { return ignores.find(cn) >= 0; }
+
+    ICOMMAND(ignore, "s", (char *arg), ignore(parseplayer(arg)));
+    ICOMMAND(unignore, "s", (char *arg), unignore(parseplayer(arg))); 
+    ICOMMAND(isignored, "s", (char *arg), intret(isignored(parseplayer(arg)) ? 1 : 0));
+
+    void setteam(const char *arg1, const char *arg2)
+    {
+        int i = parseplayer(arg1);
+        if(i>=0) addmsg(N_SETTEAM, "ris", i, arg2);
+    }
+    COMMAND(setteam, "ss");
+
+    void hashpwd(const char *pwd)
+    {
+        if(player1->clientnum<0) return;
+        string hash;
+        server::hashpassword(player1->clientnum, sessionid, pwd, hash);
+        result(hash);
+    }
+    COMMAND(hashpwd, "s");
+
+    void setmaster(const char *arg, const char *who)
+    {
+        if(!arg[0]) return;
+        int val = 1, cn = player1->clientnum;
+        if(who[0])
+        {
+            cn = parseplayer(who);
+            if(cn < 0) return;
+        }
+        string hash = "";
+        if(!arg[1] && isdigit(arg[0])) val = parseint(arg);
+        else 
+        {
+            if(cn != player1->clientnum) return;
+            server::hashpassword(player1->clientnum, sessionid, arg, hash);
+        }
+        addmsg(N_SETMASTER, "riis", cn, val, hash);
+    }
+    COMMAND(setmaster, "ss");
+    ICOMMAND(mastermode, "i", (int *val), addmsg(N_MASTERMODE, "ri", *val));
+
+    bool tryauth(const char *desc)
+    {
+        authkey *a = findauthkey(desc);
+        if(!a) return false;
+        a->lastauth = lastmillis;
+        addmsg(N_AUTHTRY, "rss", a->desc, a->name);
+        return true;
+    }
+    ICOMMAND(auth, "s", (char *desc), tryauth(desc));
+    ICOMMAND(sauth, "", (), if(servauth[0]) tryauth(servauth));
+    ICOMMAND(dauth, "s", (char *desc), if(desc[0]) tryauth(desc));
+
+    ICOMMAND(getservauth, "", (), result(servauth));
+
+    void togglespectator(int val, const char *who)
+    {
+        int i = who[0] ? parseplayer(who) : player1->clientnum;
+        if(i>=0) addmsg(N_SPECTATOR, "rii", i, val);
+    }
+    ICOMMAND(spectator, "is", (int *val, char *who), togglespectator(*val, who));
+
+    ICOMMAND(checkmaps, "", (), addmsg(N_CHECKMAPS, "r"));
+
+    int gamemode = INT_MAX, nextmode = INT_MAX;
+    string clientmap = "";
+
+    void changemapserv(const char *name, int mode)        // forced map change from the server
+    {
+        if(multiplayer(false) && !m_mp(mode))
+        {
+            conoutf(CON_ERROR, "mode %s (%d) not supported in multiplayer", server::modename(gamemode), gamemode);
+            loopi(NUMGAMEMODES) if(m_mp(STARTGAMEMODE + i)) { mode = STARTGAMEMODE + i; break; }
+        }
+
+        gamemode = mode;
+        nextmode = mode;
+        if(editmode) toggleedit();
+        if(m_demo) { entities::resetspawns(); return; }
+        if((m_edit && !name[0]) || !load_world(name))
+        {
+            emptymap(0, true, name);
+            senditemstoserver = false;
+        }
+        startgame();
+    }
+
+    void setmode(int mode)
+    {
+        if(multiplayer(false) && !m_mp(mode))
+        {
+            conoutf(CON_ERROR, "mode %s (%d) not supported in multiplayer",  server::modename(mode), mode);
+            intret(0);
+            return;
+        }
+        nextmode = mode;
+        intret(1);
+    }
+    ICOMMAND(mode, "i", (int *val), setmode(*val));
+    ICOMMAND(getmode, "", (), intret(gamemode));
+    ICOMMAND(timeremaining, "i", (int *formatted), 
+    {
+        int val = max(maplimit - lastmillis + 999, 0)/1000;
+        if(*formatted)
+        {
+            defformatstring(str, "%d:%02d", val/60, val%60);
+            result(str);
+        }
+        else intret(val);
+    });
+    ICOMMANDS("m_noitems", "i", (int *mode), { int gamemode = *mode; intret(m_noitems); });
+    ICOMMANDS("m_noammo", "i", (int *mode), { int gamemode = *mode; intret(m_noammo); });
+    ICOMMANDS("m_insta", "i", (int *mode), { int gamemode = *mode; intret(m_insta); });
+    ICOMMANDS("m_tactics", "i", (int *mode), { int gamemode = *mode; intret(m_tactics); });
+    ICOMMANDS("m_efficiency", "i", (int *mode), { int gamemode = *mode; intret(m_efficiency); });
+    ICOMMANDS("m_capture", "i", (int *mode), { int gamemode = *mode; intret(m_capture); });
+    ICOMMANDS("m_regencapture", "i", (int *mode), { int gamemode = *mode; intret(m_regencapture); });
+    ICOMMANDS("m_ctf", "i", (int *mode), { int gamemode = *mode; intret(m_ctf); });
+    ICOMMANDS("m_protect", "i", (int *mode), { int gamemode = *mode; intret(m_protect); });
+    ICOMMANDS("m_hold", "i", (int *mode), { int gamemode = *mode; intret(m_hold); });
+    ICOMMANDS("m_collect", "i", (int *mode), { int gamemode = *mode; intret(m_collect); });
+    ICOMMANDS("m_teammode", "i", (int *mode), { int gamemode = *mode; intret(m_teammode); });
+    ICOMMANDS("m_demo", "i", (int *mode), { int gamemode = *mode; intret(m_demo); });
+    ICOMMANDS("m_edit", "i", (int *mode), { int gamemode = *mode; intret(m_edit); });
+    ICOMMANDS("m_lobby", "i", (int *mode), { int gamemode = *mode; intret(m_lobby); });
+    ICOMMANDS("m_sp", "i", (int *mode), { int gamemode = *mode; intret(m_sp); });
+    ICOMMANDS("m_dmsp", "i", (int *mode), { int gamemode = *mode; intret(m_dmsp); });
+    ICOMMANDS("m_classicsp", "i", (int *mode), { int gamemode = *mode; intret(m_classicsp); });
+
+    void changemap(const char *name, int mode) // request map change, server may ignore
+    {
+        if(!remote)
+        {
+            server::forcemap(name, mode);
+            if(!isconnected()) localconnect();
+        }
+        else if(player1->state!=CS_SPECTATOR || player1->privilege) addmsg(N_MAPVOTE, "rsi", name, mode);
+    }
+    void changemap(const char *name)
+    {
+        changemap(name, m_valid(nextmode) ? nextmode : (remote ? 0 : 1));
+    }
+    ICOMMAND(map, "s", (char *name), changemap(name));
+
+    void forceintermission()
+    {
+        if(!remote && !hasnonlocalclients()) server::startintermission();
+        else addmsg(N_FORCEINTERMISSION, "r");
+    }
+
+    void forceedit(const char *name)
+    {
+        changemap(name, 1);
+    }
+
+    void newmap(int size)
+    {
+        addmsg(N_NEWMAP, "ri", size);
+    }
+
+    int needclipboard = -1;
+
+    void sendclipboard()
+    {
+        uchar *outbuf = NULL;
+        int inlen = 0, outlen = 0;
+        if(!packeditinfo(localedit, inlen, outbuf, outlen))
+        {
+            outbuf = NULL;
+            inlen = outlen = 0;
+        }
+        packetbuf p(16 + outlen, ENET_PACKET_FLAG_RELIABLE);
+        putint(p, N_CLIPBOARD);
+        putint(p, inlen);
+        putint(p, outlen);
+        if(outlen > 0) p.put(outbuf, outlen);
+        sendclientpacket(p.finalize(), 1);
+        needclipboard = -1;
+    }
+
+    void edittrigger(const selinfo &sel, int op, int arg1, int arg2, int arg3, const VSlot *vs)
+    {
+        if(m_edit) switch(op)
+        {
+            case EDIT_FLIP:
+            case EDIT_COPY:
+            case EDIT_PASTE:
+            case EDIT_DELCUBE:
+            {
+                switch(op)
+                {
+                    case EDIT_COPY: needclipboard = 0; break;
+                    case EDIT_PASTE:
+                        if(needclipboard > 0)
+                        {
+                            c2sinfo(true);
+                            sendclipboard();
+                        }
+                        break;
+                }
+                addmsg(N_EDITF + op, "ri9i4",
+                   sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient,
+                   sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner);
+                break;
+            }
+            case EDIT_ROTATE:
+            {
+                addmsg(N_EDITF + op, "ri9i5",
+                   sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient,
+                   sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner,
+                   arg1);
+                break;
+            }
+            case EDIT_MAT:
+            case EDIT_FACE:
+            {
+                addmsg(N_EDITF + op, "ri9i6",
+                   sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient,
+                   sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner,
+                   arg1, arg2);
+                break;
+            }
+            case EDIT_TEX:
+            {
+                int tex1 = shouldpacktex(arg1);
+                if(addmsg(N_EDITF + op, "ri9i6",
+                    sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient,
+                    sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner,
+                    tex1 ? tex1 : arg1, arg2))
+                {
+                    messages.pad(2);
+                    int offset = messages.length();
+                    if(tex1) packvslot(messages, arg1);
+                    *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset));
+                }
+                break;
+            }
+            case EDIT_REPLACE:
+            {
+                int tex1 = shouldpacktex(arg1), tex2 = shouldpacktex(arg2);
+                if(addmsg(N_EDITF + op, "ri9i7",
+                    sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient,
+                    sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner,
+                    tex1 ? tex1 : arg1, tex2 ? tex2 : arg2, arg3))
+                {
+                    messages.pad(2);
+                    int offset = messages.length();
+                    if(tex1) packvslot(messages, arg1);
+                    if(tex2) packvslot(messages, arg2);
+                    *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset));
+                }
+                break;
+            }
+            case EDIT_REMIP:
+            {
+                addmsg(N_EDITF + op, "r");
+                break;
+            }
+            case EDIT_VSLOT:
+            {
+                if(addmsg(N_EDITF + op, "ri9i6",
+                    sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient,
+                    sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner,
+                    arg1, arg2))
+                {
+                    messages.pad(2);
+                    int offset = messages.length();
+                    packvslot(messages, vs);
+                    *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset));
+                }
+                break;
+            }
+            case EDIT_UNDO:
+            case EDIT_REDO:
+            {
+                uchar *outbuf = NULL;
+                int inlen = 0, outlen = 0;
+                if(packundo(op, inlen, outbuf, outlen))
+                {
+                    if(addmsg(N_EDITF + op, "ri2", inlen, outlen)) messages.put(outbuf, outlen);
+                    delete[] outbuf;
+                }
+                break;
+            }
+        }
+    }
+
+    void printvar(fpsent *d, ident *id)
+    {
+        if(id) switch(id->type)
+        {
+            case ID_VAR:
+            {
+                int val = *id->storage.i;
+                string str;
+                if(val < 0)
+                    formatstring(str, "%d", val); 
+                else if(id->flags&IDF_HEX && id->maxval==0xFFFFFF)
+                    formatstring(str, "0x%.6X (%d, %d, %d)", val, (val>>16)&0xFF, (val>>8)&0xFF, val&0xFF);
+                else
+                    formatstring(str, id->flags&IDF_HEX ? "0x%X" : "%d", val);
+                conoutf(CON_INFO, id->index, "%s set map var \"%s\" to %s", colorname(d), id->name, str);
+                break;
+            }
+            case ID_FVAR:
+                conoutf(CON_INFO, id->index, "%s set map var \"%s\" to %s", colorname(d), id->name, floatstr(*id->storage.f));
+                break;
+            case ID_SVAR:
+                conoutf(CON_INFO, id->index, "%s set map var \"%s\" to \"%s\"", colorname(d), id->name, *id->storage.s);
+                break;
+        }
+    }
+
+    void vartrigger(ident *id)
+    {
+        if(!m_edit) return;
+        switch(id->type)
+        {
+            case ID_VAR:
+                addmsg(N_EDITVAR, "risi", ID_VAR, id->name, *id->storage.i);
+                break;
+
+            case ID_FVAR:
+                addmsg(N_EDITVAR, "risf", ID_FVAR, id->name, *id->storage.f);
+                break;
+
+            case ID_SVAR:
+                addmsg(N_EDITVAR, "riss", ID_SVAR, id->name, *id->storage.s);
+                break;
+            default: return;
+        }
+        printvar(player1, id);
+    }
+
+    void pausegame(bool val)
+    {
+        if(!connected) return;
+        if(!remote) server::forcepaused(val);
+        else addmsg(N_PAUSEGAME, "ri", val ? 1 : 0);
+    }
+    ICOMMAND(pausegame, "i", (int *val), pausegame(*val > 0));
+    ICOMMAND(paused, "iN$", (int *val, int *numargs, ident *id),
+    { 
+        if(*numargs > 0) pausegame(clampvar(id, *val, 0, 1) > 0); 
+        else if(*numargs < 0) intret(gamepaused ? 1 : 0);
+        else printvar(id, gamepaused ? 1 : 0); 
+    });
+
+    bool ispaused() { return gamepaused; }
+
+    bool allowmouselook() { return !gamepaused || !remote || m_edit; }
+
+    void changegamespeed(int val)
+    {
+        if(!connected) return;
+        if(!remote) server::forcegamespeed(val);
+        else addmsg(N_GAMESPEED, "ri", val);
+    }
+    ICOMMAND(gamespeed, "iN$", (int *val, int *numargs, ident *id),
+    {
+        if(*numargs > 0) changegamespeed(clampvar(id, *val, 10, 1000));
+        else if(*numargs < 0) intret(gamespeed);
+        else printvar(id, gamespeed);
+    });
+
+    int scaletime(int t) { return t*gamespeed; }
+
+    // collect c2s messages conveniently
+    vector<uchar> messages;
+    int messagecn = -1, messagereliable = false;
+
+    bool addmsg(int type, const char *fmt, ...)
+    {
+        if(!connected) return false;
+        static uchar buf[MAXTRANS];
+        ucharbuf p(buf, sizeof(buf));
+        putint(p, type);
+        int numi = 1, numf = 0, nums = 0, mcn = -1;
+        bool reliable = false;
+        if(fmt)
+        {
+            va_list args;
+            va_start(args, fmt);
+            while(*fmt) switch(*fmt++)
+            {
+                case 'r': reliable = true; break;
+                case 'c':
+                {
+                    fpsent *d = va_arg(args, fpsent *);
+                    mcn = !d || d == player1 ? -1 : d->clientnum;
+                    break;
+                }
+                case 'v':
+                {
+                    int n = va_arg(args, int);
+                    int *v = va_arg(args, int *);
+                    loopi(n) putint(p, v[i]);
+                    numi += n;
+                    break;
+                }
+
+                case 'i':
+                {
+                    int n = isdigit(*fmt) ? *fmt++-'0' : 1;
+                    loopi(n) putint(p, va_arg(args, int));
+                    numi += n;
+                    break;
+                }
+                case 'f':
+                {
+                    int n = isdigit(*fmt) ? *fmt++-'0' : 1;
+                    loopi(n) putfloat(p, (float)va_arg(args, double));
+                    numf += n;
+                    break;
+                }
+                case 's': sendstring(va_arg(args, const char *), p); nums++; break;
+            }
+            va_end(args);
+        }
+        int num = nums || numf ? 0 : numi, msgsize = server::msgsizelookup(type);
+        if(msgsize && num!=msgsize) { fatal("inconsistent msg size for %d (%d != %d)", type, num, msgsize); }
+        if(reliable) messagereliable = true;
+        if(mcn != messagecn)
+        {
+            static uchar mbuf[16];
+            ucharbuf m(mbuf, sizeof(mbuf));
+            putint(m, N_FROMAI);
+            putint(m, mcn);
+            messages.put(mbuf, m.length());
+            messagecn = mcn;
+        }
+        messages.put(buf, p.length());
+        return true;
+    }
+
+    void connectattempt(const char *name, const char *password, const ENetAddress &address)
+    {
+        copystring(connectpass, password);
+    }
+
+    void connectfail()
+    {
+        memset(connectpass, 0, sizeof(connectpass));
+    }
+
+    void gameconnect(bool _remote)
+    {
+        remote = _remote;
+        if(editmode) toggleedit();
+    }
+
+    void gamedisconnect(bool cleanup)
+    {
+        if(remote) stopfollowing();
+        ignores.setsize(0);
+        connected = remote = false;
+        player1->clientnum = -1;
+        sessionid = 0;
+        mastermode = MM_OPEN;
+        messages.setsize(0);
+        messagereliable = false;
+        messagecn = -1;
+        player1->respawn();
+        player1->lifesequence = 0;
+        player1->state = CS_ALIVE;
+        player1->privilege = PRIV_NONE;
+        sendcrc = senditemstoserver = false;
+        demoplayback = false;
+        gamepaused = false;
+        gamespeed = 100;
+        clearclients(false);
+        if(cleanup)
+        {
+            nextmode = gamemode = INT_MAX;
+            clientmap[0] = '\0';
+        }
+    }
+
+    VARP(teamcolorchat, 0, 1, 1);
+    const char *chatcolorname(fpsent *d) { return teamcolorchat ? teamcolorname(d, NULL) : colorname(d); }
+
+    void toserver(char *text) { conoutf(CON_CHAT, "%s:\f0 %s", chatcolorname(player1), text); addmsg(N_TEXT, "rcs", player1, text); }
+    COMMANDN(say, toserver, "C");
+
+    void sayteam(char *text) { conoutf(CON_TEAMCHAT, "\fs\f8[team]\fr %s: \f8%s", chatcolorname(player1), text); addmsg(N_SAYTEAM, "rcs", player1, text); }
+    COMMAND(sayteam, "C");
+
+    ICOMMAND(servcmd, "C", (char *cmd), addmsg(N_SERVCMD, "rs", cmd));
+
+    static void sendposition(fpsent *d, packetbuf &q)
+    {
+        putint(q, N_POS);
+        putuint(q, d->clientnum);
+        // 3 bits phys state, 1 bit life sequence, 2 bits move, 2 bits strafe
+        uchar physstate = d->physstate | ((d->lifesequence&1)<<3) | ((d->move&3)<<4) | ((d->strafe&3)<<6);
+        q.put(physstate);
+        ivec o = ivec(vec(d->o.x, d->o.y, d->o.z-d->eyeheight).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 material
+        uint flags = 0;
+        if(o.x < 0 || o.x > 0xFFFF) flags |= 1<<0;
+        if(o.y < 0 || o.y > 0xFFFF) flags |= 1<<1;
+        if(o.z < 0 || o.z > 0xFFFF) flags |= 1<<2;
+        if(vel > 0xFF) flags |= 1<<3;
+        if(fall > 0)
+        {
+            flags |= 1<<4;
+            if(fall > 0xFF) flags |= 1<<5;
+            if(d->falling.x || d->falling.y || d->falling.z > 0) flags |= 1<<6;
+        }
+        if((lookupmaterial(d->feetpos())&MATF_CLIP) == MAT_GAMECLIP) flags |= 1<<7;
+        putuint(q, flags);
+        loopk(3)
+        {
+            q.put(o[k]&0xFF);
+            q.put((o[k]>>8)&0xFF);
+            if(o[k] < 0 || o[k] > 0xFFFF) q.put((o[k]>>16)&0xFF);
+        }
+        uint dir = (d->yaw < 0 ? 360 + int(d->yaw)%360 : int(d->yaw)%360) + clamp(int(d->pitch+90), 0, 180)*360;
+        q.put(dir&0xFF);
+        q.put((dir>>8)&0xFF);
+        q.put(clamp(int(d->roll+90), 0, 180));
+        q.put(vel&0xFF);
+        if(vel > 0xFF) q.put((vel>>8)&0xFF);
+        float velyaw, velpitch;
+        vectoyawpitch(d->vel, velyaw, velpitch);
+        uint veldir = (velyaw < 0 ? 360 + int(velyaw)%360 : int(velyaw)%360) + clamp(int(velpitch+90), 0, 180)*360;
+        q.put(veldir&0xFF);
+        q.put((veldir>>8)&0xFF);
+        if(fall > 0)
+        {
+            q.put(fall&0xFF);
+            if(fall > 0xFF) q.put((fall>>8)&0xFF);
+            if(d->falling.x || d->falling.y || d->falling.z > 0)
+            {
+                float fallyaw, fallpitch;
+                vectoyawpitch(d->falling, fallyaw, fallpitch);
+                uint falldir = (fallyaw < 0 ? 360 + int(fallyaw)%360 : int(fallyaw)%360) + clamp(int(fallpitch+90), 0, 180)*360;
+                q.put(falldir&0xFF);
+                q.put((falldir>>8)&0xFF);
+            }
+        }
+    }
+
+    void sendposition(fpsent *d, bool reliable)
+    {
+        if(d->state != CS_ALIVE && d->state != CS_EDITING) return;
+        packetbuf q(100, reliable ? ENET_PACKET_FLAG_RELIABLE : 0);
+        sendposition(d, q);
+        sendclientpacket(q.finalize(), 0);
+    }
+
+    void sendpositions()
+    {
+        loopv(players)
+        {
+            fpsent *d = players[i];
+            if((d == player1 || d->ai) && (d->state == CS_ALIVE || d->state == CS_EDITING))
+            {
+                packetbuf q(100);
+                sendposition(d, q);
+                for(int j = i+1; j < players.length(); j++)
+                {
+                    fpsent *d = players[j];
+                    if((d == player1 || d->ai) && (d->state == CS_ALIVE || d->state == CS_EDITING))
+                        sendposition(d, q);
+                }
+                sendclientpacket(q.finalize(), 0);
+                break;
+            }
+        }
+    }
+
+    void sendmessages()
+    {
+        packetbuf p(MAXTRANS);
+        if(sendcrc)
+        {
+            p.reliable();
+            sendcrc = false;
+            const char *mname = getclientmap();
+            putint(p, N_MAPCRC);
+            sendstring(mname, p);
+            putint(p, mname[0] ? getmapcrc() : 0);
+        }
+        if(senditemstoserver)
+        {
+            if(!m_noitems || cmode!=NULL) p.reliable();
+            if(!m_noitems) entities::putitems(p);
+            if(cmode) cmode->senditems(p);
+            senditemstoserver = false;
+        }
+        if(messages.length())
+        {
+            p.put(messages.getbuf(), messages.length());
+            messages.setsize(0);
+            if(messagereliable) p.reliable();
+            messagereliable = false;
+            messagecn = -1;
+        }
+        if(totalmillis-lastping>250)
+        {
+            putint(p, N_PING);
+            putint(p, totalmillis);
+            lastping = totalmillis;
+        }
+        sendclientpacket(p.finalize(), 1);
+    }
+
+    void c2sinfo(bool force) // send update to the server
+    {
+        static int lastupdate = -1000;
+        if(totalmillis - lastupdate < 33 && !force) return; // don't update faster than 30fps
+        lastupdate = totalmillis;
+        sendpositions();
+        sendmessages();
+        flushclient();
+    }
+
+    void sendintro()
+    {
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        putint(p, N_CONNECT);
+        sendstring(player1->name, p);
+        putint(p, player1->playermodel);
+        string hash = "";
+        if(connectpass[0])
+        {
+            server::hashpassword(player1->clientnum, sessionid, connectpass, hash);
+            memset(connectpass, 0, sizeof(connectpass));
+        }
+        sendstring(hash, p);
+        authkey *a = servauth[0] && autoauth ? findauthkey(servauth) : NULL;
+        if(a)
+        {
+            a->lastauth = lastmillis;
+            sendstring(a->desc, p);
+            sendstring(a->name, p);
+        }
+        else
+        {
+            sendstring("", p);
+            sendstring("", p);
+        }
+        sendclientpacket(p.finalize(), 1);
+    }
+
+    void updatepos(fpsent *d)
+    {
+        // update the position of other clients in the game in our world
+        // don't care if he's in the scenery or other players,
+        // just don't overlap with our client
+
+        const float r = player1->radius+d->radius;
+        const float dx = player1->o.x-d->o.x;
+        const float dy = player1->o.y-d->o.y;
+        const float dz = player1->o.z-d->o.z;
+        const float rz = player1->aboveeye+d->eyeheight;
+        const float fx = (float)fabs(dx), fy = (float)fabs(dy), fz = (float)fabs(dz);
+        if(fx<r && fy<r && fz<rz && player1->state!=CS_SPECTATOR && d->state!=CS_DEAD)
+        {
+            if(fx<fy) d->o.y += dy<0 ? r-fy : -(r-fy);  // push aside
+            else      d->o.x += dx<0 ? r-fx : -(r-fx);
+        }
+        int lagtime = totalmillis-d->lastupdate;
+        if(lagtime)
+        {
+            if(d->state!=CS_SPAWNING && d->lastupdate) d->plag = (d->plag*5+lagtime)/6;
+            d->lastupdate = totalmillis;
+        }
+    }
+
+    void parsepositions(ucharbuf &p)
+    {
+        int type;
+        while(p.remaining()) switch(type = getint(p))
+        {
+            case N_DEMOPACKET: break;
+            case N_POS:                        // position of another client
+            {
+                int cn = getuint(p), physstate = p.get(), flags = getuint(p);
+                vec o, vel, falling;
+                float yaw, pitch, roll;
+                loopk(3)
+                {
+                    int n = p.get(); n |= p.get()<<8; if(flags&(1<<k)) { n |= p.get()<<16; if(n&0x800000) n |= ~0U<<24; }
+                    o[k] = n/DMF;
+                }
+                int dir = p.get(); dir |= p.get()<<8;
+                yaw = dir%360;
+                pitch = clamp(dir/360, 0, 180)-90;
+                roll = clamp(int(p.get()), 0, 180)-90;
+                int mag = p.get(); if(flags&(1<<3)) mag |= p.get()<<8;
+                dir = p.get(); dir |= p.get()<<8;
+                vecfromyawpitch(dir%360, clamp(dir/360, 0, 180)-90, 1, 0, vel);
+                vel.mul(mag/DVELF);
+                if(flags&(1<<4))
+                {
+                    mag = p.get(); if(flags&(1<<5)) mag |= p.get()<<8;
+                    if(flags&(1<<6))
+                    {
+                        dir = p.get(); dir |= p.get()<<8;
+                        vecfromyawpitch(dir%360, clamp(dir/360, 0, 180)-90, 1, 0, falling);
+                    }
+                    else falling = vec(0, 0, -1);
+                    falling.mul(mag/DVELF);
+                }
+                else falling = vec(0, 0, 0);
+                int seqcolor = (physstate>>3)&1;
+                fpsent *d = getclient(cn);
+                if(!d || d->lifesequence < 0 || seqcolor!=(d->lifesequence&1) || d->state==CS_DEAD) continue;
+                float oldyaw = d->yaw, oldpitch = d->pitch, oldroll = d->roll;
+                d->yaw = yaw;
+                d->pitch = pitch;
+                d->roll = roll;
+                d->move = (physstate>>4)&2 ? -1 : (physstate>>4)&1;
+                d->strafe = (physstate>>6)&2 ? -1 : (physstate>>6)&1;
+                vec oldpos(d->o);
+                d->o = o;
+                d->o.z += d->eyeheight;
+                d->vel = vel;
+                d->falling = falling;
+                d->physstate = physstate&7;
+                updatephysstate(d);
+                updatepos(d);
+                if(smoothmove && d->smoothmillis>=0 && oldpos.dist(d->o) < smoothdist)
+                {
+                    d->newpos = d->o;
+                    d->newyaw = d->yaw;
+                    d->newpitch = d->pitch;
+                    d->newroll = d->roll;
+                    d->o = oldpos;
+                    d->yaw = oldyaw;
+                    d->pitch = oldpitch;
+                    d->roll = oldroll;
+                    (d->deltapos = oldpos).sub(d->newpos);
+                    d->deltayaw = oldyaw - d->newyaw;
+                    if(d->deltayaw > 180) d->deltayaw -= 360;
+                    else if(d->deltayaw < -180) d->deltayaw += 360;
+                    d->deltapitch = oldpitch - d->newpitch;
+                    d->deltaroll = oldroll - d->newroll;
+                    d->smoothmillis = lastmillis;
+                }
+                else d->smoothmillis = 0;
+                if(d->state==CS_LAGGED || d->state==CS_SPAWNING) d->state = CS_ALIVE;
+                break;
+            }
+
+            case N_TELEPORT:
+            {
+                int cn = getint(p), tp = getint(p), td = getint(p);
+                fpsent *d = getclient(cn);
+                if(!d || d->lifesequence < 0 || d->state==CS_DEAD) continue;
+                entities::teleporteffects(d, tp, td, false);
+                break;
+            }
+
+            case N_JUMPPAD:
+            {
+                int cn = getint(p), jp = getint(p);
+                fpsent *d = getclient(cn);
+                if(!d || d->lifesequence < 0 || d->state==CS_DEAD) continue;
+                entities::jumppadeffects(d, jp, false);
+                break;
+            }
+
+            default:
+                neterr("type");
+                return;
+        }
+    }
+
+    void parsestate(fpsent *d, ucharbuf &p, bool resume = false)
+    {
+        if(!d) { static fpsent dummy; d = &dummy; }
+        if(resume)
+        {
+            if(d==player1) getint(p);
+            else d->state = getint(p);
+            d->frags = getint(p);
+            d->flags = getint(p);
+            d->deaths = getint(p);
+            if(d==player1) getint(p);
+            else d->quadmillis = getint(p);
+        }
+        d->lifesequence = getint(p);
+        d->health = getint(p);
+        d->maxhealth = getint(p);
+        d->armour = getint(p);
+        d->armourtype = getint(p);
+        if(resume && d==player1)
+        {
+            getint(p);
+            loopi(GUN_PISTOL-GUN_SG+1) getint(p);
+        }
+        else
+        {
+            int gun = getint(p);
+            d->gunselect = clamp(gun, int(GUN_FIST), int(GUN_PISTOL));
+            loopi(GUN_PISTOL-GUN_SG+1) d->ammo[GUN_SG+i] = getint(p);
+        }
+    }
+
+    extern int deathscore;
+
+    void parsemessages(int cn, fpsent *d, ucharbuf &p)
+    {
+        static char text[MAXTRANS];
+        int type;
+        bool mapchanged = false, demopacket = false;
+
+        while(p.remaining()) switch(type = getint(p))
+        {
+            case N_DEMOPACKET: demopacket = true; break;
+
+            case N_SERVINFO:                   // welcome messsage from the server
+            {
+                int mycn = getint(p), prot = getint(p);
+                if(prot!=PROTOCOL_VERSION)
+                {
+                    conoutf(CON_ERROR, "you are using a different game protocol (you: %d, server: %d)", PROTOCOL_VERSION, prot);
+                    disconnect();
+                    return;
+                }
+                sessionid = getint(p);
+                player1->clientnum = mycn;      // we are now connected
+                if(getint(p) > 0) conoutf("this server is password protected");
+                getstring(servinfo, p, sizeof(servinfo));
+                getstring(servauth, p, sizeof(servauth));
+                sendintro();
+                break;
+            }
+
+            case N_WELCOME:
+            {
+                connected = true;
+                notifywelcome();
+                break;
+            }
+
+            case N_PAUSEGAME:
+            {
+                bool val = getint(p) > 0;
+                int cn = getint(p);
+                fpsent *a = cn >= 0 ? getclient(cn) : NULL;
+                if(!demopacket)
+                {
+                    gamepaused = val;
+                    player1->attacking = false;
+                }
+                if(a) conoutf("%s %s the game", colorname(a), val ? "paused" : "resumed"); 
+                else conoutf("game is %s", val ? "paused" : "resumed");
+                break;
+            }
+
+            case N_GAMESPEED:
+            {
+                int val = clamp(getint(p), 10, 1000), cn = getint(p);
+                fpsent *a = cn >= 0 ? getclient(cn) : NULL;
+                if(!demopacket) gamespeed = val;
+                extern int slowmosp;
+                if(m_sp && slowmosp) break;
+                if(a) conoutf("%s set gamespeed to %d", colorname(a), val);
+                else conoutf("gamespeed is %d", val);
+                break;
+            }
+                
+            case N_CLIENT:
+            {
+                int cn = getint(p), len = getuint(p);
+                ucharbuf q = p.subbuf(len);
+                parsemessages(cn, getclient(cn), q);
+                break;
+            }
+
+            case N_SOUND:
+                if(!d) return;
+                playsound(getint(p), &d->o);
+                break;
+
+            case N_TEXT:
+            {
+                if(!d) return;
+                getstring(text, p);
+                filtertext(text, text, true, true);
+                if(isignored(d->clientnum)) break;
+                if(d->state!=CS_DEAD && d->state!=CS_SPECTATOR)
+                    particle_textcopy(d->abovehead(), text, PART_TEXT, 2000, 0x32FF64, 4.0f, -8);
+                conoutf(CON_CHAT, "%s:\f0 %s", chatcolorname(d), text);
+                break;
+            }
+
+            case N_SAYTEAM:
+            {
+                int tcn = getint(p);
+                fpsent *t = getclient(tcn);
+                getstring(text, p);
+                filtertext(text, text, true, true);
+                if(!t || isignored(t->clientnum)) break;
+                if(t->state!=CS_DEAD && t->state!=CS_SPECTATOR)
+                    particle_textcopy(t->abovehead(), text, PART_TEXT, 2000, 0x6496FF, 4.0f, -8);
+                conoutf(CON_TEAMCHAT, "\fs\f8[team]\fr %s: \f8%s", chatcolorname(t), text);
+                break;
+            }
+
+            case N_MAPCHANGE:
+                getstring(text, p);
+                filtertext(text, text, false);
+                fixmapname(text);
+                changemapserv(text, getint(p));
+                mapchanged = true;
+                if(getint(p)) entities::spawnitems();
+                else senditemstoserver = false;
+                break;
+
+            case N_FORCEDEATH:
+            {
+                int cn = getint(p);
+                fpsent *d = cn==player1->clientnum ? player1 : newclient(cn);
+                if(!d) break;
+                if(d==player1)
+                {
+                    if(editmode) toggleedit();
+                    stopfollowing();
+                    if(deathscore) showscores(true);
+                }
+                else d->resetinterp();
+                d->state = CS_DEAD;
+                break;
+            }
+
+            case N_ITEMLIST:
+            {
+                int n;
+                while((n = getint(p))>=0 && !p.overread())
+                {
+                    if(mapchanged) entities::setspawn(n, true);
+                    getint(p); // type
+                }
+                break;
+            }
+
+            case N_INITCLIENT:            // another client either connected or changed name/team
+            {
+                int cn = getint(p);
+                fpsent *d = newclient(cn);
+                if(!d)
+                {
+                    getstring(text, p);
+                    getstring(text, p);
+                    getint(p);
+                    break;
+                }
+                getstring(text, p);
+                filtertext(text, text, false, false, MAXNAMELEN);
+                if(!text[0]) copystring(text, "unnamed");
+                if(d->name[0])          // already connected
+                {
+                    if(strcmp(d->name, text) && !isignored(d->clientnum))
+                        conoutf("%s is now known as %s", colorname(d), colorname(d, text));
+                }
+                else                    // new client
+                {
+                    conoutf("\f0join:\f7 %s", colorname(d, text));
+                    if(needclipboard >= 0) needclipboard++;
+                }
+                copystring(d->name, text, MAXNAMELEN+1);
+                getstring(text, p);
+                filtertext(d->team, text, false, false, MAXTEAMLEN);
+                d->playermodel = getint(p);
+                break;
+            }
+
+            case N_SWITCHNAME:
+                getstring(text, p);
+                if(d)
+                {
+                    filtertext(text, text, false, false, MAXNAMELEN);
+                    if(!text[0]) copystring(text, "unnamed");
+                    if(strcmp(text, d->name))
+                    {
+                        if(!isignored(d->clientnum)) conoutf("%s is now known as %s", colorname(d), colorname(d, text));
+                        copystring(d->name, text, MAXNAMELEN+1);
+                    }
+                }
+                break;
+
+            case N_SWITCHMODEL:
+            {
+                int model = getint(p);
+                if(d)
+                {
+                    d->playermodel = model;
+                    if(d->ragdoll) cleanragdoll(d);
+                }
+                break;
+            }
+
+            case N_CDIS:
+                clientdisconnected(getint(p));
+                break;
+
+            case N_SPAWN:
+            {
+                if(d)
+                {
+                    if(d->state==CS_DEAD && d->lastpain) saveragdoll(d);
+                    d->respawn();
+                }
+                parsestate(d, p);
+                if(!d) break;
+                d->state = CS_SPAWNING;
+                if(player1->state==CS_SPECTATOR && following==d->clientnum)
+                    lasthit = 0;
+                break;
+            }
+
+            case N_SPAWNSTATE:
+            {
+                int scn = getint(p);
+                fpsent *s = getclient(scn);
+                if(!s) { parsestate(NULL, p); break; }
+                if(s->state==CS_DEAD && s->lastpain) saveragdoll(s);
+                if(s==player1)
+                {
+                    if(editmode) toggleedit();
+                    stopfollowing();
+                }
+                s->respawn();
+                parsestate(s, p);
+                s->state = CS_ALIVE;
+                pickgamespawn(s);
+                if(s == player1)
+                {
+                    showscores(false);
+                    lasthit = 0;
+                }
+                if(cmode) cmode->respawned(s);
+                               ai::spawned(s);
+                addmsg(N_SPAWN, "rcii", s, s->lifesequence, s->gunselect);
+                break;
+            }
+
+            case N_SHOTFX:
+            {
+                int scn = getint(p), gun = getint(p), id = getint(p);
+                vec from, to;
+                loopk(3) from[k] = getint(p)/DMF;
+                loopk(3) to[k] = getint(p)/DMF;
+                fpsent *s = getclient(scn);
+                if(!s) break;
+                if(gun>GUN_FIST && gun<=GUN_PISTOL && s->ammo[gun]) s->ammo[gun]--;
+                s->gunselect = clamp(gun, (int)GUN_FIST, (int)GUN_PISTOL);
+                s->gunwait = guns[s->gunselect].attackdelay;
+                int prevaction = s->lastaction;
+                s->lastaction = lastmillis;
+                s->lastattackgun = s->gunselect;
+                shoteffects(s->gunselect, from, to, s, false, id, prevaction);
+                break;
+            }
+
+            case N_EXPLODEFX:
+            {
+                int ecn = getint(p), gun = getint(p), id = getint(p);
+                fpsent *e = getclient(ecn);
+                if(!e) break;
+                explodeeffects(gun, e, false, id);
+                break;
+            }
+            case N_DAMAGE:
+            {
+                int tcn = getint(p),
+                    acn = getint(p),
+                    damage = getint(p),
+                    armour = getint(p),
+                    health = getint(p);
+                fpsent *target = getclient(tcn),
+                       *actor = getclient(acn);
+                if(!target || !actor) break;
+                target->armour = armour;
+                target->health = health;
+                if(target->state == CS_ALIVE && actor != player1) target->lastpain = lastmillis;
+                damaged(damage, target, actor, false);
+                break;
+            }
+
+            case N_HITPUSH:
+            {
+                int tcn = getint(p), gun = getint(p), damage = getint(p);
+                fpsent *target = getclient(tcn);
+                vec dir;
+                loopk(3) dir[k] = getint(p)/DNF;
+                if(target) target->hitpush(damage * (target->health<=0 ? deadpush : 1), dir, NULL, gun);
+                break;
+            }
+
+            case N_DIED:
+            {
+                int vcn = getint(p), acn = getint(p), frags = getint(p), tfrags = getint(p);
+                fpsent *victim = getclient(vcn),
+                       *actor = getclient(acn);
+                if(!actor) break;
+                actor->frags = frags;
+                if(m_teammode) setteaminfo(actor->team, tfrags);
+                extern int hidefrags;
+                if(actor!=player1 && (!cmode || !cmode->hidefrags() || !hidefrags))
+                {
+                    defformatstring(ds, "%d", actor->frags);
+                    particle_textcopy(actor->abovehead(), ds, PART_TEXT, 2000, 0x32FF64, 4.0f, -8);
+                }
+                if(!victim) break;
+                killed(victim, actor);
+                break;
+            }
+
+            case N_TEAMINFO:
+                for(;;)
+                {
+                    getstring(text, p);
+                    if(p.overread() || !text[0]) break;
+                    int frags = getint(p);
+                    if(p.overread()) break;
+                    if(m_teammode) setteaminfo(text, frags);
+                }
+                break;
+
+            case N_GUNSELECT:
+            {
+                if(!d) return;
+                int gun = getint(p);
+                d->gunselect = clamp(gun, int(GUN_FIST), int(GUN_PISTOL));
+                playsound(S_WEAPLOAD, &d->o);
+                break;
+            }
+
+            case N_TAUNT:
+            {
+                if(!d) return;
+                d->lasttaunt = lastmillis;
+                break;
+            }
+
+            case N_RESUME:
+            {
+                for(;;)
+                {
+                    int cn = getint(p);
+                    if(p.overread() || cn<0) break;
+                    fpsent *d = (cn == player1->clientnum ? player1 : newclient(cn));
+                    parsestate(d, p, true);
+                }
+                break;
+            }
+
+            case N_ITEMSPAWN:
+            {
+                int i = getint(p);
+                if(!entities::ents.inrange(i)) break;
+                entities::setspawn(i, true);
+                ai::itemspawned(i);
+                playsound(S_ITEMSPAWN, &entities::ents[i]->o, NULL, 0, 0, 0, -1, 0, 1500);
+                #if 0
+                const char *name = entities::itemname(i);
+                if(name) particle_text(entities::ents[i]->o, name, PART_TEXT, 2000, 0x32FF64, 4.0f, -8);
+                #endif
+                int icon = entities::itemicon(i);
+                if(icon >= 0) particle_icon(vec(0.0f, 0.0f, 4.0f).add(entities::ents[i]->o), icon%4, icon/4, PART_HUD_ICON, 2000, 0xFFFFFF, 2.0f, -8);
+                break;
+            }
+
+            case N_ITEMACC:            // server acknowledges that I picked up this item
+            {
+                int i = getint(p), cn = getint(p);
+                if(cn >= 0)
+                {
+                    fpsent *d = getclient(cn);
+                    entities::pickupeffects(i, d);
+                }
+                else entities::setspawn(i, true);
+                break;
+            }
+
+            case N_CLIPBOARD:
+            {
+                int cn = getint(p), unpacklen = getint(p), packlen = getint(p);
+                fpsent *d = getclient(cn);
+                ucharbuf q = p.subbuf(max(packlen, 0));
+                if(d) unpackeditinfo(d->edit, q.buf, q.maxlen, unpacklen);
+                break;
+            }
+            case N_UNDO:
+            case N_REDO:
+            {
+                int cn = getint(p), unpacklen = getint(p), packlen = getint(p);
+                fpsent *d = getclient(cn);
+                ucharbuf q = p.subbuf(max(packlen, 0));
+                if(d) unpackundo(q.buf, q.maxlen, unpacklen);
+                break;
+            }
+
+            case N_EDITF:              // coop editing messages
+            case N_EDITT:
+            case N_EDITM:
+            case N_FLIP:
+            case N_COPY:
+            case N_PASTE:
+            case N_ROTATE:
+            case N_REPLACE:
+            case N_DELCUBE:
+            case N_EDITVSLOT:
+            {
+                if(!d) return;
+                selinfo sel;
+                sel.o.x = getint(p); sel.o.y = getint(p); sel.o.z = getint(p);
+                sel.s.x = getint(p); sel.s.y = getint(p); sel.s.z = getint(p);
+                sel.grid = getint(p); sel.orient = getint(p);
+                sel.cx = getint(p); sel.cxs = getint(p); sel.cy = getint(p), sel.cys = getint(p);
+                sel.corner = getint(p);
+                switch(type)
+                {
+                    case N_EDITF: { int dir = getint(p), mode = getint(p); if(sel.validate()) mpeditface(dir, mode, sel, false); break; }
+                    case N_EDITT:
+                    {
+                        int tex = getint(p),
+                            allfaces = getint(p);
+                        if(p.remaining() < 2) return;
+                        int extra = lilswap(*(const ushort *)p.pad(2));
+                        if(p.remaining() < extra) return;
+                        ucharbuf ebuf = p.subbuf(extra);
+                        if(sel.validate()) mpedittex(tex, allfaces, sel, ebuf);
+                        break;
+                    }
+                    case N_EDITM: { int mat = getint(p), filter = getint(p); if(sel.validate()) mpeditmat(mat, filter, sel, false); break; }
+                    case N_FLIP: if(sel.validate()) mpflip(sel, false); break;
+                    case N_COPY: if(d && sel.validate()) mpcopy(d->edit, sel, false); break;
+                    case N_PASTE: if(d && sel.validate()) mppaste(d->edit, sel, false); break;
+                    case N_ROTATE: { int dir = getint(p); if(sel.validate()) mprotate(dir, sel, false); break; }
+                    case N_REPLACE:
+                    {
+                        int oldtex = getint(p),
+                            newtex = getint(p),
+                            insel = getint(p);
+                        if(p.remaining() < 2) return;
+                        int extra = lilswap(*(const ushort *)p.pad(2));
+                        if(p.remaining() < extra) return;
+                        ucharbuf ebuf = p.subbuf(extra);
+                        if(sel.validate()) mpreplacetex(oldtex, newtex, insel>0, sel, ebuf);
+                        break;
+                    }
+                    case N_DELCUBE: if(sel.validate()) mpdelcube(sel, false); break;
+                    case N_EDITVSLOT:
+                    {
+                        int delta = getint(p),
+                            allfaces = getint(p);
+                        if(p.remaining() < 2) return;
+                        int extra = lilswap(*(const ushort *)p.pad(2));
+                        if(p.remaining() < extra) return;
+                        ucharbuf ebuf = p.subbuf(extra);
+                        if(sel.validate()) mpeditvslot(delta, allfaces, sel, ebuf);
+                        break;
+                    }
+                }
+                break;
+            }
+            case N_REMIP:
+            {
+                if(!d) return;
+                conoutf("%s remipped", colorname(d));
+                mpremip(false);
+                break;
+            }
+            case N_EDITENT:            // coop edit of ent
+            {
+                if(!d) return;
+                int i = getint(p);
+                float x = getint(p)/DMF, y = getint(p)/DMF, z = getint(p)/DMF;
+                int type = getint(p);
+                int attr1 = getint(p), attr2 = getint(p), attr3 = getint(p), attr4 = getint(p), attr5 = getint(p);
+
+                mpeditent(i, vec(x, y, z), type, attr1, attr2, attr3, attr4, attr5, false);
+                break;
+            }
+            case N_EDITVAR:
+            {
+                if(!d) return;
+                int type = getint(p);
+                getstring(text, p);
+                string name;
+                filtertext(name, text, false);
+                ident *id = getident(name);
+                switch(type)
+                {
+                    case ID_VAR:
+                    {
+                        int val = getint(p);
+                        if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setvar(name, val);
+                        break;
+                    }
+                    case ID_FVAR:
+                    {
+                        float val = getfloat(p);
+                        if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setfvar(name, val);
+                        break;
+                    }
+                    case ID_SVAR:
+                    {
+                        getstring(text, p);
+                        if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setsvar(name, text);
+                        break;
+                    }
+                }
+                printvar(d, id);
+                break;
+            }
+
+            case N_PONG:
+                addmsg(N_CLIENTPING, "i", player1->ping = (player1->ping*5+totalmillis-getint(p))/6);
+                break;
+
+            case N_CLIENTPING:
+                if(!d) return;
+                d->ping = getint(p);
+                break;
+
+            case N_TIMEUP:
+                timeupdate(getint(p));
+                break;
+
+            case N_SERVMSG:
+                getstring(text, p);
+                conoutf("%s", text);
+                break;
+
+            case N_SENDDEMOLIST:
+            {
+                int demos = getint(p);
+                if(demos <= 0) conoutf("no demos available");
+                else loopi(demos)
+                {
+                    getstring(text, p);
+                    if(p.overread()) break;
+                    conoutf("%d. %s", i+1, text);
+                }
+                break;
+            }
+
+            case N_DEMOPLAYBACK:
+            {
+                int on = getint(p);
+                if(on) player1->state = CS_SPECTATOR;
+                else clearclients();
+                demoplayback = on!=0;
+                player1->clientnum = getint(p);
+                gamepaused = false;
+                execident(on ? "demostart" : "demoend");
+                break;
+            }
+
+            case N_CURRENTMASTER:
+            {
+                int mm = getint(p), mn;
+                loopv(players) players[i]->privilege = PRIV_NONE;
+                while((mn = getint(p))>=0 && !p.overread())
+                {
+                    fpsent *m = mn==player1->clientnum ? player1 : newclient(mn);
+                    int priv = getint(p);
+                    if(m) m->privilege = priv;
+                }
+                if(mm != mastermode)
+                {
+                    mastermode = mm;
+                    conoutf("mastermode is %s (%d)", server::mastermodename(mastermode), mastermode);
+                }
+                break;
+            }
+
+            case N_MASTERMODE:
+            {
+                mastermode = getint(p);
+                conoutf("mastermode is %s (%d)", server::mastermodename(mastermode), mastermode);
+                break;
+            }
+
+            case N_EDITMODE:
+            {
+                int val = getint(p);
+                if(!d) break;
+                if(val)
+                {
+                    d->editstate = d->state;
+                    d->state = CS_EDITING;
+                }
+                else
+                {
+                    d->state = d->editstate;
+                    if(d->state==CS_DEAD) deathstate(d, true);
+                }
+                break;
+            }
+
+            case N_SPECTATOR:
+            {
+                int sn = getint(p), val = getint(p);
+                fpsent *s;
+                if(sn==player1->clientnum)
+                {
+                    s = player1;
+                    if(val && remote && !player1->privilege) senditemstoserver = false;
+                }
+                else s = newclient(sn);
+                if(!s) return;
+                if(val)
+                {
+                    if(s==player1)
+                    {
+                        if(editmode) toggleedit();
+                        if(s->state==CS_DEAD) showscores(false);
+                        disablezoom();
+                    }
+                    s->state = CS_SPECTATOR;
+                }
+                else if(s->state==CS_SPECTATOR)
+                {
+                    if(s==player1) stopfollowing();
+                    deathstate(s, true);
+                }
+                break;
+            }
+
+            case N_SETTEAM:
+            {
+                int wn = getint(p);
+                getstring(text, p);
+                int reason = getint(p);
+                fpsent *w = getclient(wn);
+                if(!w) return;
+                filtertext(w->team, text, false, false, MAXTEAMLEN);
+                static const char * const fmt[2] = { "%s switched to team %s", "%s forced to team %s"};
+                if(reason >= 0 && size_t(reason) < sizeof(fmt)/sizeof(fmt[0]))
+                    conoutf(fmt[reason], colorname(w), w->team);
+                break;
+            }
+
+            #define PARSEMESSAGES 1
+            #include "capture.h"
+            #include "ctf.h"
+            #include "collect.h"
+            #undef PARSEMESSAGES
+
+            case N_ANNOUNCE:
+            {
+                int t = getint(p);
+                if     (t==I_QUAD)  { playsound(S_V_QUAD10, NULL, NULL, 0, 0, 0, -1, 0, 3000);  conoutf(CON_GAMEINFO, "\f2quad damage will spawn in 10 seconds!"); }
+                else if(t==I_BOOST) { playsound(S_V_BOOST10, NULL, NULL, 0, 0, 0, -1, 0, 3000); conoutf(CON_GAMEINFO, "\f2health boost will spawn in 10 seconds!"); }
+                break;
+            }
+
+            case N_NEWMAP:
+            {
+                int size = getint(p);
+                if(size>=0) emptymap(size, true, NULL);
+                else enlargemap(true);
+                if(d && d!=player1)
+                {
+                    int newsize = 0;
+                    while(1<<newsize < getworldsize()) newsize++;
+                    conoutf(size>=0 ? "%s started a new map of size %d" : "%s enlarged the map to size %d", colorname(d), newsize);
+                }
+                break;
+            }
+
+            case N_REQAUTH:
+            {
+                getstring(text, p);
+                if(autoauth && text[0] && tryauth(text)) conoutf("server requested authkey \"%s\"", text);
+                break;
+            }
+
+            case N_AUTHCHAL:
+            {
+                getstring(text, p);
+                authkey *a = findauthkey(text);
+                uint id = (uint)getint(p);
+                getstring(text, p);
+                if(a && a->lastauth && lastmillis - a->lastauth < 60*1000)
+                {
+                    vector<char> buf;
+                    answerchallenge(a->key, text, buf);
+                    //conoutf(CON_DEBUG, "answering %u, challenge %s with %s", id, text, buf.getbuf());
+                    packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+                    putint(p, N_AUTHANS);
+                    sendstring(a->desc, p);
+                    putint(p, id);
+                    sendstring(buf.getbuf(), p);
+                    sendclientpacket(p.finalize(), 1);
+                }
+                break;
+            }
+
+            case N_INITAI:
+            {
+                int bn = getint(p), on = getint(p), at = getint(p), sk = clamp(getint(p), 1, 101), pm = getint(p);
+                string name, team;
+                getstring(text, p);
+                filtertext(name, text, false, false, MAXNAMELEN);
+                getstring(text, p);
+                filtertext(team, text, false, false, MAXTEAMLEN);
+                fpsent *b = newclient(bn);
+                if(!b) break;
+                ai::init(b, at, on, sk, bn, pm, name, team);
+                break;
+            }
+
+            case N_SERVCMD:
+                getstring(text, p);
+                break;
+
+            default:
+                neterr("type", cn < 0);
+                return;
+        }
+    }
+
+    struct demoreq
+    {
+        int tag;
+        string name;
+    };
+    vector<demoreq> demoreqs;
+    enum { MAXDEMOREQS = 7 };
+    static int lastdemoreq = 0;
+
+    void receivefile(packetbuf &p)
+    {
+        int type;
+        while(p.remaining()) switch(type = getint(p))
+        {
+            case N_DEMOPACKET: return;
+            case N_SENDDEMO:
+            {
+                string fname;
+                fname[0] = '\0';
+                int tag = getint(p);
+                loopv(demoreqs) if(demoreqs[i].tag == tag)
+                {
+                    copystring(fname, demoreqs[i].name);
+                    demoreqs.remove(i);
+                    break;
+                }
+                if(!fname[0])
+                {
+                    time_t t = time(NULL);
+                    size_t len = strftime(fname, sizeof(fname), "%Y-%m-%d_%H.%M.%S", localtime(&t));
+                    fname[min(len, sizeof(fname)-1)] = '\0';
+                }
+                int len = strlen(fname);
+                if(len < 4 || strcasecmp(&fname[len-4], ".dmo")) concatstring(fname, ".dmo");
+                stream *demo = NULL;
+                if(const char *buf = server::getdemofile(fname, true)) demo = openrawfile(buf, "wb");
+                if(!demo) demo = openrawfile(fname, "wb");
+                if(!demo) return;
+                conoutf("received demo \"%s\"", fname);
+                ucharbuf b = p.subbuf(p.remaining());
+                demo->write(b.buf, b.maxlen);
+                delete demo;
+                break;
+            }
+
+            case N_SENDMAP:
+            {
+                if(!m_edit) return;
+                string oldname;
+                copystring(oldname, getclientmap());
+                defformatstring(mname, "getmap_%d", lastmillis);
+                defformatstring(fname, "packages/base/%s.ogz", mname);
+                stream *map = openrawfile(path(fname), "wb");
+                if(!map) return;
+                conoutf("received map");
+                ucharbuf b = p.subbuf(p.remaining());
+                map->write(b.buf, b.maxlen);
+                delete map;
+                if(load_world(mname, oldname[0] ? oldname : NULL))
+                    entities::spawnitems(true);
+                remove(findfile(fname, "rb"));
+                break;
+            }
+        }
+    }
+
+    void parsepacketclient(int chan, packetbuf &p)   // processes any updates from the server
+    {
+        if(p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED) return;
+        switch(chan)
+        {
+            case 0:
+                parsepositions(p);
+                break;
+
+            case 1:
+                parsemessages(-1, NULL, p);
+                break;
+
+            case 2:
+                receivefile(p);
+                break;
+        }
+    }
+
+    void getmap()
+    {
+        if(!m_edit) { conoutf(CON_ERROR, "\"getmap\" only works in coop edit mode"); return; }
+        conoutf("getting map...");
+        addmsg(N_GETMAP, "r");
+    }
+    COMMAND(getmap, "");
+
+    void stopdemo()
+    {
+        if(remote)
+        {
+            if(player1->privilege<PRIV_MASTER) return;
+            addmsg(N_STOPDEMO, "r");
+        }
+        else server::stopdemo();
+    }
+    COMMAND(stopdemo, "");
+
+    void recorddemo(int val)
+    {
+        if(remote && player1->privilege<PRIV_MASTER) return;
+        addmsg(N_RECORDDEMO, "ri", val);
+    }
+    ICOMMAND(recorddemo, "i", (int *val), recorddemo(*val));
+
+    void cleardemos(int val)
+    {
+        if(remote && player1->privilege<PRIV_MASTER) return;
+        addmsg(N_CLEARDEMOS, "ri", val);
+    }
+    ICOMMAND(cleardemos, "i", (int *val), cleardemos(*val));
+
+    void getdemo(char *val, char *name)
+    {
+        int i = 0;
+        if(isdigit(val[0]) || name[0]) i = parseint(val);
+        else name = val;
+        if(i<=0) conoutf("getting demo...");
+        else conoutf("getting demo %d...", i);
+        ++lastdemoreq;
+        if(name[0])
+        {
+            if(demoreqs.length() >= MAXDEMOREQS) demoreqs.remove(0);
+            demoreq &r = demoreqs.add();
+            r.tag = lastdemoreq;
+            copystring(r.name, name);
+        }
+        addmsg(N_GETDEMO, "rii", i, lastdemoreq);
+    }
+    ICOMMAND(getdemo, "ss", (char *val, char *name), getdemo(val, name));
+
+    void listdemos()
+    {
+        conoutf("listing demos...");
+        addmsg(N_LISTDEMOS, "r");
+    }
+    COMMAND(listdemos, "");
+
+    void sendmap()
+    {
+        if(!m_edit || (player1->state==CS_SPECTATOR && remote && !player1->privilege)) { conoutf(CON_ERROR, "\"sendmap\" only works in coop edit mode"); return; }
+        conoutf("sending map...");
+        defformatstring(mname, "sendmap_%d", lastmillis);
+        save_world(mname, true);
+        defformatstring(fname, "packages/base/%s.ogz", mname);
+        stream *map = openrawfile(path(fname), "rb");
+        if(map)
+        {
+            stream::offset len = map->size();
+            if(len > 4*1024*1024) conoutf(CON_ERROR, "map is too large");
+            else if(len <= 0) conoutf(CON_ERROR, "could not read map");
+            else
+            {
+                sendfile(-1, 2, map);
+                if(needclipboard >= 0) needclipboard++;
+            }
+            delete map;
+        }
+        else conoutf(CON_ERROR, "could not read map");
+        remove(findfile(fname, "rb"));
+    }
+    COMMAND(sendmap, "");
+
+    void gotoplayer(const char *arg)
+    {
+        if(player1->state!=CS_SPECTATOR && player1->state!=CS_EDITING) return;
+        int i = parseplayer(arg);
+        if(i>=0)
+        {
+            fpsent *d = getclient(i);
+            if(!d || d==player1) return;
+            player1->o = d->o;
+            vec dir;
+            vecfromyawpitch(player1->yaw, player1->pitch, 1, 0, dir);
+            player1->o.add(dir.mul(-32));
+            player1->resetinterp();
+        }
+    }
+    COMMANDN(goto, gotoplayer, "s");
+
+    void gotosel()
+    {
+        if(player1->state!=CS_EDITING) return;
+        player1->o = getselpos();
+        vec dir;
+        vecfromyawpitch(player1->yaw, player1->pitch, 1, 0, dir);
+        player1->o.add(dir.mul(-32));
+        player1->resetinterp();
+    }
+    COMMAND(gotosel, "");
+}
+
diff --git a/src/fpsgame/entities.cpp b/src/fpsgame/entities.cpp
new file mode 100644 (file)
index 0000000..c35a0d1
--- /dev/null
@@ -0,0 +1,709 @@
+#include "game.h"
+
+namespace entities
+{
+    using namespace game;
+
+    int extraentinfosize() { return 0; }       // size in bytes of what the 2 methods below read/write... so it can be skipped by other games
+
+    void writeent(entity &e, char *buf)   // write any additional data to disk (except for ET_ ents)
+    {
+    }
+
+    void readent(entity &e, char *buf, int ver)     // read from disk, and init
+    {
+        if(ver <= 30) switch(e.type)
+        {
+            case FLAG:
+            case MONSTER:
+            case TELEDEST:
+            case RESPAWNPOINT:
+            case BOX:
+            case BARREL:
+            case PLATFORM:
+            case ELEVATOR:
+                e.attr1 = (int(e.attr1)+180)%360;
+                break;
+        }
+        if(ver <= 31) switch(e.type)
+        {
+            case BOX:
+            case BARREL:
+            case PLATFORM:
+            case ELEVATOR:
+                int yaw = (int(e.attr1)%360 + 360)%360 + 7; 
+                e.attr1 = yaw - yaw%15;
+                break;
+        }
+    }
+
+#ifndef STANDALONE
+    vector<extentity *> ents;
+
+    vector<extentity *> &getents() { return ents; }
+
+    bool mayattach(extentity &e) { return false; }
+    bool attachent(extentity &e, extentity &a) { return false; }
+
+    const char *itemname(int i)
+    {
+        int t = ents[i]->type;
+        if(t<I_SHELLS || t>I_QUAD) return NULL;
+        return itemstats[t-I_SHELLS].name;
+    }
+
+    int itemicon(int i)
+    {
+        int t = ents[i]->type;
+        if(t<I_SHELLS || t>I_QUAD) return -1;
+        return itemstats[t-I_SHELLS].icon;
+    }
+
+    const char *entmdlname(int type)
+    {
+        static const char * const entmdlnames[] =
+        {
+            NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+            "ammo/shells", "ammo/bullets", "ammo/rockets", "ammo/rrounds", "ammo/grenades", "ammo/cartridges",
+            "health", "boost", "armor/green", "armor/yellow", "quad", "teleporter",
+            NULL, NULL,
+            "carrot",
+            NULL, NULL,
+            "checkpoint",
+            NULL, NULL,
+            NULL, NULL,
+            NULL
+        };
+        return entmdlnames[type];
+    }
+
+    const char *entmodel(const entity &e)
+    {
+        if(e.type == TELEPORT)
+        {
+            if(e.attr2 > 0) return mapmodelname(e.attr2);
+            if(e.attr2 < 0) return NULL;
+        }
+        return e.type < MAXENTTYPES ? entmdlname(e.type) : NULL;
+    }
+
+    void preloadentities()
+    {
+        loopi(MAXENTTYPES)
+        {
+            switch(i)
+            {
+                case I_SHELLS: case I_BULLETS: case I_ROCKETS: case I_ROUNDS: case I_GRENADES: case I_CARTRIDGES:
+                    if(m_noammo) continue;
+                    break;
+                case I_HEALTH: case I_BOOST: case I_GREENARMOUR: case I_YELLOWARMOUR: case I_QUAD:
+                    if(m_noitems) continue;
+                    break;
+                case CARROT: case RESPAWNPOINT:
+                    if(!m_classicsp) continue;
+                    break;
+            }
+            const char *mdl = entmdlname(i);
+            if(!mdl) continue;
+            preloadmodel(mdl);
+        }
+        loopv(ents)
+        {
+            extentity &e = *ents[i];
+            switch(e.type)
+            {
+                case TELEPORT:
+                    if(e.attr2 > 0) preloadmodel(mapmodelname(e.attr2));
+                case JUMPPAD:
+                    if(e.attr4 > 0) preloadmapsound(e.attr4);
+                    break;
+            }
+        }
+    }
+
+    void renderentities()
+    {
+        loopv(ents)
+        {
+            extentity &e = *ents[i];
+            int revs = 10;
+            switch(e.type)
+            {
+                case CARROT:
+                case RESPAWNPOINT:
+                    if(e.attr2) revs = 1;
+                    break;
+                case TELEPORT:
+                    if(e.attr2 < 0) continue;
+                    break;
+                default:
+                    if(!e.spawned() || e.type < I_SHELLS || e.type > I_QUAD) continue;
+            }
+            const char *mdlname = entmodel(e);
+            if(mdlname)
+            {
+                vec p = e.o;
+                p.z += 1+sinf(lastmillis/100.0+e.o.x+e.o.y)/20;
+                rendermodel(&e.light, mdlname, ANIM_MAPMODEL|ANIM_LOOP, p, lastmillis/(float)revs, 0, MDL_SHADOW | MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED);
+            }
+        }
+    }
+
+    void addammo(int type, int &v, bool local)
+    {
+        itemstat &is = itemstats[type-I_SHELLS];
+        v += is.add;
+        if(v>is.max) v = is.max;
+        if(local) msgsound(is.sound);
+    }
+
+    void repammo(fpsent *d, int type, bool local)
+    {
+        addammo(type, d->ammo[type-I_SHELLS+GUN_SG], local);
+    }
+
+    // these two functions are called when the server acknowledges that you really
+    // picked up the item (in multiplayer someone may grab it before you).
+
+    void pickupeffects(int n, fpsent *d)
+    {
+        if(!ents.inrange(n)) return;
+        extentity *e = ents[n];
+        int type = e->type;
+        if(type<I_SHELLS || type>I_QUAD) return;
+        e->clearspawned();
+        e->clearnopickup();
+        if(!d) return;
+        itemstat &is = itemstats[type-I_SHELLS];
+        fpsent *h = followingplayer(player1);
+        if(d!=h || isthirdperson())
+        {
+            //particle_text(d->abovehead(), is.name, PART_TEXT, 2000, 0xFFC864, 4.0f, -8);
+            particle_icon(d->abovehead(), is.icon%4, is.icon/4, PART_HUD_ICON_GREY, 2000, 0xFFFFFF, 2.0f, -8);
+        }
+        playsound(itemstats[type-I_SHELLS].sound, d!=h ? &d->o : NULL, NULL, 0, 0, 0, -1, 0, 1500);
+        d->pickup(type);
+        if(d==h) switch(type)
+        {
+            case I_BOOST:
+                conoutf(CON_GAMEINFO, "\f2you got the health boost!");
+                playsound(S_V_BOOST, NULL, NULL, 0, 0, 0, -1, 0, 3000);
+                break;
+
+            case I_QUAD:
+                conoutf(CON_GAMEINFO, "\f2you got the quad!");
+                playsound(S_V_QUAD, NULL, NULL, 0, 0, 0, -1, 0, 3000);
+                break;
+        }
+    }
+
+    // these functions are called when the client touches the item
+
+    void teleporteffects(fpsent *d, int tp, int td, bool local)
+    {
+        if(ents.inrange(tp) && ents[tp]->type == TELEPORT)
+        {
+            extentity &e = *ents[tp];
+            if(e.attr4 >= 0) 
+            {
+                int snd = S_TELEPORT, flags = 0;
+                if(e.attr4 > 0) { snd = e.attr4; flags = SND_MAP; }
+                fpsent *h = followingplayer(player1);
+                playsound(snd, d==h ? NULL : &e.o, NULL, flags);
+                if(d!=h && ents.inrange(td) && ents[td]->type == TELEDEST) playsound(snd, &ents[td]->o, NULL, flags);
+            }
+        }
+        if(local && d->clientnum >= 0)
+        {
+            sendposition(d);
+            packetbuf p(32, ENET_PACKET_FLAG_RELIABLE);
+            putint(p, N_TELEPORT);
+            putint(p, d->clientnum);
+            putint(p, tp);
+            putint(p, td);
+            sendclientpacket(p.finalize(), 0);
+            flushclient();
+        }
+    }
+
+    void jumppadeffects(fpsent *d, int jp, bool local)
+    {
+        if(ents.inrange(jp) && ents[jp]->type == JUMPPAD)
+        {
+            extentity &e = *ents[jp];
+            if(e.attr4 >= 0)
+            {
+                int snd = S_JUMPPAD, flags = 0;
+                if(e.attr4 > 0) { snd = e.attr4; flags = SND_MAP; }
+                playsound(snd, d == followingplayer(player1) ? NULL : &e.o, NULL, flags);
+            }
+        }
+        if(local && d->clientnum >= 0)
+        {
+            sendposition(d);
+            packetbuf p(16, ENET_PACKET_FLAG_RELIABLE);
+            putint(p, N_JUMPPAD);
+            putint(p, d->clientnum);
+            putint(p, jp);
+            sendclientpacket(p.finalize(), 0);
+            flushclient();
+        }
+    }
+
+    void teleport(int n, fpsent *d)     // also used by monsters
+    {
+        int e = -1, tag = ents[n]->attr1, beenhere = -1;
+        for(;;)
+        {
+            e = findentity(TELEDEST, e+1);
+            if(e==beenhere || e<0) { conoutf(CON_WARN, "no teleport destination for tag %d", tag); return; }
+            if(beenhere<0) beenhere = e;
+            if(ents[e]->attr2==tag)
+            {
+                teleporteffects(d, n, e, true);
+                d->o = ents[e]->o;
+                d->yaw = ents[e]->attr1;
+                if(ents[e]->attr3 > 0)
+                {
+                    vec dir;
+                    vecfromyawpitch(d->yaw, 0, 1, 0, dir);
+                    float speed = d->vel.magnitude2();
+                    d->vel.x = dir.x*speed;
+                    d->vel.y = dir.y*speed;
+                }
+                else d->vel = vec(0, 0, 0);
+                entinmap(d);
+                updatedynentcache(d);
+                ai::inferwaypoints(d, ents[n]->o, ents[e]->o, 16.f);
+                break;
+            }
+        }
+    }
+
+    void trypickup(int n, fpsent *d)
+    {
+        extentity *e = ents[n];
+        switch(e->type)
+        {
+            default:
+                if(d->canpickup(e->type))
+                {
+                    addmsg(N_ITEMPICKUP, "rci", d, n);
+                    e->setnopickup(); // even if someone else gets it first
+                }
+                break;
+
+            case TELEPORT:
+            {
+                if(d->lastpickup==e->type && lastmillis-d->lastpickupmillis<500) break;
+                if(e->attr3 > 0)
+                {
+                    defformatstring(hookname, "can_teleport_%d", e->attr3);
+                    if(!execidentbool(hookname, true)) break;
+                }
+                d->lastpickup = e->type;
+                d->lastpickupmillis = lastmillis;
+                teleport(n, d);
+                break;
+            }
+
+            case RESPAWNPOINT:
+                if(!m_classicsp || d!=player1 || n==respawnent) break;
+                respawnent = n;
+                conoutf(CON_GAMEINFO, "\f2respawn point set!");
+                playsound(S_V_RESPAWNPOINT);
+                break;
+
+            case JUMPPAD:
+            {
+                if(d->lastpickup==e->type && lastmillis-d->lastpickupmillis<300) break;
+                d->lastpickup = e->type;
+                d->lastpickupmillis = lastmillis;
+                jumppadeffects(d, n, true);
+                vec v((int)(char)e->attr3*10.0f, (int)(char)e->attr2*10.0f, e->attr1*12.5f);
+                if(d->ai) d->ai->becareful = true;
+                               d->falling = vec(0, 0, 0);
+                               d->physstate = PHYS_FALL;
+                d->timeinair = 1;
+                d->vel = v;
+                break;
+            }
+        }
+    }
+
+    void checkitems(fpsent *d)
+    {
+        if(d->state!=CS_ALIVE) return;
+        vec o = d->feetpos();
+        loopv(ents)
+        {
+            extentity &e = *ents[i];
+            if(e.type==NOTUSED) continue;
+            if((!e.spawned() || e.nopickup()) && e.type!=TELEPORT && e.type!=JUMPPAD && e.type!=RESPAWNPOINT) continue;
+            float dist = e.o.dist(o);
+            if(dist<(e.type==TELEPORT ? 16 : 12)) trypickup(i, d);
+        }
+    }
+
+    void checkquad(int time, fpsent *d)
+    {
+        if(d->quadmillis && (d->quadmillis -= time)<=0)
+        {
+            d->quadmillis = 0;
+            fpsent *h = followingplayer(player1);
+            playsound(S_PUPOUT, d==h ? NULL : &d->o);
+            if(d==h) conoutf(CON_GAMEINFO, "\f2quad damage is over");
+        }
+    }
+
+    void putitems(packetbuf &p)            // puts items in network stream and also spawns them locally
+    {
+        putint(p, N_ITEMLIST);
+        loopv(ents) if(ents[i]->type>=I_SHELLS && ents[i]->type<=I_QUAD && (!m_noammo || ents[i]->type<I_SHELLS || ents[i]->type>I_CARTRIDGES))
+        {
+            putint(p, i);
+            putint(p, ents[i]->type);
+        }
+        putint(p, -1);
+    }
+
+    void resetspawns() { loopv(ents) { extentity *e = ents[i]; e->clearspawned(); e->clearnopickup(); } }
+
+    void spawnitems(bool force)
+    {
+        if(m_noitems) return;
+        loopv(ents)
+        {
+            extentity *e = ents[i];
+            if(e->type>=I_SHELLS && e->type<=I_QUAD && (!m_noammo || e->type<I_SHELLS || e->type>I_CARTRIDGES))
+            {
+                e->setspawned(force || m_sp || !server::delayspawn(e->type));
+                e->clearnopickup();
+            }
+        }
+    }
+
+    void setspawn(int i, bool on) { if(ents.inrange(i)) { extentity *e = ents[i]; e->setspawned(on); e->clearnopickup(); } }
+
+    extentity *newentity() { return new fpsentity(); }
+    void deleteentity(extentity *e) { delete (fpsentity *)e; }
+
+    void clearents()
+    {
+        while(ents.length()) deleteentity(ents.pop());
+    }
+
+    enum
+    {
+        TRIG_COLLIDE    = 1<<0,
+        TRIG_TOGGLE     = 1<<1,
+        TRIG_ONCE       = 0<<2,
+        TRIG_MANY       = 1<<2,
+        TRIG_DISAPPEAR  = 1<<3,
+        TRIG_AUTO_RESET = 1<<4,
+        TRIG_RUMBLE     = 1<<5,
+        TRIG_LOCKED     = 1<<6,
+        TRIG_ENDSP      = 1<<7
+    };
+
+    static const int NUMTRIGGERTYPES = 32;
+
+    static const int triggertypes[NUMTRIGGERTYPES] =
+    {
+        -1,
+        TRIG_ONCE,                    // 1
+        TRIG_RUMBLE,                  // 2
+        TRIG_TOGGLE,                  // 3
+        TRIG_TOGGLE | TRIG_RUMBLE,    // 4
+        TRIG_MANY,                    // 5
+        TRIG_MANY | TRIG_RUMBLE,      // 6
+        TRIG_MANY | TRIG_TOGGLE,      // 7
+        TRIG_MANY | TRIG_TOGGLE | TRIG_RUMBLE,    // 8
+        TRIG_COLLIDE | TRIG_TOGGLE | TRIG_RUMBLE, // 9
+        TRIG_COLLIDE | TRIG_TOGGLE | TRIG_AUTO_RESET | TRIG_RUMBLE, // 10
+        TRIG_COLLIDE | TRIG_TOGGLE | TRIG_LOCKED | TRIG_RUMBLE,     // 11
+        TRIG_DISAPPEAR,               // 12
+        TRIG_DISAPPEAR | TRIG_RUMBLE, // 13
+        TRIG_DISAPPEAR | TRIG_COLLIDE | TRIG_LOCKED, // 14
+        -1 /* reserved 15 */,
+        -1 /* reserved 16 */,
+        -1 /* reserved 17 */,
+        -1 /* reserved 18 */,
+        -1 /* reserved 19 */,
+        -1 /* reserved 20 */,
+        -1 /* reserved 21 */,
+        -1 /* reserved 22 */,
+        -1 /* reserved 23 */,
+        -1 /* reserved 24 */,
+        -1 /* reserved 25 */,
+        -1 /* reserved 26 */,
+        -1 /* reserved 27 */,
+        -1 /* reserved 28 */,
+        TRIG_DISAPPEAR | TRIG_RUMBLE | TRIG_ENDSP, // 29
+        -1 /* reserved 30 */,
+        -1 /* reserved 31 */,
+    };
+
+    #define validtrigger(type) (triggertypes[(type) & (NUMTRIGGERTYPES-1)]>=0)
+    #define checktriggertype(type, flag) (triggertypes[(type) & (NUMTRIGGERTYPES-1)] & (flag))
+
+    static inline void cleartriggerflags(extentity &e)
+    {
+        e.flags &= ~(EF_ANIM | EF_NOVIS | EF_NOSHADOW | EF_NOCOLLIDE);
+    }
+
+    static inline void setuptriggerflags(fpsentity &e)
+    {
+        cleartriggerflags(e);
+        e.flags |= EF_ANIM;
+        if(checktriggertype(e.attr3, TRIG_COLLIDE|TRIG_DISAPPEAR)) e.flags |= EF_NOSHADOW;
+        if(!checktriggertype(e.attr3, TRIG_COLLIDE)) e.flags |= EF_NOCOLLIDE;
+        switch(e.triggerstate)
+        {
+            case TRIGGERING:
+                if(checktriggertype(e.attr3, TRIG_COLLIDE) && lastmillis-e.lasttrigger >= 500) e.flags |= EF_NOCOLLIDE;
+                break;
+            case TRIGGERED:
+                if(checktriggertype(e.attr3, TRIG_COLLIDE)) e.flags |= EF_NOCOLLIDE;
+                break;
+            case TRIGGER_DISAPPEARED:
+                e.flags |= EF_NOVIS | EF_NOCOLLIDE;
+                break;
+        }
+    }
+
+    void resettriggers()
+    {
+        loopv(ents)
+        {
+            fpsentity &e = *(fpsentity *)ents[i];
+            if(e.type != ET_MAPMODEL || !validtrigger(e.attr3)) continue;
+            e.triggerstate = TRIGGER_RESET;
+            e.lasttrigger = 0;
+            setuptriggerflags(e);
+        }
+    }
+
+    void unlocktriggers(int tag, int oldstate = TRIGGER_RESET, int newstate = TRIGGERING)
+    {
+        loopv(ents)
+        {
+            fpsentity &e = *(fpsentity *)ents[i];
+            if(e.type != ET_MAPMODEL || !validtrigger(e.attr3)) continue;
+            if(e.attr4 == tag && e.triggerstate == oldstate && checktriggertype(e.attr3, TRIG_LOCKED))
+            {
+                if(newstate == TRIGGER_RESETTING && checktriggertype(e.attr3, TRIG_COLLIDE) && overlapsdynent(e.o, 20)) continue;
+                e.triggerstate = newstate;
+                e.lasttrigger = lastmillis;
+                if(checktriggertype(e.attr3, TRIG_RUMBLE)) playsound(S_RUMBLE, &e.o);
+            }
+        }
+    }
+
+    ICOMMAND(trigger, "ii", (int *tag, int *state),
+    {
+        if(*state) unlocktriggers(*tag);
+        else unlocktriggers(*tag, TRIGGERED, TRIGGER_RESETTING);
+    });
+
+    VAR(triggerstate, -1, 0, 1);
+
+    void doleveltrigger(int trigger, int state)
+    {
+        defformatstring(aliasname, "level_trigger_%d", trigger);
+        if(identexists(aliasname))
+        {
+            triggerstate = state;
+            execident(aliasname);
+        }
+    }
+
+    void checktriggers()
+    {
+        if(player1->state != CS_ALIVE) return;
+        vec o = player1->feetpos();
+        loopv(ents)
+        {
+            fpsentity &e = *(fpsentity *)ents[i];
+            if(e.type != ET_MAPMODEL || !validtrigger(e.attr3)) continue;
+            switch(e.triggerstate)
+            {
+                case TRIGGERING:
+                case TRIGGER_RESETTING:
+                    if(lastmillis-e.lasttrigger>=1000)
+                    {
+                        if(e.attr4)
+                        {
+                            if(e.triggerstate == TRIGGERING) unlocktriggers(e.attr4);
+                            else unlocktriggers(e.attr4, TRIGGERED, TRIGGER_RESETTING);
+                        }
+                        if(checktriggertype(e.attr3, TRIG_DISAPPEAR)) e.triggerstate = TRIGGER_DISAPPEARED;
+                        else if(e.triggerstate==TRIGGERING && checktriggertype(e.attr3, TRIG_TOGGLE)) e.triggerstate = TRIGGERED;
+                        else e.triggerstate = TRIGGER_RESET;
+                    }
+                    setuptriggerflags(e);
+                    break;
+                case TRIGGER_RESET:
+                    if(e.lasttrigger)
+                    {
+                        if(checktriggertype(e.attr3, TRIG_AUTO_RESET|TRIG_MANY|TRIG_LOCKED) && e.o.dist(o)-player1->radius>=(checktriggertype(e.attr3, TRIG_COLLIDE) ? 20 : 12))
+                            e.lasttrigger = 0;
+                        break;
+                    }
+                    else if(e.o.dist(o)-player1->radius>=(checktriggertype(e.attr3, TRIG_COLLIDE) ? 20 : 12)) break;
+                    else if(checktriggertype(e.attr3, TRIG_LOCKED))
+                    {
+                        if(!e.attr4) break;
+                        doleveltrigger(e.attr4, -1);
+                        e.lasttrigger = lastmillis;
+                        break;
+                    }
+                    e.triggerstate = TRIGGERING;
+                    e.lasttrigger = lastmillis;
+                    setuptriggerflags(e);
+                    if(checktriggertype(e.attr3, TRIG_RUMBLE)) playsound(S_RUMBLE, &e.o);
+                    if(checktriggertype(e.attr3, TRIG_ENDSP)) endsp(false);
+                    if(e.attr4) doleveltrigger(e.attr4, 1);
+                    break;
+                case TRIGGERED:
+                    if(e.o.dist(o)-player1->radius<(checktriggertype(e.attr3, TRIG_COLLIDE) ? 20 : 12))
+                    {
+                        if(e.lasttrigger) break;
+                    }
+                    else if(checktriggertype(e.attr3, TRIG_AUTO_RESET))
+                    {
+                        if(lastmillis-e.lasttrigger<6000) break;
+                    }
+                    else if(checktriggertype(e.attr3, TRIG_MANY))
+                    {
+                        e.lasttrigger = 0;
+                        break;
+                    }
+                    else break;
+                    if(checktriggertype(e.attr3, TRIG_COLLIDE) && overlapsdynent(e.o, 20)) break;
+                    e.triggerstate = TRIGGER_RESETTING;
+                    e.lasttrigger = lastmillis;
+                    setuptriggerflags(e);
+                    if(checktriggertype(e.attr3, TRIG_RUMBLE)) playsound(S_RUMBLE, &e.o);
+                    if(checktriggertype(e.attr3, TRIG_ENDSP)) endsp(false);
+                    if(e.attr4) doleveltrigger(e.attr4, 0);
+                    break;
+            }
+        }
+    }
+
+    void animatemapmodel(const extentity &e, int &anim, int &basetime)
+    {
+        const fpsentity &f = (const fpsentity &)e;
+        if(validtrigger(f.attr3)) switch(f.triggerstate)
+        {
+            case TRIGGER_RESET: anim = ANIM_TRIGGER|ANIM_START; break;
+            case TRIGGERING: anim = ANIM_TRIGGER; basetime = f.lasttrigger; break;
+            case TRIGGERED: anim = ANIM_TRIGGER|ANIM_END; break;
+            case TRIGGER_RESETTING: anim = ANIM_TRIGGER|ANIM_REVERSE; basetime = f.lasttrigger; break;
+        }
+    }
+
+    void fixentity(extentity &e)
+    {
+        switch(e.type)
+        {
+            case FLAG:
+            case BOX:
+            case BARREL:
+            case PLATFORM:
+            case ELEVATOR:
+                e.attr5 = e.attr4;
+                e.attr4 = e.attr3;
+            case TELEDEST:
+                e.attr3 = e.attr2;
+            case MONSTER:
+                e.attr2 = e.attr1;
+            case RESPAWNPOINT:
+                e.attr1 = (int)player1->yaw;
+                break;
+        }
+    }
+
+    void entradius(extentity &e, bool color)
+    {
+        switch(e.type)
+        {
+            case TELEPORT:
+                loopv(ents) if(ents[i]->type == TELEDEST && e.attr1==ents[i]->attr2)
+                {
+                    renderentarrow(e, vec(ents[i]->o).sub(e.o).normalize(), e.o.dist(ents[i]->o));
+                    break;
+                }
+                break;
+
+            case JUMPPAD:
+                renderentarrow(e, vec((int)(char)e.attr3*10.0f, (int)(char)e.attr2*10.0f, e.attr1*12.5f).normalize(), 4);
+                break;
+
+            case FLAG:
+            case MONSTER:
+            case TELEDEST:
+            case RESPAWNPOINT:
+            case BOX:
+            case BARREL:
+            case PLATFORM:
+            case ELEVATOR:
+            {
+                vec dir;
+                vecfromyawpitch(e.attr1, 0, 1, 0, dir);
+                renderentarrow(e, dir, 4);
+                break;
+            }
+            case MAPMODEL:
+                if(validtrigger(e.attr3)) renderentring(e, checktriggertype(e.attr3, TRIG_COLLIDE) ? 20 : 12);
+                break;
+        }
+    }
+
+    bool printent(extentity &e, char *buf, int len)
+    {
+        return false;
+    }
+
+    const char *entnameinfo(entity &e) { return ""; }
+    const char *entname(int i)
+    {
+        static const char * const entnames[] =
+        {
+            "none?", "light", "mapmodel", "playerstart", "envmap", "particles", "sound", "spotlight",
+            "shells", "bullets", "rockets", "riflerounds", "grenades", "cartridges",
+            "health", "healthboost", "greenarmour", "yellowarmour", "quaddamage",
+            "teleport", "teledest",
+            "monster", "carrot", "jumppad",
+            "base", "respawnpoint",
+            "box", "barrel",
+            "platform", "elevator",
+            "flag",
+            "", "", "", "",
+        };
+        return i>=0 && size_t(i)<sizeof(entnames)/sizeof(entnames[0]) ? entnames[i] : "";
+    }
+
+    void editent(int i, bool local)
+    {
+        extentity &e = *ents[i];
+        if(e.type == ET_MAPMODEL && validtrigger(e.attr3))
+        {
+            fpsentity &f = (fpsentity &)e;
+            f.triggerstate = TRIGGER_RESET;
+            f.lasttrigger = 0;
+            setuptriggerflags(f);
+        }
+        else cleartriggerflags(e);
+        if(local) addmsg(N_EDITENT, "rii3ii5", i, (int)(e.o.x*DMF), (int)(e.o.y*DMF), (int)(e.o.z*DMF), e.type, e.attr1, e.attr2, e.attr3, e.attr4, e.attr5);
+    }
+
+    float dropheight(entity &e)
+    {
+        if(e.type==BASE || e.type==FLAG) return 0.0f;
+        return 4.0f;
+    }
+#endif
+}
+
diff --git a/src/fpsgame/extinfo.h b/src/fpsgame/extinfo.h
new file mode 100644 (file)
index 0000000..0e747e7
--- /dev/null
@@ -0,0 +1,145 @@
+
+#define EXT_ACK                         -1
+#define EXT_VERSION                     105
+#define EXT_NO_ERROR                    0
+#define EXT_ERROR                       1
+#define EXT_PLAYERSTATS_RESP_IDS        -10
+#define EXT_PLAYERSTATS_RESP_STATS      -11
+#define EXT_UPTIME                      0
+#define EXT_PLAYERSTATS                 1
+#define EXT_TEAMSCORE                   2
+
+/*
+    Client:
+    -----
+    A: 0 EXT_UPTIME
+    B: 0 EXT_PLAYERSTATS cn #a client number or -1 for all players#
+    C: 0 EXT_TEAMSCORE
+
+    Server:  
+    --------
+    A: 0 EXT_UPTIME EXT_ACK EXT_VERSION uptime #in seconds#
+    B: 0 EXT_PLAYERSTATS cn #send by client# EXT_ACK EXT_VERSION 0 or 1 #error, if cn was > -1 and client does not exist# ...
+         EXT_PLAYERSTATS_RESP_IDS pid(s) #1 packet#
+         EXT_PLAYERSTATS_RESP_STATS pid playerdata #1 packet for each player#
+    C: 0 EXT_TEAMSCORE EXT_ACK EXT_VERSION 0 or 1 #error, no teammode# remaining_time gamemode loop(teamdata [numbases bases] or -1)
+
+    Errors:
+    --------------
+    B:C:default: 0 command EXT_ACK EXT_VERSION EXT_ERROR
+*/
+
+    VAR(extinfoip, 0, 0, 1);
+
+    void extinfoplayer(ucharbuf &p, clientinfo *ci)
+    {
+        ucharbuf q = p;
+        putint(q, EXT_PLAYERSTATS_RESP_STATS); // send player stats following
+        putint(q, ci->clientnum); //add player id
+        putint(q, ci->ping);
+        sendstring(ci->name, q);
+        sendstring(ci->team, q);
+        putint(q, ci->state.frags);
+        putint(q, ci->state.flags);
+        putint(q, ci->state.deaths);
+        putint(q, ci->state.teamkills);
+        putint(q, ci->state.damage*100/max(ci->state.shotdamage,1));
+        putint(q, ci->state.health);
+        putint(q, ci->state.armour);
+        putint(q, ci->state.gunselect);
+        putint(q, ci->privilege);
+        putint(q, ci->state.state);
+        uint ip = extinfoip ? getclientip(ci->clientnum) : 0;
+        q.put((uchar*)&ip, 3);
+        sendserverinforeply(q);
+    }
+
+    static inline void extinfoteamscore(ucharbuf &p, const char *team, int score)
+    {
+        sendstring(team, p);
+        putint(p, score);
+        if(!smode || !smode->extinfoteam(team, p))
+            putint(p,-1); //no bases follow
+    }
+
+    void extinfoteams(ucharbuf &p)
+    {
+        putint(p, m_teammode ? 0 : 1);
+        putint(p, gamemode);
+        putint(p, max((gamelimit - gamemillis)/1000, 0));
+        if(!m_teammode) return;
+
+        vector<teamscore> scores;
+        if(smode && smode->hidefrags()) smode->getteamscores(scores);
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->state.state!=CS_SPECTATOR && ci->team[0] && scores.htfind(ci->team) < 0)
+            {
+                if(smode && smode->hidefrags()) scores.add(teamscore(ci->team, 0));
+                else { teaminfo *ti = teaminfos.access(ci->team); scores.add(teamscore(ci->team, ti ? ti->frags : 0)); }
+            }
+        }
+        loopv(scores) extinfoteamscore(p, scores[i].team, scores[i].score);
+    }
+
+    void extserverinforeply(ucharbuf &req, ucharbuf &p)
+    {
+        int extcmd = getint(req); // extended commands  
+
+        //Build a new packet
+        putint(p, EXT_ACK); //send ack
+        putint(p, EXT_VERSION); //send version of extended info
+
+        switch(extcmd)
+        {
+            case EXT_UPTIME:
+            {
+                putint(p, totalsecs); //in seconds
+                break;
+            }
+
+            case EXT_PLAYERSTATS:
+            {
+                int cn = getint(req); //a special player, -1 for all
+                
+                clientinfo *ci = NULL;
+                if(cn >= 0)
+                {
+                    loopv(clients) if(clients[i]->clientnum == cn) { ci = clients[i]; break; }
+                    if(!ci)
+                    {
+                        putint(p, EXT_ERROR); //client requested by id was not found
+                        sendserverinforeply(p);
+                        return;
+                    }
+                }
+
+                putint(p, EXT_NO_ERROR); //so far no error can happen anymore
+                
+                ucharbuf q = p; //remember buffer position
+                putint(q, EXT_PLAYERSTATS_RESP_IDS); //send player ids following
+                if(ci) putint(q, ci->clientnum);
+                else loopv(clients) putint(q, clients[i]->clientnum);
+                sendserverinforeply(q);
+            
+                if(ci) extinfoplayer(p, ci);
+                else loopv(clients) extinfoplayer(p, clients[i]);
+                return;
+            }
+
+            case EXT_TEAMSCORE:
+            {
+                extinfoteams(p);
+                break;
+            }
+
+            default:
+            {
+                putint(p, EXT_ERROR);
+                break;
+            }
+        }
+        sendserverinforeply(p);
+    }
+
diff --git a/src/fpsgame/fps.cpp b/src/fpsgame/fps.cpp
new file mode 100644 (file)
index 0000000..04b036f
--- /dev/null
@@ -0,0 +1,1313 @@
+#include "game.h"
+
+namespace game
+{
+    bool intermission = false;
+    int maptime = 0, maprealtime = 0, maplimit = -1;
+    int respawnent = -1;
+    int lasthit = 0, lastspawnattempt = 0;
+
+    int following = -1, followdir = 0;
+
+    fpsent *player1 = NULL;         // our client
+    vector<fpsent *> players;       // other clients
+    int savedammo[NUMGUNS];
+
+    bool clientoption(const char *arg) { return false; }
+
+    void taunt()
+    {
+        if(player1->state!=CS_ALIVE || player1->physstate<PHYS_SLOPE) return;
+        if(lastmillis-player1->lasttaunt<1000) return;
+        player1->lasttaunt = lastmillis;
+        addmsg(N_TAUNT, "rc", player1);
+    }
+    COMMAND(taunt, "");
+
+    ICOMMAND(getfollow, "", (),
+    {
+        fpsent *f = followingplayer();
+        intret(f ? f->clientnum : -1);
+    });
+
+       void follow(char *arg)
+    {
+        if(arg[0] ? player1->state==CS_SPECTATOR : following>=0)
+        {
+            following = arg[0] ? parseplayer(arg) : -1;
+            if(following==player1->clientnum) following = -1;
+            followdir = 0;
+            conoutf("follow %s", following>=0 ? "on" : "off");
+        }
+       }
+    COMMAND(follow, "s");
+
+    void nextfollow(int dir)
+    {
+        if(player1->state!=CS_SPECTATOR || clients.empty())
+        {
+            stopfollowing();
+            return;
+        }
+        int cur = following >= 0 ? following : (dir < 0 ? clients.length() - 1 : 0);
+        loopv(clients)
+        {
+            cur = (cur + dir + clients.length()) % clients.length();
+            if(clients[cur] && clients[cur]->state!=CS_SPECTATOR)
+            {
+                if(following<0) conoutf("follow on");
+                following = cur;
+                followdir = dir;
+                return;
+            }
+        }
+        stopfollowing();
+    }
+    ICOMMAND(nextfollow, "i", (int *dir), nextfollow(*dir < 0 ? -1 : 1));
+
+
+    const char *getclientmap() { return clientmap; }
+
+    void resetgamestate()
+    {
+        if(m_classicsp)
+        {
+            clearmovables();
+            clearmonsters();                 // all monsters back at their spawns for editing
+            entities::resettriggers();
+        }
+        clearprojectiles();
+        clearbouncers();
+    }
+
+    fpsent *spawnstate(fpsent *d)              // reset player state not persistent accross spawns
+    {
+        d->respawn();
+        d->spawnstate(gamemode);
+        return d;
+    }
+
+    void respawnself()
+    {
+        if(ispaused()) return;
+        if(m_mp(gamemode))
+        {
+            int seq = (player1->lifesequence<<16)|((lastmillis/1000)&0xFFFF);
+            if(player1->respawned!=seq) { addmsg(N_TRYSPAWN, "rc", player1); player1->respawned = seq; }
+        }
+        else
+        {
+            spawnplayer(player1);
+            showscores(false);
+            lasthit = 0;
+            if(cmode) cmode->respawned(player1);
+        }
+    }
+
+    fpsent *pointatplayer()
+    {
+        loopv(players) if(players[i] != player1 && intersect(players[i], player1->o, worldpos)) return players[i];
+        return NULL;
+    }
+
+    void stopfollowing()
+    {
+        if(following<0) return;
+        following = -1;
+        followdir = 0;
+        conoutf("follow off");
+    }
+
+    fpsent *followingplayer(fpsent *fallback)
+    {
+        if(player1->state!=CS_SPECTATOR || following<0) return fallback;
+        fpsent *target = getclient(following);
+        if(target && target->state!=CS_SPECTATOR) return target;
+        return fallback;
+    }
+
+    fpsent *hudplayer()
+    {
+        if(thirdperson && allowthirdperson()) return player1;
+        return followingplayer(player1);
+    }
+
+    void setupcamera()
+    {
+        fpsent *target = followingplayer();
+        if(target)
+        {
+            player1->yaw = target->yaw;
+            player1->pitch = target->state==CS_DEAD ? 0 : target->pitch;
+            player1->o = target->o;
+            player1->resetinterp();
+        }
+    }
+
+    bool allowthirdperson(bool msg)
+    {
+        return player1->state==CS_SPECTATOR || player1->state==CS_EDITING || m_edit || !multiplayer(msg);
+    }
+    ICOMMAND(allowthirdperson, "b", (int *msg), intret(allowthirdperson(*msg!=0) ? 1 : 0));
+
+    bool detachcamera()
+    {
+        fpsent *d = hudplayer();
+        return d->state==CS_DEAD;
+    }
+
+    bool collidecamera()
+    {
+        switch(player1->state)
+        {
+            case CS_EDITING: return false;
+            case CS_SPECTATOR: return followingplayer()!=NULL;
+        }
+        return true;
+    }
+
+    VARP(smoothmove, 0, 75, 100);
+    VARP(smoothdist, 0, 32, 64);
+
+    void predictplayer(fpsent *d, bool move)
+    {
+        d->o = d->newpos;
+        d->yaw = d->newyaw;
+        d->pitch = d->newpitch;
+        d->roll = d->newroll;
+        if(move)
+        {
+            moveplayer(d, 1, false);
+            d->newpos = d->o;
+        }
+        float k = 1.0f - float(lastmillis - d->smoothmillis)/smoothmove;
+        if(k>0)
+        {
+            d->o.add(vec(d->deltapos).mul(k));
+            d->yaw += d->deltayaw*k;
+            if(d->yaw<0) d->yaw += 360;
+            else if(d->yaw>=360) d->yaw -= 360;
+            d->pitch += d->deltapitch*k;
+            d->roll += d->deltaroll*k;
+        }
+    }
+
+    void otherplayers(int curtime)
+    {
+        loopv(players)
+        {
+            fpsent *d = players[i];
+            if(d == player1 || d->ai) continue;
+
+            if(d->state==CS_DEAD && d->ragdoll) moveragdoll(d);
+            else if(!intermission)
+            {
+                if(lastmillis - d->lastaction >= d->gunwait) d->gunwait = 0;
+                if(d->quadmillis) entities::checkquad(curtime, d);
+            }
+
+            const int lagtime = totalmillis-d->lastupdate;
+            if(!lagtime || intermission) continue;
+            else if(lagtime>1000 && d->state==CS_ALIVE)
+            {
+                d->state = CS_LAGGED;
+                continue;
+            }
+            if(d->state==CS_ALIVE || d->state==CS_EDITING)
+            {
+                if(smoothmove && d->smoothmillis>0) predictplayer(d, true);
+                else moveplayer(d, 1, false);
+            }
+            else if(d->state==CS_DEAD && !d->ragdoll && lastmillis-d->lastpain<2000) moveplayer(d, 1, true);
+        }
+    }
+
+    VARFP(slowmosp, 0, 0, 1, { if(m_sp && !slowmosp) server::forcegamespeed(100); });
+
+    void checkslowmo()
+    {
+        static int lastslowmohealth = 0;
+        server::forcegamespeed(intermission ? 100 : clamp(player1->health, 25, 200));
+        if(player1->health<player1->maxhealth && lastmillis-max(maptime, lastslowmohealth)>player1->health*player1->health/2)
+        {
+            lastslowmohealth = lastmillis;
+            player1->health++;
+        }
+    }
+
+    void updateworld()        // main game update loop
+    {
+        if(!maptime) { maptime = lastmillis; maprealtime = totalmillis; return; }
+        if(!curtime) { gets2c(); if(player1->clientnum>=0) c2sinfo(); return; }
+
+        physicsframe();
+        ai::navigate();
+        if(player1->state != CS_DEAD && !intermission)
+        {
+            if(player1->quadmillis) entities::checkquad(curtime, player1);
+        }
+        updateweapons(curtime);
+        otherplayers(curtime);
+        ai::update();
+        moveragdolls();
+        gets2c();
+        updatemovables(curtime);
+        updatemonsters(curtime);
+        if(connected)
+        {
+            if(player1->state == CS_DEAD)
+            {
+                if(player1->ragdoll) moveragdoll(player1);
+                else if(lastmillis-player1->lastpain<2000)
+                {
+                    player1->move = player1->strafe = 0;
+                    moveplayer(player1, 10, true);
+                }
+            }
+            else if(!intermission)
+            {
+                if(player1->ragdoll) cleanragdoll(player1);
+                moveplayer(player1, 10, true);
+                swayhudgun(curtime);
+                entities::checkitems(player1);
+                if(m_sp)
+                {
+                    if(slowmosp) checkslowmo();
+                    if(m_classicsp) entities::checktriggers();
+                }
+                else if(cmode) cmode->checkitems(player1);
+            }
+        }
+        if(player1->clientnum>=0) c2sinfo();   // do this last, to reduce the effective frame lag
+    }
+
+    float proximityscore(float x, float lower, float upper)
+    {
+        if(x <= lower) return 1.0f;
+        if(x >= upper) return 0.0f;
+        float a = x - lower, b = x - upper;
+        return (b * b) / (a * a + b * b);
+    }
+
+    static inline float harmonicmean(float a, float b) { return a + b > 0 ? 2 * a * b / (a + b) : 0.0f; }
+
+    // avoid spawning near other players
+    float ratespawn(dynent *d, const extentity &e)
+    {
+        fpsent *p = (fpsent *)d;
+        vec loc = vec(e.o).addz(p->eyeheight);
+        float maxrange = !m_noitems ? 400.0f : (cmode ? 300.0f : 110.0f);
+        float minplayerdist = maxrange;
+        loopv(players)
+        {
+            const fpsent *o = players[i];
+            if(o == p)
+            {
+                if(m_noitems || (o->state != CS_ALIVE && lastmillis - o->lastpain > 3000)) continue;
+            }
+            else if(o->state != CS_ALIVE || isteam(o->team, p->team)) continue;
+
+            vec dir = vec(o->o).sub(loc);
+            float dist = dir.squaredlen();
+            if(dist >= minplayerdist*minplayerdist) continue;
+            dist = sqrtf(dist);
+            dir.mul(1/dist);
+
+            // scale actual distance if not in line of sight
+            if(raycube(loc, dir, dist) < dist) dist *= 1.5f;
+            minplayerdist = min(minplayerdist, dist);
+        }
+        float rating = 1.0f - proximityscore(minplayerdist, 80.0f, maxrange);
+        return cmode ? harmonicmean(rating, cmode->ratespawn(p, e)) : rating;
+    }
+
+    void pickgamespawn(fpsent *d)
+    {
+        int ent = m_classicsp && d == player1 && respawnent >= 0 ? respawnent : -1;
+        int tag = cmode ? cmode->getspawngroup(d) : 0;
+        findplayerspawn(d, ent, tag);
+    }
+
+    void spawnplayer(fpsent *d)   // place at random spawn
+    {
+        pickgamespawn(d);
+        spawnstate(d);
+        if(d==player1)
+        {
+            if(editmode) d->state = CS_EDITING;
+            else if(d->state != CS_SPECTATOR) d->state = CS_ALIVE;
+        }
+        else d->state = CS_ALIVE;
+    }
+
+    VARP(spawnwait, 0, 0, 1000);
+
+    void respawn()
+    {
+        if(player1->state==CS_DEAD)
+        {
+            player1->attacking = false;
+            int wait = cmode ? cmode->respawnwait(player1) : 0;
+            if(wait>0)
+            {
+                lastspawnattempt = lastmillis;
+                //conoutf(CON_GAMEINFO, "\f2you must wait %d second%s before respawn!", wait, wait!=1 ? "s" : "");
+                return;
+            }
+            if(lastmillis < player1->lastpain + spawnwait) return;
+            if(m_dmsp) { changemap(clientmap, gamemode); return; }    // if we die in SP we try the same map again
+            respawnself();
+            if(m_classicsp)
+            {
+                conoutf(CON_GAMEINFO, "\f2You wasted another life! The monsters stole your armour and some ammo...");
+                loopi(NUMGUNS) if(i!=GUN_PISTOL && (player1->ammo[i] = savedammo[i]) > 5) player1->ammo[i] = max(player1->ammo[i]/3, 5);
+            }
+        }
+    }
+    COMMAND(respawn, "");
+
+    // inputs
+
+    VARP(attackspawn, 0, 1, 1);
+
+    void doattack(bool on)
+    {
+        if(!connected || intermission) return;
+        if((player1->attacking = on) && attackspawn) respawn();
+    }
+
+    VARP(jumpspawn, 0, 1, 1);
+
+    bool canjump()
+    {
+        if(!connected || intermission) return false;
+        if(jumpspawn) respawn();
+        return player1->state!=CS_DEAD;
+    }
+
+    bool allowmove(physent *d)
+    {
+        if(d->type!=ENT_PLAYER) return true;
+        return !((fpsent *)d)->lasttaunt || lastmillis-((fpsent *)d)->lasttaunt>=1000;
+    }
+
+    VARP(hitsound, 0, 0, 1);
+
+    void damaged(int damage, fpsent *d, fpsent *actor, bool local)
+    {
+        if((d->state!=CS_ALIVE && d->state != CS_LAGGED && d->state != CS_SPAWNING) || intermission) return;
+
+        if(local) damage = d->dodamage(damage);
+        else if(actor==player1) return;
+
+        fpsent *h = hudplayer();
+        if(h!=player1 && actor==h && d!=actor)
+        {
+            if(hitsound && lasthit != lastmillis) playsound(S_HIT);
+            lasthit = lastmillis;
+        }
+        if(d==h)
+        {
+            damageblend(damage);
+            damagecompass(damage, actor->o);
+        }
+        damageeffect(damage, d, d!=h);
+
+               ai::damaged(d, actor);
+
+        if(m_sp && slowmosp && d==player1 && d->health < 1) d->health = 1;
+
+        if(d->health<=0) { if(local) killed(d, actor); }
+        else if(d==h) playsound(S_PAIN6);
+        else playsound(S_PAIN1+rnd(5), &d->o);
+    }
+
+    VARP(deathscore, 0, 1, 1);
+
+    void deathstate(fpsent *d, bool restore)
+    {
+        d->state = CS_DEAD;
+        d->lastpain = lastmillis;
+        if(!restore)
+        {
+            gibeffect(max(-d->health, 0), d->vel, d);
+            d->deaths++;
+        }
+        if(d==player1)
+        {
+            if(deathscore) showscores(true);
+            disablezoom();
+            if(!restore) loopi(NUMGUNS) savedammo[i] = player1->ammo[i];
+            d->attacking = false;
+            //d->pitch = 0;
+            d->roll = 0;
+            playsound(S_DIE1+rnd(2));
+        }
+        else
+        {
+            d->move = d->strafe = 0;
+            d->resetinterp();
+            d->smoothmillis = 0;
+            playsound(S_DIE1+rnd(2), &d->o);
+        }
+    }
+
+    VARP(teamcolorfrags, 0, 1, 1);
+
+    void killed(fpsent *d, fpsent *actor)
+    {
+        if(d->state==CS_EDITING)
+        {
+            d->editstate = CS_DEAD;
+            d->deaths++;
+            if(d!=player1) d->resetinterp();
+            return;
+        }
+        else if((d->state!=CS_ALIVE && d->state != CS_LAGGED && d->state != CS_SPAWNING) || intermission) return;
+
+        if(cmode) cmode->died(d, actor);
+
+        fpsent *h = followingplayer(player1);
+        int contype = d==h || actor==h ? CON_FRAG_SELF : CON_FRAG_OTHER;
+        const char *dname = "", *aname = "";
+        if(m_teammode && teamcolorfrags)
+        {
+            dname = teamcolorname(d, "you");
+            aname = teamcolorname(actor, "you");
+        }
+        else
+        {
+            dname = colorname(d, NULL, "", "", "you");
+            aname = colorname(actor, NULL, "", "", "you");
+        }
+        if(actor->type==ENT_AI)
+            conoutf(contype, "\f2%s got killed by %s!", dname, aname);
+        else if(d==actor || actor->type==ENT_INANIMATE)
+            conoutf(contype, "\f2%s suicided%s", dname, d==player1 ? "!" : "");
+        else if(isteam(d->team, actor->team))
+        {
+            contype |= CON_TEAMKILL;
+            if(actor==player1) conoutf(contype, "\f6%s fragged a teammate (%s)", aname, dname);
+            else if(d==player1) conoutf(contype, "\f6%s got fragged by a teammate (%s)", dname, aname);
+            else conoutf(contype, "\f2%s fragged a teammate (%s)", aname, dname);
+        }
+        else
+        {
+            if(d==player1) conoutf(contype, "\f2%s got fragged by %s", dname, aname);
+            else conoutf(contype, "\f2%s fragged %s", aname, dname);
+        }
+        deathstate(d);
+               ai::killed(d, actor);
+    }
+
+    void timeupdate(int secs)
+    {
+        server::timeupdate(secs);
+        if(secs > 0)
+        {
+            maplimit = lastmillis + secs*1000;
+        }
+        else
+        {
+            intermission = true;
+            player1->attacking = false;
+            if(cmode) cmode->gameover();
+            conoutf(CON_GAMEINFO, "\f2intermission:");
+            conoutf(CON_GAMEINFO, "\f2game has ended!");
+            if(m_ctf) conoutf(CON_GAMEINFO, "\f2player frags: %d, flags: %d, deaths: %d", player1->frags, player1->flags, player1->deaths);
+            else if(m_collect) conoutf(CON_GAMEINFO, "\f2player frags: %d, skulls: %d, deaths: %d", player1->frags, player1->flags, player1->deaths);
+            else conoutf(CON_GAMEINFO, "\f2player frags: %d, deaths: %d", player1->frags, player1->deaths);
+            int accuracy = (player1->totaldamage*100)/max(player1->totalshots, 1);
+            conoutf(CON_GAMEINFO, "\f2player total damage dealt: %d, damage wasted: %d, accuracy(%%): %d", player1->totaldamage, player1->totalshots-player1->totaldamage, accuracy);
+            if(m_sp) spsummary(accuracy);
+
+            showscores(true);
+            disablezoom();
+
+            execident("intermission");
+        }
+    }
+
+    ICOMMAND(getfrags, "", (), intret(player1->frags));
+    ICOMMAND(getflags, "", (), intret(player1->flags));
+    ICOMMAND(getdeaths, "", (), intret(player1->deaths));
+    ICOMMAND(getaccuracy, "", (), intret((player1->totaldamage*100)/max(player1->totalshots, 1)));
+    ICOMMAND(gettotaldamage, "", (), intret(player1->totaldamage));
+    ICOMMAND(gettotalshots, "", (), intret(player1->totalshots));
+
+    vector<fpsent *> clients;
+
+    fpsent *newclient(int cn)   // ensure valid entity
+    {
+        if(cn < 0 || cn > max(0xFF, MAXCLIENTS + MAXBOTS))
+        {
+            neterr("clientnum", false);
+            return NULL;
+        }
+
+        if(cn == player1->clientnum) return player1;
+
+        while(cn >= clients.length()) clients.add(NULL);
+        if(!clients[cn])
+        {
+            fpsent *d = new fpsent;
+            d->clientnum = cn;
+            clients[cn] = d;
+            players.add(d);
+        }
+        return clients[cn];
+    }
+
+    fpsent *getclient(int cn)   // ensure valid entity
+    {
+        if(cn == player1->clientnum) return player1;
+        return clients.inrange(cn) ? clients[cn] : NULL;
+    }
+
+    void clientdisconnected(int cn, bool notify)
+    {
+        if(!clients.inrange(cn)) return;
+        if(following==cn)
+        {
+            if(followdir) nextfollow(followdir);
+            else stopfollowing();
+        }
+        unignore(cn);
+        fpsent *d = clients[cn];
+        if(!d) return;
+        if(notify && d->name[0]) conoutf("\f4leave:\f7 %s", colorname(d));
+        removeweapons(d);
+        removetrackedparticles(d);
+        removetrackeddynlights(d);
+        if(cmode) cmode->removeplayer(d);
+        players.removeobj(d);
+        DELETEP(clients[cn]);
+        cleardynentcache();
+    }
+
+    void clearclients(bool notify)
+    {
+        loopv(clients) if(clients[i]) clientdisconnected(i, notify);
+    }
+
+    void initclient()
+    {
+        player1 = spawnstate(new fpsent);
+        filtertext(player1->name, "unnamed", false, false, MAXNAMELEN);
+        players.add(player1);
+    }
+
+    VARP(showmodeinfo, 0, 1, 1);
+
+    void startgame()
+    {
+        clearmovables();
+        clearmonsters();
+
+        clearprojectiles();
+        clearbouncers();
+        clearragdolls();
+
+        clearteaminfo();
+
+        // reset perma-state
+        loopv(players)
+        {
+            fpsent *d = players[i];
+            d->frags = d->flags = 0;
+            d->deaths = 0;
+            d->totaldamage = 0;
+            d->totalshots = 0;
+            d->maxhealth = 100;
+            d->lifesequence = -1;
+            d->respawned = d->suicided = -2;
+        }
+
+        setclientmode();
+
+        intermission = false;
+        maptime = maprealtime = 0;
+        maplimit = -1;
+
+        if(cmode)
+        {
+            cmode->preload();
+            cmode->setup();
+        }
+
+        conoutf(CON_GAMEINFO, "\f2game mode is %s", server::modename(gamemode));
+
+        if(m_sp)
+        {
+            defformatstring(scorename, "bestscore_%s", getclientmap());
+            const char *best = getalias(scorename);
+            if(*best) conoutf(CON_GAMEINFO, "\f2try to beat your best score so far: %s", best);
+        }
+        else
+        {
+            const char *info = m_valid(gamemode) ? gamemodes[gamemode - STARTGAMEMODE].info : NULL;
+            if(showmodeinfo && info) conoutf(CON_GAMEINFO, "\f0%s", info);
+        }
+
+        if(player1->playermodel != playermodel) switchplayermodel(playermodel);
+
+        showscores(false);
+        disablezoom();
+        lasthit = 0;
+
+        execident("mapstart");
+    }
+
+    void loadingmap(const char *name)
+    {
+        execident("playsong");
+    }
+
+    void startmap(const char *name)   // called just after a map load
+    {
+        ai::savewaypoints();
+        ai::clearwaypoints(true);
+
+        respawnent = -1; // so we don't respawn at an old spot
+        if(!m_mp(gamemode)) spawnplayer(player1);
+        else findplayerspawn(player1, -1);
+        entities::resetspawns();
+        copystring(clientmap, name ? name : "");
+
+        sendmapinfo();
+    }
+
+    const char *getmapinfo()
+    {
+        return showmodeinfo && m_valid(gamemode) ? gamemodes[gamemode - STARTGAMEMODE].info : NULL;
+    }
+
+    const char *getscreenshotinfo()
+    {
+        return server::modename(gamemode, NULL);
+    }
+
+    void physicstrigger(physent *d, bool local, int floorlevel, int waterlevel, int material)
+    {
+        if(d->type==ENT_INANIMATE) return;
+        if     (waterlevel>0) { if(material!=MAT_LAVA) playsound(S_SPLASH1, d==player1 ? NULL : &d->o); }
+        else if(waterlevel<0) playsound(material==MAT_LAVA ? S_BURN : S_SPLASH2, d==player1 ? NULL : &d->o);
+        if     (floorlevel>0) { if(d==player1 || d->type!=ENT_PLAYER || ((fpsent *)d)->ai) msgsound(S_JUMP, d); }
+        else if(floorlevel<0) { if(d==player1 || d->type!=ENT_PLAYER || ((fpsent *)d)->ai) msgsound(S_LAND, d); }
+    }
+
+    void dynentcollide(physent *d, physent *o, const vec &dir)
+    {
+        switch(d->type)
+        {
+            case ENT_AI: if(dir.z > 0) stackmonster((monster *)d, o); break;
+            case ENT_INANIMATE: if(dir.z > 0) stackmovable((movable *)d, o); break;
+        }
+    }
+
+    void msgsound(int n, physent *d)
+    {
+        if(!d || d==player1)
+        {
+            addmsg(N_SOUND, "ci", d, n);
+            playsound(n);
+        }
+        else
+        {
+            if(d->type==ENT_PLAYER && ((fpsent *)d)->ai)
+                addmsg(N_SOUND, "ci", d, n);
+            playsound(n, &d->o);
+        }
+    }
+
+    int numdynents() { return players.length()+monsters.length()+movables.length(); }
+
+    dynent *iterdynents(int i)
+    {
+        if(i<players.length()) return players[i];
+        i -= players.length();
+        if(i<monsters.length()) return (dynent *)monsters[i];
+        i -= monsters.length();
+        if(i<movables.length()) return (dynent *)movables[i];
+        return NULL;
+    }
+
+    bool duplicatename(fpsent *d, const char *name = NULL, const char *alt = NULL)
+    {
+        if(!name) name = d->name;
+        if(alt && d != player1 && !strcmp(name, alt)) return true;
+        loopv(players) if(d!=players[i] && !strcmp(name, players[i]->name)) return true;
+        return false;
+    }
+
+    static string cname[3];
+    static int cidx = 0;
+
+    const char *colorname(fpsent *d, const char *name, const char *prefix, const char *suffix, const char *alt)
+    {
+        if(!name) name = alt && d == player1 ? alt : d->name;
+        bool dup = !name[0] || duplicatename(d, name, alt) || d->aitype != AI_NONE;
+        if(dup || prefix[0] || suffix[0])
+        {
+            cidx = (cidx+1)%3;
+            if(dup) formatstring(cname[cidx], d->aitype == AI_NONE ? "%s%s \fs\f5(%d)\fr%s" : "%s%s \fs\f5[%d]\fr%s", prefix, name, d->clientnum, suffix);
+            else formatstring(cname[cidx], "%s%s%s", prefix, name, suffix);
+            return cname[cidx];
+        }
+        return name;
+    }
+
+    VARP(teamcolortext, 0, 1, 1);
+
+    const char *teamcolorname(fpsent *d, const char *alt)
+    {
+        if(!teamcolortext || !m_teammode || d->state==CS_SPECTATOR) return colorname(d, NULL, "", "", alt);
+        return colorname(d, NULL, isteam(d->team, player1->team) ? "\fs\f1" : "\fs\f3", "\fr", alt);
+    }
+
+    const char *teamcolor(const char *name, bool sameteam, const char *alt)
+    {
+        if(!teamcolortext || !m_teammode) return sameteam || !alt ? name : alt;
+        cidx = (cidx+1)%3;
+        formatstring(cname[cidx], sameteam ? "\fs\f1%s\fr" : "\fs\f3%s\fr", sameteam || !alt ? name : alt);
+        return cname[cidx];
+    }
+
+    const char *teamcolor(const char *name, const char *team, const char *alt)
+    {
+        return teamcolor(name, team && isteam(team, player1->team), alt);
+    }
+
+    VARP(teamsounds, 0, 1, 1);
+
+    void teamsound(bool sameteam, int n, const vec *loc)
+    {
+        playsound(n, loc, NULL, teamsounds ? (m_teammode && sameteam ? SND_USE_ALT : SND_NO_ALT) : 0);
+    }
+
+    void teamsound(fpsent *d, int n, const vec *loc)
+    {
+        teamsound(isteam(d->team, player1->team), n, loc);
+    }
+
+    void suicide(physent *d)
+    {
+        if(d==player1 || (d->type==ENT_PLAYER && ((fpsent *)d)->ai))
+        {
+            if(d->state!=CS_ALIVE) return;
+            fpsent *pl = (fpsent *)d;
+            if(!m_mp(gamemode)) killed(pl, pl);
+            else
+            {
+                int seq = (pl->lifesequence<<16)|((lastmillis/1000)&0xFFFF);
+                if(pl->suicided!=seq) { addmsg(N_SUICIDE, "rc", pl); pl->suicided = seq; }
+            }
+        }
+        else if(d->type==ENT_AI) suicidemonster((monster *)d);
+        else if(d->type==ENT_INANIMATE) suicidemovable((movable *)d);
+    }
+    ICOMMAND(suicide, "", (), suicide(player1));
+
+    bool needminimap() { return m_ctf || m_protect || m_hold || m_capture || m_collect; }
+
+    void drawicon(int icon, float x, float y, float sz)
+    {
+        settexture("packages/hud/items.png");
+        float tsz = 0.25f, tx = tsz*(icon%4), ty = tsz*(icon/4);
+        gle::defvertex(2);
+        gle::deftexcoord0();
+        gle::begin(GL_TRIANGLE_STRIP);
+        gle::attribf(x,    y);    gle::attribf(tx,     ty);
+        gle::attribf(x+sz, y);    gle::attribf(tx+tsz, ty);
+        gle::attribf(x,    y+sz); gle::attribf(tx,     ty+tsz);
+        gle::attribf(x+sz, y+sz); gle::attribf(tx+tsz, ty+tsz);
+        gle::end();
+    }
+
+    float abovegameplayhud(int w, int h)
+    {
+        switch(hudplayer()->state)
+        {
+            case CS_EDITING:
+            case CS_SPECTATOR:
+                return 1;
+            default:
+                return 1650.0f/1800.0f;
+        }
+    }
+
+    int ammohudup[3] = { GUN_CG, GUN_RL, GUN_GL },
+        ammohuddown[3] = { GUN_RIFLE, GUN_SG, GUN_PISTOL },
+        ammohudcycle[7] = { -1, -1, -1, -1, -1, -1, -1 };
+
+    ICOMMAND(ammohudup, "V", (tagval *args, int numargs),
+    {
+        loopi(3) ammohudup[i] = i < numargs ? getweapon(args[i].getstr()) : -1;
+    });
+
+    ICOMMAND(ammohuddown, "V", (tagval *args, int numargs),
+    {
+        loopi(3) ammohuddown[i] = i < numargs ? getweapon(args[i].getstr()) : -1;
+    });
+
+    ICOMMAND(ammohudcycle, "V", (tagval *args, int numargs),
+    {
+        loopi(7) ammohudcycle[i] = i < numargs ? getweapon(args[i].getstr()) : -1;
+    });
+
+    VARP(ammohud, 0, 1, 1);
+
+    void drawammohud(fpsent *d)
+    {
+        float x = HICON_X + 2*HICON_STEP, y = HICON_Y, sz = HICON_SIZE;
+        pushhudmatrix();
+        hudmatrix.scale(1/3.2f, 1/3.2f, 1);
+        flushhudmatrix();
+        float xup = (x+sz)*3.2f, yup = y*3.2f + 0.1f*sz;
+        loopi(3)
+        {
+            int gun = ammohudup[i];
+            if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue;
+            drawicon(HICON_FIST+gun, xup, yup, sz);
+            yup += sz;
+        }
+        float xdown = x*3.2f - sz, ydown = (y+sz)*3.2f - 0.1f*sz;
+        loopi(3)
+        {
+            int gun = ammohuddown[3-i-1];
+            if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue;
+            ydown -= sz;
+            drawicon(HICON_FIST+gun, xdown, ydown, sz);
+        }
+        int offset = 0, num = 0;
+        loopi(7)
+        {
+            int gun = ammohudcycle[i];
+            if(gun < GUN_FIST || gun > GUN_PISTOL) continue;
+            if(gun == d->gunselect) offset = i + 1;
+            else if(d->ammo[gun]) num++;
+        }
+        float xcycle = (x+sz/2)*3.2f + 0.5f*num*sz, ycycle = y*3.2f-sz;
+        loopi(7)
+        {
+            int gun = ammohudcycle[(i + offset)%7];
+            if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue;
+            xcycle -= sz;
+            drawicon(HICON_FIST+gun, xcycle, ycycle, sz);
+        }
+        pophudmatrix();
+    }
+
+    VARP(healthcolors, 0, 1, 1);
+
+    void drawhudicons(fpsent *d)
+    {
+        pushhudmatrix();
+        hudmatrix.scale(2, 2, 1);
+        flushhudmatrix();
+
+        defformatstring(health, "%d", d->state==CS_DEAD ? 0 : d->health);
+        bvec healthcolor = bvec::hexcolor(healthcolors && !m_insta ? (d->state==CS_DEAD ? 0x808080 : (d->health<=25 ? 0xFF0000 : (d->health<=50 ? 0xFF8000 : (d->health<=100 ? 0xFFFFFF : 0x40C0FF)))) : 0xFFFFFF);
+        draw_text(health, (HICON_X + HICON_SIZE + HICON_SPACE)/2, HICON_TEXTY/2, healthcolor.r, healthcolor.g, healthcolor.b);
+        if(d->state!=CS_DEAD)
+        {
+            if(d->armour) draw_textf("%d", (HICON_X + HICON_STEP + HICON_SIZE + HICON_SPACE)/2, HICON_TEXTY/2, d->armour);
+            draw_textf("%d", (HICON_X + 2*HICON_STEP + HICON_SIZE + HICON_SPACE)/2, HICON_TEXTY/2, d->ammo[d->gunselect]);
+        }
+
+        pophudmatrix();
+
+        if(d->state != CS_DEAD && d->maxhealth > 100)
+        {
+            float scale = 0.66f;
+            pushhudmatrix();
+            hudmatrix.scale(scale, scale, 1);
+            flushhudmatrix();
+
+            float width, height;
+            text_boundsf(health, width, height);
+            draw_textf("/%d", (HICON_X + HICON_SIZE + HICON_SPACE + width*2)/scale, (HICON_TEXTY + height)/scale, d->maxhealth);
+
+            pophudmatrix();
+        }
+
+        drawicon(HICON_HEALTH, HICON_X, HICON_Y);
+        if(d->state!=CS_DEAD)
+        {
+            if(d->armour) drawicon(HICON_BLUE_ARMOUR+d->armourtype, HICON_X + HICON_STEP, HICON_Y);
+            drawicon(HICON_FIST+d->gunselect, HICON_X + 2*HICON_STEP, HICON_Y);
+            if(d->quadmillis) drawicon(HICON_QUAD, HICON_X + 3*HICON_STEP, HICON_Y);
+            if(ammohud) drawammohud(d);
+        }
+    }
+
+    VARP(gameclock, 0, 0, 1);
+    FVARP(gameclockscale, 1e-3f, 0.75f, 1e3f);
+    HVARP(gameclockcolour, 0, 0xFFFFFF, 0xFFFFFF);
+    VARP(gameclockalpha, 0, 255, 255);
+    HVARP(gameclocklowcolour, 0, 0xFFC040, 0xFFFFFF);
+    VARP(gameclockalign, -1, 0, 1);
+    FVARP(gameclockx, 0, 0.50f, 1);
+    FVARP(gameclocky, 0, 0.03f, 1);
+
+    void drawgameclock(int w, int h)
+    {
+        int secs = max(maplimit-lastmillis + 999, 0)/1000, mins = secs/60;
+        secs %= 60;
+
+        defformatstring(buf, "%d:%02d", mins, secs);
+        int tw = 0, th = 0;
+        text_bounds(buf, tw, th);
+
+        vec2 offset = vec2(gameclockx, gameclocky).mul(vec2(w, h).div(gameclockscale));
+        if(gameclockalign == 1) offset.x -= tw;
+        else if(gameclockalign == 0) offset.x -= tw/2.0f;
+        offset.y -= th/2.0f;
+
+        pushhudmatrix();
+        hudmatrix.scale(gameclockscale, gameclockscale, 1);
+        flushhudmatrix();
+
+        int color = mins < 1 ? gameclocklowcolour : gameclockcolour;
+        draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, gameclockalpha);
+
+        pophudmatrix();
+    }
+
+    extern int hudscore;
+    extern void drawhudscore(int w, int h);
+
+    VARP(ammobar, 0, 0, 1);
+    VARP(ammobaralign, -1, 0, 1);
+    VARP(ammobarhorizontal, 0, 0, 1);
+    VARP(ammobarflip, 0, 0, 1);
+    VARP(ammobarhideempty, 0, 1, 1);
+    VARP(ammobarsep, 0, 20, 500);
+    VARP(ammobarcountsep, 0, 20, 500);
+    FVARP(ammobarcountscale, 0.5, 1.5, 2);
+    FVARP(ammobarx, 0, 0.025f, 1.0f);
+    FVARP(ammobary, 0, 0.5f, 1.0f);
+    FVARP(ammobarscale, 0.1f, 0.5f, 1.0f);
+
+    void drawammobarcounter(const vec2 &center, const fpsent *p, int gun)
+    {
+        vec2 icondrawpos = vec2(center).sub(HICON_SIZE / 2);
+        int alpha = p->ammo[gun] ? 0xFF : 0x7F;
+        gle::color(bvec(0xFF, 0xFF, 0xFF), alpha);
+        drawicon(HICON_FIST + gun, icondrawpos.x, icondrawpos.y);
+
+        int fw, fh; text_bounds("000", fw, fh);
+        float labeloffset = HICON_SIZE / 2.0f + ammobarcountsep + ammobarcountscale * (ammobarhorizontal ? fh : fw) / 2.0f;
+        vec2 offsetdir = (ammobarhorizontal ? vec2(0, 1) : vec2(1, 0)).mul(ammobarflip ? -1 : 1);
+        vec2 labelorigin = vec2(offsetdir).mul(labeloffset).add(center);
+
+        pushhudmatrix();
+        hudmatrix.translate(labelorigin.x, labelorigin.y, 0);
+        hudmatrix.scale(ammobarcountscale, ammobarcountscale, 1);
+        flushhudmatrix();
+
+        defformatstring(label, "%d", p->ammo[gun]);
+        int tw, th; text_bounds(label, tw, th);
+        vec2 textdrawpos = vec2(-tw, -th).div(2);
+        float ammoratio = (float)p->ammo[gun] / itemstats[gun-GUN_SG].add;
+        bvec color = bvec::hexcolor(p->ammo[gun] == 0 || ammoratio >= 1.0f ? 0xFFFFFF : (ammoratio >= 0.5f ? 0xFFC040 : 0xFF0000));
+        draw_text(label, textdrawpos.x, textdrawpos.y, color.r, color.g, color.b, alpha);
+
+        pophudmatrix();
+    }
+
+    static inline bool ammobargunvisible(const fpsent *d, int gun)
+    {
+        return d->ammo[gun] > 0 || d->gunselect == gun;
+    }
+
+    void drawammobar(int w, int h, fpsent *p)
+    {
+        if(m_insta) return;
+
+        int NUMPLAYERGUNS = GUN_PISTOL - GUN_SG + 1;
+        int numvisibleguns = NUMPLAYERGUNS;
+        if(ammobarhideempty) loopi(NUMPLAYERGUNS) if(!ammobargunvisible(p, GUN_SG + i)) numvisibleguns--;
+
+        vec2 origin = vec2(ammobarx, ammobary).mul(vec2(w, h).div(ammobarscale));
+        vec2 offsetdir = ammobarhorizontal ? vec2(1, 0) : vec2(0, 1);
+        float stepsize = HICON_SIZE + ammobarsep;
+        float initialoffset = (ammobaralign - 1) * (numvisibleguns - 1) * stepsize / 2;
+
+        pushhudmatrix();
+        hudmatrix.scale(ammobarscale, ammobarscale, 1);
+        flushhudmatrix();
+
+        int numskippedguns = 0;
+        loopi(NUMPLAYERGUNS) if(ammobargunvisible(p, GUN_SG + i) || !ammobarhideempty)
+        {
+            float offset = initialoffset + (i - numskippedguns) * stepsize;
+            vec2 drawpos = vec2(offsetdir).mul(offset).add(origin);
+            drawammobarcounter(drawpos, p, GUN_SG + i);
+        }
+        else numskippedguns++;
+
+        pophudmatrix();
+    }
+
+    void gameplayhud(int w, int h)
+    {
+        pushhudmatrix();
+        hudmatrix.scale(h/1800.0f, h/1800.0f, 1);
+        flushhudmatrix();
+
+        if(player1->state==CS_SPECTATOR)
+        {
+            int pw, ph, tw, th, fw, fh;
+            text_bounds("  ", pw, ph);
+            text_bounds("SPECTATOR", tw, th);
+            th = max(th, ph);
+            fpsent *f = followingplayer();
+            text_bounds(f ? colorname(f) : " ", fw, fh);
+            fh = max(fh, ph);
+            draw_text("SPECTATOR", w*1800/h - tw - pw, 1650 - th - fh);
+            if(f)
+            {
+                int color = statuscolor(f, 0xFFFFFF);
+                draw_text(colorname(f), w*1800/h - fw - pw, 1650 - fh, (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF);
+            }
+        }
+
+        fpsent *d = hudplayer();
+        if(d->state!=CS_EDITING)
+        {
+            if(d->state!=CS_SPECTATOR) drawhudicons(d);
+            if(cmode) cmode->drawhud(d, w, h);
+        }
+
+        pophudmatrix();
+
+        if(d->state!=CS_EDITING && d->state!=CS_SPECTATOR && d->state!=CS_DEAD)
+        {
+            if(ammobar) drawammobar(w, h, d);
+        }
+
+        if(!m_edit && !m_sp)
+        {
+            if(gameclock) drawgameclock(w, h);
+            if(hudscore) drawhudscore(w, h);
+        }
+    }
+
+    int clipconsole(int w, int h)
+    {
+        if(cmode) return cmode->clipconsole(w, h);
+        return 0;
+    }
+
+    VARP(teamcrosshair, 0, 1, 1);
+    VARP(hitcrosshair, 0, 425, 1000);
+
+    const char *defaultcrosshair(int index)
+    {
+        switch(index)
+        {
+            case 2: return "data/hit.png";
+            case 1: return "data/teammate.png";
+            default: return "data/crosshair.png";
+        }
+    }
+
+    int selectcrosshair(vec &color)
+    {
+        fpsent *d = hudplayer();
+        if(d->state==CS_SPECTATOR || d->state==CS_DEAD) return -1;
+
+        if(d->state!=CS_ALIVE) return 0;
+
+        int crosshair = 0;
+        if(lasthit && lastmillis - lasthit < hitcrosshair) crosshair = 2;
+        else if(teamcrosshair)
+        {
+            dynent *o = intersectclosest(d->o, worldpos, d);
+            if(o && o->type==ENT_PLAYER && isteam(((fpsent *)o)->team, d->team))
+            {
+                crosshair = 1;
+                color = vec(0, 0, 1);
+            }
+        }
+
+        if(crosshair!=1 && !editmode && !m_insta)
+        {
+            if(d->health<=25) color = vec(1, 0, 0);
+            else if(d->health<=50) color = vec(1, 0.5f, 0);
+        }
+        if(d->gunwait) color.mul(0.5f);
+        return crosshair;
+    }
+
+    void lighteffects(dynent *e, vec &color, vec &dir)
+    {
+#if 0
+        fpsent *d = (fpsent *)e;
+        if(d->state!=CS_DEAD && d->quadmillis)
+        {
+            float t = 0.5f + 0.5f*sinf(2*M_PI*lastmillis/1000.0f);
+            color.y = color.y*(1-t) + t;
+        }
+#endif
+    }
+
+    int maxsoundradius(int n)
+    {
+        switch(n)
+        {
+            case S_JUMP:
+            case S_LAND:
+            case S_WEAPLOAD:
+            case S_ITEMAMMO:
+            case S_ITEMHEALTH:
+            case S_ITEMARMOUR:
+            case S_ITEMPUP:
+            case S_ITEMSPAWN:
+            case S_NOAMMO:
+            case S_PUPOUT:
+                return 340;
+            default:
+                return 500;
+        }
+    }
+
+    bool serverinfostartcolumn(g3d_gui *g, int i)
+    {
+        static const char * const names[] = { "ping ", "players ", "mode ", "map ", "time ", "master ", "host ", "port ", "description " };
+        static const float struts[] =       { 7,       7,          12.5f,   14,      7,      8,         14,      7,       24.5f };
+        if(size_t(i) >= sizeof(names)/sizeof(names[0])) return false;
+        g->pushlist();
+        g->text(names[i], 0xFFFF80, !i ? " " : NULL);
+        if(struts[i]) g->strut(struts[i]);
+        g->mergehits(true);
+        return true;
+    }
+
+    void serverinfoendcolumn(g3d_gui *g, int i)
+    {
+        g->mergehits(false);
+        g->column(i);
+        g->poplist();
+    }
+
+    const char *mastermodecolor(int n, const char *unknown)
+    {
+        return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodecolors)/sizeof(mastermodecolors[0])) ? mastermodecolors[n-MM_START] : unknown;
+    }
+
+    const char *mastermodeicon(int n, const char *unknown)
+    {
+        return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodeicons)/sizeof(mastermodeicons[0])) ? mastermodeicons[n-MM_START] : unknown;
+    }
+
+    bool serverinfoentry(g3d_gui *g, int i, const char *name, int port, const char *sdesc, const char *map, int ping, const vector<int> &attr, int np)
+    {
+        if(ping < 0 || attr.empty() || attr[0]!=PROTOCOL_VERSION)
+        {
+            switch(i)
+            {
+                case 0:
+                    if(g->button(" ", 0xFFFFDD, "serverunk")&G3D_UP) return true;
+                    break;
+
+                case 1:
+                case 2:
+                case 3:
+                case 4:
+                case 5:
+                    if(g->button(" ", 0xFFFFDD)&G3D_UP) return true;
+                    break;
+
+                case 6:
+                    if(g->buttonf("%s ", 0xFFFFDD, NULL, name)&G3D_UP) return true;
+                    break;
+
+                case 7:
+                    if(g->buttonf("%d ", 0xFFFFDD, NULL, port)&G3D_UP) return true;
+                    break;
+
+                case 8:
+                    if(ping < 0)
+                    {
+                        if(g->button(sdesc, 0xFFFFDD)&G3D_UP) return true;
+                    }
+                    else if(g->buttonf("[%s protocol] ", 0xFFFFDD, NULL, attr.empty() ? "unknown" : (attr[0] < PROTOCOL_VERSION ? "older" : "newer"))&G3D_UP) return true;
+                    break;
+            }
+            return false;
+        }
+
+        switch(i)
+        {
+            case 0:
+            {
+                const char *icon = attr.inrange(3) && np >= attr[3] ? "serverfull" : (attr.inrange(4) ? mastermodeicon(attr[4], "serverunk") : "serverunk");
+                if(g->buttonf("%d ", 0xFFFFDD, icon, ping)&G3D_UP) return true;
+                break;
+            }
+
+            case 1:
+                if(attr.length()>=4)
+                {
+                    if(g->buttonf(np >= attr[3] ? "\f3%d/%d " : "%d/%d ", 0xFFFFDD, NULL, np, attr[3])&G3D_UP) return true;
+                }
+                else if(g->buttonf("%d ", 0xFFFFDD, NULL, np)&G3D_UP) return true;
+                break;
+
+            case 2:
+                if(g->buttonf("%s ", 0xFFFFDD, NULL, attr.length()>=2 ? server::modename(attr[1], "") : "")&G3D_UP) return true;
+                break;
+
+            case 3:
+                if(g->buttonf("%.25s ", 0xFFFFDD, NULL, map)&G3D_UP) return true;
+                break;
+
+            case 4:
+                if(attr.length()>=3 && attr[2] > 0)
+                {
+                    int secs = clamp(attr[2], 0, 59*60+59),
+                        mins = secs/60;
+                    secs %= 60;
+                    if(g->buttonf("%d:%02d ", 0xFFFFDD, NULL, mins, secs)&G3D_UP) return true;
+                }
+                else if(g->buttonf(" ", 0xFFFFDD)&G3D_UP) return true;
+                break;
+            case 5:
+                if(g->buttonf("%s%s ", 0xFFFFDD, NULL, attr.length()>=5 ? mastermodecolor(attr[4], "") : "", attr.length()>=5 ? server::mastermodename(attr[4], "") : "")&G3D_UP) return true;
+                break;
+
+            case 6:
+                if(g->buttonf("%s ", 0xFFFFDD, NULL, name)&G3D_UP) return true;
+                break;
+
+            case 7:
+                if(g->buttonf("%d ", 0xFFFFDD, NULL, port)&G3D_UP) return true;
+                break;
+
+            case 8:
+                if(g->buttonf("%.25s", 0xFFFFDD, NULL, sdesc)&G3D_UP) return true;
+                break;
+        }
+        return false;
+    }
+
+    // any data written into this vector will get saved with the map data. Must take care to do own versioning, and endianess if applicable. Will not get called when loading maps from other games, so provide defaults.
+    void writegamedata(vector<char> &extras) {}
+    void readgamedata(vector<char> &extras) {}
+
+    const char *savedconfig() { return "config.cfg"; }
+    const char *restoreconfig() { return "restore.cfg"; }
+    const char *defaultconfig() { return "data/defaults.cfg"; }
+    const char *autoexec() { return "autoexec.cfg"; }
+    const char *savedservers() { return "servers.cfg"; }
+
+    void loadconfigs()
+    {
+        execident("playsong");
+
+        execfile("auth.cfg", false);
+    }
+}
+
diff --git a/src/fpsgame/game.h b/src/fpsgame/game.h
new file mode 100644 (file)
index 0000000..9c4cb16
--- /dev/null
@@ -0,0 +1,846 @@
+#ifndef __GAME_H__
+#define __GAME_H__
+
+#include "cube.h"
+
+// console message types
+
+enum
+{
+    CON_CHAT       = 1<<8,
+    CON_TEAMCHAT   = 1<<9,
+    CON_GAMEINFO   = 1<<10,
+    CON_FRAG_SELF  = 1<<11,
+    CON_FRAG_OTHER = 1<<12,
+    CON_TEAMKILL   = 1<<13
+};
+
+// network quantization scale
+#define DMF 16.0f                // for world locations
+#define DNF 100.0f              // for normalized vectors
+#define DVELF 1.0f              // for playerspeed based velocity vectors
+
+enum                            // static entity types
+{
+    NOTUSED = ET_EMPTY,         // entity slot not in use in map
+    LIGHT = ET_LIGHT,           // lightsource, attr1 = radius, attr2 = intensity
+    MAPMODEL = ET_MAPMODEL,     // attr1 = angle, attr2 = idx
+    PLAYERSTART,                // attr1 = angle, attr2 = team
+    ENVMAP = ET_ENVMAP,         // attr1 = radius
+    PARTICLES = ET_PARTICLES,
+    MAPSOUND = ET_SOUND,
+    SPOTLIGHT = ET_SPOTLIGHT,
+    I_SHELLS, I_BULLETS, I_ROCKETS, I_ROUNDS, I_GRENADES, I_CARTRIDGES,
+    I_HEALTH, I_BOOST,
+    I_GREENARMOUR, I_YELLOWARMOUR,
+    I_QUAD,
+    TELEPORT,                   // attr1 = idx, attr2 = model, attr3 = tag
+    TELEDEST,                   // attr1 = angle, attr2 = idx
+    MONSTER,                    // attr1 = angle, attr2 = monstertype
+    CARROT,                     // attr1 = tag, attr2 = type
+    JUMPPAD,                    // attr1 = zpush, attr2 = ypush, attr3 = xpush
+    BASE,
+    RESPAWNPOINT,
+    BOX,                        // attr1 = angle, attr2 = idx, attr3 = weight
+    BARREL,                     // attr1 = angle, attr2 = idx, attr3 = weight, attr4 = health
+    PLATFORM,                   // attr1 = angle, attr2 = idx, attr3 = tag, attr4 = speed
+    ELEVATOR,                   // attr1 = angle, attr2 = idx, attr3 = tag, attr4 = speed
+    FLAG,                       // attr1 = angle, attr2 = team
+    MAXENTTYPES
+};
+
+enum
+{
+    TRIGGER_RESET = 0,
+    TRIGGERING,
+    TRIGGERED,
+    TRIGGER_RESETTING,
+    TRIGGER_DISAPPEARED
+};
+
+struct fpsentity : extentity
+{
+    int triggerstate, lasttrigger;
+    
+    fpsentity() : triggerstate(TRIGGER_RESET), lasttrigger(0) {} 
+};
+
+enum { GUN_FIST = 0, GUN_SG, GUN_CG, GUN_RL, GUN_RIFLE, GUN_GL, GUN_PISTOL, GUN_FIREBALL, GUN_ICEBALL, GUN_SLIMEBALL, GUN_BITE, GUN_BARREL, NUMGUNS };
+enum { A_BLUE, A_GREEN, A_YELLOW };     // armour types... take 20/40/60 % off
+enum { M_NONE = 0, M_SEARCH, M_HOME, M_ATTACKING, M_PAIN, M_SLEEP, M_AIMING };  // monster states
+
+enum
+{
+    M_TEAM       = 1<<0,
+    M_NOITEMS    = 1<<1,
+    M_NOAMMO     = 1<<2,
+    M_INSTA      = 1<<3,
+    M_EFFICIENCY = 1<<4,
+    M_TACTICS    = 1<<5,
+    M_CAPTURE    = 1<<6,
+    M_REGEN      = 1<<7,
+    M_CTF        = 1<<8,
+    M_PROTECT    = 1<<9,
+    M_HOLD       = 1<<10,
+    M_EDIT       = 1<<12,
+    M_DEMO       = 1<<13,
+    M_LOCAL      = 1<<14,
+    M_LOBBY      = 1<<15,
+    M_DMSP       = 1<<16,
+    M_CLASSICSP  = 1<<17,
+    M_SLOWMO     = 1<<18,
+    M_COLLECT    = 1<<19
+};
+
+static struct gamemodeinfo
+{
+    const char *name;
+    int flags;
+    const char *info;
+} gamemodes[] =
+{
+    { "SP", M_LOCAL | M_CLASSICSP, NULL },
+    { "DMSP", M_LOCAL | M_DMSP, NULL },
+    { "demo", M_DEMO | M_LOCAL, NULL},
+    { "ffa", M_LOBBY, "Free For All: Collect items for ammo. Frag everyone to score points." },
+    { "coop edit", M_EDIT, "Cooperative Editing: Edit maps with multiple players simultaneously." },
+    { "teamplay", M_TEAM, "Teamplay: Collect items for ammo. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." },
+    { "instagib", M_NOITEMS | M_INSTA, "Instagib: You spawn with full rifle ammo and die instantly from one shot. There are no items. Frag everyone to score points." },
+    { "insta team", M_NOITEMS | M_INSTA | M_TEAM, "Instagib Team: You spawn with full rifle ammo and die instantly from one shot. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." },
+    { "efficiency", M_NOITEMS | M_EFFICIENCY, "Efficiency: You spawn with all weapons and armour. There are no items. Frag everyone to score points." },
+    { "effic team", M_NOITEMS | M_EFFICIENCY | M_TEAM, "Efficiency Team: You spawn with all weapons and armour. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." },
+    { "tactics", M_NOITEMS | M_TACTICS, "Tactics: You spawn with two random weapons and armour. There are no items. Frag everyone to score points." },
+    { "tac team", M_NOITEMS | M_TACTICS | M_TEAM, "Tactics Team: You spawn with two random weapons and armour. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." },
+    { "capture", M_NOAMMO | M_TACTICS | M_CAPTURE | M_TEAM, "Capture: Capture neutral bases or steal \fs\f3enemy bases\fr by standing next to them.  \fs\f1Your team\fr scores points for every 10 seconds it holds a base. You spawn with two random weapons and armour. Collect extra ammo that spawns at \fs\f1your bases\fr. There are no ammo items." },
+    { "regen capture", M_NOITEMS | M_CAPTURE | M_REGEN | M_TEAM, "Regen Capture: Capture neutral bases or steal \fs\f3enemy bases\fr by standing next to them. \fs\f1Your team\fr scores points for every 10 seconds it holds a base. Regenerate health and ammo by standing next to \fs\f1your bases\fr. There are no items." },
+    { "ctf", M_CTF | M_TEAM, "Capture The Flag: Capture \fs\f3the enemy flag\fr and bring it back to \fs\f1your flag\fr to score points for \fs\f1your team\fr. Collect items for ammo." },
+    { "insta ctf", M_NOITEMS | M_INSTA | M_CTF | M_TEAM, "Instagib Capture The Flag: Capture \fs\f3the enemy flag\fr and bring it back to \fs\f1your flag\fr to score points for \fs\f1your team\fr. You spawn with full rifle ammo and die instantly from one shot. There are no items." },
+    { "protect", M_CTF | M_PROTECT | M_TEAM, "Protect The Flag: Touch \fs\f3the enemy flag\fr to score points for \fs\f1your team\fr. Pick up \fs\f1your flag\fr to protect it. \fs\f1Your team\fr loses points if a dropped flag resets. Collect items for ammo." },
+    { "insta protect", M_NOITEMS | M_INSTA | M_CTF | M_PROTECT | M_TEAM, "Instagib Protect The Flag: Touch \fs\f3the enemy flag\fr to score points for \fs\f1your team\fr. Pick up \fs\f1your flag\fr to protect it. \fs\f1Your team\fr loses points if a dropped flag resets. You spawn with full rifle ammo and die instantly from one shot. There are no items." },
+    { "hold", M_CTF | M_HOLD | M_TEAM, "Hold The Flag: Hold \fs\f7the flag\fr for 20 seconds to score points for \fs\f1your team\fr. Collect items for ammo." },
+    { "insta hold", M_NOITEMS | M_INSTA | M_CTF | M_HOLD | M_TEAM, "Instagib Hold The Flag: Hold \fs\f7the flag\fr for 20 seconds to score points for \fs\f1your team\fr. You spawn with full rifle ammo and die instantly from one shot. There are no items." },
+    { "effic ctf", M_NOITEMS | M_EFFICIENCY | M_CTF | M_TEAM, "Efficiency Capture The Flag: Capture \fs\f3the enemy flag\fr and bring it back to \fs\f1your flag\fr to score points for \fs\f1your team\fr. You spawn with all weapons and armour. There are no items." },
+    { "effic protect", M_NOITEMS | M_EFFICIENCY | M_CTF | M_PROTECT | M_TEAM, "Efficiency Protect The Flag: Touch \fs\f3the enemy flag\fr to score points for \fs\f1your team\fr. Pick up \fs\f1your flag\fr to protect it. \fs\f1Your team\fr loses points if a dropped flag resets. You spawn with all weapons and armour. There are no items." },
+    { "effic hold", M_NOITEMS | M_EFFICIENCY | M_CTF | M_HOLD | M_TEAM, "Efficiency Hold The Flag: Hold \fs\f7the flag\fr for 20 seconds to score points for \fs\f1your team\fr. You spawn with all weapons and armour. There are no items." },
+    { "collect", M_COLLECT | M_TEAM, "Skull Collector: Frag \fs\f3the enemy team\fr to drop \fs\f3skulls\fr. Collect them and bring them to \fs\f3the enemy base\fr to score points for \fs\f1your team\fr or steal back \fs\f1your skulls\fr. Collect items for ammo." },
+    { "insta collect", M_NOITEMS | M_INSTA | M_COLLECT | M_TEAM, "Instagib Skull Collector: Frag \fs\f3the enemy team\fr to drop \fs\f3skulls\fr. Collect them and bring them to \fs\f3the enemy base\fr to score points for \fs\f1your team\fr or steal back \fs\f1your skulls\fr. You spawn with full rifle ammo and die instantly from one shot. There are no items." },
+    { "effic collect", M_NOITEMS | M_EFFICIENCY | M_COLLECT | M_TEAM, "Efficiency Skull Collector: Frag \fs\f3the enemy team\fr to drop \fs\f3skulls\fr. Collect them and bring them to \fs\f3the enemy base\fr to score points for \fs\f1your team\fr or steal back \fs\f1your skulls\fr. You spawn with all weapons and armour. There are no items." }
+};
+
+#define STARTGAMEMODE (-3)
+#define NUMGAMEMODES ((int)(sizeof(gamemodes)/sizeof(gamemodes[0])))
+
+#define m_valid(mode)          ((mode) >= STARTGAMEMODE && (mode) < STARTGAMEMODE + NUMGAMEMODES)
+#define m_check(mode, flag)    (m_valid(mode) && gamemodes[(mode) - STARTGAMEMODE].flags&(flag))
+#define m_checknot(mode, flag) (m_valid(mode) && !(gamemodes[(mode) - STARTGAMEMODE].flags&(flag)))
+#define m_checkall(mode, flag) (m_valid(mode) && (gamemodes[(mode) - STARTGAMEMODE].flags&(flag)) == (flag))
+#define m_checkonly(mode, flag, exclude) (m_valid(mode) && (gamemodes[(mode) - STARTGAMEMODE].flags&((flag)|(exclude))) == (flag))
+
+#define m_noitems      (m_check(gamemode, M_NOITEMS))
+#define m_noammo       (m_check(gamemode, M_NOAMMO|M_NOITEMS))
+#define m_insta        (m_check(gamemode, M_INSTA))
+#define m_tactics      (m_check(gamemode, M_TACTICS))
+#define m_efficiency   (m_check(gamemode, M_EFFICIENCY))
+#define m_capture      (m_check(gamemode, M_CAPTURE))
+#define m_capture_only (m_checkonly(gamemode, M_CAPTURE, M_REGEN))
+#define m_regencapture (m_checkall(gamemode, M_CAPTURE | M_REGEN))
+#define m_ctf          (m_check(gamemode, M_CTF))
+#define m_ctf_only     (m_checkonly(gamemode, M_CTF, M_PROTECT | M_HOLD))
+#define m_protect      (m_checkall(gamemode, M_CTF | M_PROTECT))
+#define m_hold         (m_checkall(gamemode, M_CTF | M_HOLD))
+#define m_collect      (m_check(gamemode, M_COLLECT))
+#define m_teammode     (m_check(gamemode, M_TEAM))
+#define isteam(a,b)    (m_teammode && strcmp(a, b)==0)
+
+#define m_demo         (m_check(gamemode, M_DEMO))
+#define m_edit         (m_check(gamemode, M_EDIT))
+#define m_lobby        (m_check(gamemode, M_LOBBY))
+#define m_timed        (m_checknot(gamemode, M_DEMO|M_EDIT|M_LOCAL))
+#define m_botmode      (m_checknot(gamemode, M_DEMO|M_LOCAL))
+#define m_mp(mode)     (m_checknot(mode, M_LOCAL))
+
+#define m_sp           (m_check(gamemode, M_DMSP | M_CLASSICSP))
+#define m_dmsp         (m_check(gamemode, M_DMSP))
+#define m_classicsp    (m_check(gamemode, M_CLASSICSP))
+
+enum { MM_AUTH = -1, MM_OPEN = 0, MM_VETO, MM_LOCKED, MM_PRIVATE, MM_PASSWORD, MM_START = MM_AUTH };
+
+static const char * const mastermodenames[] =  { "auth",   "open",   "veto",       "locked",     "private",    "password" };
+static const char * const mastermodecolors[] = { "",       "\f0",    "\f2",        "\f2",        "\f3",        "\f3" };
+static const char * const mastermodeicons[] =  { "server", "server", "serverlock", "serverlock", "serverpriv", "serverpriv" };
+
+// hardcoded sounds, defined in sounds.cfg
+enum
+{
+    S_JUMP = 0, S_LAND, S_RIFLE, S_PUNCH1, S_SG, S_CG,
+    S_RLFIRE, S_RLHIT, S_WEAPLOAD, S_ITEMAMMO, S_ITEMHEALTH,
+    S_ITEMARMOUR, S_ITEMPUP, S_ITEMSPAWN, S_TELEPORT, S_NOAMMO, S_PUPOUT,
+    S_PAIN1, S_PAIN2, S_PAIN3, S_PAIN4, S_PAIN5, S_PAIN6,
+    S_DIE1, S_DIE2,
+    S_FLAUNCH, S_FEXPLODE,
+    S_JUMPPAD, S_PISTOL,
+
+    S_V_FIGHT,
+    S_V_BOOST, S_V_BOOST10,
+    S_V_QUAD, S_V_QUAD10,
+
+    S_BURN,
+    S_CHAINSAW_ATTACK,
+    S_CHAINSAW_IDLE,
+
+    S_HIT
+};
+
+// network messages codes, c2s, c2c, s2c
+
+enum { PRIV_NONE = 0, PRIV_MASTER, PRIV_AUTH, PRIV_ADMIN };
+
+enum
+{
+    N_CONNECT = 0, N_SERVINFO, N_WELCOME, N_INITCLIENT, N_POS, N_TEXT, N_SOUND, N_CDIS,
+    N_SHOOT, N_EXPLODE, N_SUICIDE,
+    N_DIED, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX,
+    N_TRYSPAWN, N_SPAWNSTATE, N_SPAWN, N_FORCEDEATH,
+    N_GUNSELECT, N_TAUNT,
+    N_MAPCHANGE, N_MAPVOTE, N_TEAMINFO, N_ITEMSPAWN, N_ITEMPICKUP, N_ITEMACC, N_TELEPORT, N_JUMPPAD,
+    N_PING, N_PONG, N_CLIENTPING,
+    N_TIMEUP, N_FORCEINTERMISSION,
+    N_SERVMSG, N_ITEMLIST, N_RESUME,
+    N_EDITMODE, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, N_DELCUBE, N_REMIP, N_EDITVSLOT, N_UNDO, N_REDO, N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, N_EDITVAR,
+    N_MASTERMODE, N_KICK, N_CLEARBANS, N_CURRENTMASTER, N_SPECTATOR, N_SETMASTER, N_SETTEAM,
+    N_BASES, N_BASEINFO, N_BASESCORE, N_REPAMMO, N_BASEREGEN, N_ANNOUNCE,
+    N_LISTDEMOS, N_SENDDEMOLIST, N_GETDEMO, N_SENDDEMO,
+    N_DEMOPLAYBACK, N_RECORDDEMO, N_STOPDEMO, N_CLEARDEMOS,
+    N_TAKEFLAG, N_RETURNFLAG, N_RESETFLAG, N_INVISFLAG, N_TRYDROPFLAG, N_DROPFLAG, N_SCOREFLAG, N_INITFLAGS,
+    N_SAYTEAM,
+    N_CLIENT,
+    N_AUTHTRY, N_AUTHKICK, N_AUTHCHAL, N_AUTHANS, N_REQAUTH,
+    N_PAUSEGAME, N_GAMESPEED,
+    N_ADDBOT, N_DELBOT, N_INITAI, N_FROMAI, N_BOTLIMIT, N_BOTBALANCE,
+    N_MAPCRC, N_CHECKMAPS,
+    N_SWITCHNAME, N_SWITCHMODEL, N_SWITCHTEAM,
+    N_INITTOKENS, N_TAKETOKEN, N_EXPIRETOKENS, N_DROPTOKENS, N_DEPOSITTOKENS, N_STEALTOKENS,
+    N_SERVCMD,
+    N_DEMOPACKET,
+    NUMMSG
+};
+
+static const int msgsizes[] =               // size inclusive message token, 0 for variable or not-checked sizes
+{
+    N_CONNECT, 0, N_SERVINFO, 0, N_WELCOME, 1, N_INITCLIENT, 0, N_POS, 0, N_TEXT, 0, N_SOUND, 2, N_CDIS, 2,
+    N_SHOOT, 0, N_EXPLODE, 0, N_SUICIDE, 1,
+    N_DIED, 5, N_DAMAGE, 6, N_HITPUSH, 7, N_SHOTFX, 10, N_EXPLODEFX, 4,
+    N_TRYSPAWN, 1, N_SPAWNSTATE, 14, N_SPAWN, 3, N_FORCEDEATH, 2,
+    N_GUNSELECT, 2, N_TAUNT, 1,
+    N_MAPCHANGE, 0, N_MAPVOTE, 0, N_TEAMINFO, 0, N_ITEMSPAWN, 2, N_ITEMPICKUP, 2, N_ITEMACC, 3,
+    N_PING, 2, N_PONG, 2, N_CLIENTPING, 2,
+    N_TIMEUP, 2, N_FORCEINTERMISSION, 1,
+    N_SERVMSG, 0, N_ITEMLIST, 0, N_RESUME, 0,
+    N_EDITMODE, 2, N_EDITENT, 11, N_EDITF, 16, N_EDITT, 16, N_EDITM, 16, N_FLIP, 14, N_COPY, 14, N_PASTE, 14, N_ROTATE, 15, N_REPLACE, 17, N_DELCUBE, 14, N_REMIP, 1, N_EDITVSLOT, 16, N_UNDO, 0, N_REDO, 0, N_NEWMAP, 2, N_GETMAP, 1, N_SENDMAP, 0, N_EDITVAR, 0,
+    N_MASTERMODE, 2, N_KICK, 0, N_CLEARBANS, 1, N_CURRENTMASTER, 0, N_SPECTATOR, 3, N_SETMASTER, 0, N_SETTEAM, 0,
+    N_BASES, 0, N_BASEINFO, 0, N_BASESCORE, 0, N_REPAMMO, 1, N_BASEREGEN, 6, N_ANNOUNCE, 2,
+    N_LISTDEMOS, 1, N_SENDDEMOLIST, 0, N_GETDEMO, 3, N_SENDDEMO, 0,
+    N_DEMOPLAYBACK, 3, N_RECORDDEMO, 2, N_STOPDEMO, 1, N_CLEARDEMOS, 2,
+    N_TAKEFLAG, 3, N_RETURNFLAG, 4, N_RESETFLAG, 6, N_INVISFLAG, 3, N_TRYDROPFLAG, 1, N_DROPFLAG, 7, N_SCOREFLAG, 10, N_INITFLAGS, 0,
+    N_SAYTEAM, 0,
+    N_CLIENT, 0,
+    N_AUTHTRY, 0, N_AUTHKICK, 0, N_AUTHCHAL, 0, N_AUTHANS, 0, N_REQAUTH, 0,
+    N_PAUSEGAME, 0, N_GAMESPEED, 0,
+    N_ADDBOT, 2, N_DELBOT, 1, N_INITAI, 0, N_FROMAI, 2, N_BOTLIMIT, 2, N_BOTBALANCE, 2,
+    N_MAPCRC, 0, N_CHECKMAPS, 1,
+    N_SWITCHNAME, 0, N_SWITCHMODEL, 2, N_SWITCHTEAM, 0,
+    N_INITTOKENS, 0, N_TAKETOKEN, 2, N_EXPIRETOKENS, 0, N_DROPTOKENS, 0, N_DEPOSITTOKENS, 2, N_STEALTOKENS, 0,
+    N_SERVCMD, 0,
+    N_DEMOPACKET, 0,
+    -1
+};
+
+#define SAUERBRATEN_LANINFO_PORT 28784
+#define SAUERBRATEN_SERVER_PORT 28785
+#define SAUERBRATEN_SERVINFO_PORT 28786
+#define SAUERBRATEN_MASTER_PORT 28787
+#define PROTOCOL_VERSION 260            // bump when protocol changes
+#define DEMO_VERSION 1                  // bump when demo format changes
+#define DEMO_MAGIC "SAUERBRATEN_DEMO"
+
+struct demoheader
+{
+    char magic[16];
+    int version, protocol;
+};
+
+#define MAXNAMELEN 15
+#define MAXTEAMLEN 4
+
+enum
+{
+    HICON_BLUE_ARMOUR = 0,
+    HICON_GREEN_ARMOUR,
+    HICON_YELLOW_ARMOUR,
+
+    HICON_HEALTH,
+
+    HICON_FIST,
+    HICON_SG,
+    HICON_CG,
+    HICON_RL,
+    HICON_RIFLE,
+    HICON_GL,
+    HICON_PISTOL,
+
+    HICON_QUAD,
+
+    HICON_RED_FLAG,
+    HICON_BLUE_FLAG,
+    HICON_NEUTRAL_FLAG,
+
+    HICON_TOKEN,
+
+    HICON_X       = 20,
+    HICON_Y       = 1650,
+    HICON_TEXTY   = 1644,
+    HICON_STEP    = 490,
+    HICON_SIZE    = 120,
+    HICON_SPACE   = 40
+};
+
+static struct itemstat { int add, max, sound; const char *name; int icon, info; } itemstats[] =
+{
+    {10,    30,    S_ITEMAMMO,   "SG", HICON_SG, GUN_SG},
+    {20,    60,    S_ITEMAMMO,   "CG", HICON_CG, GUN_CG},
+    {5,     15,    S_ITEMAMMO,   "RL", HICON_RL, GUN_RL},
+    {5,     15,    S_ITEMAMMO,   "RI", HICON_RIFLE, GUN_RIFLE},
+    {10,    30,    S_ITEMAMMO,   "GL", HICON_GL, GUN_GL},
+    {30,    120,   S_ITEMAMMO,   "PI", HICON_PISTOL, GUN_PISTOL},
+    {25,    100,   S_ITEMHEALTH, "H",  HICON_HEALTH, -1},
+    {100,   200,   S_ITEMHEALTH, "MH", HICON_HEALTH, 50},
+    {100,   100,   S_ITEMARMOUR, "GA", HICON_GREEN_ARMOUR, A_GREEN},
+    {200,   200,   S_ITEMARMOUR, "YA", HICON_YELLOW_ARMOUR, A_YELLOW},
+    {20000, 30000, S_ITEMPUP,    "Q",  HICON_QUAD, -1},
+};
+
+#define MAXRAYS 20
+#define EXP_SELFDAMDIV 2
+#define EXP_SELFPUSH 2.5f
+#define EXP_DISTSCALE 1.5f
+
+static const struct guninfo { int sound, attackdelay, damage, spread, projspeed, kickamount, range, rays, hitpush, exprad, ttl; const char *name, *file; short part; } guns[NUMGUNS] =
+{
+    { S_PUNCH1,    250,  50,   0,   0,  0,   14,  1,  80,  0,    0, "fist",            "fist",   0 },
+    { S_SG,       1400,  10, 400,   0, 20, 1024, 20,  80,  0,    0, "shotgun",         "shotg",  0 },
+    { S_CG,        100,  30, 100,   0,  7, 1024,  1,  80,  0,    0, "chaingun",        "chaing", 0 },
+    { S_RLFIRE,    800, 120,   0, 320, 10, 1024,  1, 160, 40,    0, "rocketlauncher",  "rocket", 0 },
+    { S_RIFLE,    1500, 100,   0,   0, 30, 2048,  1,  80,  0,    0, "rifle",           "rifle",  0 },
+    { S_FLAUNCH,   600,  90,   0, 200, 10, 1024,  1, 250, 45, 1500, "grenadelauncher", "gl",     0 },
+    { S_PISTOL,    500,  35,  50,   0,  7, 1024,  1,  80,  0,    0, "pistol",          "pistol", 0 },
+    { S_FLAUNCH,   200,  20,   0, 200,  1, 1024,  1,  80, 40,    0, "fireball",        NULL,     PART_FIREBALL1 },
+    { S_ICEBALL,   200,  40,   0, 120,  1, 1024,  1,  80, 40,    0, "iceball",         NULL,     PART_FIREBALL2 },
+    { S_SLIMEBALL, 200,  30,   0, 640,  1, 1024,  1,  80, 40,    0, "slimeball",       NULL,     PART_FIREBALL3 },
+    { S_PIGR1,     250,  50,   0,   0,  1,   12,  1,  80,  0,    0, "bite",            NULL,     0 },
+    { -1,            0, 120,   0,   0,  0,    0,  1,  80, 40,    0, "barrel",          NULL,     0 }
+};
+
+#include "ai.h"
+
+// inherited by fpsent and server clients
+struct fpsstate
+{
+    int health, maxhealth;
+    int armour, armourtype;
+    int quadmillis;
+    int gunselect, gunwait;
+    int ammo[NUMGUNS];
+    int aitype, skill;
+
+    fpsstate() : maxhealth(100), aitype(AI_NONE), skill(0) {}
+
+    void baseammo(int gun, int k = 2, int scale = 1)
+    {
+        ammo[gun] = (itemstats[gun-GUN_SG].add*k)/scale;
+    }
+
+    void addammo(int gun, int k = 1, int scale = 1)
+    {
+        itemstat &is = itemstats[gun-GUN_SG];
+        ammo[gun] = min(ammo[gun] + (is.add*k)/scale, is.max);
+    }
+
+    bool hasmaxammo(int type)
+    {
+       const itemstat &is = itemstats[type-I_SHELLS];
+       return ammo[type-I_SHELLS+GUN_SG]>=is.max;
+    }
+
+    bool canpickup(int type)
+    {
+        if(type<I_SHELLS || type>I_QUAD) return false;
+        itemstat &is = itemstats[type-I_SHELLS];
+        switch(type)
+        {
+            case I_BOOST: return maxhealth<is.max || health<maxhealth;
+            case I_HEALTH: return health<maxhealth;
+            case I_GREENARMOUR:
+                // (100h/100g only absorbs 200 damage)
+                if(armourtype==A_YELLOW && armour>=100) return false;
+            case I_YELLOWARMOUR: return !armourtype || armour<is.max;
+            case I_QUAD: return quadmillis<is.max;
+            default: return ammo[is.info]<is.max;
+        }
+    }
+
+    void pickup(int type)
+    {
+        if(type<I_SHELLS || type>I_QUAD) return;
+        itemstat &is = itemstats[type-I_SHELLS];
+        switch(type)
+        {
+            case I_BOOST:
+                maxhealth = min(maxhealth+is.info, is.max);
+            case I_HEALTH: // boost also adds to health
+                health = min(health+is.add, maxhealth);
+                break;
+            case I_GREENARMOUR:
+            case I_YELLOWARMOUR:
+                armour = min(armour+is.add, is.max);
+                armourtype = is.info;
+                break;
+            case I_QUAD:
+                quadmillis = min(quadmillis+is.add, is.max);
+                break;
+            default:
+                ammo[is.info] = min(ammo[is.info]+is.add, is.max);
+                break;
+        }
+    }
+
+    void respawn()
+    {
+        maxhealth = 100;
+        health = maxhealth;
+        armour = 0;
+        armourtype = A_BLUE;
+        quadmillis = 0;
+        gunselect = GUN_PISTOL;
+        gunwait = 0;
+        loopi(NUMGUNS) ammo[i] = 0;
+        ammo[GUN_FIST] = 1;
+    }
+
+    void spawnstate(int gamemode)
+    {
+        if(m_demo)
+        {
+            gunselect = GUN_FIST;
+        }
+        else if(m_insta)
+        {
+            armour = 0;
+            health = 1;
+            gunselect = GUN_RIFLE;
+            ammo[GUN_RIFLE] = 100;
+        }
+        else if(m_regencapture)
+        {
+            extern int regenbluearmour;
+            if(regenbluearmour)
+            {
+                armourtype = A_BLUE;
+                armour = 25;
+            }
+            gunselect = GUN_PISTOL;
+            ammo[GUN_PISTOL] = 40;
+            ammo[GUN_GL] = 1;
+        }
+        else if(m_tactics)
+        {
+            armourtype = A_GREEN;
+            armour = 100;
+            ammo[GUN_PISTOL] = 40;
+            int spawngun1 = rnd(5)+1, spawngun2;
+            gunselect = spawngun1;
+            baseammo(spawngun1, m_noitems ? 2 : 1);
+            do spawngun2 = rnd(5)+1; while(spawngun1==spawngun2);
+            baseammo(spawngun2, m_noitems ? 2 : 1);
+            if(m_noitems) ammo[GUN_GL] += 1;
+        }
+        else if(m_efficiency)
+        {
+            armourtype = A_GREEN;
+            armour = 100;
+            loopi(5) baseammo(i+1);
+            gunselect = GUN_CG;
+            ammo[GUN_CG] /= 2;
+        }
+        else if(m_ctf || m_collect)
+        {
+            armourtype = A_BLUE;
+            armour = 50;
+            ammo[GUN_PISTOL] = 40;
+            ammo[GUN_GL] = 1;
+        }
+        else if(m_sp)
+        {
+            if(m_dmsp) 
+            {
+                armourtype = A_BLUE;
+                armour = 25;
+            }
+            ammo[GUN_PISTOL] = 80;
+            ammo[GUN_GL] = 1;
+        }
+        else
+        {
+            armourtype = A_BLUE;
+            armour = 25;
+            ammo[GUN_PISTOL] = 40;
+            ammo[GUN_GL] = 1;
+        }
+    }
+
+    // just subtract damage here, can set death, etc. later in code calling this
+    int dodamage(int damage)
+    {
+        int ad = (damage*(armourtype+1)*25)/100; // let armour absorb when possible
+        if(ad>armour) ad = armour;
+        armour -= ad;
+        damage -= ad;
+        health -= damage;
+        return damage;
+    }
+
+    int hasammo(int gun, int exclude = -1)
+    {
+        return gun >= 0 && gun <= NUMGUNS && gun != exclude && ammo[gun] > 0;
+    }
+};
+
+struct fpsent : dynent, fpsstate
+{
+    int weight;                         // affects the effectiveness of hitpush
+    int clientnum, privilege, lastupdate, plag, ping;
+    int lifesequence;                   // sequence id for each respawn, used in damage test
+    int respawned, suicided;
+    int lastpain;
+    int lastaction, lastattackgun;
+    bool attacking;
+    int attacksound, attackchan, idlesound, idlechan;
+    int lasttaunt;
+    int lastpickup, lastpickupmillis, lastbase, lastrepammo, flagpickup, tokens;
+    vec lastcollect;
+    int frags, flags, deaths, totaldamage, totalshots;
+    editinfo *edit;
+    float deltayaw, deltapitch, deltaroll, newyaw, newpitch, newroll;
+    int smoothmillis;
+
+    string name, team, info;
+    int playermodel;
+    ai::aiinfo *ai;
+    int ownernum, lastnode;
+
+    vec muzzle;
+
+    fpsent() : weight(100), clientnum(-1), privilege(PRIV_NONE), lastupdate(0), plag(0), ping(0), lifesequence(0), respawned(-1), suicided(-1), lastpain(0), attacksound(-1), attackchan(-1), idlesound(-1), idlechan(-1), frags(0), flags(0), deaths(0), totaldamage(0), totalshots(0), edit(NULL), smoothmillis(-1), playermodel(-1), ai(NULL), ownernum(-1), muzzle(-1, -1, -1)
+    {
+        name[0] = team[0] = info[0] = 0;
+        respawn();
+    }
+    ~fpsent()
+    {
+        freeeditinfo(edit);
+        if(attackchan >= 0) stopsound(attacksound, attackchan);
+        if(idlechan >= 0) stopsound(idlesound, idlechan);
+        if(ai) delete ai;
+    }
+
+    void hitpush(int damage, const vec &dir, fpsent *actor, int gun)
+    {
+        vec push(dir);
+        push.mul((actor==this && guns[gun].exprad ? EXP_SELFPUSH : 1.0f)*guns[gun].hitpush*damage/weight);
+        vel.add(push);
+    }
+
+    void stopattacksound()
+    {
+        if(attackchan >= 0) stopsound(attacksound, attackchan, 250);
+        attacksound = attackchan = -1;
+    }
+
+    void stopidlesound()
+    {
+        if(idlechan >= 0) stopsound(idlesound, idlechan, 100);
+        idlesound = idlechan = -1;
+    }
+
+    void respawn()
+    {
+        dynent::reset();
+        fpsstate::respawn();
+        respawned = suicided = -1;
+        lastaction = 0;
+        lastattackgun = gunselect;
+        attacking = false;
+        lasttaunt = 0;
+        lastpickup = -1;
+        lastpickupmillis = 0;
+        lastbase = lastrepammo = -1;
+        flagpickup = 0;
+        tokens = 0;
+        lastcollect = vec(-1e10f, -1e10f, -1e10f);
+        stopattacksound();
+        lastnode = -1;
+    }
+
+    int respawnwait(int secs, int delay = 0)
+    {
+        return max(0, secs - (::lastmillis - lastpain - delay)/1000);
+    }
+};
+
+struct teamscore
+{
+    const char *team;
+    int score;
+    teamscore() {}
+    teamscore(const char *s, int n) : team(s), score(n) {}
+
+    static bool compare(const teamscore &x, const teamscore &y)
+    {
+        if(x.score > y.score) return true;
+        if(x.score < y.score) return false;
+        return strcmp(x.team, y.team) < 0;
+    }
+};
+
+static inline uint hthash(const teamscore &t) { return hthash(t.team); }
+static inline bool htcmp(const char *key, const teamscore &t) { return htcmp(key, t.team); }
+
+#define MAXTEAMS 128
+
+struct teaminfo
+{
+    char team[MAXTEAMLEN+1];
+    int frags;
+};
+
+static inline uint hthash(const teaminfo &t) { return hthash(t.team); }
+static inline bool htcmp(const char *team, const teaminfo &t) { return !strcmp(team, t.team); }
+
+namespace entities
+{
+    extern vector<extentity *> ents;
+
+    extern const char *entmdlname(int type);
+    extern const char *itemname(int i);
+    extern int itemicon(int i);
+
+    extern void preloadentities();
+    extern void renderentities();
+    extern void resettriggers();
+    extern void checktriggers();
+    extern void checkitems(fpsent *d);
+    extern void checkquad(int time, fpsent *d);
+    extern void resetspawns();
+    extern void spawnitems(bool force = false);
+    extern void putitems(packetbuf &p);
+    extern void setspawn(int i, bool on);
+    extern void teleport(int n, fpsent *d);
+    extern void pickupeffects(int n, fpsent *d);
+    extern void teleporteffects(fpsent *d, int tp, int td, bool local = true);
+    extern void jumppadeffects(fpsent *d, int jp, bool local = true);
+
+    extern void repammo(fpsent *d, int type, bool local = true);
+}
+
+namespace game
+{
+    struct clientmode
+    {
+        virtual ~clientmode() {}
+
+        virtual void preload() {}
+        virtual int clipconsole(int w, int h) { return 0; }
+        virtual void drawhud(fpsent *d, int w, int h) {}
+        virtual void rendergame() {}
+        virtual void respawned(fpsent *d) {}
+        virtual void setup() {}
+        virtual void checkitems(fpsent *d) {}
+        virtual int respawnwait(fpsent *d, int delay = 0) { return 0; }
+        virtual int getspawngroup(fpsent *d) { return 0; }
+        virtual float ratespawn(fpsent *d, const extentity &e) { return 1.0f; }
+        virtual void senditems(packetbuf &p) {}
+        virtual void removeplayer(fpsent *d) {}
+        virtual void died(fpsent *victim, fpsent *actor) {}
+        virtual void gameover() {}
+        virtual bool hidefrags() { return false; }
+        virtual int getteamscore(const char *team) { return 0; }
+        virtual void getteamscores(vector<teamscore> &scores) {}
+        virtual void aifind(fpsent *d, ai::aistate &b, vector<ai::interest> &interests) {}
+        virtual bool aicheck(fpsent *d, ai::aistate &b) { return false; }
+        virtual bool aidefend(fpsent *d, ai::aistate &b) { return false; }
+        virtual bool aipursue(fpsent *d, ai::aistate &b) { return false; }
+    };
+
+    extern clientmode *cmode;
+    extern void setclientmode();
+
+    // fps
+    extern int gamemode, nextmode;
+    extern string clientmap;
+    extern bool intermission;
+    extern int maptime, maprealtime, maplimit;
+    extern fpsent *player1;
+    extern vector<fpsent *> players, clients;
+    extern int lastspawnattempt;
+    extern int lasthit;
+    extern int respawnent;
+    extern int following;
+    extern int smoothmove, smoothdist;
+
+    extern bool clientoption(const char *arg);
+    extern fpsent *getclient(int cn);
+    extern fpsent *newclient(int cn);
+    extern const char *colorname(fpsent *d, const char *name = NULL, const char *prefix = "", const char *suffix = "", const char *alt = NULL);
+    extern const char *teamcolorname(fpsent *d, const char *alt = "you");
+    extern const char *teamcolor(const char *name, bool sameteam, const char *alt = NULL);
+    extern const char *teamcolor(const char *name, const char *team, const char *alt = NULL);
+    extern void teamsound(bool sameteam, int n, const vec *loc = NULL);
+    extern void teamsound(fpsent *d, int n, const vec *loc = NULL);
+    extern fpsent *pointatplayer();
+    extern fpsent *hudplayer();
+    extern fpsent *followingplayer(fpsent *fallback = NULL);
+    extern void stopfollowing();
+    extern void clientdisconnected(int cn, bool notify = true);
+    extern void clearclients(bool notify = true);
+    extern void startgame();
+    extern float proximityscore(float x, float lower, float upper);
+    extern void pickgamespawn(fpsent *d);
+    extern void spawnplayer(fpsent *d);
+    extern void deathstate(fpsent *d, bool restore = false);
+    extern void damaged(int damage, fpsent *d, fpsent *actor, bool local = true);
+    extern void killed(fpsent *d, fpsent *actor);
+    extern void timeupdate(int timeremain);
+    extern void msgsound(int n, physent *d = NULL);
+    extern void drawicon(int icon, float x, float y, float sz = 120);
+    const char *mastermodecolor(int n, const char *unknown);
+    const char *mastermodeicon(int n, const char *unknown);
+
+    // client
+    extern bool connected, remote, demoplayback;
+    extern string servinfo;
+    extern vector<uchar> messages;
+
+    extern int parseplayer(const char *arg);
+    extern void ignore(int cn);
+    extern void unignore(int cn);
+    extern bool isignored(int cn);
+    extern bool addmsg(int type, const char *fmt = NULL, ...);
+    extern void switchname(const char *name);
+    extern void switchteam(const char *name);
+    extern void switchplayermodel(int playermodel);
+    extern void sendmapinfo();
+    extern void stopdemo();
+    extern void changemap(const char *name, int mode);
+    extern void forceintermission();
+    extern void c2sinfo(bool force = false);
+    extern void sendposition(fpsent *d, bool reliable = false);
+
+    // monster
+    struct monster;
+    extern vector<monster *> monsters;
+
+    extern void clearmonsters();
+    extern void preloadmonsters();
+    extern void stackmonster(monster *d, physent *o);
+    extern void updatemonsters(int curtime);
+    extern void rendermonsters();
+    extern void suicidemonster(monster *m);
+    extern void hitmonster(int damage, monster *m, fpsent *at, const vec &vel, int gun);
+    extern void monsterkilled();
+    extern void endsp(bool allkilled);
+    extern void spsummary(int accuracy);
+
+    // movable
+    struct movable;
+    extern vector<movable *> movables;
+
+    extern void clearmovables();
+    extern void stackmovable(movable *d, physent *o);
+    extern void updatemovables(int curtime);
+    extern void rendermovables();
+    extern void suicidemovable(movable *m);
+    extern void hitmovable(int damage, movable *m, fpsent *at, const vec &vel, int gun);
+
+    // weapon
+    extern int getweapon(const char *name);
+    extern void shoot(fpsent *d, const vec &targ);
+    extern void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction);
+    extern void explode(bool local, fpsent *owner, const vec &v, dynent *safe, int dam, int gun);
+    extern void explodeeffects(int gun, fpsent *d, bool local, int id = 0);
+    extern void damageeffect(int damage, fpsent *d, bool thirdperson = true);
+    extern void gibeffect(int damage, const vec &vel, fpsent *d);
+    extern float intersectdist;
+    extern bool intersect(dynent *d, const vec &from, const vec &to, float &dist = intersectdist);
+    extern dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, float &dist = intersectdist);
+    extern void clearbouncers();
+    extern void updatebouncers(int curtime);
+    extern void removebouncers(fpsent *owner);
+    extern void renderbouncers();
+    extern void clearprojectiles();
+    extern void updateprojectiles(int curtime);
+    extern void removeprojectiles(fpsent *owner);
+    extern void renderprojectiles();
+    extern void preloadbouncers();
+    extern void removeweapons(fpsent *owner);
+    extern void updateweapons(int curtime);
+    extern void gunselect(int gun, fpsent *d);
+    extern void weaponswitch(fpsent *d);
+    extern void avoidweapons(ai::avoidset &obstacles, float radius);
+
+    // scoreboard
+    extern void showscores(bool on);
+    extern void getbestplayers(vector<fpsent *> &best);
+    extern void getbestteams(vector<const char *> &best);
+    extern void clearteaminfo();
+    extern void setteaminfo(const char *team, int frags);
+    extern int statuscolor(fpsent *d, int color);
+
+    // render
+    struct playermodelinfo
+    {
+        const char *ffa, *blueteam, *redteam, *hudguns,
+                   *vwep, *quad, *armour[3],
+                   *ffaicon, *blueicon, *redicon;
+        bool ragdoll;
+    };
+
+    extern int playermodel, teamskins, testteam;
+
+    extern void saveragdoll(fpsent *d);
+    extern void clearragdolls();
+    extern void moveragdolls();
+    extern void changedplayermodel();
+    extern const playermodelinfo &getplayermodelinfo(fpsent *d);
+    extern int chooserandomplayermodel(int seed);
+    extern void swayhudgun(int curtime);
+    extern vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d);
+}
+
+namespace server
+{
+    extern const char *modename(int n, const char *unknown = "unknown");
+    extern const char *mastermodename(int n, const char *unknown = "unknown");
+    extern void startintermission();
+    extern void stopdemo();
+    extern void timeupdate(int secs);
+    extern const char *getdemofile(const char *file, bool init);
+    extern void forcemap(const char *map, int mode);
+    extern void forcepaused(bool paused);
+    extern void forcegamespeed(int speed);
+    extern void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen = MAXSTRLEN);
+    extern int msgsizelookup(int msg);
+    extern bool serveroption(const char *arg);
+    extern bool delayspawn(int type);
+}
+
+#endif
+
diff --git a/src/fpsgame/render.cpp b/src/fpsgame/render.cpp
new file mode 100644 (file)
index 0000000..d6c8b91
--- /dev/null
@@ -0,0 +1,560 @@
+#include "game.h"
+
+struct spawninfo { const extentity *e; float weight; };
+extern float gatherspawninfos(dynent *d, int tag, vector<spawninfo> &spawninfos);
+
+namespace game
+{      
+    vector<fpsent *> bestplayers;
+    vector<const char *> bestteams;
+
+    VARP(ragdoll, 0, 1, 1);
+    VARP(ragdollmillis, 0, 10000, 300000);
+    VARP(ragdollfade, 0, 1000, 300000);
+    VARFP(playermodel, 0, 0, 4, changedplayermodel());
+    VARP(forceplayermodels, 0, 0, 1);
+    VARP(hidedead, 0, 0, 2);
+
+    vector<fpsent *> ragdolls;
+
+    void saveragdoll(fpsent *d)
+    {
+        if(!d->ragdoll || !ragdollmillis || (!ragdollfade && lastmillis > d->lastpain + ragdollmillis)) return;
+        fpsent *r = new fpsent(*d);
+        r->lastupdate = ragdollfade && lastmillis > d->lastpain + max(ragdollmillis - ragdollfade, 0) ? lastmillis - max(ragdollmillis - ragdollfade, 0) : d->lastpain;
+        r->edit = NULL;
+        r->ai = NULL;
+        r->attackchan = r->idlechan = -1;
+        if(d==player1) r->playermodel = playermodel;
+        ragdolls.add(r);
+        d->ragdoll = NULL;   
+    }
+
+    void clearragdolls()
+    {
+        ragdolls.deletecontents();
+    }
+
+    void moveragdolls()
+    {
+        loopv(ragdolls)
+        {
+            fpsent *d = ragdolls[i];
+            if(lastmillis > d->lastupdate + ragdollmillis)
+            {
+                delete ragdolls.remove(i--);
+                continue;
+            }
+            moveragdoll(d);
+        }
+    }
+
+    static const playermodelinfo playermodels[5] =
+    {
+        { "mrfixit", "mrfixit/blue", "mrfixit/red", "mrfixit/hudguns", NULL, "mrfixit/horns", { "mrfixit/armor/blue", "mrfixit/armor/green", "mrfixit/armor/yellow" }, "mrfixit", "mrfixit_blue", "mrfixit_red", true },
+        { "snoutx10k", "snoutx10k/blue", "snoutx10k/red", "snoutx10k/hudguns", NULL, "snoutx10k/wings", { "snoutx10k/armor/blue", "snoutx10k/armor/green", "snoutx10k/armor/yellow" }, "snoutx10k", "snoutx10k_blue", "snoutx10k_red", true },
+        //{ "ogro/green", "ogro/blue", "ogro/red", "mrfixit/hudguns", "ogro/vwep", NULL, { NULL, NULL, NULL }, "ogro", "ogro_blue", "ogro_red", false },
+        { "ogro2", "ogro2/blue", "ogro2/red", "mrfixit/hudguns", NULL, "ogro2/quad", { "ogro2/armor/blue", "ogro2/armor/green", "ogro2/armor/yellow" }, "ogro", "ogro_blue", "ogro_red", true },
+        { "inky", "inky/blue", "inky/red", "inky/hudguns", NULL, "inky/quad", { "inky/armor/blue", "inky/armor/green", "inky/armor/yellow" }, "inky", "inky_blue", "inky_red", true },
+        { "captaincannon", "captaincannon/blue", "captaincannon/red", "captaincannon/hudguns", NULL, "captaincannon/quad", { "captaincannon/armor/blue", "captaincannon/armor/green", "captaincannon/armor/yellow" }, "captaincannon", "captaincannon_blue", "captaincannon_red", true }
+    };
+
+    int chooserandomplayermodel(int seed)
+    {
+        return (seed&0xFFFF)%(sizeof(playermodels)/sizeof(playermodels[0]));
+    }
+
+    const playermodelinfo *getplayermodelinfo(int n)
+    {
+        if(size_t(n) >= sizeof(playermodels)/sizeof(playermodels[0])) return NULL;
+        return &playermodels[n];
+    }
+
+    const playermodelinfo &getplayermodelinfo(fpsent *d)
+    {
+        const playermodelinfo *mdl = getplayermodelinfo(d==player1 || forceplayermodels ? playermodel : d->playermodel);
+        if(!mdl) mdl = getplayermodelinfo(playermodel);
+        return *mdl;
+    }
+
+    void changedplayermodel()
+    {
+        if(player1->clientnum < 0) player1->playermodel = playermodel;
+        if(player1->ragdoll) cleanragdoll(player1);
+        loopv(ragdolls) 
+        {
+            fpsent *d = ragdolls[i];
+            if(!d->ragdoll) continue;
+            if(!forceplayermodels)
+            {
+                const playermodelinfo *mdl = getplayermodelinfo(d->playermodel);
+                if(mdl) continue;
+            }
+            cleanragdoll(d);
+        }
+        loopv(players)
+        {
+            fpsent *d = players[i];
+            if(d == player1 || !d->ragdoll) continue;
+            if(!forceplayermodels)
+            {
+                const playermodelinfo *mdl = getplayermodelinfo(d->playermodel);
+                if(mdl) continue;
+            }
+            cleanragdoll(d);
+        }
+    }
+
+    void preloadplayermodel()
+    {
+        loopi(sizeof(playermodels)/sizeof(playermodels[0]))
+        {
+            const playermodelinfo *mdl = getplayermodelinfo(i);
+            if(!mdl) break;
+            if(i != playermodel && (!multiplayer(false) || forceplayermodels)) continue;
+            if(m_teammode)
+            {
+                preloadmodel(mdl->blueteam);
+                preloadmodel(mdl->redteam);
+            }
+            else preloadmodel(mdl->ffa);
+            if(mdl->vwep) preloadmodel(mdl->vwep);
+            if(mdl->quad) preloadmodel(mdl->quad);
+            loopj(3) if(mdl->armour[j]) preloadmodel(mdl->armour[j]);
+        }
+    }
+    
+    VAR(testquad, 0, 0, 1);
+    VAR(testarmour, 0, 0, 1);
+    VAR(testteam, 0, 0, 3);
+
+    void renderplayer(fpsent *d, const playermodelinfo &mdl, int team, float fade, bool mainpass)
+    {
+        int lastaction = d->lastaction, hold = mdl.vwep || d->gunselect==GUN_PISTOL ? 0 : (ANIM_HOLD1+d->gunselect)|ANIM_LOOP, attack = ANIM_ATTACK1+d->gunselect, delay = mdl.vwep ? 300 : guns[d->gunselect].attackdelay+50;
+        if(intermission && d->state!=CS_DEAD)
+        {
+            lastaction = 0;
+            hold = attack = ANIM_LOSE|ANIM_LOOP;
+            delay = 0;
+            if(m_teammode ? bestteams.htfind(d->team)>=0 : bestplayers.find(d)>=0) hold = attack = ANIM_WIN|ANIM_LOOP;
+        }
+        else if(d->state==CS_ALIVE && d->lasttaunt && lastmillis-d->lasttaunt<1000 && lastmillis-d->lastaction>delay)
+        {
+            lastaction = d->lasttaunt;
+            hold = attack = ANIM_TAUNT;
+            delay = 1000;
+        }
+        modelattach a[5];
+        static const char * const vweps[] = {"vwep/fist", "vwep/shotg", "vwep/chaing", "vwep/rocket", "vwep/rifle", "vwep/gl", "vwep/pistol"};
+        int ai = 0;
+        if((!mdl.vwep || d->gunselect!=GUN_FIST) && d->gunselect<=GUN_PISTOL)
+        {
+            int vanim = ANIM_VWEP_IDLE|ANIM_LOOP, vtime = 0;
+            if(lastaction && d->lastattackgun==d->gunselect && lastmillis < lastaction + delay)
+            {
+                vanim = ANIM_VWEP_SHOOT;
+                vtime = lastaction;
+            }
+            a[ai++] = modelattach("tag_weapon", mdl.vwep ? mdl.vwep : vweps[d->gunselect], vanim, vtime);
+        }
+        if(d->state==CS_ALIVE)
+        {
+            if((testquad || d->quadmillis) && mdl.quad)
+                a[ai++] = modelattach("tag_powerup", mdl.quad, ANIM_POWERUP|ANIM_LOOP, 0);
+            if(testarmour || d->armour)
+            {
+                int type = clamp(d->armourtype, (int)A_BLUE, (int)A_YELLOW);
+                if(mdl.armour[type])
+                    a[ai++] = modelattach("tag_shield", mdl.armour[type], ANIM_SHIELD|ANIM_LOOP, 0);
+            }
+        }
+        if(mainpass)
+        {
+            d->muzzle = vec(-1, -1, -1);
+            a[ai++] = modelattach("tag_muzzle", &d->muzzle);
+        }
+        const char *mdlname = mdl.ffa;
+        switch(testteam ? testteam-1 : team)
+        {
+            case 1: mdlname = mdl.blueteam; break;
+            case 2: mdlname = mdl.redteam; break;
+        }
+        renderclient(d, mdlname, a[0].tag ? a : NULL, hold, attack, delay, lastaction, intermission && d->state!=CS_DEAD ? 0 : d->lastpain, fade, ragdoll && mdl.ragdoll);
+#if 0
+        if(d->state!=CS_DEAD && d->quadmillis) 
+        {
+            entitylight light;
+            rendermodel(&light, "quadrings", ANIM_MAPMODEL|ANIM_LOOP, vec(d->o).sub(vec(0, 0, d->eyeheight/2)), 360*lastmillis/1000.0f, 0, MDL_DYNSHADOW | MDL_CULL_VFC | MDL_CULL_DIST);
+        }
+#endif
+    }
+
+    VARP(teamskins, 0, 0, 1);
+
+#if 0
+    // for testing spawns
+
+    float hsv2rgb(float h, float s, float v, int n)
+    {
+        float k = fmod(n + h / 60.0f, 6.0f);
+        return v - v * s * max(min(min(k, 4.0f - k), 1.0f), 0.0f);
+    }
+
+    vec hsv2rgb(float h, float s, float v)
+    {
+        return vec(hsv2rgb(h, s, v, 5), hsv2rgb(h, s, v, 3), hsv2rgb(h, s, v, 1));
+    }
+
+    void renderspawn(const vec &o, int rating, float probability)
+    {
+        defformatstring(score, "%d", rating);
+        defformatstring(percentage, "(%.2f%%)", probability * 100);
+        bvec colorvec = bvec::fromcolor(hsv2rgb(rating * 1.2f, 0.8, 1));
+        int color = (colorvec.r << 16) + (colorvec.g << 8) + colorvec.b;
+        particle_textcopy(vec(o).addz(5), score, PART_TEXT, 1, color, 5.0f);
+        particle_textcopy(vec(o).addz(1), percentage, PART_TEXT, 1, color, 4.0f);
+    }
+
+    void renderspawns()
+    {
+        vector<spawninfo> spawninfos;
+        float ratingsum = gatherspawninfos(player1, 0, spawninfos);
+        loopv(spawninfos) renderspawn(spawninfos[i].e->o, spawninfos[i].weight * 100, spawninfos[i].weight / ratingsum);
+    }
+
+    VAR(dbgspawns, 0, 0, 1);
+#endif
+
+    VARP(statusicons, 0, 1, 1);
+
+    void renderstatusicons(fpsent *d, int team, float yoffset)
+    {
+        vec p = d->abovehead().madd(camup, yoffset);
+        int icons = 0;
+        const itemstat &boost = itemstats[I_BOOST-I_SHELLS];
+        if(statusicons && (d->state==CS_ALIVE || d->state==CS_LAGGED))
+        {
+            if(d->quadmillis) icons++;
+            if(d->maxhealth>100) icons += (min(d->maxhealth, boost.max) - 100 + boost.info-1) / boost.info;
+            if(d->armour>0 && d->armourtype>=A_GREEN && !m_noitems) icons++;
+        }
+        if(icons) concatstring(d->info, " ");
+        particle_text(p, d->info, PART_TEXT, 1, team ? (team==1 ? 0x6496FF : 0xFF4B19) : 0x1EC850, 2.0f, 0, icons);
+        if(icons)
+        {
+            float tw, th;
+            text_boundsf(d->info, tw, th);
+            float offset = (tw - icons*th)/2;
+            if(d->armour>0 && d->armourtype>=A_GREEN && !m_noitems)
+            {
+                int icon = itemstats[(d->armourtype==A_YELLOW ? I_YELLOWARMOUR : I_GREENARMOUR)-I_SHELLS].icon;
+                particle_texticon(p, icon%4, icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f);
+                offset += th;
+            }
+            for(int i = 100; i < min(d->maxhealth, boost.max); i += boost.info)
+            {
+                particle_texticon(p, boost.icon%4, boost.icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f);
+                offset += th;
+            }
+            if(d->quadmillis)
+            {
+                int icon = itemstats[I_QUAD-I_SHELLS].icon;
+                particle_texticon(p, icon%4, icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f);
+                offset += th;
+            }
+        }
+    }
+
+    VARP(statusbars, 0, 1, 2);
+    FVARP(statusbarscale, 0, 1, 2);
+
+    float renderstatusbars(fpsent *d, int team)
+    {
+        if(!statusbars || m_insta || (player1->state==CS_SPECTATOR ? statusbars <= 1 : team != 1) || (d->state!=CS_ALIVE && d->state!=CS_LAGGED)) return 0;
+        vec p = d->abovehead().msub(camdir, 50/80.0f).msub(camup, 2.0f);
+        float offset = 0;
+        float scale = statusbarscale;
+        if(d->armour > 0)
+        {
+            int limit = d->armourtype==A_YELLOW ? 200 : (d->armourtype==A_GREEN ? 100 : 50);
+            int color = d->armourtype==A_YELLOW ? 0xFFC040 : (d->armourtype==A_GREEN ? 0x008C00 : 0x0B5899);
+            float size = scale*sqrtf(max(d->armour, limit)/100.0f);
+            float fill = float(d->armour)/limit;
+            offset += size;
+            particle_meter(vec(p).madd(camup, offset), fill, PART_METER, 1, color, 0, size);
+        }
+        int color = d->health<=25 ? 0xFF0000 : (d->health<=50 ? 0xFF8000 : (d->health<=100 ? 0x40FF80 : 0x40C0FF));
+        float size = scale*sqrtf(max(d->health, d->maxhealth)/100.0f);
+        float fill = float(d->health)/d->maxhealth;
+        offset += size;
+        particle_meter(vec(p).madd(camup, offset), fill, PART_METER, 1, color, 0, size);
+        return offset;
+    }
+
+    void rendergame(bool mainpass)
+    {
+        if(mainpass) ai::render();
+
+        if(intermission)
+        {
+            bestteams.shrink(0);
+            bestplayers.shrink(0);
+            if(m_teammode) getbestteams(bestteams);
+            else getbestplayers(bestplayers);
+        }
+
+        startmodelbatches();
+
+        fpsent *exclude = isthirdperson() ? NULL : followingplayer();
+        loopv(players)
+        {
+            fpsent *d = players[i];
+            if(d == player1 || d->state==CS_SPECTATOR || d->state==CS_SPAWNING || d->lifesequence < 0 || d == exclude || (d->state==CS_DEAD && hidedead)) continue;
+            int team = 0;
+            if(teamskins || m_teammode) team = isteam(player1->team, d->team) ? 1 : 2;
+            renderplayer(d, getplayermodelinfo(d), team, 1, mainpass);
+
+            vec dir = vec(d->o).sub(camera1->o);
+            float dist = dir.magnitude();
+            dir.div(dist);
+            if(d->state!=CS_EDITING && raycube(camera1->o, dir, dist, 0) < dist)
+            {
+                d->info[0] = '\0';
+                continue;
+            }
+
+            copystring(d->info, colorname(d));
+            if(d->state!=CS_DEAD)
+            {
+                float offset = renderstatusbars(d, team);
+                renderstatusicons(d, team, offset);
+            }
+        }
+        loopv(ragdolls)
+        {
+            fpsent *d = ragdolls[i];
+            int team = 0;
+            if(teamskins || m_teammode) team = isteam(player1->team, d->team) ? 1 : 2;
+            float fade = 1.0f;
+            if(ragdollmillis && ragdollfade) 
+                fade -= clamp(float(lastmillis - (d->lastupdate + max(ragdollmillis - ragdollfade, 0)))/min(ragdollmillis, ragdollfade), 0.0f, 1.0f);
+            renderplayer(d, getplayermodelinfo(d), team, fade, mainpass);
+        } 
+        if(isthirdperson() && !followingplayer() && (player1->state!=CS_DEAD || hidedead != 1)) renderplayer(player1, getplayermodelinfo(player1), teamskins || m_teammode ? 1 : 0, 1, mainpass);
+        rendermonsters();
+        rendermovables();
+        entities::renderentities();
+        renderbouncers();
+        renderprojectiles();
+        if(cmode) cmode->rendergame();
+
+#if 0
+        if(dbgspawns) renderspawns();
+#endif
+
+        endmodelbatches();
+    }
+
+    VARP(hudgun, 0, 1, 1);
+    VARP(hudgunsway, 0, 1, 1);
+    VARP(teamhudguns, 0, 1, 1);
+    VARP(chainsawhudgun, 0, 1, 1);
+    VAR(testhudgun, 0, 0, 1);
+
+    FVAR(swaystep, 1, 35.0f, 100);
+    FVAR(swayside, 0, 0.04f, 1);
+    FVAR(swayup, -1, 0.05f, 1);
+
+    float swayfade = 0, swayspeed = 0, swaydist = 0;
+    vec swaydir(0, 0, 0);
+
+    void swayhudgun(int curtime)
+    {
+        fpsent *d = hudplayer();
+        if(d->state != CS_SPECTATOR)
+        {
+            if(d->physstate >= PHYS_SLOPE)
+            {
+                swayspeed = min(sqrtf(d->vel.x*d->vel.x + d->vel.y*d->vel.y), d->maxspeed);
+                swaydist += swayspeed*curtime/1000.0f;
+                swaydist = fmod(swaydist, 2*swaystep);
+                swayfade = 1;
+            }
+            else if(swayfade > 0)
+            {
+                swaydist += swayspeed*swayfade*curtime/1000.0f;
+                swaydist = fmod(swaydist, 2*swaystep);
+                swayfade -= 0.5f*(curtime*d->maxspeed)/(swaystep*1000.0f);
+            }
+
+            float k = pow(0.7f, curtime/10.0f);
+            swaydir.mul(k);
+            vec vel(d->vel);
+            vel.add(d->falling);
+            swaydir.add(vec(vel).mul((1-k)/(15*max(vel.magnitude(), d->maxspeed))));
+        }
+    }
+
+    struct hudent : dynent
+    {
+        hudent() { type = ENT_CAMERA; }
+    } guninterp;
+
+    SVARP(hudgunsdir, "");
+
+    void drawhudmodel(fpsent *d, int anim, float speed = 0, int base = 0)
+    {
+        if(d->gunselect>GUN_PISTOL) return;
+
+        vec sway;
+        vecfromyawpitch(d->yaw, 0, 0, 1, sway);
+        float steps = swaydist/swaystep*M_PI;
+        sway.mul(swayside*cosf(steps));
+        sway.z = swayup*(fabs(sinf(steps)) - 1);
+        sway.add(swaydir).add(d->o);
+        if(!hudgunsway) sway = d->o;
+
+#if 0
+        if(player1->state!=CS_DEAD && player1->quadmillis)
+        {
+            float t = 0.5f + 0.5f*sinf(2*M_PI*lastmillis/1000.0f);
+            color.y = color.y*(1-t) + t;
+        }
+#endif
+        const playermodelinfo &mdl = getplayermodelinfo(d);
+        defformatstring(gunname, "%s/%s", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, guns[d->gunselect].file);
+        if((m_teammode || teamskins) && teamhudguns)
+            concatstring(gunname, d==player1 || isteam(d->team, player1->team) ? "/blue" : "/red");
+        else if(testteam > 1)
+            concatstring(gunname, testteam==2 ? "/blue" : "/red");
+        modelattach a[2];
+        d->muzzle = vec(-1, -1, -1);
+        a[0] = modelattach("tag_muzzle", &d->muzzle);
+        dynent *interp = NULL;
+        if(d->gunselect==GUN_FIST && chainsawhudgun)
+        {
+            anim |= ANIM_LOOP;
+            base = 0;
+            interp = &guninterp;
+        }
+        rendermodel(NULL, gunname, anim, sway, testhudgun ? 0 : d->yaw+90, testhudgun ? 0 : d->pitch, MDL_LIGHT|MDL_HUD, interp, a, base, (int)ceil(speed));
+        if(d->muzzle.x >= 0) d->muzzle = calcavatarpos(d->muzzle, 12);
+    }
+
+    void drawhudgun()
+    {
+        fpsent *d = hudplayer();
+        if(d->state==CS_SPECTATOR || d->state==CS_EDITING || !hudgun || editmode) 
+        { 
+            d->muzzle = player1->muzzle = vec(-1, -1, -1);
+            return;
+        }
+
+        int rtime = guns[d->gunselect].attackdelay;
+        if(d->lastaction && d->lastattackgun==d->gunselect && lastmillis-d->lastaction<rtime)
+        {
+            drawhudmodel(d, ANIM_GUN_SHOOT|ANIM_SETSPEED, rtime/17.0f, d->lastaction);
+        }
+        else
+        {
+            drawhudmodel(d, ANIM_GUN_IDLE|ANIM_LOOP);
+        }
+    }
+
+    void renderavatar()
+    {
+        drawhudgun();
+    }
+
+    void renderplayerpreview(int model, int team, int weap)
+    {
+        static fpsent *previewent = NULL;
+        if(!previewent)
+        {
+            previewent = new fpsent;
+            previewent->light.color = vec(1, 1, 1);
+            previewent->light.dir = vec(0, -1, 2).normalize();
+            loopi(GUN_PISTOL-GUN_FIST) previewent->ammo[GUN_FIST+1+i] = 1;
+        }
+        float height = previewent->eyeheight + previewent->aboveeye,
+              zrad = height/2;
+        vec2 xyrad = vec2(previewent->xradius, previewent->yradius).max(height/4);
+        previewent->o = calcmodelpreviewpos(vec(xyrad, zrad), previewent->yaw).addz(previewent->eyeheight - zrad);
+        previewent->gunselect = clamp(weap, int(GUN_FIST), int(GUN_PISTOL));
+        previewent->light.millis = -1;
+        const playermodelinfo *mdlinfo = getplayermodelinfo(model);
+        if(!mdlinfo) return;
+        renderplayer(previewent, *mdlinfo, team >= 0 && team <= 2 ? team : 0, 1, false);
+    }
+
+    vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d)
+    {
+        if(d->muzzle.x >= 0) return d->muzzle;
+        vec offset(from);
+        if(d!=hudplayer() || isthirdperson())
+        {
+            vec front, right;
+            vecfromyawpitch(d->yaw, d->pitch, 1, 0, front);
+            offset.add(front.mul(d->radius));
+            if(d->type!=ENT_AI)
+            {
+                offset.z += (d->aboveeye + d->eyeheight)*0.75f - d->eyeheight;
+                vecfromyawpitch(d->yaw, 0, 0, -1, right);
+                offset.add(right.mul(0.5f*d->radius));
+                offset.add(front);
+            }
+            return offset;
+        }
+        offset.add(vec(to).sub(from).normalize().mul(2));
+        if(hudgun)
+        {
+            offset.sub(vec(camup).mul(1.0f));
+            offset.add(vec(camright).mul(0.8f));
+        }
+        else offset.sub(vec(camup).mul(0.8f));
+        return offset;
+    }
+
+    void preloadweapons()
+    {
+        const playermodelinfo &mdl = getplayermodelinfo(player1);
+        loopi(NUMGUNS)
+        {
+            const char *file = guns[i].file;
+            if(!file) continue;
+            string fname;
+            if((m_teammode || teamskins) && teamhudguns)
+            {
+                formatstring(fname, "%s/%s/blue", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, file);
+                preloadmodel(fname);
+            }
+            else
+            {
+                formatstring(fname, "%s/%s", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, file);
+                preloadmodel(fname);
+            }
+            formatstring(fname, "vwep/%s", file);
+            preloadmodel(fname);
+        }
+    }
+
+    void preloadsounds()
+    {
+        for(int i = S_JUMP; i <= S_SPLASH2; i++) preloadsound(i);
+        for(int i = S_JUMPPAD; i <= S_PISTOL; i++) preloadsound(i);
+        for(int i = S_V_BOOST; i <= S_V_QUAD10; i++) preloadsound(i);
+        for(int i = S_BURN; i <= S_HIT; i++) preloadsound(i);
+    }
+
+    void preload()
+    {
+        if(hudgun) preloadweapons();
+        preloadbouncers();
+        preloadplayermodel();
+        preloadsounds();
+        entities::preloadentities();
+        if(m_sp) preloadmonsters();
+    }
+
+}
+
diff --git a/src/fpsgame/scoreboard.cpp b/src/fpsgame/scoreboard.cpp
new file mode 100644 (file)
index 0000000..ec99dda
--- /dev/null
@@ -0,0 +1,559 @@
+// creation of scoreboard
+#include "game.h"
+
+namespace game
+{
+    VARP(scoreboard2d, 0, 1, 1);
+    VARP(showservinfo, 0, 1, 1);
+    VARP(showclientnum, 0, 0, 1);
+    VARP(showpj, 0, 0, 1);
+    VARP(showping, 0, 1, 2);
+    VARP(showspectators, 0, 1, 1);
+    VARP(showspectatorping, 0, 0, 1);
+    VARP(highlightscore, 0, 1, 1);
+    VARP(showconnecting, 0, 0, 1);
+    VARP(hidefrags, 0, 1, 1);
+    VARP(showdeaths, 0, 0, 1);
+
+    static hashset<teaminfo> teaminfos;
+
+    void clearteaminfo()
+    {
+        teaminfos.clear();
+    }
+
+    void setteaminfo(const char *team, int frags)
+    {
+        teaminfo *t = teaminfos.access(team);
+        if(!t) { t = &teaminfos[team]; copystring(t->team, team, sizeof(t->team)); }
+        t->frags = frags;
+    }
+
+    static inline bool playersort(const fpsent *a, const fpsent *b)
+    {
+        if(a->state==CS_SPECTATOR)
+        {
+            if(b->state==CS_SPECTATOR) return strcmp(a->name, b->name) < 0;
+            else return false;
+        }
+        else if(b->state==CS_SPECTATOR) return true;
+        if(m_ctf || m_collect)
+        {
+            if(a->flags > b->flags) return true;
+            if(a->flags < b->flags) return false;
+        }
+        if(a->frags > b->frags) return true;
+        if(a->frags < b->frags) return false;
+        return strcmp(a->name, b->name) < 0;
+    }
+
+    void getbestplayers(vector<fpsent *> &best)
+    {
+        loopv(players)
+        {
+            fpsent *o = players[i];
+            if(o->state!=CS_SPECTATOR) best.add(o);
+        }
+        best.sort(playersort);
+        while(best.length() > 1 && best.last()->frags < best[0]->frags) best.drop();
+    }
+
+    void getbestteams(vector<const char *> &best)
+    {
+        if(cmode && cmode->hidefrags())
+        {
+            vector<teamscore> teamscores;
+            cmode->getteamscores(teamscores);
+            teamscores.sort(teamscore::compare);
+            while(teamscores.length() > 1 && teamscores.last().score < teamscores[0].score) teamscores.drop();
+            loopv(teamscores) best.add(teamscores[i].team);
+        }
+        else
+        {
+            int bestfrags = INT_MIN;
+            enumerate(teaminfos, teaminfo, t, bestfrags = max(bestfrags, t.frags));
+            if(bestfrags <= 0) loopv(players)
+            {
+                fpsent *o = players[i];
+                if(o->state!=CS_SPECTATOR && !teaminfos.access(o->team) && best.htfind(o->team) < 0) { bestfrags = 0; best.add(o->team); }
+            }
+            enumerate(teaminfos, teaminfo, t, if(t.frags >= bestfrags) best.add(t.team));
+        }
+    }
+
+    struct scoregroup : teamscore
+    {
+        vector<fpsent *> players;
+    };
+    static vector<scoregroup *> groups;
+    static vector<fpsent *> spectators;
+
+    static inline bool scoregroupcmp(const scoregroup *x, const scoregroup *y)
+    {
+        if(!x->team)
+        {
+            if(y->team) return false;
+        }
+        else if(!y->team) return true;
+        if(x->score > y->score) return true;
+        if(x->score < y->score) return false;
+        if(x->players.length() > y->players.length()) return true;
+        if(x->players.length() < y->players.length()) return false;
+        return x->team && y->team && strcmp(x->team, y->team) < 0;
+    }
+
+    static int groupplayers()
+    {
+        int numgroups = 0;
+        spectators.setsize(0);
+        loopv(players)
+        {
+            fpsent *o = players[i];
+            if(!showconnecting && !o->name[0]) continue;
+            if(o->state==CS_SPECTATOR) { spectators.add(o); continue; }
+            const char *team = m_teammode && o->team[0] ? o->team : NULL;
+            bool found = false;
+            loopj(numgroups)
+            {
+                scoregroup &g = *groups[j];
+                if(team!=g.team && (!team || !g.team || strcmp(team, g.team))) continue;
+                g.players.add(o);
+                found = true;
+            }
+            if(found) continue;
+            if(numgroups>=groups.length()) groups.add(new scoregroup);
+            scoregroup &g = *groups[numgroups++];
+            g.team = team;
+            if(!team) g.score = 0;
+            else if(cmode && cmode->hidefrags()) g.score = cmode->getteamscore(o->team);
+            else { teaminfo *ti = teaminfos.access(team); g.score = ti ? ti->frags : 0; }
+            g.players.setsize(0);
+            g.players.add(o);
+        }
+        loopi(numgroups) groups[i]->players.sort(playersort);
+        spectators.sort(playersort);
+        groups.sort(scoregroupcmp, 0, numgroups);
+        return numgroups;
+    }
+
+    int statuscolor(fpsent *d, int color)
+    {
+        if(d->privilege)
+        {
+            color = d->privilege>=PRIV_ADMIN ? 0xFF8000 : (d->privilege>=PRIV_AUTH ? 0xC040C0 : 0x40FF80);
+            if(d->state==CS_DEAD) color = (color>>1)&0x7F7F7F;
+        }
+        else if(d->state==CS_DEAD) color = 0x606060;
+        return color;
+    }
+
+    void renderscoreboard(g3d_gui &g, bool firstpass)
+    {
+        const ENetAddress *address = connectedpeer();
+        if(showservinfo && address)
+        {
+            string hostname;
+            if(enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0)
+            {
+                if(servinfo[0]) g.titlef("%.25s", 0xFFFF80, NULL, servinfo);
+                else g.titlef("%s:%d", 0xFFFF80, NULL, hostname, address->port);
+            }
+        }
+
+        g.pushlist();
+        g.spring();
+        g.text(server::modename(gamemode), 0xFFFF80);
+        g.separator();
+        const char *mname = getclientmap();
+        g.text(mname[0] ? mname : "[new map]", 0xFFFF80);
+        extern int gamespeed;
+        if(gamespeed != 100) { g.separator(); g.textf("%d.%02dx", 0xFFFF80, NULL, gamespeed/100, gamespeed%100); }
+        if(m_timed && mname[0] && (maplimit >= 0 || intermission))
+        {
+            g.separator();
+            if(intermission) g.text("intermission", 0xFFFF80);
+            else
+            {
+                int secs = max(maplimit-lastmillis+999, 0)/1000, mins = secs/60;
+                secs %= 60;
+                g.pushlist();
+                g.strut(mins >= 10 ? 4.5f : 3.5f);
+                g.textf("%d:%02d", 0xFFFF80, NULL, mins, secs);
+                g.poplist();
+            }
+        }
+        if(ispaused()) { g.separator(); g.text("paused", 0xFFFF80); }
+        g.spring();
+        g.poplist();
+
+        g.separator();
+
+        int numgroups = groupplayers();
+        loopk(numgroups)
+        {
+            if((k%2)==0) g.pushlist(); // horizontal
+
+            scoregroup &sg = *groups[k];
+            int bgcolor = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? 0x3030C0 : 0xC03030) : 0,
+                fgcolor = 0xFFFF80;
+
+            g.pushlist(); // vertical
+            g.pushlist(); // horizontal
+
+            #define loopscoregroup(o, b) \
+                loopv(sg.players) \
+                { \
+                    fpsent *o = sg.players[i]; \
+                    b; \
+                }
+
+            g.pushlist();
+            if(sg.team && m_teammode)
+            {
+                g.pushlist();
+                g.background(bgcolor, numgroups>1 ? 3 : 5);
+                g.strut(1);
+                g.poplist();
+            }
+            g.text("", 0, " ");
+            loopscoregroup(o,
+            {
+                if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1))
+                {
+                    g.pushlist();
+                    g.background(0x808080, numgroups>1 ? 3 : 5);
+                }
+                const playermodelinfo &mdl = getplayermodelinfo(o);
+                const char *icon = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon;
+                g.text("", 0, icon);
+                if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1)) g.poplist();
+            });
+            g.poplist();
+
+            if(sg.team && m_teammode)
+            {
+                g.pushlist(); // vertical
+
+                if(sg.score>=10000) g.textf("%s: WIN", fgcolor, NULL, sg.team);
+                else g.textf("%s: %d", fgcolor, NULL, sg.team, sg.score);
+
+                g.pushlist(); // horizontal
+            }
+
+            if(!cmode || !cmode->hidefrags() || !hidefrags)
+            {
+                g.pushlist();
+                g.strut(6);
+                g.text("frags", fgcolor);
+                loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->frags));
+                g.poplist();
+            }
+
+            if(showdeaths)
+            {
+                g.pushlist();
+                g.strut(6);
+                g.text("deaths", fgcolor);
+                loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->deaths));
+                g.poplist();
+            }
+
+            g.pushlist();
+            g.text("name", fgcolor);
+            g.strut(12);
+            loopscoregroup(o,
+            {
+                g.textf("%s ", statuscolor(o, 0xFFFFDD), NULL, colorname(o));
+            });
+            g.poplist();
+
+            if(multiplayer(false) || demoplayback)
+            {
+                if(showpj || showping) g.space(1);
+
+                if(showpj && showping <= 1)
+                {
+                    g.pushlist();
+                    g.strut(6);
+                    g.text("pj", fgcolor);
+                    loopscoregroup(o,
+                    {
+                        if(o->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
+                        else g.textf("%d", 0xFFFFDD, NULL, o->plag);
+                    });
+                    g.poplist();
+                }
+
+                if(showping > 1)
+                {
+                    g.pushlist();
+                    g.strut(6);
+
+                    g.pushlist();
+                    g.text("ping", fgcolor);
+                    g.space(1);
+                    g.spring();
+                    g.text("pj", fgcolor);
+                    g.poplist();
+
+                    loopscoregroup(o,
+                    {
+                        fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o;
+                        if(!p) p = o;
+                        g.pushlist();
+                        if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
+                        else
+                        {
+                            g.textf("%d", 0xFFFFDD, NULL, p->ping);
+                            g.space(1);
+                            g.spring();
+                            g.textf("%d", 0xFFFFDD, NULL, o->plag);
+                        }
+                        g.poplist();
+
+                    });
+                    g.poplist();
+                }
+                else if(showping)
+                {
+                    g.pushlist();
+                    g.text("ping", fgcolor);
+                    g.strut(6);
+                    loopscoregroup(o,
+                    {
+                        fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o;
+                        if(!p) p = o;
+                        if(!showpj && p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
+                        else g.textf("%d", 0xFFFFDD, NULL, p->ping);
+                    });
+                    g.poplist();
+                }
+            }
+
+            if(showclientnum || player1->privilege>=PRIV_MASTER)
+            {
+                g.space(1);
+                g.pushlist();
+                g.text("cn", fgcolor);
+                loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->clientnum));
+                g.poplist();
+            }
+
+            if(sg.team && m_teammode)
+            {
+                g.poplist(); // horizontal
+                g.poplist(); // vertical
+            }
+
+            g.poplist(); // horizontal
+            g.poplist(); // vertical
+
+            if(k+1<numgroups && (k+1)%2) g.space(2);
+            else g.poplist(); // horizontal
+        }
+
+        if(showspectators && spectators.length())
+        {
+            if(showclientnum || player1->privilege>=PRIV_MASTER)
+            {
+                g.pushlist();
+
+                g.pushlist();
+                g.text("spectator", 0xFFFF80, " ");
+                g.strut(12);
+                loopv(spectators)
+                {
+                    fpsent *o = spectators[i];
+                    if(o==player1 && highlightscore)
+                    {
+                        g.pushlist();
+                        g.background(0x808080, 3);
+                    }
+                    g.text(colorname(o), statuscolor(o, 0xFFFFDD), "spectator");
+                    if(o==player1 && highlightscore) g.poplist();
+                }
+                g.poplist();
+
+                if((multiplayer(false) || demoplayback) && showspectatorping)
+                {
+                    g.space(1);
+                    g.pushlist();
+                    g.text("ping", 0xFFFF80);
+                    g.strut(6);
+                    loopv(spectators)
+                    {
+                        fpsent *o = spectators[i];
+                        fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o;
+                        if(!p) p = o;
+                        if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
+                        else g.textf("%d", 0xFFFFDD, NULL, p->ping);
+                    }
+                    g.poplist();
+                }
+
+                g.space(1);
+                g.pushlist();
+                g.text("cn", 0xFFFF80);
+                loopv(spectators) g.textf("%d", 0xFFFFDD, NULL, spectators[i]->clientnum);
+                g.poplist();
+
+                g.poplist();
+            }
+            else
+            {
+                g.textf("%d spectator%s", 0xFFFF80, " ", spectators.length(), spectators.length()!=1 ? "s" : "");
+                loopv(spectators)
+                {
+                    if((i%3)==0)
+                    {
+                        g.pushlist();
+                        g.text("", 0xFFFFDD, "spectator");
+                    }
+                    fpsent *o = spectators[i];
+                    if(o==player1 && highlightscore)
+                    {
+                        g.pushlist();
+                        g.background(0x808080);
+                    }
+                    g.text(colorname(o), statuscolor(o, 0xFFFFDD));
+                    if(o==player1 && highlightscore) g.poplist();
+                    if(i+1<spectators.length() && (i+1)%3) g.space(1);
+                    else g.poplist();
+                }
+            }
+        }
+    }
+
+    struct scoreboardgui : g3d_callback
+    {
+        bool showing;
+        vec menupos;
+        int menustart;
+
+        scoreboardgui() : showing(false) {}
+
+        void show(bool on)
+        {
+            if(!showing && on)
+            {
+                menupos = menuinfrontofplayer();
+                menustart = starttime();
+            }
+            showing = on;
+        }
+
+        void gui(g3d_gui &g, bool firstpass)
+        {
+            g.start(menustart, 0.03f, NULL, false);
+            renderscoreboard(g, firstpass);
+            g.end();
+        }
+
+        void render()
+        {
+            if(showing) g3d_addgui(this, menupos, (scoreboard2d ? GUI_FORCE_2D : GUI_2D | GUI_FOLLOW) | GUI_BOTTOM);
+        }
+
+    } scoreboard;
+
+    void g3d_gamemenus()
+    {
+        scoreboard.render();
+    }
+
+    VARFN(scoreboard, showscoreboard, 0, 0, 1, scoreboard.show(showscoreboard!=0));
+
+    void showscores(bool on)
+    {
+        showscoreboard = on ? 1 : 0;
+        scoreboard.show(on);
+    }
+    ICOMMAND(showscores, "D", (int *down), showscores(*down!=0));
+
+    VARP(hudscore, 0, 0, 1);
+    FVARP(hudscorescale, 1e-3f, 1.0f, 1e3f);
+    VARP(hudscorealign, -1, 0, 1);
+    FVARP(hudscorex, 0, 0.50f, 1);
+    FVARP(hudscorey, 0, 0.03f, 1);
+    HVARP(hudscoreplayercolour, 0, 0x60A0FF, 0xFFFFFF);
+    HVARP(hudscoreenemycolour, 0, 0xFF4040, 0xFFFFFF);
+    VARP(hudscorealpha, 0, 255, 255);
+    VARP(hudscoresep, 0, 200, 1000);
+
+    void drawhudscore(int w, int h)
+    {
+        int numgroups = groupplayers();
+        if(!numgroups) return;
+
+        scoregroup *g = groups[0];
+        int score = INT_MIN, score2 = INT_MIN;
+        bool best = false;
+        if(m_teammode)
+        {
+            score = g->score;
+            best = isteam(player1->team, g->team);
+            if(numgroups > 1)
+            {
+                if(best) score2 = groups[1]->score;
+                else for(int i = 1; i < groups.length(); ++i) if(isteam(player1->team, groups[i]->team)) { score2 = groups[i]->score; break; }
+                if(score2 == INT_MIN)
+                {
+                    fpsent *p = followingplayer(player1);
+                    if(p->state==CS_SPECTATOR) score2 = groups[1]->score;
+                }
+            }
+        }
+        else
+        {
+            fpsent *p = followingplayer(player1);
+            score = g->players[0]->frags;
+            best = p == g->players[0];
+            if(g->players.length() > 1)
+            {
+                if(best || p->state==CS_SPECTATOR) score2 = g->players[1]->frags;
+                else score2 = p->frags;
+            }
+        }
+        if(score == score2 && !best) best = true;
+
+        score = clamp(score, -999, 9999);
+        defformatstring(buf, "%d", score);
+        int tw = 0, th = 0;
+        text_bounds(buf, tw, th);
+
+        string buf2;
+        int tw2 = 0, th2 = 0;
+        if(score2 > INT_MIN)
+        {
+            score2 = clamp(score2, -999, 9999);
+            formatstring(buf2, "%d", score2);
+            text_bounds(buf2, tw2, th2);
+        }
+
+        int fw = 0, fh = 0;
+        text_bounds("00", fw, fh);
+        fw = max(fw, max(tw, tw2));
+
+        vec2 offset = vec2(hudscorex, hudscorey).mul(vec2(w, h).div(hudscorescale));
+        if(hudscorealign == 1) offset.x -= 2*fw + hudscoresep;
+        else if(hudscorealign == 0) offset.x -= (2*fw + hudscoresep) / 2.0f;
+        vec2 offset2 = offset;
+        offset.x += (fw-tw)/2.0f;
+        offset.y -= th/2.0f;
+        offset2.x += fw + hudscoresep + (fw-tw2)/2.0f;
+        offset2.y -= th2/2.0f;
+
+        pushhudmatrix();
+        hudmatrix.scale(hudscorescale, hudscorescale, 1);
+        flushhudmatrix();
+
+        int color = hudscoreplayercolour, color2 = hudscoreenemycolour;
+        if(!best) swap(color, color2);
+
+        draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, hudscorealpha);
+        if(score2 > INT_MIN) draw_text(buf2, int(offset2.x), int(offset2.y), (color2>>16)&0xFF, (color2>>8)&0xFF, color2&0xFF, hudscorealpha);
+
+        pophudmatrix();
+    }
+}
+
diff --git a/src/fpsgame/server.cpp b/src/fpsgame/server.cpp
new file mode 100644 (file)
index 0000000..fb02c5e
--- /dev/null
@@ -0,0 +1,3806 @@
+#include "game.h"
+
+namespace game
+{
+    void parseoptions(vector<const char *> &args)
+    {
+        loopv(args)
+#ifndef STANDALONE
+            if(!game::clientoption(args[i]))
+#endif
+            if(!server::serveroption(args[i]))
+                conoutf(CON_ERROR, "unknown command-line option: %s", args[i]);
+    }
+
+    const char *gameident() { return "fps"; }
+}
+
+VAR(regenbluearmour, 0, 1, 1);
+
+extern ENetAddress masteraddress;
+
+namespace server
+{
+    struct server_entity            // server side version of "entity" type
+    {
+        int type;
+        int spawntime;
+        bool spawned;
+    };
+
+    static const int DEATHMILLIS = 300;
+
+    struct clientinfo;
+
+    struct gameevent
+    {
+        virtual ~gameevent() {}
+
+        virtual bool flush(clientinfo *ci, int fmillis);
+        virtual void process(clientinfo *ci) {}
+
+        virtual bool keepable() const { return false; }
+    };
+
+    struct timedevent : gameevent
+    {
+        int millis;
+
+        bool flush(clientinfo *ci, int fmillis);
+    };
+
+    struct hitinfo
+    {
+        int target;
+        int lifesequence;
+        int rays;
+        float dist;
+        vec dir;
+    };
+
+    struct shotevent : timedevent
+    {
+        int id, gun;
+        vec from, to;
+        vector<hitinfo> hits;
+
+        void process(clientinfo *ci);
+    };
+
+    struct explodeevent : timedevent
+    {
+        int id, gun;
+        vector<hitinfo> hits;
+
+        bool keepable() const { return true; }
+
+        void process(clientinfo *ci);
+    };
+
+    struct suicideevent : gameevent
+    {
+        void process(clientinfo *ci);
+    };
+
+    struct pickupevent : gameevent
+    {
+        int ent;
+
+        void process(clientinfo *ci);
+    };
+
+    template <int N>
+    struct projectilestate
+    {
+        int projs[N];
+        int numprojs;
+
+        projectilestate() : numprojs(0) {}
+
+        void reset() { numprojs = 0; }
+
+        void add(int val)
+        {
+            if(numprojs>=N) numprojs = 0;
+            projs[numprojs++] = val;
+        }
+
+        bool remove(int val)
+        {
+            loopi(numprojs) if(projs[i]==val)
+            {
+                projs[i] = projs[--numprojs];
+                return true;
+            }
+            return false;
+        }
+    };
+
+    struct gamestate : fpsstate
+    {
+        vec o;
+        int state, editstate;
+        int lastdeath, deadflush, lastspawn, lifesequence;
+        int lastshot;
+        projectilestate<8> rockets, grenades;
+        int frags, flags, deaths, teamkills, shotdamage, damage, tokens;
+        int lasttimeplayed, timeplayed;
+        float effectiveness;
+
+        gamestate() : state(CS_DEAD), editstate(CS_DEAD), lifesequence(0) {}
+
+        bool isalive(int gamemillis)
+        {
+            return state==CS_ALIVE || (state==CS_DEAD && gamemillis - lastdeath <= DEATHMILLIS);
+        }
+
+        bool waitexpired(int gamemillis)
+        {
+            return gamemillis - lastshot >= gunwait;
+        }
+
+        void reset()
+        {
+            if(state!=CS_SPECTATOR) state = editstate = CS_DEAD;
+            maxhealth = 100;
+            rockets.reset();
+            grenades.reset();
+
+            timeplayed = 0;
+            effectiveness = 0;
+            frags = flags = deaths = teamkills = shotdamage = damage = tokens = 0;
+
+            lastdeath = 0;
+
+            respawn();
+        }
+
+        void respawn()
+        {
+            fpsstate::respawn();
+            o = vec(-1e10f, -1e10f, -1e10f);
+            deadflush = 0;
+            lastspawn = -1;
+            lastshot = 0;
+            tokens = 0;
+        }
+
+        void reassign()
+        {
+            respawn();
+            rockets.reset();
+            grenades.reset();
+        }
+    };
+
+    struct savedscore
+    {
+        uint ip;
+        string name;
+        int frags, flags, deaths, teamkills, shotdamage, damage;
+        int timeplayed;
+        float effectiveness;
+
+        void save(gamestate &gs)
+        {
+            frags = gs.frags;
+            flags = gs.flags;
+            deaths = gs.deaths;
+            teamkills = gs.teamkills;
+            shotdamage = gs.shotdamage;
+            damage = gs.damage;
+            timeplayed = gs.timeplayed;
+            effectiveness = gs.effectiveness;
+        }
+
+        void restore(gamestate &gs)
+        {
+            gs.frags = frags;
+            gs.flags = flags;
+            gs.deaths = deaths;
+            gs.teamkills = teamkills;
+            gs.shotdamage = shotdamage;
+            gs.damage = damage;
+            gs.timeplayed = timeplayed;
+            gs.effectiveness = effectiveness;
+        }
+    };
+
+    extern int gamemillis, nextexceeded;
+
+    struct clientinfo
+    {
+        int clientnum, ownernum, connectmillis, sessionid, overflow;
+        string name, team, mapvote;
+        int playermodel;
+        int modevote;
+        int privilege;
+        bool connected, local, timesync;
+        int gameoffset, lastevent, pushed, exceeded;
+        gamestate state;
+        vector<gameevent *> events;
+        vector<uchar> position, messages;
+        uchar *wsdata;
+        int wslen;
+        vector<clientinfo *> bots;
+        int ping, aireinit;
+        string clientmap;
+        int mapcrc;
+        bool warned, gameclip;
+        ENetPacket *getdemo, *getmap, *clipboard;
+        int lastclipboard, needclipboard;
+        int connectauth;
+        uint authreq;
+        string authname, authdesc;
+        void *authchallenge;
+        int authkickvictim;
+        char *authkickreason;
+
+        clientinfo() : getdemo(NULL), getmap(NULL), clipboard(NULL), authchallenge(NULL), authkickreason(NULL) { reset(); }
+        ~clientinfo() { events.deletecontents(); cleanclipboard(); cleanauth(); }
+
+        void addevent(gameevent *e)
+        {
+            if(state.state==CS_SPECTATOR || events.length()>100) delete e;
+            else events.add(e);
+        }
+
+        enum
+        {
+            PUSHMILLIS = 3000
+        };
+
+        int calcpushrange()
+        {
+            ENetPeer *peer = getclientpeer(ownernum);
+            return PUSHMILLIS + (peer ? peer->roundTripTime + peer->roundTripTimeVariance : ENET_PEER_DEFAULT_ROUND_TRIP_TIME);
+        }
+
+        bool checkpushed(int millis, int range)
+        {
+            return millis >= pushed - range && millis <= pushed + range;
+        }
+
+        void scheduleexceeded()
+        {
+            if(state.state!=CS_ALIVE || !exceeded) return;
+            int range = calcpushrange();
+            if(!nextexceeded || exceeded + range < nextexceeded) nextexceeded = exceeded + range;
+        }
+
+        void setexceeded()
+        {
+            if(state.state==CS_ALIVE && !exceeded && !checkpushed(gamemillis, calcpushrange())) exceeded = gamemillis;
+            scheduleexceeded(); 
+        }
+            
+        void setpushed()
+        {
+            pushed = max(pushed, gamemillis);
+            if(exceeded && checkpushed(exceeded, calcpushrange())) exceeded = 0;
+        }
+        
+        bool checkexceeded()
+        {
+            return state.state==CS_ALIVE && exceeded && gamemillis > exceeded + calcpushrange();
+        }
+
+        void mapchange()
+        {
+            mapvote[0] = 0;
+            modevote = INT_MAX;
+            state.reset();
+            events.deletecontents();
+            overflow = 0;
+            timesync = false;
+            lastevent = 0;
+            exceeded = 0;
+            pushed = 0;
+            clientmap[0] = '\0';
+            mapcrc = 0;
+            warned = false;
+            gameclip = false;
+        }
+
+        void reassign()
+        {
+            state.reassign();
+            events.deletecontents();
+            timesync = false;
+            lastevent = 0;
+        }
+
+        void cleanclipboard(bool fullclean = true)
+        {
+            if(clipboard) { if(--clipboard->referenceCount <= 0) enet_packet_destroy(clipboard); clipboard = NULL; }
+            if(fullclean) lastclipboard = 0;
+        }
+
+        void cleanauthkick()
+        {
+            authkickvictim = -1;
+            DELETEA(authkickreason);
+        }
+
+        void cleanauth(bool full = true)
+        {
+            authreq = 0;
+            if(authchallenge) { freechallenge(authchallenge); authchallenge = NULL; }
+            if(full) cleanauthkick();
+        }
+
+        void reset()
+        {
+            name[0] = team[0] = 0;
+            playermodel = -1;
+            privilege = PRIV_NONE;
+            connected = local = false;
+            connectauth = 0;
+            position.setsize(0);
+            messages.setsize(0);
+            ping = 0;
+            aireinit = 0;
+            needclipboard = 0;
+            cleanclipboard();
+            cleanauth();
+            mapchange();
+        }
+
+        int geteventmillis(int servmillis, int clientmillis)
+        {
+            if(!timesync || (events.empty() && state.waitexpired(servmillis)))
+            {
+                timesync = true;
+                gameoffset = servmillis - clientmillis;
+                return servmillis;
+            }
+            else return gameoffset + clientmillis;
+        }
+    };
+
+    struct ban
+    {
+        int time, expire;
+        uint ip;
+    };
+
+    namespace aiman
+    {
+        extern void removeai(clientinfo *ci);
+        extern void clearai();
+        extern void checkai();
+        extern void reqadd(clientinfo *ci, int skill);
+        extern void reqdel(clientinfo *ci);
+        extern void setbotlimit(clientinfo *ci, int limit);
+        extern void setbotbalance(clientinfo *ci, bool balance);
+        extern void changemap();
+        extern void addclient(clientinfo *ci);
+        extern void changeteam(clientinfo *ci);
+    }
+
+    #define MM_MODE 0xF
+    #define MM_AUTOAPPROVE 0x1000
+    #define MM_PRIVSERV (MM_MODE | MM_AUTOAPPROVE)
+    #define MM_PUBSERV ((1<<MM_OPEN) | (1<<MM_VETO))
+    #define MM_COOPSERV (MM_AUTOAPPROVE | MM_PUBSERV | (1<<MM_LOCKED))
+
+    bool notgotitems = true;        // true when map has changed and waiting for clients to send item
+    int gamemode = 0;
+    int gamemillis = 0, gamelimit = 0, nextexceeded = 0, gamespeed = 100;
+    bool gamepaused = false, shouldstep = true;
+
+    string smapname = "";
+    int interm = 0;
+    enet_uint32 lastsend = 0;
+    int mastermode = MM_OPEN, mastermask = MM_PRIVSERV;
+    stream *mapdata = NULL;
+
+    vector<uint> allowedips;
+    vector<ban> bannedips;
+
+    void addban(uint ip, int expire)
+    {
+        allowedips.removeobj(ip);
+        ban b;
+        b.time = totalmillis;
+        b.expire = totalmillis + expire;
+        b.ip = ip;
+        loopv(bannedips) if(bannedips[i].expire - b.expire > 0) { bannedips.insert(i, b); return; }
+        bannedips.add(b);
+    }
+
+    vector<clientinfo *> connects, clients, bots;
+
+    void kickclients(uint ip, clientinfo *actor = NULL, int priv = PRIV_NONE)
+    {
+        loopvrev(clients)
+        {
+            clientinfo &c = *clients[i];
+            if(c.state.aitype != AI_NONE || c.privilege >= PRIV_ADMIN || c.local) continue;
+            if(actor && ((c.privilege > priv && !actor->local) || c.clientnum == actor->clientnum)) continue;
+            if(getclientip(c.clientnum) == ip) disconnect_client(c.clientnum, DISC_KICK);
+        }
+    }
+    struct maprotation
+    {
+        static int exclude;
+        int modes;
+        string map;
+        
+        int calcmodemask() const { return modes&(1<<NUMGAMEMODES) ? modes & ~exclude : modes; }
+        bool hasmode(int mode, int offset = STARTGAMEMODE) const { return (calcmodemask() & (1 << (mode-offset))) != 0; }
+
+        int findmode(int mode) const
+        {
+            if(!hasmode(mode)) loopi(NUMGAMEMODES) if(hasmode(i, 0)) return i+STARTGAMEMODE;
+            return mode;
+        }
+
+        bool match(int reqmode, const char *reqmap) const
+        {
+            return hasmode(reqmode) && (!map[0] || !reqmap[0] || !strcmp(map, reqmap));
+        }
+
+        bool includes(const maprotation &rot) const
+        {
+            return rot.modes == modes ? rot.map[0] && !map[0] : (rot.modes & modes) == rot.modes;
+        }
+    };
+    int maprotation::exclude = 0;
+    vector<maprotation> maprotations;
+    int curmaprotation = 0;
+
+    VAR(lockmaprotation, 0, 0, 2);
+
+    void maprotationreset()
+    {
+        maprotations.setsize(0);
+        curmaprotation = 0;
+        maprotation::exclude = 0;
+    }
+
+    void nextmaprotation()
+    {
+        curmaprotation++;
+        if(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes) return;
+        do curmaprotation--;
+        while(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes);
+        curmaprotation++;
+    }
+
+    int findmaprotation(int mode, const char *map)
+    {
+        for(int i = max(curmaprotation, 0); i < maprotations.length(); i++)
+        {
+            maprotation &rot = maprotations[i];
+            if(!rot.modes) break;
+            if(rot.match(mode, map)) return i;
+        }
+        int start;
+        for(start = max(curmaprotation, 0) - 1; start >= 0; start--) if(!maprotations[start].modes) break;
+        start++;
+        for(int i = start; i < curmaprotation; i++)
+        {
+            maprotation &rot = maprotations[i];
+            if(!rot.modes) break;
+            if(rot.match(mode, map)) return i;
+        }
+        int best = -1;
+        loopv(maprotations)
+        {
+            maprotation &rot = maprotations[i];
+            if(rot.match(mode, map) && (best < 0 || maprotations[best].includes(rot))) best = i;
+        }
+        return best;
+    }
+
+    bool searchmodename(const char *haystack, const char *needle)
+    {
+        if(!needle[0]) return true;
+        do
+        {
+            if(needle[0] != '.')
+            {
+                haystack = strchr(haystack, needle[0]);
+                if(!haystack) break;
+                haystack++;
+            }
+            const char *h = haystack, *n = needle+1;
+            for(; *h && *n; h++)
+            {
+                if(*h == *n) n++;
+                else if(*h != ' ') break; 
+            }
+            if(!*n) return true;
+            if(*n == '.') return !*h;
+        } while(needle[0] != '.');
+        return false;
+    }
+
+    int genmodemask(vector<char *> &modes)
+    {
+        int modemask = 0;
+        loopv(modes)
+        {
+            const char *mode = modes[i];
+            int op = mode[0];
+            switch(mode[0])
+            {
+                case '*':
+                    modemask |= 1<<NUMGAMEMODES;
+                    loopk(NUMGAMEMODES) if(m_checknot(k+STARTGAMEMODE, M_DEMO|M_EDIT|M_LOCAL)) modemask |= 1<<k;
+                    continue;
+                case '!':
+                    mode++;
+                    if(mode[0] != '?') break;
+                case '?':
+                    mode++;
+                    loopk(NUMGAMEMODES) if(searchmodename(gamemodes[k].name, mode))
+                    {
+                        if(op == '!') modemask &= ~(1<<k);
+                        else modemask |= 1<<k;
+                    }
+                    continue;
+            }
+            int modenum = INT_MAX;
+            if(isdigit(mode[0])) modenum = atoi(mode);
+            else loopk(NUMGAMEMODES) if(searchmodename(gamemodes[k].name, mode)) { modenum = k+STARTGAMEMODE; break; }
+            if(!m_valid(modenum)) continue;
+            switch(op)
+            {
+                case '!': modemask &= ~(1 << (modenum - STARTGAMEMODE)); break;
+                default: modemask |= 1 << (modenum - STARTGAMEMODE); break;
+            }
+        }
+        return modemask;
+    }
+         
+    bool addmaprotation(int modemask, const char *map)
+    {
+        if(!map[0]) loopk(NUMGAMEMODES) if(modemask&(1<<k) && !m_check(k+STARTGAMEMODE, M_EDIT)) modemask &= ~(1<<k);
+        if(!modemask) return false;
+        if(!(modemask&(1<<NUMGAMEMODES))) maprotation::exclude |= modemask;
+        maprotation &rot = maprotations.add();
+        rot.modes = modemask;
+        copystring(rot.map, map);
+        return true;
+    }
+        
+    void addmaprotations(tagval *args, int numargs)
+    {
+        vector<char *> modes, maps;
+        for(int i = 0; i + 1 < numargs; i += 2)
+        {
+            explodelist(args[i].getstr(), modes);
+            explodelist(args[i+1].getstr(), maps);
+            int modemask = genmodemask(modes);
+            if(maps.length()) loopvj(maps) addmaprotation(modemask, maps[j]);
+            else addmaprotation(modemask, "");
+            modes.deletearrays();
+            maps.deletearrays();
+        }
+        if(maprotations.length() && maprotations.last().modes)
+        {
+            maprotation &rot = maprotations.add();
+            rot.modes = 0;
+            rot.map[0] = '\0';
+        }
+    }
+    
+    COMMAND(maprotationreset, "");
+    COMMANDN(maprotation, addmaprotations, "ss2V");
+
+    struct demofile
+    {
+        string info;
+        uchar *data;
+        int len;
+    };
+
+    vector<demofile> demos;
+
+    bool demonextmatch = false;
+    stream *demotmp = NULL, *demorecord = NULL, *demoplayback = NULL;
+    int nextplayback = 0;
+
+    VAR(maxdemos, 0, 5, 25);
+    VAR(maxdemosize, 0, 16, 31);
+    VAR(restrictdemos, 0, 1, 1);
+    VARF(autorecorddemo, 0, 0, 1, demonextmatch = autorecorddemo!=0);
+
+    VAR(restrictpausegame, 0, 1, 1);
+    VAR(restrictgamespeed, 0, 1, 1);
+
+    SVAR(serverdesc, "");
+    SVAR(serverpass, "");
+    SVAR(adminpass, "");
+    VARF(publicserver, 0, 0, 2, {
+               switch(publicserver)
+               {
+                       case 0: default: mastermask = MM_PRIVSERV; break;
+                       case 1: mastermask = MM_PUBSERV; break;
+                       case 2: mastermask = MM_COOPSERV; break;
+               }
+       });
+    SVAR(servermotd, "");
+
+    struct teamkillkick
+    {
+        int modes, limit, ban;
+
+        bool match(int mode) const
+        {
+            return (modes&(1<<(mode-STARTGAMEMODE)))!=0;
+        }
+
+        bool includes(const teamkillkick &tk) const
+        {
+            return tk.modes != modes && (tk.modes & modes) == tk.modes;
+        }
+    };
+    vector<teamkillkick> teamkillkicks;
+
+    void teamkillkickreset()
+    {
+        teamkillkicks.setsize(0);
+    }
+
+    void addteamkillkick(char *modestr, int *limit, int *ban)
+    {
+        vector<char *> modes;
+        explodelist(modestr, modes);
+        teamkillkick &kick = teamkillkicks.add();
+        kick.modes = genmodemask(modes);
+        kick.limit = *limit;
+        kick.ban = *ban > 0 ? *ban*60000 : (*ban < 0 ? 0 : 30*60000); 
+        modes.deletearrays();
+    }
+
+    COMMAND(teamkillkickreset, "");
+    COMMANDN(teamkillkick, addteamkillkick, "sii");
+
+    struct teamkillinfo
+    {
+        uint ip;
+        int teamkills;
+    };
+    vector<teamkillinfo> teamkills;
+    bool shouldcheckteamkills = false;
+
+    void addteamkill(clientinfo *actor, clientinfo *victim, int n)
+    {
+        if(!m_timed || actor->state.aitype != AI_NONE || actor->local || actor->privilege || (victim && victim->state.aitype != AI_NONE)) return;
+        shouldcheckteamkills = true;
+        uint ip = getclientip(actor->clientnum);
+        loopv(teamkills) if(teamkills[i].ip == ip) 
+        { 
+            teamkills[i].teamkills += n;
+            return;
+        }
+        teamkillinfo &tk = teamkills.add();
+        tk.ip = ip;
+        tk.teamkills = n;
+    }
+
+    void checkteamkills()
+    {
+        teamkillkick *kick = NULL;
+        if(m_timed) loopv(teamkillkicks) if(teamkillkicks[i].match(gamemode) && (!kick || kick->includes(teamkillkicks[i])))
+            kick = &teamkillkicks[i];
+        if(kick) loopvrev(teamkills)
+        {
+            teamkillinfo &tk = teamkills[i];
+            if(tk.teamkills >= kick->limit)
+            {
+                if(kick->ban > 0) addban(tk.ip, kick->ban);
+                kickclients(tk.ip);
+                teamkills.removeunordered(i);
+            }
+        }
+        shouldcheckteamkills = false;
+    }
+
+    void *newclientinfo() { return new clientinfo; }
+    void deleteclientinfo(void *ci) { delete (clientinfo *)ci; }
+
+    clientinfo *getinfo(int n)
+    {
+        if(n < MAXCLIENTS) return (clientinfo *)getclientinfo(n);
+        n -= MAXCLIENTS;
+        return bots.inrange(n) ? bots[n] : NULL;
+    }
+
+    uint mcrc = 0;
+    vector<entity> ments;
+    vector<server_entity> sents;
+    vector<savedscore> scores;
+
+    int msgsizelookup(int msg)
+    {
+        static int sizetable[NUMMSG] = { -1 };
+        if(sizetable[0] < 0)
+        {
+            memset(sizetable, -1, sizeof(sizetable));
+            for(const int *p = msgsizes; *p >= 0; p += 2) sizetable[p[0]] = p[1];
+        }
+        return msg >= 0 && msg < NUMMSG ? sizetable[msg] : -1;
+    }
+
+    const char *modename(int n, const char *unknown)
+    {
+        if(m_valid(n)) return gamemodes[n - STARTGAMEMODE].name;
+        return unknown;
+    }
+
+    const char *mastermodename(int n, const char *unknown)
+    {
+        return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodenames)/sizeof(mastermodenames[0])) ? mastermodenames[n-MM_START] : unknown;
+    }
+
+    const char *privname(int type)
+    {
+        switch(type)
+        {
+            case PRIV_ADMIN: return "admin";
+            case PRIV_AUTH: return "auth";
+            case PRIV_MASTER: return "master";
+            default: return "unknown";
+        }
+    }
+
+    void sendservmsg(const char *s) { sendf(-1, 1, "ris", N_SERVMSG, s); }
+    void sendservmsgf(const char *fmt, ...)
+    {
+         defvformatstring(s, fmt, fmt);
+         sendf(-1, 1, "ris", N_SERVMSG, s);
+    }
+
+    void resetitems()
+    {
+        mcrc = 0;
+        ments.setsize(0);
+        sents.setsize(0);
+        //cps.reset();
+    }
+
+    bool serveroption(const char *arg)
+    {
+        if(arg[0]=='-') switch(arg[1])
+        {
+            case 'n': setsvar("serverdesc", &arg[2]); return true;
+            case 'y': setsvar("serverpass", &arg[2]); return true;
+            case 'p': setsvar("adminpass", &arg[2]); return true;
+            case 'o': setvar("publicserver", atoi(&arg[2])); return true;
+        }
+        return false;
+    }
+
+    void serverinit()
+    {
+        smapname[0] = '\0';
+        resetitems();
+    }
+
+    int numclients(int exclude = -1, bool nospec = true, bool noai = true, bool priv = false)
+    {
+        int n = 0;
+        loopv(clients) 
+        {
+            clientinfo *ci = clients[i];
+            if(ci->clientnum!=exclude && (!nospec || ci->state.state!=CS_SPECTATOR || (priv && (ci->privilege || ci->local))) && (!noai || ci->state.aitype == AI_NONE)) n++;
+        }
+        return n;
+    }
+
+    bool duplicatename(clientinfo *ci, const char *name)
+    {
+        if(!name) name = ci->name;
+        loopv(clients) if(clients[i]!=ci && !strcmp(name, clients[i]->name)) return true;
+        return false;
+    }
+
+    const char *colorname(clientinfo *ci, const char *name = NULL)
+    {
+        if(!name) name = ci->name;
+        if(name[0] && !duplicatename(ci, name) && ci->state.aitype == AI_NONE) return name;
+        static string cname[3];
+        static int cidx = 0;
+        cidx = (cidx+1)%3;
+        formatstring(cname[cidx], ci->state.aitype == AI_NONE ? "%s \fs\f5(%d)\fr" : "%s \fs\f5[%d]\fr", name, ci->clientnum);
+        return cname[cidx];
+    }
+
+    struct servmode
+    {
+        virtual ~servmode() {}
+
+        virtual void entergame(clientinfo *ci) {}
+        virtual void leavegame(clientinfo *ci, bool disconnecting = false) {}
+
+        virtual void moved(clientinfo *ci, const vec &oldpos, bool oldclip, const vec &newpos, bool newclip) {}
+        virtual bool canspawn(clientinfo *ci, bool connecting = false) { return true; }
+        virtual void spawned(clientinfo *ci) {}
+        virtual int fragvalue(clientinfo *victim, clientinfo *actor)
+        {
+            if(victim==actor || isteam(victim->team, actor->team)) return -1;
+            return 1;
+        }
+        virtual void died(clientinfo *victim, clientinfo *actor) {}
+        virtual bool canchangeteam(clientinfo *ci, const char *oldteam, const char *newteam) { return true; }
+        virtual void changeteam(clientinfo *ci, const char *oldteam, const char *newteam) {}
+        virtual void initclient(clientinfo *ci, packetbuf &p, bool connecting) {}
+        virtual void update() {}
+        virtual void cleanup() {}
+        virtual void setup() {}
+        virtual void newmap() {}
+        virtual void intermission() {}
+        virtual bool hidefrags() { return false; }
+        virtual int getteamscore(const char *team) { return 0; }
+        virtual void getteamscores(vector<teamscore> &scores) {}
+        virtual bool extinfoteam(const char *team, ucharbuf &p) { return false; }
+    };
+
+    #define SERVMODE 1
+    #include "capture.h"
+    #include "ctf.h"
+    #include "collect.h"
+
+    captureservmode capturemode;
+    ctfservmode ctfmode;
+    collectservmode collectmode;
+    servmode *smode = NULL;
+
+    bool canspawnitem(int type) { return !m_noitems && (type>=I_SHELLS && type<=I_QUAD && (!m_noammo || type<I_SHELLS || type>I_CARTRIDGES)); }
+
+    int spawntime(int type)
+    {
+        if(m_classicsp) return INT_MAX;
+        int np = numclients(-1, true, false);
+        np = np<3 ? 4 : (np>4 ? 2 : 3);         // spawn times are dependent on number of players
+        int sec = 0;
+        switch(type)
+        {
+            case I_SHELLS:
+            case I_BULLETS:
+            case I_ROCKETS:
+            case I_ROUNDS:
+            case I_GRENADES:
+            case I_CARTRIDGES: sec = np*4; break;
+            case I_HEALTH: sec = np*5; break;
+            case I_GREENARMOUR: sec = 20; break;
+            case I_YELLOWARMOUR: sec = 30; break;
+            case I_BOOST: sec = 60; break;
+            case I_QUAD: sec = 70; break;
+        }
+        return sec*1000;
+    }
+
+    bool delayspawn(int type)
+    {
+        switch(type)
+        {
+            case I_GREENARMOUR:
+            case I_YELLOWARMOUR:
+                return !m_classicsp;
+            case I_BOOST:
+            case I_QUAD:
+                return true;
+            default:
+                return false;
+        }
+    }
+    bool pickup(int i, int sender)         // server side item pickup, acknowledge first client that gets it
+    {
+        if((m_timed && gamemillis>=gamelimit) || !sents.inrange(i) || !sents[i].spawned) return false;
+        clientinfo *ci = getinfo(sender);
+        if(!ci) return false;
+        if(!ci->local && !ci->state.canpickup(sents[i].type))
+        {
+            sendf(sender, 1, "ri3", N_ITEMACC, i, -1);
+            return false;
+        }
+        sents[i].spawned = false;
+        sents[i].spawntime = spawntime(sents[i].type);
+        sendf(-1, 1, "ri3", N_ITEMACC, i, sender);
+        ci->state.pickup(sents[i].type);
+        return true;
+    }
+
+    static hashset<teaminfo> teaminfos;
+
+    void clearteaminfo()
+    {
+        teaminfos.clear();
+    }
+
+    bool teamhasplayers(const char *team) { loopv(clients) if(!strcmp(clients[i]->team, team)) return true; return false; }
+
+    bool pruneteaminfo()
+    {
+        int oldteams = teaminfos.numelems;
+        enumerate(teaminfos, teaminfo, old,
+            if(!old.frags && !teamhasplayers(old.team)) teaminfos.remove(old.team);
+        );
+        return teaminfos.numelems < oldteams;
+    }
+
+    teaminfo *addteaminfo(const char *team)
+    {
+        teaminfo *t = teaminfos.access(team);
+        if(!t)
+        {
+            if(teaminfos.numelems >= MAXTEAMS && !pruneteaminfo()) return NULL;
+            t = &teaminfos[team];
+            copystring(t->team, team, sizeof(t->team));
+            t->frags = 0;
+        }
+        return t;
+    }
+
+    clientinfo *choosebestclient(float &bestrank)
+    {
+        clientinfo *best = NULL;
+        bestrank = -1;
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->state.timeplayed<0) continue;
+            float rank = ci->state.state!=CS_SPECTATOR ? ci->state.effectiveness/max(ci->state.timeplayed, 1) : -1;
+            if(!best || rank > bestrank) { best = ci; bestrank = rank; }
+        }
+        return best;
+    }
+
+    VAR(persistteams, 0, 0, 1);
+
+    void autoteam()
+    {
+        static const char * const teamnames[2] = {"good", "evil"};
+        vector<clientinfo *> team[2];
+        float teamrank[2] = {0, 0};
+        for(int round = 0, remaining = clients.length(); remaining>=0; round++)
+        {
+            int first = round&1, second = (round+1)&1, selected = 0;
+            while(teamrank[first] <= teamrank[second])
+            {
+                float rank;
+                clientinfo *ci = choosebestclient(rank);
+                if(!ci) break;
+                if(smode && smode->hidefrags()) rank = 1;
+                else if(selected && rank<=0) break;
+                ci->state.timeplayed = -1;
+                team[first].add(ci);
+                if(rank>0) teamrank[first] += rank;
+                selected++;
+                if(rank<=0) break;
+            }
+            if(!selected) break;
+            remaining -= selected;
+        }
+        loopi(sizeof(team)/sizeof(team[0]))
+        {
+            addteaminfo(teamnames[i]);
+            loopvj(team[i])
+            {
+                clientinfo *ci = team[i][j];
+                if(!strcmp(ci->team, teamnames[i])) continue;
+                if(persistteams && ci->team[0] && (!smode || smode->canchangeteam(ci, teamnames[i], ci->team)))
+                {
+                    addteaminfo(ci->team);
+                    continue;
+                }
+                copystring(ci->team, teamnames[i], MAXTEAMLEN+1);
+                sendf(-1, 1, "riisi", N_SETTEAM, ci->clientnum, teamnames[i], -1);
+            }
+        }
+    }
+
+    struct teamrank
+    {
+        const char *name;
+        float rank;
+        int clients;
+
+        teamrank(const char *name) : name(name), rank(0), clients(0) {}
+    };
+
+    const char *chooseworstteam(const char *suggest = NULL, clientinfo *exclude = NULL)
+    {
+        teamrank teamranks[2] = { teamrank("good"), teamrank("evil") };
+        const int numteams = sizeof(teamranks)/sizeof(teamranks[0]);
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci==exclude || ci->state.aitype!=AI_NONE || ci->state.state==CS_SPECTATOR || !ci->team[0]) continue;
+            ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed;
+            ci->state.lasttimeplayed = lastmillis;
+
+            loopj(numteams) if(!strcmp(ci->team, teamranks[j].name))
+            {
+                teamrank &ts = teamranks[j];
+                ts.rank += ci->state.effectiveness/max(ci->state.timeplayed, 1);
+                ts.clients++;
+                break;
+            }
+        }
+        teamrank *worst = &teamranks[numteams-1];
+        loopi(numteams-1)
+        {
+            teamrank &ts = teamranks[i];
+            if(smode && smode->hidefrags())
+            {
+                if(ts.clients < worst->clients || (ts.clients == worst->clients && ts.rank < worst->rank)) worst = &ts;
+            }
+            else if(ts.rank < worst->rank || (ts.rank == worst->rank && ts.clients < worst->clients)) worst = &ts;
+        }
+        return worst->name;
+    }
+
+    void prunedemos(int extra = 0)
+    {
+        int n = clamp(demos.length() + extra - maxdemos, 0, demos.length());
+        if(n <= 0) return;
+        loopi(n) delete[] demos[i].data;
+        demos.remove(0, n);
+    }
+    void adddemo()
+    {
+        if(!demotmp) return;
+        int len = (int)min(demotmp->size(), stream::offset((maxdemosize<<20) + 0x10000));
+        demofile &d = demos.add();
+        time_t t = time(NULL);
+        char *timestr = ctime(&t), *trim = timestr + strlen(timestr);
+        while(trim>timestr && iscubespace(*--trim)) *trim = '\0';
+        formatstring(d.info, "%s: %s, %s, %.2f%s", timestr, modename(gamemode), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB");
+        sendservmsgf("demo \"%s\" recorded", d.info);
+        d.data = new uchar[len];
+        d.len = len;
+        demotmp->seek(0, SEEK_SET);
+        demotmp->read(d.data, len);
+        DELETEP(demotmp);
+    }
+        
+    void enddemorecord()
+    {
+        if(!demorecord) return;
+
+        DELETEP(demorecord);
+
+        if(!demotmp) return;
+        if(!maxdemos || !maxdemosize) { DELETEP(demotmp); return; }
+
+        prunedemos(1);
+        adddemo();
+    }
+
+    void writedemo(int chan, void *data, int len)
+    {
+        if(!demorecord) return;
+        int stamp[3] = { gamemillis, chan, len };
+        lilswap(stamp, 3);
+        demorecord->write(stamp, sizeof(stamp));
+        demorecord->write(data, len);
+        if(demorecord->rawtell() >= (maxdemosize<<20)) enddemorecord();
+    }
+
+    void recordpacket(int chan, void *data, int len)
+    {
+        writedemo(chan, data, len);
+    }
+
+    int welcomepacket(packetbuf &p, clientinfo *ci);
+    void sendwelcome(clientinfo *ci);
+
+    void setupdemorecord()
+    {
+        if(!m_mp(gamemode) || m_edit) return;
+
+        demotmp = opentempfile("demorecord", "w+b");
+        if(!demotmp) return;
+
+        stream *f = opengzfile(NULL, "wb", demotmp);
+        if(!f) { DELETEP(demotmp); return; }
+
+        sendservmsg("recording demo");
+
+        demorecord = f;
+
+        demoheader hdr;
+        memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic));
+        hdr.version = DEMO_VERSION;
+        hdr.protocol = PROTOCOL_VERSION;
+        lilswap(&hdr.version, 2);
+        demorecord->write(&hdr, sizeof(demoheader));
+
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        welcomepacket(p, NULL);
+        writedemo(1, p.buf, p.len);
+    }
+
+    void listdemos(int cn)
+    {
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        putint(p, N_SENDDEMOLIST);
+        putint(p, demos.length());
+        loopv(demos) sendstring(demos[i].info, p);
+        sendpacket(cn, 1, p.finalize());
+    }
+
+    void cleardemos(int n)
+    {
+        if(!n)
+        {
+            loopv(demos) delete[] demos[i].data;
+            demos.shrink(0);
+            sendservmsg("cleared all demos");
+        }
+        else if(demos.inrange(n-1))
+        {
+            delete[] demos[n-1].data;
+            demos.remove(n-1);
+            sendservmsgf("cleared demo %d", n);
+        }
+    }
+
+    static void freegetmap(ENetPacket *packet)
+    {
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->getmap == packet) ci->getmap = NULL;
+        }
+    }
+
+    static void freegetdemo(ENetPacket *packet)
+    {
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->getdemo == packet) ci->getdemo = NULL;
+        }
+    }
+
+    void senddemo(clientinfo *ci, int num, int tag)
+    {
+        if(ci->getdemo) return;
+        if(!num) num = demos.length();
+        if(!demos.inrange(num-1)) return;
+        demofile &d = demos[num-1];
+        if((ci->getdemo = sendf(ci->clientnum, 2, "riim", N_SENDDEMO, tag, d.len, d.data)))
+            ci->getdemo->freeCallback = freegetdemo;
+    }
+
+    void enddemoplayback()
+    {
+        if(!demoplayback) return;
+        DELETEP(demoplayback);
+
+        loopv(clients) sendf(clients[i]->clientnum, 1, "ri3", N_DEMOPLAYBACK, 0, clients[i]->clientnum);
+
+        sendservmsg("demo playback finished");
+
+        loopv(clients) sendwelcome(clients[i]);
+    }
+
+    SVARP(demodir, "demo");
+
+    const char *getdemofile(const char *file, bool init)
+    {
+        if(!demodir[0]) return NULL;
+        static string buf;
+        copystring(buf, demodir);
+        int dirlen = strlen(buf);
+        if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; }
+        if(init)
+        {
+            const char *dir = findfile(buf, "w");
+            if(!fileexists(dir, "w")) createdir(dir);
+        }
+        concatstring(buf, file);
+        return buf;
+    }
+
+    void setupdemoplayback()
+    {
+        if(demoplayback) return;
+        demoheader hdr;
+        string msg;
+        msg[0] = '\0';
+        string file;
+        copystring(file, smapname);
+        int len = strlen(file);
+        if(len < 4 || strcasecmp(&file[len-4], ".dmo")) concatstring(file, ".dmo");
+        if(const char *buf = getdemofile(file, false)) demoplayback = opengzfile(buf, "rb");
+        if(!demoplayback) demoplayback = opengzfile(file, "rb");
+        if(!demoplayback) formatstring(msg, "could not read demo \"%s\"", file);
+        else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic)))
+            formatstring(msg, "\"%s\" is not a demo file", file);
+        else
+        {
+            lilswap(&hdr.version, 2);
+            if(hdr.version!=DEMO_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.version<DEMO_VERSION ? "older" : "newer");
+            else if(hdr.protocol!=PROTOCOL_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.protocol<PROTOCOL_VERSION ? "older" : "newer");
+        }
+        if(msg[0])
+        {
+            DELETEP(demoplayback);
+            sendservmsg(msg);
+            return;
+        }
+
+        sendservmsgf("playing demo \"%s\"", file);
+
+        sendf(-1, 1, "ri3", N_DEMOPLAYBACK, 1, -1);
+
+        if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback))
+        {
+            enddemoplayback();
+            return;
+        }
+        lilswap(&nextplayback, 1);
+    }
+
+    void readdemo()
+    {
+        if(!demoplayback) return;
+        while(gamemillis>=nextplayback)
+        {
+            int chan, len;
+            if(demoplayback->read(&chan, sizeof(chan))!=sizeof(chan) ||
+               demoplayback->read(&len, sizeof(len))!=sizeof(len))
+            {
+                enddemoplayback();
+                return;
+            }
+            lilswap(&chan, 1);
+            lilswap(&len, 1);
+            ENetPacket *packet = enet_packet_create(NULL, len+1, 0);
+            if(!packet || demoplayback->read(packet->data+1, len)!=size_t(len))
+            {
+                if(packet) enet_packet_destroy(packet);
+                enddemoplayback();
+                return;
+            }
+            packet->data[0] = N_DEMOPACKET;
+            sendpacket(-1, chan, packet);
+            if(!packet->referenceCount) enet_packet_destroy(packet);
+            if(!demoplayback) break;
+            if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback))
+            {
+                enddemoplayback();
+                return;
+            }
+            lilswap(&nextplayback, 1);
+        }
+    }
+
+    void timeupdate(int secs)
+    {
+        if(!demoplayback) return;
+        if(secs <= 0) interm = -1;
+        else gamelimit = max(gamelimit, nextplayback + secs*1000);
+    }
+
+    void seekdemo(char *t)
+    {
+        if(!demoplayback) return;
+        bool rev = *t == '-';
+        if(rev) t++;
+        int mins = strtoul(t, &t, 10), secs = 0, millis = 0;
+        if(*t == ':') secs = strtoul(t+1, &t, 10);
+        else { secs = mins; mins = 0; }
+        if(*t == '.') millis = strtoul(t+1, &t, 10);
+        int offset = max(millis + (mins*60 + secs)*1000, 0), prevmillis = gamemillis;
+        if(rev) while(gamelimit - offset > gamemillis)
+        {
+            gamemillis = gamelimit - offset;
+            readdemo();
+        }
+        else if(offset > gamemillis)
+        {
+            gamemillis = offset;
+            readdemo();
+        }
+        if(gamemillis > prevmillis)
+        {
+            if(!interm) sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1));
+#ifndef STANDALONE
+            cleardamagescreen();
+#endif
+        }
+    }
+
+    ICOMMAND(seekdemo, "sN$", (char *t, int *numargs, ident *id),
+    {
+        if(*numargs > 0) seekdemo(t);
+        else
+        {
+            int secs = gamemillis/1000;
+            defformatstring(str, "%d:%02d.%03d", secs/60, secs%60, gamemillis%1000);
+            if(*numargs < 0) result(str);
+            else printsvar(id, str);
+        }
+    });
+
+    void stopdemo()
+    {
+        if(m_demo) enddemoplayback();
+        else enddemorecord();
+    }
+
+    void pausegame(bool val, clientinfo *ci = NULL)
+    {
+        if(gamepaused==val) return;
+        gamepaused = val;
+        sendf(-1, 1, "riii", N_PAUSEGAME, gamepaused ? 1 : 0, ci ? ci->clientnum : -1);
+    }
+
+    void checkpausegame()
+    {
+        if(!gamepaused) return;
+        int admins = 0;
+        loopv(clients) if(clients[i]->privilege >= (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) || clients[i]->local) admins++;
+        if(!admins) pausegame(false);
+    }
+
+    void forcepaused(bool paused)
+    {
+        pausegame(paused);
+    }
+
+    bool ispaused() { return gamepaused; }
+
+    void changegamespeed(int val, clientinfo *ci = NULL)
+    {
+        val = clamp(val, 10, 1000);
+        if(gamespeed==val) return;
+        gamespeed = val;
+        sendf(-1, 1, "riii", N_GAMESPEED, gamespeed, ci ? ci->clientnum : -1);
+    }
+
+    void forcegamespeed(int speed)
+    {
+        changegamespeed(speed);
+    }
+
+    int scaletime(int t) { return t*gamespeed; }
+
+    SVAR(serverauth, "");
+
+    struct userkey
+    {
+        char *name;
+        char *desc;
+        
+        userkey() : name(NULL), desc(NULL) {}
+        userkey(char *name, char *desc) : name(name), desc(desc) {}
+    };
+
+    static inline uint hthash(const userkey &k) { return ::hthash(k.name); }
+    static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); }
+
+    struct userinfo : userkey
+    {
+        void *pubkey;
+        int privilege;
+
+        userinfo() : pubkey(NULL), privilege(PRIV_NONE) {}
+        ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); }
+    };
+    hashset<userinfo> users;
+
+    void adduser(char *name, char *desc, char *pubkey, char *priv)
+    {
+        userkey key(name, desc);
+        userinfo &u = users[key];
+        if(u.pubkey) { freepubkey(u.pubkey); u.pubkey = NULL; }
+        if(!u.name) u.name = newstring(name);
+        if(!u.desc) u.desc = newstring(desc);
+        u.pubkey = parsepubkey(pubkey);
+        switch(priv[0])
+        {
+            case 'a': case 'A': u.privilege = PRIV_ADMIN; break;
+            case 'm': case 'M': default: u.privilege = PRIV_AUTH; break;
+            case 'n': case 'N': u.privilege = PRIV_NONE; break;
+        }
+    }
+    COMMAND(adduser, "ssss");
+
+    void clearusers()
+    {
+        users.clear();
+    }
+    COMMAND(clearusers, "");
+
+    void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen)
+    {
+        char buf[2*sizeof(string)];
+        formatstring(buf, "%d %d ", cn, sessionid);
+        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(hash));
+        return !strcmp(hash, given);
+    }
+
+    void revokemaster(clientinfo *ci)
+    {
+        ci->privilege = PRIV_NONE;
+        if(ci->state.state==CS_SPECTATOR && !ci->local) aiman::removeai(ci);
+    }
+
+    extern void connected(clientinfo *ci);
+
+    bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false)
+    {
+        if(authname && !val) return false;
+        const char *name = "";
+        if(val)
+        {
+            bool haspass = adminpass[0] && checkpassword(ci, adminpass, pass);
+            int wantpriv = ci->local || haspass ? PRIV_ADMIN : authpriv;
+            if(wantpriv <= ci->privilege) return true;
+            else if(wantpriv <= PRIV_MASTER && !force)
+            {
+                if(ci->state.state==CS_SPECTATOR) 
+                {
+                    sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Spectators may not claim master.");
+                    return false;
+                }
+                loopv(clients) if(ci!=clients[i] && clients[i]->privilege)
+                {
+                    sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Master is already claimed.");
+                    return false;
+                }
+                if(!authname && !(mastermask&MM_AUTOAPPROVE) && !ci->privilege && !ci->local)
+                {
+                    sendf(ci->clientnum, 1, "ris", N_SERVMSG, "This server requires you to use the \"/auth\" command to claim master.");
+                    return false;
+                }
+            }
+            if(trial) return true;
+            ci->privilege = wantpriv;
+            name = privname(ci->privilege);
+        }
+        else
+        {
+            if(!ci->privilege) return false;
+            if(trial) return true;
+            name = privname(ci->privilege);
+            revokemaster(ci);
+        }
+        bool hasmaster = false;
+        loopv(clients) if(clients[i]->local || clients[i]->privilege >= PRIV_MASTER) hasmaster = true;
+        if(!hasmaster)
+        {
+            mastermode = MM_OPEN;
+            allowedips.shrink(0);
+        }
+        string msg;
+        if(val && authname) 
+        {
+            if(authdesc && authdesc[0]) formatstring(msg, "%s claimed %s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), name, authname, authdesc);
+            else formatstring(msg, "%s claimed %s as '\fs\f5%s\fr'", colorname(ci), name, authname);
+        } 
+        else formatstring(msg, "%s %s %s", colorname(ci), val ? "claimed" : "relinquished", name);
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        putint(p, N_SERVMSG);
+        sendstring(msg, p);
+        putint(p, N_CURRENTMASTER);
+        putint(p, mastermode);
+        loopv(clients) if(clients[i]->privilege >= PRIV_MASTER)
+        {
+            putint(p, clients[i]->clientnum);
+            putint(p, clients[i]->privilege);
+        }
+        putint(p, -1);
+        sendpacket(-1, 1, p.finalize());
+        checkpausegame();
+        return true;
+    }
+
+    bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false)
+    {
+        int priv = ci->privilege;
+        if(authname)
+        {
+            if(priv >= authpriv || ci->local) authname = authdesc = NULL;
+            else priv = authpriv;
+        }
+        if((priv || ci->local) && ci->clientnum!=victim)
+        {
+            clientinfo *vinfo = (clientinfo *)getclientinfo(victim);
+            if(vinfo && vinfo->connected && (priv >= vinfo->privilege || ci->local) && vinfo->privilege < PRIV_ADMIN && !vinfo->local)
+            {
+                if(trial) return true;
+                string kicker;
+                if(authname)
+                {
+                    if(authdesc && authdesc[0]) formatstring(kicker, "%s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), authname, authdesc);
+                    else formatstring(kicker, "%s as '\fs\f5%s\fr'", colorname(ci), authname);
+                }
+                else copystring(kicker, colorname(ci));
+                if(reason && reason[0]) sendservmsgf("%s kicked %s because: %s", kicker, colorname(vinfo), reason);
+                else sendservmsgf("%s kicked %s", kicker, colorname(vinfo));
+                uint ip = getclientip(victim);
+                addban(ip, 4*60*60000);
+                kickclients(ip, ci, priv);
+            }
+        }
+        return false;
+    }
+
+    savedscore *findscore(clientinfo *ci, bool insert)
+    {
+        uint ip = getclientip(ci->clientnum);
+        if(!ip && !ci->local) return 0;
+        if(!insert)
+        {
+            loopv(clients)
+            {
+                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;
+                }
+            }
+        }
+        loopv(scores)
+        {
+            savedscore &sc = scores[i];
+            if(sc.ip == ip && !strcmp(sc.name, ci->name)) return &sc;
+        }
+        if(!insert) return 0;
+        savedscore &sc = scores.add();
+        sc.ip = ip;
+        copystring(sc.name, ci->name);
+        return &sc;
+    }
+
+    void savescore(clientinfo *ci)
+    {
+        savedscore *sc = findscore(ci, true);
+        if(sc) sc->save(ci->state);
+    }
+
+    static struct msgfilter
+    {
+        uchar msgmask[NUMMSG];
+
+        msgfilter(int msg, ...)
+        {
+            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);
+        }
+
+        uchar operator[](int msg) const { return msg >= 0 && msg < NUMMSG ? msgmask[msg] : 0; }
+    } msgfilter(-1, N_CONNECT, N_SERVINFO, N_INITCLIENT, N_WELCOME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, N_DIED, N_SPAWNSTATE, N_FORCEDEATH, N_TEAMINFO, N_ITEMACC, N_ITEMSPAWN, N_TIMEUP, N_CDIS, N_CURRENTMASTER, N_PONG, N_RESUME, N_BASESCORE, N_BASEINFO, N_BASEREGEN, N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_SENDMAP, N_DROPFLAG, N_SCOREFLAG, N_RETURNFLAG, N_RESETFLAG, N_INVISFLAG, N_CLIENT, N_AUTHCHAL, N_INITAI, N_EXPIRETOKENS, N_DROPTOKENS, N_STEALTOKENS, N_DEMOPACKET, -2, N_REMIP, N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, -3, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, N_DELCUBE, N_EDITVAR, N_EDITVSLOT, N_UNDO, N_REDO, -4, N_POS, NUMMSG),
+      connectfilter(-1, N_CONNECT, -2, N_AUTHANS, -3, N_PING, NUMMSG);
+
+    int checktype(int type, clientinfo *ci)
+    {
+        if(ci)
+        {
+            if(!ci->connected) switch(connectfilter[type])
+            {
+                // 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) break; return -1;
+            // only allowed in coop-edit, no overflow check
+            case 3: return m_edit ? type : -1;
+            // no overflow check
+            case 4: return type;
+        }
+        if(ci && ++ci->overflow >= 200) return -2;
+        return type;
+    }
+
+    struct worldstate
+    {
+        int uses, len;
+        uchar *data;
+
+        worldstate() : uses(0), len(0), data(NULL) {}
+
+        void setup(int n) { len = n; data = new uchar[n]; }
+        void cleanup() { DELETEA(data); len = 0; }
+        bool contains(const uchar *p) const { return p >= data && p < &data[len]; }
+    };
+    vector<worldstate> worldstates;
+    bool reliablemessages = false;
+
+    void cleanworldstate(ENetPacket *packet)
+    {
+        loopv(worldstates)
+        {
+            worldstate &ws = worldstates[i];
+            if(!ws.contains(packet->data)) continue;
+            ws.uses--;
+            if(ws.uses <= 0)
+            {
+                ws.cleanup();
+                worldstates.removeunordered(i);
+            }
+            break;
+        }
+    }
+
+    void flushclientposition(clientinfo &ci)
+    {
+        if(ci.position.empty() || (!hasnonlocalclients() && !demorecord)) return;
+        packetbuf p(ci.position.length(), 0);
+        p.put(ci.position.getbuf(), ci.position.length());
+        ci.position.setsize(0);
+        sendpacket(-1, 0, p.finalize(), ci.ownernum);
+    }
+
+    static void sendpositions(worldstate &ws, ucharbuf &wsbuf)
+    {
+        if(wsbuf.empty()) return;
+        int wslen = wsbuf.length();
+        recordpacket(0, wsbuf.buf, wslen);
+        wsbuf.put(wsbuf.buf, wslen);
+        loopv(clients)
+        {
+            clientinfo &ci = *clients[i];
+            if(ci.state.aitype != AI_NONE) continue;
+            uchar *data = wsbuf.buf;
+            int size = wslen;
+            if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; }
+            if(size <= 0) continue;
+            ENetPacket *packet = enet_packet_create(data, size, ENET_PACKET_FLAG_NO_ALLOCATE);
+            sendpacket(ci.clientnum, 0, packet);
+            if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; }
+            else enet_packet_destroy(packet);
+        }
+        wsbuf.offset(wsbuf.length());
+    }
+
+    static inline void addposition(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci)
+    {
+        if(bi.position.empty()) return;
+        if(wsbuf.length() + bi.position.length() > mtu) sendpositions(ws, wsbuf);
+        int offset = wsbuf.length();
+        wsbuf.put(bi.position.getbuf(), bi.position.length());
+        bi.position.setsize(0);
+        int len = wsbuf.length() - offset;
+        if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; }
+        else ci.wslen += len;
+    }
+
+    static void sendmessages(worldstate &ws, ucharbuf &wsbuf)
+    {
+        if(wsbuf.empty()) return;
+        int wslen = wsbuf.length();
+        recordpacket(1, wsbuf.buf, wslen);
+        wsbuf.put(wsbuf.buf, wslen);
+        loopv(clients)
+        {
+            clientinfo &ci = *clients[i];
+            if(ci.state.aitype != AI_NONE) continue;
+            uchar *data = wsbuf.buf;
+            int size = wslen;
+            if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; }
+            if(size <= 0) continue;
+            ENetPacket *packet = enet_packet_create(data, size, (reliablemessages ? ENET_PACKET_FLAG_RELIABLE : 0) | ENET_PACKET_FLAG_NO_ALLOCATE);
+            sendpacket(ci.clientnum, 1, packet);
+            if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; }
+            else enet_packet_destroy(packet);
+        }
+        wsbuf.offset(wsbuf.length());
+    }
+
+    static inline void addmessages(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci)
+    {
+        if(bi.messages.empty()) return;
+        if(wsbuf.length() + 10 + bi.messages.length() > mtu) sendmessages(ws, wsbuf);
+        int offset = wsbuf.length();
+        putint(wsbuf, N_CLIENT);
+        putint(wsbuf, bi.clientnum);
+        putuint(wsbuf, bi.messages.length());
+        wsbuf.put(bi.messages.getbuf(), bi.messages.length());
+        bi.messages.setsize(0);
+        int len = wsbuf.length() - offset;
+        if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; }
+        else ci.wslen += len;
+    }
+
+    bool buildworldstate()
+    {
+        int wsmax = 0;
+        loopv(clients)
+        {
+            clientinfo &ci = *clients[i];
+            ci.overflow = 0;
+            ci.wsdata = NULL;
+            wsmax += ci.position.length();
+            if(ci.messages.length()) wsmax += 10 + ci.messages.length();
+        }
+        if(wsmax <= 0)
+        {
+            reliablemessages = false;
+            return false;
+        }
+        worldstate &ws = worldstates.add();
+        ws.setup(2*wsmax);
+        int mtu = getservermtu() - 100;
+        if(mtu <= 0) mtu = ws.len;
+        ucharbuf wsbuf(ws.data, ws.len);
+        loopv(clients)
+        {
+            clientinfo &ci = *clients[i];
+            if(ci.state.aitype != AI_NONE) continue;
+            addposition(ws, wsbuf, mtu, ci, ci);
+            loopvj(ci.bots) addposition(ws, wsbuf, mtu, *ci.bots[j], ci);
+        }
+        sendpositions(ws, wsbuf);
+        loopv(clients)
+        {
+            clientinfo &ci = *clients[i];
+            if(ci.state.aitype != AI_NONE) continue;
+            addmessages(ws, wsbuf, mtu, ci, ci);
+            loopvj(ci.bots) addmessages(ws, wsbuf, mtu, *ci.bots[j], ci);
+        }
+        sendmessages(ws, wsbuf);
+        reliablemessages = false;
+        if(ws.uses) return true;
+        ws.cleanup();
+        worldstates.drop();
+        return false;
+    }
+
+    bool sendpackets(bool force)
+    {
+        if(clients.empty() || (!hasnonlocalclients() && !demorecord)) return false;
+        enet_uint32 curtime = enet_time_get()-lastsend;
+        if(curtime<33 && !force) return false;
+        bool flush = buildworldstate();
+        lastsend += curtime - (curtime%33);
+        return flush;
+    }
+
+    template<class T>
+    void sendstate(gamestate &gs, T &p)
+    {
+        putint(p, gs.lifesequence);
+        putint(p, gs.health);
+        putint(p, gs.maxhealth);
+        putint(p, gs.armour);
+        putint(p, gs.armourtype);
+        putint(p, gs.gunselect);
+        loopi(GUN_PISTOL-GUN_SG+1) putint(p, gs.ammo[GUN_SG+i]);
+    }
+
+    void spawnstate(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        gs.spawnstate(gamemode);
+        gs.lifesequence = (gs.lifesequence + 1)&0x7F;
+    }
+
+    void sendspawn(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        spawnstate(ci);
+        sendf(ci->ownernum, 1, "rii7v", N_SPAWNSTATE, ci->clientnum, gs.lifesequence,
+            gs.health, gs.maxhealth,
+            gs.armour, gs.armourtype,
+            gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG]);
+        gs.lastspawn = gamemillis;
+    }
+
+    void sendwelcome(clientinfo *ci)
+    {
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        int chan = welcomepacket(p, ci);
+        sendpacket(ci->clientnum, chan, p.finalize());
+    }
+
+    void putinitclient(clientinfo *ci, packetbuf &p)
+    {
+        if(ci->state.aitype != AI_NONE)
+        {
+            putint(p, N_INITAI);
+            putint(p, ci->clientnum);
+            putint(p, ci->ownernum);
+            putint(p, ci->state.aitype);
+            putint(p, ci->state.skill);
+            putint(p, ci->playermodel);
+            sendstring(ci->name, p);
+            sendstring(ci->team, p);
+        }
+        else
+        {
+            putint(p, N_INITCLIENT);
+            putint(p, ci->clientnum);
+            sendstring(ci->name, p);
+            sendstring(ci->team, p);
+            putint(p, ci->playermodel);
+        }
+    }
+
+    void welcomeinitclient(packetbuf &p, int exclude = -1)
+    {
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(!ci->connected || ci->clientnum == exclude) continue;
+
+            putinitclient(ci, p);
+        }
+    }
+
+    bool hasmap(clientinfo *ci)
+    {
+        return (m_edit && (clients.length() > 0 || ci->local)) ||
+               (smapname[0] && (!m_timed || gamemillis < gamelimit || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || numclients(ci->clientnum, true, true, true)));
+    }
+
+    int welcomepacket(packetbuf &p, clientinfo *ci)
+    {
+        putint(p, N_WELCOME);
+        putint(p, N_MAPCHANGE);
+        sendstring(smapname, p);
+        putint(p, gamemode);
+        putint(p, notgotitems ? 1 : 0);
+        if(!ci || (m_timed && smapname[0]))
+        {
+            putint(p, N_TIMEUP);
+            putint(p, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0);
+        }
+        if(!notgotitems)
+        {
+            putint(p, N_ITEMLIST);
+            loopv(sents) if(sents[i].spawned)
+            {
+                putint(p, i);
+                putint(p, sents[i].type);
+            }
+            putint(p, -1);
+        }
+        bool hasmaster = false;
+        if(mastermode != MM_OPEN)
+        {
+            putint(p, N_CURRENTMASTER);
+            putint(p, mastermode);
+            hasmaster = true;
+        }
+        loopv(clients) if(clients[i]->privilege >= PRIV_MASTER)
+        {
+            if(!hasmaster)
+            {
+                putint(p, N_CURRENTMASTER);
+                putint(p, mastermode);
+                hasmaster = true;
+            }
+            putint(p, clients[i]->clientnum);
+            putint(p, clients[i]->privilege);
+        }
+        if(hasmaster) putint(p, -1);
+        if(gamepaused)
+        {
+            putint(p, N_PAUSEGAME);
+            putint(p, 1);
+            putint(p, -1);
+        }
+        if(gamespeed != 100)
+        {
+            putint(p, N_GAMESPEED);
+            putint(p, gamespeed);
+            putint(p, -1);
+        }
+        if(m_teammode)
+        {
+            putint(p, N_TEAMINFO);
+            enumerate(teaminfos, teaminfo, t,
+                if(t.frags) { sendstring(t.team, p); putint(p, t.frags); }
+            );
+            sendstring("", p);
+        } 
+        if(ci)
+        {
+            putint(p, N_SETTEAM);
+            putint(p, ci->clientnum);
+            sendstring(ci->team, p);
+            putint(p, -1);
+        }
+        if(ci && (m_demo || m_mp(gamemode)) && ci->state.state!=CS_SPECTATOR)
+        {
+            if(smode && !smode->canspawn(ci, true))
+            {
+                ci->state.state = CS_DEAD;
+                putint(p, N_FORCEDEATH);
+                putint(p, ci->clientnum);
+                sendf(-1, 1, "ri2x", N_FORCEDEATH, ci->clientnum, ci->clientnum);
+            }
+            else
+            {
+                gamestate &gs = ci->state;
+                spawnstate(ci);
+                putint(p, N_SPAWNSTATE);
+                putint(p, ci->clientnum);
+                sendstate(gs, p);
+                gs.lastspawn = gamemillis;
+            }
+        }
+        if(ci && ci->state.state==CS_SPECTATOR)
+        {
+            putint(p, N_SPECTATOR);
+            putint(p, ci->clientnum);
+            putint(p, 1);
+            sendf(-1, 1, "ri3x", N_SPECTATOR, ci->clientnum, 1, ci->clientnum);
+        }
+        if(!ci || clients.length()>1)
+        {
+            putint(p, N_RESUME);
+            loopv(clients)
+            {
+                clientinfo *oi = clients[i];
+                if(ci && oi->clientnum==ci->clientnum) continue;
+                putint(p, oi->clientnum);
+                putint(p, oi->state.state);
+                putint(p, oi->state.frags);
+                putint(p, oi->state.flags);
+                putint(p, oi->state.deaths);
+                putint(p, oi->state.quadmillis);
+                sendstate(oi->state, p);
+            }
+            putint(p, -1);
+            welcomeinitclient(p, ci ? ci->clientnum : -1);
+        }
+        if(smode) smode->initclient(ci, p, true);
+        return 1;
+    }
+
+    bool restorescore(clientinfo *ci)
+    {
+        //if(ci->local) return false;
+        savedscore *sc = findscore(ci, false);
+        if(sc)
+        {
+            sc->restore(ci->state);
+            return true;
+        }
+        return false;
+    }
+
+    void sendresume(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        sendf(-1, 1, "ri3i4i6vi", N_RESUME, ci->clientnum, gs.state,
+            gs.frags, gs.flags, gs.deaths, gs.quadmillis,
+            gs.lifesequence,
+            gs.health, gs.maxhealth,
+            gs.armour, gs.armourtype,
+            gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG], -1);
+    }
+
+    void sendinitclient(clientinfo *ci)
+    {
+        packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE);
+        putinitclient(ci, p);
+        sendpacket(-1, 1, p.finalize(), ci->clientnum);
+    }
+
+    void loaditems()
+    {
+        resetitems();
+        notgotitems = true;
+        if(m_edit || !loadents(smapname, ments, &mcrc))
+            return;
+        loopv(ments) if(canspawnitem(ments[i].type))
+        {
+            server_entity se = { NOTUSED, 0, false };
+            while(sents.length()<=i) sents.add(se);
+            sents[i].type = ments[i].type;
+            if(m_mp(gamemode) && delayspawn(sents[i].type)) sents[i].spawntime = spawntime(sents[i].type);
+            else sents[i].spawned = true;
+        }
+        notgotitems = false;
+    }
+        
+    void changemap(const char *s, int mode)
+    {
+        stopdemo();
+        pausegame(false);
+        changegamespeed(100);
+        if(smode) smode->cleanup();
+        aiman::clearai();
+
+        gamemode = mode;
+        gamemillis = 0;
+        gamelimit = 10*60000;
+        interm = 0;
+        nextexceeded = 0;
+        copystring(smapname, s);
+        loaditems();
+        scores.shrink(0);
+        shouldcheckteamkills = false;
+        teamkills.shrink(0);
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed;
+        }
+
+        if(!m_mp(gamemode)) kicknonlocalclients(DISC_LOCAL);
+
+        sendf(-1, 1, "risii", N_MAPCHANGE, smapname, gamemode, 1);
+
+        if(m_capture) smode = &capturemode;
+        else if(m_ctf) smode = &ctfmode;
+        else if(m_collect) smode = &collectmode;
+        else smode = NULL;
+
+        clearteaminfo();
+        if(m_teammode) autoteam();
+
+        if(m_timed && smapname[0]) sendf(-1, 1, "ri2", N_TIMEUP, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0);
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            ci->mapchange();
+            ci->state.lasttimeplayed = lastmillis;
+            if(m_mp(gamemode) && ci->state.state!=CS_SPECTATOR) sendspawn(ci);
+        }
+
+        aiman::changemap();
+
+        if(m_demo)
+        {
+            if(clients.length()) setupdemoplayback();
+        }
+        else
+        {
+            if(demonextmatch) setupdemorecord();
+            demonextmatch = autorecorddemo!=0;
+        }
+
+        if(smode) smode->setup();
+    }
+
+    void rotatemap(bool next)
+    {
+        if(!maprotations.inrange(curmaprotation))
+        {
+            changemap("", 1);
+            return;
+        }
+        if(next) 
+        {
+            curmaprotation = findmaprotation(gamemode, smapname);
+            if(curmaprotation >= 0) nextmaprotation();
+            else curmaprotation = smapname[0] ? max(findmaprotation(gamemode, ""), 0) : 0;
+        }
+        maprotation &rot = maprotations[curmaprotation];
+        changemap(rot.map, rot.findmode(gamemode));
+    }
+    
+    struct votecount
+    {
+        char *map;
+        int mode, count;
+        votecount() {}
+        votecount(char *s, int n) : map(s), mode(n), count(0) {}
+    };
+
+    void checkvotes(bool force = false)
+    {
+        vector<votecount> votes;
+        int maxvotes = 0;
+        loopv(clients)
+        {
+            clientinfo *oi = clients[i];
+            if(oi->state.state==CS_SPECTATOR && !oi->privilege && !oi->local) continue;
+            if(oi->state.aitype!=AI_NONE) continue;
+            maxvotes++;
+            if(!m_valid(oi->modevote)) continue;
+            votecount *vc = NULL;
+            loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote==votes[j].mode)
+            {
+                vc = &votes[j];
+                break;
+            }
+            if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote));
+            vc->count++;
+        }
+        votecount *best = NULL;
+        loopv(votes) if(!best || votes[i].count > best->count || (votes[i].count == best->count && rnd(2))) best = &votes[i];
+        if(force || (best && best->count > maxvotes/2))
+        {
+            sendpackets(true);
+            if(demorecord) enddemorecord();
+            if(best && (best->count > (force ? 1 : maxvotes/2)))
+            {
+                sendservmsg(force ? "vote passed by default" : "vote passed by majority");
+                changemap(best->map, best->mode);
+            }
+            else rotatemap(true);
+        }
+    }
+
+    void forcemap(const char *map, int mode)
+    {
+        stopdemo();
+        if(!map[0] && !m_check(mode, M_EDIT)) 
+        {
+            int idx = findmaprotation(mode, smapname);
+            if(idx < 0 && smapname[0]) idx = findmaprotation(mode, "");
+            if(idx < 0) return;
+            map = maprotations[idx].map;
+        }
+        if(hasnonlocalclients()) sendservmsgf("local player forced %s on map %s", modename(mode), map[0] ? map : "[new map]");
+        changemap(map, mode);
+    }
+
+    void vote(const char *map, int reqmode, int sender)
+    {
+        clientinfo *ci = getinfo(sender);
+        if(!ci || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || (!ci->local && !m_mp(reqmode))) return;
+        if(!m_valid(reqmode)) return;
+        if(!map[0] && !m_check(reqmode, M_EDIT)) 
+        {
+            int idx = findmaprotation(reqmode, smapname);
+            if(idx < 0 && smapname[0]) idx = findmaprotation(reqmode, "");
+            if(idx < 0) return;
+            map = maprotations[idx].map;
+        }
+        if(lockmaprotation && !ci->local && ci->privilege < (lockmaprotation > 1 ? PRIV_ADMIN : PRIV_MASTER) && findmaprotation(reqmode, map) < 0) 
+        {
+            sendf(sender, 1, "ris", N_SERVMSG, "This server has locked the map rotation.");
+            return;
+        }
+        copystring(ci->mapvote, map);
+        ci->modevote = reqmode;
+        if(ci->local || (ci->privilege && mastermode>=MM_VETO))
+        {
+            sendpackets(true);
+            if(demorecord) enddemorecord();
+            if(!ci->local || hasnonlocalclients())
+                sendservmsgf("%s forced %s on map %s", colorname(ci), modename(ci->modevote), ci->mapvote[0] ? ci->mapvote : "[new map]");
+            changemap(ci->mapvote, ci->modevote);
+        }
+        else
+        {
+            sendservmsgf("%s suggests %s on map %s (select map to vote)", colorname(ci), modename(reqmode), map[0] ? map : "[new map]");
+            checkvotes();
+        }
+    }
+
+    VAR(overtime, 0, 0, 1);
+
+    bool checkovertime()
+    {
+        if(!m_timed || !overtime) return false;
+        const char* topteam = NULL;
+        int topscore = INT_MIN;
+        bool tied = false;
+        if(m_teammode)
+        {
+            vector<teamscore> scores;
+            if(smode && smode->hidefrags()) smode->getteamscores(scores);
+            loopv(clients)
+            {
+                clientinfo *ci = clients[i];
+                if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue;
+                int score = 0;
+                if(smode && smode->hidefrags())
+                {
+                    int idx = scores.htfind(ci->team);
+                    if(idx >= 0) score = scores[idx].score;
+                }
+                else if(teaminfo *ti = teaminfos.access(ci->team)) score = ti->frags;
+                if(!topteam || score > topscore) { topteam = ci->team; topscore = score; tied = false; }
+                else if(score == topscore && strcmp(ci->team, topteam)) tied = true;
+            }
+        }
+        else
+        {
+            loopv(clients)
+            {
+                clientinfo *ci = clients[i];
+                if(ci->state.state==CS_SPECTATOR) continue;
+                int score = ci->state.frags;
+                if(score > topscore) { topscore = score; tied = false; }
+                else if(score == topscore) tied = true;
+            }
+        }
+        if(!tied) return false;
+        sendservmsg("the game is tied with overtime");
+        gamelimit = max(gamemillis, gamelimit) + 2*60000;
+        sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1));
+        return true;
+    }
+
+    void checkintermission(bool force = false)
+    {
+        if(gamemillis >= gamelimit && !interm && (force || !checkovertime()))
+        {
+            sendf(-1, 1, "ri2", N_TIMEUP, 0);
+            if(smode) smode->intermission();
+            changegamespeed(100);
+            interm = gamemillis + 10000;
+        }
+    }
+
+    void startintermission() { gamelimit = min(gamelimit, gamemillis); checkintermission(true); }
+
+    void dodamage(clientinfo *target, clientinfo *actor, int damage, int gun, const vec &hitpush = vec(0, 0, 0))
+    {
+        gamestate &ts = target->state;
+        ts.dodamage(damage);
+        if(target!=actor && !isteam(target->team, actor->team)) actor->state.damage += damage;
+        sendf(-1, 1, "ri6", N_DAMAGE, target->clientnum, actor->clientnum, damage, ts.armour, ts.health);
+        if(target==actor) target->setpushed();
+        else if(!hitpush.iszero())
+        {
+            ivec v(vec(hitpush).rescale(DNF));
+            sendf(ts.health<=0 ? -1 : target->ownernum, 1, "ri7", N_HITPUSH, target->clientnum, gun, damage, v.x, v.y, v.z);
+            target->setpushed();
+        }
+        if(ts.health<=0)
+        {
+            target->state.deaths++;
+            int fragvalue = smode ? smode->fragvalue(target, actor) : (target==actor || isteam(target->team, actor->team) ? -1 : 1);
+            actor->state.frags += fragvalue;
+            if(fragvalue>0)
+            {
+                int friends = 0, enemies = 0; // note: friends also includes the fragger
+                if(m_teammode) loopv(clients) if(strcmp(clients[i]->team, actor->team)) enemies++; else friends++;
+                else { friends = 1; enemies = clients.length()-1; }
+                actor->state.effectiveness += fragvalue*friends/float(max(enemies, 1));
+            }
+            teaminfo *t = m_teammode ? teaminfos.access(actor->team) : NULL;
+            if(t) t->frags += fragvalue; 
+            sendf(-1, 1, "ri5", N_DIED, target->clientnum, actor->clientnum, actor->state.frags, t ? t->frags : 0);
+            target->position.setsize(0);
+            if(smode) smode->died(target, actor);
+            ts.state = CS_DEAD;
+            ts.lastdeath = gamemillis;
+            if(actor!=target && isteam(actor->team, target->team)) 
+            {
+                actor->state.teamkills++;
+                addteamkill(actor, target, 1);
+            }
+            ts.deadflush = ts.lastdeath + DEATHMILLIS;
+            // don't issue respawn yet until DEATHMILLIS has elapsed
+            // ts.respawn();
+        }
+    }
+
+    void suicide(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        if(gs.state!=CS_ALIVE) return;
+        int fragvalue = smode ? smode->fragvalue(ci, ci) : -1;
+        ci->state.frags += fragvalue;
+        ci->state.deaths++;
+        teaminfo *t = m_teammode ? teaminfos.access(ci->team) : NULL;
+        if(t) t->frags += fragvalue;
+        sendf(-1, 1, "ri5", N_DIED, ci->clientnum, ci->clientnum, gs.frags, t ? t->frags : 0);
+        ci->position.setsize(0);
+        if(smode) smode->died(ci, NULL);
+        gs.state = CS_DEAD;
+        gs.lastdeath = gamemillis;
+        gs.respawn();
+    }
+
+    void suicideevent::process(clientinfo *ci)
+    {
+        suicide(ci);
+    }
+
+    void explodeevent::process(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        switch(gun)
+        {
+            case GUN_RL:
+                if(!gs.rockets.remove(id)) return;
+                break;
+
+            case GUN_GL:
+                if(!gs.grenades.remove(id)) return;
+                break;
+
+            default:
+                return;
+        }
+        sendf(-1, 1, "ri4x", N_EXPLODEFX, ci->clientnum, gun, id, ci->ownernum);
+        loopv(hits)
+        {
+            hitinfo &h = hits[i];
+            clientinfo *target = getinfo(h.target);
+            if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.dist<0 || h.dist>guns[gun].exprad) continue;
+
+            bool dup = false;
+            loopj(i) if(hits[j].target==h.target) { dup = true; break; }
+            if(dup) continue;
+
+            int damage = guns[gun].damage;
+            if(gs.quadmillis) damage *= 4;
+            damage = int(damage*(1-h.dist/EXP_DISTSCALE/guns[gun].exprad));
+            if(target==ci) damage /= EXP_SELFDAMDIV;
+            dodamage(target, ci, damage, gun, h.dir);
+        }
+    }
+
+    void shotevent::process(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        int wait = millis - gs.lastshot;
+        if(!gs.isalive(gamemillis) ||
+           wait<gs.gunwait ||
+           gun<GUN_FIST || gun>GUN_PISTOL ||
+           gs.ammo[gun]<=0 || (guns[gun].range && from.dist(to) > guns[gun].range + 1))
+            return;
+        if(gun!=GUN_FIST) gs.ammo[gun]--;
+        gs.lastshot = millis;
+        gs.gunwait = guns[gun].attackdelay;
+        sendf(-1, 1, "rii9x", N_SHOTFX, ci->clientnum, gun, id,
+                int(from.x*DMF), int(from.y*DMF), int(from.z*DMF),
+                int(to.x*DMF), int(to.y*DMF), int(to.z*DMF),
+                ci->ownernum);
+        gs.shotdamage += guns[gun].damage*(gs.quadmillis ? 4 : 1)*guns[gun].rays;
+        switch(gun)
+        {
+            case GUN_RL: gs.rockets.add(id); break;
+            case GUN_GL: gs.grenades.add(id); break;
+            default:
+            {
+                int totalrays = 0, maxrays = guns[gun].rays;
+                loopv(hits)
+                {
+                    hitinfo &h = hits[i];
+                    clientinfo *target = getinfo(h.target);
+                    if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.rays<1 || h.dist > guns[gun].range + 1) continue;
+
+                    totalrays += h.rays;
+                    if(totalrays>maxrays) continue;
+                    int damage = h.rays*guns[gun].damage;
+                    if(gs.quadmillis) damage *= 4;
+                    dodamage(target, ci, damage, gun, h.dir);
+                }
+                break;
+            }
+        }
+    }
+
+    void pickupevent::process(clientinfo *ci)
+    {
+        gamestate &gs = ci->state;
+        if(m_mp(gamemode) && !gs.isalive(gamemillis)) return;
+        pickup(ent, ci->clientnum);
+    }
+
+    bool gameevent::flush(clientinfo *ci, int fmillis)
+    {
+        process(ci);
+        return true;
+    }
+
+    bool timedevent::flush(clientinfo *ci, int fmillis)
+    {
+        if(millis > fmillis) return false;
+        else if(millis >= ci->lastevent)
+        {
+            ci->lastevent = millis;
+            process(ci);
+        }
+        return true;
+    }
+
+    void clearevent(clientinfo *ci)
+    {
+        delete ci->events.remove(0);
+    }
+
+    void flushevents(clientinfo *ci, int millis)
+    {
+        while(ci->events.length())
+        {
+            gameevent *ev = ci->events[0];
+            if(ev->flush(ci, millis)) clearevent(ci);
+            else break;
+        }
+    }
+
+    void processevents()
+    {
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(curtime>0 && ci->state.quadmillis) ci->state.quadmillis = max(ci->state.quadmillis-curtime, 0);
+            flushevents(ci, gamemillis);
+        }
+    }
+
+    void cleartimedevents(clientinfo *ci)
+    {
+        int keep = 0;
+        loopv(ci->events)
+        {
+            if(ci->events[i]->keepable())
+            {
+                if(keep < i)
+                {
+                    for(int j = keep; j < i; j++) delete ci->events[j];
+                    ci->events.remove(keep, i - keep);
+                    i = keep;
+                }
+                keep = i+1;
+                continue;
+            }
+        }
+        while(ci->events.length() > keep) delete ci->events.pop();
+        ci->timesync = false;
+    }
+
+    void serverupdate()
+    {
+        if(shouldstep && !gamepaused)
+        {
+            gamemillis += curtime;
+
+            if(m_demo) readdemo();
+            else if(!m_timed || gamemillis < gamelimit)
+            {
+                processevents();
+                if(curtime)
+                {
+                    loopv(sents) if(sents[i].spawntime) // spawn entities when timer reached
+                    {
+                        int oldtime = sents[i].spawntime;
+                        sents[i].spawntime -= curtime;
+                        if(sents[i].spawntime<=0)
+                        {
+                            sents[i].spawntime = 0;
+                            sents[i].spawned = true;
+                            sendf(-1, 1, "ri2", N_ITEMSPAWN, i);
+                        }
+                        else if(sents[i].spawntime<=10000 && oldtime>10000 && (sents[i].type==I_QUAD || sents[i].type==I_BOOST))
+                        {
+                            sendf(-1, 1, "ri2", N_ANNOUNCE, sents[i].type);
+                        }
+                    }
+                }
+                aiman::checkai();
+                if(smode) smode->update();
+            }
+        }
+
+        while(bannedips.length() && bannedips[0].expire-totalmillis <= 0) bannedips.remove(0);
+        loopv(connects) if(totalmillis-connects[i]->connectmillis>15000) disconnect_client(connects[i]->clientnum, DISC_TIMEOUT);
+
+        if(nextexceeded && gamemillis > nextexceeded && (!m_timed || gamemillis < gamelimit))
+        {
+            nextexceeded = 0;
+            loopvrev(clients) 
+            {
+                clientinfo &c = *clients[i];
+                if(c.state.aitype != AI_NONE) continue;
+                if(c.checkexceeded()) disconnect_client(c.clientnum, DISC_MSGERR);
+                else c.scheduleexceeded();
+            }
+        }
+
+        if(shouldcheckteamkills) checkteamkills();
+
+        if(shouldstep && !gamepaused)
+        {
+            if(m_timed && smapname[0] && gamemillis-curtime>0) checkintermission();
+            if(interm > 0 && gamemillis>interm)
+            {
+                if(demorecord) enddemorecord();
+                interm = -1;
+                checkvotes(true);
+            }
+        }
+
+        shouldstep = clients.length() > 0;
+    }
+
+    void forcespectator(clientinfo *ci)
+    {
+        if(ci->state.state==CS_ALIVE) suicide(ci);
+        if(smode) smode->leavegame(ci);
+        ci->state.state = CS_SPECTATOR;
+        ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed;
+        if(!ci->local && (!ci->privilege || ci->warned)) aiman::removeai(ci);
+        sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 1);
+    }
+
+    struct crcinfo
+    {
+        int crc, matches;
+
+        crcinfo() {}
+        crcinfo(int crc, int matches) : crc(crc), matches(matches) {}
+
+        static bool compare(const crcinfo &x, const crcinfo &y) { return x.matches > y.matches; }
+    };
+
+    VAR(modifiedmapspectator, 0, 1, 2);
+
+    void checkmaps(int req = -1)
+    {
+        if(m_edit || !smapname[0]) return;
+        vector<crcinfo> crcs;
+        int total = 0, unsent = 0, invalid = 0;
+        if(mcrc) crcs.add(crcinfo(mcrc, clients.length() + 1));
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE) continue;
+            total++;
+            if(!ci->clientmap[0])
+            {
+                if(ci->mapcrc < 0) invalid++;
+                else if(!ci->mapcrc) unsent++;
+            }
+            else
+            {
+                crcinfo *match = NULL;
+                loopvj(crcs) if(crcs[j].crc == ci->mapcrc) { match = &crcs[j]; break; }
+                if(!match) crcs.add(crcinfo(ci->mapcrc, 1));
+                else match->matches++;
+            }
+        }
+        if(!mcrc && total - unsent < min(total, 4)) return;
+        crcs.sort(crcinfo::compare);
+        string msg;
+        loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue;
+            formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname);
+            sendf(req, 1, "ris", N_SERVMSG, msg);
+            if(req < 0) ci->warned = true;
+        }
+        if(crcs.length() >= 2) loopv(crcs)
+        {
+            crcinfo &info = crcs[i];
+            if(i || info.matches <= crcs[i+1].matches) loopvj(clients)
+            {
+                clientinfo *ci = clients[j];
+                if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue;
+                formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname);
+                sendf(req, 1, "ris", N_SERVMSG, msg);
+                if(req < 0) ci->warned = true;
+            }
+        }
+        if(req < 0 && modifiedmapspectator && (mcrc || modifiedmapspectator > 1)) loopv(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(!ci->local && ci->warned && ci->state.state != CS_SPECTATOR) forcespectator(ci);
+        }
+    }
+
+    bool shouldspectate(clientinfo *ci)
+    {
+        return !ci->local && ci->warned && modifiedmapspectator && (mcrc || modifiedmapspectator > 1);
+    }
+
+    void unspectate(clientinfo *ci)
+    {
+        if(shouldspectate(ci)) return;
+        ci->state.state = CS_DEAD;
+        ci->state.respawn();
+        ci->state.lasttimeplayed = lastmillis;
+        aiman::addclient(ci);
+        sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 0);
+        if(ci->clientmap[0] || ci->mapcrc) checkmaps();
+        if(!hasmap(ci)) rotatemap(true);
+    }
+
+    void sendservinfo(clientinfo *ci)
+    {
+        sendf(ci->clientnum, 1, "ri5ss", N_SERVINFO, ci->clientnum, PROTOCOL_VERSION, ci->sessionid, serverpass[0] ? 1 : 0, serverdesc, serverauth);
+    }
+
+    void noclients()
+    {
+        bannedips.shrink(0);
+        aiman::clearai();
+    }
+
+    void localconnect(int n)
+    {
+        clientinfo *ci = getinfo(n);
+        ci->clientnum = ci->ownernum = n;
+        ci->connectmillis = totalmillis;
+        ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF;
+        ci->local = true;
+
+        connects.add(ci);
+        sendservinfo(ci);
+    }
+
+    void localdisconnect(int n)
+    {
+        if(m_demo) enddemoplayback();
+        clientdisconnect(n);
+    }
+
+    int clientconnect(int n, uint ip)
+    {
+        clientinfo *ci = getinfo(n);
+        ci->clientnum = ci->ownernum = n;
+        ci->connectmillis = totalmillis;
+        ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF;
+
+        connects.add(ci);
+        if(!m_mp(gamemode)) return DISC_LOCAL;
+        sendservinfo(ci);
+        return DISC_NONE;
+    }
+
+    void clientdisconnect(int n)
+    {
+        clientinfo *ci = getinfo(n);
+        loopv(clients) if(clients[i]->authkickvictim == ci->clientnum) clients[i]->cleanauth(); 
+        if(ci->connected)
+        {
+            if(ci->privilege) setmaster(ci, false);
+            if(smode) smode->leavegame(ci, true);
+            ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed;
+            savescore(ci);
+            sendf(-1, 1, "ri2", N_CDIS, n);
+            clients.removeobj(ci);
+            aiman::removeai(ci);
+            if(!numclients(-1, false, true)) noclients(); // bans clear when server empties
+            if(ci->local) checkpausegame();
+        }
+        else connects.removeobj(ci);
+    }
+
+    int reserveclients() { return 3; }
+
+    extern void verifybans();
+
+    struct banlist
+    {
+        vector<ipmask> bans;
+
+        void clear() { bans.shrink(0); }
+
+        bool check(uint ip)
+        {
+            loopv(bans) if(bans[i].check(ip)) return true;
+            return false;
+        }
+
+        void add(const char *ipname)
+        {
+            ipmask ban;
+            ban.parse(ipname);
+            bans.add(ban);
+
+            verifybans();
+        }
+    } ipbans, gbans; 
+
+    bool checkbans(uint ip)
+    {
+        loopv(bannedips) if(bannedips[i].ip==ip) return true;
+        return ipbans.check(ip) || gbans.check(ip);
+    }
+
+    void verifybans()
+    {
+        loopvrev(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->state.aitype != AI_NONE || ci->local || ci->privilege >= PRIV_ADMIN) continue;
+            if(checkbans(getclientip(ci->clientnum))) disconnect_client(ci->clientnum, DISC_IPBAN);
+        }
+    }
+
+    ICOMMAND(clearipbans, "", (), ipbans.clear());
+    ICOMMAND(ipban, "s", (const char *ipname), ipbans.add(ipname));
+       
+    int allowconnect(clientinfo *ci, const char *pwd = "")
+    {
+        if(ci->local) return DISC_NONE;
+        if(!m_mp(gamemode)) return DISC_LOCAL;
+        if(serverpass[0])
+        {
+            if(!checkpassword(ci, serverpass, pwd)) return DISC_PASSWORD;
+            return DISC_NONE;
+        }
+        if(adminpass[0] && checkpassword(ci, adminpass, pwd)) return DISC_NONE;
+        if(numclients(-1, false, true)>=maxclients) return DISC_MAXCLIENTS;
+        uint ip = getclientip(ci->clientnum);
+        if(checkbans(ip)) return DISC_IPBAN;
+        if(mastermode>=MM_PRIVATE && allowedips.find(ip)<0) return DISC_PRIVATE;
+        return DISC_NONE;
+    }
+
+    bool allowbroadcast(int n)
+    {
+        clientinfo *ci = getinfo(n);
+        return ci && ci->connected;
+    }
+
+    clientinfo *findauth(uint id)
+    {
+        loopv(clients) if(clients[i]->authreq == id) return clients[i];
+        return NULL;
+    }
+
+
+    void authfailed(clientinfo *ci)
+    {
+        if(!ci) return;
+        ci->cleanauth();
+        if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth);
+    }
+
+    void authfailed(uint id)
+    {
+        authfailed(findauth(id));
+    }
+
+    void authsucceeded(uint id)
+    {
+        clientinfo *ci = findauth(id);
+        if(!ci) return;
+        ci->cleanauth(ci->connectauth!=0);
+        if(ci->connectauth) connected(ci);
+        if(ci->authkickvictim >= 0)
+        {
+            if(setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH, false, true))
+                trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH);    
+            ci->cleanauthkick();
+        }
+        else setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH);
+    }
+
+    void authchallenged(uint id, const char *val, const char *desc = "")
+    {
+        clientinfo *ci = findauth(id);
+        if(!ci) return;
+        sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val);
+    }
+
+    uint nextauthreq = 0;
+
+    bool tryauth(clientinfo *ci, const char *user, const char *desc)
+    {
+        ci->cleanauth();
+        if(!nextauthreq) nextauthreq = 1;
+        ci->authreq = nextauthreq++;
+        filtertext(ci->authname, user, false, false, 100);
+        copystring(ci->authdesc, desc);
+        if(ci->authdesc[0])
+        {
+            userinfo *u = users.access(userkey(ci->authname, ci->authdesc));
+            if(u) 
+            {
+                uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() };
+                vector<char> buf;
+                ci->authchallenge = genchallenge(u->pubkey, seed, sizeof(seed), buf);
+                sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, ci->authreq, buf.getbuf());
+            }
+            else ci->cleanauth();
+        }
+        else if(!requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname))
+        {
+            ci->cleanauth();
+            sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server");
+        }
+        if(ci->authreq) return true;
+        if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth);
+        return false;
+    }
+
+    bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc)
+    {
+        if(ci->authreq != id || strcmp(ci->authdesc, desc)) 
+        {
+            ci->cleanauth();
+            return !ci->connectauth;
+        }
+        for(char *s = val; *s; s++)
+        {
+            if(!isxdigit(*s)) { *s = '\0'; break; }
+        }
+        if(desc[0])
+        {
+            if(ci->authchallenge && checkchallenge(val, ci->authchallenge))
+            {
+                userinfo *u = users.access(userkey(ci->authname, ci->authdesc));
+                if(u) 
+                {
+                    if(ci->connectauth) connected(ci);
+                    if(ci->authkickvictim >= 0)
+                    {
+                        if(setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege, false, true))
+                            trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, u->privilege);
+                    }
+                    else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege);
+                }
+            }
+            ci->cleanauth(); 
+        } 
+        else if(!requestmasterf("confauth %u %s\n", id, val))
+        {
+            ci->cleanauth();
+            sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server");
+        }
+        return ci->authreq || !ci->connectauth;
+    }
+
+    void masterconnected()
+    {
+    }
+
+    void masterdisconnected()
+    {
+        loopvrev(clients)
+        {
+            clientinfo *ci = clients[i];
+            if(ci->authreq) authfailed(ci); 
+        }
+    }
+
+    void processmasterinput(const char *cmd, int cmdlen, const char *args)
+    {
+        uint id;
+        string val;
+        if(sscanf(cmd, "failauth %u", &id) == 1)
+            authfailed(id);
+        else if(sscanf(cmd, "succauth %u", &id) == 1)
+            authsucceeded(id);
+        else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2)
+            authchallenged(id, val);
+        else if(matchstring(cmd, cmdlen, "cleargbans"))
+            gbans.clear();
+        else if(sscanf(cmd, "addgban %100s", val) == 1)
+            gbans.add(val);
+    }
+
+    void receivefile(int sender, uchar *data, int len)
+    {
+        if(!m_edit || len <= 0 || len > 4*1024*1024) return;
+        clientinfo *ci = getinfo(sender);
+        if(ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) return;
+        if(mapdata) DELETEP(mapdata);
+        mapdata = opentempfile("mapdata", "w+b");
+        if(!mapdata) { sendf(sender, 1, "ris", N_SERVMSG, "failed to open temporary file for map"); return; }
+        mapdata->write(data, len);
+        sendservmsgf("[%s sent a map to server, \"/getmap\" to receive it]", colorname(ci));
+    }
+
+    void sendclipboard(clientinfo *ci)
+    {
+        if(!ci->lastclipboard || !ci->clipboard) return;
+        bool flushed = false;
+        loopv(clients)
+        {
+            clientinfo &e = *clients[i];
+            if(e.clientnum != ci->clientnum && e.needclipboard - ci->lastclipboard >= 0)
+            {
+                if(!flushed) { flushserver(true); flushed = true; }
+                sendpacket(e.clientnum, 1, ci->clipboard);
+            }
+        }
+    }
+
+    void connected(clientinfo *ci)
+    {
+        if(m_demo) enddemoplayback();
+
+        if(!hasmap(ci)) rotatemap(false);
+
+        shouldstep = true;
+
+        connects.removeobj(ci);
+        clients.add(ci);
+
+        ci->connectauth = 0;
+        ci->connected = true;
+        ci->needclipboard = totalmillis ? totalmillis : 1;
+        if(mastermode>=MM_LOCKED) ci->state.state = CS_SPECTATOR;
+        ci->state.lasttimeplayed = lastmillis;
+
+        const char *worst = m_teammode ? chooseworstteam(NULL, ci) : NULL;
+        copystring(ci->team, worst ? worst : "good", MAXTEAMLEN+1);
+
+        sendwelcome(ci);
+        if(restorescore(ci)) sendresume(ci);
+        sendinitclient(ci);
+
+        aiman::addclient(ci);
+
+        if(m_demo) setupdemoplayback();
+
+        if(servermotd[0]) sendf(ci->clientnum, 1, "ris", N_SERVMSG, servermotd);
+    }
+
+    void parsepacket(int sender, int chan, packetbuf &p)     // has to parse exactly each byte of the packet
+    {
+        if(sender<0 || p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED || chan > 2) return;
+        char text[MAXTRANS];
+        int type;
+        clientinfo *ci = sender>=0 ? getinfo(sender) : NULL, *cq = ci, *cm = ci;
+        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))
+            {
+                case N_CONNECT:
+                {
+                    getstring(text, p);
+                    filtertext(text, text, false, false, MAXNAMELEN);
+                    if(!text[0]) copystring(text, "unnamed");
+                    copystring(ci->name, text, MAXNAMELEN+1);
+                    ci->playermodel = getint(p);
+
+                    string password, authdesc, authname;
+                    getstring(password, p, sizeof(password));
+                    getstring(authdesc, p, sizeof(authdesc));
+                    getstring(authname, p, sizeof(authname));
+                    int disc = allowconnect(ci, password);
+                    if(disc)
+                    {
+                        if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !tryauth(ci, authname, authdesc))
+                        {
+                            disconnect_client(sender, disc);
+                            return;
+                        }
+                        ci->connectauth = disc;
+                    }
+                    else connected(ci);
+                    break;
+                }
+
+                case N_AUTHANS:
+                {
+                    string desc, ans;
+                    getstring(desc, p, sizeof(desc));
+                    uint id = (uint)getint(p);
+                    getstring(ans, p, sizeof(ans));
+                    if(!answerchallenge(ci, id, ans, desc)) 
+                    {
+                        disconnect_client(sender, ci->connectauth);
+                        return;
+                    }
+                    break;
+                }
+
+                case N_PING:
+                    getint(p);
+                    break;
+
+                default:
+                    disconnect_client(sender, DISC_MSGERR);
+                    return;
+            }
+            return;
+        }
+        else if(chan==2)
+        {
+            receivefile(sender, p.buf, p.maxlen);
+            return;
+        }
+
+        if(p.packet->flags&ENET_PACKET_FLAG_RELIABLE) reliablemessages = true;
+        #define QUEUE_AI clientinfo *cm = cq;
+        #define QUEUE_MSG { if(cm && (!cm->local || demorecord || hasnonlocalclients())) while(curmsg<p.length()) cm->messages.add(p.buf[curmsg++]); }
+        #define QUEUE_BUF(body) { \
+            if(cm && (!cm->local || demorecord || hasnonlocalclients())) \
+            { \
+                curmsg = p.length(); \
+                { body; } \
+            } \
+        }
+        #define QUEUE_INT(n) QUEUE_BUF(putint(cm->messages, n))
+        #define QUEUE_UINT(n) QUEUE_BUF(putuint(cm->messages, n))
+        #define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cm->messages))
+        int curmsg;
+        while((curmsg = p.length()) < p.maxlen) switch(type = checktype(getint(p), ci))
+        {
+            case N_POS:
+            {
+                int pcn = getuint(p); 
+                p.get(); 
+                uint flags = getuint(p);
+                clientinfo *cp = getinfo(pcn);
+                if(cp && pcn != sender && cp->ownernum != sender) cp = NULL;
+                vec pos;
+                loopk(3)
+                {
+                    int n = p.get(); n |= p.get()<<8; if(flags&(1<<k)) { n |= p.get()<<16; if(n&0x800000) n |= ~0U<<24; }
+                    pos[k] = n/DMF;
+                }
+                loopk(3) p.get();
+                int mag = p.get(); if(flags&(1<<3)) mag |= p.get()<<8;
+                int dir = p.get(); dir |= p.get()<<8;
+                vec vel = vec((dir%360)*RAD, (clamp(dir/360, 0, 180)-90)*RAD).mul(mag/DVELF);
+                if(flags&(1<<4))
+                {
+                    p.get(); if(flags&(1<<5)) p.get();
+                    if(flags&(1<<6)) loopk(2) p.get();
+                }
+                if(cp)
+                {
+                    if((!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING))
+                    {
+                        if(!ci->local && !m_edit && max(vel.magnitude2(), (float)fabs(vel.z)) >= 180)
+                            cp->setexceeded();
+                        cp->position.setsize(0);
+                        while(curmsg<p.length()) cp->position.add(p.buf[curmsg++]);
+                    }
+                    if(smode && cp->state.state==CS_ALIVE) smode->moved(cp, cp->state.o, cp->gameclip, pos, (flags&0x80)!=0);
+                    cp->state.o = pos;
+                    cp->gameclip = (flags&0x80)!=0;
+                }
+                break;
+            }
+
+            case N_TELEPORT:
+            {
+                int pcn = getint(p), teleport = getint(p), teledest = getint(p);
+                clientinfo *cp = getinfo(pcn);
+                if(cp && pcn != sender && cp->ownernum != sender) cp = NULL;
+                if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING))
+                {
+                    flushclientposition(*cp);
+                    sendf(-1, 0, "ri4x", N_TELEPORT, pcn, teleport, teledest, cp->ownernum); 
+                }
+                break;
+            }
+
+            case N_JUMPPAD:
+            {
+                int pcn = getint(p), jumppad = getint(p);
+                clientinfo *cp = getinfo(pcn);
+                if(cp && pcn != sender && cp->ownernum != sender) cp = NULL;
+                if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING))
+                {
+                    cp->setpushed();
+                    flushclientposition(*cp);
+                    sendf(-1, 0, "ri3x", N_JUMPPAD, pcn, jumppad, cp->ownernum);
+                }
+                break;
+            }
+                
+            case N_FROMAI:
+            {
+                int qcn = getint(p);
+                if(qcn < 0) cq = ci;
+                else
+                {
+                    cq = getinfo(qcn);
+                    if(cq && qcn != sender && cq->ownernum != sender) cq = NULL;
+                }
+                break;
+            }
+
+            case N_EDITMODE:
+            {
+                int val = getint(p);
+                if(!ci->local && !m_edit) break;
+                if(val ? ci->state.state!=CS_ALIVE && ci->state.state!=CS_DEAD : ci->state.state!=CS_EDITING) break;
+                if(smode)
+                {
+                    if(val) smode->leavegame(ci);
+                    else smode->entergame(ci);
+                }
+                if(val)
+                {
+                    ci->state.editstate = ci->state.state;
+                    ci->state.state = CS_EDITING;
+                    ci->events.setsize(0);
+                    ci->state.rockets.reset();
+                    ci->state.grenades.reset();
+                }
+                else ci->state.state = ci->state.editstate;
+                QUEUE_MSG;
+                break;
+            }
+
+            case N_MAPCRC:
+            {
+                getstring(text, p);
+                int crc = getint(p);
+                if(!ci) break;
+                if(strcmp(text, smapname))
+                {
+                    if(ci->clientmap[0])
+                    {
+                        ci->clientmap[0] = '\0';
+                        ci->mapcrc = 0;
+                    }
+                    else if(ci->mapcrc > 0) ci->mapcrc = 0;
+                    break;
+                }
+                copystring(ci->clientmap, text);
+                ci->mapcrc = text[0] ? crc : 1;
+                checkmaps();
+                if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL;
+                break;
+            }
+
+            case N_CHECKMAPS:
+                checkmaps(sender);
+                break;
+
+            case N_TRYSPAWN:
+                if(!ci || !cq || cq->state.state!=CS_DEAD || cq->state.lastspawn>=0 || (smode && !smode->canspawn(cq))) break;
+                if(!ci->clientmap[0] && !ci->mapcrc)
+                {
+                    ci->mapcrc = -1;
+                    checkmaps();
+                    if(ci == cq) { if(ci->state.state != CS_DEAD) break; }
+                    else if(cq->ownernum != ci->clientnum) { cq = NULL; break; }
+                }
+                if(cq->state.deadflush)
+                {
+                    flushevents(cq, cq->state.deadflush);
+                    cq->state.respawn();
+                }
+                cleartimedevents(cq);
+                sendspawn(cq);
+                break;
+
+            case N_GUNSELECT:
+            {
+                int gunselect = getint(p);
+                if(!cq || cq->state.state!=CS_ALIVE) break;
+                cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST;
+                QUEUE_AI;
+                QUEUE_MSG;
+                break;
+            }
+
+            case N_SPAWN:
+            {
+                int ls = getint(p), gunselect = getint(p);
+                if(!cq || (cq->state.state!=CS_ALIVE && cq->state.state!=CS_DEAD && cq->state.state!=CS_EDITING) || ls!=cq->state.lifesequence || cq->state.lastspawn<0) break;
+                cq->state.lastspawn = -1;
+                cq->state.state = CS_ALIVE;
+                cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST;
+                cq->exceeded = 0;
+                if(smode) smode->spawned(cq);
+                QUEUE_AI;
+                QUEUE_BUF({
+                    putint(cm->messages, N_SPAWN);
+                    sendstate(cq->state, cm->messages);
+                });
+                break;
+            }
+
+            case N_SUICIDE:
+            {
+                if(cq) cq->addevent(new suicideevent);
+                break;
+            }
+
+            case N_SHOOT:
+            {
+                shotevent *shot = new shotevent;
+                shot->id = getint(p);
+                shot->millis = cq ? cq->geteventmillis(gamemillis, shot->id) : 0;
+                shot->gun = getint(p);
+                loopk(3) shot->from[k] = getint(p)/DMF;
+                loopk(3) shot->to[k] = getint(p)/DMF;
+                int hits = getint(p);
+                loopk(hits)
+                {
+                    if(p.overread()) break;
+                    hitinfo &hit = shot->hits.add();
+                    hit.target = getint(p);
+                    hit.lifesequence = getint(p);
+                    hit.dist = getint(p)/DMF;
+                    hit.rays = getint(p);
+                    loopk(3) hit.dir[k] = getint(p)/DNF;
+                }
+                if(cq) 
+                {
+                    cq->addevent(shot);
+                    cq->setpushed();
+                }
+                else delete shot;
+                break;
+            }
+
+            case N_EXPLODE:
+            {
+                explodeevent *exp = new explodeevent;
+                int cmillis = getint(p);
+                exp->millis = cq ? cq->geteventmillis(gamemillis, cmillis) : 0;
+                exp->gun = getint(p);
+                exp->id = getint(p);
+                int hits = getint(p);
+                loopk(hits)
+                {
+                    if(p.overread()) break;
+                    hitinfo &hit = exp->hits.add();
+                    hit.target = getint(p);
+                    hit.lifesequence = getint(p);
+                    hit.dist = getint(p)/DMF;
+                    hit.rays = getint(p);
+                    loopk(3) hit.dir[k] = getint(p)/DNF;
+                }
+                if(cq) cq->addevent(exp);
+                else delete exp;
+                break;
+            }
+
+            case N_ITEMPICKUP:
+            {
+                int n = getint(p);
+                if(!cq) break;
+                pickupevent *pickup = new pickupevent;
+                pickup->ent = n;
+                cq->addevent(pickup);
+                break;
+            }
+
+            case N_TEXT:
+            {
+                QUEUE_AI;
+                QUEUE_MSG;
+                getstring(text, p);
+                filtertext(text, text, true, true);
+                QUEUE_STR(text);
+                if(isdedicatedserver() && cq) logoutf("%s: %s", colorname(cq), text);
+                break;
+            }
+
+            case N_SAYTEAM:
+            {
+                getstring(text, p);
+                if(!ci || !cq || (ci->state.state==CS_SPECTATOR && !ci->local && !ci->privilege) || !m_teammode || !cq->team[0]) break;
+                filtertext(text, text, true, true);
+                loopv(clients)
+                {
+                    clientinfo *t = clients[i];
+                    if(t==cq || t->state.state==CS_SPECTATOR || t->state.aitype != AI_NONE || strcmp(cq->team, t->team)) continue;
+                    sendf(t->clientnum, 1, "riis", N_SAYTEAM, cq->clientnum, text);
+                }
+                if(isdedicatedserver() && cq) logoutf("%s <%s>: %s", colorname(cq), cq->team, text);
+                break;
+            }
+
+            case N_SWITCHNAME:
+            {
+                QUEUE_MSG;
+                getstring(text, p);
+                filtertext(ci->name, text, false, false, MAXNAMELEN);
+                if(!ci->name[0]) copystring(ci->name, "unnamed");
+                QUEUE_STR(ci->name);
+                break;
+            }
+
+            case N_SWITCHMODEL:
+            {
+                ci->playermodel = getint(p);
+                QUEUE_MSG;
+                break;
+            }
+
+            case N_SWITCHTEAM:
+            {
+                getstring(text, p);
+                filtertext(text, text, false, false, MAXTEAMLEN);
+                if(m_teammode && text[0] && strcmp(ci->team, text) && (!smode || smode->canchangeteam(ci, ci->team, text)) && addteaminfo(text))
+                {
+                    if(ci->state.state==CS_ALIVE) suicide(ci);
+                    copystring(ci->team, text);
+                    aiman::changeteam(ci);
+                    sendf(-1, 1, "riisi", N_SETTEAM, sender, ci->team, ci->state.state==CS_SPECTATOR ? -1 : 0);
+                }
+                break;
+            }
+
+            case N_MAPVOTE:
+            {
+                getstring(text, p);
+                filtertext(text, text, false);
+                fixmapname(text);
+                int reqmode = getint(p);
+                vote(text, reqmode, sender);
+                break;
+            }
+
+            case N_ITEMLIST:
+            {
+                if((ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || !notgotitems || strcmp(ci->clientmap, smapname)) { while(getint(p)>=0 && !p.overread()) getint(p); break; }
+                int n;
+                while((n = getint(p))>=0 && n<MAXENTS && !p.overread())
+                {
+                    server_entity se = { NOTUSED, 0, false };
+                    while(sents.length()<=n) sents.add(se);
+                    sents[n].type = getint(p);
+                    if(canspawnitem(sents[n].type))
+                    {
+                        if(m_mp(gamemode) && delayspawn(sents[n].type)) sents[n].spawntime = spawntime(sents[n].type);
+                        else sents[n].spawned = true;
+                    }
+                }
+                notgotitems = false;
+                break;
+            }
+
+            case N_EDITENT:
+            {
+                int i = getint(p);
+                loopk(3) getint(p);
+                int type = getint(p);
+                loopk(5) getint(p);
+                if(!ci || ci->state.state==CS_SPECTATOR) break;
+                QUEUE_MSG;
+                bool canspawn = canspawnitem(type);
+                if(i<MAXENTS && (sents.inrange(i) || canspawnitem(type)))
+                {
+                    server_entity se = { NOTUSED, 0, false };
+                    while(sents.length()<=i) sents.add(se);
+                    sents[i].type = type;
+                    if(canspawn ? !sents[i].spawned : (sents[i].spawned || sents[i].spawntime))
+                    {
+                        sents[i].spawntime = canspawn ? 1 : 0;
+                        sents[i].spawned = false;
+                    }
+                }
+                break;
+            }
+
+            case N_EDITVAR:
+            {
+                int type = getint(p);
+                getstring(text, p);
+                switch(type)
+                {
+                    case ID_VAR: getint(p); break;
+                    case ID_FVAR: getfloat(p); break;
+                    case ID_SVAR: getstring(text, p);
+                }
+                if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG;
+                break;
+            }
+
+            case N_PING:
+                sendf(sender, 1, "i2", N_PONG, getint(p));
+                break;
+
+            case N_CLIENTPING:
+            {
+                int ping = getint(p);
+                if(ci)
+                {
+                    ci->ping = ping;
+                    loopv(ci->bots) ci->bots[i]->ping = ping;
+                }
+                QUEUE_MSG;
+                break;
+            }
+
+            case N_MASTERMODE:
+            {
+                int mm = getint(p);
+                if((ci->privilege || ci->local) && mm>=MM_OPEN && mm<=MM_PRIVATE)
+                {
+                    if((ci->privilege>=PRIV_ADMIN || ci->local) || (mastermask&(1<<mm)))
+                    {
+                        mastermode = mm;
+                        allowedips.shrink(0);
+                        if(mm>=MM_PRIVATE)
+                        {
+                            loopv(clients) allowedips.add(getclientip(clients[i]->clientnum));
+                        }
+                        sendf(-1, 1, "rii", N_MASTERMODE, mastermode);
+                        //sendservmsgf("mastermode is now %s (%d)", mastermodename(mastermode), mastermode);
+                    }
+                    else
+                    {
+                        defformatstring(s, "mastermode %d is disabled on this server", mm);
+                        sendf(sender, 1, "ris", N_SERVMSG, s);
+                    }
+                }
+                break;
+            }
+
+            case N_CLEARBANS:
+            {
+                if(ci->privilege || ci->local)
+                {
+                    bannedips.shrink(0);
+                    sendservmsg("cleared all bans");
+                }
+                break;
+            }
+
+            case N_KICK:
+            {
+                int victim = getint(p);
+                getstring(text, p);
+                filtertext(text, text);
+                trykick(ci, victim, text);
+                break;
+            }
+
+            case N_SPECTATOR:
+            {
+                int spectator = getint(p), val = getint(p);
+                if(!ci->privilege && !ci->local && (spectator!=sender || (ci->state.state==CS_SPECTATOR && mastermode>=MM_LOCKED))) break;
+                clientinfo *spinfo = (clientinfo *)getclientinfo(spectator); // no bots
+                if(!spinfo || !spinfo->connected || (spinfo->state.state==CS_SPECTATOR ? val : !val)) break;
+
+                if(spinfo->state.state!=CS_SPECTATOR && val) forcespectator(spinfo);
+                else if(spinfo->state.state==CS_SPECTATOR && !val) unspectate(spinfo);
+
+                if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL;
+                break;
+            }
+
+            case N_SETTEAM:
+            {
+                int who = getint(p);
+                getstring(text, p);
+                filtertext(text, text, false, false, MAXTEAMLEN);
+                if(!ci->privilege && !ci->local) break;
+                clientinfo *wi = getinfo(who);
+                if(!m_teammode || !text[0] || !wi || !wi->connected || !strcmp(wi->team, text)) break;
+                if((!smode || smode->canchangeteam(wi, wi->team, text)) && addteaminfo(text))
+                {
+                    if(wi->state.state==CS_ALIVE) suicide(wi);
+                    copystring(wi->team, text, MAXTEAMLEN+1);
+                }
+                aiman::changeteam(wi);
+                sendf(-1, 1, "riisi", N_SETTEAM, who, wi->team, 1);
+                break;
+            }
+
+            case N_FORCEINTERMISSION:
+                if(ci->local && !hasnonlocalclients()) startintermission();
+                break;
+
+            case N_RECORDDEMO:
+            {
+                int val = getint(p);
+                if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break;
+                if(!maxdemos || !maxdemosize) 
+                {
+                    sendf(ci->clientnum, 1, "ris", N_SERVMSG, "the server has disabled demo recording");
+                    break;
+                }
+                demonextmatch = val!=0;
+                sendservmsgf("demo recording is %s for next match", demonextmatch ? "enabled" : "disabled");
+                break;
+            }
+
+            case N_STOPDEMO:
+            {
+                if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break;
+                stopdemo();
+                break;
+            }
+
+            case N_CLEARDEMOS:
+            {
+                int demo = getint(p);
+                if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break;
+                cleardemos(demo);
+                break;
+            }
+
+            case N_LISTDEMOS:
+                if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break;
+                listdemos(sender);
+                break;
+
+            case N_GETDEMO:
+            {
+                int n = getint(p), tag = getint(p);
+                if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break;
+                senddemo(ci, n, tag);
+                break;
+            }
+
+            case N_GETMAP:
+                if(!mapdata) sendf(sender, 1, "ris", N_SERVMSG, "no map to send");
+                else if(ci->getmap) sendf(sender, 1, "ris", N_SERVMSG, "already sending map");
+                else
+                {
+                    sendservmsgf("[%s is getting the map]", colorname(ci));
+                    if((ci->getmap = sendfile(sender, 2, mapdata, "ri", N_SENDMAP)))
+                        ci->getmap->freeCallback = freegetmap;
+                    ci->needclipboard = totalmillis ? totalmillis : 1;
+                }
+                break;
+
+            case N_NEWMAP:
+            {
+                int size = getint(p);
+                if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break;
+                if(size>=0)
+                {
+                    smapname[0] = '\0';
+                    resetitems();
+                    notgotitems = false;
+                    if(smode) smode->newmap();
+                }
+                QUEUE_MSG;
+                break;
+            }
+
+            case N_SETMASTER:
+            {
+                int mn = getint(p), val = getint(p);
+                getstring(text, p);
+                if(mn != ci->clientnum)
+                {
+                    if(!ci->privilege && !ci->local) break;
+                    clientinfo *minfo = (clientinfo *)getclientinfo(mn);
+                    if(!minfo || !minfo->connected || (!ci->local && minfo->privilege >= ci->privilege) || (val && minfo->privilege)) break;
+                    setmaster(minfo, val!=0, "", NULL, NULL, PRIV_MASTER, true);
+                }
+                else setmaster(ci, val!=0, text);
+                // don't broadcast the master password
+                break;
+            }
+
+            case N_ADDBOT:
+            {
+                aiman::reqadd(ci, getint(p));
+                break;
+            }
+
+            case N_DELBOT:
+            {
+                aiman::reqdel(ci);
+                break;
+            }
+
+            case N_BOTLIMIT:
+            {
+                int limit = getint(p);
+                if(ci) aiman::setbotlimit(ci, limit);
+                break;
+            }
+
+            case N_BOTBALANCE:
+            {
+                int balance = getint(p);
+                if(ci) aiman::setbotbalance(ci, balance!=0);
+                break;
+            }
+
+            case N_AUTHTRY:
+            {
+                string desc, name;
+                getstring(desc, p, sizeof(desc));
+                getstring(name, p, sizeof(name));
+                tryauth(ci, name, desc);
+                break;
+            }
+
+            case N_AUTHKICK:
+            {
+                string desc, name;
+                getstring(desc, p, sizeof(desc));
+                getstring(name, p, sizeof(name));
+                int victim = getint(p);
+                getstring(text, p);
+                filtertext(text, text);
+                int authpriv = PRIV_AUTH;
+                if(desc[0])
+                {
+                    userinfo *u = users.access(userkey(name, desc));
+                    if(u) authpriv = u->privilege; else break;
+                }
+                if(ci->local || ci->privilege >= authpriv) trykick(ci, victim, text);
+                else if(trykick(ci, victim, text, name, desc, authpriv, true) && tryauth(ci, name, desc))
+                {
+                    ci->authkickvictim = victim;
+                    ci->authkickreason = newstring(text);
+                } 
+                break;
+            }
+
+            case N_AUTHANS:
+            {
+                string desc, ans;
+                getstring(desc, p, sizeof(desc));
+                uint id = (uint)getint(p);
+                getstring(ans, p, sizeof(ans));
+                answerchallenge(ci, id, ans, desc);
+                break;
+            }
+
+            case N_PAUSEGAME:
+            {
+                int val = getint(p);
+                if(ci->privilege < (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break;
+                pausegame(val > 0, ci);
+                break;
+            }
+
+            case N_GAMESPEED:
+            {
+                int val = getint(p);
+                if(ci->privilege < (restrictgamespeed ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break;
+                changegamespeed(val, ci);
+                break;
+            }
+
+            case N_COPY:
+                ci->cleanclipboard();
+                ci->lastclipboard = totalmillis ? totalmillis : 1;
+                goto genericmsg;
+
+            case N_PASTE:
+                if(ci->state.state!=CS_SPECTATOR) sendclipboard(ci);
+                goto genericmsg;
+    
+            case N_CLIPBOARD:
+            {
+                int unpacklen = getint(p), packlen = getint(p); 
+                ci->cleanclipboard(false);
+                if(ci->state.state==CS_SPECTATOR)
+                {
+                    if(packlen > 0) p.subbuf(packlen);
+                    break;
+                }
+                if(packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) 
+                {
+                    if(packlen > 0) p.subbuf(packlen);
+                    packlen = unpacklen = 0;
+                }
+                packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE);
+                putint(q, N_CLIPBOARD);
+                putint(q, ci->clientnum);
+                putint(q, unpacklen);
+                putint(q, packlen); 
+                if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen);
+                ci->clipboard = q.finalize();
+                ci->clipboard->referenceCount++;
+                break;
+            } 
+
+            case N_EDITT:
+            case N_REPLACE:
+            case N_EDITVSLOT:
+            {
+                int size = server::msgsizelookup(type);
+                if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; }
+                loopi(size-1) getint(p);
+                if(p.remaining() < 2) { disconnect_client(sender, DISC_MSGERR); return; }
+                int extra = lilswap(*(const ushort *)p.pad(2));
+                if(p.remaining() < extra) { disconnect_client(sender, DISC_MSGERR); return; }
+                p.pad(extra);
+                if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG;
+                break;
+            }
+
+            case N_UNDO:
+            case N_REDO:
+            {
+                int unpacklen = getint(p), packlen = getint(p);
+                if(!ci || ci->state.state==CS_SPECTATOR || packlen <= 0 || packlen > (1<<16) || unpacklen <= 0)
+                {
+                    if(packlen > 0) p.subbuf(packlen);
+                    break;
+                }
+                if(p.remaining() < packlen) { disconnect_client(sender, DISC_MSGERR); return; }
+                packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE);
+                putint(q, type);
+                putint(q, ci->clientnum);
+                putint(q, unpacklen);
+                putint(q, packlen);
+                if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen);
+                sendpacket(-1, 1, q.finalize(), ci->clientnum);
+                break;
+            }
+
+            case N_SERVCMD:
+                getstring(text, p);
+                break;
+                     
+            #define PARSEMESSAGES 1
+            #include "capture.h"
+            #include "ctf.h"
+            #include "collect.h"
+            #undef PARSEMESSAGES
+
+            case -1:
+                disconnect_client(sender, DISC_MSGERR);
+                return;
+
+            case -2:
+                disconnect_client(sender, DISC_OVERFLOW);
+                return;
+
+            default: genericmsg:
+            {
+                int size = server::msgsizelookup(type);
+                if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; }
+                loopi(size-1) getint(p);
+                if(ci) switch(msgfilter[type])
+                {
+                    case 2: case 3: if(ci->state.state != CS_SPECTATOR) QUEUE_MSG; break;
+                    default: if(cq && (ci != cq || ci->state.state!=CS_SPECTATOR)) { QUEUE_AI; QUEUE_MSG; } break;
+                }
+                break;
+            }
+        }
+    }
+
+    int laninfoport() { return SAUERBRATEN_LANINFO_PORT; }
+    int serverinfoport(int servport) { return servport < 0 ? SAUERBRATEN_SERVINFO_PORT : servport+1; }
+    int serverport(int infoport) { return infoport < 0 ? SAUERBRATEN_SERVER_PORT : infoport-1; }
+    const char *defaultmaster() { return "master.sauerbraten.org"; }
+    int masterport() { return SAUERBRATEN_MASTER_PORT; }
+    int numchannels() { return 3; }
+
+    #include "extinfo.h"
+
+    void serverinforeply(ucharbuf &req, ucharbuf &p)
+    {
+        if(req.remaining() && !getint(req))
+        {
+            extserverinforeply(req, p);
+            return;
+        }
+
+        putint(p, numclients(-1, false, true));
+        putint(p, gamepaused || gamespeed != 100 ? 7 : 5);                   // number of attrs following
+        putint(p, PROTOCOL_VERSION);    // generic attributes, passed back below
+        putint(p, gamemode);
+        putint(p, m_timed ? max((gamelimit - gamemillis)/1000, 0) : 0);
+        putint(p, maxclients);
+        putint(p, serverpass[0] ? MM_PASSWORD : (!m_mp(gamemode) ? MM_PRIVATE : (mastermode || mastermask&MM_AUTOAPPROVE ? mastermode : MM_AUTH)));
+        if(gamepaused || gamespeed != 100)
+        {
+            putint(p, gamepaused ? 1 : 0);
+            putint(p, gamespeed);
+        }
+        sendstring(smapname, p);
+        sendstring(serverdesc, p);
+        sendserverinforeply(p);
+    }
+
+    bool servercompatible(char *name, char *sdec, char *map, int ping, const vector<int> &attr, int np)
+    {
+        return attr.length() && attr[0]==PROTOCOL_VERSION;
+    }
+
+    #include "aiman.h"
+}
+
diff --git a/src/fpsgame/waypoint.cpp b/src/fpsgame/waypoint.cpp
new file mode 100644 (file)
index 0000000..ed504e9
--- /dev/null
@@ -0,0 +1,808 @@
+#include "game.h"
+
+extern selinfo sel;
+
+namespace ai
+{
+    using namespace game;
+
+    vector<waypoint> waypoints;
+
+    bool clipped(const vec &o)
+    {
+        int material = lookupmaterial(o), clipmat = material&MATF_CLIP;
+        return clipmat == MAT_CLIP || material&MAT_DEATH || (material&MATF_VOLUME) == MAT_LAVA;
+    }
+
+    int getweight(const vec &o)
+    {
+        vec pos = o; pos.z += ai::JUMPMIN;
+        if(!insideworld(vec(pos.x, pos.y, min(pos.z, getworldsize() - 1e-3f)))) return -2;
+        float dist = raycube(pos, vec(0, 0, -1), 0, RAY_CLIPMAT);
+        int posmat = lookupmaterial(pos), weight = 1;
+        if(isliquid(posmat&MATF_VOLUME)) weight *= 5;
+        if(dist >= 0)
+        {
+            weight = int(dist/ai::JUMPMIN);
+            pos.z -= clamp(dist-8.0f, 0.0f, pos.z);
+            int trgmat = lookupmaterial(pos);
+            if(trgmat&MAT_DEATH || (trgmat&MATF_VOLUME) == MAT_LAVA) weight *= 10;
+            else if(isliquid(trgmat&MATF_VOLUME)) weight *= 2;
+        }
+        return weight;
+    }
+
+    enum
+    {
+        WPCACHE_STATIC = 0,
+        WPCACHE_DYNAMIC,
+        NUMWPCACHES
+    };
+
+    struct wpcache
+    {
+        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; }
+        };
+
+        vector<node> nodes;
+        int firstwp, lastwp;
+        vec bbmin, bbmax;
+
+        wpcache() { clear(); }
+
+        void clear()
+        {
+            nodes.setsize(0);
+            firstwp = lastwp = -1;
+            bbmin = vec(1e16f, 1e16f, 1e16f);
+            bbmax = vec(-1e16f, -1e16f, -1e16f);
+        }
+
+        void build(int first = 0, int last = -1)
+        {
+            if(last < 0) last = waypoints.length();
+            vector<int> indices;
+            for(int i = first; i < last; i++)
+            {
+                waypoint &w = waypoints[i];
+                indices.add(i);
+                if(firstwp < 0) firstwp = i;
+                float radius = WAYPOINTRADIUS;
+                bbmin.min(vec(w.o).sub(radius));
+                bbmax.max(vec(w.o).add(radius));
+            }
+            if(first < last) lastwp = max(lastwp, last-1);
+            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 axis = 2;
+            loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k;
+
+            vec leftmin(1e16f, 1e16f, 1e16f), leftmax(-1e16f, -1e16f, -1e16f), rightmin(1e16f, 1e16f, 1e16f), rightmax(-1e16f, -1e16f, -1e16f);
+            float split = 0.5f*(vmax[axis] + vmin[axis]), splitleft = -1e16f, splitright = 1e16f;
+            int left, right;
+            for(left = 0, right = numindices; left < right;)
+            {
+                waypoint &w = waypoints[indices[left]];
+                float radius = WAYPOINTRADIUS;
+                if(max(split - (w.o[axis]-radius), 0.0f) > max((w.o[axis]+radius) - split, 0.0f))
+                {
+                    ++left;
+                    splitleft = max(splitleft, w.o[axis]+radius);
+                    leftmin.min(vec(w.o).sub(radius));
+                    leftmax.max(vec(w.o).add(radius));
+                }
+                else
+                {
+                    --right;
+                    swap(indices[left], indices[right]);
+                    splitright = min(splitright, w.o[axis]-radius);
+                    rightmin.min(vec(w.o).sub(radius));
+                    rightmax.max(vec(w.o).add(radius));
+                }
+            }
+
+            if(!left || right==numindices)
+            {
+                leftmin = rightmin = vec(1e16f, 1e16f, 1e16f);
+                leftmax = rightmax = vec(-1e16f, -1e16f, -1e16f);
+                left = right = numindices/2;
+                splitleft = -1e16f;
+                splitright = 1e16f;
+                loopi(numindices)
+                {
+                    waypoint &w = waypoints[indices[i]];
+                    float radius = WAYPOINTRADIUS;
+                    if(i < left)
+                    {
+                        splitleft = max(splitleft, w.o[axis]+radius);
+                        leftmin.min(vec(w.o).sub(radius));
+                        leftmax.max(vec(w.o).add(radius));
+                    }
+                    else
+                    {
+                        splitright = min(splitright, w.o[axis]-radius);
+                        rightmin.min(vec(w.o).sub(radius));
+                        rightmax.max(vec(w.o).add(radius));
+                    }
+                }
+            }
+
+            int offset = nodes.length();
+            node &curnode = nodes.add();
+            curnode.split[0] = splitleft;
+            curnode.split[1] = splitright;
+
+            if(left<=1) curnode.child[0] = (axis<<30) | (left>0 ? indices[0] : 0x3FFFFFFF);
+            else
+            {
+                curnode.child[0] = (axis<<30) | (nodes.length()-offset);
+                if(left) build(indices, left, leftmin, leftmax);
+            }
+
+            if(numindices-right<=1) curnode.child[1] = (1<<31) | (left<=1 ? 1<<30 : 0) | (numindices-right>0 ? indices[right] : 0x3FFFFFFF);
+            else
+            {
+                curnode.child[1] = (left<=1 ? 1<<30 : 0) | (nodes.length()-offset);
+                if(numindices-right) build(&indices[right], numindices-right, rightmin, rightmax);
+            }
+        }
+    } wpcaches[NUMWPCACHES];
+
+    static int invalidatedwpcaches = 0, clearedwpcaches = (1<<NUMWPCACHES)-1, numinvalidatewpcaches = 0, lastwpcache = 0;
+
+    static inline void invalidatewpcache(int wp)
+    {
+        if(++numinvalidatewpcaches >= 1000) { numinvalidatewpcaches = 0; invalidatedwpcaches = (1<<NUMWPCACHES)-1; }
+        else
+        {
+            loopi(WPCACHE_DYNAMIC) if(wp >= wpcaches[i].firstwp && wp <= wpcaches[i].lastwp) { invalidatedwpcaches |= 1<<i; return; }
+            invalidatedwpcaches |= 1<<WPCACHE_DYNAMIC;
+        }
+    }
+
+    void clearwpcache(bool full = true)
+    {
+        loopi(NUMWPCACHES) if(full || invalidatedwpcaches&(1<<i)) { wpcaches[i].clear(); clearedwpcaches |= 1<<i; }
+        if(full || invalidatedwpcaches == (1<<NUMWPCACHES)-1)
+        {
+            numinvalidatewpcaches = 0;
+            lastwpcache = 0;
+        }
+        invalidatedwpcaches = 0;
+    }
+    ICOMMAND(clearwpcache, "", (), clearwpcache());
+
+    avoidset wpavoid;
+
+    void buildwpcache()
+    {
+        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();
+
+        wpavoid.clear();
+               loopv(waypoints) if(waypoints[i].weight < 0) wpavoid.avoidnear(NULL, waypoints[i].o.z + WAYPOINTRADIUS, waypoints[i].o, WAYPOINTRADIUS);
+    }
+
+    struct wpcachestack
+    {
+        wpcache::node *node;
+        float tmin, tmax;
+    };
+
+    vector<wpcache::node *> wpcachestack;
+
+    int closestwaypoint(const vec &pos, float mindist, bool links, fpsent *d)
+    {
+        if(waypoints.empty()) return -1;
+        if(clearedwpcaches) buildwpcache();
+
+        #define CHECKCLOSEST(index) do { \
+            int n = (index); \
+            if(n < waypoints.length()) \
+            { \
+                const waypoint &w = waypoints[n]; \
+                if(!links || w.links[0]) \
+                { \
+                    float dist = w.o.squaredist(pos); \
+                    if(dist < mindist*mindist) { closest = n; mindist = sqrtf(dist); } \
+                } \
+            } \
+        } while(0)
+        int closest = -1;
+        wpcache::node *curnode;
+        loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        {
+            int axis = curnode->axis();
+            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
+            if(dist1 >= mindist)
+            {
+                if(dist2 < mindist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKCLOSEST(curnode->childindex(1));
+                }
+            }
+            else if(curnode->isleaf(0))
+            {
+                CHECKCLOSEST(curnode->childindex(0));
+                if(dist2 < mindist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKCLOSEST(curnode->childindex(1));
+                }
+            }
+            else
+            {
+                if(dist2 < mindist)
+                {
+                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
+                    else CHECKCLOSEST(curnode->childindex(1));
+                }
+                curnode += curnode->childindex(0);
+                continue;
+            }
+            if(wpcachestack.empty()) break;
+            curnode = wpcachestack.pop();
+        }
+        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKCLOSEST(i); }
+        return closest;
+    }
+
+    void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results)
+    {
+        if(waypoints.empty()) return;
+        if(clearedwpcaches) buildwpcache();
+
+        float mindist2 = mindist*mindist, maxdist2 = maxdist*maxdist;
+        #define CHECKWITHIN(index) do { \
+            int n = (index); \
+            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)
+        wpcache::node *curnode;
+        loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        {
+            int axis = curnode->axis();
+            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
+            if(dist1 >= maxdist)
+            {
+                if(dist2 < maxdist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKWITHIN(curnode->childindex(1));
+                }
+            }
+            else if(curnode->isleaf(0))
+            {
+                CHECKWITHIN(curnode->childindex(0));
+                if(dist2 < maxdist)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKWITHIN(curnode->childindex(1));
+                }
+            }
+            else
+            {
+                if(dist2 < maxdist)
+                {
+                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
+                    else CHECKWITHIN(curnode->childindex(1));
+                }
+                curnode += curnode->childindex(0);
+                continue;
+            }
+            if(wpcachestack.empty()) break;
+            curnode = wpcachestack.pop();
+        }
+        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKWITHIN(i); }
+    }
+
+    void avoidset::avoidnear(void *owner, float above, const vec &pos, float limit)
+    {
+        if(ai::waypoints.empty()) return;
+        if(clearedwpcaches) buildwpcache();
+
+        float limit2 = limit*limit;
+        #define CHECKNEAR(index) do { \
+            int n = (index); \
+            if(n < ai::waypoints.length()) \
+            { \
+                const waypoint &w = ai::waypoints[n]; \
+                if(w.o.squaredist(pos) < limit2) add(owner, above, n); \
+            } \
+        } while(0)
+        wpcache::node *curnode;
+        loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;)
+        {
+            int axis = curnode->axis();
+            float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis];
+            if(dist1 >= limit)
+            {
+                if(dist2 < limit)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKNEAR(curnode->childindex(1));
+                }
+            }
+            else if(curnode->isleaf(0))
+            {
+                CHECKNEAR(curnode->childindex(0));
+                if(dist2 < limit)
+                {
+                    if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; }
+                    CHECKNEAR(curnode->childindex(1));
+                }
+            }
+            else
+            {
+                if(dist2 < limit)
+                {
+                    if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1));
+                    else CHECKNEAR(curnode->childindex(1));
+                }
+                curnode += curnode->childindex(0);
+                continue;
+            }
+            if(wpcachestack.empty()) break;
+            curnode = wpcachestack.pop();
+        }
+        for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKNEAR(i); }
+    }
+
+    int avoidset::remap(fpsent *d, int n, vec &pos, bool retry)
+    {
+        if(!obstacles.empty())
+        {
+            int cur = 0;
+            loopv(obstacles)
+            {
+                obstacle &ob = obstacles[i];
+                int next = cur + ob.numwaypoints;
+                if(ob.owner != d)
+                {
+                    for(; cur < next; cur++) if(waypoints[cur] == n)
+                    {
+                        if(ob.above < 0) return retry ? n : -1;
+                        vec above(pos.x, pos.y, ob.above);
+                        if(above.z-d->o.z >= ai::JUMPMAX)
+                            return retry ? n : -1; // too much scotty
+                        int node = closestwaypoint(above, ai::SIGHTMIN, true, d);
+                        if(ai::iswaypoint(node) && node != n)
+                        { // try to reroute above their head?
+                            if(!find(node, d))
+                            {
+                                pos = ai::waypoints[node].o;
+                                return node;
+                            }
+                            else return retry ? n : -1;
+                        }
+                        else
+                        {
+                            vec old = d->o;
+                            d->o = vec(above).add(vec(0, 0, d->eyeheight));
+                            bool col = collide(d, vec(0, 0, 1));
+                            d->o = old;
+                            if(!col)
+                            {
+                                pos = above;
+                                return n;
+                            }
+                            else return retry ? n : -1;
+                        }
+                    }
+                }
+                cur = next;
+            }
+        }
+        return n;
+    }
+
+    static inline float heapscore(waypoint *q) { return q->score(); }
+
+    bool route(fpsent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries)
+    {
+        if(waypoints.empty() || !iswaypoint(node) || !iswaypoint(goal) || goal == node || !waypoints[node].links[0])
+            return false;
+
+        static ushort routeid = 1;
+        static vector<waypoint *> queue;
+
+        if(!routeid)
+        {
+            loopv(waypoints) waypoints[i].route = 0;
+            routeid = 1;
+        }
+
+        if(d)
+        {
+            if(retries <= 1 && d->ai) loopi(ai::NUMPREVNODES) if(d->ai->prevnodes[i] != node && iswaypoint(d->ai->prevnodes[i]))
+            {
+                waypoints[d->ai->prevnodes[i]].route = routeid;
+                waypoints[d->ai->prevnodes[i]].curscore = -1;
+                waypoints[d->ai->prevnodes[i]].estscore = 0;
+            }
+                       if(retries <= 0)
+                       {
+                               loopavoid(obstacles, d,
+                               {
+                                       if(iswaypoint(wp) && wp != node && wp != goal && waypoints[node].find(wp) < 0 && waypoints[goal].find(wp) < 0)
+                                       {
+                                               waypoints[wp].route = routeid;
+                                               waypoints[wp].curscore = -1;
+                                               waypoints[wp].estscore = 0;
+                                       }
+                               });
+                       }
+        }
+
+        waypoints[node].route = routeid;
+        waypoints[node].curscore = waypoints[node].estscore = 0;
+        waypoints[node].prev = 0;
+        queue.setsize(0);
+        queue.add(&waypoints[node]);
+        route.setsize(0);
+
+        int lowest = -1;
+        while(!queue.empty())
+        {
+            waypoint &m = *queue.removeheap();
+            float prevscore = m.curscore;
+            m.curscore = -1;
+            loopi(MAXWAYPOINTLINKS)
+            {
+                int link = m.links[i];
+                if(!link) break;
+                if(iswaypoint(link) && (link == node || link == goal || waypoints[link].links[0]))
+                {
+                    waypoint &n = waypoints[link];
+                    int weight = max(n.weight, 1);
+                    float curscore = prevscore + n.o.dist(m.o)*weight;
+                    if(n.route == routeid && curscore >= n.curscore) continue;
+                    n.curscore = curscore;
+                    n.prev = ushort(&m - &waypoints[0]);
+                    if(n.route != routeid)
+                    {
+                        n.estscore = n.o.dist(waypoints[goal].o)*weight;
+                        if(n.estscore <= WAYPOINTRADIUS*4 && (lowest < 0 || n.estscore <= waypoints[lowest].estscore))
+                            lowest = link;
+                        n.route = routeid;
+                        if(link == goal) goto foundgoal;
+                        queue.addheap(&n);
+                    }
+                    else loopvj(queue) if(queue[j] == &n) { queue.upheap(j); break; }
+                }
+            }
+        }
+        foundgoal:
+
+        routeid++;
+
+        if(lowest >= 0) // otherwise nothing got there
+        {
+            for(waypoint *m = &waypoints[lowest]; m > &waypoints[0]; m = &waypoints[m->prev])
+                route.add(m - &waypoints[0]); // just keep it stored backward
+        }
+
+        return !route.empty();
+    }
+
+    VARF(dropwaypoints, 0, 0, 1, { player1->lastnode = -1; });
+
+    int addwaypoint(const vec &o, int weight = -1)
+    {
+        if(waypoints.length() > MAXWAYPOINTS) return -1;
+        int n = waypoints.length();
+        waypoints.add(waypoint(o, weight >= 0 ? weight : getweight(o)));
+        invalidatewpcache(n);
+        return n;
+    }
+
+    void linkwaypoint(waypoint &a, int n)
+    {
+        loopi(MAXWAYPOINTLINKS)
+        {
+            if(a.links[i] == n) return;
+            if(!a.links[i]) { a.links[i] = n; return; }
+        }
+        a.links[rnd(MAXWAYPOINTLINKS)] = n;
+    }
+
+    string loadedwaypoints = "";
+
+    static inline bool shouldnavigate()
+    {
+        if(dropwaypoints) return true;
+        loopvrev(players) if(players[i]->aitype != AI_NONE) return true;
+        return false;
+    }
+
+    static inline bool shoulddrop(fpsent *d)
+    {
+        return !d->ai && (dropwaypoints || !loadedwaypoints[0]);
+    }
+
+    void inferwaypoints(fpsent *d, const vec &o, const vec &v, float mindist)
+    {
+        if(!shouldnavigate()) return;
+       if(shoulddrop(d))
+       {
+                       if(waypoints.empty()) seedwaypoints();
+                       int from = closestwaypoint(o, mindist, false), to = closestwaypoint(v, mindist, false);
+                       if(!iswaypoint(from)) from = addwaypoint(o);
+                       if(!iswaypoint(to)) to = addwaypoint(v);
+                       if(d->lastnode != from && iswaypoint(d->lastnode) && iswaypoint(from))
+                               linkwaypoint(waypoints[d->lastnode], from);
+                       if(iswaypoint(to))
+                       {
+                               if(from != to && iswaypoint(from) && iswaypoint(to))
+                                       linkwaypoint(waypoints[from], to);
+                               d->lastnode = to;
+                       }
+               }
+               else d->lastnode = closestwaypoint(v, WAYPOINTRADIUS*2, false, d);
+    }
+
+    void navigate(fpsent *d)
+    {
+        vec v(d->feetpos());
+        if(d->state != CS_ALIVE) { d->lastnode = -1; return; }
+        bool dropping = shoulddrop(d);
+        int mat = lookupmaterial(v);
+        if((mat&MATF_CLIP) == MAT_CLIP || (mat&MATF_VOLUME) == MAT_LAVA || mat&MAT_DEATH) dropping = false;
+        float dist = dropping ? WAYPOINTRADIUS : (d->ai ? WAYPOINTRADIUS : SIGHTMIN);
+        int curnode = closestwaypoint(v, dist, false, d), prevnode = d->lastnode;
+        if(!iswaypoint(curnode) && dropping)
+        {
+                       if(waypoints.empty()) seedwaypoints();
+               curnode = addwaypoint(v);
+        }
+        if(iswaypoint(curnode))
+        {
+            if(dropping && d->lastnode != curnode && iswaypoint(d->lastnode))
+            {
+                linkwaypoint(waypoints[d->lastnode], curnode);
+                if(!d->timeinair) linkwaypoint(waypoints[curnode], d->lastnode);
+            }
+            d->lastnode = curnode;
+            if(d->ai && iswaypoint(prevnode) && d->lastnode != prevnode) d->ai->addprevnode(prevnode);
+        }
+        else if(!iswaypoint(d->lastnode) || waypoints[d->lastnode].o.squaredist(v) > SIGHTMIN*SIGHTMIN)
+                       d->lastnode = closestwaypoint(v, SIGHTMAX, false, d);
+    }
+
+    void navigate()
+    {
+       if(shouldnavigate()) loopv(players) ai::navigate(players[i]);
+        if(invalidatedwpcaches) clearwpcache(false);
+    }
+
+    void clearwaypoints(bool full)
+    {
+        waypoints.setsize(0);
+        clearwpcache();
+        if(full)
+        {
+            loadedwaypoints[0] = '\0';
+            dropwaypoints = 0;
+        }
+    }
+    ICOMMAND(clearwaypoints, "", (), clearwaypoints());
+
+    void seedwaypoints()
+    {
+        if(waypoints.empty()) addwaypoint(vec(0, 0, 0));
+        loopv(entities::ents)
+        {
+            extentity &e = *entities::ents[i];
+            switch(e.type)
+            {
+                case PLAYERSTART: case TELEPORT: case JUMPPAD: case FLAG: case BASE:
+                    addwaypoint(e.o);
+                    break;
+                default:
+                    if(e.type >= I_SHELLS && e.type <= I_QUAD) addwaypoint(e.o);
+                    break;
+            }
+        }
+    }
+
+    void remapwaypoints()
+    {
+        vector<ushort> remap;
+        int total = 0;
+        loopv(waypoints) remap.add(waypoints[i].links[1] == 0xFFFF ? 0 : total++);
+        total = 0;
+        loopvj(waypoints)
+        {
+            if(waypoints[j].links[1] == 0xFFFF) continue;
+            waypoint &w = waypoints[total];
+            if(j != total) w = waypoints[j];
+            int k = 0;
+            loopi(MAXWAYPOINTLINKS)
+            {
+                int link = w.links[i];
+                if(!link) break;
+                if((w.links[k] = remap[link])) k++;
+            }
+            if(k < MAXWAYPOINTLINKS) w.links[k] = 0;
+            total++;
+        }
+        waypoints.setsize(total);
+    }
+
+    bool cleanwaypoints()
+    {
+        int cleared = 0;
+        for(int i = 1; i < waypoints.length(); i++)
+        {
+            waypoint &w = waypoints[i];
+            if(clipped(w.o))
+            {
+                w.links[0] = 0;
+                w.links[1] = 0xFFFF;
+                cleared++;
+            }
+        }
+        if(cleared)
+        {
+            player1->lastnode = -1;
+            loopv(players) if(players[i]) players[i]->lastnode = -1;
+            remapwaypoints();
+            clearwpcache();
+            return true;
+        }
+        return false;
+    }
+
+    bool getwaypointfile(const char *mname, char *wptname)
+    {
+        if(!mname || !*mname) mname = getclientmap();
+        if(!*mname) return false;
+
+        string pakname, mapname, cfgname;
+        getmapfilenames(mname, NULL, pakname, mapname, cfgname);
+        nformatstring(wptname, MAXSTRLEN, "packages/%s.wpt", mapname);
+        path(wptname);
+        return true;
+    }
+
+    void loadwaypoints(bool force, const char *mname)
+    {
+        string wptname;
+        if(!getwaypointfile(mname, wptname)) return;
+        if(!force && (waypoints.length() || !strcmp(loadedwaypoints, wptname))) return;
+
+        stream *f = opengzfile(wptname, "rb");
+        if(!f) return;
+        char magic[4];
+        if(f->read(magic, 4) < 4 || memcmp(magic, "OWPT", 4)) { delete f; return; }
+
+        copystring(loadedwaypoints, wptname);
+
+        waypoints.setsize(0);
+        waypoints.add(vec(0, 0, 0));
+        ushort numwp = f->getlil<ushort>();
+        loopi(numwp)
+        {
+            if(f->end()) break;
+            vec o;
+            o.x = f->getlil<float>();
+            o.y = f->getlil<float>();
+            o.z = f->getlil<float>();
+            waypoint &w = waypoints.add(waypoint(o, getweight(o)));
+            int numlinks = f->getchar(), k = 0;
+            loopi(numlinks)
+            {
+                if((w.links[k] = f->getlil<ushort>()))
+                {
+                    if(++k >= MAXWAYPOINTLINKS) break;
+                }
+            }
+        }
+
+        delete f;
+        conoutf("loaded %d waypoints from %s", numwp, wptname);
+
+        if(!cleanwaypoints()) clearwpcache();
+    }
+    ICOMMAND(loadwaypoints, "s", (char *mname), loadwaypoints(true, mname));
+
+    void savewaypoints(bool force, const char *mname)
+    {
+        if((!dropwaypoints && !force) || waypoints.empty()) return;
+
+        string wptname;
+        if(!getwaypointfile(mname, wptname)) return;
+
+        stream *f = opengzfile(wptname, "wb");
+        if(!f) return;
+        f->write("OWPT", 4);
+        f->putlil<ushort>(waypoints.length()-1);
+        for(int i = 1; i < waypoints.length(); i++)
+        {
+            waypoint &w = waypoints[i];
+            f->putlil<float>(w.o.x);
+            f->putlil<float>(w.o.y);
+            f->putlil<float>(w.o.z);
+            int numlinks = 0;
+            loopj(MAXWAYPOINTLINKS) { if(!w.links[j]) break; numlinks++; }
+            f->putchar(numlinks);
+            loopj(numlinks) f->putlil<ushort>(w.links[j]);
+        }
+
+        delete f;
+        conoutf("saved %d waypoints to %s", waypoints.length()-1, wptname);
+    }
+
+    ICOMMAND(savewaypoints, "s", (char *mname), savewaypoints(true, mname));
+
+    void delselwaypoints()
+    {
+        if(noedit(true)) return;
+        vec o = vec(sel.o).sub(0.1f), s = vec(sel.s).mul(sel.grid).add(o).add(0.1f);
+        int cleared = 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.links[0] = 0;
+                w.links[1] = 0xFFFF;
+                cleared++;
+            }
+        }
+        if(cleared)
+        {
+            player1->lastnode = -1;
+            remapwaypoints();
+            clearwpcache();
+        }
+    }
+    COMMAND(delselwaypoints, "");
+
+    void movewaypoints(const vec &d)
+    {
+        if(noedit(true)) return;
+        int worldsize = getworldsize();
+        if(d.x < -worldsize || d.x > worldsize || d.y < -worldsize || d.y > worldsize || d.z < -worldsize || d.z > worldsize)
+        {
+            clearwaypoints();
+            return;
+        }
+        int cleared = 0;
+        for(int i = 1; i < waypoints.length(); i++)
+        {
+            waypoint &w = waypoints[i];
+            w.o.add(d);
+            if(!insideworld(w.o)) { w.links[0] = 0; w.links[1] = 0xFFFF; cleared++; }
+        }
+        if(cleared)
+        {
+            player1->lastnode = -1;
+            remapwaypoints();
+        }
+        clearwpcache();
+    }
+    ICOMMAND(movewaypoints, "iii", (int *dx, int *dy, int *dz), movewaypoints(vec(*dx, *dy, *dz)));
+}
+
diff --git a/src/fpsgame/weapon.cpp b/src/fpsgame/weapon.cpp
new file mode 100644 (file)
index 0000000..3c7f575
--- /dev/null
@@ -0,0 +1,1003 @@
+// weapon.cpp: all shooting and effects code, projectile management
+#include "game.h"
+
+namespace game
+{
+    static const int MONSTERDAMAGEFACTOR = 4;
+    static const int OFFSETMILLIS = 500;
+    vec rays[MAXRAYS];
+
+    struct hitmsg
+    {
+        int target, lifesequence, info1, info2;
+        ivec dir;
+    };
+    vector<hitmsg> hits;
+
+    VARP(maxdebris, 10, 25, 1000);
+    VARP(maxbarreldebris, 5, 10, 1000);
+
+    ICOMMAND(getweapon, "", (), intret(player1->gunselect));
+
+    void gunselect(int gun, fpsent *d)
+    {
+        if(gun!=d->gunselect)
+        {
+            addmsg(N_GUNSELECT, "rci", d, gun);
+            playsound(S_WEAPLOAD, d == player1 ? NULL : &d->o);
+        }
+        d->gunselect = gun;
+    }
+
+    void nextweapon(int dir, bool force = false)
+    {
+        if(player1->state!=CS_ALIVE) return;
+        dir = (dir < 0 ? NUMGUNS-1 : 1);
+        int gun = player1->gunselect;
+        loopi(NUMGUNS)
+        {
+            gun = (gun + dir)%NUMGUNS;
+            if(force || player1->ammo[gun]) break;
+        }
+        if(gun != player1->gunselect) gunselect(gun, player1);
+        else playsound(S_NOAMMO);
+    }
+    ICOMMAND(nextweapon, "ii", (int *dir, int *force), nextweapon(*dir, *force!=0));
+
+    int getweapon(const char *name)
+    {
+        const char *abbrevs[] = { "FI", "SG", "CG", "RL", "RI", "GL", "PI" };
+        if(isdigit(name[0])) return parseint(name);
+        else loopi(sizeof(abbrevs)/sizeof(abbrevs[0])) if(!strcasecmp(abbrevs[i], name)) return i;
+        return -1;
+    }
+
+    void setweapon(const char *name, bool force = false)
+    {
+        int gun = getweapon(name);
+        if(player1->state!=CS_ALIVE || gun<GUN_FIST || gun>GUN_PISTOL) return;
+        if(force || player1->ammo[gun]) gunselect(gun, player1);
+        else playsound(S_NOAMMO);
+    }
+    ICOMMAND(setweapon, "si", (char *name, int *force), setweapon(name, *force!=0));
+
+    void cycleweapon(int numguns, int *guns, bool force = false)
+    {
+        if(numguns<=0 || player1->state!=CS_ALIVE) return;
+        int offset = 0;
+        loopi(numguns) if(guns[i] == player1->gunselect) { offset = i+1; break; }
+        loopi(numguns)
+        {
+            int gun = guns[(i+offset)%numguns];
+            if(gun>=0 && gun<NUMGUNS && (force || player1->ammo[gun]))
+            {
+                gunselect(gun, player1);
+                return;
+            }
+        }
+        playsound(S_NOAMMO);
+    }
+    ICOMMAND(cycleweapon, "V", (tagval *args, int numargs),
+    {
+         int numguns = min(numargs, 7);
+         int guns[7];
+         loopi(numguns) guns[i] = getweapon(args[i].getstr());
+         cycleweapon(numguns, guns);
+    });
+
+    void weaponswitch(fpsent *d)
+    {
+        if(d->state!=CS_ALIVE) return;
+        int s = d->gunselect;
+        if     (s!=GUN_CG     && d->ammo[GUN_CG])     s = GUN_CG;
+        else if(s!=GUN_RL     && d->ammo[GUN_RL])     s = GUN_RL;
+        else if(s!=GUN_SG     && d->ammo[GUN_SG])     s = GUN_SG;
+        else if(s!=GUN_RIFLE  && d->ammo[GUN_RIFLE])  s = GUN_RIFLE;
+        else if(s!=GUN_GL     && d->ammo[GUN_GL])     s = GUN_GL;
+        else if(s!=GUN_PISTOL && d->ammo[GUN_PISTOL]) s = GUN_PISTOL;
+        else                                          s = GUN_FIST;
+
+        gunselect(s, d);
+    }
+
+    ICOMMAND(weapon, "V", (tagval *args, int numargs),
+    {
+        if(player1->state!=CS_ALIVE) return;
+        loopi(7)
+        {
+            const char *name = i < numargs ? args[i].getstr() : "";
+            if(name[0])
+            {
+                int gun = getweapon(name);
+                if(gun >= GUN_FIST && gun <= GUN_PISTOL && gun != player1->gunselect && player1->ammo[gun]) { gunselect(gun, player1); return; }
+            } else { weaponswitch(player1); return; }
+        }
+        playsound(S_NOAMMO);
+    });
+
+    void offsetray(const vec &from, const vec &to, int spread, float range, vec &dest)
+    {
+        vec offset;
+        do offset = vec(rndscale(1), rndscale(1), rndscale(1)).sub(0.5f);
+        while(offset.squaredlen() > 0.5f*0.5f);
+        offset.mul((to.dist(from)/1024)*spread);
+        offset.z /= 2;
+        dest = vec(offset).add(to);
+        if(dest != from)
+        {
+            vec dir = vec(dest).sub(from).normalize();
+            raycubepos(from, dir, dest, range, RAY_CLIPMAT|RAY_ALPHAPOLY);
+        }
+    }
+
+    void createrays(int gun, const vec &from, const vec &to)             // create random spread of rays
+    {
+        loopi(guns[gun].rays) offsetray(from, to, guns[gun].spread, guns[gun].range, rays[i]);
+    }
+
+    enum { BNC_GRENADE, BNC_GIBS, BNC_DEBRIS, BNC_BARRELDEBRIS };
+
+    struct bouncer : physent
+    {
+        int lifetime, bounces;
+        float lastyaw, roll;
+        bool local;
+        fpsent *owner;
+        int bouncetype, variant;
+        vec offset;
+        int offsetmillis;
+        float offsetheight;
+        int id;
+        entitylight light;
+
+        bouncer() : bounces(0), roll(0), variant(0)
+        {
+            type = ENT_BOUNCE;
+        }
+
+        vec offsetpos()
+        {
+            vec pos(o);
+            if(offsetmillis > 0)
+            {
+                pos.add(vec(offset).mul(offsetmillis/float(OFFSETMILLIS)));
+                if(offsetheight >= 0) pos.z = max(pos.z, o.z - max(offsetheight - eyeheight, 0.0f));
+            }
+            return pos;
+        }
+
+        void limitoffset()
+        {
+            if(bouncetype == BNC_GRENADE && offsetmillis > 0 && offset.z < 0)
+                offsetheight = raycube(vec(o.x + offset.x, o.y + offset.y, o.z), vec(0, 0, -1), -offset.z);
+            else offsetheight = -1;
+        } 
+    };
+
+    vector<bouncer *> bouncers;
+
+    vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d);
+
+    void newbouncer(const vec &from, const vec &to, bool local, int id, fpsent *owner, int type, int lifetime, int speed, entitylight *light = NULL)
+    {
+        bouncer &bnc = *bouncers.add(new bouncer);
+        bnc.o = from;
+        bnc.radius = bnc.xradius = bnc.yradius = type==BNC_DEBRIS ? 0.5f : 1.5f;
+        bnc.eyeheight = bnc.radius;
+        bnc.aboveeye = bnc.radius;
+        bnc.lifetime = lifetime;
+        bnc.local = local;
+        bnc.owner = owner;
+        bnc.bouncetype = type;
+        bnc.id = local ? lastmillis : id;
+        if(light) bnc.light = *light;
+
+        switch(type)
+        {
+            case BNC_GRENADE: bnc.collidetype = COLLIDE_ELLIPSE_PRECISE; break;
+            case BNC_DEBRIS: case BNC_BARRELDEBRIS: bnc.variant = rnd(4); break;
+            case BNC_GIBS: bnc.variant = rnd(3); break;
+        }
+
+        vec dir(to);
+        dir.sub(from).safenormalize();
+        bnc.vel = dir;
+        bnc.vel.mul(speed);
+
+        avoidcollision(&bnc, dir, owner, 0.1f);
+
+        if(type==BNC_GRENADE)
+        {
+            bnc.offset = hudgunorigin(GUN_GL, from, to, owner);
+            if(owner==followingplayer(player1) && !isthirdperson()) bnc.offset.sub(owner->o).rescale(16).add(owner->o);
+        }
+        else bnc.offset = from;
+        bnc.offset.sub(bnc.o);
+        bnc.offsetmillis = OFFSETMILLIS;
+        bnc.limitoffset();
+
+        bnc.resetinterp();
+    }
+
+    void bounced(physent *d, const vec &surface)
+    {
+        if(d->type != ENT_BOUNCE) return;
+        bouncer *b = (bouncer *)d;
+        if(b->bouncetype != BNC_GIBS || b->bounces >= 2) return;
+        b->bounces++;
+        adddecal(DECAL_BLOOD, vec(b->o).sub(vec(surface).mul(b->radius)), surface, 2.96f/b->bounces, bvec(0x60, 0xFF, 0xFF), rnd(4));
+    }
+        
+    void updatebouncers(int time)
+    {
+        loopv(bouncers)
+        {
+            bouncer &bnc = *bouncers[i];
+            if(bnc.bouncetype==BNC_GRENADE && bnc.vel.magnitude() > 50.0f)
+            {
+                vec pos = bnc.offsetpos();
+                regular_particle_splash(PART_SMOKE, 1, 150, pos, 0x404040, 2.4f, 50, -20);
+            }
+            vec old(bnc.o);
+            bool stopped = false;
+            if(bnc.bouncetype==BNC_GRENADE) stopped = bounce(&bnc, 0.6f, 0.5f, 0.8f) || (bnc.lifetime -= time)<0;
+            else
+            {
+                // cheaper variable rate physics for debris, gibs, etc.
+                for(int rtime = time; rtime > 0;)
+                {
+                    int qtime = min(30, rtime);
+                    rtime -= qtime;
+                    if((bnc.lifetime -= qtime)<0 || bounce(&bnc, qtime/1000.0f, 0.6f, 0.5f, 1)) { stopped = true; break; }
+                }
+            }
+            if(stopped)
+            {
+                if(bnc.bouncetype==BNC_GRENADE)
+                {
+                    int qdam = guns[GUN_GL].damage*(bnc.owner->quadmillis ? 4 : 1);
+                    hits.setsize(0);
+                    explode(bnc.local, bnc.owner, bnc.o, NULL, qdam, GUN_GL);
+                    adddecal(DECAL_SCORCH, bnc.o, vec(0, 0, 1), guns[GUN_GL].exprad/2);
+                    if(bnc.local)
+                        addmsg(N_EXPLODE, "rci3iv", bnc.owner, lastmillis-maptime, GUN_GL, bnc.id-maptime,
+                                hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf());
+                }
+                delete bouncers.remove(i--);
+            }
+            else
+            {
+                bnc.roll += old.sub(bnc.o).magnitude()/(4*RAD);
+                bnc.offsetmillis = max(bnc.offsetmillis-time, 0);
+                bnc.limitoffset();
+            }
+        }
+    }
+
+    void removebouncers(fpsent *owner)
+    {
+        loopv(bouncers) if(bouncers[i]->owner==owner) { delete bouncers[i]; bouncers.remove(i--); }
+    }
+
+    void clearbouncers() { bouncers.deletecontents(); }
+
+    struct projectile
+    {
+        vec dir, o, to, offset;
+        float speed;
+        fpsent *owner;
+        int gun;
+        bool local;
+        int offsetmillis;
+        int id;
+        entitylight light;
+    };
+    vector<projectile> projs;
+
+    void clearprojectiles() { projs.shrink(0); }
+
+    void newprojectile(const vec &from, const vec &to, float speed, bool local, int id, fpsent *owner, int gun)
+    {
+        projectile &p = projs.add();
+        p.dir = vec(to).sub(from).safenormalize();
+        p.o = from;
+        p.to = to;
+        p.offset = hudgunorigin(gun, from, to, owner);
+        p.offset.sub(from);
+        p.speed = speed;
+        p.local = local;
+        p.owner = owner;
+        p.gun = gun;
+        p.offsetmillis = OFFSETMILLIS;
+        p.id = local ? lastmillis : id;
+    }
+
+    void removeprojectiles(fpsent *owner)
+    {
+        // can't use loopv here due to strange GCC optimizer bug
+        int len = projs.length();
+        loopi(len) if(projs[i].owner==owner) { projs.remove(i--); len--; }
+    }
+
+    VARP(blood, 0, 1, 1);
+
+    void damageeffect(int damage, fpsent *d, bool thirdperson)
+    {
+        vec p = d->o;
+        p.z += 0.6f*(d->eyeheight + d->aboveeye) - d->eyeheight;
+        if(blood) particle_splash(PART_BLOOD, damage/10, 1000, p, 0x60FFFF, 2.96f);
+        if(thirdperson)
+        {
+            defformatstring(ds, "%d", damage);
+            particle_textcopy(d->abovehead(), ds, PART_TEXT, 2000, 0xFF4B19, 4.0f, -8);
+        }
+    }
+
+    void spawnbouncer(const vec &p, const vec &vel, fpsent *d, int type, entitylight *light = NULL)
+    {
+        vec to(rnd(100)-50, rnd(100)-50, rnd(100)-50);
+        if(to.iszero()) to.z += 1;
+        to.normalize();
+        to.add(p);
+        newbouncer(p, to, true, 0, d, type, rnd(1000)+1000, rnd(100)+20, light);
+    }
+
+    void gibeffect(int damage, const vec &vel, fpsent *d)
+    {
+        if(!blood || damage <= 0) return;
+        vec from = d->abovehead();
+        loopi(min(damage/25, 40)+1) spawnbouncer(from, vel, d, BNC_GIBS);
+    }
+
+    void hit(int damage, dynent *d, fpsent *at, const vec &vel, int gun, float info1, int info2 = 1)
+    {
+        if(at==player1 && d!=at)
+        {
+            extern int hitsound;
+            if(hitsound && lasthit != lastmillis) playsound(S_HIT);
+            lasthit = lastmillis;
+        }
+
+        if(d->type==ENT_INANIMATE)
+        {
+            hitmovable(damage, (movable *)d, at, vel, gun);
+            return;
+        }
+
+        fpsent *f = (fpsent *)d;
+
+        f->lastpain = lastmillis;
+        if(at->type==ENT_PLAYER && !isteam(at->team, f->team)) at->totaldamage += damage;
+
+        if(f->type==ENT_AI || !m_mp(gamemode) || f==at) f->hitpush(damage, vel, at, gun);
+
+        if(f->type==ENT_AI) hitmonster(damage, (monster *)f, at, vel, gun);
+        else if(!m_mp(gamemode)) damaged(damage, f, at);
+        else
+        {
+            hitmsg &h = hits.add();
+            h.target = f->clientnum;
+            h.lifesequence = f->lifesequence;
+            h.info1 = int(info1*DMF);
+            h.info2 = info2;
+            h.dir = f==at ? ivec(0, 0, 0) : ivec(vec(vel).mul(DNF));
+            if(at==player1)
+            {
+                damageeffect(damage, f);
+                if(f==player1)
+                {
+                    damageblend(damage);
+                    damagecompass(damage, at ? at->o : f->o);
+                    playsound(S_PAIN6);
+                }
+                else playsound(S_PAIN1+rnd(5), &f->o);
+            }
+        }
+    }
+
+    void hitpush(int damage, dynent *d, fpsent *at, vec &from, vec &to, int gun, int rays)
+    {
+        hit(damage, d, at, vec(to).sub(from).safenormalize(), gun, from.dist(to), rays);
+    }
+
+    float projdist(dynent *o, vec &dir, const vec &v)
+    {
+        vec middle = o->o;
+        middle.z += (o->aboveeye-o->eyeheight)/2;
+        float dist = middle.dist(v, dir);
+        dir.div(dist);
+        if(dist<0) dist = 0;
+        return dist;
+    }
+
+    void radialeffect(dynent *o, const vec &v, int qdam, fpsent *at, int gun)
+    {
+        if(o->state!=CS_ALIVE) return;
+        vec dir;
+        float dist = projdist(o, dir, v);
+        if(dist<guns[gun].exprad)
+        {
+            int damage = (int)(qdam*(1-dist/EXP_DISTSCALE/guns[gun].exprad));
+            if(o==at) damage /= EXP_SELFDAMDIV;
+            hit(damage, o, at, dir, gun, dist);
+        }
+    }
+
+    FVARP(explodebright, 0, 1, 1);
+
+    void explode(bool local, fpsent *owner, const vec &v, dynent *safe, int damage, int gun)
+    {
+        particle_splash(PART_SPARK, 200, 300, v, 0xB49B4B, 0.24f);
+        playsound(gun!=GUN_GL ? S_RLHIT : S_FEXPLODE, &v);
+        int color = gun!=GUN_GL ? 0xFF8080 : 0x80FFFF;
+        if((gun==GUN_RL || gun==GUN_GL) && explodebright < 1) color = vec::hexcolor(color).mul(explodebright).tohexcolor();
+        particle_fireball(v, guns[gun].exprad, gun!=GUN_GL ? PART_EXPLOSION : PART_EXPLOSION_BLUE, gun!=GUN_GL ? -1 : int((guns[gun].exprad-4.0f)*15), color, 4.0f);
+        if(gun==GUN_RL) adddynlight(v, 1.15f*guns[gun].exprad, vec(2, 1.5f, 1), 700, 100, 0, guns[gun].exprad/2, vec(1, 0.75f, 0.5f));
+        else if(gun==GUN_GL) adddynlight(v, 1.15f*guns[gun].exprad, vec(0.5f, 1.5f, 2), 600, 100, 0, 8, vec(0.25f, 1, 1));
+        else adddynlight(v, 1.15f*guns[gun].exprad, vec(2, 1.5f, 1), 700, 100);
+        int numdebris = gun==GUN_BARREL ? rnd(max(maxbarreldebris-5, 1))+5 : rnd(maxdebris-5)+5;
+        vec debrisvel = vec(owner->o).sub(v).safenormalize(), debrisorigin(v);
+        if(gun==GUN_RL) debrisorigin.add(vec(debrisvel).mul(8));
+        if(numdebris)
+        {
+            entitylight light;
+            lightreaching(debrisorigin, light.color, light.dir);
+            loopi(numdebris)
+                spawnbouncer(debrisorigin, debrisvel, owner, gun==GUN_BARREL ? BNC_BARRELDEBRIS : BNC_DEBRIS, &light);
+        }
+        if(!local) return;
+        int numdyn = numdynents();
+        loopi(numdyn)
+        {
+            dynent *o = iterdynents(i);
+            if(o->o.reject(v, o->radius + guns[gun].exprad) || o==safe) continue;
+            radialeffect(o, v, damage, owner, gun);
+        }
+    }
+
+    void projsplash(projectile &p, vec &v, dynent *safe, int damage)
+    {
+        if(guns[p.gun].part)
+        {
+            particle_splash(PART_SPARK, 100, 200, v, 0xB49B4B, 0.24f);
+            playsound(S_FEXPLODE, &v);
+            // no push?
+        }
+        else
+        {
+            explode(p.local, p.owner, v, safe, damage, GUN_RL);
+            adddecal(DECAL_SCORCH, v, vec(p.dir).neg(), guns[p.gun].exprad/2);
+        }
+    }
+
+    void explodeeffects(int gun, fpsent *d, bool local, int id)
+    {
+        if(local) return;
+        switch(gun)
+        {
+            case GUN_RL:
+                loopv(projs)
+                {
+                    projectile &p = projs[i];
+                    if(p.gun == gun && p.owner == d && p.id == id && !p.local)
+                    {
+                        vec pos(p.o);
+                        pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS)));
+                        explode(p.local, p.owner, pos, NULL, 0, GUN_RL);
+                        adddecal(DECAL_SCORCH, pos, vec(p.dir).neg(), guns[gun].exprad/2);
+                        projs.remove(i);
+                        break;
+                    }
+                }
+                break;
+            case GUN_GL:
+                loopv(bouncers)
+                {
+                    bouncer &b = *bouncers[i];
+                    if(b.bouncetype == BNC_GRENADE && b.owner == d && b.id == id && !b.local)
+                    {
+                        vec pos = b.offsetpos();
+                        explode(b.local, b.owner, pos, NULL, 0, GUN_GL);
+                        adddecal(DECAL_SCORCH, pos, vec(0, 0, 1), guns[gun].exprad/2);
+                        delete bouncers.remove(i);
+                        break;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    bool projdamage(dynent *o, projectile &p, vec &v, int qdam)
+    {
+        if(o->state!=CS_ALIVE) return false;
+        if(!intersect(o, p.o, v)) return false;
+        projsplash(p, v, o, qdam);
+        vec dir;
+        projdist(o, dir, v);
+        hit(qdam, o, p.owner, dir, p.gun, 0);
+        return true;
+    }
+
+    void updateprojectiles(int time)
+    {
+        loopv(projs)
+        {
+            projectile &p = projs[i];
+            p.offsetmillis = max(p.offsetmillis-time, 0);
+            int qdam = guns[p.gun].damage*(p.owner->quadmillis ? 4 : 1);
+            if(p.owner->type==ENT_AI) qdam /= MONSTERDAMAGEFACTOR;
+            vec dv;
+            float dist = p.to.dist(p.o, dv); 
+            dv.mul(time/max(dist*1000/p.speed, float(time)));
+            vec v = vec(p.o).add(dv);
+            bool exploded = false;
+            hits.setsize(0);
+            if(p.local)
+            {
+                vec halfdv = vec(dv).mul(0.5f), bo = vec(p.o).add(halfdv);
+                float br = max(fabs(halfdv.x), fabs(halfdv.y)) + 1;
+                loopj(numdynents())
+                {
+                    dynent *o = iterdynents(j);
+                    if(p.owner==o || o->o.reject(bo, o->radius + br)) continue;
+                    if(projdamage(o, p, v, qdam)) { exploded = true; break; }
+                }
+            }
+            if(!exploded)
+            {
+                if(dist<4)
+                {
+                    if(p.o!=p.to) // if original target was moving, reevaluate endpoint
+                    {
+                        if(raycubepos(p.o, p.dir, p.to, 0, RAY_CLIPMAT|RAY_ALPHAPOLY)>=4) continue;
+                    }
+                    projsplash(p, v, NULL, qdam);
+                    exploded = true;
+                }
+                else
+                {
+                    vec pos(v);
+                    pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS)));
+                    if(guns[p.gun].part)
+                    {
+                         regular_particle_splash(PART_SMOKE, 2, 300, pos, 0x404040, 0.6f, 150, -20);
+                         int color = 0xFFFFFF;
+                         switch(guns[p.gun].part)
+                         {
+                            case PART_FIREBALL1: color = 0xFFC8C8; break;
+                         }
+                         particle_splash(guns[p.gun].part, 1, 1, pos, color, 4.8f, 150, 20);
+                    }
+                    else regular_particle_splash(PART_SMOKE, 2, 300, pos, 0x404040, 2.4f, 50, -20);
+                }
+            }
+            if(exploded)
+            {
+                if(p.local)
+                    addmsg(N_EXPLODE, "rci3iv", p.owner, lastmillis-maptime, p.gun, p.id-maptime,
+                            hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf());
+                projs.remove(i--);
+            }
+            else p.o = v;
+        }
+    }
+
+    extern int chainsawhudgun;
+
+    VARP(muzzleflash, 0, 1, 1);
+    VARP(muzzlelight, 0, 1, 1);
+
+    void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction)     // create visual effect from a shot
+    {
+        int sound = guns[gun].sound, pspeed = 25;
+        switch(gun)
+        {
+            case GUN_FIST:
+                if(d->type==ENT_PLAYER && chainsawhudgun) sound = S_CHAINSAW_ATTACK;
+                break;
+
+            case GUN_SG:
+            {
+                if(!local) createrays(gun, from, to);
+                if(muzzleflash && d->muzzle.x >= 0)
+                    particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH3, 0xFFFFFF, 2.75f, d);
+                loopi(guns[gun].rays)
+                {
+                    particle_splash(PART_SPARK, 20, 250, rays[i], 0xB49B4B, 0.24f);
+                    particle_flare(hudgunorigin(gun, from, rays[i], d), rays[i], 300, PART_STREAK, 0xFFC864, 0.28f);
+                    if(!local) adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f);
+                }
+                if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 30, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d);
+                break;
+            }
+
+            case GUN_CG:
+            case GUN_PISTOL:
+            {
+                particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f);
+                particle_flare(hudgunorigin(gun, from, to, d), to, 600, PART_STREAK, 0xFFC864, 0.28f);
+                if(muzzleflash && d->muzzle.x >= 0)
+                    particle_flare(d->muzzle, d->muzzle, gun==GUN_CG ? 100 : 200, PART_MUZZLE_FLASH1, 0xFFFFFF, gun==GUN_CG ? 2.25f : 1.25f, d);
+                if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 2.0f);
+                if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), gun==GUN_CG ? 30 : 15, vec(0.5f, 0.375f, 0.25f), gun==GUN_CG ? 50 : 100, gun==GUN_CG ? 50 : 100, DL_FLASH, 0, vec(0, 0, 0), d);
+                break;
+            }
+
+            case GUN_RL:
+                if(muzzleflash && d->muzzle.x >= 0)
+                    particle_flare(d->muzzle, d->muzzle, 250, PART_MUZZLE_FLASH2, 0xFFFFFF, 3.0f, d);
+            case GUN_FIREBALL:
+            case GUN_ICEBALL:
+            case GUN_SLIMEBALL:
+                pspeed = guns[gun].projspeed;
+                if(d->type==ENT_AI) pspeed /= 2;
+                newprojectile(from, to, (float)pspeed, local, id, d, gun);
+                break;
+
+            case GUN_GL:
+            {
+                float dist = from.dist(to);
+                vec up = to;
+                up.z += dist/8;
+                if(muzzleflash && d->muzzle.x >= 0)
+                    particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH2, 0xFFFFFF, 1.5f, d);
+                if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 20, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d);
+                newbouncer(from, up, local, id, d, BNC_GRENADE, guns[gun].ttl, guns[gun].projspeed);
+                break;
+            }
+
+            case GUN_RIFLE:
+                particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f);
+                particle_trail(PART_SMOKE, 500, hudgunorigin(gun, from, to, d), to, 0x404040, 0.6f, 20);
+                if(muzzleflash && d->muzzle.x >= 0)
+                    particle_flare(d->muzzle, d->muzzle, 150, PART_MUZZLE_FLASH3, 0xFFFFFF, 1.25f, d);
+                if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 3.0f);
+                if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 25, vec(0.5f, 0.375f, 0.25f), 75, 75, DL_FLASH, 0, vec(0, 0, 0), d);
+                break;
+        }
+
+        bool looped = false;
+        if(d->attacksound >= 0 && d->attacksound != sound) d->stopattacksound();
+        if(d->idlesound >= 0) d->stopidlesound();
+        fpsent *h = followingplayer(player1);
+        switch(sound)
+        {
+            case S_CHAINSAW_ATTACK:
+                if(d->attacksound >= 0) looped = true;
+                d->attacksound = sound;
+                d->attackchan = playsound(sound, d==h ? NULL : &d->o, NULL, 0, -1, 100, d->attackchan);
+                break;
+            default:
+                playsound(sound, d==h ? NULL : &d->o);
+                break;
+        }
+        if(d->quadmillis && lastmillis-prevaction>200 && !looped) playsound(S_ITEMPUP, d==h ? NULL : &d->o);
+    }
+
+    void particletrack(physent *owner, vec &o, vec &d)
+    {
+        if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return;
+        fpsent *pl = (fpsent *)owner;
+        if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return;
+        float dist = o.dist(d);
+        o = pl->muzzle;
+        if(dist <= 0) d = o;
+        else
+        {
+            vecfromyawpitch(owner->yaw, owner->pitch, 1, 0, d);
+            float newdist = raycube(owner->o, d, dist, RAY_CLIPMAT|RAY_ALPHAPOLY);
+            d.mul(min(newdist, dist)).add(owner->o);
+        }
+    }
+
+    void dynlighttrack(physent *owner, vec &o, vec &hud)
+    {
+        if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return;
+        fpsent *pl = (fpsent *)owner;
+        if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return;
+        o = pl->muzzle;
+        hud = owner == followingplayer(player1) ? vec(pl->o).add(vec(0, 0, 2)) : pl->muzzle;
+    }
+
+    float intersectdist = 1e16f;
+
+    bool intersect(dynent *d, const vec &from, const vec &to, float &dist)   // if lineseg hits entity bounding box
+    {
+        vec bottom(d->o), top(d->o);
+        bottom.z -= d->eyeheight;
+        top.z += d->aboveeye;
+        return linecylinderintersect(from, to, bottom, top, d->radius, dist);
+    }
+
+    dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, float &bestdist)
+    {
+        dynent *best = NULL;
+        bestdist = 1e16f;
+        loopi(numdynents())
+        {
+            dynent *o = iterdynents(i);
+            if(o==at || o->state!=CS_ALIVE) continue;
+            float dist;
+            if(!intersect(o, from, to, dist)) continue;
+            if(dist<bestdist)
+            {
+                best = o;
+                bestdist = dist;
+            }
+        }
+        return best;
+    }
+
+    void shorten(vec &from, vec &target, float dist)
+    {
+        target.sub(from).mul(min(1.0f, dist)).add(from);
+    }
+
+    void raydamage(vec &from, vec &to, fpsent *d)
+    {
+        int qdam = guns[d->gunselect].damage;
+        if(d->quadmillis) qdam *= 4;
+        if(d->type==ENT_AI) qdam /= MONSTERDAMAGEFACTOR;
+        dynent *o;
+        float dist;
+        if(guns[d->gunselect].rays > 1)
+        {
+            dynent *hits[MAXRAYS];
+            int maxrays = guns[d->gunselect].rays;
+            loopi(maxrays) 
+            {
+                if((hits[i] = intersectclosest(from, rays[i], d, dist))) shorten(from, rays[i], dist);
+                else adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f);
+            }
+            loopi(maxrays) if(hits[i])
+            {
+                o = hits[i];
+                hits[i] = NULL;
+                int numhits = 1;
+                for(int j = i+1; j < maxrays; j++) if(hits[j] == o)
+                {
+                    hits[j] = NULL;
+                    numhits++;
+                }
+                hitpush(numhits*qdam, o, d, from, to, d->gunselect, numhits);
+            }
+        }
+        else if((o = intersectclosest(from, to, d, dist)))
+        {
+            shorten(from, to, dist);
+            hitpush(qdam, o, d, from, to, d->gunselect, 1);
+        }
+        else if(d->gunselect!=GUN_FIST && d->gunselect!=GUN_BITE) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), d->gunselect==GUN_RIFLE ? 3.0f : 2.0f);
+    }
+
+    void shoot(fpsent *d, const vec &targ)
+    {
+        int prevaction = d->lastaction, attacktime = lastmillis-prevaction;
+        if(attacktime<d->gunwait) return;
+        d->gunwait = 0;
+        if((d==player1 || d->ai) && !d->attacking) return;
+        d->lastaction = lastmillis;
+        d->lastattackgun = d->gunselect;
+        if(!d->ammo[d->gunselect])
+        {
+            if(d==player1)
+            {
+                msgsound(S_NOAMMO, d);
+                d->gunwait = 600;
+                d->lastattackgun = -1;
+                weaponswitch(d);
+            }
+            return;
+        }
+        if(d->gunselect) d->ammo[d->gunselect]--;
+
+        vec from = d->o, to = targ, dir = vec(to).sub(from).safenormalize();
+        float dist = to.dist(from);
+        vec kickback = vec(dir).mul(guns[d->gunselect].kickamount*-2.5f);
+        d->vel.add(kickback);
+        float shorten = 0;
+        if(guns[d->gunselect].range && dist > guns[d->gunselect].range)
+            shorten = guns[d->gunselect].range;
+        float barrier = raycube(d->o, dir, dist, RAY_CLIPMAT|RAY_ALPHAPOLY);
+        if(barrier > 0 && barrier < dist && (!shorten || barrier < shorten))
+            shorten = barrier;
+        if(shorten) to = vec(dir).mul(shorten).add(from);
+
+        if(guns[d->gunselect].rays > 1) createrays(d->gunselect, from, to);
+        else if(guns[d->gunselect].spread) offsetray(from, to, guns[d->gunselect].spread, guns[d->gunselect].range, to);
+
+        hits.setsize(0);
+
+        if(!guns[d->gunselect].projspeed) raydamage(from, to, d);
+
+        shoteffects(d->gunselect, from, to, d, true, 0, prevaction);
+
+        if(d==player1 || d->ai)
+        {
+            addmsg(N_SHOOT, "rci2i6iv", d, lastmillis-maptime, d->gunselect,
+                   (int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF),
+                   (int)(to.x*DMF), (int)(to.y*DMF), (int)(to.z*DMF),
+                   hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf());
+        }
+
+               d->gunwait = guns[d->gunselect].attackdelay;
+               if(d->gunselect == GUN_PISTOL && d->ai) d->gunwait += int(d->gunwait*(((101-d->skill)+rnd(111-d->skill))/100.f));
+        d->totalshots += guns[d->gunselect].damage*(d->quadmillis ? 4 : 1)*guns[d->gunselect].rays;
+    }
+
+    void adddynlights()
+    {
+        loopv(projs)
+        {
+            projectile &p = projs[i];
+            if(p.gun!=GUN_RL) continue;
+            vec pos(p.o);
+            pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS)));
+            adddynlight(pos, 20, vec(1, 0.75f, 0.5f));
+        }
+        loopv(bouncers)
+        {
+            bouncer &bnc = *bouncers[i];
+            if(bnc.bouncetype!=BNC_GRENADE) continue;
+            vec pos = bnc.offsetpos();
+            adddynlight(pos, 8, vec(0.25f, 1, 1));
+        }
+    }
+
+    static const char * const projnames[2] = { "projectiles/grenade", "projectiles/rocket" };
+    static const char * const gibnames[3] = { "gibs/gib01", "gibs/gib02", "gibs/gib03" };
+    static const char * const debrisnames[4] = { "debris/debris01", "debris/debris02", "debris/debris03", "debris/debris04" };
+    static const char * const barreldebrisnames[4] = { "barreldebris/debris01", "barreldebris/debris02", "barreldebris/debris03", "barreldebris/debris04" };
+         
+    void preloadbouncers()
+    {
+        loopi(sizeof(projnames)/sizeof(projnames[0])) preloadmodel(projnames[i]);
+        loopi(sizeof(gibnames)/sizeof(gibnames[0])) preloadmodel(gibnames[i]);
+        loopi(sizeof(debrisnames)/sizeof(debrisnames[0])) preloadmodel(debrisnames[i]);
+        loopi(sizeof(barreldebrisnames)/sizeof(barreldebrisnames[0])) preloadmodel(barreldebrisnames[i]);
+    }
+
+    void renderbouncers()
+    {
+        float yaw, pitch;
+        loopv(bouncers)
+        {
+            bouncer &bnc = *bouncers[i];
+            vec pos = bnc.offsetpos();
+            vec vel(bnc.vel);
+            if(vel.magnitude() <= 25.0f) yaw = bnc.lastyaw;
+            else
+            {
+                vectoyawpitch(vel, yaw, pitch);
+                yaw += 90;
+                bnc.lastyaw = yaw;
+            }
+            pitch = -bnc.roll;
+            if(bnc.bouncetype==BNC_GRENADE)
+                rendermodel(&bnc.light, "projectiles/grenade", ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST|MDL_DYNSHADOW);
+            else
+            {
+                const char *mdl = NULL;
+                int cull = MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED;
+                float fade = 1;
+                if(bnc.lifetime < 250) fade = bnc.lifetime/250.0f;
+                switch(bnc.bouncetype)
+                {
+                    case BNC_GIBS: mdl = gibnames[bnc.variant]; cull |= MDL_LIGHT|MDL_LIGHT_FAST|MDL_DYNSHADOW; break;
+                    case BNC_DEBRIS: mdl = debrisnames[bnc.variant]; break;
+                    case BNC_BARRELDEBRIS: mdl = barreldebrisnames[bnc.variant]; break;
+                    default: continue;
+                }
+                rendermodel(&bnc.light, mdl, ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, cull, NULL, NULL, 0, 0, fade);
+            }
+        }
+    }
+
+    void renderprojectiles()
+    {
+        float yaw, pitch;
+        loopv(projs)
+        {
+            projectile &p = projs[i];
+            if(p.gun!=GUN_RL) continue;
+            float dist = min(p.o.dist(p.to)/32.0f, 1.0f);
+            vec pos = vec(p.o).add(vec(p.offset).mul(dist*p.offsetmillis/float(OFFSETMILLIS))),
+                v = dist < 1e-6f ? p.dir : vec(p.to).sub(pos).normalize();
+            // the amount of distance in front of the smoke trail needs to change if the model does
+            vectoyawpitch(v, yaw, pitch);
+            yaw += 90;
+            v.mul(3);
+            v.add(pos);
+            rendermodel(&p.light, "projectiles/rocket", ANIM_MAPMODEL|ANIM_LOOP, v, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST);
+        }
+    }
+
+    void checkattacksound(fpsent *d, bool local)
+    {
+        int gun = -1;
+        switch(d->attacksound)
+        {
+            case S_CHAINSAW_ATTACK:
+                if(chainsawhudgun) gun = GUN_FIST;
+                break;
+            default:
+                return;
+        }
+        if(gun >= 0 && gun < NUMGUNS &&
+           d->clientnum >= 0 && d->state == CS_ALIVE &&
+           d->lastattackgun == gun && lastmillis - d->lastaction < guns[gun].attackdelay + 50)
+        {
+            d->attackchan = playsound(d->attacksound, local ? NULL : &d->o, NULL, 0, -1, -1, d->attackchan);
+            if(d->attackchan < 0) d->attacksound = -1;
+        }
+        else d->stopattacksound();
+    }
+
+    void checkidlesound(fpsent *d, bool local)
+    {
+        int sound = -1, radius = 0;
+        if(d->clientnum >= 0 && d->state == CS_ALIVE) switch(d->gunselect)
+        {
+            case GUN_FIST:
+                if(chainsawhudgun && d->attacksound < 0)
+                {
+                    sound = S_CHAINSAW_IDLE;
+                    radius = 50;
+                }
+                break;
+        }
+        if(d->idlesound != sound)
+        {
+            if(d->idlesound >= 0) d->stopidlesound();
+            if(sound >= 0)
+            {
+                d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, 100, d->idlechan, radius);
+                if(d->idlechan >= 0) d->idlesound = sound;
+            }
+        }
+        else if(sound >= 0)
+        {
+            d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, -1, d->idlechan, radius);
+            if(d->idlechan < 0) d->idlesound = -1;
+        }
+    }
+
+    void removeweapons(fpsent *d)
+    {
+        removebouncers(d);
+        removeprojectiles(d);
+    }
+
+    void updateweapons(int curtime)
+    {
+        updateprojectiles(curtime);
+        if(player1->clientnum>=0 && player1->state==CS_ALIVE) shoot(player1, worldpos); // only shoot when connected to server
+        updatebouncers(curtime); // need to do this after the player shoots so grenades don't end up inside player's BB next frame
+        fpsent *following = followingplayer();
+        if(!following) following = player1;
+        loopv(players)
+        {
+            fpsent *d = players[i];
+            checkattacksound(d, d==following);
+            checkidlesound(d, d==following);
+        }
+    }
+
+    void avoidweapons(ai::avoidset &obstacles, float radius)
+    {
+        loopv(projs)
+        {
+            projectile &p = projs[i];
+            obstacles.avoidnear(NULL, p.o.z + guns[p.gun].exprad + 1, p.o, radius + guns[p.gun].exprad);
+        }
+        loopv(bouncers)
+        {
+            bouncer &bnc = *bouncers[i];
+            if(bnc.bouncetype != BNC_GRENADE) continue;
+            obstacles.avoidnear(NULL, bnc.o.z + guns[GUN_GL].exprad + 1, bnc.o, radius + guns[GUN_GL].exprad);
+        }
+    }
+};
+
diff --git a/src/readme_source.txt b/src/readme_source.txt
new file mode 100644 (file)
index 0000000..93bc637
--- /dev/null
@@ -0,0 +1,92 @@
+Sauerbraten source code license, usage, and documentation.
+
+You may use the Sauerbraten source code if you abide by the ZLIB license
+http://www.opensource.org/licenses/zlib-license.php
+(very similar to the BSD license):
+
+
+LICENSE
+=======
+
+Sauerbraten game engine source code, any release.
+
+Copyright (C) 2001-2020 Wouter van Oortmerssen, Lee Salzman, Mike Dysart, Robert Pointon, and Quinton Reeves
+
+This software is provided 'as-is', without any express or implied
+warranty.  In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+   claim that you wrote the original software. If you use this software
+   in a product, an acknowledgment in the product documentation would be
+   appreciated but is not required.
+2. Altered source versions must be plainly marked as such, and must not be
+   misrepresented as being the original software.
+3. This notice may not be removed or altered from any source distribution.
+
+
+LICENSE NOTES
+=============
+The license covers the source code found in the "src" directory of this
+archive as well as the .cfg files under the "data" directory. The included 
+ENet network library which Sauerbraten uses is covered by an MIT-style 
+license, which is however compatible with the above license for all 
+practical purposes.
+
+Game media included in the game (maps, textures, sounds, models etc.)
+are NOT covered by this license, and may have individual copyrights and
+distribution restrictions (see individual readmes).
+
+
+USAGE
+=====
+Compiling the sources should be straight forward.
+
+Unix users need to make sure to have the development version of all libs
+installed (OpenGL, SDL, SDL_mixer, SDL_image, zlib). The included
+Makefile can be used to build.
+
+Windows users can use the included Visual Studio project files in the vcpp 
+directory,  which references the lib/include directories for the external 
+libraries and should thus be self contained. Release mode builds will place 
+executables in the bin dir ready for testing and distribution.
+
+An alternative to Visual Studio for Windows is MinGW/MSYS, which can be compiled
+using the provided Makefile. Another alternative for Windows is to compile under
+Code::Blocks with the provided vcpp/sauerbraten.cbp project file.
+
+The Sauerbraten sources are very small, compact, and non-redundant, so anyone
+wishing to modify the source code should be able to gain an overview of
+Sauerbraten's inner workings by simply reading through the source code in its
+entirety. Small amounts of comments should guide you through the more
+tricky sections.
+
+When reading the source code and trying to understand Sauerbaten's internal design,
+keep in mind the goal of Cube: minimalism. I wanted to create a very complete
+game / game engine with absolutely minimal means, and made a sport out of it
+keeping the implementation small and simple. Sauerbraten is not a commercial 
+product, it is merely the author's idea of a fun little programming project.
+
+
+AUTHORS
+======
+Wouter "Aardappel" van Oortmerssen
+http://strlen.com
+
+Lee "eihrul" Salzman 
+http://sauerbraten.org/lee/
+
+Mike "Gilt" Dysart
+
+Robert "baby-rabbit" Pointon
+http://www.fernlightning.com
+
+Quinton "Quin" Reeves
+http://www.redeclipse.net
+
+For additional authors/contributors, see the Sauerbraten binary distribution readme.
+
diff --git a/src/shared/command.h b/src/shared/command.h
new file mode 100644 (file)
index 0000000..475e492
--- /dev/null
@@ -0,0 +1,335 @@
+// script binding functionality
+
+enum { VAL_NULL = 0, VAL_INT, VAL_FLOAT, VAL_STR, VAL_ANY, VAL_CODE, VAL_MACRO, VAL_IDENT };
+
+enum
+{
+    CODE_START = 0,
+    CODE_OFFSET,
+    CODE_POP,
+    CODE_ENTER,
+    CODE_EXIT,
+    CODE_VAL,
+    CODE_VALI,
+    CODE_MACRO,
+    CODE_BOOL,
+    CODE_BLOCK,
+    CODE_COMPILE,
+    CODE_FORCE,
+    CODE_RESULT,
+    CODE_IDENT, CODE_IDENTU, CODE_IDENTARG,
+    CODE_COM, CODE_COMD, CODE_COMC, CODE_COMV,
+    CODE_CONC, CODE_CONCW, CODE_CONCM, CODE_DOWN,
+    CODE_SVAR, CODE_SVAR1,
+    CODE_IVAR, CODE_IVAR1, CODE_IVAR2, CODE_IVAR3,
+    CODE_FVAR, CODE_FVAR1,
+    CODE_LOOKUP, CODE_LOOKUPU, CODE_LOOKUPARG, CODE_ALIAS, CODE_ALIASU, CODE_ALIASARG, CODE_CALL, CODE_CALLU, CODE_CALLARG,
+    CODE_PRINT,
+    CODE_LOCAL,
+
+    CODE_OP_MASK = 0x3F,
+    CODE_RET = 6,
+    CODE_RET_MASK = 0xC0,
+
+    /* return type flags */
+    RET_NULL   = VAL_NULL<<CODE_RET,
+    RET_STR    = VAL_STR<<CODE_RET,
+    RET_INT    = VAL_INT<<CODE_RET,
+    RET_FLOAT  = VAL_FLOAT<<CODE_RET,
+};
+
+enum { ID_VAR, ID_FVAR, ID_SVAR, ID_COMMAND, ID_ALIAS, ID_LOCAL };
+
+enum { IDF_PERSIST = 1<<0, IDF_OVERRIDE = 1<<1, IDF_HEX = 1<<2, IDF_READONLY = 1<<3, IDF_OVERRIDDEN = 1<<4, IDF_UNKNOWN = 1<<5, IDF_ARG = 1<<6, IDF_EMUVAR = 1<<7 };
+
+struct ident;
+
+struct identval
+{
+    union
+    {
+        int i;      // ID_VAR, VAL_INT
+        float f;    // ID_FVAR, VAL_FLOAT
+        char *s;    // ID_SVAR, VAL_STR
+        const uint *code; // VAL_CODE
+        ident *id;  // VAL_IDENT
+    };
+};
+
+struct tagval : identval
+{
+    int type;
+
+    void setint(int val) { type = VAL_INT; i = val; }
+    void setfloat(float val) { type = VAL_FLOAT; f = val; }
+    void setstr(char *val) { type = VAL_STR; s = val; }
+    void setnull() { type = VAL_NULL; i = 0; }
+    void setcode(const uint *val) { type = VAL_CODE; code = val; }
+    void setmacro(const uint *val) { type = VAL_MACRO; code = val; }
+    void setident(ident *val) { type = VAL_IDENT; id = val; }
+
+    const char *getstr() const;
+    int getint() const;
+    float getfloat() const;
+    bool getbool() const;
+
+    void cleanup();
+};
+        
+struct identstack
+{
+    identval val;
+    int valtype;
+    identstack *next;
+};
+
+union identvalptr
+{
+    int *i;   // ID_VAR
+    float *f; // ID_FVAR
+    char **s; // ID_SVAR
+};
+
+typedef void (__cdecl *identfun)();
+
+struct ident
+{
+    uchar type; // one of ID_* above
+    union
+    {
+        uchar valtype; // ID_ALIAS
+        uchar numargs; // ID_COMMAND
+    };
+    ushort flags;
+    int index;
+    const char *name;
+    union
+    {
+        struct // ID_VAR, ID_FVAR, ID_SVAR
+        {
+            union
+            {
+                struct { int minval, maxval; };     // ID_VAR
+                struct { float minvalf, maxvalf; }; // ID_FVAR
+            };
+            identvalptr storage;
+            identval overrideval;
+        };
+        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
+    
+    ident() {}
+    // ID_VAR
+    ident(int t, const char *n, int m, int x, int *s, void *f = NULL, int flags = 0)
+        : type(t), flags(flags | (m > x ? IDF_READONLY : 0)), name(n), minval(m), maxval(x), fun((identfun)f)
+    { storage.i = s; }
+    // ID_FVAR
+    ident(int t, const char *n, float m, float x, float *s, void *f = NULL, int flags = 0)
+        : type(t), flags(flags | (m > x ? IDF_READONLY : 0)), name(n), minvalf(m), maxvalf(x), fun((identfun)f)
+    { storage.f = s; }
+    // ID_SVAR
+    ident(int t, const char *n, char **s, void *f = NULL, int flags = 0)
+        : type(t), flags(flags), name(n), fun((identfun)f)
+    { storage.s = s; }
+    // ID_ALIAS
+    ident(int t, const char *n, char *a, int flags)
+        : type(t), valtype(VAL_STR), flags(flags), name(n), code(NULL), stack(NULL)
+    { val.s = a; }
+    ident(int t, const char *n, int a, int flags)
+        : type(t), valtype(VAL_INT), flags(flags), name(n), code(NULL), stack(NULL)
+    { val.i = a; }
+    ident(int t, const char *n, float a, int flags)
+        : type(t), valtype(VAL_FLOAT), flags(flags), name(n), code(NULL), stack(NULL)
+    { val.f = a; }
+    ident(int t, const char *n, int flags)
+        : type(t), valtype(VAL_NULL), flags(flags), name(n), code(NULL), stack(NULL)
+    {}
+    ident(int t, const char *n, const tagval &v, int flags)
+        : type(t), valtype(v.type), flags(flags), name(n), code(NULL), stack(NULL)
+    { val = v; }
+    // ID_COMMAND
+    ident(int t, const char *n, const char *args, uint argmask, int numargs, void *f = NULL, int flags = 0)
+        : type(t), numargs(numargs), flags(flags), name(n), args(args), argmask(argmask), fun((identfun)f)
+    {}
+
+    void changed() { if(fun) fun(); }
+
+    void setval(const tagval &v)
+    {
+        valtype = v.type;
+        val = v;
+    }
+   
+    void setval(const identstack &v)
+    {
+        valtype = v.valtype;
+        val = v.val;
+    }
+    void forcenull()
+    {
+        if(valtype==VAL_STR) delete[] val.s;
+        valtype = VAL_NULL;
+    }
+
+    float getfloat() const;
+    int getint() const;
+    const char *getstr() const;
+    void getval(tagval &v) const;
+};
+
+extern void addident(ident *id);
+
+extern tagval *commandret;
+extern const char *intstr(int v);
+extern void intret(int v);
+extern const char *floatstr(float v);
+extern void floatret(float v);
+extern void stringret(char *s);
+extern void result(tagval &v);
+extern void result(const char *s);
+
+static inline int parseint(const char *s)
+{
+    return int(strtoul(s, NULL, 0));
+}
+
+static inline float parsefloat(const char *s)
+{
+    // not all platforms (windows) can parse hexadecimal integers via strtod
+    char *end;
+    double val = strtod(s, &end);
+    return val || end==s || (*end!='x' && *end!='X') ? float(val) : float(parseint(s));
+}
+
+static inline void intformat(char *buf, int v, int len = 20) { nformatstring(buf, len, "%d", v); }
+static inline void floatformat(char *buf, float v, int len = 20) { nformatstring(buf, len, v==int(v) ? "%.1f" : "%.6g", v); }
+
+static inline const char *getstr(const identval &v, int type) 
+{
+    switch(type)
+    {
+        case VAL_STR: case VAL_MACRO: return v.s;
+        case VAL_INT: return intstr(v.i);
+        case VAL_FLOAT: return floatstr(v.f);
+        default: return "";
+    }
+}
+inline const char *tagval::getstr() const { return ::getstr(*this, type); }
+inline const char *ident::getstr() const { return ::getstr(val, valtype); }
+
+static inline int getint(const identval &v, int type)
+{
+    switch(type)
+    {
+        case VAL_INT: return v.i;
+        case VAL_FLOAT: return int(v.f);
+        case VAL_STR: case VAL_MACRO: return parseint(v.s); 
+        default: return 0;
+    }
+}
+inline int tagval::getint() const { return ::getint(*this, type); }
+inline int ident::getint() const { return ::getint(val, valtype); }
+
+static inline float getfloat(const identval &v, int type)
+{
+    switch(type)
+    {
+        case VAL_FLOAT: return v.f;
+        case VAL_INT: return float(v.i);
+        case VAL_STR: case VAL_MACRO: return parsefloat(v.s);
+        default: return 0.0f;
+    }
+}
+inline float tagval::getfloat() const { return ::getfloat(*this, type); }
+inline float ident::getfloat() const { return ::getfloat(val, valtype); } 
+
+inline void ident::getval(tagval &v) const
+{
+    switch(valtype)
+    {
+        case VAL_STR: case VAL_MACRO: v.setstr(newstring(val.s)); break;
+        case VAL_INT: v.setint(val.i); break;
+        case VAL_FLOAT: v.setfloat(val.f); break;
+        default: v.setnull(); break;
+    }
+}
+
+// nasty macros for registering script functions, abuses globals to avoid excessive infrastructure
+#define KEYWORD(name, type) UNUSED static bool __dummy_##name = addkeyword(type, #name)
+#define COMMANDN(name, fun, nargs) UNUSED static bool __dummy_##fun = addcommand(#name, (identfun)fun, nargs)
+#define COMMAND(name, nargs) COMMANDN(name, name, nargs)
+
+#define _VAR(name, global, min, cur, max, persist)  int global = variable(#name, min, cur, max, &global, NULL, persist)
+#define VARN(name, global, min, cur, max) _VAR(name, global, min, cur, max, 0)
+#define VARNP(name, global, min, cur, max) _VAR(name, global, min, cur, max, IDF_PERSIST)
+#define VARNR(name, global, min, cur, max) _VAR(name, global, min, cur, max, IDF_OVERRIDE)
+#define VAR(name, min, cur, max) _VAR(name, name, min, cur, max, 0)
+#define VARP(name, min, cur, max) _VAR(name, name, min, cur, max, IDF_PERSIST)
+#define VARR(name, min, cur, max) _VAR(name, name, min, cur, max, IDF_OVERRIDE)
+#define _VARF(name, global, min, cur, max, body, persist)  void var_##name(); int global = variable(#name, min, cur, max, &global, var_##name, persist); void var_##name() { body; }
+#define VARFN(name, global, min, cur, max, body) _VARF(name, global, min, cur, max, body, 0)
+#define VARF(name, min, cur, max, body) _VARF(name, name, min, cur, max, body, 0)
+#define VARFP(name, min, cur, max, body) _VARF(name, name, min, cur, max, body, IDF_PERSIST)
+#define VARFR(name, min, cur, max, body) _VARF(name, name, min, cur, max, body, IDF_OVERRIDE)
+#define VARFNP(name, global, min, cur, max, body) _VARF(name, global, min, cur, max, body, IDF_PERSIST)
+
+#define _HVAR(name, global, min, cur, max, persist)  int global = variable(#name, min, cur, max, &global, NULL, persist | IDF_HEX)
+#define HVARN(name, global, min, cur, max) _HVAR(name, global, min, cur, max, 0)
+#define HVARNP(name, global, min, cur, max) _HVAR(name, global, min, cur, max, IDF_PERSIST)
+#define HVARNR(name, global, min, cur, max) _HVAR(name, global, min, cur, max, IDF_OVERRIDE)
+#define HVAR(name, min, cur, max) _HVAR(name, name, min, cur, max, 0)
+#define HVARP(name, min, cur, max) _HVAR(name, name, min, cur, max, IDF_PERSIST)
+#define HVARR(name, min, cur, max) _HVAR(name, name, min, cur, max, IDF_OVERRIDE)
+#define _HVARF(name, global, min, cur, max, body, persist)  void var_##name(); int global = variable(#name, min, cur, max, &global, var_##name, persist | IDF_HEX); void var_##name() { body; }
+#define HVARFN(name, global, min, cur, max, body) _HVARF(name, global, min, cur, max, body, 0)
+#define HVARF(name, min, cur, max, body) _HVARF(name, name, min, cur, max, body, 0)
+#define HVARFP(name, min, cur, max, body) _HVARF(name, name, min, cur, max, body, IDF_PERSIST)
+#define HVARFR(name, min, cur, max, body) _HVARF(name, name, min, cur, max, body, IDF_OVERRIDE)
+
+#define _FVAR(name, global, min, cur, max, persist) float global = fvariable(#name, min, cur, max, &global, NULL, persist)
+#define FVARN(name, global, min, cur, max) _FVAR(name, global, min, cur, max, 0)
+#define FVARNP(name, global, min, cur, max) _FVAR(name, global, min, cur, max, IDF_PERSIST)
+#define FVARNR(name, global, min, cur, max) _FVAR(name, global, min, cur, max, IDF_OVERRIDE)
+#define FVAR(name, min, cur, max) _FVAR(name, name, min, cur, max, 0)
+#define FVARP(name, min, cur, max) _FVAR(name, name, min, cur, max, IDF_PERSIST)
+#define FVARR(name, min, cur, max) _FVAR(name, name, min, cur, max, IDF_OVERRIDE)
+#define _FVARF(name, global, min, cur, max, body, persist) void var_##name(); float global = fvariable(#name, min, cur, max, &global, var_##name, persist); void var_##name() { body; }
+#define FVARFN(name, global, min, cur, max, body) _FVARF(name, global, min, cur, max, body, 0)
+#define FVARF(name, min, cur, max, body) _FVARF(name, name, min, cur, max, body, 0)
+#define FVARFP(name, min, cur, max, body) _FVARF(name, name, min, cur, max, body, IDF_PERSIST)
+#define FVARFR(name, min, cur, max, body) _FVARF(name, name, min, cur, max, body, IDF_OVERRIDE)
+
+#define _SVAR(name, global, cur, persist) char *global = svariable(#name, cur, &global, NULL, persist)
+#define SVARN(name, global, cur) _SVAR(name, global, cur, 0)
+#define SVARNP(name, global, cur) _SVAR(name, global, cur, IDF_PERSIST)
+#define SVARNR(name, global, cur) _SVAR(name, global, cur, IDF_OVERRIDE)
+#define SVAR(name, cur) _SVAR(name, name, cur, 0)
+#define SVARP(name, cur) _SVAR(name, name, cur, IDF_PERSIST)
+#define SVARR(name, cur) _SVAR(name, name, cur, IDF_OVERRIDE)
+#define _SVARF(name, global, cur, body, persist) void var_##name(); char *global = svariable(#name, cur, &global, var_##name, persist); void var_##name() { body; }
+#define SVARFN(name, global, cur, body) _SVARF(name, global, cur, body, 0)
+#define SVARF(name, cur, body) _SVARF(name, name, cur, body, 0)
+#define SVARFP(name, cur, body) _SVARF(name, name, cur, body, IDF_PERSIST)
+#define SVARFR(name, cur, body) _SVARF(name, name, cur, body, IDF_OVERRIDE)
+
+// anonymous inline commands, uses nasty template trick with line numbers to keep names unique
+#define ICOMMANDNS(name, cmdname, nargs, proto, b) template<int N> struct cmdname; template<> struct cmdname<__LINE__> { static bool init; static void run proto; }; bool cmdname<__LINE__>::init = addcommand(name, (identfun)cmdname<__LINE__>::run, nargs); void cmdname<__LINE__>::run proto \
+    { b; }
+#define ICOMMANDN(name, cmdname, nargs, proto, b) ICOMMANDNS(#name, cmdname, nargs, proto, b)
+#define ICOMMANDNAME(name) _icmd_##name
+#define ICOMMAND(name, nargs, proto, b) ICOMMANDN(name, ICOMMANDNAME(name), nargs, proto, b)
+#define ICOMMANDSNAME _icmds_
+#define ICOMMANDS(name, nargs, proto, b) ICOMMANDNS(name, ICOMMANDSNAME, nargs, proto, b)
diff --git a/src/shared/crypto.cpp b/src/shared/crypto.cpp
new file mode 100644 (file)
index 0000000..134afc5
--- /dev/null
@@ -0,0 +1,944 @@
+#include "cube.h"
+
+///////////////////////// cryptography /////////////////////////////////
+
+/* Based off the reference implementation of Tiger, a cryptographically
+ * secure 192 bit hash function by Ross Anderson and Eli Biham. More info at:
+ * http://www.cs.technion.ac.il/~biham/Reports/Tiger/
+ */
+
+#define TIGER_PASSES 3
+
+namespace tiger
+{
+    typedef unsigned long long int chunk;
+
+    union hashval
+    {
+        uchar bytes[3*8];
+        chunk chunks[3];
+    };
+
+    chunk sboxes[4*256];
+
+    void compress(const chunk *str, chunk state[3])
+    {
+        chunk a, b, c;
+        chunk aa, bb, cc;
+        chunk x0, x1, x2, x3, x4, x5, x6, x7;
+
+        a = state[0];
+        b = state[1];
+        c = state[2];
+
+        x0=str[0]; x1=str[1]; x2=str[2]; x3=str[3];
+        x4=str[4]; x5=str[5]; x6=str[6]; x7=str[7];
+
+        aa = a;
+        bb = b;
+        cc = c;
+
+        loop(pass_no, TIGER_PASSES)
+        {
+            if(pass_no)
+            {
+                x0 -= x7 ^ 0xA5A5A5A5A5A5A5A5ULL; x1 ^= x0; x2 += x1; x3 -= x2 ^ ((~x1)<<19);
+                x4 ^= x3; x5 += x4; x6 -= x5 ^ ((~x4)>>23); x7 ^= x6;
+                x0 += x7; x1 -= x0 ^ ((~x7)<<19); x2 ^= x1; x3 += x2;
+                x4 -= x3 ^ ((~x2)>>23); x5 ^= x4; x6 += x5; x7 -= x6 ^ 0x0123456789ABCDEFULL;
+            }
+
+#define sb1 (sboxes)
+#define sb2 (sboxes+256)
+#define sb3 (sboxes+256*2)
+#define sb4 (sboxes+256*3)
+
+#define round(a, b, c, x) \
+      c ^= x; \
+      a -= sb1[((c)>>(0*8))&0xFF] ^ sb2[((c)>>(2*8))&0xFF] ^ \
+       sb3[((c)>>(4*8))&0xFF] ^ sb4[((c)>>(6*8))&0xFF] ; \
+      b += sb4[((c)>>(1*8))&0xFF] ^ sb3[((c)>>(3*8))&0xFF] ^ \
+       sb2[((c)>>(5*8))&0xFF] ^ sb1[((c)>>(7*8))&0xFF] ; \
+      b *= mul;
+
+            uint mul = !pass_no ? 5 : (pass_no==1 ? 7 : 9);
+            round(a, b, c, x0) round(b, c, a, x1) round(c, a, b, x2) round(a, b, c, x3)
+            round(b, c, a, x4) round(c, a, b, x5) round(a, b, c, x6) round(b, c, a, x7)
+
+            chunk tmp = a; a = c; c = b; b = tmp;
+        }
+
+        a ^= aa;
+        b -= bb;
+        c += cc;
+
+        state[0] = a;
+        state[1] = b;
+        state[2] = c;
+    }
+
+    void gensboxes()
+    {
+        const char *str = "Tiger - A Fast New Hash Function, by Ross Anderson and Eli Biham";
+        chunk state[3] = { 0x0123456789ABCDEFULL, 0xFEDCBA9876543210ULL, 0xF096A5B4C3B2E187ULL };
+        uchar temp[64];
+
+        if(!*(const uchar *)&islittleendian) loopj(64) temp[j^7] = str[j];
+        else loopj(64) temp[j] = str[j];
+        loopi(1024) loop(col, 8) ((uchar *)&sboxes[i])[col] = i&0xFF;
+
+        int abc = 2;
+        loop(pass, 5) loopi(256) for(int sb = 0; sb < 1024; sb += 256)
+        {
+            abc++;
+            if(abc >= 3) { abc = 0; compress((chunk *)temp, state); }
+            loop(col, 8)
+            {
+                uchar val = ((uchar *)&sboxes[sb+i])[col];
+                ((uchar *)&sboxes[sb+i])[col] = ((uchar *)&sboxes[sb + ((uchar *)&state[abc])[col]])[col];
+                ((uchar *)&sboxes[sb + ((uchar *)&state[abc])[col]])[col] = val;
+            }
+        }
+    }
+
+    void hash(const uchar *str, int length, hashval &val)
+    {
+        static bool init = false;
+        if(!init) { gensboxes(); init = true; }
+
+        uchar temp[64];
+
+        val.chunks[0] = 0x0123456789ABCDEFULL;
+        val.chunks[1] = 0xFEDCBA9876543210ULL;
+        val.chunks[2] = 0xF096A5B4C3B2E187ULL;
+
+        int i = length;
+        for(; i >= 64; i -= 64, str += 64)
+        {
+            if(!*(const uchar *)&islittleendian)
+            {
+                loopj(64) temp[j^7] = str[j];
+                compress((chunk *)temp, val.chunks);
+            }
+            else compress((chunk *)str, val.chunks);
+        }
+
+        int j;
+        if(!*(const uchar *)&islittleendian)
+        {
+            for(j = 0; j < i; j++) temp[j^7] = str[j];
+            temp[j^7] = 0x01;
+            while(++j&7) temp[j^7] = 0;
+        }
+        else
+        {
+            for(j = 0; j < i; j++) temp[j] = str[j];
+            temp[j] = 0x01;
+            while(++j&7) temp[j] = 0;
+        }
+
+        if(j > 56)
+        {
+            while(j < 64) temp[j++] = 0;
+            compress((chunk *)temp, val.chunks);
+            j = 0;
+        }
+        while(j < 56) temp[j++] = 0;
+        *(chunk *)(temp+56) = (chunk)length<<3;
+        compress((chunk *)temp, val.chunks);
+        if(!*(const uchar *)&islittleendian)
+        {
+            loopk(3) 
+            {
+                uchar *c = &val.bytes[k*sizeof(chunk)];
+                loopl(sizeof(chunk)/2) swap(c[l], c[sizeof(chunk)-1-l]);
+            }
+        }
+    }
+}
+
+/* Elliptic curve cryptography based on NIST DSS prime curves. */
+
+#define BI_DIGIT_BITS 16
+#define BI_DIGIT_MASK ((1<<BI_DIGIT_BITS)-1)
+
+template<int BI_DIGITS> struct bigint
+{
+    typedef ushort digit;
+    typedef uint dbldigit;
+
+    int len;
+    digit digits[BI_DIGITS];
+
+    bigint() {}
+    bigint(digit n) { if(n) { len = 1; digits[0] = n; } else len = 0; }
+    bigint(const char *s) { parse(s); }
+    template<int Y_DIGITS> bigint(const bigint<Y_DIGITS> &y) { *this = y; }
+
+    static int parsedigits(ushort *digits, int maxlen, const char *s)
+    {
+        int slen = 0;
+        while(isxdigit(s[slen])) slen++;
+        int len = (slen+2*sizeof(ushort)-1)/(2*sizeof(ushort));
+        if(len>maxlen) return 0;
+        memset(digits, 0, len*sizeof(ushort));
+        loopi(slen)
+        {
+            int c = s[slen-i-1];
+            if(isalpha(c)) c = toupper(c) - 'A' + 10;
+            else if(isdigit(c)) c -= '0';
+            else return 0;
+            digits[i/(2*sizeof(ushort))] |= c<<(4*(i%(2*sizeof(ushort))));
+        }
+        return len;
+    }
+
+    void parse(const char *s)
+    {
+        len = parsedigits(digits, BI_DIGITS, s);
+        shrink();
+    }
+
+    void zero() { len = 0; }
+
+    void print(stream *out) const
+    {
+        vector<char> buf;
+        printdigits(buf);
+        out->write(buf.getbuf(), buf.length());
+    }
+
+    void printdigits(vector<char> &buf) const
+    {
+        loopi(len)
+        {
+            digit d = digits[len-i-1];
+            loopj(BI_DIGIT_BITS/4)
+            {
+                uint shift = BI_DIGIT_BITS - (j+1)*4;
+                int val = (d >> shift) & 0xF;
+                if(val < 10) buf.add('0' + val);
+                else buf.add('a' + val - 10);
+            }
+        }
+    }
+
+    template<int Y_DIGITS> bigint &operator=(const bigint<Y_DIGITS> &y)
+    {
+        len = y.len;
+        memcpy(digits, y.digits, len*sizeof(digit));
+        return *this;
+    }
+
+    bool iszero() const { return !len; }
+    bool isone() const { return len==1 && digits[0]==1; }
+
+    int numbits() const
+    {
+        if(!len) return 0;
+        int bits = len*BI_DIGIT_BITS;
+        digit last = digits[len-1], mask = 1<<(BI_DIGIT_BITS-1);
+        while(mask)
+        {
+            if(last&mask) return bits;
+            bits--;
+            mask >>= 1;
+        }
+        return 0;
+    }
+
+    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;
+        int maxlen = max(x.len, y.len), i;
+        for(i = 0; i < y.len || carry; i++)
+        {
+             carry += (i < x.len ? (dbldigit)x.digits[i] : 0) + (i < y.len ? (dbldigit)y.digits[i] : 0);
+             digits[i] = (digit)carry;
+             carry >>= BI_DIGIT_BITS;
+        }
+        if(i < x.len && this != &x) memcpy(&digits[i], &x.digits[i], (x.len - i)*sizeof(digit));
+        len = max(i, maxlen);
+        return *this;
+    }
+    template<int Y_DIGITS> bigint &add(const bigint<Y_DIGITS> &y) { return add(*this, y); }
+
+    template<int X_DIGITS, int Y_DIGITS> bigint &sub(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
+    {
+        ASSERT(x >= y);
+        dbldigit borrow = 0;
+        int i;
+        for(i = 0; i < y.len || borrow; i++)
+        {
+             borrow = (1<<BI_DIGIT_BITS) + (dbldigit)x.digits[i] - (i<y.len ? (dbldigit)y.digits[i] : 0) - borrow;
+             digits[i] = (digit)borrow;
+             borrow = (borrow>>BI_DIGIT_BITS)^1;
+        }
+        if(i < x.len && this != &x) memcpy(&digits[i], &x.digits[i], (x.len - i)*sizeof(digit));
+        len = x.len;
+        shrink();
+        return *this;
+    }
+    template<int Y_DIGITS> bigint &sub(const bigint<Y_DIGITS> &y) { return sub(*this, y); }
+
+    void shrink() { while(len > 0 && !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 = clamp(y.len, 0, 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; }
+        memset(digits, 0, y.len*sizeof(digit));
+        loopi(x.len)
+        {
+            dbldigit carry = 0;
+            loopj(y.len)
+            {
+                carry += (dbldigit)x.digits[i] * (dbldigit)y.digits[j] + (dbldigit)digits[i+j];
+                digits[i+j] = (digit)carry;
+                carry >>= BI_DIGIT_BITS;
+            }
+            digits[i+y.len] = carry;
+        }
+        len = x.len + y.len;
+        shrink();
+        return *this;
+    }
+
+    bigint &rshift(int n)
+    {
+        assert(len <= BI_DIGITS);
+        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(digits[dig]>>n);
+        for(int i = dig+1; i < len; i++)
+        {
+            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);
+        shrink();
+        return *this;
+    }
+
+    bigint &lshift(int n)
+    {
+        if(!len || n<=0) return *this;
+        int dig = n/BI_DIGIT_BITS;
+        n %= BI_DIGIT_BITS;
+        digit carry = 0;
+        loopirev(len)
+        {
+            digit tmp = digits[i];
+            digits[i+dig] = digit((tmp<<n) | carry);
+            carry = digit(tmp>>(BI_DIGIT_BITS-n));
+        }
+        len += dig;
+        if(carry) digits[len++] = carry;
+        if(dig) memset(digits, 0, dig*sizeof(digit));
+        return *this;
+    }
+
+    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 = clamp(y.len-from, 0, 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;
+        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); }
+    template<int Y_DIGITS> bool operator<(const bigint<Y_DIGITS> &y) const
+    {
+        if(len<y.len) return true;
+        if(len>y.len) return false;
+        loopirev(len)
+        {
+            if(digits[i]<y.digits[i]) return true;
+            if(digits[i]>y.digits[i]) return false;
+        }
+        return false;
+    }
+    template<int Y_DIGITS> bool operator>(const bigint<Y_DIGITS> &y) const { return y<*this; }
+    template<int Y_DIGITS> bool operator<=(const bigint<Y_DIGITS> &y) const { return !(y<*this); }
+    template<int Y_DIGITS> bool operator>=(const bigint<Y_DIGITS> &y) const { return !(*this<y); }
+};
+
+#define GF_BITS         192
+#define GF_DIGITS       ((GF_BITS+BI_DIGIT_BITS-1)/BI_DIGIT_BITS)
+
+typedef bigint<GF_DIGITS+1> gfint;
+
+/* NIST prime Galois fields.
+ * 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
+{
+    static const gfield P;
+
+    gfield() {}
+    gfield(digit n) : gfint(n) {}
+    gfield(const char *s) : gfint(s) {}
+
+    template<int Y_DIGITS> gfield(const bigint<Y_DIGITS> &y) : gfint(y) {}
+
+    template<int Y_DIGITS> gfield &operator=(const bigint<Y_DIGITS> &y)
+    {
+        gfint::operator=(y);
+        return *this;
+    }
+
+    template<int X_DIGITS, int Y_DIGITS> gfield &add(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
+    {
+        gfint::add(x, y);
+        if(*this >= P) gfint::sub(*this, P);
+        return *this;
+    }
+    template<int Y_DIGITS> gfield &add(const bigint<Y_DIGITS> &y) { return add(*this, y); }
+
+    template<int X_DIGITS> gfield &mul2(const bigint<X_DIGITS> &x) { return add(x, x); }
+    gfield &mul2() { return mul2(*this); }
+
+    gfield &div2()
+    {
+        if(hasbit(0)) gfint::add(*this, P);
+        rshift(1);
+        return *this;
+    }
+
+    template<int X_DIGITS, int Y_DIGITS> gfield &sub(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
+    {
+        if(x < y)
+        {
+            gfint tmp; /* necessary if this==&y, using this instead would clobber y */
+            tmp.add(x, P);
+            gfint::sub(tmp, y);
+        }
+        else gfint::sub(x, y);
+        return *this;
+    }
+    template<int Y_DIGITS> gfield &sub(const bigint<Y_DIGITS> &y) { return sub(*this, y); }
+
+    template<int X_DIGITS> gfield &neg(const bigint<X_DIGITS> &x)
+    {
+        gfint::sub(P, x);
+        return *this;
+    }
+    gfield &neg() { return neg(*this); }
+
+    template<int X_DIGITS> gfield &square(const bigint<X_DIGITS> &x) { return mul(x, x); }
+    gfield &square() { return square(*this); }
+
+    template<int X_DIGITS, int Y_DIGITS> gfield &mul(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
+    {
+        bigint<X_DIGITS+Y_DIGITS> result;
+        result.mul(x, y);
+        reduce(result);
+        return *this;
+    }
+    template<int Y_DIGITS> gfield &mul(const bigint<Y_DIGITS> &y) { return mul(*this, y); }
+
+    template<int RESULT_DIGITS> void reduce(const bigint<RESULT_DIGITS> &result)
+    {
+#if GF_BITS==192
+        // B = T + S1 + S2 + S3 mod p
+        copyshrinkdigits(result, GF_DIGITS); // T
+
+        if(result.morebits(192))
+        {
+            gfield s;
+            s.copybits(0, result, 192, 64);
+            s.dupbits(64, 0, 64);
+            s.shrinkbits(128);
+            add(s); // S1
+
+            if(result.morebits(256))
+            {
+                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))
+                {
+                    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
+    }
+
+    template<int X_DIGITS, int Y_DIGITS> gfield &pow(const bigint<X_DIGITS> &x, const bigint<Y_DIGITS> &y)
+    {
+        gfield a(x);
+        if(y.hasbit(0)) *this = a;
+        else
+        {
+            len = 1;
+            digits[0] = 1;
+            if(!y.len) return *this;
+        }
+        for(int i = 1, j = y.numbits(); i < j; i++)
+        {
+            a.square();
+            if(y.hasbit(i)) mul(a);
+        }
+        return *this;
+    }
+    template<int Y_DIGITS> gfield &pow(const bigint<Y_DIGITS> &y) { return pow(*this, y); }
+
+    bool invert(const gfield &x)
+    {
+        if(!x.len) return false;
+        gfint u(x), v(P), A((gfint::digit)1), C((gfint::digit)0);
+        while(!u.iszero())
+        {
+            int ushift = 0, ashift = 0;
+            while(!u.hasbit(ushift))
+            {
+                ushift++;
+                if(A.hasbit(ashift))
+                {
+                    if(ashift) { A.rshift(ashift); ashift = 0; }
+                    A.add(P);
+                }
+                ashift++;
+            }
+            if(ushift) u.rshift(ushift);
+            if(ashift) A.rshift(ashift);
+            int vshift = 0, cshift = 0;
+            while(!v.hasbit(vshift))
+            {
+                vshift++;
+                if(C.hasbit(cshift))
+                {
+                    if(cshift) { C.rshift(cshift); cshift = 0; }
+                    C.add(P);
+                }
+                cshift++;
+            }
+            if(vshift) v.rshift(vshift);
+            if(cshift) C.rshift(cshift);
+            if(u >= v)
+            {
+                u.sub(v);
+                if(A < C) A.add(P);
+                A.sub(C);
+            }
+            else
+            {
+                v.sub(v, u);
+                if(C < A) C.add(P);
+                C.sub(A);
+            }
+        }
+        if(C >= P) gfint::sub(C, P);
+        else { len = C.len; memcpy(digits, C.digits, len*sizeof(digit)); }
+        ASSERT(*this < P);
+        return true;
+    }
+    void invert() { invert(*this); }
+
+    template<int X_DIGITS> static int legendre(const bigint<X_DIGITS> &x)
+    {
+        static const gfint Psub1div2(gfint(P).sub(bigint<1>(1)).rshift(1));
+        gfield L;
+        L.pow(x, Psub1div2);
+        if(!L.len) return 0;
+        if(L.len==1) return 1;
+        return -1;
+    }
+    int legendre() const { return legendre(*this); }
+
+    bool sqrt(const gfield &x)
+    {
+        if(!x.len) { len = 0; return true; }
+#if GF_BITS==224
+#error Unsupported GF
+#else
+        ASSERT((P.digits[0]%4)==3);
+        static const gfint Padd1div4(gfint(P).add(bigint<1>(1)).rshift(2));
+        switch(legendre(x))
+        {
+            case 0: len = 0; return true;
+            case -1: return false;
+            default: pow(x, Padd1div4); return true;
+        }
+#endif
+    }
+    bool sqrt() { return sqrt(*this); }
+};
+
+struct ecjacobian
+{
+    static const gfield B;
+    static const ecjacobian base;
+    static const ecjacobian origin;
+
+    gfield x, y, z;
+
+    ecjacobian() {}
+    ecjacobian(const gfield &x, const gfield &y) : x(x), y(y), z(bigint<1>(1)) {}
+    ecjacobian(const gfield &x, const gfield &y, const gfield &z) : x(x), y(y), z(z) {}
+
+    void mul2()
+    {
+        if(z.iszero()) return;
+        else if(y.iszero()) { *this = origin; return; }
+        gfield a, b, c, d;
+        d.sub(x, c.square(z));
+        d.mul(c.add(x));
+        c.mul2(d).add(d);
+        z.mul(y).add(z);
+        a.square(y);
+        b.mul2(a);
+        d.mul2(x).mul(b);
+        x.square(c).sub(d).sub(d);
+        a.square(b).add(a);
+        y.sub(d, x).mul(c).sub(a);
+    }
+
+    void add(const ecjacobian &q)
+    {
+        if(q.z.iszero()) return;
+        else if(z.iszero()) { *this = q; return; }
+        gfield a, b, c, d, e, f;
+        a.square(z);
+        b.mul(q.y, a).mul(z);
+        a.mul(q.x);
+        if(q.z.isone())
+        {
+            c.add(x, a);
+            d.add(y, b);
+            a.sub(x, a);
+            b.sub(y, b);
+        }
+        else
+        {
+            f.mul(y, e.square(q.z)).mul(q.z);
+            e.mul(x);
+            c.add(e, a);
+            d.add(f, b);
+            a.sub(e, a);
+            b.sub(f, b);
+        }
+        if(a.iszero()) { if(b.iszero()) mul2(); else *this = origin; return; }
+        if(!q.z.isone()) z.mul(q.z);
+        z.mul(a);
+        x.square(b).sub(f.mul(c, e.square(a)));
+        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)
+    {
+        *this = origin;
+        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); }
+
+    void normalize()
+    {
+        if(z.iszero() || z.isone()) return;
+        gfield tmp;
+        z.invert();
+        tmp.square(z);
+        x.mul(tmp);
+        y.mul(tmp).mul(z);
+        z = bigint<1>(1);
+    }
+
+    bool calcy(bool ybit)
+    {
+        gfield y2, tmp;
+        y2.square(x).mul(x).sub(tmp.add(x, x).add(x)).add(B);
+        if(!y.sqrt(y2)) { y.zero(); return false; }
+        if(y.hasbit(0) != ybit) y.neg();
+        return true;
+    }
+
+    void print(vector<char> &buf)
+    {
+        normalize();
+        buf.add(y.hasbit(0) ? '-' : '+');
+        x.printdigits(buf);
+    }
+
+    void parse(const char *s)
+    {
+        bool ybit = *s++ == '-';
+        x.parse(s);
+        calcy(ybit);
+        z = bigint<1>(1);
+    }
+};
+
+const ecjacobian ecjacobian::origin(gfield((gfield::digit)1), gfield((gfield::digit)1), gfield((gfield::digit)0));
+
+#if GF_BITS==192
+const gfield gfield::P("fffffffffffffffffffffffffffffffeffffffffffffffff");
+const gfield ecjacobian::B("64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1");
+const ecjacobian ecjacobian::base(
+    gfield("188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012"),
+    gfield("07192b95ffc8da78631011ed6b24cdd573f977a11e794811")
+);
+#elif GF_BITS==224
+const gfield gfield::P("ffffffffffffffffffffffffffffffff000000000000000000000001");
+const gfield ecjacobian::B("b4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4");
+const ecjacobian ecjacobian::base(
+    gfield("b70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21"),
+    gfield("bd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34")
+);
+#elif GF_BITS==256
+const gfield gfield::P("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff");
+const gfield ecjacobian::B("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b");
+const ecjacobian ecjacobian::base(
+    gfield("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"),
+    gfield("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5")
+);
+#elif GF_BITS==384
+const gfield gfield::P("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff");
+const gfield ecjacobian::B("b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef");
+const ecjacobian ecjacobian::base(
+    gfield("aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"),
+    gfield("3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f")
+);
+#elif GF_BITS==521
+const gfield gfield::P("1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
+const gfield ecjacobian::B("051953eb968e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00");
+const ecjacobian ecjacobian::base(
+    gfield("c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"),
+    gfield("11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650")
+);
+#else
+#error Unsupported GF
+#endif
+
+void calcpubkey(gfint privkey, vector<char> &pubstr)
+{
+    ecjacobian c(ecjacobian::base);
+    c.mul(privkey);
+    c.normalize();
+    c.print(pubstr);
+    pubstr.add('\0');
+}
+
+bool calcpubkey(const char *privstr, vector<char> &pubstr)
+{
+    if(!privstr[0]) return false;
+    gfint privkey;
+    privkey.parse(privstr);
+    calcpubkey(privkey, pubstr);
+    return true;
+}
+
+void genprivkey(const char *seed, vector<char> &privstr, vector<char> &pubstr)
+{
+    tiger::hashval hash;
+    tiger::hash((const uchar *)seed, (int)strlen(seed), hash);
+    bigint<8*sizeof(hash.bytes)/BI_DIGIT_BITS> privkey;
+    memcpy(privkey.digits, hash.bytes, sizeof(hash.bytes));
+    privkey.len = 8*sizeof(hash.bytes)/BI_DIGIT_BITS;
+    privkey.shrink();
+    privkey.printdigits(privstr);
+    privstr.add('\0');
+
+    calcpubkey(privkey, pubstr);
+}
+
+bool hashstring(const char *str, char *result, int maxlen)
+{
+    tiger::hashval hv;
+    if(maxlen < 2*(int)sizeof(hv.bytes) + 1) return false;
+    tiger::hash((uchar *)str, strlen(str), hv);
+    loopi(sizeof(hv.bytes))
+    {
+        uchar c = hv.bytes[i];
+        *result++ = "0123456789abcdef"[c&0xF];
+        *result++ = "0123456789abcdef"[c>>4];
+    }
+    *result = '\0';
+    return true;
+}
+
+void answerchallenge(const char *privstr, const char *challenge, vector<char> &answerstr)
+{
+    gfint privkey;
+    privkey.parse(privstr);
+    ecjacobian answer;
+    answer.parse(challenge);
+    answer.mul(privkey);
+    answer.normalize();
+    answer.x.printdigits(answerstr);
+    answerstr.add('\0');
+}
+
+void *parsepubkey(const char *pubstr)
+{
+    ecjacobian *pubkey = new ecjacobian;
+    pubkey->parse(pubstr);
+    return pubkey;
+}
+
+void freepubkey(void *pubkey)
+{
+    delete (ecjacobian *)pubkey;
+}
+
+void *genchallenge(void *pubkey, const void *seed, int seedlen, vector<char> &challengestr)
+{
+    tiger::hashval 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;
+    challenge.shrink();
+
+    ecjacobian answer(*(ecjacobian *)pubkey);
+    answer.mul(challenge);
+    answer.normalize();
+
+    ecjacobian secret(ecjacobian::base);
+    secret.mul(challenge);
+    secret.normalize();
+
+    secret.print(challengestr);
+    challengestr.add('\0');
+   
+    return new gfield(answer.x);
+}
+
+void freechallenge(void *answer)
+{
+    delete (gfint *)answer;
+}
+
+bool checkchallenge(const char *answerstr, void *correct)
+{
+    gfint answer(answerstr);
+    return answer == *(gfint *)correct;
+}
+
diff --git a/src/shared/cube.h b/src/shared/cube.h
new file mode 100644 (file)
index 0000000..ed1c207
--- /dev/null
@@ -0,0 +1,68 @@
+#ifndef __CUBE_H__
+#define __CUBE_H__
+
+#define _FILE_OFFSET_BITS 64
+
+#ifdef WIN32
+#define _USE_MATH_DEFINES
+#endif
+#include <math.h>
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <assert.h>
+#include <time.h>
+
+#ifdef WIN32
+  #define WIN32_LEAN_AND_MEAN
+  #ifdef _WIN32_WINNT
+  #undef _WIN32_WINNT
+  #endif
+  #define _WIN32_WINNT 0x0500
+  #include "windows.h"
+  #ifndef _WINDOWS
+    #define _WINDOWS
+  #endif
+  #ifndef __GNUC__
+    #include <eh.h>
+    #include <dbghelp.h>
+    #include <intrin.h>
+  #endif
+  #define ZLIB_DLL
+#endif
+
+#ifndef STANDALONE
+  #ifdef __APPLE__
+    #include "SDL.h"
+    #define GL_GLEXT_LEGACY
+    #define __glext_h_
+    #include <OpenGL/gl.h>
+  #else
+    #include <SDL.h>
+    #include <SDL_opengl.h>
+  #endif
+#endif
+
+#include <enet/enet.h>
+
+#include <zlib.h>
+
+#include "tools.h"
+#include "geom.h"
+#include "ents.h"
+#include "command.h"
+
+#ifndef STANDALONE
+#include "glexts.h"
+#include "glemu.h"
+#endif
+
+#include "iengine.h"
+#include "igame.h"
+
+#endif
+
diff --git a/src/shared/cube2font.c b/src/shared/cube2font.c
new file mode 100644 (file)
index 0000000..94e407d
--- /dev/null
@@ -0,0 +1,556 @@
+#include <stdlib.h>\r
+#include <string.h>\r
+#include <stdio.h>\r
+#include <stdarg.h>\r
+#include <limits.h>\r
+#include <zlib.h>\r
+#include <ft2build.h>\r
+#include FT_FREETYPE_H\r
+#include FT_STROKER_H\r
+#include FT_GLYPH_H\r
+\r
+typedef unsigned char uchar;\r
+typedef unsigned short ushort;\r
+typedef unsigned int uint;\r
+\r
+int imin(int a, int b) { return a < b ? a : b; }\r
+int imax(int a, int b) { return a > b ? a : b; }\r
+\r
+void fatal(const char *fmt, ...)\r
+{\r
+    va_list v;\r
+    va_start(v, fmt);\r
+    vfprintf(stderr, fmt, v);\r
+    va_end(v);\r
+    fputc('\n', stderr);\r
+\r
+    exit(EXIT_FAILURE);\r
+}\r
+\r
+uint bigswap(uint n)\r
+{\r
+    const int islittleendian = 1;\r
+    return *(const uchar *)&islittleendian ? (n<<24) | (n>>24) | ((n>>8)&0xFF00) | ((n<<8)&0xFF0000) : n;\r
+}\r
+\r
+size_t writebig(FILE *f, uint n)\r
+{\r
+    n = bigswap(n);\r
+    return fwrite(&n, 1, sizeof(n), f);\r
+}\r
+\r
+void writepngchunk(FILE *f, const char *type, uchar *data, uint len)\r
+{\r
+    uint crc;\r
+    writebig(f, len);\r
+    fwrite(type, 1, 4, f);\r
+    fwrite(data, 1, len, f);\r
+\r
+    crc = crc32(0, Z_NULL, 0);\r
+    crc = crc32(crc, (const Bytef *)type, 4);\r
+    if(data) crc = crc32(crc, data, len);\r
+    writebig(f, crc);\r
+}\r
+\r
+struct pngihdr\r
+{\r
+    uint width, height;\r
+    uchar bitdepth, colortype, compress, filter, interlace;\r
+};\r
+\r
+void savepng(const char *filename, uchar *data, int w, int h, int bpp, int flip)\r
+{\r
+    const uchar signature[] = { 137, 80, 78, 71, 13, 10, 26, 10 };\r
+    struct pngihdr ihdr;\r
+    FILE *f;\r
+    long idat;\r
+    uint len, crc;\r
+    z_stream z;\r
+    uchar buf[1<<12];\r
+    int i, j;\r
+\r
+    memset(&ihdr, 0, sizeof(ihdr));\r
+    ihdr.width = bigswap(w);\r
+    ihdr.height = bigswap(h);\r
+    ihdr.bitdepth = 8;\r
+    switch(bpp)\r
+    {\r
+        case 1: ihdr.colortype = 0; break;\r
+        case 2: ihdr.colortype = 4; break;\r
+        case 3: ihdr.colortype = 2; break;\r
+        case 4: ihdr.colortype = 6; break;\r
+        default: fatal("cube2font: invalid PNG bpp"); return;\r
+    }\r
+    f = fopen(filename, "wb");\r
+    if(!f) { fatal("cube2font: could not write to %s", filename); return; }\r
+\r
+    fwrite(signature, 1, sizeof(signature), f);\r
+\r
+    writepngchunk(f, "IHDR", (uchar *)&ihdr, 13);\r
+\r
+    idat = ftell(f);\r
+    len = 0;\r
+    fwrite("\0\0\0\0IDAT", 1, 8, f);\r
+    crc = crc32(0, Z_NULL, 0);\r
+    crc = crc32(crc, (const Bytef *)"IDAT", 4);\r
+\r
+    z.zalloc = NULL;\r
+    z.zfree = NULL;\r
+    z.opaque = NULL;\r
+\r
+    if(deflateInit(&z, Z_BEST_COMPRESSION) != Z_OK)\r
+        goto error;\r
+\r
+    z.next_out = (Bytef *)buf;\r
+    z.avail_out = sizeof(buf);\r
+\r
+    for(i = 0; i < h; i++)\r
+    {\r
+        uchar filter = 0;\r
+        for(j = 0; j < 2; j++)\r
+        {\r
+            z.next_in = j ? (Bytef *)data + (flip ? h-i-1 : i)*w*bpp : (Bytef *)&filter;\r
+            z.avail_in = j ? w*bpp : 1;\r
+            while(z.avail_in > 0)\r
+            {\r
+                if(deflate(&z, Z_NO_FLUSH) != Z_OK) goto cleanuperror;\r
+                #define FLUSHZ do { \\r
+                    int flush = sizeof(buf) - z.avail_out; \\r
+                    crc = crc32(crc, buf, flush); \\r
+                    len += flush; \\r
+                    fwrite(buf, 1, flush, f); \\r
+                    z.next_out = (Bytef *)buf; \\r
+                    z.avail_out = sizeof(buf); \\r
+                } while(0)\r
+                FLUSHZ;\r
+            }\r
+        }\r
+    }\r
+\r
+    for(;;)\r
+    {\r
+        int err = deflate(&z, Z_FINISH);\r
+        if(err != Z_OK && err != Z_STREAM_END) goto cleanuperror;\r
+        FLUSHZ;\r
+        if(err == Z_STREAM_END) break;\r
+    }\r
+\r
+    deflateEnd(&z);\r
+\r
+    fseek(f, idat, SEEK_SET);\r
+    writebig(f, len);\r
+    fseek(f, 0, SEEK_END);\r
+    writebig(f, crc);\r
+\r
+    writepngchunk(f, "IEND", NULL, 0);\r
+\r
+    fclose(f);\r
+    return;\r
+\r
+cleanuperror:\r
+    deflateEnd(&z);\r
+\r
+error:\r
+    fclose(f);\r
+\r
+    fatal("cube2font: failed saving PNG to %s", filename);\r
+}\r
+\r
+enum\r
+{\r
+    CT_PRINT   = 1<<0,\r
+    CT_SPACE   = 1<<1,\r
+    CT_DIGIT   = 1<<2,\r
+    CT_ALPHA   = 1<<3,\r
+    CT_LOWER   = 1<<4,\r
+    CT_UPPER   = 1<<5,\r
+    CT_UNICODE = 1<<6\r
+};\r
+#define CUBECTYPE(s, p, d, a, A, u, U) \\r
+    0, U, U, U, U, U, U, U, U, s, s, s, s, s, U, U, \\r
+    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \\r
+    s, p, p, p, p, p, p, p, p, p, p, p, p, p, p, p, \\r
+    d, d, d, d, d, d, d, d, d, d, p, p, p, p, p, p, \\r
+    p, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, \\r
+    A, A, A, A, A, A, A, A, A, A, A, p, p, p, p, p, \\r
+    p, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, \\r
+    a, a, a, a, a, a, a, a, a, a, a, p, p, p, p, U, \\r
+    U, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, \\r
+    u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, \\r
+    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \\r
+    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \\r
+    u, U, u, U, u, U, u, U, U, u, U, u, U, u, U, U, \\r
+    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \\r
+    U, U, U, U, u, u, u, u, u, u, u, u, u, u, u, u, \\r
+    u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, u\r
+const uchar cubectype[256] =\r
+{\r
+    CUBECTYPE(CT_SPACE,\r
+              CT_PRINT,\r
+              CT_PRINT|CT_DIGIT,\r
+              CT_PRINT|CT_ALPHA|CT_LOWER,\r
+              CT_PRINT|CT_ALPHA|CT_UPPER,\r
+              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_LOWER,\r
+              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_UPPER)\r
+};\r
+int iscubeprint(uchar c) { return cubectype[c]&CT_PRINT; }\r
+int iscubespace(uchar c) { return cubectype[c]&CT_SPACE; }\r
+int iscubealpha(uchar c) { return cubectype[c]&CT_ALPHA; }\r
+int iscubealnum(uchar c) { return cubectype[c]&(CT_ALPHA|CT_DIGIT); }\r
+int iscubelower(uchar c) { return cubectype[c]&CT_LOWER; }\r
+int iscubeupper(uchar c) { return cubectype[c]&CT_UPPER; }\r
+const int cube2unichars[256] =\r
+{\r
+    0, 192, 193, 194, 195, 196, 197, 198, 199, 9, 10, 11, 12, 13, 200, 201,\r
+    202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 213, 214, 216, 217, 218, 219,\r
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,\r
+    48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,\r
+    64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,\r
+    80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,\r
+    96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,\r
+    112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 220,\r
+    221, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237,\r
+    238, 239, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 252, 253, 255, 0x104,\r
+    0x105, 0x106, 0x107, 0x10C, 0x10D, 0x10E, 0x10F, 0x118, 0x119, 0x11A, 0x11B, 0x11E, 0x11F, 0x130, 0x131, 0x141,\r
+    0x142, 0x143, 0x144, 0x147, 0x148, 0x150, 0x151, 0x152, 0x153, 0x158, 0x159, 0x15A, 0x15B, 0x15E, 0x15F, 0x160,\r
+    0x161, 0x164, 0x165, 0x16E, 0x16F, 0x170, 0x171, 0x178, 0x179, 0x17A, 0x17B, 0x17C, 0x17D, 0x17E, 0x404, 0x411,\r
+    0x413, 0x414, 0x416, 0x417, 0x418, 0x419, 0x41B, 0x41F, 0x423, 0x424, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B,\r
+    0x42C, 0x42D, 0x42E, 0x42F, 0x431, 0x432, 0x433, 0x434, 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D,\r
+    0x43F, 0x442, 0x444, 0x446, 0x447, 0x448, 0x449, 0x44A, 0x44B, 0x44C, 0x44D, 0x44E, 0x44F, 0x454, 0x490, 0x491\r
+};\r
+int cube2uni(uchar c)\r
+{\r
+    return cube2unichars[c];\r
+}\r
+\r
+const char *encodeutf8(int uni)\r
+{\r
+    static char buf[7];\r
+    char *dst = buf;\r
+    if(uni <= 0x7F) { *dst++ = uni; goto uni1; }\r
+    else if(uni <= 0x7FF) { *dst++ = 0xC0 | (uni>>6); goto uni2; }\r
+    else if(uni <= 0xFFFF) { *dst++ = 0xE0 | (uni>>12); goto uni3; }\r
+    else if(uni <= 0x1FFFFF) { *dst++ = 0xF0 | (uni>>18); goto uni4; }\r
+    else if(uni <= 0x3FFFFFF) { *dst++ = 0xF8 | (uni>>24); goto uni5; }\r
+    else if(uni <= 0x7FFFFFFF) { *dst++ = 0xFC | (uni>>30); goto uni6; }\r
+    else goto uni1;\r
+uni6: *dst++ = 0x80 | ((uni>>24)&0x3F);\r
+uni5: *dst++ = 0x80 | ((uni>>18)&0x3F);\r
+uni4: *dst++ = 0x80 | ((uni>>12)&0x3F);\r
+uni3: *dst++ = 0x80 | ((uni>>6)&0x3F);\r
+uni2: *dst++ = 0x80 | (uni&0x3F);\r
+uni1: *dst++ = '\0';\r
+    return buf;\r
+}\r
+\r
+struct fontchar { int code, uni, tex, x, y, w, h, offx, offy, offset, advance; FT_BitmapGlyph color, alpha; };\r
+\r
+const char *texdir = "";\r
+\r
+const char *texfilename(const char *name, int texnum)\r
+{\r
+    static char file[256];\r
+    snprintf(file, sizeof(file), "%s%d.png", name, texnum);\r
+    return file;\r
+}\r
+\r
+const char *texname(const char *name, int texnum)\r
+{\r
+    static char file[512];\r
+    snprintf(file, sizeof(file), "<grey>%s%s", texdir, texfilename(name, texnum));\r
+    return file;\r
+}\r
+\r
+void writetexs(const char *name, struct fontchar *chars, int numchars, int numtexs, int tw, int th)\r
+{\r
+    int tex;\r
+    uchar *pixels = (uchar *)malloc(tw*th*2);\r
+    if(!pixels) fatal("cube2font: failed allocating textures");\r
+    for(tex = 0; tex < numtexs; tex++)\r
+    {\r
+        const char *file = texfilename(name, tex);\r
+        int texchars = 0, i;\r
+        uchar *dst, *src;\r
+        memset(pixels, 0, tw*th*2);\r
+        for(i = 0; i < numchars; i++)\r
+        {\r
+            struct fontchar *c = &chars[i];\r
+            int x, y;\r
+            if(c->tex != tex) continue;\r
+            texchars++;\r
+            dst = &pixels[2*((c->y + c->offy - c->color->top)*tw + c->x + c->color->left - c->offx)];\r
+            src = (uchar *)c->color->bitmap.buffer;\r
+            for(y = 0; y < c->color->bitmap.rows; y++)\r
+            {\r
+                for(x = 0; x < c->color->bitmap.width; x++)\r
+                    dst[2*x] = src[x];\r
+                src += c->color->bitmap.pitch;\r
+                dst += 2*tw;\r
+            }\r
+            dst = &pixels[2*((c->y + c->offy - c->alpha->top)*tw + c->x + c->alpha->left - c->offx)];\r
+            src = (uchar *)c->alpha->bitmap.buffer;\r
+            for(y = 0; y < c->alpha->bitmap.rows; y++)\r
+            {\r
+                for(x = 0; x < c->alpha->bitmap.width; x++)\r
+                    dst[2*x+1] = src[x];\r
+                src += c->alpha->bitmap.pitch;\r
+                dst += 2*tw;\r
+            }\r
+        }\r
+        printf("cube2font: writing %d chars to %s\n", texchars, file);\r
+        savepng(file, pixels, tw, th, 2, 0);\r
+   }\r
+   free(pixels);\r
+}\r
+\r
+void writecfg(const char *name, struct fontchar *chars, int numchars, int x1, int y1, int x2, int y2, int sw, int sh, int argc, char **argv)\r
+{\r
+    FILE *f;\r
+    char file[256];\r
+    int i, lastcode = 0, lasttex = 0;\r
+    snprintf(file, sizeof(file), "%s.cfg", name);\r
+    f = fopen(file, "w");\r
+    if(!f) fatal("cube2font: failed writing %s", file);\r
+    printf("cube2font: writing %d chars to %s\n", numchars, file);\r
+    fprintf(f, "//");\r
+    for(i = 1; i < argc; i++)\r
+        fprintf(f, " %s", argv[i]);\r
+    fprintf(f, "\n");\r
+    fprintf(f, "font \"%s\" \"%s\" %d %d\n", name, texname(name, 0), sw, sh);\r
+    for(i = 0; i < numchars; i++)\r
+    {\r
+        struct fontchar *c = &chars[i];\r
+        if(!lastcode && lastcode < c->code)\r
+        {\r
+            fprintf(f, "fontoffset \"%s\"\n", encodeutf8(c->uni));\r
+            lastcode = c->code;\r
+        }\r
+        else if(lastcode < c->code)\r
+        {\r
+            if(lastcode + 1 == c->code)\r
+                fprintf(f, "fontskip // %d\n", lastcode);\r
+            else\r
+                fprintf(f, "fontskip %d // %d .. %d\n", c->code - lastcode, lastcode, c->code-1);\r
+            lastcode = c->code;\r
+        }\r
+        if(lasttex != c->tex)\r
+        {\r
+            fprintf(f, "\nfonttex \"%s\"\n", texname(name, c->tex));\r
+            lasttex = c->tex;\r
+        }\r
+        if(c->code != c->uni)\r
+            fprintf(f, "fontchar %d %d %d %d %d %d %d // %s (%d -> 0x%X)\n", c->x, c->y, c->w, c->h, c->offx+c->offset, y2-c->offy, c->advance, encodeutf8(c->uni), c->code, c->uni);\r
+        else\r
+            fprintf(f, "fontchar %d %d %d %d %d %d %d // %s (%d)\n", c->x, c->y, c->w, c->h, c->offx+c->offset, y2-c->offy, c->advance, encodeutf8(c->uni), c->code);\r
+        lastcode++;\r
+    }\r
+    fclose(f);\r
+}\r
+\r
+int groupchar(int c)\r
+{\r
+    switch(c)\r
+    {\r
+    case 0x152: case 0x153: case 0x178: return 1;\r
+    }\r
+    if(c < 127 || c >= 0x2000) return 0;\r
+    if(c < 0x100) return 1;\r
+    if(c < 0x400) return 2;\r
+    return 3;\r
+}\r
+\r
+int sortchars(const void *x, const void *y)\r
+{\r
+    const struct fontchar *xc = *(const struct fontchar **)x, *yc = *(const struct fontchar **)y;\r
+    int xg = groupchar(xc->uni), yg = groupchar(yc->uni);\r
+    if(xg < yg) return -1;\r
+    if(xg > yg) return 1;\r
+    if(xc->h != yc->h) return yc->h - xc->h;\r
+    if(xc->w != yc->w) return yc->w - xc->w;\r
+    return yc->uni - xc->uni;\r
+}\r
+\r
+int scorechar(struct fontchar *f, int pad, int tw, int th, int rw, int rh, int ry)\r
+{\r
+    int score = 0;\r
+    if(rw + f->w > tw) { ry += rh + pad; score = 1; }\r
+    if(ry + f->h > th) score = 2;\r
+    return score;\r
+}\r
+\r
+int main(int argc, char **argv)\r
+{\r
+    FT_Library l;\r
+    FT_Face f;\r
+    FT_Stroker s, s2;\r
+    int i, pad, offset, advance, w, h, tw, th, c, trial = -2, rw = 0, rh = 0, ry = 0, x1 = INT_MAX, x2 = INT_MIN, y1 = INT_MAX, y2 = INT_MIN, w2 = 0, h2 = 0, sw = 0, sh = 0;\r
+    float outborder = 0, inborder = 0;\r
+    struct fontchar chars[256];\r
+    struct fontchar *order[256];\r
+    int numchars = 0, numtex = 0;\r
+    if(argc < 11)\r
+        fatal("Usage: cube2font infile outfile outborder[:inborder] pad offset advance charwidth charheight texwidth texheight [spacewidth spaceheight texdir]");\r
+    sscanf(argv[3], "%f:%f", &outborder, &inborder);\r
+    pad = atoi(argv[4]);\r
+    offset = atoi(argv[5]);\r
+    advance = atoi(argv[6]);\r
+    w = atoi(argv[7]);\r
+    h = atoi(argv[8]);\r
+    tw = atoi(argv[9]);\r
+    th = atoi(argv[10]);\r
+    if(argc > 11) sw = atoi(argv[11]);\r
+    if(argc > 12) sh = atoi(argv[12]);\r
+    if(argc > 13) texdir = argv[13];\r
+    if(FT_Init_FreeType(&l))\r
+        fatal("cube2font: failed initing freetype");\r
+    if(FT_New_Face(l, argv[1], 0, &f) ||\r
+       FT_Set_Charmap(f, f->charmaps[0]) ||\r
+       FT_Set_Pixel_Sizes(f, w, h) ||\r
+       FT_Stroker_New(l, &s) ||\r
+       FT_Stroker_New(l, &s2))\r
+        fatal("cube2font: failed loading font %s", argv[1]);\r
+    if(outborder > 0) FT_Stroker_Set(s, (FT_Fixed)(outborder * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);\r
+    if(inborder > 0) FT_Stroker_Set(s2, (FT_Fixed)(inborder * 64), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);\r
+    for(c = 0; c < 256; c++) if(iscubeprint(c))\r
+    {\r
+        FT_Glyph p, p2;\r
+        FT_BitmapGlyph b, b2;\r
+        struct fontchar *dst = &chars[numchars];\r
+        dst->code = c;\r
+        dst->uni = cube2uni(c);\r
+        if(FT_Load_Char(f, dst->uni, FT_LOAD_DEFAULT))\r
+            fatal("cube2font: failed loading character %s", encodeutf8(dst->uni));\r
+        FT_Get_Glyph(f->glyph, &p);\r
+        p2 = p;\r
+        if(outborder > 0) FT_Glyph_StrokeBorder(&p, s, 0, 0);\r
+        if(inborder > 0) FT_Glyph_StrokeBorder(&p2, s2, 1, 0);\r
+        FT_Glyph_To_Bitmap(&p, FT_RENDER_MODE_NORMAL, 0, 1);\r
+        if(inborder > 0 || outborder > 0) FT_Glyph_To_Bitmap(&p2, FT_RENDER_MODE_NORMAL, 0, 1);\r
+        else p2 = p;\r
+        b = (FT_BitmapGlyph)p;\r
+        b2 = (FT_BitmapGlyph)p2;\r
+        dst->tex = -1;\r
+        dst->x = INT_MIN;\r
+        dst->y = INT_MIN;\r
+        dst->offx = imin(b->left, b2->left);\r
+        dst->offy = imax(b->top, b2->top);\r
+        dst->offset = offset;\r
+        dst->advance = offset + ((p->advance.x+0xFFFF)>>16) + advance;\r
+        dst->w = imax(b->left + b->bitmap.width, b2->left + b2->bitmap.width) - dst->offx;\r
+        dst->h = dst->offy - imin(b->top - b->bitmap.rows, b2->top - b2->bitmap.rows);\r
+        dst->alpha = b;\r
+        dst->color = b2;\r
+        order[numchars++] = dst;\r
+    }\r
+    qsort(order, numchars, sizeof(order[0]), sortchars);\r
+    for(i = 0; i < numchars;)\r
+    {\r
+        struct fontchar *dst;\r
+        int j, k, trial0, prevscore, dstscore, fitscore;\r
+        for(trial0 = trial, prevscore = -1; (trial -= 2) >= trial0-512;)\r
+        {\r
+            int g, fw = rw, fh = rh, fy = ry, curscore = 0, reused = 0;\r
+            for(j = i; j < numchars; j++)\r
+            {\r
+                dst = order[j];\r
+                if(dst->tex >= 0 || dst->tex <= trial) continue;\r
+                g = groupchar(dst->uni);\r
+                dstscore = scorechar(dst, pad, tw, th, fw, fh, fy);\r
+                for(k = j; k < numchars; k++)\r
+                {\r
+                    struct fontchar *fit = order[k];\r
+                    if(fit->tex >= 0 || fit->tex <= trial) continue;\r
+                    if(fit->tex >= trial0 && groupchar(fit->uni) != g) break;\r
+                    fitscore = scorechar(fit, pad, tw, th, fw, fh, fy);\r
+                    if(fitscore < dstscore || (fitscore == dstscore && fit->h > dst->h))\r
+                    {\r
+                        dst = fit;\r
+                        dstscore = fitscore;\r
+                    }\r
+                }\r
+                if(fw + dst->w > tw)\r
+                {\r
+                    fy += fh + pad;\r
+                    fw = fh = 0;\r
+                }\r
+                if(fy + dst->h > th)\r
+                {\r
+                    fy = fw = fh = 0;\r
+                    if(curscore > 0) break;\r
+                }\r
+                if(dst->tex >= trial+1 && dst->tex <= trial+2) { dst->tex = trial; reused++; }\r
+                else dst->tex = trial;\r
+                fw += dst->w + pad;\r
+                fh = imax(fh, dst->h);\r
+                if(dst != order[j]) --j;\r
+                curscore++;\r
+            }\r
+            if(reused < prevscore || curscore <= prevscore) break;\r
+            prevscore = curscore;\r
+        }\r
+        for(; i < numchars; i++)\r
+        {\r
+            dst = order[i];\r
+            if(dst->tex >= 0) continue;\r
+            dstscore = scorechar(dst, pad, tw, th, rw, rh, ry);\r
+            for(j = i; j < numchars; j++)\r
+            {\r
+                struct fontchar *fit = order[j];\r
+                if(fit->tex < trial || fit->tex > trial+2) continue;\r
+                fitscore = scorechar(fit, pad, tw, th, rw, rh, ry);\r
+                if(fitscore < dstscore || (fitscore == dstscore && fit->h > dst->h))\r
+                {\r
+                    dst = fit;\r
+                    dstscore = fitscore;\r
+                }\r
+            }\r
+            if(dst->tex < trial || dst->tex > trial+2) break;\r
+            if(rw + dst->w > tw)\r
+            {\r
+                ry += rh + pad;\r
+                rw = rh = 0;\r
+            }\r
+            if(ry + dst->h > th)\r
+            {\r
+                ry = rw = rh = 0;\r
+                numtex++;\r
+            }\r
+            dst->tex = numtex;\r
+            dst->x = rw;\r
+            dst->y = ry;\r
+            rw += dst->w + pad;\r
+            rh = imax(rh, dst->h);\r
+            y1 = imin(y1, dst->offy - dst->h);\r
+            y2 = imax(y2, dst->offy);\r
+            x1 = imin(x1, dst->offx);\r
+            x2 = imax(x2, dst->offx + dst->w);\r
+            w2 = imax(w2, dst->w);\r
+            h2 = imax(h2, dst->h);\r
+            if(dst != order[i]) --i;\r
+        }\r
+    }\r
+    if(rh > 0) numtex++;\r
+#if 0\r
+    if(sw <= 0)\r
+    {\r
+        if(FT_Load_Char(f, ' ', FT_LOAD_DEFAULT))\r
+            fatal("cube2font: failed loading space character");\r
+        sw = (f->glyph->advance.x+0x3F)>>6;\r
+    }\r
+#endif\r
+    if(sh <= 0) sh = y2 - y1; \r
+    if(sw <= 0) sw = sh/3;\r
+    writetexs(argv[2], chars, numchars, numtex, tw, th);\r
+    writecfg(argv[2], chars, numchars, x1, y1, x2, y2, sw, sh, argc, argv);\r
+    for(i = 0; i < numchars; i++)\r
+    {\r
+        if(chars[i].alpha != chars[i].color) FT_Done_Glyph((FT_Glyph)chars[i].alpha);\r
+        FT_Done_Glyph((FT_Glyph)chars[i].color);\r
+    }\r
+    FT_Stroker_Done(s);\r
+    FT_Stroker_Done(s2);\r
+    FT_Done_FreeType(l);\r
+    printf("cube2font: (%d, %d) .. (%d, %d) = (%d, %d) / (%d, %d), %d texs\n", x1, y1, x2, y2, x2 - x1, y2 - y1, w2, h2, numtex);\r
+    return EXIT_SUCCESS;\r
+}\r
+\r
diff --git a/src/shared/ents.h b/src/shared/ents.h
new file mode 100644 (file)
index 0000000..f4da8f5
--- /dev/null
@@ -0,0 +1,237 @@
+// this file defines static map entities ("entity") and dynamic entities (players/monsters, "dynent")
+// the gamecode extends these types to add game specific functionality
+
+// ET_*: the only static entity types dictated by the engine... rest are gamecode dependent
+
+enum { ET_EMPTY=0, ET_LIGHT, ET_MAPMODEL, ET_PLAYERSTART, ET_ENVMAP, ET_PARTICLES, ET_SOUND, ET_SPOTLIGHT, ET_GAMESPECIFIC };
+
+struct entity                                   // persistent map entity
+{
+    vec o;                                      // position
+    short attr1, attr2, attr3, attr4, attr5;
+    uchar type;                                 // type is one of the above
+    uchar reserved;
+};
+
+struct entitylight
+{
+    vec color, dir;
+    int millis;
+
+    entitylight() : color(1, 1, 1), dir(0, 0, 1), millis(-1) {}
+};
+
+enum
+{
+    EF_NOVIS     = 1<<0,
+    EF_NOSHADOW  = 1<<1,
+    EF_NOCOLLIDE = 1<<2,
+    EF_ANIM      = 1<<3,
+    EF_OCTA      = 1<<4,
+    EF_RENDER    = 1<<5,
+    EF_SOUND     = 1<<6,
+    EF_SPAWNED   = 1<<7,
+    EF_NOPICKUP  = 1<<8
+};
+
+struct extentity : entity                       // part of the entity that doesn't get saved to disk
+{
+    int flags;  // the only dynamic state of a map entity
+    entitylight light;
+    extentity *attached;
+
+    extentity() : flags(0), attached(NULL) {}
+
+    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; }
+
+    bool nopickup() const { return (flags&EF_NOPICKUP) != 0; }
+    void setnopickup(bool val) { if(val) flags |= EF_NOPICKUP; else flags &= ~EF_NOPICKUP; }
+    void setnopickup() { flags |= EF_NOPICKUP; }
+    void clearnopickup() { flags &= ~EF_NOPICKUP; }
+};
+
+#define MAXENTS 10000
+
+//extern vector<extentity *> ents;                // map entities
+
+enum { CS_ALIVE = 0, CS_DEAD, CS_SPAWNING, CS_LAGGED, CS_EDITING, CS_SPECTATOR };
+
+enum { PHYS_FLOAT = 0, PHYS_FALL, PHYS_SLIDE, PHYS_SLOPE, PHYS_FLOOR, PHYS_STEP_UP, PHYS_STEP_DOWN, PHYS_BOUNCE };
+
+enum { ENT_PLAYER = 0, ENT_AI, ENT_INANIMATE, ENT_CAMERA, ENT_BOUNCE };
+
+enum { COLLIDE_NONE = 0, COLLIDE_ELLIPSE, COLLIDE_OBB, COLLIDE_ELLIPSE_PRECISE };
+
+struct physent                                  // base entity type, can be affected by physics
+{
+    vec o, vel, falling;                        // origin, velocity
+    vec deltapos, newpos;                       // movement interpolation
+    float yaw, pitch, roll;
+    float maxspeed;                             // cubes per second, 100 for player
+    float radius, eyeheight, aboveeye;          // bounding box size
+    float xradius, yradius, zmargin;
+    vec floor;                                  // the normal of floor the dynent is on
+
+    ushort timeinair;
+    uchar inwater;
+    bool jumping;
+    schar move, strafe;
+
+    uchar physstate;                            // one of PHYS_* above
+    uchar state, editstate;                     // one of CS_* above
+    uchar type;                                 // one of ENT_* above
+    uchar collidetype;                          // one of COLLIDE_* above           
+
+    bool blocked;                               // used by physics to signal ai
+
+    physent() : o(0, 0, 0), deltapos(0, 0, 0), newpos(0, 0, 0), yaw(0), pitch(0), roll(0), maxspeed(100), 
+               radius(4.1f), eyeheight(14), aboveeye(1), xradius(4.1f), yradius(4.1f), zmargin(0),
+               state(CS_ALIVE), editstate(CS_ALIVE), type(ENT_PLAYER),
+               collidetype(COLLIDE_ELLIPSE),
+               blocked(false)
+               { reset(); }
+              
+    void resetinterp()
+    {
+        newpos = o;
+        deltapos = vec(0, 0, 0);
+    }
+
+    void reset()
+    {
+        inwater = 0;
+        timeinair = 0;
+        jumping = false;
+        strafe = move = 0;
+        physstate = PHYS_FALL;
+        vel = falling = vec(0, 0, 0);
+        floor = vec(0, 0, 1);
+    }
+
+    vec feetpos(float offset = 0) const { return vec(o).add(vec(0, 0, offset - eyeheight)); }
+    vec headpos(float offset = 0) const { return vec(o).add(vec(0, 0, offset)); }
+
+    bool maymove() const { return timeinair || physstate < PHYS_FLOOR || vel.squaredlen() > 1e-4f || deltapos.squaredlen() > 1e-4f; } 
+};
+
+enum
+{
+    ANIM_DEAD = 0, ANIM_DYING, ANIM_IDLE,
+    ANIM_FORWARD, ANIM_BACKWARD, ANIM_LEFT, ANIM_RIGHT,
+    ANIM_HOLD1, ANIM_HOLD2, ANIM_HOLD3, ANIM_HOLD4, ANIM_HOLD5, ANIM_HOLD6, ANIM_HOLD7,
+    ANIM_ATTACK1, ANIM_ATTACK2, ANIM_ATTACK3, ANIM_ATTACK4, ANIM_ATTACK5, ANIM_ATTACK6, ANIM_ATTACK7,
+    ANIM_PAIN,
+    ANIM_JUMP, ANIM_SINK, ANIM_SWIM,
+    ANIM_EDIT, ANIM_LAG, ANIM_TAUNT, ANIM_WIN, ANIM_LOSE,
+    ANIM_GUN_IDLE, ANIM_GUN_SHOOT,
+    ANIM_VWEP_IDLE, ANIM_VWEP_SHOOT, ANIM_SHIELD, ANIM_POWERUP,
+    ANIM_MAPMODEL, ANIM_TRIGGER,
+    NUMANIMS
+};
+
+static const char * const animnames[] =
+{
+    "dead", "dying", "idle",
+    "forward", "backward", "left", "right",
+    "hold 1", "hold 2", "hold 3", "hold 4", "hold 5", "hold 6", "hold 7",
+    "attack 1", "attack 2", "attack 3", "attack 4", "attack 5", "attack 6", "attack 7",
+    "pain",
+    "jump", "sink", "swim",
+    "edit", "lag", "taunt", "win", "lose",
+    "gun idle", "gun shoot",
+    "vwep idle", "vwep shoot", "shield", "powerup",
+    "mapmodel", "trigger"
+};
+
+#define ANIM_ALL         0x7F
+#define ANIM_INDEX       0x7F
+#define ANIM_LOOP        (1<<7)
+#define ANIM_START       (1<<8)
+#define ANIM_END         (1<<9)
+#define ANIM_REVERSE     (1<<10)
+#define ANIM_CLAMP       (ANIM_START|ANIM_END)
+#define ANIM_DIR         0x780
+#define ANIM_SECONDARY   11
+#define ANIM_NOSKIN      (1<<22)
+#define ANIM_SETTIME     (1<<23)
+#define ANIM_FULLBRIGHT  (1<<24)
+#define ANIM_REUSE       (1<<25)
+#define ANIM_NORENDER    (1<<26)
+#define ANIM_RAGDOLL     (1<<27)
+#define ANIM_SETSPEED    (1<<28)
+#define ANIM_NOPITCH     (1<<29)
+#define ANIM_GHOST       (1<<30)
+#define ANIM_FLAGS       (0x1FF<<22)
+
+struct animinfo // description of a character's animation
+{
+    int anim, frame, range, basetime;
+    float speed;
+    uint varseed;
+
+    animinfo() : anim(0), frame(0), range(0), basetime(0), speed(100.0f), varseed(0) { }
+
+    bool operator==(const animinfo &o) const { return frame==o.frame && range==o.range && (anim&(ANIM_SETTIME|ANIM_DIR))==(o.anim&(ANIM_SETTIME|ANIM_DIR)) && (anim&ANIM_SETTIME || basetime==o.basetime) && speed==o.speed; }
+    bool operator!=(const animinfo &o) const { return frame!=o.frame || range!=o.range || (anim&(ANIM_SETTIME|ANIM_DIR))!=(o.anim&(ANIM_SETTIME|ANIM_DIR)) || (!(anim&ANIM_SETTIME) && basetime!=o.basetime) || speed!=o.speed; }
+};
+
+struct animinterpinfo // used for animation blending of animated characters
+{
+    animinfo prev, cur;
+    int lastswitch;
+    void *lastmodel;
+
+    animinterpinfo() : lastswitch(-1), lastmodel(NULL) {}
+
+    void reset() { lastswitch = -1; }
+};
+
+#define MAXANIMPARTS 3
+
+struct occludequery;
+struct ragdolldata;
+
+struct dynent : physent                         // animated characters, or characters that can receive input
+{
+    bool k_left, k_right, k_up, k_down;         // see input code
+
+    entitylight light;
+    animinterpinfo animinterp[MAXANIMPARTS];
+    ragdolldata *ragdoll;
+    occludequery *query;
+    int lastrendered;
+    uchar occluded;
+
+    dynent() : ragdoll(NULL), query(NULL), lastrendered(0), occluded(0)
+    { 
+        reset(); 
+    }
+
+    ~dynent()
+    {
+#ifndef STANDALONE
+        extern void cleanragdoll(dynent *d);
+        if(ragdoll) cleanragdoll(this);
+#endif
+    }
+               
+    void stopmoving()
+    {
+        k_left = k_right = k_up = k_down = jumping = false;
+        move = strafe = 0;
+    }
+        
+    void reset()
+    {
+        physent::reset();
+        stopmoving();
+        loopi(MAXANIMPARTS) animinterp[i].reset();
+    }
+
+    vec abovehead() { return vec(o).add(vec(0, 0, aboveeye+4)); }
+};
+
+
diff --git a/src/shared/geom.cpp b/src/shared/geom.cpp
new file mode 100644 (file)
index 0000000..43206e1
--- /dev/null
@@ -0,0 +1,257 @@
+
+#include "cube.h"
+
+static inline double det2x2(double a, double b, double c, double d) { return a*d - b*c; }
+static inline double det3x3(double a1, double a2, double a3,
+                            double b1, double b2, double b3,
+                            double c1, double c2, double c3)
+{
+    return a1 * det2x2(b2, b3, c2, c3)
+         - b1 * det2x2(a2, a3, c2, c3)
+         + c1 * det2x2(a2, a3, b2, b3);
+}
+
+bool matrix4::invert(const matrix4 &m, double mindet)
+{
+    double a1 = m.a.x, a2 = m.a.y, a3 = m.a.z, a4 = m.a.w,
+           b1 = m.b.x, b2 = m.b.y, b3 = m.b.z, b4 = m.b.w,
+           c1 = m.c.x, c2 = m.c.y, c3 = m.c.z, c4 = m.c.w,
+           d1 = m.d.x, d2 = m.d.y, d3 = m.d.z, d4 = m.d.w,
+           det1 =  det3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4),
+           det2 = -det3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4),
+           det3 =  det3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4),
+           det4 = -det3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4),
+           det = a1*det1 + b1*det2 + c1*det3 + d1*det4;
+
+    if(fabs(det) < mindet) return false;
+
+    double invdet = 1/det;
+
+    a.x = det1 * invdet;
+    a.y = det2 * invdet;
+    a.z = det3 * invdet;
+    a.w = det4 * invdet;
+
+    b.x = -det3x3(b1, b3, b4, c1, c3, c4, d1, d3, d4) * invdet;
+    b.y =  det3x3(a1, a3, a4, c1, c3, c4, d1, d3, d4) * invdet;
+    b.z = -det3x3(a1, a3, a4, b1, b3, b4, d1, d3, d4) * invdet;
+    b.w =  det3x3(a1, a3, a4, b1, b3, b4, c1, c3, c4) * invdet;
+
+    c.x =  det3x3(b1, b2, b4, c1, c2, c4, d1, d2, d4) * invdet;
+    c.y = -det3x3(a1, a2, a4, c1, c2, c4, d1, d2, d4) * invdet;
+    c.z =  det3x3(a1, a2, a4, b1, b2, b4, d1, d2, d4) * invdet;
+    c.w = -det3x3(a1, a2, a4, b1, b2, b4, c1, c2, c4) * invdet;
+
+    d.x = -det3x3(b1, b2, b3, c1, c2, c3, d1, d2, d3) * invdet;
+    d.y =  det3x3(a1, a2, a3, c1, c2, c3, d1, d2, d3) * invdet;
+    d.z = -det3x3(a1, a2, a3, b1, b2, b3, d1, d2, d3) * invdet;
+    d.w =  det3x3(a1, a2, a3, b1, b2, b3, c1, c2, c3) * invdet;
+
+    return true;
+}
+
+bool raysphereintersect(const vec &center, float radius, const vec &o, const vec &ray, float &dist)
+{
+    vec c(center);
+    c.sub(o);
+    float v = c.dot(ray),
+          inside = radius*radius - c.squaredlen();
+    if(inside<0 && v<0) return false;
+    float d = inside + v*v;
+    if(d<0) return false;
+    dist = v - sqrt(d);
+    return true;
+}
+
+bool rayboxintersect(const vec &b, const vec &s, const vec &o, const vec &ray, float &dist, int &orient)
+{
+    loop(d, 3) if(ray[d])
+    {
+        int dc = ray[d]<0 ? 1 : 0;
+        float pdist = (b[d]+s[d]*dc - o[d]) / ray[d];
+        vec v(ray);
+        v.mul(pdist).add(o);
+        if(v[R[d]] >= b[R[d]] && v[R[d]] <= b[R[d]]+s[R[d]]
+        && v[C[d]] >= b[C[d]] && v[C[d]] <= b[C[d]]+s[C[d]])
+        {
+            dist = pdist;
+            orient = 2*d+dc;
+            return true;
+        }
+    }
+    return false;
+}
+
+bool linecylinderintersect(const vec &from, const vec &to, const vec &start, const vec &end, float radius, float &dist)
+{
+    vec d(end), m(from), n(to);
+    d.sub(start);
+    m.sub(start);
+    n.sub(from);
+    float md = m.dot(d),
+          nd = n.dot(d),
+          dd = d.squaredlen();
+    if(md < 0 && md + nd < 0) return false;
+    if(md > dd && md + nd > dd) return false;
+    float nn = n.squaredlen(),
+          mn = m.dot(n),
+          a = dd*nn - nd*nd,
+          k = m.squaredlen() - radius*radius,
+          c = dd*k - md*md;
+    if(fabs(a) < 0.005f)
+    {
+        if(c > 0) return false;
+        if(md < 0) dist = -mn / nn;
+        else if(md > dd) dist = (nd - mn) / nn;
+        else dist = 0;
+        return true;
+    }
+    else if(c > 0)
+    {
+        float b = dd*mn - nd*md,
+              discrim = b*b - a*c;
+        if(discrim < 0) return false;
+        dist = (-b - sqrtf(discrim)) / a;
+    }
+    else dist = 0;
+    float offset = md + dist*nd;
+    if(offset < 0)
+    {
+        if(nd <= 0) return false;
+        dist = -md / nd;
+        if(k + dist*(2*mn + dist*nn) > 0) return false;
+    }
+    else if(offset > dd)
+    {
+        if(nd >= 0) return false;
+        dist = (dd - md) / nd;
+        if(k + dd - 2*md + dist*(2*(mn-nd) + dist*nn) > 0) return false;
+    }
+    return dist >= 0 && dist <= 1;
+}
+
+extern const vec2 sincos360[721] =
+{
+    vec2(1.00000000, 0.00000000), vec2(0.99984770, 0.01745241), vec2(0.99939083, 0.03489950), vec2(0.99862953, 0.05233596), vec2(0.99756405, 0.06975647), vec2(0.99619470, 0.08715574), // 0
+    vec2(0.99452190, 0.10452846), vec2(0.99254615, 0.12186934), vec2(0.99026807, 0.13917310), vec2(0.98768834, 0.15643447), vec2(0.98480775, 0.17364818), vec2(0.98162718, 0.19080900), // 6
+    vec2(0.97814760, 0.20791169), vec2(0.97437006, 0.22495105), vec2(0.97029573, 0.24192190), vec2(0.96592583, 0.25881905), vec2(0.96126170, 0.27563736), vec2(0.95630476, 0.29237170), // 12
+    vec2(0.95105652, 0.30901699), vec2(0.94551858, 0.32556815), vec2(0.93969262, 0.34202014), vec2(0.93358043, 0.35836795), vec2(0.92718385, 0.37460659), vec2(0.92050485, 0.39073113), // 18
+    vec2(0.91354546, 0.40673664), vec2(0.90630779, 0.42261826), vec2(0.89879405, 0.43837115), vec2(0.89100652, 0.45399050), vec2(0.88294759, 0.46947156), vec2(0.87461971, 0.48480962), // 24
+    vec2(0.86602540, 0.50000000), vec2(0.85716730, 0.51503807), vec2(0.84804810, 0.52991926), vec2(0.83867057, 0.54463904), vec2(0.82903757, 0.55919290), vec2(0.81915204, 0.57357644), // 30
+    vec2(0.80901699, 0.58778525), vec2(0.79863551, 0.60181502), vec2(0.78801075, 0.61566148), vec2(0.77714596, 0.62932039), vec2(0.76604444, 0.64278761), vec2(0.75470958, 0.65605903), // 36
+    vec2(0.74314483, 0.66913061), vec2(0.73135370, 0.68199836), vec2(0.71933980, 0.69465837), vec2(0.70710678, 0.70710678), vec2(0.69465837, 0.71933980), vec2(0.68199836, 0.73135370), // 42
+    vec2(0.66913061, 0.74314483), vec2(0.65605903, 0.75470958), vec2(0.64278761, 0.76604444), vec2(0.62932039, 0.77714596), vec2(0.61566148, 0.78801075), vec2(0.60181502, 0.79863551), // 48
+    vec2(0.58778525, 0.80901699), vec2(0.57357644, 0.81915204), vec2(0.55919290, 0.82903757), vec2(0.54463904, 0.83867057), vec2(0.52991926, 0.84804810), vec2(0.51503807, 0.85716730), // 54
+    vec2(0.50000000, 0.86602540), vec2(0.48480962, 0.87461971), vec2(0.46947156, 0.88294759), vec2(0.45399050, 0.89100652), vec2(0.43837115, 0.89879405), vec2(0.42261826, 0.90630779), // 60
+    vec2(0.40673664, 0.91354546), vec2(0.39073113, 0.92050485), vec2(0.37460659, 0.92718385), vec2(0.35836795, 0.93358043), vec2(0.34202014, 0.93969262), vec2(0.32556815, 0.94551858), // 66
+    vec2(0.30901699, 0.95105652), vec2(0.29237170, 0.95630476), vec2(0.27563736, 0.96126170), vec2(0.25881905, 0.96592583), vec2(0.24192190, 0.97029573), vec2(0.22495105, 0.97437006), // 72
+    vec2(0.20791169, 0.97814760), vec2(0.19080900, 0.98162718), vec2(0.17364818, 0.98480775), vec2(0.15643447, 0.98768834), vec2(0.13917310, 0.99026807), vec2(0.12186934, 0.99254615), // 78
+    vec2(0.10452846, 0.99452190), vec2(0.08715574, 0.99619470), vec2(0.06975647, 0.99756405), vec2(0.05233596, 0.99862953), vec2(0.03489950, 0.99939083), vec2(0.01745241, 0.99984770), // 84
+    vec2(0.00000000, 1.00000000), vec2(-0.01745241, 0.99984770), vec2(-0.03489950, 0.99939083), vec2(-0.05233596, 0.99862953), vec2(-0.06975647, 0.99756405), vec2(-0.08715574, 0.99619470), // 90
+    vec2(-0.10452846, 0.99452190), vec2(-0.12186934, 0.99254615), vec2(-0.13917310, 0.99026807), vec2(-0.15643447, 0.98768834), vec2(-0.17364818, 0.98480775), vec2(-0.19080900, 0.98162718), // 96
+    vec2(-0.20791169, 0.97814760), vec2(-0.22495105, 0.97437006), vec2(-0.24192190, 0.97029573), vec2(-0.25881905, 0.96592583), vec2(-0.27563736, 0.96126170), vec2(-0.29237170, 0.95630476), // 102
+    vec2(-0.30901699, 0.95105652), vec2(-0.32556815, 0.94551858), vec2(-0.34202014, 0.93969262), vec2(-0.35836795, 0.93358043), vec2(-0.37460659, 0.92718385), vec2(-0.39073113, 0.92050485), // 108
+    vec2(-0.40673664, 0.91354546), vec2(-0.42261826, 0.90630779), vec2(-0.43837115, 0.89879405), vec2(-0.45399050, 0.89100652), vec2(-0.46947156, 0.88294759), vec2(-0.48480962, 0.87461971), // 114
+    vec2(-0.50000000, 0.86602540), vec2(-0.51503807, 0.85716730), vec2(-0.52991926, 0.84804810), vec2(-0.54463904, 0.83867057), vec2(-0.55919290, 0.82903757), vec2(-0.57357644, 0.81915204), // 120
+    vec2(-0.58778525, 0.80901699), vec2(-0.60181502, 0.79863551), vec2(-0.61566148, 0.78801075), vec2(-0.62932039, 0.77714596), vec2(-0.64278761, 0.76604444), vec2(-0.65605903, 0.75470958), // 126
+    vec2(-0.66913061, 0.74314483), vec2(-0.68199836, 0.73135370), vec2(-0.69465837, 0.71933980), vec2(-0.70710678, 0.70710678), vec2(-0.71933980, 0.69465837), vec2(-0.73135370, 0.68199836), // 132
+    vec2(-0.74314483, 0.66913061), vec2(-0.75470958, 0.65605903), vec2(-0.76604444, 0.64278761), vec2(-0.77714596, 0.62932039), vec2(-0.78801075, 0.61566148), vec2(-0.79863551, 0.60181502), // 138
+    vec2(-0.80901699, 0.58778525), vec2(-0.81915204, 0.57357644), vec2(-0.82903757, 0.55919290), vec2(-0.83867057, 0.54463904), vec2(-0.84804810, 0.52991926), vec2(-0.85716730, 0.51503807), // 144
+    vec2(-0.86602540, 0.50000000), vec2(-0.87461971, 0.48480962), vec2(-0.88294759, 0.46947156), vec2(-0.89100652, 0.45399050), vec2(-0.89879405, 0.43837115), vec2(-0.90630779, 0.42261826), // 150
+    vec2(-0.91354546, 0.40673664), vec2(-0.92050485, 0.39073113), vec2(-0.92718385, 0.37460659), vec2(-0.93358043, 0.35836795), vec2(-0.93969262, 0.34202014), vec2(-0.94551858, 0.32556815), // 156
+    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), // 162
+    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), // 168
+    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), // 174
+    vec2(-1.00000000, 0.00000000), vec2(-0.99984770, -0.01745241), vec2(-0.99939083, -0.03489950), vec2(-0.99862953, -0.05233596), vec2(-0.99756405, -0.06975647), vec2(-0.99619470, -0.08715574), // 180
+    vec2(-0.99452190, -0.10452846), vec2(-0.99254615, -0.12186934), vec2(-0.99026807, -0.13917310), vec2(-0.98768834, -0.15643447), vec2(-0.98480775, -0.17364818), vec2(-0.98162718, -0.19080900), // 186
+    vec2(-0.97814760, -0.20791169), vec2(-0.97437006, -0.22495105), vec2(-0.97029573, -0.24192190), vec2(-0.96592583, -0.25881905), vec2(-0.96126170, -0.27563736), vec2(-0.95630476, -0.29237170), // 192
+    vec2(-0.95105652, -0.30901699), vec2(-0.94551858, -0.32556815), vec2(-0.93969262, -0.34202014), vec2(-0.93358043, -0.35836795), vec2(-0.92718385, -0.37460659), vec2(-0.92050485, -0.39073113), // 198
+    vec2(-0.91354546, -0.40673664), vec2(-0.90630779, -0.42261826), vec2(-0.89879405, -0.43837115), vec2(-0.89100652, -0.45399050), vec2(-0.88294759, -0.46947156), vec2(-0.87461971, -0.48480962), // 204
+    vec2(-0.86602540, -0.50000000), vec2(-0.85716730, -0.51503807), vec2(-0.84804810, -0.52991926), vec2(-0.83867057, -0.54463904), vec2(-0.82903757, -0.55919290), vec2(-0.81915204, -0.57357644), // 210
+    vec2(-0.80901699, -0.58778525), vec2(-0.79863551, -0.60181502), vec2(-0.78801075, -0.61566148), vec2(-0.77714596, -0.62932039), vec2(-0.76604444, -0.64278761), vec2(-0.75470958, -0.65605903), // 216
+    vec2(-0.74314483, -0.66913061), vec2(-0.73135370, -0.68199836), vec2(-0.71933980, -0.69465837), vec2(-0.70710678, -0.70710678), vec2(-0.69465837, -0.71933980), vec2(-0.68199836, -0.73135370), // 222
+    vec2(-0.66913061, -0.74314483), vec2(-0.65605903, -0.75470958), vec2(-0.64278761, -0.76604444), vec2(-0.62932039, -0.77714596), vec2(-0.61566148, -0.78801075), vec2(-0.60181502, -0.79863551), // 228
+    vec2(-0.58778525, -0.80901699), vec2(-0.57357644, -0.81915204), vec2(-0.55919290, -0.82903757), vec2(-0.54463904, -0.83867057), vec2(-0.52991926, -0.84804810), vec2(-0.51503807, -0.85716730), // 234
+    vec2(-0.50000000, -0.86602540), vec2(-0.48480962, -0.87461971), vec2(-0.46947156, -0.88294759), vec2(-0.45399050, -0.89100652), vec2(-0.43837115, -0.89879405), vec2(-0.42261826, -0.90630779), // 240
+    vec2(-0.40673664, -0.91354546), vec2(-0.39073113, -0.92050485), vec2(-0.37460659, -0.92718385), vec2(-0.35836795, -0.93358043), vec2(-0.34202014, -0.93969262), vec2(-0.32556815, -0.94551858), // 246
+    vec2(-0.30901699, -0.95105652), vec2(-0.29237170, -0.95630476), vec2(-0.27563736, -0.96126170), vec2(-0.25881905, -0.96592583), vec2(-0.24192190, -0.97029573), vec2(-0.22495105, -0.97437006), // 252
+    vec2(-0.20791169, -0.97814760), vec2(-0.19080900, -0.98162718), vec2(-0.17364818, -0.98480775), vec2(-0.15643447, -0.98768834), vec2(-0.13917310, -0.99026807), vec2(-0.12186934, -0.99254615), // 258
+    vec2(-0.10452846, -0.99452190), vec2(-0.08715574, -0.99619470), vec2(-0.06975647, -0.99756405), vec2(-0.05233596, -0.99862953), vec2(-0.03489950, -0.99939083), vec2(-0.01745241, -0.99984770), // 264
+    vec2(-0.00000000, -1.00000000), vec2(0.01745241, -0.99984770), vec2(0.03489950, -0.99939083), vec2(0.05233596, -0.99862953), vec2(0.06975647, -0.99756405), vec2(0.08715574, -0.99619470), // 270
+    vec2(0.10452846, -0.99452190), vec2(0.12186934, -0.99254615), vec2(0.13917310, -0.99026807), vec2(0.15643447, -0.98768834), vec2(0.17364818, -0.98480775), vec2(0.19080900, -0.98162718), // 276
+    vec2(0.20791169, -0.97814760), vec2(0.22495105, -0.97437006), vec2(0.24192190, -0.97029573), vec2(0.25881905, -0.96592583), vec2(0.27563736, -0.96126170), vec2(0.29237170, -0.95630476), // 282
+    vec2(0.30901699, -0.95105652), vec2(0.32556815, -0.94551858), vec2(0.34202014, -0.93969262), vec2(0.35836795, -0.93358043), vec2(0.37460659, -0.92718385), vec2(0.39073113, -0.92050485), // 288
+    vec2(0.40673664, -0.91354546), vec2(0.42261826, -0.90630779), vec2(0.43837115, -0.89879405), vec2(0.45399050, -0.89100652), vec2(0.46947156, -0.88294759), vec2(0.48480962, -0.87461971), // 294
+    vec2(0.50000000, -0.86602540), vec2(0.51503807, -0.85716730), vec2(0.52991926, -0.84804810), vec2(0.54463904, -0.83867057), vec2(0.55919290, -0.82903757), vec2(0.57357644, -0.81915204), // 300
+    vec2(0.58778525, -0.80901699), vec2(0.60181502, -0.79863551), vec2(0.61566148, -0.78801075), vec2(0.62932039, -0.77714596), vec2(0.64278761, -0.76604444), vec2(0.65605903, -0.75470958), // 306
+    vec2(0.66913061, -0.74314483), vec2(0.68199836, -0.73135370), vec2(0.69465837, -0.71933980), vec2(0.70710678, -0.70710678), vec2(0.71933980, -0.69465837), vec2(0.73135370, -0.68199836), // 312
+    vec2(0.74314483, -0.66913061), vec2(0.75470958, -0.65605903), vec2(0.76604444, -0.64278761), vec2(0.77714596, -0.62932039), vec2(0.78801075, -0.61566148), vec2(0.79863551, -0.60181502), // 318
+    vec2(0.80901699, -0.58778525), vec2(0.81915204, -0.57357644), vec2(0.82903757, -0.55919290), vec2(0.83867057, -0.54463904), vec2(0.84804810, -0.52991926), vec2(0.85716730, -0.51503807), // 324
+    vec2(0.86602540, -0.50000000), vec2(0.87461971, -0.48480962), vec2(0.88294759, -0.46947156), vec2(0.89100652, -0.45399050), vec2(0.89879405, -0.43837115), vec2(0.90630779, -0.42261826), // 330
+    vec2(0.91354546, -0.40673664), vec2(0.92050485, -0.39073113), vec2(0.92718385, -0.37460659), vec2(0.93358043, -0.35836795), vec2(0.93969262, -0.34202014), vec2(0.94551858, -0.32556815), // 336
+    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), // 342
+    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), // 348
+    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), // 354
+    vec2(1.00000000, 0.00000000), vec2(0.99984770, 0.01745241), vec2(0.99939083, 0.03489950), vec2(0.99862953, 0.05233596), vec2(0.99756405, 0.06975647), vec2(0.99619470, 0.08715574), // 360
+    vec2(0.99452190, 0.10452846), vec2(0.99254615, 0.12186934), vec2(0.99026807, 0.13917310), vec2(0.98768834, 0.15643447), vec2(0.98480775, 0.17364818), vec2(0.98162718, 0.19080900), // 366
+    vec2(0.97814760, 0.20791169), vec2(0.97437006, 0.22495105), vec2(0.97029573, 0.24192190), vec2(0.96592583, 0.25881905), vec2(0.96126170, 0.27563736), vec2(0.95630476, 0.29237170), // 372
+    vec2(0.95105652, 0.30901699), vec2(0.94551858, 0.32556815), vec2(0.93969262, 0.34202014), vec2(0.93358043, 0.35836795), vec2(0.92718385, 0.37460659), vec2(0.92050485, 0.39073113), // 378
+    vec2(0.91354546, 0.40673664), vec2(0.90630779, 0.42261826), vec2(0.89879405, 0.43837115), vec2(0.89100652, 0.45399050), vec2(0.88294759, 0.46947156), vec2(0.87461971, 0.48480962), // 384
+    vec2(0.86602540, 0.50000000), vec2(0.85716730, 0.51503807), vec2(0.84804810, 0.52991926), vec2(0.83867057, 0.54463904), vec2(0.82903757, 0.55919290), vec2(0.81915204, 0.57357644), // 390
+    vec2(0.80901699, 0.58778525), vec2(0.79863551, 0.60181502), vec2(0.78801075, 0.61566148), vec2(0.77714596, 0.62932039), vec2(0.76604444, 0.64278761), vec2(0.75470958, 0.65605903), // 396
+    vec2(0.74314483, 0.66913061), vec2(0.73135370, 0.68199836), vec2(0.71933980, 0.69465837), vec2(0.70710678, 0.70710678), vec2(0.69465837, 0.71933980), vec2(0.68199836, 0.73135370), // 402
+    vec2(0.66913061, 0.74314483), vec2(0.65605903, 0.75470958), vec2(0.64278761, 0.76604444), vec2(0.62932039, 0.77714596), vec2(0.61566148, 0.78801075), vec2(0.60181502, 0.79863551), // 408
+    vec2(0.58778525, 0.80901699), vec2(0.57357644, 0.81915204), vec2(0.55919290, 0.82903757), vec2(0.54463904, 0.83867057), vec2(0.52991926, 0.84804810), vec2(0.51503807, 0.85716730), // 414
+    vec2(0.50000000, 0.86602540), vec2(0.48480962, 0.87461971), vec2(0.46947156, 0.88294759), vec2(0.45399050, 0.89100652), vec2(0.43837115, 0.89879405), vec2(0.42261826, 0.90630779), // 420
+    vec2(0.40673664, 0.91354546), vec2(0.39073113, 0.92050485), vec2(0.37460659, 0.92718385), vec2(0.35836795, 0.93358043), vec2(0.34202014, 0.93969262), vec2(0.32556815, 0.94551858), // 426
+    vec2(0.30901699, 0.95105652), vec2(0.29237170, 0.95630476), vec2(0.27563736, 0.96126170), vec2(0.25881905, 0.96592583), vec2(0.24192190, 0.97029573), vec2(0.22495105, 0.97437006), // 432
+    vec2(0.20791169, 0.97814760), vec2(0.19080900, 0.98162718), vec2(0.17364818, 0.98480775), vec2(0.15643447, 0.98768834), vec2(0.13917310, 0.99026807), vec2(0.12186934, 0.99254615), // 438
+    vec2(0.10452846, 0.99452190), vec2(0.08715574, 0.99619470), vec2(0.06975647, 0.99756405), vec2(0.05233596, 0.99862953), vec2(0.03489950, 0.99939083), vec2(0.01745241, 0.99984770), // 444
+    vec2(0.00000000, 1.00000000), vec2(-0.01745241, 0.99984770), vec2(-0.03489950, 0.99939083), vec2(-0.05233596, 0.99862953), vec2(-0.06975647, 0.99756405), vec2(-0.08715574, 0.99619470), // 450
+    vec2(-0.10452846, 0.99452190), vec2(-0.12186934, 0.99254615), vec2(-0.13917310, 0.99026807), vec2(-0.15643447, 0.98768834), vec2(-0.17364818, 0.98480775), vec2(-0.19080900, 0.98162718), // 456
+    vec2(-0.20791169, 0.97814760), vec2(-0.22495105, 0.97437006), vec2(-0.24192190, 0.97029573), vec2(-0.25881905, 0.96592583), vec2(-0.27563736, 0.96126170), vec2(-0.29237170, 0.95630476), // 462
+    vec2(-0.30901699, 0.95105652), vec2(-0.32556815, 0.94551858), vec2(-0.34202014, 0.93969262), vec2(-0.35836795, 0.93358043), vec2(-0.37460659, 0.92718385), vec2(-0.39073113, 0.92050485), // 468
+    vec2(-0.40673664, 0.91354546), vec2(-0.42261826, 0.90630779), vec2(-0.43837115, 0.89879405), vec2(-0.45399050, 0.89100652), vec2(-0.46947156, 0.88294759), vec2(-0.48480962, 0.87461971), // 474
+    vec2(-0.50000000, 0.86602540), vec2(-0.51503807, 0.85716730), vec2(-0.52991926, 0.84804810), vec2(-0.54463904, 0.83867057), vec2(-0.55919290, 0.82903757), vec2(-0.57357644, 0.81915204), // 480
+    vec2(-0.58778525, 0.80901699), vec2(-0.60181502, 0.79863551), vec2(-0.61566148, 0.78801075), vec2(-0.62932039, 0.77714596), vec2(-0.64278761, 0.76604444), vec2(-0.65605903, 0.75470958), // 486
+    vec2(-0.66913061, 0.74314483), vec2(-0.68199836, 0.73135370), vec2(-0.69465837, 0.71933980), vec2(-0.70710678, 0.70710678), vec2(-0.71933980, 0.69465837), vec2(-0.73135370, 0.68199836), // 492
+    vec2(-0.74314483, 0.66913061), vec2(-0.75470958, 0.65605903), vec2(-0.76604444, 0.64278761), vec2(-0.77714596, 0.62932039), vec2(-0.78801075, 0.61566148), vec2(-0.79863551, 0.60181502), // 498
+    vec2(-0.80901699, 0.58778525), vec2(-0.81915204, 0.57357644), vec2(-0.82903757, 0.55919290), vec2(-0.83867057, 0.54463904), vec2(-0.84804810, 0.52991926), vec2(-0.85716730, 0.51503807), // 504
+    vec2(-0.86602540, 0.50000000), vec2(-0.87461971, 0.48480962), vec2(-0.88294759, 0.46947156), vec2(-0.89100652, 0.45399050), vec2(-0.89879405, 0.43837115), vec2(-0.90630779, 0.42261826), // 510
+    vec2(-0.91354546, 0.40673664), vec2(-0.92050485, 0.39073113), vec2(-0.92718385, 0.37460659), vec2(-0.93358043, 0.35836795), vec2(-0.93969262, 0.34202014), vec2(-0.94551858, 0.32556815), // 516
+    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), // 522
+    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), // 528
+    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), // 534
+    vec2(-1.00000000, 0.00000000), vec2(-0.99984770, -0.01745241), vec2(-0.99939083, -0.03489950), vec2(-0.99862953, -0.05233596), vec2(-0.99756405, -0.06975647), vec2(-0.99619470, -0.08715574), // 540
+    vec2(-0.99452190, -0.10452846), vec2(-0.99254615, -0.12186934), vec2(-0.99026807, -0.13917310), vec2(-0.98768834, -0.15643447), vec2(-0.98480775, -0.17364818), vec2(-0.98162718, -0.19080900), // 546
+    vec2(-0.97814760, -0.20791169), vec2(-0.97437006, -0.22495105), vec2(-0.97029573, -0.24192190), vec2(-0.96592583, -0.25881905), vec2(-0.96126170, -0.27563736), vec2(-0.95630476, -0.29237170), // 552
+    vec2(-0.95105652, -0.30901699), vec2(-0.94551858, -0.32556815), vec2(-0.93969262, -0.34202014), vec2(-0.93358043, -0.35836795), vec2(-0.92718385, -0.37460659), vec2(-0.92050485, -0.39073113), // 558
+    vec2(-0.91354546, -0.40673664), vec2(-0.90630779, -0.42261826), vec2(-0.89879405, -0.43837115), vec2(-0.89100652, -0.45399050), vec2(-0.88294759, -0.46947156), vec2(-0.87461971, -0.48480962), // 564
+    vec2(-0.86602540, -0.50000000), vec2(-0.85716730, -0.51503807), vec2(-0.84804810, -0.52991926), vec2(-0.83867057, -0.54463904), vec2(-0.82903757, -0.55919290), vec2(-0.81915204, -0.57357644), // 570
+    vec2(-0.80901699, -0.58778525), vec2(-0.79863551, -0.60181502), vec2(-0.78801075, -0.61566148), vec2(-0.77714596, -0.62932039), vec2(-0.76604444, -0.64278761), vec2(-0.75470958, -0.65605903), // 576
+    vec2(-0.74314483, -0.66913061), vec2(-0.73135370, -0.68199836), vec2(-0.71933980, -0.69465837), vec2(-0.70710678, -0.70710678), vec2(-0.69465837, -0.71933980), vec2(-0.68199836, -0.73135370), // 582
+    vec2(-0.66913061, -0.74314483), vec2(-0.65605903, -0.75470958), vec2(-0.64278761, -0.76604444), vec2(-0.62932039, -0.77714596), vec2(-0.61566148, -0.78801075), vec2(-0.60181502, -0.79863551), // 588
+    vec2(-0.58778525, -0.80901699), vec2(-0.57357644, -0.81915204), vec2(-0.55919290, -0.82903757), vec2(-0.54463904, -0.83867057), vec2(-0.52991926, -0.84804810), vec2(-0.51503807, -0.85716730), // 594
+    vec2(-0.50000000, -0.86602540), vec2(-0.48480962, -0.87461971), vec2(-0.46947156, -0.88294759), vec2(-0.45399050, -0.89100652), vec2(-0.43837115, -0.89879405), vec2(-0.42261826, -0.90630779), // 600
+    vec2(-0.40673664, -0.91354546), vec2(-0.39073113, -0.92050485), vec2(-0.37460659, -0.92718385), vec2(-0.35836795, -0.93358043), vec2(-0.34202014, -0.93969262), vec2(-0.32556815, -0.94551858), // 606
+    vec2(-0.30901699, -0.95105652), vec2(-0.29237170, -0.95630476), vec2(-0.27563736, -0.96126170), vec2(-0.25881905, -0.96592583), vec2(-0.24192190, -0.97029573), vec2(-0.22495105, -0.97437006), // 612
+    vec2(-0.20791169, -0.97814760), vec2(-0.19080900, -0.98162718), vec2(-0.17364818, -0.98480775), vec2(-0.15643447, -0.98768834), vec2(-0.13917310, -0.99026807), vec2(-0.12186934, -0.99254615), // 618
+    vec2(-0.10452846, -0.99452190), vec2(-0.08715574, -0.99619470), vec2(-0.06975647, -0.99756405), vec2(-0.05233596, -0.99862953), vec2(-0.03489950, -0.99939083), vec2(-0.01745241, -0.99984770), // 624
+    vec2(-0.00000000, -1.00000000), vec2(0.01745241, -0.99984770), vec2(0.03489950, -0.99939083), vec2(0.05233596, -0.99862953), vec2(0.06975647, -0.99756405), vec2(0.08715574, -0.99619470), // 630
+    vec2(0.10452846, -0.99452190), vec2(0.12186934, -0.99254615), vec2(0.13917310, -0.99026807), vec2(0.15643447, -0.98768834), vec2(0.17364818, -0.98480775), vec2(0.19080900, -0.98162718), // 636
+    vec2(0.20791169, -0.97814760), vec2(0.22495105, -0.97437006), vec2(0.24192190, -0.97029573), vec2(0.25881905, -0.96592583), vec2(0.27563736, -0.96126170), vec2(0.29237170, -0.95630476), // 642
+    vec2(0.30901699, -0.95105652), vec2(0.32556815, -0.94551858), vec2(0.34202014, -0.93969262), vec2(0.35836795, -0.93358043), vec2(0.37460659, -0.92718385), vec2(0.39073113, -0.92050485), // 648
+    vec2(0.40673664, -0.91354546), vec2(0.42261826, -0.90630779), vec2(0.43837115, -0.89879405), vec2(0.45399050, -0.89100652), vec2(0.46947156, -0.88294759), vec2(0.48480962, -0.87461971), // 654
+    vec2(0.50000000, -0.86602540), vec2(0.51503807, -0.85716730), vec2(0.52991926, -0.84804810), vec2(0.54463904, -0.83867057), vec2(0.55919290, -0.82903757), vec2(0.57357644, -0.81915204), // 660
+    vec2(0.58778525, -0.80901699), vec2(0.60181502, -0.79863551), vec2(0.61566148, -0.78801075), vec2(0.62932039, -0.77714596), vec2(0.64278761, -0.76604444), vec2(0.65605903, -0.75470958), // 666
+    vec2(0.66913061, -0.74314483), vec2(0.68199836, -0.73135370), vec2(0.69465837, -0.71933980), vec2(0.70710678, -0.70710678), vec2(0.71933980, -0.69465837), vec2(0.73135370, -0.68199836), // 672
+    vec2(0.74314483, -0.66913061), vec2(0.75470958, -0.65605903), vec2(0.76604444, -0.64278761), vec2(0.77714596, -0.62932039), vec2(0.78801075, -0.61566148), vec2(0.79863551, -0.60181502), // 678
+    vec2(0.80901699, -0.58778525), vec2(0.81915204, -0.57357644), vec2(0.82903757, -0.55919290), vec2(0.83867057, -0.54463904), vec2(0.84804810, -0.52991926), vec2(0.85716730, -0.51503807), // 684
+    vec2(0.86602540, -0.50000000), vec2(0.87461971, -0.48480962), vec2(0.88294759, -0.46947156), vec2(0.89100652, -0.45399050), vec2(0.89879405, -0.43837115), vec2(0.90630779, -0.42261826), // 690
+    vec2(0.91354546, -0.40673664), vec2(0.92050485, -0.39073113), vec2(0.92718385, -0.37460659), vec2(0.93358043, -0.35836795), vec2(0.93969262, -0.34202014), vec2(0.94551858, -0.32556815), // 696
+    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        
+};
+
diff --git a/src/shared/geom.h b/src/shared/geom.h
new file mode 100644 (file)
index 0000000..3adccc6
--- /dev/null
@@ -0,0 +1,1828 @@
+struct vec;
+struct vec4;
+
+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; }
+    vec2 &safenormalize() { float m = magnitude(); if(m) mul(1/m); 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 &square()           { mul(*this); 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 &recip()            { x = 1/x; y = 1/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 &min(const vec2 &o) { x = ::min(x, o.x); y = ::min(y, o.y); return *this; }
+    vec2 &max(const vec2 &o) { x = ::max(x, o.x); y = ::max(y, o.y); return *this; }
+    vec2 &min(float f)       { x = ::min(x, f); y = ::min(y, f); return *this; }
+    vec2 &max(float f)       { x = ::max(x, f); y = ::max(y, f); return *this; }
+    vec2 &abs() { x = fabs(x); y = fabs(y); return *this; }
+    vec2 &clamp(float l, float h) { x = ::clamp(x, l, h); y = ::clamp(y, l, h); return *this; }
+    vec2 &reflect(const vec2 &n) { float k = 2*dot(n); x -= k*n.x; y -= k*n.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; }
+    template<class B> vec2 &madd(const vec2 &a, const B &b) { return add(vec2(a).mul(b)); }
+    template<class B> vec2 &msub(const vec2 &a, const B &b) { return sub(vec2(a).mul(b)); }
+};
+
+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 ivec;
+
+struct vec
+{
+    union
+    {
+        struct { float x, y, z; };
+        struct { float r, g, b; };
+        float v[3];
+    };
+
+    vec() {}
+    explicit vec(int a) : x(a), y(a), z(a) {} 
+    explicit vec(float a) : x(a), y(a), z(a) {} 
+    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(const 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 ivec &v);
+
+    vec(float yaw, float pitch) : x(-sinf(yaw)*cosf(pitch)), y(cosf(yaw)*cosf(pitch)), z(sinf(pitch)) {}
+
+    float &operator[](int i)       { return v[i]; }
+    float  operator[](int i) const { return v[i]; }
+    
+    vec &set(int i, float f) { v[i] = f; return *this; }
+
+    bool operator==(const vec &o) const { return x == o.x && y == o.y && z == o.z; }
+    bool operator!=(const vec &o) const { return x != o.x || y != o.y || z != o.z; }
+
+    bool iszero() const { return x==0 && y==0 && z==0; }
+    float squaredlen() const { return x*x + y*y + z*z; }
+    template<class T> float dot2(const T &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 &pow(float f)        { x = ::pow(x, f); y = ::pow(y, f); z = ::pow(z, f); return *this; }
+    vec &exp()               { x = ::exp(x); y = ::exp(y); z = ::exp(z); return *this; }
+    vec &exp2()              { x = ::exp2(x); y = ::exp2(y); z = ::exp2(z); return *this; }
+    vec &sqrt()              { x = sqrtf(x); y = sqrtf(y); z = sqrtf(z); return *this; }
+    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 &square()            { mul(*this); return *this; }
+    vec &div(const vec &o)   { x /= o.x; y /= o.y; z /= o.z; return *this; }
+    vec &div(float f)        { x /= f; y /= f; z /= f; return *this; }
+    vec &recip()             { x = 1/x; y = 1/y; z = 1/z; return *this; }
+    vec &add(const vec &o)   { x += o.x; y += o.y; z += o.z; return *this; }
+    vec &add(float f)        { x += f; y += f; z += f; return *this; }
+    vec &add2(float f)       { x += f; y += f; return *this; }
+    vec &addz(float f)       { z += f; return *this; }
+    vec &sub(const vec &o)   { x -= o.x; y -= o.y; z -= o.z; return *this; }
+    vec &sub(float f)        { x -= f; y -= f; z -= f; return *this; }
+    vec &sub2(float f)       { x -= f; y -= f; return *this; }
+    vec &subz(float f)       { z -= f; return *this; }
+    vec &neg2()              { x = -x; y = -y; return *this; }
+    vec &neg()               { x = -x; y = -y; z = -z; return *this; }
+    vec &min(const vec &o)   { x = ::min(x, o.x); y = ::min(y, o.y); z = ::min(z, o.z); return *this; }
+    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; }
+    float magnitude2() const { return sqrtf(dot2(*this)); }
+    float magnitude() const  { return sqrtf(squaredlen()); }
+    vec &normalize()         { div(magnitude()); return *this; }
+    vec &safenormalize()     { float m = magnitude(); if(m) div(m); return *this; }
+    bool isnormalized() const { float m = squaredlen(); return (m>0.99f && m<1.01f); }
+    float squaredist(const vec &e) const { return vec(*this).sub(e).squaredlen(); }
+    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); }
+    bool reject(const vec &o, float r) { return x>o.x+r || x<o.x-r || y>o.y+r || y<o.y-r; }
+    template<class A, class B>
+    vec &cross(const A &a, const B &b) { x = a.y*b.z-a.z*b.y; y = a.z*b.x-a.x*b.z; z = a.x*b.y-a.y*b.x; return *this; }
+    vec &cross(const vec &o, const vec &a, const vec &b) { return cross(vec(a).sub(o), vec(b).sub(o)); }
+    float scalartriple(const vec &a, const vec &b) const { return x*(a.y*b.z-a.z*b.y) + y*(a.z*b.x-a.x*b.z) + z*(a.x*b.y-a.y*b.x); }
+    vec &reflectz(float rz) { z = 2*rz - z; return *this; }
+    vec &reflect(const vec &n) { float k = 2*dot(n); x -= k*n.x; y -= k*n.y; z -= k*n.z; return *this; }
+    vec &project(const vec &n) { float k = dot(n); x -= k*n.x; y -= k*n.y; z -= k*n.z; return *this; }
+    vec &projectxydir(const vec &n) { if(n.z) z = -(x*n.x/n.z + y*n.y/n.z); return *this; }
+    vec &projectxy(const vec &n)
+    {
+        float m = squaredlen(), k = dot(n);
+        projectxydir(n);
+        rescale(sqrtf(::max(m - k*k, 0.0f)));
+        return *this;
+    }
+    vec &projectxy(const vec &n, float threshold)
+    {
+        float m = squaredlen(), k = ::min(dot(n), threshold);
+        projectxydir(n);
+        rescale(sqrtf(::max(m - k*k, 0.0f)));
+        return *this;
+    }
+    vec &lerp(const vec &b, float t) { x += (b.x-x)*t; y += (b.y-y)*t; z += (b.z-z)*t; return *this; }
+    vec &lerp(const vec &a, const vec &b, float t) { x = a.x + (b.x-a.x)*t; y = a.y + (b.y-a.y)*t; z = a.z + (b.z-a.z)*t; return *this; }
+    template<class B> vec &madd(const vec &a, const B &b) { return add(vec(a).mul(b)); }
+    template<class B> vec &msub(const vec &a, const B &b) { return sub(vec(a).mul(b)); }
+
+    vec &rescale(float k)
+    {
+        float mag = magnitude();
+        if(mag > 1e-6f) mul(k / mag);
+        return *this;
+    }
+
+    vec &rotate_around_z(float c, float s) { float rx = x, ry = y; x = c*rx-s*ry; y = c*ry+s*rx; return *this; }
+    vec &rotate_around_x(float c, float s) { float ry = y, rz = z; y = c*ry-s*rz; z = c*rz+s*ry; return *this; }
+    vec &rotate_around_y(float c, float s) { float rx = x, rz = z; x = c*rx+s*rz; z = c*rz-s*rx; return *this; }
+
+    vec &rotate_around_z(float angle) { return rotate_around_z(cosf(angle), sinf(angle)); }
+    vec &rotate_around_x(float angle) { return rotate_around_x(cosf(angle), sinf(angle)); }
+    vec &rotate_around_y(float angle) { return rotate_around_y(cosf(angle), sinf(angle)); }
+
+    vec &rotate_around_z(const vec2 &sc) { return rotate_around_z(sc.x, sc.y); }
+    vec &rotate_around_x(const vec2 &sc) { return rotate_around_x(sc.x, sc.y); }
+    vec &rotate_around_y(const vec2 &sc) { return rotate_around_y(sc.x, sc.y); }
+
+    vec &rotate(float c, float s, const vec &d)
+    {
+        *this = vec(x*(d.x*d.x*(1-c)+c) + y*(d.x*d.y*(1-c)-d.z*s) + z*(d.x*d.z*(1-c)+d.y*s),
+                    x*(d.y*d.x*(1-c)+d.z*s) + y*(d.y*d.y*(1-c)+c) + z*(d.y*d.z*(1-c)-d.x*s),
+                    x*(d.x*d.z*(1-c)-d.y*s) + y*(d.y*d.z*(1-c)+d.x*s) + z*(d.z*d.z*(1-c)+c));
+        return *this;
+    }
+    vec &rotate(float angle, const vec &d) { return rotate(cosf(angle), sinf(angle), d); }
+    vec &rotate(const vec2 &sc, const vec &d) { return rotate(sc.x, sc.y, d); }
+
+    void orthogonal(const vec &d)
+    {
+        *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
+    {
+        s.sub(vec(*this).mul(dot(s)));
+        t.sub(vec(*this).mul(dot(t)))
+         .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;
+        loopi(3)
+        {
+            if     (v[i] < min[i]) { float delta = v[i]-min[i]; sqrdist += delta*delta; }
+            else if(v[i] > max[i]) { float delta = max[i]-v[i]; sqrdist += delta*delta; }
+        }
+        return sqrtf(sqrdist);
+    }
+
+    template<class T, class S> float dist_to_bb(const T &o, S size) const
+    {
+        return dist_to_bb(o, T(o).add(size));
+    }
+
+    static vec hexcolor(int color)
+    {
+        return vec(((color>>16)&0xFF)*(1.0f/255.0f), ((color>>8)&0xFF)*(1.0f/255.0f), (color&0xFF)*(1.0f/255.0f));
+    }
+    int tohexcolor() const { 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;
+}
+
+static inline uint hthash(const vec &k)
+{
+    union { uint i; float f; } x, y, z;
+    x.f = k.x; y.f = k.y; z.f = k.z;
+    uint v = x.i^y.i^z.i;
+    return v + (v>>12);
+}
+
+struct vec4
+{
+    union
+    {
+        struct { float x, y, z, w; };
+        struct { float r, g, b, a; };
+        float v[4];
+    };
+
+    vec4() {}
+    explicit vec4(const vec &p, float w = 0) : x(p.x), y(p.y), z(p.z), w(w) {}
+    vec4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {}
+    explicit vec4(const float *v) : x(v[0]), y(v[1]), z(v[2]), w(v[3]) {}
+
+    float &operator[](int i)       { return v[i]; }
+    float  operator[](int i) const { return v[i]; }
+
+    template<class T> float dot3(const T &o) const { return x*o.x + y*o.y + z*o.z; }
+    float dot(const vec4 &o) const { return dot3(o) + w*o.w; }
+    float dot(const vec &o) const  { return x*o.x + y*o.y + z*o.z + w; }
+    float squaredlen() const { return dot(*this); }
+    float magnitude() const  { return sqrtf(squaredlen()); }
+    float magnitude3() const { return sqrtf(dot3(*this)); }
+    vec4 &normalize() { mul(1/magnitude()); return *this; }
+    vec4 &safenormalize() { float m = magnitude(); if(m) mul(1/m); return *this; }
+
+    vec4 &lerp(const vec4 &b, float t)
+    {
+        x += (b.x-x)*t;
+        y += (b.y-y)*t;
+        z += (b.z-z)*t;
+        w += (b.w-w)*t;
+        return *this;
+    }
+    vec4 &lerp(const vec4 &a, const vec4 &b, float t) 
+    { 
+        x = a.x+(b.x-a.x)*t; 
+        y = a.y+(b.y-a.y)*t; 
+        z = a.z+(b.z-a.z)*t;
+        w = a.w+(b.w-a.w)*t;
+        return *this;
+    }
+
+    vec4 &mul3(float f)      { x *= f; y *= f; z *= f; return *this; }
+    vec4 &mul(float f)       { mul3(f); w *= f; return *this; }
+    vec4 &mul(const vec4 &o) { x *= o.x; y *= o.y; z *= o.z; w *= o.w; return *this; }
+    vec4 &square()           { mul(*this); return *this; }
+    vec4 &div3(float f)      { x /= f; y /= f; z /= f; return *this; }
+    vec4 &div(float f)       { div3(f); w /= f; return *this; }
+    vec4 &div(const vec4 &o) { x /= o.x; y /= o.y; z /= o.z; w /= o.w; return *this; }
+    vec4 &recip()            { x = 1/x; y = 1/y; z = 1/z; w = 1/w; return *this; }
+    vec4 &add(const vec4 &o) { x += o.x; y += o.y; z += o.z; w += o.w; return *this; }
+    vec4 &addw(float f)      { w += f; return *this; }
+    vec4 &sub(const vec4 &o) { x -= o.x; y -= o.y; z -= o.z; w -= o.w; return *this; }
+    vec4 &subw(float f)      { w -= f; return *this; }
+    vec4 &neg3()             { x = -x; y = -y; z = -z; return *this; }
+    vec4 &neg()              { neg3(); w = -w; return *this; }
+    template<class B> vec4 &madd(const vec4 &a, const B &b) { return add(vec4(a).mul(b)); }
+    template<class B> vec4 &msub(const vec4 &a, const B &b) { return sub(vec4(a).mul(b)); }
+
+    void setxyz(const vec &v) { x = v.x; y = v.y; z = v.z; }
+
+    vec4 &rotate_around_z(float c, float s) { float rx = x, ry = y; x = c*rx-s*ry; y = c*ry+s*rx; return *this; }
+    vec4 &rotate_around_x(float c, float s) { float ry = y, rz = z; y = c*ry-s*rz; z = c*rz+s*ry; return *this; }
+    vec4 &rotate_around_y(float c, float s) { float rx = x, rz = z; x = c*rx+s*rz; z = c*rz-s*rx; return *this; }
+
+    vec4 &rotate_around_z(float angle) { return rotate_around_z(cosf(angle), sinf(angle)); }
+    vec4 &rotate_around_x(float angle) { return rotate_around_x(cosf(angle), sinf(angle)); }
+    vec4 &rotate_around_y(float angle) { return rotate_around_y(cosf(angle), sinf(angle)); }
+};
+
+inline vec::vec(const vec4 &v) : x(v.x), y(v.y), z(v.z) {}
+
+struct matrix3;
+struct matrix4x3;
+struct matrix4;
+
+struct quat : vec4
+{
+    quat() {}
+    quat(float x, float y, float z, float w) : vec4(x, y, z, w) {}
+    quat(const vec &axis, float angle)
+    {
+        w = cosf(angle/2);
+        float s = sinf(angle/2);
+        x = s*axis.x;
+        y = s*axis.y;
+        z = s*axis.z;
+    }
+    explicit quat(const vec &v)
+    {
+        x = v.x;
+        y = v.y;
+        z = v.z;
+        restorew();
+    }
+    explicit quat(const matrix3 &m) { convertmatrix(m); }
+    explicit quat(const matrix4x3 &m) { convertmatrix(m); }
+    explicit quat(const matrix4 &m) { convertmatrix(m); }
+
+    void restorew() { w = 1.0f-x*x-y*y-z*z; w = w<0 ? 0 : -sqrtf(w); }
+    
+    quat &add(const vec4 &o) { vec4::add(o); return *this; }
+    quat &sub(const vec4 &o) { vec4::sub(o); return *this; }
+    quat &mul(float k) { vec4::mul(k); return *this; }
+
+    quat &mul(const quat &p, const quat &o)
+    {
+        x = p.w*o.x + p.x*o.w + p.y*o.z - p.z*o.y;
+        y = p.w*o.y - p.x*o.z + p.y*o.w + p.z*o.x;
+        z = p.w*o.z + p.x*o.y - p.y*o.x + p.z*o.w;
+        w = p.w*o.w - p.x*o.x - p.y*o.y - p.z*o.z;
+        return *this;
+    }
+    quat &mul(const quat &o) { return mul(quat(*this), o); }
+
+    quat &invert() { neg3(); return *this; }
+
+    void calcangleaxis(float &angle, vec &axis)
+    {
+        float rr = dot3(*this);
+        if(rr>0)
+        {
+            angle = 2*acosf(w);
+            axis = vec(x, y, z).mul(1/rr); 
+        }
+        else { angle = 0; axis = vec(0, 0, 1); }
+    }
+
+    vec rotate(const vec &v) const
+    {
+        return vec().cross(*this, vec().cross(*this, v).add(vec(v).mul(w))).mul(2).add(v);
+    }
+
+    vec invertedrotate(const vec &v) const
+    {
+        return vec().cross(*this, vec().cross(*this, v).sub(vec(v).mul(w))).mul(2).add(v);
+    }
+
+    template<class M>
+    void convertmatrix(const M &m)
+    {
+        float trace = m.a.x + m.b.y + m.c.z;
+        if(trace>0)
+        {
+            float r = sqrtf(1 + trace), inv = 0.5f/r;
+            w = 0.5f*r;
+            x = (m.b.z - m.c.y)*inv;
+            y = (m.c.x - m.a.z)*inv;
+            z = (m.a.y - m.b.x)*inv;
+        }
+        else if(m.a.x > m.b.y && m.a.x > m.c.z)
+        {
+            float r = sqrtf(1 + m.a.x - m.b.y - m.c.z), inv = 0.5f/r;
+            x = 0.5f*r;
+            y = (m.a.y + m.b.x)*inv;
+            z = (m.c.x + m.a.z)*inv;
+            w = (m.b.z - m.c.y)*inv;
+        }
+        else if(m.b.y > m.c.z)
+        {
+            float r = sqrtf(1 + m.b.y - m.a.x - m.c.z), inv = 0.5f/r;
+            x = (m.a.y + m.b.x)*inv;
+            y = 0.5f*r;
+            z = (m.b.z + m.c.y)*inv;
+            w = (m.c.x - m.a.z)*inv;
+        }
+        else
+        {
+            float r = sqrtf(1 + m.c.z - m.a.x - m.b.y), inv = 0.5f/r;
+            x = (m.c.x + m.a.z)*inv;
+            y = (m.b.z + m.c.y)*inv;
+            z = 0.5f*r;
+            w = (m.a.y - m.b.x)*inv;
+        }
+    }
+};
+
+struct dualquat
+{
+    quat real, dual;
+
+    dualquat() {}
+    dualquat(const quat &q, const vec &p) 
+        : real(q),
+          dual(0.5f*( p.x*q.w + p.y*q.z - p.z*q.y),
+               0.5f*(-p.x*q.z + p.y*q.w + p.z*q.x),
+               0.5f*( p.x*q.y - p.y*q.x + p.z*q.w),
+              -0.5f*( p.x*q.x + p.y*q.y + p.z*q.z))
+    {
+    }
+    explicit dualquat(const quat &q) : real(q), dual(0, 0, 0, 0) {}
+    explicit dualquat(const matrix4x3 &m);
+
+    dualquat &mul(float k) { real.mul(k); dual.mul(k); return *this; }
+    dualquat &add(const dualquat &d) { real.add(d.real); dual.add(d.dual); return *this; }
+
+    dualquat &lerp(const dualquat &to, float t)
+    {
+        float k = real.dot(to.real) < 0 ? -t : t;
+        real.mul(1-t).add(vec4(to.real).mul(k));
+        dual.mul(1-t).add(vec4(to.dual).mul(k));
+        return *this;
+    }
+    dualquat &lerp(const dualquat &from, const dualquat &to, float t)
+    {
+        float k = from.real.dot(to.real) < 0 ? -t : t;
+        (real = from.real).mul(1-t).add(vec4(to.real).mul(k));
+        (dual = from.dual).mul(1-t).add(vec4(to.dual).mul(k));
+        return *this;
+    }
+
+    dualquat &invert()
+    {
+        real.invert();
+        dual.invert();
+        dual.sub(quat(real).mul(2*real.dot(dual)));
+        return *this;
+    }
+    
+    void mul(const dualquat &p, const dualquat &o)
+    {
+        real.mul(p.real, o.real);
+        dual.mul(p.real, o.dual).add(quat().mul(p.dual, o.real));
+    }       
+    void mul(const dualquat &o) { mul(dualquat(*this), o); }    
+  
+    void mulorient(const quat &q)
+    {
+        real.mul(q, quat(real));
+        dual.mul(quat(q).invert(), quat(dual));
+    }
+
+    void mulorient(const quat &q, const dualquat &base)
+    {
+        quat trans;
+        trans.mul(base.dual, quat(base.real).invert());
+        dual.mul(quat(q).invert(), quat(real).mul(trans).add(dual));
+
+        real.mul(q, quat(real));
+        dual.add(quat().mul(real, trans.invert())).sub(quat(real).mul(2*base.real.dot(base.dual)));
+    }
+
+    void normalize()
+    {
+        float invlen = 1/real.magnitude();
+        real.mul(invlen);
+        dual.mul(invlen);
+    }
+
+    void translate(const vec &p)
+    {
+        dual.x +=  0.5f*( p.x*real.w + p.y*real.z - p.z*real.y);
+        dual.y +=  0.5f*(-p.x*real.z + p.y*real.w + p.z*real.x);
+        dual.z +=  0.5f*( p.x*real.y - p.y*real.x + p.z*real.w);
+        dual.w += -0.5f*( p.x*real.x + p.y*real.y + p.z*real.z);
+    }
+
+    void scale(float k)
+    {
+        dual.mul(k);
+    }
+
+    void fixantipodal(const dualquat &d)
+    {
+        if(real.dot(d.real) < 0)
+        {
+            real.neg();
+            dual.neg();
+        }
+    }
+
+    void accumulate(const dualquat &d, float k)
+    {
+        if(real.dot(d.real) < 0) k = -k;
+        real.add(vec4(d.real).mul(k));
+        dual.add(vec4(d.dual).mul(k));
+    }
+
+    vec transform(const vec &v) const
+    {
+        return vec().cross(real, vec().cross(real, v).add(vec(v).mul(real.w)).add(vec(dual))).add(vec(dual).mul(real.w)).sub(vec(real).mul(dual.w)).mul(2).add(v);
+    }
+
+    quat transform(const quat &q) const
+    {
+        return quat().mul(real, q);
+    }
+
+    vec transposedtransform(const vec &v) const
+    {
+        return dualquat(*this).invert().transform(v);
+    }
+
+    vec transformnormal(const vec &v) const
+    {
+        return real.rotate(v);
+    }
+
+    vec transposedtransformnormal(const vec &v) const
+    {
+        return real.invertedrotate(v);
+    }
+
+    vec gettranslation() const
+    {
+        return vec().cross(real, dual).add(vec(dual).mul(real.w)).sub(vec(real).mul(dual.w)).mul(2);
+    }
+};
+
+struct matrix3
+{
+    vec a, b, c;
+
+    matrix3() {}
+    matrix3(const vec &a, const vec &b, const vec &c) : a(a), b(b), c(c) {}
+    explicit matrix3(float angle, const vec &axis) { rotate(angle, axis); }
+    explicit matrix3(const quat &q)
+    {
+        float x = q.x, y = q.y, z = q.z, w = q.w,
+              tx = 2*x, ty = 2*y, tz = 2*z,
+              txx = tx*x, tyy = ty*y, tzz = tz*z,
+              txy = tx*y, txz = tx*z, tyz = ty*z,
+              twx = w*tx, twy = w*ty, twz = w*tz;
+        a = vec(1 - (tyy + tzz), txy + twz, txz - twy);
+        b = vec(txy - twz, 1 - (txx + tzz), tyz + twx);
+        c = vec(txz + twy, tyz - twx, 1 - (txx + tyy));
+    }
+    explicit matrix3(const matrix4x3 &m);
+    explicit matrix3(const matrix4 &m);
+
+    void mul(const matrix3 &m, const matrix3 &n)
+    {
+        a = vec(m.a).mul(n.a.x).madd(m.b, n.a.y).madd(m.c, n.a.z);
+        b = vec(m.a).mul(n.b.x).madd(m.b, n.b.y).madd(m.c, n.b.z);
+        c = vec(m.a).mul(n.c.x).madd(m.b, n.c.y).madd(m.c, n.c.z);
+    }
+    void mul(const matrix3 &n) { mul(matrix3(*this), n); }
+
+    void multranspose(const matrix3 &m, const matrix3 &n)
+    {
+        a = vec(m.a).mul(n.a.x).madd(m.b, n.b.x).madd(m.c, n.c.x);
+        b = vec(m.a).mul(n.a.y).madd(m.b, n.b.y).madd(m.c, n.c.y);
+        c = vec(m.a).mul(n.a.z).madd(m.b, n.b.z).madd(m.c, n.c.z);
+    }
+    void multranspose(const matrix3 &n) { multranspose(matrix3(*this), n); }
+
+    void transposemul(const matrix3 &m, const matrix3 &n)
+    {
+        a = vec(m.a.dot(n.a), m.b.dot(n.a), m.c.dot(n.a));
+        b = vec(m.a.dot(n.b), m.b.dot(n.b), m.c.dot(n.b));
+        c = vec(m.a.dot(n.c), m.b.dot(n.c), m.c.dot(n.c));
+    }
+    void transposemul(const matrix3 &n) { transposemul(matrix3(*this), n); }
+
+    void transpose()
+    {
+        swap(a.y, b.x); swap(a.z, c.x);
+        swap(b.z, c.y);
+    }
+
+    template<class M>
+    void transpose(const M &m)
+    {
+        a = vec(m.a.x, m.b.x, m.c.x);
+        b = vec(m.a.y, m.b.y, m.c.y);
+        c = vec(m.a.z, m.b.z, m.c.z);
+    }
+
+    void invert(const matrix3 &o)
+    {
+        vec unscale(1/o.a.squaredlen(), 1/o.b.squaredlen(), 1/o.c.squaredlen());
+        transpose(o);
+        a.mul(unscale);
+        b.mul(unscale);
+        c.mul(unscale);
+    }
+    void invert() { invert(matrix3(*this)); }
+
+    void normalize()
+    {
+        a.normalize();
+        b.normalize();
+        c.normalize();
+    }
+
+    void scale(float k)
+    {
+        a.mul(k);
+        b.mul(k);
+        c.mul(k);
+    }
+
+    void rotate(float angle, const vec &axis)
+    {
+        rotate(cosf(angle), sinf(angle), axis);
+    }
+
+    void rotate(float ck, float sk, const vec &axis)
+    {
+        a = vec(axis.x*axis.x*(1-ck)+ck, axis.x*axis.y*(1-ck)+axis.z*sk, axis.x*axis.z*(1-ck)-axis.y*sk);
+        b = vec(axis.x*axis.y*(1-ck)-axis.z*sk, axis.y*axis.y*(1-ck)+ck, axis.y*axis.z*(1-ck)+axis.x*sk);
+        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));
+    }
+
+    float trace() const { return a.x + b.y + c.z; }
+
+    bool calcangleaxis(float tr, float &angle, vec &axis, float threshold = 1e-16f) const
+    {
+        if(tr <= -1)
+        {
+            if(a.x >= b.y && a.x >= c.z)
+            {
+                float r = 1 + a.x - b.y - c.z;
+                if(r <= threshold) return false;
+                r = sqrtf(r);
+                axis.x = 0.5f*r;
+                axis.y = b.x/r;
+                axis.z = c.x/r;
+            }
+            else if(b.y >= c.z)
+            {
+                float r = 1 + b.y - a.x - c.z;
+                if(r <= threshold) return false;
+                r = sqrtf(r);
+                axis.y = 0.5f*r;
+                axis.x = b.x/r;
+                axis.z = c.y/r;
+            }
+            else
+            {
+                float r = 1 + b.y - a.x - c.z;
+                if(r <= threshold) return false;
+                r = sqrtf(r);
+                axis.z = 0.5f*r;
+                axis.x = c.x/r;
+                axis.y = c.y/r;
+            }
+            angle = M_PI;
+        }
+        else if(tr >= 3)
+        {
+            axis = vec(0, 0, 1);
+            angle = 0;
+        }
+        else
+        {
+            axis = vec(b.z - c.y, c.x - a.z, a.y - b.x);
+            float r = axis.squaredlen();
+            if(r <= threshold) return false;
+            axis.mul(1/sqrtf(r));
+            angle = acosf(0.5f*(tr - 1));
+        }
+        return true;
+    }
+
+    bool calcangleaxis(float &angle, vec &axis, float threshold = 1e-16f) const { return calcangleaxis(trace(), angle, axis, threshold); }
+
+    vec transform(const vec &o) const
+    {
+        return vec(a).mul(o.x).madd(b, o.y).madd(c, o.z);
+    }
+    vec transposedtransform(const vec &o) const { return vec(a.dot(o), b.dot(o), c.dot(o)); }
+    vec abstransform(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());
+    }
+    vec abstransposedtransform(const vec &o) const
+    {
+        return vec(a.absdot(o), b.absdot(o), c.absdot(o));
+    }
+
+    void identity()
+    {
+        a = vec(1, 0, 0);
+        b = vec(0, 1, 0);
+        c = vec(0, 0, 1);
+    }
+
+    void rotate_around_x(float ck, float sk)
+    {
+        vec rb = vec(b).mul(ck).madd(c, sk),
+            rc = vec(c).mul(ck).msub(b, sk);
+        b = rb;
+        c = rc;
+    }
+    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 ck, float sk)
+    {
+        vec rc = vec(c).mul(ck).madd(a, sk),
+            ra = vec(a).mul(ck).msub(c, sk);
+        c = rc;
+        a = ra;
+    }
+    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 ck, float sk)
+    {
+        vec ra = vec(a).mul(ck).madd(b, sk),
+            rb = vec(b).mul(ck).msub(a, sk);
+        a = ra;
+        b = rb;
+    }
+    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) { return vec(a).mul(o.x).madd(b, o.y); }
+    vec transposedtransform(const vec2 &o) const { return vec(a.dot2(o), b.dot2(o), c.dot2(o)); }
+
+    vec rowx() const { return vec(a.x, b.x, c.x); }
+    vec rowy() const { return vec(a.y, b.y, c.y); }
+    vec rowz() const { return vec(a.z, b.z, c.z); }
+};
+
+struct matrix4x3
+{
+    vec a, b, c, d;
+
+    matrix4x3() {}
+    matrix4x3(const vec &a, const vec &b, const vec &c, const vec &d) : a(a), b(b), c(c), d(d) {}
+    matrix4x3(const matrix3 &rot, const vec &trans) : a(rot.a), b(rot.b), c(rot.c), d(trans) {}
+    matrix4x3(const dualquat &dq)
+    {
+        vec4 r = vec4(dq.real).mul(1/dq.real.squaredlen()), rr = vec4(r).mul(dq.real);
+        r.mul(2);
+        float xy = r.x*dq.real.y, xz = r.x*dq.real.z, yz = r.y*dq.real.z,
+              wx = r.w*dq.real.x, wy = r.w*dq.real.y, wz = r.w*dq.real.z;
+        a = vec(rr.w + rr.x - rr.y - rr.z, xy + wz, xz - wy);
+        b = vec(xy - wz, rr.w + rr.y - rr.x - rr.z, yz + wx);
+        c = vec(xz + wy, yz - wx, rr.w + rr.z - rr.x - rr.y);
+        d = vec(-(dq.dual.w*r.x - dq.dual.x*r.w + dq.dual.y*r.z - dq.dual.z*r.y),
+                -(dq.dual.w*r.y - dq.dual.x*r.z - dq.dual.y*r.w + dq.dual.z*r.x),
+                -(dq.dual.w*r.z + dq.dual.x*r.y - dq.dual.y*r.x - dq.dual.z*r.w));
+
+    }
+    explicit matrix4x3(const matrix4 &m);
+
+    void mul(float k)
+    {
+        a.mul(k);
+        b.mul(k);
+        c.mul(k);
+        d.mul(k);
+    }
+
+    void setscale(float x, float y, float z) { a.x = x; b.y = y; c.z = z; }
+    void setscale(const vec &v) { setscale(v.x, v.y, v.z); }
+    void setscale(float n) { setscale(n, n, n); }
+
+    void scale(float x, float y, float z)
+    {
+        a.mul(x);
+        b.mul(y);
+        c.mul(z);
+    }
+    void scale(const vec &v) { scale(v.x, v.y, v.z); }
+    void scale(float n) { scale(n, n, n); }
+
+    void settranslation(const vec &p) { d = p; }
+    void settranslation(float x, float y, float z) { d = vec(x, y, z); }
+
+    void translate(const vec &p) { d.madd(a, p.x).madd(b, p.y).madd(c, p.z); }
+    void translate(float x, float y, float z) { translate(vec(x, y, z)); }
+    void translate(const vec &p, float scale) { translate(vec(p).mul(scale)); }
+
+    void posttranslate(const vec &p) { d.add(p); }
+    void posttranslate(float x, float y, float z) { posttranslate(vec(x, y, z)); }
+    void posttranslate(const vec &p, float scale) { d.madd(p, scale); }
+
+    void accumulate(const matrix4x3 &m, float k)
+    {
+        a.madd(m.a, k);
+        b.madd(m.b, k);
+        c.madd(m.c, k);
+        d.madd(m.d, k);
+    }
+
+    void normalize()
+    {
+        a.normalize();
+        b.normalize();
+        c.normalize();
+    }
+
+    void lerp(const matrix4x3 &to, float t)
+    {
+        a.lerp(to.a, t);
+        b.lerp(to.b, t);
+        c.lerp(to.c, t);
+        d.lerp(to.d, t);
+    }
+    void lerp(const matrix4x3 &from, const matrix4x3 &to, float t)
+    {
+        a.lerp(from.a, to.a, t);
+        b.lerp(from.b, to.b, t);
+        c.lerp(from.c, to.c, t);
+        d.lerp(from.d, to.d, t);
+    }
+
+    void identity()
+    {
+        a = vec(1, 0, 0);
+        b = vec(0, 1, 0);
+        c = vec(0, 0, 1);
+        d = vec(0, 0, 0);
+    }
+
+    void mul(const matrix4x3 &m, const matrix4x3 &n)
+    {
+        a = vec(m.a).mul(n.a.x).madd(m.b, n.a.y).madd(m.c, n.a.z);
+        b = vec(m.a).mul(n.b.x).madd(m.b, n.b.y).madd(m.c, n.b.z);
+        c = vec(m.a).mul(n.c.x).madd(m.b, n.c.y).madd(m.c, n.c.z);
+        d = vec(m.d).madd(m.a, n.d.x).madd(m.b, n.d.y).madd(m.c, n.d.z);
+    }
+    void mul(const matrix4x3 &n) { mul(matrix4x3(*this), n); }
+
+    void mul(const matrix3 &m, const matrix4x3 &n)
+    {
+        a = vec(m.a).mul(n.a.x).madd(m.b, n.a.y).madd(m.c, n.a.z);
+        b = vec(m.a).mul(n.b.x).madd(m.b, n.b.y).madd(m.c, n.b.z);
+        c = vec(m.a).mul(n.c.x).madd(m.b, n.c.y).madd(m.c, n.c.z);
+        d = vec(m.a).mul(n.d.x).madd(m.b, n.d.y).madd(m.c, n.d.z);
+    }
+
+    void mul(const matrix3 &rot, const vec &trans, const matrix4x3 &n)
+    {
+        mul(rot, n);
+        d.add(trans);
+    }
+
+    void transpose()
+    {
+        d = vec(a.dot(d), b.dot(d), c.dot(d)).neg();
+        swap(a.y, b.x); swap(a.z, c.x);
+        swap(b.z, c.y);
+    }
+
+    void transpose(const matrix4x3 &o)
+    {
+        a = vec(o.a.x, o.b.x, o.c.x);
+        b = vec(o.a.y, o.b.y, o.c.y);
+        c = vec(o.a.z, o.b.z, o.c.z);
+        d = vec(o.a.dot(o.d), o.b.dot(o.d), o.c.dot(o.d)).neg();
+    }
+
+    void transposemul(const matrix4x3 &m, const matrix4x3 &n)
+    {
+        vec t(m.a.dot(m.d), m.b.dot(m.d), m.c.dot(m.d));
+        a = vec(m.a.dot(n.a), m.b.dot(n.a), m.c.dot(n.a));
+        b = vec(m.a.dot(n.b), m.b.dot(n.b), m.c.dot(n.b));
+        c = vec(m.a.dot(n.c), m.b.dot(n.c), m.c.dot(n.c));
+        d = vec(m.a.dot(n.d), m.b.dot(n.d), m.c.dot(n.d)).sub(t);
+    }
+
+    void multranspose(const matrix4x3 &m, const matrix4x3 &n)
+    {
+        vec t(n.a.dot(n.d), n.b.dot(n.d), n.c.dot(n.d));
+        a = vec(m.a).mul(n.a.x).madd(m.b, n.b.x).madd(m.c, n.c.x);
+        b = vec(m.a).mul(n.a.y).madd(m.b, n.b.y).madd(m.c, n.c.y);
+        c = vec(m.a).mul(n.a.z).madd(m.b, n.b.z).madd(m.c, n.c.z);
+        d = vec(m.d).msub(m.a, t.x).msub(m.b, t.y).msub(m.c, t.z);
+    }
+
+    void invert(const matrix4x3 &o)
+    {
+        vec unscale(1/o.a.squaredlen(), 1/o.b.squaredlen(), 1/o.c.squaredlen());
+        transpose(o);
+        a.mul(unscale);
+        b.mul(unscale);
+        c.mul(unscale);
+        d.mul(unscale);
+    }
+    void invert() { invert(matrix4x3(*this)); }
+
+    void rotate(float angle, const vec &d)
+    {
+        rotate(cosf(angle), sinf(angle), d);
+    }
+
+    void rotate(float ck, float sk, const vec &axis)
+    {
+        matrix3 m;
+        m.rotate(ck, sk, axis);
+        *this = matrix4x3(m, vec(0, 0, 0));
+    }
+
+    void rotate_around_x(float ck, float sk)
+    {
+        vec rb = vec(b).mul(ck).madd(c, sk),
+            rc = vec(c).mul(ck).msub(b, sk);
+        b = rb;
+        c = rc;
+    }
+    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 ck, float sk)
+    {
+        vec rc = vec(c).mul(ck).madd(a, sk),
+            ra = vec(a).mul(ck).msub(c, sk);
+        c = rc;
+        a = ra;
+    }
+    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 ck, float sk)
+    {
+        vec ra = vec(a).mul(ck).madd(b, sk),
+            rb = vec(b).mul(ck).msub(a, sk);
+        a = ra;
+        b = rb;
+    }
+    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 vec &o) const { return vec(d).madd(a, o.x).madd(b, o.y).madd(c, o.z); }
+    vec transposedtransform(const vec &o) const { vec p = vec(o).sub(d); return vec(a.dot(p), b.dot(p), c.dot(p)); }
+    vec transformnormal(const vec &o) const { return vec(a).mul(o.x).madd(b, o.y).madd(c, o.z); }
+    vec transposedtransformnormal(const vec &o) const { return vec(a.dot(o), b.dot(o), c.dot(o)); }
+    vec transform(const vec2 &o) const { return vec(d).madd(a, o.x).madd(b, o.y); }
+
+    vec4 rowx() const { return vec4(a.x, b.x, c.x, d.x); }
+    vec4 rowy() const { return vec4(a.y, b.y, c.y, d.y); }
+    vec4 rowz() const { return vec4(a.z, b.z, c.z, d.z); }
+};
+
+inline dualquat::dualquat(const matrix4x3 &m) : real(m)
+{
+    dual.x =  0.5f*( m.d.x*real.w + m.d.y*real.z - m.d.z*real.y);
+    dual.y =  0.5f*(-m.d.x*real.z + m.d.y*real.w + m.d.z*real.x);
+    dual.z =  0.5f*( m.d.x*real.y - m.d.y*real.x + m.d.z*real.w);
+    dual.w = -0.5f*( m.d.x*real.x + m.d.y*real.y + m.d.z*real.z);
+}
+
+inline matrix3::matrix3(const matrix4x3 &m) : a(m.a), b(m.b), c(m.c) {}
+
+struct plane : vec
+{
+    float offset;
+
+    float dist(const vec &p) const { return dot(p)+offset; }
+    float dist(const vec4 &p) const { return p.dot3(*this) + p.w*offset; }
+    bool operator==(const plane &p) const { return x==p.x && y==p.y && z==p.z && offset==p.offset; }
+    bool operator!=(const plane &p) const { return x!=p.x || y!=p.y || z!=p.z || offset!=p.offset; }
+
+    plane() {}
+    plane(const vec &c, float off) : vec(c), offset(off) {} 
+    plane(const vec4 &p) : vec(p), offset(p.w) {}
+    plane(int d, float off)
+    {
+        x = y = z = 0.0f;
+        v[d] = 1.0f;
+        offset = -off;
+    }
+    plane(float a, float b, float c, float d) : vec(a, b, c), offset(d) {}
+
+    void toplane(const vec &n, const vec &p)
+    {
+        x = n.x; y = n.y; z = n.z;
+        offset = -dot(p);
+    }
+
+    bool toplane(const vec &a, const vec &b, const vec &c)
+    {
+        cross(vec(b).sub(a), vec(c).sub(a));
+        float mag = magnitude();
+        if(!mag) return false;
+        div(mag);
+        offset = -dot(a);
+        return true;
+    }
+
+    bool rayintersect(const vec &o, const vec &ray, float &dist)
+    {
+        float cosalpha = dot(ray);
+        if(cosalpha==0) return false;
+        float deltac = offset+dot(o);
+        dist -= deltac/cosalpha;
+        return true;
+    }
+
+    plane &reflectz(float rz)
+    {
+        offset += 2*rz*z;
+        z = -z;
+        return *this; 
+    }
+
+    plane &invert()
+    {
+        neg();
+        offset = -offset;
+        return *this;
+    }
+
+    plane &scale(float k)
+    {
+        mul(k);
+        return *this;
+    }
+
+    plane &translate(const vec &p)
+    {
+        offset += dot(p);
+        return *this;
+    }
+
+    plane &normalize()
+    {
+        float mag = magnitude();
+        div(mag);
+        offset /= mag; 
+        return *this;
+    }
+
+    float zintersect(const vec &p) const { return -(x*p.x+y*p.y+offset)/z; }
+    float zdelta(const vec &p) const { return -(x*p.x+y*p.y)/z; }
+    float zdist(const vec &p) const { return p.z-zintersect(p); }
+};
+
+struct triangle
+{
+    vec a, b, c;
+
+    triangle(const vec &a, const vec &b, const vec &c) : a(a), b(b), c(c) {}
+    triangle() {}
+
+    triangle &add(const vec &o) { a.add(o); b.add(o); c.add(o); return *this; }
+    triangle &sub(const vec &o) { a.sub(o); b.sub(o); c.sub(o); return *this; }
+
+    bool operator==(const triangle &t) const { return a == t.a && b == t.b && c == t.c; }
+};
+
+/**
+
+Sauerbraten uses 3 different linear coordinate systems
+which are oriented around each of the axis dimensions.
+
+So any point within the game can be defined by four coordinates: (d, x, y, z)
+
+d is the reference axis dimension
+x is the coordinate of the ROW dimension
+y is the coordinate of the COL dimension
+z is the coordinate of the reference dimension (DEPTH)
+
+typically, if d is not used, then it is implicitly the Z dimension.
+ie: d=z => x=x, y=y, z=z
+
+**/
+
+// DIM: X=0 Y=1 Z=2.
+const int R[3]  = {1, 2, 0}; // row
+const int C[3]  = {2, 0, 1}; // col
+const int D[3]  = {0, 1, 2}; // depth
+
+struct ivec4;
+struct ivec2;
+struct usvec;
+struct svec;
+
+struct ivec
+{
+    union
+    {
+        struct { int x, y, z; };
+        struct { int r, g, b; };
+        int v[3];
+    };
+
+    ivec() {}
+    explicit ivec(const vec &v) : x(int(v.x)), y(int(v.y)), z(int(v.z)) {}
+    ivec(int a, int b, int c) : x(a), y(b), z(c) {}
+    ivec(int d, int row, int col, int depth)
+    {
+        v[R[d]] = row;
+        v[C[d]] = col;
+        v[D[d]] = depth;
+    }
+    ivec(int i, const ivec &co, int size) : x(co.x+((i&1)>>0)*size), y(co.y+((i&2)>>1)*size), z(co.z +((i&4)>>2)*size) {}
+    explicit ivec(const ivec4 &v);
+    explicit ivec(const ivec2 &v, int z = 0);
+    explicit ivec(const usvec &v);
+    explicit ivec(const svec &v);
+
+    int &operator[](int i)       { return v[i]; }
+    int  operator[](int i) const { return v[i]; }
+
+    //int idx(int i) { return v[i]; }
+    bool operator==(const ivec &v) const { return x==v.x && y==v.y && z==v.z; }
+    bool operator!=(const ivec &v) const { return x!=v.x || y!=v.y || z!=v.z; }
+    bool iszero() const { return x==0 && y==0 && z==0; }
+    ivec &shl(int n) { x<<= n; y<<= n; z<<= n; return *this; }
+    ivec &shr(int n) { x>>= n; y>>= n; z>>= n; return *this; }
+    ivec &mul(int n) { x *= n; y *= n; z *= n; return *this; }
+    ivec &div(int n) { x /= n; y /= n; z /= n; return *this; }
+    ivec &add(int n) { x += n; y += n; z += n; return *this; }
+    ivec &sub(int n) { x -= n; y -= n; z -= n; return *this; }
+    ivec &mul(const ivec &v) { x *= v.x; y *= v.y; z *= v.z; return *this; }
+    ivec &div(const ivec &v) { x /= v.x; y /= v.y; z /= v.z; return *this; }
+    ivec &add(const ivec &v) { x += v.x; y += v.y; z += v.z; return *this; }
+    ivec &sub(const ivec &v) { x -= v.x; y -= v.y; z -= v.z; return *this; }
+    ivec &mask(int n) { x &= n; y &= n; z &= n; return *this; }
+    ivec &neg() { return mul(-1); }
+    ivec &min(const ivec &o) { x = ::min(x, o.x); y = ::min(y, o.y); z = ::min(z, o.z); return *this; }
+    ivec &max(const ivec &o) { x = ::max(x, o.x); y = ::max(y, o.y); z = ::max(z, o.z); return *this; }
+    ivec &min(int n) { x = ::min(x, n); y = ::min(y, n); z = ::min(z, n); return *this; }
+    ivec &max(int n) { x = ::max(x, n); y = ::max(y, n); z = ::max(z, n); return *this; }
+    ivec &abs() { x = ::abs(x); y = ::abs(y); z = ::abs(z); return *this; }
+    ivec &clamp(int l, int h) { x = ::clamp(x, l, h); y = ::clamp(y, l, h); z = ::clamp(z, l, h); return *this; }
+    ivec &cross(const ivec &a, const ivec &b) { x = a.y*b.z-a.z*b.y; y = a.z*b.x-a.x*b.z; z = a.x*b.y-a.y*b.x; return *this; }
+    int dot(const ivec &o) const { return x*o.x + y*o.y + z*o.z; }
+    float dist(const plane &p) const { return x*p.x + y*p.y + z*p.z + p.offset; }
+
+    static inline ivec floor(const vec &o) { return ivec(int(::floor(o.x)), int(::floor(o.y)), int(::floor(o.z))); }
+    static inline ivec ceil(const vec &o) { return ivec(int(::ceil(o.x)), int(::ceil(o.y)), int(::ceil(o.z))); }
+};
+
+inline vec::vec(const ivec &v) : x(v.x), y(v.y), z(v.z) {}
+
+static inline bool htcmp(const ivec &x, const ivec &y)
+{
+    return x == y;
+}  
+
+static inline uint hthash(const ivec &k)
+{
+    return k.x^k.y^k.z;
+}  
+
+struct ivec2
+{
+    union
+    {
+        struct { int x, y; };
+        int v[2];
+    };
+
+    ivec2() {}
+    ivec2(int x, int y) : x(x), y(y) {}
+    explicit ivec2(const vec2 &v) : x(int(v.x)), y(int(v.y)) {}
+    explicit ivec2(const ivec &v) : x(v.x), y(v.y) {}
+
+    int &operator[](int i)       { return v[i]; }
+    int  operator[](int i) const { return v[i]; }
+
+    bool operator==(const ivec2 &o) const { return x == o.x && y == o.y; }
+    bool operator!=(const ivec2 &o) const { return x != o.x || y != o.y; }
+
+    bool iszero() const { return x==0 && y==0; }
+    ivec2 &shl(int n) { x<<= n; y<<= n; return *this; }
+    ivec2 &shr(int n) { x>>= n; y>>= n; return *this; }
+    ivec2 &mul(int n) { x *= n; y *= n; return *this; }
+    ivec2 &div(int n) { x /= n; y /= n; return *this; }
+    ivec2 &add(int n) { x += n; y += n; return *this; }
+    ivec2 &sub(int n) { x -= n; y -= n; return *this; }
+    ivec2 &mul(const ivec2 &v) { x *= v.x; y *= v.y; return *this; }
+    ivec2 &div(const ivec2 &v) { x /= v.x; y /= v.y; return *this; }
+    ivec2 &add(const ivec2 &v) { x += v.x; y += v.y; return *this; }
+    ivec2 &sub(const ivec2 &v) { x -= v.x; y -= v.y; return *this; }
+    ivec2 &mask(int n) { x &= n; y &= n; return *this; }
+    ivec2 &neg() { x = -x; y = -y; return *this; }
+    ivec2 &min(const ivec2 &o) { x = ::min(x, o.x); y = ::min(y, o.y); return *this; }
+    ivec2 &max(const ivec2 &o) { x = ::max(x, o.x); y = ::max(y, o.y); return *this; }
+    ivec2 &min(int n) { x = ::min(x, n); y = ::min(y, n); return *this; }
+    ivec2 &max(int n) { x = ::max(x, n); y = ::max(y, n); return *this; }
+    ivec2 &abs() { x = ::abs(x); y = ::abs(y); return *this; }
+    int dot(const ivec2 &o) const { return x*o.x + y*o.y; }
+    int cross(const ivec2 &o) const { return x*o.y - y*o.x; }
+};
+
+inline ivec::ivec(const ivec2 &v, int z) : x(v.x), y(v.y), z(z) {}
+
+static inline bool htcmp(const ivec2 &x, const ivec2 &y)
+{
+    return x == y;
+}
+
+static inline uint hthash(const ivec2 &k)
+{
+    return k.x^k.y;
+}
+
+struct ivec4
+{
+    union
+    {
+        struct { int x, y, z, w; };
+        struct { int r, g, b, a; };
+        int v[4];
+    };
+
+    ivec4() {}
+    explicit ivec4(const ivec &p, int w = 0) : x(p.x), y(p.y), z(p.z), w(w) {}
+    ivec4(int x, int y, int z, int w) : x(x), y(y), z(z), w(w) {}
+    explicit ivec4(const vec4 &v) : x(int(v.x)), y(int(v.y)), z(int(v.z)), w(int(v.w)) {}
+
+    bool operator==(const ivec4 &o) const { return x == o.x && y == o.y && z == o.z && w == o.w; }
+    bool operator!=(const ivec4 &o) const { return x != o.x || y != o.y || z != o.z || w != o.w; }
+};
+
+inline ivec::ivec(const ivec4 &v) : x(v.x), y(v.y), z(v.z) {}
+
+static inline bool htcmp(const ivec4 &x, const ivec4 &y)
+{
+    return x == y;
+}
+
+static inline uint hthash(const ivec4 &k)
+{
+    return k.x^k.y^k.z^k.w;
+}
+
+struct bvec4;
+
+struct bvec
+{
+    union
+    {
+        struct { uchar x, y, z; };
+        struct { uchar r, g, b; };
+        uchar v[3];
+    };
+
+    bvec() {}
+    bvec(uchar x, uchar y, uchar z) : x(x), y(y), z(z) {}
+    explicit 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(const bvec4 &v);
+
+    uchar &operator[](int i)       { return v[i]; }
+    uchar  operator[](int i) const { return v[i]; }
+
+    bool operator==(const bvec &v) const { return x==v.x && y==v.y && z==v.z; }
+    bool operator!=(const bvec &v) const { return x!=v.x || y!=v.y || z!=v.z; }
+
+    bool iszero() const { return x==0 && y==0 && z==0; }
+
+    vec tonormal() const { return vec(x*(2.0f/255.0f)-1.0f, y*(2.0f/255.0f)-1.0f, z*(2.0f/255.0f)-1.0f); }
+
+    bvec &normalize()
+    {
+        vec n(x-127.5f, y-127.5f, z-127.5f);
+        float mag = 127.5f/n.magnitude();
+        x = uchar(n.x*mag+127.5f);
+        y = uchar(n.y*mag+127.5f);
+        z = uchar(n.z*mag+127.5f);
+        return *this;
+    }
+
+    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 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; }
+
+    void scale(int k, int d) { x = uchar((x*k)/d); y = uchar((y*k)/d); z = uchar((z*k)/d); }
+
+    bvec &shl(int n) { x<<= n; y<<= n; z<<= n; return *this; }
+    bvec &shr(int n) { x>>= n; y>>= n; z>>= n; return *this; }
+
+    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)); }
+
+    static bvec from565(ushort c) { return bvec((((c>>11)&0x1F)*527 + 15) >> 6, (((c>>5)&0x3F)*259 + 35) >> 6, ((c&0x1F)*527 + 15) >> 6); }
+
+    static bvec hexcolor(int color)
+    {
+        return bvec((color>>16)&0xFF, (color>>8)&0xFF, color&0xFF);
+    }
+    int tohexcolor() const { return (int(r)<<16)|(int(g)<<8)|int(b); }
+};
+
+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 tonormal() 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 usvec
+{
+    union
+    {
+        struct { ushort x, y, z; };
+        ushort v[3];
+    };
+
+    ushort &operator[](int i) { return v[i]; }
+    ushort operator[](int i) const { return v[i]; }
+};
+
+inline ivec::ivec(const usvec &v) : x(v.x), y(v.y), z(v.z) {}
+
+struct svec
+{
+    union
+    {
+        struct { short x, y, z; };
+        short v[3];
+    };
+
+    svec() {}
+    svec(short x, short y, short z) : x(x), y(y), z(z) {}
+    explicit svec(const ivec &v) : x(v.x), y(v.y), z(v.z) {}
+
+    short &operator[](int i) { return v[i]; }
+    short operator[](int i) const { return v[i]; }
+};
+
+inline ivec::ivec(const svec &v) : x(v.x), y(v.y), z(v.z) {}
+
+struct svec2
+{
+    union
+    {
+        struct { short x, y; };
+        short v[2];
+    };
+
+    svec2() {}
+    svec2(short x, short y) : x(x), y(y) {}
+
+    short &operator[](int i) { return v[i]; }
+    short operator[](int i) const { return v[i]; }
+
+    bool operator==(const svec2 &o) const { return x == o.x && y == o.y; }
+    bool operator!=(const svec2 &o) const { return x != o.x || y != o.y; }
+
+    bool iszero() const { return x==0 && y==0; }
+};
+
+struct dvec4
+{
+    double x, y, z, w;
+
+    dvec4() {}
+    dvec4(double x, double y, double z, double w) : x(x), y(y), z(z), w(w) {}
+    dvec4(const vec4 &v) : x(v.x), y(v.y), z(v.z), w(v.w) {}
+
+    template<class B> dvec4 &madd(const dvec4 &a, const B &b) { return add(dvec4(a).mul(b)); }
+    dvec4 &mul(double f)       { x *= f; y *= f; z *= f; w *= f; return *this; }
+    dvec4 &mul(const dvec4 &o) { x *= o.x; y *= o.y; z *= o.z; w *= o.w; return *this; }
+    dvec4 &add(double f)       { x += f; y += f; z += f; w += f; return *this; }
+    dvec4 &add(const dvec4 &o) { x += o.x; y += o.y; z += o.z; w += o.w; return *this; }
+
+    operator vec4() const { return vec4(x, y, z, w); }
+};
+
+struct matrix4
+{
+    vec4 a, b, c, d;
+
+    matrix4() {}
+    matrix4(const float *m) : a(m), b(m+4), c(m+8), d(m+12) {}
+    matrix4(const vec &a, const vec &b, const vec &c = vec(0, 0, 1))
+        : a(a.x, b.x, c.x, 0), b(a.y, b.y, c.y, 0), c(a.z, b.z, c.z, 0), d(0, 0, 0, 1)
+    {}
+    matrix4(const vec4 &a, const vec4 &b, const vec4 &c, const vec4 &d = vec4(0, 0, 0, 1))
+        : a(a), b(b), c(c), d(d)
+    {}
+    matrix4(const matrix4x3 &m)
+        : a(m.a, 0), b(m.b, 0), c(m.c, 0), d(m.d, 1)
+    {}
+    matrix4(const matrix3 &rot, const vec &trans)
+        : a(rot.a, 0), b(rot.b, 0), c(rot.c, 0), d(trans, 1)
+    {}
+
+    void mul(const matrix4 &x, const matrix3 &y)
+    {
+        a = vec4(x.a).mul(y.a.x).madd(x.b, y.a.y).madd(x.c, y.a.z);
+        b = vec4(x.a).mul(y.b.x).madd(x.b, y.b.y).madd(x.c, y.b.z);
+        c = vec4(x.a).mul(y.c.x).madd(x.b, y.c.y).madd(x.c, y.c.z);
+        d = x.d;
+    }
+    void mul(const matrix3 &y) { mul(matrix4(*this), y); }
+
+    template<class T> void mult(const matrix4 &x, const matrix4 &y)
+    {
+        a = T(x.a).mul(y.a.x).madd(x.b, y.a.y).madd(x.c, y.a.z).madd(x.d, y.a.w);
+        b = T(x.a).mul(y.b.x).madd(x.b, y.b.y).madd(x.c, y.b.z).madd(x.d, y.b.w);
+        c = T(x.a).mul(y.c.x).madd(x.b, y.c.y).madd(x.c, y.c.z).madd(x.d, y.c.w);
+        d = T(x.a).mul(y.d.x).madd(x.b, y.d.y).madd(x.c, y.d.z).madd(x.d, y.d.w);
+    }
+
+    void mul(const matrix4 &x, const matrix4 &y) { mult<vec4>(x, y); }
+    void mul(const matrix4 &y) { mult<vec4>(matrix4(*this), y); }
+
+    void muld(const matrix4 &x, const matrix4 &y) { mult<dvec4>(x, y); }
+    void muld(const matrix4 &y) { mult<dvec4>(matrix4(*this), y); }
+
+    void rotate_around_x(float ck, float sk)
+    {
+        vec4 rb = vec4(b).mul(ck).madd(c, sk),
+             rc = vec4(c).mul(ck).msub(b, sk);
+        b = rb;
+        c = rc;
+    }
+    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 ck, float sk)
+    {
+        vec4 rc = vec4(c).mul(ck).madd(a, sk),
+             ra = vec4(a).mul(ck).msub(c, sk);
+        c = rc;
+        a = ra;
+    }
+    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 ck, float sk)
+    {
+        vec4 ra = vec4(a).mul(ck).madd(b, sk),
+             rb = vec4(b).mul(ck).msub(a, sk);
+        a = ra;
+        b = rb;
+    }
+    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); }
+
+    void rotate(float ck, float sk, const vec &axis)
+    {
+        matrix3 m;
+        m.rotate(ck, sk, axis);
+        mul(m);
+    }
+    void rotate(float angle, const vec &dir) { rotate(cosf(angle), sinf(angle), dir); }
+    void rotate(const vec2 &sc, const vec &dir) { rotate(sc.x, sc.y, dir); }
+
+    void identity()
+    {
+        a = vec4(1, 0, 0, 0);
+        b = vec4(0, 1, 0, 0);
+        c = vec4(0, 0, 1, 0);
+        d = vec4(0, 0, 0, 1);
+    }
+
+    void settranslation(const vec &v) { d.setxyz(v); }
+    void settranslation(float x, float y, float z) { d.x = x; d.y = y; d.z = z; }
+
+    void translate(const vec &p) { d.madd(a, p.x).madd(b, p.y).madd(c, p.z); }
+    void translate(float x, float y, float z) { translate(vec(x, y, z)); }
+    void translate(const vec &p, float scale) { translate(vec(p).mul(scale)); }
+
+    void setscale(float x, float y, float z) { a.x = x; b.y = y; c.z = z; }
+    void setscale(const vec &v) { setscale(v.x, v.y, v.z); }
+    void setscale(float n) { setscale(n, n, n); }
+
+    void scale(float x, float y, float z)
+    {
+        a.mul(x);
+        b.mul(y);
+        c.mul(z);
+    }
+    void scale(const vec &v) { scale(v.x, v.y, v.z); }
+    void scale(float n) { scale(n, n, n); }
+
+    void scalexy(float x, float y)
+    {
+        a.x *= x; a.y *= y;
+        b.x *= x; b.y *= y;
+        c.x *= x; c.y *= y;
+        d.x *= x; d.y *= y;
+    }
+
+    void scalez(float k)
+    {
+        a.z *= k;
+        b.z *= k;
+        c.z *= k;
+        d.z *= k;
+    }
+
+    void reflectz(float z)
+    {
+        d.add(vec4(c).mul(2*z));
+        c.neg();
+    }
+
+    void projective(float zscale = 0.5f, float zoffset = 0.5f)
+    {
+        a.x = 0.5f*(a.x + a.w);
+        a.y = 0.5f*(a.y + a.w);
+        b.x = 0.5f*(b.x + b.w);
+        b.y = 0.5f*(b.y + b.w);
+        c.x = 0.5f*(c.x + c.w);
+        c.y = 0.5f*(c.y + c.w);
+        d.x = 0.5f*(d.x + d.w);
+        d.y = 0.5f*(d.y + d.w);
+        a.z = zscale*a.z + zoffset*a.w;
+        b.z = zscale*b.z + zoffset*b.w;
+        c.z = zscale*c.z + zoffset*c.w;
+        d.z = zscale*d.z + zoffset*d.w;
+    }
+
+    void jitter(float x, float y)
+    {
+        a.x += x * a.w;
+        a.y += y * a.w;
+        b.x += x * b.w;
+        b.y += y * b.w;
+        c.x += x * c.w;
+        c.y += y * c.w;
+        d.x += x * d.w;
+        d.y += y * d.w;
+    }
+
+    void transpose()
+    {
+        swap(a.y, b.x); swap(a.z, c.x); swap(a.w, d.x);
+        swap(b.z, c.y); swap(b.w, d.y);
+        swap(c.w, d.z);
+    }
+
+    void transpose(const matrix4 &m)
+    {
+        a = vec4(m.a.x, m.b.x, m.c.x, m.d.x);
+        b = vec4(m.a.y, m.b.y, m.c.y, m.d.y);
+        c = vec4(m.a.z, m.b.z, m.c.z, m.d.z);
+        d = vec4(m.a.w, m.b.w, m.c.w, m.d.w);
+    }
+
+    void frustum(float left, float right, float bottom, float top, float znear, float zfar)
+    {
+        float width = right - left, height = top - bottom, zrange = znear - zfar;
+        a = vec4(2*znear/width, 0, 0, 0);
+        b = vec4(0, 2*znear/height, 0, 0);
+        c = vec4((right + left)/width, (top + bottom)/height, (zfar + znear)/zrange, -1);
+        d = vec4(0, 0, 2*znear*zfar/zrange, 0);
+    }
+
+    void perspective(float fovy, float aspect, float znear, float zfar)
+    {
+        float ydist = znear * tan(fovy/2*RAD), xdist = ydist * aspect;
+        frustum(-xdist, xdist, -ydist, ydist, znear, zfar);
+    }
+
+    void ortho(float left, float right, float bottom, float top, float znear, float zfar)
+    {
+        float width = right - left, height = top - bottom, zrange = znear - zfar;
+        a = vec4(2/width, 0, 0, 0);
+        b = vec4(0, 2/height, 0, 0);
+        c = vec4(0, 0, 2/zrange, 0);
+        d = vec4(-(right+left)/width, -(top+bottom)/height, (zfar+znear)/zrange, 1);
+    }
+
+    void clip(const plane &p, const matrix4 &m)
+    {
+        float x = ((p.x<0 ? -1 : (p.x>0 ? 1 : 0)) + m.c.x) / m.a.x,
+              y = ((p.y<0 ? -1 : (p.y>0 ? 1 : 0)) + m.c.y) / m.b.y,
+              w = (1 + m.c.z) / m.d.z,
+            scale = 2 / (x*p.x + y*p.y - p.z + w*p.offset);
+        a = vec4(m.a.x, m.a.y, p.x*scale, m.a.w);
+        b = vec4(m.b.x, m.b.y, p.y*scale, m.b.w);
+        c = vec4(m.c.x, m.c.y, p.z*scale + 1.0f, m.c.w);
+        d = vec4(m.d.x, m.d.y, p.offset*scale, m.d.w);
+    }
+
+    void transform(const vec &in, vec &out) const
+    {
+        out = vec(a).mul(in.x).add(vec(b).mul(in.y)).add(vec(c).mul(in.z)).add(vec(d));
+    }
+
+    void transform(const vec4 &in, vec &out) const
+    {
+        out = vec(a).mul(in.x).add(vec(b).mul(in.y)).add(vec(c).mul(in.z)).add(vec(d).mul(in.w));
+    }
+
+    void transform(const vec &in, vec4 &out) const
+    {
+        out = vec4(a).mul(in.x).madd(b, in.y).madd(c, in.z).add(d);
+    }
+
+    void transform(const vec4 &in, vec4 &out) const
+    {
+        out = vec4(a).mul(in.x).madd(b, in.y).madd(c, in.z).madd(d, in.w);
+    }
+
+    template<class T, class U> T transform(const U &in) const
+    {
+        T v;
+        transform(in, v);
+        return v;
+    }
+
+    template<class T> vec perspectivetransform(const T &in) const
+    {
+        vec4 v;
+        transform(in, v);
+        return vec(v).div(v.w);
+    }
+
+    void transformnormal(const vec &in, vec &out) const
+    {
+        out = vec(a).mul(in.x).add(vec(b).mul(in.y)).add(vec(c).mul(in.z));
+    }
+
+    void transformnormal(const vec &in, vec4 &out) const
+    {
+        out = vec4(a).mul(in.x).madd(b, in.y).madd(c, in.z);
+    }
+
+    template<class T, class U> T transformnormal(const U &in) const
+    {
+        T v;
+        transformnormal(in, v);
+        return v;
+    }
+
+    void transposedtransform(const vec &in, vec &out) const
+    {
+        vec p = vec(in).sub(vec(d));
+        out.x = a.dot3(p);
+        out.y = b.dot3(p);
+        out.z = c.dot3(p);
+    }
+
+    void transposedtransformnormal(const vec &in, vec &out) const
+    {
+        out.x = a.dot3(in);
+        out.y = b.dot3(in);
+        out.z = c.dot3(in);
+    }
+
+    void transposedtransform(const plane &in, plane &out) const
+    {
+        out.x = in.dist(a);
+        out.y = in.dist(b);
+        out.z = in.dist(c);
+        out.offset = in.dist(d);
+    }
+
+    float getscale() const
+    {
+        return sqrtf(a.x*a.y + b.x*b.x + c.x*c.x);
+    }
+
+    vec gettranslation() const
+    {
+        return vec(d);
+    }
+
+    vec4 rowx() const { return vec4(a.x, b.x, c.x, d.x); }
+    vec4 rowy() const { return vec4(a.y, b.y, c.y, d.y); }
+    vec4 rowz() const { return vec4(a.z, b.z, c.z, d.z); }
+    vec4 roww() const { return vec4(a.w, b.w, c.w, d.w); }
+
+    bool invert(const matrix4 &m, double mindet = 1.0e-12);
+
+    vec2 lineardepthscale() const
+    {
+        return vec2(d.w, -d.z).div(c.z*d.w - d.z*c.w);
+    }
+};
+
+inline matrix3::matrix3(const matrix4 &m)
+    : a(m.a), b(m.b), c(m.c)
+{}
+
+inline matrix4x3::matrix4x3(const matrix4 &m)
+    : a(m.a), b(m.b), c(m.c), d(m.d)
+{}
+
+struct matrix2
+{
+    vec2 a, b;
+
+    matrix2() {}
+    matrix2(const vec2 &a, const vec2 &b) : a(a), b(b) {}
+    explicit matrix2(const matrix4 &m) : a(m.a), b(m.b) {}
+    explicit matrix2(const matrix3 &m) : a(m.a), b(m.b) {}
+};
+
+struct squat
+{
+    short x, y, z, w;
+
+    squat() {}
+    squat(const vec4 &q) { convert(q); }
+
+    void convert(const vec4 &q)
+    {
+        x = short(q.x*32767.5f-0.5f);
+        y = short(q.y*32767.5f-0.5f);
+        z = short(q.z*32767.5f-0.5f);
+        w = short(q.w*32767.5f-0.5f);
+    }
+
+    void lerp(const vec4 &a, const vec4 &b, float t)
+    {
+        vec4 q;
+        q.lerp(a, b, t);
+        convert(q);
+    }
+};
+
+extern bool raysphereintersect(const vec &center, float radius, const vec &o, const vec &ray, float &dist);
+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 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)]; }
+static inline float cos360(int angle) { return sincos360[angle].x; }
+static inline float sin360(int angle) { return sincos360[angle].y; }
+static inline float tan360(int angle) { const vec2 &sc = sincos360[angle]; return sc.y/sc.x; }
+static inline float cotan360(int angle) { const vec2 &sc = sincos360[angle]; return sc.x/sc.y; }
+
diff --git a/src/shared/glemu.cpp b/src/shared/glemu.cpp
new file mode 100644 (file)
index 0000000..5658eb8
--- /dev/null
@@ -0,0 +1,355 @@
+#include "cube.h"
+
+extern int glversion;
+extern int intel_mapbufferrange_bug;
+
+namespace gle
+{
+    struct attribinfo
+    {
+        int type, size, formatsize, offset;
+        GLenum format;
+
+        attribinfo() : type(0), size(0), formatsize(0), offset(0), format(GL_FALSE) {}
+
+        bool operator==(const attribinfo &a) const
+        {
+            return type == a.type && size == a.size && format == a.format && offset == a.offset;
+        }
+        bool operator!=(const attribinfo &a) const
+        {
+            return type != a.type || size != a.size || format != a.format || offset != a.offset;
+        }
+    };
+
+    extern const char * const attribnames[MAXATTRIBS] = { "vvertex", "vcolor", "vtexcoord0", "vtexcoord1", "vnormal", "vtangent", "vboneweight", "vboneindex" };
+    ucharbuf attribbuf;
+    static uchar *attribdata;
+    static attribinfo attribdefs[MAXATTRIBS], lastattribs[MAXATTRIBS];
+    int enabled = 0;
+    static int numattribs = 0, attribmask = 0, numlastattribs = 0, lastattribmask = 0, vertexsize = 0, lastvertexsize = 0;
+    static GLenum primtype = GL_TRIANGLES;
+    static uchar *lastbuf = NULL;
+    static bool changedattribs = false;
+    static vector<GLint> multidrawstart;
+    static vector<GLsizei> multidrawcount;
+
+    #define MAXQUADS (0x10000/4)
+    static GLuint quadindexes = 0;
+    static bool quadsenabled = false;
+
+    #define MAXVBOSIZE (4*1024*1024)
+    static GLuint vbo = 0;
+    static int vbooffset = MAXVBOSIZE;
+
+    static GLuint defaultvao = 0;
+
+    void enablequads()
+    {
+        quadsenabled = true;
+
+        if(glversion < 300) return;
+
+        if(quadindexes)
+        {
+            glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, quadindexes);
+            return;
+        }
+
+        glGenBuffers_(1, &quadindexes);
+        ushort *data = new ushort[MAXQUADS*6], *dst = data;
+        for(int idx = 0; idx < MAXQUADS*4; idx += 4, dst += 6)
+        {
+            dst[0] = idx;
+            dst[1] = idx + 1;
+            dst[2] = idx + 2;
+            dst[3] = idx + 0;
+            dst[4] = idx + 2;
+            dst[5] = idx + 3;
+        }
+        glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, quadindexes);
+        glBufferData_(GL_ELEMENT_ARRAY_BUFFER, MAXQUADS*6*sizeof(ushort), data, GL_STATIC_DRAW);
+        delete[] data;
+    }
+
+    void disablequads()
+    {
+        quadsenabled = false;
+    
+        if(glversion < 300) return;
+
+        glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, 0);
+    }
+
+    void drawquads(int offset, int count)
+    {
+        if(count <= 0) return;
+        if(glversion < 300)
+        {
+            glDrawArrays(GL_QUADS, offset*4, count*4);
+            return;
+        }
+        if(offset + count > MAXQUADS)
+        {
+            if(offset >= MAXQUADS) return;
+            count = MAXQUADS - offset;
+        }
+        glDrawRangeElements_(GL_TRIANGLES, offset*4, (offset + count)*4-1, count*6, GL_UNSIGNED_SHORT, (ushort *)0 + offset*6);
+    }
+
+    void defattrib(int type, int size, int format)
+    {
+        if(type == ATTRIB_VERTEX)
+        {
+            numattribs = attribmask = 0;
+            vertexsize = 0;
+        }
+        changedattribs = true;
+        attribmask |= 1<<type;
+        attribinfo &a = attribdefs[numattribs++];
+        a.type = type;
+        a.size = size;
+        a.format = format;
+        switch(format)
+        {
+            case 'B': case GL_UNSIGNED_BYTE:  a.formatsize = 1; a.format = GL_UNSIGNED_BYTE; break;
+            case 'b': case GL_BYTE:           a.formatsize = 1; a.format = GL_BYTE; break;
+            case 'S': case GL_UNSIGNED_SHORT: a.formatsize = 2; a.format = GL_UNSIGNED_SHORT; break;
+            case 's': case GL_SHORT:          a.formatsize = 2; a.format = GL_SHORT; break;
+            case 'I': case GL_UNSIGNED_INT:   a.formatsize = 4; a.format = GL_UNSIGNED_INT; break;
+            case 'i': case GL_INT:            a.formatsize = 4; a.format = GL_INT; break;
+            case 'f': case GL_FLOAT:          a.formatsize = 4; a.format = GL_FLOAT; break;
+            case 'd': case GL_DOUBLE:         a.formatsize = 8; a.format = GL_DOUBLE; break;
+            default:                          a.formatsize = 0; a.format = GL_FALSE; break;
+        }
+        a.formatsize *= size;
+        a.offset = vertexsize;
+        vertexsize += a.formatsize;
+    }
+
+    void defattribs(const char *fmt)
+    {
+        for(;; fmt += 3)
+        {
+            GLenum format;
+            switch(fmt[0])
+            {
+                case 'v': format = ATTRIB_VERTEX; break;
+                case 'c': format = ATTRIB_COLOR; break;
+                case 't': format = ATTRIB_TEXCOORD0; break;
+                case 'T': format = ATTRIB_TEXCOORD1; break;
+                case 'n': format = ATTRIB_NORMAL; break;
+                case 'x': format = ATTRIB_TANGENT; break;
+                case 'w': format = ATTRIB_BONEWEIGHT; break;
+                case 'i': format = ATTRIB_BONEINDEX; break;
+                default: return;
+            }
+            defattrib(format, fmt[1]-'0', fmt[2]);
+        }
+    }
+
+    static inline void setattrib(const attribinfo &a, uchar *buf)
+    {
+        switch(a.type)
+        {
+            case ATTRIB_VERTEX:
+            case ATTRIB_TEXCOORD0:
+            case ATTRIB_TEXCOORD1:
+            case ATTRIB_BONEINDEX:
+                glVertexAttribPointer_(a.type, a.size, a.format, GL_FALSE, vertexsize, buf);
+                break;
+            case ATTRIB_COLOR:
+            case ATTRIB_NORMAL:
+            case ATTRIB_TANGENT:
+            case ATTRIB_BONEWEIGHT:
+                glVertexAttribPointer_(a.type, a.size, a.format, GL_TRUE, vertexsize, buf);
+                break;
+        }
+        if(!(enabled&(1<<a.type)))
+        {
+            glEnableVertexAttribArray_(a.type);
+            enabled |= 1<<a.type;
+        }
+    }
+
+    static inline void unsetattrib(const attribinfo &a)
+    {
+        glDisableVertexAttribArray_(a.type);
+        enabled &= ~(1<<a.type);
+    }
+
+    static inline void setattribs(uchar *buf)
+    {
+        bool forceattribs = numattribs != numlastattribs || vertexsize != lastvertexsize || buf != lastbuf;
+        if(forceattribs || changedattribs)
+        {
+            int diffmask = enabled & lastattribmask & ~attribmask;
+            if(diffmask) loopi(numlastattribs)
+            {
+                const attribinfo &a = lastattribs[i];
+                if(diffmask & (1<<a.type)) unsetattrib(a);
+            }
+            uchar *src = buf;
+            loopi(numattribs)
+            {
+                const attribinfo &a = attribdefs[i];
+                if(forceattribs || a != lastattribs[i])
+                {
+                    setattrib(a, src);
+                    lastattribs[i] = a;
+                }
+                src += a.formatsize;
+            }
+            lastbuf = buf;
+            numlastattribs = numattribs;
+            lastattribmask = attribmask;
+            lastvertexsize = vertexsize;
+            changedattribs = false;
+        }
+    }
+
+    void begin(GLenum mode)
+    {
+        primtype = mode;
+    }
+
+    void begin(GLenum mode, int numverts)
+    {
+        primtype = mode;
+        if(glversion >= 300 && !intel_mapbufferrange_bug)
+        {
+            int len = numverts * vertexsize;
+            if(vbooffset + len >= MAXVBOSIZE)
+            {
+                len = min(len, MAXVBOSIZE);
+                if(!vbo) glGenBuffers_(1, &vbo);
+                glBindBuffer_(GL_ARRAY_BUFFER, vbo);
+                glBufferData_(GL_ARRAY_BUFFER, MAXVBOSIZE, NULL, GL_STREAM_DRAW);
+                vbooffset = 0;
+            }
+            else if(!lastvertexsize) glBindBuffer_(GL_ARRAY_BUFFER, vbo);
+            void *buf = glMapBufferRange_(GL_ARRAY_BUFFER, vbooffset, len, GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT);
+            if(buf) attribbuf.reset((uchar *)buf, len);
+        }
+    }
+
+    void multidraw()
+    {
+        int start = multidrawstart.length() ? multidrawstart.last() + multidrawcount.last() : 0,
+            count = attribbuf.length()/vertexsize - start;
+        if(count > 0)
+        {
+            multidrawstart.add(start);
+            multidrawcount.add(count);
+        }
+    }
+
+    int end()
+    {
+        uchar *buf = attribbuf.getbuf();
+        if(attribbuf.empty())
+        {
+            if(buf != attribdata)
+            {
+                glUnmapBuffer_(GL_ARRAY_BUFFER);
+                attribbuf.reset(attribdata, MAXVBOSIZE);
+            }
+            return 0;
+        }
+        int start = 0;
+        if(glversion >= 300)
+        {
+            if(buf == attribdata)
+            {
+                if(vbooffset + attribbuf.length() >= MAXVBOSIZE)
+                {
+                    if(!vbo) glGenBuffers_(1, &vbo);
+                    glBindBuffer_(GL_ARRAY_BUFFER, vbo);
+                    glBufferData_(GL_ARRAY_BUFFER, MAXVBOSIZE, NULL, GL_STREAM_DRAW);
+                    vbooffset = 0;
+                }
+                else if(!lastvertexsize) glBindBuffer_(GL_ARRAY_BUFFER, vbo);
+                void *dst = intel_mapbufferrange_bug ? NULL :
+                    glMapBufferRange_(GL_ARRAY_BUFFER, vbooffset, attribbuf.length(), GL_MAP_WRITE_BIT|GL_MAP_INVALIDATE_RANGE_BIT|GL_MAP_UNSYNCHRONIZED_BIT);
+                if(dst)
+                {
+                    memcpy(dst, attribbuf.getbuf(), attribbuf.length());
+                    glUnmapBuffer_(GL_ARRAY_BUFFER);
+                }
+                else glBufferSubData_(GL_ARRAY_BUFFER, vbooffset, attribbuf.length(), attribbuf.getbuf());
+            }
+            else glUnmapBuffer_(GL_ARRAY_BUFFER);
+            buf = (uchar *)0 + vbooffset;
+            if(vertexsize == lastvertexsize && buf >= lastbuf)
+            {
+                start = int(buf - lastbuf)/vertexsize;
+                if(primtype == GL_QUADS && (start%4 || start + attribbuf.length()/vertexsize >= 4*MAXQUADS))
+                    start = 0;
+                else buf = lastbuf;
+            }
+            vbooffset += attribbuf.length();
+        }
+        setattribs(buf);
+        int numvertexes = attribbuf.length()/vertexsize;
+        if(primtype == GL_QUADS)
+        {
+            if(!quadsenabled) enablequads();
+            for(int quads = numvertexes/4;;)
+            {
+                int count = min(quads, MAXQUADS);
+                drawquads(start/4, count);
+                quads -= count;
+                if(quads <= 0) break;
+                setattribs(buf + 4*count*vertexsize);
+                start = 0;
+            }
+        }
+        else
+        {
+            if(multidrawstart.length())
+            {
+                multidraw();
+                if(start) loopv(multidrawstart) multidrawstart[i] += start;
+                glMultiDrawArrays_(primtype, multidrawstart.getbuf(), multidrawcount.getbuf(), multidrawstart.length());
+                multidrawstart.setsize(0);
+                multidrawcount.setsize(0);
+            }
+            else glDrawArrays(primtype, start, numvertexes);
+        }
+        attribbuf.reset(attribdata, MAXVBOSIZE);
+        return numvertexes;
+    }
+
+    void forcedisable()
+    {
+        for(int i = 0; enabled; i++) if(enabled&(1<<i)) { glDisableVertexAttribArray_(i); enabled &= ~(1<<i); }
+        numlastattribs = lastattribmask = lastvertexsize = 0;
+        lastbuf = NULL;
+        if(quadsenabled) disablequads();
+        if(glversion >= 300) glBindBuffer_(GL_ARRAY_BUFFER, 0);
+    }
+
+    void setup()
+    {
+        if(glversion >= 300)
+        {
+            if(!defaultvao) glGenVertexArrays_(1, &defaultvao);
+            glBindVertexArray_(defaultvao);
+        }
+        attribdata = new uchar[MAXVBOSIZE];
+        attribbuf.reset(attribdata, MAXVBOSIZE);
+    }
+
+    void cleanup()
+    {
+        disable();
+
+        if(quadindexes) { glDeleteBuffers_(1, &quadindexes); quadindexes = 0; }
+
+        if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; }
+        vbooffset = MAXVBOSIZE;
+
+        if(defaultvao) { glDeleteVertexArrays_(1, &defaultvao); defaultvao = 0; }
+    }
+}
+
diff --git a/src/shared/glemu.h b/src/shared/glemu.h
new file mode 100644 (file)
index 0000000..94af8f9
--- /dev/null
@@ -0,0 +1,180 @@
+namespace gle
+{
+    enum
+    {
+        ATTRIB_VERTEX       = 0,
+        ATTRIB_COLOR        = 1,
+        ATTRIB_TEXCOORD0    = 2,
+        ATTRIB_TEXCOORD1    = 3,
+        ATTRIB_NORMAL       = 4,
+        ATTRIB_TANGENT      = 5,
+        ATTRIB_BONEWEIGHT   = 6,
+        ATTRIB_BONEINDEX    = 7,
+        MAXATTRIBS          = 8
+    };
+
+    extern const char * const attribnames[MAXATTRIBS];
+    extern ucharbuf attribbuf;
+
+    extern int enabled;
+    extern void forcedisable();
+    static inline void disable() { if(enabled) forcedisable(); }
+
+    extern void begin(GLenum mode);
+    extern void begin(GLenum mode, int numverts);
+    extern void defattribs(const char *fmt);
+    extern void defattrib(int type, int size, int format);
+
+    #define GLE_DEFATTRIB(name, type, defaultsize, defaultformat) \
+        static inline void def##name(int size = defaultsize, int format = defaultformat) { defattrib(type, size, format); }
+
+    GLE_DEFATTRIB(vertex, ATTRIB_VERTEX, 3, GL_FLOAT)
+    GLE_DEFATTRIB(color, ATTRIB_COLOR, 3, GL_FLOAT)
+    GLE_DEFATTRIB(texcoord0, ATTRIB_TEXCOORD0, 2, GL_FLOAT)
+    GLE_DEFATTRIB(texcoord1, ATTRIB_TEXCOORD1, 2, GL_FLOAT)
+    GLE_DEFATTRIB(normal, ATTRIB_NORMAL, 3, GL_FLOAT)
+    GLE_DEFATTRIB(tangent, ATTRIB_TANGENT, 4, GL_FLOAT)
+    GLE_DEFATTRIB(boneweight, ATTRIB_BONEWEIGHT, 4, GL_UNSIGNED_BYTE)
+    GLE_DEFATTRIB(boneindex, ATTRIB_BONEINDEX, 4, GL_UNSIGNED_BYTE)
+
+    #define GLE_INITATTRIB(name, index, suffix, type) \
+        static inline void name##suffix(type x) { glVertexAttrib1##suffix##_(index, x); } \
+        static inline void name##suffix(type x, type y) { glVertexAttrib2##suffix##_(index, x, y); } \
+        static inline void name##suffix(type x, type y, type z) { glVertexAttrib3##suffix##_(index, x, y, z); } \
+        static inline void name##suffix(type x, type y, type z, type w) { glVertexAttrib4##suffix##_(index, x, y, z, w); }
+    #define GLE_INITATTRIBF(name, index) \
+        GLE_INITATTRIB(name, index, f, float) \
+        static inline void name(const vec &v) { glVertexAttrib3fv_(index, v.v); } \
+        static inline void name(const vec &v, float w) { glVertexAttrib4f_(index, v.x, v.y, v.z, w); } \
+        static inline void name(const vec2 &v) { glVertexAttrib2fv_(index, v.v); } \
+        static inline void name(const vec4 &v) { glVertexAttrib4fv_(index, v.v); }
+    #define GLE_INITATTRIBN(name, index, suffix, type, defaultw) \
+        static inline void name##suffix(type x, type y, type z, type w = defaultw) { glVertexAttrib4N##suffix##_(index, x, y, z, w); }
+
+    GLE_INITATTRIBF(vertex, ATTRIB_VERTEX)
+    GLE_INITATTRIBF(color, ATTRIB_COLOR)
+    static inline void color(const bvec4 &v) { glVertexAttrib4Nubv_(ATTRIB_COLOR, v.v); }
+    static inline void color(const bvec &v, uchar alpha = 255) { color(bvec4(v, alpha)); }
+    static inline void colorub(uchar x, uchar y, uchar z, uchar w = 255) { color(bvec4(x, y, z, w)); }
+    GLE_INITATTRIBF(texcoord0, ATTRIB_TEXCOORD0)
+    GLE_INITATTRIBF(texcoord1, ATTRIB_TEXCOORD1)
+    static inline void normal(float x, float y, float z) { glVertexAttrib4f_(ATTRIB_NORMAL, x, y, z, 0.0f); }
+    static inline void normal(const vec &v) { glVertexAttrib4f_(ATTRIB_NORMAL, v.x, v.y, v.z, 0.0f); }
+    static inline void tangent(float x, float y, float z, float w = 1.0f) { glVertexAttrib4f_(ATTRIB_TANGENT, x, y, z, w); }
+    static inline void tangent(const vec &v, float w = 1.0f) { glVertexAttrib4f_(ATTRIB_TANGENT, v.x, v.y, v.z, w); }
+    static inline void tangent(const vec4 &v) { glVertexAttrib4fv_(ATTRIB_TANGENT, v.v); }
+
+    #define GLE_ATTRIBPOINTER(name, index, defaultnormalized, defaultsize, defaulttype, prepare) \
+        static inline void enable##name() { prepare; glEnableVertexAttribArray_(index); } \
+        static inline void disable##name() { glDisableVertexAttribArray_(index); } \
+        static inline void name##pointer(int stride, const void *data, GLenum type = defaulttype, int size = defaultsize, GLenum normalized = defaultnormalized) { \
+            prepare; \
+            glVertexAttribPointer_(index, size, type, normalized, stride, data); \
+        }
+
+    static inline void enableattrib(int index) { disable(); glEnableVertexAttribArray_(index); }
+    static inline void disableattrib(int index) { glDisableVertexAttribArray_(index); }
+    GLE_ATTRIBPOINTER(vertex, ATTRIB_VERTEX, GL_FALSE, 3, GL_FLOAT, disable())
+    GLE_ATTRIBPOINTER(color, ATTRIB_COLOR, GL_TRUE, 4, GL_UNSIGNED_BYTE, )
+    GLE_ATTRIBPOINTER(texcoord0, ATTRIB_TEXCOORD0, GL_FALSE, 2, GL_FLOAT, )
+    GLE_ATTRIBPOINTER(texcoord1, ATTRIB_TEXCOORD1, GL_FALSE, 2, GL_FLOAT, )
+    GLE_ATTRIBPOINTER(normal, ATTRIB_NORMAL, GL_TRUE, 3, GL_FLOAT, )
+    GLE_ATTRIBPOINTER(tangent, ATTRIB_TANGENT, GL_TRUE, 4, GL_FLOAT, )
+    GLE_ATTRIBPOINTER(boneweight, ATTRIB_BONEWEIGHT, GL_TRUE, 4, GL_UNSIGNED_BYTE, )
+    GLE_ATTRIBPOINTER(boneindex, ATTRIB_BONEINDEX, GL_FALSE, 4, GL_UNSIGNED_BYTE, )
+
+    static inline void bindebo(GLuint ebo) { disable(); glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, ebo); }
+    static inline void clearebo() { glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, 0); }
+    static inline void bindvbo(GLuint vbo) { disable(); glBindBuffer_(GL_ARRAY_BUFFER, vbo); }
+    static inline void clearvbo() { glBindBuffer_(GL_ARRAY_BUFFER, 0); }
+
+    template<class T>
+    static inline void attrib(T x)
+    {
+        if(attribbuf.check(sizeof(T)))
+        {
+            T *buf = (T *)attribbuf.pad(sizeof(T));
+            buf[0] = x;
+        }
+    }
+
+    template<class T>
+    static inline void attrib(T x, T y)
+    {
+        if(attribbuf.check(2*sizeof(T)))
+        {
+            T *buf = (T *)attribbuf.pad(2*sizeof(T));
+            buf[0] = x;
+            buf[1] = y;
+        }
+    }
+
+    template<class T>
+    static inline void attrib(T x, T y, T z)
+    {
+        if(attribbuf.check(3*sizeof(T)))
+        {
+            T *buf = (T *)attribbuf.pad(3*sizeof(T));
+            buf[0] = x;
+            buf[1] = y;
+            buf[2] = z;
+        }
+    }
+
+    template<class T>
+    static inline void attrib(T x, T y, T z, T w)
+    {
+        if(attribbuf.check(4*sizeof(T)))
+        {
+            T *buf = (T *)attribbuf.pad(4*sizeof(T));
+            buf[0] = x;
+            buf[1] = y;
+            buf[2] = z;
+            buf[3] = w;
+        }
+    }
+
+    template<size_t N, class T>
+    static inline void attribv(const T *v)
+    {
+        attribbuf.put((const uchar *)v, N*sizeof(T));
+    }
+
+    #define GLE_ATTRIB(suffix, type) \
+        static inline void attrib##suffix(type x) { attrib<type>(x); } \
+        static inline void attrib##suffix(type x, type y) { attrib<type>(x, y); } \
+        static inline void attrib##suffix(type x, type y, type z) { attrib<type>(x, y, z); } \
+        static inline void attrib##suffix(type x, type y, type z, type w) { attrib<type>(x, y, z, w); }
+
+    GLE_ATTRIB(f, float)
+    GLE_ATTRIB(d, double)
+    GLE_ATTRIB(b, char)
+    GLE_ATTRIB(ub, uchar)
+    GLE_ATTRIB(s, short)
+    GLE_ATTRIB(us, ushort)
+    GLE_ATTRIB(i, int)
+    GLE_ATTRIB(ui, uint)
+
+    static inline void attrib(const vec &v) { attribf(v.x, v.y, v.z); }
+    static inline void attrib(const vec &v, float w) { attribf(v.x, v.y, v.z, w); }
+    static inline void attrib(const vec2 &v) { attribf(v.x, v.y); }
+    static inline void attrib(const vec4 &v) { attribf(v.x, v.y, v.z, v.w); }
+    static inline void attrib(const ivec &v) { attribi(v.x, v.y, v.z); }
+    static inline void attrib(const ivec &v, int w) { attribi(v.x, v.y, v.z, w); }
+    static inline void attrib(const ivec2 &v) { attribi(v.x, v.y); }
+    static inline void attrib(const ivec4 &v) { attribi(v.x, v.y, v.z, v.w); }
+    static inline void attrib(const bvec &b) { attribub(b.x, b.y, b.z); }
+    static inline void attrib(const bvec &b, uchar w) { attribub(b.x, b.y, b.z, w); }
+    static inline void attrib(const bvec4 &b) { attribub(b.x, b.y, b.z, b.w); }
+
+    extern void multidraw();
+    extern int end();
+
+    extern void enablequads();
+    extern void disablequads();
+    extern void drawquads(int offset, int count);
+
+    extern void setup();
+    extern void cleanup();
+}
+
diff --git a/src/shared/glexts.h b/src/shared/glexts.h
new file mode 100644 (file)
index 0000000..59509c1
--- /dev/null
@@ -0,0 +1,488 @@
+#ifndef APIENTRY
+#define APIENTRY
+#endif
+#ifndef APIENTRYP
+#define APIENTRYP APIENTRY *
+#endif
+
+// OpenGL deprecated functionality
+#ifndef GL_QUADS
+#define GL_QUADS                      0x0007
+#endif
+
+#ifndef GL_ALPHA
+#define GL_ALPHA                      0x1906
+#endif
+#ifndef GL_ALPHA8
+#define GL_ALPHA8                     0x803C
+#endif
+#ifndef GL_ALPHA16
+#define GL_ALPHA16                    0x803E
+#endif
+#ifndef GL_COMPRESSED_ALPHA
+#define GL_COMPRESSED_ALPHA           0x84E9
+#endif
+
+#ifndef GL_LUMINANCE
+#define GL_LUMINANCE                  0x1909
+#endif
+#ifndef GL_LUMINANCE8
+#define GL_LUMINANCE8                 0x8040
+#endif
+#ifndef GL_LUMINANCE16
+#define GL_LUMINANCE16                0x8042
+#endif
+#ifndef GL_COMPRESSED_LUMINANCE
+#define GL_COMPRESSED_LUMINANCE       0x84EA
+#endif
+
+#ifndef GL_LUMINANCE_ALPHA
+#define GL_LUMINANCE_ALPHA            0x190A
+#endif
+#ifndef GL_LUMINANCE8_ALPHA8
+#define GL_LUMINANCE8_ALPHA8          0x8045
+#endif
+#ifndef GL_LUMINANCE16_ALPHA16
+#define GL_LUMINANCE16_ALPHA16        0x8048
+#endif
+#ifndef GL_COMPRESSED_LUMINANCE_ALPHA
+#define GL_COMPRESSED_LUMINANCE_ALPHA 0x84EB
+#endif
+
+// OpenGL 1.3
+#ifndef WIN32
+#define glActiveTexture_ glActiveTexture
+
+#define glBlendEquation_ glBlendEquation
+#define glBlendColor_ glBlendColor
+
+#define glTexImage3D_ glTexImage3D
+#define glTexSubImage3D_ glTexSubImage3D
+#define glCopyTexSubImage3D_ glCopyTexSubImage3D
+
+#define glCompressedTexImage3D_ glCompressedTexImage3D
+#define glCompressedTexImage2D_ glCompressedTexImage2D
+#define glCompressedTexSubImage3D_ glCompressedTexSubImage3D
+#define glCompressedTexSubImage2D_ glCompressedTexSubImage2D
+#define glGetCompressedTexImage_ glGetCompressedTexImage
+
+#define glDrawRangeElements_ glDrawRangeElements
+#else
+extern PFNGLACTIVETEXTUREPROC       glActiveTexture_;
+
+extern PFNGLBLENDEQUATIONPROC glBlendEquation_;
+extern PFNGLBLENDCOLORPROC    glBlendColor_;
+
+extern PFNGLTEXIMAGE3DPROC        glTexImage3D_;
+extern PFNGLTEXSUBIMAGE3DPROC     glTexSubImage3D_;
+extern PFNGLCOPYTEXSUBIMAGE3DPROC glCopyTexSubImage3D_;
+
+extern PFNGLCOMPRESSEDTEXIMAGE3DPROC    glCompressedTexImage3D_;
+extern PFNGLCOMPRESSEDTEXIMAGE2DPROC    glCompressedTexImage2D_;
+extern PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC glCompressedTexSubImage3D_;
+extern PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC glCompressedTexSubImage2D_;
+extern PFNGLGETCOMPRESSEDTEXIMAGEPROC   glGetCompressedTexImage_;
+
+extern PFNGLDRAWRANGEELEMENTSPROC glDrawRangeElements_;
+#endif
+
+// OpenGL 2.0
+#ifdef __APPLE__
+#define glMultiDrawArrays_ glMultiDrawArrays
+#define glMultiDrawElements_ glMultiDrawElements
+
+#define glBlendFuncSeparate_ glBlendFuncSeparate
+#define glBlendEquationSeparate_ glBlendEquationSeparate
+#define glStencilOpSeparate_ glStencilOpSeparate
+#define glStencilFuncSeparate_ glStencilFuncSeparate
+#define glStencilMaskSeparate_ glStencilMaskSeparate
+
+#define glGenBuffers_ glGenBuffers
+#define glBindBuffer_ glBindBuffer
+#define glMapBuffer_ glMapBuffer
+#define glUnmapBuffer_ glUnmapBuffer
+#define glBufferData_ glBufferData
+#define glBufferSubData_ glBufferSubData
+#define glDeleteBuffers_ glDeleteBuffers
+#define glGetBufferSubData_ glGetBufferSubData
+
+#define glGenQueries_ glGenQueries
+#define glDeleteQueries_ glDeleteQueries
+#define glBeginQuery_ glBeginQuery
+#define glEndQuery_ glEndQuery
+#define glGetQueryiv_ glGetQueryiv
+#define glGetQueryObjectiv_ glGetQueryObjectiv
+#define glGetQueryObjectuiv_ glGetQueryObjectuiv
+
+#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 glUniform2i_ glUniform2i
+#define glUniform3i_ glUniform3i
+#define glUniform4i_ glUniform4i
+#define glUniform1iv_ glUniform1iv
+#define glUniform2iv_ glUniform2iv
+#define glUniform3iv_ glUniform3iv
+#define glUniform4iv_ glUniform4iv
+#define glUniformMatrix2fv_ glUniformMatrix2fv
+#define glUniformMatrix3fv_ glUniformMatrix3fv
+#define glUniformMatrix4fv_ glUniformMatrix4fv
+#define glBindAttribLocation_ glBindAttribLocation
+#define glGetActiveUniform_ glGetActiveUniform
+#define glEnableVertexAttribArray_ glEnableVertexAttribArray
+#define glDisableVertexAttribArray_ glDisableVertexAttribArray
+
+#define glVertexAttrib1f_ glVertexAttrib1f
+#define glVertexAttrib1fv_ glVertexAttrib1fv
+#define glVertexAttrib1s_ glVertexAttrib1s
+#define glVertexAttrib1sv_ glVertexAttrib1sv
+#define glVertexAttrib2f_ glVertexAttrib2f
+#define glVertexAttrib2fv_ glVertexAttrib2fv
+#define glVertexAttrib2s_ glVertexAttrib2s
+#define glVertexAttrib2sv_ glVertexAttrib2sv
+#define glVertexAttrib3f_ glVertexAttrib3f
+#define glVertexAttrib3fv_ glVertexAttrib3fv
+#define glVertexAttrib3s_ glVertexAttrib3s
+#define glVertexAttrib3sv_ glVertexAttrib3sv
+#define glVertexAttrib4f_ glVertexAttrib4f
+#define glVertexAttrib4fv_ glVertexAttrib4fv
+#define glVertexAttrib4s_ glVertexAttrib4s
+#define glVertexAttrib4sv_ glVertexAttrib4sv
+#define glVertexAttrib4bv_ glVertexAttrib4bv
+#define glVertexAttrib4iv_ glVertexAttrib4iv
+#define glVertexAttrib4ubv_ glVertexAttrib4ubv
+#define glVertexAttrib4uiv_ glVertexAttrib4uiv
+#define glVertexAttrib4usv_ glVertexAttrib4usv
+#define glVertexAttrib4Nbv_ glVertexAttrib4Nbv
+#define glVertexAttrib4Niv_ glVertexAttrib4Niv
+#define glVertexAttrib4Nub_ glVertexAttrib4Nub
+#define glVertexAttrib4Nubv_ glVertexAttrib4Nubv
+#define glVertexAttrib4Nuiv_ glVertexAttrib4Nuiv
+#define glVertexAttrib4Nusv_ glVertexAttrib4Nusv
+#define glVertexAttribPointer_ glVertexAttribPointer
+
+#define glDrawBuffers_ glDrawBuffers
+#else
+extern PFNGLMULTIDRAWARRAYSPROC   glMultiDrawArrays_;
+extern PFNGLMULTIDRAWELEMENTSPROC glMultiDrawElements_;
+
+extern PFNGLBLENDFUNCSEPARATEPROC glBlendFuncSeparate_;
+extern PFNGLBLENDEQUATIONSEPARATEPROC glBlendEquationSeparate_;
+extern PFNGLSTENCILOPSEPARATEPROC glStencilOpSeparate_;
+extern PFNGLSTENCILFUNCSEPARATEPROC glStencilFuncSeparate_;
+extern PFNGLSTENCILMASKSEPARATEPROC glStencilMaskSeparate_;
+
+extern PFNGLGENBUFFERSPROC       glGenBuffers_;
+extern PFNGLBINDBUFFERPROC       glBindBuffer_;
+extern PFNGLMAPBUFFERPROC        glMapBuffer_;
+extern PFNGLUNMAPBUFFERPROC      glUnmapBuffer_;
+extern PFNGLBUFFERDATAPROC       glBufferData_;
+extern PFNGLBUFFERSUBDATAPROC    glBufferSubData_;
+extern PFNGLDELETEBUFFERSPROC    glDeleteBuffers_;
+extern PFNGLGETBUFFERSUBDATAPROC glGetBufferSubData_;
+
+extern PFNGLGENQUERIESPROC        glGenQueries_;
+extern PFNGLDELETEQUERIESPROC     glDeleteQueries_;
+extern PFNGLBEGINQUERYPROC        glBeginQuery_;
+extern PFNGLENDQUERYPROC          glEndQuery_;
+extern PFNGLGETQUERYIVPROC        glGetQueryiv_;
+extern PFNGLGETQUERYOBJECTIVPROC  glGetQueryObjectiv_;
+extern PFNGLGETQUERYOBJECTUIVPROC glGetQueryObjectuiv_;
+
+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 PFNGLUNIFORM2IPROC                glUniform2i_;
+extern PFNGLUNIFORM3IPROC                glUniform3i_;
+extern PFNGLUNIFORM4IPROC                glUniform4i_;
+extern PFNGLUNIFORM1IVPROC               glUniform1iv_;
+extern PFNGLUNIFORM2IVPROC               glUniform2iv_;
+extern PFNGLUNIFORM3IVPROC               glUniform3iv_;
+extern PFNGLUNIFORM4IVPROC               glUniform4iv_;
+extern PFNGLUNIFORMMATRIX2FVPROC         glUniformMatrix2fv_;
+extern PFNGLUNIFORMMATRIX3FVPROC         glUniformMatrix3fv_;
+extern PFNGLUNIFORMMATRIX4FVPROC         glUniformMatrix4fv_;
+extern PFNGLBINDATTRIBLOCATIONPROC       glBindAttribLocation_;
+extern PFNGLGETACTIVEUNIFORMPROC         glGetActiveUniform_;
+extern PFNGLENABLEVERTEXATTRIBARRAYPROC  glEnableVertexAttribArray_;
+extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray_;
+
+extern PFNGLVERTEXATTRIB1FPROC           glVertexAttrib1f_;
+extern PFNGLVERTEXATTRIB1FVPROC          glVertexAttrib1fv_;
+extern PFNGLVERTEXATTRIB1SPROC           glVertexAttrib1s_;
+extern PFNGLVERTEXATTRIB1SVPROC          glVertexAttrib1sv_;
+extern PFNGLVERTEXATTRIB2FPROC           glVertexAttrib2f_;
+extern PFNGLVERTEXATTRIB2FVPROC          glVertexAttrib2fv_;
+extern PFNGLVERTEXATTRIB2SPROC           glVertexAttrib2s_;
+extern PFNGLVERTEXATTRIB2SVPROC          glVertexAttrib2sv_;
+extern PFNGLVERTEXATTRIB3FPROC           glVertexAttrib3f_;
+extern PFNGLVERTEXATTRIB3FVPROC          glVertexAttrib3fv_;
+extern PFNGLVERTEXATTRIB3SPROC           glVertexAttrib3s_;
+extern PFNGLVERTEXATTRIB3SVPROC          glVertexAttrib3sv_;
+extern PFNGLVERTEXATTRIB4FPROC           glVertexAttrib4f_;
+extern PFNGLVERTEXATTRIB4FVPROC          glVertexAttrib4fv_;
+extern PFNGLVERTEXATTRIB4SPROC           glVertexAttrib4s_;
+extern PFNGLVERTEXATTRIB4SVPROC          glVertexAttrib4sv_;
+extern PFNGLVERTEXATTRIB4BVPROC          glVertexAttrib4bv_;
+extern PFNGLVERTEXATTRIB4IVPROC          glVertexAttrib4iv_;
+extern PFNGLVERTEXATTRIB4UBVPROC         glVertexAttrib4ubv_;
+extern PFNGLVERTEXATTRIB4UIVPROC         glVertexAttrib4uiv_;
+extern PFNGLVERTEXATTRIB4USVPROC         glVertexAttrib4usv_;
+extern PFNGLVERTEXATTRIB4NBVPROC         glVertexAttrib4Nbv_;
+extern PFNGLVERTEXATTRIB4NIVPROC         glVertexAttrib4Niv_;
+extern PFNGLVERTEXATTRIB4NUBPROC         glVertexAttrib4Nub_;
+extern PFNGLVERTEXATTRIB4NUBVPROC        glVertexAttrib4Nubv_;
+extern PFNGLVERTEXATTRIB4NUIVPROC        glVertexAttrib4Nuiv_;
+extern PFNGLVERTEXATTRIB4NUSVPROC        glVertexAttrib4Nusv_;
+extern PFNGLVERTEXATTRIBPOINTERPROC      glVertexAttribPointer_;
+
+extern PFNGLDRAWBUFFERSPROC glDrawBuffers_;
+#endif
+
+#ifndef GL_VERSION_2_1
+#define GL_VERSION_2_1 1
+#define GL_PIXEL_PACK_BUFFER              0x88EB
+#define GL_PIXEL_UNPACK_BUFFER            0x88EC
+#endif
+
+#ifndef GL_EXT_texture_filter_anisotropic
+#define GL_EXT_texture_filter_anisotropic 1
+#define GL_TEXTURE_MAX_ANISOTROPY_EXT     0x84FE
+#define GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT 0x84FF
+#endif
+
+#ifndef GL_EXT_texture_compression_s3tc
+#define GL_EXT_texture_compression_s3tc 1
+#define GL_COMPRESSED_RGB_S3TC_DXT1_EXT   0x83F0
+#define GL_COMPRESSED_RGBA_S3TC_DXT1_EXT  0x83F1
+#define GL_COMPRESSED_RGBA_S3TC_DXT3_EXT  0x83F2
+#define GL_COMPRESSED_RGBA_S3TC_DXT5_EXT  0x83F3
+#endif
+
+#ifndef GL_ARB_framebuffer_object
+#define GL_ARB_framebuffer_object 1
+#define GL_DEPTH_STENCIL_ATTACHMENT       0x821A
+#define GL_DEPTH_STENCIL                  0x84F9
+#define GL_UNSIGNED_INT_24_8              0x84FA
+#define GL_DEPTH24_STENCIL8               0x88F0
+#define GL_FRAMEBUFFER_BINDING            0x8CA6
+#define GL_READ_FRAMEBUFFER               0x8CA8
+#define GL_DRAW_FRAMEBUFFER               0x8CA9
+#define GL_FRAMEBUFFER_COMPLETE           0x8CD5
+#define GL_COLOR_ATTACHMENT0              0x8CE0
+#define GL_COLOR_ATTACHMENT1              0x8CE1
+#define GL_DEPTH_ATTACHMENT               0x8D00
+#define GL_STENCIL_ATTACHMENT             0x8D20
+#define GL_FRAMEBUFFER                    0x8D40
+#define GL_RENDERBUFFER                   0x8D41
+typedef void (APIENTRYP PFNGLBINDRENDERBUFFERPROC) (GLenum target, GLuint renderbuffer);
+typedef void (APIENTRYP PFNGLDELETERENDERBUFFERSPROC) (GLsizei n, const GLuint *renderbuffers);
+typedef void (APIENTRYP PFNGLGENRENDERBUFFERSPROC) (GLsizei n, GLuint *renderbuffers);
+typedef void (APIENTRYP PFNGLRENDERBUFFERSTORAGEPROC) (GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
+typedef void (APIENTRYP PFNGLBINDFRAMEBUFFERPROC) (GLenum target, GLuint framebuffer);
+typedef void (APIENTRYP PFNGLDELETEFRAMEBUFFERSPROC) (GLsizei n, const GLuint *framebuffers);
+typedef void (APIENTRYP PFNGLGENFRAMEBUFFERSPROC) (GLsizei n, GLuint *framebuffers);
+typedef GLenum (APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSPROC) (GLenum target);
+typedef void (APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DPROC) (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
+typedef void (APIENTRYP PFNGLFRAMEBUFFERRENDERBUFFERPROC) (GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
+typedef void (APIENTRYP PFNGLGENERATEMIPMAPPROC) (GLenum target);
+typedef void (APIENTRYP PFNGLBLITFRAMEBUFFERPROC) (GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+#endif
+
+// GL_EXT_framebuffer_object
+extern PFNGLBINDRENDERBUFFERPROC        glBindRenderbuffer_;
+extern PFNGLDELETERENDERBUFFERSPROC     glDeleteRenderbuffers_;
+extern PFNGLGENFRAMEBUFFERSPROC         glGenRenderbuffers_;
+extern PFNGLRENDERBUFFERSTORAGEPROC     glRenderbufferStorage_;
+extern PFNGLCHECKFRAMEBUFFERSTATUSPROC  glCheckFramebufferStatus_;
+extern PFNGLBINDFRAMEBUFFERPROC         glBindFramebuffer_;
+extern PFNGLDELETEFRAMEBUFFERSPROC      glDeleteFramebuffers_;
+extern PFNGLGENFRAMEBUFFERSPROC         glGenFramebuffers_;
+extern PFNGLFRAMEBUFFERTEXTURE2DPROC    glFramebufferTexture2D_;
+extern PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer_;
+extern PFNGLGENERATEMIPMAPPROC          glGenerateMipmap_;
+
+// GL_EXT_framebuffer_blit
+extern PFNGLBLITFRAMEBUFFERPROC         glBlitFramebuffer_;
+
+#ifndef GL_ARB_texture_rg
+#define GL_ARB_texture_rg 1
+#define GL_RG                             0x8227
+#define GL_R8                             0x8229
+#define GL_R16                            0x822A
+#define GL_RG8                            0x822B
+#define GL_RG16                           0x822C
+#define GL_R16F                           0x822D
+#define GL_R32F                           0x822E
+#define GL_RG16F                          0x822F
+#define GL_RG32F                          0x8230
+#endif
+
+#ifndef GL_EXT_texture_compression_latc
+#define GL_EXT_texture_compression_latc 1
+#define GL_COMPRESSED_LUMINANCE_LATC1_EXT              0x8C70
+#define GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT        0x8C72
+#endif
+
+#ifndef GL_ARB_texture_compression_rgtc
+#define GL_ARB_texture_compression_rgtc 1
+#define GL_COMPRESSED_RED_RGTC1           0x8DBB
+#define GL_COMPRESSED_RG_RGTC2            0x8DBD
+#endif
+
+#ifndef GL_ARB_map_buffer_range
+#define GL_ARB_map_buffer_range 1
+#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_;
+
+#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
+extern PFNGLGETUNIFORMINDICESPROC       glGetUniformIndices_;
+extern PFNGLGETACTIVEUNIFORMSIVPROC     glGetActiveUniformsiv_;
+extern PFNGLGETUNIFORMBLOCKINDEXPROC    glGetUniformBlockIndex_;
+extern PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv_;
+extern PFNGLUNIFORMBLOCKBINDINGPROC     glUniformBlockBinding_;
+
+#ifndef GL_VERSION_3_0
+#define GL_VERSION_3_0 1
+#define GL_NUM_EXTENSIONS                 0x821D
+#define GL_COMPARE_REF_TO_TEXTURE         0x884E
+#define GL_MAX_VARYING_COMPONENTS         0x8B4B
+#define GL_RGBA32F                        0x8814
+#define GL_RGB32F                         0x8815
+#define GL_RGBA16F                        0x881A
+#define GL_RGB16F                         0x881B
+#define GL_COMPRESSED_RED                 0x8225
+#define GL_COMPRESSED_RG                  0x8226
+typedef void (APIENTRYP PFNGLBINDBUFFERRANGEPROC) (GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
+typedef void (APIENTRYP PFNGLBINDBUFFERBASEPROC) (GLenum target, GLuint index, GLuint buffer);
+typedef void (APIENTRYP PFNGLBINDFRAGDATALOCATIONPROC) (GLuint program, GLuint color, const GLchar *name);
+typedef const GLubyte * (APIENTRYP PFNGLGETSTRINGIPROC) (GLenum name, GLuint index);
+#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
+
+extern PFNGLGETSTRINGIPROC           glGetStringi_;
+extern PFNGLBINDFRAGDATALOCATIONPROC glBindFragDataLocation_;
+extern PFNGLBINDBUFFERBASEPROC       glBindBufferBase_;
+extern PFNGLBINDBUFFERRANGEPROC      glBindBufferRange_;
+
+#ifndef GL_VERSION_3_1
+#define GL_VERSION_3_1 1
+#define GL_TEXTURE_RECTANGLE              0x84F5
+#endif
+
+#ifndef GL_ARB_vertex_array_object
+#define GL_ARB_vertex_array_object 1
+#define GL_VERTEX_ARRAY_BINDING           0x85B5
+typedef void (APIENTRYP PFNGLBINDVERTEXARRAYPROC) (GLuint array);
+typedef void (APIENTRYP PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *arrays);
+typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays);
+typedef GLboolean (APIENTRYP PFNGLISVERTEXARRAYPROC) (GLuint array);
+#endif
+extern PFNGLBINDVERTEXARRAYPROC    glBindVertexArray_;
+extern PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays_;
+extern PFNGLGENVERTEXARRAYSPROC    glGenVertexArrays_;
+extern PFNGLISVERTEXARRAYPROC      glIsVertexArray_;
+
+#ifndef GL_ARB_texture_swizzle
+#define GL_ARB_texture_swizzle 1
+#define GL_TEXTURE_SWIZZLE_R              0x8E42
+#define GL_TEXTURE_SWIZZLE_G              0x8E43
+#define GL_TEXTURE_SWIZZLE_B              0x8E44
+#define GL_TEXTURE_SWIZZLE_A              0x8E45
+#define GL_TEXTURE_SWIZZLE_RGBA           0x8E46
+#endif
+
diff --git a/src/shared/iengine.h b/src/shared/iengine.h
new file mode 100644 (file)
index 0000000..32b1957
--- /dev/null
@@ -0,0 +1,583 @@
+// the interface the game uses to access the engine
+
+extern int curtime;                     // current frame time
+extern int lastmillis;                  // last time
+extern int elapsedtime;                 // elapsed frame time
+extern int totalmillis;                 // total elapsed time
+extern uint totalsecs;
+extern int gamespeed, paused;
+
+enum
+{
+    MATF_INDEX_SHIFT  = 0,
+    MATF_VOLUME_SHIFT = 2,
+    MATF_CLIP_SHIFT   = 5,
+    MATF_FLAG_SHIFT   = 8,
+
+    MATF_INDEX  = 3 << MATF_INDEX_SHIFT,
+    MATF_VOLUME = 7 << MATF_VOLUME_SHIFT,
+    MATF_CLIP   = 7 << MATF_CLIP_SHIFT,
+    MATF_FLAGS  = 0xFF << MATF_FLAG_SHIFT
+};
+
+enum // cube empty-space materials
+{
+    MAT_AIR      = 0,                      // the default, fill the empty space with air
+    MAT_WATER    = 1 << MATF_VOLUME_SHIFT, // fill with water, showing waves at the surface
+    MAT_LAVA     = 2 << MATF_VOLUME_SHIFT, // fill with lava
+    MAT_GLASS    = 3 << MATF_VOLUME_SHIFT, // behaves like clip but is blended blueish
+
+    MAT_NOCLIP   = 1 << MATF_CLIP_SHIFT,  // collisions always treat cube as empty
+    MAT_CLIP     = 2 << MATF_CLIP_SHIFT,  // collisions always treat cube as solid
+    MAT_GAMECLIP = 3 << MATF_CLIP_SHIFT,  // game specific clip material
+
+    MAT_DEATH    = 1 << MATF_FLAG_SHIFT,  // force player suicide
+    MAT_ALPHA    = 4 << MATF_FLAG_SHIFT   // alpha blended
+};
+
+#define isliquid(mat) ((mat)==MAT_WATER || (mat)==MAT_LAVA)
+#define isclipped(mat) ((mat)==MAT_GLASS)
+#define isdeadly(mat) ((mat)==MAT_LAVA)
+
+extern void lightent(extentity &e, float height = 8.0f);
+extern void lightreaching(const vec &target, vec &color, vec &dir, bool fast = false, extentity *e = 0, float ambient = 0.4f);
+extern entity *brightestlight(const vec &target, const vec &dir);
+
+enum { RAY_BB = 1, RAY_POLY = 3, RAY_ALPHAPOLY = 7, RAY_ENTS = 9, RAY_CLIPMAT = 16, RAY_SKIPFIRST = 32, RAY_EDITMAT = 64, RAY_SHADOW = 128, RAY_PASS = 256, RAY_SKIPSKY = 512, RAY_SKYTEX = 1024 };
+
+extern float raycube   (const vec &o, const vec &ray,     float radius = 0, int mode = RAY_CLIPMAT, int size = 0, extentity *t = 0);
+extern float raycubepos(const vec &o, const vec &ray, vec &hit, float radius = 0, int mode = RAY_CLIPMAT, int size = 0);
+extern float rayfloor  (const vec &o, vec &floor, int mode = 0, float radius = 0);
+extern bool  raycubelos(const vec &o, const vec &dest, vec &hitpos);
+
+extern int thirdperson;
+extern bool isthirdperson();
+
+extern bool settexture(const char *name, int clamp = 0);
+
+// octaedit
+
+enum { EDIT_FACE = 0, EDIT_TEX, EDIT_MAT, EDIT_FLIP, EDIT_COPY, EDIT_PASTE, EDIT_ROTATE, EDIT_REPLACE, EDIT_DELCUBE, EDIT_REMIP, EDIT_VSLOT, EDIT_UNDO, EDIT_REDO };
+
+struct selinfo
+{
+    int corner;
+    int cx, cxs, cy, cys;
+    ivec o, s;
+    int grid, orient;
+    selinfo() : corner(0), cx(0), cxs(0), cy(0), cys(0), o(0, 0, 0), s(0, 0, 0), grid(8), orient(0) {}
+    int size() const    { return s.x*s.y*s.z; }
+    int us(int d) const { return s[d]*grid; }
+    bool operator==(const selinfo &sel) const { return o==sel.o && s==sel.s && grid==sel.grid && orient==sel.orient; }
+    bool validate()
+    {
+        extern int worldsize;
+        if(grid <= 0 || grid >= worldsize) return false;
+        if(o.x >= worldsize || o.y >= worldsize || o.z >= worldsize) return false;
+        if(o.x < 0) { s.x -= (grid - 1 - o.x)/grid; o.x = 0; } 
+        if(o.y < 0) { s.y -= (grid - 1 - o.y)/grid; o.y = 0; } 
+        if(o.z < 0) { s.z -= (grid - 1 - o.z)/grid; o.z = 0; } 
+        s.x = clamp(s.x, 0, (worldsize - o.x)/grid);
+        s.y = clamp(s.y, 0, (worldsize - o.y)/grid);
+        s.z = clamp(s.z, 0, (worldsize - o.z)/grid);
+        return s.x > 0 && s.y > 0 && s.z > 0;
+    }
+};
+
+struct editinfo;
+extern editinfo *localedit;
+
+extern bool editmode;
+
+extern int shouldpacktex(int index);
+extern bool packeditinfo(editinfo *e, int &inlen, uchar *&outbuf, int &outlen);
+extern bool unpackeditinfo(editinfo *&e, const uchar *inbuf, int inlen, int outlen);
+extern void freeeditinfo(editinfo *&e);
+extern void pruneundos(int maxremain = 0);
+extern bool packundo(int op, int &inlen, uchar *&outbuf, int &outlen);
+extern bool unpackundo(const uchar *inbuf, int inlen, int outlen);
+extern bool noedit(bool view = false, bool msg = true);
+extern void toggleedit(bool force = true);
+extern void mpeditface(int dir, int mode, selinfo &sel, bool local);
+extern void mpedittex(int tex, int allfaces, selinfo &sel, bool local);
+extern bool mpedittex(int tex, int allfaces, selinfo &sel, ucharbuf &buf);
+extern void mpeditmat(int matid, int filter, selinfo &sel, bool local);
+extern void mpflip(selinfo &sel, bool local);
+extern void mpcopy(editinfo *&e, selinfo &sel, bool local);
+extern void mppaste(editinfo *&e, selinfo &sel, bool local);
+extern void mprotate(int cw, selinfo &sel, bool local);
+extern void mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, bool local);
+extern bool mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, ucharbuf &buf);
+extern void mpdelcube(selinfo &sel, bool local);
+extern bool mpeditvslot(int delta, int allfaces, selinfo &sel, ucharbuf &buf);
+extern void mpremip(bool local);
+
+// texture
+
+struct VSlot;
+
+extern void packvslot(vector<uchar> &buf, int index);
+extern void packvslot(vector<uchar> &buf, const VSlot *vs);
+
+// command
+extern int variable(const char *name, int min, int cur, int max, int *storage, identfun fun, int flags);
+extern float fvariable(const char *name, float min, float cur, float max, float *storage, identfun fun, int flags);
+extern char *svariable(const char *name, const char *cur, char **storage, identfun fun, int flags);
+extern void setvar(const char *name, int i, bool dofunc = true, bool doclamp = true);
+extern void setfvar(const char *name, float f, bool dofunc = true, bool doclamp = true);
+extern void setsvar(const char *name, const char *str, bool dofunc = true);
+extern void setvarchecked(ident *id, int val);
+extern void setfvarchecked(ident *id, float val);
+extern void setsvarchecked(ident *id, const char *val);
+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 bool identexists(const char *name);
+extern ident *getident(const char *name);
+extern ident *newident(const char *name, int flags = 0);
+extern ident *readident(const char *name);
+extern ident *writeident(const char *name, int flags = 0);
+extern bool addcommand(const char *name, identfun fun, const char *narg);
+extern bool addkeyword(int type, const char *name);
+extern uint *compilecode(const char *p);
+extern void keepcode(uint *p);
+extern void freecode(uint *p);
+extern void executeret(const uint *code, tagval &result = *commandret);
+extern void executeret(const char *p, tagval &result = *commandret);
+extern void executeret(ident *id, tagval *args, int numargs, bool lookup = false, tagval &result = *commandret);
+extern char *executestr(const uint *code);
+extern char *executestr(const char *p);
+extern char *executestr(ident *id, tagval *args, int numargs, bool lookup = false);
+extern char *execidentstr(const char *name, bool lookup = false);
+extern int execute(const uint *code);
+extern int execute(const char *p);
+extern int execute(ident *id, tagval *args, int numargs, bool lookup = false);
+extern int execident(const char *name, int noid = 0, bool lookup = false);
+extern bool executebool(const uint *code);
+extern bool executebool(const char *p);
+extern bool executebool(ident *id, tagval *args, int numargs, bool lookup = false);
+extern bool execidentbool(const char *name, bool noid = false, bool lookup = false);
+extern bool execfile(const char *cfgfile, bool msg = true);
+extern void alias(const char *name, const char *action);
+extern void alias(const char *name, tagval &v);
+extern const char *getalias(const char *name);
+extern const char *escapestring(const char *s);
+extern const char *escapeid(const char *s);
+static inline const char *escapeid(ident &id) { return escapeid(id.name); }
+extern bool validateblock(const char *s);
+extern void explodelist(const char *s, vector<char *> &elems, int limit = -1);
+extern char *indexlist(const char *s, int pos);
+extern int listlen(const char *s);
+extern void printvar(ident *id);
+extern void printvar(ident *id, int i);
+extern void printfvar(ident *id, float f);
+extern void printsvar(ident *id, const char *s);
+extern int clampvar(ident *id, int i, int minval, int maxval);
+extern float clampfvar(ident *id, float f, float minval, float maxval);
+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); }
+
+// console
+
+enum
+{
+    CON_INFO  = 1<<0,
+    CON_WARN  = 1<<1,
+    CON_ERROR = 1<<2,
+    CON_DEBUG = 1<<3,
+    CON_INIT  = 1<<4,
+    CON_ECHO  = 1<<5,
+
+    CON_FLAGS = 0xFFFF,
+    CON_TAG_SHIFT = 16,
+    CON_TAG_MASK = (0x7FFF << CON_TAG_SHIFT)
+};
+
+extern void conoutf(const char *s, ...) PRINTFARGS(1, 2);
+extern void conoutf(int type, const char *s, ...) PRINTFARGS(2, 3);
+extern void conoutf(int type, int tag, const char *s, ...) PRINTFARGS(3, 4);
+extern void conoutfv(int type, const char *fmt, va_list args);
+
+extern FILE *getlogfile();
+extern void setlogfile(const char *fname);
+extern void closelogfile();
+extern void logoutfv(const char *fmt, va_list args);
+extern void logoutf(const char *fmt, ...) PRINTFARGS(1, 2);
+
+// menus
+extern vec menuinfrontofplayer();
+extern void newgui(char *name, char *contents, char *header = NULL, char *init = NULL);
+extern void showgui(const char *name);
+extern int cleargui(int n = 0);
+
+// octa
+extern int lookupmaterial(const vec &o);
+
+static inline bool insideworld(const vec &o)
+{
+       extern int worldsize;
+    return o.x>=0 && o.x<worldsize && o.y>=0 && o.y<worldsize && o.z>=0 && o.z<worldsize;
+}
+
+static inline bool insideworld(const ivec &o)
+{
+       extern int worldsize;
+    return uint(o.x)<uint(worldsize) && uint(o.y)<uint(worldsize) && uint(o.z)<uint(worldsize);
+}
+
+// world
+extern bool emptymap(int factor, bool force, const char *mname = "", bool usecfg = true);
+extern bool enlargemap(bool force);
+extern int findentity(int type, int index = 0, int attr1 = -1, int attr2 = -1);
+extern void findents(int low, int high, bool notspawned, const vec &pos, const vec &radius, vector<int> &found);
+extern void mpeditent(int i, const vec &o, int type, int attr1, int attr2, int attr3, int attr4, int attr5, bool local);
+extern vec getselpos();
+extern int getworldsize();
+extern int getmapversion();
+extern void renderentcone(const extentity &e, const vec &dir, float radius, float angle);
+extern void renderentarrow(const extentity &e, const vec &dir, float radius);
+extern void renderentattachment(const extentity &e);
+extern void renderentsphere(const extentity &e, float radius);
+extern void renderentring(const extentity &e, float radius, int axis = 0);
+
+// main
+extern void fatal(const char *s, ...) PRINTFARGS(1, 2);
+
+// rendertext
+extern bool setfont(const char *name);
+extern void pushfont();
+extern bool popfont();
+extern void gettextres(int &w, int &h);
+extern void draw_text(const char *str, int left, int top, int r = 255, int g = 255, int b = 255, int a = 255, int cursor = -1, int maxwidth = -1);
+extern void draw_textf(const char *fstr, int left, int top, ...) PRINTFARGS(1, 4);
+extern float text_widthf(const char *str);
+extern void text_boundsf(const char *str, float &width, float &height, int maxwidth = -1);
+extern int text_visible(const char *str, float hitx, float hity, int maxwidth);
+extern void text_posf(const char *str, int cursor, float &cx, float &cy, int maxwidth);
+
+static inline int text_width(const char *str)
+{
+    return int(ceil(text_widthf(str)));
+}
+
+static inline void text_bounds(const char *str, int &width, int &height, int maxwidth = -1)
+{
+    float widthf, heightf;
+    text_boundsf(str, widthf, heightf, maxwidth);
+    width = int(ceil(widthf));
+    height = int(ceil(heightf));
+}
+
+static inline void text_pos(const char *str, int cursor, int &cx, int &cy, int maxwidth)
+{
+    float cxf, cyf;
+    text_posf(str, cursor, cxf, cyf, maxwidth);
+    cx = int(cxf);
+    cy = int(cyf);
+}
+
+// renderva
+enum
+{
+    DL_SHRINK = 1<<0,
+    DL_EXPAND = 1<<1,
+    DL_FLASH  = 1<<2
+};
+
+extern void adddynlight(const vec &o, float radius, const vec &color, int fade = 0, int peak = 0, int flags = 0, float initradius = 0, const vec &initcolor = vec(0, 0, 0), physent *owner = NULL);
+extern void dynlightreaching(const vec &target, vec &color, vec &dir, bool hud = false);
+extern void removetrackeddynlights(physent *owner = NULL);
+
+// rendergl
+extern physent *camera1;
+extern vec worldpos, camdir, camright, camup;
+
+extern void disablezoom();
+
+extern vec calcavatarpos(const vec &pos, float dist);
+extern vec calcmodelpreviewpos(const vec &radius, float &yaw);
+
+extern void damageblend(int n);
+extern void damagecompass(int n, const vec &loc);
+extern void cleardamagescreen();
+
+extern vec minimapcenter, minimapradius, minimapscale;
+extern void bindminimap();
+
+extern matrix4 hudmatrix;
+extern void resethudmatrix();
+extern void pushhudmatrix();
+extern void flushhudmatrix(bool flushparams = true);
+extern void pophudmatrix(bool flush = true, bool flushparams = true);
+extern void pushhudscale(float sx, float sy = 0);
+extern void pushhudtranslate(float tx, float ty, float sx = 0, float sy = 0);
+
+// renderparticles
+enum
+{
+    PART_BLOOD = 0,
+    PART_WATER,
+    PART_SMOKE,
+    PART_STEAM,
+    PART_FLAME,
+    PART_FIREBALL1, PART_FIREBALL2, PART_FIREBALL3,
+    PART_STREAK, PART_LIGHTNING,
+    PART_EXPLOSION, PART_EXPLOSION_BLUE,
+    PART_SPARK, PART_EDIT,
+    PART_SNOW,
+    PART_MUZZLE_FLASH1, PART_MUZZLE_FLASH2, PART_MUZZLE_FLASH3,
+    PART_HUD_ICON,
+    PART_HUD_ICON_GREY,
+    PART_TEXT,
+    PART_TEXT_ICON,
+    PART_METER, PART_METER_VS,
+    PART_LENS_FLARE
+};
+
+extern bool canaddparticles();
+extern void regular_particle_splash(int type, int num, int fade, const vec &p, int color = 0xFFFFFF, float size = 1.0f, int radius = 150, int gravity = 2, int delay = 0);
+extern void regular_particle_flame(int type, const vec &p, float radius, float height, int color, int density = 3, float scale = 2.0f, float speed = 200.0f, float fade = 600.0f, int gravity = -15);
+extern void particle_splash(int type, int num, int fade, const vec &p, int color = 0xFFFFFF, float size = 1.0f, int radius = 150, int gravity = 2);
+extern void particle_trail(int type, int fade, const vec &from, const vec &to, int color = 0xFFFFFF, float size = 1.0f, int gravity = 20);
+extern void particle_text(const vec &s, const char *t, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0, int offset = 0);
+extern void particle_textcopy(const vec &s, const char *t, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0);
+extern void particle_texticon(const vec &s, int ix, int iy, float offset, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0);
+extern void particle_icon(const vec &s, int ix, int iy, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0);
+extern void particle_meter(const vec &s, float val, int type, int fade = 1, int color = 0xFFFFFF, int color2 = 0xFFFFF, float size = 2.0f);
+extern void particle_flare(const vec &p, const vec &dest, int fade, int type, int color = 0xFFFFFF, float size = 0.28f, physent *owner = NULL);
+extern void particle_fireball(const vec &dest, float max, int type, int fade = -1, int color = 0xFFFFFF, float size = 4.0f);
+extern void removetrackedparticles(physent *owner = NULL);
+
+// decal
+enum
+{
+    DECAL_SCORCH = 0,
+    DECAL_BLOOD,
+    DECAL_BULLET
+};
+
+extern void adddecal(int type, const vec &center, const vec &surface, float radius, const bvec &color = bvec(0xFF, 0xFF, 0xFF), int info = 0);
+
+// worldio
+extern bool load_world(const char *mname, const char *cname = NULL);
+extern bool save_world(const char *mname, bool nolms = false);
+extern void fixmapname(char *name);
+extern void getmapfilenames(const char *fname, const char *cname, char *pakname, char *mapname, char *cfgname);
+extern uint getmapcrc();
+extern void clearmapcrc();
+extern bool loadents(const char *fname, vector<entity> &ents, uint *crc = NULL);
+
+// physics
+extern vec collidewall;
+extern int collideinside;
+extern physent *collideplayer;
+
+extern void moveplayer(physent *pl, int moveres, bool local);
+extern bool moveplayer(physent *pl, int moveres, bool local, int curtime);
+extern bool collide(physent *d, const vec &dir = vec(0, 0, 0), float cutoff = 0.0f, bool playercol = true, bool insideplayercol = false);
+extern bool bounce(physent *d, float secs, float elasticity, float waterfric, float grav);
+extern bool bounce(physent *d, float elasticity, float waterfric, float grav);
+extern void avoidcollision(physent *d, const vec &dir, physent *obstacle, float space);
+extern bool overlapsdynent(const vec &o, float radius);
+extern bool movecamera(physent *pl, const vec &dir, float dist, float stepdist);
+extern void physicsframe();
+extern void dropenttofloor(entity *e);
+extern bool droptofloor(vec &o, float radius, float height);
+
+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 moveplatform(physent *p, const vec &dir);
+extern void updatephysstate(physent *d);
+extern void cleardynentcache();
+extern void updatedynentcache(physent *d);
+extern bool entinmap(dynent *d, bool avoidplayers = false);
+extern void findplayerspawn(dynent *d, int forceent = -1, int tag = 0);
+
+// sound
+enum
+{
+    SND_MAP     = 1<<0,
+    SND_NO_ALT  = 1<<1,
+    SND_USE_ALT = 1<<2
+};
+
+extern int playsound(int n, const vec *loc = NULL, extentity *ent = NULL, int flags = 0, int loops = 0, int fade = 0, int chanid = -1, int radius = 0, int expire = -1);
+extern int playsoundname(const char *s, const vec *loc = NULL, int vol = 0, int flags = 0, int loops = 0, int fade = 0, int chanid = -1, int radius = 0, int expire = -1);
+extern void preloadsound(int n);
+extern void preloadmapsound(int n);
+extern bool stopsound(int n, int chanid, int fade = 0);
+extern void stopsounds();
+extern void initsound();
+
+// rendermodel
+enum { MDL_CULL_VFC = 1<<0, MDL_CULL_DIST = 1<<1, MDL_CULL_OCCLUDED = 1<<2, MDL_CULL_QUERY = 1<<3, MDL_SHADOW = 1<<4, MDL_DYNSHADOW = 1<<5, MDL_LIGHT = 1<<6, MDL_DYNLIGHT = 1<<7, MDL_FULLBRIGHT = 1<<8, MDL_NORENDER = 1<<9, MDL_LIGHT_FAST = 1<<10, MDL_HUD = 1<<11, MDL_GHOST = 1<<12 };
+
+struct model;
+struct modelattach
+{
+    const char *tag, *name;
+    int anim, basetime;
+    vec *pos;
+    model *m;
+
+    modelattach() : tag(NULL), name(NULL), anim(-1), basetime(0), pos(NULL), m(NULL) {}
+    modelattach(const char *tag, const char *name, int anim = -1, int basetime = 0) : tag(tag), name(name), anim(anim), basetime(basetime), pos(NULL), m(NULL) {}
+    modelattach(const char *tag, vec *pos) : tag(tag), name(NULL), anim(-1), basetime(0), pos(pos), m(NULL) {}
+};
+
+extern void startmodelbatches();
+extern void endmodelbatches();
+extern void rendermodel(entitylight *light, const char *mdl, int anim, const vec &o, float yaw = 0, float pitch = 0, int cull = MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED | MDL_LIGHT, dynent *d = NULL, modelattach *a = NULL, int basetime = 0, int basetime2 = 0, float trans = 1);
+extern void abovemodel(vec &o, const char *mdl);
+extern void rendershadow(dynent *d);
+extern void renderclient(dynent *d, const char *mdlname, modelattach *attachments, int hold, int attack, int attackdelay, int lastaction, int lastpain, float fade = 1, bool ragdoll = false);
+extern void interpolateorientation(dynent *d, float &interpyaw, float &interppitch);
+extern void setbbfrommodel(dynent *d, const char *mdl);
+extern const char *mapmodelname(int i);
+extern model *loadmodel(const char *name, int i = -1, bool msg = false);
+extern void preloadmodel(const char *name);
+extern void flushpreloadedmodels(bool msg = true);
+
+// ragdoll
+
+extern void moveragdoll(dynent *d);
+extern void cleanragdoll(dynent *d);
+
+// server
+#define MAXCLIENTS 128                 // DO NOT set this any higher
+#define MAXTRANS 5000                  // max amount of data to swallow in 1 go
+
+extern int maxclients;
+
+enum { DISC_NONE = 0, DISC_EOP, DISC_LOCAL, DISC_KICK, DISC_MSGERR, DISC_IPBAN, DISC_PRIVATE, DISC_MAXCLIENTS, DISC_TIMEOUT, DISC_OVERFLOW, DISC_PASSWORD, DISC_NUM };
+
+extern void *getclientinfo(int i);
+extern ENetPeer *getclientpeer(int i);
+extern ENetPacket *sendf(int cn, int chan, const char *format, ...);
+extern ENetPacket *sendfile(int cn, int chan, stream *file, const char *format = "", ...);
+extern void sendpacket(int cn, int chan, ENetPacket *packet, int exclude = -1);
+extern void flushserver(bool force);
+extern int getservermtu();
+extern int getnumclients();
+extern uint getclientip(int n);
+extern void localconnect();
+extern const char *disconnectreason(int reason);
+extern void disconnect_client(int n, int reason);
+extern void kicknonlocalclients(int reason = DISC_NONE);
+extern bool hasnonlocalclients();
+extern bool haslocalclients();
+extern void sendserverinforeply(ucharbuf &p);
+extern bool requestmaster(const char *req);
+extern bool requestmasterf(const char *fmt, ...) PRINTFARGS(1, 2);
+extern bool isdedicatedserver();
+
+// client
+extern void sendclientpacket(ENetPacket *packet, int chan);
+extern void flushclient();
+extern void disconnect(bool async = false, bool cleanup = true);
+extern bool isconnected(bool attempt = false, bool local = true);
+extern const ENetAddress *connectedpeer();
+extern bool multiplayer(bool msg = true);
+extern void neterr(const char *s, bool disc = true);
+extern void gets2c();
+extern void notifywelcome();
+
+// crypto
+extern void genprivkey(const char *seed, vector<char> &privstr, vector<char> &pubstr);
+extern bool calcpubkey(const char *privstr, vector<char> &pubstr);
+extern bool hashstring(const char *str, char *result, int maxlen);
+extern void answerchallenge(const char *privstr, const char *challenge, vector<char> &answerstr);
+extern void *parsepubkey(const char *pubstr);
+extern void freepubkey(void *pubkey);
+extern void *genchallenge(void *pubkey, const void *seed, int seedlen, vector<char> &challengestr);
+extern void freechallenge(void *answer);
+extern bool checkchallenge(const char *answerstr, void *correct);
+
+// 3dgui
+struct Texture;
+struct VSlot;
+
+enum { G3D_DOWN = 1, G3D_UP = 2, G3D_PRESSED = 4, G3D_ROLLOVER = 8, G3D_DRAGGED = 16 };
+
+enum { EDITORFOCUSED = 1, EDITORUSED, EDITORFOREVER };
+
+struct g3d_gui
+{
+    virtual ~g3d_gui() {}
+
+    virtual void start(int starttime, float basescale, int *tab = NULL, bool allowinput = true) = 0;
+    virtual void end() = 0;
+
+    virtual int text(const char *text, int color, const char *icon = NULL) = 0;
+    int textf(const char *fmt, int color, const char *icon = NULL, ...) PRINTFARGS(2, 5)
+    {
+        defvformatstring(str, icon, fmt);
+        return text(str, color, icon);
+    }
+    virtual int button(const char *text, int color, const char *icon = NULL) = 0;
+    int buttonf(const char *fmt, int color, const char *icon = NULL, ...) PRINTFARGS(2, 5)
+    {
+        defvformatstring(str, icon, fmt);
+        return button(str, color, icon);
+    }
+    virtual int title(const char *text, int color, const char *icon = NULL) = 0;
+    int titlef(const char *fmt, int color, const char *icon = NULL, ...) PRINTFARGS(2, 5)
+    {
+        defvformatstring(str, icon, fmt);
+        return title(str, color, icon);
+    }
+    virtual void background(int color, int parentw = 0, int parenth = 0) = 0;
+
+    virtual void pushlist() {}
+    virtual void poplist() {}
+
+    virtual bool allowautotab(bool on) = 0;
+    virtual bool shouldtab() { return false; }
+       virtual void tab(const char *name = NULL, int color = 0) = 0;
+    virtual int image(Texture *t, float scale, const char *overlaid = NULL) = 0;
+    virtual int texture(VSlot &vslot, float scale, bool overlaid = true) = 0;
+    virtual int playerpreview(int model, int team, int weap, float scale, const char *overlaid = NULL) { return 0; }
+    virtual int modelpreview(const char *name, int anim, float scale, const char *overlaid = NULL, bool throttle = false) { return 0; }
+    virtual int prefabpreview(const char *prefab, const vec &color, float scale, const char *overlaid = NULL, bool throttle = false) { return 0; }
+    virtual void slider(int &val, int vmin, int vmax, int color, const char *label = NULL) = 0;
+    virtual void separator() = 0;
+       virtual void progress(float percent) = 0;
+       virtual void strut(float size) = 0;
+    virtual void space(float size) = 0;
+    virtual void spring(int weight = 1) = 0;
+    virtual void column(int col) = 0;
+    virtual char *keyfield(const char *name, int color, int length, int height = 0, const char *initval = NULL, int initmode = EDITORFOCUSED) = 0;
+    virtual char *field(const char *name, int color, int length, int height = 0, const char *initval = NULL, int initmode = EDITORFOCUSED) = 0;
+    virtual void textbox(const char *text, int width, int height, int color = 0xFFFFFF) = 0;
+    virtual bool mergehits(bool on) = 0;
+};
+
+struct g3d_callback
+{
+    virtual ~g3d_callback() {}
+
+    int starttime() { return totalmillis; }
+
+    virtual void gui(g3d_gui &g, bool firstpass) = 0;
+};
+
+enum
+{
+    GUI_2D       = 1<<0,
+    GUI_FOLLOW   = 1<<1,
+    GUI_FORCE_2D = 1<<2,
+    GUI_BOTTOM   = 1<<3
+};
+
+extern void g3d_addgui(g3d_callback *cb, vec &origin, int flags = 0);
+extern bool g3d_movecursor(int dx, int dy);
+extern void g3d_cursorpos(float &x, float &y);
+extern void g3d_resetcursor();
+extern void g3d_limitscale(float scale);
+
diff --git a/src/shared/igame.h b/src/shared/igame.h
new file mode 100644 (file)
index 0000000..113ca17
--- /dev/null
@@ -0,0 +1,130 @@
+// the interface the engine uses to run the gameplay module
+
+namespace entities
+{
+    extern void editent(int i, bool local);
+    extern const char *entnameinfo(entity &e);
+    extern const char *entname(int i);
+    extern int extraentinfosize();
+    extern void writeent(entity &e, char *buf);
+    extern void readent(entity &e, char *buf, int ver);
+    extern float dropheight(entity &e);
+    extern void fixentity(extentity &e);
+    extern void entradius(extentity &e, bool color);
+    extern bool mayattach(extentity &e);
+    extern bool attachent(extentity &e, extentity &a);
+    extern bool printent(extentity &e, char *buf, int len);
+    extern extentity *newentity();
+    extern void deleteentity(extentity *e);
+    extern void clearents();
+    extern vector<extentity *> &getents();
+    extern const char *entmodel(const entity &e);
+    extern void animatemapmodel(const extentity &e, int &anim, int &basetime);
+}
+
+namespace game
+{
+    extern void parseoptions(vector<const char *> &args);
+
+    extern void gamedisconnect(bool cleanup);
+    extern void parsepacketclient(int chan, packetbuf &p);
+    extern void connectattempt(const char *name, const char *password, const ENetAddress &address);
+    extern void connectfail();
+    extern void gameconnect(bool _remote);
+    extern bool allowedittoggle();
+    extern void edittoggled(bool on);
+    extern void writeclientinfo(stream *f);
+    extern void toserver(char *text);
+    extern void changemap(const char *name);
+    extern void forceedit(const char *name);
+    extern bool ispaused();
+    extern int scaletime(int t);
+    extern bool allowmouselook();
+
+    extern const char *gameident();
+    extern const char *savedconfig();
+    extern const char *restoreconfig();
+    extern const char *defaultconfig();
+    extern const char *autoexec();
+    extern const char *savedservers();
+    extern void loadconfigs();
+
+    extern void updateworld();
+    extern void initclient();
+    extern void physicstrigger(physent *d, bool local, int floorlevel, int waterlevel, int material = 0);
+    extern void bounced(physent *d, const vec &surface);
+    extern void edittrigger(const selinfo &sel, int op, int arg1 = 0, int arg2 = 0, int arg3 = 0, const VSlot *vs = NULL);
+    extern void vartrigger(ident *id);
+    extern void dynentcollide(physent *d, physent *o, const vec &dir);
+    extern const char *getclientmap();
+    extern const char *getmapinfo();
+    extern const char *getscreenshotinfo();
+    extern void resetgamestate();
+    extern void suicide(physent *d);
+    extern float ratespawn(dynent *d, const extentity &e);
+    extern void newmap(int size);
+    extern void loadingmap(const char *name);
+    extern void startmap(const char *name);
+    extern void preload();
+    extern float abovegameplayhud(int w, int h);
+    extern void gameplayhud(int w, int h);
+    extern bool canjump();
+    extern bool allowmove(physent *d);
+    extern void doattack(bool on);
+    extern dynent *iterdynents(int i);
+    extern int numdynents();
+    extern void rendergame(bool mainpass);
+    extern void renderavatar();
+    extern void renderplayerpreview(int model, int team, int weap);
+    extern void writegamedata(vector<char> &extras);
+    extern void readgamedata(vector<char> &extras);
+    extern int clipconsole(int w, int h);
+    extern void g3d_gamemenus();
+    extern const char *defaultcrosshair(int index);
+    extern int selectcrosshair(vec &color);
+    extern void lighteffects(dynent *d, vec &color, vec &dir);
+    extern void setupcamera();
+    extern bool allowthirdperson(bool msg = false);
+    extern bool detachcamera();
+    extern bool collidecamera();
+    extern void adddynlights();
+    extern void particletrack(physent *owner, vec &o, vec &d);
+    extern void dynlighttrack(physent *owner, vec &o, vec &hud);
+    extern int maxsoundradius(int n);
+    extern bool serverinfostartcolumn(g3d_gui *g, int i);
+    extern void serverinfoendcolumn(g3d_gui *g, int i);
+    extern bool serverinfoentry(g3d_gui *g, int i, const char *name, int port, const char *desc, const char *map, int ping, const vector<int> &attr, int np);
+    extern bool needminimap();
+} 
+namespace server
+{
+    extern void *newclientinfo();
+    extern void deleteclientinfo(void *ci);
+    extern void serverinit();
+    extern int reserveclients();
+    extern int numchannels();
+    extern void clientdisconnect(int n);
+    extern int clientconnect(int n, uint ip);
+    extern void localdisconnect(int n);
+    extern void localconnect(int n);
+    extern bool allowbroadcast(int n);
+    extern void recordpacket(int chan, void *data, int len);
+    extern void parsepacket(int sender, int chan, packetbuf &p);
+    extern void sendservmsg(const char *s);
+    extern bool sendpackets(bool force = false);
+    extern void serverinforeply(ucharbuf &req, ucharbuf &p);
+    extern void serverupdate();
+    extern bool servercompatible(char *name, char *sdec, char *map, int ping, const vector<int> &attr, int np);
+    extern int laninfoport();
+    extern int serverinfoport(int servport = -1);
+    extern int serverport(int infoport = -1);
+    extern const char *defaultmaster();
+    extern int masterport();
+    extern void processmasterinput(const char *cmd, int cmdlen, const char *args);
+    extern void masterconnected();
+    extern void masterdisconnected();
+    extern bool ispaused();
+    extern int scaletime(int t);
+}
+
diff --git a/src/shared/stream.cpp b/src/shared/stream.cpp
new file mode 100644 (file)
index 0000000..f2b586e
--- /dev/null
@@ -0,0 +1,1264 @@
+#include "cube.h"
+
+///////////////////////////// console ////////////////////////
+
+void conoutf(const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    conoutfv(CON_INFO, fmt, args);
+    va_end(args);
+}
+
+void conoutf(int type, const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    conoutfv(type, fmt, args);
+    va_end(args);
+}
+
+void conoutf(int type, int tag, const char *fmt, ...)
+{
+    va_list args;
+    va_start(args, fmt);
+    conoutfv(type | ((tag << CON_TAG_SHIFT) & CON_TAG_MASK), fmt, args);
+    va_end(args);
+}
+
+///////////////////////// character conversion ///////////////
+
+#define CUBECTYPE(s, p, d, a, A, u, U) \
+    0, U, U, U, U, U, U, U, U, s, s, s, s, s, U, U, \
+    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \
+    s, p, p, p, p, p, p, p, p, p, p, p, p, p, p, p, \
+    d, d, d, d, d, d, d, d, d, d, p, p, p, p, p, p, \
+    p, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, \
+    A, A, A, A, A, A, A, A, A, A, A, p, p, p, p, p, \
+    p, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, \
+    a, a, a, a, a, a, a, a, a, a, a, p, p, p, p, U, \
+    U, u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, \
+    u, u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, \
+    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \
+    u, U, u, U, u, U, u, U, u, U, u, U, u, U, u, U, \
+    u, U, u, U, u, U, u, U, U, u, U, u, U, u, U, U, \
+    U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, U, \
+    U, U, U, U, u, u, u, u, u, u, u, u, u, u, u, u, \
+    u, u, u, u, u, u, u, u, u, u, u, u, u, u, U, u
+
+extern const uchar cubectype[256] =
+{
+    CUBECTYPE(CT_SPACE,
+              CT_PRINT,
+              CT_PRINT|CT_DIGIT,
+              CT_PRINT|CT_ALPHA|CT_LOWER,
+              CT_PRINT|CT_ALPHA|CT_UPPER,
+              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_LOWER,
+              CT_PRINT|CT_UNICODE|CT_ALPHA|CT_UPPER)
+};
+extern const int cube2unichars[256] =
+{
+    0, 192, 193, 194, 195, 196, 197, 198, 199, 9, 10, 11, 12, 13, 200, 201,
+    202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 213, 214, 216, 217, 218, 219,
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
+    48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
+    64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
+    80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
+    96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
+    112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 220,
+    221, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237,
+    238, 239, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 252, 253, 255, 0x104,
+    0x105, 0x106, 0x107, 0x10C, 0x10D, 0x10E, 0x10F, 0x118, 0x119, 0x11A, 0x11B, 0x11E, 0x11F, 0x130, 0x131, 0x141,
+    0x142, 0x143, 0x144, 0x147, 0x148, 0x150, 0x151, 0x152, 0x153, 0x158, 0x159, 0x15A, 0x15B, 0x15E, 0x15F, 0x160,
+    0x161, 0x164, 0x165, 0x16E, 0x16F, 0x170, 0x171, 0x178, 0x179, 0x17A, 0x17B, 0x17C, 0x17D, 0x17E, 0x404, 0x411,
+    0x413, 0x414, 0x416, 0x417, 0x418, 0x419, 0x41B, 0x41F, 0x423, 0x424, 0x426, 0x427, 0x428, 0x429, 0x42A, 0x42B,
+    0x42C, 0x42D, 0x42E, 0x42F, 0x431, 0x432, 0x433, 0x434, 0x436, 0x437, 0x438, 0x439, 0x43A, 0x43B, 0x43C, 0x43D,
+    0x43F, 0x442, 0x444, 0x446, 0x447, 0x448, 0x449, 0x44A, 0x44B, 0x44C, 0x44D, 0x44E, 0x44F, 0x454, 0x490, 0x491
+};
+extern const int uni2cubeoffsets[8] =
+{
+    0, 256, 658, 658, 512, 658, 658, 658
+};
+extern const uchar uni2cubechars[878] =
+{
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 11, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
+    64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
+    96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    1, 2, 3, 4, 5, 6, 7, 8, 14, 15, 16, 17, 18, 19, 20, 21, 0, 22, 23, 24, 25, 26, 27, 0, 28, 29, 30, 31, 127, 128, 0, 129,
+    130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 0, 146, 147, 148, 149, 150, 151, 0, 152, 153, 154, 155, 156, 157, 0, 158,
+    0, 0, 0, 0, 159, 160, 161, 162, 0, 0, 0, 0, 163, 164, 165, 166, 0, 0, 0, 0, 0, 0, 0, 0, 167, 168, 169, 170, 0, 0, 171, 172,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 173, 174, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 175, 176, 177, 178, 0, 0, 179, 180, 0, 0, 0, 0, 0, 0, 0, 181, 182, 183, 184, 0, 0, 0, 0, 185, 186, 187, 188, 0, 0, 189, 190,
+    191, 192, 0, 0, 193, 194, 0, 0, 0, 0, 0, 0, 0, 0, 195, 196, 197, 198, 0, 0, 0, 0, 0, 0, 199, 200, 201, 202, 203, 204, 205, 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, 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, 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, 17, 0, 0, 206, 83, 73, 21, 74, 0, 0, 0, 0, 0, 0, 0, 65, 207, 66, 208, 209, 69, 210, 211, 212, 213, 75, 214, 77, 72, 79, 215,
+    80, 67, 84, 216, 217, 88, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 97, 228, 229, 230, 231, 101, 232, 233, 234, 235, 236, 237, 238, 239, 111, 240,
+    112, 99, 241, 121, 242, 120, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 0, 141, 0, 0, 253, 115, 105, 145, 106, 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, 0, 0, 0, 0, 0, 0, 0, 254, 255, 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, 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, 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, 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, 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
+};
+
+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];
+    if(dstbuf == srcbuf)
+    {
+        int len = min(dstlen, srclen);
+        for(const uchar *end4 = &srcbuf[len&~3]; src < end4; src += 4) if(*(const int *)src & 0x80808080) goto decode;
+        for(const uchar *end = &srcbuf[len]; src < end; src++) if(*src & 0x80) goto decode;
+        if(carry) *carry += len;
+        return len;
+    }
+
+decode:
+    dst += src - srcbuf;
+    while(src < srcend && dst < dstend)
+    {
+        int c = *src++;
+        if(c < 0x80) *dst++ = c;
+        else if(c >= 0xC0)
+        {
+            int uni;
+            if(c >= 0xE0)
+            {
+                if(c >= 0xF0)
+                {
+                    if(c >= 0xF8)
+                    {
+                        if(c >= 0xFC)
+                        {
+                            if(c >= 0xFE) continue;
+                            uni = c&1; if(srcend - src < 5) break;
+                            c = *src; if((c&0xC0) != 0x80) continue; src++; uni = (uni<<6) | (c&0x3F);
+                        }
+                        else { uni = c&3; if(srcend - src < 4) break; }
+                        c = *src; if((c&0xC0) != 0x80) continue; src++; uni = (uni<<6) | (c&0x3F);
+                    }
+                    else { uni = c&7; if(srcend - src < 3) break; }
+                    c = *src; if((c&0xC0) != 0x80) continue; src++; uni = (uni<<6) | (c&0x3F);
+                }
+                else { uni = c&0xF; if(srcend - src < 2) break; }
+                c = *src; if((c&0xC0) != 0x80) continue; src++; uni = (uni<<6) | (c&0x3F);
+            }
+            else { uni = c&0x1F; if(srcend - src < 1) break; }
+            c = *src; if((c&0xC0) != 0x80) continue; src++; uni = (uni<<6) | (c&0x3F);
+            c = uni2cube(uni);
+            if(!c) continue;
+            *dst++ = c;
+        }
+    }
+    if(carry) *carry += src - srcbuf;
+    return dst - dstbuf;
+}
+
+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];
+    if(src < srcend && dst < dstend) do
+    {
+        int uni = cube2uni(*src);
+        if(uni <= 0x7F)
+        {
+            if(dst >= dstend) goto done;
+            const uchar *end = min(srcend, &src[dstend-dst]);
+            do 
+            { 
+                if(uni == '\f')
+                {
+                    if(++src >= srcend) goto done;
+                    goto uni1;
+                }
+                *dst++ = uni; 
+                if(++src >= end) goto done; 
+                uni = cube2uni(*src); 
+            } 
+            while(uni <= 0x7F);
+        }
+        if(uni <= 0x7FF) { if(dst + 2 > dstend) goto done; *dst++ = 0xC0 | (uni>>6); goto uni2; }
+        else if(uni <= 0xFFFF) { if(dst + 3 > dstend) goto done; *dst++ = 0xE0 | (uni>>12); goto uni3; }
+        else if(uni <= 0x1FFFFF) { if(dst + 4 > dstend) goto done; *dst++ = 0xF0 | (uni>>18); goto uni4; }
+        else if(uni <= 0x3FFFFFF) { if(dst + 5 > dstend) goto done; *dst++ = 0xF8 | (uni>>24); goto uni5; }
+        else if(uni <= 0x7FFFFFFF) { if(dst + 6 > dstend) goto done; *dst++ = 0xFC | (uni>>30); goto uni6; }
+        else goto uni1;
+    uni6: *dst++ = 0x80 | ((uni>>24)&0x3F);
+    uni5: *dst++ = 0x80 | ((uni>>18)&0x3F);
+    uni4: *dst++ = 0x80 | ((uni>>12)&0x3F);
+    uni3: *dst++ = 0x80 | ((uni>>6)&0x3F);
+    uni2: *dst++ = 0x80 | (uni&0x3F);
+    uni1:;
+    } 
+    while(++src < srcend);
+
+done:
+    if(carry) *carry += src - srcbuf;
+    return dst - dstbuf;
+}
+
+///////////////////////// file system ///////////////////////
+
+#ifdef WIN32
+#include <shlobj.h>
+#else
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <dirent.h>
+#endif
+
+string homedir = "";
+struct packagedir
+{
+    char *dir, *filter;
+    size_t dirlen, filterlen;
+};
+vector<packagedir> packagedirs;
+
+char *makerelpath(const char *dir, const char *file, const char *prefix, const char *cmd)
+{
+    static string tmp;
+    if(prefix) copystring(tmp, prefix);
+    else tmp[0] = '\0';
+    if(file[0]=='<')
+    {
+        const char *end = strrchr(file, '>');
+        if(end)
+        {
+            size_t len = strlen(tmp);
+            copystring(&tmp[len], file, min(sizeof(tmp)-len, size_t(end+2-file)));
+            file = end+1;
+        }
+    }
+    if(cmd) concatstring(tmp, cmd);
+    if(dir)
+    {
+        defformatstring(pname, "%s/%s", dir, file);
+        concatstring(tmp, pname);
+    }
+    else concatstring(tmp, file);
+    return tmp;
+}
+
+
+char *path(char *s)
+{
+    for(char *curpart = s;;)
+    {
+        char *endpart = strchr(curpart, '&');
+        if(endpart) *endpart = '\0';
+        if(curpart[0]=='<')
+        {
+            char *file = strrchr(curpart, '>');
+            if(!file) return s;
+            curpart = file+1;
+        }
+        for(char *t = curpart; (t = strpbrk(t, "/\\")); *t++ = PATHDIV);
+        for(char *prevdir = NULL, *curdir = curpart;;)
+        {
+            prevdir = curdir[0]==PATHDIV ? curdir+1 : curdir;
+            curdir = strchr(prevdir, PATHDIV);
+            if(!curdir) break;
+            if(prevdir+1==curdir && prevdir[0]=='.')
+            {
+                memmove(prevdir, curdir+1, strlen(curdir+1)+1);
+                curdir = prevdir;
+            }
+            else if(curdir[1]=='.' && curdir[2]=='.' && curdir[3]==PATHDIV)
+            {
+                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;
+            }
+        }
+        if(endpart)
+        {
+            *endpart = '&';
+            curpart = endpart+1;
+        }
+        else break;
+    }
+    return s;
+}
+
+char *path(const char *s, bool copy)
+{
+    static string tmp;
+    copystring(tmp, s);
+    path(tmp);
+    return tmp;
+}
+
+const char *parentdir(const char *directory)
+{
+    const char *p = directory + strlen(directory);
+    while(p > directory && *p != '/' && *p != '\\') p--;
+    static string parent;
+    size_t len = p-directory+1;
+    copystring(parent, directory, len);
+    return parent;
+}
+
+bool fileexists(const char *path, const char *mode)
+{
+    bool exists = true;
+    if(mode[0]=='w' || mode[0]=='a') path = parentdir(path);
+#ifdef WIN32
+    if(GetFileAttributes(path[0] ? path : ".\\") == INVALID_FILE_ATTRIBUTES) exists = false;
+#else
+    if(access(path[0] ? path : ".", mode[0]=='w' || mode[0]=='a' ? W_OK : (mode[0]=='d' ? X_OK : R_OK)) == -1) exists = false;
+#endif
+    return exists;
+}
+
+bool createdir(const char *path)
+{
+    size_t len = strlen(path);
+    if(path[len-1]==PATHDIV)
+    {
+        static string strip;
+        path = copystring(strip, path, len);
+    }
+#ifdef WIN32
+    return CreateDirectory(path, NULL)!=0;
+#else
+    return mkdir(path, 0777)==0;
+#endif
+}
+
+size_t fixpackagedir(char *dir)
+{
+    path(dir);
+    size_t len = strlen(dir);
+    if(len > 0 && dir[len-1] != PATHDIV)
+    {
+        dir[len] = PATHDIV;
+        dir[len+1] = '\0';
+    }
+    return len;
+}
+
+bool subhomedir(char *dst, int len, const char *src)
+{
+    const char *sub = strstr(src, "$HOME");
+    if(!sub) sub = strchr(src, '~');
+    if(sub && sub-src < len)
+    {
+#ifdef WIN32
+        char home[MAX_PATH+1];
+        home[0] = '\0';
+        if(SHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, home) != S_OK || !home[0]) return false;
+#else
+        const char *home = getenv("HOME");
+        if(!home || !home[0]) return false;
+#endif
+        dst[sub-src] = '\0';
+        concatstring(dst, home, len);
+        concatstring(dst, sub+(*sub == '~' ? 1 : strlen("$HOME")), len);
+    }
+    return true;
+}
+
+const char *sethomedir(const char *dir)
+{
+    string pdir;
+    copystring(pdir, dir);
+    if(!subhomedir(pdir, sizeof(pdir), dir) || !fixpackagedir(pdir)) return NULL;
+    copystring(homedir, pdir);
+    return homedir;
+}
+
+const char *addpackagedir(const char *dir)
+{
+    string pdir;
+    copystring(pdir, dir);
+    if(!subhomedir(pdir, sizeof(pdir), dir) || !fixpackagedir(pdir)) return NULL;
+    char *filter = pdir;
+    for(;;)
+    {
+        static int len = strlen("packages");
+        filter = strstr(filter, "packages");
+        if(!filter) break;
+        if(filter > pdir && filter[-1] == PATHDIV && filter[len] == PATHDIV) break;
+        filter += len;
+    }    
+    packagedir &pf = packagedirs.add();
+    pf.dir = filter ? newstring(pdir, filter-pdir) : newstring(pdir);
+    pf.dirlen = filter ? filter-pdir : strlen(pdir);
+    pf.filter = filter ? newstring(filter) : NULL;
+    pf.filterlen = filter ? strlen(filter) : 0;
+    return pf.dir;
+}
+
+const char *findfile(const char *filename, const char *mode)
+{
+    static string s;
+    if(homedir[0])
+    {
+        formatstring(s, "%s%s", homedir, filename);
+        if(fileexists(s, mode)) return s;
+        if(mode[0]=='w' || mode[0]=='a')
+        {
+            string dirs;
+            copystring(dirs, s);
+            char *dir = strchr(dirs[0]==PATHDIV ? dirs+1 : dirs, PATHDIV);
+            while(dir)
+            {
+                *dir = '\0';
+                if(!fileexists(dirs, "d") && !createdir(dirs)) return s;
+                *dir = PATHDIV;
+                dir = strchr(dir+1, PATHDIV);
+            }
+            return s;
+        }
+    }
+    if(mode[0]=='w' || mode[0]=='a') return filename;
+    loopv(packagedirs)
+    {
+        packagedir &pf = packagedirs[i];
+        if(pf.filter && strncmp(filename, pf.filter, pf.filterlen)) continue;
+        formatstring(s, "%s%s", pf.dir, filename);
+        if(fileexists(s, mode)) return s;
+    }
+    if(mode[0]=='e') return NULL;
+    return filename;
+}
+
+bool listdir(const char *dirname, bool rel, const char *ext, vector<char *> &files)
+{
+    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);
+    if(Find != INVALID_HANDLE_VALUE)
+    {
+        do {
+            if(!ext) files.add(newstring(FindFileData.cFileName));
+            else
+            {
+                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
+    defformatstring(pathname, rel ? "./%s" : "%s", dirname);
+    DIR *d = opendir(pathname);
+    if(d)
+    {
+        struct dirent *de;
+        while((de = readdir(d)) != NULL)
+        {
+            if(!ext) files.add(newstring(de->d_name));
+            else
+            {
+                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
+    else return false;
+}
+
+int listfiles(const char *dir, const char *ext, vector<char *> &files)
+{
+    string dirname;
+    copystring(dirname, dir);
+    path(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++;
+    string s;
+    if(homedir[0])
+    {
+        formatstring(s, "%s%s", homedir, dirname);
+        if(listdir(s, false, ext, files)) dirs++;
+    }
+    loopv(packagedirs)
+    {
+        packagedir &pf = packagedirs[i];
+        if(pf.filter && strncmp(dirname, pf.filter, dirlen == pf.filterlen-1 ? dirlen : pf.filterlen))
+            continue;
+        formatstring(s, "%s%s", pf.dir, dirname);
+        if(listdir(s, false, ext, files)) dirs++;
+    }
+#ifndef STANDALONE
+    dirs += listzipfiles(dirname, ext, files);
+#endif
+    return dirs;
+}
+
+#ifndef STANDALONE
+static Sint64 rwopsseek(SDL_RWops *rw, Sint64 pos, int whence)
+{
+    stream *f = (stream *)rw->hidden.unknown.data1;
+    if((!pos && whence==SEEK_CUR) || f->seek(pos, whence)) return (int)f->tell();
+    return -1;
+}
+
+static size_t rwopsread(SDL_RWops *rw, void *buf, size_t size, size_t nmemb)
+{
+    stream *f = (stream *)rw->hidden.unknown.data1;
+    return f->read(buf, size*nmemb)/size;
+}
+
+static size_t rwopswrite(SDL_RWops *rw, const void *buf, size_t size, size_t nmemb)
+{
+    stream *f = (stream *)rw->hidden.unknown.data1;
+    return f->write(buf, size*nmemb)/size;
+}
+
+static int rwopsclose(SDL_RWops *rw)
+{
+    return 0;
+}
+
+SDL_RWops *stream::rwops()
+{
+    SDL_RWops *rw = SDL_AllocRW();
+    if(!rw) return NULL;
+    rw->hidden.unknown.data1 = this;
+    rw->seek = rwopsseek;
+    rw->read = rwopsread;
+    rw->write = rwopswrite;
+    rw->close = rwopsclose;
+    return rw;
+}
+#endif
+
+stream::offset stream::size()
+{
+    offset pos = tell(), endpos;
+    if(pos < 0 || !seek(0, SEEK_END)) return -1;
+    endpos = tell();
+    return pos == endpos || seek(pos, SEEK_SET) ? endpos : -1;
+}
+
+bool stream::getline(char *str, size_t len)
+{
+    loopi(len-1)
+    {
+        if(read(&str[i], 1) != 1) { str[i] = '\0'; return i > 0; }
+        else if(str[i] == '\n') { str[i+1] = '\0'; return true; }
+    }
+    if(len > 0) str[len-1] = '\0';
+    return true;
+}
+
+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);
+    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);
+    int len = vsnprintf(buf, sizeof(buf), fmt, args);
+    va_end(args);
+    if(len <= 0) return 0;
+    if(len >= (int)sizeof(buf))
+    {
+        str = new char[len+1];
+        va_start(args, fmt);
+        vsnprintf(str, len+1, fmt, args);
+        va_end(args);
+    }
+#endif
+    size_t n = write(str, len);
+    if(str != buf) delete[] str;
+    return n;
+}
+
+struct filestream : stream
+{
+    FILE *file;
+
+    filestream() : file(NULL) {}
+    ~filestream() { close(); }
+
+    bool open(const char *name, const char *mode)
+    {
+        if(file) return false;
+        file = fopen(name, mode);
+        return file!=NULL;
+    }
+
+    bool opentemp(const char *name, const char *mode)
+    {
+        if(file) return false;
+#ifdef WIN32
+        file = fopen(name, mode);
+#else
+        file = tmpfile();
+#endif
+        return file!=NULL;
+    }
+
+    void close()
+    {
+        if(file) { fclose(file); file = NULL; }
+    }
+
+    bool end() { return feof(file)!=0; }
+    offset tell() 
+    { 
+#ifdef WIN32
+#if defined(__GNUC__) && !defined(__MINGW32__)
+        offset off = ftello64(file);
+#else
+        offset off = _ftelli64(file);
+#endif
+#else
+        offset off = ftello(file);
+#endif
+        // ftello returns LONG_MAX for directories on some platforms
+        return off + 1 >= 0 ? off : -1;
+    }
+    bool seek(offset pos, int whence) 
+    { 
+#ifdef WIN32
+#if defined(__GNUC__) && !defined(__MINGW32__)
+        return fseeko64(file, pos, whence) >= 0;
+#else
+        return _fseeki64(file, pos, whence) >= 0;
+#endif
+#else
+        return fseeko(file, pos, whence) >= 0;
+#endif
+    }
+
+    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, size_t len) { return fgets(str, len, file)!=NULL; }
+    bool putstring(const char *str) { return fputs(str, file)!=EOF; }
+
+    size_t printf(const char *fmt, ...)
+    {
+        va_list v;
+        va_start(v, fmt);
+        int result = vfprintf(file, fmt, v);
+        va_end(v);
+        return max(result, 0);
+    }
+};
+
+#ifndef STANDALONE
+VAR(dbggz, 0, 0, 1);
+#endif
+
+struct gzstream : stream
+{
+    enum
+    {
+        MAGIC1   = 0x1F,
+        MAGIC2   = 0x8B,
+        BUFSIZE  = 16384,
+        OS_UNIX  = 0x03
+    };
+
+    enum
+    {
+        F_ASCII    = 0x01,
+        F_CRC      = 0x02,
+        F_EXTRA    = 0x04,
+        F_NAME     = 0x08,
+        F_COMMENT  = 0x10,
+        F_RESERVED = 0xE0
+    };
+
+    stream *file;
+    z_stream zfile;
+    uchar *buf;
+    bool reading, writing, autoclose;
+    uint crc;
+    size_t headersize;
+
+    gzstream() : file(NULL), buf(NULL), reading(false), writing(false), autoclose(false), crc(0), headersize(0)
+    {
+        zfile.zalloc = NULL;
+        zfile.zfree = NULL;
+        zfile.opaque = NULL;
+        zfile.next_in = zfile.next_out = NULL;
+        zfile.avail_in = zfile.avail_out = 0;
+    }
+
+    ~gzstream()
+    {
+        close();
+    }
+
+    void writeheader()
+    {
+        uchar header[] = { MAGIC1, MAGIC2, Z_DEFLATED, 0, 0, 0, 0, 0, 0, OS_UNIX };
+        file->write(header, sizeof(header));
+    }
+
+    void readbuf(size_t size = BUFSIZE)
+    {
+        if(!zfile.avail_in) zfile.next_in = (Bytef *)buf;
+        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;
+    }
+
+    uchar readbyte(size_t size = BUFSIZE)
+    {
+        if(!zfile.avail_in) readbuf(size);
+        if(!zfile.avail_in) return 0;
+        zfile.avail_in--;
+        return *(uchar *)zfile.next_in++;
+    }
+
+    void skipbytes(size_t n)
+    {
+        while(n > 0 && zfile.avail_in > 0)
+        {
+            size_t skipped = min(n, size_t(zfile.avail_in));
+            zfile.avail_in -= skipped;
+            zfile.next_in += skipped;
+            n -= skipped;
+        }
+        if(n <= 0) return;
+        file->seek(n, SEEK_CUR);
+    }
+
+    bool checkheader()
+    {
+        readbuf(10);
+        if(readbyte() != MAGIC1 || readbyte() != MAGIC2 || readbyte() != Z_DEFLATED) return false;
+        uchar flags = readbyte();
+        if(flags & F_RESERVED) return false;
+        skipbytes(6);
+        if(flags & F_EXTRA)
+        {
+            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 = size_t(file->tell() - zfile.avail_in);
+        return zfile.avail_in > 0 || !file->end();
+    }
+
+    bool open(stream *f, const char *mode, bool needclose, int level)
+    {
+        if(file) return false;
+        for(; *mode; mode++)
+        {
+            if(*mode=='r') { reading = true; break; }
+            else if(*mode=='w') { writing = true; break; }
+        }
+        if(reading)
+        {
+            if(inflateInit2(&zfile, -MAX_WBITS) != Z_OK) reading = false;
+        }
+        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;
+
+        file = f;
+        crc = crc32(0, NULL, 0);
+        buf = new uchar[BUFSIZE];
+
+        if(reading)
+        {
+            if(!checkheader()) { stopreading(); return false; }
+        }
+        else if(writing) writeheader();
+
+        autoclose = needclose;
+        return true;
+    }
+
+    uint getcrc() { return crc; }
+
+    void finishreading()
+    {
+        if(!reading) return;
+#ifndef STANDALONE
+        if(dbggz)
+        {
+            uint checkcrc = 0, checksize = 0;
+            loopi(4) checkcrc |= uint(readbyte()) << (i*8);
+            loopi(4) checksize |= uint(readbyte()) << (i*8);
+            if(checkcrc != crc)
+                conoutf(CON_DEBUG, "gzip crc check failed: read %X, calculated %X", checkcrc, crc);
+            if(checksize != zfile.total_out)
+                conoutf(CON_DEBUG, "gzip size check failed: read %u, calculated %u", checksize, uint(zfile.total_out));
+        }
+#endif
+    }
+
+    void stopreading()
+    {
+        if(!reading) return;
+        inflateEnd(&zfile);
+        reading = false;
+    }
+
+    void finishwriting()
+    {
+        if(!writing) return;
+        for(;;)
+        {
+            int err = zfile.avail_out > 0 ? deflate(&zfile, Z_FINISH) : Z_OK;
+            if(err != Z_OK && err != Z_STREAM_END) break;
+            flushbuf();
+            if(err == Z_STREAM_END) break;
+        }
+        uchar trailer[8] =
+        {
+            uchar(crc&0xFF), uchar((crc>>8)&0xFF), uchar((crc>>16)&0xFF), uchar((crc>>24)&0xFF),
+            uchar(zfile.total_in&0xFF), uchar((zfile.total_in>>8)&0xFF), uchar((zfile.total_in>>16)&0xFF), uchar((zfile.total_in>>24)&0xFF)
+        };
+        file->write(trailer, sizeof(trailer));
+    }
+
+    void stopwriting()
+    {
+        if(!writing) return;
+        deflateEnd(&zfile);
+        writing = false;
+    }
+
+    void close()
+    {
+        if(reading) finishreading();
+        stopreading();
+        if(writing) finishwriting();
+        stopwriting();
+        DELETEA(buf);
+        if(autoclose) DELETEP(file);
+    }
+
+    bool end() { return !reading && !writing; }
+    offset tell() { return reading ? zfile.total_out : (writing ? zfile.total_in : offset(-1)); }
+    offset rawtell() { return file ? file->tell() : offset(-1); }
+
+    offset size()
+    {
+        if(!file) return -1;
+        offset pos = tell();
+        if(!file->seek(-4, SEEK_END)) return -1;
+        uint isize = file->getlil<uint>();
+        return file->seek(pos, SEEK_SET) ? isize : offset(-1);
+    }
+
+    offset rawsize() { return file ? file->size() : offset(-1); }
+
+    bool seek(offset pos, int whence)
+    {
+        if(writing || !reading) return false;
+
+        if(whence == SEEK_END)
+        {
+            uchar skip[512];
+            while(read(skip, sizeof(skip)) == sizeof(skip));
+            return !pos;
+        }
+        else if(whence == SEEK_CUR) pos += zfile.total_out;
+
+        if(pos >= (offset)zfile.total_out) pos -= zfile.total_out;
+        else if(pos < 0 || !file->seek(headersize, SEEK_SET)) return false;
+        else
+        {
+            if(zfile.next_in && zfile.total_in <= uint(zfile.next_in - buf))
+            {
+                zfile.avail_in += zfile.total_in;
+                zfile.next_in -= zfile.total_in;
+            }
+            else
+            {
+                zfile.avail_in = 0;
+                zfile.next_in = NULL;
+            }
+            inflateReset(&zfile);
+            crc = crc32(0, NULL, 0);
+        }
+
+        uchar skip[512];
+        while(pos > 0)
+        {
+            size_t skipped = (size_t)min(pos, (offset)sizeof(skip));
+            if(read(skip, skipped) != skipped) { stopreading(); return false; }
+            pos -= skipped;
+        }
+
+        return true;
+    }
+
+    size_t read(void *buf, size_t len)
+    {
+        if(!reading || !buf || !len) return 0;
+        zfile.next_out = (Bytef *)buf;
+        zfile.avail_out = len;
+        while(zfile.avail_out > 0)
+        {
+            if(!zfile.avail_in)
+            {
+                readbuf(BUFSIZE);
+                if(!zfile.avail_in) { stopreading(); break; }
+            }
+            int err = inflate(&zfile, Z_NO_FLUSH);
+            if(err == Z_STREAM_END) { crc = crc32(crc, (Bytef *)buf, len - zfile.avail_out); finishreading(); stopreading(); return len - zfile.avail_out; }
+            else if(err != Z_OK) { stopreading(); break; }
+        }
+        crc = crc32(crc, (Bytef *)buf, len - zfile.avail_out);
+        return len - zfile.avail_out;
+    }
+
+    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) != BUFSIZE - zfile.avail_out || (full && !file->flush()))
+                return false;
+        }
+        zfile.next_out = buf;
+        zfile.avail_out = BUFSIZE;
+        return true;
+    }
+
+    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 && !flushbuf()) { stopwriting(); break; }
+            int err = deflate(&zfile, Z_NO_FLUSH);
+            if(err != Z_OK) { stopwriting(); break; }
+        }
+        crc = crc32(crc, (Bytef *)buf, len - zfile.avail_in);
+        return len - zfile.avail_in;
+    }
+};
+
+struct utf8stream : stream
+{
+    enum
+    {
+        BUFSIZE = 4096
+    };
+    stream *file;
+    offset pos;
+    size_t bufread, bufcarry, buflen;
+    bool reading, writing, autoclose;
+    uchar buf[BUFSIZE]; 
+
+    utf8stream() : file(NULL), pos(0), bufread(0), bufcarry(0), buflen(0), reading(false), writing(false), autoclose(false)
+    {
+    }
+
+    ~utf8stream()
+    {
+        close();
+    }
+
+    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; }
+        size_t n = file->read(&buf[buflen], min(size, BUFSIZE - buflen));
+        if(n <= 0) return false;
+        buflen += n;
+        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;
+    }
+
+    bool checkheader()
+    {
+        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;
+    }
+            
+    bool open(stream *f, const char *mode, bool needclose)
+    {
+        if(file) return false;
+        for(; *mode; mode++)
+        {
+            if(*mode=='r') { reading = true; break; }
+            else if(*mode=='w') { writing = true; break; }
+        }
+        if(!reading && !writing) return false;
+       
+        file = f;
+       
+        if(reading) checkheader();
+        autoclose = needclose;
+        return true;
+    } 
+
+    void finishreading() 
+    {
+        if(!reading) return;
+    }
+
+    void stopreading()
+    {
+        if(!reading) return;
+        reading = false;
+    }
+
+    void stopwriting()
+    {
+        if(!writing) return;
+        writing = false;
+    }
+
+    void close()
+    {
+        stopreading();
+        stopwriting();
+        if(autoclose) DELETEP(file);
+    }
+
+    bool end() { return !reading && !writing; }
+    offset tell() { return reading || writing ? pos : offset(-1); }
+
+    bool seek(offset off, int whence)
+    {
+        if(writing || !reading) return false;
+
+        if(whence == SEEK_END)
+        {
+            uchar skip[512];
+            while(read(skip, sizeof(skip)) == sizeof(skip));
+            return !off;
+        }
+        else if(whence == SEEK_CUR) off += pos;
+       
+        if(off >= pos) off -= pos;
+        else if(off < 0 || !file->seek(0, SEEK_SET)) return false;
+        else
+        {
+            bufread = bufcarry = buflen = 0;
+            pos = 0;
+            checkheader(); 
+        }
+
+        uchar skip[512];
+        while(off > 0)
+        {
+            size_t skipped = (size_t)min(off, (offset)sizeof(skip));
+            if(read(skip, skipped) != skipped) { stopreading(); return false; }
+            off -= skipped;
+        }
+        
+        return true;
+    }
+
+    size_t read(void *dst, size_t len)
+    {
+        if(!reading || !dst || !len) return 0;
+        size_t next = 0;
+        while(next < len)
+        {
+            if(bufread >= bufcarry) { if(readbuf(BUFSIZE)) continue; stopreading(); break; }
+            size_t n = min(len - next, bufcarry - bufread);
+            memcpy(&((uchar *)dst)[next], &buf[bufread], n);
+            next += n;
+            bufread += n;
+        }
+        pos += next;
+        return next;
+    }
+
+    bool getline(char *dst, size_t len)
+    {
+        if(!reading || !dst || !len) return false;
+        --len;
+        size_t next = 0;
+        while(next < len)
+        {
+            if(bufread >= bufcarry) { if(readbuf(BUFSIZE)) continue; stopreading(); if(!next) return false; break; }
+            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);
+            next += n;
+            bufread += n;
+        }
+        dst[next] = '\0';
+        pos += next;
+        return true;
+    }
+
+    size_t write(const void *src, size_t len)
+    {
+        if(!writing || !src || !len) return 0;
+        uchar dst[512];
+        size_t next = 0;
+        while(next < len)
+        {
+            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)
+{
+    const char *found = findfile(filename, mode);
+    if(!found) return NULL;
+    filestream *file = new filestream;
+    if(!file->open(found, mode)) { delete file; return NULL; }
+    return file;
+}
+
+stream *openfile(const char *filename, const char *mode)
+{
+#ifndef STANDALONE
+    stream *s = openzipfile(filename, mode);
+    if(s) return s;
+#endif
+    return openrawfile(filename, mode);
+}
+
+stream *opentempfile(const char *name, const char *mode)
+{
+    const char *found = findfile(name, mode);
+    filestream *file = new filestream;
+    if(!file->opentemp(found ? found : name, mode)) { delete file; return NULL; }
+    return file;
+}
+
+stream *opengzfile(const char *filename, const char *mode, stream *file, int level)
+{
+    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; delete gz; return NULL; }
+    return gz;
+}
+
+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; delete utf8; return NULL; }
+    return utf8;
+}
+
+char *loadfile(const char *fn, size_t *size, bool utf8)
+{
+    stream *f = openfile(fn, "rb");
+    if(!f) return NULL;
+    stream::offset fsize = f->size();
+    if(fsize <= 0) { delete f; return NULL; }
+    size_t len = fsize;
+    char *buf = new (false) char[len+1];
+    if(!buf) { delete f; return NULL; }
+    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;
+    } 
+    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);
+    buf[len] = '\0';
+    if(size!=NULL) *size = len;
+    return buf;
+}
+
diff --git a/src/shared/tools.cpp b/src/shared/tools.cpp
new file mode 100644 (file)
index 0000000..ca82e8f
--- /dev/null
@@ -0,0 +1,244 @@
+// implementation of generic tools
+
+#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); }
+
+void *operator new(size_t size, bool err)
+{
+    void *p = malloc(size);
+    if(!p && err) abort();
+    return p;
+}
+
+void *operator new[](size_t size, bool err)
+{
+    void *p = malloc(size);
+    if(!p && err) abort();
+    return p;
+}
+
+////////////////////////// rnd numbers ////////////////////////////////////////
+
+#define N (624)             
+#define M (397)                
+#define K (0x9908B0DFU)       
+
+static uint state[N];
+static int next = N;
+
+void seedMT(uint seed)
+{
+    state[0] = seed;
+    for(uint i = 1; i < N; i++)
+        state[i] = seed = 1812433253U * (seed ^ (seed >> 30)) + i;
+    next = 0;
+}
+
+uint randomMT()
+{
+    int cur = next;
+    if(++next >= N)
+    {
+        if(next > N) { seedMT(5489U + time(NULL)); cur = next++; }
+        else next = 0;
+    }
+    uint y = (state[cur] & 0x80000000U) | (state[next] & 0x7FFFFFFFU);
+    state[cur] = y = state[cur < N-M ? cur + M : cur + M-N] ^ (y >> 1) ^ (-int(y & 1U) & K);
+    y ^= (y >> 11);
+    y ^= (y <<  7) & 0x9D2C5680U;
+    y ^= (y << 15) & 0xEFC60000U;
+    y ^= (y >> 18);
+    return y;
+}
+
+///////////////////////// network ///////////////////////
+
+// all network traffic is in 32bit ints, which are then compressed using the following simple scheme (assumes that most values are small).
+
+template<class T>
+static inline void putint_(T &p, int n)
+{
+    if(n<128 && n>-127) p.put(n);
+    else if(n<0x8000 && n>=-0x8000) { p.put(0x80); p.put(n); p.put(n>>8); }
+    else { p.put(0x81); p.put(n); p.put(n>>8); p.put(n>>16); p.put(n>>24); }
+}
+void putint(ucharbuf &p, int n) { putint_(p, n); }
+void putint(packetbuf &p, int n) { putint_(p, n); }
+void putint(vector<uchar> &p, int n) { putint_(p, n); }
+
+int getint(ucharbuf &p)
+{
+    int c = (schar)p.get();
+    if(c==-128) { int n = p.get(); n |= ((schar)p.get())<<8; return n; }
+    else if(c==-127) { int n = p.get(); n |= p.get()<<8; n |= p.get()<<16; return n|(p.get()<<24); }
+    else return c;
+}
+
+// much smaller encoding for unsigned integers up to 28 bits, but can handle signed
+template<class T>
+static inline void putuint_(T &p, int n)
+{
+    if(n < 0 || n >= (1<<21))
+    {
+        p.put(0x80 | (n & 0x7F));
+        p.put(0x80 | ((n >> 7) & 0x7F));
+        p.put(0x80 | ((n >> 14) & 0x7F));
+        p.put(n >> 21);
+    }
+    else if(n < (1<<7)) p.put(n);
+    else if(n < (1<<14))
+    {
+        p.put(0x80 | (n & 0x7F));
+        p.put(n >> 7);
+    }
+    else
+    {
+        p.put(0x80 | (n & 0x7F));
+        p.put(0x80 | ((n >> 7) & 0x7F));
+        p.put(n >> 14);
+    }
+}
+void putuint(ucharbuf &p, int n) { putuint_(p, n); }
+void putuint(packetbuf &p, int n) { putuint_(p, n); }
+void putuint(vector<uchar> &p, int n) { putuint_(p, n); }
+
+int getuint(ucharbuf &p)
+{
+    int n = p.get();
+    if(n & 0x80)
+    {
+        n += (p.get() << 7) - 0x80;
+        if(n & (1<<14)) n += (p.get() << 14) - (1<<14);
+        if(n & (1<<21)) n += (p.get() << 21) - (1<<21);
+        if(n & (1<<28)) n |= ~0U<<28;
+    }
+    return n;
+}
+
+template<class T>
+static inline void putfloat_(T &p, float f)
+{
+    lilswap(&f, 1);
+    p.put((uchar *)&f, sizeof(float));
+}
+void putfloat(ucharbuf &p, float f) { putfloat_(p, f); }
+void putfloat(packetbuf &p, float f) { putfloat_(p, f); }
+void putfloat(vector<uchar> &p, float f) { putfloat_(p, f); }
+
+float getfloat(ucharbuf &p)
+{
+    float f;
+    p.get((uchar *)&f, sizeof(float));
+    return lilswap(f);
+}
+
+template<class T>
+static inline void sendstring_(const char *t, T &p)
+{
+    while(*t) putint(p, *t++);
+    putint(p, 0);
+}
+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, size_t len)
+{
+    char *t = text;
+    do
+    {
+        if(t>=&text[len]) { text[len-1] = 0; return; }
+        if(!p.remaining()) { *t = 0; return; }
+        *t = getint(p);
+    }
+    while(*t++);
+}
+
+void filtertext(char *dst, const char *src, bool whitespace, bool forcespace, size_t len)
+{
+    for(int c = uchar(*src); c; c = uchar(*++src))
+    {
+        if(c == '\f')
+        {
+            if(!*++src) break;
+            continue;
+        }
+        if(!iscubeprint(c))
+        {
+            if(!iscubespace(c) || !whitespace) continue;
+            if(forcespace) c = ' ';
+        }
+        *dst++ = c;
+        if(!--len) break;
+    }
+    *dst = '\0';
+}
+
+void ipmask::parse(const char *name)
+{
+    union { uchar b[sizeof(enet_uint32)]; enet_uint32 i; } ipconv, maskconv;
+    ipconv.i = 0;
+    maskconv.i = 0;
+    loopi(4)
+    {
+        char *end = NULL;
+        int n = strtol(name, &end, 10);
+        if(!end) break;
+        if(end > name) { ipconv.b[i] = n; maskconv.b[i] = 0xFF; }
+        name = end;
+        while(int c = *name)
+        {
+            ++name;
+            if(c == '.') break;
+            if(c == '/')
+            {
+                int range = clamp(int(strtol(name, NULL, 10)), 0, 32);
+                mask = range ? ENET_HOST_TO_NET_32(0xFFffFFff << (32 - range)) : maskconv.i;
+                ip = ipconv.i & mask;
+                return;
+            }
+        }
+    }
+    ip = ipconv.i;
+    mask = maskconv.i;
+}
+
+int ipmask::print(char *buf) const
+{
+    char *start = buf;
+    union { uchar b[sizeof(enet_uint32)]; enet_uint32 i; } ipconv, maskconv;
+    ipconv.i = ip;
+    maskconv.i = mask;
+    int lastdigit = -1;
+    loopi(4) if(maskconv.b[i])
+    {
+        if(lastdigit >= 0) *buf++ = '.';
+        loopj(i - lastdigit - 1) { *buf++ = '*'; *buf++ = '.'; }
+        buf += sprintf(buf, "%d", ipconv.b[i]);
+        lastdigit = i;
+    }
+    enet_uint32 bits = ~ENET_NET_TO_HOST_32(mask);
+    int range = 32;
+    for(; (bits&0xFF) == 0xFF; bits >>= 8) range -= 8;
+    for(; bits&1; bits >>= 1) --range;
+    if(!bits && range%8) buf += sprintf(buf, "/%d", range);
+    return int(buf-start);
+}
+
diff --git a/src/shared/tools.h b/src/shared/tools.h
new file mode 100644 (file)
index 0000000..c66f5bd
--- /dev/null
@@ -0,0 +1,1408 @@
+// generic useful stuff for any C++ program
+
+#ifndef _TOOLS_H
+#define _TOOLS_H
+
+#ifdef NULL
+#undef NULL
+#endif
+#define NULL 0
+
+typedef signed char schar;
+typedef unsigned char uchar;
+typedef unsigned short ushort;
+typedef unsigned int uint;
+typedef unsigned long ulong;
+typedef signed long long int llong;
+typedef unsigned long long int ullong;
+
+#ifdef _DEBUG
+#define ASSERT(c) assert(c)
+#else
+#define ASSERT(c) if(c) {}
+#endif
+
+#if defined(__GNUC__) || (defined(_MSC_VER) && _MSC_VER >= 1400)
+#define RESTRICT __restrict
+#else
+#define RESTRICT
+#endif
+
+#ifdef __GNUC__
+#define UNUSED __attribute__((unused))
+#else
+#define UNUSED
+#endif
+
+void *operator new(size_t, bool);
+void *operator new[](size_t, bool);
+inline void *operator new(size_t, void *p) { return p; }
+inline void *operator new[](size_t, void *p) { return p; }
+inline void operator delete(void *, void *) {}
+inline void operator delete[](void *, void *) {}
+
+#ifdef swap
+#undef swap
+#endif
+template<class T>
+static inline void swap(T &a, T &b)
+{
+    T t = a;
+    a = b;
+    b = t;
+}
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+template<class T>
+static inline T max(T a, T b)
+{
+    return a > b ? a : b;
+}
+template<class T>
+static inline T min(T a, T b)
+{
+    return a < b ? a : b;
+}
+template<class T, class U>
+static inline T clamp(T a, U b, U c)
+{
+    return max(T(b), min(a, T(c)));
+}
+
+#ifdef __GNUC__
+#define bitscan(mask) (__builtin_ffs(mask)-1)
+#else
+#ifdef WIN32
+#pragma intrinsic(_BitScanForward)
+static inline int bitscan(uint mask)
+{
+    ulong i;
+    return _BitScanForward(&i, mask) ? i : -1;
+}
+#else
+static inline int bitscan(uint mask)
+{
+    if(!mask) return -1;
+    int i = 1;
+    if(!(mask&0xFFFF)) { i += 16; mask >>= 16; }
+    if(!(mask&0xFF)) { i += 8; mask >>= 8; }
+    if(!(mask&0xF)) { i += 4; mask >>= 4; }
+    if(!(mask&3)) { i += 2; mask >>= 2; }
+    return i - (mask&1);
+}
+#endif
+#endif
+
+#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 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 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; }
+
+#define PI  (3.1415927f)
+#define PI2 (2*PI)
+#define SQRT2 (1.4142136f)
+#define SQRT3 (1.7320508f)
+#define RAD (PI / 180.0f)
+
+#ifdef WIN32
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#ifndef M_LN2
+#define M_LN2 0.693147180559945309417
+#endif
+
+#ifndef __GNUC__
+#pragma warning (3: 4189)       // local variable is initialized but not referenced
+#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
+#endif
+
+#define strcasecmp _stricmp
+#define strncasecmp _strnicmp
+#define PATHDIV '\\'
+
+#else
+#define __cdecl
+#define _vsnprintf vsnprintf
+#define PATHDIV '/'
+#endif
+
+#ifdef __GNUC__
+#define PRINTFARGS(fmt, args) __attribute__((format(printf, fmt, args)))
+#else
+#define PRINTFARGS(fmt, args)
+#endif
+
+// easy safe strings
+
+#define MAXSTRLEN 260
+typedef char string[MAXSTRLEN];
+
+inline void vformatstring(char *d, const char *fmt, va_list v, int len) { _vsnprintf(d, len, fmt, v); d[len-1] = 0; }
+template<size_t N> inline void vformatstring(char (&d)[N], const char *fmt, va_list v) { vformatstring(d, fmt, v, N); }
+
+inline char *copystring(char *d, const char *s, size_t len)
+{
+    size_t slen = min(strlen(s), len-1);
+    memcpy(d, s, slen);
+    d[slen] = 0;
+    return d;
+}
+template<size_t N> inline char *copystring(char (&d)[N], const char *s) { return copystring(d, s, N); }
+
+inline char *concatstring(char *d, const char *s, size_t len) { size_t used = strlen(d); return used < len ? copystring(d+used, s, len-used) : d; }
+template<size_t N> inline char *concatstring(char (&d)[N], const char *s) { return concatstring(d, s, N); }
+
+inline char *prependstring(char *d, const char *s, size_t len)
+{
+    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;
+}
+template<size_t N> inline char *prependstring(char (&d)[N], const char *s) { return prependstring(d, s, N); }
+
+inline void nformatstring(char *d, int len, const char *fmt, ...) PRINTFARGS(3, 4);
+inline void nformatstring(char *d, int len, const char *fmt, ...)
+{
+    va_list v;
+    va_start(v, fmt);
+    vformatstring(d, fmt, v, len);
+    va_end(v);
+}
+
+template<size_t N> inline void formatstring(char (&d)[N], const char *fmt, ...) PRINTFARGS(2, 3);
+template<size_t N> inline void formatstring(char (&d)[N], const char *fmt, ...)
+{
+    va_list v;
+    va_start(v, fmt);
+    vformatstring(d, fmt, v, int(N));
+    va_end(v);
+}
+
+template<size_t N> inline void concformatstring(char (&d)[N], const char *fmt, ...) PRINTFARGS(2, 3);
+template<size_t N> inline void concformatstring(char (&d)[N], const char *fmt, ...)
+{
+    va_list v;
+    va_start(v, fmt);
+    int len = strlen(d);
+    vformatstring(d + len, fmt, v, int(N) - len);
+    va_end(v);
+}
+
+#define defformatstring(d,...) string d; formatstring(d, __VA_ARGS__)
+#define defvformatstring(d,last,fmt) string d; { va_list ap; va_start(ap, last); vformatstring(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)           { 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++)
+#define loopvrev(v) for(int i = (v).length()-1; i>=0; i--)
+
+template<class T> inline void memclear(T *p, size_t n) { memset((void *)p, 0, n * sizeof(T)); }
+template<class T> inline void memclear(T &p) { memset((void *)&p, 0, sizeof(T)); }
+template<class T, size_t N> inline void memclear(T (&p)[N]) { memset((void *)p, 0, N * sizeof(T)); }
+
+template <class T>
+struct databuf
+{
+    enum
+    {
+        OVERREAD  = 1<<0,
+        OVERWROTE = 1<<1
+    };
+
+    T *buf;
+    int len, maxlen;
+    uchar flags;
+
+    databuf() : buf(NULL), len(0), maxlen(0), flags(0) {}
+
+    template<class U>
+    databuf(T *buf, U maxlen) : buf(buf), len(0), maxlen((int)maxlen), flags(0) {}
+
+    void reset()
+    {
+        len = 0;
+        flags = 0;
+    }
+
+    void reset(T *buf_, int maxlen_)
+    {
+        reset();
+        buf = buf_;
+        maxlen = maxlen_;
+    }
+
+    const T &get()
+    {
+        static T overreadval = 0;
+        if(len<maxlen) return buf[len++];
+        flags |= OVERREAD;
+        return overreadval;
+    }
+
+    databuf subbuf(int sz)
+    {
+        sz = clamp(sz, 0, maxlen-len);
+        len += sz;
+        return databuf(&buf[len-sz], sz);
+    }
+
+    T *pad(int numvals)
+    {
+        T *vals = &buf[len];
+        len += min(numvals, maxlen-len);
+        return vals;
+    }
+
+    void put(const T &val)
+    {
+        if(len<maxlen) buf[len++] = val;
+        else flags |= OVERWROTE;
+    }
+
+    void put(const T *vals, int numvals)
+    {
+        if(maxlen-len<numvals) flags |= OVERWROTE;
+        memcpy(&buf[len], (const void *)vals, min(maxlen-len, numvals)*sizeof(T));
+        len += min(maxlen-len, numvals);
+    }
+
+    int get(T *vals, int numvals)
+    {
+        int read = min(maxlen-len, numvals);
+        if(read<numvals) flags |= OVERREAD;
+        memcpy(vals, (void *)&buf[len], read*sizeof(T));
+        len += read;
+        return read;
+    }
+
+    void offset(int n)
+    {
+        n = min(n, maxlen);
+        buf += n;
+        maxlen -= n;
+        len = max(len-n, 0);
+    }
+
+    T *getbuf() const { return buf; }
+    bool empty() const { return len==0; }
+    int length() const { return len; }
+    int remaining() const { return maxlen-len; }
+    bool overread() const { return (flags&OVERREAD)!=0; }
+    bool overwrote() const { return (flags&OVERWROTE)!=0; }
+
+    bool check(int n) { return remaining() >= n; }
+
+    void forceoverread()
+    {
+        len = maxlen;
+        flags |= OVERREAD;
+    }
+};
+
+typedef databuf<char> charbuf;
+typedef databuf<uchar> ucharbuf;
+
+struct packetbuf : ucharbuf
+{
+    ENetPacket *packet;
+    int growth;
+
+    packetbuf(ENetPacket *packet) : ucharbuf(packet->data, packet->dataLength), packet(packet), growth(0) {}
+    packetbuf(int growth, int pflags = 0) : growth(growth)
+    {
+        packet = enet_packet_create(NULL, growth, pflags);
+        buf = (uchar *)packet->data;
+        maxlen = packet->dataLength;
+    }
+    ~packetbuf() { cleanup(); }
+
+    void reliable() { packet->flags |= ENET_PACKET_FLAG_RELIABLE; }
+
+    void resize(int n)
+    {
+        enet_packet_resize(packet, n);
+        buf = (uchar *)packet->data;
+        maxlen = packet->dataLength;
+    }
+
+    void checkspace(int n)
+    {
+        if(len + n > maxlen && packet && growth > 0) resize(max(len + n, maxlen + growth));
+    }
+
+    ucharbuf subbuf(int sz)
+    {
+        checkspace(sz);
+        return ucharbuf::subbuf(sz);
+    }
+
+    void put(const uchar &val)
+    {
+        checkspace(1);
+        ucharbuf::put(val);
+    }
+
+    void put(const uchar *vals, int numvals)
+    {
+        checkspace(numvals);
+        ucharbuf::put(vals, numvals);
+    }
+
+    ENetPacket *finalize()
+    {
+        resize(len);
+        return packet;
+    }
+
+    void cleanup()
+    {
+        if(growth > 0 && packet && !packet->referenceCount) { enet_packet_destroy(packet); packet = NULL; buf = NULL; len = maxlen = 0; }
+    }
+};
+
+template<class T>
+static inline float heapscore(const T &n) { return n; }
+
+struct sortless
+{
+    template<class T> bool operator()(const T &x, const T &y) const { return x < y; }
+    bool operator()(char *x, char *y) const { return strcmp(x, y) < 0; }
+    bool operator()(const char *x, const char *y) const { return strcmp(x, y) < 0; }
+};
+
+struct sortnameless
+{
+    template<class T> bool operator()(const T &x, const T &y) const { return sortless()(x.name, y.name); }
+    template<class T> bool operator()(T *x, T *y) const { return sortless()(x->name, y->name); }
+    template<class T> bool operator()(const T *x, const T *y) const { return sortless()(x->name, y->name); }
+};
+
+template<class T, class F>
+static inline void insertionsort(T *start, T *end, F fun)
+{
+    for(T *i = start+1; i < end; i++)
+    {
+        if(fun(*i, i[-1]))
+        {
+            T tmp = *i;
+            *i = i[-1];
+            T *j = i-1;
+            for(; j > start && fun(tmp, j[-1]); --j)
+                *j = j[-1];
+            *j = tmp;
+        }
+    }
+
+}
+
+template<class T, class F>
+static inline void insertionsort(T *buf, int n, F fun)
+{
+    insertionsort(buf, buf+n, fun);
+}
+
+template<class T>
+static inline void insertionsort(T *buf, int n)
+{
+    insertionsort(buf, buf+n, sortless());
+}
+
+template<class T, class F>
+static inline void quicksort(T *start, T *end, F fun)
+{
+    while(end-start > 10)
+    {
+        T *mid = &start[(end-start)/2], *i = start+1, *j = end-2, pivot;
+        if(fun(*start, *mid)) /* start < mid */
+        {
+            if(fun(end[-1], *start)) { pivot = *start; *start = end[-1]; end[-1] = *mid; } /* end < start < mid */
+            else if(fun(end[-1], *mid)) { pivot = end[-1]; end[-1] = *mid; } /* start <= end < mid */
+            else { pivot = *mid; } /* start < mid <= end */
+        }
+        else if(fun(*start, end[-1])) { pivot = *start; *start = *mid; } /*mid <= start < end */
+        else if(fun(*mid, end[-1])) { pivot = end[-1]; end[-1] = *start; *start = *mid; } /* mid < end <= start */
+        else { pivot = *mid; swap(*start, end[-1]); }  /* end <= mid <= start */
+        *mid = end[-2];
+        do
+        {
+            while(fun(*i, pivot)) if(++i >= j) goto partitioned;
+            while(fun(pivot, *--j)) if(i >= j) goto partitioned;
+            swap(*i, *j);
+        }
+        while(++i < j);
+    partitioned:
+        end[-2] = *i;
+        *i = pivot;
+
+        if(i-start < end-(i+1))
+        {
+            quicksort(start, i, fun);
+            start = i+1;
+        }
+        else
+        {
+            quicksort(i+1, end, fun);
+            end = i;
+        }
+    }
+
+    insertionsort(start, end, fun);
+}
+
+template<class T, class F>
+static inline void quicksort(T *buf, int n, F fun)
+{
+    quicksort(buf, buf+n, fun);
+}
+
+template<class T>
+static inline void quicksort(T *buf, int n)
+{
+    quicksort(buf, buf+n, sortless());
+}
+
+template<class T> struct isclass
+{
+    template<class C> static char test(void (C::*)(void));
+    template<class C> static int test(...);
+    enum { yes = sizeof(test<T>(0)) == 1 ? 1 : 0, no = yes^1 };
+};
+
+static inline uint hthash(const char *key)
+{
+    uint h = 5381;
+    for(int i = 0, k; (k = key[i]); i++) h = ((h<<5)+h)^k;    // bernstein k=33 xor
+    return h;
+}
+
+static inline bool htcmp(const char *x, const char *y)
+{
+    return !strcmp(x, y);
+}
+
+struct stringslice
+{
+    const char *str;
+    int len;
+    stringslice() {}
+    stringslice(const char *str, int len) : str(str), len(len) {}
+    stringslice(const char *str, const char *end) : str(str), len(int(end-str)) {}
+
+    const char *end() const { return &str[len]; }
+};
+
+inline char *newstring(const stringslice &s) { return newstring(s.str, s.len); }
+inline const char *stringptr(const char *s) { return s; }
+inline const char *stringptr(const stringslice &s) { return s.str; }
+inline int stringlen(const char *s) { return int(strlen(s)); }
+inline int stringlen(const stringslice &s) { return s.len; }
+
+inline char *copystring(char *d, const stringslice &s, size_t len)
+{
+    size_t slen = min(size_t(s.len), len-1);
+    memcpy(d, s.str, slen);
+    d[slen] = 0;
+    return d;
+}
+template<size_t N> inline char *copystring(char (&d)[N], const stringslice &s) { return copystring(d, s, N); }
+
+static inline uint memhash(const void *ptr, int len)
+{
+    const uchar *data = (const uchar *)ptr;
+    uint h = 5381;
+    loopi(len) h = ((h<<5)+h)^data[i];
+    return h;
+}
+
+static inline uint hthash(const stringslice &s) { return memhash(s.str, s.len); }
+
+static inline bool htcmp(const stringslice &x, const char *y)
+{
+    return x.len == (int)strlen(y) && !memcmp(x.str, y, x.len);
+}
+
+static inline uint hthash(int key)
+{
+    return key;
+}
+
+static inline bool htcmp(int x, int y)
+{
+    return x==y;
+}
+
+#ifndef STANDALONE
+static inline uint hthash(GLuint key)
+{
+    return key;
+}
+
+static inline bool htcmp(GLuint x, GLuint y)
+{
+    return x==y;
+}
+#endif
+
+template <class T> struct vector
+{
+    static const int MINSIZE = 8;
+
+    T *buf;
+    int alen, ulen;
+
+    vector() : buf(NULL), alen(0), ulen(0)
+    {
+    }
+
+    vector(const vector &v) : buf(NULL), alen(0), ulen(0)
+    {
+        *this = v;
+    }
+
+    ~vector() { shrink(0); if(buf) delete[] (uchar *)buf; }
+
+    vector<T> &operator=(const vector<T> &v)
+    {
+        shrink(0);
+        if(v.length() > alen) growbuf(v.length());
+        loopv(v) add(v[i]);
+        return *this;
+    }
+
+    T &add(const T &x)
+    {
+        if(ulen==alen) growbuf(ulen+1);
+        new (&buf[ulen]) T(x);
+        return buf[ulen++];
+    }
+
+    T &add()
+    {
+        if(ulen==alen) growbuf(ulen+1);
+        new (&buf[ulen]) T;
+        return buf[ulen++];
+    }
+
+    T &dup()
+    {
+        if(ulen==alen) growbuf(ulen+1);
+        new (&buf[ulen]) T(buf[ulen-1]);
+        return buf[ulen++];
+    }
+
+    void move(vector<T> &v)
+    {
+        if(!ulen)
+        {
+            swap(buf, v.buf);
+            swap(ulen, v.ulen);
+            swap(alen, v.alen);
+        }
+        else
+        {
+            growbuf(ulen+v.ulen);
+            if(v.ulen) memcpy(&buf[ulen], (void  *)v.buf, v.ulen*sizeof(T));
+            ulen += v.ulen;
+            v.ulen = 0;
+        }
+    }
+
+    bool inrange(size_t i) const { return i<size_t(ulen); }
+    bool inrange(int i) const { return i>=0 && i<ulen; }
+
+    T &pop() { return buf[--ulen]; }
+    T &last() { return buf[ulen-1]; }
+    void drop() { ulen--; buf[ulen].~T(); }
+    bool empty() const { return ulen==0; }
+
+    int capacity() const { return alen; }
+    int length() const { return ulen; }
+    T &operator[](int i) { ASSERT(i>=0 && i<ulen); return buf[i]; }
+    const T &operator[](int i) const { ASSERT(i >= 0 && i<ulen); return buf[i]; }
+
+    void disown() { buf = NULL; alen = ulen = 0; }
+
+    void shrink(int i) { ASSERT(i<=ulen); if(isclass<T>::no) ulen = i; else while(ulen>i) drop(); }
+    void setsize(int i) { ASSERT(i<=ulen); ulen = i; }
+
+    void deletecontents() { while(!empty()) delete   pop(); }
+    void deletearrays() { while(!empty()) delete[] pop(); }
+
+    T *getbuf() { return buf; }
+    const T *getbuf() const { return buf; }
+    bool inbuf(const T *e) const { return e >= buf && e < &buf[ulen]; }
+
+    template<class F>
+    void sort(F fun, int i = 0, int n = -1)
+    {
+        quicksort(&buf[i], n < 0 ? ulen-i : n, fun);
+    }
+
+    void sort() { sort(sortless()); }
+    void sortname() { sort(sortnameless()); }
+
+    void growbuf(int sz)
+    {
+        int olen = alen;
+        if(alen <= 0) alen = max(MINSIZE, sz);
+        else while(alen < sz) alen += alen/2;
+        if(alen <= olen) return;
+        uchar *newbuf = new uchar[alen*sizeof(T)];
+        if(olen > 0)
+        {
+            if(ulen > 0) memcpy(newbuf, (void *)buf, ulen*sizeof(T));
+            delete[] (uchar *)buf;
+        }
+        buf = (T *)newbuf;
+    }
+
+    databuf<T> reserve(int sz)
+    {
+        if(alen-ulen < sz) growbuf(ulen+sz);
+        return databuf<T>(&buf[ulen], sz);
+    }
+
+    void advance(int sz)
+    {
+        ulen += sz;
+    }
+
+    void addbuf(const databuf<T> &p)
+    {
+        advance(p.length());
+    }
+
+    T *pad(int n)
+    {
+        T *buf = reserve(n).buf;
+        advance(n);
+        return buf;
+    }
+
+    void put(const T &v) { add(v); }
+
+    void put(const T *v, int n)
+    {
+        databuf<T> buf = reserve(n);
+        buf.put(v, n);
+        addbuf(buf);
+    }
+
+    void remove(int i, int n)
+    {
+        for(int p = i+n; p<ulen; p++) buf[p-n] = buf[p];
+        ulen -= n;
+    }
+
+    T remove(int i)
+    {
+        T e = buf[i];
+        for(int p = i+1; p<ulen; p++) buf[p-1] = buf[p];
+        ulen--;
+        return e;
+    }
+
+    T removeunordered(int i)
+    {
+        T e = buf[i];
+        ulen--;
+        if(ulen>0) buf[i] = buf[ulen];
+        return e;
+    }
+
+    template<class U>
+    int find(const U &o)
+    {
+        loopi(ulen) if(buf[i]==o) return i;
+        return -1;
+    }
+
+    void addunique(const T &o)
+    {
+        if(find(o) < 0) add(o);
+    }
+
+    void removeobj(const T &o)
+    {
+        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)
+    {
+        if(!ulen) return;
+        loopi(ulen-1) if(buf[i]==o)
+        {
+            buf[i] = buf[ulen-1];
+            break;
+        }
+        ulen--;
+    }
+
+    T &insert(int i, const T &e)
+    {
+        add(T());
+        for(int p = ulen-1; p>i; p--) buf[p] = buf[p-1];
+        buf[i] = e;
+        return buf[i];
+    }
+
+    T *insert(int i, const T *e, int n)
+    {
+        if(alen-ulen < n) growbuf(ulen+n);
+        loopj(n) add(T());
+        for(int p = ulen-1; p>=i+n; p--) buf[p] = buf[p-n];
+        loopj(n) buf[i+j] = e[j];
+        return &buf[i];
+    }
+
+    void reverse()
+    {
+        loopi(ulen/2) swap(buf[i], buf[ulen-1-i]);
+    }
+
+    static int heapparent(int i) { return (i - 1) >> 1; }
+    static int heapchild(int i) { return (i << 1) + 1; }
+
+    void buildheap()
+    {
+        for(int i = ulen/2; i >= 0; i--) downheap(i);
+    }
+
+    int upheap(int i)
+    {
+        float score = heapscore(buf[i]);
+        while(i > 0)
+        {
+            int pi = heapparent(i);
+            if(score >= heapscore(buf[pi])) break;
+            swap(buf[i], buf[pi]);
+            i = pi;
+        }
+        return i;
+    }
+
+    T &addheap(const T &x)
+    {
+        add(x);
+        return buf[upheap(ulen-1)];
+    }
+
+    int downheap(int i)
+    {
+        float score = heapscore(buf[i]);
+        for(;;)
+        {
+            int ci = heapchild(i);
+            if(ci >= ulen) break;
+            float cscore = heapscore(buf[ci]);
+            if(score > cscore)
+            {
+               if(ci+1 < ulen && heapscore(buf[ci+1]) < cscore) { swap(buf[ci+1], buf[i]); i = ci+1; }
+               else { swap(buf[ci], buf[i]); i = ci; }
+            }
+            else if(ci+1 < ulen && heapscore(buf[ci+1]) < score) { swap(buf[ci+1], buf[i]); i = ci+1; }
+            else break;
+        }
+        return i;
+    }
+
+    T removeheap()
+    {
+        T e = removeunordered(0);
+        if(ulen) downheap(0);
+        return e;
+    }
+
+    template<class K> 
+    int htfind(const K &key)
+    {
+        loopi(ulen) if(htcmp(key, buf[i])) return i;
+        return -1;
+    }
+};
+
+template<class H, class E, class K, class T> struct hashbase
+{
+    typedef E elemtype;
+    typedef K keytype;
+    typedef T datatype;
+
+    enum { CHUNKSIZE = 64 };
+
+    struct chain { E elem; chain *next; };
+    struct chainchunk { chain chains[CHUNKSIZE]; chainchunk *next; };
+
+    int size;
+    int numelems;
+    chain **chains;
+
+    chainchunk *chunks;
+    chain *unused;
+
+    enum { DEFAULTSIZE = 1<<10 };
+
+    hashbase(int size = DEFAULTSIZE)
+      : size(size)
+    {
+        numelems = 0;
+        chunks = NULL;
+        unused = NULL;
+        chains = new chain *[size];
+        memset(chains, 0, size*sizeof(chain *));
+    }
+
+    ~hashbase()
+    {
+        DELETEA(chains);
+        deletechunks();
+    }
+
+    chain *insert(uint h)
+    {
+        if(!unused)
+        {
+            chainchunk *chunk = new chainchunk;
+            chunk->next = chunks;
+            chunks = chunk;
+            loopi(CHUNKSIZE-1) chunk->chains[i].next = &chunk->chains[i+1];
+            chunk->chains[CHUNKSIZE-1].next = unused;
+            unused = chunk->chains;
+        }
+        chain *c = unused;
+        unused = unused->next;
+        c->next = chains[h];
+        chains[h] = c;
+        numelems++;
+        return c;
+    }
+
+    template<class U>
+    T &insert(uint h, const U &key)
+    {
+        chain *c = insert(h);
+        H::setkey(c->elem, key);
+        return H::getdata(c->elem);
+    }
+
+    #define HTFIND(success, fail) \
+        uint h = hthash(key)&(this->size-1); \
+        for(chain *c = this->chains[h]; c; c = c->next) \
+        { \
+            if(htcmp(key, H::getkey(c->elem))) return success H::getdata(c->elem); \
+        } \
+        return (fail);
+
+    template<class U>
+    T *access(const U &key)
+    {
+        HTFIND(&, NULL);
+    }
+
+    template<class U, class V>
+    T &access(const U &key, const V &elem)
+    {
+        HTFIND( , insert(h, key) = elem);
+    }
+
+    template<class U>
+    T &operator[](const U &key)
+    {
+        HTFIND( , insert(h, key));
+    }
+
+    template<class U>
+    T &find(const U &key, T &notfound)
+    {
+        HTFIND( , notfound);
+    }
+
+    template<class U>
+    const T &find(const U &key, const T &notfound)
+    {
+        HTFIND( , notfound);
+    }
+
+    template<class U>
+    bool remove(const U &key)
+    {
+        uint h = hthash(key)&(size-1);
+        for(chain **p = &chains[h], *c = chains[h]; c; p = &c->next, c = c->next)
+        {
+            if(htcmp(key, H::getkey(c->elem)))
+            {
+                *p = c->next;
+                c->elem.~E();
+                new (&c->elem) E;
+                c->next = unused;
+                unused = c;
+                numelems--;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void deletechunks()
+    {
+        for(chainchunk *nextchunk; chunks; chunks = nextchunk)
+        {
+            nextchunk = chunks->next;
+            delete chunks;
+        }
+    }
+
+    void clear()
+    {
+        if(!numelems) return;
+        memset(chains, 0, size*sizeof(chain *));
+        numelems = 0;
+        unused = NULL;
+        deletechunks();
+    }
+
+    static inline chain *enumnext(void *i) { return ((chain *)i)->next; }
+    static inline K &enumkey(void *i) { return H::getkey(((chain *)i)->elem); }
+    static inline T &enumdata(void *i) { return H::getdata(((chain *)i)->elem); }
+};
+
+template<class T> struct hashset : hashbase<hashset<T>, T, T, T>
+{
+    typedef hashbase<hashset<T>, T, T, T> basetype;
+
+    hashset(int size = basetype::DEFAULTSIZE) : basetype(size) {}
+
+    static inline const T &getkey(const T &elem) { return elem; }
+    static inline T &getdata(T &elem) { return elem; }
+    template<class K> static inline void setkey(T &elem, const K &key) {}
+
+    template<class V>
+    T &add(const V &elem)
+    {
+        return basetype::access(elem, elem);
+    }
+};
+
+template<class T> struct hashnameset : hashbase<hashnameset<T>, T, const char *, T>
+{
+    typedef hashbase<hashnameset<T>, T, const char *, T> basetype;
+
+    hashnameset(int size = basetype::DEFAULTSIZE) : basetype(size) {}
+
+    template<class U> static inline const char *getkey(const U &elem) { return elem.name; }
+    template<class U> static inline const char *getkey(U *elem) { return elem->name; }
+    static inline T &getdata(T &elem) { return elem; }
+    template<class K> static inline void setkey(T &elem, const K &key) {}
+
+    template<class V>
+    T &add(const V &elem)
+    {
+        return basetype::access(getkey(elem), elem);
+    }
+};
+
+template<class K, class T> struct hashtableentry
+{
+    K key;
+    T data;
+};
+
+template<class K, class T> struct hashtable : hashbase<hashtable<K, T>, hashtableentry<K, T>, K, T>
+{
+    typedef hashbase<hashtable<K, T>, hashtableentry<K, T>, K, T> basetype;
+    typedef typename basetype::elemtype elemtype;
+
+    hashtable(int size = basetype::DEFAULTSIZE) : basetype(size) {}
+
+    static inline K &getkey(elemtype &elem) { return elem.key; }
+    static inline T &getdata(elemtype &elem) { return elem.data; }
+    template<class U> static inline void setkey(elemtype &elem, const U &key) { elem.key = key; }
+};
+
+#define enumeratekt(ht,k,e,t,f,b) loopi((ht).size) for(void *ec = (ht).chains[i]; ec;) { k &e = (ht).enumkey(ec); t &f = (ht).enumdata(ec); ec = (ht).enumnext(ec); b; }
+#define enumerate(ht,t,e,b)       loopi((ht).size) for(void *ec = (ht).chains[i]; ec;) { t &e = (ht).enumdata(ec); ec = (ht).enumnext(ec); b; }
+
+struct unionfind
+{
+    struct ufval
+    {
+        int rank, next;
+
+        ufval() : rank(0), next(-1) {}
+    };
+
+    vector<ufval> ufvals;
+
+    int find(int k)
+    {
+        if(k>=ufvals.length()) return k;
+        while(ufvals[k].next>=0) k = ufvals[k].next;
+        return k;
+    }
+
+    int compressfind(int k)
+    {
+        if(ufvals[k].next<0) return k;
+        return ufvals[k].next = compressfind(ufvals[k].next);
+    }
+
+    void unite (int x, int y)
+    {
+        while(ufvals.length() <= max(x, y)) ufvals.add();
+        x = compressfind(x);
+        y = compressfind(y);
+        if(x==y) return;
+        ufval &xval = ufvals[x], &yval = ufvals[y];
+        if(xval.rank < yval.rank) xval.next = y;
+        else
+        {
+            yval.next = x;
+            if(xval.rank==yval.rank) yval.rank++;
+        }
+    }
+};
+
+template <class T, int SIZE> struct queue
+{
+    int head, tail, len;
+    T data[SIZE];
+
+    queue() { clear(); }
+
+    void clear() { head = tail = len = 0; }
+
+    int capacity() const { return SIZE; }
+    int length() const { return len; }
+    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()
+    {
+        T &t = data[tail];
+        tail++;
+        if(tail >= SIZE) tail -= SIZE;
+        if(len < SIZE) len++;
+        return t;
+    }
+    T &add(const T &e) { return add() = e; }
+
+    databuf<T> reserve(int sz)
+    {
+        if(!len) head = tail = 0;
+        return databuf<T>(&data[tail], min(sz, SIZE-tail));
+    }
+
+    void advance(int sz)
+    {
+        if(len + sz > SIZE) sz = SIZE - len;
+        tail += sz;
+        if(tail >= SIZE) tail -= SIZE;
+        len += sz;
+    }
+
+    void addbuf(const databuf<T> &p)
+    {
+        advance(p.length());
+    }
+
+    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()
+    {
+        T &t = data[head];
+        head++;
+        if(head >= SIZE) head -= SIZE;
+        len--; 
+        return t;
+    }
+
+    T remove(int offset)
+    {
+        T val = removing(offset);
+        if(head+offset >= SIZE) for(int i = head+offset - SIZE + 1; i < tail; i++) data[i-1] = data[i];
+        else if(head < tail) for(int i = head+offset + 1; i < tail; i++) data[i-1] = data[i];
+        else
+        {
+            for(int i = head+offset + 1; i < SIZE; i++) data[i-1] = data[i];
+            data[SIZE-1] = data[0];
+            for(int i = 1; i < tail; i++) data[i-1] = data[i];
+        }
+        tail--;
+        if(tail < 0) tail += SIZE;
+        len--;
+        return val;
+    }
+
+    T &operator[](int offset) { return removing(offset); }
+    const T &operator[](int offset) const { return removing(offset); }
+};
+
+template <class T, int SIZE> struct reversequeue : queue<T, SIZE>
+{
+    T &operator[](int offset) { return queue<T, SIZE>::added(offset); }
+    const T &operator[](int offset) const { return queue<T, SIZE>::added(offset); }
+};
+
+const int islittleendian = 1;
+#ifdef SDL_BYTEORDER
+#define endianswap16 SDL_Swap16
+#define endianswap32 SDL_Swap32
+#define endianswap64 SDL_Swap64
+#else
+inline ushort endianswap16(ushort n) { return (n<<8) | (n>>8); }
+inline uint endianswap32(uint n) { return (n<<24) | (n>>24) | ((n>>8)&0xFF00) | ((n<<8)&0xFF0000); }
+inline ullong endianswap64(ullong n) { return endianswap32(uint(n >> 32)) | ((ullong)endianswap32(uint(n)) << 32); }
+#endif
+template<class T> inline T endianswap(T n) { union { T t; uint i; } conv; conv.t = n; conv.i = endianswap32(conv.i); return conv.t; }
+template<> inline ushort endianswap<ushort>(ushort n) { return endianswap16(n); }
+template<> inline short endianswap<short>(short n) { return endianswap16(n); }
+template<> inline uint endianswap<uint>(uint n) { return endianswap32(n); }
+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, 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, size_t len) {}
+#ifdef SDL_BYTEORDER
+#if SDL_BYTEORDER == SDL_LIL_ENDIAN
+#define lilswap endiansame
+#define bigswap endianswap
+#else
+#define lilswap endianswap
+#define bigswap endiansame
+#endif
+#elif defined(__BYTE_ORDER__)
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define lilswap endiansame
+#define bigswap endianswap
+#else
+#define lilswap endianswap
+#define bigswap endiansame
+#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, 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, 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 */
+#ifdef getchar
+#undef getchar
+#endif
+#ifdef putchar
+#undef putchar
+#endif
+
+struct stream
+{
+#ifdef WIN32
+#if defined(__GNUC__) && !defined(__MINGW32__)
+    typedef off64_t offset;
+#else
+    typedef __int64 offset;
+#endif
+#else
+    typedef off_t offset;
+#endif
+
+    virtual ~stream() {}
+    virtual void close() = 0;
+    virtual bool end() = 0;
+    virtual offset tell() { return -1; }
+    virtual offset rawtell() { return tell(); }
+    virtual bool seek(offset pos, int whence = SEEK_SET) { return false; }
+    virtual offset size();
+    virtual offset rawsize() { return size(); }
+    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, 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 size_t printf(const char *fmt, ...) PRINTFARGS(2, 3);
+    virtual uint getcrc() { return 0; }
+
+    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> 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>()); }
+
+#ifndef STANDALONE
+    SDL_RWops *rwops();
+#endif
+};
+
+template<class T>
+struct streambuf
+{
+    stream *s;
+
+    streambuf(stream *s) : s(s) {}
+    
+    T get() { return s->get<T>(); }
+    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, size_t numvals) { s->put(vals, numvals); } 
+    size_t length() { return s->size(); }
+};
+
+enum
+{
+    CT_PRINT   = 1<<0,
+    CT_SPACE   = 1<<1,
+    CT_DIGIT   = 1<<2,
+    CT_ALPHA   = 1<<3,
+    CT_LOWER   = 1<<4,
+    CT_UPPER   = 1<<5,
+    CT_UNICODE = 1<<6
+};
+extern const uchar cubectype[256];
+static inline int iscubeprint(uchar c) { return cubectype[c]&CT_PRINT; }
+static inline int iscubespace(uchar c) { return cubectype[c]&CT_SPACE; }
+static inline int iscubealpha(uchar c) { return cubectype[c]&CT_ALPHA; }
+static inline int iscubealnum(uchar c) { return cubectype[c]&(CT_ALPHA|CT_DIGIT); }
+static inline int iscubelower(uchar c) { return cubectype[c]&CT_LOWER; }
+static inline int iscubeupper(uchar c) { return cubectype[c]&CT_UPPER; }
+static inline int iscubepunct(uchar c) { return cubectype[c] == CT_PRINT; }
+static inline int cube2uni(uchar c)
+{ 
+    extern const int cube2unichars[256]; 
+    return cube2unichars[c]; 
+}
+static inline uchar uni2cube(int c)
+{
+    extern const int uni2cubeoffsets[8];
+    extern const uchar uni2cubechars[];
+    return uint(c) <= 0x7FF ? uni2cubechars[uni2cubeoffsets[c>>8] + (c&0xFF)] : 0;
+}
+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 string homedir;
+
+extern char *makerelpath(const char *dir, const char *file, const char *prefix = NULL, const char *cmd = NULL);
+extern char *path(char *s);
+extern char *path(const char *s, bool copy);
+extern const char *parentdir(const char *directory);
+extern bool fileexists(const char *path, const char *mode);
+extern bool createdir(const char *path);
+extern size_t fixpackagedir(char *dir);
+extern const char *sethomedir(const char *dir);
+extern const char *addpackagedir(const char *dir);
+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, 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);
+extern void seedMT(uint seed);
+extern uint randomMT();
+
+extern void putint(ucharbuf &p, int n);
+extern void putint(packetbuf &p, int n);
+extern void putint(vector<uchar> &p, int n);
+extern int getint(ucharbuf &p);
+extern void putuint(ucharbuf &p, int n);
+extern void putuint(packetbuf &p, int n);
+extern void putuint(vector<uchar> &p, int n);
+extern int getuint(ucharbuf &p);
+extern void putfloat(ucharbuf &p, float f);
+extern void putfloat(packetbuf &p, float f);
+extern void putfloat(vector<uchar> &p, float f);
+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, size_t len);
+template<size_t N> static inline void getstring(char (&t)[N], ucharbuf &p) { getstring(t, p, N); }
+extern void filtertext(char *dst, const char *src, bool whitespace, bool forcespace, size_t len);
+template<size_t N> static inline void filtertext(char (&dst)[N], const char *src, bool whitespace = true, bool forcespace = false) { filtertext(dst, src, whitespace, forcespace, N-1); }
+
+struct ipmask
+{
+    enet_uint32 ip, mask;
+
+    void parse(const char *name);
+    int print(char *buf) const;
+    bool check(enet_uint32 host) const { return (host & mask) == ip; }
+};
+
+#endif
+
diff --git a/src/shared/zip.cpp b/src/shared/zip.cpp
new file mode 100644 (file)
index 0000000..c35fa87
--- /dev/null
@@ -0,0 +1,588 @@
+#include "cube.h"
+
+enum
+{
+    ZIP_LOCAL_FILE_SIGNATURE = 0x04034B50,
+    ZIP_LOCAL_FILE_SIZE      = 30,
+    ZIP_FILE_SIGNATURE       = 0x02014B50,
+    ZIP_FILE_SIZE            = 46,
+    ZIP_DIRECTORY_SIGNATURE  = 0x06054B50,
+    ZIP_DIRECTORY_SIZE       = 22
+};
+
+struct ziplocalfileheader
+{
+    uint signature;
+    ushort version, flags, compression, modtime, moddate;
+    uint crc32, compressedsize, uncompressedsize;
+    ushort namelength, extralength;
+};
+
+struct zipfileheader
+{
+    uint signature;
+    ushort version, needversion, flags, compression, modtime, moddate;
+    uint crc32, compressedsize, uncompressedsize; 
+    ushort namelength, extralength, commentlength, disknumber, internalattribs;
+    uint externalattribs, offset;
+};
+
+struct zipdirectoryheader
+{
+    uint signature;
+    ushort disknumber, directorydisk, diskentries, entries;
+    uint size, offset;
+    ushort commentlength;
+};
+
+struct zipfile
+{
+    char *name;
+    uint header, offset, size, compressedsize;
+
+    zipfile() : name(NULL), header(0), offset(~0U), size(0), compressedsize(0)
+    {
+    }
+    ~zipfile() 
+    { 
+        DELETEA(name); 
+    }
+};
+
+struct zipstream;
+
+struct ziparchive
+{
+    char *name;
+    FILE *data;
+    hashnameset<zipfile> files;
+    int openfiles;
+    zipstream *owner;
+
+    ziparchive() : name(NULL), data(NULL), files(512), openfiles(0), owner(NULL)
+    {
+    }
+    ~ziparchive()
+    {
+        DELETEA(name);
+        if(data) { fclose(data); data = NULL; }
+    }
+};
+
+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;
+    long end = max(offset - 0xFFFFL - ZIP_DIRECTORY_SIZE, 0L);
+    size_t len = 0;
+    const uint signature = lilswap<uint>(ZIP_DIRECTORY_SIGNATURE);
+
+    while(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 || 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(!src || &buf[len] - src < ZIP_DIRECTORY_SIZE) return false;
+
+    hdr.signature = lilswap(*(uint *)src); src += 4;
+    hdr.disknumber = lilswap(*(ushort *)src); src += 2;
+    hdr.directorydisk = lilswap(*(ushort *)src); src += 2;
+    hdr.diskentries = lilswap(*(ushort *)src); src += 2;
+    hdr.entries = lilswap(*(ushort *)src); src += 2;
+    hdr.size = lilswap(*(uint *)src); src += 4;
+    hdr.offset = lilswap(*(uint *)src); src += 4;
+    hdr.commentlength = lilswap(*(ushort *)src); src += 2;
+
+    if(hdr.signature != ZIP_DIRECTORY_SIGNATURE || hdr.disknumber != hdr.directorydisk || hdr.diskentries != hdr.entries) return false;
+
+    return true;
+}
+
+#ifndef STANDALONE
+VAR(dbgzip, 0, 0, 1);
+#endif
+
+static bool readzipdirectory(const char *archname, FILE *f, int entries, int offset, uint size, vector<zipfile> &files)
+{
+    uchar *buf = new (false) uchar[size], *src = buf;
+    if(!buf || 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;
+
+        zipfileheader hdr;
+        hdr.signature = lilswap(*(uint *)src); src += 4;
+        hdr.version = lilswap(*(ushort *)src); src += 2;
+        hdr.needversion = lilswap(*(ushort *)src); src += 2;
+        hdr.flags = lilswap(*(ushort *)src); src += 2;
+        hdr.compression = lilswap(*(ushort *)src); src += 2;
+        hdr.modtime = lilswap(*(ushort *)src); src += 2;
+        hdr.moddate = lilswap(*(ushort *)src); src += 2;
+        hdr.crc32 = lilswap(*(uint *)src); src += 4;
+        hdr.compressedsize = lilswap(*(uint *)src); src += 4;
+        hdr.uncompressedsize = lilswap(*(uint *)src); src += 4;
+        hdr.namelength = lilswap(*(ushort *)src); src += 2;
+        hdr.extralength = lilswap(*(ushort *)src); src += 2;
+        hdr.commentlength = lilswap(*(ushort *)src); src += 2;
+        hdr.disknumber = lilswap(*(ushort *)src); src += 2;
+        hdr.internalattribs = lilswap(*(ushort *)src); src += 2;
+        hdr.externalattribs = lilswap(*(uint *)src); src += 4;
+        hdr.offset = lilswap(*(uint *)src); src += 4;
+        if(hdr.signature != ZIP_FILE_SIGNATURE) break;
+        if(!hdr.namelength || !hdr.uncompressedsize || (hdr.compression && (hdr.compression != Z_DEFLATED || !hdr.compressedsize)))
+        {
+            src += hdr.namelength + hdr.extralength + hdr.commentlength;
+            continue;
+        }
+        if(src + hdr.namelength > &buf[size]) break;
+
+        string pname;
+        int namelen = min((int)hdr.namelength, (int)sizeof(pname)-1);
+        memcpy(pname, src, namelen);
+        pname[namelen] = '\0';
+        path(pname);
+        char *name = newstring(pname);
+    
+        zipfile &f = files.add();
+        f.name = name;
+        f.header = hdr.offset;
+        f.size = hdr.uncompressedsize;
+        f.compressedsize = hdr.compression ? hdr.compressedsize : 0;
+#ifndef STANDALONE
+        if(dbgzip) conoutf(CON_DEBUG, "%s: file %s, size %d, compress %d, flags %x", archname, name, hdr.uncompressedsize, hdr.compression, hdr.flags);
+#endif
+
+        src += hdr.namelength + hdr.extralength + hdr.commentlength;
+    }
+    delete[] buf;
+
+    return files.length() > 0;
+}
+
+static bool readlocalfileheader(FILE *f, ziplocalfileheader &h, uint offset)
+{
+    uchar buf[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;
+    h.version = lilswap(*(ushort *)src); src += 2;
+    h.flags = lilswap(*(ushort *)src); src += 2;
+    h.compression = lilswap(*(ushort *)src); src += 2;
+    h.modtime = lilswap(*(ushort *)src); src += 2;
+    h.moddate = lilswap(*(ushort *)src); src += 2;
+    h.crc32 = lilswap(*(uint *)src); src += 4;
+    h.compressedsize = lilswap(*(uint *)src); src += 4;
+    h.uncompressedsize = lilswap(*(uint *)src); src += 4;
+    h.namelength = lilswap(*(ushort *)src); src += 2;
+    h.extralength = lilswap(*(ushort *)src); src += 2;
+    if(h.signature != ZIP_LOCAL_FILE_SIGNATURE) return false;
+    // h.uncompressedsize or h.compressedsize may be zero - so don't validate
+    return true;
+}
+
+static vector<ziparchive *> archives;
+
+ziparchive *findzip(const char *name)
+{
+    loopv(archives) if(!strcmp(name, archives[i]->name)) return archives[i];
+    return NULL;
+}
+
+static bool checkprefix(vector<zipfile> &files, const char *prefix, int prefixlen)
+{
+    loopv(files)
+    {
+        if(!strncmp(files[i].name, prefix, prefixlen)) return false;
+    }
+    return true;
+}
+
+static void mountzip(ziparchive &arch, vector<zipfile> &files, const char *mountdir, const char *stripdir)
+{
+    string packagesdir = "packages/";
+    path(packagesdir);
+    size_t striplen = stripdir ? strlen(stripdir) : 0;
+    if(!mountdir && !stripdir) loopv(files)
+    {
+        zipfile &f = files[i];
+        const char *foundpackages = strstr(f.name, packagesdir);
+        if(foundpackages)
+        {
+            if(foundpackages > f.name) 
+            {
+                stripdir = f.name;
+                striplen = foundpackages - f.name;
+            }
+            break;
+        }
+        const char *foundogz = strstr(f.name, ".ogz");
+        if(foundogz)
+        {
+            const char *ogzdir = foundogz;
+            while(--ogzdir >= f.name && *ogzdir != PATHDIV);
+            if(ogzdir < f.name || checkprefix(files, f.name, ogzdir + 1 - f.name))
+            {
+                if(ogzdir >= f.name)
+                {
+                    stripdir = f.name;
+                    striplen = ogzdir + 1 - f.name;
+                }
+                if(!mountdir) mountdir = "packages/base/";
+                break;
+            }
+        }    
+    }
+    string mdir = "", fname;
+    if(mountdir)
+    {
+        copystring(mdir, mountdir);
+        if(fixpackagedir(mdir) <= 1) mdir[0] = '\0';
+    }
+    loopv(files)
+    {
+        zipfile &f = files[i];
+        formatstring(fname, "%s%s", mdir, striplen && !strncmp(f.name, stripdir, striplen) ? &f.name[striplen] : f.name);
+        if(arch.files.access(fname)) continue;
+        char *mname = newstring(fname);
+        zipfile &mf = arch.files[mname];
+        mf = f;
+        mf.name = mname;
+    }
+}
+
+bool addzip(const char *name, const char *mount = NULL, const char *strip = NULL)
+{
+    string pname;
+    copystring(pname, name);
+    path(pname);
+    size_t plen = strlen(pname);
+    if(plen < 4 || !strchr(&pname[plen-4], '.')) concatstring(pname, ".zip");
+
+    ziparchive *exists = findzip(pname);
+    if(exists) 
+    {
+        conoutf(CON_ERROR, "already added zip %s", pname);
+        return true;
+    }
+    FILE *f = fopen(findfile(pname, "rb"), "rb");
+    if(!f) 
+    {
+        conoutf(CON_ERROR, "could not open file %s", pname);
+        return false;
+    }
+    zipdirectoryheader h;
+    vector<zipfile> files;
+    if(!findzipdirectory(f, h) || !readzipdirectory(pname, f, h.entries, h.offset, h.size, files))
+    {
+        conoutf(CON_ERROR, "could not read directory in zip %s", pname);
+        fclose(f);
+        return false;
+    }
+    
+    ziparchive *arch = new ziparchive;
+    arch->name = newstring(pname);
+    arch->data = f;
+    mountzip(*arch, files, mount, strip);
+    archives.add(arch);
+
+    conoutf("added zip %s", pname);
+    return true;
+} 
+     
+bool removezip(const char *name)
+{
+    string pname;
+    copystring(pname, name);
+    path(pname);
+    int plen = (int)strlen(pname);
+    if(plen < 4 || !strchr(&pname[plen-4], '.')) concatstring(pname, ".zip");
+    ziparchive *exists = findzip(pname);
+    if(!exists)
+    {
+        conoutf(CON_ERROR, "zip %s is not loaded", pname);
+        return false;
+    }
+    if(exists->openfiles)
+    {
+        conoutf(CON_ERROR, "zip %s has open files", pname);
+        return false;
+    }
+    conoutf("removed zip %s", exists->name);
+    archives.removeobj(exists); 
+    delete exists;
+    return true;
+}
+
+struct zipstream : stream
+{
+    enum
+    {
+        BUFSIZE  = 16384
+    };
+
+    ziparchive *arch;
+    zipfile *info;
+    z_stream zfile;
+    uchar *buf;
+    uint reading;
+    bool ended;
+
+    zipstream() : arch(NULL), info(NULL), buf(NULL), reading(~0U), ended(false)
+    {
+        zfile.zalloc = NULL;
+        zfile.zfree = NULL;
+        zfile.opaque = NULL;
+        zfile.next_in = zfile.next_out = NULL;
+        zfile.avail_in = zfile.avail_out = 0;
+    }
+
+    ~zipstream()
+    {
+        close();
+    }
+
+    void readbuf(uint size = BUFSIZE)
+    {
+        if(!zfile.avail_in) zfile.next_in = (Bytef *)buf;
+        size = min(size, uint(&buf[BUFSIZE] - &zfile.next_in[zfile.avail_in]));
+        if(arch->owner != this)
+        {
+            arch->owner = NULL;
+            if(fseek(arch->data, reading, SEEK_SET) >= 0) arch->owner = this;
+            else return;
+        }
+        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;
+    }
+
+    bool open(ziparchive *a, zipfile *f)
+    {
+        if(f->offset == ~0U)
+        {
+            ziplocalfileheader h;
+            a->owner = NULL;
+            if(!readlocalfileheader(a->data, h, f->header)) return false;
+            f->offset = f->header + ZIP_LOCAL_FILE_SIZE + h.namelength + h.extralength;
+        }
+
+        if(f->compressedsize && inflateInit2(&zfile, -MAX_WBITS) != Z_OK) return false;
+
+        a->openfiles++;
+        arch = a;
+        info = f;
+        reading = f->offset;
+        ended = false;
+        if(f->compressedsize) buf = new uchar[BUFSIZE];
+        return true;
+    }
+
+    void stopreading()
+    {
+        if(reading == ~0U) return;
+#ifndef STANDALONE
+        if(dbgzip) conoutf(CON_DEBUG, 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 = ~0U;
+    }
+
+    void close()
+    {
+        stopreading();
+        DELETEA(buf);
+        if(arch) { arch->owner = NULL; arch->openfiles--; arch = NULL; }
+    }
+
+    offset size() { return info->size; }
+    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 == ~0U) return false;
+        if(!info->compressedsize)
+        {
+            switch(whence)
+            {
+                case SEEK_END: pos += info->offset + info->size; break; 
+                case SEEK_CUR: pos += reading; break;
+                case SEEK_SET: pos += info->offset; break;
+                default: return false;
+            } 
+            pos = clamp(pos, offset(info->offset), offset(info->offset + info->size));
+            arch->owner = NULL;
+            if(fseek(arch->data, int(pos), SEEK_SET) < 0) return false;
+            arch->owner = this;
+            reading = pos;
+            ended = false;
+            return true;
+        }
+        switch(whence)
+        {
+            case SEEK_END: pos += info->size; break; 
+            case SEEK_CUR: pos += zfile.total_out; break;
+            case SEEK_SET: break;
+            default: return false;
+        }
+
+        if(pos >= (offset)info->size)
+        {
+            reading = info->offset + info->compressedsize;
+            zfile.next_in += zfile.avail_in;
+            zfile.avail_in = 0;
+            zfile.total_in = info->compressedsize; 
+            zfile.total_out = info->size;
+            arch->owner = NULL;
+            ended = false;
+            return true;
+        }
+
+        if(pos < 0) return false;
+        if(pos >= (offset)zfile.total_out) pos -= zfile.total_out;
+        else 
+        {
+            if(zfile.next_in && zfile.total_in <= uint(zfile.next_in - buf))
+            {
+                zfile.avail_in += zfile.total_in;
+                zfile.next_in -= zfile.total_in;
+            }
+            else
+            {
+                arch->owner = NULL;
+                zfile.avail_in = 0;
+                zfile.next_in = NULL;
+                reading = info->offset;
+            }
+            inflateReset(&zfile);
+        }
+
+        uchar skip[512];
+        while(pos > 0)
+        {
+            size_t skipped = (size_t)min(pos, (offset)sizeof(skip));
+            if(read(skip, skipped) != skipped) return false;
+            pos -= skipped;
+        }
+
+        ended = false;
+        return true;
+    }
+
+    size_t read(void *buf, size_t len)
+    {
+        if(reading == ~0U || !buf || !len) return 0;
+        if(!info->compressedsize)
+        {
+            if(arch->owner != this)
+            {
+                arch->owner = NULL;
+                if(fseek(arch->data, reading, SEEK_SET) < 0) { stopreading(); return 0; }
+                arch->owner = this;
+            }
+              
+            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;
+        }
+
+        zfile.next_out = (Bytef *)buf;
+        zfile.avail_out = len;
+        while(zfile.avail_out > 0)
+        {
+            if(!zfile.avail_in) readbuf(BUFSIZE);
+            int err = inflate(&zfile, Z_NO_FLUSH);
+            if(err != Z_OK) 
+            {
+                if(err == Z_STREAM_END) ended = true;
+                else
+                {
+#ifndef STANDALONE
+                    if(dbgzip) conoutf(CON_DEBUG, "inflate error: %s", zError(err));
+#endif
+                    stopreading(); 
+                }
+                break; 
+            }
+        }
+        return len - zfile.avail_out;
+    }
+};
+
+stream *openzipfile(const char *name, const char *mode)
+{
+    for(; *mode; mode++) if(*mode=='w' || *mode=='a') return NULL;
+    loopvrev(archives)
+    {
+        ziparchive *arch = archives[i];
+        zipfile *f = arch->files.access(name);
+        if(!f) continue;
+        zipstream *s = new zipstream;
+        if(s->open(arch, f)) return s;
+        delete s;
+    }
+    return NULL;
+}
+
+bool findzipfile(const char *name)
+{
+    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)
+{
+    size_t extsize = ext ? strlen(ext)+1 : 0, dirsize = strlen(dir);
+    int dirs = 0;
+    loopvrev(archives)
+    {
+        ziparchive *arch = archives[i];
+        int oldsize = files.length();
+        enumerate(arch->files, zipfile, f,
+        {
+            if(strncmp(f.name, dir, dirsize)) continue;
+            const char *name = f.name + dirsize;
+            if(name[0] == PATHDIV) name++;
+            if(strchr(name, PATHDIV)) continue;
+            if(!ext) files.add(newstring(name));
+            else
+            {
+                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++;
+    }
+    return dirs;
+}
+
+#ifndef STANDALONE
+ICOMMAND(addzip, "sss", (const char *name, const char *mount, const char *strip), addzip(name, mount[0] ? mount : NULL, strip[0] ? strip : NULL));
+ICOMMAND(removezip, "s", (const char *name), removezip(name));
+#endif
+