about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/engine/client/ec_client.c246
-rw-r--r--src/engine/e_demorec.c354
-rw-r--r--src/engine/e_demorec.h63
-rw-r--r--src/engine/e_engine.c3
-rw-r--r--src/engine/e_if_client.h21
-rw-r--r--src/engine/e_if_modc.h2
-rw-r--r--src/engine/e_if_other.h6
-rw-r--r--src/engine/e_if_server.h2
-rw-r--r--src/engine/server/es_server.c90
-rw-r--r--src/game/client/clienthooks.cpp1
-rw-r--r--src/game/client/component.hpp1
-rw-r--r--src/game/client/components/hud.cpp3
-rw-r--r--src/game/client/components/mapimages.cpp2
-rw-r--r--src/game/client/components/mapimages.hpp4
-rw-r--r--src/game/client/components/maplayers.cpp2
-rw-r--r--src/game/client/components/menus.cpp92
-rw-r--r--src/game/client/components/menus.hpp39
-rw-r--r--src/game/client/components/menus_browser.cpp4
-rw-r--r--src/game/client/components/menus_demo.cpp390
-rw-r--r--src/game/client/components/menus_ingame.cpp2
-rw-r--r--src/game/client/components/players.cpp4
-rw-r--r--src/game/client/gameclient.cpp90
-rw-r--r--src/game/client/gameclient.hpp5
-rw-r--r--src/game/server/entities/character.cpp2
-rw-r--r--src/game/server/entities/laser.cpp2
-rw-r--r--src/game/server/entities/projectile.cpp4
-rw-r--r--src/game/server/entity.cpp14
-rw-r--r--src/game/server/entity.hpp38
-rw-r--r--src/game/server/eventhandler.cpp4
-rw-r--r--src/game/server/gamecontext.cpp26
-rw-r--r--src/game/server/gamecontext.hpp2
-rw-r--r--src/game/server/gamecontroller.cpp15
32 files changed, 1406 insertions, 127 deletions
diff --git a/src/engine/client/ec_client.c b/src/engine/client/ec_client.c
index 8d8bdad7..3e7e8c27 100644
--- a/src/engine/client/ec_client.c
+++ b/src/engine/client/ec_client.c
@@ -23,6 +23,8 @@
 
 #include <engine/e_huffman.h>
 
+#include <engine/e_demorec.h>
+
 #include <mastersrv/mastersrv.h>
 #include <versionsrv/versionsrv.h>
 
@@ -70,8 +72,13 @@ static char versionstr[10] = "0";
 /* pinging */
 static int64 ping_start_time = 0;
 
+/* */
+static char current_map[256] = {0};
+static int current_map_crc = 0;
+
 /* map download */
 static char mapdownload_filename[256] = {0};
+static char mapdownload_name[256] = {0};
 static IOHANDLE mapdownload_file = 0;
 static int mapdownload_chunk = 0;
 static int mapdownload_crc = 0;
@@ -85,7 +92,6 @@ static SERVER_INFO current_server_info = {0};
 static int current_tick = 0;
 static float intratick = 0;
 static float ticktime = 0;
-
 static int prev_tick = 0;
 
 /* predicted time */
@@ -231,6 +237,9 @@ static SNAPSTORAGE_HOLDER *snapshots[NUM_SNAPSHOT_TYPES];
 static int recived_snapshots;
 static char snapshot_incomming_data[MAX_SNAPSHOT_SIZE];
 
+static SNAPSTORAGE_HOLDER demorec_snapshotholders[NUM_SNAPSHOT_TYPES];
+static char *demorec_snapshotdata[NUM_SNAPSHOT_TYPES][2][MAX_SNAPSHOT_SIZE];
+
 /* --- */
 
 void *snap_get_item(int snapid, int index, SNAP_ITEM *item)
@@ -310,8 +319,15 @@ int client_send_msg()
 		packet.flags = NETSENDFLAG_VITAL;
 	if(info->flags&MSGFLAG_FLUSH)
 		packet.flags = NETSENDFLAG_FLUSH;
+		
+	if(info->flags&MSGFLAG_RECORD)
+	{
+		if(demorec_isrecording())
+			demorec_record_write("MESG", packet.data_size, packet.data);
+	}
 
-	netclient_send(net, &packet);
+	if(!(info->flags&MSGFLAG_NOSEND))
+		netclient_send(net, &packet);
 	return 0;
 }
 
@@ -525,6 +541,10 @@ void client_connect(const char *server_address_str)
 
 void client_disconnect_with_reason(const char *reason)
 {
+	/* stop demo playback */
+	demorec_playback_stop();
+	
+	/* */
 	rcon_authed = 0;
 	netclient_disconnect(net, reason);
 	client_set_state(CLIENTSTATE_OFFLINE);
@@ -659,7 +679,7 @@ static void client_render()
 	client_debug_render();
 }
 
-static const char *client_load_map(const char *filename, int wanted_crc)
+static const char *client_load_map(const char *name, const char *filename, int wanted_crc)
 {
 	static char errormsg[128];
 	DATAFILE *df;
@@ -676,7 +696,7 @@ static const char *client_load_map(const char *filename, int wanted_crc)
 	
 	/* get the crc of the map */
 	crc = datafile_crc(filename);
-	if(crc != wanted_crc)
+	if(0 && crc != wanted_crc) /* TODO: FIX ME!!! */
 	{
 		datafile_unload(df);
 		str_format(errormsg, sizeof(errormsg), "map differs from the server. %08x != %08x", crc, wanted_crc);
@@ -686,6 +706,10 @@ static const char *client_load_map(const char *filename, int wanted_crc)
 	dbg_msg("client", "loaded map '%s'", filename);
 	recived_snapshots = 0;
 	map_set(df);
+	
+	str_copy(current_map, name, sizeof(current_map));
+	current_map_crc = crc;
+	
 	return NULL;
 }
 
@@ -699,14 +723,14 @@ static const char *client_load_map_search(const char *mapname, int wanted_crc)
 	
 	/* try the normal maps folder */
 	str_format(buf, sizeof(buf), "maps/%s.map", mapname);
-	error = client_load_map(buf, wanted_crc);
+	error = client_load_map(mapname, buf, wanted_crc);
 	if(!error)
 		return error;
 
 	/* try the downloaded maps */
 	str_format(buf2, sizeof(buf2), "maps/%s_%8x.map", mapname, wanted_crc);
 	engine_savepath(buf2, buf, sizeof(buf));
-	error = client_load_map(buf, wanted_crc);
+	error = client_load_map(mapname, buf, wanted_crc);
 	return error;
 }
 
@@ -862,6 +886,7 @@ static void client_process_packet(NETCHUNK *packet)
 						dbg_msg("client/network", "starting to download map to '%s'", mapdownload_filename);
 						
 						mapdownload_chunk = 0;
+						str_copy(mapdownload_name, map, sizeof(mapdownload_name));
 						mapdownload_file = io_open(mapdownload_filename, IOFLAG_WRITE);
 						mapdownload_crc = map_crc;
 						mapdownload_totalsize = -1;
@@ -904,7 +929,7 @@ static void client_process_packet(NETCHUNK *packet)
 					mapdownload_totalsize = -1;
 					
 					/* load map */
-					error = client_load_map(mapdownload_filename, mapdownload_crc);
+					error = client_load_map(mapdownload_name, mapdownload_filename, mapdownload_crc);
 					if(!error)
 					{
 						dbg_msg("client/network", "loading done");
@@ -1107,6 +1132,28 @@ static void client_process_packet(NETCHUNK *packet)
 						/* add new */
 						snapstorage_add(&snapshot_storage, game_tick, time_get(), snapsize, (SNAPSHOT*)tmpbuffer3, 1);
 						
+						/* add snapshot to demo */
+						if(demorec_isrecording())
+						{
+							DEMOREC_TICKMARKER marker;
+
+							/* write tick marker */
+							marker.tick = game_tick;
+							swap_endian(&marker, sizeof(int), sizeof(marker)/sizeof(int));
+							demorec_record_write("TICK", sizeof(marker), &marker);
+							
+							/* build snap and possibly add some messages */
+							modc_recordkeyframe();
+							
+							/*
+							snapbuild_init(&builder);
+							mods_snap(-1);
+							snapshot_size = snapbuild_finish(&builder, data);*/
+							
+							/* write snapshot */
+							demorec_record_write("SNAP", snapsize, tmpbuffer3);							
+						}
+						
 						/* apply snapshot, cycle pointers */
 						recived_snapshots++;
 
@@ -1147,6 +1194,9 @@ static void client_process_packet(NETCHUNK *packet)
 		else
 		{
 			/* game message */
+			if(demorec_isrecording())
+				demorec_record_write("MESG", packet->data_size, packet->data);
+
 			modc_message(msg);
 		}
 	}
@@ -1161,20 +1211,23 @@ static void client_pump_network()
 
 	netclient_update(net);
 
-	/* check for errors */
-	if(client_state() != CLIENTSTATE_OFFLINE && netclient_state(net) == NETSTATE_OFFLINE)
+	if(client_state() != CLIENTSTATE_DEMOPLAYBACK)
 	{
-		client_set_state(CLIENTSTATE_OFFLINE);
-		dbg_msg("client", "offline error='%s'", netclient_error_string(net));
-	}
+		/* check for errors */
+		if(client_state() != CLIENTSTATE_OFFLINE && netclient_state(net) == NETSTATE_OFFLINE)
+		{
+			client_set_state(CLIENTSTATE_OFFLINE);
+			dbg_msg("client", "offline error='%s'", netclient_error_string(net));
+		}
 
-	/* */
-	if(client_state() == CLIENTSTATE_CONNECTING && netclient_state(net) == NETSTATE_ONLINE)
-	{
-		/* we switched to online */
-		dbg_msg("client", "connected, sending info");
-		client_set_state(CLIENTSTATE_LOADING);
-		client_send_info();
+		/* */
+		if(client_state() == CLIENTSTATE_CONNECTING && netclient_state(net) == NETSTATE_ONLINE)
+		{
+			/* we switched to online */
+			dbg_msg("client", "connected, sending info");
+			client_set_state(CLIENTSTATE_LOADING);
+			client_send_info();
+		}
 	}
 	
 	/* process packets */
@@ -1182,11 +1235,89 @@ static void client_pump_network()
 		client_process_packet(&packet);
 }
 
+static void client_democallback(DEMOREC_CHUNK chunk, void *data)
+{
+	/* dbg_msg("client/playback", "got %c%c%c%c", chunk.type[0], chunk.type[1], chunk.type[2], chunk.type[3]); */
+	
+	if(mem_comp(chunk.type, "SNAP", 4) == 0)
+	{
+		/* handle snapshots */
+		SNAPSTORAGE_HOLDER *temp = snapshots[SNAP_PREV];
+		snapshots[SNAP_PREV] = snapshots[SNAP_CURRENT];
+		snapshots[SNAP_CURRENT] = temp;
+		
+		mem_copy(snapshots[SNAP_CURRENT]->snap, data, chunk.size);
+		mem_copy(snapshots[SNAP_CURRENT]->alt_snap, data, chunk.size);
+		
+		modc_newsnapshot();
+		modc_predict();
+	}
+	else if(mem_comp(chunk.type, "MESG", 4) == 0)
+	{
+		/* handle messages */
+		int sys = 0;
+		int msg = msg_unpack_start(data, chunk.size, &sys);
+		if(!sys)
+			modc_message(msg);
+	}
+	
+}
+
+const DEMOPLAYBACK_INFO *client_demoplayer_getinfo()
+{
+	static DEMOPLAYBACK_INFO ret;
+	const DEMOREC_PLAYBACKINFO *info = demorec_playback_info();
+	ret.first_tick = info->first_tick;
+	ret.last_tick = info->last_tick;
+	ret.current_tick = info->current_tick;
+	ret.paused = info->paused;
+	ret.speed = info->speed;
+	return &ret;
+}
+
+void client_demoplayer_setpos(float percent)
+{
+	const DEMOREC_PLAYBACKINFO *info = demorec_playback_info();
+	int point = (int)((info->seekable_points-1)*percent);
+	demorec_playback_set(point);
+}
+
+void client_demoplayer_setspeed(float speed)
+{
+	demorec_playback_setspeed(speed);
+}
+
+void client_demoplayer_setpause(int paused)
+{
+	if(paused)
+		demorec_playback_pause();
+	else
+		demorec_playback_unpause();
+}
+
 static void client_update()
 {
-	/* switch snapshot */
-	if(client_state() != CLIENTSTATE_OFFLINE && recived_snapshots >= 3)
+	if(client_state() == CLIENTSTATE_DEMOPLAYBACK)
+	{
+		demorec_playback_update();
+		if(demorec_isplaying())
+		{
+			/* update timers */
+			const DEMOREC_PLAYBACKINFO *info = demorec_playback_info();			
+			current_tick = info->current_tick;
+			prev_tick = info->previous_tick;
+			intratick = info->intratick;
+			ticktime = info->ticktime;
+		}
+		else
+		{
+			/* disconnect on error */
+			client_disconnect();
+		}
+	}
+	else if(client_state() != CLIENTSTATE_OFFLINE && recived_snapshots >= 3)
 	{
+		/* switch snapshot */
 		int repredict = 0;
 		int64 freq = time_freq();
 		int64 now = st_get(&game_time, time_get());
@@ -1393,7 +1524,6 @@ static void client_run()
 		int64 frame_start_time = time_get();
 		frames++;
 		
-		
 		perf_start(&rootscope);
 
 		/* */
@@ -1582,6 +1712,67 @@ static void con_addfavorite(void *result, void *user_data)
 		client_serverbrowse_addfavorite(addr);
 }
 
+void client_demoplayer_play(const char *filename)
+{
+	int crc;
+	client_disconnect();
+	
+	/* try to start playback */
+	demorec_playback_registercallback(client_democallback);
+	
+	if(demorec_playback_load(filename))
+		return;
+	
+	/* load map */
+	crc = (demorec_playback_info()->header.crc[0]<<24)|
+		(demorec_playback_info()->header.crc[1]<<16)|
+		(demorec_playback_info()->header.crc[2]<<8)|
+		(demorec_playback_info()->header.crc[3]);
+	client_load_map_search(demorec_playback_info()->header.map, crc);
+	modc_connected();
+	
+	/* setup buffers */	
+	mem_zero(demorec_snapshotdata, sizeof(demorec_snapshotdata));
+
+	snapshots[SNAP_CURRENT] = &demorec_snapshotholders[SNAP_CURRENT];
+	snapshots[SNAP_PREV] = &demorec_snapshotholders[SNAP_PREV];
+	
+	snapshots[SNAP_CURRENT]->snap = (SNAPSHOT *)demorec_snapshotdata[SNAP_CURRENT][0];
+	snapshots[SNAP_CURRENT]->alt_snap = (SNAPSHOT *)demorec_snapshotdata[SNAP_CURRENT][1];
+	snapshots[SNAP_CURRENT]->snap_size = 0;
+	snapshots[SNAP_CURRENT]->tick = -1;
+	
+	snapshots[SNAP_PREV]->snap = (SNAPSHOT *)demorec_snapshotdata[SNAP_PREV][0];
+	snapshots[SNAP_PREV]->alt_snap = (SNAPSHOT *)demorec_snapshotdata[SNAP_PREV][1];
+	snapshots[SNAP_PREV]->snap_size = 0;
+	snapshots[SNAP_PREV]->tick = -1;
+
+	/* enter demo playback state */
+	client_set_state(CLIENTSTATE_DEMOPLAYBACK);
+	
+	demorec_playback_play();
+	modc_entergame();
+}
+
+static void con_play(void *result, void *user_data)
+{
+	client_demoplayer_play(console_arg_string(result, 0));
+}
+
+static void con_record(void *result, void *user_data)
+{
+	char filename[512];
+	char path[512];
+	str_format(filename, sizeof(filename), "demos/%s.demo", console_arg_string(result, 0));
+	engine_savepath(filename, path, sizeof(path));
+	demorec_record_start(path, modc_net_version(), current_map, current_map_crc, "client");
+}
+
+static void con_stoprecord(void *result, void *user_data)
+{
+	demorec_record_stop();
+}
+
 static void client_register_commands()
 {
 	MACRO_REGISTER_COMMAND("quit", "", con_quit, 0x0);
@@ -1591,6 +1782,10 @@ static void client_register_commands()
 	MACRO_REGISTER_COMMAND("screenshot", "", con_screenshot, 0x0);
 	MACRO_REGISTER_COMMAND("rcon", "r", con_rcon, 0x0);
 
+	MACRO_REGISTER_COMMAND("play", "r", con_play, 0x0);
+	MACRO_REGISTER_COMMAND("record", "s", con_record, 0);
+	MACRO_REGISTER_COMMAND("stoprecord", "", con_stoprecord, 0);
+
 	MACRO_REGISTER_COMMAND("add_favorite", "s", con_addfavorite, 0x0);
 }
 
@@ -1599,6 +1794,13 @@ void client_save_line(const char *line)
 	engine_config_write_line(line);	
 }
 
+const char *client_user_directory()
+{
+	static char path[1024] = {0};
+	fs_storage_path("Teeworlds", path, sizeof(path));
+	return path;
+}
+
 int main(int argc, char **argv)
 {
 	/* init the engine */
diff --git a/src/engine/e_demorec.c b/src/engine/e_demorec.c
new file mode 100644
index 00000000..67f7f237
--- /dev/null
+++ b/src/engine/e_demorec.c
@@ -0,0 +1,354 @@
+
+#include <base/system.h>
+#include "e_demorec.h"
+#include "e_memheap.h"
+#include "e_if_other.h"
+
+static IOHANDLE record_file = 0;
+static const unsigned char header_marker[8] = {'T', 'W', 'D', 'E', 'M', 'O', 0, 1};
+
+/* Record */
+
+int demorec_isrecording() { return record_file != 0; }
+
+int demorec_record_start(const char *filename, const char *netversion, const char *map, int crc, const char *type)
+{
+	DEMOREC_HEADER header;
+	if(record_file)
+		return -1;
+
+	record_file = io_open(filename, IOFLAG_WRITE);
+	
+	if(!record_file)
+	{
+		dbg_msg("demorec/record", "Unable to open '%s' for recording", filename);
+		return -1;
+	}
+	
+	/* write header */
+	mem_zero(&header, sizeof(header));
+	mem_copy(header.marker, header_marker, sizeof(header.marker));
+	str_copy(header.netversion, netversion, sizeof(header.netversion));
+	str_copy(header.map, map, sizeof(header.map));
+	str_copy(header.type, type, sizeof(header.type));
+	header.crc[0] = (crc>>24)&0xff;
+	header.crc[1] = (crc>>16)&0xff;
+	header.crc[2] = (crc>>8)&0xff;
+	header.crc[3] = (crc)&0xff;
+	io_write(record_file, &header, sizeof(header));
+	
+	dbg_msg("demorec/record", "Recording to '%s'", filename);
+	return 0;
+}
+
+void demorec_record_write(const char *type, int size, const void *data)
+{
+	DEMOREC_CHUNK chunk;
+	if(!record_file)
+		return;
+	
+	chunk.type[0] = type[0];
+	chunk.type[1] = type[1];
+	chunk.type[2] = type[2];
+	chunk.type[3] = type[3];
+	chunk.size = size;
+	swap_endian(&chunk.size, sizeof(int), 1);
+	io_write(record_file, &chunk, sizeof(chunk));
+	io_write(record_file, data, size);
+}
+
+int demorec_record_stop()
+{
+	if(!record_file)
+		return -1;
+		
+	dbg_msg("demorec/record", "Stopped recording");
+	io_close(record_file);
+	record_file = 0;
+	return 0;
+}
+
+/* Playback */
+
+typedef struct KEYFRAME
+{
+	long filepos;
+	int tick;
+} KEYFRAME;
+
+typedef struct KEYFRAME_SEARCH
+{
+	KEYFRAME frame;
+	struct KEYFRAME_SEARCH *next;
+} KEYFRAME_SEARCH;
+
+static IOHANDLE play_file = 0;
+static DEMOREC_PLAYCALLBACK play_callback = 0;
+static KEYFRAME *keyframes = 0;
+
+static DEMOREC_PLAYBACKINFO playbackinfo;
+const DEMOREC_PLAYBACKINFO *demorec_playback_info() { return &playbackinfo; }
+
+int demorec_isplaying() { return play_file != 0; }
+
+int demorec_playback_registercallback(DEMOREC_PLAYCALLBACK cb)
+{
+	play_callback = cb;
+	return 0;
+}
+
+static int read_chunk_header(DEMOREC_CHUNK *chunk)
+{
+	if(io_read(play_file, chunk, sizeof(*chunk)) != sizeof(*chunk))
+		return -1;
+	swap_endian(&chunk->size, sizeof(int), 1);
+	return 0;
+}
+
+static void scan_file()
+{
+	long start_pos;
+	HEAP *heap = 0;
+	KEYFRAME_SEARCH *first_key = 0;
+	KEYFRAME_SEARCH *current_key = 0;
+	DEMOREC_CHUNK chunk;
+	int i;
+	
+	heap = memheap_create();
+
+	start_pos = io_tell(play_file);
+	playbackinfo.seekable_points = 0;
+
+	while(1)
+	{
+		long current_pos = io_tell(play_file);
+
+		if(read_chunk_header(&chunk))
+			break;
+		
+		/* read the chunk */
+			
+		if(mem_comp(chunk.type, "TICK", 4) == 0)
+		{
+			KEYFRAME_SEARCH *key;
+			DEMOREC_TICKMARKER marker;
+			io_read(play_file, &marker, chunk.size);
+			swap_endian(&marker.tick, sizeof(int), 1);
+			
+			/* save the position */
+			key = memheap_allocate(heap, sizeof(KEYFRAME_SEARCH));
+			key->frame.filepos = current_pos;
+			key->frame.tick = marker.tick;
+			key->next = 0;
+			if(current_key)
+				current_key->next = key;
+			if(!first_key)
+				first_key = key;
+			current_key = key;
+			
+			if(playbackinfo.first_tick == -1)
+				playbackinfo.first_tick = marker.tick;
+			playbackinfo.last_tick = marker.tick;
+			playbackinfo.seekable_points++;
+		}
+		else
+			io_skip(play_file, chunk.size);
+	}
+
+	/* copy all the frames to an array instead for fast access */
+	keyframes = (KEYFRAME*)mem_alloc(playbackinfo.seekable_points*sizeof(KEYFRAME), 1);
+	for(current_key = first_key, i = 0; current_key; current_key = current_key->next, i++)
+		keyframes[i] = current_key->frame;
+		
+	/* destroy the temporary heap and seek back to the start */
+	memheap_destroy(heap);
+	io_seek(play_file, start_pos, IOSEEK_START);
+}
+
+static void do_tick()
+{
+	static char buffer[64*1024];
+	DEMOREC_CHUNK chunk;
+
+	/* update ticks */
+	playbackinfo.previous_tick = playbackinfo.current_tick;
+	playbackinfo.current_tick = playbackinfo.next_tick;
+
+	while(1)
+	{
+		int r = read_chunk_header(&chunk);
+		if(chunk.size > sizeof(buffer))
+		{
+			dbg_msg("demorec/playback", "chunk is too big %d", chunk.size);
+			r = 1;
+		}
+		
+		if(r)
+		{
+			/* stop on error or eof */
+			demorec_playback_stop();
+			break;
+		}
+		
+		/* read the chunk */
+		io_read(play_file, buffer, chunk.size);
+			
+		if(mem_comp(chunk.type, "TICK", 4) == 0)
+		{
+			DEMOREC_TICKMARKER marker = *(DEMOREC_TICKMARKER *)buffer;
+			swap_endian(&marker.tick, sizeof(int), 1);
+			playbackinfo.next_tick = marker.tick;
+			break;
+		}
+		else if(play_callback)
+			play_callback(chunk, buffer);
+	}
+}
+
+void demorec_playback_pause()
+{
+	playbackinfo.paused = 1;
+}
+
+void demorec_playback_unpause()
+{
+	if(playbackinfo.paused)
+	{
+		/*playbackinfo.start_tick = playbackinfo.current_tick;
+		playbackinfo.start_time = time_get();*/
+		playbackinfo.paused = 0;
+	}
+}
+
+int demorec_playback_load(const char *filename)
+{
+	play_file = io_open(filename, IOFLAG_READ);
+	if(!play_file)
+	{
+		dbg_msg("demorec/playback", "could not open '%s'", filename);
+		return -1;
+	}
+	
+	/* clear the playback info */
+	mem_zero(&playbackinfo, sizeof(playbackinfo));
+	playbackinfo.first_tick = -1;
+	playbackinfo.last_tick = -1;
+	/*playbackinfo.start_tick = -1;*/
+	playbackinfo.next_tick = -1;
+	playbackinfo.current_tick = -1;
+	playbackinfo.previous_tick = -1;
+	playbackinfo.speed = 1;
+
+	/* read the header */	
+	io_read(play_file, &playbackinfo.header, sizeof(playbackinfo.header));
+	if(mem_comp(playbackinfo.header.marker, header_marker, sizeof(header_marker)) != 0)
+	{
+		dbg_msg("demorec/playback", "'%s' is not a demo file", filename);
+		io_close(play_file);
+		play_file = 0;
+		return -1;
+	}
+	
+	/* scan the file for interessting points */
+	scan_file();
+	
+	/* ready for playback */	
+	return 0;
+}
+
+int demorec_playback_play()
+{
+	/* fill in previous and next tick */
+	while(playbackinfo.previous_tick == -1)
+		do_tick();
+
+	/* set start info */
+	/*playbackinfo.start_tick = playbackinfo.previous_tick;
+	playbackinfo.start_time = time_get();*/
+	playbackinfo.current_time = playbackinfo.previous_tick*time_freq()/SERVER_TICK_SPEED;
+	playbackinfo.last_update = time_get();
+	return 0;
+}
+
+int demorec_playback_set(int keyframe)
+{
+	if(!play_file)
+		return -1;
+	if(keyframe < 0 || keyframe >= playbackinfo.seekable_points)
+		return -1;
+	
+	io_seek(play_file, keyframes[keyframe].filepos, IOSEEK_START);
+
+	/*playbackinfo.start_tick = -1;*/
+	playbackinfo.next_tick = -1;
+	playbackinfo.current_tick = -1;
+	playbackinfo.previous_tick = -1;
+	
+	demorec_playback_play();
+	
+	return 0;
+}
+
+void demorec_playback_setspeed(float speed)
+{
+	playbackinfo.speed = speed;
+}
+
+int demorec_playback_update()
+{
+	int64 now = time_get();
+	int64 deltatime = now-playbackinfo.last_update;
+	playbackinfo.last_update = now;
+	
+	if(playbackinfo.paused)
+	{
+		
+	}
+	else
+	{
+		int64 freq = time_freq();
+		playbackinfo.current_time += (int64)(deltatime*(double)playbackinfo.speed);
+		
+		while(1)
+		{
+			int64 curtick_start = (playbackinfo.current_tick)*freq/SERVER_TICK_SPEED;
+
+			/* break if we are ready */		
+			if(curtick_start > playbackinfo.current_time)
+				break;
+			
+			/* do one more tick */
+			do_tick();
+		}
+
+		/* update intratick */
+		{	
+			int64 curtick_start = (playbackinfo.current_tick)*freq/SERVER_TICK_SPEED;
+			int64 prevtick_start = (playbackinfo.previous_tick)*freq/SERVER_TICK_SPEED;
+			playbackinfo.intratick = (playbackinfo.current_time - prevtick_start) / (float)(curtick_start-prevtick_start);
+			playbackinfo.ticktime = (playbackinfo.current_time - prevtick_start) / (float)freq;
+		}
+		
+		if(playbackinfo.current_tick == playbackinfo.previous_tick ||
+			playbackinfo.current_tick == playbackinfo.next_tick)
+		{
+			dbg_msg("demorec/playback", "tick error prev=%d cur=%d next=%d",
+				playbackinfo.previous_tick, playbackinfo.current_tick, playbackinfo.next_tick);
+		}
+	}
+	
+	return 0;
+}
+
+int demorec_playback_stop(const char *filename)
+{
+	if(!play_file)
+		return -1;
+		
+	dbg_msg("demorec/playback", "Stopped playback");
+	io_close(play_file);
+	play_file = 0;
+	mem_free(keyframes);
+	keyframes = 0;
+	return 0;
+}
diff --git a/src/engine/e_demorec.h b/src/engine/e_demorec.h
new file mode 100644
index 00000000..f6ceb382
--- /dev/null
+++ b/src/engine/e_demorec.h
@@ -0,0 +1,63 @@
+
+typedef struct DEMOREC_HEADER
+{
+	char marker[8];
+	char netversion[64];
+	char map[64];
+	unsigned char crc[4];
+	char type[8];
+} DEMOREC_HEADER;
+
+typedef struct DEMOREC_CHUNK
+{
+	char type[4];
+	int size;
+} DEMOREC_CHUNK;
+
+typedef struct DEMOREC_TICKMARKER
+{
+	int tick;
+} DEMOREC_TICKMARKER;
+
+typedef struct DEMOREC_PLAYBACKINFO
+{
+	DEMOREC_HEADER header;
+	
+	int paused;
+	float speed;
+	
+	int64 last_update;
+	int64 current_time;
+	
+	int first_tick;
+	int last_tick;
+	
+	int seekable_points;
+	
+	int next_tick;
+	int current_tick;
+	int previous_tick;
+	
+	float intratick;
+	float ticktime;
+} DEMOREC_PLAYBACKINFO;
+
+int demorec_record_start(const char *filename, const char *netversion, const char *map, int map_crc, const char *type);
+int demorec_isrecording();
+void demorec_record_write(const char *type, int size, const void *data);
+int demorec_record_stop();
+
+typedef void (*DEMOREC_PLAYCALLBACK)(DEMOREC_CHUNK chunk, void *data);
+
+int demorec_playback_registercallback(DEMOREC_PLAYCALLBACK cb);
+int demorec_playback_load(const char *filename);
+int demorec_playback_play();
+void demorec_playback_pause();
+void demorec_playback_unpause();
+void demorec_playback_setspeed(float speed);
+int demorec_playback_set(int keyframe);
+int demorec_playback_update();
+const DEMOREC_PLAYBACKINFO *demorec_playback_info();
+int demorec_isplaying();
+int demorec_playback_stop();
+
diff --git a/src/engine/e_engine.c b/src/engine/e_engine.c
index c8a1b07b..d739a589 100644
--- a/src/engine/e_engine.c
+++ b/src/engine/e_engine.c
@@ -63,6 +63,9 @@ void engine_init(const char *appname)
 
 			str_format(path, sizeof(path), "%s/maps", application_save_path);
 			fs_makedir(path);
+
+			str_format(path, sizeof(path), "%s/demos", application_save_path);
+			fs_makedir(path);
 		}
 	}
 
diff --git a/src/engine/e_if_client.h b/src/engine/e_if_client.h
index 08402ad1..7aef86e9 100644
--- a/src/engine/e_if_client.h
+++ b/src/engine/e_if_client.h
@@ -17,12 +17,14 @@ enum
 		CLIENTSTATE_CONNECTING - The client is trying to connect to a server.
 		CLIENTSTATE_LOADING - The client has connected to a server and is loading resources.
 		CLIENTSTATE_ONLINE - The client is connected to a server and running the game.
+		CLIENTSTATE_DEMOPLAYBACK - The client is playing a demo
 		CLIENTSTATE_QUITING - The client is quiting.
 	*/
 	CLIENTSTATE_OFFLINE=0,
 	CLIENTSTATE_CONNECTING,
 	CLIENTSTATE_LOADING,
 	CLIENTSTATE_ONLINE,
+	CLIENTSTATE_DEMOPLAYBACK,
 	CLIENTSTATE_QUITING,
 
 	/* Constants: Image Formats
@@ -541,6 +543,23 @@ enum
 
 void client_serverbrowse_set(NETADDR *addr, int type, int token, SERVER_INFO *info);
 
-
 int client_serverbrowse_refreshingmasters();
+
+
+typedef struct DEMOPLAYBACK_INFO
+{
+	int first_tick;
+	int last_tick;
+	int current_tick;
+	int paused;
+	float speed;
+} DEMOPLAYBACK_INFO;
+
+void client_demoplayer_play(const char *filename);
+const DEMOPLAYBACK_INFO *client_demoplayer_getinfo();
+void client_demoplayer_setpos(float percent);
+void client_demoplayer_setpause(int paused);
+void client_demoplayer_setspeed(float speed);
+const char *client_user_directory();
+
 #endif
diff --git a/src/engine/e_if_modc.h b/src/engine/e_if_modc.h
index 8839d5f1..14ab9ded 100644
--- a/src/engine/e_if_modc.h
+++ b/src/engine/e_if_modc.h
@@ -142,4 +142,6 @@ int modc_snap_input(int *data);
 */
 const char *modc_net_version();
 
+
+void modc_recordkeyframe();
 #endif
diff --git a/src/engine/e_if_other.h b/src/engine/e_if_other.h
index 85148d85..ca09f48b 100644
--- a/src/engine/e_if_other.h
+++ b/src/engine/e_if_other.h
@@ -16,7 +16,6 @@ enum
 	
 	SNAP_CURRENT=0,
 	SNAP_PREV=1,
-	
 
 	MASK_NONE=0,
 	MASK_SET,
@@ -306,7 +305,10 @@ void snap_set_staticsize(int type, int size);
 enum
 {
 	MSGFLAG_VITAL=1,
-	MSGFLAG_FLUSH=2
+	MSGFLAG_FLUSH=2,
+	MSGFLAG_NORECORD=4,
+	MSGFLAG_RECORD=8,
+	MSGFLAG_NOSEND=16
 };
 
 /* message sending */
diff --git a/src/engine/e_if_server.h b/src/engine/e_if_server.h
index 98e2b452..b165ae3c 100644
--- a/src/engine/e_if_server.h
+++ b/src/engine/e_if_server.h
@@ -135,4 +135,6 @@ int server_tick();
 */
 int server_tickspeed();
 
+int server_ban_add(NETADDR addr, int type, int seconds);
+int server_ban_remove(NETADDR addr);
 #endif
diff --git a/src/engine/server/es_server.c b/src/engine/server/es_server.c
index d761e81e..306227b8 100644
--- a/src/engine/server/es_server.c
+++ b/src/engine/server/es_server.c
@@ -18,6 +18,7 @@
 #include <engine/e_config.h>
 #include <engine/e_packer.h>
 #include <engine/e_datafile.h>
+#include <engine/e_demorec.h>
 
 #include <mastersrv/mastersrv.h>
 
@@ -38,6 +39,8 @@ static int browseinfo_progression = -1;
 static int64 lastheartbeat;
 /*static NETADDR4 master_server;*/
 
+static IOHANDLE demorec_file = 0;
+
 static char current_map[64];
 static int current_map_crc;
 static unsigned char *current_map_data = 0;
@@ -66,7 +69,6 @@ static int snap_id_usage;
 static int snap_id_inusage;
 static int snap_id_inited = 0;
 
-
 enum
 {
 	SRVCLIENT_STATE_EMPTY = 0,
@@ -338,19 +340,26 @@ int server_send_msg(int client_id)
 	if(info->flags&MSGFLAG_FLUSH)
 		packet.flags |= NETSENDFLAG_FLUSH;
 	
-	if(client_id == -1)
+	/* write message to demo recorder */
+	if(!(info->flags&MSGFLAG_NORECORD))
+		demorec_record_write("MESG", info->size, info->data);
+
+	if(!(info->flags&MSGFLAG_NOSEND))
 	{
-		/* broadcast */
-		int i;
-		for(i = 0; i < MAX_CLIENTS; i++)
-			if(clients[i].state == SRVCLIENT_STATE_INGAME)
-			{
-				packet.client_id = i;
-				netserver_send(net, &packet);
-			}
+		if(client_id == -1)
+		{
+			/* broadcast */
+			int i;
+			for(i = 0; i < MAX_CLIENTS; i++)
+				if(clients[i].state == SRVCLIENT_STATE_INGAME)
+				{
+					packet.client_id = i;
+					netserver_send(net, &packet);
+				}
+		}
+		else
+			netserver_send(net, &packet);
 	}
-	else
-		netserver_send(net, &packet);
 	return 0;
 }
 
@@ -364,7 +373,29 @@ static void server_do_snap()
 		mods_presnap();
 		perf_end();
 	}
+	
+	/* create snapshot for demo recording */
+	if(demorec_file)
+	{
+		char data[MAX_SNAPSHOT_SIZE];
+		int snapshot_size;
+		DEMOREC_TICKMARKER marker;
+
+		/* write tick marker */
+		marker.tick = server_tick();
+		swap_endian(&marker, sizeof(int), sizeof(marker)/sizeof(int));
+		demorec_record_write("TICK", sizeof(marker), &marker);
+		
+		/* build snap and possibly add some messages */
+		snapbuild_init(&builder);
+		mods_snap(-1);
+		snapshot_size = snapbuild_finish(&builder, data);
+		
+		/* write snapshot */
+		demorec_record_write("SNAP", snapshot_size, data);
+	}
 
+	/* create snapshots for all clients */
 	for(i = 0; i < MAX_CLIENTS; i++)
 	{
 		/* client must be ingame to recive snapshots */
@@ -788,6 +819,17 @@ static void server_process_client_packet(NETCHUNK *packet)
 	}
 }
 
+
+int server_ban_add(NETADDR addr, int type, int seconds)
+{
+	return netserver_ban_add(net, addr, type, seconds);	
+}
+
+int server_ban_remove(NETADDR addr)
+{
+	return netserver_ban_remove(net, addr);
+}
+
 static void server_send_serverinfo(NETADDR *addr, int token)
 {
 	NETCHUNK packet;
@@ -1116,6 +1158,15 @@ static void con_kick(void *result, void *user_data)
 	server_kick(console_arg_int(result, 0), "kicked by console");
 }
 
+static void con_ban(void *result, void *user_data)
+{
+	NETADDR addr;
+	const char *str = console_arg_string(result, 0);
+	
+	if(net_addr_from_str(&addr, str) == 0)
+		server_ban_add(addr, 1, 60);
+}
+
 static void con_status(void *result, void *user_data)
 {
 	int i;
@@ -1135,14 +1186,27 @@ static void con_status(void *result, void *user_data)
 static void con_shutdown(void *result, void *user_data)
 {
 	run_server = 0;
-	/*server_kick(console_arg_int(result, 0), "kicked by console");*/
+}
+
+static void con_record(void *result, void *user_data)
+{
+	demorec_record_start(console_arg_string(result, 0), mods_net_version(), current_map, current_map_crc, "server");
+}
+
+static void con_stoprecord(void *result, void *user_data)
+{
+	demorec_record_stop();
 }
 
 static void server_register_commands()
 {
 	MACRO_REGISTER_COMMAND("kick", "i", con_kick, 0);
+	MACRO_REGISTER_COMMAND("ban", "r", con_ban, 0);
 	MACRO_REGISTER_COMMAND("status", "", con_status, 0);
 	MACRO_REGISTER_COMMAND("shutdown", "", con_shutdown, 0);
+
+	MACRO_REGISTER_COMMAND("record", "s", con_record, 0);
+	MACRO_REGISTER_COMMAND("stoprecord", "", con_stoprecord, 0);
 }
 
 int main(int argc, char **argv)
diff --git a/src/game/client/clienthooks.cpp b/src/game/client/clienthooks.cpp
index 88a7722a..1611b00d 100644
--- a/src/game/client/clienthooks.cpp
+++ b/src/game/client/clienthooks.cpp
@@ -17,6 +17,7 @@ extern "C" void modc_init() { gameclient.on_init(); }
 extern "C" void modc_connected() { gameclient.on_connected(); }
 extern "C" void modc_predict() { gameclient.on_predict(); }
 extern "C" void modc_newsnapshot() { gameclient.on_snapshot(); }
+extern "C" void modc_recordkeyframe() { gameclient.on_recordkeyframe(); }
 extern "C" int modc_snap_input(int *data) { return gameclient.on_snapinput(data); }
 extern "C" void modc_statechange(int state, int old) { gameclient.on_statechange(state, old); }
 extern "C" void modc_render() { gameclient.on_render(); }
diff --git a/src/game/client/component.hpp b/src/game/client/component.hpp
index f03165d2..3d5edb86 100644
--- a/src/game/client/component.hpp
+++ b/src/game/client/component.hpp
@@ -18,6 +18,7 @@ public:
 	virtual void on_save() {};
 	virtual void on_reset() {};
 	virtual void on_render() {};
+	virtual void on_mapload() {};
 	virtual void on_message(int msg, void *rawmsg) {}
 	virtual bool on_mousemove(float x, float y) { return false; }
 	virtual bool on_input(INPUT_EVENT e) { return false; }
diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp
index 86ce6257..2b1c1663 100644
--- a/src/game/client/components/hud.cpp
+++ b/src/game/client/components/hud.cpp
@@ -305,7 +305,8 @@ void HUD::on_render()
 
 	render_goals();
 	render_fps();
-	render_connectionwarning();
+	if(client_state() != CLIENTSTATE_DEMOPLAYBACK)
+		render_connectionwarning();
 	render_tunewarning();
 	render_teambalancewarning();
 	render_voting();
diff --git a/src/game/client/components/mapimages.cpp b/src/game/client/components/mapimages.cpp
index 9dc2447a..7f839da0 100644
--- a/src/game/client/components/mapimages.cpp
+++ b/src/game/client/components/mapimages.cpp
@@ -8,7 +8,7 @@ MAPIMAGES::MAPIMAGES()
 	count = 0;
 }
 
-void MAPIMAGES::on_reset()
+void MAPIMAGES::on_mapload()
 {
 	// unload all textures
 	for(int i = 0; i < count; i++)
diff --git a/src/game/client/components/mapimages.hpp b/src/game/client/components/mapimages.hpp
index e1e0063d..cba46033 100644
--- a/src/game/client/components/mapimages.hpp
+++ b/src/game/client/components/mapimages.hpp
@@ -9,7 +9,7 @@ public:
 	
 	int get(int index) const { return textures[index]; }
 	int num() const { return count; }
-	
-	virtual void on_reset();
+
+	virtual void on_mapload();
 };
 
diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp
index e1518036..c6fea413 100644
--- a/src/game/client/components/maplayers.cpp
+++ b/src/game/client/components/maplayers.cpp
@@ -50,7 +50,7 @@ static void envelope_eval(float time_offset, int env, float *channels)
 
 void MAPLAYERS::on_render()
 {
-	if(client_state() != CLIENTSTATE_ONLINE)
+	if(client_state() != CLIENTSTATE_ONLINE && client_state() != CLIENTSTATE_DEMOPLAYBACK)
 		return;
 	
 	vec2 center = gameclient.camera->center;
diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
index b1043870..f71f0d96 100644
--- a/src/game/client/components/menus.cpp
+++ b/src/game/client/components/menus.cpp
@@ -32,6 +32,11 @@ vec4 MENUS::color_tabbar_active;
 vec4 MENUS::color_tabbar_inactive_ingame;
 vec4 MENUS::color_tabbar_active_ingame;
 
+
+float MENUS::button_height = 25.0f;
+float MENUS::listheader_height = 16.0f;
+float MENUS::fontmod_height = 0.8f;
+
 INPUT_EVENT MENUS::inputevents[MAX_INPUTEVENTS];
 int MENUS::num_inputevents;
 
@@ -84,9 +89,10 @@ MENUS::MENUS()
 	menu_active = true;
 	num_inputevents = 0;
 	
-	last_input = time_get();
+	demos = 0;
+	num_demos = 0;
 	
-	button_height = 25.0f;
+	last_input = time_get();
 }
 
 vec4 MENUS::button_color_mul(const void *id)
@@ -111,22 +117,27 @@ void MENUS::ui_draw_browse_icon(int what, const RECT *r)
 void MENUS::ui_draw_menu_button(const void *id, const char *text, int checked, const RECT *r, const void *extra)
 {
 	ui_draw_rect(r, vec4(1,1,1,0.5f)*button_color_mul(id), CORNER_ALL, 5.0f);
-	ui_do_label(r, text, 18.0f, 0);
+	ui_do_label(r, text, r->h*fontmod_height, 0);
 }
 
 void MENUS::ui_draw_keyselect_button(const void *id, const char *text, int checked, const RECT *r, const void *extra)
 {
 	ui_draw_rect(r, vec4(1,1,1,0.5f)*button_color_mul(id), CORNER_ALL, 5.0f);
-	ui_do_label(r, text, 14.0f, 0);
+	ui_do_label(r, text, r->h*fontmod_height, 0);
 }
 
 void MENUS::ui_draw_menu_tab_button(const void *id, const char *text, int checked, const RECT *r, const void *extra)
 {
+	int corners = CORNER_T;
+	vec4 colormod(1,1,1,1);
+	if(extra)
+		corners = *(int *)extra;
+	
 	if(checked)
-		ui_draw_rect(r, color_tabbar_active, CORNER_T, 10.0f);
+		ui_draw_rect(r, color_tabbar_active, corners, 10.0f);
 	else
-		ui_draw_rect(r, color_tabbar_inactive, CORNER_T, 10.0f);
-	ui_do_label(r, text, 22.0f, 0);
+		ui_draw_rect(r, color_tabbar_inactive, corners, 10.0f);
+	ui_do_label(r, text, r->h*fontmod_height, 0);
 }
 
 
@@ -136,7 +147,7 @@ void MENUS::ui_draw_settings_tab_button(const void *id, const char *text, int ch
 		ui_draw_rect(r, color_tabbar_active, CORNER_R, 10.0f);
 	else
 		ui_draw_rect(r, color_tabbar_inactive, CORNER_R, 10.0f);
-	ui_do_label(r, text, 20.0f, 0);
+	ui_do_label(r, text, r->h*fontmod_height, 0);
 }
 
 void MENUS::ui_draw_grid_header(const void *id, const char *text, int checked, const RECT *r, const void *extra)
@@ -145,7 +156,7 @@ void MENUS::ui_draw_grid_header(const void *id, const char *text, int checked, c
 		ui_draw_rect(r, vec4(1,1,1,0.5f), CORNER_T, 5.0f);
 	RECT t;
 	ui_vsplit_l(r, 5.0f, 0, &t);
-	ui_do_label(&t, text, 14.0f, -1);
+	ui_do_label(&t, text, r->h*fontmod_height, -1);
 }
 
 void MENUS::ui_draw_list_row(const void *id, const char *text, int checked, const RECT *r, const void *extra)
@@ -156,7 +167,7 @@ void MENUS::ui_draw_list_row(const void *id, const char *text, int checked, cons
 		ui_margin(&sr, 1.5f, &sr);
 		ui_draw_rect(&sr, vec4(1,1,1,0.5f), CORNER_ALL, 4.0f);
 	}
-	ui_do_label(r, text, 14.0f, -1);
+	ui_do_label(r, text, r->h*fontmod_height, -1);
 }
 
 void MENUS::ui_draw_checkbox_common(const void *id, const char *text, const char *boxtext, const RECT *r)
@@ -171,8 +182,8 @@ void MENUS::ui_draw_checkbox_common(const void *id, const char *text, const char
 	ui_margin(&c, 2.0f, &c);
 	ui_draw_rect(&c, vec4(1,1,1,0.25f)*button_color_mul(id), CORNER_ALL, 3.0f);
 	c.y += 2;
-	ui_do_label(&c, boxtext, 12.0f, 0);
-	ui_do_label(&t, text, 14.0f, -1);
+	ui_do_label(&c, boxtext, r->h*fontmod_height*0.75f, 0);
+	ui_do_label(&t, text, r->h*fontmod_height*0.8f, -1);
 }
 
 void MENUS::ui_draw_checkbox(const void *id, const char *text, int checked, const RECT *r, const void *extra)
@@ -489,31 +500,43 @@ int MENUS::render_menubar(RECT r)
 			ui_vsplit_l(&box, 30.0f, 0, &box); 
 		}
 
-		ui_vsplit_l(&box, 110.0f, &button, &box);
+		ui_vsplit_l(&box, 100.0f, &button, &box);
 		static int internet_button=0;
-		if (ui_do_button(&internet_button, "Internet", active_page==PAGE_INTERNET, &button, ui_draw_menu_tab_button, 0))
+		int corners = CORNER_TL;
+		if (ui_do_button(&internet_button, "Internet", active_page==PAGE_INTERNET, &button, ui_draw_menu_tab_button, &corners))
 		{
 			client_serverbrowse_refresh(BROWSETYPE_INTERNET);
 			new_page = PAGE_INTERNET;
 		}
 
-		ui_vsplit_l(&box, 4.0f, 0, &box);
-		ui_vsplit_l(&box, 90.0f, &button, &box);
+		//ui_vsplit_l(&box, 4.0f, 0, &box);
+		ui_vsplit_l(&box, 80.0f, &button, &box);
 		static int lan_button=0;
-		if (ui_do_button(&lan_button, "LAN", active_page==PAGE_LAN, &button, ui_draw_menu_tab_button, 0))
+		corners = 0;
+		if (ui_do_button(&lan_button, "LAN", active_page==PAGE_LAN, &button, ui_draw_menu_tab_button, &corners))
 		{
 			client_serverbrowse_refresh(BROWSETYPE_LAN);
 			new_page = PAGE_LAN;
 		}
 
-		ui_vsplit_l(&box, 4.0f, 0, &box);
-		ui_vsplit_l(&box, 120.0f, &button, &box);
+		//ui_vsplit_l(&box, 4.0f, 0, &box);
+		ui_vsplit_l(&box, 110.0f, &button, &box);
 		static int favorites_button=0;
-		if (ui_do_button(&favorites_button, "Favorites", active_page==PAGE_FAVORITES, &button, ui_draw_menu_tab_button, 0))
+		corners = CORNER_TR;
+		if (ui_do_button(&favorites_button, "Favorites", active_page==PAGE_FAVORITES, &button, ui_draw_menu_tab_button, &corners))
 		{
 			client_serverbrowse_refresh(BROWSETYPE_FAVORITES);
 			new_page  = PAGE_FAVORITES;
 		}
+		
+		ui_vsplit_l(&box, 4.0f*5, 0, &box);
+		ui_vsplit_l(&box, 100.0f, &button, &box);
+		static int demos_button=0;
+		if (ui_do_button(&demos_button, "Demos", active_page==PAGE_DEMOS, &button, ui_draw_menu_tab_button, 0))
+		{
+			//client_serverbrowse_refresh(BROWSETYPE_FAVORITES);
+			new_page  = PAGE_DEMOS;
+		}		
 	}
 	else
 	{
@@ -547,13 +570,13 @@ int MENUS::render_menubar(RECT r)
 	ui_vsplit_r(&box, 30.0f, &box, 0);
 	*/
 	
-	ui_vsplit_r(&box, 110.0f, &box, &button);
+	ui_vsplit_r(&box, 90.0f, &box, &button);
 	static int quit_button=0;
 	if (ui_do_button(&quit_button, "Quit", 0, &button, ui_draw_menu_tab_button, 0))
 		popup = POPUP_QUIT;
 
 	ui_vsplit_r(&box, 10.0f, &box, &button);
-	ui_vsplit_r(&box, 110.0f, &box, &button);
+	ui_vsplit_r(&box, 120.0f, &box, &button);
 	static int settings_button=0;
 	if (ui_do_button(&settings_button, "Settings", active_page==PAGE_SETTINGS, &button, ui_draw_menu_tab_button, 0))
 		new_page = PAGE_SETTINGS;
@@ -663,7 +686,7 @@ int MENUS::render()
 	if(popup == POPUP_NONE)
 	{
 		// do tab bar
-		ui_hsplit_t(&screen, 26.0f, &tab_bar, &main_view);
+		ui_hsplit_t(&screen, 24.0f, &tab_bar, &main_view);
 		ui_vmargin(&tab_bar, 20.0f, &tab_bar);
 		render_menubar(tab_bar);
 		
@@ -691,6 +714,8 @@ int MENUS::render()
 			render_serverbrowser(main_view);
 		else if(config.ui_page == PAGE_LAN)
 			render_serverbrowser(main_view);
+		else if(config.ui_page == PAGE_DEMOS)
+			render_demolist(main_view);
 		else if(config.ui_page == PAGE_FAVORITES)
 			render_serverbrowser(main_view);
 		else if(config.ui_page == PAGE_SETTINGS)
@@ -922,7 +947,7 @@ void MENUS::on_statechange(int new_state, int old_state)
 		popup = POPUP_CONNECTING;
 	else if(new_state == CLIENTSTATE_CONNECTING)
 		popup = POPUP_CONNECTING;
-	else if (new_state == CLIENTSTATE_ONLINE)
+	else if (new_state == CLIENTSTATE_ONLINE || new_state == CLIENTSTATE_DEMOPLAYBACK)
 	{
 		popup = POPUP_NONE;
 		menu_active = false;
@@ -931,17 +956,19 @@ void MENUS::on_statechange(int new_state, int old_state)
 
 void MENUS::on_render()
 {
-	if(client_state() != CLIENTSTATE_ONLINE)
+	if(client_state() != CLIENTSTATE_ONLINE && client_state() != CLIENTSTATE_DEMOPLAYBACK)
 		menu_active = true;
+
+	if(client_state() == CLIENTSTATE_DEMOPLAYBACK)
+	{
+		RECT screen = *ui_screen();
+		gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+		render_demoplayer(screen);
+	}
 	
 	if(!menu_active)
 		return;
 		
-
-
-	if(inp_key_down('M')) button_height += 1.0f;
-	if(inp_key_down('N')) button_height -= 1.0f;
-		
 	// update colors
 	vec3 rgb = hsl_to_rgb(vec3(config.ui_color_hue/255.0f, config.ui_color_sat/255.0f, config.ui_color_lht/255.0f));
 	gui_color = vec4(rgb.r, rgb.g, rgb.b, config.ui_color_alpha/255.0f);
@@ -1002,8 +1029,8 @@ void MENUS::on_render()
 	}
 	else
 	{
-		//render_background();
-		render();
+		if(client_state() != CLIENTSTATE_DEMOPLAYBACK)
+			render();
 
 		// render cursor
 		gfx_texture_set(data->images[IMAGE_CURSOR].id);
@@ -1024,6 +1051,7 @@ void MENUS::on_render()
 			gfx_text_set_cursor(&cursor, 10, 10, 10, TEXTFLAG_RENDER);
 			gfx_text_ex(&cursor, buf, -1);
 		}
+		
 	}
 
 	num_inputevents = 0;
diff --git a/src/game/client/components/menus.hpp b/src/game/client/components/menus.hpp
index 8aae925f..51090b93 100644
--- a/src/game/client/components/menus.hpp
+++ b/src/game/client/components/menus.hpp
@@ -15,6 +15,9 @@ class MENUS : public COMPONENT
 	
 	static vec4 button_color_mul(const void *id);
 
+
+	static void ui_draw_demoplayer_button(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+
 	static void ui_draw_browse_icon(int what, const RECT *r);
 	static void ui_draw_menu_button(const void *id, const char *text, int checked, const RECT *r, const void *extra);
 	static void ui_draw_keyselect_button(const void *id, const char *text, int checked, const RECT *r, const void *extra);
@@ -33,6 +36,19 @@ class MENUS : public COMPONENT
 	static int ui_do_key_reader(void *id, const RECT *rect, int key);
 	static void ui_do_getbuttons(int start, int stop, RECT view);
 
+	struct LISTBOXITEM
+	{
+		int visible;
+		int selected;
+		RECT rect;
+	};
+	
+	static void ui_do_listbox_start(void *id, const RECT *rect, float row_height, const char *title, int num_items, int selected_index);
+	static LISTBOXITEM ui_do_listbox_nextitem(void *id);
+	static int ui_do_listbox_end();
+	
+	//static void demolist_listdir_callback(const char *name, int is_dir, void *user);
+	//static void demolist_list_callback(const RECT *rect, int index, void *user);
 
 	enum
 	{
@@ -53,6 +69,7 @@ class MENUS : public COMPONENT
 		PAGE_INTERNET,
 		PAGE_LAN,
 		PAGE_FAVORITES,
+		PAGE_DEMOS,
 		PAGE_SETTINGS,
 		PAGE_SYSTEM,
 	};
@@ -71,7 +88,9 @@ class MENUS : public COMPONENT
 	static int num_inputevents;
 	
 	// some settings
-	float button_height;
+	static float button_height;
+	static float listheader_height;
+	static float fontmod_height;
 	
 	// for graphic settings
 	bool need_restart;
@@ -79,6 +98,20 @@ class MENUS : public COMPONENT
 	// for call vote
 	int callvote_selectedplayer;
 	int callvote_selectedmap;
+	
+	// demo
+	struct DEMOITEM
+	{
+		char filename[512];
+		char name[256];
+	};
+	
+	DEMOITEM *demos;
+	int num_demos;
+		
+	void demolist_populate();
+	static void demolist_count_callback(const char *name, int is_dir, void *user);
+	static void demolist_fetch_callback(const char *name, int is_dir, void *user);
 
 	// found in menus.cpp
 	int render();
@@ -87,6 +120,10 @@ class MENUS : public COMPONENT
 	int render_menubar(RECT r);
 	void render_news(RECT main_view);
 	
+	// found in menus_demo.cpp
+	void render_demoplayer(RECT main_view);
+	void render_demolist(RECT main_view);
+	
 	// found in menus_ingame.cpp
 	void render_game(RECT main_view);
 	void render_serverinfo(RECT main_view);
diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp
index a4e730e6..3cba5311 100644
--- a/src/game/client/components/menus_browser.cpp
+++ b/src/game/client/components/menus_browser.cpp
@@ -21,7 +21,7 @@ void MENUS::render_serverbrowser_serverlist(RECT view)
 	RECT headers;
 	RECT status;
 	
-	ui_hsplit_t(&view, 16.0f, &headers, &view);
+	ui_hsplit_t(&view, listheader_height, &headers, &view);
 	ui_hsplit_b(&view, 20.0f, &view, &status);
 	
 	// split of the scrollbar
@@ -634,7 +634,7 @@ void MENUS::render_serverbrowser(RECT main_view)
 		ui_vmargin(&button, 2.0f, &button);
 		//ui_vmargin(&button, 2.0f, &button);
 		static int join_button = 0;
-		if(ui_do_button(&join_button, "Connect", 0, &button, ui_draw_menu_button, 0) || inp_key_down(KEY_ENTER))
+		if(ui_do_button(&join_button, "Connect", 0, &button, ui_draw_menu_button, 0)) // || inp_key_down(KEY_ENTER))
 			client_connect(config.ui_server_address);
 					
 		ui_hsplit_b(&button_box, 5.0f, &button_box, &button);
diff --git a/src/game/client/components/menus_demo.cpp b/src/game/client/components/menus_demo.cpp
new file mode 100644
index 00000000..81717bf7
--- /dev/null
+++ b/src/game/client/components/menus_demo.cpp
@@ -0,0 +1,390 @@
+
+#include <base/math.hpp>
+
+//#include <string.h> // strcmp, strlen, strncpy
+//#include <stdlib.h> // atoi
+
+#include <engine/e_client_interface.h>
+#include <game/client/gc_render.hpp>
+#include <game/client/gameclient.hpp>
+
+//#include <game/generated/g_protocol.hpp>
+//#include <game/generated/gc_data.hpp>
+
+#include <game/client/ui.hpp>
+//#include <game/client/gameclient.hpp>
+//#include <game/client/animstate.hpp>
+
+#include "menus.hpp"
+
+void MENUS::ui_draw_demoplayer_button(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	ui_draw_rect(r, vec4(1,1,1, checked ? 0.10f : 0.5f)*button_color_mul(id), CORNER_ALL, 5.0f);
+	ui_do_label(r, text, 14.0f, 0);
+}
+
+void MENUS::render_demoplayer(RECT main_view)
+{
+	const DEMOPLAYBACK_INFO *info = client_demoplayer_getinfo();
+	
+	const float seekbar_height = 15.0f;
+	const float buttonbar_height = 20.0f;
+	const float margins = 5.0f;
+	float total_height;
+	
+	if(menu_active)
+		total_height = seekbar_height+buttonbar_height+margins*3;
+	else
+		total_height = seekbar_height+margins*2;
+	
+	ui_hsplit_b(&main_view, total_height, 0, &main_view);
+	ui_vsplit_l(&main_view, 250.0f, 0, &main_view);
+	ui_vsplit_r(&main_view, 250.0f, &main_view, 0);
+	
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_T, 10.0f);
+		
+	ui_margin(&main_view, 5.0f, &main_view);
+	
+	RECT seekbar, buttonbar;
+	
+	if(menu_active)
+	{
+		ui_hsplit_t(&main_view, seekbar_height, &seekbar, &buttonbar);
+		ui_hsplit_t(&buttonbar, margins, 0, &buttonbar);
+	}
+	else
+		seekbar = main_view;
+
+	// do seekbar
+	{
+		static int seekbar_id = 0;
+		void *id = &seekbar_id;
+		char buffer[128];
+		
+		ui_draw_rect(&seekbar, vec4(0,0,0,0.5f), CORNER_ALL, 5.0f);
+		
+		int current_tick = info->current_tick - info->first_tick;
+		int total_ticks = info->last_tick - info->first_tick;
+		
+		float amount = current_tick/(float)total_ticks;
+		
+		RECT filledbar = seekbar;
+		filledbar.w = 10.0f + (filledbar.w-10.0f)*amount;
+		
+		ui_draw_rect(&filledbar, vec4(1,1,1,0.5f), CORNER_ALL, 5.0f);
+		
+		str_format(buffer, sizeof(buffer), "%d:%02d / %d:%02d",
+			current_tick/SERVER_TICK_SPEED/60, (current_tick/SERVER_TICK_SPEED)%60,
+			total_ticks/SERVER_TICK_SPEED/60, (total_ticks/SERVER_TICK_SPEED)%60);
+		ui_do_label(&seekbar, buffer, seekbar.h*0.70f, 0);
+
+		// do the logic
+	    int inside = ui_mouse_inside(&seekbar);
+			
+		if(ui_active_item() == id)
+		{
+			if(!ui_mouse_button(0))
+				ui_set_active_item(0);
+			else
+			{
+				float amount = (ui_mouse_x()-seekbar.x)/(float)seekbar.w;
+				if(amount > 0 && amount < 1.0f)
+				{
+					gameclient.on_reset();
+					client_demoplayer_setpos(amount);
+				}
+			}
+		}
+		else if(ui_hot_item() == id)
+		{
+			if(ui_mouse_button(0))
+				ui_set_active_item(id);
+		}		
+		
+		if(inside)
+			ui_set_hot_item(id);
+	}	
+	
+
+	if(menu_active)
+	{
+		// do buttons
+		RECT button;
+
+		// pause button
+		ui_vsplit_l(&buttonbar, buttonbar_height, &button, &buttonbar);
+		static int pause_button = 0;
+		if(ui_do_button(&pause_button, "| |", info->paused, &button, ui_draw_demoplayer_button, 0))
+			client_demoplayer_setpause(!info->paused);
+		
+		// play button
+		ui_vsplit_l(&buttonbar, margins, 0, &buttonbar);
+		ui_vsplit_l(&buttonbar, buttonbar_height, &button, &buttonbar);
+		static int play_button = 0;
+		if(ui_do_button(&play_button, ">", !info->paused, &button, ui_draw_demoplayer_button, 0))
+		{
+			client_demoplayer_setpause(0);
+			client_demoplayer_setspeed(1.0f);
+		}
+
+		// slowdown
+		ui_vsplit_l(&buttonbar, margins, 0, &buttonbar);
+		ui_vsplit_l(&buttonbar, buttonbar_height, &button, &buttonbar);
+		static int slowdown_button = 0;
+		if(ui_do_button(&slowdown_button, "<<", 0, &button, ui_draw_demoplayer_button, 0))
+		{
+			if(info->speed > 4.0f) client_demoplayer_setspeed(4.0f);
+			else if(info->speed > 2.0f) client_demoplayer_setspeed(2.0f);
+			else if(info->speed > 1.0f) client_demoplayer_setspeed(1.0f);
+			else if(info->speed > 0.5f) client_demoplayer_setspeed(0.5f);
+			else client_demoplayer_setspeed(0.05f);
+		}
+		
+		// fastforward
+		ui_vsplit_l(&buttonbar, margins, 0, &buttonbar);
+		ui_vsplit_l(&buttonbar, buttonbar_height, &button, &buttonbar);
+		static int fastforward_button = 0;
+		if(ui_do_button(&fastforward_button, ">>", 0, &button, ui_draw_demoplayer_button, 0))
+		{
+			if(info->speed < 0.5f) client_demoplayer_setspeed(0.5f);
+			else if(info->speed < 1.0f) client_demoplayer_setspeed(1.0f);
+			else if(info->speed < 2.0f) client_demoplayer_setspeed(2.0f);
+			else if(info->speed < 4.0f) client_demoplayer_setspeed(4.0f);
+			else client_demoplayer_setspeed(8.0f);
+		}
+
+		// speed meter
+		ui_vsplit_l(&buttonbar, margins*3, 0, &buttonbar);
+		char buffer[64];
+		if(info->speed >= 1.0f)
+			str_format(buffer, sizeof(buffer), "x%.0f", info->speed);
+		else
+			str_format(buffer, sizeof(buffer), "x%.1f", info->speed);
+		ui_do_label(&buttonbar, buffer, button.h*0.7f, -1);
+
+		// exit button
+		ui_vsplit_r(&buttonbar, buttonbar_height*3, &buttonbar, &button);
+		static int exit_button = 0;
+		if(ui_do_button(&exit_button, "Exit", 0, &button, ui_draw_demoplayer_button, 0))
+			client_disconnect();
+	}
+}
+
+static RECT listbox_originalview;
+static RECT listbox_view;
+static float listbox_rowheight;
+static int listbox_itemindex;
+static int listbox_selected_index;
+static int listbox_new_selected;
+
+void MENUS::ui_do_listbox_start(void *id, const RECT *rect, float row_height, const char *title, int num_items, int selected_index)
+{
+	RECT scroll, row;
+	RECT view = *rect;
+	RECT header, footer;
+	
+	// draw header
+	ui_hsplit_t(&view, listheader_height, &header, &view);
+	ui_draw_rect(&header, vec4(1,1,1,0.25f), CORNER_T, 5.0f); 
+	ui_do_label(&header, title, header.h*fontmod_height, 0);
+
+	// draw footers
+	ui_hsplit_b(&view, listheader_height, &view, &footer);
+	ui_draw_rect(&footer, vec4(1,1,1,0.25f), CORNER_B, 5.0f); 
+	ui_vsplit_l(&footer, 10.0f, 0, &footer);
+
+	// background
+	ui_draw_rect(&view, vec4(0,0,0,0.15f), 0, 0);
+
+	// prepare the scroll
+	ui_vsplit_r(&view, 15, &view, &scroll);
+
+	// setup the variables	
+	listbox_originalview = view;
+	listbox_selected_index = selected_index;
+	listbox_new_selected = selected_index;
+	listbox_itemindex = 0;
+	listbox_rowheight = row_height;
+	//int num_servers = client_serverbrowse_sorted_num();
+
+
+	// do the scrollbar
+	ui_hsplit_t(&view, listbox_rowheight, &row, 0);
+
+	int num = (int)(listbox_originalview.h/row.h);
+	static float scrollvalue = 0;
+	ui_hmargin(&scroll, 5.0f, &scroll);
+	scrollvalue = ui_do_scrollbar_v(id, &scroll, scrollvalue);
+
+	int start = (int)(num*scrollvalue);
+	if(start < 0)
+		start = 0;
+	
+	// the list
+	listbox_view = listbox_originalview;
+	ui_vmargin(&listbox_view, 5.0f, &listbox_view);
+	ui_clip_enable(&listbox_view);
+	listbox_view.y -= scrollvalue*num*row.h;	
+}
+
+MENUS::LISTBOXITEM MENUS::ui_do_listbox_nextitem(void *id)
+{
+	RECT row;
+	LISTBOXITEM item = {0};
+	ui_hsplit_t(&listbox_view, listbox_rowheight-2.0f, &row, &listbox_view);
+	ui_hsplit_t(&listbox_view, 2.0f, 0, &listbox_view);
+
+	RECT select_hit_box = row;
+
+	item.visible = 1;
+	if(listbox_selected_index == listbox_itemindex)
+		item.selected = 1;
+	
+	// make sure that only those in view can be selected
+	if(row.y+row.h > listbox_originalview.y)
+	{
+		
+		if(select_hit_box.y < listbox_originalview.y) // clip the selection
+		{
+			select_hit_box.h -= listbox_originalview.y-select_hit_box.y;
+			select_hit_box.y = listbox_originalview.y;
+		}
+		
+		if(ui_do_button(id, "", listbox_selected_index==listbox_itemindex, &select_hit_box, 0, 0))
+			listbox_new_selected = listbox_itemindex;
+	}
+	else
+		item.visible = 0;
+	
+	item.rect = row;
+	
+	// check if we need to do more
+	if(row.y > listbox_originalview.y+listbox_originalview.h)
+		item.visible = 0;
+
+	if(listbox_selected_index==listbox_itemindex)
+	{
+		//selected_index = i;
+		RECT r = row;
+		ui_margin(&r, 1.5f, &r);
+		ui_draw_rect(&r, vec4(1,1,1,0.5f), CORNER_ALL, 4.0f);
+	}	
+
+	listbox_itemindex++;
+
+	ui_vmargin(&item.rect, 5.0f, &item.rect);
+
+	return item;
+}
+
+int MENUS::ui_do_listbox_end()
+{
+	ui_clip_disable();
+	return listbox_new_selected;
+}
+
+/*
+void MENUS::demolist_listdir_callback(const char *name, int is_dir, void *user)
+{
+
+	(*count)++;
+	LISTBOXITEM item = ui_do_listbox_nextitem((void*)(10+*count));
+	if(item.visible)
+		ui_do_label(&item.rect, name, item.rect.h*fontmod_height, -1);
+}
+
+
+	DEMOITEM *demos;
+	int num_demos;
+	*/
+	
+void MENUS::demolist_count_callback(const char *name, int is_dir, void *user)
+{
+	if(is_dir || name[0] == '.')
+		return;
+	(*(int *)user)++;
+}
+
+struct FETCH_CALLBACKINFO
+{
+	MENUS *self;
+	const char *prefix;
+	int count;
+};
+
+void MENUS::demolist_fetch_callback(const char *name, int is_dir, void *user)
+{
+	if(is_dir || name[0] == '.')
+		return;
+			
+	FETCH_CALLBACKINFO *info = (FETCH_CALLBACKINFO *)user;
+	
+	if(info->count == info->self->num_demos)
+		return;
+	
+	str_format(info->self->demos[info->count].filename, sizeof(info->self->demos[info->count].filename), "%s/%s", info->prefix, name);
+	str_copy(info->self->demos[info->count].name, name, sizeof(info->self->demos[info->count].name));
+	info->count++;
+}
+
+
+void MENUS::demolist_populate()
+{
+	if(demos)
+		mem_free(demos);
+	demos = 0;
+	num_demos = 0;
+	
+	char buf[512];
+	str_format(buf, sizeof(buf), "%s/demos", client_user_directory());
+	fs_listdir(buf, demolist_count_callback, &num_demos);
+	fs_listdir("demos", demolist_count_callback, &num_demos);
+	
+	demos = (DEMOITEM *)mem_alloc(sizeof(DEMOITEM)*num_demos, 1);
+	mem_zero(demos, sizeof(DEMOITEM)*num_demos);
+
+	FETCH_CALLBACKINFO info = {this, buf, 0};
+	fs_listdir(buf, demolist_fetch_callback, &info);
+	info.prefix = "demos";
+	fs_listdir("demos", demolist_fetch_callback, &info);
+}
+
+
+void MENUS::render_demolist(RECT main_view)
+{
+	static int inited = 0;
+	if(!inited)
+		demolist_populate();
+	inited = 1;
+	
+	// render background
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
+	ui_margin(&main_view, 10.0f, &main_view);
+	
+	RECT buttonbar;
+	ui_hsplit_b(&main_view, button_height+5.0f, &main_view, &buttonbar);
+	ui_hsplit_t(&buttonbar, 5.0f, 0, &buttonbar);
+	
+	static int selected_item = -1;
+	static int num_items = 0;
+	static int demolist_id = 0;
+	
+	ui_do_listbox_start(&demolist_id, &main_view, 17.0f, "Demos", num_items, selected_item);
+	for(int i = 0; i < num_demos; i++)
+	{
+		LISTBOXITEM item = ui_do_listbox_nextitem((void*)(10+i));
+		if(item.visible)
+			ui_do_label(&item.rect, demos[i].name, item.rect.h*fontmod_height, -1);
+	}
+	selected_item = ui_do_listbox_end();
+	
+	RECT button;
+	ui_vsplit_r(&buttonbar, 120.0f, &buttonbar, &button);
+	static int play_button = 0;
+	if(ui_do_button(&play_button, "Play", 0, &button, ui_draw_menu_button, 0))
+	{
+		if(selected_item >= 0 && selected_item < num_demos)
+			client_demoplayer_play(demos[selected_item].filename);
+	}
+}
+
diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp
index 33b4ffca..cfd92daa 100644
--- a/src/game/client/components/menus_ingame.cpp
+++ b/src/game/client/components/menus_ingame.cpp
@@ -283,7 +283,7 @@ void MENUS::render_servercontrol_map(RECT main_view)
 
 		ui_vmargin(&button, 5.0f, &button);
 		ui_do_label(&button, gameclient.maplist->name(i), 18.0f, -1);
-	}	
+	}
 }
 
 void MENUS::render_servercontrol_kick(RECT main_view)
diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp
index 6bf4221e..02977632 100644
--- a/src/game/client/components/players.cpp
+++ b/src/game/client/components/players.cpp
@@ -132,7 +132,7 @@ void PLAYERS::render_player(
 	gameclient.clients[info.cid].angle = angle;
 	vec2 direction = get_direction((int)(angle*256.0f));
 	
-	if(info.local && config.cl_predict)
+	if(info.local && config.cl_predict && client_state() != CLIENTSTATE_DEMOPLAYBACK)
 	{
 		if(!gameclient.snap.local_character || (gameclient.snap.local_character->health < 0) || (gameclient.snap.gameobj && gameclient.snap.gameobj->game_over))
 		{
@@ -149,6 +149,8 @@ void PLAYERS::render_player(
 	vec2 position = mix(vec2(prev.x, prev.y), vec2(player.x, player.y), intratick);
 	vec2 vel = mix(vec2(prev.vx/256.0f, prev.vy/256.0f), vec2(player.vx/256.0f, player.vy/256.0f), intratick);
 	
+	//dbg_msg("", "%d %d %d %d %f", prev.x, prev.y, player.x, player.y, intratick);
+	
 	gameclient.flow->add(position, vel*100.0f, 10.0f);
 	
 	render_info.got_airjump = player.jumped&2?0:1;
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
index 3aed1cd1..fee12eb0 100644
--- a/src/game/client/gameclient.cpp
+++ b/src/game/client/gameclient.cpp
@@ -262,8 +262,9 @@ void GAMECLIENT::on_connected()
 	layers_init();
 	col_init();
 	render_tilemap_generate_skip();
-		
-	on_reset();	
+
+	for(int i = 0; i < all.num; i++)
+		all.components[i]->on_mapload();
 	
 	// send the inital info
 	send_info(true);
@@ -313,8 +314,44 @@ void GAMECLIENT::update_local_character_pos()
 	}
 }
 
+
+static void evolve(NETOBJ_CHARACTER *character, int tick)
+{
+	WORLD_CORE tempworld;
+	CHARACTER_CORE tempcore;
+	mem_zero(&tempcore, sizeof(tempcore));
+	tempcore.world = &tempworld;
+	tempcore.read(character);
+	//tempcore.input.direction = character->wanted_direction;
+	while(character->tick < tick)
+	{
+		character->tick++;
+		tempcore.tick(false);
+		tempcore.move();
+		tempcore.quantize();
+	}
+	
+	tempcore.write(character);
+}
+
+
 void GAMECLIENT::on_render()
 {
+	// perform dead reckoning
+	// TODO: move this to a betterlocation
+	/*
+	for(int i = 0; i < MAX_CLIENTS; i++)
+	{
+		if(!snap.characters[i].active)
+			continue;
+					
+		// perform dead reckoning
+		if(snap.characters[i].prev.tick)
+			evolve(&snap.characters[i].prev, client_prevtick());
+		if(snap.characters[i].cur.tick)
+			evolve(&snap.characters[i].cur, client_tick());
+	}*/
+	
 	// update the local character position
 	update_local_character_pos();
 	
@@ -393,6 +430,9 @@ void GAMECLIENT::on_message(int msgtype)
 		if(clients[msg->cid].skin_name[0] == 'x' || clients[msg->cid].skin_name[1] == '_')
 			str_copy(clients[msg->cid].skin_name, "default", 64);
 		
+		clients[msg->cid].color_body = msg->color_body;
+		clients[msg->cid].color_feet = msg->color_feet;
+		
 		clients[msg->cid].skin_info.color_body = skins->get_color(msg->color_body);
 		clients[msg->cid].skin_info.color_feet = skins->get_color(msg->color_feet);
 		clients[msg->cid].skin_info.size = 64;
@@ -402,6 +442,8 @@ void GAMECLIENT::on_message(int msgtype)
 		if(clients[msg->cid].skin_id < 0)
 			clients[msg->cid].skin_id = 0;
 		
+		clients[msg->cid].use_custom_color = msg->use_custom_color;
+		
 		if(msg->use_custom_color)
 			clients[msg->cid].skin_info.texture = gameclient.skins->get(clients[msg->cid].skin_id)->color_texture;
 		else
@@ -485,27 +527,6 @@ void GAMECLIENT::process_events()
 	}
 }
 
-static void evolve(NETOBJ_CHARACTER *character, int tick)
-{
-	WORLD_CORE tempworld;
-	CHARACTER_CORE tempcore;
-	mem_zero(&tempcore, sizeof(tempcore));
-	tempcore.world = &tempworld;
-	tempcore.read(character);
-	//tempcore.input.direction = character->wanted_direction;
-	if(tick-character->tick > 50*3)
-		dbg_msg("", "%d -> %d = %d", character->tick, tick, tick-character->tick);
-	while(character->tick < tick)
-	{
-		character->tick++;
-		tempcore.tick(false);
-		tempcore.move();
-		tempcore.quantize();
-	}
-	
-	tempcore.write(character);
-}
-
 void GAMECLIENT::on_snapshot()
 {
 	// clear out the invalid pointers
@@ -582,7 +603,6 @@ void GAMECLIENT::on_snapshot()
 					snap.characters[item.id].prev = *((const NETOBJ_CHARACTER *)old);
 					snap.characters[item.id].cur = *((const NETOBJ_CHARACTER *)data);
 					
-					// perform dead reckoning
 					if(snap.characters[item.id].prev.tick)
 						evolve(&snap.characters[item.id].prev, client_prevtick());
 					if(snap.characters[item.id].cur.tick)
@@ -596,6 +616,9 @@ void GAMECLIENT::on_snapshot()
 		}
 	}
 	
+	if(client_state() == CLIENTSTATE_DEMOPLAYBACK)
+		gameclient.snap.spectate = true;
+	
 	// setup local pointers
 	if(snap.local_cid >= 0)
 	{
@@ -779,6 +802,25 @@ void GAMECLIENT::send_kill(int client_id)
 	client_send_msg();
 }
 
+void GAMECLIENT::on_recordkeyframe()
+{
+	for(int i = 0; i < MAX_CLIENTS; i++)
+	{
+		if(!snap.player_infos[i])
+			continue;
+			
+		NETMSG_SV_SETINFO msg;
+		msg.cid = i;
+		msg.name = clients[i].name;
+		msg.skin = clients[i].skin_name;
+		msg.use_custom_color = clients[i].use_custom_color;
+		msg.color_body = clients[i].color_body;
+		msg.color_feet = clients[i].color_feet;
+		msg.pack(MSGFLAG_NOSEND|MSGFLAG_RECORD);
+		client_send_msg();
+	}
+}
+
 void GAMECLIENT::con_team(void *result, void *user_data)
 {
 	((GAMECLIENT*)user_data)->send_switch_team(console_arg_int(result, 0));
diff --git a/src/game/client/gameclient.hpp b/src/game/client/gameclient.hpp
index 3ced19ce..b9ee325e 100644
--- a/src/game/client/gameclient.hpp
+++ b/src/game/client/gameclient.hpp
@@ -83,6 +83,10 @@ public:
 	// client data
 	struct CLIENT_DATA
 	{
+		int use_custom_color;
+		int color_body;
+		int color_feet;
+		
 		char name[64];
 		char skin_name[64];
 		int skin_id;
@@ -115,6 +119,7 @@ public:
 	void on_snapshot();
 	void on_predict();
 	int on_snapinput(int *data);
+	void on_recordkeyframe();
 
 	// actions
 	// TODO: move these
diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp
index 071407b1..0516ddc5 100644
--- a/src/game/server/entities/character.cpp
+++ b/src/game/server/entities/character.cpp
@@ -828,7 +828,7 @@ bool CHARACTER::take_damage(vec2 force, int dmg, int from, int weapon)
 
 void CHARACTER::snap(int snaping_client)
 {
-	if(distance(game.players[snaping_client]->view_pos, pos) > 1000.0f)
+	if(networkclipped(snaping_client))
 		return;
 	
 	NETOBJ_CHARACTER *character = (NETOBJ_CHARACTER *)snap_new_item(NETOBJTYPE_CHARACTER, player->client_id, sizeof(NETOBJ_CHARACTER));
diff --git a/src/game/server/entities/laser.cpp b/src/game/server/entities/laser.cpp
index 4e9909ba..8b512d82 100644
--- a/src/game/server/entities/laser.cpp
+++ b/src/game/server/entities/laser.cpp
@@ -100,7 +100,7 @@ void LASER::tick()
 
 void LASER::snap(int snapping_client)
 {
-	if(distance(game.players[snapping_client]->view_pos, pos) > 1000.0f)
+	if(networkclipped(snapping_client))
 		return;
 
 	NETOBJ_LASER *obj = (NETOBJ_LASER *)snap_new_item(NETOBJTYPE_LASER, id, sizeof(NETOBJ_LASER));
diff --git a/src/game/server/entities/projectile.cpp b/src/game/server/entities/projectile.cpp
index a2e10437..cd15ba10 100644
--- a/src/game/server/entities/projectile.cpp
+++ b/src/game/server/entities/projectile.cpp
@@ -78,9 +78,7 @@ void PROJECTILE::tick()
 		if(flags & PROJECTILE_FLAGS_EXPLODE)
 			game.create_explosion(curpos, owner, weapon, false);
 		else if(targetchr)
-		{
 			targetchr->take_damage(direction * max(0.001f, force), damage, owner, weapon);
-		}
 
 		game.world.destroy_entity(this);
 	}
@@ -100,7 +98,7 @@ void PROJECTILE::snap(int snapping_client)
 {
 	float ct = (server_tick()-start_tick)/(float)server_tickspeed();
 	
-	if(distance(game.players[snapping_client]->view_pos, get_pos(ct)) > 1000.0f)
+	if(networkclipped(snapping_client, get_pos(ct)))
 		return;
 
 	NETOBJ_PROJECTILE *proj = (NETOBJ_PROJECTILE *)snap_new_item(NETOBJTYPE_PROJECTILE, id, sizeof(NETOBJ_PROJECTILE));
diff --git a/src/game/server/entity.cpp b/src/game/server/entity.cpp
index 2cc7c8f7..1af5f60a 100644
--- a/src/game/server/entity.cpp
+++ b/src/game/server/entity.cpp
@@ -26,3 +26,17 @@ ENTITY::~ENTITY()
 	game.world.remove_entity(this);
 	snap_free_id(id);
 }
+
+int ENTITY::networkclipped(int snapping_client)
+{
+	return networkclipped(snapping_client, pos);
+}
+
+int ENTITY::networkclipped(int snapping_client, vec2 check_pos)
+{
+	if(snapping_client == -1)
+		return 0;
+	if(distance(game.players[snapping_client]->view_pos, check_pos) > 1000.0f)
+		return 1;
+	return 0;
+}
diff --git a/src/game/server/entity.hpp b/src/game/server/entity.hpp
index 8ccb2d9a..debe57b6 100644
--- a/src/game/server/entity.hpp
+++ b/src/game/server/entity.hpp
@@ -74,34 +74,34 @@ public:
 
 	/*
 		Function: destroy
-		Destorys the entity.
+			Destorys the entity.
 	*/
 	virtual void destroy() { delete this; }
 		
 	/*
 		Function: reset
-		Called when the game resets the map. Puts the entity
-		back to it's starting state or perhaps destroys it.
+			Called when the game resets the map. Puts the entity
+			back to it's starting state or perhaps destroys it.
 	*/
 	virtual void reset() {}
 	
 	/*
 		Function: tick
-		Called progress the entity to the next tick. Updates
-		and moves the entity to it's new state and position.
+			Called progress the entity to the next tick. Updates
+			and moves the entity to it's new state and position.
 	*/
 	virtual void tick() {}
 
 	/*
 		Function: tick_defered
-		Called after all entities tick() function has been called.
+			Called after all entities tick() function has been called.
 	*/
 	virtual void tick_defered() {}
 	
 	/*
 		Function: snap
-		Called when a new snapshot is being generated for a specific
-		client.
+			Called when a new snapshot is being generated for a specific
+			client.
 		
 		Arguments:
 			snapping_client - ID of the client which snapshot is
@@ -110,16 +110,34 @@ public:
 				recording.
 	*/
 	virtual void snap(int snapping_client) {}
+	
+	/*
+		Function: networkclipped(int snapping_client)
+			Performs a series of test to see if a client can see the
+			entity.
+
+		Arguments:
+			snapping_client - ID of the client which snapshot is
+				being generated. Could be -1 to create a complete
+				snapshot of everything in the game for demo
+				recording.
+			
+		Returns:
+			Non-zero if the entity doesn't have to be in the snapshot.
+	*/
+	int networkclipped(int snapping_client);
+	int networkclipped(int snapping_client, vec2 check_pos);
+		
 
 	/*
 		Variable: proximity_radius
-		Contains the physical size of the entity.
+			Contains the physical size of the entity.
 	*/
 	float proximity_radius;
 	
 	/*
 		Variable: pos
-		Contains the current posititon of the entity.
+			Contains the current posititon of the entity.
 	*/
 	vec2 pos;
 };
diff --git a/src/game/server/eventhandler.cpp b/src/game/server/eventhandler.cpp
index ce6a3b71..761eaf2c 100644
--- a/src/game/server/eventhandler.cpp
+++ b/src/game/server/eventhandler.cpp
@@ -36,10 +36,10 @@ void EVENTHANDLER::snap(int snapping_client)
 {
 	for(int i = 0; i < num_events; i++)
 	{
-		if(cmask_is_set(client_masks[i], snapping_client))
+		if(snapping_client == -1 || cmask_is_set(client_masks[i], snapping_client))
 		{
 			NETEVENT_COMMON *ev = (NETEVENT_COMMON *)&data[offsets[i]];
-			if(distance(game.players[snapping_client]->view_pos, vec2(ev->x, ev->y)) < 1500.0f)
+			if(snapping_client == -1 || distance(game.players[snapping_client]->view_pos, vec2(ev->x, ev->y)) < 1500.0f)
 			{
 				void *d = snap_new_item(types[i], i, sizes[i]);
 				mem_copy(d, &data[offsets[i]], sizes[i]);
diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp
index fb426ad6..0cdd4227 100644
--- a/src/game/server/gamecontext.cpp
+++ b/src/game/server/gamecontext.cpp
@@ -171,7 +171,7 @@ void GAMECONTEXT::send_chat(int chatter_cid, int team, const char *text)
 }
 
 
-void GAMECONTEXT::send_info(int who, int to_who)
+void GAMECONTEXT::send_info(int who, int to_who, bool recordonly)
 {
 	NETMSG_SV_SETINFO msg;
 	msg.cid = who;
@@ -180,9 +180,17 @@ void GAMECONTEXT::send_info(int who, int to_who)
 	msg.use_custom_color = players[who]->use_custom_color;
 	msg.color_body = players[who]->color_body;
 	msg.color_feet = players[who]->color_feet;
-	msg.pack(MSGFLAG_VITAL);
 	
-	server_send_msg(to_who);
+	if(recordonly)
+	{
+		msg.pack(MSGFLAG_NOSEND);
+		server_send_msg(to_who);
+	}
+	else
+	{
+		msg.pack(MSGFLAG_VITAL);
+		server_send_msg(to_who);
+	}
 }
 
 void GAMECONTEXT::send_emoticon(int cid, int emoticon)
@@ -333,6 +341,18 @@ void GAMECONTEXT::tick()
 
 void GAMECONTEXT::snap(int client_id)
 {
+	// check if we are recording a demo
+	if(client_id == -1)
+	{
+		// we are recording, make sure that we set
+		// the info for all players all the time
+		for(int i = 0; i < MAX_CLIENTS; i++)
+		{
+			if(game.players[i])
+				send_info(i, -1, true);
+		}
+	}
+	
 	world.snap(client_id);
 	controller->snap(client_id);
 	events.snap(client_id);
diff --git a/src/game/server/gamecontext.hpp b/src/game/server/gamecontext.hpp
index 124df645..8a1ff918 100644
--- a/src/game/server/gamecontext.hpp
+++ b/src/game/server/gamecontext.hpp
@@ -77,7 +77,7 @@ public:
 	void send_emoticon(int cid, int emoticon);
 	void send_weapon_pickup(int cid, int weapon);
 	void send_broadcast(const char *text, int cid);
-	void send_info(int who, int to_who);
+	void send_info(int who, int to_who, bool recordonly = false);
 
 };
 
diff --git a/src/game/server/gamecontroller.cpp b/src/game/server/gamecontroller.cpp
index 7329240b..aa8c09b7 100644
--- a/src/game/server/gamecontroller.cpp
+++ b/src/game/server/gamecontroller.cpp
@@ -457,8 +457,19 @@ void GAMECONTROLLER::snap(int snapping_client)
 	gameobj->round_num = (strlen(config.sv_maprotation) || config.sv_rounds_per_map > 1) ? config.sv_rounds_per_map : 0;
 	gameobj->round_current = round_count+1;
 	
-	gameobj->teamscore_red = is_teamplay() ? teamscore[0] : game.players[snapping_client]->score;
-	gameobj->teamscore_blue = teamscore[1];
+	
+	if(snapping_client == -1)
+	{
+		// we are recording a demo, just set the scores
+		gameobj->teamscore_red = teamscore[0];
+		gameobj->teamscore_blue = teamscore[1];
+	}
+	else
+	{
+		// TODO: this little hack should be removed
+		gameobj->teamscore_red = is_teamplay() ? teamscore[0] : game.players[snapping_client]->score;
+		gameobj->teamscore_blue = teamscore[1];
+	}
 }
 
 int GAMECONTROLLER::get_auto_team(int notthisid)