#include "quakedef.h" #ifdef WEBCLIENT #define DOWNLOADMENU #endif #ifdef DOWNLOADMENU #define ROOTDOWNLOADABLESSOURCE "http://fteqw.com/downloadables.txt" #define INSTALLEDFILES "installed.lst" //the file that resides in the quakedir (saying what's installed). #define DPF_HAVEAVERSION 1 //any old version #define DPF_WANTTOINSTALL 2 //user selected it #define DPF_DISPLAYVERSION 4 //some sort of conflict, the package is listed twice, so show versions so the user knows what's old. #define DPF_DELETEONUNINSTALL 8 //for previously installed packages, remove them from the list #define DPF_DOWNLOADING 16 #define DPF_ENQUED 32 int dlcount=1; //note: these are allocated for the life of the exe char *downloadablelist[256] = { ROOTDOWNLOADABLESSOURCE }; char *downloadablelistnameprefix[256] = { "" }; char downloadablelistreceived[256]; //well int numdownloadablelists = 1; typedef struct package_s { char fullname[256]; char *name; char src[256]; char dest[64]; char gamedir[16]; unsigned int version; //integral. int dlnum; int flags; struct package_s *next; } package_t; typedef struct { menucustom_t *list; char intermediatefilename[MAX_QPATH]; char pathprefix[MAX_QPATH]; int parsedsourcenum; qboolean populated; } dlmenu_t; package_t *availablepackages; int numpackages; static package_t *BuildPackageList(vfsfile_t *f, int flags, char *prefix) { char line[1024]; package_t *p; package_t *first = NULL; char *sl; int version; do { if (!VFS_GETS(f, line, sizeof(line)-1)) break; while((sl=strchr(line, '\n'))) *sl = '\0'; while((sl=strchr(line, '\r'))) *sl = '\0'; Cmd_TokenizeString (line, false, false); } while (!Cmd_Argc()); if (strcmp(Cmd_Argv(0), "version")) return NULL; //it's not the right format. version = atoi(Cmd_Argv(1)); if (version != 0 && version != 1) { Con_Printf("Packagelist is of a future or incompatible version\n"); return NULL; //it's not the right version. } while(1) { if (!VFS_GETS(f, line, sizeof(line)-1)) break; while((sl=strchr(line, '\n'))) *sl = '\0'; while((sl=strchr(line, '\r'))) *sl = '\0'; Cmd_TokenizeString (line, false, false); if (Cmd_Argc()) { if (!strcmp(Cmd_Argv(0), "sublist")) { int i; sl = Cmd_Argv(1); for (i = 0; i < numdownloadablelists; i++) { if (!strcmp(downloadablelist[i], sl)) break; } if (i == numdownloadablelists && i != 256) { downloadablelist[i] = BZ_Malloc(strlen(sl)+1); strcpy(downloadablelist[i], sl); if (*prefix) sl = va("%s/%s", prefix, Cmd_Argv(2)); else sl = Cmd_Argv(2); downloadablelistnameprefix[i] = BZ_Malloc(strlen(sl)+1); strcpy(downloadablelistnameprefix[i], sl); numdownloadablelists++; i++; } continue; } if (Cmd_Argc() > 5 || Cmd_Argc() < 3) { Con_Printf("Package list is bad - %s\n", line); continue; //but try the next line away } p = BZ_Malloc(sizeof(*p)); if (*prefix) Q_strncpyz(p->fullname, va("%s/%s", prefix, Cmd_Argv(0)), sizeof(p->fullname)); else Q_strncpyz(p->fullname, Cmd_Argv(0), sizeof(p->fullname)); p->name = p->fullname; while((sl = strchr(p->name, '/'))) p->name = sl+1; Q_strncpyz(p->src, Cmd_Argv(1), sizeof(p->src)); Q_strncpyz(p->dest, Cmd_Argv(2), sizeof(p->dest)); p->version = atoi(Cmd_Argv(3)); Q_strncpyz(p->gamedir, Cmd_Argv(4), sizeof(p->gamedir)); if (!*p->gamedir) strcpy(p->gamedir, "id1"); p->flags = flags; p->next = first; first = p; } } return first; } static void WriteInstalledPackages(void) { char *s; package_t *p; vfsfile_t *f = FS_OpenVFS(INSTALLEDFILES, "wb", FS_ROOT); if (!f) { Con_Printf("menu_download: Can't update installed list\n"); return; } s = "version 1\n"; VFS_WRITE(f, s, strlen(s)); for (p = availablepackages; p ; p=p->next) { if (p->flags & DPF_HAVEAVERSION) { s = va("\"%s\" \"%s\" \"%s\" %i \"%s\"\n", p->fullname, p->src, p->dest, p->version, p->gamedir); VFS_WRITE(f, s, strlen(s)); } } VFS_CLOSE(f); } static qboolean ComparePackages(package_t **l, package_t *p) { int v = strcmp((*l)->fullname, p->fullname); if (v < 0) { p->next = (*l); (*l) = p; return true; } else if (v == 0) { if (p->version == (*l)->version) if (!strcmp((*l)->dest, p->dest)) { /*package matches, free, don't add*/ strcpy((*l)->src, p->src); //use the source of the new package (existing packages are read FIRST) (*l)->flags |= p->flags; (*l)->flags &= ~DPF_DELETEONUNINSTALL; BZ_Free(p); return true; } p->flags |= DPF_DISPLAYVERSION; (*l)->flags |= DPF_DISPLAYVERSION; } return false; } static void InsertPackage(package_t **l, package_t *p) { package_t *lp; if (!*l) //there IS no list. { *l = p; p->next = NULL; return; } if (ComparePackages(l, p)) return; for (lp = *l; lp->next; lp=lp->next) { if (ComparePackages(&lp->next, p)) return; } lp->next = p; p->next = NULL; } static void ConcatPackageLists(package_t *l2) { package_t *n; while(l2) { n = l2->next; InsertPackage(&availablepackages, l2); l2 = n; numpackages++; } } static void dlnotification(struct dl_download *dl) { int i; vfsfile_t *f; f = dl->file; dl->file = NULL; i = dl->user_num; if (f) { downloadablelistreceived[i] = 1; ConcatPackageLists(BuildPackageList(f, 0, downloadablelistnameprefix[i])); VFS_CLOSE(f); } else downloadablelistreceived[i] = -1; } static void MD_Draw (int x, int y, struct menucustom_s *c, struct menu_s *m) { package_t *p; int fl; p = c->data; if (p) { Draw_FunString (x+4, y, "^Ue080^Ue082"); fl = p->flags & (DPF_HAVEAVERSION | DPF_WANTTOINSTALL); if ((p->flags & (DPF_DOWNLOADING|DPF_ENQUED)) && ((int)(realtime*4)&1)) fl |= DPF_HAVEAVERSION; //flicker have if we're downloading it. switch(fl) { case 0: Draw_FunString (x+8, y, "^Ue081"); break; case DPF_HAVEAVERSION: Draw_FunString (x, y, "REM"); break; case DPF_WANTTOINSTALL: Draw_FunString (x, y, "GET"); break; case DPF_HAVEAVERSION | DPF_WANTTOINSTALL: Draw_FunString (x+8, y, "^Ue083"); break; } if (&m->selecteditem->common == &c->common) Draw_AltFunString (x+48, y, p->name); else Draw_FunString(x+48, y, p->name); if (p->flags & DPF_DISPLAYVERSION) { Draw_FunString(x+48+strlen(p->name)*8, y, va(" (%i.%i)", p->version/1000, p->version%1000)); } } } static qboolean MD_Key (struct menucustom_s *c, struct menu_s *m, int key) { package_t *p, *p2; p = c->data; if (key == K_ENTER || key == K_MOUSE1) { p->flags ^= DPF_WANTTOINSTALL; if (p->flags&DPF_WANTTOINSTALL) { for (p2 = availablepackages; p2; p2 = p2->next) { if (p == p2) continue; if (!strcmp(p->dest, p2->dest)) p2->flags &= ~DPF_WANTTOINSTALL; } } else p->flags &= ~DPF_ENQUED; return true; } return false; } qboolean MD_PopMenu (union menuoption_s *mo,struct menu_s *m,int key) { if (key == K_ENTER || key == K_MOUSE1) { M_RemoveMenu(m); return true; } return false; } static void Menu_Download_Got(struct dl_download *dl); qboolean MD_ApplyDownloads (union menuoption_s *mo,struct menu_s *m,int key) { if (key == K_ENTER || key == K_MOUSE1) { char *temp; package_t *last = NULL, *p; for (p = availablepackages; p ; p=p->next) { if (!(p->flags&DPF_WANTTOINSTALL) && (p->flags&DPF_HAVEAVERSION)) { //if we don't want it but we have it anyway: if (*p->gamedir) { char *fname = va("%s/%s", p->gamedir, p->dest); FS_Remove(fname, FS_ROOT); } else FS_Remove(p->dest, FS_GAME); p->flags&=~DPF_HAVEAVERSION; //FIXME: This is error prone. WriteInstalledPackages(); if (p->flags & DPF_DELETEONUNINSTALL) { if (last) last->next = p->next; else availablepackages = p->next; // BZ_Free(p); return true; } } last = p; } for (p = availablepackages; p ; p=p->next) { if ((p->flags&DPF_WANTTOINSTALL) && !(p->flags&(DPF_HAVEAVERSION|DPF_DOWNLOADING))) { //if we want it and don't have it: p->dlnum = dlcount++; temp = va("dl_%i.tmp", p->dlnum); Con_Printf("Downloading %s (to %s)\n", p->fullname, temp); p->flags|=DPF_DOWNLOADING; if (!HTTP_CL_Get(p->src, temp, Menu_Download_Got)) p->flags&=~DPF_DOWNLOADING; } } return true; } return false; } void M_AddItemsToDownloadMenu(menu_t *m) { char path[MAX_QPATH]; int y; package_t *p; menucustom_t *c; char *slash; menuoption_t *mo; dlmenu_t *info = m->data; int prefixlen; p = availablepackages; // MC_AddRedText(m, 0, 40, "WntHav", false); prefixlen = strlen(info->pathprefix); y = 48+4; for (p = availablepackages; p; p = p->next) { if (strncmp(p->fullname, info->pathprefix, prefixlen)) continue; slash = strchr(p->fullname+prefixlen, '/'); if (slash) { Q_strncpyz(path, p->fullname, MAX_QPATH); slash = strchr(path+prefixlen, '/'); if (slash) *slash = '\0'; for (mo = m->options; mo; mo = mo->common.next) if (mo->common.type == mt_button) if (!strcmp(mo->button.text, path + prefixlen)) break; if (!mo) { MC_AddConsoleCommand(m, 6*8, y, path+prefixlen, va("menu_download \"%s/\"", path)); y += 8; } } else { c = MC_AddCustom(m, 0, y, p); c->draw = MD_Draw; c->key = MD_Key; c->common.width = 320; c->common.height = 8; y += 8; } } y+=4; MC_AddCommand(m, 0, y, " Back", MD_PopMenu); y+=8; MC_AddCommand(m, 0, y, " Apply", MD_ApplyDownloads); /* for (pn = 1, p = availablepackages; p && pn < info->firstpackagenum ; p=p->next, pn++) m-> if (lastpathlen != p->name - p->fullname || strncmp(p->fullname, lastpath, lastpathlen)) { lastpathlen = p->name - p->fullname; lastpath = p->fullname; if (!lastpathlen) Draw_FunStringLen(x+40, y, "/", 1); else Draw_FunStringLen(x+40, y, p->fullname, lastpathlen); y+=8; } Draw_Character (x, y, 128); Draw_Character (x+8, y, 130); Draw_Character (x+16, y, 128); Draw_Character (x+24, y, 130); //if you want it if (p->flags&DPF_WANTTOINSTALL) Draw_Character (x+4, y, 131); else Draw_Character (x+4, y, 129); //if you have it already if (p->flags&(DPF_HAVEAVERSION | ((((int)(realtime*4))&1)?DPF_DOWNLOADING:0) )) Draw_Character (x+20, y, 131); else Draw_Character (x+20, y, 129); if (pn == info->highlightednum) Draw_Alt_String(x+48, y, p->name); else Draw_String(x+48, y, p->name); if (p->flags & DPF_DISPLAYVERSION) { Draw_String(x+48+strlen(p->name)*8, y, va(" (%i.%i)", p->version/1000, p->version%1000)); } */ } void M_Download_UpdateStatus(struct menu_s *m) { struct dl_download *dl; dlmenu_t *info = m->data; int i; while (!cls.downloadmethod && (info->parsedsourcenum==-1 || info->parsedsourcenum < numdownloadablelists)) { //done downloading char basename[64]; info->parsedsourcenum++; if (info->parsedsourcenum < numdownloadablelists) { if (!downloadablelistreceived[info->parsedsourcenum]) { sprintf(basename, "dlinfo_%i.inf", info->parsedsourcenum); dl = HTTP_CL_Get(downloadablelist[info->parsedsourcenum], basename, dlnotification); if (dl) dl->user_num = info->parsedsourcenum; else Con_Printf("Could not contact server\n"); return; } } } for (i = 0; i < numdownloadablelists; i++) { if (!downloadablelistreceived[i]) { // Draw_String(x+8, y+8, "Waiting for package list"); return; } } if (!availablepackages) { // Draw_String(x+8, y+8, "Could not obtain a package list"); return; } if (!info->populated) { info->populated = true; M_AddItemsToDownloadMenu(m); } } /* static void M_Download_Draw (int x, int y, struct menucustom_s *c, struct menu_s *m) { int pn; int lastpathlen = 0; char *lastpath=""; package_t *p; dlmenu_t *info = m->data; int i; y+=8; Draw_Alt_String(x+4, y, "I H"); (info->highlightednum==0?Draw_Alt_String:Draw_String)(x+40, y, "Apply changes"); y+=8; for (pn = 1, p = availablepackages; p && pn < info->firstpackagenum ; p=p->next, pn++) ; for (; p; p = p->next, y+=8, pn++) { if (lastpathlen != p->name - p->fullname || strncmp(p->fullname, lastpath, lastpathlen)) { lastpathlen = p->name - p->fullname; lastpath = p->fullname; if (!lastpathlen) Draw_FunStringLen(x+40, y, "/", 1); else Draw_FunStringLen(x+40, y, p->fullname, lastpathlen); y+=8; } Draw_Character (x, y, 128); Draw_Character (x+8, y, 130); Draw_Character (x+16, y, 128); Draw_Character (x+24, y, 130); //if you want it if (p->flags&DPF_WANTTOINSTALL) Draw_Character (x+4, y, 131); else Draw_Character (x+4, y, 129); //if you have it already if (p->flags&(DPF_HAVEAVERSION | ((((int)(realtime*4))&1)?DPF_DOWNLOADING:0) )) Draw_Character (x+20, y, 131); else Draw_Character (x+20, y, 129); if (pn == info->highlightednum) Draw_Alt_String(x+48, y, p->name); else Draw_String(x+48, y, p->name); if (p->flags & DPF_DISPLAYVERSION) { Draw_String(x+48+strlen(p->name)*8, y, va(" (%i.%i)", p->version/1000, p->version%1000)); } } } */ static void Menu_Download_Got(struct dl_download *dl) { char *fname = dl->localname; qboolean successful = dl->status == DL_FINISHED; char *ext; package_t *p; int dlnum = atoi(fname+3); for (p = availablepackages; p ; p=p->next) { if (p->dlnum == dlnum) { char *destname; char *diskname = fname; if (!successful) { p->flags &= ~DPF_DOWNLOADING; Con_Printf("Couldn't download %s (from %s to %s)\n", p->name, p->src, p->dest); return; } if (!(p->flags & DPF_DOWNLOADING)) { Con_Printf("menu_download: We're not downloading %s, apparently\n", p->dest); return; } p->flags &= ~DPF_DOWNLOADING; ext = COM_FileExtension(p->dest); if (!stricmp(ext, "pak") || !stricmp(ext, "pk3")) FS_UnloadPackFiles(); //we reload them after if (*p->gamedir) destname = va("%s/%s", p->gamedir, p->dest); else destname = va("%s", p->dest); if (FS_Remove(destname, *p->gamedir?FS_ROOT:FS_GAME)) Con_Printf("Deleted old %s\n", destname); if (!FS_Rename2(diskname, destname, FS_GAME, *p->gamedir?FS_ROOT:FS_GAME)) { Con_Printf("Couldn't rename %s to %s. Removed instead.\n", diskname, destname); FS_Remove (diskname, FS_GAME); return; } Con_Printf("Downloaded %s (to %s)\n", p->name, destname); p->flags |= DPF_HAVEAVERSION; WriteInstalledPackages(); ext = COM_FileExtension(p->dest); if (!stricmp(ext, "pak") || !stricmp(ext, "pk3")) FS_ReloadPackFiles(); return; } } Con_Printf("menu_download: Can't figure out where %s came from\n", fname); } /* static qboolean M_Download_Key (struct menucustom_s *c, struct menu_s *m, int key) { char *temp; int pn; package_t *p, *p2; dlmenu_t *info = m->data; switch (key) { case K_UPARROW: if (info->highlightednum>0) info->highlightednum--; return true; case K_DOWNARROW: //cap range when drawing if (info->highlightednum < numpackages) info->highlightednum++; return true; case K_ENTER: if (!info->highlightednum) { //do it //uninstall packages first package_t *last = NULL; for (p = availablepackages; p ; p=p->next) { if (!(p->flags&DPF_WANTTOINSTALL) && (p->flags&DPF_HAVEAVERSION)) { //if we don't want it but we have it anyway: if (*p->gamedir) { char *fname = va("%s", p->gamedir, p->dest); FS_Remove(fname, FS_BASE); } else FS_Remove(p->dest, FS_GAME); p->flags&=~DPF_HAVEAVERSION; //FIXME: This is error prone. WriteInstalledPackages(); if (p->flags & DPF_DELETEONUNINSTALL) { if (last) last->next = p->next; else availablepackages = p->next; BZ_Free(p); return M_Download_Key(c, m, key); //I'm lazy. } } last = p; } for (p = availablepackages; p ; p=p->next) { if ((p->flags&DPF_WANTTOINSTALL) && !(p->flags&(DPF_HAVEAVERSION|DPF_DOWNLOADING))) { //if we want it and don't have it: p->dlnum = dlcount++; temp = va("dl_%i.tmp", p->dlnum); Con_Printf("Downloading %s (to %s)\n", p->fullname, temp); p->flags|=DPF_DOWNLOADING; if (!HTTP_CL_Get(p->src, temp, Menu_Download_Got)) p->flags&=~DPF_DOWNLOADING; } } } else { for (pn = 1, p = availablepackages; p && pn < info->highlightednum ; p=p->next, pn++) ; if (p) { p->flags = (p->flags&~DPF_WANTTOINSTALL) | DPF_WANTTOINSTALL - (p->flags&DPF_WANTTOINSTALL); if (p->flags&DPF_WANTTOINSTALL) { for (p2 = availablepackages; p2; p2 = p2->next) { if (p == p2) continue; if (!strcmp(p->dest, p2->dest)) p2->flags &= ~DPF_WANTTOINSTALL; } } } } return true; } return false; } */ void Menu_DownloadStuff_f (void) { int i; menu_t *menu; dlmenu_t *info; key_dest = key_menu; m_state = m_complex; menu = M_CreateMenu(sizeof(dlmenu_t)); info = menu->data; menu->event = M_Download_UpdateStatus; /* menu->selecteditem = (menuoption_t *)(info->list = MC_AddCustom(menu, 0, 32, NULL)); info->list->draw = M_Download_Draw; info->list->key = M_Download_Key; */ info->parsedsourcenum = -1; Q_strncpyz(info->pathprefix, Cmd_Argv(1), sizeof(info->pathprefix)); if (!*info->pathprefix) { for (i = 0; i < numdownloadablelists; i++) downloadablelistreceived[i] = 0; } MC_AddWhiteText(menu, 24, 8, "Downloads", false); MC_AddWhiteText(menu, 16, 24, "\35\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\36\37", false); { static qboolean loadedinstalled; vfsfile_t *f = loadedinstalled?NULL:FS_OpenVFS(INSTALLEDFILES, "rb", FS_ROOT); loadedinstalled = true; if (f) { ConcatPackageLists(BuildPackageList(f, DPF_DELETEONUNINSTALL|DPF_HAVEAVERSION|DPF_WANTTOINSTALL, "")); VFS_CLOSE(f); } } } #elif defined(WEBCLIENT) void Menu_DownloadStuff_f (void) { Con_Printf("Not yet reimplemented\n"); } #endif #if defined(AVAIL_ZLIB) && defined(WEBCLIENT) static int numbootdownloads; #include "fs.h" extern searchpathfuncs_t zipfilefuncs; static int CL_BootDownload_Extract(const char *fname, int fsize, void *ptr) { char buffer[512*1024]; 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; if (VFS_WRITE(decompressedpak, buffer, read) != read) { Con_Printf("write failed writing %s. disk full?\n", fname); break; } } VFS_CLOSE(decompressedpak); } VFS_CLOSE(compressedpak); } } return true; } static void CL_BootDownload_Complete(struct dl_download *dl) { void *zip; /* int sz; char *buf; FILE *f; sz = VFS_GETLEN(dl->file); buf = malloc(sz); VFS_READ(dl->file, buf, sz); f = fopen("C:/Games/Quake/test/emptybasedir/test.zip", "wb"); fwrite(buf, 1, sz, f); fclose(f); free(buf); */ 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", CL_BootDownload_Extract, zip); zipfilefuncs.EnumerateFiles(zip, "*/*.pak", CL_BootDownload_Extract, zip); zipfilefuncs.EnumerateFiles(zip, "*/*/*.pk3", CL_BootDownload_Extract, zip); zipfilefuncs.EnumerateFiles(zip, "*/*/*.pak", CL_BootDownload_Extract, zip); /*close it, delete the temp file from disk, etc*/ zipfilefuncs.ClosePath(zip); /*restart the filesystem so those new files can be found*/ Cmd_ExecuteString("fs_restart\n", RESTRICT_LOCAL); } if (!--numbootdownloads) { CL_ExecInitialConfigs(); Cmd_StuffCmds(); Cbuf_Execute (); Cmd_ExecuteString("vid_restart\n", RESTRICT_LOCAL); } } qboolean CL_CheckBootDownloads(void) { char *downloads = fs_gamedownload.string; char token[2048]; char *c, *s; vfsfile_t *f; struct dl_download *dl; int mirrors; while ((downloads = COM_ParseOut(downloads, token, sizeof(token)))) { //FIXME: do we want to add some sort of file size indicator? c = token; while(*c && *c != ':' && *c != '|') c++; if (!*c) //erp? continue; *c++ = 0; f = FS_OpenVFS(token, "rb", FS_ROOT); if (f) { Con_DPrintf("Already have %s\n", token); VFS_CLOSE(f); continue; } mirrors = 1; for (s = c; *s; s++) { if (*s == '|') mirrors++; } mirrors = rand() % mirrors; while(mirrors) { mirrors--; while(*c != '|') c++; c++; } for (s = c; *s; s++) { if (*s == '|') *s = 0; } Con_Printf("Attempting to download %s\n", c); dl = HTTP_CL_Get(c, token, CL_BootDownload_Complete); if (dl) { #ifdef MULTITHREAD DL_CreateThread(dl, FS_OpenTemp(), CL_BootDownload_Complete); #endif numbootdownloads++; } } return !numbootdownloads; } #else qboolean CL_CheckBootDownloads(void) { return true; } #endif