about summary refs log tree commit diff
path: root/src/game/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/game/client')
-rw-r--r--src/game/client/animstate.cpp95
-rw-r--r--src/game/client/animstate.hpp24
-rw-r--r--src/game/client/component.hpp22
-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.cpp (renamed from src/game/client/gc_console.cpp)623
-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.cpp (renamed from src/game/client/gc_flow.cpp)87
-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.cpp (renamed from src/game/client/gc_particles.cpp)48
-rw-r--r--src/game/client/components/particles.hpp91
-rw-r--r--src/game/client/components/players.cpp (renamed from src/game/client/gc_render_obj.cpp)316
-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.cpp (renamed from src/game/client/gc_skin.cpp)46
-rw-r--r--src/game/client/components/skins.hpp36
-rw-r--r--src/game/client/gameclient.cpp548
-rw-r--r--src/game/client/gameclient.hpp109
-rw-r--r--src/game/client/gc_anim.hpp14
-rw-r--r--src/game/client/gc_client.cpp1629
-rw-r--r--src/game/client/gc_client.hpp262
-rw-r--r--src/game/client/gc_console.hpp19
-rw-r--r--src/game/client/gc_effects.cpp39
-rw-r--r--src/game/client/gc_hooks.cpp478
-rw-r--r--src/game/client/gc_menu.cpp2161
-rw-r--r--src/game/client/gc_menu.hpp5
-rw-r--r--src/game/client/gc_render.cpp257
-rw-r--r--src/game/client/gc_render.hpp14
-rw-r--r--src/game/client/gc_skin.hpp21
-rw-r--r--src/game/client/lineinput.cpp65
-rw-r--r--src/game/client/lineinput.hpp23
62 files changed, 6513 insertions, 5492 deletions
diff --git a/src/game/client/animstate.cpp b/src/game/client/animstate.cpp
new file mode 100644
index 00000000..d8c20dec
--- /dev/null
+++ b/src/game/client/animstate.cpp
@@ -0,0 +1,95 @@
+
+#include <base/math.hpp>
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+#include <game/generated/gc_data.hpp>
+
+#include "animstate.hpp"
+
+static void anim_seq_eval(ANIM_SEQUENCE *seq, float time, ANIM_KEYFRAME *frame)
+{
+	if(seq->num_frames == 0)
+	{
+		frame->time = 0;
+		frame->x = 0;
+		frame->y = 0;
+		frame->angle = 0;
+	}
+	else if(seq->num_frames == 1)
+	{
+		*frame = seq->frames[0];
+	}
+	else
+	{
+		//time = max(0.0f, min(1.0f, time / duration)); // TODO: use clamp
+		ANIM_KEYFRAME *frame1 = 0;
+		ANIM_KEYFRAME *frame2 = 0;
+		float blend = 0.0f;
+
+		// TODO: make this smarter.. binary search
+		for (int i = 1; i < seq->num_frames; i++)
+		{
+			if (seq->frames[i-1].time <= time && seq->frames[i].time >= time)
+			{
+				frame1 = &seq->frames[i-1];
+				frame2 = &seq->frames[i];
+				blend = (time - frame1->time) / (frame2->time - frame1->time);
+				break;
+			}
+		}
+
+		if (frame1 && frame2)
+		{
+			frame->time = time;
+			frame->x = mix(frame1->x, frame2->x, blend);
+			frame->y = mix(frame1->y, frame2->y, blend);
+			frame->angle = mix(frame1->angle, frame2->angle, blend);
+		}
+	}
+}
+
+static void anim_add_keyframe(ANIM_KEYFRAME *seq, ANIM_KEYFRAME *added, float amount)
+{
+	seq->x += added->x*amount;
+	seq->y += added->y*amount;
+	seq->angle += added->angle*amount;
+}
+
+static void anim_add(ANIMSTATE *state, ANIMSTATE *added, float amount)
+{
+	anim_add_keyframe(&state->body, &added->body, amount);
+	anim_add_keyframe(&state->back_foot, &added->back_foot, amount);
+	anim_add_keyframe(&state->front_foot, &added->front_foot, amount);
+	anim_add_keyframe(&state->attach, &added->attach, amount);
+}
+
+
+void ANIMSTATE::set(ANIMATION *anim, float time)
+{
+	anim_seq_eval(&anim->body, time, &body);
+	anim_seq_eval(&anim->back_foot, time, &back_foot);
+	anim_seq_eval(&anim->front_foot, time, &front_foot);
+	anim_seq_eval(&anim->attach, time, &attach);
+}
+
+void ANIMSTATE::add(ANIMATION *anim, float time, float amount)
+{
+	ANIMSTATE add;
+	add.set(anim, time);
+	anim_add(this, &add, amount);
+}
+
+ANIMSTATE *ANIMSTATE::get_idle()
+{
+	static ANIMSTATE state;
+	static bool init = true;
+	
+	if(init)
+	{
+		state.set(&data->animations[ANIM_BASE], 0);
+		state.add(&data->animations[ANIM_IDLE], 0, 1.0f);
+		init = false;
+	}
+	
+	return &state;
+}
diff --git a/src/game/client/animstate.hpp b/src/game/client/animstate.hpp
new file mode 100644
index 00000000..4b84dd66
--- /dev/null
+++ b/src/game/client/animstate.hpp
@@ -0,0 +1,24 @@
+#ifndef GAME_CLIENT_ANIMATION_H
+#define GAME_CLIENT_ANIMATION_H
+
+class ANIMSTATE
+{
+public:
+	ANIM_KEYFRAME body;
+	ANIM_KEYFRAME back_foot;
+	ANIM_KEYFRAME front_foot;
+	ANIM_KEYFRAME attach;
+	
+	void set(ANIMATION *anim, float time);
+	void add(ANIMATION *added, float time, float amount);
+	
+	static ANIMSTATE *get_idle();
+};
+
+//void anim_seq_eval(ANIM_SEQUENCE *seq, float time, ANIM_KEYFRAME *frame);
+//void anim_eval(ANIMATION *anim, float time, ANIM_STATE *state);
+//void anim_add_keyframe(ANIM_KEYFRAME *seq, ANIM_KEYFRAME *added, float amount);
+//void anim_add(ANIM_STATE *state, ANIM_STATE *added, float amount);
+//void anim_eval_add(ANIM_STATE *state, ANIMATION *anim, float time, float amount);
+
+#endif
diff --git a/src/game/client/component.hpp b/src/game/client/component.hpp
new file mode 100644
index 00000000..5bc7830d
--- /dev/null
+++ b/src/game/client/component.hpp
@@ -0,0 +1,22 @@
+#ifndef GAME_CLIENT_GAMESYSTEM_H
+#define GAME_CLIENT_GAMESYSTEM_H
+
+#include <engine/e_client_interface.h>
+
+class GAMECLIENT;
+
+class COMPONENT
+{
+public:
+	GAMECLIENT *client;
+	
+	virtual void on_statechange(int new_state, int old_state) {};
+	virtual void on_init() {};
+	virtual void on_reset() {};
+	virtual void on_render() {};
+	virtual void on_message(int msg, void *rawmsg) {}
+	virtual bool on_mousemove(float x, float y) { return false; }
+	virtual bool on_input(INPUT_EVENT e) { return false; }
+};
+
+#endif
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/gc_console.cpp b/src/game/client/components/console.cpp
index eb73ebc6..6437b718 100644
--- a/src/game/client/gc_console.cpp
+++ b/src/game/client/components/console.cpp
@@ -1,5 +1,5 @@
-#include "gc_console.hpp"
-#include "../generated/gc_data.hpp"
+//#include "gc_console.hpp"
+#include <game/generated/gc_data.hpp>
 
 #include <base/system.h>
 
@@ -14,10 +14,14 @@ extern "C" {
 #include <cstring>
 #include <cstdio>
 
-#include "gc_ui.hpp"
-#include "gc_client.hpp"
+#include <game/client/gc_ui.hpp>
+#include <game/client/gc_client.hpp>
 
-#include "../version.hpp"
+#include <game/version.hpp>
+
+#include <game/client/lineinput.hpp>
+
+#include "console.hpp"
 
 enum
 {
@@ -27,335 +31,307 @@ enum
 	CONSOLE_CLOSING,
 };
 
-class CONSOLE
+CONSOLE::INSTANCE::INSTANCE(int t)
 {
-public:
-	char history_data[65536];
-	RINGBUFFER *history;
-	char *history_entry;
-	
-	char backlog_data[65536];
-	RINGBUFFER *backlog;
-
-
-	line_input input;
-	
-	int type;
+	// init ringbuffers
+	history = ringbuf_init(history_data, sizeof(history_data));
+	backlog = ringbuf_init(backlog_data, sizeof(backlog_data));
 	
-public:
-	CONSOLE(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;
-	}
+	history_entry = 0x0;
 	
-	void execute_line(const char *line)
+	type = t;
+}
+
+void CONSOLE::INSTANCE::execute_line(const char *line)
+{
+	if(type == 0)
+		console_execute_line(line);
+	else
 	{
-		if(type == 0)
-			console_execute_line(line);
+		if(client_rcon_authed())
+			client_rcon(line);
 		else
-		{
-			if(client_rcon_authed())
-				client_rcon(line);
-			else
-				client_rcon_auth("", line);
-		}
+			client_rcon_auth("", line);
 	}
+}
+
+void CONSOLE::INSTANCE::on_input(INPUT_EVENT e)
+{
+	bool handled = false;
 	
-	void handle_event(INPUT_EVENT e)
+	if(e.flags&INPFLAG_PRESS)
 	{
-		bool handled = false;
-		
-		if(e.flags&INPFLAG_PRESS)
+		if(e.key == KEY_ENTER || e.key == KEY_KP_ENTER)
 		{
-			if(e.key == KEY_ENTER || e.key == KEY_KP_ENTER)
+			if(input.get_string()[0])
 			{
-				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;
-				}
+				char *entry = (char *)ringbuf_allocate(history, input.get_length()+1);
+				mem_copy(entry, input.get_string(), input.get_length()+1);
 				
-				handled = true;
+				execute_line(input.get_string());
+				input.clear();
+				history_entry = 0x0;
 			}
-			else if (e.key == KEY_UP)
+			
+			handled = true;
+		}
+		else if (e.key == KEY_UP)
+		{
+			if (history_entry)
 			{
-				if (history_entry)
-				{
-					char *test = (char *)ringbuf_prev(history, 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;
+				if (test)
+					history_entry = test;
 			}
-			else if (e.key == KEY_DOWN)
+			else
+				history_entry = (char *)ringbuf_last(history);
+
+			if (history_entry)
 			{
-				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;
+				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 print_line(const char *line)
-	{
-		int len = strlen(line);
+	if(!handled)
+		input.process_input(e);
+}
 
-		if (len > 255)
-			len = 255;
+void CONSOLE::INSTANCE::print_line(const char *line)
+{
+	int len = strlen(line);
 
-		char *entry = (char *)ringbuf_allocate(backlog, len+1);
-		mem_copy(entry, line, len+1);
-	}
-};
+	if (len > 255)
+		len = 255;
 
-static CONSOLE local_console(0);
-static CONSOLE remote_console(1);
+	char *entry = (char *)ringbuf_allocate(backlog, len+1);
+	mem_copy(entry, line, len+1);
+}
 
-static int console_type = 0;
-static int console_state = CONSOLE_CLOSED;
-static float state_change_end = 0.0f;
-static const float state_change_duration = 0.1f;
+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;
+}
 
-static float time_now()
+float CONSOLE::time_now()
 {
 	static long long time_start = time_get();
 	return float(time_get()-time_start)/float(time_freq());
 }
 
-static CONSOLE *current_console()
+CONSOLE::INSTANCE *CONSOLE::current_console()
 {
     if(console_type != 0)
     	return &remote_console;
     return &local_console;
 }
 
-static void client_console_print(const char *str)
+void CONSOLE::on_reset()
 {
-	local_console.print_line(str);
 }
 
-void console_rcon_print(const char *line)
+// only defined for 0<=t<=1
+static float console_scale_func(float t)
 {
-	remote_console.print_line(line);
+	//return t;
+	return sinf(acosf(1.0f-t));
 }
 
-static void con_team(void *result, void *user_data)
+void CONSOLE::on_render()
 {
-	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));
-}
+    RECT screen = *ui_screen();
+	float console_max_height = screen.h*3/5.0f;
+	float console_height;
 
-static void con_sayteam(void *result, void *user_data)
-{
-	chat_say(1, console_arg_string(result, 0));
-}
+	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;
 
-static 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");
-}
+	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);
 
-void send_kill(int client_id);
+	console_height = console_height_scale*console_max_height;
 
-static void con_kill(void *result, void *user_data)
-{
-	send_kill(-1);
-}
+	gfx_mapscreen(screen.x, screen.y, screen.w, screen.h);
 
-static char keybindings[KEY_LAST][128] = {{0}};
+	// 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();
 
-const char *binds_get(int keyid)
-{
-	if(keyid > 0 && keyid < KEY_LAST)
-		return keybindings[keyid];
-	return "";
-}
+	// 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();
 
-void binds_set(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]);
-}
+	// 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();
 
-void binds_unbindall()
-{
-	for(int i = 0; i < KEY_LAST; i++)
-		keybindings[i][0] = 0;
-}
+	// 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();
 
-void binds_default()
-{
-	binds_unbindall();
+	{
+		float font_size = 10.0f;
+		float row_height = font_size*1.25f;
+		float x = 3;
+		float y = console_height - row_height - 2;
 
-	// set default key bindings
-	binds_set(KEY_F1, "toggle_local_console");
-	binds_set(KEY_F2, "toggle_remote_console");
-	binds_set(KEY_TAB, "+scoreboard");
-	binds_set(KEY_F10, "screenshot");
-	
-	binds_set('A', "+left");
-	binds_set('D', "+right");
-	binds_set(KEY_SPACE, "+jump");
-	binds_set(KEY_MOUSE_1, "+fire");
-	binds_set(KEY_MOUSE_2, "+hook");
-	binds_set(KEY_LSHIFT, "+emote");
-
-	binds_set('1', "+weapon1");
-	binds_set('2', "+weapon2");
-	binds_set('3', "+weapon3");
-	binds_set('4', "+weapon4");
-	binds_set('5', "+weapon5");
-	
-	binds_set(KEY_MOUSE_WHEEL_UP, "+prevweapon");
-	binds_set(KEY_MOUSE_WHEEL_DOWN, "+nextweapon");
-	
-	binds_set('T', "chat all");
-	binds_set('Y', "chat team");
+		// 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);
+		}
+	}	
 }
 
-static int get_key_id(const char *key_name)
+void CONSOLE::on_message(int msgtype, void *rawmsg)
 {
-	// 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)
+bool CONSOLE::on_input(INPUT_EVENT e)
 {
-	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));
+	return false;
 }
 
 
-static void con_unbind(void *result, void *user_data)
+/*
+static void client_console_print(const char *str)
 {
-	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, "");
+	// 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_unbindall(void *result, void *user_data)
+/*
+static void con_team(void *result, void *user_data)
 {
-	binds_unbindall();
+	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_dump_binds(void *result, void *user_data)
+static void con_sayteam(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]);
-	}
+	chat_say(1, console_arg_string(result, 0));
 }
 
-void binds_save()
+void send_kill(int client_id);
+
+static void con_kill(void *result, void *user_data)
 {
-	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);
-	}
+	send_kill(-1);
 }
 
 static void con_key_input_state(void *result, void *user_data)
@@ -399,6 +375,8 @@ 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);
@@ -515,142 +493,11 @@ void console_toggle(int type)
 	console_type = type;
 }
 
-// only defined for 0<=t<=1
-static float console_scale_func(float t)
-{
-	//return t;
-	return sinf(acosf(1.0f-t));
-}
 
-void console_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;
-    
-    CONSOLE *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);
-		}
-	}
-}
 
 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/gc_flow.cpp b/src/game/client/components/flow.cpp
index b18a41e6..b2f983e6 100644
--- a/src/game/client/gc_flow.cpp
+++ b/src/game/client/components/flow.cpp
@@ -1,19 +1,34 @@
-#include <engine/e_client_interface.h>
-#include <engine/e_config.h>
-#include "gc_client.hpp"
-#include "../layers.hpp"
+#include <game/mapitems.hpp>
+#include <game/layers.hpp>
+#include "flow.hpp"
 
-struct FLOWCELL
+FLOW::FLOW()
 {
-	vec2 vel;
-};
+	cells = 0;
+	height = 0;
+	width = 0;
+	spacing = 16;
+}
+	
+void FLOW::dbg_render()
+{
+	if(!cells)
+		return;
 
-static FLOWCELL *cells = 0;
-static int height = 0;
-static int width = 0;
-static int spacing = 16;
+	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()
+void FLOW::init()
 {
 	if(cells)
 	{
@@ -26,15 +41,15 @@ void flow_init()
 	height = tilemap->height*32/spacing;
 
 	// allocate and clear	
-	cells = (FLOWCELL *)mem_alloc(sizeof(FLOWCELL)*width*height, 1);
+	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()
+void FLOW::update()
 {
-	if(!config.cl_flow)
+	if(!cells)
 		return;
 		
 	for(int y = 0; y < height; y++)
@@ -42,46 +57,28 @@ void flow_update()
 			cells[y*width+x].vel *= 0.85f;
 }
 
-void flow_dbg_render()
-{
-	if(!config.cl_flow)
-		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_add(vec2 pos, vec2 vel, float size)
+vec2 FLOW::get(vec2 pos)
 {
-	if(!config.cl_flow)
-		return;
-		
+	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;
+		return vec2(0,0);
 	
-	cells[y*width+x].vel += vel;
+	return cells[y*width+x].vel;	
 }
 
-vec2 flow_get(vec2 pos)
+void FLOW::add(vec2 pos, vec2 vel, float size)
 {
-	if(!config.cl_flow)
-		return vec2(0,0);
-	
+	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 vec2(0,0);
+		return;
 	
-	return cells[y*width+x].vel;
+	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/gc_particles.cpp b/src/game/client/components/particles.cpp
index 504ebbab..0c449263 100644
--- a/src/game/client/gc_particles.cpp
+++ b/src/game/client/components/particles.cpp
@@ -1,19 +1,18 @@
-#include <engine/e_client_interface.h>
-#include "gc_client.hpp"
-#include "../generated/gc_data.hpp"
+#include <base/math.hpp>
+#include <game/generated/gc_data.hpp>
+#include <game/client/gc_render.hpp>
+#include "particles.hpp"
 
-// NOTE: the way the particle system works isn't very cache friendly
-
-enum
+PARTICLES::PARTICLES()
 {
-	MAX_PARTICLES=1024*8,
-};
+	on_reset();
+	render_trail.parts = this;
+	render_explosions.parts = this;
+	render_general.parts = this;
+}
 
-static PARTICLE particles[MAX_PARTICLES];
-static int first_free = -1;
-static int first_part[NUM_PARTGROUPS] = {-1};
 
-void particle_reset()
+void PARTICLES::on_reset()
 {
 	// reset particles
 	for(int i = 0; i < MAX_PARTICLES; i++)
@@ -26,12 +25,11 @@ void particle_reset()
 	particles[MAX_PARTICLES-1].next_part = -1;
 	first_free = 0;
 
-	for(int i = 0; i < NUM_PARTGROUPS; i++)
+	for(int i = 0; i < NUM_GROUPS; i++)
 		first_part[i] = -1;
 }
 
-
-void particle_add(int group, PARTICLE *part)
+void PARTICLES::add(int group, PARTICLE *part)
 {
 	if (first_free == -1)
 		return;
@@ -55,7 +53,7 @@ void particle_add(int group, PARTICLE *part)
 	particles[id].life = 0;
 }
 
-void particle_update(float time_passed)
+void PARTICLES::update(float time_passed)
 {
 	static float friction_fraction = 0;
 	friction_fraction += time_passed;
@@ -70,13 +68,13 @@ void particle_update(float time_passed)
 		friction_fraction -= 0.05f;
 	}
 	
-	for(int g = 0; g < NUM_PARTGROUPS; g++)
+	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 += 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
@@ -84,7 +82,9 @@ void particle_update(float time_passed)
 			
 			// move the point
 			vec2 vel = particles[i].vel*time_passed;
-			move_point(&particles[i].pos, &vel, 0.1f+0.9f*frandom(), NULL);
+			
+			// 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;
@@ -115,7 +115,15 @@ void particle_update(float time_passed)
 	}
 }
 
-void particle_render(int group)
+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();
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/gc_render_obj.cpp b/src/game/client/components/players.cpp
index aa64a127..3178b82f 100644
--- a/src/game/client/gc_render_obj.cpp
+++ b/src/game/client/components/players.cpp
@@ -1,229 +1,26 @@
-/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
-#include <math.h>
-#include <stdio.h>
 
-#include <base/math.hpp>
+extern "C" {
+	#include <engine/e_config.h>
+}
 
 #include <engine/e_client_interface.h>
-#include <engine/e_config.h>
-#include <game/generated/gc_data.hpp>
 #include <game/generated/g_protocol.hpp>
-#include "gc_render.hpp"
-#include "gc_anim.hpp"
-#include "gc_client.hpp"
-#include "gc_skin.hpp"
-
-
-void render_projectile(const NETOBJ_PROJECTILE *current, int itemid)
-{
-	if(debug_firedelay)
-	{
-		int64 delay = time_get()-debug_firedelay;
-		dbg_msg("game", "firedelay=%.2f ms", delay/(float)time_freq()*1000.0f);
-		debug_firedelay = 0;
-	}
-	
-	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)
-	{
-		effect_smoketrail(pos, vel*-1);
-		flow_add(pos, vel*1000*client_frametime(), 10.0f);
-		gfx_quads_setrotation(client_localtime()*pi*2*2 + itemid);
-	}
-	else
-	{
-		effect_bullettrail(pos);
-		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 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)
-		{
-			effect_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 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(netobjects.local_info && current->carried_by == netobjects.local_info->cid)
-		pos = local_character_pos;
-
-    gfx_quads_draw(pos.x, pos.y-size*0.75f, size, size*2);
-    gfx_quads_end();
-}
-
-
-void 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();	
-}
-
+#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"
 
-static void render_hand(TEE_RENDER_INFO *info, vec2 center_pos, vec2 dir, float angle_offset, vec2 post_rot_offset)
+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);
@@ -266,7 +63,7 @@ static void render_hand(TEE_RENDER_INFO *info, vec2 center_pos, vec2 dir, float
 	gfx_quads_end();
 }
 
-void render_player(
+void PLAYERS::render_player(
 	const NETOBJ_CHARACTER *prev_char,
 	const NETOBJ_CHARACTER *player_char,
 	const NETOBJ_PLAYER_INFO *prev_info,
@@ -279,25 +76,25 @@ void render_player(
 	player = *player_char;
 
 	NETOBJ_PLAYER_INFO info = *player_info;
-	TEE_RENDER_INFO render_info = client_datas[info.cid].render_info;
+	TEE_RENDER_INFO render_info = gameclient.clients[info.cid].render_info;
 
 	// check for teamplay modes
 	bool is_teamplay = false;
-	if(netobjects.gameobj && netobjects.gameobj->gametype != GAMETYPE_DM)
-		is_teamplay = true;
+	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 = skin_find("x_ninja");
+		int skin = gameclient.skins->find("x_ninja");
 		if(skin != -1)
 		{
 			if(is_teamplay)
-				render_info.texture = skin_get(skin)->color_texture;
+				render_info.texture = gameclient.skins->get(skin)->color_texture;
 			else
 			{
-				render_info.texture = skin_get(skin)->org_texture;
+				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);
 			}
@@ -320,13 +117,13 @@ void render_player(
 	if(player.attacktick != prev.attacktick)
 		mixspeed = 0.1f;
 	
-	float angle = mix(client_datas[info.cid].angle, player.angle/256.0f, mixspeed);
-	client_datas[info.cid].angle = angle;
+	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(!netobjects.local_character || (netobjects.local_character->health < 0) || (netobjects.gameobj && netobjects.gameobj->game_over))
+		if(!gameclient.snap.local_character || (gameclient.snap.local_character->health < 0) || (gameclient.snap.gameobj && gameclient.snap.gameobj->game_over))
 		{
 		}
 		else
@@ -341,7 +138,7 @@ void render_player(
 	vec2 position = mix(vec2(prev.x, prev.y), vec2(player.x, player.y), intratick);
 	vec2 vel = mix(vec2(prev.vx/256.0f, prev.vy/256.0f), vec2(player.vx/256.0f, player.vy/256.0f), intratick);
 	
-	flow_add(position, vel*100.0f, 10.0f);
+	gameclient.flow->add(position, vel*100.0f, 10.0f);
 	
 	render_info.got_airjump = player.jumped&2?0:1;
 
@@ -354,25 +151,25 @@ void render_player(
 
 	// evaluate animation
 	float walk_time = fmod(position.x, 100.0f)/100.0f;
-	ANIM_STATE state;
-	anim_eval(&data->animations[ANIM_BASE], 0, &state);
+	ANIMSTATE state;
+	state.set(&data->animations[ANIM_BASE], 0);
 
 	if(inair)
-		anim_eval_add(&state, &data->animations[ANIM_INAIR], 0, 1.0f); // TODO: some sort of time here
+		state.add(&data->animations[ANIM_INAIR], 0, 1.0f); // TODO: some sort of time here
 	else if(stationary)
-		anim_eval_add(&state, &data->animations[ANIM_IDLE], 0, 1.0f); // TODO: some sort of time here
+		state.add(&data->animations[ANIM_IDLE], 0, 1.0f); // TODO: some sort of time here
 	else if(!want_other_dir)
-		anim_eval_add(&state, &data->animations[ANIM_WALK], walk_time, 1.0f);
+		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);
-		anim_eval_add(&state, &data->animations[ANIM_HAMMER_SWING], a, 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);
-		anim_eval_add(&state, &data->animations[ANIM_NINJA_SWING], a, 1.0f);
+		state.add(&data->animations[ANIM_NINJA_SWING], a, 1.0f);
 	}
 	
 	// do skidding
@@ -385,7 +182,7 @@ void render_player(
 			skid_sound_time = time_get();
 		}
 		
-		effect_skidtrail(
+		gameclient.effects->skidtrail(
 			position+vec2(-player.wanted_direction*6,12),
 			vec2(-player.wanted_direction*100*length(vel),-50)
 		);
@@ -403,7 +200,7 @@ void render_player(
 		
 		if(player_char->hooked_player != -1)
 		{
-			if(netobjects.local_info && player_char->hooked_player == netobjects.local_info->cid)
+			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());
@@ -477,12 +274,12 @@ void render_player(
 			{
 				gfx_quads_setrotation(-pi/2-state.attach.angle*pi*2);
 				p.x -= data->weapons.id[iw].offsetx;
-				effect_powerupshine(p+vec2(32,0), vec2(32,12));
+				gameclient.effects->powerupshine(p+vec2(32,0), vec2(32,12));
 			}
 			else
 			{
 				gfx_quads_setrotation(-pi/2+state.attach.angle*pi*2);
-				effect_powerupshine(p-vec2(32,0), vec2(32,12));
+				gameclient.effects->powerupshine(p-vec2(32,0), vec2(32,12));
 			}
 			draw_sprite(p.x, p.y, data->weapons.id[iw].visual_size);
 
@@ -582,13 +379,13 @@ void render_player(
 		gfx_quads_end();
 	}
 
-	if (client_datas[info.cid].emoticon_start != -1 && client_datas[info.cid].emoticon_start + 2 * client_tickspeed() > client_tick())
+	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() - client_datas[info.cid].emoticon_start;
-		int from_end = client_datas[info.cid].emoticon_start + 2 * client_tickspeed() - client_tick();
+		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;
 
@@ -609,7 +406,7 @@ void render_player(
 
 		gfx_setcolor(1.0f,1.0f,1.0f,a);
 		// client_datas::emoticon is an offset from the first emoticon
-		select_sprite(SPRITE_OOP + client_datas[info.cid].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();
 	}
@@ -620,9 +417,9 @@ void render_player(
 		//gfx_text_color
 		float a = 1;
 		if(config.cl_nameplates_always == 0)
-			a = clamp(1-powf(distance(local_target_pos, position)/200.0f,16.0f), 0.0f, 1.0f);
+			a = clamp(1-powf(distance(gameclient.local_target_pos, position)/200.0f,16.0f), 0.0f, 1.0f);
 			
-		const char *name = client_datas[info.cid].name;
+		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);
@@ -637,3 +434,30 @@ void render_player(
 		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/gc_skin.cpp b/src/game/client/components/skins.cpp
index e0aa2c84..eae45c4d 100644
--- a/src/game/client/gc_skin.cpp
+++ b/src/game/client/components/skins.cpp
@@ -7,20 +7,18 @@
 #include <base/math.hpp>
 
 #include <engine/e_client_interface.h>
-#include "gc_skin.hpp"
+#include "skins.hpp"
 
-enum
+SKINS::SKINS()
 {
-	MAX_SKINS=256,
-};
-
-static skin skins[MAX_SKINS] = {{0}};
-static int num_skins = 0;
+	num_skins = 0;
+}
 
-static void skinscan(const char *name, int is_dir, void *user)
+void SKINS::skinscan(const char *name, int is_dir, void *user)
 {
+	SKINS *self = (SKINS *)user;
 	int l = strlen(name);
-	if(l < 4 || is_dir || num_skins == MAX_SKINS)
+	if(l < 4 || is_dir || self->num_skins == MAX_SKINS)
 		return;
 	if(strcmp(name+l-4, ".png") != 0)
 		return;
@@ -34,7 +32,7 @@ static void skinscan(const char *name, int is_dir, void *user)
 		return;
 	}
 	
-	skins[num_skins].org_texture = gfx_load_texture_raw(info.width, info.height, info.format, info.data, info.format, 0);
+	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;
@@ -54,7 +52,7 @@ static void skinscan(const char *name, int is_dir, void *user)
 				}
 			}
 			
-		skins[num_skins].blood_color = normalize(vec3(colors[0], colors[1], colors[2]));
+		self->skins[self->num_skins].blood_color = normalize(vec3(colors[0], colors[1], colors[2]));
 	}
 	
 	// create colorless version
@@ -107,33 +105,34 @@ static void skinscan(const char *name, int is_dir, void *user)
 			}
 	}
 	
-	skins[num_skins].color_texture = gfx_load_texture_raw(info.width, info.height, info.format, info.data, info.format, 0);
+	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(skins[num_skins].name, name, min((int)sizeof(skins[num_skins].name),l-4));
-	dbg_msg("game", "load skin %s", skins[num_skins].name);
-	num_skins++;
+	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 skin_init()
+void SKINS::init()
 {
 	// load skins
-	fs_listdir("data/skins", skinscan, 0);
+	num_skins = 0;
+	fs_listdir("data/skins", skinscan, this);
 }
 
-int skin_num()
+int SKINS::num()
 {
 	return num_skins;	
 }
 
-const skin *skin_get(int index)
+const SKINS::SKIN *SKINS::get(int index)
 {
 	return &skins[index%num_skins];
 }
 
-int skin_find(const char *name)
+int SKINS::find(const char *name)
 {
 	for(int i = 0; i < num_skins; i++)
 	{
@@ -143,9 +142,6 @@ int skin_find(const char *name)
 	return -1;
 }
 
-
-
-
 // these converter functions were nicked from some random internet pages
 static float hue_to_rgb(float v1, float v2, float h)
 {
@@ -157,7 +153,7 @@ static float hue_to_rgb(float v1, float v2, float h)
    return v1;
 }
 
-vec3 hsl_to_rgb(vec3 in)
+static vec3 hsl_to_rgb(vec3 in)
 {
 	float v1, v2;
 	vec3 out;
@@ -185,7 +181,7 @@ vec3 hsl_to_rgb(vec3 in)
 	return out;
 }
 
-vec4 skin_get_color(int v)
+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);
+};
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
new file mode 100644
index 00000000..0fb6c61e
--- /dev/null
+++ b/src/game/client/gameclient.cpp
@@ -0,0 +1,548 @@
+#include <engine/e_client_interface.h>
+#include <game/generated/g_protocol.hpp>
+
+#include "gameclient.hpp"
+#include "gc_client.hpp"
+#include "components/killmessages.hpp"
+#include "components/chat.hpp"
+#include "components/motd.hpp"
+#include "components/broadcast.hpp"
+#include "components/console.hpp"
+#include "components/binds.hpp"
+#include "components/particles.hpp"
+#include "components/menus.hpp"
+#include "components/skins.hpp"
+#include "components/flow.hpp"
+#include "components/players.hpp"
+#include "components/items.hpp"
+#include "components/maplayers.hpp"
+#include "components/camera.hpp"
+#include "components/hud.hpp"
+#include "components/debughud.hpp"
+#include "components/controls.hpp"
+#include "components/effects.hpp"
+
+GAMECLIENT gameclient;
+
+// instanciate all systems
+static KILLMESSAGES killmessages;
+static CAMERA camera;
+static CHAT chat;
+static MOTD motd;
+static BROADCAST broadcast;
+static CONSOLE console;
+static BINDS binds;
+static PARTICLES particles;
+static MENUS menus;
+static SKINS skins;
+static FLOW flow;
+static HUD hud;
+static DEBUGHUD debughud;
+static CONTROLS controls;
+static EFFECTS effects;
+
+static PLAYERS players;
+static ITEMS items;
+
+static MAPLAYERS maplayers_background(MAPLAYERS::TYPE_BACKGROUND);
+static MAPLAYERS maplayers_foreground(MAPLAYERS::TYPE_FOREGROUND);
+
+
+GAMECLIENT::STACK::STACK() { num = 0; }
+void GAMECLIENT::STACK::add(class COMPONENT *component) { components[num++] = component; }
+
+void GAMECLIENT::on_init()
+{
+	// setup pointers
+	binds = &::binds;
+	console = &::console;
+	particles = &::particles;
+	menus = &::menus;
+	skins = &::skins;
+	chat = &::chat;
+	flow = &::flow;
+	camera = &::camera;
+	controls = &::controls;
+	effects = &::effects;
+	
+	// make a list of all the systems, make sure to add them in the corrent render order
+	all.add(skins);
+	all.add(effects); // doesn't render anything, just updates effects
+	all.add(particles);
+	all.add(binds);
+	all.add(controls);
+	all.add(camera);
+	all.add(particles); // doesn't render anything, just updates all the particles
+	
+	all.add(&maplayers_background); // first to render
+	all.add(&particles->render_trail);
+	all.add(&particles->render_explosions);
+	all.add(&items);
+	all.add(&players);
+	all.add(&maplayers_foreground);
+	all.add(&particles->render_general);
+	all.add(&hud);
+	all.add(&killmessages);
+	all.add(chat);
+	all.add(&broadcast);
+	all.add(&debughud);
+	all.add(&motd);
+	all.add(menus);
+	all.add(console);
+	
+	// build the input stack
+	input.add(console);
+	input.add(menus);
+	input.add(chat);
+	input.add(controls);
+	input.add(binds);
+
+	// init all components
+	for(int i = 0; i < all.num; i++)
+		all.components[i]->on_init();
+		
+	/*
+	input_stack.add_handler(console_input_special_binds, 0); // F1-Fx binds
+	input_stack.add_handler(console_input_cli, 0); // console
+	input_stack.add_handler(chat_input_handle, 0); // chat
+	//input_stack.add_handler() // ui
+	input_stack.add_handler(console_input_normal_binds, 0); // binds
+	*/
+}
+
+void GAMECLIENT::dispatch_input()
+{
+	// handle mouse movement
+	int x=0, y=0;
+	inp_mouse_relative(&x, &y);
+	for(int h = 0; h < input.num; h++)
+	{
+		if(input.components[h]->on_mousemove(x, y))
+			break;
+	}
+	
+	// handle key presses
+	for(int i = 0; i < inp_num_events(); i++)
+	{
+		INPUT_EVENT e = inp_get_event(i);
+		
+		for(int h = 0; h < input.num; h++)
+		{
+			if(input.components[h]->on_input(e))
+			{
+				//dbg_msg("", "%d char=%d key=%d flags=%d", h, e.ch, e.key, e.flags);
+				break;
+			}
+		}
+	}
+	
+	// clear all events for this frame
+	inp_clear_events();	
+}
+
+
+int GAMECLIENT::on_snapinput(int *data)
+{
+	return controls->snapinput(data);
+}
+
+
+void GAMECLIENT::on_connected()
+{
+	on_reset();	
+	
+	// send the inital info
+	send_info(true);
+}
+
+void GAMECLIENT::on_reset()
+{
+	// clear out the invalid pointers
+	last_new_predicted_tick = -1;
+	mem_zero(&gameclient.snap, sizeof(gameclient.snap));
+
+	for(int i = 0; i < MAX_CLIENTS; i++)
+	{
+		clients[i].name[0] = 0;
+		clients[i].skin_id = 0;
+		clients[i].team = 0;
+		clients[i].angle = 0;
+		clients[i].emoticon = 0;
+		clients[i].emoticon_start = -1;
+		clients[i].skin_info.texture = gameclient.skins->get(0)->color_texture;
+		clients[i].skin_info.color_body = vec4(1,1,1,1);
+		clients[i].skin_info.color_feet = vec4(1,1,1,1);
+		clients[i].update_render_info();
+	}
+	
+	for(int i = 0; i < all.num; i++)
+		all.components[i]->on_reset();
+}
+
+
+void GAMECLIENT::update_local_character_pos()
+{
+	if(config.cl_predict)
+	{
+		if(!snap.local_character || (snap.local_character->health < 0) || (snap.gameobj && snap.gameobj->game_over))
+		{
+			// don't use predicted
+		}
+		else
+			local_character_pos = mix(predicted_prev_char.pos, predicted_char.pos, client_predintratick());
+	}
+	else if(snap.local_character && snap.local_prev_character)
+	{
+		local_character_pos = mix(
+			vec2(snap.local_prev_character->x, snap.local_prev_character->y),
+			vec2(snap.local_character->x, snap.local_character->y), client_intratick());
+	}
+}
+
+void GAMECLIENT::on_render()
+{
+	// update the local character position
+	update_local_character_pos();
+	
+	// dispatch all input to systems
+	dispatch_input();
+	
+	// render all systems
+	for(int i = 0; i < all.num; i++)
+		all.components[i]->on_render();
+}
+
+void GAMECLIENT::on_message(int msgtype)
+{
+	void *rawmsg = netmsg_secure_unpack(msgtype);
+	if(!rawmsg)
+	{
+		dbg_msg("client", "dropped weird message '%s' (%d), failed on '%s'", netmsg_get_name(msgtype), msgtype, netmsg_failed_on());
+		return;
+	}
+
+	// TODO: this should be done smarter
+	for(int i = 0; i < all.num; i++)
+		all.components[i]->on_message(msgtype, rawmsg);
+}
+
+void GAMECLIENT::on_statechange(int new_state, int old_state)
+{
+	// clear out the invalid pointers
+	mem_zero(&gameclient.snap, sizeof(gameclient.snap));
+		
+	for(int i = 0; i < all.num; i++)
+		all.components[i]->on_statechange(new_state, old_state);
+}
+
+
+
+void GAMECLIENT::process_events()
+{
+	int snaptype = SNAP_CURRENT;
+	int num = snap_num_items(snaptype);
+	for(int index = 0; index < num; index++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(snaptype, index, &item);
+
+		if(item.type == NETEVENTTYPE_DAMAGEIND)
+		{
+			NETEVENT_DAMAGEIND *ev = (NETEVENT_DAMAGEIND *)data;
+			gameclient.effects->damage_indicator(vec2(ev->x, ev->y), get_direction(ev->angle));
+		}
+		else if(item.type == NETEVENTTYPE_AIRJUMP)
+		{
+			NETEVENT_COMMON *ev = (NETEVENT_COMMON *)data;
+			gameclient.effects->air_jump(vec2(ev->x, ev->y));
+		}
+		else if(item.type == NETEVENTTYPE_EXPLOSION)
+		{
+			NETEVENT_EXPLOSION *ev = (NETEVENT_EXPLOSION *)data;
+			gameclient.effects->explosion(vec2(ev->x, ev->y));
+		}
+		else if(item.type == NETEVENTTYPE_SPAWN)
+		{
+			NETEVENT_SPAWN *ev = (NETEVENT_SPAWN *)data;
+			gameclient.effects->playerspawn(vec2(ev->x, ev->y));
+		}
+		else if(item.type == NETEVENTTYPE_DEATH)
+		{
+			NETEVENT_DEATH *ev = (NETEVENT_DEATH *)data;
+			gameclient.effects->playerdeath(vec2(ev->x, ev->y), ev->cid);
+		}
+		else if(item.type == NETEVENTTYPE_SOUNDWORLD)
+		{
+			NETEVENT_SOUNDWORLD *ev = (NETEVENT_SOUNDWORLD *)data;
+			snd_play_random(CHN_WORLD, ev->soundid, 1.0f, vec2(ev->x, ev->y));
+		}
+	}
+}
+
+void GAMECLIENT::on_snapshot()
+{
+	// clear out the invalid pointers
+	mem_zero(&gameclient.snap, sizeof(gameclient.snap));
+
+	static int snapshot_count = 0;
+	snapshot_count++;
+	
+	// secure snapshot
+	{
+		int num = snap_num_items(SNAP_CURRENT);
+		for(int index = 0; index < num; index++)
+		{
+			SNAP_ITEM item;
+			void *data = snap_get_item(SNAP_CURRENT, index, &item);
+			if(netobj_validate(item.type, data, item.datasize) != 0)
+			{
+				if(config.debug)
+					dbg_msg("game", "invalidated index=%d type=%d (%s) size=%d id=%d", index, item.type, netobj_get_name(item.type), item.datasize, item.id);
+				snap_invalidate_item(SNAP_CURRENT, index);
+			}
+		}
+	}
+		
+	process_events();
+
+	if(config.dbg_stress)
+	{
+		if((client_tick()%250) == 0)
+		{
+			NETMSG_CL_SAY msg;
+			msg.team = -1;
+			msg.message = "galenskap!!!!";
+			msg.pack(MSGFLAG_VITAL);
+			client_send_msg();
+		}
+	}
+
+	// setup world view
+	{
+		// 1. fetch local player
+		// 2. set him to the center
+		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_PLAYER_INFO)
+			{
+				const NETOBJ_PLAYER_INFO *info = (const NETOBJ_PLAYER_INFO *)data;
+				
+				gameclient.clients[info->cid].team = info->team;
+				
+				if(info->local)
+				{
+					gameclient.snap.local_info = info;
+					const void *data = snap_find_item(SNAP_CURRENT, NETOBJTYPE_CHARACTER, item.id);
+					if(data)
+					{
+						gameclient.snap.local_character = (const NETOBJ_CHARACTER *)data;
+						gameclient.local_character_pos = vec2(gameclient.snap.local_character->x, gameclient.snap.local_character->y);
+
+						const void *p = snap_find_item(SNAP_PREV, NETOBJTYPE_CHARACTER, item.id);
+						if(p)
+							gameclient.snap.local_prev_character = (NETOBJ_CHARACTER *)p;
+					}
+				}
+			}
+			else if(item.type == NETOBJTYPE_GAME)
+				gameclient.snap.gameobj = (NETOBJ_GAME *)data;
+			else if(item.type == NETOBJTYPE_FLAG)
+			{
+				gameclient.snap.flags[item.id%2] = (const NETOBJ_FLAG *)data;
+			}
+		}
+	}
+
+	for(int i = 0; i < MAX_CLIENTS; i++)
+		gameclient.clients[i].update_render_info();
+
+}
+
+void GAMECLIENT::on_predict()
+{
+	CHARACTER_CORE before_prev_char = predicted_prev_char;
+	CHARACTER_CORE before_char = predicted_char;
+
+	// repredict character
+	WORLD_CORE world;
+	world.tuning = tuning;
+	int local_cid = -1;
+
+	// search for players
+	for(int i = 0; i < snap_num_items(SNAP_CURRENT); i++)
+	{
+		SNAP_ITEM item;
+		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
+		int client_id = item.id;
+
+		if(item.type == NETOBJTYPE_CHARACTER)
+		{
+			const NETOBJ_CHARACTER *character = (const NETOBJ_CHARACTER *)data;
+			gameclient.clients[client_id].predicted.world = &world;
+			world.characters[client_id] = &gameclient.clients[client_id].predicted;
+
+			gameclient.clients[client_id].predicted.read(character);
+		}
+		else if(item.type == NETOBJTYPE_PLAYER_INFO)
+		{
+			const NETOBJ_PLAYER_INFO *info = (const NETOBJ_PLAYER_INFO *)data;
+			if(info->local)
+				local_cid = client_id;
+		}
+	}
+	
+	// we can't predict without our own id
+	if(local_cid == -1)
+		return;
+
+	// predict
+	for(int tick = client_tick()+1; tick <= client_predtick(); tick++)
+	{
+		// fetch the local
+		if(tick == client_predtick() && world.characters[local_cid])
+			predicted_prev_char = *world.characters[local_cid];
+		
+		// first calculate where everyone should move
+		for(int c = 0; c < MAX_CLIENTS; c++)
+		{
+			if(!world.characters[c])
+				continue;
+
+			mem_zero(&world.characters[c]->input, sizeof(world.characters[c]->input));
+			if(local_cid == c)
+			{
+				// apply player input
+				int *input = client_get_input(tick);
+				if(input)
+					world.characters[c]->input = *((NETOBJ_PLAYER_INPUT*)input);
+			}
+
+			world.characters[c]->tick();
+		}
+
+		// move all players and quantize their data
+		for(int c = 0; c < MAX_CLIENTS; c++)
+		{
+			if(!world.characters[c])
+				continue;
+
+			world.characters[c]->move();
+			world.characters[c]->quantize();
+		}
+		
+		if(tick > last_new_predicted_tick)
+		{
+			last_new_predicted_tick = tick;
+			
+			if(local_cid != -1 && world.characters[local_cid])
+			{
+				vec2 pos = world.characters[local_cid]->pos;
+				int events = world.characters[local_cid]->triggered_events;
+				if(events&COREEVENT_GROUND_JUMP) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
+				if(events&COREEVENT_AIR_JUMP)
+				{
+					gameclient.effects->air_jump(pos);
+					snd_play_random(CHN_WORLD, SOUND_PLAYER_AIRJUMP, 1.0f, pos);
+				}
+				//if(events&COREEVENT_HOOK_LAUNCH) snd_play_random(CHN_WORLD, SOUND_HOOK_LOOP, 1.0f, pos);
+				//if(events&COREEVENT_HOOK_ATTACH_PLAYER) snd_play_random(CHN_WORLD, SOUND_HOOK_ATTACH_PLAYER, 1.0f, pos);
+				if(events&COREEVENT_HOOK_ATTACH_GROUND) snd_play_random(CHN_WORLD, SOUND_HOOK_ATTACH_GROUND, 1.0f, pos);
+				//if(events&COREEVENT_HOOK_RETRACT) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
+			}
+
+
+			/*
+			dbg_msg("predict", "%d %d %d", tick,
+				(int)world.players[c]->pos.x, (int)world.players[c]->pos.y,
+				(int)world.players[c]->vel.x, (int)world.players[c]->vel.y);*/
+		}
+		
+		if(tick == client_predtick() && world.characters[local_cid])
+			predicted_char = *world.characters[local_cid];
+	}
+	
+	if(config.debug && predicted_tick == client_predtick())
+	{
+		if(predicted_char.pos.x != before_char.pos.x ||
+			predicted_char.pos.y != before_char.pos.y)
+		{
+			dbg_msg("client", "prediction error, (%d %d) (%d %d)", 
+				(int)before_char.pos.x, (int)before_char.pos.y,
+				(int)predicted_char.pos.x, (int)predicted_char.pos.y);
+		}
+
+		if(predicted_prev_char.pos.x != before_prev_char.pos.x ||
+			predicted_prev_char.pos.y != before_prev_char.pos.y)
+		{
+			dbg_msg("client", "prediction error, prev (%d %d) (%d %d)", 
+				(int)before_prev_char.pos.x, (int)before_prev_char.pos.y,
+				(int)predicted_prev_char.pos.x, (int)predicted_prev_char.pos.y);
+		}
+	}
+	
+	predicted_tick = client_predtick();
+}
+
+void GAMECLIENT::CLIENT_DATA::update_render_info()
+{
+	render_info = skin_info;
+
+	// force team colors
+	if(gameclient.snap.gameobj && gameclient.snap.gameobj->flags&GAMEFLAG_TEAMS)
+	{
+		const int team_colors[2] = {65387, 10223467};
+		if(team >= 0 || team <= 1)
+		{
+			render_info.texture = gameclient.skins->get(skin_id)->color_texture;
+			render_info.color_body = gameclient.skins->get_color(team_colors[team]);
+			render_info.color_feet = gameclient.skins->get_color(team_colors[team]);
+		}
+	}		
+}
+
+
+
+
+void GAMECLIENT::send_switch_team(int team)
+{
+	NETMSG_CL_SETTEAM msg;
+	msg.team = team;
+	msg.pack(MSGFLAG_VITAL);
+	client_send_msg();	
+}
+
+void GAMECLIENT::send_info(bool start)
+{
+	if(start)
+	{
+		NETMSG_CL_STARTINFO msg;
+		msg.name = config.player_name;
+		msg.skin = config.player_skin;
+		msg.use_custom_color = config.player_use_custom_color;
+		msg.color_body = config.player_color_body;
+		msg.color_feet = config.player_color_feet;
+		msg.pack(MSGFLAG_VITAL|MSGFLAG_FLUSH);
+	}
+	else
+	{
+		NETMSG_CL_CHANGEINFO msg;
+		msg.name = config.player_name;
+		msg.skin = config.player_skin;
+		msg.use_custom_color = config.player_use_custom_color;
+		msg.color_body = config.player_color_body;
+		msg.color_feet = config.player_color_feet;
+		msg.pack(MSGFLAG_VITAL);
+	}
+	client_send_msg();
+}
+
+void GAMECLIENT::send_kill(int client_id)
+{
+	NETMSG_CL_KILL msg;
+	msg.pack(MSGFLAG_VITAL);
+	client_send_msg();
+}
diff --git a/src/game/client/gameclient.hpp b/src/game/client/gameclient.hpp
new file mode 100644
index 00000000..335c3695
--- /dev/null
+++ b/src/game/client/gameclient.hpp
@@ -0,0 +1,109 @@
+
+#include <base/vmath.hpp>
+#include <game/gamecore.hpp>
+#include "gc_render.hpp"
+
+class GAMECLIENT
+{
+	class STACK
+	{
+	public:
+		enum
+		{
+			MAX_COMPONENTS = 64,
+		};
+	
+		STACK();
+		void add(class COMPONENT *component);
+		
+		class COMPONENT *components[MAX_COMPONENTS];
+		int num;
+	};
+	
+	STACK all;
+	STACK input;
+	
+	void dispatch_input();
+	void process_events();
+	void update_local_character_pos();
+
+	int predicted_tick;
+	int last_new_predicted_tick;
+
+public:
+
+	vec2 local_character_pos;
+	vec2 local_target_pos;
+
+	// snap pointers
+	struct SNAPSTATE
+	{
+		const NETOBJ_CHARACTER *local_character;
+		const NETOBJ_CHARACTER *local_prev_character;
+		const NETOBJ_PLAYER_INFO *local_info;
+		const NETOBJ_FLAG *flags[2];
+		const NETOBJ_GAME *gameobj;
+
+		const NETOBJ_PLAYER_INFO *player_infos[MAX_CLIENTS];
+		const NETOBJ_PLAYER_INFO *info_by_score[MAX_CLIENTS];
+		int num_players;
+	};
+
+	SNAPSTATE snap;
+	
+	// client data
+	struct CLIENT_DATA
+	{
+		char name[64];
+		char skin_name[64];
+		int skin_id;
+		int skin_color;
+		int team;
+		int emoticon;
+		int emoticon_start;
+		CHARACTER_CORE predicted;
+		
+		TEE_RENDER_INFO skin_info; // this is what the server reports
+		TEE_RENDER_INFO render_info; // this is what we use
+		
+		float angle;
+		
+		void update_render_info();
+	};
+
+	CLIENT_DATA clients[MAX_CLIENTS];
+	
+	void on_reset();
+
+	// hooks
+	void on_connected();
+	void on_render();
+	void on_init();
+	void on_statechange(int new_state, int old_state);
+	void on_message(int msgtype);
+	void on_snapshot();
+	void on_predict();
+	int on_snapinput(int *data);
+
+	// actions
+	// TODO: move these
+	void send_switch_team(int team);
+	void send_info(bool start);
+	void send_kill(int client_id);
+	
+	// pointers to all systems
+	class CONSOLE *console;
+	class BINDS *binds;
+	class PARTICLES *particles;
+	class MENUS *menus;
+	class SKINS *skins;
+	class FLOW *flow;
+	class CHAT *chat;
+	class DAMAGEIND *damageind;
+	class CAMERA *camera;
+	class CONTROLS *controls;
+	class EFFECTS *effects;
+};
+
+extern GAMECLIENT gameclient;
+
diff --git a/src/game/client/gc_anim.hpp b/src/game/client/gc_anim.hpp
deleted file mode 100644
index f6e9aac3..00000000
--- a/src/game/client/gc_anim.hpp
+++ /dev/null
@@ -1,14 +0,0 @@
-
-struct ANIM_STATE
-{
-	ANIM_KEYFRAME body;
-	ANIM_KEYFRAME back_foot;
-	ANIM_KEYFRAME front_foot;
-	ANIM_KEYFRAME attach;
-};
-
-void anim_seq_eval(ANIM_SEQUENCE *seq, float time, ANIM_KEYFRAME *frame);
-void anim_eval(ANIMATION *anim, float time, ANIM_STATE *state);
-void anim_add_keyframe(ANIM_KEYFRAME *seq, ANIM_KEYFRAME *added, float amount);
-void anim_add(ANIM_STATE *state, ANIM_STATE *added, float amount);
-void anim_eval_add(ANIM_STATE *state, ANIMATION *anim, float time, float amount);
diff --git a/src/game/client/gc_client.cpp b/src/game/client/gc_client.cpp
index f9900ab3..2418b6ea 100644
--- a/src/game/client/gc_client.cpp
+++ b/src/game/client/gc_client.cpp
@@ -6,8 +6,6 @@
 #include <string.h>
 
 extern "C" {
-	#include <engine/e_config.h> // TODO: this shouldn't be here
-	#include <engine/client/ec_font.h> // TODO: this shouldn't be here
 	#include <engine/e_client_interface.h>
 };
 
@@ -16,70 +14,16 @@ extern "C" {
 #include "../layers.hpp"
 #include "gc_map_image.hpp"
 #include "../generated/gc_data.hpp"
-#include "gc_menu.hpp"
-#include "gc_skin.hpp"
 #include "gc_ui.hpp"
 #include "gc_client.hpp"
 #include "gc_render.hpp"
-#include "gc_anim.hpp"
-#include "gc_console.hpp"
 
-//struct data_container *data = 0;
-int64 debug_firedelay = 0;
-
-NETOBJ_PLAYER_INPUT input_data = {0};
-int input_target_lock = 0;
-int input_direction_left = 0;
-int input_direction_right = 0;
-
-int chat_mode = CHATMODE_NONE;
-bool menu_active = false;
-bool menu_game_active = false;
-int emoticon_selector_active = 0;
-int scoreboard_active = 0;
-static int emoticon_selected_emote = -1;
+#include "components/skins.hpp"
+#include "components/damageind.hpp"
+#include "gameclient.hpp"
 
 TUNING_PARAMS tuning;
 
-vec2 mouse_pos;
-vec2 local_character_pos;
-vec2 local_target_pos;
-
-/*
-const NETOBJ_PLAYER_CHARACTER *local_character = 0;
-const NETOBJ_PLAYER_CHARACTER *local_prev_character = 0;
-const NETOBJ_PLAYER_INFO *local_info = 0;
-const NETOBJ_FLAG *flags[2] = {0,0};
-const NETOBJ_GAME *gameobj = 0;
-*/
-
-SNAPSTATE netobjects;
-
-int picked_up_weapon = -1;
-
-CLIENT_DATA client_datas[MAX_CLIENTS];
-void CLIENT_DATA::update_render_info()
-{
-	render_info = skin_info;
-
-	// force team colors
-	if(netobjects.gameobj && netobjects.gameobj->gametype != GAMETYPE_DM)
-	{
-		const int team_colors[2] = {65387, 10223467};
-		if(team >= 0 || team <= 1)
-		{
-			render_info.texture = skin_get(skin_id)->color_texture;
-			render_info.color_body = skin_get_color(team_colors[team]);
-			render_info.color_feet = skin_get_color(team_colors[team]);
-		}
-	}		
-}
-
-
-// broadcasts
-char broadcast_text[1024] = {0};
-int64 broadcast_time = 0;
-
 void snd_play_random(int chn, int setid, float vol, vec2 pos)
 {
 	SOUNDSET *set = &data->sounds[setid];
@@ -101,1570 +45,3 @@ void snd_play_random(int chn, int setid, float vol, vec2 pos)
 	snd_play_at(chn, set->sounds[id].id, 0, pos.x, pos.y);
 	set->last = id;
 }
-
-
-void send_switch_team(int team)
-{
-	NETMSG_CL_SETTEAM msg;
-	msg.team = team;
-	msg.pack(MSGFLAG_VITAL);
-	client_send_msg();	
-}
-
-class damage_indicators
-{
-public:
-	int64 lastupdate;
-	struct item
-	{
-		vec2 pos;
-		vec2 dir;
-		float life;
-		float startangle;
-	};
-
-	enum
-	{
-		MAX_ITEMS=64,
-	};
-
-	damage_indicators()
-	{
-		lastupdate = 0;
-		num_items = 0;
-	}
-
-	item items[MAX_ITEMS];
-	int num_items;
-
-	item *create_i()
-	{
-		if (num_items < MAX_ITEMS)
-		{
-			item *p = &items[num_items];
-			num_items++;
-			return p;
-		}
-		return 0;
-	}
-
-	void destroy_i(item *i)
-	{
-		num_items--;
-		*i = items[num_items];
-	}
-
-	void 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 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();
-	}
-
-};
-
-static damage_indicators dmgind;
-
-void effect_damage_indicator(vec2 pos, vec2 dir)
-{
-	dmgind.create(pos, dir);
-}
-
-void render_damage_indicators()
-{
-	dmgind.render();
-}
-
-static line_input chat_input;
-static const int chat_max_lines = 10;
-
-bool chat_input_handle(INPUT_EVENT e, void *user_data)
-{
-	if(chat_mode == CHATMODE_NONE)
-		return false;
-
-	if(e.flags&INPFLAG_PRESS && (e.key == KEY_ENTER || e.key == KEY_KP_ENTER))
-	{
-		if(chat_input.get_string()[0])
-			chat_say(chat_mode == CHATMODE_ALL ? 0 : 1, chat_input.get_string());
-		chat_mode = CHATMODE_NONE;
-	}
-	else
-		chat_input.process_input(e);
-	
-	return true;
-}
-
-struct chatline
-{
-	int tick;
-	int client_id;
-	int team;
-	int name_color;
-	char name[64];
-	char text[512];
-};
-
-chatline chat_lines[chat_max_lines];
-static int chat_current_line = 0;
-
-void chat_reset()
-{
-	for(int i = 0; i < chat_max_lines; i++)
-		chat_lines[i].tick = -1000000;
-	chat_current_line = 0;
-}
-
-void chat_add_line(int client_id, int team, const char *line)
-{
-	chat_current_line = (chat_current_line+1)%chat_max_lines;
-	chat_lines[chat_current_line].tick = client_tick();
-	chat_lines[chat_current_line].client_id = client_id;
-	chat_lines[chat_current_line].team = team;
-	chat_lines[chat_current_line].name_color = -2;
-
-	if(client_id == -1) // server message
-	{
-		str_copy(chat_lines[chat_current_line].name, "*** ", sizeof(chat_lines[chat_current_line].name));
-		str_format(chat_lines[chat_current_line].text, sizeof(chat_lines[chat_current_line].text), "%s", line);
-	}
-	else
-	{
-		if(client_datas[client_id].team == -1)
-			chat_lines[chat_current_line].name_color = -1;
-
-		if(netobjects.gameobj && netobjects.gameobj->gametype != GAMETYPE_DM)
-		{
-			if(client_datas[client_id].team == 0)
-				chat_lines[chat_current_line].name_color = 0;
-			else if(client_datas[client_id].team == 1)
-				chat_lines[chat_current_line].name_color = 1;
-		}
-		
-		str_copy(chat_lines[chat_current_line].name, client_datas[client_id].name, sizeof(chat_lines[chat_current_line].name));
-		str_format(chat_lines[chat_current_line].text, sizeof(chat_lines[chat_current_line].text), ": %s", line);
-	}
-	
-	if(config.debug)
-		dbg_msg("message", "chat cid=%d team=%d line='%s'", client_id, team, line);
-	dbg_msg("chat", "%s%s", chat_lines[chat_current_line].name, chat_lines[chat_current_line].text);
-	
-}
-
-
-KILLMSG killmsgs[killmsg_max];
-int killmsg_current = 0;
-
-//bool add_trail = false;
-
-line_input::line_input()
-{
-	clear();
-}
-
-void line_input::clear()
-{
-	mem_zero(str, sizeof(str));
-	len = 0;
-	cursor_pos = 0;
-}
-
-void line_input::set(const char *string)
-{
-	str_copy(str, string, sizeof(str));
-	len = strlen(str);
-	cursor_pos = len;
-}
-
-void line_input::process_input(INPUT_EVENT e)
-{
-	if(cursor_pos > len)
-		cursor_pos = len;
-	
-	char c = e.ch;
-	int k = e.key;
-	
-	if (!(c >= 0 && c < 32))
-	{
-		if (len < sizeof(str) - 1 && cursor_pos < sizeof(str) - 1)
-		{
-			memmove(str + cursor_pos + 1, str + cursor_pos, len - cursor_pos + 1);
-			str[cursor_pos] = c;
-			cursor_pos++;
-			len++;
-		}
-	}
-	
-	if(e.flags&INPFLAG_PRESS)
-	{
-		if (k == KEY_BACKSPACE && cursor_pos > 0)
-		{
-			memmove(str + cursor_pos - 1, str + cursor_pos, len - cursor_pos + 1);
-			cursor_pos--;
-			len--;
-		}
-		else if (k == KEY_DEL && cursor_pos < len)
-		{
-			memmove(str + cursor_pos, str + cursor_pos + 1, len - cursor_pos);
-			len--;
-		}
-		else if (k == KEY_LEFT && cursor_pos > 0)
-			cursor_pos--;
-		else if (k == KEY_RIGHT && cursor_pos < len)
-			cursor_pos++;
-		else if (k == KEY_HOME)
-			cursor_pos = 0;
-		else if (k == KEY_END)
-			cursor_pos = len;
-	}
-}
-
-INPUT_STACK_HANDLER::INPUT_STACK_HANDLER()
-{
-	num_handlers = 0;
-}
-
-void INPUT_STACK_HANDLER::add_handler(CALLBACK cb, void *user)
-{
-	user_data[num_handlers] = user;
-	handlers[num_handlers++] = cb;
-}
-
-void INPUT_STACK_HANDLER::dispatch_input()
-{
-	for(int i = 0; i < inp_num_events(); i++)
-	{
-		INPUT_EVENT e = inp_get_event(i);
-		
-		for(int h = 0; h < num_handlers; h++)
-		{
-			if(handlers[h](e, user_data[h]))
-			{
-				//dbg_msg("", "%d char=%d key=%d flags=%d", h, e.ch, e.key, e.flags);
-				break;
-			}
-		}
-	}
-	
-	inp_clear_events();
-}
-
-
-INPUT_STACK_HANDLER input_stack;
-
-extern int render_popup(const char *caption, const char *text, const char *button_text);
-
-void process_events(int snaptype)
-{
-	int num = snap_num_items(snaptype);
-	for(int index = 0; index < num; index++)
-	{
-		SNAP_ITEM item;
-		const void *data = snap_get_item(snaptype, index, &item);
-
-		if(item.type == NETEVENTTYPE_DAMAGEIND)
-		{
-			NETEVENT_DAMAGEIND *ev = (NETEVENT_DAMAGEIND *)data;
-			effect_damage_indicator(vec2(ev->x, ev->y), get_direction(ev->angle));
-		}
-		else if(item.type == NETEVENTTYPE_AIRJUMP)
-		{
-			NETEVENT_COMMON *ev = (NETEVENT_COMMON *)data;
-			effect_air_jump(vec2(ev->x, ev->y));
-		}
-		else if(item.type == NETEVENTTYPE_EXPLOSION)
-		{
-			NETEVENT_EXPLOSION *ev = (NETEVENT_EXPLOSION *)data;
-			effect_explosion(vec2(ev->x, ev->y));
-		}
-		/*else if(item.type == EVENT_SMOKE)
-		{
-			EV_EXPLOSION *ev = (EV_EXPLOSION *)data;
-			vec2 p(ev->x, ev->y);
-		}*/
-		else if(item.type == NETEVENTTYPE_SPAWN)
-		{
-			NETEVENT_SPAWN *ev = (NETEVENT_SPAWN *)data;
-			effect_playerspawn(vec2(ev->x, ev->y));
-		}
-		else if(item.type == NETEVENTTYPE_DEATH)
-		{
-			NETEVENT_DEATH *ev = (NETEVENT_DEATH *)data;
-			effect_playerdeath(vec2(ev->x, ev->y), ev->cid);
-		}
-		else if(item.type == NETEVENTTYPE_SOUNDWORLD)
-		{
-			NETEVENT_SOUNDWORLD *ev = (NETEVENT_SOUNDWORLD *)data;
-			snd_play_random(CHN_WORLD, ev->soundid, 1.0f, vec2(ev->x, ev->y));
-		}
-	}
-}
-
-void clear_object_pointers()
-{
-	// clear out the invalid pointers
-	mem_zero(&netobjects, sizeof(netobjects));
-}
-
-void send_info(bool start)
-{
-	if(start)
-	{
-		NETMSG_CL_STARTINFO msg;
-		msg.name = config.player_name;
-		msg.skin = config.player_skin;
-		msg.use_custom_color = config.player_use_custom_color;
-		msg.color_body = config.player_color_body;
-		msg.color_feet = config.player_color_feet;
-		msg.pack(MSGFLAG_VITAL|MSGFLAG_FLUSH);
-	}
-	else
-	{
-		NETMSG_CL_CHANGEINFO msg;
-		msg.name = config.player_name;
-		msg.skin = config.player_skin;
-		msg.use_custom_color = config.player_use_custom_color;
-		msg.color_body = config.player_color_body;
-		msg.color_feet = config.player_color_feet;
-		msg.pack(MSGFLAG_VITAL);
-	}
-	client_send_msg();
-}
-
-void send_emoticon(int emoticon)
-{
-	NETMSG_CL_EMOTICON msg;
-	msg.emoticon = emoticon;
-	msg.pack(MSGFLAG_VITAL);
-	client_send_msg();
-}
-
-void send_kill(int client_id)
-{
-	NETMSG_CL_KILL msg;
-	msg.pack(MSGFLAG_VITAL);
-	client_send_msg();
-}
-
-void anim_seq_eval(ANIM_SEQUENCE *seq, float time, ANIM_KEYFRAME *frame)
-{
-	if(seq->num_frames == 0)
-	{
-		frame->time = 0;
-		frame->x = 0;
-		frame->y = 0;
-		frame->angle = 0;
-	}
-	else if(seq->num_frames == 1)
-	{
-		*frame = seq->frames[0];
-	}
-	else
-	{
-		//time = max(0.0f, min(1.0f, time / duration)); // TODO: use clamp
-		ANIM_KEYFRAME *frame1 = 0;
-		ANIM_KEYFRAME *frame2 = 0;
-		float blend = 0.0f;
-
-		// TODO: make this smarter.. binary search
-		for (int i = 1; i < seq->num_frames; i++)
-		{
-			if (seq->frames[i-1].time <= time && seq->frames[i].time >= time)
-			{
-				frame1 = &seq->frames[i-1];
-				frame2 = &seq->frames[i];
-				blend = (time - frame1->time) / (frame2->time - frame1->time);
-				break;
-			}
-		}
-
-		if (frame1 && frame2)
-		{
-			frame->time = time;
-			frame->x = mix(frame1->x, frame2->x, blend);
-			frame->y = mix(frame1->y, frame2->y, blend);
-			frame->angle = mix(frame1->angle, frame2->angle, blend);
-		}
-	}
-}
-
-void anim_eval(ANIMATION *anim, float time, ANIM_STATE *state)
-{
-	anim_seq_eval(&anim->body, time, &state->body);
-	anim_seq_eval(&anim->back_foot, time, &state->back_foot);
-	anim_seq_eval(&anim->front_foot, time, &state->front_foot);
-	anim_seq_eval(&anim->attach, time, &state->attach);
-}
-
-void anim_add_keyframe(ANIM_KEYFRAME *seq, ANIM_KEYFRAME *added, float amount)
-{
-	seq->x += added->x*amount;
-	seq->y += added->y*amount;
-	seq->angle += added->angle*amount;
-}
-
-void anim_add(ANIM_STATE *state, ANIM_STATE *added, float amount)
-{
-	anim_add_keyframe(&state->body, &added->body, amount);
-	anim_add_keyframe(&state->back_foot, &added->back_foot, amount);
-	anim_add_keyframe(&state->front_foot, &added->front_foot, amount);
-	anim_add_keyframe(&state->attach, &added->attach, amount);
-}
-
-void anim_eval_add(ANIM_STATE *state, ANIMATION *anim, float time, float amount)
-{
-	ANIM_STATE add;
-	anim_eval(anim, time, &add);
-	anim_add(state, &add, amount);
-}
-
-static void 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);
-	}
-}
-
-static vec2 emoticon_selector_mouse;
-
-void emoticon_selector_render()
-{
-	int x, y;
-	inp_mouse_relative(&x, &y);
-
-	emoticon_selector_mouse.x += x;
-	emoticon_selector_mouse.y += y;
-
-	if (length(emoticon_selector_mouse) > 140)
-		emoticon_selector_mouse = normalize(emoticon_selector_mouse) * 140;
-
-	float selected_angle = get_angle(emoticon_selector_mouse) + 2*pi/24;
-	if (selected_angle < 0)
-		selected_angle += 2*pi;
-
-	if (length(emoticon_selector_mouse) > 100)
-		emoticon_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 = emoticon_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(emoticon_selector_mouse.x+screen.w/2,emoticon_selector_mouse.y+screen.h/2,24,24);
-    gfx_quads_end();
-}
-
-void 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(netobjects.gameobj && netobjects.gameobj->time_limit)
-	{
-		char buf[64];
-		str_format(buf, sizeof(buf), "Time Limit: %d min", netobjects.gameobj->time_limit);
-		gfx_text(0, x+w/2, y, 24.0f, buf, -1);
-	}
-	if(netobjects.gameobj && netobjects.gameobj->score_limit)
-	{
-		char buf[64];
-		str_format(buf, sizeof(buf), "Score Limit: %d", netobjects.gameobj->score_limit);
-		gfx_text(0, x+40, y, 24.0f, buf, -1);
-	}
-}
-
-void 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, client_datas[info->cid].name);
-				count++;
-			}
-		}
-	}
-	
-	gfx_text(0, x+10, y, 32, buffer, (int)w-20);
-}
-
-void render_scoreboard(float x, float y, float w, int team, const char *title)
-{
-	ANIM_STATE idlestate;
-	anim_eval(&data->animations[ANIM_BASE], 0, &idlestate);
-	anim_eval_add(&idlestate, &data->animations[ANIM_IDLE], 0, 1.0f);
-
-	//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(netobjects.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(netobjects.gameobj)
-		{
-			char buf[128];
-			int score = team ? netobjects.gameobj->teamscore_blue : netobjects.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, client_datas[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((netobjects.flags[0] && netobjects.flags[0]->carried_by == info->cid) ||
-			(netobjects.flags[1] && netobjects.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(&idlestate, &client_datas[info->cid].render_info, EMOTE_NORMAL, vec2(1,0), vec2(x+90, y+28));
-
-		
-		y += 50.0f;
-	}
-}
-/*
-static int do_input(int *v, int key)
-{
-	*v += inp_key_presses(key) + inp_key_releases(key);
-	if((*v&1) != inp_key_state(key))
-		(*v)++;
-	*v &= INPUT_STATE_MASK;
-	
-	return (*v&1);
-}*/
-
-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();
-}
-
-void chat_enable_mode(int team)
-{
-	if(chat_mode == CHATMODE_NONE)
-	{
-		if(team)
-			chat_mode = CHATMODE_TEAM;
-		else
-			chat_mode = CHATMODE_ALL;
-		
-		chat_input.clear();
-		inp_clear_events();
-	}
-}
-
-void render_game()
-{
-	// update the effects
-	effects_update();
-	particle_update(client_frametime());
-
-	
-	float width = 400*3.0f*gfx_screenaspect();
-	float height = 400*3.0f;
-
-	bool spectate = false;
-
-	if(config.cl_predict)
-	{
-		if(!netobjects.local_character || (netobjects.local_character->health < 0) || (netobjects.gameobj && netobjects.gameobj->game_over))
-		{
-			// don't use predicted
-		}
-		else
-			local_character_pos = mix(predicted_prev_char.pos, predicted_char.pos, client_predintratick());
-	}
-	else if(netobjects.local_character && netobjects.local_prev_character)
-	{
-		local_character_pos = mix(
-			vec2(netobjects.local_prev_character->x, netobjects.local_prev_character->y),
-			vec2(netobjects.local_character->x, netobjects.local_character->y), client_intratick());
-	}
-	
-	if(netobjects.local_info && netobjects.local_info->team == -1)
-		spectate = true;
-
-	ANIM_STATE idlestate;
-	anim_eval(&data->animations[ANIM_BASE], 0, &idlestate);
-	anim_eval_add(&idlestate, &data->animations[ANIM_IDLE], 0, 1.0f);
-
-	if(inp_key_down(KEY_ESC))
-	{
-		if(server_motd_time)
-			server_motd_time = 0;
-		else if (chat_mode)
-			chat_mode = CHATMODE_NONE;
-		else if(!console_active())
-		{
-			menu_active = !menu_active;
-			if(menu_active)
-				menu_game_active = true;
-		}
-	}
-
-	// make sure to send our info again if the menu closes	
-	static bool menu_was_active = false;
-	if(menu_active)
-		menu_was_active = true;
-	else if(menu_was_active)
-	{
-		send_info(false);
-		menu_was_active = false;
-	}
-
-	//
-	float camera_max_distance = 200.0f;
-	float deadzone = config.cl_mouse_deadzone;
-	float follow_factor = config.cl_mouse_followfactor/100.0f;
-	float mouse_max = min(camera_max_distance/follow_factor + deadzone, (float)config.cl_mouse_max_distance);
-	vec2 camera_offset(0, 0);
-
-	// fetch new input
-	if(!menu_active)
-	{
-		int x, y;
-		if(!emoticon_selector_active)
-		{
-			inp_mouse_relative(&x, &y);
-			mouse_pos += vec2(x, y);
-		}
-		
-		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;
-		}
-		else
-		{
-			float l = length(mouse_pos);
-			
-			if(l > mouse_max)
-			{
-				mouse_pos = normalize(mouse_pos)*mouse_max;
-				l = mouse_max;
-			}
-
-			float offset_amount = max(l-deadzone, 0) * follow_factor;
-			if(l > 0.0001f) // make sure that this isn't 0
-				camera_offset = normalize(mouse_pos)*offset_amount;
-		}
-	}
-
-	// set listner pos
-	if(spectate)
-	{
-		local_target_pos = mouse_pos;
-		snd_set_listener_pos(mouse_pos.x, mouse_pos.y);
-	}
-	else
-	{
-		local_target_pos = local_character_pos + mouse_pos;
-		snd_set_listener_pos(local_character_pos.x, local_character_pos.y);
-	}
-
-	// center at char but can be moved when mouse is far away
-	/*
-	float offx = 0, offy = 0;
-	if (config.cl_dynamic_camera)
-	{
-		if(mouse_pos.x > deadzone) offx = mouse_pos.x-deadzone;
-		if(mouse_pos.x <-deadzone) offx = mouse_pos.x+deadzone;
-		if(mouse_pos.y > deadzone) offy = mouse_pos.y-deadzone;
-		if(mouse_pos.y <-deadzone) offy = mouse_pos.y+deadzone;
-		offx = offx*2/3;
-		offy = offy*2/3;
-	}*/
-
-	// render the world
-	float zoom = 1.0f;
-
-	if(spectate)
-		render_world(mouse_pos.x, mouse_pos.y, zoom);
-	else
-	{
-		render_world(local_character_pos.x+camera_offset.x, local_character_pos.y+camera_offset.y, zoom);
-
-		// draw screen box
-		if(0)
-		{
-			gfx_texture_set(-1);
-			gfx_blend_normal();
-			gfx_lines_begin();
-				float cx = local_character_pos.x+camera_offset.x;
-				float cy = local_character_pos.y+camera_offset.y;
-				float w = 400*3/2;
-				float h = 300*3/2;
-				gfx_lines_draw(cx-w,cy-h,cx+w,cy-h);
-				gfx_lines_draw(cx+w,cy-h,cx+w,cy+h);
-				gfx_lines_draw(cx+w,cy+h,cx-w,cy+h);
-				gfx_lines_draw(cx-w,cy+h,cx-w,cy-h);
-			gfx_lines_end();
-		}
-	}
-
-
-	// pseudo format
-	// ZOOM ZOOM
-	/*
-	float zoom = 3.0;
-
-	// DEBUG TESTING
-	if(zoom > 3.01f)
-	{
-		gfx_clear_mask(0);
-
-		gfx_texture_set(-1);
-		gfx_blend_normal();
-
-		gfx_mask_op(MASK_NONE, 1);
-
-		gfx_quads_begin();
-		gfx_setcolor(0.65f,0.78f,0.9f,1.0f);
-
-		float fov;
-		if (zoom > 3.01f)
-			fov = pi * (zoom - 3.0f) / 6.0f;
-		else
-			fov = pi / 6.0f;
-
-		float fade = 0.7f;
-
-
-		float a = get_angle(normalize(vec2(mouse_pos.x, mouse_pos.y)));
-		vec2 d = get_dir(a);
-		vec2 d0 = get_dir(a-fov/2.0f);
-		vec2 d1 = get_dir(a+fov/2.0f);
-
-		vec2 cd0 = get_dir(a-(fov*fade)/2.0f); // center direction
-		vec2 cd1 = get_dir(a+(fov*fade)/2.0f);
-
-		vec2 p0n = local_character_pos + d0*32.0f;
-		vec2 p1n = local_character_pos + d1*32.0f;
-		vec2 p0f = local_character_pos + d0*1000.0f;
-		vec2 p1f = local_character_pos + d1*1000.0f;
-
-		vec2 cn = local_character_pos + d*32.0f;
-		vec2 cf = local_character_pos + d*1000.0f;
-
-		vec2 cp0n = local_character_pos + cd0*32.0f;
-		vec2 cp0f = local_character_pos + cd0*1000.0f;
-		vec2 cp1n = local_character_pos + cd1*32.0f;
-		vec2 cp1f = local_character_pos + cd1*1000.0f;
-
-		gfx_quads_draw_freeform(
-			p0n.x,p0n.y,
-			p1n.x,p1n.y,
-			p0f.x,p0f.y,
-			p1f.x,p1f.y);
-		gfx_quads_end();
-
-		gfx_mask_op(MASK_SET, 0);
-
-		render_world(local_character_pos.x+offx, local_character_pos.y+offy, 2.0f);
-
-		gfx_mask_op(MASK_NONE, 0);
-
-		mapscreen_to_world(local_character_pos.x+offx, local_character_pos.y+offy, 1.0f);
-
-		gfx_texture_set(-1);
-		gfx_blend_normal();
-		gfx_quads_begin();
-		gfx_setcolor(0.5f,0.9f,0.5f,0.25f);
-		float r=0.5f, g=1.0f, b=0.5f;
-		float r2=r*0.25f, g2=g*0.25f, b2=b*0.25f;
-
-		gfx_setcolor(r,g,b,0.2f);
-		gfx_quads_draw_freeform(
-			cn.x,cn.y,
-			cn.x,cn.y,
-			cp0f.x,cp0f.y,
-			cp1f.x,cp1f.y);
-
-		gfx_setcolorvertex(0, r, g, b, 0.2f);
-		gfx_setcolorvertex(1, r2, g2, b2, 0.9f);
-		gfx_setcolorvertex(2, r, g, b, 0.2f);
-		gfx_setcolorvertex(3, r2, g2, b2, 0.9f);
-		gfx_quads_draw_freeform(
-			cn.x,cn.y,
-			p0n.x,p0n.y,
-			cp0f.x,cp0f.y,
-			p0f.x,p0f.y);
-
-		gfx_quads_draw_freeform(
-			cn.x,cn.y,
-			p1n.x,p1n.y,
-			cp1f.x,cp1f.y,
-			p1f.x,p1f.y);
-
-		gfx_quads_end();
-	}*/
-
-	if(netobjects.local_character && !spectate && !(netobjects.gameobj && netobjects.gameobj->game_over))
-	{
-		gfx_texture_set(data->images[IMAGE_GAME].id);
-		gfx_quads_begin();
-
-		// render cursor
-		if (!menu_active && !emoticon_selector_active)
-		{
-			select_sprite(data->weapons.id[netobjects.local_character->weapon%NUM_WEAPONS].sprite_cursor);
-			float cursorsize = 64;
-			draw_sprite(local_target_pos.x, local_target_pos.y, cursorsize);
-		}
-		
-		float x = 5;
-		float y = 5;
-
-		// render ammo count
-		// render gui stuff
-		gfx_quads_end();
-		gfx_quads_begin();
-		gfx_mapscreen(0,0,300*gfx_screenaspect(),300);
-		
-		// if weaponstage is active, put a "glow" around the stage ammo
-		select_sprite(data->weapons.id[netobjects.local_character->weapon%NUM_WEAPONS].sprite_proj);
-		for (int i = 0; i < min(netobjects.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 < netobjects.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 < netobjects.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();
-	}
-
-	// render kill messages
-	{
-		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, client_datas[killmsgs[r].killer].name, -1);
-			float victimname_w = gfx_text_width(0, font_size, client_datas[killmsgs[r].victim].name, -1);
-
-			float x = startx;
-
-			// render victim name
-			x -= victimname_w;
-			gfx_text(0, x, y, font_size, client_datas[killmsgs[r].victim].name, -1);
-
-			// render victim tee
-			x -= 24.0f;
-			
-			if(netobjects.gameobj && netobjects.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(client_datas[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(&idlestate, &client_datas[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(netobjects.gameobj && netobjects.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(client_datas[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(&idlestate, &client_datas[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, client_datas[killmsgs[r].killer].name, -1);
-			}
-
-			y += 44;
-		}
-	}
-
-	// render chat
-	{
-		gfx_mapscreen(0,0,300*gfx_screenaspect(),300);
-		float x = 10.0f;
-		float y = 300.0f-30.0f;
-		if(chat_mode != CHATMODE_NONE)
-		{
-			// render chat input
-			TEXT_CURSOR cursor;
-			gfx_text_set_cursor(&cursor, x, y, 8.0f, TEXTFLAG_RENDER);
-			cursor.line_width = 300.0f;
-			
-			if(chat_mode == CHATMODE_ALL)
-				gfx_text_ex(&cursor, "All: ", -1);
-			else if(chat_mode == CHATMODE_TEAM)
-				gfx_text_ex(&cursor, "Team: ", -1);
-			else
-				gfx_text_ex(&cursor, "Chat: ", -1);
-				
-			gfx_text_ex(&cursor, chat_input.get_string(), chat_input.cursor_offset());
-			TEXT_CURSOR marker = cursor;
-			gfx_text_ex(&marker, "|", -1);
-			gfx_text_ex(&cursor, chat_input.get_string()+chat_input.cursor_offset(), -1);
-		}
-
-		y -= 8;
-
-		int i;
-		for(i = 0; i < chat_max_lines; i++)
-		{
-			int r = ((chat_current_line-i)+chat_max_lines)%chat_max_lines;
-			if(client_tick() > chat_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, chat_lines[r].name, -1);
-			gfx_text_ex(&cursor, chat_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(chat_lines[r].client_id == -1)
-				gfx_text_color(1,1,0.5f,1); // system
-			else if(chat_lines[r].team)
-				gfx_text_color(0.45f,0.9f,0.45f,1); // team message
-			else if(chat_lines[r].name_color == 0)
-				gfx_text_color(1.0f,0.5f,0.5f,1); // red
-			else if(chat_lines[r].name_color == 1)
-				gfx_text_color(0.7f,0.7f,1.0f,1); // blue
-			else if(chat_lines[r].name_color == -1)
-				gfx_text_color(0.75f,0.5f,0.75f, 1); // spectator
-				
-			// render name
-			gfx_text_ex(&cursor, chat_lines[r].name, -1);
-
-			// render line
-			gfx_text_color(1,1,1,1);
-			if(chat_lines[r].client_id == -1)
-				gfx_text_color(1,1,0.5f,1); // system
-			else if(chat_lines[r].team)
-				gfx_text_color(0.65f,1,0.65f,1); // team message
-
-			gfx_text_ex(&cursor, chat_lines[r].text, -1);
-		}
-
-		gfx_text_color(1,1,1,1);
-	}
-
-	// render goals
-	if(netobjects.gameobj)
-	{
-		int gametype = netobjects.gameobj->gametype;
-		
-		float whole = 300*gfx_screenaspect();
-		float half = whole/2.0f;
-		
-		gfx_mapscreen(0,0,300*gfx_screenaspect(),300);
-		if(!netobjects.gameobj->sudden_death)
-		{
-			char buf[32];
-			int time = 0;
-			if(netobjects.gameobj->time_limit)
-			{
-				time = netobjects.gameobj->time_limit*60 - ((client_tick()-netobjects.gameobj->round_start_tick)/client_tickspeed());
-
-				if(netobjects.gameobj->game_over)
-					time  = 0;
-			}
-			else
-				time = (client_tick()-netobjects.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(netobjects.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(!(netobjects.gameobj && netobjects.gameobj->game_over) && (gametype == GAMETYPE_TDM || gametype == GAMETYPE_CTF))
-		{
-			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?netobjects.gameobj->teamscore_blue:netobjects.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(netobjects.flags[t])
-					{
- 						if(netobjects.flags[t]->carried_by == -2 || (netobjects.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(netobjects.flags[t]->carried_by >= 0)
-						{
-							int id = netobjects.flags[t]->carried_by%MAX_CLIENTS;
-							const char *name = client_datas[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 = client_datas[id].render_info;
-							info.size = 18.0f;
-							
-							render_tee(&idlestate, &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(netobjects.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 = netobjects.gameobj->warmup/SERVER_TICK_SPEED;
-			if(seconds < 5)
-				str_format(buf, sizeof(buf), "%d.%d", seconds, (netobjects.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);
-		}
-	}
-
-	if (menu_active)
-	{
-		menu_render();
-		return;
-	}
-
-	// do emoticon
-	if(emoticon_selector_active)
-		emoticon_selector_render();
-	else
-	{
-		emoticon_selector_mouse = vec2(0,0);	
-	
-		if(emoticon_selected_emote != -1)
-		{
-			send_emoticon(emoticon_selected_emote);
-			emoticon_selected_emote = -1;
-		}
-	}
-	
-	// render debug stuff
-	
-	{
-		float w = 300*gfx_screenaspect();
-		gfx_mapscreen(0, 0, w, 300);
-
-		char buf[512];
-		if(config.cl_showfps)
-		{
-			str_format(buf, sizeof(buf), "%d", (int)(1.0f/client_frametime()));
-			gfx_text(0, w-10-gfx_text_width(0,12,buf,-1), 10, 12, buf, -1);
-		}
-	}
-	
-	if(config.debug && netobjects.local_character && netobjects.local_prev_character)
-	{
-		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(netobjects.local_character->vx/256.0f, netobjects.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(),
-			netobjects.local_character->x,
-			netobjects.local_character->y
-		);
-		gfx_text(0, 150, 50, 12, buf, -1);
-	}
-
-
-	bool do_scoreboard = false;
-
-	// if we are dead
-	if(!spectate && (!netobjects.local_character || netobjects.local_character->health < 0))
-		do_scoreboard = true;
-	
-	// if we the game is over
-	if(netobjects.gameobj && netobjects.gameobj->game_over)
-		do_scoreboard = true;
-	
-	// showing motd, skip it
-	if(time_get() < server_motd_time)
-		do_scoreboard = false;
-	
-	// always show if we really want
-	if(scoreboard_active)
-	{
-		server_motd_time = 0; // disables the motd
-		do_scoreboard = true;
-	}
-		
-	// render motd
-	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));
-	}
-
-	// render scoreboard
-	if(do_scoreboard)
-	{
-		gfx_mapscreen(0, 0, width, height);
-
-		float w = 650.0f;
-
-		if(netobjects.gameobj && netobjects.gameobj->gametype == GAMETYPE_DM)
-		{
-			render_scoreboard(width/2-w/2, 150.0f, w, 0, 0);
-			//render_scoreboard(gameobj, 0, 0, -1, 0);
-		}
-		else
-		{
-				
-			if(netobjects.gameobj && netobjects.gameobj->game_over)
-			{
-				const char *text = "DRAW!";
-				if(netobjects.gameobj->teamscore_red > netobjects.gameobj->teamscore_blue)
-					text = "Red Team Wins!";
-				else if(netobjects.gameobj->teamscore_blue > netobjects.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);
-	}
-	
-	{
-		gfx_mapscreen(0, 0, 300*gfx_screenaspect(), 300);
-
-		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);
-		}
-		
-		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);
-		}
-		
-		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);
-		}
-		
-		// render tuning debugging
-		if(config.dbg_tuning)
-		{
-			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);
-	}
-	
-}
-
-
-extern "C" const char *modc_getitemname(int type)
-{
-	return netobj_get_name(type);
-}
diff --git a/src/game/client/gc_client.hpp b/src/game/client/gc_client.hpp
index 2b6ccd46..371338ff 100644
--- a/src/game/client/gc_client.hpp
+++ b/src/game/client/gc_client.hpp
@@ -14,50 +14,12 @@ enum
 	CHN_GLOBAL,
 };
 
-//extern struct data_container *data;
-
-extern vec2 mouse_pos;
-extern vec2 local_character_pos;
-extern vec2 local_target_pos;
-
-// snap pointers
-struct SNAPSTATE
-{
-	const NETOBJ_CHARACTER *local_character;
-	const NETOBJ_CHARACTER *local_prev_character;
-	const NETOBJ_PLAYER_INFO *local_info;
-	const NETOBJ_FLAG *flags[2];
-	const NETOBJ_GAME *gameobj;
-
-	const NETOBJ_PLAYER_INFO *player_infos[MAX_CLIENTS];
-	const NETOBJ_PLAYER_INFO *info_by_score[MAX_CLIENTS];
-	int num_players;
-};
-
-extern SNAPSTATE netobjects;
-
-/*
-extern const NETOBJ_PLAYER_CHARACTER *local_character;
-extern const NETOBJ_PLAYER_CHARACTER *local_prev_character;
-extern const NETOBJ_PLAYER_INFO *local_info;
-extern const NETOBJ_FLAG *flags[2];
-extern const NETOBJ_GAME *gameobj;
-* */
-
 extern TUNING_PARAMS tuning;
 
 // predicted players
 extern CHARACTER_CORE predicted_prev_char;
 extern CHARACTER_CORE predicted_char;
 
-// input
-extern NETOBJ_PLAYER_INPUT input_data;
-extern int input_direction_left;
-extern int input_direction_right;
-
-// debug
-extern int64 debug_firedelay;
-
 // extra projs
 enum
 {
@@ -66,211 +28,49 @@ enum
 
 extern NETOBJ_PROJECTILE extraproj_projectiles[MAX_EXTRA_PROJECTILES];
 extern int extraproj_num;
-
 void extraproj_reset();
 
-// chat
-enum
-{
-	CHATMODE_NONE=0,
-	CHATMODE_ALL,
-	CHATMODE_TEAM,
-};
-
-extern int chat_mode;
-void chat_add_line(int client_id, int team, const char *line);
-void chat_reset();
-bool chat_input_handle(INPUT_EVENT e, void *user_data);
-
-// broadcasts
-extern char broadcast_text[1024];
-extern int64 broadcast_time;
-
-// motd
-extern int64 server_motd_time;
-extern char server_motd[900]; // FUGLY
-
-// line input helter
-class line_input
-{
-	char str[256];
-	unsigned len;
-	unsigned cursor_pos;
-public:
-	class callback
-	{
-	public:
-		virtual ~callback() {}
-		virtual bool event(INPUT_EVENT e) = 0;
-	};
-
-	line_input();
-	void clear();
-	void process_input(INPUT_EVENT e);
-	void set(const char *string);
-	const char *get_string() const { return str; }
-	int get_length() const { return len; }
-	unsigned cursor_offset() const { return cursor_pos; }
-};
-
-class INPUT_STACK_HANDLER
-{
-public:
-	typedef bool (*CALLBACK)(INPUT_EVENT e, void *user);
-	
-	INPUT_STACK_HANDLER();
-	void add_handler(CALLBACK cb, void *user_data);
-	void dispatch_input();
-	
-private:
-	enum
-	{
-		MAX_HANDLERS=16
-	};
-	
-	CALLBACK handlers[MAX_HANDLERS];
-	void *user_data[MAX_HANDLERS];
-	int num_handlers;
-};
-
-extern INPUT_STACK_HANDLER input_stack;
-
-
-extern int emoticon_selector_active; // TODO: ugly
-extern int scoreboard_active; // TODO: ugly
-
-// client data
-struct CLIENT_DATA
-{
-	char name[64];
-	char skin_name[64];
-	int skin_id;
-	int skin_color;
-	int team;
-	int emoticon;
-	int emoticon_start;
-	CHARACTER_CORE predicted;
-	
-	TEE_RENDER_INFO skin_info; // this is what the server reports
-	TEE_RENDER_INFO render_info; // this is what we use
-	
-	float angle;
-	
-	void update_render_info();
-};
-
-extern CLIENT_DATA client_datas[MAX_CLIENTS];
-
-// 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;
-};
-
-const int killmsg_max = 5;
-extern KILLMSG killmsgs[killmsg_max];
-extern int killmsg_current;
-
-//
-void send_switch_team(int team);
-
 // various helpers
 void snd_play_random(int chn, int setid, float vol, vec2 pos);
-void process_events(int snaptype);
-void clear_object_pointers();
-void reset_projectile_particles();
-void send_info(bool start);
-void send_emoticon(int emoticon);
 
-void chat_say(int team, const char *line);
 void chat_enable_mode(int team);
 
 inline vec2 random_dir() { return normalize(vec2(frandom()-0.5f, frandom()-0.5f)); }
 
-
-// effects
-void effects_update();
-
-void effect_bullettrail(vec2 pos);
-void effect_smoketrail(vec2 pos, vec2 vel);
-void effect_skidtrail(vec2 pos, vec2 vel);
-void effect_explosion(vec2 pos);
-void effect_air_jump(vec2 pos);
-void effect_damage_indicator(vec2 pos, vec2 dir);
-void effect_playerspawn(vec2 pos);
-void effect_playerdeath(vec2 pos, int cid);
-void effect_powerupshine(vec2 pos, vec2 size);
-
-// particles
-struct PARTICLE
+inline float hue_to_rgb(float v1, float v2, float h)
 {
-	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;
-};
-
-enum
+   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;
+}
+
+inline vec3 hsl_to_rgb(vec3 in)
 {
-	PARTGROUP_PROJECTILE_TRAIL=0,
-	PARTGROUP_EXPLOSIONS,
-	PARTGROUP_GENERAL,
-	NUM_PARTGROUPS
-};
+	float v1, v2;
+	vec3 out;
 
-void particle_add(int group, PARTICLE *part);
-void particle_render(int group);
-void particle_update(float time_passed);
-void particle_reset();
+	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);
 
-// flow grid
-vec2 flow_get(vec2 pos);
-void flow_add(vec2 pos, vec2 vel, float size);
-void flow_dbg_render();
-void flow_init();
-void flow_update();
+		v1 = 2 * in.l - v2;
 
-//
-void binds_default();
-void binds_save();
-void binds_set(int keyid, const char *str);
-const char *binds_get(int keyid);
+		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;
+}
diff --git a/src/game/client/gc_console.hpp b/src/game/client/gc_console.hpp
deleted file mode 100644
index 0f5e7b9f..00000000
--- a/src/game/client/gc_console.hpp
+++ /dev/null
@@ -1,19 +0,0 @@
-#ifndef _GC_CONSOLE_H
-#define _GC_CONSOLE_H
-
-#include <engine/e_client_interface.h>
-
-bool console_input_cli(INPUT_EVENT e, void *user_data);
-bool console_input_special_binds(INPUT_EVENT e, void *user_data);
-bool console_input_normal_binds(INPUT_EVENT e, void *user_data);
-
-//void console_handle_input();
-
-void console_clear(int type);
-void console_toggle(int tpye);
-void console_render();
-int console_active();
-void client_console_init();
-void console_rcon_print(const char *line);
-
-#endif
diff --git a/src/game/client/gc_effects.cpp b/src/game/client/gc_effects.cpp
index 387e1471..f8c4e2c8 100644
--- a/src/game/client/gc_effects.cpp
+++ b/src/game/client/gc_effects.cpp
@@ -1,8 +1,13 @@
 #include <engine/e_client_interface.h>
 #include "gc_client.hpp"
-#include "gc_skin.hpp"
 #include "../generated/gc_data.hpp"
 
+#include "components/particles.hpp"
+#include "components/skins.hpp"
+#include "components/flow.hpp"
+#include "components/damageind.hpp"
+#include "gameclient.hpp"
+
 static bool add_50hz = false;
 static bool add_100hz = false;
 
@@ -21,10 +26,15 @@ void effect_air_jump(vec2 pos)
 	p.gravity = 500;
 	p.friction = 0.7f;
 	p.flow_affected = 0.0f;
-	particle_add(PARTGROUP_GENERAL, &p);
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
 
 	p.pos = pos + vec2(6.0f, 16.0f);
-	particle_add(PARTGROUP_GENERAL, &p);
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
+}
+
+void effect_damage_indicator(vec2 pos, vec2 dir)
+{
+	gameclient.damageind->create(pos, dir);
 }
 
 void effect_powerupshine(vec2 pos, vec2 size)
@@ -45,7 +55,7 @@ void effect_powerupshine(vec2 pos, vec2 size)
 	p.gravity = 500;
 	p.friction = 0.9f;
 	p.flow_affected = 0.0f;
-	particle_add(PARTGROUP_GENERAL, &p);
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
 }
 
 void effect_smoketrail(vec2 pos, vec2 vel)
@@ -63,7 +73,7 @@ void effect_smoketrail(vec2 pos, vec2 vel)
 	p.end_size = 0;
 	p.friction = 0.7;
 	p.gravity = frandom()*-500.0f;
-	particle_add(PARTGROUP_PROJECTILE_TRAIL, &p);
+	gameclient.particles->add(PARTICLES::GROUP_PROJECTILE_TRAIL, &p);
 }
 
 
@@ -83,7 +93,7 @@ void effect_skidtrail(vec2 pos, vec2 vel)
 	p.friction = 0.7f;
 	p.gravity = frandom()*-500.0f;
 	p.color = vec4(0.75f,0.75f,0.75f,1.0f);
-	particle_add(PARTGROUP_GENERAL, &p);	
+	gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);	
 }
 
 void effect_bullettrail(vec2 pos)
@@ -99,7 +109,7 @@ void effect_bullettrail(vec2 pos)
 	p.start_size = 8.0f;
 	p.end_size = 0;
 	p.friction = 0.7f;
-	particle_add(PARTGROUP_PROJECTILE_TRAIL, &p);
+	gameclient.particles->add(PARTICLES::GROUP_PROJECTILE_TRAIL, &p);
 }
 
 void effect_playerspawn(vec2 pos)
@@ -119,7 +129,7 @@ void effect_playerspawn(vec2 pos)
 		p.gravity = frandom()*-400.0f;
 		p.friction = 0.7f;
 		p.color = vec4(0xb5/255.0f, 0x50/255.0f, 0xcb/255.0f, 1.0f);
-		particle_add(PARTGROUP_GENERAL, &p);
+		gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
 		
 	}
 }
@@ -130,7 +140,7 @@ void effect_playerdeath(vec2 pos, int cid)
 
 	if(cid >= 0)	
 	{
-		const skin *s = skin_get(client_datas[cid].skin_id);
+		const SKINS::SKIN *s = gameclient.skins->get(gameclient.clients[cid].skin_id);
 		if(s)
 			blood_color = s->blood_color;
 	}
@@ -151,7 +161,7 @@ void effect_playerdeath(vec2 pos, int cid)
 		p.friction = 0.8f;
 		vec3 c = blood_color * (0.75f + frandom()*0.25f);
 		p.color = vec4(c.r, c.g, c.b, 0.75f);
-		particle_add(PARTGROUP_GENERAL, &p);
+		gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
 	}
 }
 
@@ -166,7 +176,7 @@ void effect_explosion(vec2 pos)
 				continue;
 			
 			float a = 1 - (length(vec2(x,y)) / length(vec2(8,8)));
-			flow_add(pos+vec2(x,y)*16, normalize(vec2(x,y))*5000.0f*a, 10.0f);
+			gameclient.flow->add(pos+vec2(x,y)*16, normalize(vec2(x,y))*5000.0f*a, 10.0f);
 		}
 		
 	// add the explosion
@@ -178,7 +188,7 @@ void effect_explosion(vec2 pos)
 	p.start_size = 150.0f;
 	p.end_size = 0;
 	p.rot = frandom()*pi*2;
-	particle_add(PARTGROUP_EXPLOSIONS, &p);
+	gameclient.particles->add(PARTICLES::GROUP_EXPLOSIONS, &p);
 	
 	// add the smoke
 	for(int i = 0; i < 24; i++)
@@ -194,7 +204,7 @@ void effect_explosion(vec2 pos)
 		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());
-		particle_add(PARTGROUP_GENERAL, &p);
+		gameclient.particles->add(PARTICLES::GROUP_GENERAL, &p);
 	}
 }
 
@@ -220,6 +230,5 @@ void effects_update()
 		add_50hz = false;
 		
 	if(add_50hz)
-		flow_update();
-		
+		gameclient.flow->update();
 }
diff --git a/src/game/client/gc_hooks.cpp b/src/game/client/gc_hooks.cpp
index 8db0995f..bef6f508 100644
--- a/src/game/client/gc_hooks.cpp
+++ b/src/game/client/gc_hooks.cpp
@@ -14,11 +14,13 @@ extern "C" {
 
 #include <game/layers.hpp>
 
+
+#include "gameclient.hpp"
+#include "components/skins.hpp"
+
 #include "gc_client.hpp"
-#include "gc_skin.hpp"
 #include "gc_render.hpp"
 #include "gc_map_image.hpp"
-#include "gc_console.hpp"
 
 extern unsigned char internal_data[];
 
@@ -31,7 +33,7 @@ static float load_current;
 
 extern "C" void modc_console_init()
 {
-	client_console_init();
+	//client_console_init();
 }
 
 //binds_save()
@@ -41,8 +43,8 @@ static void load_sounds_thread(void *do_render)
 	// load sounds
 	for(int s = 0; s < data->num_sounds; s++)
 	{
-		if(do_render)
-			render_loading(load_current/load_total);
+		//if(do_render) // TODO: repair me
+			//render_loading(load_current/load_total);
 		for(int i = 0; i < data->sounds[s].num_sounds; i++)
 		{
 			int id = snd_load_wv(data->sounds[s].sounds[i].filename);
@@ -58,26 +60,20 @@ extern "C" void modc_init()
 {
 	for(int i = 0; i < NUM_NETOBJTYPES; i++)
 		snap_set_staticsize(i, netobj_get_size(i));
+		
+	gameclient.on_init();
 	
 	static FONT_SET default_font;
 	int64 start = time_get();
 	
-	// setup input stack
-	input_stack.add_handler(console_input_special_binds, 0); // F1-Fx binds
-	input_stack.add_handler(console_input_cli, 0); // console
-	input_stack.add_handler(chat_input_handle, 0); // chat
-	//input_stack.add_handler() // ui
-	input_stack.add_handler(console_input_normal_binds, 0); // binds
-	
-	
 	int before = gfx_memory_usage();
 	font_set_load(&default_font, "data/fonts/default_font%d.tfnt", "data/fonts/default_font%d.png", "data/fonts/default_font%d_b.png", 14, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 36);
 	dbg_msg("font", "gfx memory used for font textures: %d", gfx_memory_usage()-before);
 	
 	gfx_text_set_default_font(&default_font);
 
-	particle_reset();
-	menu_init();
+	//particle_reset();
+	//menu_init();
 	
 	// setup sound channels
 	snd_set_channel(CHN_GUI, 1.0f, 0.0f);
@@ -100,12 +96,14 @@ extern "C" void modc_init()
 	// load textures
 	for(int i = 0; i < data->num_images; i++)
 	{
-		render_loading(load_current/load_total);
+		// TODO: repair me
+		//render_loading(load_current/load_total);
 		data->images[i].id = gfx_load_texture(data->images[i].filename, IMG_AUTO, 0);
 		load_current++;
 	}
 
-	skin_init();
+	gameclient.skins->init();
+	//skin_init();
 	
 	if(config.cl_threadsoundloading)
 		thread_create(load_sounds_thread, 0);
@@ -118,370 +116,41 @@ extern "C" void modc_init()
 
 extern "C" void modc_save_config()
 {
-	binds_save();
-}
-
-extern "C" void modc_entergame()
-{
-}
-
-extern "C" void modc_shutdown()
-{
-	// shutdown the menu
+	//binds_save();
 }
 
 
 CHARACTER_CORE predicted_prev_char;
 CHARACTER_CORE predicted_char;
-static int predicted_tick = 0;
-static int last_new_predicted_tick = -1;
-
-extern "C" void modc_predict()
-{
-	CHARACTER_CORE before_prev_char = predicted_prev_char;
-	CHARACTER_CORE before_char = predicted_char;
-
-	// repredict character
-	WORLD_CORE world;
-	world.tuning = tuning;
-	int local_cid = -1;
-
-	// search for players
-	for(int i = 0; i < snap_num_items(SNAP_CURRENT); i++)
-	{
-		SNAP_ITEM item;
-		const void *data = snap_get_item(SNAP_CURRENT, i, &item);
-		int client_id = item.id;
-
-		if(item.type == NETOBJTYPE_CHARACTER)
-		{
-			const NETOBJ_CHARACTER *character = (const NETOBJ_CHARACTER *)data;
-			client_datas[client_id].predicted.world = &world;
-			world.characters[client_id] = &client_datas[client_id].predicted;
-
-			client_datas[client_id].predicted.read(character);
-		}
-		else if(item.type == NETOBJTYPE_PLAYER_INFO)
-		{
-			const NETOBJ_PLAYER_INFO *info = (const NETOBJ_PLAYER_INFO *)data;
-			if(info->local)
-				local_cid = client_id;
-		}
-	}
-	
-	// we can't predict without our own id
-	if(local_cid == -1)
-		return;
-
-	// predict
-	for(int tick = client_tick()+1; tick <= client_predtick(); tick++)
-	{
-		// fetch the local
-		if(tick == client_predtick() && world.characters[local_cid])
-			predicted_prev_char = *world.characters[local_cid];
-		
-		// first calculate where everyone should move
-		for(int c = 0; c < MAX_CLIENTS; c++)
-		{
-			if(!world.characters[c])
-				continue;
-
-			mem_zero(&world.characters[c]->input, sizeof(world.characters[c]->input));
-			if(local_cid == c)
-			{
-				// apply player input
-				int *input = client_get_input(tick);
-				if(input)
-					world.characters[c]->input = *((NETOBJ_PLAYER_INPUT*)input);
-			}
-
-			world.characters[c]->tick();
-		}
-
-		// move all players and quantize their data
-		for(int c = 0; c < MAX_CLIENTS; c++)
-		{
-			if(!world.characters[c])
-				continue;
-
-			world.characters[c]->move();
-			world.characters[c]->quantize();
-		}
-		
-		if(tick > last_new_predicted_tick)
-		{
-			last_new_predicted_tick = tick;
-			
-			if(local_cid != -1 && world.characters[local_cid])
-			{
-				vec2 pos = world.characters[local_cid]->pos;
-				int events = world.characters[local_cid]->triggered_events;
-				if(events&COREEVENT_GROUND_JUMP) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
-				if(events&COREEVENT_AIR_JUMP)
-				{
-					effect_air_jump(pos);
-					snd_play_random(CHN_WORLD, SOUND_PLAYER_AIRJUMP, 1.0f, pos);
-				}
-				//if(events&COREEVENT_HOOK_LAUNCH) snd_play_random(CHN_WORLD, SOUND_HOOK_LOOP, 1.0f, pos);
-				//if(events&COREEVENT_HOOK_ATTACH_PLAYER) snd_play_random(CHN_WORLD, SOUND_HOOK_ATTACH_PLAYER, 1.0f, pos);
-				if(events&COREEVENT_HOOK_ATTACH_GROUND) snd_play_random(CHN_WORLD, SOUND_HOOK_ATTACH_GROUND, 1.0f, pos);
-				//if(events&COREEVENT_HOOK_RETRACT) snd_play_random(CHN_WORLD, SOUND_PLAYER_JUMP, 1.0f, pos);
-			}
 
-
-			/*
-			dbg_msg("predict", "%d %d %d", tick,
-				(int)world.players[c]->pos.x, (int)world.players[c]->pos.y,
-				(int)world.players[c]->vel.x, (int)world.players[c]->vel.y);*/
-		}
-		
-		if(tick == client_predtick() && world.characters[local_cid])
-			predicted_char = *world.characters[local_cid];
-	}
-	
-	if(config.debug && predicted_tick == client_predtick())
-	{
-		if(predicted_char.pos.x != before_char.pos.x ||
-			predicted_char.pos.y != before_char.pos.y)
-		{
-			dbg_msg("client", "prediction error, (%d %d) (%d %d)", 
-				(int)before_char.pos.x, (int)before_char.pos.y,
-				(int)predicted_char.pos.x, (int)predicted_char.pos.y);
-		}
-
-		if(predicted_prev_char.pos.x != before_prev_char.pos.x ||
-			predicted_prev_char.pos.y != before_prev_char.pos.y)
-		{
-			dbg_msg("client", "prediction error, prev (%d %d) (%d %d)", 
-				(int)before_prev_char.pos.x, (int)before_prev_char.pos.y,
-				(int)predicted_prev_char.pos.x, (int)predicted_prev_char.pos.y);
-		}
-	}
-	
-	predicted_tick = client_predtick();
-}
-
-
-extern "C" void modc_newsnapshot()
-{
-	static int snapshot_count = 0;
-	snapshot_count++;
-	
-	// secure snapshot
-	{
-		int num = snap_num_items(SNAP_CURRENT);
-		for(int index = 0; index < num; index++)
-		{
-			SNAP_ITEM item;
-			void *data = snap_get_item(SNAP_CURRENT, index, &item);
-			if(netobj_validate(item.type, data, item.datasize) != 0)
-			{
-				if(config.debug)
-					dbg_msg("game", "invalidated index=%d type=%d (%s) size=%d id=%d", index, item.type, netobj_get_name(item.type), item.datasize, item.id);
-				snap_invalidate_item(SNAP_CURRENT, index);
-			}
-		}
-	}
-	
-	
-	process_events(SNAP_CURRENT);
-
-	if(config.dbg_stress)
-	{
-		if((client_tick()%250) == 0)
-		{
-			NETMSG_CL_SAY msg;
-			msg.team = -1;
-			msg.message = "galenskap!!!!";
-			msg.pack(MSGFLAG_VITAL);
-			client_send_msg();
-		}
-	}
-
-	clear_object_pointers();
-
-	// setup world view
-	{
-		// 1. fetch local player
-		// 2. set him to the center
-		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_PLAYER_INFO)
-			{
-				const NETOBJ_PLAYER_INFO *info = (const NETOBJ_PLAYER_INFO *)data;
-				
-				client_datas[info->cid].team = info->team;
-				
-				if(info->local)
-				{
-					netobjects.local_info = info;
-					const void *data = snap_find_item(SNAP_CURRENT, NETOBJTYPE_CHARACTER, item.id);
-					if(data)
-					{
-						netobjects.local_character = (const NETOBJ_CHARACTER *)data;
-						local_character_pos = vec2(netobjects.local_character->x, netobjects.local_character->y);
-
-						const void *p = snap_find_item(SNAP_PREV, NETOBJTYPE_CHARACTER, item.id);
-						if(p)
-							netobjects.local_prev_character = (NETOBJ_CHARACTER *)p;
-					}
-				}
-			}
-			else if(item.type == NETOBJTYPE_GAME)
-				netobjects.gameobj = (NETOBJ_GAME *)data;
-			else if(item.type == NETOBJTYPE_FLAG)
-			{
-				netobjects.flags[item.id%2] = (const NETOBJ_FLAG *)data;
-			}
-		}
-	}
-
-	for(int i = 0; i < MAX_CLIENTS; i++)
-		client_datas[i].update_render_info();
-}
-
-extern "C" void modc_render()
-{
-	// this should be moved around abit
-	if(client_state() == CLIENTSTATE_ONLINE)
-		render_game();
-	else
-		menu_render();
-
-	input_stack.dispatch_input();
-	console_render();
-}
+extern "C" void modc_entergame() {}
+extern "C" void modc_shutdown() {}
+extern "C" void modc_predict() { gameclient.on_predict(); }
+extern "C" void modc_newsnapshot() { gameclient.on_snapshot(); }
+extern "C" int modc_snap_input(int *data) { return gameclient.on_snapinput(data); }
+extern "C" void modc_statechange(int state, int old) { gameclient.on_statechange(state, old); }
+extern "C" void modc_render() { gameclient.on_render(); }
 
 extern "C" void modc_rcon_line(const char *line)
 {
-	console_rcon_print(line);
-}
-
-extern "C" int modc_snap_input(int *data)
-{
-	static NETOBJ_PLAYER_INPUT last_data = {0};
-	static int64 last_send_time = 0;
-	
-	// update player state
-	if(chat_mode != CHATMODE_NONE)
-		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
-	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);
-}
-
-void menu_do_disconnected();
-void menu_do_connecting();
-void menu_do_connected();
-
-extern "C" void modc_statechange(int state, int old)
-{
-	clear_object_pointers();
-	
-	if(state == CLIENTSTATE_OFFLINE)
-	{
-	 	menu_do_disconnected();
-	 	menu_game_active = false;
-	}
-	else if(state == CLIENTSTATE_LOADING)
-		menu_do_connecting();
-	else if(state == CLIENTSTATE_CONNECTING)
-		menu_do_connecting();
-	else if (state == CLIENTSTATE_ONLINE)
-	{
-		menu_active = false;
-	 	menu_game_active = true;
-	 	//snapshot_count = 0;
-	 	
-		menu_do_connected();
-	}
+	//console_rcon_print(line);
 }
 
+/*
 NETOBJ_PROJECTILE extraproj_projectiles[MAX_EXTRA_PROJECTILES];
 int extraproj_num;
 
 void extraproj_reset()
 {
 	extraproj_num = 0;
-}
-
-char server_motd[900] = {0};
-int64 server_motd_time = 0;
+}*/
 
 extern "C" void modc_message(int msgtype)
 {
 	// special messages
 	if(msgtype == NETMSGTYPE_SV_EXTRAPROJECTILE)
 	{
+		/*
 		int num = msg_unpack_int();
 		
 		for(int k = 0; k < num; k++)
@@ -500,7 +169,7 @@ extern "C" void modc_message(int msgtype)
 			}
 		}
 		
-		return;
+		return;*/
 	}
 	else if(msgtype == NETMSGTYPE_SV_TUNEPARAMS)
 	{
@@ -518,7 +187,9 @@ extern "C" void modc_message(int msgtype)
 		tuning = new_tuning;
 		return;
 	}
-	
+
+	gameclient.on_message(msgtype);
+
 	// normal 
 	void *rawmsg = netmsg_secure_unpack(msgtype);
 	if(!rawmsg)
@@ -526,25 +197,23 @@ extern "C" void modc_message(int msgtype)
 		dbg_msg("client", "dropped weird message '%s' (%d), failed on '%s'", netmsg_get_name(msgtype), msgtype, netmsg_failed_on());
 		return;
 	}
-		
+	
+	
 	if(msgtype == NETMSGTYPE_SV_CHAT)
 	{
-		NETMSG_SV_CHAT *msg = (NETMSG_SV_CHAT *)rawmsg;
-		chat_add_line(msg->cid, msg->team, msg->message);
 
-		if(msg->cid >= 0)
-			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);
 	}
 	else 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;
+		*/
 	}
 	else if(msgtype == NETMSGTYPE_SV_MOTD)
 	{
+		/*
 		NETMSG_SV_MOTD *msg = (NETMSG_SV_MOTD *)rawmsg;
 
 		// process escaping			
@@ -566,43 +235,45 @@ extern "C" void modc_message(int msgtype)
 			server_motd_time = time_get()+time_freq()*config.cl_motd_time;
 		else
 			server_motd_time = 0;
+			*/
 	}
 	else if(msgtype == NETMSGTYPE_SV_SETINFO)
 	{
 		NETMSG_SV_SETINFO *msg = (NETMSG_SV_SETINFO *)rawmsg;
 		
-		str_copy(client_datas[msg->cid].name, msg->name, 64);
-		str_copy(client_datas[msg->cid].skin_name, msg->skin, 64);
+		str_copy(gameclient.clients[msg->cid].name, msg->name, 64);
+		str_copy(gameclient.clients[msg->cid].skin_name, msg->skin, 64);
 		
 		// make sure that we don't set a special skin on the client
-		if(client_datas[msg->cid].skin_name[0] == 'x' || client_datas[msg->cid].skin_name[1] == '_')
-			str_copy(client_datas[msg->cid].skin_name, "default", 64);
+		if(gameclient.clients[msg->cid].skin_name[0] == 'x' || gameclient.clients[msg->cid].skin_name[1] == '_')
+			str_copy(gameclient.clients[msg->cid].skin_name, "default", 64);
 		
-		client_datas[msg->cid].skin_info.color_body = skin_get_color(msg->color_body);
-		client_datas[msg->cid].skin_info.color_feet = skin_get_color(msg->color_feet);
-		client_datas[msg->cid].skin_info.size = 64;
+		gameclient.clients[msg->cid].skin_info.color_body = gameclient.skins->get_color(msg->color_body);
+		gameclient.clients[msg->cid].skin_info.color_feet = gameclient.skins->get_color(msg->color_feet);
+		gameclient.clients[msg->cid].skin_info.size = 64;
 		
 		// find new skin
-		client_datas[msg->cid].skin_id = skin_find(client_datas[msg->cid].skin_name);
-		if(client_datas[msg->cid].skin_id < 0)
-			client_datas[msg->cid].skin_id = 0;
+		gameclient.clients[msg->cid].skin_id = gameclient.skins->find(gameclient.clients[msg->cid].skin_name);
+		if(gameclient.clients[msg->cid].skin_id < 0)
+			gameclient.clients[msg->cid].skin_id = 0;
 		
 		if(msg->use_custom_color)
-			client_datas[msg->cid].skin_info.texture = skin_get(client_datas[msg->cid].skin_id)->color_texture;
+			gameclient.clients[msg->cid].skin_info.texture = gameclient.skins->get(gameclient.clients[msg->cid].skin_id)->color_texture;
 		else
 		{
-			client_datas[msg->cid].skin_info.texture = skin_get(client_datas[msg->cid].skin_id)->org_texture;
-			client_datas[msg->cid].skin_info.color_body = vec4(1,1,1,1);
-			client_datas[msg->cid].skin_info.color_feet = vec4(1,1,1,1);
+			gameclient.clients[msg->cid].skin_info.texture = gameclient.skins->get(gameclient.clients[msg->cid].skin_id)->org_texture;
+			gameclient.clients[msg->cid].skin_info.color_body = vec4(1,1,1,1);
+			gameclient.clients[msg->cid].skin_info.color_feet = vec4(1,1,1,1);
 		}
 
-		client_datas[msg->cid].update_render_info();
+		gameclient.clients[msg->cid].update_render_info();
 	}
     else if(msgtype == NETMSGTYPE_SV_WEAPONPICKUP)
     {
-    	NETMSG_SV_WEAPONPICKUP *msg = (NETMSG_SV_WEAPONPICKUP *)rawmsg;
+    	// TODO: repair me
+    	/*NETMSG_SV_WEAPONPICKUP *msg = (NETMSG_SV_WEAPONPICKUP *)rawmsg;
         if(config.cl_autoswitch_weapons)
-        	input_data.wanted_weapon = msg->weapon+1;
+        	input_data.wanted_weapon = msg->weapon+1;*/
     }
 	else if(msgtype == NETMSGTYPE_SV_READYTOENTER)
 	{
@@ -610,8 +281,11 @@ extern "C" void modc_message(int msgtype)
 	}
 	else if(msgtype == NETMSGTYPE_SV_KILLMSG)
 	{
+		/*
 		NETMSG_SV_KILLMSG *msg = (NETMSG_SV_KILLMSG *)rawmsg;
 		
+		gameclient.killmsgs.handle_message((NETMSG_SV_KILLMSG *)rawmsg);
+		
 		// unpack messages
 		KILLMSG kill;
 		kill.killer = msg->killer;
@@ -622,15 +296,15 @@ extern "C" void modc_message(int msgtype)
 
 		// add the message
 		killmsg_current = (killmsg_current+1)%killmsg_max;
-		killmsgs[killmsg_current] = kill;
+		killmsgs[killmsg_current] = kill;*/
 	}
 	else if (msgtype == NETMSGTYPE_SV_EMOTICON)
 	{
 		NETMSG_SV_EMOTICON *msg = (NETMSG_SV_EMOTICON *)rawmsg;
 
 		// apply
-		client_datas[msg->cid].emoticon = msg->emoticon;
-		client_datas[msg->cid].emoticon_start = client_tick();
+		gameclient.clients[msg->cid].emoticon = msg->emoticon;
+		gameclient.clients[msg->cid].emoticon_start = client_tick();
 	}
 	else if(msgtype == NETMSGTYPE_SV_SOUNDGLOBAL)
 	{
@@ -645,35 +319,17 @@ extern "C" void modc_connected()
 	layers_init();
 	col_init();
 	img_init();
-	flow_init();
+	//flow_init();
 	
 	render_tilemap_generate_skip();
 	
+	gameclient.on_connected();
 	//tilemap_init();
-	chat_reset();
-	particle_reset();
-	extraproj_reset();
+	//particle_reset();
+	//extraproj_reset();
 	
-	clear_object_pointers();
-	last_new_predicted_tick = -1;
-
-	for(int i = 0; i < MAX_CLIENTS; i++)
-	{
-		client_datas[i].name[0] = 0;
-		client_datas[i].skin_id = 0;
-		client_datas[i].team = 0;
-		client_datas[i].angle = 0;
-		client_datas[i].emoticon = 0;
-		client_datas[i].emoticon_start = -1;
-		client_datas[i].skin_info.texture = skin_get(0)->color_texture;
-		client_datas[i].skin_info.color_body = vec4(1,1,1,1);
-		client_datas[i].skin_info.color_feet = vec4(1,1,1,1);
-		client_datas[i].update_render_info();
-	}
-
-	for(int i = 0; i < killmsg_max; i++)
-		killmsgs[i].tick = -100000;
-	send_info(true);
+	//last_new_predicted_tick = -1;
 }
 
 extern "C" const char *modc_net_version() { return GAME_NETVERSION; }
+extern "C" const char *modc_getitemname(int type) { return netobj_get_name(type); }
diff --git a/src/game/client/gc_menu.cpp b/src/game/client/gc_menu.cpp
deleted file mode 100644
index fb7c5437..00000000
--- a/src/game/client/gc_menu.cpp
+++ /dev/null
@@ -1,2161 +0,0 @@
-/* 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>
-
-
-extern "C" {
-	#include <engine/e_client_interface.h>
-	#include <engine/e_config.h>
-	#include <engine/client/ec_font.h>
-}
-
-#include "../version.hpp"
-#include <game/generated/g_protocol.hpp>
-
-#include "../generated/gc_data.hpp"
-#include "gc_render.hpp"
-#include "gc_anim.hpp"
-#include "gc_skin.hpp"
-#include "gc_ui.hpp"
-#include "gc_client.hpp"
-#include <mastersrv/mastersrv.h>
-
-//extern data_container *data;
-
-extern bool menu_active;
-//extern bool menu_game_active;
-
-static bool need_restart = false;
-
-enum
-{
-	POPUP_NONE=0,
-	POPUP_FIRST_LAUNCH,
-	POPUP_CONNECTING,
-	POPUP_DISCONNECTED,
-	POPUP_PASSWORD,
-	POPUP_QUIT, 
-};
-
-static int popup = POPUP_NONE;
-
-static vec4 gui_color(0.65f,0.78f,0.9f, 0.5f);
-
-static vec4 color_tabbar_inactive_outgame(0,0,0,0.25f);
-static vec4 color_tabbar_active_outgame(0,0,0,0.5f);
-
-static float color_ingame_scale_i = 0.5f;
-static float color_ingame_scale_a = 0.2f;
-static vec4 color_tabbar_inactive_ingame(gui_color.r*color_ingame_scale_i, gui_color.g*color_ingame_scale_i, gui_color.b*color_ingame_scale_i,0.75f);
-static vec4 color_tabbar_active_ingame(gui_color.r*color_ingame_scale_a, gui_color.g*color_ingame_scale_a, gui_color.b*color_ingame_scale_a,0.85f);
-
-static vec4 color_tabbar_inactive = color_tabbar_inactive_outgame;
-static vec4 color_tabbar_active = color_tabbar_active_outgame;
-
-enum
-{
-	PAGE_NEWS=0,
-	PAGE_GAME,
-	PAGE_SERVER_INFO,
-	PAGE_INTERNET,
-	PAGE_LAN,
-	PAGE_FAVORITES,
-	PAGE_SETTINGS,
-	PAGE_SYSTEM,
-};
-
-static int menu_game_page = PAGE_GAME;
-
-static void 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();
-}
-
-static vec4 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);
-}
-
-static void 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);
-}
-
-static void 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);
-}
-
-static void 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);
-}
-
-
-static void 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);
-}
-
-static void 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);
-}
-
-static void 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);
-}
-
-static void 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);	
-}
-
-static void 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);
-}
-
-
-static void 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 ui_do_edit_box(void *id, const RECT *rect, char *str, int str_size, float font_size, bool hidden=false)
-{
-    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 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 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 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;
-}
-
-
-static int menu2_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 = menu_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
-			menu_game_page = new_page;
-	}
-		
-	return 0;
-}
-
-static void menu2_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 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);
-	
-	menu2_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();
-}
-
-static void menu2_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)
-			{
-				const char *type = "???";
-				if(item->game_type == GAMETYPE_DM) type = "DM";
-				else if(item->game_type == GAMETYPE_TDM) type = "TDM";
-				else if(item->game_type == GAMETYPE_CTF) type = "CTF";
-				ui_do_label(&button, type, 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);
-		static const char *game_types[] = { "DM", "TDM", "CTF" };
-		if (selected_server->game_type >= 0 && selected_server->game_type < (int)(sizeof(game_types)/sizeof(*game_types)))
-			ui_do_label(&row, game_types[selected_server->game_type], 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<<GAMETYPE_DM), &button, ui_draw_checkbox, 0))
-		config.b_filter_gametype ^= (1<<GAMETYPE_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<<GAMETYPE_TDM), &button, ui_draw_checkbox, 0))
-		config.b_filter_gametype ^= (1<<GAMETYPE_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<<GAMETYPE_CTF), &button, ui_draw_checkbox, 0))
-		config.b_filter_gametype ^= (1<<GAMETYPE_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);
-	}
-}
-
-static void menu2_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)((skin_num()-num)*scrollvalue);
-	if(start < 0)
-		start = 0;
-		
-	ANIM_STATE state;
-	anim_eval(&data->animations[ANIM_BASE], 0, &state);
-	anim_eval_add(&state, &data->animations[ANIM_IDLE], 0, 1.0f);
-	//anim_eval_add(&state, &data->animations[ANIM_WALK], fmod(client_localtime(), 1.0f), 1.0f);
-		
-	for(int i = start; i < start+num && i < skin_num(); i++)
-	{
-		const skin *s = skin_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 = skin_get_color(config.player_color_body);
-			info.color_feet = skin_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(&state, &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);
-
-static void menu2_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 = 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)
-		{
-			binds_set(oldid, "");
-			binds_set(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);
-	if (ui_do_button((void*)binds_default, "Reset to defaults", 0, &button, ui_draw_menu_button, 0))
-		binds_default();
-}
-
-static void menu2_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);
-	}		
-}
-
-static void menu2_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);
-	}
-}*/
-
-static void menu2_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)
-		menu2_render_settings_player(main_view);
-	else if(settings_page == 1)
-		menu2_render_settings_controls(main_view);
-	else if(settings_page == 2)
-		menu2_render_settings_graphics(main_view);
-	else if(settings_page == 3)
-		menu2_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);
-	}
-}
-
-static void menu2_render_news(RECT main_view)
-{
-	ui_draw_rect(&main_view, color_tabbar_active, CORNER_ALL, 10.0f);
-}
-
-static void menu2_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(netobjects.local_info && netobjects.gameobj)
-	{
-		if(netobjects.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))
-			{
-				send_switch_team(-1);
-				menu_active = false;
-			}
-		}
-		
-		if(netobjects.gameobj->gametype == GAMETYPE_DM)
-		{
-			if(netobjects.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))
-				{
-					send_switch_team(0);
-					menu_active = false;
-				}
-			}						
-		}
-		else
-		{
-			if(netobjects.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))
-				{
-					send_switch_team(0);
-					menu_active = false;
-				}
-			}
-
-			if(netobjects.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))
-				{
-					send_switch_team(1);
-					menu_active = false;
-				}
-			}
-		}
-	}
-}
-
-void menu2_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);
-	gfx_text(0, view.x, view.y, 16, server_motd, -1);
-}
-
-void menu_do_disconnected()
-{
-	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;
-	}
-}
-
-void menu_do_connecting()
-{
-	popup = POPUP_CONNECTING;
-}
-
-void menu_do_connected()
-{
-	popup = POPUP_NONE;
-}
-
-void menu_init()
-{
-	if(config.cl_show_welcome)
-		popup = POPUP_FIRST_LAUNCH;
-	config.cl_show_welcome = 0;
-}
-
-int menu2_render()
-{
-	if(0)
-	{
-		gfx_mapscreen(0,0,10*4/3.0f,10);
-		gfx_clear(gui_color.r, gui_color.g, gui_color.b);
-		
-		ANIM_STATE state;
-		anim_eval(&data->animations[ANIM_BASE], 0, &state);
-		anim_eval_add(&state, &data->animations[ANIM_IDLE], 0, 1.0f);
-		//anim_eval_add(&state, &data->animations[ANIM_WALK], fmod(client_localtime(), 1.0f), 1.0f);
-			
-		for(int i = 0; i < skin_num(); i++)
-		{
-			float x = (i/8)*3;
-			float y = (i%8);
-			for(int c = 0; c < 2; c++)
-			{
-				//int colors[2] = {54090, 10998628};
-				//int colors[2] = {65432, 9895832}; // NEW
-				int colors[2] = {65387, 10223467}; // NEW
-				
-				TEE_RENDER_INFO info;
-				info.texture = skin_get(i)->color_texture;
-				info.color_feet = info.color_body = skin_get_color(colors[c]);
-				//info.color_feet = info.color_body = vec4(1,1,1,1);
-				info.size = 1.0f; //ui_scale()*16.0f;
-				//render_tee(&state, &info, 0, vec2(sinf(client_localtime()*3), cosf(client_localtime()*3)), vec2(1+x+c,1+y));
-				render_tee(&state, &info, 0, vec2(1,0), vec2(1+x+c,1+y));
-			}
-		}
-			
-		return 0;
-	}
-	
-    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
-	{
-		menu2_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);
-		menu2_render_menubar(tab_bar);
-			
-		// render current page
-		if(client_state() != CLIENTSTATE_OFFLINE)
-		{
-			if(menu_game_page == PAGE_GAME)
-				menu2_render_game(main_view);
-			else if(menu_game_page == PAGE_SERVER_INFO)
-				menu2_render_serverinfo(main_view);
-			else if(menu_game_page == PAGE_SETTINGS)
-				menu2_render_settings(main_view);
-		}
-		else if(config.ui_page == PAGE_NEWS)
-			menu2_render_news(main_view);
-		else if(config.ui_page == PAGE_INTERNET)
-			menu2_render_serverbrowser(main_view);
-		else if(config.ui_page == PAGE_LAN)
-			menu2_render_serverbrowser(main_view);
-		else if(config.ui_page == PAGE_FAVORITES)
-			menu2_render_serverbrowser(main_view);
-		else if(config.ui_page == PAGE_SETTINGS)
-			menu2_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 menu_render()
-{
-	static int mouse_x = 0;
-	static int mouse_y = 0;
-
-	// 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);
-
-	color_ingame_scale_i = 0.5f;
-	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);
-
-
-    // handle mouse movement
-    float mx, my;
-    {
-        int rx, ry;
-        inp_mouse_relative(&rx, &ry);
-        mouse_x += rx;
-        mouse_y += ry;
-        if(mouse_x < 0) mouse_x = 0;
-        if(mouse_y < 0) mouse_y = 0;
-        if(mouse_x > gfx_screenwidth()) mouse_x = gfx_screenwidth();
-        if(mouse_y > gfx_screenheight()) mouse_y = gfx_screenheight();
-            
-        // update the ui
-        RECT *screen = ui_screen();
-        mx = (mouse_x/(float)gfx_screenwidth())*screen->w;
-        my = (mouse_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);
-    }
-    
-	menu2_render();
-	
-    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();
-
-	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/gc_menu.hpp b/src/game/client/gc_menu.hpp
deleted file mode 100644
index ba12894c..00000000
--- a/src/game/client/gc_menu.hpp
+++ /dev/null
@@ -1,5 +0,0 @@
-/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
-#ifndef __MENU_H
-#define __MENU_H
-
-#endif
diff --git a/src/game/client/gc_render.cpp b/src/game/client/gc_render.cpp
index 56e82c69..6dacc354 100644
--- a/src/game/client/gc_render.cpp
+++ b/src/game/client/gc_render.cpp
@@ -8,14 +8,14 @@
 #include <game/generated/gc_data.hpp>
 #include <game/generated/g_protocol.hpp>
 #include <game/layers.hpp>
+#include "animstate.hpp"
 #include "gc_render.hpp"
-#include "gc_anim.hpp"
 #include "gc_client.hpp"
 #include "gc_map_image.hpp"
 
 static float sprite_w_scale;
 static float sprite_h_scale;
-
+/*
 static void layershot_begin()
 {
 	if(!config.cl_layershot)
@@ -33,7 +33,7 @@ static void layershot_end()
 	str_format(buf, sizeof(buf), "screenshots/layers_%04d.png", config.cl_layershot);
 	gfx_screenshot_direct(buf);
 	config.cl_layershot++;
-}
+}*/
 
 void select_sprite(SPRITE *spr, int flags, int sx, int sy)
 {
@@ -83,8 +83,6 @@ void draw_sprite(float x, float y, float size)
 	gfx_quads_draw(x, y, size*sprite_w_scale, size*sprite_h_scale);
 }
 
-
-
 void draw_round_rect_ext(float x, float y, float w, float h, float r, int corners)
 {
 	int num = 8;
@@ -155,7 +153,7 @@ void ui_draw_rect(const RECT *r, vec4 color, int corners, float rounding)
 	gfx_quads_end();
 }
 
-void render_tee(ANIM_STATE *anim, TEE_RENDER_INFO *info, int emote, vec2 dir, vec2 pos)
+void render_tee(ANIMSTATE *anim, TEE_RENDER_INFO *info, int emote, vec2 dir, vec2 pos)
 {
 	vec2 direction = dir;
 	vec2 position = pos;
@@ -281,137 +279,6 @@ void mapscreen_to_world(float center_x, float center_y, float parallax_x, float
 	points[3] = offset_y+center_y+height/2;
 }
 
-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 render_layers(float center_x, float center_y, int pass)
-{
-	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(pass == -1)
-				render = true;
-			else if(pass == 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();
-	}
-}
-
 void render_tilemap_generate_skip()
 {
 	for(int g = 0; g < layers_num_groups(); g++)
@@ -444,119 +311,3 @@ void render_tilemap_generate_skip()
 		}
 	}
 }
-
-static void render_items()
-{
-	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);
-	}
-}
-
-
-static void render_players()
-{
-	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
-					);
-			}
-		}
-	}
-}
-
-// renders the complete game world
-void render_world(float center_x, float center_y, float zoom)
-{	
-	// render background layers
-	render_layers(center_x, center_y, 0);
-	gfx_clip_disable();
-	
-	// render trails
-	layershot_begin();
-	particle_render(PARTGROUP_PROJECTILE_TRAIL);
-	layershot_end();
-
-	// render items
-	layershot_begin();
-	render_items();
-	layershot_end();
-
-	// render players above all
-	layershot_begin();
-	render_players();
-	layershot_end();
-
-	// render particles
-	layershot_begin();
-	particle_render(PARTGROUP_EXPLOSIONS);
-	particle_render(PARTGROUP_GENERAL);
-	layershot_end();
-	
-	if(config.dbg_flow)
-		flow_dbg_render();
-
-	// render foreground layers
-	layershot_begin();
-	render_layers(center_x, center_y, 1);
-	layershot_end();
-	gfx_clip_disable();
-
-	// render damage indications
-	layershot_begin();
-	render_damage_indicators();
-	layershot_end();
-	
-	config.cl_layershot = 0;
-}
diff --git a/src/game/client/gc_render.hpp b/src/game/client/gc_render.hpp
index e48d0fcd..088ff78a 100644
--- a/src/game/client/gc_render.hpp
+++ b/src/game/client/gc_render.hpp
@@ -50,9 +50,6 @@ void draw_round_rect_ext(float x, float y, float w, float h, float r, int corner
 void ui_draw_rect(const RECT *r, vec4 color, int corners, float rounding);
 
 // larger rendering methods
-void menu_render();
-void render_game();
-void render_world(float center_x, float center_y, float zoom);
 void render_loading(float percent);
 
 void render_damage_indicators();
@@ -61,15 +58,8 @@ void render_particles();
 void render_tilemap_generate_skip();
 
 // object render methods (gc_render_obj.cpp)
-void render_tee(class ANIM_STATE *anim, TEE_RENDER_INFO *info, int emote, vec2 dir, vec2 pos);
-void render_flag(const struct NETOBJ_FLAG *prev, const struct NETOBJ_FLAG *current);
-void render_pickup(const struct NETOBJ_PICKUP *prev, const struct NETOBJ_PICKUP *current);
-void render_projectile(const struct NETOBJ_PROJECTILE *current, int itemid);
-void render_laser(const struct NETOBJ_LASER *current);
-void render_player(
-	const struct NETOBJ_CHARACTER *prev_char, const struct NETOBJ_CHARACTER *player_char,
-	const struct NETOBJ_PLAYER_INFO *prev_info, const struct NETOBJ_PLAYER_INFO *player_info);
-	
+void render_tee(class ANIMSTATE *anim, TEE_RENDER_INFO *info, int emote, vec2 dir, vec2 pos);
+
 // map render methods (gc_render_map.cpp)
 void render_eval_envelope(ENVPOINT *points, int num_points, int channels, float time, float *result);
 void render_quads(QUAD *quads, int num_quads, void (*eval)(float time_offset, int env, float *channels), int flags);
diff --git a/src/game/client/gc_skin.hpp b/src/game/client/gc_skin.hpp
deleted file mode 100644
index 786f6768..00000000
--- a/src/game/client/gc_skin.hpp
+++ /dev/null
@@ -1,21 +0,0 @@
-/* copyright (c) 2007 magnus auvinen, see licence.txt for more info */
-#include <base/vmath.hpp>
-
-// do this better and nicer
-typedef struct 
-{
-	int org_texture;
-	int color_texture;
-	char name[31];
-	char term[1];
-	vec3 blood_color;
-} skin;
-
-vec4 skin_get_color(int v);
-void skin_init();
-int skin_num();
-const skin *skin_get(int index);
-int skin_find(const char *name);
-
-
-vec3 hsl_to_rgb(vec3 in);
diff --git a/src/game/client/lineinput.cpp b/src/game/client/lineinput.cpp
new file mode 100644
index 00000000..7a02d1f9
--- /dev/null
+++ b/src/game/client/lineinput.cpp
@@ -0,0 +1,65 @@
+#include <engine/e_client_interface.h>
+#include <string.h> // strlen
+#include "lineinput.hpp"
+
+LINEINPUT::LINEINPUT()
+{
+	clear();
+}
+
+void LINEINPUT::clear()
+{
+	mem_zero(str, sizeof(str));
+	len = 0;
+	cursor_pos = 0;
+}
+
+void LINEINPUT::set(const char *string)
+{
+	str_copy(str, string, sizeof(str));
+	len = strlen(str);
+	cursor_pos = len;
+}
+
+void LINEINPUT::process_input(INPUT_EVENT e)
+{
+	if(cursor_pos > len)
+		cursor_pos = len;
+	
+	char c = e.ch;
+	int k = e.key;
+	
+	if (!(c >= 0 && c < 32))
+	{
+		if (len < sizeof(str) - 1 && cursor_pos < sizeof(str) - 1)
+		{
+			memmove(str + cursor_pos + 1, str + cursor_pos, len - cursor_pos + 1);
+			str[cursor_pos] = c;
+			cursor_pos++;
+			len++;
+		}
+	}
+	
+	if(e.flags&INPFLAG_PRESS)
+	{
+		if (k == KEY_BACKSPACE && cursor_pos > 0)
+		{
+			memmove(str + cursor_pos - 1, str + cursor_pos, len - cursor_pos + 1);
+			cursor_pos--;
+			len--;
+		}
+		else if (k == KEY_DEL && cursor_pos < len)
+		{
+			memmove(str + cursor_pos, str + cursor_pos + 1, len - cursor_pos);
+			len--;
+		}
+		else if (k == KEY_LEFT && cursor_pos > 0)
+			cursor_pos--;
+		else if (k == KEY_RIGHT && cursor_pos < len)
+			cursor_pos++;
+		else if (k == KEY_HOME)
+			cursor_pos = 0;
+		else if (k == KEY_END)
+			cursor_pos = len;
+	}
+}
diff --git a/src/game/client/lineinput.hpp b/src/game/client/lineinput.hpp
new file mode 100644
index 00000000..cf42a7a5
--- /dev/null
+++ b/src/game/client/lineinput.hpp
@@ -0,0 +1,23 @@
+
+// line input helter
+class LINEINPUT
+{
+	char str[256];
+	unsigned len;
+	unsigned cursor_pos;
+public:
+	class CALLBACK
+	{
+	public:
+		virtual ~CALLBACK() {}
+		virtual bool event(INPUT_EVENT e) = 0;
+	};
+
+	LINEINPUT();
+	void clear();
+	void process_input(INPUT_EVENT e);
+	void set(const char *string);
+	const char *get_string() const { return str; }
+	int get_length() const { return len; }
+	unsigned cursor_offset() const { return cursor_pos; }
+};