diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e8c9c31820c3ba1fcf46691cff5f4476df98220c..4971736caf807faf927eb088593fdcc44ef753bc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -279,6 +279,7 @@ if(${SRB2_CONFIG_HAVE_BLUA})
 		blua/lfunc.c
 		blua/lgc.c
 		blua/linit.c
+		blua/liolib.c
 		blua/llex.c
 		blua/lmem.c
 		blua/lobject.c
diff --git a/src/blua/Makefile.cfg b/src/blua/Makefile.cfg
index 8d2e7371428db75ced4042be0f33167a594d65f3..659faf3c812948a407e33c5036a74652e18f5e86 100644
--- a/src/blua/Makefile.cfg
+++ b/src/blua/Makefile.cfg
@@ -18,6 +18,7 @@ OBJS:=$(OBJS) \
 	$(OBJDIR)/ldo.o \
 	$(OBJDIR)/lfunc.o \
 	$(OBJDIR)/linit.o \
+	$(OBJDIR)/liolib.o \
 	$(OBJDIR)/llex.o \
 	$(OBJDIR)/lmem.o \
 	$(OBJDIR)/lobject.o \
diff --git a/src/blua/linit.c b/src/blua/linit.c
index 52b02dbe7f917c3f48e09c262f61a22987ae359b..d17390b20a01dca0cff486c4c25cc5e31cd9570a 100644
--- a/src/blua/linit.c
+++ b/src/blua/linit.c
@@ -17,6 +17,7 @@
 static const luaL_Reg lualibs[] = {
   {"", luaopen_base},
   {LUA_TABLIBNAME, luaopen_table},
+  {LUA_IOLIBNAME, luaopen_io},
   {LUA_STRLIBNAME, luaopen_string},
   {NULL, NULL}
 };
diff --git a/src/blua/liolib.c b/src/blua/liolib.c
new file mode 100644
index 0000000000000000000000000000000000000000..378b8c86d71a44a6146f053d86ff542399c6c704
--- /dev/null
+++ b/src/blua/liolib.c
@@ -0,0 +1,637 @@
+/*
+** $Id: liolib.c,v 2.73.1.3 2008/01/18 17:47:43 roberto Exp $
+** Standard I/O (and system) library
+** See Copyright Notice in lua.h
+*/
+
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define liolib_c
+#define LUA_LIB
+
+#include "lua.h"
+
+#include "lauxlib.h"
+#include "lualib.h"
+#include "../i_system.h"
+#include "../g_game.h"
+#include "../d_netfil.h"
+#include "../lua_libs.h"
+#include "../byteptr.h"
+#include "../lua_script.h"
+#include "../m_misc.h"
+
+
+#define IO_INPUT	1
+#define IO_OUTPUT	2
+
+#define FILELIMIT (1024 * 1024) // Size limit for reading/writing files
+
+#define FMT_FILECALLBACKID "file_callback_%d"
+
+
+static const char *whitelist[] = { // Allow scripters to write files of these types to SRB2's folder
+	".bmp",
+	".cfg",
+	".csv",
+	".dat",
+	".png",
+	".sav2",
+	".txt",
+};
+
+
+static int pushresult (lua_State *L, int i, const char *filename) {
+  int en = errno;  /* calls to Lua API may change this value */
+  if (i) {
+    lua_pushboolean(L, 1);
+    return 1;
+  }
+  else {
+    lua_pushnil(L);
+    if (filename)
+      lua_pushfstring(L, "%s: %s", filename, strerror(en));
+    else
+      lua_pushfstring(L, "%s", strerror(en));
+    lua_pushinteger(L, en);
+    return 3;
+  }
+}
+
+
+#define tofilep(L)	((FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE))
+
+
+static int io_type (lua_State *L) {
+  void *ud;
+  luaL_checkany(L, 1);
+  ud = lua_touserdata(L, 1);
+  lua_getfield(L, LUA_REGISTRYINDEX, LUA_FILEHANDLE);
+  if (ud == NULL || !lua_getmetatable(L, 1) || !lua_rawequal(L, -2, -1))
+    lua_pushnil(L);  /* not a file */
+  else if (*((FILE **)ud) == NULL)
+    lua_pushliteral(L, "closed file");
+  else
+    lua_pushliteral(L, "file");
+  return 1;
+}
+
+
+static FILE *tofile (lua_State *L) {
+  FILE **f = tofilep(L);
+  if (*f == NULL)
+    luaL_error(L, "attempt to use a closed file");
+  return *f;
+}
+
+
+
+/*
+** When creating file handles, always creates a `closed' file handle
+** before opening the actual file; so, if there is a memory error, the
+** file is not left opened.
+*/
+static FILE **newfile (lua_State *L) {
+  FILE **pf = (FILE **)lua_newuserdata(L, sizeof(FILE *));
+  *pf = NULL;  /* file handle is currently `closed' */
+  luaL_getmetatable(L, LUA_FILEHANDLE);
+  lua_setmetatable(L, -2);
+  return pf;
+}
+
+
+/*
+** function to (not) close the standard files stdin, stdout, and stderr
+*/
+static int io_noclose (lua_State *L) {
+  lua_pushnil(L);
+  lua_pushliteral(L, "cannot close standard file");
+  return 2;
+}
+
+
+/*
+** function to close regular files
+*/
+static int io_fclose (lua_State *L) {
+  FILE **p = tofilep(L);
+  int ok = (fclose(*p) == 0);
+  *p = NULL;
+  return pushresult(L, ok, NULL);
+}
+
+
+static int aux_close (lua_State *L) {
+  lua_getfenv(L, 1);
+  lua_getfield(L, -1, "__close");
+  return (lua_tocfunction(L, -1))(L);
+}
+
+
+static int io_close (lua_State *L) {
+  if (lua_isnone(L, 1))
+    lua_rawgeti(L, LUA_ENVIRONINDEX, IO_OUTPUT);
+  tofile(L);  /* make sure argument is a file */
+  return aux_close(L);
+}
+
+
+static int io_gc (lua_State *L) {
+  FILE *f = *tofilep(L);
+  /* ignore closed files */
+  if (f != NULL)
+    aux_close(L);
+  return 0;
+}
+
+
+static int io_tostring (lua_State *L) {
+  FILE *f = *tofilep(L);
+  if (f == NULL)
+    lua_pushliteral(L, "file (closed)");
+  else
+    lua_pushfstring(L, "file (%p)", f);
+  return 1;
+}
+
+
+// Create directories in the path
+void MakePathDirs(char *path)
+{
+	char *c;
+
+	for (c = path; *c; c++)
+		if (*c == '/' || *c == '\\')
+		{
+			char sep = *c;
+			*c = '\0';
+			I_mkdir(path, 0755);
+			*c = sep;
+		}
+}
+
+
+static int CheckFileName(lua_State *L, const char *filename)
+{
+	int length = strlen(filename);
+	boolean pass = false;
+	size_t i;
+
+	if (strchr(filename, '\\'))
+	{
+		luaL_error(L, "access denied to %s: \\ is not allowed, use / instead", filename);
+		return pushresult(L,0,filename);
+	}
+
+	for (i = 0; i < (sizeof (whitelist) / sizeof(const char *)); i++)
+		if (!stricmp(&filename[length - strlen(whitelist[i])], whitelist[i]))
+		{
+			pass = true;
+			break;
+		}
+	if (strstr(filename, "./")
+		|| strstr(filename, "..") || strchr(filename, ':')
+		|| filename[0] == '/'
+		|| !pass)
+	{
+		luaL_error(L, "access denied to %s", filename);
+		return pushresult(L,0,filename);
+	}
+
+	return 0;
+}
+
+static int io_open (lua_State *L) {
+	const char *filename = luaL_checkstring(L, 1);
+	const char *mode = luaL_optstring(L, 2, "r");
+	int checkresult;
+
+	checkresult = CheckFileName(L, filename);
+	if (checkresult)
+		return checkresult;
+
+	luaL_checktype(L, 3, LUA_TFUNCTION);
+
+	if (!(strchr(mode, 'r') || strchr(mode, '+')))
+		luaL_error(L, "open() is only for reading, use openlocal() for writing");
+
+	AddLuaFileTransfer(filename, mode);
+
+	return 0;
+}
+
+
+static int io_openlocal (lua_State *L) {
+	FILE **pf;
+	const char *filename = luaL_checkstring(L, 1);
+	const char *mode = luaL_optstring(L, 2, "r");
+	char *realfilename;
+	luafiletransfer_t *filetransfer;
+	int checkresult;
+
+	checkresult = CheckFileName(L, filename);
+	if (checkresult)
+		return checkresult;
+
+	realfilename = va("%s" PATHSEP "%s", luafiledir, filename);
+
+	if (client && strnicmp(filename, "client/", strlen("client/")))
+		I_Error("Access denied to %s\n"
+		        "Clients can only access files stored in luafiles/client/\n",
+		        filename);
+
+	// Prevent access if the file is being downloaded
+	for (filetransfer = luafiletransfers; filetransfer; filetransfer = filetransfer->next)
+		if (!stricmp(filetransfer->filename, filename))
+			I_Error("Access denied to %s\n"
+			        "Files can't be opened while being downloaded\n",
+			        filename);
+
+	MakePathDirs(realfilename);
+
+	// Open and return the file
+	pf = newfile(L);
+	*pf = fopen(realfilename, mode);
+	return (*pf == NULL) ? pushresult(L, 0, filename) : 1;
+}
+
+
+void Got_LuaFile(UINT8 **cp, INT32 playernum)
+{
+	FILE **pf = NULL;
+	UINT8 success = READUINT8(*cp); // The first (and only) byte indicates whether the file could be opened
+
+	if (playernum != serverplayer)
+	{
+		CONS_Alert(CONS_WARNING, M_GetText("Illegal luafile command received from %s\n"), player_names[playernum]);
+		if (server)
+			SendKick(playernum, KICK_MSG_CON_FAIL);
+		return;
+	}
+
+	if (!luafiletransfers)
+		I_Error("No Lua file transfer\n");
+
+	// Retrieve the callback and push it on the stack
+	lua_pushfstring(gL, FMT_FILECALLBACKID, luafiletransfers->id);
+	lua_gettable(gL, LUA_REGISTRYINDEX);
+
+	// Push the first argument (file handle or nil) on the stack
+	if (success)
+	{
+		pf = newfile(gL); // Create and push the file handle
+		*pf = fopen(luafiletransfers->realfilename, luafiletransfers->mode); // Open the file
+		if (!*pf)
+			I_Error("Can't open file \"%s\"\n", luafiletransfers->realfilename); // The file SHOULD exist
+	}
+	else
+		lua_pushnil(gL);
+
+	// Push the second argument (file name) on the stack
+	lua_pushstring(gL, luafiletransfers->filename);
+
+	// Call the callback
+	LUA_Call(gL, 2);
+
+	if (success)
+	{
+		// Close the file
+		if (*pf)
+		{
+			fclose(*pf);
+			*pf = NULL;
+		}
+
+		if (client)
+			remove(luafiletransfers->realfilename);
+	}
+
+	RemoveLuaFileTransfer();
+
+	if (server && luafiletransfers)
+	{
+		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);
+		}
+	}
+}
+
+
+void StoreLuaFileCallback(INT32 id)
+{
+	lua_pushfstring(gL, FMT_FILECALLBACKID, id);
+	lua_pushvalue(gL, 3); // Parameter 3 is the callback
+	lua_settable(gL, LUA_REGISTRYINDEX); // registry[callbackid] = callback
+}
+
+
+void RemoveLuaFileCallback(INT32 id)
+{
+	lua_pushfstring(gL, FMT_FILECALLBACKID, id);
+	lua_pushnil(gL);
+	lua_settable(gL, LUA_REGISTRYINDEX); // registry[callbackid] = nil
+}
+
+
+static int io_tmpfile (lua_State *L) {
+  FILE **pf = newfile(L);
+  *pf = tmpfile();
+  return (*pf == NULL) ? pushresult(L, 0, NULL) : 1;
+}
+
+
+static int io_readline (lua_State *L);
+
+
+static void aux_lines (lua_State *L, int idx, int toclose) {
+  lua_pushvalue(L, idx);
+  lua_pushboolean(L, toclose);  /* close/not close file when finished */
+  lua_pushcclosure(L, io_readline, 2);
+}
+
+
+static int f_lines (lua_State *L) {
+  tofile(L);  /* check that it's a valid file handle */
+  aux_lines(L, 1, 0);
+  return 1;
+}
+
+
+/*
+** {======================================================
+** READ
+** =======================================================
+*/
+
+
+static int read_number (lua_State *L, FILE *f) {
+  lua_Number d;
+  if (fscanf(f, LUA_NUMBER_SCAN, &d) == 1) {
+    lua_pushnumber(L, d);
+    return 1;
+  }
+  else return 0;  /* read fails */
+}
+
+
+static int test_eof (lua_State *L, FILE *f) {
+  int c = getc(f);
+  ungetc(c, f);
+  lua_pushlstring(L, NULL, 0);
+  return (c != EOF);
+}
+
+
+static int read_line (lua_State *L, FILE *f) {
+  luaL_Buffer b;
+  luaL_buffinit(L, &b);
+  for (;;) {
+    size_t l;
+    char *p = luaL_prepbuffer(&b);
+    if (fgets(p, LUAL_BUFFERSIZE, f) == NULL) {  /* eof? */
+      luaL_pushresult(&b);  /* close buffer */
+      return (lua_objlen(L, -1) > 0);  /* check whether read something */
+    }
+    l = strlen(p);
+    if (l == 0 || p[l-1] != '\n')
+      luaL_addsize(&b, l);
+    else {
+      luaL_addsize(&b, l - 1);  /* do not include `eol' */
+      luaL_pushresult(&b);  /* close buffer */
+      return 1;  /* read at least an `eol' */
+    }
+  }
+}
+
+
+static int read_chars (lua_State *L, FILE *f, size_t n) {
+  size_t rlen;  /* how much to read */
+  size_t nr;  /* number of chars actually read */
+  luaL_Buffer b;
+  luaL_buffinit(L, &b);
+  rlen = LUAL_BUFFERSIZE;  /* try to read that much each time */
+  do {
+    char *p = luaL_prepbuffer(&b);
+    if (rlen > n) rlen = n;  /* cannot read more than asked */
+    nr = fread(p, sizeof(char), rlen, f);
+    luaL_addsize(&b, nr);
+    n -= nr;  /* still have to read `n' chars */
+  } while (n > 0 && nr == rlen);  /* until end of count or eof */
+  luaL_pushresult(&b);  /* close buffer */
+  return (n == 0 || lua_objlen(L, -1) > 0);
+}
+
+
+static int g_read (lua_State *L, FILE *f, int first) {
+  int nargs = lua_gettop(L) - 1;
+  int success;
+  int n;
+  clearerr(f);
+  if (nargs == 0) {  /* no arguments? */
+    success = read_line(L, f);
+    n = first+1;  /* to return 1 result */
+  }
+  else {  /* ensure stack space for all results and for auxlib's buffer */
+    luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments");
+    success = 1;
+    for (n = first; nargs-- && success; n++) {
+      if (lua_type(L, n) == LUA_TNUMBER) {
+        size_t l = (size_t)lua_tointeger(L, n);
+        success = (l == 0) ? test_eof(L, f) : read_chars(L, f, l);
+      }
+      else {
+        const char *p = lua_tostring(L, n);
+        luaL_argcheck(L, p && p[0] == '*', n, "invalid option");
+        switch (p[1]) {
+          case 'n':  /* number */
+            success = read_number(L, f);
+            break;
+          case 'l':  /* line */
+            success = read_line(L, f);
+            break;
+          case 'a':  /* file */
+            read_chars(L, f, ~((size_t)0));  /* read MAX_SIZE_T chars */
+            success = 1; /* always success */
+            break;
+          default:
+            return luaL_argerror(L, n, "invalid format");
+        }
+      }
+    }
+  }
+  if (ferror(f))
+    return pushresult(L, 0, NULL);
+  if (!success) {
+    lua_pop(L, 1);  /* remove last result */
+    lua_pushnil(L);  /* push nil instead */
+  }
+  return n - first;
+}
+
+
+static int f_read (lua_State *L) {
+  return g_read(L, tofile(L), 2);
+}
+
+
+static int io_readline (lua_State *L) {
+  FILE *f = *(FILE **)lua_touserdata(L, lua_upvalueindex(1));
+  int sucess;
+  if (f == NULL)  /* file is already closed? */
+    luaL_error(L, "file is already closed");
+  sucess = read_line(L, f);
+  if (ferror(f))
+    return luaL_error(L, "%s", strerror(errno));
+  if (sucess) return 1;
+  else {  /* EOF */
+    if (lua_toboolean(L, lua_upvalueindex(2))) {  /* generator created file? */
+      lua_settop(L, 0);
+      lua_pushvalue(L, lua_upvalueindex(1));
+      aux_close(L);  /* close it */
+    }
+    return 0;
+  }
+}
+
+/* }====================================================== */
+
+
+static int g_write (lua_State *L, FILE *f, int arg) {
+  int nargs = lua_gettop(L) - 1;
+  int status = 1;
+  size_t count;
+  for (; nargs--; arg++) {
+    if (lua_type(L, arg) == LUA_TNUMBER) {
+      /* optimization: could be done exactly as for strings */
+      status = status &&
+          fprintf(f, LUA_NUMBER_FMT, lua_tonumber(L, arg)) > 0;
+    }
+    else {
+      size_t l;
+      const char *s = luaL_checklstring(L, arg, &l);
+	  count += l;
+	  if (ftell(f) + l > FILELIMIT)
+	  {
+		luaL_error(L,"write limit bypassed in file. Changes have been discarded.");
+		break;
+	  }
+      status = status && (fwrite(s, sizeof(char), l, f) == l);
+    }
+  }
+  return pushresult(L, status, NULL);
+}
+
+
+static int f_write (lua_State *L) {
+  return g_write(L, tofile(L), 2);
+}
+
+
+static int f_seek (lua_State *L) {
+  static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END};
+  static const char *const modenames[] = {"set", "cur", "end", NULL};
+  FILE *f = tofile(L);
+  int op = luaL_checkoption(L, 2, "cur", modenames);
+  long offset = luaL_optlong(L, 3, 0);
+  op = fseek(f, offset, mode[op]);
+  if (op)
+    return pushresult(L, 0, NULL);  /* error */
+  else {
+    lua_pushinteger(L, ftell(f));
+    return 1;
+  }
+}
+
+
+static int f_setvbuf (lua_State *L) {
+  static const int mode[] = {_IONBF, _IOFBF, _IOLBF};
+  static const char *const modenames[] = {"no", "full", "line", NULL};
+  FILE *f = tofile(L);
+  int op = luaL_checkoption(L, 2, NULL, modenames);
+  lua_Integer sz = luaL_optinteger(L, 3, LUAL_BUFFERSIZE);
+  int res = setvbuf(f, NULL, mode[op], sz);
+  return pushresult(L, res == 0, NULL);
+}
+
+
+static int f_flush (lua_State *L) {
+  return pushresult(L, fflush(tofile(L)) == 0, NULL);
+}
+
+
+static const luaL_Reg iolib[] = {
+  {"close", io_close},
+  {"open", io_open},
+  {"openlocal", io_openlocal},
+  {"tmpfile", io_tmpfile},
+  {"type", io_type},
+  {NULL, NULL}
+};
+
+
+static const luaL_Reg flib[] = {
+  {"close", io_close},
+  {"flush", f_flush},
+  {"lines", f_lines},
+  {"read", f_read},
+  {"seek", f_seek},
+  {"setvbuf", f_setvbuf},
+  {"write", f_write},
+  {"__gc", io_gc},
+  {"__tostring", io_tostring},
+  {NULL, NULL}
+};
+
+
+static void createmeta (lua_State *L) {
+  luaL_newmetatable(L, LUA_FILEHANDLE);  /* create metatable for file handles */
+  lua_pushvalue(L, -1);  /* push metatable */
+  lua_setfield(L, -2, "__index");  /* metatable.__index = metatable */
+  luaL_register(L, NULL, flib);  /* file methods */
+}
+
+
+static void createstdfile (lua_State *L, FILE *f, int k, const char *fname) {
+  *newfile(L) = f;
+  if (k > 0) {
+    lua_pushvalue(L, -1);
+    lua_rawseti(L, LUA_ENVIRONINDEX, k);
+  }
+  lua_pushvalue(L, -2);  /* copy environment */
+  lua_setfenv(L, -2);  /* set it */
+  lua_setfield(L, -3, fname);
+}
+
+
+static void newfenv (lua_State *L, lua_CFunction cls) {
+  lua_createtable(L, 0, 1);
+  lua_pushcfunction(L, cls);
+  lua_setfield(L, -2, "__close");
+}
+
+
+LUALIB_API int luaopen_io (lua_State *L) {
+  createmeta(L);
+  /* create (private) environment (with fields IO_INPUT, IO_OUTPUT, __close) */
+  newfenv(L, io_fclose);
+  lua_replace(L, LUA_ENVIRONINDEX);
+  /* open library */
+  luaL_register(L, LUA_IOLIBNAME, iolib);
+  /* create (and set) default files */
+  newfenv(L, io_noclose);  /* close function for default files */
+  createstdfile(L, stdin, IO_INPUT, "stdin");
+  createstdfile(L, stdout, IO_OUTPUT, "stdout");
+  createstdfile(L, stderr, 0, "stderr");
+  lua_pop(L, 1);  /* pop environment for default files */
+  return 1;
+}
+
diff --git a/src/blua/lualib.h b/src/blua/lualib.h
index 6ebe272877c0d627f9df1f294f00a853714afc56..4ea97edf3d22d585b57390b2db435b4ceec73330 100644
--- a/src/blua/lualib.h
+++ b/src/blua/lualib.h
@@ -21,6 +21,9 @@ LUALIB_API int (luaopen_base) (lua_State *L);
 #define LUA_TABLIBNAME	"table"
 LUALIB_API int (luaopen_table) (lua_State *L);
 
+#define LUA_IOLIBNAME	"io"
+LUALIB_API int (luaopen_io) (lua_State *L);
+
 #define LUA_STRLIBNAME	"string"
 LUALIB_API int (luaopen_string) (lua_State *L);
 
diff --git a/src/d_clisrv.c b/src/d_clisrv.c
index f5fea366fe73cc0d0c80004429cbe370588fe8c2..ad3d7ab2db465cefcb62e67847845ed8aad7e84e 100644
--- a/src/d_clisrv.c
+++ b/src/d_clisrv.c
@@ -3197,6 +3197,10 @@ void D_QuitNetGame(void)
 
 	// abort send/receive of files
 	CloseNetFile();
+#ifdef HAVE_BLUA
+	RemoveAllLuaFileTransfers();
+	waitingforluafiletransfer = false;
+#endif
 
 	if (server)
 	{
@@ -3609,6 +3613,10 @@ static void HandleConnect(SINT8 node)
 		SV_SendRefuse(node, M_GetText("Too many players from\nthis node."));
 	else if (netgame && !netbuffer->u.clientcfg.localplayers) // Stealth join?
 		SV_SendRefuse(node, M_GetText("No players from\nthis node."));
+#ifdef HAVE_BLUA
+	else if (luafiletransfers)
+		SV_SendRefuse(node, M_GetText("The server is broadcasting a file\nrequested by a Lua script.\nPlease wait a bit and then\ntry rejoining."));
+#endif
 	else
 	{
 #ifndef NONET
@@ -4195,6 +4203,20 @@ static void HandlePacketFromPlayer(SINT8 node)
 			Net_CloseConnection(node);
 			nodeingame[node] = false;
 			break;
+#ifdef HAVE_BLUA
+		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);
+			}
+			break;
+		case PT_HASLUAFILE:
+			if (server && luafiletransfers && luafiletransfers->nodestatus[node] == LFTNS_SENDING)
+				SV_HandleLuaFileSent(node);
+			break;
+#endif
 // -------------------------------------------- CLIENT RECEIVE ----------
 		case PT_RESYNCHEND:
 			// Only accept PT_RESYNCHEND from the server.
@@ -4322,6 +4344,12 @@ static void HandlePacketFromPlayer(SINT8 node)
 			if (client)
 				Got_Filetxpak();
 			break;
+#ifdef HAVE_BLUA
+		case PT_SENDINGLUAFILE:
+			if (client)
+				CL_PrepareDownloadLuaFile();
+			break;
+#endif
 		default:
 			DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n",
 				netbuffer->packettype, node));
diff --git a/src/d_clisrv.h b/src/d_clisrv.h
index 10a1d714df22435f2bb08c1bcf7815bb874df4bc..42da896702b7a38578c253442ef33886832ab864 100644
--- a/src/d_clisrv.h
+++ b/src/d_clisrv.h
@@ -67,6 +67,12 @@ typedef enum
 	PT_RESYNCHEND,    // Player is now resynched and is being requested to remake the gametic
 	PT_RESYNCHGET,    // Player got resynch packet
 
+#ifdef HAVE_BLUA
+	PT_SENDINGLUAFILE, // Server telling a client Lua needs to open a file
+	PT_ASKLUAFILE,     // Client telling the server they don't have the file
+	PT_HASLUAFILE,     // Client telling the server they have the file
+#endif
+
 	// Add non-PT_CANFAIL packet types here to avoid breaking MS compatibility.
 
 	PT_CANFAIL,       // This is kind of a priority. Anything bigger than CANFAIL
diff --git a/src/d_main.c b/src/d_main.c
index f8727433f06b8b16109efac78b768980b03b8b4b..27aceb6e6d56df02d0e672544e4fa5d617112ceb 100644
--- a/src/d_main.c
+++ b/src/d_main.c
@@ -1124,7 +1124,10 @@ void D_SRB2Main(void)
 			// can't use sprintf since there is %u in savegamename
 			strcatbf(savegamename, srb2home, PATHSEP);
 
-#else
+#ifdef HAVE_BLUA
+			snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", srb2home);
+#endif
+#else // DEFAULTDIR
 			snprintf(srb2home, sizeof srb2home, "%s", userhome);
 			snprintf(downloaddir, sizeof downloaddir, "%s", userhome);
 			if (dedicated)
@@ -1134,7 +1137,11 @@ void D_SRB2Main(void)
 
 			// can't use sprintf since there is %u in savegamename
 			strcatbf(savegamename, userhome, PATHSEP);
+
+#ifdef HAVE_BLUA
+			snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", userhome);
 #endif
+#endif // DEFAULTDIR
 		}
 
 		configfile[sizeof configfile - 1] = '\0';
diff --git a/src/d_net.c b/src/d_net.c
index f7848f16e2c3ceb2f7f3f1a507c5ccfae6eb981b..77a58e9bd60b16897ae96e3b13980120eb50dfc7 100644
--- a/src/d_net.c
+++ b/src/d_net.c
@@ -715,6 +715,10 @@ void Net_CloseConnection(INT32 node)
 
 	InitNode(&nodes[node]);
 	SV_AbortSendFiles(node);
+#ifdef HAVE_BLUA
+	if (server)
+		SV_AbortLuaFileTransfer(node);
+#endif
 	I_NetFreeNodenum(node);
 #endif
 }
@@ -799,6 +803,12 @@ static const char *packettypename[NUMPACKETTYPE] =
 	"RESYNCHEND",
 	"RESYNCHGET",
 
+#ifdef HAVE_BLUA
+	"SENDINGLUAFILE",
+	"ASKLUAFILE",
+	"HASLUAFILE",
+#endif
+
 	"FILEFRAGMENT",
 	"TEXTCMD",
 	"TEXTCMD2",
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index c25929929a2e46fe840253dcc965b48f973d4a4e..c6ea974aeb5f4b31d330f49714a088cacba09645 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -417,7 +417,8 @@ const char *netxcmdnames[MAXNETXCMD - 1] =
 	"SUICIDE",
 #ifdef HAVE_BLUA
 	"LUACMD",
-	"LUAVAR"
+	"LUAVAR",
+	"LUAFILE"
 #endif
 };
 
@@ -453,6 +454,7 @@ void D_RegisterServerCommands(void)
 	RegisterNetXCmd(XD_RUNSOC, Got_RunSOCcmd);
 #ifdef HAVE_BLUA
 	RegisterNetXCmd(XD_LUACMD, Got_Luacmd);
+	RegisterNetXCmd(XD_LUAFILE, Got_LuaFile);
 #endif
 
 	// Remote Administration
diff --git a/src/d_netcmd.h b/src/d_netcmd.h
index 2b0ddd1854ae58bc39eac7fe6bcc46a34bb9a4bf..6e6ae0c0a9530a1d9f937f0865839cfc5f57edef 100644
--- a/src/d_netcmd.h
+++ b/src/d_netcmd.h
@@ -145,6 +145,7 @@ typedef enum
 #ifdef HAVE_BLUA
 	XD_LUACMD,      // 22
 	XD_LUAVAR,      // 23
+	XD_LUAFILE,     // 24
 #endif
 	MAXNETXCMD
 } netxcmd_t;
diff --git a/src/d_netfil.c b/src/d_netfil.c
index 3926ff14dabda91207dd90900cc000513211935f..9ce423cd43a72192ff19425ae315b462dcff4a3e 100644
--- a/src/d_netfil.c
+++ b/src/d_netfil.c
@@ -69,6 +69,7 @@ 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;
 
@@ -94,6 +95,13 @@ char downloaddir[512] = "DOWNLOAD";
 INT32 lastfilenum = -1;
 #endif
 
+#ifdef HAVE_BLUA
+luafiletransfer_t *luafiletransfers = NULL;
+boolean waitingforluafiletransfer = false;
+char luafiledir[256 + 16] = "luafiles";
+#endif
+
+
 /** Fills a serverinfo packet with information about wad files loaded.
   *
   * \todo Give this function a better name since it is in global scope.
@@ -159,6 +167,7 @@ void D_ParseFileneeded(INT32 fileneedednum_parm, UINT8 *fileneededstr)
 		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;
 	}
 }
 
@@ -170,6 +179,7 @@ void CL_PrepareDownloadSaveGame(const char *tmpsave)
 	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,
@@ -448,6 +458,164 @@ void CL_LoadServerFiles(void)
 	}
 }
 
+#ifdef HAVE_BLUA
+void AddLuaFileTransfer(const char *filename, const char *mode)
+{
+	luafiletransfer_t **prevnext; // A pointer to the "next" field of the last transfer in the list
+	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)
+		prevnext = &((*prevnext)->next);
+
+	// Allocate file transfer information and append it to the transfer list
+	filetransfer = malloc(sizeof(luafiletransfer_t));
+	if (!filetransfer)
+		I_Error("AddLuaFileTransfer: Out of memory\n");
+	*prevnext = filetransfer;
+	filetransfer->next = NULL;
+
+	// Allocate the file name
+	filetransfer->filename = strdup(filename);
+	if (!filetransfer->filename)
+		I_Error("AddLuaFileTransfer: Out of memory\n");
+
+	// Create and allocate the real file name
+	if (server)
+		filetransfer->realfilename = strdup(va("%s" PATHSEP "%s",
+												luafiledir, filename));
+	else
+		filetransfer->realfilename = strdup(va("%s" PATHSEP "client" PATHSEP "$$$%d%d.tmp",
+												luafiledir, rand(), rand()));
+	if (!filetransfer->realfilename)
+		I_Error("AddLuaFileTransfer: Out of memory\n");
+
+	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);
+			}
+		}
+	}
+
+	// Store the callback so it can be called once everyone has the file
+	filetransfer->id = id;
+	StoreLuaFileCallback(id);
+	id++;
+
+	if (waitingforluafiletransfer)
+	{
+		waitingforluafiletransfer = false;
+		CL_PrepareDownloadLuaFile();
+	}
+}
+
+void SV_PrepareSendLuaFileToNextNode(void)
+{
+	INT32 i;
+	UINT8 success = 1;
+
+    // Find a client to send the file to
+	for (i = 1; i < MAXNETNODES; i++)
+		if (nodeingame[i] && luafiletransfers->nodestatus[i] == LFTNS_WAITING) // Node waiting
+		{
+			// Tell the client we're about to send them the file
+			netbuffer->packettype = PT_SENDINGLUAFILE;
+			if (!HSendPacket(i, true, 0, 0))
+				I_Error("Failed to send a PT_SENDINGLUAFILE packet\n"); // !!! Todo: Handle failure a bit better lol
+
+			luafiletransfers->nodestatus[i] = LFTNS_ASKED;
+
+			return;
+		}
+
+	// No client found, everyone has the file
+	// Send a net command with 1 as its first byte to indicate the file could be opened
+	SendNetXCmd(XD_LUAFILE, &success, 1);
+}
+
+void SV_HandleLuaFileSent(UINT8 node)
+{
+	luafiletransfers->nodestatus[node] = LFTNS_SENT;
+	SV_PrepareSendLuaFileToNextNode();
+}
+
+void RemoveLuaFileTransfer(void)
+{
+	luafiletransfer_t *filetransfer = luafiletransfers;
+
+	RemoveLuaFileCallback(filetransfer->id);
+
+	luafiletransfers = filetransfer->next;
+
+	free(filetransfer->filename);
+	free(filetransfer->realfilename);
+	free(filetransfer);
+}
+
+void RemoveAllLuaFileTransfers(void)
+{
+	while (luafiletransfers)
+		RemoveLuaFileTransfer();
+}
+
+void SV_AbortLuaFileTransfer(INT32 node)
+{
+	if (luafiletransfers
+	&& (luafiletransfers->nodestatus[node] == LFTNS_ASKED
+	||  luafiletransfers->nodestatus[node] == LFTNS_SENDING))
+	{
+		luafiletransfers->nodestatus[node] = LFTNS_WAITING;
+		SV_PrepareSendLuaFileToNextNode();
+	}
+}
+
+void CL_PrepareDownloadLuaFile(void)
+{
+	// If there is no transfer in the list, this normally means the server
+	// called io.open before us, so we have to wait until we call it too
+	if (!luafiletransfers)
+	{
+		waitingforluafiletransfer = 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].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);
+}
+#endif
+
 // Number of files to send
 // Little optimization to quickly test if there is a file in the queue
 static INT32 filestosend = 0;
@@ -458,6 +626,7 @@ static INT32 filestosend = 0;
   * \param filename The file to send
   * \param fileid ???
   * \sa SV_SendRam
+  * \sa SV_SendLuaFile
   *
   */
 static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid)
@@ -548,6 +717,7 @@ static boolean SV_SendFile(INT32 node, const char *filename, UINT8 fileid)
   * \param freemethod How to free the block after it has been sent
   * \param fileid ???
   * \sa SV_SendFile
+  * \sa SV_SendLuaFile
   *
   */
 void SV_SendRam(INT32 node, void *data, size_t size, freemethod_t freemethod, UINT8 fileid)
@@ -579,6 +749,57 @@ void SV_SendRam(INT32 node, void *data, size_t size, freemethod_t freemethod, UI
 	filestosend++;
 }
 
+#ifdef HAVE_BLUA
+/** Adds a file requested by Lua to the file list for a node
+  *
+  * \param node The node to send the file to
+  * \param filename The file to send
+  * \sa SV_SendFile
+  * \sa SV_SendRam
+  *
+  */
+boolean SV_SendLuaFile(INT32 node, const char *filename, boolean textmode)
+{
+	filetx_t **q; // A pointer to the "next" field of the last file in the list
+	filetx_t *p; // The new file request
+	//INT32 i;
+	//char wadfilename[MAX_WADPATH];
+
+	luafiletransfers->nodestatus[node] = LFTNS_SENDING;
+
+	// Find the last file in the list and set a pointer to its "next" field
+	q = &transfer[node].txlist;
+	while (*q)
+		q = &((*q)->next);
+
+	// 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");
+
+	// Initialise with zeros
+	memset(p, 0, sizeof (filetx_t));
+
+	// Allocate the file name
+	p->id.filename = (char *)malloc(MAX_WADPATH); // !!!
+	if (!p->id.filename)
+		I_Error("SV_SendLuaFile: 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
+	filestosend++;
+	return true;
+}
+#endif
+
 /** Stops sending a file for a node, and removes the file request from the list,
   * either because the file has been fully sent or because the node was disconnected
   *
@@ -684,7 +905,7 @@ void SV_FileSendTicker(void)
 				long filesize;
 
 				transfer[i].currentfile =
-					fopen(f->id.filename, "rb");
+					fopen(f->id.filename, f->textmode ? "r" : "rb");
 
 				if (!transfer[i].currentfile)
 					I_Error("File %s does not exist",
@@ -715,11 +936,20 @@ void SV_FileSendTicker(void)
 			size = f->size-transfer[i].position;
 		if (ram)
 			M_Memcpy(p->data, &f->id.ram[transfer[i].position], size);
-		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));
+		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));
+			}
+		}
 		p->position = LONG(transfer[i].position);
 		// Put flag so receiver knows the total size
-		if (transfer[i].position + size == f->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);
@@ -728,7 +958,7 @@ void SV_FileSendTicker(void)
 		if (HSendPacket(i, true, 0, FILETXHEADER + size)) // Reliable SEND
 		{ // Success
 			transfer[i].position = (UINT32)(transfer[i].position + size);
-			if (transfer[i].position == f->size) // Finish?
+			if (transfer[i].position == f->size || (f->textmode && feof(transfer[i].currentfile))) // Finish?
 				SV_EndFileSend(i);
 		}
 		else
@@ -772,7 +1002,7 @@ void Got_Filetxpak(void)
 	{
 		if (file->file)
 			I_Error("Got_Filetxpak: already open file\n");
-		file->file = fopen(filename, "wb");
+		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);
@@ -793,7 +1023,7 @@ void Got_Filetxpak(void)
 		}
 		// We can receive packet in the wrong order, anyway all os support gaped file
 		fseek(file->file, pos, SEEK_SET);
-		if (fwrite(netbuffer->u.filetxpak.data,size,1,file->file) != 1)
+		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;
 
@@ -805,6 +1035,14 @@ void Got_Filetxpak(void)
 			file->status = FS_FOUND;
 			CONS_Printf(M_GetText("Downloading %s...(done)\n"),
 				filename);
+#ifdef HAVE_BLUA
+			if (luafiletransfers)
+			{
+				// Tell the server we have received the file
+				netbuffer->packettype = PT_HASLUAFILE;
+				HSendPacket(servernode, true, 0, 0);
+			}
+#endif
 		}
 	}
 	else
diff --git a/src/d_netfil.h b/src/d_netfil.h
index 8214ccd4c7f20306477e8a6f475255480c502156..f0a7cf8cc60feff8fe21a64ddd36624c882c4908 100644
--- a/src/d_netfil.h
+++ b/src/d_netfil.h
@@ -13,6 +13,7 @@
 #ifndef __D_NETFIL__
 #define __D_NETFIL__
 
+#include "d_net.h"
 #include "w_wad.h"
 
 typedef enum
@@ -43,6 +44,7 @@ typedef struct
 	UINT32 currentsize;
 	UINT32 totalsize;
 	filestatus_t status; // The value returned by recsearch
+	boolean textmode; // For files requested by Lua without the "b" option
 } fileneeded_t;
 
 extern INT32 fileneedednum;
@@ -70,6 +72,44 @@ boolean CL_CheckDownloadable(void);
 boolean CL_SendRequestFile(void);
 boolean Got_RequestFilePak(INT32 node);
 
+#ifdef HAVE_BLUA
+typedef enum
+{
+	LFTNS_WAITING, // This node is waiting for the server to send the file
+	LFTNS_ASKED, // The server has told the node they're ready to send the file
+	LFTNS_SENDING, // The server is sending the file to this node
+	LFTNS_SENT // The node already has the file
+} luafiletransfernodestatus_t;
+
+typedef struct luafiletransfer_s
+{
+	char *filename;
+	char *realfilename;
+	char mode[4]; // rb+/wb+/ab+ + null character
+	INT32 id; // Callback ID
+	luafiletransfernodestatus_t nodestatus[MAXNETNODES];
+	struct luafiletransfer_s *next;
+} luafiletransfer_t;
+
+extern luafiletransfer_t *luafiletransfers;
+extern boolean waitingforluafiletransfer;
+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_HandleLuaFileSent(UINT8 node);
+void RemoveLuaFileTransfer(void);
+void RemoveAllLuaFileTransfers(void);
+void SV_AbortLuaFileTransfer(INT32 node);
+void CL_PrepareDownloadLuaFile(void);
+void Got_LuaFile(UINT8 **cp, INT32 playernum);
+void StoreLuaFileCallback(INT32 id);
+void RemoveLuaFileCallback(INT32 id);
+void MakePathDirs(char *path);
+#endif
+
 void SV_AbortSendFiles(INT32 node);
 void CloseNetFile(void);