about summary refs log tree commit diff
path: root/src/engine/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/engine/client')
-rw-r--r--src/engine/client/client.c253
-rw-r--r--src/engine/client/gfx.c154
-rw-r--r--src/engine/client/ui.c2
3 files changed, 285 insertions, 124 deletions
diff --git a/src/engine/client/client.c b/src/engine/client/client.c
index 63414f8e..26865f37 100644
--- a/src/engine/client/client.c
+++ b/src/engine/client/client.c
@@ -33,10 +33,6 @@ static int info_request_begin;
 static int info_request_end;
 static int snapshot_part;
 static int64 local_start_time;
-static int64 game_start_time;
-
-static float snapshot_latency = 0;
-static float prediction_latency = 0;
 
 static int extra_polating = 0;
 static int debug_font;
@@ -48,6 +44,8 @@ static int window_must_refocus = 0;
 static int snaploss = 0;
 static int snapcrcerrors = 0;
 
+static int ack_game_tick = -1;
+
 static int current_recv_tick = 0;
 
 // current time
@@ -62,10 +60,123 @@ static struct // TODO: handle input better
 {
 	int data[MAX_INPUT_SIZE]; // the input data
 	int tick; // the tick that the input is for
-	float latency; // prediction latency when we sent this input
+	int64 game_time; // prediction latency when we sent this input
+	int64 time;
 } inputs[200];
 static int current_input = 0;
 
+enum
+{
+	GRAPH_MAX=256,
+};
+
+typedef struct
+{
+	float min, max;
+	float values[GRAPH_MAX];
+	int index;
+} GRAPH;
+
+static void graph_add(GRAPH *g, float v)
+{
+	g->values[g->index] = v;
+	g->index = (g->index+1)&(GRAPH_MAX-1);
+}
+
+static void graph_render(GRAPH *g, float x, float y, float w, float h)
+{
+	int i;
+	gfx_texture_set(-1);
+	
+	gfx_quads_begin();
+	gfx_setcolor(0, 0, 0, 1);
+	gfx_quads_drawTL(x, y, w, h);
+	gfx_quads_end();
+		
+	gfx_lines_begin();
+	gfx_setcolor(0.5f, 0.5f, 0.5f, 1);
+	gfx_lines_draw(x, y+(h*3)/4, x+w, y+(h*3)/4);
+	gfx_lines_draw(x, y+h/2, x+w, y+h/2);
+	gfx_lines_draw(x, y+h/4, x+w, y+h/4);
+	for(i = 1; i < GRAPH_MAX; i++)
+	{
+		float a0 = (i-1)/(float)GRAPH_MAX;
+		float a1 = i/(float)GRAPH_MAX;
+		int i0 = (g->index+i-1)&(GRAPH_MAX-1);
+		int i1 = (g->index+i)&(GRAPH_MAX-1);
+		
+		float v0 = g->values[i0];
+		float v1 = g->values[i1];
+		
+		gfx_setcolor(0, 1, 0, 1);
+		gfx_lines_draw(x+a0*w, y+h-v0*h, x+a1*w, y+h-v1*h);
+	}
+	gfx_lines_end();
+}
+
+typedef struct
+{
+	int64 snap;
+	int64 current;
+	int64 target;
+	
+	int64 rlast;
+	int64 tlast;
+	GRAPH graph;
+} SMOOTHTIME;
+
+static void st_init(SMOOTHTIME *st, int64 target)
+{
+	st->snap = time_get();
+	st->current = target;
+	st->target = target;
+}
+
+static int64 st_get(SMOOTHTIME *st, int64 now)
+{
+	int64 c = st->current + (now - st->snap);
+	int64 t = st->target + (now - st->snap);
+	
+	// it's faster to adjust upward instead of downward
+	// we might need to adjust these abit
+	float adjust_speed = 0.3f;
+	if(t < c)
+		adjust_speed *= 5.0f;
+	
+	float a = ((now-st->snap)/(float)time_freq())*adjust_speed;
+	if(a > 1)
+		a = 1;
+		
+	int64 r = c + (int64)((t-c)*a);
+	
+	{
+		int64 drt = now - st->rlast;
+		int64 dtt = r - st->tlast;
+		
+		st->rlast = now;
+		st->tlast = r;
+		
+		if(drt == 0)
+			graph_add(&st->graph, 0.5f);
+		else
+			graph_add(&st->graph, (((dtt/(float)drt)-1.0f)*2.5f)+0.5f);
+	}
+	
+	return r;
+}
+
+static void st_update(SMOOTHTIME *st, int64 target)
+{
+	int64 now = time_get();
+	st->current = st_get(st, now);
+	st->snap = now;
+	st->target = target;
+}
+
+SMOOTHTIME game_time;
+SMOOTHTIME predicted_time;
+
+GRAPH intra_graph;
 
 // --- input snapping ---
 static int input_data[MAX_INPUT_SIZE] = {0};
@@ -125,8 +236,7 @@ static void snap_init()
 	snapshots[SNAP_CURRENT] = 0;
 	snapshots[SNAP_PREV] = 0;
 	recived_snapshots = 0;
-	game_start_time = -1;
-	
+	current_predtick = 0;
 }
 
 // ------ time functions ------
@@ -159,7 +269,6 @@ int client_send_msg()
 static void client_send_info()
 {
 	recived_snapshots = 0;
-	game_start_time = -1;
 
 	msg_pack_start_system(NETMSG_INFO, MSGFLAG_VITAL);
 	msg_pack_string(modc_net_version(), 128);
@@ -192,12 +301,18 @@ static void client_send_error(const char *error)
 
 static void client_send_input()
 {
+	if(current_predtick <= 0)
+		return;
+		
 	msg_pack_start_system(NETMSG_INPUT, 0);
+	msg_pack_int(ack_game_tick);
 	msg_pack_int(current_predtick);
 	msg_pack_int(input_data_size);
-	
+
+	int64 now = time_get();	
 	inputs[current_input].tick = current_predtick;
-	inputs[current_input].latency = prediction_latency;
+	inputs[current_input].game_time = st_get(&predicted_time, now);
+	inputs[current_input].time = now;
 	
 	int i;
 	for(i = 0; i < input_data_size/4; i++)
@@ -384,9 +499,6 @@ void client_connect(const char *server_address_str)
 	for(i = 0; i < 200; i++)
 		inputs[i].tick = -1;
 	current_input = 0;
-	
-	snapshot_latency = 0;
-	prediction_latency = 0;
 }
 
 void client_disconnect()
@@ -424,17 +536,23 @@ static void client_debug_render()
 	static float frametime_avg = 0;
 	frametime_avg = frametime_avg*0.9f + frametime*0.1f;
 	char buffer[512];
-	sprintf(buffer, "send: %6d recv: %6d snaploss: %d snaplatency: %4.2f %c  predlatency: %4.2f  mem %dk   gfxmem: %dk  fps: %3d",
+	sprintf(buffer, "ticks: %8d %8d send: %6d recv: %6d snaploss: %d %c  mem %dk   gfxmem: %dk  fps: %3d",
+		current_tick, current_predtick,
 		(current.send_bytes-prev.send_bytes)*10,
 		(current.recv_bytes-prev.recv_bytes)*10,
 		snaploss,
-		snapshot_latency*1000.0f, extra_polating?'E':' ',
-		prediction_latency*1000.0f,
+		extra_polating?'E':' ',
 		mem_allocated()/1024,
 		gfx_memory_usage()/1024,
 		(int)(1.0f/frametime_avg));
 	gfx_quads_text(2, 2, 16, buffer);
 	
+	
+	// render graphs
+	gfx_mapscreen(0,0,400.0f,300.0f);
+	graph_render(&game_time.graph, 300, 10, 90, 50);
+	graph_render(&predicted_time.graph, 300, 10+50+10, 90, 50);
+	graph_render(&intra_graph, 300, 10+50+10+50+10, 90, 50);
 }
 
 void client_quit()
@@ -597,19 +715,15 @@ static void client_process_packet(NETPACKET *packet)
 					{
 						if(inputs[k].tick == input_predtick)
 						{
-							float wanted_latency = inputs[k].latency - time_left/1000.0f + 0.01f;
-							if(wanted_latency > prediction_latency)
-								prediction_latency = prediction_latency*0.90f + wanted_latency*0.10f;
-							else
-								prediction_latency = prediction_latency*0.95f + wanted_latency*0.05f;
-							//dbg_msg("DEBUG", "predlatency=%f", prediction_latency);
+							//-1000/50
+							int margin = 1000/50;
+							int64 target = inputs[k].game_time + (time_get() - inputs[k].time);
+							st_update(&predicted_time, target - (int64)(((time_left-margin)/1000.0f)*time_freq()));
 							break;
 						}
 					}
 				}
 				
-				//dbg_msg("DEBUG", "new predlatency=%f", prediction_latency);
-				
 				if(snapshot_part == part && game_tick > current_recv_tick)
 				{
 					// TODO: clean this up abit
@@ -642,10 +756,7 @@ static void client_process_packet(NETPACKET *packet)
 								
 								// ack snapshot
 								// TODO: combine this with the input message
-								msg_pack_start_system(NETMSG_SNAPACK, 0);
-								msg_pack_int(-1);
-								msg_pack_end();
-								client_send_msg();
+								ack_game_tick = -1;
 								return;
 							}
 						}
@@ -668,18 +779,15 @@ static void client_process_packet(NETPACKET *packet)
 						
 						unsigned char tmpbuffer3[MAX_SNAPSHOT_SIZE];
 						int snapsize = snapshot_unpack_delta(deltashot, (SNAPSHOT*)tmpbuffer3, deltadata, deltasize);
-						if(snapshot_crc((SNAPSHOT*)tmpbuffer3) != crc)
+						if(msg != NETMSG_SNAPEMPTY && snapshot_crc((SNAPSHOT*)tmpbuffer3) != crc)
 						{
 							if(config.debug)
-								dbg_msg("client", "snapshot crc error\n");
+								dbg_msg("client", "snapshot crc error");
 							snapcrcerrors++;
 							if(snapcrcerrors > 25)
 							{
 								// to many errors, send reset
-								msg_pack_start_system(NETMSG_SNAPACK, 0);
-								msg_pack_int(-1);
-								msg_pack_end();
-								client_send_msg();
+								ack_game_tick = -1;
 								snapcrcerrors = 0;
 							}
 							return;
@@ -718,12 +826,21 @@ static void client_process_packet(NETPACKET *packet)
 						// we got two snapshots until we see us self as connected
 						if(recived_snapshots == 2)
 						{
+							// start at 200ms and work from there
+							st_init(&predicted_time, (game_tick+10)*time_freq()/50);
+							st_init(&game_time, (game_tick-2)*time_freq()/50);
 							snapshots[SNAP_PREV] = snapshot_storage.first;
 							snapshots[SNAP_CURRENT] = snapshot_storage.last;
 							local_start_time = time_get();
 							client_set_state(CLIENTSTATE_ONLINE);
 						}
+						
+						st_update(&game_time, (game_tick-2)*time_freq()/50);
+						//client_send_input();
 
+						//st_update(&predicted_time, game_tick*time_freq());
+
+						/*
 						int64 now = time_get();
 						int64 t = now - game_tick*time_freq()/50;
 						if(game_start_time == -1 || t < game_start_time)
@@ -736,17 +853,16 @@ static void client_process_packet(NETPACKET *packet)
 						int64 wanted = game_start_time+(game_tick*time_freq())/50;
 						float current_latency = (now-wanted)/(float)time_freq();
 						snapshot_latency = snapshot_latency*0.95f+current_latency*0.05f;
+						*/
 						
 						// ack snapshot
-						msg_pack_start_system(NETMSG_SNAPACK, 0);
-						msg_pack_int(game_tick);
-						msg_pack_end();
-						client_send_msg();
+						//dbg_msg("snap!", "%d", game_tick);
+						ack_game_tick = game_tick;
 					}
 				}
 				else
 				{
-					dbg_msg("client", "snapshot reset!");
+					dbg_msg("client", "snapsht reset!");
 					snapshot_part = 0;
 				}
 			}
@@ -759,7 +875,6 @@ static void client_process_packet(NETPACKET *packet)
 	}
 }
 
-
 static void client_pump_network()
 {
 	netclient_update(net);
@@ -842,16 +957,14 @@ static void client_run(const char *direct_connect_server)
 		if(recived_snapshots >= 3)
 		{
 			int repredict = 0;
-			int64 now = time_get();
+			//int64 now = time_get();
+			int64 now = st_get(&game_time, time_get());
 			while(1)
 			{
 				SNAPSTORAGE_HOLDER *cur = snapshots[SNAP_CURRENT];
-				int64 tickstart = game_start_time + (cur->tick+1)*time_freq()/50;
-				int64 t = tickstart;
-				if(snapshot_latency > 0)
-					t += (int64)(time_freq()*(snapshot_latency*1.1f));
+				int64 tickstart = (cur->tick)*time_freq()/50;
 
-				if(t < now)
+				if(tickstart < now)
 				{
 					SNAPSTORAGE_HOLDER *next = snapshots[SNAP_CURRENT]->next;
 					if(next)
@@ -880,36 +993,28 @@ static void client_run(const char *direct_connect_server)
 					break;
 				}
 			}
+
+			//tg_add(&game_time_graph, now, extra_polating);
 			
 			if(snapshots[SNAP_CURRENT] && snapshots[SNAP_PREV])
 			{
-				int64 curtick_start = game_start_time + (snapshots[SNAP_CURRENT]->tick+1)*time_freq()/50;
-				if(snapshot_latency > 0)
-					curtick_start += (int64)(time_freq()*(snapshot_latency*1.1f));
-					
-				int64 prevtick_start = game_start_time + (snapshots[SNAP_PREV]->tick+1)*time_freq()/50;
-				if(snapshot_latency > 0)
-					prevtick_start += (int64)(time_freq()*(snapshot_latency*1.1f));
-					
+				int64 curtick_start = (snapshots[SNAP_CURRENT]->tick)*time_freq()/50;
+				int64 prevtick_start = (snapshots[SNAP_PREV]->tick)*time_freq()/50;
 				intratick = (now - prevtick_start) / (float)(curtick_start-prevtick_start);
-
-				// 25 frames ahead
-				int64 last_pred_game_time = 0;
-				int64 predicted_game_time = (now - game_start_time) + (int64)(time_freq()*prediction_latency);
-				if(predicted_game_time < last_pred_game_time)
-					predicted_game_time = last_pred_game_time;
-				last_pred_game_time = predicted_game_time;
 				
-				//int64 predictiontime = game_start_time + time_freq()+ prediction_latency*SERVER_TICK_SPEED
-				int new_predtick = (predicted_game_time*SERVER_TICK_SPEED) / time_freq();
+				graph_add(&intra_graph, intratick*0.25f);
 				
-				int64 predtick_start_time = (new_predtick*time_freq())/SERVER_TICK_SPEED;
-				intrapredtick = (predicted_game_time - predtick_start_time)*SERVER_TICK_SPEED/(float)time_freq();
+				int64 pred_now = st_get(&predicted_time, time_get());
+				//tg_add(&predicted_time_graph, pred_now, 0);
+				int prev_pred_tick = (int)(pred_now*50/time_freq());
+				int new_pred_tick = prev_pred_tick+1;
+				curtick_start = new_pred_tick*time_freq()/50;
+				prevtick_start = prev_pred_tick*time_freq()/50;
+				intrapredtick = (pred_now - prevtick_start) / (float)(curtick_start-prevtick_start);
 				
-				if(new_predtick > current_predtick)
+				if(new_pred_tick > current_predtick)
 				{
-					//dbg_msg("")
-					current_predtick = new_predtick;
+					current_predtick = new_pred_tick;
 					repredict = 1;
 					
 					// send input
@@ -918,9 +1023,13 @@ static void client_run(const char *direct_connect_server)
 			}
 			
 			//intrapredtick = current_predtick
-			
+
+			// only do sane predictions			
 			if(repredict)
-				modc_predict();
+			{
+				if(current_predtick > current_tick && current_predtick < current_tick+50)
+					modc_predict();
+			}
 		}
 
 		// STRESS TEST: join the server again
@@ -970,11 +1079,15 @@ static void client_run(const char *direct_connect_server)
 		
 			if(inp_key_pressed(KEY_F5))
 			{
+				ack_game_tick = -1;
+				client_send_input();
+				/*
 				// ack snapshot
 				msg_pack_start_system(NETMSG_SNAPACK, 0);
 				msg_pack_int(-1);
 				msg_pack_end();
 				client_send_msg();
+				*/
 			}
 		}
 			
diff --git a/src/engine/client/gfx.c b/src/engine/client/gfx.c
index b48740b6..e2aa1c81 100644
--- a/src/engine/client/gfx.c
+++ b/src/engine/client/gfx.c
@@ -15,6 +15,12 @@
 #define GL_COMPRESSED_RGB_ARB 0x84ED
 #define GL_COMPRESSED_RGBA_ARB 0x84EE
 
+enum
+{
+	DRAWING_QUADS=1,
+	DRAWING_LINES=2,
+};
+
 //
 typedef struct { float x, y, z; } VEC3;
 typedef struct { float u, v; } TEXCOORD;
@@ -39,7 +45,7 @@ static int do_screenshot = 0;
 static int screen_width = -1;
 static int screen_height = -1;
 static float rotation = 0;
-static int quads_drawing = 0;
+static int drawing = 0;
 
 static float screen_x0 = 0;
 static float screen_y0 = 0;
@@ -71,39 +77,48 @@ static const unsigned char null_texture_data[] = {
 	0x00,0x00,0xff,0xff, 0x00,0x00,0xff,0xff, 0xff,0xff,0x00,0xff, 0xff,0xff,0x00,0xff, 
 };
 
-static void draw_quad(int _bflush)
+static void flush()
 {
-	if (!_bflush && ((num_vertices + 4) < vertex_buffer_size))
-	{
-		// Just add
-		num_vertices += 4;
-	}
-	else if (num_vertices)
-	{
-		if (!_bflush)
-			num_vertices += 4;
-			
-		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
-		//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-
-		glVertexPointer(3, GL_FLOAT,
-				sizeof(VERTEX),
-				(char*)vertices);
-		glTexCoordPointer(2, GL_FLOAT,
-				sizeof(VERTEX),
-				(char*)vertices + sizeof(float)*3);
-		glColorPointer(4, GL_FLOAT,
-				sizeof(VERTEX),
-				(char*)vertices + sizeof(float)*5);
-		glEnableClientState(GL_VERTEX_ARRAY);
-		glEnableClientState(GL_TEXTURE_COORD_ARRAY);
-		glEnableClientState(GL_COLOR_ARRAY);
-		glDrawArrays(GL_QUADS, 0, num_vertices);
+	if(num_vertices == 0)
+		return;
 		
-		// Reset pointer
-		num_vertices = 0;
-	}
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+	glVertexPointer(3, GL_FLOAT,
+			sizeof(VERTEX),
+			(char*)vertices);
+	glTexCoordPointer(2, GL_FLOAT,
+			sizeof(VERTEX),
+			(char*)vertices + sizeof(float)*3);
+	glColorPointer(4, GL_FLOAT,
+			sizeof(VERTEX),
+			(char*)vertices + sizeof(float)*5);
+	glEnableClientState(GL_VERTEX_ARRAY);
+	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
+	glEnableClientState(GL_COLOR_ARRAY);
+	
+	if(drawing == DRAWING_QUADS)
+		glDrawArrays(GL_QUADS, 0, num_vertices);
+	else if(drawing == DRAWING_LINES)
+		glDrawArrays(GL_LINES, 0, num_vertices);
+	
+	// Reset pointer
+	num_vertices = 0;	
+}
+
+static void draw_line()
+{
+	num_vertices += 2;
+	if((num_vertices + 2) >= vertex_buffer_size)
+		flush();
+}
+
+static void draw_quad()
+{
+	num_vertices += 4;
+	if((num_vertices + 4) >= vertex_buffer_size)
+		flush();
 }
 
 int gfx_init()
@@ -499,7 +514,7 @@ int gfx_screenheight()
 
 void gfx_texture_set(int slot)
 {
-	dbg_assert(quads_drawing == 0, "called gfx_texture_set within quads_begin");
+	dbg_assert(drawing == 0, "called gfx_texture_set within begin");
 	if(slot == -1)
 		glDisable(GL_TEXTURE_2D);
 	else
@@ -551,49 +566,49 @@ void gfx_setoffset(float x, float y)
 
 void gfx_quads_begin()
 {
-	dbg_assert(quads_drawing == 0, "called quads_begin twice");
-	quads_drawing++;
+	dbg_assert(drawing == 0, "called quads_begin twice");
+	drawing = DRAWING_QUADS;
 	
 	gfx_quads_setsubset(0,0,1,1);
 	gfx_quads_setrotation(0);
-	gfx_quads_setcolor(1,1,1,1);
+	gfx_setcolor(1,1,1,1);
 }
 
 void gfx_quads_end()
 {
-	dbg_assert(quads_drawing == 1, "called quads_end without quads_begin");
-	draw_quad(1);
-	quads_drawing--;
+	dbg_assert(drawing == DRAWING_QUADS, "called quads_end without begin");
+	flush();
+	drawing = 0;
 }
 
 
 void gfx_quads_setrotation(float angle)
 {
-	dbg_assert(quads_drawing == 1, "called gfx_quads_setrotation without quads_begin");
+	dbg_assert(drawing == DRAWING_QUADS, "called gfx_quads_setrotation without begin");
 	rotation = angle;
 }
 
-void gfx_quads_setcolorvertex(int i, float r, float g, float b, float a)
+void gfx_setcolorvertex(int i, float r, float g, float b, float a)
 {
-	dbg_assert(quads_drawing == 1, "called gfx_quads_setcolorvertex without quads_begin");
+	dbg_assert(drawing != 0, "called gfx_quads_setcolorvertex without begin");
 	color[i].r = r;
 	color[i].g = g;
 	color[i].b = b;
 	color[i].a = a;
 }
 
-void gfx_quads_setcolor(float r, float g, float b, float a)
+void gfx_setcolor(float r, float g, float b, float a)
 {
-	dbg_assert(quads_drawing == 1, "called gfx_quads_setcolor without quads_begin");
-	gfx_quads_setcolorvertex(0, r, g, b, a);
-	gfx_quads_setcolorvertex(1, r, g, b, a);
-	gfx_quads_setcolorvertex(2, r, g, b, a);
-	gfx_quads_setcolorvertex(3, r, g, b, a);
+	dbg_assert(drawing != 0, "called gfx_quads_setcolor without begin");
+	gfx_setcolorvertex(0, r, g, b, a);
+	gfx_setcolorvertex(1, r, g, b, a);
+	gfx_setcolorvertex(2, r, g, b, a);
+	gfx_setcolorvertex(3, r, g, b, a);
 }
 
 void gfx_quads_setsubset(float tl_u, float tl_v, float br_u, float br_v)
 {
-	dbg_assert(quads_drawing == 1, "called gfx_quads_setsubset without quads_begin");
+	dbg_assert(drawing == DRAWING_QUADS, "called gfx_quads_setsubset without begin");
 
 	texture[0].u = tl_u;
 	texture[0].v = tl_v;
@@ -631,7 +646,7 @@ void gfx_quads_draw(float x, float y, float w, float h)
 
 void gfx_quads_drawTL(float x, float y, float width, float height)
 {
-	dbg_assert(quads_drawing == 1, "called quads_draw without quads_begin");
+	dbg_assert(drawing == DRAWING_QUADS, "called quads_draw without begin");
 	
 	VEC3 center;
 	center.x = x + width/2;
@@ -662,7 +677,7 @@ void gfx_quads_drawTL(float x, float y, float width, float height)
 	vertices[num_vertices + 3].color = color[3];
 	rotate(&center, &vertices[num_vertices + 3].pos);
 	
-	draw_quad(0);
+	draw_quad();
 }
 
 void gfx_quads_draw_freeform(
@@ -671,7 +686,7 @@ void gfx_quads_draw_freeform(
 	float x2, float y2,
 	float x3, float y3)
 {
-	dbg_assert(quads_drawing == 1, "called quads_draw_freeform without quads_begin");
+	dbg_assert(drawing == DRAWING_QUADS, "called quads_draw_freeform without begin");
 	
 	vertices[num_vertices].pos.x = x0;
 	vertices[num_vertices].pos.y = y0;
@@ -693,7 +708,7 @@ void gfx_quads_draw_freeform(
 	vertices[num_vertices + 3].tex = texture[3];
 	vertices[num_vertices + 3].color = color[3];
 	
-	draw_quad(0);
+	draw_quad();
 }
 
 void gfx_quads_text(float x, float y, float size, const char *text)
@@ -884,3 +899,36 @@ float gfx_pretty_text_width(float size, const char *text_, int length)
 
 	return w;
 }
+
+
+
+void gfx_lines_begin()
+{
+	dbg_assert(drawing == 0, "called begin twice");
+	drawing = DRAWING_LINES;
+	gfx_setcolor(1,1,1,1);
+}
+
+void gfx_lines_end()
+{
+	dbg_assert(drawing == DRAWING_LINES, "called end without begin");
+	flush();
+	drawing = 0;
+}
+
+void gfx_lines_draw(float x0, float y0, float x1, float y1)
+{
+	dbg_assert(drawing == DRAWING_LINES, "called draw without begin");
+	
+	vertices[num_vertices].pos.x = x0;
+	vertices[num_vertices].pos.y = y0;
+	vertices[num_vertices].tex = texture[0];
+	vertices[num_vertices].color = color[0];
+
+	vertices[num_vertices + 1].pos.x = x1;
+	vertices[num_vertices + 1].pos.y = y1;
+	vertices[num_vertices + 1].tex = texture[1];
+	vertices[num_vertices + 1].color = color[1];
+	
+	draw_line();
+}
diff --git a/src/engine/client/ui.c b/src/engine/client/ui.c
index c67b1b13..eb07e7b5 100644
--- a/src/engine/client/ui.c
+++ b/src/engine/client/ui.c
@@ -70,7 +70,7 @@ void ui_do_image(int texture, float x, float y, float w, float h)
     gfx_blend_normal();
     gfx_texture_set(texture);
     gfx_quads_begin();
-    gfx_quads_setcolor(1,1,1,1);
+    gfx_setcolor(1,1,1,1);
     gfx_quads_setsubset(
         0.0f, // startx
         0.0f, // starty