#include "quakedef.h" #include "winquake.h" #include "sys_plugfte.h" #include "../http/iweb.h" static void UnpackAndExtractPakFiles_Complete(struct dl_download *dl); static void pscript_property_splash_sets(struct context *ctx, const char *val); void *globalmutex; #ifdef _MSC_VER # ifdef _WIN64 # pragma comment(lib, MSVCLIBSPATH "zlib64.lib") # else # pragma comment(lib, MSVCLIBSPATH "zlib.lib") # endif #endif void BZ_Free(void *ptr) { free(ptr); } void *BZF_Malloc(int size) { return malloc(size); } //FIXME: we can't use this. void *BZ_Malloc(int size) { return BZF_Malloc(size); } void QDECL Q_strncpyz(char *d, const char *s, int n) { int i; n--; if (n < 0) return; //this could be an error for (i=0; *s; i++) { if (i == n) break; *d++ = *s++; } *d='\0'; } char *COM_SkipPath (const char *pathname) { const char *last; last = pathname; while (*pathname) { if (*pathname=='/' || *pathname == '\\') last = pathname+1; pathname++; } return (char *)last; } void VARGS Q_vsnprintfz (char *dest, size_t size, const char *fmt, va_list argptr) { vsnprintf (dest, size, fmt, argptr); dest[size-1] = 0; } void VARGS Q_snprintfz (char *dest, size_t size, const char *fmt, ...) { va_list argptr; va_start (argptr, fmt); Q_vsnprintfz(dest, size, fmt, argptr); va_end (argptr); } char *COM_TrimString(char *str) { int i; static char buffer[256]; while (*str <= ' ' && *str>'\0') str++; for (i = 0; i < 255; i++) { if (*str <= ' ') break; buffer[i] = *str++; } buffer[i] = '\0'; return buffer; } void VARGS Con_Printf (const char *fmt, ...) { va_list argptr; char dest[256]; va_start (argptr, fmt); Q_vsnprintfz(dest, sizeof(dest), fmt, argptr); va_end (argptr); OutputDebugString(dest); } #ifdef _WIN32 // don't use these functions in MSVC8 #if (_MSC_VER < 1400) int VARGS linuxlike_snprintf(char *buffer, int size, const char *format, ...) { #undef _vsnprintf int ret; va_list argptr; if (size <= 0) return 0; size--; va_start (argptr, format); ret = _vsnprintf (buffer,size, format,argptr); va_end (argptr); buffer[size] = '\0'; return ret; } int VARGS linuxlike_vsnprintf(char *buffer, int size, const char *format, va_list argptr) { #undef _vsnprintf int ret; if (size <= 0) return 0; size--; ret = _vsnprintf (buffer,size, format,argptr); buffer[size] = '\0'; return ret; } #else int VARGS linuxlike_snprintf_vc8(char *buffer, int size, const char *format, ...) { int ret; va_list argptr; va_start (argptr, format); ret = vsnprintf_s (buffer,size, _TRUNCATE, format,argptr); va_end (argptr); return ret; } #endif #endif #include "netinc.h" #if 0//def _MSC_VER #include #define pgetaddrinfo getaddrinfo #define pfreeaddrinfo freeaddrinfo #else #include #endif qboolean NET_StringToSockaddr (const char *s, int defaultport, struct sockaddr_qstorage *sadr, int *addrfamily, int *addrsize) { struct addrinfo *addrinfo = NULL; struct addrinfo *pos; struct addrinfo udp6hint; int error; char *port; char dupbase[256]; int len; #ifndef pgetaddrinfo int (WINAPI *pgetaddrinfo)(const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res); void (WINAPI *pfreeaddrinfo)(struct addrinfo *ai); void *lib = LoadLibrary("ws2_32.dll"); if (!lib) return false; pgetaddrinfo = (void*)GetProcAddress(lib, "getaddrinfo"); pfreeaddrinfo = (void*)GetProcAddress(lib, "freeaddrinfo"); if (!pgetaddrinfo || !pfreeaddrinfo) return false; #endif if (!(*s)) return false; memset (sadr, 0, sizeof(*sadr)); memset(&udp6hint, 0, sizeof(udp6hint)); udp6hint.ai_family = 0;//Any... we check for AF_INET6 or 4 udp6hint.ai_socktype = SOCK_DGRAM; udp6hint.ai_protocol = IPPROTO_UDP; //handle parsing of ipv6 literal addresses if (*s == '[') { port = strstr(s, "]"); if (!port) error = EAI_NONAME; else { len = port - (s+1); if (len >= sizeof(dupbase)) len = sizeof(dupbase)-1; strncpy(dupbase, s+1, len); dupbase[len] = '\0'; error = pgetaddrinfo(dupbase, (port[1] == ':')?port+2:NULL, &udp6hint, &addrinfo); } } else { port = strrchr(s, ':'); if (port) { len = port - s; if (len >= sizeof(dupbase)) len = sizeof(dupbase)-1; strncpy(dupbase, s, len); dupbase[len] = '\0'; error = pgetaddrinfo(dupbase, port+1, &udp6hint, &addrinfo); } else error = EAI_NONAME; if (error) //failed, try string with no port. error = pgetaddrinfo(s, NULL, &udp6hint, &addrinfo); //remember, this func will return any address family that could be using the udp protocol... (ip4 or ip6) } if (error) { return false; } ((struct sockaddr*)sadr)->sa_family = 0; for (pos = addrinfo; pos; pos = pos->ai_next) { switch(pos->ai_family) { default: //unrecognised address families are ignored. break; case AF_INET6: if (((struct sockaddr_in *)sadr)->sin_family == AF_INET6) break; //first one should be best... //fallthrough case AF_INET: memcpy(sadr, pos->ai_addr, pos->ai_addrlen); if (pos->ai_family == AF_INET) goto dblbreak; //don't try finding any more, this is quake, they probably prefer ip4... break; } } dblbreak: pfreeaddrinfo (addrinfo); if (!((struct sockaddr*)sadr)->sa_family) //none suitablefound return false; if (addrfamily) *addrfamily = ((struct sockaddr*)sadr)->sa_family; if (((struct sockaddr*)sadr)->sa_family == AF_INET) { if (!((struct sockaddr_in *)sadr)->sin_port) ((struct sockaddr_in *)sadr)->sin_port = htons(defaultport); if (addrsize) *addrsize = sizeof(struct sockaddr_in); } else { if (!((struct sockaddr_in6 *)sadr)->sin6_port) ((struct sockaddr_in6 *)sadr)->sin6_port = htons(defaultport); if (addrsize) *addrsize = sizeof(struct sockaddr_in6); } return true; } char *COM_ParseOut (const char *data, char *out, int outlen) { int c; int len; len = 0; out[0] = 0; if (!data) return NULL; // skip whitespace skipwhite: while ( (c = *data) <= ' ') { if (c == 0) return NULL; // end of file; data++; } // skip // comments if (c=='/') { if (data[1] == '/') { while (*data && *data != '\n') data++; goto skipwhite; } } //skip / * comments if (c == '/' && data[1] == '*') { data+=2; while(*data) { if (*data == '*' && data[1] == '/') { data+=2; goto skipwhite; } data++; } goto skipwhite; } // handle quoted strings specially if (c == '\"') { data++; while (1) { if (len >= outlen-1) { out[len] = 0; return (char*)data; } c = *data++; if (c=='\"' || !c) { out[len] = 0; return (char*)data; } out[len] = c; len++; } } // parse a regular word do { if (len >= outlen-1) { out[len] = 0; return (char*)data; } out[len] = c; data++; len++; c = *data; } while (c>32); out[len] = 0; return (char*)data; } typedef struct { vfsfile_t funcs; char *data; int maxlen; int writepos; int readpos; } vfspipe_t; void VFSPIPE_Close(vfsfile_t *f) { vfspipe_t *p = (vfspipe_t*)f; free(p->data); free(p); } unsigned long VFSPIPE_GetLen(vfsfile_t *f) { vfspipe_t *p = (vfspipe_t*)f; return p->writepos - p->readpos; } unsigned long VFSPIPE_Tell(vfsfile_t *f) { return 0; } qboolean VFSPIPE_Seek(vfsfile_t *f, unsigned long offset) { OutputDebugStringA("Seeking is a bad plan, mmkay?\n"); return false; } int VFSPIPE_ReadBytes(vfsfile_t *f, void *buffer, int len) { vfspipe_t *p = (vfspipe_t*)f; if (len > p->writepos - p->readpos) len = p->writepos - p->readpos; memcpy(buffer, p->data+p->readpos, len); p->readpos += len; if (p->readpos > 8192) { //shift the memory down periodically //fixme: use cyclic buffer? max size, etc? memmove(p->data, p->data+p->readpos, p->writepos-p->readpos); p->writepos -= p->readpos; p->readpos = 0; } return len; } int VFSPIPE_WriteBytes(vfsfile_t *f, const void *buffer, int len) { vfspipe_t *p = (vfspipe_t*)f; if (p->writepos + len > p->maxlen) { p->maxlen = p->writepos + len; p->data = realloc(p->data, p->maxlen); } memcpy(p->data+p->writepos, buffer, len); p->writepos += len; return len; } vfsfile_t *VFSPIPE_Open(void) { vfspipe_t *newf; newf = malloc(sizeof(*newf)); newf->data = NULL; newf->maxlen = 0; newf->readpos = 0; newf->writepos = 0; newf->funcs.Close = VFSPIPE_Close; newf->funcs.Flush = NULL; newf->funcs.GetLen = VFSPIPE_GetLen; newf->funcs.ReadBytes = VFSPIPE_ReadBytes; newf->funcs.Seek = VFSPIPE_Seek; newf->funcs.Tell = VFSPIPE_Tell; newf->funcs.WriteBytes = VFSPIPE_WriteBytes; newf->funcs.seekingisabadplan = true; return &newf->funcs; } #if defined(OFFICIAL_RELEASE) #define BUILDTYPE "rel" #else #define BUILDTYPE "test" #define UPDATE_URL "http://triptohell.info/moodles/" #define UPDATE_URL_VERSION UPDATE_URL "version.txt" #ifdef _WIN64 #define UPDATE_URL_BUILD UPDATE_URL "win64/fte" EXETYPE ".exe" #else #define UPDATE_URL_BUILD UPDATE_URL "win32/fte" EXETYPE ".exe" #endif #endif //#if defined(GLQUAKE) && defined(D3DQUAKE) // #define EXETYPE "qw" //#elif defined(GLQUAKE) // #ifdef MINIMAL // #define EXETYPE "minglqw" // #else #define EXETYPE "glqw" // #endif //#elif defined(D3DQUAKE) // #define EXETYPE "d3dqw" //#elif defined(SWQUAKE) // #define EXETYPE "swqw" //#else //erm... // #define EXETYPE "qw" //#endif qboolean MyRegGetStringValue(HKEY base, char *keyname, char *valuename, void *data, DWORD datalen) { qboolean result = false; DWORD resultlen = datalen - 1; HKEY subkey; DWORD type = REG_NONE; if (RegOpenKeyEx(base, keyname, 0, KEY_READ, &subkey) == ERROR_SUCCESS) { result = ERROR_SUCCESS == RegQueryValueEx(subkey, valuename, NULL, &type, data, &datalen); RegCloseKey (subkey); } if (type == REG_SZ || type == REG_EXPAND_SZ) ((char*)data)[datalen] = 0; else ((char*)data)[0] = 0; return result; } void MyRegSetValue(HKEY base, char *keyname, char *valuename, int type, void *data, DWORD datalen) { HKEY subkey; if (RegCreateKeyEx(base, keyname, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &subkey, NULL) == ERROR_SUCCESS) { RegSetValueEx(subkey, valuename, 0, type, data, datalen); RegCloseKey (subkey); } } void MyRegDeleteKeyValue(HKEY base, char *keyname, char *valuename) { HKEY subkey; if (RegOpenKeyEx(base, keyname, 0, KEY_WRITE, &subkey) == ERROR_SUCCESS) { RegDeleteValue(subkey, valuename); RegCloseKey (subkey); } } qboolean Update_GetHomeDirectory(char *homedir, int homedirsize) { HMODULE shfolder = LoadLibrary("shfolder.dll"); if (shfolder) { HRESULT (WINAPI *dSHGetFolderPath) (HWND hwndOwner, int nFolder, HANDLE hToken, DWORD dwFlags, LPTSTR pszPath); dSHGetFolderPath = (void *)GetProcAddress(shfolder, "SHGetFolderPathA"); if (dSHGetFolderPath) { char folder[MAX_PATH]; // 0x5 == CSIDL_PERSONAL if (dSHGetFolderPath(NULL, 0x5, NULL, 0, folder) == S_OK) { Q_snprintfz(homedir, homedirsize, "%s/My Games/%s/", folder, FULLENGINENAME); return true; } } // FreeLibrary(shfolder); } return false; } #include "fs.h" struct dl_download *enginedownloadactive; void Update_Version_Updated(struct dl_download *dl) { //happens in a thread, avoid va if (dl->file) { if (dl->status == DL_FINISHED) { char buf[8192]; unsigned int size = 0, chunk, fullsize; char pendingname[MAX_OSPATH]; FILE *pending; Update_GetHomeDirectory(pendingname, sizeof(pendingname)); Q_strncatz(pendingname, DISTRIBUTION BUILDTYPE EXETYPE".tmp", sizeof(pendingname)); fullsize = VFS_GETLEN(dl->file); pending = fopen(pendingname, "wb"); if (pending) { while(1) { chunk = VFS_READ(dl->file, buf, sizeof(buf)); if (!chunk) break; size += fwrite(buf, 1, chunk, pending); } fclose(pending); if (fullsize == size) { MyRegSetValue(HKEY_CURRENT_USER, "Software\\"FULLENGINENAME, "pending" BUILDTYPE EXETYPE, REG_SZ, pendingname, strlen(pendingname)+1); } else { dl->status = DL_FAILED; } } } VFS_CLOSE(dl->file); dl->file = NULL; } } qboolean Plug_GetDownloadedName(char *updatedpath, int updatedpathlen) { char pendingpath[MAX_OSPATH]; char temppath[MAX_OSPATH]; Sys_LockMutex(globalmutex); if (!enginedownloadactive || (enginedownloadactive->status == DL_FINISHED || enginedownloadactive->status == DL_FAILED)) { if (enginedownloadactive) DL_Close(enginedownloadactive); enginedownloadactive = NULL; /*if there's some downloaded binary which isn't running yet, kill the old active one and update it*/ MyRegGetStringValue(HKEY_CURRENT_USER, "Software\\"FULLENGINENAME, "pending" BUILDTYPE EXETYPE, pendingpath, sizeof(pendingpath)); if (*pendingpath) { MyRegDeleteKeyValue(HKEY_CURRENT_USER, "Software\\"FULLENGINENAME, "pending" BUILDTYPE EXETYPE); Update_GetHomeDirectory(temppath, sizeof(temppath)); CreateDirectory(temppath, NULL); Q_strncatz(temppath, "cur" BUILDTYPE EXETYPE".exe", sizeof(temppath)); DeleteFile(temppath); if (MoveFile(pendingpath, temppath)) MyRegSetValue(HKEY_CURRENT_USER, "Software\\"FULLENGINENAME, BUILDTYPE EXETYPE, REG_SZ, temppath, strlen(temppath)+1); } /*grab the binary to run from the registry*/ MyRegGetStringValue(HKEY_CURRENT_USER, "Software\\"FULLENGINENAME, BUILDTYPE EXETYPE, updatedpath, updatedpathlen); if (!*updatedpath) { /*ooer, its not set, try and download one. updates are handled by the client itself.*/ enginedownloadactive = DL_Create(UPDATE_URL_BUILD); if (enginedownloadactive) { enginedownloadactive->user_ctx = NULL; DL_CreateThread(enginedownloadactive, VFSPIPE_Open(), Update_Version_Updated); } } } Sys_UnlockMutex(globalmutex); return !!*updatedpath; } struct context { struct contextpublic pub; void *windowhnd; int windowleft; int windowtop; int windowwidth; int windowheight; int waitingfordatafiles; char *datadownload; char *gamename; char *password; char *onstart; char *onend; char *ondemoend; char *curserver; //updated by engine void *hostinstance; int read; int written; qtvfile_t qtvf; unsigned char *splashdata; int splashwidth; int splashheight; struct dl_download *splashdownload; struct dl_download *packagelist; void *mutex; void *thread; char resetvideo; qboolean shutdown; qboolean multiplecontexts; struct context *next; struct browserfuncs bfuncs; #ifdef _WIN32 HANDLE pipetoengine; HANDLE pipefromengine; HANDLE engineprocess; #endif }; #ifdef _WIN32 extern HWND sys_parentwindow; extern unsigned int sys_parentwidth; extern unsigned int sys_parentheight; HINSTANCE global_hInstance; static char binarypath[MAX_PATH]; BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { char *bp; switch (fdwReason) { case DLL_PROCESS_ATTACH: global_hInstance = hinstDLL; GetModuleFileName(global_hInstance, binarypath, sizeof(binarypath)); bp = COM_SkipPath(binarypath); if (bp) *bp = 0; break; default: break; } return TRUE; } #endif struct context *activecontext; struct context *contextlist; #define ADDRARG(x) do {if (argc < maxargs) argv[argc++] = strdup(x);} while(0) //for strings that are safe #define ADDCARG(x) do {if (argc < maxargs) argv[argc++] = cleanarg(x);} while(0) //for arguments that we don't trust. char *cleanarg(char *arg) { unsigned char *c; //skip over any leading spaces. while (*arg <= ' ') arg++; //reject anything with a leading + or - if (*arg == '-' || *arg == '+') return strdup("badarg"); //clean up the argument for (c = (unsigned char *)arg; *c; c++) { //remove special chars... we automagically add quotes so any that exist are someone trying to get past us. if (*c == ';' || *c == '\n' || *c == '\"') *c = '?'; //remove other control chars. //we allow spaces if (*c < ' ') *c = '?'; } if (*arg) { char *out = malloc(strlen(arg)+3); strcpy(out+1, arg); out[0] = '\"'; strcat(out, "\""); return out; } return strdup("\"\""); } qboolean Plug_GetBinaryName(char *exe, int exelen, char *basedir, int basedirlen) { char buffer[1024]; char cmd[64]; char value[1024]; FILE *f; Q_snprintfz(buffer, sizeof(buffer), "%s%s", binarypath, "npfte.txt"); *exe = 0; *basedir = 0; f = fopen(buffer, "rt"); if (f) { while(fgets(buffer, sizeof(buffer), f)) { *cmd = 0; *value = 0; COM_ParseOut(COM_ParseOut(buffer, cmd, sizeof(cmd)), value, sizeof(value)); if (!strcmp(cmd, "relexe")) Q_snprintfz(exe, exelen, "%s%s", binarypath, value); else if (!strcmp(cmd, "absexe")) Q_strncpyz(exe, value, exelen); else if (!strcmp(cmd, "basedir")) Q_strncpyz(basedir, value, basedirlen); } fclose(f); } if (!*exe) return Plug_GetDownloadedName(exe, exelen); return false; } int Plug_GenCommandline(struct context *ctx, char **argv, int maxargs) { char *s; int argc; char tok[256]; char exe[1024]; char basedir[1024]; qboolean autoupdate; *exe = 0; *basedir = 0; autoupdate = Plug_GetBinaryName(exe, sizeof(exe), basedir, sizeof(basedir)); if (!*exe) return 0; //error argv[0] = strdup(exe); argc = 1; if (autoupdate) { ADDRARG("-autoupdate"); } ADDRARG("-plugin"); if (*basedir) { ADDRARG("-basedir"); ADDCARG(basedir); } switch(ctx->qtvf.connectiontype) { default: break; case QTVCT_STREAM: ADDRARG("+qtvplay"); ADDCARG(ctx->qtvf.server); break; case QTVCT_CONNECT: ADDRARG("+connect"); ADDCARG(ctx->qtvf.server); break; case QTVCT_JOIN: ADDRARG("+join"); ADDCARG(ctx->qtvf.server); break; case QTVCT_OBSERVE: ADDRARG("+observe"); ADDCARG(ctx->qtvf.server); break; case QTVCT_MAP: ADDRARG("+map"); ADDCARG(ctx->qtvf.server); break; } if (ctx->password) { ADDRARG("+password"); ADDCARG(ctx->password); } //figure out the game dirs (first token is the base game) s = ctx->gamename; s = COM_ParseOut(s, tok, sizeof(tok)); if (!*tok || !strcmp(tok, "q1") || !strcmp(tok, "qw") || !strcmp(tok, "quake")) ADDRARG("-quake"); else if (!strcmp(tok, "q2") || !strcmp(tok, "quake2")) ADDRARG("-q2"); else if (!strcmp(tok, "q3") || !strcmp(tok, "quake3")) ADDRARG("-q3"); else if (!strcmp(tok, "hl") || !strcmp(tok, "halflife")) ADDRARG("-halflife"); else if (!strcmp(tok, "h2") || !strcmp(tok, "hexen2")) ADDRARG("-hexen2"); else if (!strcmp(tok, "nex") || !strcmp(tok, "nexuiz")) ADDRARG("-nexuiz"); else { ADDRARG("-basegame"); ADDCARG(tok); } //later options are additions to that while ((s = COM_ParseOut(s, tok, sizeof(tok)))) { if (argc == sizeof(argv)/sizeof(argv[0])) break; ADDRARG("-addbasegame"); ADDCARG(tok); } if (ctx->datadownload) { ADDRARG("-manifest"); ADDCARG(ctx->datadownload); } return argc; } qboolean Plug_GenCommandlineString(struct context *ctx, char *cmdline, int cmdlinelen) { char *argv[64]; int argc, i; argc = Plug_GenCommandline(ctx, argv, 64); if (!argc) return false; for (i = 0; i < argc; i++) { //add quotes for any arguments with spaces if (strchr(argv[i], ' ')) { Q_strncatz(cmdline, "\"", cmdlinelen); Q_strncatz(cmdline, argv[i], cmdlinelen); Q_strncatz(cmdline, "\"", cmdlinelen); } else Q_strncatz(cmdline, argv[i], cmdlinelen); Q_strncatz(cmdline, " ", cmdlinelen); } return true; } void Plug_ExecuteCommand(struct context *ctx, char *message, ...) { va_list va; char finalmessage[1024]; DWORD written = 0; va_start (va, message); vsnprintf (finalmessage, sizeof(finalmessage)-1, message, va); va_end (va); WriteFile(ctx->pipetoengine, finalmessage, strlen(finalmessage), &written, NULL); } qboolean Plug_CreatePluginProcess(struct context *ctx) { char cmdline[8192]; PROCESS_INFORMATION childinfo; STARTUPINFO startinfo; SECURITY_ATTRIBUTES pipesec = {sizeof(pipesec), NULL, TRUE}; if (!Plug_GenCommandlineString(ctx, cmdline, sizeof(cmdline))) return false; memset(&startinfo, 0, sizeof(startinfo)); startinfo.cb = sizeof(startinfo); startinfo.hStdInput = NULL; startinfo.hStdError = NULL; startinfo.hStdOutput = NULL; startinfo.dwFlags |= STARTF_USESTDHANDLES; //create pipes for the stdin/stdout. CreatePipe(&ctx->pipefromengine, &startinfo.hStdOutput, &pipesec, 0); CreatePipe(&startinfo.hStdInput, &ctx->pipetoengine, &pipesec, 0); SetHandleInformation(ctx->pipefromengine, HANDLE_FLAG_INHERIT, 0); SetHandleInformation(ctx->pipetoengine, HANDLE_FLAG_INHERIT, 0); Plug_ExecuteCommand(ctx, "vid_recenter %i %i %i %i %#p\n", ctx->windowleft, ctx->windowtop, ctx->windowwidth, ctx->windowheight, (void*)ctx->windowhnd); CreateProcess(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, binarypath, &startinfo, &childinfo); //these ends of the pipes were inherited by now, so we can discard them in the caller. CloseHandle(startinfo.hStdOutput); CloseHandle(startinfo.hStdInput); return true; } int Plug_PluginThread(void *ctxptr) { char buffer[1024]; char *nl; int bufoffs = 0; struct context *ctx = ctxptr; #if 0 //I really don't know what to do about multiple active clients downloading at once. //maybe just depricate this feature. android ports have the same issue with missing files. //we should probably just have the engine take charge of the downloads. if (ctx->datadownload) { char token[1024]; struct dl_download *dl; char *s = ctx->datadownload; char *c; vfsfile_t *f; while ((s = COM_ParseOut(s, token, sizeof(token)))) { //FIXME: do we want to add some sort of file size indicator? c = strchr(token, ':'); if (!c) continue; *c++ = 0; f = VFSSTDIO_Open(va("%s/%s", basedir, token), "rb", NULL); if (f) { Plug_ExecuteCommand(ctx, "echo " "Already have %s\n", token); VFS_CLOSE(f); continue; } Plug_ExecuteCommand(ctx, "echo " "Attempting to download %s\n", c); dl = DL_Create(c); dl->user_ctx = ctx; dl->next = ctx->packagelist; if (DL_CreateThread(dl, FS_OpenTemp(), UnpackAndExtractPakFiles_Complete)) ctx->packagelist = dl; } ctx->pub.downloading = true; while(!ctx->shutdown && ctx->packagelist) { int total=0, done=0; ctx->resetvideo = false; Sys_LockMutex(ctx->mutex); for (dl = ctx->packagelist; dl; dl = dl->next) { total += dl->totalsize; done += dl->completed; } dl = ctx->packagelist; if (total != ctx->pub.dlsize || done != ctx->pub.dldone) { ctx->pub.dlsize = total; ctx->pub.dldone = done; if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); } if (!dl->file) ctx->packagelist = dl->next; else dl = NULL; Sys_UnlockMutex(ctx->mutex); /*file downloads are not canceled while the plugin is locked, to avoid a race condition*/ if (dl) DL_Close(dl); Sleep(10); } ctx->pub.downloading = false; } #endif if (!Plug_CreatePluginProcess(ctx)) { if (!enginedownloadactive) { strcpy(ctx->pub.statusmessage, "Unable to determine engine to run. Plugin is not correctly installed."); if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); ctx->pub.running = false; } else { int os = -1; while(1) { Sys_LockMutex(globalmutex); if (!enginedownloadactive) { Sys_UnlockMutex(globalmutex); if (!Plug_CreatePluginProcess(ctx)) ctx->pub.running = false; break; } if (os != enginedownloadactive->status) { os = enginedownloadactive->status; switch(os) { case DL_PENDING: strcpy(ctx->pub.statusmessage, "Download pending."); break; case DL_FAILED: strcpy(ctx->pub.statusmessage, "Engine download failed. Try later, or fix internet connection."); os = -2; //stop trying. ctx->pub.running = false; break; case DL_RESOLVING: strcpy(ctx->pub.statusmessage, "Download pending. Resolving hostname."); break; case DL_QUERY: strcpy(ctx->pub.statusmessage, "Download pending. Reqeusting file."); break; case DL_ACTIVE: Q_snprintfz(ctx->pub.statusmessage, sizeof(ctx->pub.statusmessage), "Download pending. At %u of %u.", enginedownloadactive->completed, enginedownloadactive->totalsize); os = -1; //keep refreshing break; case DL_FINISHED: strcpy(ctx->pub.statusmessage, "Download completed!"); DL_Close(enginedownloadactive); enginedownloadactive = NULL; break; } Sys_UnlockMutex(globalmutex); if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); } else Sys_UnlockMutex(globalmutex); if (os == -2) break; Sleep(1000); } } } if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); if (ctx->pub.running) while(1) { DWORD avail; //use Peek so we can read exactly how much there is without blocking, so we don't have to read byte-by-byte. PeekNamedPipe(ctx->pipefromengine, NULL, 0, NULL, &avail, NULL); if (!avail) avail = 1; //so we do actually sleep. if (avail > sizeof(buffer)-1 - bufoffs) avail = sizeof(buffer)-1 - bufoffs; if (!ReadFile(ctx->pipefromengine, buffer + bufoffs, avail, &avail, NULL) || !avail) { //broken pipe, client died. *ctx->pub.statusmessage = 0; break; } bufoffs += avail; while(1) { buffer[bufoffs] = 0; nl = strchr(buffer, '\n'); if (nl) { *nl = 0; if (!strncmp(buffer, "status ", 7)) { //don't just strcpy it, copy by byte, saves locking. int i = strlen(buffer+7)+1; if (i > sizeof(ctx->pub.statusmessage)) i = sizeof(ctx->pub.statusmessage); ctx->pub.statusmessage[i] = 0; while (i-->0) { ctx->pub.statusmessage[i] = buffer[7+i]; } if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); } else if (!strcmp(buffer, "status")) { *ctx->pub.statusmessage = 0; if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); } else if (!strcmp(buffer, "curserver")) { Plug_LockPlugin(ctx, true); free(ctx->curserver); ctx->curserver = strdup(buffer + 10); Plug_LockPlugin(ctx, false); } else { //handle anything else we need to handle here OutputDebugStringA("Unknown command from engine \""); OutputDebugStringA(buffer); OutputDebugStringA("\"\n"); } } else break; } } if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); if (ctx == activecontext) activecontext = NULL; ctx->pub.running = false; return 0; } void Plug_LockPlugin(struct context *ctx, qboolean lockstate) { if (!ctx || !ctx->mutex) return; if (lockstate) Sys_LockMutex(ctx->mutex); else Sys_UnlockMutex(ctx->mutex); } //#define Plug_LockPlugin(c,s) do{Plug_LockPlugin(c,s);VS_DebugLocation(__FILE__, __LINE__, s?"Lock":"Unlock"); }while(0) //begins the context, fails if one is already active qboolean Plug_StartContext(struct context *ctx) { if (activecontext && !ctx->multiplecontexts) return false; if (ctx->pub.running) return true; if (ctx->thread) Plug_StopContext(ctx, true); ctx->pub.running = true; if (!ctx->multiplecontexts) activecontext = ctx; if (!ctx->mutex) ctx->mutex = Sys_CreateMutex(); ctx->thread = Sys_CreateThread("pluginctx", Plug_PluginThread, ctx, THREADP_NORMAL, 0); return true; } //asks a context to stop, is not instant. void Plug_StopContext(struct context *ctx, qboolean wait) { void *thread; if (ctx == NULL) ctx = activecontext; if (!ctx) return; Plug_ExecuteCommand(ctx, "quit force\n"); thread = ctx->thread; if (ctx->thread) { if (wait) { while (ctx->pub.running && ctx->windowhnd) { MSG msg; while (PeekMessage(&msg, ctx->windowhnd, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } Sleep(10); } Sys_WaitOnThread(ctx->thread); ctx->thread = NULL; } } } //creates a plugin context struct context *Plug_CreateContext(void *sysctx, const struct browserfuncs *funcs) { struct context *ctx; if (!sysctx || !funcs) return NULL; ctx = malloc(sizeof(struct context)); if (!ctx) return NULL; memset(ctx, 0, sizeof(struct context)); memcpy(&ctx->bfuncs, funcs, sizeof(ctx->bfuncs)); //link the instance to the context and the context to the instance ctx->hostinstance = sysctx; ctx->gamename = strdup("q1"); //add it to the linked list ctx->next = contextlist; contextlist = ctx; ctx->qtvf.connectiontype = QTVCT_NONE; return ctx; } //change the plugin's parent window, width, and height, returns true if the window handle actually changed, false otherwise qboolean Plug_ChangeWindow(struct context *ctx, void *whnd, int left, int top, int width, int height) { qboolean result = false; Plug_LockPlugin(ctx, true); //if the window changed if (ctx->windowhnd != whnd) { result = true; ctx->windowhnd = whnd; ctx->resetvideo = 2; } ctx->windowleft = left; ctx->windowtop = top; ctx->windowwidth = width; ctx->windowheight = height; if (ctx->pub.running && !ctx->resetvideo) ctx->resetvideo = true; if (ctx->pub.running) Plug_ExecuteCommand(ctx, "vid_recenter %i %i %i %i %#p\n", ctx->windowleft, ctx->windowtop, ctx->windowwidth, ctx->windowheight, (void*)ctx->windowhnd); Plug_LockPlugin(ctx, false); return result; } void Plug_DestroyContext(struct context *ctx) { struct context *prev; if (ctx == contextlist) contextlist = ctx->next; else { for (prev = contextlist; prev->next; prev = prev->next) { if (prev->next == ctx) { prev->next = ctx->next; break; } } } if (ctx->splashdownload) { DL_Close(ctx->splashdownload); ctx->splashdownload = NULL; } Plug_StopContext(ctx, true); if (ctx->mutex) Sys_DestroyMutex(ctx->mutex); //actually these ifs are not required, just the frees if (ctx->gamename) free(ctx->gamename); if (ctx->password) free(ctx->password); if (ctx->datadownload) free(ctx->datadownload); if (ctx->splashdata) free(ctx->splashdata); free(ctx); } //////////////////////////////////////// #if 0 #include "fs.h" extern searchpathfuncs_t zipfilefuncs; static int ExtractDataFile(const char *fname, int fsize, void *ptr) { char buffer[8192]; int read; void *zip = ptr; flocation_t loc; int slashes; const char *s; vfsfile_t *compressedpak; vfsfile_t *decompressedpak; if (zipfilefuncs.FindFile(zip, &loc, fname, NULL)) { compressedpak = zipfilefuncs.OpenVFS(zip, &loc, "rb"); if (compressedpak) { //this extra logic is so we can handle things like nexuiz/data/blah.pk3 //as well as just data/blah.pk3 slashes = 0; for (s = strchr(fname, '/'); s; s = strchr(s+1, '/')) slashes++; for (; slashes > 1; slashes--) fname = strchr(fname, '/')+1; if (!slashes) { FS_CreatePath(fname, FS_GAMEONLY); decompressedpak = FS_OpenVFS(fname, "wb", FS_GAMEONLY); } else { FS_CreatePath(fname, FS_ROOT); decompressedpak = FS_OpenVFS(fname, "wb", FS_ROOT); } if (decompressedpak) { for(;;) { read = VFS_READ(compressedpak, buffer, sizeof(buffer)); if (read <= 0) break; VFS_WRITE(decompressedpak, buffer, read); } VFS_CLOSE(decompressedpak); } VFS_CLOSE(compressedpak); } } return true; } static void UnpackAndExtractPakFiles_Complete(struct dl_download *dl) { extern searchpathfuncs_t zipfilefuncs; void *zip; Plug_LockPlugin(dl->user_ctx, true); if (dl->status == DL_FINISHED) zip = zipfilefuncs.OpenNew(dl->file, dl->url); else zip = NULL; /*the zip code will have eaten the file handle*/ dl->file = NULL; if (zip) { /*scan it to extract its contents*/ zipfilefuncs.EnumerateFiles(zip, "*.pk3", ExtractDataFile, zip); zipfilefuncs.EnumerateFiles(zip, "*.pak", ExtractDataFile, zip); /*close it, delete the temp file from disk, etc*/ zipfilefuncs.ClosePath(zip); /*restart the filesystem so those new files can be found*/ Plug_ExecuteCommand(dl->user_ctx, "fs_restart\n"); } Plug_LockPlugin(dl->user_ctx, false); } #endif void LoadSplashImage(struct dl_download *dl) { struct context *ctx = dl->user_ctx; vfsfile_t *f = dl->file; int x, y; int width = 0; int height = 0; int len; char *buffer; unsigned char *image; Plug_LockPlugin(ctx, true); ctx->splashwidth = 0; ctx->splashheight = 0; image = ctx->splashdata; ctx->splashdata = NULL; free(image); Plug_LockPlugin(ctx, false); if (!f) { if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); return; } len = VFS_GETLEN(f); buffer = malloc(len); VFS_READ(f, buffer, len); VFS_CLOSE(f); dl->file = NULL; image = NULL; if (!image) image = ReadJPEGFile(buffer, len, &width, &height); if (!image) image = ReadPNGFile(buffer, len, &width, &height, dl->url); free(buffer); if (image) { Plug_LockPlugin(ctx, true); if (ctx->splashdata) free(ctx->splashdata); ctx->splashdata = malloc(width*height*4); for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { ctx->splashdata[(y*width + x)*4+0] = image[((height-y-1)*width + x)*4+2]; ctx->splashdata[(y*width + x)*4+1] = image[((height-y-1)*width + x)*4+1]; ctx->splashdata[(y*width + x)*4+2] = image[((height-y-1)*width + x)*4+0]; } } ctx->splashwidth = width; ctx->splashheight = height; BZ_Free(image); Plug_LockPlugin(ctx, false); if (ctx->bfuncs.StatusChanged) ctx->bfuncs.StatusChanged(ctx->hostinstance); } } #if 0 static void ReadQTVFileDescriptor(struct context *ctx, vfsfile_t *f, const char *name) { CL_ParseQTVFile(f, name, &ctx->qtvf); pscript_property_splash_sets(ctx, ctx->qtvf.splashscreen); } void CL_QTVPlay (vfsfile_t *newf, qboolean iseztv); static void BeginDemo(struct context *ctx, vfsfile_t *f, const char *name) { if (!activecontext) activecontext = ctx; CL_QTVPlay(f, false); } static void EndDemo(struct context *ctx, vfsfile_t *f, const char *name) { Cmd_ExecuteString("disconnect", RESTRICT_LOCAL); } #endif ///////////////////////////////////// struct pscript_property { char *name; char *cvarname; char *(*getstring)(struct context *ctx); void (*setstring)(struct context *ctx, const char *val); int (*getint)(struct context *ctx); void (*setint)(struct context *ctx, int val); float (*getfloat)(struct context *ctx); void (*setfloat)(struct context *ctx, float val); }; int pscript_property_running_getb(struct context *ctx) { if (ctx->pub.running) return true; else return false; } void pscript_property_running_setb(struct context *ctx, int i) { i = !!i; if (ctx->pub.running == i) return; if (i) Plug_StartContext(ctx); else Plug_StopContext(ctx, false); } char *pscript_property_startserver_gets(struct context *ctx) { return strdup(ctx->qtvf.server); } void pscript_property_startserver_sets(struct context *ctx, const char *val) { if (strchr(val, '$') || strchr(val, ';') || strchr(val, '\n')) return; ctx->qtvf.connectiontype = QTVCT_JOIN; Q_strncpyz(ctx->qtvf.server, val, sizeof(ctx->qtvf.server)); } char *pscript_property_curserver_gets(struct context *ctx) { extern char lastdemoname[]; if (!pscript_property_running_getb(ctx)) return pscript_property_startserver_gets(ctx); if (ctx->curserver) return strdup(ctx->curserver); else return strdup(""); } void pscript_property_curserver_sets(struct context *ctx, const char *val) { if (strchr(val, '$') || strchr(val, ';') || strchr(val, '\n')) return; if (!pscript_property_running_getb(ctx)) { pscript_property_startserver_sets(ctx, val); return; } Plug_ExecuteCommand(ctx, "connect \"%s\"\n", val); } void pscript_property_stream_sets(struct context *ctx, const char *val) { if (strchr(val, '$') || strchr(val, ';') || strchr(val, '\n')) return; ctx->qtvf.connectiontype = QTVCT_STREAM; Q_strncpyz(ctx->qtvf.server, val, sizeof(ctx->qtvf.server)); Plug_ExecuteCommand(ctx, "qtvplay \"%s\"\n", val); } void pscript_property_map_sets(struct context *ctx, const char *val) { if (strchr(val, '$') || strchr(val, ';') || strchr(val, '\n')) return; ctx->qtvf.connectiontype = QTVCT_MAP; Q_strncpyz(ctx->qtvf.server, val, sizeof(ctx->qtvf.server)); Plug_ExecuteCommand(ctx, "map \"%s\"\n", val); } float pscript_property_curver_getf(struct context *ctx) { int base = FTE_VER_MAJOR * 10000 + FTE_VER_MINOR * 100; return base; // return version_number(); } void pscript_property_availver_setf(struct context *ctx, float val) { ctx->pub.availver = val; if (ctx->pub.availver <= pscript_property_curver_getf(ctx)) ctx->pub.availver = 0; } void pscript_property_datadownload_sets(struct context *ctx, const char *val) { free(ctx->datadownload); ctx->datadownload = strdup(val); } void pscript_property_game_sets(struct context *ctx, const char *val) { if (strchr(val, '$') || strchr(val, ';') || strchr(val, '\n')) return; if (!strstr(val, ".")) if (!strstr(val, "/")) if (!strstr(val, "\\")) if (!strstr(val, ":")) { free(ctx->gamename); ctx->gamename = strdup(val); } } void pscript_property_splash_sets(struct context *ctx, const char *val) { if (ctx->splashdownload) DL_Close(ctx->splashdownload); ctx->splashdownload = NULL; if (val != ctx->qtvf.splashscreen) Q_strncpyz(ctx->qtvf.splashscreen, val, sizeof(ctx->qtvf.splashscreen)); ctx->splashdownload = DL_Create(ctx->qtvf.splashscreen); ctx->splashdownload->user_ctx = ctx; if (!DL_CreateThread(ctx->splashdownload, VFSPIPE_Open(), LoadSplashImage)) { DL_Close(ctx->splashdownload); ctx->splashdownload = NULL; } } char *pscript_property_build_gets(struct context *ctx) { return strdup(DISTRIBUTION " " __DATE__ " " __TIME__ #if defined(DEBUG) || defined(_DEBUG) " (debug)" #endif ); } float pscript_property_multi_getf(struct context *ctx) { return ctx->multiplecontexts; } void pscript_property_multi_setf(struct context *ctx, float f) { ctx->multiplecontexts = !!f; } static struct pscript_property pscript_properties[] = { {"", NULL, pscript_property_curserver_gets, pscript_property_curserver_sets}, {"server", NULL, pscript_property_curserver_gets, pscript_property_curserver_sets}, {"running", NULL, NULL, NULL, pscript_property_running_getb, pscript_property_running_setb}, {"startserver", NULL, pscript_property_startserver_gets, pscript_property_startserver_sets}, {"join", NULL, NULL, pscript_property_curserver_sets}, {"playername", "name"}, {NULL, "skin"}, {NULL, "team"}, {NULL, "topcolor"}, {NULL, "bottomcolor"}, {NULL, "password"}, //cvars are write only, just so you know. // {NULL, "spectator"}, {"mapsrc", "cl_download_mapsrc"}, {"fullscreen", "vid_fullscreen"}, {"datadownload",NULL, NULL, pscript_property_datadownload_sets}, {"game", NULL, NULL, pscript_property_game_sets}, {"availver", NULL, NULL, NULL, NULL, NULL, NULL, pscript_property_availver_setf}, {"plugver", NULL, NULL, NULL, NULL, NULL, pscript_property_curver_getf}, {"multiple", NULL, NULL, NULL, NULL, NULL, pscript_property_multi_getf, pscript_property_multi_setf}, {"splash", NULL, NULL, pscript_property_splash_sets}, {"stream", NULL, NULL, pscript_property_stream_sets}, {"map", NULL, NULL, pscript_property_map_sets}, {"build", NULL, pscript_property_build_gets}, {NULL} }; int Plug_FindProp(struct context *ctx, const char *field) { struct pscript_property *prop; for (prop = pscript_properties; prop->name||prop->cvarname; prop++) { if (!stricmp(prop->name?prop->name:prop->cvarname, field)) { // if (prop->onlyifactive) // { // if (!ctx->pub.running) // return -1; // } return prop - pscript_properties; } } return -1; } qboolean Plug_SetString(struct context *ctx, int fieldidx, const char *value) { struct pscript_property *field = pscript_properties + fieldidx; if (!ctx || fieldidx < 0 || fieldidx >= sizeof(pscript_properties)/sizeof(pscript_properties[0]) || !value) return false; if (field->setstring) { Plug_LockPlugin(ctx, true); field->setstring(ctx, value); Plug_LockPlugin(ctx, false); } else if (field->setint) { Plug_LockPlugin(ctx, true); field->setint(ctx, atoi(value)); Plug_LockPlugin(ctx, false); } else if (field->setfloat) { Plug_LockPlugin(ctx, true); field->setfloat(ctx, atof(value)); Plug_LockPlugin(ctx, false); } else if (field->cvarname && ctx->pub.running) { Plug_LockPlugin(ctx, true); Plug_ExecuteCommand(ctx, "%s \"%s\"\n", field->cvarname, value); Plug_LockPlugin(ctx, false); } else return false; return true; } qboolean Plug_SetWString(struct context *ctx, int fieldidx, const wchar_t *value) { char tmp[1024]; wcstombs(tmp, value, sizeof(tmp)); return Plug_SetString(ctx, fieldidx, tmp); } qboolean Plug_SetInteger(struct context *ctx, int fieldidx, int value) { struct pscript_property *field = pscript_properties + fieldidx; if (!ctx || fieldidx < 0 || fieldidx >= sizeof(pscript_properties)/sizeof(pscript_properties[0])) return false; if (field->setint) { Plug_LockPlugin(ctx, true); field->setint(ctx, value); Plug_LockPlugin(ctx, false); } else if (field->setfloat) { Plug_LockPlugin(ctx, true); field->setfloat(ctx, value); Plug_LockPlugin(ctx, false); } else if (field->cvarname && ctx->pub.running) { Plug_LockPlugin(ctx, true); Plug_ExecuteCommand(ctx, "%s \"%i\"\n", field->cvarname, value); Plug_LockPlugin(ctx, false); } else return false; return true; } qboolean Plug_SetFloat(struct context *ctx, int fieldidx, float value) { struct pscript_property *field = pscript_properties + fieldidx; if (!ctx || fieldidx < 0 || fieldidx >= sizeof(pscript_properties)/sizeof(pscript_properties[0])) return false; if (field->setfloat) { Plug_LockPlugin(ctx, true); field->setfloat(ctx, value); Plug_LockPlugin(ctx, false); } else if (field->setint) { Plug_LockPlugin(ctx, true); field->setint(ctx, value); Plug_LockPlugin(ctx, false); } else if (field->cvarname && ctx->pub.running) { Plug_LockPlugin(ctx, true); Plug_ExecuteCommand(ctx, "%s \"%f\"\n", field->cvarname, value); Plug_LockPlugin(ctx, false); } else return false; return true; } qboolean Plug_GetString(struct context *ctx, int fieldidx, const char **value) { struct pscript_property *field = pscript_properties + fieldidx; if (!ctx || fieldidx < 0 || fieldidx >= sizeof(pscript_properties)/sizeof(pscript_properties[0])) return false; if (field->getstring) { *value = field->getstring(ctx); return true; } return false; } void Plug_GotString(const char *value) { free((char*)value); } qboolean Plug_GetInteger(struct context *ctx, int fieldidx, int *value) { struct pscript_property *field = pscript_properties + fieldidx; if (!ctx || fieldidx < 0 || fieldidx >= sizeof(pscript_properties)/sizeof(pscript_properties[0])) return false; if (field->getint) { *value = field->getint(ctx); return true; } return false; } qboolean Plug_GetFloat(struct context *ctx, int fieldidx, float *value) { struct pscript_property *field = pscript_properties + fieldidx; if (!ctx || fieldidx < 0 || fieldidx >= sizeof(pscript_properties)/sizeof(pscript_properties[0])) return false; if (field->getfloat) { *value = field->getfloat(ctx); return true; } return false; } #ifdef _WIN32 void *Plug_GetSplashBack(struct context *ctx, void *hdc, int *width, int *height) { BITMAPINFOHEADER bmh; if (!ctx->splashdata) return NULL; bmh.biSize = sizeof(bmh); bmh.biWidth = *width = ctx->splashwidth; bmh.biHeight = *height = ctx->splashheight; bmh.biPlanes = 1; bmh.biBitCount = 32; bmh.biCompression = BI_RGB; bmh.biSizeImage = 0; bmh.biXPelsPerMeter = 0; bmh.biYPelsPerMeter = 0; bmh.biClrUsed = 0; bmh.biClrImportant = 0; return CreateDIBitmap(hdc, &bmh, CBM_INIT, (LPSTR)ctx->splashdata, (LPBITMAPINFO)&bmh, DIB_RGB_COLORS ); } void Plug_ReleaseSplashBack(struct context *ctx, void *bmp) { DeleteObject(bmp); } #endif static const struct plugfuncs exportedplugfuncs_1 = { Plug_CreateContext, Plug_DestroyContext, Plug_LockPlugin, Plug_StartContext, Plug_StopContext, Plug_ChangeWindow, Plug_FindProp, Plug_SetString, Plug_GetString, Plug_GotString, Plug_SetInteger, Plug_GetInteger, Plug_SetFloat, Plug_GetFloat, Plug_GetSplashBack, Plug_ReleaseSplashBack, Plug_SetWString }; const struct plugfuncs *Plug_GetFuncs(int ver) { if (!globalmutex) globalmutex = Sys_CreateMutex(); if (ver == 1) return &exportedplugfuncs_1; else return NULL; }