diff --git a/src/blua/lbaselib.c b/src/blua/lbaselib.c
index 3c919cb64607cbf80b9826ed0e01f913c86d2307..644565c28847204daa8e312459ef75e3fb6cfe31 100644
--- a/src/blua/lbaselib.c
+++ b/src/blua/lbaselib.c
@@ -11,6 +11,10 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include "../doomdef.h"
+#include "../lua_script.h"
+#include "../w_wad.h"
+
 #define lbaselib_c
 #define LUA_LIB
 
@@ -263,6 +267,27 @@ static int luaB_ipairs (lua_State *L) {
 }
 
 
+// Edited to load PK3 entries instead
+static int luaB_dofile (lua_State *L) {
+	const char *filename = luaL_checkstring(L, 1);
+	char fullfilename[256];
+	UINT16 lumpnum;
+	int n = lua_gettop(L);
+
+	if (wadfiles[numwadfiles - 1]->type != RET_PK3)
+		luaL_error(L, "dofile() only works with PK3 files");
+
+	snprintf(fullfilename, sizeof(fullfilename), "Lua/%s", filename);
+	lumpnum = W_CheckNumForFullNamePK3(fullfilename, numwadfiles - 1, 0);
+	if (lumpnum == INT16_MAX)
+		luaL_error(L, "can't find script " LUA_QS, fullfilename);
+
+	LUA_LoadLump(numwadfiles - 1, lumpnum, false);
+
+	return lua_gettop(L) - n;
+}
+
+
 static int luaB_assert (lua_State *L) {
   luaL_checkany(L, 1);
   if (!lua_toboolean(L, 1))
@@ -380,6 +405,7 @@ static const luaL_Reg base_funcs[] = {
   {"assert", luaB_assert},
   {"collectgarbage", luaB_collectgarbage},
   {"error", luaB_error},
+  {"dofile", luaB_dofile},
   {"gcinfo", luaB_gcinfo},
   {"getfenv", luaB_getfenv},
   {"getmetatable", luaB_getmetatable},
diff --git a/src/blua/liolib.c b/src/blua/liolib.c
index b430521942aa3f4b129313a8681247096e44e9f9..a055aad3f600a4482502da148467bfe622751fe3 100644
--- a/src/blua/liolib.c
+++ b/src/blua/liolib.c
@@ -284,8 +284,16 @@ void Got_LuaFile(UINT8 **cp, INT32 playernum)
 	// Push the first argument (file handle or nil) on the stack
 	if (success)
 	{
+		char mode[4];
+
+		// Ensure we are opening in binary mode
+		// (if it's a text file, newlines have been converted already)
+		strcpy(mode, luafiletransfers->mode);
+		if (!strchr(mode, 'b'))
+			strcat(mode, "b");
+
 		pf = newfile(gL); // Create and push the file handle
-		*pf = fopen(luafiletransfers->realfilename, luafiletransfers->mode); // Open the file
+		*pf = fopen(luafiletransfers->realfilename, mode); // Open the file
 		if (!*pf)
 			I_Error("Can't open file \"%s\"\n", luafiletransfers->realfilename); // The file SHOULD exist
 	}
@@ -313,17 +321,14 @@ void Got_LuaFile(UINT8 **cp, INT32 playernum)
 
 	RemoveLuaFileTransfer();
 
-	if (server && luafiletransfers)
+	if (waitingforluafilecommand)
 	{
-		if (FIL_FileOK(luafiletransfers->realfilename))
-			SV_PrepareSendLuaFileToNextNode();
-		else
-		{
-			// Send a net command with 0 as its first byte to indicate the file couldn't be opened
-			success = 0;
-			SendNetXCmd(XD_LUAFILE, &success, 1);
-		}
+		waitingforluafilecommand = false;
+		CL_PrepareDownloadLuaFile();
 	}
+
+	if (server && luafiletransfers)
+		SV_PrepareSendLuaFile();
 }
 
 
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index 7ff836c6ef17f55d27ad6be87f33da0cb00a7892..95927710a3cda2f281b27f0c03dc0f0d6c7313a1 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -1163,6 +1163,497 @@ static void CV_LoadPlayerNames(UINT8 **p)
 }
 
 #ifdef CLIENT_LOADINGSCREEN
+#define SNAKE_SPEED 5
+
+#define SNAKE_NUM_BLOCKS_X 20
+#define SNAKE_NUM_BLOCKS_Y 10
+#define SNAKE_BLOCK_SIZE 12
+#define SNAKE_BORDER_SIZE 12
+
+#define SNAKE_MAP_WIDTH  (SNAKE_NUM_BLOCKS_X * SNAKE_BLOCK_SIZE)
+#define SNAKE_MAP_HEIGHT (SNAKE_NUM_BLOCKS_Y * SNAKE_BLOCK_SIZE)
+
+#define SNAKE_LEFT_X ((BASEVIDWIDTH - SNAKE_MAP_WIDTH) / 2 - SNAKE_BORDER_SIZE)
+#define SNAKE_RIGHT_X (SNAKE_LEFT_X + SNAKE_MAP_WIDTH + SNAKE_BORDER_SIZE * 2 - 1)
+#define SNAKE_BOTTOM_Y (BASEVIDHEIGHT - 48)
+#define SNAKE_TOP_Y (SNAKE_BOTTOM_Y - SNAKE_MAP_HEIGHT - SNAKE_BORDER_SIZE * 2 + 1)
+
+enum snake_bonustype_s {
+	SNAKE_BONUS_NONE = 0,
+	SNAKE_BONUS_SLOW,
+	SNAKE_BONUS_FAST,
+	SNAKE_BONUS_GHOST,
+	SNAKE_BONUS_NUKE,
+	SNAKE_BONUS_SCISSORS,
+	SNAKE_BONUS_REVERSE,
+	SNAKE_BONUS_EGGMAN,
+	SNAKE_NUM_BONUSES,
+};
+
+static const char *snake_bonuspatches[] = {
+	NULL,
+	"DL_SLOW",
+	"TVSSC0",
+	"TVIVC0",
+	"TVARC0",
+	"DL_SCISSORS",
+	"TVRCC0",
+	"TVEGC0",
+};
+
+static const char *snake_backgrounds[] = {
+	"RVPUMICF",
+	"FRSTRCKF",
+	"TAR",
+	"MMFLRB4",
+	"RVDARKF1",
+	"RVZWALF1",
+	"RVZWALF4",
+	"RVZWALF5",
+	"RVZGRS02",
+	"RVZGRS04",
+};
+
+typedef struct snake_s
+{
+	boolean paused;
+	boolean pausepressed;
+	tic_t time;
+	tic_t nextupdate;
+	boolean gameover;
+	UINT8 background;
+
+	UINT16 snakelength;
+	enum snake_bonustype_s snakebonus;
+	tic_t snakebonustime;
+	UINT8 snakex[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
+	UINT8 snakey[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
+	UINT8 snakedir[SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y];
+
+	UINT8 applex;
+	UINT8 appley;
+
+	enum snake_bonustype_s bonustype;
+	UINT8 bonusx;
+	UINT8 bonusy;
+} snake_t;
+
+static snake_t *snake = NULL;
+
+static void Snake_Initialise(void)
+{
+	if (!snake)
+		snake = malloc(sizeof(snake_t));
+
+	snake->paused = false;
+	snake->pausepressed = false;
+	snake->time = 0;
+	snake->nextupdate = SNAKE_SPEED;
+	snake->gameover = false;
+	snake->background = M_RandomKey(sizeof(snake_backgrounds) / sizeof(*snake_backgrounds));
+
+	snake->snakelength = 1;
+	snake->snakebonus = SNAKE_BONUS_NONE;
+	snake->snakex[0] = M_RandomKey(SNAKE_NUM_BLOCKS_X);
+	snake->snakey[0] = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
+	snake->snakedir[0] = 0;
+	snake->snakedir[1] = 0;
+
+	snake->applex = M_RandomKey(SNAKE_NUM_BLOCKS_X);
+	snake->appley = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
+
+	snake->bonustype = SNAKE_BONUS_NONE;
+}
+
+static UINT8 Snake_GetOppositeDir(UINT8 dir)
+{
+	if (dir == 1 || dir == 3)
+		return dir + 1;
+	else if (dir == 2 || dir == 4)
+		return dir - 1;
+	else
+		return 12 + 5 - dir;
+}
+
+static void Snake_FindFreeSlot(UINT8 *x, UINT8 *y, UINT8 headx, UINT8 heady)
+{
+	UINT16 i;
+
+	do
+	{
+		*x = M_RandomKey(SNAKE_NUM_BLOCKS_X);
+		*y = M_RandomKey(SNAKE_NUM_BLOCKS_Y);
+
+		for (i = 0; i < snake->snakelength; i++)
+			if (*x == snake->snakex[i] && *y == snake->snakey[i])
+				break;
+	} while (i < snake->snakelength || (*x == headx && *y == heady));
+}
+
+static void Snake_Handle(void)
+{
+	UINT8 x, y;
+	UINT8 oldx, oldy;
+	UINT16 i;
+
+	// Handle retry
+	if (snake->gameover && (PLAYER1INPUTDOWN(gc_jump) || gamekeydown[KEY_ENTER]))
+	{
+		Snake_Initialise();
+		snake->pausepressed = true; // Avoid accidental pause on respawn
+	}
+
+	// Handle pause
+	if (PLAYER1INPUTDOWN(gc_pause) || gamekeydown[KEY_ENTER])
+	{
+		if (!snake->pausepressed)
+			snake->paused = !snake->paused;
+		snake->pausepressed = true;
+	}
+	else
+		snake->pausepressed = false;
+
+	if (snake->paused)
+		return;
+
+	snake->time++;
+
+	x = snake->snakex[0];
+	y = snake->snakey[0];
+	oldx = snake->snakex[1];
+	oldy = snake->snakey[1];
+
+	// Update direction
+	if (gamekeydown[KEY_LEFTARROW])
+	{
+		if (snake->snakelength < 2 || x <= oldx)
+			snake->snakedir[0] = 1;
+	}
+	else if (gamekeydown[KEY_RIGHTARROW])
+	{
+		if (snake->snakelength < 2 || x >= oldx)
+			snake->snakedir[0] = 2;
+	}
+	else if (gamekeydown[KEY_UPARROW])
+	{
+		if (snake->snakelength < 2 || y <= oldy)
+			snake->snakedir[0] = 3;
+	}
+	else if (gamekeydown[KEY_DOWNARROW])
+	{
+		if (snake->snakelength < 2 || y >= oldy)
+			snake->snakedir[0] = 4;
+	}
+
+	if (snake->snakebonustime)
+	{
+		snake->snakebonustime--;
+		if (!snake->snakebonustime)
+			snake->snakebonus = SNAKE_BONUS_NONE;
+	}
+
+	snake->nextupdate--;
+	if (snake->nextupdate)
+		return;
+	if (snake->snakebonus == SNAKE_BONUS_SLOW)
+		snake->nextupdate = SNAKE_SPEED * 2;
+	else if (snake->snakebonus == SNAKE_BONUS_FAST)
+		snake->nextupdate = SNAKE_SPEED * 2 / 3;
+	else
+		snake->nextupdate = SNAKE_SPEED;
+
+	if (snake->gameover)
+		return;
+
+	// Find new position
+	switch (snake->snakedir[0])
+	{
+		case 1:
+			if (x > 0)
+				x--;
+			else
+				snake->gameover = true;
+			break;
+		case 2:
+			if (x < SNAKE_NUM_BLOCKS_X - 1)
+				x++;
+			else
+				snake->gameover = true;
+			break;
+		case 3:
+			if (y > 0)
+				y--;
+			else
+				snake->gameover = true;
+			break;
+		case 4:
+			if (y < SNAKE_NUM_BLOCKS_Y - 1)
+				y++;
+			else
+				snake->gameover = true;
+			break;
+	}
+
+	// Check collision with snake
+	if (snake->snakebonus != SNAKE_BONUS_GHOST)
+		for (i = 1; i < snake->snakelength - 1; i++)
+			if (x == snake->snakex[i] && y == snake->snakey[i])
+			{
+				if (snake->snakebonus == SNAKE_BONUS_SCISSORS)
+				{
+					snake->snakebonus = SNAKE_BONUS_NONE;
+					snake->snakelength = i;
+					S_StartSound(NULL, sfx_adderr);
+				}
+				else
+					snake->gameover = true;
+			}
+
+	if (snake->gameover)
+	{
+		S_StartSound(NULL, sfx_lose);
+		return;
+	}
+
+	// Check collision with apple
+	if (x == snake->applex && y == snake->appley)
+	{
+		if (snake->snakelength + 1 < SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y)
+		{
+			snake->snakelength++;
+			snake->snakex  [snake->snakelength - 1] = snake->snakex  [snake->snakelength - 2];
+			snake->snakey  [snake->snakelength - 1] = snake->snakey  [snake->snakelength - 2];
+			snake->snakedir[snake->snakelength - 1] = snake->snakedir[snake->snakelength - 2];
+		}
+
+		// Spawn new apple
+		Snake_FindFreeSlot(&snake->applex, &snake->appley, x, y);
+
+		// Spawn new bonus
+		if (!(snake->snakelength % 5))
+		{
+			do
+			{
+				snake->bonustype = M_RandomKey(SNAKE_NUM_BONUSES - 1) + 1;
+			} while (snake->snakelength > SNAKE_NUM_BLOCKS_X * SNAKE_NUM_BLOCKS_Y * 3 / 4
+				&& (snake->bonustype == SNAKE_BONUS_EGGMAN || snake->bonustype == SNAKE_BONUS_FAST || snake->bonustype == SNAKE_BONUS_REVERSE));
+
+			Snake_FindFreeSlot(&snake->bonusx, &snake->bonusy, x, y);
+		}
+
+		S_StartSound(NULL, sfx_s3k6b);
+	}
+
+	if (snake->snakelength > 1 && snake->snakedir[0])
+	{
+		UINT8 dir = snake->snakedir[0];
+
+		oldx = snake->snakex[1];
+		oldy = snake->snakey[1];
+
+		// Move
+		for (i = snake->snakelength - 1; i > 0; i--)
+		{
+			snake->snakex[i] = snake->snakex[i - 1];
+			snake->snakey[i] = snake->snakey[i - 1];
+			snake->snakedir[i] = snake->snakedir[i - 1];
+		}
+
+		// Handle corners
+		if      (x < oldx && dir == 3)
+			dir = 5;
+		else if (x > oldx && dir == 3)
+			dir = 6;
+		else if (x < oldx && dir == 4)
+			dir = 7;
+		else if (x > oldx && dir == 4)
+			dir = 8;
+		else if (y < oldy && dir == 1)
+			dir = 9;
+		else if (y < oldy && dir == 2)
+			dir = 10;
+		else if (y > oldy && dir == 1)
+			dir = 11;
+		else if (y > oldy && dir == 2)
+			dir = 12;
+		snake->snakedir[1] = dir;
+	}
+
+	snake->snakex[0] = x;
+	snake->snakey[0] = y;
+
+	// Check collision with bonus
+	if (snake->bonustype != SNAKE_BONUS_NONE && x == snake->bonusx && y == snake->bonusy)
+	{
+		S_StartSound(NULL, sfx_ncchip);
+
+		switch (snake->bonustype)
+		{
+		case SNAKE_BONUS_SLOW:
+			snake->snakebonus = SNAKE_BONUS_SLOW;
+			snake->snakebonustime = 20 * TICRATE;
+			break;
+		case SNAKE_BONUS_FAST:
+			snake->snakebonus = SNAKE_BONUS_FAST;
+			snake->snakebonustime = 20 * TICRATE;
+			break;
+		case SNAKE_BONUS_GHOST:
+			snake->snakebonus = SNAKE_BONUS_GHOST;
+			snake->snakebonustime = 10 * TICRATE;
+			break;
+		case SNAKE_BONUS_NUKE:
+			for (i = 0; i < snake->snakelength; i++)
+			{
+				snake->snakex  [i] = snake->snakex  [0];
+				snake->snakey  [i] = snake->snakey  [0];
+				snake->snakedir[i] = snake->snakedir[0];
+			}
+
+			S_StartSound(NULL, sfx_bkpoof);
+			break;
+		case SNAKE_BONUS_SCISSORS:
+			snake->snakebonus = SNAKE_BONUS_SCISSORS;
+			snake->snakebonustime = 60 * TICRATE;
+			break;
+		case SNAKE_BONUS_REVERSE:
+			for (i = 0; i < (snake->snakelength + 1) / 2; i++)
+			{
+				UINT16 i2 = snake->snakelength - 1 - i;
+				UINT8 tmpx   = snake->snakex  [i];
+				UINT8 tmpy   = snake->snakey  [i];
+				UINT8 tmpdir = snake->snakedir[i];
+
+				// Swap first segment with last segment
+				snake->snakex  [i] = snake->snakex  [i2];
+				snake->snakey  [i] = snake->snakey  [i2];
+				snake->snakedir[i] = Snake_GetOppositeDir(snake->snakedir[i2]);
+				snake->snakex  [i2] = tmpx;
+				snake->snakey  [i2] = tmpy;
+				snake->snakedir[i2] = Snake_GetOppositeDir(tmpdir);
+			}
+
+			snake->snakedir[0] = 0;
+
+			S_StartSound(NULL, sfx_gravch);
+			break;
+		default:
+			if (snake->snakebonus != SNAKE_BONUS_GHOST)
+			{
+				snake->gameover = true;
+				S_StartSound(NULL, sfx_lose);
+			}
+		}
+
+		snake->bonustype = SNAKE_BONUS_NONE;
+	}
+}
+
+static void Snake_Draw(void)
+{
+	INT16 i;
+
+	// Background
+	V_DrawFlatFill(
+		SNAKE_LEFT_X + SNAKE_BORDER_SIZE,
+		SNAKE_TOP_Y  + SNAKE_BORDER_SIZE,
+		SNAKE_MAP_WIDTH,
+		SNAKE_MAP_HEIGHT,
+		W_GetNumForName(snake_backgrounds[snake->background])
+	);
+
+	// Borders
+	V_DrawFill(SNAKE_LEFT_X, SNAKE_TOP_Y, SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_BORDER_SIZE, 242); // Top
+	V_DrawFill(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_TOP_Y, SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, 242); // Right
+	V_DrawFill(SNAKE_LEFT_X + SNAKE_BORDER_SIZE, SNAKE_TOP_Y + SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, SNAKE_BORDER_SIZE + SNAKE_MAP_WIDTH, SNAKE_BORDER_SIZE, 242); // Bottom
+	V_DrawFill(SNAKE_LEFT_X, SNAKE_TOP_Y + SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE, SNAKE_BORDER_SIZE + SNAKE_MAP_HEIGHT, 242); // Left
+
+	// Apple
+	V_DrawFixedPatch(
+		(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->applex * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
+		(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->appley * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
+		FRACUNIT / 4,
+		0,
+		W_CachePatchLongName("DL_APPLE", PU_HUDGFX),
+		NULL
+	);
+
+	// Bonus
+	if (snake->bonustype != SNAKE_BONUS_NONE)
+		V_DrawFixedPatch(
+			(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->bonusx * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2    ) * FRACUNIT,
+			(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->bonusy * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2 + 4) * FRACUNIT,
+			FRACUNIT / 2,
+			0,
+			W_CachePatchLongName(snake_bonuspatches[snake->bonustype], PU_HUDGFX),
+			NULL
+		);
+
+	// Snake
+	if (!snake->gameover || snake->time % 8 < 8 / 2) // Blink if game over
+	{
+		for (i = snake->snakelength - 1; i >= 0; i--)
+		{
+			const char *patchname;
+			UINT8 dir = snake->snakedir[i];
+
+			if (i == 0) // Head
+			{
+				switch (dir)
+				{
+					case  1: patchname = "DL_SNAKEHEAD_L"; break;
+					case  2: patchname = "DL_SNAKEHEAD_R"; break;
+					case  3: patchname = "DL_SNAKEHEAD_T"; break;
+					case  4: patchname = "DL_SNAKEHEAD_B"; break;
+					default: patchname = "DL_SNAKEHEAD_M";
+				}
+			}
+			else // Body
+			{
+				switch (dir)
+				{
+					case  1: patchname = "DL_SNAKEBODY_L"; break;
+					case  2: patchname = "DL_SNAKEBODY_R"; break;
+					case  3: patchname = "DL_SNAKEBODY_T"; break;
+					case  4: patchname = "DL_SNAKEBODY_B"; break;
+					case  5: patchname = "DL_SNAKEBODY_LT"; break;
+					case  6: patchname = "DL_SNAKEBODY_RT"; break;
+					case  7: patchname = "DL_SNAKEBODY_LB"; break;
+					case  8: patchname = "DL_SNAKEBODY_RB"; break;
+					case  9: patchname = "DL_SNAKEBODY_TL"; break;
+					case 10: patchname = "DL_SNAKEBODY_TR"; break;
+					case 11: patchname = "DL_SNAKEBODY_BL"; break;
+					case 12: patchname = "DL_SNAKEBODY_BR"; break;
+					default: patchname = "DL_SNAKEBODY_B";
+				}
+			}
+
+			V_DrawFixedPatch(
+				(SNAKE_LEFT_X + SNAKE_BORDER_SIZE + snake->snakex[i] * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
+				(SNAKE_TOP_Y  + SNAKE_BORDER_SIZE + snake->snakey[i] * SNAKE_BLOCK_SIZE + SNAKE_BLOCK_SIZE / 2) * FRACUNIT,
+				i == 0 && dir == 0 ? FRACUNIT / 5 : FRACUNIT / 2,
+				snake->snakebonus == SNAKE_BONUS_GHOST ? V_TRANSLUCENT : 0,
+				W_CachePatchLongName(patchname, PU_HUDGFX),
+				NULL
+			);
+		}
+	}
+
+	// Length
+	V_DrawString(SNAKE_RIGHT_X + 4, SNAKE_TOP_Y, V_MONOSPACE, va("%u", snake->snakelength));
+
+	// Bonus
+	if (snake->snakebonus != SNAKE_BONUS_NONE
+	&& (snake->snakebonustime >= 3 * TICRATE || snake->time % 4 < 4 / 2))
+		V_DrawFixedPatch(
+			(SNAKE_RIGHT_X + 10) * FRACUNIT,
+			(SNAKE_TOP_Y + 24) * FRACUNIT,
+			FRACUNIT / 2,
+			0,
+			W_CachePatchLongName(snake_bonuspatches[snake->snakebonus], PU_HUDGFX),
+			NULL
+		);
+}
+
 //
 // CL_DrawConnectionStatus
 //
@@ -1177,8 +1668,8 @@ static inline void CL_DrawConnectionStatus(void)
 		V_DrawFadeScreen(0xFF00, 16); // force default
 
 	// Draw the bottom box.
-	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-24-8, 32, 1);
-	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-24, V_YELLOWMAP, "Press ESC to abort");
+	M_DrawTextBox(BASEVIDWIDTH/2-128-8, BASEVIDHEIGHT-16-8, 32, 1);
+	V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-16, V_YELLOWMAP, "Press ESC to abort");
 
 	if (cl_mode != CL_DOWNLOADFILES)
 	{
@@ -1187,8 +1678,9 @@ static inline void CL_DrawConnectionStatus(void)
 		// 15 pal entries total.
 		const char *cltext;
 
-		for (i = 0; i < 16; ++i)
-			V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-24, 16, 8, palstart + ((animtime - i) & 15));
+		if (!(cl_mode == CL_DOWNLOADSAVEGAME && lastfilenum != -1))
+			for (i = 0; i < 16; ++i)
+				V_DrawFill((BASEVIDWIDTH/2-128) + (i * 16), BASEVIDHEIGHT-16, 16, 8, palstart + ((animtime - i) & 15));
 
 		switch (cl_mode)
 		{
@@ -1196,11 +1688,23 @@ static inline void CL_DrawConnectionStatus(void)
 			case CL_DOWNLOADSAVEGAME:
 				if (lastfilenum != -1)
 				{
+					UINT32 currentsize = fileneeded[lastfilenum].currentsize;
+					UINT32 totalsize = fileneeded[lastfilenum].totalsize;
+					INT32 dldlength;
+
 					cltext = M_GetText("Downloading game state...");
 					Net_GetNetStat();
-					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE,
-						va(" %4uK",fileneeded[lastfilenum].currentsize>>10));
-					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE,
+
+					dldlength = (INT32)((currentsize/(double)totalsize) * 256);
+					if (dldlength > 256)
+						dldlength = 256;
+					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+					V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
+
+					V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
+						va(" %4uK/%4uK",currentsize>>10,totalsize>>10));
+
+					V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
 						va("%3.1fK/s ", ((double)getbps)/1024));
 				}
 				else
@@ -1215,7 +1719,7 @@ static inline void CL_DrawConnectionStatus(void)
 				cltext = M_GetText("Connecting to server...");
 				break;
 		}
-		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP, cltext);
+		V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP, cltext);
 	}
 	else
 	{
@@ -1226,12 +1730,14 @@ static inline void CL_DrawConnectionStatus(void)
 			fileneeded_t *file = &fileneeded[lastfilenum];
 			char *filename = file->filename;
 
+			Snake_Draw();
+
 			Net_GetNetStat();
 			dldlength = (INT32)((file->currentsize/(double)file->totalsize) * 256);
 			if (dldlength > 256)
 				dldlength = 256;
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, 256, 8, 111);
-			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, dldlength, 8, 96);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, 256, 8, 111);
+			V_DrawFill(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, dldlength, 8, 96);
 
 			memset(tempname, 0, sizeof(tempname));
 			// offset filename to just the name only part
@@ -1249,15 +1755,15 @@ static inline void CL_DrawConnectionStatus(void)
 				strncpy(tempname, filename, sizeof(tempname)-1);
 			}
 
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP,
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
 				va(M_GetText("Downloading \"%s\""), tempname));
-			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE,
+			V_DrawString(BASEVIDWIDTH/2-128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
 				va(" %4uK/%4uK",fileneeded[lastfilenum].currentsize>>10,file->totalsize>>10));
-			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-24, V_20TRANS|V_MONOSPACE,
+			V_DrawRightAlignedString(BASEVIDWIDTH/2+128, BASEVIDHEIGHT-16, V_20TRANS|V_MONOSPACE,
 				va("%3.1fK/s ", ((double)getbps)/1024));
 		}
 		else
-			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-24-32, V_YELLOWMAP,
+			V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT-16-24, V_YELLOWMAP,
 				M_GetText("Waiting to download files..."));
 	}
 }
@@ -1605,7 +2111,7 @@ static void SV_SendSaveGame(INT32 node)
 		WRITEUINT32(savebuffer, 0);
 	}
 
-	SV_SendRam(node, buffertosend, length, SF_RAM, 0);
+	AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
 	save_p = NULL;
 
 	// Remember when we started sending the savegame so we can handle timeouts
@@ -1985,8 +2491,11 @@ static boolean CL_ServerConnectionSearchTicker(boolean viams, tic_t *asksent)
 					return false;
 				}
 				// no problem if can't send packet, we will retry later
-				if (CL_SendRequestFile())
+				if (CL_SendFileRequest())
+				{
 					cl_mode = CL_DOWNLOADFILES;
+					Snake_Initialise();
+				}
 			}
 		}
 		else
@@ -2050,6 +2559,12 @@ static boolean CL_ServerConnectionTicker(boolean viams, const char *tmpsave, tic
 			if (waitmore)
 				break; // exit the case
 
+			if (snake)
+			{
+				free(snake);
+				snake = NULL;
+			}
+
 			cl_mode = CL_ASKJOIN; // don't break case continue to cljoin request now
 			/* FALLTHRU */
 
@@ -2096,36 +2611,54 @@ static boolean CL_ServerConnectionTicker(boolean viams, const char *tmpsave, tic
 	// Call it only once by tic
 	if (*oldtic != I_GetTime())
 	{
-		INT32 key;
-
 		I_OsPolling();
-		key = I_GetKey();
-		if (key == KEY_ESCAPE || key == KEY_JOY1+1)
+		for (; eventtail != eventhead; eventtail = (eventtail+1) & (MAXEVENTS-1))
+			G_MapEventsToControls(&events[eventtail]);
+
+		if (gamekeydown[KEY_ESCAPE] || gamekeydown[KEY_JOY1+1])
 		{
 			CONS_Printf(M_GetText("Network game synchronization aborted.\n"));
 //				M_StartMessage(M_GetText("Network game synchronization aborted.\n\nPress ESC\n"), NULL, MM_NOTHING);
+
+			if (snake)
+			{
+				free(snake);
+				snake = NULL;
+			}
+
 			D_QuitNetGame();
 			CL_Reset();
 			D_StartTitle();
+			memset(gamekeydown, 0, NUMKEYS);
 			return false;
 		}
+		else if (cl_mode == CL_DOWNLOADFILES && snake)
+			Snake_Handle();
+
+		if (client && (cl_mode == CL_DOWNLOADFILES || cl_mode == CL_DOWNLOADSAVEGAME))
+			FileReceiveTicker();
 
 		// why are these here? this is for servers, we're a client
 		//if (key == 's' && server)
 		//	doomcom->numnodes = (INT16)pnumnodes;
-		//SV_FileSendTicker();
+		//FileSendTicker();
 		*oldtic = I_GetTime();
 
 #ifdef CLIENT_LOADINGSCREEN
 		if (client && cl_mode != CL_CONNECTED && cl_mode != CL_ABORTED)
 		{
-			F_MenuPresTicker(true); // title sky
-			F_TitleScreenTicker(true);
-			F_TitleScreenDrawer();
+			if (cl_mode != CL_DOWNLOADFILES && cl_mode != CL_DOWNLOADSAVEGAME)
+			{
+				F_MenuPresTicker(true); // title sky
+				F_TitleScreenTicker(true);
+				F_TitleScreenDrawer();
+			}
 			CL_DrawConnectionStatus();
 			I_UpdateNoVsync(); // page flip or blit buffer
 			if (moviemode)
 				M_SaveFrame();
+			S_UpdateSounds();
+			S_UpdateClosedCaptions();
 		}
 #else
 		CON_Drawer();
@@ -3247,6 +3780,7 @@ void D_QuitNetGame(void)
 	CloseNetFile();
 	RemoveAllLuaFileTransfers();
 	waitingforluafiletransfer = false;
+	waitingforluafilecommand = false;
 
 	if (server)
 	{
@@ -3920,13 +4454,23 @@ static void HandlePacketFromAwayNode(SINT8 node)
 				break;
 			}
 			SERVERONLY
-			Got_Filetxpak();
+			PT_FileFragment();
+			break;
+
+		case PT_FILEACK:
+			if (server)
+				PT_FileAck();
+			break;
+
+		case PT_FILERECEIVED:
+			if (server)
+				PT_FileReceived();
 			break;
 
 		case PT_REQUESTFILE:
 			if (server)
 			{
-				if (!cv_downloading.value || !Got_RequestFilePak(node))
+				if (!cv_downloading.value || !PT_RequestFile(node))
 					Net_CloseConnection(node); // close connection if one of the requested files could not be sent, or you disabled downloading anyway
 			}
 			else
@@ -4222,11 +4766,7 @@ static void HandlePacketFromPlayer(SINT8 node)
 			break;
 		case PT_ASKLUAFILE:
 			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_ASKED)
-			{
-				char *name = va("%s" PATHSEP "%s", luafiledir, luafiletransfers->filename);
-				boolean textmode = !strchr(luafiletransfers->mode, 'b');
-				SV_SendLuaFile(node, name, textmode);
-			}
+				AddLuaFileToSendQueue(node, luafiletransfers->realfilename);
 			break;
 		case PT_HASLUAFILE:
 			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING)
@@ -4357,7 +4897,15 @@ static void HandlePacketFromPlayer(SINT8 node)
 				break;
 			}
 			if (client)
-				Got_Filetxpak();
+				PT_FileFragment();
+			break;
+		case PT_FILEACK:
+			if (server)
+				PT_FileAck();
+			break;
+		case PT_FILERECEIVED:
+			if (server)
+				PT_FileReceived();
 			break;
 		case PT_SENDINGLUAFILE:
 			if (client)
@@ -5047,7 +5595,7 @@ void NetUpdate(void)
 		CON_Ticker();
 	}
 
-	SV_FileSendTicker();
+	FileSendTicker();
 }
 
 /** Returns the number of players playing.
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index 1135b043c1bd0a01c0c45da6c0883cfcdc550784..6b06764f95dd9fc23ee6bad43d3f7a4e6c0651d0 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -76,6 +76,8 @@ typedef enum
 	                  // In addition, this packet can't occupy all the available slots.
 
 	PT_FILEFRAGMENT = PT_CANFAIL, // A part of a file.
+	PT_FILEACK,
+	PT_FILERECEIVED,
 
 	PT_TEXTCMD,       // Extra text commands from the client.
 	PT_TEXTCMD2,      // Splitscreen text commands.
@@ -324,13 +326,30 @@ typedef struct
 	UINT8 varlengthinputs[0]; // Playernames and netvars
 } ATTRPACK serverconfig_pak;
 
-typedef struct {
+typedef struct
+{
 	UINT8 fileid;
+	UINT32 filesize;
+	UINT8 iteration;
 	UINT32 position;
 	UINT16 size;
 	UINT8 data[0]; // Size is variable using hardware_MAXPACKETLENGTH
 } ATTRPACK filetx_pak;
 
+typedef struct
+{
+	UINT32 start;
+	UINT32 acks;
+} ATTRPACK fileacksegment_t;
+
+typedef struct
+{
+	UINT8 fileid;
+	UINT8 iteration;
+	UINT8 numsegments;
+	fileacksegment_t segments[0];
+} ATTRPACK fileack_pak;
+
 #ifdef _MSC_VER
 #pragma warning(default : 4200)
 #endif
@@ -446,6 +465,8 @@ typedef struct
 		UINT8 resynchgot;                   //
 		UINT8 textcmd[MAXTEXTCMD+1];        //       66049 bytes (wut??? 64k??? More like 257 bytes...)
 		filetx_pak filetxpak;               //         139 bytes
+		fileack_pak fileack;
+		UINT8 filereceived;
 		clientconfig_pak clientcfg;         //         136 bytes
 		UINT8 md5sum[16];
 		serverinfo_pak serverinfo;          //        1024 bytes
diff --git a/src/d_main.c b/src/d_main.c
index e4978205cc1b38028014dd7fc927cf638322713c..6057e65a770056ae74ff92dd9e05a83fa5decb37 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1266,6 +1266,9 @@ void D_SRB2Main(void)
 
 	// rand() needs seeded regardless of password
 	srand((unsigned int)time(NULL));
+	rand();
+	rand();
+	rand();
 
 	if (M_CheckParm("-password") && M_IsNextParm())
 		D_SetPassword(M_GetNextParm());
diff --git a/src/d_net.c b/src/d_net.c
index 8e62b8d25a43c6b5b8aeb40a73a1aee51329d30c..2823ce2191ebcde53dc1c2a21d1a8ff9e03bef08 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -806,6 +806,9 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"HASLUAFILE",
 
 	"FILEFRAGMENT",
+	"FILEACK",
+	"FILERECEIVED",
+
 	"TEXTCMD",
 	"TEXTCMD2",
 	"CLIENTJOIN",
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index 84070135a165d1e54a7b884135275ba08a29957b..8b75741ae177cd7c3b78b6ae9b11b546090ad263 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -502,6 +502,8 @@ void D_RegisterServerCommands(void)
 	COM_AddCommand("archivetest", Command_Archivetest_f);
 #endif
 
+	COM_AddCommand("downloads", Command_Downloads_f);
+
 	// for master server connection
 	AddMServCommands();
 
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 6d3ac7f9d02705539bff9afd890d911d5088346e..7b99fddfbb4e25776d20582ea4998333a2ca946f 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -56,7 +56,7 @@
 #include <errno.h>
 
 // Prototypes
-static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid);
+static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid);
 
 // Sender structure
 typedef struct filetx_s
@@ -69,7 +69,6 @@ typedef struct filetx_s
 	UINT32 size; // Size of the file
 	UINT8 fileid;
 	INT32 node; // Destination
-	boolean textmode; // For files requested by Lua without the "b" option
 	struct filetx_s *next; // Next file in the list
 } filetx_t;
 
@@ -77,8 +76,13 @@ typedef struct filetx_s
 typedef struct filetran_s
 {
 	filetx_t *txlist; // Linked list of all files for the node
+	UINT8 iteration;
+	UINT8 ackediteration;
 	UINT32 position; // The current position in the file
+	boolean *ackedfragments;
+	UINT32 ackedsize;
 	FILE *currentfile; // The file currently being sent/received
+	tic_t dontsenduntil;
 } filetran_t;
 static filetran_t transfer[MAXNETNODES];
 
@@ -88,8 +92,20 @@ static filetran_t transfer[MAXNETNODES];
 // Receiver structure
 INT32 fileneedednum; // Number of files needed to join the server
 fileneeded_t fileneeded[MAX_WADFILES]; // List of needed files
+static tic_t lasttimeackpacketsent = 0;
 char downloaddir[512] = "DOWNLOAD";
 
+// For resuming failed downloads
+typedef struct
+{
+	char filename[MAX_WADPATH];
+	UINT8 md5sum[16];
+	boolean *receivedfragments;
+	UINT32 fragmentsize;
+	UINT32 currentsize;
+} pauseddownload_t;
+static pauseddownload_t *pauseddownload = NULL;
+
 #ifdef CLIENT_LOADINGSCREEN
 // for cl loading screen
 INT32 lastfilenum = -1;
@@ -97,6 +113,7 @@ INT32 lastfilenum = -1;
 
 luafiletransfer_t *luafiletransfers = NULL;
 boolean waitingforluafiletransfer = false;
+boolean waitingforluafilecommand = false;
 char luafiledir[256 + 16] = "luafiles";
 
 
@@ -159,25 +176,29 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
 	for (i = 0; i < fileneedednum; i++)
 	{
 		fileneeded[i].status = FS_NOTFOUND; // We haven't even started looking for the file yet
+		fileneeded[i].justdownloaded = false;
 		filestatus = READUINT8(p); // The first byte is the file status
 		fileneeded[i].willsend = (UINT8)(filestatus >> 4);
 		fileneeded[i].totalsize = READUINT32(p); // The four next bytes are the file size
 		fileneeded[i].file = NULL; // The file isn't open yet
 		READSTRINGN(p, fileneeded[i].filename, MAX_WADPATH); // The next bytes are the file name
 		READMEM(p, fileneeded[i].md5sum, 16); // The last 16 bytes are the file checksum
-		fileneeded[i].textmode = false;
 	}
 }
 
 void CL_PrepareDownloadSaveGame(const char *tmpsave)
 {
+#ifdef CLIENT_LOADINGSCREEN
+	lastfilenum = -1;
+#endif
+
 	fileneedednum = 1;
 	fileneeded[0].status = FS_REQUESTED;
+	fileneeded[0].justdownloaded = false;
 	fileneeded[0].totalsize = UINT32_MAX;
 	fileneeded[0].file = NULL;
 	memset(fileneeded[0].md5sum, 0, 16);
 	strcpy(fileneeded[0].filename, tmpsave);
-	fileneeded[0].textmode = false;
 }
 
 /** Checks the server to see if we CAN download all the files,
@@ -246,6 +267,31 @@ boolean CL_CheckDownloadable(void)
 	return false;
 }
 
+/** Returns true if a needed file transfer can be resumed
+  *
+  * \param file The needed file to resume the transfer for
+  * \return True if the transfer can be resumed
+  *
+  */
+static boolean CL_CanResumeDownload(fileneeded_t *file)
+{
+	return pauseddownload
+		&& !strcmp(pauseddownload->filename, file->filename) // Same name
+		&& !memcmp(pauseddownload->md5sum, file->md5sum, 16) // Same checksum
+		&& pauseddownload->fragmentsize == file->fragmentsize; // Same fragment size
+}
+
+void CL_AbortDownloadResume(void)
+{
+	if (!pauseddownload)
+		return;
+
+	free(pauseddownload->receivedfragments);
+	remove(pauseddownload->filename);
+	free(pauseddownload);
+	pauseddownload = NULL;
+}
+
 /** Sends requests for files in the ::fileneeded table with a status of
   * ::FS_NOTFOUND.
   *
@@ -253,7 +299,7 @@ boolean CL_CheckDownloadable(void)
   * \note Sends a PT_REQUESTFILE packet
   *
   */
-boolean CL_SendRequestFile(void)
+boolean CL_SendFileRequest(void)
 {
 	char *p;
 	INT32 i;
@@ -298,7 +344,7 @@ boolean CL_SendRequestFile(void)
 
 // get request filepak and put it on the send queue
 // returns false if a requested file was not found or cannot be sent
-boolean Got_RequestFilePak(INT32 node)
+boolean PT_RequestFile(INT32 node)
 {
 	char wad[MAX_WADPATH+1];
 	UINT8 *p = netbuffer->u.textcmd;
@@ -309,7 +355,7 @@ boolean Got_RequestFilePak(INT32 node)
 		if (id == 0xFF)
 			break;
 		READSTRINGN(p, wad, MAX_WADPATH);
-		if (!SV_SendFile(node, wad, id))
+		if (!AddFileToSendQueue(node, wad, id))
 		{
 			SV_AbortSendFiles(node);
 			return false; // don't read the rest of the files
@@ -462,8 +508,6 @@ void AddLuaFileTransfer(const char *filename, const char *mode)
 	luafiletransfer_t *filetransfer;
 	static INT32 id;
 
-	//CONS_Printf("AddLuaFileTransfer \"%s\"\n", filename);
-
 	// Find the last transfer in the list and set a pointer to its "next" field
 	prevnext = &luafiletransfers;
 	while (*prevnext)
@@ -493,26 +537,11 @@ void AddLuaFileTransfer(const char *filename, const char *mode)
 
 	strlcpy(filetransfer->mode, mode, sizeof(filetransfer->mode));
 
-	if (server)
-	{
-		INT32 i;
-
-		// Set status to "waiting" for everyone
-		for (i = 0; i < MAXNETNODES; i++)
-			filetransfer->nodestatus[i] = LFTNS_WAITING;
-
-		if (!luafiletransfers->next) // Only if there is no transfer already going on
-		{
-			if (FIL_ReadFileOK(filetransfer->realfilename))
-				SV_PrepareSendLuaFileToNextNode();
-			else
-			{
-				// Send a net command with 0 as its first byte to indicate the file couldn't be opened
-				UINT8 success = 0;
-				SendNetXCmd(XD_LUAFILE, &success, 1);
-			}
-		}
-	}
+	// Only if there is no transfer already going on
+	if (server && filetransfer == luafiletransfers)
+		SV_PrepareSendLuaFile();
+	else
+		filetransfer->ongoing = false;
 
 	// Store the callback so it can be called once everyone has the file
 	filetransfer->id = id;
@@ -526,7 +555,7 @@ void AddLuaFileTransfer(const char *filename, const char *mode)
 	}
 }
 
-void SV_PrepareSendLuaFileToNextNode(void)
+static void SV_PrepareSendLuaFileToNextNode(void)
 {
 	INT32 i;
 	UINT8 success = 1;
@@ -550,6 +579,45 @@ void SV_PrepareSendLuaFileToNextNode(void)
 	SendNetXCmd(XD_LUAFILE, &success, 1);
 }
 
+void SV_PrepareSendLuaFile(void)
+{
+	char *binfilename;
+	INT32 i;
+
+	luafiletransfers->ongoing = true;
+
+	// Set status to "waiting" for everyone
+	for (i = 0; i < MAXNETNODES; i++)
+		luafiletransfers->nodestatus[i] = LFTNS_WAITING;
+
+	if (FIL_ReadFileOK(luafiletransfers->realfilename))
+	{
+		// If opening in text mode, convert all newlines to LF
+		if (!strchr(luafiletransfers->mode, 'b'))
+		{
+			binfilename = strdup(va("%s" PATHSEP "$$$%d%d.tmp",
+				luafiledir, rand(), rand()));
+			if (!binfilename)
+				I_Error("SV_PrepareSendLuaFile: Out of memory\n");
+
+			if (!FIL_ConvertTextFileToBinary(luafiletransfers->realfilename, binfilename))
+				I_Error("SV_PrepareSendLuaFile: Failed to convert file newlines\n");
+
+			// Use the temporary file instead
+			free(luafiletransfers->realfilename);
+			luafiletransfers->realfilename = binfilename;
+		}
+
+		SV_PrepareSendLuaFileToNextNode();
+	}
+	else
+	{
+		// Send a net command with 0 as its first byte to indicate the file couldn't be opened
+		UINT8 success = 0;
+		SendNetXCmd(XD_LUAFILE, &success, 1);
+	}
+}
+
 void SV_HandleLuaFileSent(UINT8 node)
 {
 	luafiletransfers->nodestatus[node] = LFTNS_SENT;
@@ -560,6 +628,10 @@ void RemoveLuaFileTransfer(void)
 {
 	luafiletransfer_t *filetransfer = luafiletransfers;
 
+	// If it was a temporary file, delete it
+	if (server && !strchr(filetransfer->mode, 'b'))
+		remove(filetransfer->realfilename);
+
 	RemoveLuaFileCallback(filetransfer->id);
 
 	luafiletransfers = filetransfer->next;
@@ -596,20 +668,28 @@ void CL_PrepareDownloadLuaFile(void)
 		return;
 	}
 
+	if (luafiletransfers->ongoing)
+	{
+		waitingforluafilecommand = true;
+		return;
+	}
+
 	// Tell the server we are ready to receive the file
 	netbuffer->packettype = PT_ASKLUAFILE;
 	HSendPacket(servernode, true, 0, 0);
 
 	fileneedednum = 1;
 	fileneeded[0].status = FS_REQUESTED;
+	fileneeded[0].justdownloaded = false;
 	fileneeded[0].totalsize = UINT32_MAX;
 	fileneeded[0].file = NULL;
 	memset(fileneeded[0].md5sum, 0, 16);
 	strcpy(fileneeded[0].filename, luafiletransfers->realfilename);
-	fileneeded[0].textmode = !strchr(luafiletransfers->mode, 'b');
 
 	// Make sure all directories in the file path exist
 	MakePathDirs(fileneeded[0].filename);
+
+	luafiletransfers->ongoing = true;
 }
 
 // Number of files to send
@@ -620,12 +700,12 @@ static INT32 filestosend = 0;
   *
   * \param node The node to send the file to
   * \param filename The file to send
-  * \param fileid ???
-  * \sa SV_SendRam
-  * \sa SV_SendLuaFile
+  * \param fileid The index of the file in the list of added files
+  * \sa AddRamToSendQueue
+  * \sa AddLuaFileToSendQueue
   *
   */
-static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid)
+static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid)
 {
 	filetx_t **q; // A pointer to the "next" field of the last file in the list
 	filetx_t *p; // The new file request
@@ -643,7 +723,7 @@ static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid)
 	// Allocate a file request and append it to the file list
 	p = *q = (filetx_t *)malloc(sizeof (filetx_t));
 	if (!p)
-		I_Error("SV_SendFile: No more memory\n");
+		I_Error("AddFileToSendQueue: No more memory\n");
 
 	// Initialise with zeros
 	memset(p, 0, sizeof (filetx_t));
@@ -651,7 +731,7 @@ static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid)
 	// Allocate the file name
 	p->id.filename = (char *)malloc(MAX_WADPATH);
 	if (!p->id.filename)
-		I_Error("SV_SendFile: No more memory\n");
+		I_Error("AddFileToSendQueue: No more memory\n");
 
 	// Set the file name and get rid of the path
 	strlcpy(p->id.filename, filename, MAX_WADPATH);
@@ -711,12 +791,12 @@ static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid)
   * \param data The memory block to send
   * \param size The size of the block in bytes
   * \param freemethod How to free the block after it has been sent
-  * \param fileid ???
-  * \sa SV_SendFile
-  * \sa SV_SendLuaFile
+  * \param fileid The index of the file in the list of added files
+  * \sa AddFileToSendQueue
+  * \sa AddLuaFileToSendQueue
   *
   */
-void SV_SendRam(INT32 node, void *data, size_t size, freemethod_t freemethod, UINT8 fileid)
+void AddRamToSendQueue(INT32 node, void *data, size_t size, freemethod_t freemethod, UINT8 fileid)
 {
 	filetx_t **q; // A pointer to the "next" field of the last file in the list
 	filetx_t *p; // The new file request
@@ -729,7 +809,7 @@ void SV_SendRam(INT32 node, void *data, size_t size, freemethod_t freemethod, UI
 	// Allocate a file request and append it to the file list
 	p = *q = (filetx_t *)malloc(sizeof (filetx_t));
 	if (!p)
-		I_Error("SV_SendRam: No more memory\n");
+		I_Error("AddRamToSendQueue: No more memory\n");
 
 	// Initialise with zeros
 	memset(p, 0, sizeof (filetx_t));
@@ -749,11 +829,11 @@ void SV_SendRam(INT32 node, void *data, size_t size, freemethod_t freemethod, UI
   *
   * \param node The node to send the file to
   * \param filename The file to send
-  * \sa SV_SendFile
-  * \sa SV_SendRam
+  * \sa AddFileToSendQueue
+  * \sa AddRamToSendQueue
   *
   */
-boolean SV_SendLuaFile(INT32 node, const char *filename, boolean textmode)
+boolean AddLuaFileToSendQueue(INT32 node, const char *filename)
 {
 	filetx_t **q; // A pointer to the "next" field of the last file in the list
 	filetx_t *p; // The new file request
@@ -770,7 +850,7 @@ boolean SV_SendLuaFile(INT32 node, const char *filename, boolean textmode)
 	// Allocate a file request and append it to the file list
 	p = *q = (filetx_t *)malloc(sizeof (filetx_t));
 	if (!p)
-		I_Error("SV_SendLuaFile: No more memory\n");
+		I_Error("AddLuaFileToSendQueue: No more memory\n");
 
 	// Initialise with zeros
 	memset(p, 0, sizeof (filetx_t));
@@ -778,15 +858,12 @@ boolean SV_SendLuaFile(INT32 node, const char *filename, boolean textmode)
 	// Allocate the file name
 	p->id.filename = (char *)malloc(MAX_WADPATH); // !!!
 	if (!p->id.filename)
-		I_Error("SV_SendLuaFile: No more memory\n");
+		I_Error("AddLuaFileToSendQueue: No more memory\n");
 
 	// Set the file name and get rid of the path
 	strlcpy(p->id.filename, filename, MAX_WADPATH); // !!!
 	//nameonly(p->id.filename);
 
-	// Open in text mode if required by the Lua script
-	p->textmode = textmode;
-
 	DEBFILE(va("Sending Lua file %s to %d\n", filename, node));
 	p->ram = SF_FILE; // It's a file, we need to close it and free its name once we're done sending it
 	p->next = NULL; // End of list
@@ -804,7 +881,8 @@ static void SV_EndFileSend(INT32 node)
 {
 	filetx_t *p = transfer[node].txlist;
 
-	// Free the file request according to the freemethod parameter used with SV_SendFile/Ram
+	// Free the file request according to the freemethod
+	// parameter used with AddFileToSendQueue/AddRamToSendQueue
 	switch (p->ram)
 	{
 		case SF_FILE: // It's a file, close it and free its filename
@@ -829,43 +907,32 @@ static void SV_EndFileSend(INT32 node)
 
 	// Indicate that the transmission is over
 	transfer[node].currentfile = NULL;
+	if (transfer[node].ackedfragments)
+		free(transfer[node].ackedfragments);
+	transfer[node].ackedfragments = NULL;
 
 	filestosend--;
 }
 
 #define PACKETPERTIC net_bandwidth/(TICRATE*software_MAXPACKETLENGTH)
+#define FILEFRAGMENTSIZE (software_MAXPACKETLENGTH - (FILETXHEADER + BASEPACKETSIZE))
 
 /** Handles file transmission
-  *
-  * \todo Use an acknowledging method more adapted to file transmission
-  *       The current download speed suffers from lack of ack packets,
-  *       especially when the one downloading has high latency
   *
   */
-void SV_FileSendTicker(void)
+void FileSendTicker(void)
 {
 	static INT32 currentnode = 0;
 	filetx_pak *p;
-	size_t size;
+	size_t fragmentsize;
 	filetx_t *f;
 	INT32 packetsent, ram, i, j;
-	INT32 maxpacketsent;
 
 	if (!filestosend) // No file to send
 		return;
 
-	if (cv_downloadspeed.value) // New (and experimental) behavior
-	{
+	if (cv_downloadspeed.value) // New behavior
 		packetsent = cv_downloadspeed.value;
-		// Don't send more packets than we have free acks
-#ifndef NONET
-		maxpacketsent = Net_GetFreeAcks(false) - 5; // Let 5 extra acks just in case
-#else
-		maxpacketsent = 1;
-#endif
-		if (packetsent > maxpacketsent && maxpacketsent > 0) // Send at least one packet
-			packetsent = maxpacketsent;
-	}
 	else // Old behavior
 	{
 		packetsent = PACKETPERTIC;
@@ -882,11 +949,12 @@ void SV_FileSendTicker(void)
 			i = (i+1) % MAXNETNODES, j++)
 		{
 			if (transfer[i].txlist)
-				goto found;
+				break;
 		}
 		// no transfer to do
-		I_Error("filestosend=%d but no file to send found\n", filestosend);
-	found:
+		if (j >= MAXNETNODES)
+			I_Error("filestosend=%d but no file to send found\n", filestosend);
+
 		currentnode = (i+1) % MAXNETNODES;
 		f = transfer[i].txlist;
 		ram = f->ram;
@@ -899,7 +967,7 @@ void SV_FileSendTicker(void)
 				long filesize;
 
 				transfer[i].currentfile =
-					fopen(f->id.filename, f->textmode ? "r" : "rb");
+					fopen(f->id.filename, "rb");
 
 				if (!transfer[i].currentfile)
 					I_Error("File %s does not exist",
@@ -920,57 +988,232 @@ void SV_FileSendTicker(void)
 			}
 			else // Sending RAM
 				transfer[i].currentfile = (FILE *)1; // Set currentfile to a non-null value to indicate that it is open
+
+			transfer[i].iteration = 1;
+			transfer[i].ackediteration = 0;
 			transfer[i].position = 0;
+			transfer[i].ackedsize = 0;
+
+			transfer[i].ackedfragments = calloc(f->size / FILEFRAGMENTSIZE + 1, sizeof(*transfer[i].ackedfragments));
+			if (!transfer[i].ackedfragments)
+				I_Error("FileSendTicker: No more memory\n");
+
+			transfer[i].dontsenduntil = 0;
+		}
+
+		// If the client hasn't acknowledged any fragment from the previous iteration,
+		// it is most likely because their acks haven't had enough time to reach the server
+		// yet, due to latency. In that case, we wait a little to avoid useless resend.
+		if (I_GetTime() < transfer[i].dontsenduntil)
+			continue;
+
+		// Find the first non-acknowledged fragment
+		while (transfer[i].ackedfragments[transfer[i].position / FILEFRAGMENTSIZE])
+		{
+			transfer[i].position += FILEFRAGMENTSIZE;
+			if (transfer[i].position >= f->size)
+			{
+				if (transfer[i].ackediteration < transfer[i].iteration)
+					transfer[i].dontsenduntil = I_GetTime() + TICRATE / 2;
+
+				transfer[i].position = 0;
+				transfer[i].iteration++;
+			}
 		}
 
 		// Build a packet containing a file fragment
 		p = &netbuffer->u.filetxpak;
-		size = software_MAXPACKETLENGTH - (FILETXHEADER + BASEPACKETSIZE);
-		if (f->size-transfer[i].position < size)
-			size = f->size-transfer[i].position;
+		fragmentsize = FILEFRAGMENTSIZE;
+		if (f->size-transfer[i].position < fragmentsize)
+			fragmentsize = f->size-transfer[i].position;
 		if (ram)
-			M_Memcpy(p->data, &f->id.ram[transfer[i].position], size);
+			M_Memcpy(p->data, &f->id.ram[transfer[i].position], fragmentsize);
 		else
 		{
-			size_t n = fread(p->data, 1, size, transfer[i].currentfile);
-			if (n != size) // Either an error or Windows turning CR-LF into LF
-			{
-				if (f->textmode && feof(transfer[i].currentfile))
-                    size = n;
-				else if (fread(p->data, 1, size, transfer[i].currentfile) != size)
-					I_Error("SV_FileSendTicker: can't read %s byte on %s at %d because %s", sizeu1(size), f->id.filename, transfer[i].position, M_FileError(transfer[i].currentfile));
-			}
+			fseek(transfer[i].currentfile, transfer[i].position, SEEK_SET);
+
+			if (fread(p->data, 1, fragmentsize, transfer[i].currentfile) != fragmentsize)
+				I_Error("FileSendTicker: can't read %s byte on %s at %d because %s", sizeu1(fragmentsize), f->id.filename, transfer[i].position, M_FileError(transfer[i].currentfile));
 		}
+		p->iteration = transfer[i].iteration;
 		p->position = LONG(transfer[i].position);
-		// Put flag so receiver knows the total size
-		if (transfer[i].position + size == f->size || (f->textmode && feof(transfer[i].currentfile)))
-			p->position |= LONG(0x80000000);
 		p->fileid = f->fileid;
-		p->size = SHORT((UINT16)size);
+		p->filesize = LONG(f->size);
+		p->size = SHORT((UINT16)FILEFRAGMENTSIZE);
 
 		// Send the packet
-		if (HSendPacket(i, true, 0, FILETXHEADER + size)) // Reliable SEND
+		if (HSendPacket(i, false, 0, FILETXHEADER + fragmentsize)) // Don't use the default acknowledgement system
 		{ // Success
-			transfer[i].position = (UINT32)(transfer[i].position + size);
-			if (transfer[i].position == f->size || (f->textmode && feof(transfer[i].currentfile))) // Finish?
-				SV_EndFileSend(i);
+			transfer[i].position = (UINT32)(transfer[i].position + fragmentsize);
+			if (transfer[i].position >= f->size)
+			{
+				if (transfer[i].ackediteration < transfer[i].iteration)
+					transfer[i].dontsenduntil = I_GetTime() + TICRATE / 2;
+
+				transfer[i].position = 0;
+				transfer[i].iteration++;
+			}
 		}
 		else
 		{ // Not sent for some odd reason, retry at next call
-			if (!ram)
-				fseek(transfer[i].currentfile,transfer[i].position, SEEK_SET);
 			// Exit the while (can't send this one so why should i send the next?)
 			break;
 		}
 	}
 }
 
-void Got_Filetxpak(void)
+void PT_FileAck(void)
+{
+	fileack_pak *packet = &netbuffer->u.fileack;
+	INT32 node = doomcom->remotenode;
+	filetran_t *trans = &transfer[node];
+	INT32 i, j;
+
+	// Wrong file id? Ignore it, it's probably a late packet
+	if (!(trans->txlist && packet->fileid == trans->txlist->fileid))
+		return;
+
+	if (packet->numsegments * sizeof(*packet->segments) != doomcom->datalength - BASEPACKETSIZE - sizeof(*packet))
+	{
+		Net_CloseConnection(node);
+		return;
+	}
+
+	if (packet->iteration > trans->ackediteration)
+	{
+		trans->ackediteration = packet->iteration;
+		if (trans->ackediteration >= trans->iteration - 1)
+			trans->dontsenduntil = 0;
+	}
+
+	for (i = 0; i < packet->numsegments; i++)
+	{
+		fileacksegment_t *segment = &packet->segments[i];
+
+		for (j = 0; j < 32; j++)
+			if (LONG(segment->acks) & (1 << j))
+			{
+				if (LONG(segment->start) * FILEFRAGMENTSIZE >= trans->txlist->size)
+				{
+					Net_CloseConnection(node);
+					return;
+				}
+
+				if (!trans->ackedfragments[LONG(segment->start) + j])
+				{
+					trans->ackedfragments[LONG(segment->start) + j] = true;
+					trans->ackedsize += FILEFRAGMENTSIZE;
+
+					// If the last missing fragment was acked, finish!
+					if (trans->ackedsize == trans->txlist->size)
+					{
+						SV_EndFileSend(node);
+						return;
+					}
+				}
+			}
+	}
+}
+
+void PT_FileReceived(void)
+{
+	filetx_t *trans = transfer[doomcom->remotenode].txlist;
+
+	if (trans && netbuffer->u.filereceived == trans->fileid)
+		SV_EndFileSend(doomcom->remotenode);
+}
+
+static void SendAckPacket(fileack_pak *packet, UINT8 fileid)
+{
+	size_t packetsize;
+	INT32 i;
+
+	packetsize = sizeof(*packet) + packet->numsegments * sizeof(*packet->segments);
+
+	// Finalise the packet
+	packet->fileid = fileid;
+	for (i = 0; i < packet->numsegments; i++)
+	{
+		packet->segments[i].start = LONG(packet->segments[i].start);
+		packet->segments[i].acks = LONG(packet->segments[i].acks);
+	}
+
+	// Send the packet
+	netbuffer->packettype = PT_FILEACK;
+	M_Memcpy(&netbuffer->u.fileack, packet, packetsize);
+	HSendPacket(servernode, false, 0, packetsize);
+
+	// Clear the packet
+	memset(packet, 0, sizeof(*packet) + 512);
+}
+
+static void AddFragmentToAckPacket(fileack_pak *packet, UINT8 iteration, UINT32 fragmentpos, UINT8 fileid)
+{
+	fileacksegment_t *segment = &packet->segments[packet->numsegments - 1];
+
+	packet->iteration = max(packet->iteration, iteration);
+
+    if (packet->numsegments == 0
+		|| fragmentpos < segment->start
+		|| fragmentpos - segment->start >= 32)
+	{
+		// If the packet becomes too big, send it
+		if ((packet->numsegments + 1) * sizeof(*segment) > 512)
+			SendAckPacket(packet, fileid);
+
+		packet->numsegments++;
+		segment = &packet->segments[packet->numsegments - 1];
+		segment->start = fragmentpos;
+	}
+
+	// Set the bit that represents the fragment
+	segment->acks |= 1 << (fragmentpos - segment->start);
+}
+
+void FileReceiveTicker(void)
+{
+	INT32 i;
+
+	for (i = 0; i < fileneedednum; i++)
+	{
+		fileneeded_t *file = &fileneeded[i];
+
+		if (file->status == FS_DOWNLOADING)
+		{
+			if (lasttimeackpacketsent - I_GetTime() > TICRATE / 2)
+				SendAckPacket(file->ackpacket, i);
+
+			// When resuming a tranfer, start with telling
+			// the server what parts we already received
+			if (file->ackresendposition != UINT32_MAX && file->status == FS_DOWNLOADING)
+			{
+				// Acknowledge ~70 MB/s, whichs means the client sends ~18 KB/s
+				INT32 j;
+				for (j = 0; j < 2048; j++)
+				{
+					if (file->receivedfragments[file->ackresendposition])
+						AddFragmentToAckPacket(file->ackpacket, file->iteration, file->ackresendposition, i);
+
+					file->ackresendposition++;
+					if (file->ackresendposition * file->fragmentsize >= file->totalsize)
+					{
+						file->ackresendposition = UINT32_MAX;
+						break;
+					}
+				}
+			}
+		}
+	}
+}
+
+void PT_FileFragment(void)
 {
 	INT32 filenum = netbuffer->u.filetxpak.fileid;
 	fileneeded_t *file = &fileneeded[filenum];
+	UINT32 fragmentpos = LONG(netbuffer->u.filetxpak.position);
+	UINT16 fragmentsize = SHORT(netbuffer->u.filetxpak.size);
+	UINT16 boundedfragmentsize = doomcom->datalength - BASEPACKETSIZE - sizeof(netbuffer->u.filetxpak);
 	char *filename;
-	static INT32 filetime = 0;
 
 	filename = va("%s", file->filename);
 	nameonly(filename);
@@ -995,49 +1238,105 @@ void Got_Filetxpak(void)
 	if (file->status == FS_REQUESTED)
 	{
 		if (file->file)
-			I_Error("Got_Filetxpak: already open file\n");
-		file->file = fopen(filename, file->textmode ? "w" : "wb");
-		if (!file->file)
-			I_Error("Can't create file %s: %s", filename, strerror(errno));
-		CONS_Printf("\r%s...\n",filename);
-		file->currentsize = 0;
+			I_Error("PT_FileFragment: already open file\n");
+
 		file->status = FS_DOWNLOADING;
+		file->fragmentsize = fragmentsize;
+		file->iteration = 0;
+
+		file->ackpacket = calloc(1, sizeof(*file->ackpacket) + 512);
+		if (!file->ackpacket)
+			I_Error("FileSendTicker: No more memory\n");
+
+		if (CL_CanResumeDownload(file))
+		{
+			file->file = fopen(filename, "r+b");
+			if (!file->file)
+				I_Error("Can't reopen file %s: %s", filename, strerror(errno));
+			CONS_Printf("\r%s...\n", filename);
+
+			CONS_Printf("Resuming download...\n");
+			file->currentsize = pauseddownload->currentsize;
+			file->receivedfragments = pauseddownload->receivedfragments;
+			file->ackresendposition = 0;
+
+			free(pauseddownload);
+			pauseddownload = NULL;
+		}
+		else
+		{
+			CL_AbortDownloadResume();
+
+			file->file = fopen(filename, "wb");
+			if (!file->file)
+				I_Error("Can't create file %s: %s", filename, strerror(errno));
+
+			CONS_Printf("\r%s...\n",filename);
+
+			file->currentsize = 0;
+			file->totalsize = LONG(netbuffer->u.filetxpak.filesize);
+			file->ackresendposition = UINT32_MAX; // Only used for resumed downloads
+
+			file->receivedfragments = calloc(file->totalsize / fragmentsize + 1, sizeof(*file->receivedfragments));
+			if (!file->receivedfragments)
+				I_Error("FileSendTicker: No more memory\n");
+		}
+
+		lasttimeackpacketsent = I_GetTime();
 	}
 
 	if (file->status == FS_DOWNLOADING)
 	{
-		UINT32 pos = LONG(netbuffer->u.filetxpak.position);
-		UINT16 size = SHORT(netbuffer->u.filetxpak.size);
-		// Use a special trick to know when the file is complete (not always used)
-		// WARNING: file fragments can arrive out of order so don't stop yet!
-		if (pos & 0x80000000)
-		{
-			pos &= ~0x80000000;
-			file->totalsize = pos + size;
-		}
-		// We can receive packet in the wrong order, anyway all os support gaped file
-		fseek(file->file, pos, SEEK_SET);
-		if (size && fwrite(netbuffer->u.filetxpak.data,size,1,file->file) != 1)
-			I_Error("Can't write to %s: %s\n",filename, M_FileError(file->file));
-		file->currentsize += size;
-
-		// Finished?
-		if (file->currentsize == file->totalsize)
+		if (fragmentpos >= file->totalsize)
+			I_Error("Invalid file fragment\n");
+
+		file->iteration = max(file->iteration, netbuffer->u.filetxpak.iteration);
+
+		if (!file->receivedfragments[fragmentpos / fragmentsize]) // Not received yet
 		{
-			fclose(file->file);
-			file->file = NULL;
-			file->status = FS_FOUND;
-			CONS_Printf(M_GetText("Downloading %s...(done)\n"),
-				filename);
-			if (luafiletransfers)
+			file->receivedfragments[fragmentpos / fragmentsize] = true;
+
+			// We can receive packets in the wrong order, anyway all OSes support gaped files
+			fseek(file->file, fragmentpos, SEEK_SET);
+			if (fragmentsize && fwrite(netbuffer->u.filetxpak.data, boundedfragmentsize, 1, file->file) != 1)
+				I_Error("Can't write to %s: %s\n",filename, M_FileError(file->file));
+			file->currentsize += boundedfragmentsize;
+
+			AddFragmentToAckPacket(file->ackpacket, file->iteration, fragmentpos / fragmentsize, filenum);
+
+			// Finished?
+			if (file->currentsize == file->totalsize)
 			{
+				fclose(file->file);
+				file->file = NULL;
+				free(file->receivedfragments);
+				free(file->ackpacket);
+				file->status = FS_FOUND;
+				file->justdownloaded = true;
+				CONS_Printf(M_GetText("Downloading %s...(done)\n"),
+					filename);
+
 				// Tell the server we have received the file
-				netbuffer->packettype = PT_HASLUAFILE;
-				HSendPacket(servernode, true, 0, 0);
+				netbuffer->packettype = PT_FILERECEIVED;
+				netbuffer->u.filereceived = filenum;
+				HSendPacket(servernode, true, 0, 1);
+
+				if (luafiletransfers)
+				{
+					// Tell the server we have received the file
+					netbuffer->packettype = PT_HASLUAFILE;
+					HSendPacket(servernode, true, 0, 0);
+				}
 			}
 		}
+		else // Already received
+		{
+			// If they are sending us the fragment again, it's probably because
+			// they missed our previous ack, so we must re-acknowledge it
+			AddFragmentToAckPacket(file->ackpacket, file->iteration, fragmentpos / fragmentsize, filenum);
+		}
 	}
-	else
+	else if (!file->justdownloaded)
 	{
 		const char *s;
 		switch(file->status)
@@ -1060,12 +1359,6 @@ void Got_Filetxpak(void)
 		}
 		I_Error("Received a file not requested (file id: %d, file status: %s)\n", filenum, s);
 	}
-	// Send ack back quickly
-	if (++filetime == 3)
-	{
-		Net_SendAcks(servernode);
-		filetime = 0;
-	}
 
 #ifdef CLIENT_LOADINGSCREEN
 	lastfilenum = filenum;
@@ -1078,7 +1371,7 @@ void Got_Filetxpak(void)
  * \return True if the node is downloading a file
  *
  */
-boolean SV_SendingFile(INT32 node)
+boolean SendingFile(INT32 node)
 {
 	return transfer[node].txlist != NULL;
 }
@@ -1107,12 +1400,62 @@ void CloseNetFile(void)
 		if (fileneeded[i].status == FS_DOWNLOADING && fileneeded[i].file)
 		{
 			fclose(fileneeded[i].file);
-			// File is not complete delete it
-			remove(fileneeded[i].filename);
+			free(fileneeded[i].ackpacket);
+
+			if (!pauseddownload && i != 0) // 0 is either srb2.srb or the gamestate...
+			{
+				// Don't remove the file, save it for later in case we resume the download
+				pauseddownload = malloc(sizeof(*pauseddownload));
+				if (!pauseddownload)
+					I_Error("CloseNetFile: No more memory\n");
+
+				strcpy(pauseddownload->filename, fileneeded[i].filename);
+				memcpy(pauseddownload->md5sum, fileneeded[i].md5sum, 16);
+				pauseddownload->currentsize = fileneeded[i].currentsize;
+				pauseddownload->receivedfragments = fileneeded[i].receivedfragments;
+				pauseddownload->fragmentsize = fileneeded[i].fragmentsize;
+			}
+			else
+			{
+				free(fileneeded[i].receivedfragments);
+				// File is not complete delete it
+				remove(fileneeded[i].filename);
+			}
 		}
+}
+
+void Command_Downloads_f(void)
+{
+	INT32 node;
+
+	for (node = 0; node < MAXNETNODES; node++)
+		if (transfer[node].txlist
+		&& transfer[node].txlist->ram == SF_FILE) // Node is downloading a file?
+		{
+			const char *name = transfer[node].txlist->id.filename;
+			UINT32 position = transfer[node].ackedsize;
+			UINT32 size = transfer[node].txlist->size;
+			char ratecolor;
 
-	// Remove PT_FILEFRAGMENT from acknowledge list
-	Net_AbortPacketType(PT_FILEFRAGMENT);
+			// Avoid division by zero errors
+			if (!size)
+				size = 1;
+
+			name = &name[strlen(name) - nameonlylength(name)];
+			switch (4 * (position - 1) / size)
+			{
+				case 0: ratecolor = '\x85'; break;
+				case 1: ratecolor = '\x87'; break;
+				case 2: ratecolor = '\x82'; break;
+				case 3: ratecolor = '\x83'; break;
+				default: ratecolor = '\x80';
+			}
+
+			CONS_Printf("%2d  %c%s  ", node, ratecolor, name); // Node and file name
+			CONS_Printf("\x80%uK\x84/\x80%uK ", position / 1024, size / 1024); // Progress in kB
+			CONS_Printf("\x80(%c%u%%\x80)  ", ratecolor, (UINT32)(100.0 * position / size)); // Progress in %
+			CONS_Printf("%s\n", I_GetNodeAddress(node)); // Address and newline
+		}
 }
 
 // Functions cut and pasted from Doomatic :)
diff --git a/src/d_netfil.h b/src/d_netfil.h
index 7d6efada03cde57a29791257a126363d0ffb6d8d..2225157cb4762acd20d38d904709b5e50d4d16c7 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -14,6 +14,7 @@
 #define __D_NETFIL__
 
 #include "d_net.h"
+#include "d_clisrv.h"
 #include "w_wad.h"
 
 typedef enum
@@ -39,12 +40,18 @@ typedef struct
 	UINT8 willsend; // Is the server willing to send it?
 	char filename[MAX_WADPATH];
 	UINT8 md5sum[16];
+	filestatus_t status; // The value returned by recsearch
+	boolean justdownloaded; // To prevent late fragments from causing an I_Error
+
 	// Used only for download
 	FILE *file;
+	boolean *receivedfragments;
+	UINT32 fragmentsize;
+	UINT8 iteration;
+	fileack_pak *ackpacket;
 	UINT32 currentsize;
 	UINT32 totalsize;
-	filestatus_t status; // The value returned by recsearch
-	boolean textmode; // For files requested by Lua without the "b" option
+	UINT32 ackresendposition; // Used when resuming downloads
 } fileneeded_t;
 
 extern INT32 fileneedednum;
@@ -61,16 +68,20 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave);
 
 INT32 CL_CheckFiles(void);
 void CL_LoadServerFiles(void);
-void SV_SendRam(INT32 node, void *data, size_t size, freemethod_t freemethod,
+void AddRamToSendQueue(INT32 node, void *data, size_t size, freemethod_t freemethod,
 	UINT8 fileid);
 
-void SV_FileSendTicker(void);
-void Got_Filetxpak(void);
-boolean SV_SendingFile(INT32 node);
+void FileSendTicker(void);
+void PT_FileAck(void);
+void PT_FileReceived(void);
+boolean SendingFile(INT32 node);
+
+void FileReceiveTicker(void);
+void PT_FileFragment(void);
 
 boolean CL_CheckDownloadable(void);
-boolean CL_SendRequestFile(void);
-boolean Got_RequestFilePak(INT32 node);
+boolean CL_SendFileRequest(void);
+boolean PT_RequestFile(INT32 node);
 
 typedef enum
 {
@@ -86,18 +97,19 @@ typedef struct luafiletransfer_s
 	char *realfilename;
 	char mode[4]; // rb+/wb+/ab+ + null character
 	INT32 id; // Callback ID
+	boolean ongoing;
 	luafiletransfernodestatus_t nodestatus[MAXNETNODES];
 	struct luafiletransfer_s *next;
 } luafiletransfer_t;
 
 extern luafiletransfer_t *luafiletransfers;
 extern boolean waitingforluafiletransfer;
+extern boolean waitingforluafilecommand;
 extern char luafiledir[256 + 16];
 
 void AddLuaFileTransfer(const char *filename, const char *mode);
-void SV_PrepareSendLuaFileToNextNode(void);
-boolean SV_SendLuaFile(INT32 node, const char *filename, boolean textmode);
-void SV_PrepareSendLuaFile(const char *filename);
+void SV_PrepareSendLuaFile(void);
+boolean AddLuaFileToSendQueue(INT32 node, const char *filename);
 void SV_HandleLuaFileSent(UINT8 node);
 void RemoveLuaFileTransfer(void);
 void RemoveAllLuaFileTransfers(void);
@@ -110,6 +122,9 @@ void MakePathDirs(char *path);
 
 void SV_AbortSendFiles(INT32 node);
 void CloseNetFile(void);
+void CL_AbortDownloadResume(void);
+
+void Command_Downloads_f(void);
 
 boolean fileexist(char *filename, time_t ptime);
 
diff --git a/src/dehacked.c b/src/dehacked.c
index bd8a07b9019d09a3035e2d93f44dffa34e146386..88aaeac5473a0a3328cc432946316774f93af6fe 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -769,7 +769,8 @@ static void readthing(MYFILE *f, INT32 num)
 static void readskincolor(MYFILE *f, INT32 num)
 {
 	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
-	char *word, *word2, *word3;
+	char *word = s;
+	char *word2;
 	char *tmp;
 
 	Color_cons_t[num].value = num;
@@ -781,32 +782,31 @@ static void readskincolor(MYFILE *f, INT32 num)
 			if (s[0] == '\n')
 				break;
 
+			// First remove trailing newline, if there is one
+			tmp = strchr(s, '\n');
+			if (tmp)
+				*tmp = '\0';
+
 			tmp = strchr(s, '#');
 			if (tmp)
 				*tmp = '\0';
 			if (s == tmp)
 				continue; // Skip comment lines, but don't break.
 
-			word = strtok(s, " ");
-			if (word)
-				strupr(word);
+			// Get the part before the " = "
+			tmp = strchr(s, '=');
+			if (tmp)
+				*(tmp-1) = '\0';
 			else
 				break;
+			strupr(word);
 
-			word2 = strtok(NULL, " = ");
-			if (word2) {
-				word3 = Z_StrDup(word2);
-				strupr(word2);
-			} else
-				break;
-			if (word2[strlen(word2)-1] == '\n')
-				word2[strlen(word2)-1] = '\0';
-			if (word3[strlen(word3)-1] == '\n')
-				word3[strlen(word3)-1] = '\0';
+			// Now get the part after
+			word2 = tmp += 2;
 
 			if (fastcmp(word, "NAME"))
 			{
-				deh_strlcpy(skincolors[num].name, word3,
+				deh_strlcpy(skincolors[num].name, word2,
 					sizeof (skincolors[num].name), va("Skincolor %d: name", num));
 			}
 			else if (fastcmp(word, "RAMP"))
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index f08fae105b0cefc43f3b85eea0351395ba3958de..6fe9f7a36627fb9c5cae862e5124b059e59e0cd3 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -2610,7 +2610,7 @@ EXPORT void HWRAPI(CreateModelVBOs) (model_t *model)
 	}
 }
 
-#define BUFFER_OFFSET(i) ((char*)(i))
+#define BUFFER_OFFSET(i) ((void*)(i))
 
 static void DrawModelEx(model_t *model, INT32 frameIndex, INT32 duration, INT32 tics, INT32 nextFrameIndex, FTransform *pos, float scale, UINT8 flipped, FSurfaceInfo *Surface)
 {
diff --git a/src/i_tcp.c b/src/i_tcp.c
index 373ea1bd03836514cd219030fd72fb1431791126..5180869a53b60772ded94d18495bd17906cef137 100644
--- a/src/i_tcp.c
+++ b/src/i_tcp.c
@@ -487,7 +487,7 @@ static void cleanupnodes(void)
 
 	// Why can't I start at zero?
 	for (j = 1; j < MAXNETNODES; j++)
-		if (!(nodeingame[j] || SV_SendingFile(j)))
+		if (!(nodeingame[j] || SendingFile(j)))
 			nodeconnected[j] = false;
 }
 
diff --git a/src/lua_script.c b/src/lua_script.c
index 06ea18b0ee9e574604a38cc524ed4da9c185ffd5..374294d6eb065ae21a226b0a314f69bc47f6aa34 100644
--- a/src/lua_script.c
+++ b/src/lua_script.c
@@ -447,11 +447,13 @@ void LUA_ClearExtVars(void)
 // Use this variable to prevent certain functions from running
 // if they were not called on lump load
 // (i.e. they were called in hooks or coroutines etc)
-boolean lua_lumploading = false;
+INT32 lua_lumploading = 0;
 
 // Load a script from a MYFILE
-static inline void LUA_LoadFile(MYFILE *f, char *name)
+static inline void LUA_LoadFile(MYFILE *f, char *name, boolean noresults)
 {
+	int errorhandlerindex;
+
 	if (!name)
 		name = wadfiles[f->wad]->filename;
 	CONS_Printf("Loading Lua script from %s\n", name);
@@ -460,21 +462,22 @@ static inline void LUA_LoadFile(MYFILE *f, char *name)
 	lua_pushinteger(gL, f->wad);
 	lua_setfield(gL, LUA_REGISTRYINDEX, "WAD");
 
-	lua_lumploading = true; // turn on loading flag
+	lua_lumploading++; // turn on loading flag
 
 	lua_pushcfunction(gL, LUA_GetErrorMessage);
-	if (luaL_loadbuffer(gL, f->data, f->size, va("@%s",name)) || lua_pcall(gL, 0, 0, lua_gettop(gL) - 1)) {
+	errorhandlerindex = lua_gettop(gL);
+	if (luaL_loadbuffer(gL, f->data, f->size, va("@%s",name)) || lua_pcall(gL, 0, noresults ? 0 : LUA_MULTRET, lua_gettop(gL) - 1)) {
 		CONS_Alert(CONS_WARNING,"%s\n",lua_tostring(gL,-1));
 		lua_pop(gL,1);
 	}
 	lua_gc(gL, LUA_GCCOLLECT, 0);
-	lua_pop(gL, 1); // Pop error handler
+	lua_remove(gL, errorhandlerindex);
 
-	lua_lumploading = false; // turn off again
+	lua_lumploading--; // turn off again
 }
 
 // Load a script from a lump
-void LUA_LoadLump(UINT16 wad, UINT16 lump)
+void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults)
 {
 	MYFILE f;
 	char *name;
@@ -501,7 +504,7 @@ void LUA_LoadLump(UINT16 wad, UINT16 lump)
 		name[len] = '\0';
 	}
 
-	LUA_LoadFile(&f, name); // actually load file!
+	LUA_LoadFile(&f, name, noresults); // actually load file!
 
 	free(name);
 	Z_Free(f.data);
diff --git a/src/lua_script.h b/src/lua_script.h
index 9568503e13e33a94538254fc498e5580e24d6cb3..5a3520d11996db85806704f6e88be1d9f8a16f6c 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -37,10 +37,10 @@
 void LUA_ClearExtVars(void);
 #endif
 
-extern boolean lua_lumploading; // is LUA_LoadLump being called?
+extern INT32 lua_lumploading; // is LUA_LoadLump being called?
 
 int LUA_GetErrorMessage(lua_State *L);
-void LUA_LoadLump(UINT16 wad, UINT16 lump);
+void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults);
 #ifdef LUA_ALLOW_BYTECODE
 void LUA_DumpFile(const char *filename);
 #endif
diff --git a/src/m_misc.c b/src/m_misc.c
index 920a13198295322b22eb8e228705616904738d37..c527d22960ae18a2ad6ccaa90793c68292c438f5 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -297,6 +297,44 @@ size_t FIL_ReadFileTag(char const *name, UINT8 **buffer, INT32 tag)
 	return length;
 }
 
+/** Makes a copy of a text file with all newlines converted into LF newlines.
+  *
+  * \param textfilename The name of the source file
+  * \param binfilename The name of the destination file
+  */
+boolean FIL_ConvertTextFileToBinary(const char *textfilename, const char *binfilename)
+{
+	FILE *textfile;
+	FILE *binfile;
+	UINT8 buffer[1024];
+	size_t count;
+	boolean success;
+
+	textfile = fopen(textfilename, "r");
+	if (!textfile)
+		return false;
+
+	binfile = fopen(binfilename, "wb");
+	if (!binfile)
+	{
+		fclose(textfile);
+		return false;
+	}
+
+	do
+	{
+		count = fread(buffer, 1, sizeof(buffer), textfile);
+		fwrite(buffer, 1, count, binfile);
+	} while (count);
+
+	success = !(ferror(textfile) || ferror(binfile));
+
+	fclose(textfile);
+	fclose(binfile);
+
+	return success;
+}
+
 /** Check if the filename exists
   *
   * \param name   Filename to check.
diff --git a/src/m_misc.h b/src/m_misc.h
index d64faea59e9bc85f73ec0b1c71516d783bd82254..dbded37d0a47fdc81c0488098e5bf7ac8e677143 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -48,6 +48,8 @@ boolean FIL_WriteFile(char const *name, const void *source, size_t length);
 size_t FIL_ReadFileTag(char const *name, UINT8 **buffer, INT32 tag);
 #define FIL_ReadFile(n, b) FIL_ReadFileTag(n, b, PU_STATIC)
 
+boolean FIL_ConvertTextFileToBinary(const char *textfilename, const char *binfilename);
+
 boolean FIL_FileExists(const char *name);
 boolean FIL_WriteFileOK(char const *name);
 boolean FIL_ReadFileOK(char const *name);
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 75d32c50616da692ed88c94a7e736126823f4f77..578874574d70a97e404e6aead5c4d36c359c9746 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -5106,9 +5106,11 @@ void A_SignPlayer(mobj_t *actor)
 	INT32 locvar2 = var2;
 	skin_t *skin = NULL;
 	mobj_t *ov;
-	UINT16 facecolor, signcolor = (UINT16)locvar2;
+	UINT16 facecolor, signcolor = 0;
 	UINT32 signframe = states[actor->info->raisestate].frame;
 
+	facecolor = signcolor = (UINT16)locvar2;
+
 	if (LUA_CallAction("A_SignPlayer", actor))
 		return;
 
@@ -5213,7 +5215,7 @@ void A_SignPlayer(mobj_t *actor)
 
 	actor->tracer->color = signcolor;
 	if (signcolor && signcolor < numskincolors)
-		signframe += (15 - skincolors[signcolor].invshade);
+		signframe += (15 - skincolors[facecolor].invshade);
 	actor->tracer->frame = signframe;
 }
 
diff --git a/src/sdl/i_system.c b/src/sdl/i_system.c
index 19126801162d6b7dd1ad1a78d20691c9664357e8..27e015ef7ca01bfa82727ad7987e62d46d9d5ce0 100644
--- a/src/sdl/i_system.c
+++ b/src/sdl/i_system.c
@@ -301,6 +301,7 @@ static void I_ReportSignal(int num, int coredumped)
 FUNCNORETURN static ATTRNORETURN void signal_handler(INT32 num)
 {
 	D_QuitNetGame(); // Fix server freezes
+	CL_AbortDownloadResume();
 	I_ReportSignal(num, 0);
 	I_ShutdownSystem();
 	signal(num, SIG_DFL);               //default signal action
@@ -2323,6 +2324,7 @@ void I_Quit(void)
 		G_StopMetalRecording(false);
 
 	D_QuitNetGame();
+	CL_AbortDownloadResume();
 	M_FreePlayerSetupColors();
 	I_ShutdownMusic();
 	I_ShutdownSound();
@@ -2440,6 +2442,7 @@ void I_Error(const char *error, ...)
 		G_StopMetalRecording(false);
 
 	D_QuitNetGame();
+	CL_AbortDownloadResume();
 	M_FreePlayerSetupColors();
 	I_ShutdownMusic();
 	I_ShutdownSound();
diff --git a/src/sdl/mixer_sound.c b/src/sdl/mixer_sound.c
index 9a713608cfc94697d7808aba8e15f7cd114f8e0e..5a086880ab5160f1809a6bc222cb91e6e17dd6e5 100644
--- a/src/sdl/mixer_sound.c
+++ b/src/sdl/mixer_sound.c
@@ -880,13 +880,18 @@ boolean I_SetSongSpeed(float speed)
 #ifdef HAVE_OPENMPT
 	if (openmpt_mhandle)
 	{
-		char modspd[13];
-
 		if (speed > 4.0f)
 			speed = 4.0f; // Limit this to 4x to prevent crashing, stupid fix but... ~SteelT 27/9/19
-
-		sprintf(modspd, "%g", speed);
-		openmpt_module_ctl_set(openmpt_mhandle, "play.tempo_factor", modspd);
+#if OPENMPT_API_VERSION_MAJOR < 1 && OPENMPT_API_VERSION_MINOR < 5
+		{
+			// deprecated in 0.5.0
+			char modspd[13];
+			sprintf(modspd, "%g", speed);
+			openmpt_module_ctl_set(openmpt_mhandle, "play.tempo_factor", modspd);
+		}
+#else
+		openmpt_module_ctl_set_floatingpoint(openmpt_mhandle, "play.tempo_factor", (double)speed);
+#endif
 		return true;
 	}
 #else
diff --git a/src/w_wad.c b/src/w_wad.c
index c01780ec29aa0521eade995d49e242cad1442f2b..9af661b5762c01530ee2c0e4f9cfe22d177768f1 100644
--- a/src/w_wad.c
+++ b/src/w_wad.c
@@ -199,12 +199,20 @@ static inline void W_LoadDehackedLumpsPK3(UINT16 wadnum, boolean mainfile)
 {
 	UINT16 posStart, posEnd;
 
-	posStart = W_CheckNumForFolderStartPK3("Lua/", wadnum, 0);
+	posStart = W_CheckNumForFullNamePK3("Init.lua", wadnum, 0);
 	if (posStart != INT16_MAX)
 	{
-		posEnd = W_CheckNumForFolderEndPK3("Lua/", wadnum, posStart);
-		for (; posStart < posEnd; posStart++)
-			LUA_LoadLump(wadnum, posStart);
+		LUA_LoadLump(wadnum, posStart, true);
+	}
+	else
+	{
+		posStart = W_CheckNumForFolderStartPK3("Lua/", wadnum, 0);
+		if (posStart != INT16_MAX)
+		{
+			posEnd = W_CheckNumForFolderEndPK3("Lua/", wadnum, posStart);
+			for (; posStart < posEnd; posStart++)
+				LUA_LoadLump(wadnum, posStart, true);
+		}
 	}
 
 	posStart = W_CheckNumForFolderStartPK3("SOC/", wadnum, 0);
@@ -236,7 +244,7 @@ static inline void W_LoadDehackedLumps(UINT16 wadnum, boolean mainfile)
 		lumpinfo_t *lump_p = wadfiles[wadnum]->lumpinfo;
 		for (lump = 0; lump < wadfiles[wadnum]->numlumps; lump++, lump_p++)
 			if (memcmp(lump_p->name,"LUA_",4)==0)
-				LUA_LoadLump(wadnum, lump);
+				LUA_LoadLump(wadnum, lump, true);
 	}
 
 	{
@@ -863,7 +871,7 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup)
 		DEH_LoadDehackedLumpPwad(numwadfiles - 1, 0, mainfile);
 		break;
 	case RET_LUA:
-		LUA_LoadLump(numwadfiles - 1, 0);
+		LUA_LoadLump(numwadfiles - 1, 0, true);
 		break;
 	default:
 		break;
diff --git a/src/win32/win_sys.c b/src/win32/win_sys.c
index 6c4ff4484fbcbca3097a316c3502248b1af682ba..a374a2587b0b9d9c01786aa9696a4d58f34b2980 100644
--- a/src/win32/win_sys.c
+++ b/src/win32/win_sys.c
@@ -467,6 +467,7 @@ static void signal_handler(int num)
 	char sigdef[64];
 
 	D_QuitNetGame(); // Fix server freezes
+	CL_AbortDownloadResume();
 	I_ShutdownSystem();
 
 	switch (num)
@@ -652,6 +653,7 @@ void I_Error(const char *error, ...)
 		G_StopMetalRecording(false);
 
 	D_QuitNetGame();
+	CL_AbortDownloadResume();
 	M_FreePlayerSetupColors();
 
 	// shutdown everything that was started
@@ -748,6 +750,7 @@ void I_Quit(void)
 	// or something else that will be finished by I_ShutdownSystem(),
 	// so do it before.
 	D_QuitNetGame();
+	CL_AbortDownloadResume();
 
 	M_FreePlayerSetupColors();