about summary refs log tree commit diff
path: root/src/game/client/components
diff options
context:
space:
mode:
authorMagnus Auvinen <magnus.auvinen@gmail.com>2008-08-27 15:48:50 +0000
committerMagnus Auvinen <magnus.auvinen@gmail.com>2008-08-27 15:48:50 +0000
commitdfe499248f1b1236487156b28e4a535d7963fe35 (patch)
treea750b0f28cfd3f3e252602681412ac1adc6d29c7 /src/game/client/components
parentd711dd190cac809a9bd278fba03ed974812bb863 (diff)
downloadzcatch-dfe499248f1b1236487156b28e4a535d7963fe35.tar.gz
zcatch-dfe499248f1b1236487156b28e4a535d7963fe35.zip
major commit. game client restructure. not complete, loads of stuff not working, but the structure is there
Diffstat (limited to 'src/game/client/components')
-rw-r--r--src/game/client/components/binds.cpp181
-rw-r--r--src/game/client/components/binds.hpp19
-rw-r--r--src/game/client/components/broadcast.cpp37
-rw-r--r--src/game/client/components/broadcast.hpp14
-rw-r--r--src/game/client/components/camera.cpp45
-rw-r--r--src/game/client/components/camera.hpp13
-rw-r--r--src/game/client/components/chat.cpp201
-rw-r--r--src/game/client/components/chat.hpp49
-rw-r--r--src/game/client/components/console.cpp503
-rw-r--r--src/game/client/components/console.hpp54
-rw-r--r--src/game/client/components/controls.cpp184
-rw-r--r--src/game/client/components/controls.hpp19
-rw-r--r--src/game/client/components/damageind.cpp66
-rw-r--r--src/game/client/components/damageind.hpp31
-rw-r--r--src/game/client/components/debughud.cpp113
-rw-r--r--src/game/client/components/debughud.hpp10
-rw-r--r--src/game/client/components/effects.cpp240
-rw-r--r--src/game/client/components/effects.hpp23
-rw-r--r--src/game/client/components/emoticon.cpp121
-rw-r--r--src/game/client/components/emoticon.hpp22
-rw-r--r--src/game/client/components/flow.cpp84
-rw-r--r--src/game/client/components/flow.hpp25
-rw-r--r--src/game/client/components/hud.cpp273
-rw-r--r--src/game/client/components/hud.hpp21
-rw-r--r--src/game/client/components/items.cpp254
-rw-r--r--src/game/client/components/items.hpp13
-rw-r--r--src/game/client/components/killmessages.cpp130
-rw-r--r--src/game/client/components/killmessages.hpp24
-rw-r--r--src/game/client/components/maplayers.cpp159
-rw-r--r--src/game/client/components/maplayers.hpp16
-rw-r--r--src/game/client/components/menus.cpp1051
-rw-r--r--src/game/client/components/menus.hpp96
-rw-r--r--src/game/client/components/menus_browser.cpp531
-rw-r--r--src/game/client/components/menus_settings.cpp567
-rw-r--r--src/game/client/components/motd.cpp75
-rw-r--r--src/game/client/components/motd.hpp15
-rw-r--r--src/game/client/components/particles.cpp155
-rw-r--r--src/game/client/components/particles.hpp91
-rw-r--r--src/game/client/components/players.cpp463
-rw-r--r--src/game/client/components/players.hpp16
-rw-r--r--src/game/client/components/scoreboard.cpp254
-rw-r--r--src/game/client/components/scoreboard.hpp11
-rw-r--r--src/game/client/components/skins.cpp188
-rw-r--r--src/game/client/components/skins.hpp36
44 files changed, 6493 insertions, 0 deletions
diff --git a/src/game/client/components/binds.cpp b/src/game/client/components/binds.cpp
new file mode 100644
index 00000000..5d033ad4
--- /dev/null
+++ b/src/game/client/components/binds.cpp
@@ -0,0 +1,181 @@
+extern "C" {
+	#include <engine/e_console.h>
+}
+
+#include "binds.hpp"
+
+BINDS::BINDS()
+{
+	mem_zero(keybindings, sizeof(keybindings));
+}
+
+void BINDS::bind(int keyid, const char *str)
+{
+	if(keyid < 0 && keyid >= KEY_LAST)
+		return;
+		
+	str_copy(keybindings[keyid], str, sizeof(keybindings[keyid]));
+	if(!keybindings[keyid][0])
+		dbg_msg("binds", "unbound %s (%d)", inp_key_name(keyid), keyid);
+	else
+		dbg_msg("binds", "bound %s (%d) = %s", inp_key_name(keyid), keyid, keybindings[keyid]);
+}
+
+
+bool BINDS::on_input(INPUT_EVENT e)
+{
+	// don't handle invalid events and keys that arn't set to anything
+	if(e.key <= 0 || e.key >= KEY_LAST || keybindings[e.key][0] == 0)
+		return false;
+
+	int stroke = 0;
+	if(e.flags&INPFLAG_PRESS)
+		stroke = 1;
+	console_execute_line_stroked(stroke, keybindings[e.key]);
+	return true;
+}
+
+void BINDS::unbindall()
+{
+	for(int i = 0; i < KEY_LAST; i++)
+		keybindings[i][0] = 0;
+}
+
+const char *BINDS::get(int keyid)
+{
+	if(keyid > 0 && keyid < KEY_LAST)
+		return keybindings[keyid];
+	return "";
+}
+
+void BINDS::set_defaults()
+{
+	unbindall();
+
+	// set default key bindings
+	bind(KEY_F1, "toggle_local_console");
+	bind(KEY_F2, "toggle_remote_console");
+	bind(KEY_TAB, "+scoreboard");
+	bind(KEY_F10, "screenshot");
+	
+	bind('A', "+left");
+	bind('D', "+right");
+	bind(KEY_SPACE, "+jump");
+	bind(KEY_MOUSE_1, "+fire");
+	bind(KEY_MOUSE_2, "+hook");
+	bind(KEY_LSHIFT, "+emote");
+
+	bind('1', "+weapon1");
+	bind('2', "+weapon2");
+	bind('3', "+weapon3");
+	bind('4', "+weapon4");
+	bind('5', "+weapon5");
+	
+	bind(KEY_MOUSE_WHEEL_UP, "+prevweapon");
+	bind(KEY_MOUSE_WHEEL_DOWN, "+nextweapon");
+	
+	bind('T', "chat all");
+	bind('Y', "chat team");	
+}
+
+void BINDS::on_init()
+{
+	set_defaults();
+}
+
+/*
+static int get_key_id(const char *key_name)
+{
+	// check for numeric
+	if(key_name[0] == '#')
+	{
+		int i = atoi(key_name+1);
+		if(i > 0 && i < KEY_LAST)
+			return i; // numeric
+	}
+		
+	// search for key
+	for(int i = 0; i < KEY_LAST; i++)
+	{
+		if(strcmp(key_name, inp_key_name(i)) == 0)
+			return i;
+	}
+	
+	return 0;
+}
+
+static void con_bind(void *result, void *user_data)
+{
+	const char *key_name = console_arg_string(result, 0);
+	int id = get_key_id(key_name);
+	
+	if(!id)
+	{
+		dbg_msg("binds", "key %s not found", key_name);
+		return;
+	}
+	
+	binds_set(id, console_arg_string(result, 1));
+}
+
+
+static void con_unbind(void *result, void *user_data)
+{
+	const char *key_name = console_arg_string(result, 0);
+	int id = get_key_id(key_name);
+	
+	if(!id)
+	{
+		dbg_msg("binds", "key %s not found", key_name);
+		return;
+	}
+	
+	binds_set(id, "");
+}
+
+
+static void con_unbindall(void *result, void *user_data)
+{
+	binds_unbindall();
+}
+
+
+static void con_dump_binds(void *result, void *user_data)
+{
+	for(int i = 0; i < KEY_LAST; i++)
+	{
+		if(keybindings[i][0] == 0)
+			continue;
+		dbg_msg("binds", "%s (%d) = %s", inp_key_name(i), i, keybindings[i]);
+	}
+}
+
+void binds_save()
+{
+	char buffer[256];
+	char *end = buffer+sizeof(buffer)-8;
+	client_save_line("unbindall");
+	for(int i = 0; i < KEY_LAST; i++)
+	{
+		if(keybindings[i][0] == 0)
+			continue;
+		str_format(buffer, sizeof(buffer), "bind %s ", inp_key_name(i));
+		
+		// process the string. we need to escape some characters
+		const char *src = keybindings[i];
+		char *dst = buffer + strlen(buffer);
+		*dst++ = '"';
+		while(*src && dst < end)
+		{
+			if(*src == '"' || *src == '\\') // escape \ and "
+				*dst++ = '\\';
+			*dst++ = *src++;
+		}
+		*dst++ = '"';
+		*dst++ = 0;
+		
+		client_save_line(buffer);
+	}
+}
+
+*/
diff --git a/src/game/client/components/binds.hpp b/src/game/client/components/binds.hpp
new file mode 100644
index 00000000..304b4a8d
--- /dev/null
+++ b/src/game/client/components/binds.hpp
@@ -0,0 +1,19 @@
+#include <game/client/component.hpp>
+
+class BINDS : public COMPONENT
+{
+	char keybindings[KEY_LAST][128];
+public:
+	BINDS();
+	
+	void bind(int keyid, const char *str);
+	void set_defaults();
+	void unbindall();
+	const char *get(int keyid);
+	
+	/*virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);*/
+	virtual void on_init();
+	virtual bool on_input(INPUT_EVENT e);
+};
diff --git a/src/game/client/components/broadcast.cpp b/src/game/client/components/broadcast.cpp
new file mode 100644
index 00000000..31ff6a71
--- /dev/null
+++ b/src/game/client/components/broadcast.cpp
@@ -0,0 +1,37 @@
+#include <engine/e_client_interface.h>
+#include <engine/e_config.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/gameclient.hpp>
+//#include <game/client/gc_anim.hpp>
+#include <game/client/gc_client.hpp>
+
+#include "broadcast.hpp"
+	
+void BROADCAST::on_reset()
+{
+	broadcast_time = 0;
+}
+
+void BROADCAST::on_render()
+{
+	gfx_mapscreen(0, 0, 300*gfx_screenaspect(), 300);
+		
+	if(time_get() < broadcast_time)
+	{
+		float w = gfx_text_width(0, 14, broadcast_text, -1);
+		gfx_text(0, 150*gfx_screenaspect()-w/2, 35, 14, broadcast_text, -1);
+	}
+}
+
+void BROADCAST::on_message(int msgtype, void *rawmsg)
+{
+	if(msgtype == NETMSGTYPE_SV_BROADCAST)
+	{
+		NETMSG_SV_BROADCAST *msg = (NETMSG_SV_BROADCAST *)rawmsg;
+		str_copy(broadcast_text, msg->message, sizeof(broadcast_text));
+		broadcast_time = time_get()+time_freq()*10;
+	}
+}
+
diff --git a/src/game/client/components/broadcast.hpp b/src/game/client/components/broadcast.hpp
new file mode 100644
index 00000000..102201cc
--- /dev/null
+++ b/src/game/client/components/broadcast.hpp
@@ -0,0 +1,14 @@
+#include <game/client/component.hpp>
+
+class BROADCAST : public COMPONENT
+{
+public:
+	// broadcasts
+	char broadcast_text[1024];
+	int64 broadcast_time;
+	
+	virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);
+};
+
diff --git a/src/game/client/components/camera.cpp b/src/game/client/components/camera.cpp
new file mode 100644
index 00000000..32442031
--- /dev/null
+++ b/src/game/client/components/camera.cpp
@@ -0,0 +1,45 @@
+extern "C" {
+	#include <engine/e_config.h>
+	#include <engine/e_client_interface.h>
+}
+
+#include <base/math.hpp>
+#include <game/collision.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/component.hpp>
+
+#include "camera.hpp"
+#include "controls.hpp"
+
+CAMERA::CAMERA()
+{
+}
+
+void CAMERA::on_render()
+{
+	//vec2 center;
+	zoom = 1.0f;
+	
+	bool spectate = false;
+
+	if(spectate)
+		center = gameclient.controls->mouse_pos;
+	else
+	{
+
+		float l = length(gameclient.controls->mouse_pos);
+		float deadzone = config.cl_mouse_deadzone;
+		float follow_factor = config.cl_mouse_followfactor/100.0f;
+		vec2 camera_offset(0, 0);
+
+		float offset_amount = max(l-deadzone, 0.0f) * follow_factor;
+		if(l > 0.0001f) // make sure that this isn't 0
+			camera_offset = normalize(gameclient.controls->mouse_pos)*offset_amount;
+		
+		center = gameclient.local_character_pos + camera_offset;
+	}
+
+	// set listner pos
+	snd_set_listener_pos(center.x, center.y);
+}
+
diff --git a/src/game/client/components/camera.hpp b/src/game/client/components/camera.hpp
new file mode 100644
index 00000000..1cb05f5b
--- /dev/null
+++ b/src/game/client/components/camera.hpp
@@ -0,0 +1,13 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+class CAMERA : public COMPONENT
+{	
+public:
+	vec2 center;
+	float zoom;
+
+	CAMERA();
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp
new file mode 100644
index 00000000..ecd29ab8
--- /dev/null
+++ b/src/game/client/components/chat.cpp
@@ -0,0 +1,201 @@
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/gameclient.hpp>
+//#include <game/client/gc_anim.hpp>
+#include <game/client/gc_client.hpp>
+
+#include "chat.hpp"
+
+void CHAT::on_reset()
+{
+	mode = MODE_NONE;
+	for(int i = 0; i < MAX_LINES; i++)
+		lines[i].tick = -1000000;
+	current_line = 0;
+}
+
+
+bool CHAT::on_input(INPUT_EVENT e)
+{
+	if(mode == MODE_NONE)
+		return false;
+
+	if(e.flags&INPFLAG_PRESS && (e.key == KEY_ENTER || e.key == KEY_KP_ENTER))
+	{
+		if(input.get_string()[0])
+			gameclient.chat->say(mode == MODE_ALL ? 0 : 1, input.get_string());
+		mode = MODE_NONE;
+	}
+	else
+		input.process_input(e);
+	
+	return true;
+}
+
+
+void CHAT::enable_mode(int team)
+{
+	if(mode == MODE_NONE)
+	{
+		if(team)
+			mode = MODE_TEAM;
+		else
+			mode = MODE_ALL;
+		
+		input.clear();
+		inp_clear_events();
+	}
+}
+
+void CHAT::on_message(int msgtype, void *rawmsg)
+{
+	if(msgtype == NETMSGTYPE_SV_CHAT)
+	{
+		NETMSG_SV_CHAT *msg = (NETMSG_SV_CHAT *)rawmsg;
+		add_line(msg->cid, msg->team, msg->message);
+
+		if(msg->cid >= 0) // TODO: repair me
+			snd_play(CHN_GUI, data->sounds[SOUND_CHAT_CLIENT].sounds[0].id, 0);
+		else
+			snd_play(CHN_GUI, data->sounds[SOUND_CHAT_SERVER].sounds[0].id, 0);
+	}
+}
+
+
+
+void CHAT::add_line(int client_id, int team, const char *line)
+{
+	current_line = (current_line+1)%MAX_LINES;
+	lines[current_line].tick = client_tick();
+	lines[current_line].client_id = client_id;
+	lines[current_line].team = team;
+	lines[current_line].name_color = -2;
+
+	if(client_id == -1) // server message
+	{
+		str_copy(lines[current_line].name, "*** ", sizeof(lines[current_line].name));
+		str_format(lines[current_line].text, sizeof(lines[current_line].text), "%s", line);
+	}
+	else
+	{
+		if(gameclient.clients[client_id].team == -1)
+			lines[current_line].name_color = -1;
+
+		if(gameclient.snap.gameobj && gameclient.snap.gameobj->flags&GAMEFLAG_TEAMS)
+		{
+			if(gameclient.clients[client_id].team == 0)
+				lines[current_line].name_color = 0;
+			else if(gameclient.clients[client_id].team == 1)
+				lines[current_line].name_color = 1;
+		}
+		
+		str_copy(lines[current_line].name, gameclient.clients[client_id].name, sizeof(lines[current_line].name));
+		str_format(lines[current_line].text, sizeof(lines[current_line].text), ": %s", line);
+	}
+	
+	dbg_msg("chat", "%s%s", lines[current_line].name, lines[current_line].text);
+}
+
+void CHAT::on_render()
+{
+	gfx_mapscreen(0,0,300*gfx_screenaspect(),300);
+	float x = 10.0f;
+	float y = 300.0f-30.0f;
+	if(mode != MODE_NONE)
+	{
+		// render chat input
+		TEXT_CURSOR cursor;
+		gfx_text_set_cursor(&cursor, x, y, 8.0f, TEXTFLAG_RENDER);
+		cursor.line_width = 300.0f;
+		
+		if(mode == MODE_ALL)
+			gfx_text_ex(&cursor, "All: ", -1);
+		else if(mode == MODE_TEAM)
+			gfx_text_ex(&cursor, "Team: ", -1);
+		else
+			gfx_text_ex(&cursor, "Chat: ", -1);
+			
+		gfx_text_ex(&cursor, input.get_string(), input.cursor_offset());
+		TEXT_CURSOR marker = cursor;
+		gfx_text_ex(&marker, "|", -1);
+		gfx_text_ex(&cursor, input.get_string()+input.cursor_offset(), -1);
+	}
+
+	y -= 8;
+
+	int i;
+	for(i = 0; i < MAX_LINES; i++)
+	{
+		int r = ((current_line-i)+MAX_LINES)%MAX_LINES;
+		if(client_tick() > lines[r].tick+50*15)
+			break;
+
+		float begin = x;
+		float fontsize = 8.0f;
+		
+		// get the y offset
+		TEXT_CURSOR cursor;
+		gfx_text_set_cursor(&cursor, begin, 0, fontsize, 0);
+		cursor.line_width = 300.0f;
+		gfx_text_ex(&cursor, lines[r].name, -1);
+		gfx_text_ex(&cursor, lines[r].text, -1);
+		y -= cursor.y + cursor.font_size;
+
+		// reset the cursor
+		gfx_text_set_cursor(&cursor, begin, y, fontsize, TEXTFLAG_RENDER);
+		cursor.line_width = 300.0f;
+
+		// render name
+		gfx_text_color(0.8f,0.8f,0.8f,1);
+		if(lines[r].client_id == -1)
+			gfx_text_color(1,1,0.5f,1); // system
+		else if(lines[r].team)
+			gfx_text_color(0.45f,0.9f,0.45f,1); // team message
+		else if(lines[r].name_color == 0)
+			gfx_text_color(1.0f,0.5f,0.5f,1); // red
+		else if(lines[r].name_color == 1)
+			gfx_text_color(0.7f,0.7f,1.0f,1); // blue
+		else if(lines[r].name_color == -1)
+			gfx_text_color(0.75f,0.5f,0.75f, 1); // spectator
+			
+		// render name
+		gfx_text_ex(&cursor, lines[r].name, -1);
+
+		// render line
+		gfx_text_color(1,1,1,1);
+		if(lines[r].client_id == -1)
+			gfx_text_color(1,1,0.5f,1); // system
+		else if(lines[r].team)
+			gfx_text_color(0.65f,1,0.65f,1); // team message
+
+		gfx_text_ex(&cursor, lines[r].text, -1);
+	}
+
+	gfx_text_color(1,1,1,1);
+}
+
+void con_chat(void *result, void *user_data)
+{
+	/*
+	const char *mode = console_arg_string(result, 0);
+	if(strcmp(mode, "all") == 0)
+		chat_enable_mode(0);
+	else if(strcmp(mode, "team") == 0)
+		chat_enable_mode(1);
+	else
+		dbg_msg("console", "expected all or team as mode");
+		*/
+}
+
+
+void CHAT::say(int team, const char *line)
+{
+	// send chat message
+	NETMSG_CL_SAY msg;
+	msg.team = team;
+	msg.message = line;
+	msg.pack(MSGFLAG_VITAL);
+	client_send_msg();
+}
diff --git a/src/game/client/components/chat.hpp b/src/game/client/components/chat.hpp
new file mode 100644
index 00000000..caec18b2
--- /dev/null
+++ b/src/game/client/components/chat.hpp
@@ -0,0 +1,49 @@
+#include <game/client/component.hpp>
+#include <game/client/lineinput.hpp>
+
+class CHAT : public COMPONENT
+{
+public:
+	LINEINPUT input;
+	
+	enum 
+	{
+		MAX_LINES = 10,
+	};
+
+	struct LINE
+	{
+		int tick;
+		int client_id;
+		int team;
+		int name_color;
+		char name[64];
+		char text[512];
+	};
+
+	LINE lines[MAX_LINES];
+	int current_line;
+
+	// chat
+	enum
+	{
+		MODE_NONE=0,
+		MODE_ALL,
+		MODE_TEAM,
+	};
+
+	int mode;
+	
+	void add_line(int client_id, int team, const char *line);
+	//void chat_reset();
+	//bool chat_input_handle(INPUT_EVENT e, void *user_data);
+	
+	void enable_mode(int team);
+	
+	void say(int team, const char *line);
+	
+	virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);
+	virtual bool on_input(INPUT_EVENT e);
+};
diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp
new file mode 100644
index 00000000..6437b718
--- /dev/null
+++ b/src/game/client/components/console.cpp
@@ -0,0 +1,503 @@
+//#include "gc_console.hpp"
+#include <game/generated/gc_data.hpp>
+
+#include <base/system.h>
+
+extern "C" {
+	#include <engine/e_client_interface.h>
+	#include <engine/e_config.h>
+	#include <engine/e_console.h>
+	#include <engine/e_ringbuffer.h>
+	#include <engine/client/ec_font.h>
+}
+
+#include <cstring>
+#include <cstdio>
+
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_client.hpp>
+
+#include <game/version.hpp>
+
+#include <game/client/lineinput.hpp>
+
+#include "console.hpp"
+
+enum
+{
+	CONSOLE_CLOSED,
+	CONSOLE_OPENING,
+	CONSOLE_OPEN,
+	CONSOLE_CLOSING,
+};
+
+CONSOLE::INSTANCE::INSTANCE(int t)
+{
+	// init ringbuffers
+	history = ringbuf_init(history_data, sizeof(history_data));
+	backlog = ringbuf_init(backlog_data, sizeof(backlog_data));
+	
+	history_entry = 0x0;
+	
+	type = t;
+}
+
+void CONSOLE::INSTANCE::execute_line(const char *line)
+{
+	if(type == 0)
+		console_execute_line(line);
+	else
+	{
+		if(client_rcon_authed())
+			client_rcon(line);
+		else
+			client_rcon_auth("", line);
+	}
+}
+
+void CONSOLE::INSTANCE::on_input(INPUT_EVENT e)
+{
+	bool handled = false;
+	
+	if(e.flags&INPFLAG_PRESS)
+	{
+		if(e.key == KEY_ENTER || e.key == KEY_KP_ENTER)
+		{
+			if(input.get_string()[0])
+			{
+				char *entry = (char *)ringbuf_allocate(history, input.get_length()+1);
+				mem_copy(entry, input.get_string(), input.get_length()+1);
+				
+				execute_line(input.get_string());
+				input.clear();
+				history_entry = 0x0;
+			}
+			
+			handled = true;
+		}
+		else if (e.key == KEY_UP)
+		{
+			if (history_entry)
+			{
+				char *test = (char *)ringbuf_prev(history, history_entry);
+
+				if (test)
+					history_entry = test;
+			}
+			else
+				history_entry = (char *)ringbuf_last(history);
+
+			if (history_entry)
+			{
+				unsigned int len = strlen(history_entry);
+				if (len < sizeof(input) - 1)
+					input.set(history_entry);
+			}
+			handled = true;
+		}
+		else if (e.key == KEY_DOWN)
+		{
+			if (history_entry)
+				history_entry = (char *)ringbuf_next(history, history_entry);
+
+			if (history_entry)
+			{
+				unsigned int len = strlen(history_entry);
+				if (len < sizeof(input) - 1)
+					input.set(history_entry);
+			}
+			else
+				input.clear();
+			handled = true;
+		}
+	}
+	
+	if(!handled)
+		input.process_input(e);
+}
+
+void CONSOLE::INSTANCE::print_line(const char *line)
+{
+	int len = strlen(line);
+
+	if (len > 255)
+		len = 255;
+
+	char *entry = (char *)ringbuf_allocate(backlog, len+1);
+	mem_copy(entry, line, len+1);
+}
+
+CONSOLE::CONSOLE()
+: local_console(0), remote_console(1)
+{
+	console_type = 0;
+	console_state = CONSOLE_CLOSED;
+	state_change_end = 0.0f;
+	state_change_duration = 0.1f;
+}
+
+float CONSOLE::time_now()
+{
+	static long long time_start = time_get();
+	return float(time_get()-time_start)/float(time_freq());
+}
+
+CONSOLE::INSTANCE *CONSOLE::current_console()
+{
+    if(console_type != 0)
+    	return &remote_console;
+    return &local_console;
+}
+
+void CONSOLE::on_reset()
+{
+}
+
+// only defined for 0<=t<=1
+static float console_scale_func(float t)
+{
+	//return t;
+	return sinf(acosf(1.0f-t));
+}
+
+void CONSOLE::on_render()
+{
+
+    RECT screen = *ui_screen();
+	float console_max_height = screen.h*3/5.0f;
+	float console_height;
+
+	float progress = (time_now()-(state_change_end-state_change_duration))/float(state_change_duration);
+
+	if (progress >= 1.0f)
+	{
+		if (console_state == CONSOLE_CLOSING)
+			console_state = CONSOLE_CLOSED;
+		else if (console_state == CONSOLE_OPENING)
+			console_state = CONSOLE_OPEN;
+
+		progress = 1.0f;
+	}
+	
+	if (console_state == CONSOLE_CLOSED)
+		return;
+
+	float console_height_scale;
+
+	if (console_state == CONSOLE_OPENING)
+		console_height_scale = console_scale_func(progress);
+	else if (console_state == CONSOLE_CLOSING)
+		console_height_scale = console_scale_func(1.0f-progress);
+	else //if (console_state == CONSOLE_OPEN)
+		console_height_scale = console_scale_func(1.0f);
+
+	console_height = console_height_scale*console_max_height;
+
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+
+	// do console shadow
+	gfx_texture_set(-1);
+    gfx_quads_begin();
+    gfx_setcolorvertex(0, 0,0,0, 0.5f);
+    gfx_setcolorvertex(1, 0,0,0, 0.5f);
+    gfx_setcolorvertex(2, 0,0,0, 0.0f);
+    gfx_setcolorvertex(3, 0,0,0, 0.0f);
+    gfx_quads_drawTL(0,console_height,screen.w,10.0f);
+    gfx_quads_end();
+
+	// do background
+	gfx_texture_set(data->images[IMAGE_CONSOLE_BG].id);
+    gfx_quads_begin();
+    gfx_setcolor(0.2f, 0.2f, 0.2f,0.9f);
+    if(console_type != 0)
+	    gfx_setcolor(0.4f, 0.2f, 0.2f,0.9f);
+    gfx_quads_setsubset(0,-console_height*0.075f,screen.w*0.075f*0.5f,0);
+    gfx_quads_drawTL(0,0,screen.w,console_height);
+    gfx_quads_end();
+
+	// do small bar shadow
+	gfx_texture_set(-1);
+    gfx_quads_begin();
+    gfx_setcolorvertex(0, 0,0,0, 0.0f);
+    gfx_setcolorvertex(1, 0,0,0, 0.0f);
+    gfx_setcolorvertex(2, 0,0,0, 0.25f);
+    gfx_setcolorvertex(3, 0,0,0, 0.25f);
+    gfx_quads_drawTL(0,console_height-20,screen.w,10);
+    gfx_quads_end();
+
+	// do the lower bar
+	gfx_texture_set(data->images[IMAGE_CONSOLE_BAR].id);
+    gfx_quads_begin();
+    gfx_setcolor(1.0f, 1.0f, 1.0f, 0.9f);
+    gfx_quads_setsubset(0,0.1f,screen.w*0.015f,1-0.1f);
+    gfx_quads_drawTL(0,console_height-10.0f,screen.w,10.0f);
+    gfx_quads_end();
+    
+    console_height -= 10.0f;
+    
+    INSTANCE *console = current_console();
+
+	{
+		float font_size = 10.0f;
+		float row_height = font_size*1.25f;
+		float x = 3;
+		float y = console_height - row_height - 2;
+
+		// render prompt		
+		TEXT_CURSOR cursor;
+		gfx_text_set_cursor(&cursor, x, y, font_size, TEXTFLAG_RENDER);
+
+		const char *prompt = "> ";
+		if(console_type)
+		{
+			if(client_state() == CLIENTSTATE_ONLINE)
+			{
+				if(client_rcon_authed())
+					prompt = "rcon> ";
+				else
+					prompt = "ENTER PASSWORD> ";
+			}
+			else
+				prompt = "NOT CONNECTED> ";
+		}
+
+		gfx_text_ex(&cursor, prompt, -1);
+		
+		// render console input
+		gfx_text_ex(&cursor, console->input.get_string(), console->input.cursor_offset());
+		TEXT_CURSOR marker = cursor;
+		gfx_text_ex(&marker, "|", -1);
+		gfx_text_ex(&cursor, console->input.get_string()+console->input.cursor_offset(), -1);
+		
+		// render version
+		char buf[128];
+		str_format(buf, sizeof(buf), "v%s", GAME_VERSION);
+		float version_width = gfx_text_width(0, font_size, buf, -1);
+		gfx_text(0, screen.w-version_width-5, y, font_size, buf, -1);
+
+		// render log
+		y -= row_height;
+		char *entry = (char *)ringbuf_last(console->backlog);
+		while (y > 0.0f && entry)
+		{
+			gfx_text(0, x, y, font_size, entry, -1);
+			y -= row_height;
+
+			entry = (char *)ringbuf_prev(console->backlog, entry);
+		}
+	}	
+}
+
+void CONSOLE::on_message(int msgtype, void *rawmsg)
+{
+}
+
+bool CONSOLE::on_input(INPUT_EVENT e)
+{
+	return false;
+}
+
+
+/*
+static void client_console_print(const char *str)
+{
+	// TODO: repair me
+	//local_console.print_line(str);
+}
+
+void console_rcon_print(const char *line)
+{
+	// TODO: repair me
+	//remote_console.print_line(line);
+}*/
+
+/*
+static void con_team(void *result, void *user_data)
+{
+	send_switch_team(console_arg_int(result, 0));
+}
+
+static void con_say(void *result, void *user_data)
+{
+	chat_say(0, console_arg_string(result, 0));
+}
+
+static void con_sayteam(void *result, void *user_data)
+{
+	chat_say(1, console_arg_string(result, 0));
+}
+
+void send_kill(int client_id);
+
+static void con_kill(void *result, void *user_data)
+{
+	send_kill(-1);
+}
+
+static void con_key_input_state(void *result, void *user_data)
+{
+	((int *)user_data)[0] = console_arg_int(result, 0);
+}
+
+static void con_key_input_counter(void *result, void *user_data)
+{
+	int *v = (int *)user_data;
+	if(((*v)&1) != console_arg_int(result, 0))
+		(*v)++;
+	*v &= INPUT_STATE_MASK;
+}
+
+static void con_key_input_weapon(void *result, void *user_data)
+{
+	int w = (char *)user_data - (char *)0;
+	if(console_arg_int(result, 0))
+		input_data.wanted_weapon = w;
+}
+
+static void con_key_input_nextprev_weapon(void *result, void *user_data)
+{
+	con_key_input_counter(result, user_data);
+	input_data.wanted_weapon = 0;
+}
+
+static void con_toggle_local_console(void *result, void *user_data)
+{
+	console_toggle(0);
+}
+
+static void con_toggle_remote_console(void *result, void *user_data)
+{
+	console_toggle(1);
+}
+
+static void con_emote(void *result, void *user_data)
+{
+	send_emoticon(console_arg_int(result, 0));
+}
+
+extern void con_chat(void *result, void *user_data);
+
+void client_console_init()
+{
+	console_register_print_callback(client_console_print);
+
+	//
+	MACRO_REGISTER_COMMAND("toggle_local_console", "", con_toggle_local_console, 0x0);
+	MACRO_REGISTER_COMMAND("toggle_remote_console", "", con_toggle_remote_console, 0x0);
+
+	//
+	MACRO_REGISTER_COMMAND("team", "i", con_team, 0x0);
+	MACRO_REGISTER_COMMAND("kill", "", con_kill, 0x0);
+	
+	// bindings
+	MACRO_REGISTER_COMMAND("bind", "sr", con_bind, 0x0);
+	MACRO_REGISTER_COMMAND("unbind", "s", con_unbind, 0x0);
+	MACRO_REGISTER_COMMAND("unbindall", "", con_unbindall, 0x0);
+	
+	MACRO_REGISTER_COMMAND("dump_binds", "", con_dump_binds, 0x0);
+
+	// chatting
+	MACRO_REGISTER_COMMAND("say", "r", con_say, 0x0);
+	MACRO_REGISTER_COMMAND("say_team", "r", con_sayteam, 0x0);
+	MACRO_REGISTER_COMMAND("chat", "s", con_chat, 0x0);
+	MACRO_REGISTER_COMMAND("emote", "i", con_emote, 0);
+
+	// game commands
+	MACRO_REGISTER_COMMAND("+left", "", con_key_input_state, &input_direction_left);
+	MACRO_REGISTER_COMMAND("+right", "", con_key_input_state, &input_direction_right);
+	MACRO_REGISTER_COMMAND("+jump", "", con_key_input_state, &input_data.jump);
+	MACRO_REGISTER_COMMAND("+hook", "", con_key_input_state, &input_data.hook);
+	MACRO_REGISTER_COMMAND("+fire", "", con_key_input_counter, &input_data.fire);
+	MACRO_REGISTER_COMMAND("+weapon1", "", con_key_input_weapon, (void *)1);
+	MACRO_REGISTER_COMMAND("+weapon2", "", con_key_input_weapon, (void *)2);
+	MACRO_REGISTER_COMMAND("+weapon3", "", con_key_input_weapon, (void *)3);
+	MACRO_REGISTER_COMMAND("+weapon4", "", con_key_input_weapon, (void *)4);
+	MACRO_REGISTER_COMMAND("+weapon5", "", con_key_input_weapon, (void *)5);
+
+	MACRO_REGISTER_COMMAND("+nextweapon", "", con_key_input_nextprev_weapon, &input_data.next_weapon);
+	MACRO_REGISTER_COMMAND("+prevweapon", "", con_key_input_nextprev_weapon, &input_data.prev_weapon);
+	
+	MACRO_REGISTER_COMMAND("+emote", "", con_key_input_state, &emoticon_selector_active);
+	MACRO_REGISTER_COMMAND("+scoreboard", "", con_key_input_state, &scoreboard_active);
+	
+	binds_default();
+}
+
+bool console_input_cli(INPUT_EVENT e, void *user_data)
+{
+	if(!console_active())
+		return false;
+	
+	if(e.key == KEY_ESC && (e.flags&INPFLAG_PRESS))
+		console_toggle(console_type);
+	else
+		current_console()->handle_event(e);
+	return true;
+}
+
+static bool console_execute_event(INPUT_EVENT e)
+{
+	// don't handle invalid events and keys that arn't set to anything
+	if(e.key <= 0 || e.key >= KEY_LAST || keybindings[e.key][0] == 0)
+		return false;
+
+	int stroke = 0;
+	if(e.flags&INPFLAG_PRESS)
+		stroke = 1;
+	console_execute_line_stroked(stroke, keybindings[e.key]);
+	return true;
+}
+
+bool console_input_special_binds(INPUT_EVENT e, void *user_data)
+{
+	// only handle function keys
+	if(e.key < KEY_F1 || e.key > KEY_F25)
+		return false;
+	return console_execute_event(e);
+}
+
+bool console_input_normal_binds(INPUT_EVENT e, void *user_data)
+{
+	// need to be ingame for these binds
+	if(client_state() != CLIENTSTATE_ONLINE)
+		return false;
+	return console_execute_event(e);
+}
+
+void console_toggle(int type)
+{
+	if(console_type != type && (console_state == CONSOLE_OPEN || console_state == CONSOLE_OPENING))
+	{
+		// don't toggle console, just switch what console to use
+	}
+	else
+	{	
+		if (console_state == CONSOLE_CLOSED || console_state == CONSOLE_OPEN)
+		{
+			state_change_end = time_now()+state_change_duration;
+		}
+		else
+		{
+			float progress = state_change_end-time_now();
+			float reversed_progress = state_change_duration-progress;
+
+			state_change_end = time_now()+reversed_progress;
+		}
+
+		if (console_state == CONSOLE_CLOSED || console_state == CONSOLE_CLOSING)
+			console_state = CONSOLE_OPENING;
+		else
+			console_state = CONSOLE_CLOSING;
+	}
+
+	console_type = type;
+}
+
+
+
+int console_active()
+{
+	return console_state != CONSOLE_CLOSED;
+}
+
+*/
diff --git a/src/game/client/components/console.hpp b/src/game/client/components/console.hpp
new file mode 100644
index 00000000..988e4ea3
--- /dev/null
+++ b/src/game/client/components/console.hpp
@@ -0,0 +1,54 @@
+extern "C" {
+	#include <engine/e_client_interface.h>
+	#include <engine/e_config.h>
+	#include <engine/e_console.h>
+	#include <engine/e_ringbuffer.h>
+	#include <engine/client/ec_font.h>
+}
+
+#include <game/client/component.hpp>
+
+class CONSOLE : public COMPONENT
+{
+	class INSTANCE
+	{
+	public:
+		char history_data[65536];
+		RINGBUFFER *history;
+		char *history_entry;
+		
+		char backlog_data[65536];
+		RINGBUFFER *backlog;
+
+		LINEINPUT input;
+		
+		int type;
+		
+	public:
+		INSTANCE(int t);
+
+		void execute_line(const char *line);
+		
+		void on_input(INPUT_EVENT e);
+		void print_line(const char *line);
+	};
+	
+	INSTANCE local_console;
+	INSTANCE remote_console;
+	
+	INSTANCE *current_console();
+	float time_now();
+	
+	int console_type;
+	int console_state;
+	float state_change_end;
+	float state_change_duration;
+	
+public:
+	CONSOLE();
+
+	virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);
+	virtual bool on_input(INPUT_EVENT e);
+};
diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp
new file mode 100644
index 00000000..9f0ba30a
--- /dev/null
+++ b/src/game/client/components/controls.cpp
@@ -0,0 +1,184 @@
+extern "C" {
+	#include <engine/e_config.h>
+	#include <engine/e_console.h>
+	#include <engine/e_client_interface.h>
+}
+
+#include <base/math.hpp>
+#include <game/collision.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/component.hpp>
+
+#include "controls.hpp"
+
+CONTROLS::CONTROLS()
+{
+}
+
+static void con_key_input_state(void *result, void *user_data)
+{
+	((int *)user_data)[0] = console_arg_int(result, 0);
+}
+
+static void con_key_input_counter(void *result, void *user_data)
+{
+	int *v = (int *)user_data;
+	if(((*v)&1) != console_arg_int(result, 0))
+		(*v)++;
+	*v &= INPUT_STATE_MASK;
+}
+/*
+static void con_key_input_weapon(void *result, void *user_data)
+{
+	int w = (char *)user_data - (char *)0;
+	if(console_arg_int(result, 0))
+		input_data.wanted_weapon = w;
+}
+
+static void con_key_input_nextprev_weapon(void *result, void *user_data)
+{
+	con_key_input_counter(result, user_data);
+	input_data.wanted_weapon = 0;
+}*/
+
+void CONTROLS::on_init()
+{
+	// game commands
+	MACRO_REGISTER_COMMAND("+left", "", con_key_input_state, &input_direction_left);
+	MACRO_REGISTER_COMMAND("+right", "", con_key_input_state, &input_direction_right);
+	MACRO_REGISTER_COMMAND("+jump", "", con_key_input_state, &input_data.jump);
+	MACRO_REGISTER_COMMAND("+hook", "", con_key_input_state, &input_data.hook);
+	MACRO_REGISTER_COMMAND("+fire", "", con_key_input_counter, &input_data.fire);
+	/*
+	MACRO_REGISTER_COMMAND("+weapon1", "", con_key_input_weapon, (void *)1);
+	MACRO_REGISTER_COMMAND("+weapon2", "", con_key_input_weapon, (void *)2);
+	MACRO_REGISTER_COMMAND("+weapon3", "", con_key_input_weapon, (void *)3);
+	MACRO_REGISTER_COMMAND("+weapon4", "", con_key_input_weapon, (void *)4);
+	MACRO_REGISTER_COMMAND("+weapon5", "", con_key_input_weapon, (void *)5);
+
+	MACRO_REGISTER_COMMAND("+nextweapon", "", con_key_input_nextprev_weapon, &input_data.next_weapon);
+	MACRO_REGISTER_COMMAND("+prevweapon", "", con_key_input_nextprev_weapon, &input_data.prev_weapon);
+	*/
+}
+
+int CONTROLS::snapinput(int *data)
+{
+	static NETOBJ_PLAYER_INPUT last_data = {0};
+	static int64 last_send_time = 0;
+	
+	// update player state
+	/*if(chat_mode != CHATMODE_NONE) // TODO: repair me
+		input_data.player_state = PLAYERSTATE_CHATTING;
+	else if(menu_active)
+		input_data.player_state = PLAYERSTATE_IN_MENU;
+	else
+		input_data.player_state = PLAYERSTATE_PLAYING;*/
+	last_data.player_state = input_data.player_state;
+	
+	// we freeze the input if chat or menu is activated
+	/* repair me
+	if(menu_active || chat_mode != CHATMODE_NONE || console_active())
+	{
+		last_data.direction = 0;
+		last_data.hook = 0;
+		last_data.jump = 0;
+		
+		input_data = last_data;
+			
+		mem_copy(data, &input_data, sizeof(input_data));
+		return sizeof(input_data);
+	}*/
+	
+	input_data.target_x = (int)mouse_pos.x;
+	input_data.target_y = (int)mouse_pos.y;
+	if(!input_data.target_x && !input_data.target_y)
+		input_data.target_y = 1;
+		
+	// set direction
+	input_data.direction = 0;
+	if(input_direction_left && !input_direction_right)
+		input_data.direction = -1;
+	if(!input_direction_left && input_direction_right)
+		input_data.direction = 1;
+
+	// stress testing
+	if(config.dbg_stress)
+	{
+		float t = client_localtime();
+		mem_zero(&input_data, sizeof(input_data));
+
+		input_data.direction = ((int)t/2)&1;
+		input_data.jump = ((int)t);
+		input_data.fire = ((int)(t*10));
+		input_data.hook = ((int)(t*2))&1;
+		input_data.wanted_weapon = ((int)t)%NUM_WEAPONS;
+		input_data.target_x = (int)(sinf(t*3)*100.0f);
+		input_data.target_y = (int)(cosf(t*3)*100.0f);
+	}
+
+	// check if we need to send input
+	bool send = false;
+	if(input_data.direction != last_data.direction) send = true;
+	else if(input_data.jump != last_data.jump) send = true;
+	else if(input_data.fire != last_data.fire) send = true;
+	else if(input_data.hook != last_data.hook) send = true;
+	else if(input_data.player_state != last_data.player_state) send = true;
+	else if(input_data.wanted_weapon != last_data.wanted_weapon) send = true;
+	else if(input_data.next_weapon != last_data.next_weapon) send = true;
+	else if(input_data.prev_weapon != last_data.prev_weapon) send = true;
+
+	if(time_get() > last_send_time + time_freq()/5)
+		send = true;
+
+	last_data = input_data;
+	if(!send)
+		return 0;
+		
+	// copy and return size	
+	last_send_time = time_get();
+	mem_copy(data, &input_data, sizeof(input_data));
+	return sizeof(input_data);	
+}
+
+bool CONTROLS::on_mousemove(float x, float y)
+{
+	mouse_pos += vec2(x, y); // TODO: ugly
+
+	bool spectate = false;
+
+	//
+	float camera_max_distance = 200.0f;
+	float follow_factor = config.cl_mouse_followfactor/100.0f;
+	float deadzone = config.cl_mouse_deadzone;
+	float mouse_max = min(camera_max_distance/follow_factor + deadzone, (float)config.cl_mouse_max_distance);
+	
+	//vec2 camera_offset(0, 0);
+
+	if(spectate)
+	{
+		if(mouse_pos.x < 200.0f) mouse_pos.x = 200.0f;
+		if(mouse_pos.y < 200.0f) mouse_pos.y = 200.0f;
+		if(mouse_pos.x > col_width()*32-200.0f) mouse_pos.x = col_width()*32-200.0f;
+		if(mouse_pos.y > col_height()*32-200.0f) mouse_pos.y = col_height()*32-200.0f;
+		
+		target_pos = mouse_pos;
+	}
+	else
+	{
+		float l = length(mouse_pos);
+		
+		if(l > mouse_max)
+		{
+			mouse_pos = normalize(mouse_pos)*mouse_max;
+			l = mouse_max;
+		}
+		
+		target_pos = gameclient.local_character_pos + mouse_pos;
+
+		//float offset_amount = max(l-deadzone, 0.0f) * follow_factor;
+		//if(l > 0.0001f) // make sure that this isn't 0
+			//camera_offset = normalize(mouse_pos)*offset_amount;
+	}
+	
+	return true;
+}
diff --git a/src/game/client/components/controls.hpp b/src/game/client/components/controls.hpp
new file mode 100644
index 00000000..d875522a
--- /dev/null
+++ b/src/game/client/components/controls.hpp
@@ -0,0 +1,19 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+class CONTROLS : public COMPONENT
+{	
+public:
+	vec2 mouse_pos;
+	vec2 target_pos;
+
+	NETOBJ_PLAYER_INPUT input_data;
+	int input_direction_left;
+	int input_direction_right;
+
+	CONTROLS();
+	virtual bool on_mousemove(float x, float y);
+	virtual void on_init();
+	
+	int snapinput(int *data);
+};
diff --git a/src/game/client/components/damageind.cpp b/src/game/client/components/damageind.cpp
new file mode 100644
index 00000000..15ca4b18
--- /dev/null
+++ b/src/game/client/components/damageind.cpp
@@ -0,0 +1,66 @@
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/gamecore.hpp> // get_angle
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_render.hpp>
+#include "damageind.hpp"
+
+DAMAGEIND::DAMAGEIND()
+{
+	lastupdate = 0;
+	num_items = 0;
+}
+
+DAMAGEIND::ITEM *DAMAGEIND::create_i()
+{
+	if (num_items < MAX_ITEMS)
+	{
+		ITEM *p = &items[num_items];
+		num_items++;
+		return p;
+	}
+	return 0;
+}
+
+void DAMAGEIND::destroy_i(DAMAGEIND::ITEM *i)
+{
+	num_items--;
+	*i = items[num_items];
+}
+
+void DAMAGEIND::create(vec2 pos, vec2 dir)
+{
+	ITEM *i = create_i();
+	if (i)
+	{
+		i->pos = pos;
+		i->life = 0.75f;
+		i->dir = dir*-1;
+		i->startangle = (( (float)rand()/(float)RAND_MAX) - 1.0f) * 2.0f * pi;
+	}
+}
+
+void DAMAGEIND::on_render()
+{
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+	for(int i = 0; i < num_items;)
+	{
+		vec2 pos = mix(items[i].pos+items[i].dir*75.0f, items[i].pos, clamp((items[i].life-0.60f)/0.15f, 0.0f, 1.0f));
+
+		items[i].life -= client_frametime();
+		if(items[i].life < 0.0f)
+			destroy_i(&items[i]);
+		else
+		{
+			gfx_setcolor(1.0f,1.0f,1.0f, items[i].life/0.1f);
+			gfx_quads_setrotation(items[i].startangle + items[i].life * 2.0f);
+			select_sprite(SPRITE_STAR1);
+			draw_sprite(pos.x, pos.y, 48.0f);
+			i++;
+		}
+	}
+	gfx_quads_end();
+}
diff --git a/src/game/client/components/damageind.hpp b/src/game/client/components/damageind.hpp
new file mode 100644
index 00000000..c74af9ca
--- /dev/null
+++ b/src/game/client/components/damageind.hpp
@@ -0,0 +1,31 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+class DAMAGEIND : public COMPONENT
+{
+	int64 lastupdate;
+	struct ITEM
+	{
+		vec2 pos;
+		vec2 dir;
+		float life;
+		float startangle;
+	};
+
+	enum
+	{
+		MAX_ITEMS=64,
+	};
+
+	ITEM items[MAX_ITEMS];
+	int num_items;
+
+	ITEM *create_i();
+	void destroy_i(ITEM *i);
+
+public:	
+	DAMAGEIND();
+
+	void create(vec2 pos, vec2 dir);
+	virtual void on_render();
+};
diff --git a/src/game/client/components/debughud.cpp b/src/game/client/components/debughud.cpp
new file mode 100644
index 00000000..7ec7b88f
--- /dev/null
+++ b/src/game/client/components/debughud.cpp
@@ -0,0 +1,113 @@
+#include <memory.h> // memcmp
+
+extern "C" {
+	#include <engine/e_config.h>
+}
+
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/layers.hpp>
+
+#include <game/client/gameclient.hpp>
+#include <game/client/animstate.hpp>
+#include <game/client/gc_client.hpp>
+#include <game/client/gc_render.hpp>
+
+//#include "controls.hpp"
+//#include "camera.hpp"
+#include "debughud.hpp"
+
+void DEBUGHUD::render_netcorrections()
+{
+	if(!config.debug || !gameclient.snap.local_character || !gameclient.snap.local_prev_character)
+		return;
+
+	gfx_mapscreen(0, 0, 300*gfx_screenaspect(), 300);
+	
+	/*float speed = distance(vec2(netobjects.local_prev_character->x, netobjects.local_prev_character->y),
+		vec2(netobjects.local_character->x, netobjects.local_character->y));*/
+
+	float velspeed = length(vec2(gameclient.snap.local_character->vx/256.0f, gameclient.snap.local_character->vy/256.0f))*50;
+	
+	float ramp = velocity_ramp(velspeed, tuning.velramp_start, tuning.velramp_range, tuning.velramp_curvature);
+	
+	char buf[512];
+	str_format(buf, sizeof(buf), "%.0f\n%.0f\n%.2f\n%d %s\n%d %d",
+		velspeed, velspeed*ramp, ramp,
+		netobj_num_corrections(), netobj_corrected_on(),
+		gameclient.snap.local_character->x,
+		gameclient.snap.local_character->y
+	);
+	gfx_text(0, 150, 50, 12, buf, -1);
+}
+
+void DEBUGHUD::render_tuning()
+{
+	// render tuning debugging
+	if(!config.dbg_tuning)
+		return;
+		
+	TUNING_PARAMS standard_tuning;
+		
+	gfx_mapscreen(0, 0, 300*gfx_screenaspect(), 300);
+	
+	float y = 50.0f;
+	int count = 0;
+	for(int i = 0; i < tuning.num(); i++)
+	{
+		char buf[128];
+		float current, standard;
+		tuning.get(i, &current);
+		standard_tuning.get(i, &standard);
+		
+		if(standard == current)
+			gfx_text_color(1,1,1,1.0f);
+		else
+			gfx_text_color(1,0.25f,0.25f,1.0f);
+
+		float w;
+		float x = 5.0f;
+		
+		str_format(buf, sizeof(buf), "%.2f", standard);
+		x += 20.0f;
+		w = gfx_text_width(0, 5, buf, -1);
+		gfx_text(0x0, x-w, y+count*6, 5, buf, -1);
+
+		str_format(buf, sizeof(buf), "%.2f", current);
+		x += 20.0f;
+		w = gfx_text_width(0, 5, buf, -1);
+		gfx_text(0x0, x-w, y+count*6, 5, buf, -1);
+
+		x += 5.0f;
+		gfx_text(0x0, x, y+count*6, 5, tuning.names[i], -1);
+		
+		count++;
+	}
+	
+	y = y+count*6;
+	
+	gfx_texture_set(-1);
+	gfx_blend_normal();
+	gfx_lines_begin();
+	float height = 50.0f;
+	float pv = 1;
+	for(int i = 0; i < 100; i++)
+	{
+		float speed = i/100.0f * 3000;
+		float ramp = velocity_ramp(speed, tuning.velramp_start, tuning.velramp_range, tuning.velramp_curvature);
+		float rampedspeed = (speed * ramp)/1000.0f;
+		gfx_lines_draw((i-1)*2, y+height-pv*height, i*2, y+height-rampedspeed*height);
+		//gfx_lines_draw((i-1)*2, 200, i*2, 200);
+		pv = rampedspeed;
+	}
+	gfx_lines_end();
+	gfx_text_color(1,1,1,1);
+}
+
+void DEBUGHUD::on_render()
+{
+	render_tuning();
+	render_netcorrections();
+}
diff --git a/src/game/client/components/debughud.hpp b/src/game/client/components/debughud.hpp
new file mode 100644
index 00000000..473b2ce2
--- /dev/null
+++ b/src/game/client/components/debughud.hpp
@@ -0,0 +1,10 @@
+#include <game/client/component.hpp>
+
+class DEBUGHUD : public COMPONENT
+{	
+	void render_netcorrections();
+	void render_tuning();
+public:
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/effects.cpp b/src/game/client/components/effects.cpp
new file mode 100644
index 00000000..000d0f3a
--- /dev/null
+++ b/src/game/client/components/effects.cpp
@@ -0,0 +1,240 @@
+#include <engine/e_client_interface.h>
+//#include <gc_client.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/components/particles.hpp>
+#include <game/client/components/skins.hpp>
+#include <game/client/components/flow.hpp>
+#include <game/client/components/damageind.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/gc_client.hpp>
+
+#include "effects.hpp"
+
+EFFECTS::EFFECTS()
+{
+	add_50hz = false;
+	add_100hz = false;
+}
+
+void EFFECTS::air_jump(vec2 pos)
+{
+	PARTICLE p;
+	p.set_default();
+	p.spr = SPRITE_PART_AIRJUMP;
+	p.pos = pos + vec2(-6.0f, 16.0f);
+	p.vel = vec2(0, -200);
+	p.life_span = 0.5f;
+	p.start_size = 48.0f;
+	p.end_size = 0;
+	p.rot = frandom()*pi*2;
+	p.rotspeed = pi*2;
+	p.gravity = 500;
+	p.friction = 0.7f;
+	p.flow_affected = 0.0f;
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+
+	p.pos = pos + vec2(6.0f, 16.0f);
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+}
+
+void EFFECTS::damage_indicator(vec2 pos, vec2 dir)
+{
+	gameclient.damageind->create(pos, dir);
+}
+
+void EFFECTS::powerupshine(vec2 pos, vec2 size)
+{
+	if(!add_50hz)
+		return;
+		
+	PARTICLE p;
+	p.set_default();
+	p.spr = SPRITE_PART_SLICE;
+	p.pos = pos + vec2((frandom()-0.5f)*size.x, (frandom()-0.5f)*size.y);
+	p.vel = vec2(0, 0);
+	p.life_span = 0.5f;
+	p.start_size = 16.0f;
+	p.end_size = 0;
+	p.rot = frandom()*pi*2;
+	p.rotspeed = pi*2;
+	p.gravity = 500;
+	p.friction = 0.9f;
+	p.flow_affected = 0.0f;
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+}
+
+void EFFECTS::smoketrail(vec2 pos, vec2 vel)
+{
+	if(!add_50hz)
+		return;
+		
+	PARTICLE p;
+	p.set_default();
+	p.spr = SPRITE_PART_SMOKE;
+	p.pos = pos;
+	p.vel = vel + random_dir()*50.0f;
+	p.life_span = 0.5f + frandom()*0.5f;
+	p.start_size = 12.0f + frandom()*8;
+	p.end_size = 0;
+	p.friction = 0.7;
+	p.gravity = frandom()*-500.0f;
+	gameclient.particles->add(PARTICLES::GROUP_PROJECTILE_TRAIL, &p);
+}
+
+
+void EFFECTS::skidtrail(vec2 pos, vec2 vel)
+{
+	if(!add_100hz)
+		return;
+	
+	PARTICLE p;
+	p.set_default();
+	p.spr = SPRITE_PART_SMOKE;
+	p.pos = pos;
+	p.vel = vel + random_dir()*50.0f;
+	p.life_span = 0.5f + frandom()*0.5f;
+	p.start_size = 24.0f + frandom()*12;
+	p.end_size = 0;
+	p.friction = 0.7f;
+	p.gravity = frandom()*-500.0f;
+	p.color = vec4(0.75f,0.75f,0.75f,1.0f);
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);	
+}
+
+void EFFECTS::bullettrail(vec2 pos)
+{
+	if(!add_100hz)
+		return;
+		
+	PARTICLE p;
+	p.set_default();
+	p.spr = SPRITE_PART_BALL;
+	p.pos = pos;
+	p.life_span = 0.25f + frandom()*0.25f;
+	p.start_size = 8.0f;
+	p.end_size = 0;
+	p.friction = 0.7f;
+	gameclient.particles->add(PARTICLES::GROUP_PROJECTILE_TRAIL, &p);
+}
+
+void EFFECTS::playerspawn(vec2 pos)
+{
+	for(int i = 0; i < 32; i++)
+	{
+		PARTICLE p;
+		p.set_default();
+		p.spr = SPRITE_PART_SHELL;
+		p.pos = pos;
+		p.vel = random_dir() * (pow(frandom(), 3)*600.0f);
+		p.life_span = 0.3f + frandom()*0.3f;
+		p.start_size = 64.0f + frandom()*32;
+		p.end_size = 0;
+		p.rot = frandom()*pi*2;
+		p.rotspeed = frandom();
+		p.gravity = frandom()*-400.0f;
+		p.friction = 0.7f;
+		p.color = vec4(0xb5/255.0f, 0x50/255.0f, 0xcb/255.0f, 1.0f);
+		gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+		
+	}
+}
+
+void EFFECTS::playerdeath(vec2 pos, int cid)
+{
+	vec3 blood_color(1.0f,1.0f,1.0f);
+
+	if(cid >= 0)	
+	{
+		const SKINS::SKIN *s = gameclient.skins->get(gameclient.clients[cid].skin_id);
+		if(s)
+			blood_color = s->blood_color;
+	}
+	
+	for(int i = 0; i < 64; i++)
+	{
+		PARTICLE p;
+		p.set_default();
+		p.spr = SPRITE_PART_SPLAT01 + (rand()%3);
+		p.pos = pos;
+		p.vel = random_dir() * ((frandom()+0.1f)*900.0f);
+		p.life_span = 0.3f + frandom()*0.3f;
+		p.start_size = 24.0f + frandom()*16;
+		p.end_size = 0;
+		p.rot = frandom()*pi*2;
+		p.rotspeed = (frandom()-0.5f) * pi;
+		p.gravity = 800.0f;
+		p.friction = 0.8f;
+		vec3 c = blood_color * (0.75f + frandom()*0.25f);
+		p.color = vec4(c.r, c.g, c.b, 0.75f);
+		gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+	}
+}
+
+
+void EFFECTS::explosion(vec2 pos)
+{
+	// add to flow
+	for(int y = -8; y <= 8; y++)
+		for(int x = -8; x <= 8; x++)
+		{
+			if(x == 0 && y == 0)
+				continue;
+			
+			float a = 1 - (length(vec2(x,y)) / length(vec2(8,8)));
+			gameclient.flow->add(pos+vec2(x,y)*16, normalize(vec2(x,y))*5000.0f*a, 10.0f);
+		}
+		
+	// add the explosion
+	PARTICLE p;
+	p.set_default();
+	p.spr = SPRITE_PART_EXPL01;
+	p.pos = pos;
+	p.life_span = 0.4f;
+	p.start_size = 150.0f;
+	p.end_size = 0;
+	p.rot = frandom()*pi*2;
+	gameclient.particles->add(PARTICLES::GROUP_EXPLOSIONS, &p);
+	
+	// add the smoke
+	for(int i = 0; i < 24; i++)
+	{
+		PARTICLE p;
+		p.set_default();
+		p.spr = SPRITE_PART_SMOKE;
+		p.pos = pos;
+		p.vel = random_dir() * ((1.0f + frandom()*0.2f) * 1000.0f);
+		p.life_span = 0.5f + frandom()*0.4f;
+		p.start_size = 32.0f + frandom()*8;
+		p.end_size = 0;
+		p.gravity = frandom()*-800.0f;
+		p.friction = 0.4f;
+		p.color = mix(vec4(0.75f,0.75f,0.75f,1.0f), vec4(0.5f,0.5f,0.5f,1.0f), frandom());
+		gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+	}
+}
+
+void EFFECTS::on_render()
+{
+	static int64 last_update_100hz = 0;
+	static int64 last_update_50hz = 0;
+
+	if(time_get()-last_update_100hz > time_freq()/100)
+	{
+		add_100hz = true;
+		last_update_100hz = time_get();
+	}
+	else
+		add_100hz = false;
+
+	if(time_get()-last_update_50hz > time_freq()/100)
+	{
+		add_50hz = true;
+		last_update_50hz = time_get();
+	}
+	else
+		add_50hz = false;
+		
+	if(add_50hz)
+		gameclient.flow->update();
+}
diff --git a/src/game/client/components/effects.hpp b/src/game/client/components/effects.hpp
new file mode 100644
index 00000000..13af8947
--- /dev/null
+++ b/src/game/client/components/effects.hpp
@@ -0,0 +1,23 @@
+#include <game/client/component.hpp>
+
+class EFFECTS : public COMPONENT
+{	
+	bool add_50hz;
+	bool add_100hz;
+public:
+	EFFECTS();
+
+	virtual void on_render();
+
+	void bullettrail(vec2 pos);
+	void smoketrail(vec2 pos, vec2 vel);
+	void skidtrail(vec2 pos, vec2 vel);
+	void explosion(vec2 pos);
+	void air_jump(vec2 pos);
+	void damage_indicator(vec2 pos, vec2 dir);
+	void playerspawn(vec2 pos);
+	void playerdeath(vec2 pos, int cid);
+	void powerupshine(vec2 pos, vec2 size);
+
+	void update();
+};
diff --git a/src/game/client/components/emoticon.cpp b/src/game/client/components/emoticon.cpp
new file mode 100644
index 00000000..f359530a
--- /dev/null
+++ b/src/game/client/components/emoticon.cpp
@@ -0,0 +1,121 @@
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/gamecore.hpp> // get_angle
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_render.hpp>
+#include "emoticon.hpp"
+
+EMOTICON::EMOTICON()
+{
+	on_reset();
+}
+
+void EMOTICON::on_reset()
+{
+	selector_active = 0;
+	selected_emote = -1;
+}
+
+void EMOTICON::on_message(int msgtype, void *rawmsg)
+{
+}
+
+bool EMOTICON::on_input(INPUT_EVENT e)
+{
+	return false;
+}
+
+
+void EMOTICON::draw_circle(float x, float y, float r, int segments)
+{
+	float f_segments = (float)segments;
+	for(int i = 0; i < segments; i+=2)
+	{
+		float a1 = i/f_segments * 2*pi;
+		float a2 = (i+1)/f_segments * 2*pi;
+		float a3 = (i+2)/f_segments * 2*pi;
+		float ca1 = cosf(a1);
+		float ca2 = cosf(a2);
+		float ca3 = cosf(a3);
+		float sa1 = sinf(a1);
+		float sa2 = sinf(a2);
+		float sa3 = sinf(a3);
+
+		gfx_quads_draw_freeform(
+			x, y,
+			x+ca1*r, y+sa1*r,
+			x+ca3*r, y+sa3*r,
+			x+ca2*r, y+sa2*r);
+	}
+}
+
+	
+void EMOTICON::on_render()
+{
+	int x, y;
+	inp_mouse_relative(&x, &y);
+
+	selector_mouse.x += x;
+	selector_mouse.y += y;
+
+	if (length(selector_mouse) > 140)
+		selector_mouse = normalize(selector_mouse) * 140;
+
+	float selected_angle = get_angle(selector_mouse) + 2*pi/24;
+	if (selected_angle < 0)
+		selected_angle += 2*pi;
+
+	if (length(selector_mouse) > 100)
+		selected_emote = (int)(selected_angle / (2*pi) * 12.0f);
+
+    RECT screen = *ui_screen();
+
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+
+	gfx_blend_normal();
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.3f);
+	draw_circle(screen.w/2, screen.h/2, 160, 64);
+	gfx_quads_end();
+
+	gfx_texture_set(data->images[IMAGE_EMOTICONS].id);
+	gfx_quads_begin();
+
+	for (int i = 0; i < 12; i++)
+	{
+		float angle = 2*pi*i/12.0;
+		if (angle > pi)
+			angle -= 2*pi;
+
+		bool selected = selected_emote == i;
+
+		float size = selected ? 96 : 64;
+
+		float nudge_x = 120 * cos(angle);
+		float nudge_y = 120 * sin(angle);
+		select_sprite(SPRITE_OOP + i);
+		gfx_quads_draw(screen.w/2 + nudge_x, screen.h/2 + nudge_y, size, size);
+	}
+
+	gfx_quads_end();
+
+    gfx_texture_set(data->images[IMAGE_CURSOR].id);
+    gfx_quads_begin();
+    gfx_setcolor(1,1,1,1);
+    gfx_quads_drawTL(selector_mouse.x+screen.w/2,selector_mouse.y+screen.h/2,24,24);
+    gfx_quads_end();
+}
+
+
+
+void EMOTICON::emote(int emoticon)
+{
+	NETMSG_CL_EMOTICON msg;
+	msg.emoticon = emoticon;
+	msg.pack(MSGFLAG_VITAL);
+	client_send_msg();
+}
diff --git a/src/game/client/components/emoticon.hpp b/src/game/client/components/emoticon.hpp
new file mode 100644
index 00000000..17e977ab
--- /dev/null
+++ b/src/game/client/components/emoticon.hpp
@@ -0,0 +1,22 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+class EMOTICON : public COMPONENT
+{
+	void draw_circle(float x, float y, float r, int segments);
+	
+	vec2 selector_mouse;
+	int selector_active;
+	int selected_emote;
+	
+public:
+	EMOTICON();
+	
+	virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);
+	virtual bool on_input(INPUT_EVENT e);
+
+	void emote(int emoticon);
+};
+
diff --git a/src/game/client/components/flow.cpp b/src/game/client/components/flow.cpp
new file mode 100644
index 00000000..b2f983e6
--- /dev/null
+++ b/src/game/client/components/flow.cpp
@@ -0,0 +1,84 @@
+#include <game/mapitems.hpp>
+#include <game/layers.hpp>
+#include "flow.hpp"
+
+FLOW::FLOW()
+{
+	cells = 0;
+	height = 0;
+	width = 0;
+	spacing = 16;
+}
+	
+void FLOW::dbg_render()
+{
+	if(!cells)
+		return;
+
+	gfx_texture_set(-1);
+	gfx_lines_begin();
+	for(int y = 0; y < height; y++)
+		for(int x = 0; x < width; x++)
+		{
+			vec2 pos(x*spacing, y*spacing);
+			vec2 vel = cells[y*width+x].vel * 0.01f;
+			gfx_lines_draw(pos.x, pos.y, pos.x+vel.x, pos.y+vel.y);
+		}
+		
+	gfx_lines_end();
+}
+
+void FLOW::init()
+{
+	if(cells)
+	{
+		mem_free(cells);
+		cells = 0;
+	}
+	
+	MAPITEM_LAYER_TILEMAP *tilemap = layers_game_layer();
+	width = tilemap->width*32/spacing;
+	height = tilemap->height*32/spacing;
+
+	// allocate and clear	
+	cells = (CELL *)mem_alloc(sizeof(CELL)*width*height, 1);
+	for(int y = 0; y < height; y++)
+		for(int x = 0; x < width; x++)
+			cells[y*width+x].vel = vec2(0.0f, 0.0f);
+}
+
+void FLOW::update()
+{
+	if(!cells)
+		return;
+		
+	for(int y = 0; y < height; y++)
+		for(int x = 0; x < width; x++)
+			cells[y*width+x].vel *= 0.85f;
+}
+
+vec2 FLOW::get(vec2 pos)
+{
+	if(!cells)
+		return vec2(0,0);
+	
+	int x = (int)(pos.x / spacing);
+	int y = (int)(pos.y / spacing);
+	if(x < 0 || y < 0 || x >= width || y >= height)
+		return vec2(0,0);
+	
+	return cells[y*width+x].vel;	
+}
+
+void FLOW::add(vec2 pos, vec2 vel, float size)
+{
+	if(!cells)
+		return;
+		
+	int x = (int)(pos.x / spacing);
+	int y = (int)(pos.y / spacing);
+	if(x < 0 || y < 0 || x >= width || y >= height)
+		return;
+	
+	cells[y*width+x].vel += vel;
+}
diff --git a/src/game/client/components/flow.hpp b/src/game/client/components/flow.hpp
new file mode 100644
index 00000000..351b1f69
--- /dev/null
+++ b/src/game/client/components/flow.hpp
@@ -0,0 +1,25 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+class FLOW : public COMPONENT
+{
+	struct CELL
+	{
+		vec2 vel;
+	};
+
+	CELL *cells;
+	int height;
+	int width;
+	int spacing;
+	
+	void dbg_render();
+	void init();
+public:
+	FLOW();
+	
+	vec2 get(vec2 pos);
+	void add(vec2 pos, vec2 vel, float size);
+	void update();
+};
+
diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp
new file mode 100644
index 00000000..0e8d371c
--- /dev/null
+++ b/src/game/client/components/hud.cpp
@@ -0,0 +1,273 @@
+#include <memory.h> // memcmp
+
+extern "C" {
+	#include <engine/e_config.h>
+}
+
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/layers.hpp>
+
+#include <game/client/gameclient.hpp>
+#include <game/client/animstate.hpp>
+#include <game/client/gc_client.hpp>
+#include <game/client/gc_render.hpp>
+
+#include "controls.hpp"
+#include "camera.hpp"
+#include "hud.hpp"
+
+HUD::HUD()
+{
+	
+}
+	
+void HUD::on_reset()
+{
+}
+
+void HUD::render_goals()
+{
+	// TODO: split this up into these:
+	// render_gametimer
+	// render_suddendeath
+	// render_scorehud
+	// render_warmuptimer
+	
+	int gametype = gameclient.snap.gameobj->gametype;
+	int gameflags = gameclient.snap.gameobj->flags;
+	
+	float whole = 300*gfx_screenaspect();
+	float half = whole/2.0f;
+
+
+	gfx_mapscreen(0,0,300*gfx_screenaspect(),300);
+	if(!gameclient.snap.gameobj->sudden_death)
+	{
+		char buf[32];
+		int time = 0;
+		if(gameclient.snap.gameobj->time_limit)
+		{
+			time = gameclient.snap.gameobj->time_limit*60 - ((client_tick()-gameclient.snap.gameobj->round_start_tick)/client_tickspeed());
+
+			if(gameclient.snap.gameobj->game_over)
+				time  = 0;
+		}
+		else
+			time = (client_tick()-gameclient.snap.gameobj->round_start_tick)/client_tickspeed();
+
+		str_format(buf, sizeof(buf), "%d:%02d", time /60, time %60);
+		float w = gfx_text_width(0, 16, buf, -1);
+		gfx_text(0, half-w/2, 2, 16, buf, -1);
+	}
+
+	if(gameclient.snap.gameobj->sudden_death)
+	{
+		const char *text = "Sudden Death";
+		float w = gfx_text_width(0, 16, text, -1);
+		gfx_text(0, half-w/2, 2, 16, text, -1);
+	}
+
+	// render small score hud
+	if(!(gameclient.snap.gameobj && gameclient.snap.gameobj->game_over) && (gameflags&GAMEFLAG_TEAMS))
+	{
+		for(int t = 0; t < 2; t++)
+		{
+			gfx_blend_normal();
+			gfx_texture_set(-1);
+			gfx_quads_begin();
+			if(t == 0)
+				gfx_setcolor(1,0,0,0.25f);
+			else
+				gfx_setcolor(0,0,1,0.25f);
+			draw_round_rect(whole-40, 300-40-15+t*20, 50, 18, 5.0f);
+			gfx_quads_end();
+
+			char buf[32];
+			str_format(buf, sizeof(buf), "%d", t?gameclient.snap.gameobj->teamscore_blue:gameclient.snap.gameobj->teamscore_red);
+			float w = gfx_text_width(0, 14, buf, -1);
+			
+			if(gametype == GAMETYPE_CTF)
+			{
+				gfx_text(0, whole-20-w/2+5, 300-40-15+t*20, 14, buf, -1);
+				if(gameclient.snap.flags[t])
+				{
+					if(gameclient.snap.flags[t]->carried_by == -2 || (gameclient.snap.flags[t]->carried_by == -1 && ((client_tick()/10)&1)))
+					{
+						gfx_blend_normal();
+						gfx_texture_set(data->images[IMAGE_GAME].id);
+						gfx_quads_begin();
+
+						if(t == 0) select_sprite(SPRITE_FLAG_RED);
+						else select_sprite(SPRITE_FLAG_BLUE);
+						
+						float size = 16;					
+						gfx_quads_drawTL(whole-40+5, 300-40-15+t*20+1, size/2, size);
+						gfx_quads_end();
+					}
+					else if(gameclient.snap.flags[t]->carried_by >= 0)
+					{
+						int id = gameclient.snap.flags[t]->carried_by%MAX_CLIENTS;
+						const char *name = gameclient.clients[id].name;
+						float w = gfx_text_width(0, 10, name, -1);
+						gfx_text(0, whole-40-5-w, 300-40-15+t*20+2, 10, name, -1);
+						TEE_RENDER_INFO info = gameclient.clients[id].render_info;
+						info.size = 18.0f;
+						
+						render_tee(ANIMSTATE::get_idle(), &info, EMOTE_NORMAL, vec2(1,0),
+							vec2(whole-40+10, 300-40-15+9+t*20+1));
+					}
+				}
+			}
+			else
+				gfx_text(0, whole-20-w/2, 300-40-15+t*20, 14, buf, -1);
+		}
+	}
+
+	// render warmup timer
+	if(gameclient.snap.gameobj->warmup)
+	{
+		char buf[256];
+		float w = gfx_text_width(0, 24, "Warmup", -1);
+		gfx_text(0, 150*gfx_screenaspect()+-w/2, 50, 24, "Warmup", -1);
+
+		int seconds = gameclient.snap.gameobj->warmup/SERVER_TICK_SPEED;
+		if(seconds < 5)
+			str_format(buf, sizeof(buf), "%d.%d", seconds, (gameclient.snap.gameobj->warmup*10/SERVER_TICK_SPEED)%10);
+		else
+			str_format(buf, sizeof(buf), "%d", seconds);
+		w = gfx_text_width(0, 24, buf, -1);
+		gfx_text(0, 150*gfx_screenaspect()+-w/2, 75, 24, buf, -1);
+	}	
+}
+
+static void mapscreen_to_group(float center_x, float center_y, MAPITEM_GROUP *group)
+{
+	float points[4];
+	mapscreen_to_world(center_x, center_y, group->parallax_x/100.0f, group->parallax_y/100.0f,
+		group->offset_x, group->offset_y, gfx_screenaspect(), 1.0f, points);
+	gfx_mapscreen(points[0], points[1], points[2], points[3]);
+}
+
+void HUD::render_fps()
+{
+	if(config.cl_showfps)
+	{
+		char buf[512];
+		str_format(buf, sizeof(buf), "%d", (int)(1.0f/client_frametime()));
+		gfx_text(0, width-10-gfx_text_width(0,12,buf,-1), 10, 12, buf, -1);
+	}
+}
+
+void HUD::render_connectionwarning()
+{
+	if(client_connection_problems())
+	{
+		const char *text = "Connection Problems...";
+		float w = gfx_text_width(0, 24, text, -1);
+		gfx_text(0, 150*gfx_screenaspect()-w/2, 50, 24, text, -1);
+	}
+}
+
+void HUD::render_tunewarning()
+{
+	TUNING_PARAMS standard_tuning;
+
+	// render warning about non standard tuning
+	bool flash = time_get()/(time_freq()/2)%2 == 0;
+	if(config.cl_warning_tuning && memcmp(&standard_tuning, &tuning, sizeof(TUNING_PARAMS)) != 0)
+	{
+		const char *text = "Warning! Server is running non-standard tuning.";
+		if(flash)
+			gfx_text_color(1,0.4f,0.4f,1.0f);
+		else
+			gfx_text_color(0.75f,0.2f,0.2f,1.0f);
+		gfx_text(0x0, 5, 40, 6, text, -1);
+		gfx_text_color(1,1,1,1);
+	}
+}		
+
+void HUD::render_cursor()
+{
+	mapscreen_to_group(gameclient.camera->center.x, gameclient.camera->center.y, layers_game_group());
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+
+	// render cursor
+	// TODO: repair me
+	//if (!menu_active)
+	{
+		dbg_msg("", "%f %f", gameclient.controls->target_pos.x, gameclient.controls->target_pos.y);
+		select_sprite(data->weapons.id[gameclient.snap.local_character->weapon%NUM_WEAPONS].sprite_cursor);
+		float cursorsize = 64;
+		draw_sprite(gameclient.controls->target_pos.x, gameclient.controls->target_pos.y, cursorsize);
+	}
+	gfx_quads_end();
+}
+
+void HUD::render_healthandammo()
+{
+	//mapscreen_to_group(gacenter_x, center_y, layers_game_group());
+
+	float x = 5;
+	float y = 5;
+
+	// render ammo count
+	// render gui stuff
+	gfx_quads_begin();
+	gfx_mapscreen(0,0,width,300);
+	
+	// if weaponstage is active, put a "glow" around the stage ammo
+	select_sprite(data->weapons.id[gameclient.snap.local_character->weapon%NUM_WEAPONS].sprite_proj);
+	for (int i = 0; i < min(gameclient.snap.local_character->ammocount, 10); i++)
+		gfx_quads_drawTL(x+i*12,y+24,10,10);
+
+	gfx_quads_end();
+
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+	int h = 0;
+
+	// render health
+	select_sprite(SPRITE_HEALTH_FULL);
+	for(; h < gameclient.snap.local_character->health; h++)
+		gfx_quads_drawTL(x+h*12,y,10,10);
+
+	select_sprite(SPRITE_HEALTH_EMPTY);
+	for(; h < 10; h++)
+		gfx_quads_drawTL(x+h*12,y,10,10);
+
+	// render armor meter
+	h = 0;
+	select_sprite(SPRITE_ARMOR_FULL);
+	for(; h < gameclient.snap.local_character->armor; h++)
+		gfx_quads_drawTL(x+h*12,y+12,10,10);
+
+	select_sprite(SPRITE_ARMOR_EMPTY);
+	for(; h < 10; h++)
+		gfx_quads_drawTL(x+h*12,y+12,10,10);
+	gfx_quads_end();
+}
+
+void HUD::on_render()
+{
+	if(!gameclient.snap.gameobj)
+		return;
+		
+	width = 300*gfx_screenaspect();
+
+	bool spectate = false;
+	if(gameclient.snap.local_info && gameclient.snap.local_info->team == -1)
+		spectate = true;
+	
+	if(gameclient.snap.local_character && !spectate && !(gameclient.snap.gameobj && gameclient.snap.gameobj->game_over))
+		render_healthandammo();
+
+	render_goals();
+	render_fps();
+	render_connectionwarning();
+	render_tunewarning();
+	render_cursor();
+}
diff --git a/src/game/client/components/hud.hpp b/src/game/client/components/hud.hpp
new file mode 100644
index 00000000..f8eb5e8f
--- /dev/null
+++ b/src/game/client/components/hud.hpp
@@ -0,0 +1,21 @@
+#include <game/client/component.hpp>
+
+class HUD : public COMPONENT
+{	
+	float width;
+	
+	void render_cursor();
+	
+	void render_fps();
+	void render_connectionwarning();
+	void render_tunewarning();
+	void render_healthandammo();
+	void render_goals();
+	
+public:
+	HUD();
+	
+	virtual void on_reset();
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/items.cpp b/src/game/client/components/items.cpp
new file mode 100644
index 00000000..15cc7524
--- /dev/null
+++ b/src/game/client/components/items.cpp
@@ -0,0 +1,254 @@
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/gamecore.hpp> // get_angle
+#include <game/client/gameclient.hpp>
+#include <game/client/gc_client.hpp>
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_render.hpp>
+
+#include <game/client/components/flow.hpp>
+#include <game/client/components/effects.hpp>
+
+#include "items.hpp"
+
+void ITEMS::render_projectile(const NETOBJ_PROJECTILE *current, int itemid)
+{
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+
+	// get positions
+	float curvature = 0;
+	float speed = 0;
+	if(current->type == WEAPON_GRENADE)
+	{
+		curvature = tuning.grenade_curvature;
+		speed = tuning.grenade_speed;
+	}
+	else if(current->type == WEAPON_SHOTGUN)
+	{
+		curvature = tuning.shotgun_curvature;
+		speed = tuning.shotgun_speed;
+	}
+	else if(current->type == WEAPON_GUN)
+	{
+		curvature = tuning.gun_curvature;
+		speed = tuning.gun_speed;
+	}
+
+	float ct = (client_tick()-current->start_tick)/(float)SERVER_TICK_SPEED + client_ticktime()*1/(float)SERVER_TICK_SPEED;
+	vec2 startpos(current->x, current->y);
+	vec2 startvel(current->vx/100.0f, current->vy/100.0f);
+	vec2 pos = calc_pos(startpos, startvel, curvature, speed, ct);
+	vec2 prevpos = calc_pos(startpos, startvel, curvature, speed, ct-0.001f);
+
+	select_sprite(data->weapons.id[clamp(current->type, 0, NUM_WEAPONS-1)].sprite_proj);
+	vec2 vel = pos-prevpos;
+	//vec2 pos = mix(vec2(prev->x, prev->y), vec2(current->x, current->y), client_intratick());
+	
+
+	// add particle for this projectile
+	if(current->type == WEAPON_GRENADE)
+	{
+		gameclient.effects->smoketrail(pos, vel*-1);
+		gameclient.flow->add(pos, vel*1000*client_frametime(), 10.0f);
+		gfx_quads_setrotation(client_localtime()*pi*2*2 + itemid);
+	}
+	else
+	{
+		gameclient.effects->bullettrail(pos);
+		gameclient.flow->add(pos, vel*1000*client_frametime(), 10.0f);
+
+		if(length(vel) > 0.00001f)
+			gfx_quads_setrotation(get_angle(vel));
+		else
+			gfx_quads_setrotation(0);
+
+	}
+
+	gfx_quads_draw(pos.x, pos.y, 32, 32);
+	gfx_quads_setrotation(0);
+	gfx_quads_end();
+}
+
+void ITEMS::render_pickup(const NETOBJ_PICKUP *prev, const NETOBJ_PICKUP *current)
+{
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+	vec2 pos = mix(vec2(prev->x, prev->y), vec2(current->x, current->y), client_intratick());
+	float angle = 0.0f;
+	float size = 64.0f;
+	if (current->type == POWERUP_WEAPON)
+	{
+		angle = 0; //-pi/6;//-0.25f * pi * 2.0f;
+		select_sprite(data->weapons.id[clamp(current->subtype, 0, NUM_WEAPONS-1)].sprite_body);
+		size = data->weapons.id[clamp(current->subtype, 0, NUM_WEAPONS-1)].visual_size;
+	}
+	else
+	{
+		const int c[] = {
+			SPRITE_PICKUP_HEALTH,
+			SPRITE_PICKUP_ARMOR,
+			SPRITE_PICKUP_WEAPON,
+			SPRITE_PICKUP_NINJA
+			};
+		select_sprite(c[current->type]);
+
+		if(c[current->type] == SPRITE_PICKUP_NINJA)
+		{
+			gameclient.effects->powerupshine(pos, vec2(96,18));
+			size *= 2.0f;
+			pos.x += 10.0f;
+		}
+	}
+
+	gfx_quads_setrotation(angle);
+
+	float offset = pos.y/32.0f + pos.x/32.0f;
+	pos.x += cosf(client_localtime()*2.0f+offset)*2.5f;
+	pos.y += sinf(client_localtime()*2.0f+offset)*2.5f;
+	draw_sprite(pos.x, pos.y, size);
+	gfx_quads_end();
+}
+
+void ITEMS::render_flag(const NETOBJ_FLAG *prev, const NETOBJ_FLAG *current)
+{
+	float angle = 0.0f;
+	float size = 42.0f;
+
+	gfx_blend_normal();
+	gfx_texture_set(data->images[IMAGE_GAME].id);
+	gfx_quads_begin();
+
+	if(current->team == 0) // red team
+		select_sprite(SPRITE_FLAG_RED);
+	else
+		select_sprite(SPRITE_FLAG_BLUE);
+
+	gfx_quads_setrotation(angle);
+
+	vec2 pos = mix(vec2(prev->x, prev->y), vec2(current->x, current->y), client_intratick());
+	
+	// make sure that the flag isn't interpolated between capture and return
+	if(prev->carried_by != current->carried_by)
+		pos = vec2(current->x, current->y);
+
+	// make sure to use predicted position if we are the carrier
+	if(gameclient.snap.local_info && current->carried_by == gameclient.snap.local_info->cid)
+		pos = gameclient.local_character_pos;
+
+	gfx_quads_draw(pos.x, pos.y-size*0.75f, size, size*2);
+	gfx_quads_end();
+}
+
+
+void ITEMS::render_laser(const struct NETOBJ_LASER *current)
+{
+	vec2 pos = vec2(current->x, current->y);
+	vec2 from = vec2(current->from_x, current->from_y);
+	vec2 dir = normalize(pos-from);
+
+	float ticks = client_tick() + client_intratick() - current->start_tick;
+	float ms = (ticks/50.0f) * 1000.0f;
+	float a =  ms / tuning.laser_bounce_delay;
+	a = clamp(a, 0.0f, 1.0f);
+	float ia = 1-a;
+	
+	vec2 out, border;
+	
+	gfx_blend_normal();
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	
+	//vec4 inner_color(0.15f,0.35f,0.75f,1.0f);
+	//vec4 outer_color(0.65f,0.85f,1.0f,1.0f);
+
+	// do outline
+	vec4 outer_color(0.075f,0.075f,0.25f,1.0f);
+	gfx_setcolor(outer_color.r,outer_color.g,outer_color.b,1.0f);
+	out = vec2(dir.y, -dir.x) * (7.0f*ia);
+
+	gfx_quads_draw_freeform(
+			from.x-out.x, from.y-out.y,
+			from.x+out.x, from.y+out.y,
+			pos.x-out.x, pos.y-out.y,
+			pos.x+out.x, pos.y+out.y
+		);
+
+	// do inner	
+	vec4 inner_color(0.5f,0.5f,1.0f,1.0f);
+	out = vec2(dir.y, -dir.x) * (5.0f*ia);
+	gfx_setcolor(inner_color.r, inner_color.g, inner_color.b, 1.0f); // center
+	
+	gfx_quads_draw_freeform(
+			from.x-out.x, from.y-out.y,
+			from.x+out.x, from.y+out.y,
+			pos.x-out.x, pos.y-out.y,
+			pos.x+out.x, pos.y+out.y
+		);
+		
+	gfx_quads_end();
+	
+	// render head
+	{
+		gfx_blend_normal();
+		gfx_texture_set(data->images[IMAGE_PARTICLES].id);
+		gfx_quads_begin();
+
+		int sprites[] = {SPRITE_PART_SPLAT01, SPRITE_PART_SPLAT02, SPRITE_PART_SPLAT03};
+		select_sprite(sprites[client_tick()%3]);
+		gfx_quads_setrotation(client_tick());
+		gfx_setcolor(outer_color.r,outer_color.g,outer_color.b,1.0f);
+		gfx_quads_draw(pos.x, pos.y, 24,24);
+		gfx_setcolor(inner_color.r, inner_color.g, inner_color.b, 1.0f);
+		gfx_quads_draw(pos.x, pos.y, 20,20);
+		gfx_quads_end();
+	}
+	
+	gfx_blend_normal();	
+}
+
+void ITEMS::on_render()
+{
+	int num = snap_num_items(SNAP_CURRENT);
+	for(int i = 0; i < num; i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+		if(item.type == NETOBJTYPE_PROJECTILE)
+		{
+			render_projectile((const NETOBJ_PROJECTILE *)data, item.id);
+		}
+		else if(item.type == NETOBJTYPE_PICKUP)
+		{
+			const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+			if(prev)
+				render_pickup((const NETOBJ_PICKUP *)prev, (const NETOBJ_PICKUP *)data);
+		}
+		else if(item.type == NETOBJTYPE_LASER)
+		{
+			render_laser((const NETOBJ_LASER *)data);
+		}
+		else if(item.type == NETOBJTYPE_FLAG)
+		{
+			const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+			if (prev)
+				render_flag((const NETOBJ_FLAG *)prev, (const NETOBJ_FLAG *)data);
+		}
+	}
+
+	// render extra projectiles
+	/*
+	for(int i = 0; i < extraproj_num; i++)
+	{
+		if(extraproj_projectiles[i].start_tick < client_tick())
+		{
+			extraproj_projectiles[i] = extraproj_projectiles[extraproj_num-1];
+			extraproj_num--;
+		}
+		else
+			render_projectile(&extraproj_projectiles[i], 0);
+	}*/
+}
diff --git a/src/game/client/components/items.hpp b/src/game/client/components/items.hpp
new file mode 100644
index 00000000..2f33c8c4
--- /dev/null
+++ b/src/game/client/components/items.hpp
@@ -0,0 +1,13 @@
+#include <game/client/component.hpp>
+
+class ITEMS : public COMPONENT
+{	
+	void render_projectile(const NETOBJ_PROJECTILE *current, int itemid);
+	void render_pickup(const NETOBJ_PICKUP *prev, const NETOBJ_PICKUP *current);
+	void render_flag(const NETOBJ_FLAG *prev, const NETOBJ_FLAG *current);
+	void render_laser(const struct NETOBJ_LASER *current);
+	
+public:
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/killmessages.cpp b/src/game/client/components/killmessages.cpp
new file mode 100644
index 00000000..41ba2e48
--- /dev/null
+++ b/src/game/client/components/killmessages.cpp
@@ -0,0 +1,130 @@
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/gameclient.hpp>
+#include <game/client/animstate.hpp>
+#include <game/client/gc_client.hpp>
+
+#include "killmessages.hpp"
+
+void KILLMESSAGES::on_reset()
+{
+	killmsg_current = 0;
+	for(int i = 0; i < killmsg_max; i++)
+		killmsgs[i].tick = -100000;
+}
+
+void KILLMESSAGES::on_message(int msgtype, void *rawmsg)
+{
+	if(msgtype == NETMSGTYPE_SV_KILLMSG)
+	{
+		NETMSG_SV_KILLMSG *msg = (NETMSG_SV_KILLMSG *)rawmsg;
+		
+		// unpack messages
+		KILLMSG kill;
+		kill.killer = msg->killer;
+		kill.victim = msg->victim;
+		kill.weapon = msg->weapon;
+		kill.mode_special = msg->mode_special;
+		kill.tick = client_tick();
+
+		// add the message
+		killmsg_current = (killmsg_current+1)%killmsg_max;
+		killmsgs[killmsg_current] = kill;		
+	}
+}
+
+void KILLMESSAGES::on_render()
+{
+	float width = 400*3.0f*gfx_screenaspect();
+	float height = 400*3.0f;
+
+	gfx_mapscreen(0, 0, width*1.5f, height*1.5f);
+	float startx = width*1.5f-10.0f;
+	float y = 20.0f;
+
+	for(int i = 0; i < killmsg_max; i++)
+	{
+
+		int r = (killmsg_current+i+1)%killmsg_max;
+		if(client_tick() > killmsgs[r].tick+50*10)
+			continue;
+
+		float font_size = 36.0f;
+		float killername_w = gfx_text_width(0, font_size, gameclient.clients[killmsgs[r].killer].name, -1);
+		float victimname_w = gfx_text_width(0, font_size, gameclient.clients[killmsgs[r].victim].name, -1);
+
+		float x = startx;
+
+		// render victim name
+		x -= victimname_w;
+		gfx_text(0, x, y, font_size, gameclient.clients[killmsgs[r].victim].name, -1);
+
+		// render victim tee
+		x -= 24.0f;
+		
+		if(gameclient.snap.gameobj && gameclient.snap.gameobj->gametype == GAMETYPE_CTF)
+		{
+			if(killmsgs[r].mode_special&1)
+			{
+				gfx_blend_normal();
+				gfx_texture_set(data->images[IMAGE_GAME].id);
+				gfx_quads_begin();
+
+				if(gameclient.clients[killmsgs[r].victim].team == 0) select_sprite(SPRITE_FLAG_BLUE);
+				else select_sprite(SPRITE_FLAG_RED);
+				
+				float size = 56.0f;
+				gfx_quads_drawTL(x, y-16, size/2, size);
+				gfx_quads_end();					
+			}
+		}
+		
+		render_tee(ANIMSTATE::get_idle(), &gameclient.clients[killmsgs[r].victim].render_info, EMOTE_PAIN, vec2(-1,0), vec2(x, y+28));
+		x -= 32.0f;
+		
+		// render weapon
+		x -= 44.0f;
+		if (killmsgs[r].weapon >= 0)
+		{
+			gfx_texture_set(data->images[IMAGE_GAME].id);
+			gfx_quads_begin();
+			select_sprite(data->weapons.id[killmsgs[r].weapon].sprite_body);
+			draw_sprite(x, y+28, 96);
+			gfx_quads_end();
+		}
+		x -= 52.0f;
+
+		if(killmsgs[r].victim != killmsgs[r].killer)
+		{
+			if(gameclient.snap.gameobj && gameclient.snap.gameobj->gametype == GAMETYPE_CTF)
+			{
+				if(killmsgs[r].mode_special&2)
+				{
+					gfx_blend_normal();
+					gfx_texture_set(data->images[IMAGE_GAME].id);
+					gfx_quads_begin();
+
+					if(gameclient.clients[killmsgs[r].killer].team == 0) select_sprite(SPRITE_FLAG_BLUE, SPRITE_FLAG_FLIP_X);
+					else select_sprite(SPRITE_FLAG_RED, SPRITE_FLAG_FLIP_X);
+					
+					float size = 56.0f;
+					gfx_quads_drawTL(x-56, y-16, size/2, size);
+					gfx_quads_end();				
+				}
+			}				
+			
+			// render killer tee
+			x -= 24.0f;
+			render_tee(ANIMSTATE::get_idle(), &gameclient.clients[killmsgs[r].killer].render_info, EMOTE_ANGRY, vec2(1,0), vec2(x, y+28));
+			x -= 32.0f;
+
+			// render killer name
+			x -= killername_w;
+			gfx_text(0, x, y, font_size, gameclient.clients[killmsgs[r].killer].name, -1);
+		}
+
+		y += 44;
+	}
+}
diff --git a/src/game/client/components/killmessages.hpp b/src/game/client/components/killmessages.hpp
new file mode 100644
index 00000000..f29e0bdf
--- /dev/null
+++ b/src/game/client/components/killmessages.hpp
@@ -0,0 +1,24 @@
+#include <game/client/component.hpp>
+
+class KILLMESSAGES : public COMPONENT
+{
+public:
+	// kill messages
+	struct KILLMSG
+	{
+		int weapon;
+		int victim;
+		int killer;
+		int mode_special; // for CTF, if the guy is carrying a flag for example
+		int tick;
+	};
+
+	static const int killmsg_max = 5;
+	KILLMSG killmsgs[killmsg_max];
+	int killmsg_current;
+
+	virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);
+};
+
diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp
new file mode 100644
index 00000000..8ed6264b
--- /dev/null
+++ b/src/game/client/components/maplayers.cpp
@@ -0,0 +1,159 @@
+
+extern "C" {
+	#include <engine/e_config.h>
+}
+
+#include <game/layers.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/component.hpp>
+#include <game/client/gc_render.hpp>
+#include <game/client/gc_map_image.hpp>
+
+#include <game/client/components/camera.hpp>
+
+#include "maplayers.hpp"
+
+MAPLAYERS::MAPLAYERS(int t)
+{
+	type = t;
+}
+
+
+static void mapscreen_to_group(float center_x, float center_y, MAPITEM_GROUP *group)
+{
+	float points[4];
+	mapscreen_to_world(center_x, center_y, group->parallax_x/100.0f, group->parallax_y/100.0f,
+		group->offset_x, group->offset_y, gfx_screenaspect(), 1.0f, points);
+	gfx_mapscreen(points[0], points[1], points[2], points[3]);
+}
+
+
+static void envelope_eval(float time_offset, int env, float *channels)
+{
+	channels[0] = 0;
+	channels[1] = 0;
+	channels[2] = 0;
+	channels[3] = 0;
+
+	ENVPOINT *points;
+
+	{
+		int start, num;
+		map_get_type(MAPITEMTYPE_ENVPOINTS, &start, &num);
+		if(num)
+			points = (ENVPOINT *)map_get_item(start, 0, 0);
+	}
+	
+	int start, num;
+	map_get_type(MAPITEMTYPE_ENVELOPE, &start, &num);
+	
+	if(env >= num)
+		return;
+	
+	MAPITEM_ENVELOPE *item = (MAPITEM_ENVELOPE *)map_get_item(start+env, 0, 0);
+	render_eval_envelope(points+item->start_point, item->num_points, 4, client_localtime()+time_offset, channels);
+}
+
+void MAPLAYERS::on_render()
+{
+	vec2 center = gameclient.camera->center;
+	//float center_x = gameclient.camera->center.x;
+	//float center_y = gameclient.camera->center.y;
+	
+	bool passed_gamelayer = false;
+	
+	for(int g = 0; g < layers_num_groups(); g++)
+	{
+		MAPITEM_GROUP *group = layers_get_group(g);
+		
+		if(group->version >= 2 && group->use_clipping)
+		{
+			// set clipping
+			float points[4];
+			mapscreen_to_group(center.x, center.y, layers_game_group());
+			gfx_getscreen(&points[0], &points[1], &points[2], &points[3]);
+			float x0 = (group->clip_x - points[0]) / (points[2]-points[0]);
+			float y0 = (group->clip_y - points[1]) / (points[3]-points[1]);
+			float x1 = ((group->clip_x+group->clip_w) - points[0]) / (points[2]-points[0]);
+			float y1 = ((group->clip_y+group->clip_h) - points[1]) / (points[3]-points[1]);
+			
+			gfx_clip_enable((int)(x0*gfx_screenwidth()), (int)(y0*gfx_screenheight()),
+				(int)((x1-x0)*gfx_screenwidth()), (int)((y1-y0)*gfx_screenheight()));
+		}		
+		
+		mapscreen_to_group(center.x, center.y, group);
+		
+		for(int l = 0; l < group->num_layers; l++)
+		{
+			MAPITEM_LAYER *layer = layers_get_layer(group->start_layer+l);
+			bool render = false;
+			bool is_game_layer = false;
+			
+			// skip rendering if detail layers if not wanted
+			if(layer->flags&LAYERFLAG_DETAIL && !config.gfx_high_detail)
+				continue;
+			
+			if(layer == (MAPITEM_LAYER*)layers_game_layer())
+			{
+				is_game_layer = true;
+				passed_gamelayer = 1;
+			}
+				
+			if(type == -1)
+				render = true;
+			else if(type == 0)
+			{
+				if(passed_gamelayer)
+					return;
+				render = true;
+			}
+			else
+			{
+				if(passed_gamelayer && !is_game_layer)
+					render = true;
+			}
+			
+			if(render && !is_game_layer)
+			{
+				//layershot_begin();
+				
+				if(layer->type == LAYERTYPE_TILES)
+				{
+					MAPITEM_LAYER_TILEMAP *tmap = (MAPITEM_LAYER_TILEMAP *)layer;
+					if(tmap->image == -1)
+						gfx_texture_set(-1);
+					else
+						gfx_texture_set(img_get(tmap->image));
+						
+					TILE *tiles = (TILE *)map_get_data(tmap->data);
+					gfx_blend_none();
+					render_tilemap(tiles, tmap->width, tmap->height, 32.0f, vec4(1,1,1,1), TILERENDERFLAG_EXTEND|LAYERRENDERFLAG_OPAQUE);
+					gfx_blend_normal();
+					render_tilemap(tiles, tmap->width, tmap->height, 32.0f, vec4(1,1,1,1), TILERENDERFLAG_EXTEND|LAYERRENDERFLAG_TRANSPARENT);
+				}
+				else if(layer->type == LAYERTYPE_QUADS)
+				{
+					MAPITEM_LAYER_QUADS *qlayer = (MAPITEM_LAYER_QUADS *)layer;
+					if(qlayer->image == -1)
+						gfx_texture_set(-1);
+					else
+						gfx_texture_set(img_get(qlayer->image));
+
+					QUAD *quads = (QUAD *)map_get_data_swapped(qlayer->data);
+					
+					gfx_blend_none();
+					render_quads(quads, qlayer->num_quads, envelope_eval, LAYERRENDERFLAG_OPAQUE);
+					gfx_blend_normal();
+					render_quads(quads, qlayer->num_quads, envelope_eval, LAYERRENDERFLAG_TRANSPARENT);
+				}
+				
+				//layershot_end();	
+			}
+		}
+		
+		gfx_clip_disable();
+	}
+	
+	gfx_clip_disable();
+}
+
diff --git a/src/game/client/components/maplayers.hpp b/src/game/client/components/maplayers.hpp
new file mode 100644
index 00000000..c2919f08
--- /dev/null
+++ b/src/game/client/components/maplayers.hpp
@@ -0,0 +1,16 @@
+#include <game/client/component.hpp>
+
+class MAPLAYERS : public COMPONENT
+{	
+	int type;
+public:
+	enum
+	{
+		TYPE_BACKGROUND=0,
+		TYPE_FOREGROUND,
+	};
+
+	MAPLAYERS(int type);
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
new file mode 100644
index 00000000..fc409821
--- /dev/null
+++ b/src/game/client/components/menus.cpp
@@ -0,0 +1,1051 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <base/system.h>
+#include <base/math.hpp>
+#include <base/vmath.hpp>
+
+#include "menus.hpp"
+#include "skins.hpp"
+
+extern "C" {
+	#include <engine/e_client_interface.h>
+	#include <engine/e_config.h>
+	#include <engine/client/ec_font.h>
+}
+
+#include <game/version.hpp>
+#include <game/generated/g_protocol.hpp>
+
+#include <game/generated/gc_data.hpp>
+#include <game/client/components/binds.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/animstate.hpp>
+#include <game/client/gc_render.hpp>
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_client.hpp>
+#include <mastersrv/mastersrv.h>
+
+vec4 MENUS::gui_color;
+vec4 MENUS::color_tabbar_inactive_outgame;
+vec4 MENUS::color_tabbar_active_outgame;
+vec4 MENUS::color_tabbar_inactive;
+vec4 MENUS::color_tabbar_active;
+vec4 MENUS::color_tabbar_inactive_ingame;
+vec4 MENUS::color_tabbar_active_ingame;
+
+
+MENUS::MENUS()
+{
+	popup = POPUP_NONE;
+	active_page = PAGE_INTERNET;
+	game_page = PAGE_GAME;
+	
+	need_restart = false;
+	menu_active = true;
+}
+
+vec4 MENUS::button_color_mul(const void *id)
+{
+	if(ui_active_item() == id)
+		return vec4(1,1,1,0.5f);
+	else if(ui_hot_item() == id)
+		return vec4(1,1,1,1.5f);
+	return vec4(1,1,1,1);
+}
+
+void MENUS::ui_draw_browse_icon(int what, const RECT *r)
+{
+	gfx_texture_set(data->images[IMAGE_BROWSEICONS].id);
+	gfx_quads_begin();
+	select_sprite(SPRITE_BROWSE_PROGRESS1); // default
+	if(what == -1)
+	{
+	}
+	else if(what <= 100)
+	{
+		if(what < 66)
+			select_sprite(SPRITE_BROWSE_PROGRESS2);
+		else
+			select_sprite(SPRITE_BROWSE_PROGRESS3);
+	}
+	else if(what&0x100)
+	{
+		select_sprite(SPRITE_BROWSE_LOCK);
+	}
+	gfx_quads_drawTL(r->x,r->y,r->w,r->h);
+	gfx_quads_end();
+}
+
+
+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);
+}
+
+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);
+}
+
+void MENUS::ui_draw_menu_tab_button(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	if(checked)
+		ui_draw_rect(r, color_tabbar_active, CORNER_T, 10.0f);
+	else
+		ui_draw_rect(r, color_tabbar_inactive, CORNER_T, 10.0f);
+	ui_do_label(r, text, 22.0f, 0);
+}
+
+
+void MENUS::ui_draw_settings_tab_button(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	if(checked)
+		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);
+}
+
+void MENUS::ui_draw_grid_header(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	if(checked)
+		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);
+}
+
+void MENUS::ui_draw_list_row(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	if(checked)
+	{
+		RECT sr = *r;
+		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);
+}
+
+void MENUS::ui_draw_checkbox_common(const void *id, const char *text, const char *boxtext, const RECT *r)
+{
+	RECT c = *r;
+	RECT t = *r;
+	c.w = c.h;
+	t.x += c.w;
+	t.w -= c.w;
+	ui_vsplit_l(&t, 5.0f, 0, &t);
+	
+	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);	
+}
+
+void MENUS::ui_draw_checkbox(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	ui_draw_checkbox_common(id, text, checked?"X":"", r);
+}
+
+
+void MENUS::ui_draw_checkbox_number(const void *id, const char *text, int checked, const RECT *r, const void *extra)
+{
+	char buf[16];
+	str_format(buf, sizeof(buf), "%d", checked);
+	ui_draw_checkbox_common(id, text, buf, r);
+}
+
+int MENUS::ui_do_edit_box(void *id, const RECT *rect, char *str, int str_size, float font_size, bool hidden)
+{
+    int inside = ui_mouse_inside(rect);
+	int r = 0;
+	static int at_index = 0;
+
+	if(ui_last_active_item() == id)
+	{
+		int len = strlen(str);
+
+		if (inside && ui_mouse_button(0))
+		{
+			int mx_rel = (int)(ui_mouse_x() - rect->x);
+
+			for (int i = 1; i <= len; i++)
+			{
+				if (gfx_text_width(0, font_size, str, i) + 10 > mx_rel)
+				{
+					at_index = i - 1;
+					break;
+				}
+
+				if (i == len)
+					at_index = len;
+			}
+		}
+
+		for(int i = 0; i < inp_num_events(); i++)
+		{
+			INPUT_EVENT e = inp_get_event(i);
+			char c = e.ch;
+			int k = e.key;
+
+			if (at_index > len)
+				at_index = len;
+			
+			if (!(c >= 0 && c < 32))
+			{
+				if (len < str_size - 1 && at_index < str_size - 1)
+				{
+					memmove(str + at_index + 1, str + at_index, len - at_index + 1);
+					str[at_index] = c;
+					at_index++;
+				}
+			}
+			
+			if(e.flags&INPFLAG_PRESS)
+			{
+				if (k == KEY_BACKSPACE && at_index > 0)
+				{
+					memmove(str + at_index - 1, str + at_index, len - at_index + 1);
+					at_index--;
+				}
+				else if (k == KEY_DEL && at_index < len)
+					memmove(str + at_index, str + at_index + 1, len - at_index);
+				else if (k == KEY_ENTER)
+					ui_clear_last_active_item();
+				else if (k == KEY_LEFT && at_index > 0)
+					at_index--;
+				else if (k == KEY_RIGHT && at_index < len)
+					at_index++;
+				else if (k == KEY_HOME)
+					at_index = 0;
+				else if (k == KEY_END)
+					at_index = len;
+			}
+		}
+		
+		r = 1;
+	}
+
+	bool just_got_active = false;
+	
+	if(ui_active_item() == id)
+	{
+		if(!ui_mouse_button(0))
+			ui_set_active_item(0);
+	}
+	else if(ui_hot_item() == id)
+	{
+		if(ui_mouse_button(0))
+		{
+			if (ui_last_active_item() != id)
+				just_got_active = true;
+			ui_set_active_item(id);
+		}
+	}
+	
+	if(inside)
+		ui_set_hot_item(id);
+
+	RECT textbox = *rect;
+	ui_draw_rect(&textbox, vec4(1,1,1,0.5f), CORNER_ALL, 5.0f);
+	ui_vmargin(&textbox, 5.0f, &textbox);
+	
+	const char *display_str = str;
+	char stars[128];
+	
+	if(hidden)
+	{
+		unsigned s = strlen(str);
+		if(s >= sizeof(stars))
+			s = sizeof(stars)-1;
+		memset(stars, '*', s);
+		stars[s] = 0;
+		display_str = stars;
+	}
+
+	ui_do_label(&textbox, display_str, font_size, -1);
+	
+	if (ui_last_active_item() == id && !just_got_active)
+	{
+		float w = gfx_text_width(0, font_size, display_str, at_index);
+		textbox.x += w*ui_scale();
+		ui_do_label(&textbox, "_", font_size, -1);
+	}
+
+	return r;
+}
+
+float MENUS::ui_do_scrollbar_v(const void *id, const RECT *rect, float current)
+{
+	RECT handle;
+	static float offset_y;
+	ui_hsplit_t(rect, 33, &handle, 0);
+
+	handle.y += (rect->h-handle.h)*current;
+
+	/* logic */
+    float ret = current;
+    int inside = ui_mouse_inside(&handle);
+
+	if(ui_active_item() == id)
+	{
+		if(!ui_mouse_button(0))
+			ui_set_active_item(0);
+		
+		float min = rect->y;
+		float max = rect->h-handle.h;
+		float cur = ui_mouse_y()-offset_y;
+		ret = (cur-min)/max;
+		if(ret < 0.0f) ret = 0.0f;
+		if(ret > 1.0f) ret = 1.0f;
+	}
+	else if(ui_hot_item() == id)
+	{
+		if(ui_mouse_button(0))
+		{
+			ui_set_active_item(id);
+			offset_y = ui_mouse_y()-handle.y;
+		}
+	}
+	
+	if(inside)
+		ui_set_hot_item(id);
+
+	// render
+	RECT rail;
+	ui_vmargin(rect, 5.0f, &rail);
+	ui_draw_rect(&rail, vec4(1,1,1,0.25f), 0, 0.0f);
+
+	RECT slider = handle;
+	slider.w = rail.x-slider.x;
+	ui_draw_rect(&slider, vec4(1,1,1,0.25f), CORNER_L, 2.5f);
+	slider.x = rail.x+rail.w;
+	ui_draw_rect(&slider, vec4(1,1,1,0.25f), CORNER_R, 2.5f);
+
+	slider = handle;
+	ui_margin(&slider, 5.0f, &slider);
+	ui_draw_rect(&slider, vec4(1,1,1,0.25f)*button_color_mul(id), CORNER_ALL, 2.5f);
+	
+    return ret;
+}
+
+
+
+float MENUS::ui_do_scrollbar_h(const void *id, const RECT *rect, float current)
+{
+	RECT handle;
+	static float offset_x;
+	ui_vsplit_l(rect, 33, &handle, 0);
+
+	handle.x += (rect->w-handle.w)*current;
+
+	/* logic */
+    float ret = current;
+    int inside = ui_mouse_inside(&handle);
+
+	if(ui_active_item() == id)
+	{
+		if(!ui_mouse_button(0))
+			ui_set_active_item(0);
+		
+		float min = rect->x;
+		float max = rect->w-handle.w;
+		float cur = ui_mouse_x()-offset_x;
+		ret = (cur-min)/max;
+		if(ret < 0.0f) ret = 0.0f;
+		if(ret > 1.0f) ret = 1.0f;
+	}
+	else if(ui_hot_item() == id)
+	{
+		if(ui_mouse_button(0))
+		{
+			ui_set_active_item(id);
+			offset_x = ui_mouse_x()-handle.x;
+		}
+	}
+	
+	if(inside)
+		ui_set_hot_item(id);
+
+	// render
+	RECT rail;
+	ui_hmargin(rect, 5.0f, &rail);
+	ui_draw_rect(&rail, vec4(1,1,1,0.25f), 0, 0.0f);
+
+	RECT slider = handle;
+	slider.h = rail.y-slider.y;
+	ui_draw_rect(&slider, vec4(1,1,1,0.25f), CORNER_T, 2.5f);
+	slider.y = rail.y+rail.h;
+	ui_draw_rect(&slider, vec4(1,1,1,0.25f), CORNER_B, 2.5f);
+
+	slider = handle;
+	ui_margin(&slider, 5.0f, &slider);
+	ui_draw_rect(&slider, vec4(1,1,1,0.25f)*button_color_mul(id), CORNER_ALL, 2.5f);
+	
+    return ret;
+}
+
+int MENUS::ui_do_key_reader(void *id, const RECT *rect, int key)
+{
+	// process
+	static bool mouse_released = true;
+	int inside = ui_mouse_inside(rect);
+	int new_key = key;
+	
+	if(!ui_mouse_button(0))
+		mouse_released = true;
+
+	if(ui_active_item() == id)
+	{
+		for(int i = 0; i < inp_num_events(); i++)
+		{
+			INPUT_EVENT e = inp_get_event(i);
+			if(e.flags&INPFLAG_PRESS && e.key && e.key != KEY_ESC)
+			{
+				new_key = e.key;
+				ui_set_active_item(0);
+				mouse_released = false;
+				inp_clear_events();
+				break;
+			}
+		}
+	}
+	else if(ui_hot_item() == id)
+	{
+		if(ui_mouse_button(0) && mouse_released)
+			ui_set_active_item(id);
+	}
+	
+	if(inside)
+		ui_set_hot_item(id);
+
+	// draw
+	if (ui_active_item() == id)
+		ui_draw_keyselect_button(id, "???", 0, rect, 0);
+	else
+	{
+		if(key == 0)
+			ui_draw_keyselect_button(id, "", 0, rect, 0);
+		else
+			ui_draw_keyselect_button(id, inp_key_name(key), 0, rect, 0);
+	}
+	return new_key;
+}
+
+
+int MENUS::render_menubar(RECT r)
+{
+	RECT box = r;
+	RECT button;
+	
+	int active_page = config.ui_page;
+	int new_page = -1;
+	
+	if(client_state() != CLIENTSTATE_OFFLINE)
+		active_page = game_page;
+	
+	if(client_state() == CLIENTSTATE_OFFLINE)
+	{
+		/* offline menus */
+		if(0) // this is not done yet
+		{
+			ui_vsplit_l(&box, 90.0f, &button, &box);
+			static int news_button=0;
+			if (ui_do_button(&news_button, "News", active_page==PAGE_NEWS, &button, ui_draw_menu_tab_button, 0))
+				new_page = PAGE_NEWS;
+			ui_vsplit_l(&box, 30.0f, 0, &box); 
+		}
+
+		ui_vsplit_l(&box, 110.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))
+		{
+			client_serverbrowse_refresh(0);
+			new_page = PAGE_INTERNET;
+		}
+
+		ui_vsplit_l(&box, 4.0f, 0, &box);
+		ui_vsplit_l(&box, 90.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))
+		{
+			client_serverbrowse_refresh(1);
+			new_page = PAGE_LAN;
+		}
+
+		if(0) // this one is not done yet
+		{
+			ui_vsplit_l(&box, 4.0f, 0, &box);
+			ui_vsplit_l(&box, 120.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))
+				new_page  = PAGE_FAVORITES;
+		}
+
+
+	}
+	else
+	{
+		/* online menus */
+		ui_vsplit_l(&box, 90.0f, &button, &box);
+		static int game_button=0;
+		if (ui_do_button(&game_button, "Game", active_page==PAGE_GAME, &button, ui_draw_menu_tab_button, 0))
+			new_page = PAGE_GAME;
+
+		ui_vsplit_l(&box, 4.0f, 0, &box);
+		ui_vsplit_l(&box, 140.0f, &button, &box);
+		static int server_info_button=0;
+		if (ui_do_button(&server_info_button, "Server Info", active_page==PAGE_SERVER_INFO, &button, ui_draw_menu_tab_button, 0))
+			new_page = PAGE_SERVER_INFO;
+			
+		ui_vsplit_l(&box, 30.0f, 0, &box);
+	}
+		
+	/*
+	ui_vsplit_r(&box, 110.0f, &box, &button);
+	static int system_button=0;
+	if (ui_do_button(&system_button, "System", config.ui_page==PAGE_SYSTEM, &button, ui_draw_menu_tab_button, 0))
+		config.ui_page = PAGE_SYSTEM;
+		
+	ui_vsplit_r(&box, 30.0f, &box, 0);
+	*/
+	
+	ui_vsplit_r(&box, 110.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);
+	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;
+	
+	if(new_page != -1)
+	{
+		if(client_state() == CLIENTSTATE_OFFLINE)
+			config.ui_page = new_page;
+		else
+			game_page = new_page;
+	}
+		
+	return 0;
+}
+
+void MENUS::render_background()
+{
+	RECT s = *ui_screen();
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+		vec4 bottom(gui_color.r*0.6f, gui_color.g*0.6f, gui_color.b*0.6f, 1.0f);
+		vec4 top(gui_color.r, gui_color.g, gui_color.b, 1.0f);
+		gfx_setcolorvertex(0, top.r, top.g, top.b, top.a);
+		gfx_setcolorvertex(1, top.r, top.g, top.b, top.a);
+		gfx_setcolorvertex(2, bottom.r, bottom.g, bottom.b, bottom.a);
+		gfx_setcolorvertex(3, bottom.r, bottom.g, bottom.b, bottom.a);
+		gfx_quads_drawTL(0, 0, s.w, s.h);
+	gfx_quads_end();
+	
+	if(data->images[IMAGE_BANNER].id != 0)
+	{
+		gfx_texture_set(data->images[IMAGE_BANNER].id);
+		gfx_quads_begin();
+		gfx_setcolor(0,0,0,0.05f);
+		gfx_quads_setrotation(-pi/4+0.15f);
+		gfx_quads_draw(400, 300, 1000, 250);
+		gfx_quads_end();
+	}
+}
+
+void MENUS::render_loading(float percent)
+{
+	// need up date this here to get correct
+	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);
+	
+    RECT screen = *ui_screen();
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+	
+	render_background();
+
+	float tw;
+
+	float w = 700;
+	float h = 200;
+	float x = screen.w/2-w/2;
+	float y = screen.h/2-h/2;
+
+	gfx_blend_normal();
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.50f);
+	draw_round_rect(x, y, w, h, 40.0f);
+	gfx_quads_end();
+
+
+	const char *caption = "Loading";
+
+	tw = gfx_text_width(0, 48.0f, caption, -1);
+	RECT r;
+	r.x = x;
+	r.y = y+20;
+	r.w = w;
+	r.h = h;
+	ui_do_label(&r, caption, 48.0f, 0, -1);
+
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(1,1,1,0.75f);
+	draw_round_rect(x+40, y+h-75, (w-80)*percent, 25, 5.0f);
+	gfx_quads_end();
+
+	gfx_swap();
+}
+
+void MENUS::render_news(RECT main_view)
+{
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
+}
+
+void MENUS::render_game(RECT main_view)
+{
+	RECT button;
+	ui_hsplit_t(&main_view, 45.0f, &main_view, 0);
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
+
+	ui_hsplit_t(&main_view, 10.0f, 0, &main_view);
+	ui_hsplit_t(&main_view, 25.0f, &main_view, 0);
+	ui_vmargin(&main_view, 10.0f, &main_view);
+	
+	ui_vsplit_r(&main_view, 120.0f, &main_view, &button);
+	static int disconnect_button = 0;
+	if(ui_do_button(&disconnect_button, "Disconnect", 0, &button, ui_draw_menu_button, 0))
+		client_disconnect();
+
+	if(gameclient.snap.local_info && gameclient.snap.gameobj)
+	{
+		if(gameclient.snap.local_info->team != -1)
+		{
+			ui_vsplit_l(&main_view, 10.0f, &button, &main_view);
+			ui_vsplit_l(&main_view, 120.0f, &button, &main_view);
+			static int spectate_button = 0;
+			if(ui_do_button(&spectate_button, "Spectate", 0, &button, ui_draw_menu_button, 0))
+			{
+				gameclient.send_switch_team(-1);
+				menu_active = false;
+			}
+		}
+		
+		if(gameclient.snap.gameobj->flags & GAMEFLAG_TEAMS)
+		{
+			if(gameclient.snap.local_info->team != 0)
+			{
+				ui_vsplit_l(&main_view, 10.0f, &button, &main_view);
+				ui_vsplit_l(&main_view, 120.0f, &button, &main_view);
+				static int spectate_button = 0;
+				if(ui_do_button(&spectate_button, "Join Red", 0, &button, ui_draw_menu_button, 0))
+				{
+					gameclient.send_switch_team(0);
+					menu_active = false;
+				}
+			}
+
+			if(gameclient.snap.local_info->team != 1)
+			{
+				ui_vsplit_l(&main_view, 10.0f, &button, &main_view);
+				ui_vsplit_l(&main_view, 120.0f, &button, &main_view);
+				static int spectate_button = 0;
+				if(ui_do_button(&spectate_button, "Join Blue", 0, &button, ui_draw_menu_button, 0))
+				{
+					gameclient.send_switch_team(1);
+					menu_active = false;
+				}
+			}
+		}
+		else
+		{
+			if(gameclient.snap.local_info->team != 0)
+			{
+				ui_vsplit_l(&main_view, 10.0f, &button, &main_view);
+				ui_vsplit_l(&main_view, 120.0f, &button, &main_view);
+				static int spectate_button = 0;
+				if(ui_do_button(&spectate_button, "Join Game", 0, &button, ui_draw_menu_button, 0))
+				{
+					gameclient.send_switch_team(0);
+					menu_active = false;
+				}
+			}						
+		}
+	}
+}
+
+void MENUS::render_serverinfo(RECT main_view)
+{
+	// render background
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
+	
+	// render motd
+	RECT view;
+	ui_margin(&main_view, 10.0f, &view);
+	//void gfx_text(void *font, float x, float y, float size, const char *text, int max_width);
+	// TODO: repair me
+	//gfx_text(0, view.x, view.y, 16, server_motd, -1);
+}
+
+void MENUS::init()
+{
+	if(config.cl_show_welcome)
+		popup = POPUP_FIRST_LAUNCH;
+	config.cl_show_welcome = 0;
+}
+
+int MENUS::render()
+{
+    RECT screen = *ui_screen();
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+
+	static bool first = true;
+	if(first)
+	{
+		if(config.ui_page == PAGE_INTERNET)
+			client_serverbrowse_refresh(0);
+		else if(config.ui_page == PAGE_LAN)
+			client_serverbrowse_refresh(1);
+		first = false;
+	}
+	
+	if(client_state() == CLIENTSTATE_ONLINE)
+	{
+		color_tabbar_inactive = color_tabbar_inactive_ingame;
+		color_tabbar_active = color_tabbar_active_ingame;
+	}
+	else
+	{
+		render_background();
+		color_tabbar_inactive = color_tabbar_inactive_outgame;
+		color_tabbar_active = color_tabbar_active_outgame;
+	}
+	
+	RECT tab_bar;
+	RECT main_view;
+
+	// some margin around the screen
+	ui_margin(&screen, 10.0f, &screen);
+	
+	if(popup == POPUP_NONE)
+	{
+		// do tab bar
+		ui_hsplit_t(&screen, 26.0f, &tab_bar, &main_view);
+		ui_vmargin(&tab_bar, 20.0f, &tab_bar);
+		render_menubar(tab_bar);
+			
+		// render current page
+		if(client_state() != CLIENTSTATE_OFFLINE)
+		{
+			if(game_page == PAGE_GAME)
+				render_game(main_view);
+			else if(game_page == PAGE_SERVER_INFO)
+				render_serverinfo(main_view);
+			else if(game_page == PAGE_SETTINGS)
+				render_settings(main_view);
+		}
+		else if(config.ui_page == PAGE_NEWS)
+			render_news(main_view);
+		else if(config.ui_page == PAGE_INTERNET)
+			render_serverbrowser(main_view);
+		else if(config.ui_page == PAGE_LAN)
+			render_serverbrowser(main_view);
+		else if(config.ui_page == PAGE_FAVORITES)
+			render_serverbrowser(main_view);
+		else if(config.ui_page == PAGE_SETTINGS)
+			render_settings(main_view);
+	}
+	else
+	{
+		// make sure that other windows doesn't do anything funnay!
+		//ui_set_hot_item(0);
+		//ui_set_active_item(0);
+		char buf[128];
+		const char *title = "";
+		const char *extra_text = "";
+		const char *button_text = "";
+		int extra_align = 0;
+		
+		if(popup == POPUP_CONNECTING)
+		{
+			title = "Connecting to";
+			extra_text = config.ui_server_address;  // TODO: query the client about the address
+			button_text = "Abort";
+			if(client_mapdownload_totalsize() > 0)
+			{
+				title = "Downloading map";
+				str_format(buf, sizeof(buf), "%d/%d KiB", client_mapdownload_amount()/1024, client_mapdownload_totalsize()/1024);
+				extra_text = buf;
+			}
+		}
+		else if(popup == POPUP_DISCONNECTED)
+		{
+			title = "Disconnected";
+			extra_text = client_error_string();
+			button_text = "Ok";
+			extra_align = -1;
+		}
+		else if(popup == POPUP_PASSWORD)
+		{
+			title = "Password Error";
+			extra_text = client_error_string();
+			button_text = "Try Again";
+		}
+		else if(popup == POPUP_QUIT)
+		{
+			title = "Quit";
+			extra_text = "Are you sure that you want to quit?";
+		}
+		else if(popup == POPUP_FIRST_LAUNCH)
+		{
+			title = "Welcome to Teeworlds";
+			extra_text =
+			"As this is the first time you launch the game, please enter your nick name below. "
+			"It's recommended that you check the settings to adjust them to your liking "
+			"before joining a server.";
+			button_text = "Ok";
+			extra_align = -1;
+		}
+		
+		RECT box, part;
+		box = screen;
+		ui_vmargin(&box, 150.0f, &box);
+		ui_hmargin(&box, 150.0f, &box);
+		
+		// render the box
+		ui_draw_rect(&box, vec4(0,0,0,0.5f), CORNER_ALL, 15.0f);
+		 
+		ui_hsplit_t(&box, 20.f, &part, &box);
+		ui_hsplit_t(&box, 24.f, &part, &box);
+		ui_do_label(&part, title, 24.f, 0);
+		ui_hsplit_t(&box, 20.f, &part, &box);
+		ui_hsplit_t(&box, 24.f, &part, &box);
+		ui_vmargin(&part, 20.f, &part);
+		
+		if(extra_align == -1)
+			ui_do_label(&part, extra_text, 20.f, -1, (int)part.w);
+		else
+			ui_do_label(&part, extra_text, 20.f, 0, -1);
+
+		if(popup == POPUP_QUIT)
+		{
+			RECT yes, no;
+			ui_hsplit_b(&box, 20.f, &box, &part);
+			ui_hsplit_b(&box, 24.f, &box, &part);
+			ui_vmargin(&part, 80.0f, &part);
+			
+			ui_vsplit_mid(&part, &no, &yes);
+			
+			ui_vmargin(&yes, 20.0f, &yes);
+			ui_vmargin(&no, 20.0f, &no);
+
+			static int button_abort = 0;
+			if(ui_do_button(&button_abort, "No", 0, &no, ui_draw_menu_button, 0) || inp_key_down(KEY_ESC))
+				popup = POPUP_NONE;
+
+			static int button_tryagain = 0;
+			if(ui_do_button(&button_tryagain, "Yes", 0, &yes, ui_draw_menu_button, 0) || inp_key_down(KEY_ENTER))
+				client_quit();
+		}
+		else if(popup == POPUP_PASSWORD)
+		{
+			RECT label, textbox, tryagain, abort;
+			
+			ui_hsplit_b(&box, 20.f, &box, &part);
+			ui_hsplit_b(&box, 24.f, &box, &part);
+			ui_vmargin(&part, 80.0f, &part);
+			
+			ui_vsplit_mid(&part, &abort, &tryagain);
+			
+			ui_vmargin(&tryagain, 20.0f, &tryagain);
+			ui_vmargin(&abort, 20.0f, &abort);
+			
+			static int button_abort = 0;
+			if(ui_do_button(&button_abort, "Abort", 0, &abort, ui_draw_menu_button, 0) || inp_key_down(KEY_ESC))
+				popup = POPUP_NONE;
+
+			static int button_tryagain = 0;
+			if(ui_do_button(&button_tryagain, "Try again", 0, &tryagain, ui_draw_menu_button, 0) || inp_key_down(KEY_ENTER))
+			{
+				client_connect(config.ui_server_address);
+			}
+			
+			ui_hsplit_b(&box, 60.f, &box, &part);
+			ui_hsplit_b(&box, 24.f, &box, &part);
+			
+			ui_vsplit_l(&part, 60.0f, 0, &label);
+			ui_vsplit_l(&label, 100.0f, 0, &textbox);
+			ui_vsplit_l(&textbox, 20.0f, 0, &textbox);
+			ui_vsplit_r(&textbox, 60.0f, &textbox, 0);
+			ui_do_label(&label, "Password:", 20, -1);
+			ui_do_edit_box(&config.password, &textbox, config.password, sizeof(config.password), 14.0f, true);
+		}
+		else if(popup == POPUP_FIRST_LAUNCH)
+		{
+			RECT label, textbox;
+			
+			ui_hsplit_b(&box, 20.f, &box, &part);
+			ui_hsplit_b(&box, 24.f, &box, &part);
+			ui_vmargin(&part, 80.0f, &part);
+			
+			static int enter_button = 0;
+			if(ui_do_button(&enter_button, "Enter", 0, &part, ui_draw_menu_button, 0) || inp_key_down(KEY_ENTER))
+				popup = POPUP_NONE;
+			
+			ui_hsplit_b(&box, 40.f, &box, &part);
+			ui_hsplit_b(&box, 24.f, &box, &part);
+			
+			ui_vsplit_l(&part, 60.0f, 0, &label);
+			ui_vsplit_l(&label, 100.0f, 0, &textbox);
+			ui_vsplit_l(&textbox, 20.0f, 0, &textbox);
+			ui_vsplit_r(&textbox, 60.0f, &textbox, 0);
+			ui_do_label(&label, "Nickname:", 20, -1);
+			ui_do_edit_box(&config.player_name, &textbox, config.player_name, sizeof(config.player_name), 14.0f);
+		}
+		else
+		{
+			ui_hsplit_b(&box, 20.f, &box, &part);
+			ui_hsplit_b(&box, 24.f, &box, &part);
+			ui_vmargin(&part, 120.0f, &part);
+
+			static int button = 0;
+			if(ui_do_button(&button, button_text, 0, &part, ui_draw_menu_button, 0) || inp_key_down(KEY_ESC) || inp_key_down(KEY_ENTER))
+			{
+				if(popup == POPUP_CONNECTING)
+					client_disconnect();
+				popup = POPUP_NONE;
+			}
+		}
+	}
+	
+	return 0;
+}
+
+void MENUS::on_reset()
+{
+}
+
+bool MENUS::on_mousemove(float x, float y)
+{
+	if(!menu_active)
+		return false;
+		
+	mouse_pos.x += x;
+	mouse_pos.y += y;
+	if(mouse_pos.x < 0) mouse_pos.x = 0;
+	if(mouse_pos.y < 0) mouse_pos.y = 0;
+	if(mouse_pos.x > gfx_screenwidth()) mouse_pos.x = gfx_screenwidth();
+	if(mouse_pos.y > gfx_screenheight()) mouse_pos.y = gfx_screenheight();
+	
+	return true;
+}
+
+bool MENUS::on_input(INPUT_EVENT e)
+{
+	return false;
+}
+
+void MENUS::on_statechange(int new_state, int old_state)
+{
+	if(new_state == CLIENTSTATE_OFFLINE)
+	{
+		popup = POPUP_NONE;
+		if(client_error_string() && client_error_string()[0] != 0)
+		{
+			if(strstr(client_error_string(), "password"))
+			{
+				popup = POPUP_PASSWORD;
+				ui_set_hot_item(&config.password);
+				ui_set_active_item(&config.password);
+			}
+			else
+				popup = POPUP_DISCONNECTED;
+		}	}
+	else if(new_state == CLIENTSTATE_LOADING)
+		popup = POPUP_CONNECTING;
+	else if(new_state == CLIENTSTATE_CONNECTING)
+		popup = POPUP_CONNECTING;
+	else if (new_state == CLIENTSTATE_ONLINE)
+	{
+		popup = POPUP_NONE;
+		menu_active = false;
+	 	//menu_game_active = true;
+	 	//snapshot_count = 0;
+	}
+}
+
+void MENUS::on_render()
+{
+	if(!menu_active)
+		return;
+
+	// 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);
+
+	color_tabbar_inactive_outgame = vec4(0,0,0,0.25f);
+	color_tabbar_active_outgame = vec4(0,0,0,0.5f);
+
+	float color_ingame_scale_i = 0.5f;
+	float color_ingame_scale_a = 0.2f;
+	color_tabbar_inactive_ingame = vec4(
+		gui_color.r*color_ingame_scale_i,
+		gui_color.g*color_ingame_scale_i,
+		gui_color.b*color_ingame_scale_i,
+		gui_color.a*0.8f);
+	
+	color_tabbar_active_ingame = vec4(
+		gui_color.r*color_ingame_scale_a,
+		gui_color.g*color_ingame_scale_a,
+		gui_color.b*color_ingame_scale_a,
+		gui_color.a);
+    
+	// update the ui
+	RECT *screen = ui_screen();
+	float mx = (mouse_pos.x/(float)gfx_screenwidth())*screen->w;
+	float my = (mouse_pos.y/(float)gfx_screenheight())*screen->h;
+		
+	int buttons = 0;
+	if(inp_key_pressed(KEY_MOUSE_1)) buttons |= 1;
+	if(inp_key_pressed(KEY_MOUSE_2)) buttons |= 2;
+	if(inp_key_pressed(KEY_MOUSE_3)) buttons |= 4;
+		
+	ui_update(mx,my,mx*3.0f,my*3.0f,buttons);
+    
+    // render
+	render();
+	
+	// render cursor
+    gfx_texture_set(data->images[IMAGE_CURSOR].id);
+    gfx_quads_begin();
+    gfx_setcolor(1,1,1,1);
+    gfx_quads_drawTL(mx,my,24,24);
+    gfx_quads_end();
+
+	// render debug information
+	if(config.debug)
+	{
+		RECT screen = *ui_screen();
+		gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
+
+		char buf[512];
+		str_format(buf, sizeof(buf), "%p %p %p", ui_hot_item(), ui_active_item(), ui_last_active_item());
+		TEXT_CURSOR cursor;
+		gfx_text_set_cursor(&cursor, 10, 10, 10, TEXTFLAG_RENDER);
+		gfx_text_ex(&cursor, buf, -1);
+	}
+
+}
diff --git a/src/game/client/components/menus.hpp b/src/game/client/components/menus.hpp
new file mode 100644
index 00000000..4d45c65a
--- /dev/null
+++ b/src/game/client/components/menus.hpp
@@ -0,0 +1,96 @@
+#include <base/vmath.hpp>
+
+#include <game/client/component.hpp>
+#include <game/client/gc_ui.hpp>
+
+class MENUS : public COMPONENT
+{	
+	static vec4 gui_color;
+	static vec4 color_tabbar_inactive_outgame;
+	static vec4 color_tabbar_active_outgame;
+	static vec4 color_tabbar_inactive_ingame;
+	static vec4 color_tabbar_active_ingame;
+	static vec4 color_tabbar_inactive;
+	static vec4 color_tabbar_active;
+	
+	static vec4 button_color_mul(const void *id);
+
+	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);
+	static void ui_draw_menu_tab_button(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+	static void ui_draw_settings_tab_button(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+	static void ui_draw_grid_header(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+	static void ui_draw_list_row(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+	static void ui_draw_checkbox_common(const void *id, const char *text, const char *boxtext, const RECT *r);
+	static void ui_draw_checkbox(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+	static void ui_draw_checkbox_number(const void *id, const char *text, int checked, const RECT *r, const void *extra);
+	static int ui_do_edit_box(void *id, const RECT *rect, char *str, int str_size, float font_size, bool hidden=false);
+
+	static float ui_do_scrollbar_v(const void *id, const RECT *rect, float current);
+	static float ui_do_scrollbar_h(const void *id, const RECT *rect, float current);
+
+	static int ui_do_key_reader(void *id, const RECT *rect, int key);
+
+
+	enum
+	{
+		POPUP_NONE=0,
+		POPUP_FIRST_LAUNCH,
+		POPUP_CONNECTING,
+		POPUP_DISCONNECTED,
+		POPUP_PASSWORD,
+		POPUP_QUIT, 
+	};
+
+	enum
+	{
+		PAGE_NEWS=0,
+		PAGE_GAME,
+		PAGE_SERVER_INFO,
+		PAGE_INTERNET,
+		PAGE_LAN,
+		PAGE_FAVORITES,
+		PAGE_SETTINGS,
+		PAGE_SYSTEM,
+	};
+
+	int game_page;
+	int popup;
+	int active_page;
+	bool menu_active;
+	vec2 mouse_pos;
+	
+	// for graphic settings
+	bool need_restart;
+
+	// found in menus.cpp
+	int render();
+	void render_background();
+	void render_loading(float percent);
+	int render_menubar(RECT r);
+	void render_news(RECT main_view);
+	void render_game(RECT main_view);
+	void render_serverinfo(RECT main_view);
+	
+	// found in menus_browser.cpp
+	void render_serverbrowser(RECT main_view);
+	
+	// found in menus_settings.cpp
+	void render_settings_player(RECT main_view);
+	void render_settings_controls(RECT main_view);
+	void render_settings_graphics(RECT main_view);
+	void render_settings_sound(RECT main_view);
+	void render_settings(RECT main_view);
+	
+public:
+	MENUS();
+
+	void init();
+
+	virtual void on_statechange(int new_state, int old_state);
+	virtual void on_reset();
+	virtual void on_render();
+	virtual bool on_input(INPUT_EVENT e);
+	virtual bool on_mousemove(float x, float y);
+};
diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp
new file mode 100644
index 00000000..05ca620b
--- /dev/null
+++ b/src/game/client/components/menus_browser.cpp
@@ -0,0 +1,531 @@
+
+#include <string.h> // strcmp, strlen, strncpy
+#include <stdlib.h> // atoi
+
+extern "C" {
+	#include <engine/e_client_interface.h>
+	#include <engine/e_config.h>
+	#include <engine/client/ec_font.h>
+}
+
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_render.hpp>
+#include "menus.hpp"
+
+void MENUS::render_serverbrowser(RECT main_view)
+{
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
+	
+	RECT view;
+	ui_margin(&main_view, 10.0f, &view);
+	
+	RECT headers;
+	RECT filters;
+	RECT status;
+	RECT toolbox;
+	RECT server_details;
+	RECT server_scoreboard;
+
+	//ui_hsplit_t(&view, 20.0f, &status, &view);
+	ui_hsplit_b(&view, 110.0f, &view, &filters);
+
+	// split off a piece for details and scoreboard
+	ui_vsplit_r(&view, 200.0f, &view, &server_details);
+
+	// server list
+	ui_hsplit_t(&view, 16.0f, &headers, &view);
+	//ui_hsplit_b(&view, 110.0f, &view, &filters);
+	ui_hsplit_b(&view, 5.0f, &view, 0);
+	ui_hsplit_b(&view, 20.0f, &view, &status);
+
+	//ui_vsplit_r(&filters, 300.0f, &filters, &toolbox);
+	//ui_vsplit_r(&filters, 150.0f, &filters, 0);
+
+	ui_vsplit_mid(&filters, &filters, &toolbox);
+	ui_vsplit_r(&filters, 50.0f, &filters, 0);
+	
+	// split of the scrollbar
+	ui_draw_rect(&headers, vec4(1,1,1,0.25f), CORNER_T, 5.0f);
+	ui_vsplit_r(&headers, 20.0f, &headers, 0);
+	
+	struct column
+	{
+		int id;
+		int sort;
+		const char *caption;
+		int direction;
+		float width;
+		int flags;
+		RECT rect;
+		RECT spacer;
+	};
+	
+	enum
+	{
+		FIXED=1,
+		SPACER=2,
+		
+		COL_FLAGS=0,
+		COL_NAME,
+		COL_GAMETYPE,
+		COL_MAP,
+		COL_PLAYERS,
+		COL_PING,
+		COL_PROGRESS,
+		COL_VERSION,
+	};
+	
+	static column cols[] = {
+		{-1,			-1,						" ",		-1, 10.0f, 0, {0}, {0}},
+		{COL_FLAGS,		-1,						" ",		-1, 20.0f, 0, {0}, {0}},
+		{COL_NAME,		BROWSESORT_NAME,		"Name",		0, 300.0f, 0, {0}, {0}},
+		{COL_GAMETYPE,	BROWSESORT_GAMETYPE,	"Type",		1, 50.0f, 0, {0}, {0}},
+		{COL_MAP,		BROWSESORT_MAP,			"Map", 		1, 100.0f, 0, {0}, {0}},
+		{COL_PLAYERS,	BROWSESORT_NUMPLAYERS,	"Players",	1, 60.0f, 0, {0}, {0}},
+		{-1,			-1,						" ",		1, 10.0f, 0, {0}, {0}},
+		{COL_PING,		BROWSESORT_PING,		"Ping",		1, 40.0f, FIXED, {0}, {0}},
+	};
+	
+	int num_cols = sizeof(cols)/sizeof(column);
+	
+	// do layout
+	for(int i = 0; i < num_cols; i++)
+	{
+		if(cols[i].direction == -1)
+		{
+			ui_vsplit_l(&headers, cols[i].width, &cols[i].rect, &headers);
+			
+			if(i+1 < num_cols)
+			{
+				//cols[i].flags |= SPACER;
+				ui_vsplit_l(&headers, 2, &cols[i].spacer, &headers);
+			}
+		}
+	}
+	
+	for(int i = num_cols-1; i >= 0; i--)
+	{
+		if(cols[i].direction == 1)
+		{
+			ui_vsplit_r(&headers, cols[i].width, &headers, &cols[i].rect);
+			ui_vsplit_r(&headers, 2, &headers, &cols[i].spacer);
+		}
+	}
+	
+	for(int i = 0; i < num_cols; i++)
+	{
+		if(cols[i].direction == 0)
+			cols[i].rect = headers;
+	}
+	
+	// do headers
+	for(int i = 0; i < num_cols; i++)
+	{
+		if(ui_do_button(cols[i].caption, cols[i].caption, config.b_sort == cols[i].sort, &cols[i].rect, ui_draw_grid_header, 0))
+		{
+			if(cols[i].sort != -1)
+			{
+				if(config.b_sort == cols[i].sort)
+					config.b_sort_order ^= 1;
+				else
+					config.b_sort_order = 0;
+				config.b_sort = cols[i].sort;
+			}
+		}
+	}
+	
+	ui_draw_rect(&view, vec4(0,0,0,0.15f), 0, 0);
+	
+	RECT scroll;
+	ui_vsplit_r(&view, 15, &view, &scroll);
+	
+	int num_servers = client_serverbrowse_sorted_num();
+	
+	int num = (int)(view.h/cols[0].rect.h);
+	static int scrollbar = 0;
+	static float scrollvalue = 0;
+	//static int selected_index = -1;
+	ui_hmargin(&scroll, 5.0f, &scroll);
+	scrollvalue = ui_do_scrollbar_v(&scrollbar, &scroll, scrollvalue);
+	
+	int scrollnum = num_servers-num+10;
+	if(scrollnum > 0)
+	{
+		if(inp_key_presses(KEY_MOUSE_WHEEL_UP))
+			scrollvalue -= 1.0f/scrollnum;
+		if(inp_key_presses(KEY_MOUSE_WHEEL_DOWN))
+			scrollvalue += 1.0f/scrollnum;
+			
+		if(scrollvalue < 0) scrollvalue = 0;
+		if(scrollvalue > 1) scrollvalue = 1;
+	}
+	else
+		scrollnum = 0;
+
+	// set clipping
+	ui_clip_enable(&view);
+	
+	int start = (int)(scrollnum*scrollvalue);
+	if(start < 0)
+		start = 0;
+	
+	RECT original_view = view;
+	view.y -= scrollvalue*scrollnum*cols[0].rect.h;
+	
+	int new_selected = -1;
+	int selected_index = -1;
+	int num_players = 0;
+
+	for (int i = 0; i < num_servers; i++)
+	{
+		SERVER_INFO *item = client_serverbrowse_sorted_get(i);
+		num_players += item->num_players;
+	}
+	
+	for (int i = 0; i < num_servers; i++)
+	{
+		int item_index = i;
+		SERVER_INFO *item = client_serverbrowse_sorted_get(item_index);
+		RECT row;
+        RECT select_hit_box;
+			
+		int selected = strcmp(item->address, config.ui_server_address) == 0; //selected_index==item_index;
+				
+		ui_hsplit_t(&view, 17.0f, &row, &view);
+		select_hit_box = row;
+	
+		if(selected)
+		{
+			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);
+		}
+
+
+		// make sure that only those in view can be selected
+		if(row.y+row.h > original_view.y)
+		{
+			if(select_hit_box.y < original_view.y) // clip the selection
+			{
+				select_hit_box.h -= original_view.y-select_hit_box.y;
+				select_hit_box.y = original_view.y;
+			}
+			
+			if(ui_do_button(item, "", selected, &select_hit_box, 0, 0))
+			{
+				new_selected = item_index;
+			}
+		}
+		
+		// check if we need to do more
+		if(row.y > original_view.y+original_view.h)
+			break;
+
+		for(int c = 0; c < num_cols; c++)
+		{
+			RECT button;
+			char temp[64];
+			button.x = cols[c].rect.x;
+			button.y = row.y;
+			button.h = row.h;
+			button.w = cols[c].rect.w;
+			
+			//int s = 0;
+			int id = cols[c].id;
+
+			//s = ui_do_button(item, "L", l, &button, ui_draw_browse_icon, 0);
+			
+			if(id == COL_FLAGS)
+			{
+				if(item->flags&1)
+					ui_draw_browse_icon(0x100, &button);
+			}
+			else if(id == COL_NAME)
+			{
+				TEXT_CURSOR cursor;
+				gfx_text_set_cursor(&cursor, button.x, button.y, 12.0f, TEXTFLAG_RENDER);
+				
+				if(config.b_filter_string[0] && (item->quicksearch_hit&BROWSEQUICK_SERVERNAME))
+				{
+					// highlight the parts that matches
+					const char *s = str_find_nocase(item->name, config.b_filter_string);
+					if(s)
+					{
+						gfx_text_ex(&cursor, item->name, (int)(s-item->name));
+						gfx_text_color(0.4f,0.4f,1.0f,1);
+						gfx_text_ex(&cursor, s, strlen(config.b_filter_string));
+						gfx_text_color(1,1,1,1);
+						gfx_text_ex(&cursor, s+strlen(config.b_filter_string), -1);
+					}
+					else
+						gfx_text_ex(&cursor, item->name, -1);
+				}
+				else
+					gfx_text_ex(&cursor, item->name, -1);
+			}
+			else if(id == COL_MAP)
+				ui_do_label(&button, item->map, 12.0f, -1);
+			else if(id == COL_PLAYERS)
+			{
+				str_format(temp, sizeof(temp), "%i/%i", item->num_players, item->max_players);
+				if(config.b_filter_string[0] && (item->quicksearch_hit&BROWSEQUICK_PLAYERNAME))
+					gfx_text_color(0.4f,0.4f,1.0f,1);
+				ui_do_label(&button, temp, 12.0f, 1);
+				gfx_text_color(1,1,1,1);
+			}
+			else if(id == COL_PING)
+			{
+				str_format(temp, sizeof(temp), "%i", item->latency);
+				ui_do_label(&button, temp, 12.0f, 1);
+			}
+			else if(id == COL_PROGRESS)
+			{
+				if(item->progression > 100)
+					item->progression = 100;
+				ui_draw_browse_icon(item->progression, &button);
+			}
+			else if(id == COL_VERSION)
+			{
+				const char *version = item->version;
+				if(strcmp(version, "0.3 e2d7973c6647a13c") == 0) // TODO: remove me later on
+					version = "0.3.0";
+				ui_do_label(&button, version, 12.0f, 1);
+			}			
+			else if(id == COL_GAMETYPE)
+				ui_do_label(&button, item->gametype, 12.0f, 0);
+		}
+	}
+
+	ui_clip_disable();
+	
+	if(new_selected != -1)
+	{
+		// select the new server
+		SERVER_INFO *item = client_serverbrowse_sorted_get(new_selected);
+		strncpy(config.ui_server_address, item->address, sizeof(config.ui_server_address));
+		if(inp_mouse_doubleclick())
+			client_connect(config.ui_server_address);
+	}
+	
+	SERVER_INFO *selected_server = client_serverbrowse_sorted_get(selected_index);
+	RECT server_header;
+
+	ui_vsplit_l(&server_details, 10.0f, 0x0, &server_details);
+
+	// split off a piece to use for scoreboard
+	ui_hsplit_t(&server_details, 140.0f, &server_details, &server_scoreboard);
+	ui_hsplit_b(&server_details, 10.0f, &server_details, 0x0);
+
+	// server details
+	const float font_size = 12.0f;
+	ui_hsplit_t(&server_details, 20.0f, &server_header, &server_details);
+	ui_draw_rect(&server_header, vec4(1,1,1,0.25f), CORNER_T, 4.0f);
+	ui_draw_rect(&server_details, vec4(0,0,0,0.15f), CORNER_B, 4.0f);
+	ui_vsplit_l(&server_header, 8.0f, 0x0, &server_header);
+	ui_do_label(&server_header, "Server Details: ", font_size+2.0f, -1);
+
+	ui_vsplit_l(&server_details, 5.0f, 0x0, &server_details);
+
+	ui_margin(&server_details, 3.0f, &server_details);
+
+	if (selected_server)
+	{
+		RECT row;
+		static const char *labels[] = { "Version:", "Game Type:", "Progression:", "Ping:" };
+
+		RECT left_column;
+		RECT right_column;
+
+		ui_vsplit_l(&server_details, 5.0f, 0x0, &server_details);
+		ui_vsplit_l(&server_details, 80.0f, &left_column, &right_column);
+
+		for (int i = 0; i < 4; i++)
+		{
+			ui_hsplit_t(&left_column, 15.0f, &row, &left_column);
+			ui_do_label(&row, labels[i], font_size, -1);
+		}
+
+		ui_hsplit_t(&right_column, 15.0f, &row, &right_column);
+		ui_do_label(&row, selected_server->version, font_size, -1);
+
+		ui_hsplit_t(&right_column, 15.0f, &row, &right_column);
+		ui_do_label(&row, selected_server->gametype, font_size, -1);
+
+		char temp[16];
+
+		if(selected_server->progression < 0)
+			str_format(temp, sizeof(temp), "N/A");
+		else
+			str_format(temp, sizeof(temp), "%d%%", selected_server->progression);
+		ui_hsplit_t(&right_column, 15.0f, &row, &right_column);
+		ui_do_label(&row, temp, font_size, -1);
+
+		str_format(temp, sizeof(temp), "%d", selected_server->latency);
+		ui_hsplit_t(&right_column, 15.0f, &row, &right_column);
+		ui_do_label(&row, temp, font_size, -1);
+	}
+	
+	// server scoreboard
+	ui_hsplit_b(&server_scoreboard, 10.0f, &server_scoreboard, 0x0);
+	ui_hsplit_t(&server_scoreboard, 20.0f, &server_header, &server_scoreboard);
+	ui_draw_rect(&server_header, vec4(1,1,1,0.25f), CORNER_T, 4.0f);
+	ui_draw_rect(&server_scoreboard, vec4(0,0,0,0.15f), CORNER_B, 4.0f);
+	ui_vsplit_l(&server_header, 8.0f, 0x0, &server_header);
+	ui_do_label(&server_header, "Scoreboard: ", font_size+2.0f, -1);
+
+	ui_vsplit_l(&server_scoreboard, 5.0f, 0x0, &server_scoreboard);
+
+	ui_margin(&server_scoreboard, 3.0f, &server_scoreboard);
+
+	if (selected_server)
+	{
+		for (int i = 0; i < selected_server->num_players; i++)
+		{
+			RECT row;
+			char temp[16];
+			ui_hsplit_t(&server_scoreboard, 16.0f, &row, &server_scoreboard);
+
+			str_format(temp, sizeof(temp), "%d", selected_server->players[i].score);
+			ui_do_label(&row, temp, font_size, -1);
+
+			ui_vsplit_l(&row, 25.0f, 0x0, &row);
+		
+			TEXT_CURSOR cursor;
+			gfx_text_set_cursor(&cursor, row.x, row.y, 12.0f, TEXTFLAG_RENDER);
+			
+			const char *name = selected_server->players[i].name;
+			if(config.b_filter_string[0])
+			{
+				// highlight the parts that matches
+				const char *s = str_find_nocase(name, config.b_filter_string);
+				if(s)
+				{
+					gfx_text_ex(&cursor, name, (int)(s-name));
+					gfx_text_color(0.4f,0.4f,1,1);
+					gfx_text_ex(&cursor, s, strlen(config.b_filter_string));
+					gfx_text_color(1,1,1,1);
+					gfx_text_ex(&cursor, s+strlen(config.b_filter_string), -1);
+				}
+				else
+					gfx_text_ex(&cursor, name, -1);
+			}
+			else
+				gfx_text_ex(&cursor, name, -1);
+			
+			/*ui_do_label(&row, selected_server->player_names[i], font_size, -1);*/
+		}
+	}
+	
+	RECT button;
+	RECT types;
+	ui_hsplit_t(&filters, 20.0f, &button, &filters);
+	ui_do_label(&button, "Quick search: ", 14.0f, -1);
+	ui_vsplit_l(&button, 95.0f, 0, &button);
+	ui_do_edit_box(&config.b_filter_string, &button, config.b_filter_string, sizeof(config.b_filter_string), 14.0f);
+
+	ui_vsplit_l(&filters, 180.0f, &filters, &types);
+
+	// render filters
+	ui_hsplit_t(&filters, 20.0f, &button, &filters);
+	if (ui_do_button(&config.b_filter_empty, "Has people playing", config.b_filter_empty, &button, ui_draw_checkbox, 0))
+		config.b_filter_empty ^= 1;
+
+	ui_hsplit_t(&filters, 20.0f, &button, &filters);
+	if (ui_do_button(&config.b_filter_full, "Server not full", config.b_filter_full, &button, ui_draw_checkbox, 0))
+		config.b_filter_full ^= 1;
+
+	ui_hsplit_t(&filters, 20.0f, &button, &filters);
+	if (ui_do_button(&config.b_filter_pw, "No password", config.b_filter_pw, &button, ui_draw_checkbox, 0))
+		config.b_filter_pw ^= 1;
+
+	ui_hsplit_t(&filters, 20.0f, &button, &filters);
+	if (ui_do_button((char *)&config.b_filter_compatversion, "Compatible Version", config.b_filter_compatversion, &button, ui_draw_checkbox, 0))
+		config.b_filter_compatversion ^= 1;
+
+	// game types
+	/*
+	ui_hsplit_t(&types, 20.0f, &button, &types);
+	if (ui_do_button(&config.b_filter_gametype, "DM", config.b_filter_gametype&(1<<GAME_TYPE_DM), &button, ui_draw_checkbox, 0))
+		config.b_filter_gametype ^= (1<<GAME_TYPE_DM);
+
+	ui_hsplit_t(&types, 20.0f, &button, &types);
+	if (ui_do_button((char *)&config.b_filter_gametype + 1, "TDM", config.b_filter_gametype&(1<<GAME_TYPE_TDM), &button, ui_draw_checkbox, 0))
+		config.b_filter_gametype ^= (1<<GAME_TYPE_TDM);
+
+	ui_hsplit_t(&types, 20.0f, &button, &types);
+	if (ui_do_button((char *)&config.b_filter_gametype + 2, "CTF", config.b_filter_gametype&(1<<GAME_TYPE_CTF), &button, ui_draw_checkbox, 0))
+		config.b_filter_gametype ^= (1<<GAME_TYPE_CTF);
+	*/
+
+	// ping
+	ui_hsplit_t(&types, 2.0f, &button, &types);
+	ui_hsplit_t(&types, 20.0f, &button, &types);
+	{
+		RECT editbox;
+		ui_vsplit_l(&button, 40.0f, &editbox, &button);
+		ui_vsplit_l(&button, 5.0f, &button, &button);
+		
+		char buf[8];
+		str_format(buf, sizeof(buf), "%d", config.b_filter_ping);
+		ui_do_edit_box(&config.b_filter_ping, &editbox, buf, sizeof(buf), 14.0f);
+		config.b_filter_ping = atoi(buf);
+		
+		ui_do_label(&button, "Maximum ping", 14.0f, -1);
+	}
+
+
+	// render status
+	ui_draw_rect(&status, vec4(1,1,1,0.25f), CORNER_B, 5.0f);
+	ui_vmargin(&status, 50.0f, &status);
+	char buf[128];
+	str_format(buf, sizeof(buf), "%d of %d servers, %d players", client_serverbrowse_sorted_num(), client_serverbrowse_num(), num_players);
+	ui_do_label(&status, buf, 14.0f, -1);
+
+	// render toolbox
+	{
+		RECT buttons, button;
+		ui_hsplit_b(&toolbox, 25.0f, &toolbox, &buttons);
+
+		ui_vsplit_r(&buttons, 100.0f, &buttons, &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))
+			client_connect(config.ui_server_address);
+
+		ui_vsplit_r(&buttons, 20.0f, &buttons, &button);
+		ui_vsplit_r(&buttons, 100.0f, &buttons, &button);
+		ui_vmargin(&button, 2.0f, &button);
+		static int refresh_button = 0;
+		if(ui_do_button(&refresh_button, "Refresh", 0, &button, ui_draw_menu_button, 0))
+		{
+			if(config.ui_page == PAGE_INTERNET)
+				client_serverbrowse_refresh(0);
+			else if(config.ui_page == PAGE_LAN)
+				client_serverbrowse_refresh(1);
+		}
+
+		//ui_vsplit_r(&buttons, 30.0f, &buttons, &button);
+		ui_vsplit_l(&buttons, 120.0f, &button, &buttons);
+		static int clear_button = 0;
+		if(ui_do_button(&clear_button, "Reset Filter", 0, &button, ui_draw_menu_button, 0))
+		{
+			config.b_filter_full = 0;
+			config.b_filter_empty = 0;
+			config.b_filter_pw = 0;
+			config.b_filter_ping = 999;
+			config.b_filter_gametype = 0xf;
+			config.b_filter_compatversion = 1;
+			config.b_filter_string[0] = 0;
+		}
+
+		
+		ui_hsplit_t(&toolbox, 20.0f, &button, &toolbox);
+		ui_do_label(&button, "Host address:", 14.0f, -1);
+		ui_vsplit_l(&button, 100.0f, 0, &button);
+		ui_do_edit_box(&config.ui_server_address, &button, config.ui_server_address, sizeof(config.ui_server_address), 14.0f);
+	}
+}
diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp
new file mode 100644
index 00000000..f031c20f
--- /dev/null
+++ b/src/game/client/components/menus_settings.cpp
@@ -0,0 +1,567 @@
+
+#include <base/math.hpp>
+
+#include <string.h> // strcmp, strlen, strncpy
+#include <stdlib.h> // atoi
+
+extern "C" {
+	#include <engine/e_client_interface.h>
+	#include <engine/e_config.h>
+	#include <engine/client/ec_font.h>
+}
+
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_render.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/animstate.hpp>
+
+#include "binds.hpp"
+#include "menus.hpp"
+#include "skins.hpp"
+
+void MENUS::render_settings_player(RECT main_view)
+{
+	RECT button;
+	RECT skinselection;
+	ui_vsplit_l(&main_view, 300.0f, &main_view, &skinselection);
+
+
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+
+	// render settings
+	{	
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		ui_do_label(&button, "Name:", 14.0, -1);
+		ui_vsplit_l(&button, 80.0f, 0, &button);
+		ui_vsplit_l(&button, 180.0f, &button, 0);
+		ui_do_edit_box(config.player_name, &button, config.player_name, sizeof(config.player_name), 14.0f);
+
+		static int dynamic_camera_button = 0;
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		if(ui_do_button(&dynamic_camera_button, "Dynamic Camera", config.cl_mouse_deadzone != 0, &button, ui_draw_checkbox, 0))
+		{
+			
+			if(config.cl_mouse_deadzone)
+			{
+				config.cl_mouse_followfactor = 0;
+				config.cl_mouse_max_distance = 400;
+				config.cl_mouse_deadzone = 0;
+			}
+			else
+			{
+				config.cl_mouse_followfactor = 60;
+				config.cl_mouse_max_distance = 1000;
+				config.cl_mouse_deadzone = 300;
+			}
+		}
+
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		if (ui_do_button(&config.cl_autoswitch_weapons, "Switch weapon on pickup", config.cl_autoswitch_weapons, &button, ui_draw_checkbox, 0))
+			config.cl_autoswitch_weapons ^= 1;
+			
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		if (ui_do_button(&config.cl_nameplates, "Show name plates", config.cl_nameplates, &button, ui_draw_checkbox, 0))
+			config.cl_nameplates ^= 1;
+
+		//if(config.cl_nameplates)
+		{
+			ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+			ui_vsplit_l(&button, 15.0f, 0, &button);
+			if (ui_do_button(&config.cl_nameplates_always, "Always show name plates", config.cl_nameplates_always, &button, ui_draw_checkbox, 0))
+				config.cl_nameplates_always ^= 1;
+		}
+			
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		if (ui_do_button(&config.player_color_body, "Custom colors", config.player_use_custom_color, &button, ui_draw_checkbox, 0))
+			config.player_use_custom_color = config.player_use_custom_color?0:1;
+		
+		if(config.player_use_custom_color)
+		{
+			int *colors[2];
+			colors[0] = &config.player_color_body;
+			colors[1] = &config.player_color_feet;
+			
+			const char *parts[] = {"Body", "Feet"};
+			const char *labels[] = {"Hue", "Sat.", "Lht."};
+			static int color_slider[2][3] = {{0}};
+			//static float v[2][3] = {{0, 0.5f, 0.25f}, {0, 0.5f, 0.25f}};
+				
+			for(int i = 0; i < 2; i++)
+			{
+				RECT text;
+				ui_hsplit_t(&main_view, 20.0f, &text, &main_view);
+				ui_vsplit_l(&text, 15.0f, 0, &text);
+				ui_do_label(&text, parts[i], 14.0f, -1);
+				
+				int prevcolor = *colors[i];
+				int color = 0;
+				for(int s = 0; s < 3; s++)
+				{
+					RECT text;
+					ui_hsplit_t(&main_view, 19.0f, &button, &main_view);
+					ui_vsplit_l(&button, 30.0f, 0, &button);
+					ui_vsplit_l(&button, 30.0f, &text, &button);
+					ui_vsplit_r(&button, 5.0f, &button, 0);
+					ui_hsplit_t(&button, 4.0f, 0, &button);
+					
+					float k = ((prevcolor>>((2-s)*8))&0xff)  / 255.0f;
+					k = ui_do_scrollbar_h(&color_slider[i][s], &button, k);
+					color <<= 8;
+					color += clamp((int)(k*255), 0, 255);
+					ui_do_label(&text, labels[s], 15.0f, -1);
+					 
+				}
+				
+				*colors[i] = color;
+				ui_hsplit_t(&main_view, 5.0f, 0, &main_view);
+			}
+		}
+	}
+		
+	// draw header
+	RECT header, footer;
+	ui_hsplit_t(&skinselection, 20, &header, &skinselection);
+	ui_draw_rect(&header, vec4(1,1,1,0.25f), CORNER_T, 5.0f); 
+	ui_do_label(&header, "Skins", 18.0f, 0);
+
+	// draw footers	
+	ui_hsplit_b(&skinselection, 20, &skinselection, &footer);
+	ui_draw_rect(&footer, vec4(1,1,1,0.25f), CORNER_B, 5.0f); 
+	ui_vsplit_l(&footer, 10.0f, 0, &footer);
+
+	// modes
+	ui_draw_rect(&skinselection, vec4(0,0,0,0.15f), 0, 0);
+
+	RECT scroll;
+	ui_vsplit_r(&skinselection, 15, &skinselection, &scroll);
+
+	RECT list = skinselection;
+	ui_hsplit_t(&list, 50, &button, &list);
+	
+	int num = (int)(skinselection.h/button.h);
+	static float scrollvalue = 0;
+	static int scrollbar = 0;
+	ui_hmargin(&scroll, 5.0f, &scroll);
+	scrollvalue = ui_do_scrollbar_v(&scrollbar, &scroll, scrollvalue);
+
+	int start = (int)((gameclient.skins->num()-num)*scrollvalue);
+	if(start < 0)
+		start = 0;
+		
+	for(int i = start; i < start+num && i < gameclient.skins->num(); i++)
+	{
+		const SKINS::SKIN *s = gameclient.skins->get(i);
+		
+		// no special skins
+		if(s->name[0] == 'x' && s->name[1] == '_')
+		{
+			num++;
+			continue;
+		}
+		
+		char buf[128];
+		str_format(buf, sizeof(buf), "%s", s->name);
+		int selected = 0;
+		if(strcmp(s->name, config.player_skin) == 0)
+			selected = 1;
+		
+		TEE_RENDER_INFO info;
+		info.texture = s->org_texture;
+		info.color_body = vec4(1,1,1,1);
+		info.color_feet = vec4(1,1,1,1);
+		if(config.player_use_custom_color)
+		{
+			info.color_body = gameclient.skins->get_color(config.player_color_body);
+			info.color_feet = gameclient.skins->get_color(config.player_color_feet);
+			info.texture = s->color_texture;
+		}
+			
+		info.size = ui_scale()*50.0f;
+		
+		RECT icon;
+		RECT text;
+		ui_vsplit_l(&button, 50.0f, &icon, &text);
+		
+		if(ui_do_button(s, "", selected, &button, ui_draw_list_row, 0))
+			config_set_player_skin(&config, s->name);
+		
+		ui_hsplit_t(&text, 12.0f, 0, &text); // some margin from the top
+		ui_do_label(&text, buf, 18.0f, 0);
+		
+		ui_hsplit_t(&icon, 5.0f, 0, &icon); // some margin from the top
+		render_tee(ANIMSTATE::get_idle(), &info, 0, vec2(1, 0), vec2(icon.x+icon.w/2, icon.y+icon.h/2));
+		
+		if(config.debug)
+		{
+			gfx_texture_set(-1);
+			gfx_quads_begin();
+			gfx_setcolor(s->blood_color.r, s->blood_color.g, s->blood_color.b, 1.0f);
+			gfx_quads_drawTL(icon.x, icon.y, 12, 12);
+			gfx_quads_end();
+		}
+		
+		ui_hsplit_t(&list, 50, &button, &list);
+	}
+}
+
+typedef void (*assign_func_callback)(CONFIGURATION *config, int value);
+
+void MENUS::render_settings_controls(RECT main_view)
+{
+	RECT right_part;
+	ui_vsplit_l(&main_view, 300.0f, &main_view, &right_part);
+
+	{
+		RECT button, label;
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		ui_vsplit_l(&button, 110.0f, &label, &button);
+		ui_do_label(&label, "Mouse sens.", 14.0f, -1);
+		ui_hmargin(&button, 2.0f, &button);
+		config.inp_mousesens = (int)(ui_do_scrollbar_h(&config.inp_mousesens, &button, (config.inp_mousesens-5)/500.0f)*500.0f)+5;
+		//*key.key = ui_do_key_reader(key.key, &button, *key.key);
+		ui_hsplit_t(&main_view, 20.0f, 0, &main_view);
+	}
+	
+	typedef struct 
+	{
+		const char *name;
+		const char *command;
+		int keyid;
+	} KEYINFO;
+
+	KEYINFO keys[] = 
+	{
+		{ "Move Left:", "+left", 0},
+		{ "Move Right:", "+right", 0 },
+		{ "Jump:", "+jump", 0 },
+		{ "Fire:", "+fire", 0 },
+		{ "Hook:", "+hook", 0 },
+		{ "Hammer:", "+weapon1", 0 },
+		{ "Pistol:", "+weapon2", 0 },
+		{ "Shotgun:", "+weapon3", 0 },
+		{ "Grenade:", "+weapon4", 0 },
+		{ "Rifle:", "+weapon5", 0 },
+		{ "Next Weapon:", "+nextweapon", 0 },
+		{ "Prev. Weapon:", "+prevweapon", 0 },
+		{ "Emoticon:", "+emote", 0 },
+		{ "Chat:", "chat all", 0 },
+		{ "Team Chat:", "chat team", 0 },
+		{ "Console:", "toggle_local_console", 0 },
+		{ "Remote Console:", "toggle_remote_console", 0 },
+		{ "Screenshot:", "screenshot", 0 },
+	};
+
+	const int key_count = sizeof(keys) / sizeof(KEYINFO);
+	
+	// this is kinda slow, but whatever
+	for(int keyid = 0; keyid < KEY_LAST; keyid++)
+	{
+		const char *bind = gameclient.binds->get(keyid);
+		if(!bind[0])
+			continue;
+			
+		for(int i = 0; i < key_count; i++)
+			if(strcmp(bind, keys[i].command) == 0)
+			{
+				keys[i].keyid = keyid;
+				break;
+			}
+	}
+	
+	for (int i = 0; i < key_count; i++)
+    {
+		KEYINFO key = keys[i];
+    	RECT button, label;
+    	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+    	ui_vsplit_l(&button, 110.0f, &label, &button);
+    	
+		ui_do_label(&label, key.name, 14.0f, -1);
+		int oldid = key.keyid;
+		int newid = ui_do_key_reader((void *)keys[i].name, &button, oldid);
+		if(newid != oldid)
+		{
+			gameclient.binds->bind(oldid, "");
+			gameclient.binds->bind(newid, keys[i].command);
+		}
+    	ui_hsplit_t(&main_view, 5.0f, 0, &main_view);
+    }	
+    
+    // defaults
+	RECT button;
+	ui_hsplit_b(&right_part, 25.0f, &right_part, &button);
+	ui_vsplit_l(&button, 50.0f, 0, &button);
+	static int default_button = 0;
+	if (ui_do_button((void*)&default_button, "Reset to defaults", 0, &button, ui_draw_menu_button, 0))
+		gameclient.binds->set_defaults();
+}
+
+void MENUS::render_settings_graphics(RECT main_view)
+{
+	RECT button;
+	char buf[128];
+	
+	static const int MAX_RESOLUTIONS = 256;
+	static VIDEO_MODE modes[MAX_RESOLUTIONS];
+	static int num_modes = -1;
+	
+	if(num_modes == -1)
+		num_modes = gfx_get_video_modes(modes, MAX_RESOLUTIONS);
+	
+	RECT modelist;
+	ui_vsplit_l(&main_view, 300.0f, &main_view, &modelist);
+	
+	// draw allmodes switch
+	RECT header, footer;
+	ui_hsplit_t(&modelist, 20, &button, &modelist);
+	if (ui_do_button(&config.gfx_display_all_modes, "Show only supported", config.gfx_display_all_modes^1, &button, ui_draw_checkbox, 0))
+	{
+		config.gfx_display_all_modes ^= 1;
+		num_modes = gfx_get_video_modes(modes, MAX_RESOLUTIONS);
+	}
+	
+	// draw header
+	ui_hsplit_t(&modelist, 20, &header, &modelist);
+	ui_draw_rect(&header, vec4(1,1,1,0.25f), CORNER_T, 5.0f); 
+	ui_do_label(&header, "Display Modes", 14.0f, 0);
+
+	// draw footers	
+	ui_hsplit_b(&modelist, 20, &modelist, &footer);
+	str_format(buf, sizeof(buf), "Current: %dx%d %d bit", config.gfx_screen_width, config.gfx_screen_height, config.gfx_color_depth);
+	ui_draw_rect(&footer, vec4(1,1,1,0.25f), CORNER_B, 5.0f); 
+	ui_vsplit_l(&footer, 10.0f, 0, &footer);
+	ui_do_label(&footer, buf, 14.0f, -1);
+
+	// modes
+	ui_draw_rect(&modelist, vec4(0,0,0,0.15f), 0, 0);
+
+	RECT scroll;
+	ui_vsplit_r(&modelist, 15, &modelist, &scroll);
+
+	RECT list = modelist;
+	ui_hsplit_t(&list, 20, &button, &list);
+	
+	int num = (int)(modelist.h/button.h);
+	static float scrollvalue = 0;
+	static int scrollbar = 0;
+	ui_hmargin(&scroll, 5.0f, &scroll);
+	scrollvalue = ui_do_scrollbar_v(&scrollbar, &scroll, scrollvalue);
+
+	int start = (int)((num_modes-num)*scrollvalue);
+	if(start < 0)
+		start = 0;
+		
+	for(int i = start; i < start+num && i < num_modes; i++)
+	{
+		int depth = modes[i].red+modes[i].green+modes[i].blue;
+		if(depth < 16)
+			depth = 16;
+		else if(depth > 16)
+			depth = 24;
+			
+		int selected = 0;
+		if(config.gfx_color_depth == depth &&
+			config.gfx_screen_width == modes[i].width &&
+			config.gfx_screen_height == modes[i].height)
+		{
+			selected = 1;
+		}
+		
+		str_format(buf, sizeof(buf), "  %dx%d %d bit", modes[i].width, modes[i].height, depth);
+		if(ui_do_button(&modes[i], buf, selected, &button, ui_draw_list_row, 0))
+		{
+			config.gfx_color_depth = depth;
+			config.gfx_screen_width = modes[i].width;
+			config.gfx_screen_height = modes[i].height;
+			if(!selected)
+				need_restart = true;
+		}
+		
+		ui_hsplit_t(&list, 20, &button, &list);
+	}
+	
+	
+	// switches
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.gfx_fullscreen, "Fullscreen", config.gfx_fullscreen, &button, ui_draw_checkbox, 0))
+	{
+		config.gfx_fullscreen ^= 1;
+		need_restart = true;
+	}
+
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.gfx_vsync, "V-Sync", config.gfx_vsync, &button, ui_draw_checkbox, 0))
+		config.gfx_vsync ^= 1;
+
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.gfx_fsaa_samples, "FSAA samples", config.gfx_fsaa_samples, &button, ui_draw_checkbox_number, 0))
+	{
+		if(config.gfx_fsaa_samples < 2) config.gfx_fsaa_samples = 2;
+		else if(config.gfx_fsaa_samples < 4) config.gfx_fsaa_samples = 4;
+		else if(config.gfx_fsaa_samples < 6) config.gfx_fsaa_samples = 6;
+		else if(config.gfx_fsaa_samples < 8) config.gfx_fsaa_samples = 8;
+		else if(config.gfx_fsaa_samples < 16) config.gfx_fsaa_samples = 16;
+		else if(config.gfx_fsaa_samples >= 16) config.gfx_fsaa_samples = 0;
+		need_restart = true;
+	}
+		
+	ui_hsplit_t(&main_view, 40.0f, &button, &main_view);
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.gfx_texture_quality, "Quality Textures", config.gfx_texture_quality, &button, ui_draw_checkbox, 0))
+	{
+		config.gfx_texture_quality ^= 1;
+		need_restart = true;
+	}
+
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.gfx_texture_compression, "Texture Compression", config.gfx_texture_compression, &button, ui_draw_checkbox, 0))
+	{
+		config.gfx_texture_compression ^= 1;
+		need_restart = true;
+	}
+
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.gfx_high_detail, "High Detail", config.gfx_high_detail, &button, ui_draw_checkbox, 0))
+		config.gfx_high_detail ^= 1;
+
+	//
+	
+	RECT text;
+	ui_hsplit_t(&main_view, 20.0f, 0, &main_view);
+	ui_hsplit_t(&main_view, 20.0f, &text, &main_view);
+	//ui_vsplit_l(&text, 15.0f, 0, &text);
+	ui_do_label(&text, "UI Color", 14.0f, -1);
+	
+	const char *labels[] = {"Hue", "Sat.", "Lht.", "Alpha"};
+	int *color_slider[4] = {&config.ui_color_hue, &config.ui_color_sat, &config.ui_color_lht, &config.ui_color_alpha};
+	for(int s = 0; s < 4; s++)
+	{
+		RECT text;
+		ui_hsplit_t(&main_view, 19.0f, &button, &main_view);
+		ui_vmargin(&button, 15.0f, &button);
+		ui_vsplit_l(&button, 30.0f, &text, &button);
+		ui_vsplit_r(&button, 5.0f, &button, 0);
+		ui_hsplit_t(&button, 4.0f, 0, &button);
+		
+		float k = (*color_slider[s]) / 255.0f;
+		k = ui_do_scrollbar_h(color_slider[s], &button, k);
+		*color_slider[s] = (int)(k*255.0f);
+		ui_do_label(&text, labels[s], 15.0f, -1);
+	}		
+}
+
+void MENUS::render_settings_sound(RECT main_view)
+{
+	RECT button;
+	ui_vsplit_l(&main_view, 300.0f, &main_view, 0);
+	
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.snd_enable, "Use Sounds", config.snd_enable, &button, ui_draw_checkbox, 0))
+	{
+		config.snd_enable ^= 1;
+		need_restart = true;
+	}
+	
+	if(!config.snd_enable)
+		return;
+	
+	ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+	if (ui_do_button(&config.snd_nonactive_mute, "Mute when not active", config.snd_nonactive_mute, &button, ui_draw_checkbox, 0))
+		config.snd_nonactive_mute ^= 1;
+		
+	// sample rate box
+	{
+		char buf[64];
+		str_format(buf, sizeof(buf), "%d", config.snd_rate);
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		ui_do_label(&button, "Sample Rate", 14.0f, -1);
+		ui_vsplit_l(&button, 110.0f, 0, &button);
+		ui_vsplit_l(&button, 180.0f, &button, 0);
+		ui_do_edit_box(&config.snd_rate, &button, buf, sizeof(buf), 14.0f);
+		int before = config.snd_rate;
+		config.snd_rate = atoi(buf);
+		
+		if(config.snd_rate != before)
+			need_restart = true;
+
+		if(config.snd_rate < 1)
+			config.snd_rate = 1;
+	}
+	
+	// volume slider
+	{
+		RECT button, label;
+		ui_hsplit_t(&main_view, 5.0f, &button, &main_view);
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		ui_vsplit_l(&button, 110.0f, &label, &button);
+		ui_hmargin(&button, 2.0f, &button);
+		ui_do_label(&label, "Sound Volume", 14.0f, -1);
+		config.snd_volume = (int)(ui_do_scrollbar_h(&config.snd_volume, &button, config.snd_volume/100.0f)*100.0f);
+		ui_hsplit_t(&main_view, 20.0f, 0, &main_view);
+	}
+}
+
+
+	/*
+static void menu2_render_settings_network(RECT main_view)
+{
+	RECT button;
+	ui_vsplit_l(&main_view, 300.0f, &main_view, 0);
+	
+	{
+		ui_hsplit_t(&main_view, 20.0f, &button, &main_view);
+		ui_do_label(&button, "Rcon Password", 14.0, -1);
+		ui_vsplit_l(&button, 110.0f, 0, &button);
+		ui_vsplit_l(&button, 180.0f, &button, 0);
+		ui_do_edit_box(&config.rcon_password, &button, config.rcon_password, sizeof(config.rcon_password), true);
+	}
+}*/
+
+void MENUS::render_settings(RECT main_view)
+{
+	static int settings_page = 0;
+	
+	// render background
+	RECT temp, tabbar;
+	ui_vsplit_r(&main_view, 120.0f, &main_view, &tabbar);
+	ui_draw_rect(&main_view, color_tabbar_active, CORNER_B|CORNER_TL, 10.0f);
+	ui_hsplit_t(&tabbar, 50.0f, &temp, &tabbar);
+	ui_draw_rect(&temp, color_tabbar_active, CORNER_R, 10.0f);
+	
+	ui_hsplit_t(&main_view, 10.0f, 0, &main_view);
+	
+	RECT button;
+	
+	const char *tabs[] = {"Player", "Controls", "Graphics", "Sound"};
+	int num_tabs = (int)(sizeof(tabs)/sizeof(*tabs));
+
+	for(int i = 0; i < num_tabs; i++)
+	{
+		ui_hsplit_t(&tabbar, 10, &button, &tabbar);
+		ui_hsplit_t(&tabbar, 26, &button, &tabbar);
+		if(ui_do_button(tabs[i], tabs[i], settings_page == i, &button, ui_draw_settings_tab_button, 0))
+			settings_page = i;
+	}
+	
+	ui_margin(&main_view, 10.0f, &main_view);
+	
+	if(settings_page == 0)
+		render_settings_player(main_view);
+	else if(settings_page == 1)
+		render_settings_controls(main_view);
+	else if(settings_page == 2)
+		render_settings_graphics(main_view);
+	else if(settings_page == 3)
+		render_settings_sound(main_view);
+
+	if(need_restart)
+	{
+		RECT restart_warning;
+		ui_hsplit_b(&main_view, 40, &main_view, &restart_warning);
+		ui_do_label(&restart_warning, "You must restart the game for all settings to take effect.", 15.0f, -1, 220);
+	}
+}
diff --git a/src/game/client/components/motd.cpp b/src/game/client/components/motd.cpp
new file mode 100644
index 00000000..bd04d089
--- /dev/null
+++ b/src/game/client/components/motd.cpp
@@ -0,0 +1,75 @@
+#include <engine/e_client_interface.h>
+#include <engine/e_config.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/client/gameclient.hpp>
+//#include <game/client/gc_anim.hpp>
+#include <game/client/gc_client.hpp>
+
+#include "motd.hpp"
+	
+void MOTD::on_reset()
+{
+	server_motd_time = 0;
+}
+
+void MOTD::on_render()
+{
+	float width = 400*3.0f*gfx_screenaspect();
+	float height = 400*3.0f;
+	
+	// TODO: repair me
+	if(/* !do_scoreboard && */ time_get() < server_motd_time)
+	{
+		gfx_mapscreen(0, 0, width, height);
+		
+		float h = 800.0f;
+		float w = 650.0f;
+		float x = width/2 - w/2;
+		float y = 150.0f;
+
+		gfx_blend_normal();
+		gfx_texture_set(-1);
+		gfx_quads_begin();
+		gfx_setcolor(0,0,0,0.5f);
+		draw_round_rect(x, y, w, h, 40.0f);
+		gfx_quads_end();
+
+		gfx_text(0, x+40.0f, y+40.0f, 32.0f, server_motd, (int)(w-80.0f));
+	}
+}
+
+void MOTD::on_message(int msgtype, void *rawmsg)
+{
+	if(msgtype == NETMSGTYPE_SV_MOTD)
+	{
+		NETMSG_SV_MOTD *msg = (NETMSG_SV_MOTD *)rawmsg;
+
+		// process escaping			
+		str_copy(server_motd, msg->message, sizeof(server_motd));
+		for(int i = 0; server_motd[i]; i++)
+		{
+			if(server_motd[i] == '\\')
+			{
+				if(server_motd[i+1] == 'n')
+				{
+					server_motd[i] = ' ';
+					server_motd[i+1] = '\n';
+					i++;
+				}
+			}
+		}
+
+		if(server_motd[0] && config.cl_motd_time)
+			server_motd_time = time_get()+time_freq()*config.cl_motd_time;
+		else
+			server_motd_time = 0;
+	}
+}
+
+bool MOTD::on_input(INPUT_EVENT e)
+{
+	return false;
+}
+
diff --git a/src/game/client/components/motd.hpp b/src/game/client/components/motd.hpp
new file mode 100644
index 00000000..e26dff9b
--- /dev/null
+++ b/src/game/client/components/motd.hpp
@@ -0,0 +1,15 @@
+#include <game/client/component.hpp>
+
+class MOTD : public COMPONENT
+{
+public:
+	// motd
+	int64 server_motd_time;
+	char server_motd[900]; // FUGLY
+	
+	virtual void on_reset();
+	virtual void on_render();
+	virtual void on_message(int msgtype, void *rawmsg);
+	virtual bool on_input(INPUT_EVENT e);
+};
+
diff --git a/src/game/client/components/particles.cpp b/src/game/client/components/particles.cpp
new file mode 100644
index 00000000..0c449263
--- /dev/null
+++ b/src/game/client/components/particles.cpp
@@ -0,0 +1,155 @@
+#include <base/math.hpp>
+#include <game/generated/gc_data.hpp>
+#include <game/client/gc_render.hpp>
+#include "particles.hpp"
+
+PARTICLES::PARTICLES()
+{
+	on_reset();
+	render_trail.parts = this;
+	render_explosions.parts = this;
+	render_general.parts = this;
+}
+
+
+void PARTICLES::on_reset()
+{
+	// reset particles
+	for(int i = 0; i < MAX_PARTICLES; i++)
+	{
+		particles[i].prev_part = i-1;
+		particles[i].next_part = i+1;
+	}
+	
+	particles[0].prev_part = 0;
+	particles[MAX_PARTICLES-1].next_part = -1;
+	first_free = 0;
+
+	for(int i = 0; i < NUM_GROUPS; i++)
+		first_part[i] = -1;
+}
+
+void PARTICLES::add(int group, PARTICLE *part)
+{
+	if (first_free == -1)
+		return;
+		
+	// remove from the free list
+	int id = first_free;
+	first_free = particles[id].next_part;
+	particles[first_free].prev_part = -1;
+	
+	// copy data
+	particles[id] = *part;
+	
+	// insert to the group list
+	particles[id].prev_part = -1;
+	particles[id].next_part = first_part[group];
+	if(first_part[group] != -1)
+		particles[first_part[group]].prev_part = id;
+	first_part[group] = id;
+	
+	// set some parameters
+	particles[id].life = 0;
+}
+
+void PARTICLES::update(float time_passed)
+{
+	static float friction_fraction = 0;
+	friction_fraction += time_passed;
+
+	if(friction_fraction > 2.0f) // safty messure
+		friction_fraction = 0;
+	
+	int friction_count = 0;
+	while(friction_fraction > 0.05f)
+	{
+		friction_count++;
+		friction_fraction -= 0.05f;
+	}
+	
+	for(int g = 0; g < NUM_GROUPS; g++)
+	{
+		int i = first_part[g];
+		while(i != -1)
+		{
+			int next = particles[i].next_part;
+			//particles[i].vel += flow_get(particles[i].pos)*time_passed * particles[i].flow_affected;
+			particles[i].vel.y += particles[i].gravity*time_passed;
+			
+			for(int f = 0; f < friction_count; f++) // apply friction
+				particles[i].vel *= particles[i].friction;
+			
+			// move the point
+			vec2 vel = particles[i].vel*time_passed;
+			
+			// TODO: repair me
+			//move_point(&particles[i].pos, &vel, 0.1f+0.9f*frandom(), NULL);
+			particles[i].vel = vel* (1.0f/time_passed);
+			
+			particles[i].life += time_passed;
+			particles[i].rot += time_passed * particles[i].rotspeed;
+
+			// check particle death
+			if(particles[i].life > particles[i].life_span)
+			{
+				// remove it from the group list
+				if(particles[i].prev_part != -1)
+					particles[particles[i].prev_part].next_part = particles[i].next_part;
+				else
+					first_part[g] = particles[i].next_part;
+					
+				if(particles[i].next_part != -1)
+					particles[particles[i].next_part].prev_part = particles[i].prev_part;
+					
+				// insert to the free list
+				if(first_free != -1)
+					particles[first_free].prev_part = i;
+				particles[i].prev_part = -1;
+				particles[i].next_part = first_free;
+				first_free = i;
+			}
+			
+			i = next;
+		}
+	}
+}
+
+void PARTICLES::on_render()
+{
+	static int64 lasttime = 0;
+	int64 t = time_get();
+	update((float)((t-lasttime)/(double)time_freq()));
+	lasttime = t;
+}
+
+void PARTICLES::render_group(int group)
+{
+	gfx_blend_normal();
+	//gfx_blend_additive();
+	gfx_texture_set(data->images[IMAGE_PARTICLES].id);
+	gfx_quads_begin();
+
+	int i = first_part[group];
+	while(i != -1)
+	{
+		select_sprite(particles[i].spr);
+		float a = particles[i].life / particles[i].life_span;
+		vec2 p = particles[i].pos;
+		float size = mix(particles[i].start_size, particles[i].end_size, a);
+
+		gfx_quads_setrotation(particles[i].rot);
+
+		gfx_setcolor(
+			particles[i].color.r,
+			particles[i].color.g,
+			particles[i].color.b,
+			particles[i].color.a); // pow(a, 0.75f) * 
+
+		gfx_quads_draw(p.x, p.y, size, size);
+		
+		i = particles[i].next_part;
+	}
+	gfx_quads_end();
+	gfx_blend_normal();
+}
diff --git a/src/game/client/components/particles.hpp b/src/game/client/components/particles.hpp
new file mode 100644
index 00000000..6c466d94
--- /dev/null
+++ b/src/game/client/components/particles.hpp
@@ -0,0 +1,91 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+// particles
+struct PARTICLE
+{
+	void set_default()
+	{
+		vel = vec2(0,0);
+		life_span = 0;
+		start_size = 32;
+		end_size = 32;
+		rot = 0;
+		rotspeed = 0;
+		gravity = 0;
+		friction = 0;
+		flow_affected = 1.0f;
+		color = vec4(1,1,1,1);
+	}
+	
+	vec2 pos;
+	vec2 vel;
+
+	int spr;
+
+	float flow_affected;
+
+	float life_span;
+	
+	float start_size;
+	float end_size;
+
+	float rot;
+	float rotspeed;
+
+	float gravity;
+	float friction;
+
+	vec4 color;
+	
+	// set by the particle system
+	float life;
+	int prev_part;
+	int next_part;
+};
+
+class PARTICLES : public COMPONENT
+{
+	friend class GAMECLIENT;
+public:
+	enum
+	{
+		GROUP_PROJECTILE_TRAIL=0,
+		GROUP_EXPLOSIONS,
+		GROUP_GENERAL,
+		NUM_GROUPS
+	};
+
+	PARTICLES();
+	
+	void add(int group, PARTICLE *part);
+	
+	virtual void on_reset();
+	virtual void on_render();
+
+private:
+	
+	enum
+	{
+		MAX_PARTICLES=1024*8,
+	};
+
+	PARTICLE particles[MAX_PARTICLES];
+	int first_free;
+	int first_part[NUM_GROUPS];
+	
+	void render_group(int group);
+	void update(float time_passed);
+
+	template<int TGROUP>
+	class RENDER_GROUP : public COMPONENT
+	{
+	public:
+		PARTICLES *parts;
+		virtual void on_render() { parts->render_group(TGROUP); }
+	};
+	
+	RENDER_GROUP<GROUP_PROJECTILE_TRAIL> render_trail;
+	RENDER_GROUP<GROUP_EXPLOSIONS> render_explosions;
+	RENDER_GROUP<GROUP_GENERAL> render_general;
+};
diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp
new file mode 100644
index 00000000..3178b82f
--- /dev/null
+++ b/src/game/client/components/players.cpp
@@ -0,0 +1,463 @@
+
+extern "C" {
+	#include <engine/e_config.h>
+}
+
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include <game/gamecore.hpp> // get_angle
+#include <game/client/animstate.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/gc_client.hpp>
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_render.hpp>
+
+#include <game/client/components/flow.hpp>
+#include <game/client/components/skins.hpp>
+#include <game/client/components/effects.hpp>
+
+#include "players.hpp"
+
+void PLAYERS::render_hand(TEE_RENDER_INFO *info, vec2 center_pos, vec2 dir, float angle_offset, vec2 post_rot_offset)
+{
+	// for drawing hand
+	//const skin *s = skin_get(skin_id);
+	
+	float basesize = 10.0f;
+	//dir = normalize(hook_pos-pos);
+
+	vec2 hand_pos = center_pos + dir;
+	float angle = get_angle(dir);
+	if (dir.x < 0)
+		angle -= angle_offset;
+	else
+		angle += angle_offset;
+
+	vec2 dirx = dir;
+	vec2 diry(-dir.y,dir.x);
+
+	if (dir.x < 0)
+		diry = -diry;
+
+	hand_pos += dirx * post_rot_offset.x;
+	hand_pos += diry * post_rot_offset.y;
+
+	//gfx_texture_set(data->images[IMAGE_CHAR_DEFAULT].id);
+	gfx_texture_set(info->texture);
+	gfx_quads_begin();
+	gfx_setcolor(info->color_body.r, info->color_body.g, info->color_body.b, info->color_body.a);
+
+	// two passes
+	for (int i = 0; i < 2; i++)
+	{
+		bool outline = i == 0;
+
+		select_sprite(outline?SPRITE_TEE_HAND_OUTLINE:SPRITE_TEE_HAND, 0, 0, 0);
+		gfx_quads_setrotation(angle);
+		gfx_quads_draw(hand_pos.x, hand_pos.y, 2*basesize, 2*basesize);
+	}
+
+	gfx_quads_setrotation(0);
+	gfx_quads_end();
+}
+
+void PLAYERS::render_player(
+	const NETOBJ_CHARACTER *prev_char,
+	const NETOBJ_CHARACTER *player_char,
+	const NETOBJ_PLAYER_INFO *prev_info,
+	const NETOBJ_PLAYER_INFO *player_info
+	)
+{
+	NETOBJ_CHARACTER prev;
+	NETOBJ_CHARACTER player;
+	prev = *prev_char;
+	player = *player_char;
+
+	NETOBJ_PLAYER_INFO info = *player_info;
+	TEE_RENDER_INFO render_info = gameclient.clients[info.cid].render_info;
+
+	// check for teamplay modes
+	bool is_teamplay = false;
+	if(gameclient.snap.gameobj)
+		is_teamplay = gameclient.snap.gameobj->flags&GAMEFLAG_TEAMS != 0;
+
+	// check for ninja	
+	if (player.weapon == WEAPON_NINJA)
+	{
+		// change the skin for the player to the ninja
+		int skin = gameclient.skins->find("x_ninja");
+		if(skin != -1)
+		{
+			if(is_teamplay)
+				render_info.texture = gameclient.skins->get(skin)->color_texture;
+			else
+			{
+				render_info.texture = gameclient.skins->get(skin)->org_texture;
+				render_info.color_body = vec4(1,1,1,1);
+				render_info.color_feet = vec4(1,1,1,1);
+			}
+		}	
+	}
+	
+	// set size
+	render_info.size = 64.0f;
+
+	float intratick = client_intratick();
+	float ticktime = client_ticktime();
+	
+	if(player.health < 0) // dont render dead players
+		return;
+
+	//float angle = mix((float)prev.angle, (float)player.angle, intratick)/256.0f;
+	
+	// TODO: fix this good!
+	float mixspeed = 0.05f;
+	if(player.attacktick != prev.attacktick)
+		mixspeed = 0.1f;
+	
+	float angle = mix(gameclient.clients[info.cid].angle, player.angle/256.0f, mixspeed);
+	gameclient.clients[info.cid].angle = angle;
+	vec2 direction = get_direction((int)(angle*256.0f));
+	
+	if(info.local && config.cl_predict)
+	{
+		if(!gameclient.snap.local_character || (gameclient.snap.local_character->health < 0) || (gameclient.snap.gameobj && gameclient.snap.gameobj->game_over))
+		{
+		}
+		else
+		{
+			// apply predicted results
+			predicted_char.write(&player);
+			predicted_prev_char.write(&prev);
+			intratick = client_predintratick();
+		}
+	}
+
+	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);
+	
+	gameclient.flow->add(position, vel*100.0f, 10.0f);
+	
+	render_info.got_airjump = player.jumped&2?0:1;
+
+	if(prev.health < 0) // Don't flicker from previous position
+		position = vec2(player.x, player.y);
+
+	bool stationary = player.vx < 1 && player.vx > -1;
+	bool inair = col_check_point(player.x, player.y+16) == 0;
+	bool want_other_dir = (player.wanted_direction == -1 && vel.x > 0) || (player.wanted_direction == 1 && vel.x < 0);
+
+	// evaluate animation
+	float walk_time = fmod(position.x, 100.0f)/100.0f;
+	ANIMSTATE state;
+	state.set(&data->animations[ANIM_BASE], 0);
+
+	if(inair)
+		state.add(&data->animations[ANIM_INAIR], 0, 1.0f); // TODO: some sort of time here
+	else if(stationary)
+		state.add(&data->animations[ANIM_IDLE], 0, 1.0f); // TODO: some sort of time here
+	else if(!want_other_dir)
+		state.add(&data->animations[ANIM_WALK], walk_time, 1.0f);
+
+	if (player.weapon == WEAPON_HAMMER)
+	{
+		float a = clamp((client_tick()-player.attacktick+ticktime)/10.0f, 0.0f, 1.0f);
+		state.add(&data->animations[ANIM_HAMMER_SWING], a, 1.0f);
+	}
+	if (player.weapon == WEAPON_NINJA)
+	{
+		float a = clamp((client_tick()-player.attacktick+ticktime)/40.0f, 0.0f, 1.0f);
+		state.add(&data->animations[ANIM_NINJA_SWING], a, 1.0f);
+	}
+	
+	// do skidding
+	if(!inair && want_other_dir && length(vel*50) > 500.0f)
+	{
+		static int64 skid_sound_time = 0;
+		if(time_get()-skid_sound_time > time_freq()/10)
+		{
+			snd_play_random(CHN_WORLD, SOUND_PLAYER_SKID, 0.25f, position);
+			skid_sound_time = time_get();
+		}
+		
+		gameclient.effects->skidtrail(
+			position+vec2(-player.wanted_direction*6,12),
+			vec2(-player.wanted_direction*100*length(vel),-50)
+		);
+	}
+
+	// draw hook
+	if (prev.hook_state>0 && player.hook_state>0)
+	{
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+		//gfx_quads_begin();
+
+		vec2 pos = position;
+		vec2 hook_pos;
+		
+		if(player_char->hooked_player != -1)
+		{
+			if(gameclient.snap.local_info && player_char->hooked_player == gameclient.snap.local_info->cid)
+			{
+				hook_pos = mix(vec2(predicted_prev_char.pos.x, predicted_prev_char.pos.y),
+					vec2(predicted_char.pos.x, predicted_char.pos.y), client_predintratick());
+			}
+			else
+				hook_pos = mix(vec2(prev_char->hook_x, prev_char->hook_y), vec2(player_char->hook_x, player_char->hook_y), client_intratick());
+		}
+		else
+			hook_pos = mix(vec2(prev.hook_x, prev.hook_y), vec2(player.hook_x, player.hook_y), intratick);
+
+		float d = distance(pos, hook_pos);
+		vec2 dir = normalize(pos-hook_pos);
+
+		gfx_quads_setrotation(get_angle(dir)+pi);
+
+		// render head
+		select_sprite(SPRITE_HOOK_HEAD);
+		gfx_quads_draw(hook_pos.x, hook_pos.y, 24,16);
+
+		// render chain
+		select_sprite(SPRITE_HOOK_CHAIN);
+		int i = 0;
+		for(float f = 24; f < d && i < 1024; f += 24, i++)
+		{
+			vec2 p = hook_pos + dir*f;
+			gfx_quads_draw(p.x, p.y,24,16);
+		}
+
+		gfx_quads_setrotation(0);
+		gfx_quads_end();
+
+		render_hand(&render_info, position, normalize(hook_pos-pos), -pi/2, vec2(20, 0));
+	}
+
+	// draw gun
+	{
+		gfx_texture_set(data->images[IMAGE_GAME].id);
+		gfx_quads_begin();
+		gfx_quads_setrotation(state.attach.angle*pi*2+angle);
+
+		// normal weapons
+		int iw = clamp(player.weapon, 0, NUM_WEAPONS-1);
+		select_sprite(data->weapons.id[iw].sprite_body, direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0);
+
+		vec2 dir = direction;
+		float recoil = 0.0f;
+		vec2 p;
+		if (player.weapon == WEAPON_HAMMER)
+		{
+			// Static position for hammer
+			p = position + vec2(state.attach.x, state.attach.y);
+			p.y += data->weapons.id[iw].offsety;
+			// if attack is under way, bash stuffs
+			if(direction.x < 0)
+			{
+				gfx_quads_setrotation(-pi/2-state.attach.angle*pi*2);
+				p.x -= data->weapons.id[iw].offsetx;
+			}
+			else
+			{
+				gfx_quads_setrotation(-pi/2+state.attach.angle*pi*2);
+			}
+			draw_sprite(p.x, p.y, data->weapons.id[iw].visual_size);
+		}
+		else if (player.weapon == WEAPON_NINJA)
+		{
+			p = position;
+			p.y += data->weapons.id[iw].offsety;
+
+			if(direction.x < 0)
+			{
+				gfx_quads_setrotation(-pi/2-state.attach.angle*pi*2);
+				p.x -= data->weapons.id[iw].offsetx;
+				gameclient.effects->powerupshine(p+vec2(32,0), vec2(32,12));
+			}
+			else
+			{
+				gfx_quads_setrotation(-pi/2+state.attach.angle*pi*2);
+				gameclient.effects->powerupshine(p-vec2(32,0), vec2(32,12));
+			}
+			draw_sprite(p.x, p.y, data->weapons.id[iw].visual_size);
+
+			// HADOKEN
+			if ((client_tick()-player.attacktick) <= (SERVER_TICK_SPEED / 6) && data->weapons.id[iw].num_sprite_muzzles)
+			{
+				int itex = rand() % data->weapons.id[iw].num_sprite_muzzles;
+				float alpha = 1.0f;
+				if (alpha > 0.0f && data->weapons.id[iw].sprite_muzzles[itex])
+				{
+					vec2 dir = vec2(player_char->x,player_char->y) - vec2(prev_char->x, prev_char->y);
+					dir = normalize(dir);
+					float hadokenangle = get_angle(dir);
+					gfx_quads_setrotation(hadokenangle);
+					//float offsety = -data->weapons[iw].muzzleoffsety;
+					select_sprite(data->weapons.id[iw].sprite_muzzles[itex], 0);
+					vec2 diry(-dir.y,dir.x);
+					p = position;
+					float offsetx = data->weapons.id[iw].muzzleoffsetx;
+					p -= dir * offsetx;
+					draw_sprite(p.x, p.y, 160.0f);
+				}
+			}
+		}
+		else
+		{
+			// TODO: should be an animation
+			recoil = 0;
+			float a = (client_tick()-player.attacktick+intratick)/5.0f;
+			if(a < 1)
+				recoil = sinf(a*pi);
+			p = position + dir * data->weapons.id[iw].offsetx - dir*recoil*10.0f;
+			p.y += data->weapons.id[iw].offsety;
+			draw_sprite(p.x, p.y, data->weapons.id[iw].visual_size);
+		}
+
+		if (player.weapon == WEAPON_GUN || player.weapon == WEAPON_SHOTGUN)
+		{
+			// check if we're firing stuff
+			if(data->weapons.id[iw].num_sprite_muzzles)//prev.attackticks)
+			{
+				float alpha = 0.0f;
+				int phase1tick = (client_tick() - player.attacktick);
+				if (phase1tick < (data->weapons.id[iw].muzzleduration + 3))
+				{
+					float t = ((((float)phase1tick) + intratick)/(float)data->weapons.id[iw].muzzleduration);
+					alpha = LERP(2.0, 0.0f, min(1.0f,max(0.0f,t)));
+				}
+
+				int itex = rand() % data->weapons.id[iw].num_sprite_muzzles;
+				if (alpha > 0.0f && data->weapons.id[iw].sprite_muzzles[itex])
+				{
+					float offsety = -data->weapons.id[iw].muzzleoffsety;
+					select_sprite(data->weapons.id[iw].sprite_muzzles[itex], direction.x < 0 ? SPRITE_FLAG_FLIP_Y : 0);
+					if(direction.x < 0)
+						offsety = -offsety;
+
+					vec2 diry(-dir.y,dir.x);
+					vec2 muzzlepos = p + dir * data->weapons.id[iw].muzzleoffsetx + diry * offsety;
+
+					draw_sprite(muzzlepos.x, muzzlepos.y, data->weapons.id[iw].visual_size);
+				}
+			}
+		}
+		gfx_quads_end();
+
+		switch (player.weapon)
+		{
+			case WEAPON_GUN: render_hand(&render_info, p, direction, -3*pi/4, vec2(-15, 4)); break;
+			case WEAPON_SHOTGUN: render_hand(&render_info, p, direction, -pi/2, vec2(-5, 4)); break;
+			case WEAPON_GRENADE: render_hand(&render_info, p, direction, -pi/2, vec2(-4, 7)); break;
+		}
+
+	}
+
+	// render the "shadow" tee
+	if(info.local && config.debug)
+	{
+		vec2 ghost_position = mix(vec2(prev_char->x, prev_char->y), vec2(player_char->x, player_char->y), client_intratick());
+		TEE_RENDER_INFO ghost = render_info;
+		ghost.color_body.a = 0.5f;
+		ghost.color_feet.a = 0.5f;
+		render_tee(&state, &ghost, player.emote, direction, ghost_position); // render ghost
+	}
+
+	render_info.size = 64.0f; // force some settings
+	render_info.color_body.a = 1.0f;
+	render_info.color_feet.a = 1.0f;
+	render_tee(&state, &render_info, player.emote, direction, position);
+
+	if(player.player_state == PLAYERSTATE_CHATTING)
+	{
+		gfx_texture_set(data->images[IMAGE_EMOTICONS].id);
+		gfx_quads_begin();
+		select_sprite(SPRITE_DOTDOT);
+		gfx_quads_draw(position.x + 24, position.y - 40, 64,64);
+		gfx_quads_end();
+	}
+
+	if (gameclient.clients[info.cid].emoticon_start != -1 && gameclient.clients[info.cid].emoticon_start + 2 * client_tickspeed() > client_tick())
+	{
+		gfx_texture_set(data->images[IMAGE_EMOTICONS].id);
+		gfx_quads_begin();
+
+		int since_start = client_tick() - gameclient.clients[info.cid].emoticon_start;
+		int from_end = gameclient.clients[info.cid].emoticon_start + 2 * client_tickspeed() - client_tick();
+
+		float a = 1;
+
+		if (from_end < client_tickspeed() / 5)
+			a = from_end / (client_tickspeed() / 5.0);
+
+		float h = 1;
+		if (since_start < client_tickspeed() / 10)
+			h = since_start / (client_tickspeed() / 10.0);
+
+		float wiggle = 0;
+		if (since_start < client_tickspeed() / 5)
+			wiggle = since_start / (client_tickspeed() / 5.0);
+
+		float wiggle_angle = sin(5*wiggle);
+
+		gfx_quads_setrotation(pi/6*wiggle_angle);
+
+		gfx_setcolor(1.0f,1.0f,1.0f,a);
+		// client_datas::emoticon is an offset from the first emoticon
+		select_sprite(SPRITE_OOP + gameclient.clients[info.cid].emoticon);
+		gfx_quads_draw(position.x, position.y - 23 - 32*h, 64, 64*h);
+		gfx_quads_end();
+	}
+	
+	// render name plate
+	if(!info.local && config.cl_nameplates)
+	{
+		//gfx_text_color
+		float a = 1;
+		if(config.cl_nameplates_always == 0)
+			a = clamp(1-powf(distance(gameclient.local_target_pos, position)/200.0f,16.0f), 0.0f, 1.0f);
+			
+		const char *name = gameclient.clients[info.cid].name;
+		float tw = gfx_text_width(0, 28.0f, name, -1);
+		gfx_text_color(1,1,1,a);
+		gfx_text(0, position.x-tw/2.0f, position.y-60, 28.0f, name, -1);
+		
+		if(config.debug) // render client id when in debug aswell
+		{
+			char buf[128];
+			str_format(buf, sizeof(buf),"%d", info.cid);
+			gfx_text(0, position.x, position.y-90, 28.0f, buf, -1);
+		}
+
+		gfx_text_color(1,1,1,1);
+	}
+}
+
+void PLAYERS::on_render()
+{
+	int num = snap_num_items(SNAP_CURRENT);
+	for(int i = 0; i < num; i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+		if(item.type == NETOBJTYPE_CHARACTER)
+		{
+			const void *prev = snap_find_item(SNAP_PREV, item.type, item.id);
+			const void *prev_info = snap_find_item(SNAP_PREV, NETOBJTYPE_PLAYER_INFO, item.id);
+			const void *info = snap_find_item(SNAP_CURRENT, NETOBJTYPE_PLAYER_INFO, item.id);
+
+			if(prev && prev_info && info)
+			{
+				render_player(
+						(const NETOBJ_CHARACTER *)prev,
+						(const NETOBJ_CHARACTER *)data,
+						(const NETOBJ_PLAYER_INFO *)prev_info,
+						(const NETOBJ_PLAYER_INFO *)info
+					);
+			}
+		}
+	}
+}
diff --git a/src/game/client/components/players.hpp b/src/game/client/components/players.hpp
new file mode 100644
index 00000000..bdce91de
--- /dev/null
+++ b/src/game/client/components/players.hpp
@@ -0,0 +1,16 @@
+#include <game/client/component.hpp>
+
+class PLAYERS : public COMPONENT
+{	
+	void render_hand(class TEE_RENDER_INFO *info, vec2 center_pos, vec2 dir, float angle_offset, vec2 post_rot_offset);
+	void render_player(
+		const class NETOBJ_CHARACTER *prev_char,
+		const class NETOBJ_CHARACTER *player_char,
+		const class NETOBJ_PLAYER_INFO *prev_info,
+		const class NETOBJ_PLAYER_INFO *player_info
+	);	
+	
+public:
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/scoreboard.cpp b/src/game/client/components/scoreboard.cpp
new file mode 100644
index 00000000..075f249e
--- /dev/null
+++ b/src/game/client/components/scoreboard.cpp
@@ -0,0 +1,254 @@
+#include <string.h>
+
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+#include <game/client/gameclient.hpp>
+#include <game/client/animstate.hpp>
+#include <game/client/gc_render.hpp>
+#include "scoreboard.hpp"
+
+void SCOREBOARD::render_goals(float x, float y, float w)
+{
+	float h = 50.0f;
+
+	gfx_blend_normal();
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.5f);
+	draw_round_rect(x-10.f, y-10.f, w, h, 10.0f);
+	gfx_quads_end();
+
+	// render goals
+	//y = ystart+h-54;
+	if(gameclient.snap.gameobj && gameclient.snap.gameobj->time_limit)
+	{
+		char buf[64];
+		str_format(buf, sizeof(buf), "Time Limit: %d min", gameclient.snap.gameobj->time_limit);
+		gfx_text(0, x+w/2, y, 24.0f, buf, -1);
+	}
+	if(gameclient.snap.gameobj && gameclient.snap.gameobj->score_limit)
+	{
+		char buf[64];
+		str_format(buf, sizeof(buf), "Score Limit: %d", gameclient.snap.gameobj->score_limit);
+		gfx_text(0, x+40, y, 24.0f, buf, -1);
+	}
+}
+
+void SCOREBOARD::render_spectators(float x, float y, float w)
+{
+	char buffer[1024*4];
+	int count = 0;
+	float h = 120.0f;
+	
+	str_copy(buffer, "Spectators: ", sizeof(buffer));
+
+	gfx_blend_normal();
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.5f);
+	draw_round_rect(x-10.f, y-10.f, w, h, 10.0f);
+	gfx_quads_end();
+	
+	for(int i = 0; i < snap_num_items(SNAP_CURRENT); i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+		if(item.type == NETOBJTYPE_PLAYER_INFO)
+		{
+			const NETOBJ_PLAYER_INFO *info = (const NETOBJ_PLAYER_INFO *)data;
+			if(info->team == -1)
+			{
+				if(count)
+					strcat(buffer, ", ");
+				strcat(buffer, gameclient.clients[info->cid].name);
+				count++;
+			}
+		}
+	}
+	
+	gfx_text(0, x+10, y, 32, buffer, (int)w-20);
+}
+
+void SCOREBOARD::render_scoreboard(float x, float y, float w, int team, const char *title)
+{
+	//float ystart = y;
+	float h = 750.0f;
+
+	gfx_blend_normal();
+	gfx_texture_set(-1);
+	gfx_quads_begin();
+	gfx_setcolor(0,0,0,0.5f);
+	draw_round_rect(x-10.f, y-10.f, w, h, 40.0f);
+	gfx_quads_end();
+
+	// render title
+	if(!title)
+	{
+		if(gameclient.snap.gameobj->game_over)
+			title = "Game Over";
+		else
+			title = "Score Board";
+	}
+
+	float tw = gfx_text_width(0, 48, title, -1);
+
+	if(team == -1)
+	{
+		gfx_text(0, x+w/2-tw/2, y, 48, title, -1);
+	}
+	else
+	{
+		gfx_text(0, x+10, y, 48, title, -1);
+
+		if(gameclient.snap.gameobj)
+		{
+			char buf[128];
+			int score = team ? gameclient.snap.gameobj->teamscore_blue : gameclient.snap.gameobj->teamscore_red;
+			str_format(buf, sizeof(buf), "%d", score);
+			tw = gfx_text_width(0, 48, buf, -1);
+			gfx_text(0, x+w-tw-30, y, 48, buf, -1);
+		}
+	}
+
+	y += 54.0f;
+
+	// find players
+	const NETOBJ_PLAYER_INFO *players[MAX_CLIENTS] = {0};
+	int num_players = 0;
+	for(int i = 0; i < snap_num_items(SNAP_CURRENT); i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+
+		if(item.type == NETOBJTYPE_PLAYER_INFO)
+		{
+			players[num_players] = (const NETOBJ_PLAYER_INFO *)data;
+			num_players++;
+		}
+	}
+
+	// sort players
+	for(int k = 0; k < num_players; k++) // ffs, bubblesort
+	{
+		for(int i = 0; i < num_players-k-1; i++)
+		{
+			if(players[i]->score < players[i+1]->score)
+			{
+				const NETOBJ_PLAYER_INFO *tmp = players[i];
+				players[i] = players[i+1];
+				players[i+1] = tmp;
+			}
+		}
+	}
+
+	// render headlines
+	gfx_text(0, x+10, y, 24.0f, "Score", -1);
+	gfx_text(0, x+125, y, 24.0f, "Name", -1);
+	gfx_text(0, x+w-70, y, 24.0f, "Ping", -1);
+	y += 29.0f;
+
+	// render player scores
+	for(int i = 0; i < num_players; i++)
+	{
+		const NETOBJ_PLAYER_INFO *info = players[i];
+
+		// make sure that we render the correct team
+		if(team == -1 || info->team != team)
+			continue;
+
+		char buf[128];
+		float font_size = 35.0f;
+		if(info->local)
+		{
+			// background so it's easy to find the local player
+			gfx_texture_set(-1);
+			gfx_quads_begin();
+			gfx_setcolor(1,1,1,0.25f);
+			draw_round_rect(x, y, w-20, 48, 20.0f);
+			gfx_quads_end();
+		}
+
+		str_format(buf, sizeof(buf), "%4d", info->score);
+		gfx_text(0, x+60-gfx_text_width(0, font_size,buf,-1), y, font_size, buf, -1);
+		
+		gfx_text(0, x+128, y, font_size, gameclient.clients[info->cid].name, -1);
+
+		str_format(buf, sizeof(buf), "%4d", info->latency);
+		float tw = gfx_text_width(0, font_size, buf, -1);
+		gfx_text(0, x+w-tw-35, y, font_size, buf, -1);
+
+		// render avatar
+		if((gameclient.snap.flags[0] && gameclient.snap.flags[0]->carried_by == info->cid) ||
+			(gameclient.snap.flags[1] && gameclient.snap.flags[1]->carried_by == info->cid))
+		{
+			gfx_blend_normal();
+			gfx_texture_set(data->images[IMAGE_GAME].id);
+			gfx_quads_begin();
+
+			if(info->team == 0) select_sprite(SPRITE_FLAG_BLUE, SPRITE_FLAG_FLIP_X);
+			else select_sprite(SPRITE_FLAG_RED, SPRITE_FLAG_FLIP_X);
+			
+			float size = 64.0f;
+			gfx_quads_drawTL(x+55, y-15, size/2, size);
+			gfx_quads_end();
+		}
+		
+		render_tee(ANIMSTATE::get_idle(), &gameclient.clients[info->cid].render_info, EMOTE_NORMAL, vec2(1,0), vec2(x+90, y+28));
+
+		
+		y += 50.0f;
+	}
+}
+
+void SCOREBOARD::on_render()
+{
+	
+	// TODO: repair me
+	/*
+	bool do_scoreboard = false;
+
+	// if we are dead
+	if(!spectate && (!gameclient.snap.local_character || gameclient.snap.local_character->health < 0))
+		do_scoreboard = true;
+	
+	// if we the game is over
+	if(gameclient.snap.gameobj && gameclient.snap.gameobj->game_over)
+		do_scoreboard = true;*/
+
+	
+	float width = 400*3.0f*gfx_screenaspect();
+	float height = 400*3.0f;
+	
+	gfx_mapscreen(0, 0, width, height);
+
+	float w = 650.0f;
+
+	if(gameclient.snap.gameobj && !(gameclient.snap.gameobj->flags&GAMEFLAG_TEAMS))
+	{
+		render_scoreboard(width/2-w/2, 150.0f, w, 0, 0);
+		//render_scoreboard(gameobj, 0, 0, -1, 0);
+	}
+	else
+	{
+			
+		if(gameclient.snap.gameobj && gameclient.snap.gameobj->game_over)
+		{
+			const char *text = "DRAW!";
+			if(gameclient.snap.gameobj->teamscore_red > gameclient.snap.gameobj->teamscore_blue)
+				text = "Red Team Wins!";
+			else if(gameclient.snap.gameobj->teamscore_blue > gameclient.snap.gameobj->teamscore_red)
+				text = "Blue Team Wins!";
+				
+			float w = gfx_text_width(0, 92.0f, text, -1);
+			gfx_text(0, width/2-w/2, 45, 92.0f, text, -1);
+		}
+		
+		render_scoreboard(width/2-w-20, 150.0f, w, 0, "Red Team");
+		render_scoreboard(width/2 + 20, 150.0f, w, 1, "Blue Team");
+	}
+
+	render_goals(width/2-w/2, 150+750+25, w);
+	render_spectators(width/2-w/2, 150+750+25+50+25, w);
+}
diff --git a/src/game/client/components/scoreboard.hpp b/src/game/client/components/scoreboard.hpp
new file mode 100644
index 00000000..b1913cec
--- /dev/null
+++ b/src/game/client/components/scoreboard.hpp
@@ -0,0 +1,11 @@
+#include <game/client/component.hpp>
+
+class SCOREBOARD : public COMPONENT
+{
+	void render_goals(float x, float y, float w);
+	void render_spectators(float x, float y, float w);
+	void render_scoreboard(float x, float y, float w, int team, const char *title);
+public:
+	virtual void on_render();
+};
+
diff --git a/src/game/client/components/skins.cpp b/src/game/client/components/skins.cpp
new file mode 100644
index 00000000..eae45c4d
--- /dev/null
+++ b/src/game/client/components/skins.cpp
@@ -0,0 +1,188 @@
+/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <base/system.h>
+#include <base/math.hpp>
+
+#include <engine/e_client_interface.h>
+#include "skins.hpp"
+
+SKINS::SKINS()
+{
+	num_skins = 0;
+}
+
+void SKINS::skinscan(const char *name, int is_dir, void *user)
+{
+	SKINS *self = (SKINS *)user;
+	int l = strlen(name);
+	if(l < 4 || is_dir || self->num_skins == MAX_SKINS)
+		return;
+	if(strcmp(name+l-4, ".png") != 0)
+		return;
+		
+	char buf[512];
+	str_format(buf, sizeof(buf), "data/skins/%s", name);
+	IMAGE_INFO info;
+	if(!gfx_load_png(&info, buf))
+	{
+		dbg_msg("game", "failed to load skin from %s", name);
+		return;
+	}
+	
+	self->skins[self->num_skins].org_texture = gfx_load_texture_raw(info.width, info.height, info.format, info.data, info.format, 0);
+	
+	int body_size = 96; // body size
+	unsigned char *d = (unsigned char *)info.data;
+	int pitch = info.width*4;
+
+	// dig out blood color
+	{
+		int colors[3] = {0};
+		for(int y = 0; y < body_size; y++)
+			for(int x = 0; x < body_size; x++)
+			{
+				if(d[y*pitch+x*4+3] > 128)
+				{
+					colors[0] += d[y*pitch+x*4+0];
+					colors[1] += d[y*pitch+x*4+1];
+					colors[2] += d[y*pitch+x*4+2];
+				}
+			}
+			
+		self->skins[self->num_skins].blood_color = normalize(vec3(colors[0], colors[1], colors[2]));
+	}
+	
+	// create colorless version
+	int step = info.format == IMG_RGBA ? 4 : 3;
+
+	// make the texture gray scale
+	for(int i = 0; i < info.width*info.height; i++)
+	{
+		int v = (d[i*step]+d[i*step+1]+d[i*step+2])/3;
+		d[i*step] = v;
+		d[i*step+1] = v;
+		d[i*step+2] = v;
+	}
+
+	
+	if(1)
+	{
+		int freq[256] = {0};
+		int org_weight = 0;
+		int new_weight = 192;
+		
+		// find most common frequence
+		for(int y = 0; y < body_size; y++)
+			for(int x = 0; x < body_size; x++)
+			{
+				if(d[y*pitch+x*4+3] > 128)
+					freq[d[y*pitch+x*4]]++;
+			}
+		
+		for(int i = 1; i < 256; i++)
+		{
+			if(freq[org_weight] < freq[i])
+				org_weight = i;
+		}
+
+		// reorder
+		int inv_org_weight = 255-org_weight;
+		int inv_new_weight = 255-new_weight;
+		for(int y = 0; y < body_size; y++)
+			for(int x = 0; x < body_size; x++)
+			{
+				int v = d[y*pitch+x*4];
+				if(v <= org_weight)
+					v = (int)(((v/(float)org_weight) * new_weight));
+				else
+					v = (int)(((v-org_weight)/(float)inv_org_weight)*inv_new_weight + new_weight);
+				d[y*pitch+x*4] = v;
+				d[y*pitch+x*4+1] = v;
+				d[y*pitch+x*4+2] = v;
+			}
+	}
+	
+	self->skins[self->num_skins].color_texture = gfx_load_texture_raw(info.width, info.height, info.format, info.data, info.format, 0);
+	mem_free(info.data);
+
+	// set skin data	
+	strncpy(self->skins[self->num_skins].name, name, min((int)sizeof(self->skins[self->num_skins].name),l-4));
+	dbg_msg("game", "load skin %s", self->skins[self->num_skins].name);
+	self->num_skins++;
+}
+
+
+void SKINS::init()
+{
+	// load skins
+	num_skins = 0;
+	fs_listdir("data/skins", skinscan, this);
+}
+
+int SKINS::num()
+{
+	return num_skins;	
+}
+
+const SKINS::SKIN *SKINS::get(int index)
+{
+	return &skins[index%num_skins];
+}
+
+int SKINS::find(const char *name)
+{
+	for(int i = 0; i < num_skins; i++)
+	{
+		if(strcmp(skins[i].name, name) == 0)
+			return i;
+	}
+	return -1;
+}
+
+// these converter functions were nicked from some random internet pages
+static float hue_to_rgb(float v1, float v2, float h)
+{
+   if(h < 0) h += 1;
+   if(h > 1) h -= 1;
+   if((6 * h) < 1) return v1 + ( v2 - v1 ) * 6 * h;
+   if((2 * h) < 1) return v2;
+   if((3 * h) < 2) return v1 + ( v2 - v1 ) * ((2.0f/3.0f) - h) * 6;
+   return v1;
+}
+
+static vec3 hsl_to_rgb(vec3 in)
+{
+	float v1, v2;
+	vec3 out;
+
+	if(in.s == 0)
+	{
+		out.r = in.l;
+		out.g = in.l;
+		out.b = in.l;
+	}
+	else
+	{
+		if(in.l < 0.5f) 
+			v2 = in.l * (1 + in.s);
+		else           
+			v2 = (in.l+in.s) - (in.s*in.l);
+
+		v1 = 2 * in.l - v2;
+
+		out.r = hue_to_rgb(v1, v2, in.h + (1.0f/3.0f));
+		out.g = hue_to_rgb(v1, v2, in.h);
+		out.b = hue_to_rgb(v1, v2, in.h - (1.0f/3.0f));
+	} 
+
+	return out;
+}
+
+vec4 SKINS::get_color(int v)
+{
+	vec3 r = hsl_to_rgb(vec3((v>>16)/255.0f, ((v>>8)&0xff)/255.0f, 0.5f+(v&0xff)/255.0f*0.5f));
+	return vec4(r.r, r.g, r.b, 1.0f);
+}
diff --git a/src/game/client/components/skins.hpp b/src/game/client/components/skins.hpp
new file mode 100644
index 00000000..078fd71d
--- /dev/null
+++ b/src/game/client/components/skins.hpp
@@ -0,0 +1,36 @@
+#include <base/vmath.hpp>
+#include <game/client/component.hpp>
+
+class SKINS : public COMPONENT
+{
+public:
+	// do this better and nicer
+	typedef struct 
+	{
+		int org_texture;
+		int color_texture;
+		char name[31];
+		char term[1];
+		vec3 blood_color;
+	} SKIN;
+
+	SKINS();
+	
+	void init();
+	
+	vec4 get_color(int v);
+	int num();
+	const SKIN *get(int index);
+	int find(const char *name);
+	
+private:
+	enum
+	{
+		MAX_SKINS=256,
+	};
+
+	SKIN skins[MAX_SKINS];
+	int num_skins;
+
+	static void skinscan(const char *name, int is_dir, void *user);
+};