diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8c9c3182..4971736ca 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 8d2e73714..659faf3c8 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 52b02dbe7..d17390b20 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 000000000..6ebde08a3 --- /dev/null +++ b/src/blua/liolib.c @@ -0,0 +1,720 @@ +/* +** $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 +#include +#include +#include + +#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 *const fnames[] = {"input", "output"}; +static const char *whitelist[] = { // Allow scripters to write files of these types to SRB2's folder + ".txt", + ".sav2", + ".cfg", + ".png", + ".bmp" +}; + + +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; + } +} + + +static void fileerror (lua_State *L, int arg, const char *filename) { + lua_pushfstring(L, "%s: %s", filename, strerror(errno)); + luaL_argerror(L, arg, lua_tostring(L, -1)); +} + + +#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_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 io_open (lua_State *L) { + FILE **pf; + const char *filename = luaL_checkstring(L, 1); + boolean pass = false; + size_t i; + int length = strlen(filename); + const char *mode = luaL_optstring(L, 2, "r"); + luafiletransfer_t *filetransfer; + + 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); + } + + luaL_checktype(L, 4, LUA_TFUNCTION); + + if (lua_isnil(L, 3) && (strchr(mode, 'r') || strchr(mode, '+'))) // Synched reading + { + AddLuaFileTransfer(filename, mode); + + /*pf = newfile(L); + *pf = fopen(realfilename, mode); + return (*pf == NULL) ? pushresult(L, 0, filename) : 1;*/ + } + else // Local I/O + { + char *realfilename = va("%s" PATHSEP "%s", luafiledir, filename); + player_t *player = *((player_t **)luaL_checkudata(L, 3, META_PLAYER)); + + if (!player) + return LUA_ErrInvalid(L, "player_t"); + + if (player != &players[consoleplayer]) + return 0; + + if (client && strnicmp(filename, "shared/", strlen("shared/"))) + I_Error("Access denied to %s\n" + "Clients can only access files stored in luafiles/shared/\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); + + // The callback is the last argument, no need to push it again + + // Push the first argument (file handle) on the stack + pf = newfile(gL); // Create and push the file handle + *pf = fopen(realfilename, mode); // Open the file + if (!*pf) + { + lua_pop(gL, 1); + lua_pushnil(gL); + } + + // Push the second argument (file name) on the stack + lua_pushstring(gL, filename); + + // Call the callback + LUA_Call(gL, 2); + + // Close the file + if (*pf) + { + fclose(*pf); + *pf = NULL; + } + } + + return 0; // !!! Todo: error handling? +} + + +void Got_LuaFile(UINT8 **cp, INT32 playernum) +{ + FILE **pf; + boolean 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 + UINT8 success = 0; + SendNetXCmd(XD_LUAFILE, &success, 1); + } + } +} + + +void StoreLuaFileCallback(INT32 id) +{ + lua_pushfstring(gL, FMT_FILECALLBACKID, id); + lua_pushvalue(gL, 4); // Parameter 4 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 FILE *getiofile (lua_State *L, int findex) { + FILE *f; + lua_rawgeti(L, LUA_ENVIRONINDEX, findex); + f = *(FILE **)lua_touserdata(L, -1); + if (f == NULL) + luaL_error(L, "standard %s file is closed", fnames[findex - 1]); + return f; +} + + +static int g_iofile (lua_State *L, int f, const char *mode) { + if (!lua_isnoneornil(L, 1)) { + const char *filename = lua_tostring(L, 1); + if (filename) { + FILE **pf = newfile(L); + *pf = fopen(filename, mode); + if (*pf == NULL) + fileerror(L, 1, filename); + } + else { + tofile(L); /* check that it's a valid file handle */ + lua_pushvalue(L, 1); + } + lua_rawseti(L, LUA_ENVIRONINDEX, f); + } + /* return current value */ + lua_rawgeti(L, LUA_ENVIRONINDEX, f); + return 1; +} + + +static int io_input (lua_State *L) { + return g_iofile(L, IO_INPUT, "r"); +} + + +static int io_output (lua_State *L) { + return g_iofile(L, IO_OUTPUT, "w"); +} + + +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; +} + + +static int io_lines (lua_State *L) { + if (lua_isnoneornil(L, 1)) { /* no arguments? */ + /* will iterate over default input */ + lua_rawgeti(L, LUA_ENVIRONINDEX, IO_INPUT); + return f_lines(L); + } + else { + const char *filename = luaL_checkstring(L, 1); + FILE **pf = newfile(L); + *pf = fopen(filename, "r"); + if (*pf == NULL) + fileerror(L, 1, filename); + aux_lines(L, lua_gettop(L), 1); + 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 io_read (lua_State *L) { + return g_read(L, getiofile(L, IO_INPUT), 1); +} + + +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 io_write (lua_State *L) { + return g_write(L, getiofile(L, IO_OUTPUT), 1); +} + + +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 io_flush (lua_State *L) { + return pushresult(L, fflush(getiofile(L, IO_OUTPUT)) == 0, NULL); +} + + +static int f_flush (lua_State *L) { + return pushresult(L, fflush(tofile(L)) == 0, NULL); +} + + +static const luaL_Reg iolib[] = { + {"flush", io_flush}, + {"input", io_input}, + {"lines", io_lines}, + {"open", io_open}, + {"output", io_output}, + {"read", io_read}, + {"tmpfile", io_tmpfile}, + {"type", io_type}, + {"write", io_write}, + {NULL, NULL} +}; + + +static const luaL_Reg flib[] = { + {"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 6ebe27287..4ea97edf3 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 8b87672bf..6b0f49544 100644 --- a/src/d_clisrv.c +++ b/src/d_clisrv.c @@ -3164,6 +3164,8 @@ void D_QuitNetGame(void) // abort send/receive of files CloseNetFile(); + RemoveLuaFileTransfers(); + waitingforluafiletransfer = false; if (server) { @@ -3534,6 +3536,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 @@ -4117,6 +4123,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. @@ -4244,6 +4264,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 408d0f8dd..4aaa9be6b 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 15d3c8041..b4eebb275 100644 --- a/src/d_main.c +++ b/src/d_main.c @@ -1113,6 +1113,7 @@ void D_SRB2Main(void) // can't use sprintf since there is %u in savegamename strcatbf(savegamename, srb2home, PATHSEP); + snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", srb2home); #else snprintf(srb2home, sizeof srb2home, "%s", userhome); snprintf(downloaddir, sizeof downloaddir, "%s", userhome); @@ -1123,6 +1124,8 @@ void D_SRB2Main(void) // can't use sprintf since there is %u in savegamename strcatbf(savegamename, userhome, PATHSEP); + + snprintf(luafiledir, sizeof luafiledir, "%s" PATHSEP "luafiles", userhome); #endif } diff --git a/src/d_net.c b/src/d_net.c index 573c9cfe9..ddb991235 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,10 @@ static const char *packettypename[NUMPACKETTYPE] = "RESYNCHEND", "RESYNCHGET", + "SENDINGLUAFILE", + "ASKLUAFILE", + "HASLUAFILE", + "FILEFRAGMENT", "TEXTCMD", "TEXTCMD2", diff --git a/src/d_netcmd.c b/src/d_netcmd.c index a597bad83..42b560a85 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 8f857c6db..5f3967a67 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 93b4b1990..5ceec2aaa 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] = "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 "shared" 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_FileOK(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 RemoveLuaFileTransfers(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 17aeb8b7e..0d0b16975 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]; + +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 RemoveLuaFileTransfers(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);