From 0907e66cffa35218c1313982fb14a8eb136e8da0 Mon Sep 17 00:00:00 2001 From: Spoike Date: Sat, 17 Jul 2021 15:11:35 +0000 Subject: [PATCH] Tweak QI plugin to translate quaddicted's map database to fte's meta format. Make 'map package:map' download+run a map with the specified package enabled. git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@5976 fc73d0e0-1445-4013-8a0c-d673dee63da5 --- engine/client/cl_main.c | 1 - engine/client/m_download.c | 602 +++++++++++++++++++++++++++---------- engine/common/common.h | 1 + engine/common/fs.c | 13 +- engine/common/fs.h | 11 + engine/common/plugin.c | 13 +- engine/http/httpclient.c | 19 +- engine/http/iweb.h | 1 + engine/server/sv_ccmds.c | 22 ++ plugins/plugin.h | 8 +- plugins/qi/qi.c | 229 +++++++++++++- 11 files changed, 750 insertions(+), 170 deletions(-) diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index 872b2ebf..a920289f 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -5378,7 +5378,6 @@ extern int waitingformanifest; void Host_DoRunFile(hrf_t *f); void CL_PlayDemoStream(vfsfile_t *file, char *filename, qboolean issyspath, int demotype, float bufferdelay); void CL_ParseQTVDescriptor(vfsfile_t *f, const char *name); -qboolean FS_PathURLCache(char *url, char *path, size_t pathsize); //guesses the file type based upon its file extension. mdl/md3/iqm distinctions are not important, so we can usually get away with this in the context of quake. unsigned int Host_GuessFileType(const char *mimetype, const char *filename) diff --git a/engine/client/m_download.c b/engine/client/m_download.c index c1568be0..90abf48f 100644 --- a/engine/client/m_download.c +++ b/engine/client/m_download.c @@ -78,6 +78,7 @@ cvar_t pkg_autoupdate = CVARFD("pkg_autoupdate", "-1", CVAR_NOTFROMSERVER|CVAR_N #define DPF_SIGNATUREREJECTED (1u<<17) //signature is bad #define DPF_SIGNATUREACCEPTED (1u<<18) //signature is good (required for dll/so/exe files) #define DPF_SIGNATUREUNKNOWN (1u<<19) //signature is unknown +#define DPF_HIDEUNLESSPRESENT (1u<<20) //hidden in the ui, unless present. #define DPF_MARKED (DPF_USERMARKED|DPF_AUTOMARKED) //flags that will enable it #define DPF_ALLMARKED (DPF_USERMARKED|DPF_AUTOMARKED|DPF_MANIMARKED) //flags that will download it without necessarily enabling it. @@ -133,6 +134,7 @@ typedef struct package_s { char version[16]; char *arch; char *qhash; + char *packprefix; //extra weirdness to skip embedded gamedirs or force extra maps/ nesting quint64_t filesize; //in bytes, as part of verifying the hash. char *filesha1; @@ -168,10 +170,12 @@ typedef struct package_s { DEP_NEEDFEATURE, //requires a specific feature to be available (typically controlled via a cvar) // DEP_MIRROR, // DEP_FAILEDMIRROR, + DEP_MAP, //This package contains this map. woo. DEP_SOURCE, //which source url we found this package from DEP_EXTRACTNAME, //a file that will be installed - DEP_FILE //a file that will be installed + DEP_FILE, //a file that will be installed. will be loaded as a package, where appropriate. + DEP_CACHEFILE //an installed file that's relative to the downloads/ subdir } dtype; char name[1]; } *deps; @@ -194,6 +198,12 @@ static char *manifestpackages; //metapackage named by the manicfest. static char *declinedpackages; //metapackage named by the manicfest. static int domanifestinstall; //SECURITY_MANIFEST_* +static struct +{ + char *package; //package to load. don't forget its dependancies too. + char *map; //the map to load. +} pm_onload; + #ifndef SERVERONLY //static qboolean pluginpromptshown; //so we only show prompts for new externally-installed plugins once, instead of every time the file is reloaded. #endif @@ -209,7 +219,7 @@ static qboolean pm_packagesinstalled; //FIXME: these are allocated for the life of the exe. changing basedir should purge the list. static size_t pm_numsources = 0; -static struct +static struct pm_source_s { char *url; //url to query. unique. char *prefix; //category prefix for packages from this source. @@ -229,13 +239,17 @@ static struct #define SRCFL_NESTED (1u<<1) //discovered from a different source. always disabled. #define SRCFL_MANIFEST (1u<<2) //not saved. often default to enabled. #define SRCFL_USER (1u<<3) //user explicitly added it. included into installed.lst. enabled (if trusted). - #define SRCFLMASK_FROM (SRCFL_HISTORIC|SRCFL_NESTED|SRCFL_MANIFEST|SRCFL_USER) //mask of flags, forming priority for replacements. - #define SRCFL_DISABLED (1u<<4) //source was explicitly disabled. - #define SRCFL_ENABLED (1u<<5) //source was explicitly enabled. - #define SRCFL_PROMPTED (1u<<6) //source was explicitly enabled. - #define SRCFL_ONCE (1u<<7) //autoupdates are disabled, but the user is viewing packages anyway. enabled via a prompt. + #define SRCFL_PLUGIN (1u<<4) //user explicitly added it. included into installed.lst. enabled (if trusted). + #define SRCFLMASK_FROM (SRCFL_HISTORIC|SRCFL_NESTED|SRCFL_MANIFEST|SRCFL_USER|SRCFL_PLUGIN) //mask of flags, forming priority for replacements. + #define SRCFL_DISABLED (1u<<5) //source was explicitly disabled. + #define SRCFL_ENABLED (1u<<6) //source was explicitly enabled. + #define SRCFL_PROMPTED (1u<<7) //source was explicitly enabled. + #define SRCFL_ONCE (1u<<8) //autoupdates are disabled, but the user is viewing packages anyway. enabled via a prompt. unsigned int flags; struct dl_download *curdl; //the download context + + void *module; //plugins + plugupdatesourcefuncs_t *funcs; } *pm_source/*[pm_maxsources]*/; static int downloadablessequence; //bumped any time any package is purged @@ -478,33 +492,47 @@ static void PM_ValidatePackage(package_t *p) struct packagedep_s *dep; vfsfile_t *pf; char temp[MAX_OSPATH]; - p->flags &=~ (DPF_NATIVE|DPF_CACHED|DPF_CORRUPT); + p->flags &=~ (DPF_NATIVE|DPF_CACHED|DPF_CORRUPT|DPF_HIDEUNLESSPRESENT); if (p->flags & DPF_ENABLED) { for (dep = p->deps; dep; dep = dep->next) { char *n; - if (dep->dtype != DEP_FILE) - continue; - if (*p->gamedir) - n = va("%s/%s", p->gamedir, dep->name); - else - n = dep->name; - pf = FS_OpenVFS(n, "rb", p->fsroot); - if (pf) + if (dep->dtype == DEP_CACHEFILE) { - VFS_CLOSE(pf); - p->flags |= DPF_NATIVE; - } - else if (PM_TryGenCachedName(n, p, temp, sizeof(temp))) - { - pf = FS_OpenVFS(temp, "rb", p->fsroot); +// p->flags |= DPF_HIDEUNLESSPRESENT; + n = va("downloads/%s", dep->name); + pf = FS_OpenVFS(n, "rb", p->fsroot); if (pf) { VFS_CLOSE(pf); p->flags |= DPF_CACHED; } } + else if (dep->dtype == DEP_FILE) + { + if (*p->gamedir) + n = va("%s/%s", p->gamedir, dep->name); + else + n = dep->name; + pf = FS_OpenVFS(n, "rb", p->fsroot); + if (pf) + { + VFS_CLOSE(pf); + p->flags |= DPF_NATIVE; + } + else if (PM_TryGenCachedName(n, p, temp, sizeof(temp))) + { + pf = FS_OpenVFS(temp, "rb", p->fsroot); + if (pf) + { + VFS_CLOSE(pf); + p->flags |= DPF_CACHED; + } + } + } + else + continue; if (!(p->flags & (DPF_NATIVE|DPF_CACHED))) Con_Printf("WARNING: %s (%s) no longer exists\n", p->name, n); } @@ -516,19 +544,29 @@ static void PM_ValidatePackage(package_t *p) char *n; struct packagedep_s *odep; unsigned int fl = DPF_NATIVE; - if (dep->dtype != DEP_FILE) - continue; - if (*p->gamedir) - n = va("%s/%s", p->gamedir, dep->name); - else - n = dep->name; - pf = FS_OpenVFS(n, "rb", p->fsroot); - if (!pf && PM_TryGenCachedName(n, p, temp, sizeof(temp))) + if (dep->dtype == DEP_FILE) { - pf = FS_OpenVFS(temp, "rb", p->fsroot); - fl = DPF_CACHED; - //fixme: skip any archive checks + if (*p->gamedir) + n = va("%s/%s", p->gamedir, dep->name); + else + n = dep->name; + pf = FS_OpenVFS(n, "rb", p->fsroot); + if (!pf && PM_TryGenCachedName(n, p, temp, sizeof(temp))) + { + pf = FS_OpenVFS(temp, "rb", p->fsroot); + fl = DPF_CACHED; + //fixme: skip any archive checks + } } + else if (dep->dtype == DEP_CACHEFILE) + { +// p->flags |= DPF_HIDEUNLESSPRESENT; + fl = DPF_CACHED; + n = va("downloads/%s", dep->name); + pf = FS_OpenVFS(n, "rb", p->fsroot); + } + else + continue; if (pf) { @@ -714,10 +752,56 @@ static qboolean PM_MergePackage(package_t *oldp, package_t *newp) return false; } +static int QDECL PM_PackageSortOrdering(const void *l, const void *r) +{ //for qsort. + const package_t *a=*(package_t*const*)l, *b=*(package_t*const*)r; + const char *ac, *bc; + int order; + + //sort by categories + ac = a->category?a->category:""; + bc = b->category?b->category:""; + order = Q_strcasecmp(ac,bc); + if (order) + return order; + + //otherwise sort by title. + ac = a->title?a->title:a->name; + bc = b->title?b->title:b->name; + order = Q_strcasecmp(ac,bc); + return order; +} +static void PM_ResortPackages(void) +{ + int i, count; + package_t **sorted; + package_t *p; + for (count = 0, p = availablepackages; p; p=p->next) + count++; + if (!count) + return; + sorted = Z_Malloc(sizeof(*sorted)*count); + for (count = 0, p = availablepackages; p; p=p->next) + sorted[count++] = p; + qsort(sorted, count, sizeof(*sorted), PM_PackageSortOrdering); + availablepackages = NULL; + for (i = count; i --> 0; ) + { + sorted[i]->next = availablepackages; + sorted[i]->link = &availablepackages; + + if (availablepackages) + availablepackages->link = &sorted[i]->next; + availablepackages = sorted[i]; + } + Z_Free(sorted); +} + static package_t *PM_InsertPackage(package_t *p) { package_t **link; int v; + for (link = &availablepackages; *link; link = &(*link)->next) { package_t *prev = *link; @@ -755,7 +839,7 @@ static package_t *PM_InsertPackage(package_t *p) //FIXME: replace prev... } } - v = strcmp(prev->name, p->name); + v = PM_PackageSortOrdering(&prev, &p); if (v > 0) break; //insert before this one else if (v == 0) @@ -870,7 +954,7 @@ static qboolean PM_CheckFile(const char *filename, enum fs_relative base) return false; } -static void PM_AddSubList(const char *url, const char *prefix, unsigned int flags) +static void PM_AddSubListModule(void *module, plugupdatesourcefuncs_t *funcs, const char *url, const char *prefix, unsigned int flags) { size_t i; if (!prefix) @@ -888,6 +972,11 @@ static void PM_AddSubList(const char *url, const char *prefix, unsigned int flag { unsigned int newpri = flags&SRCFLMASK_FROM; unsigned int oldpri = pm_source[i].flags&SRCFLMASK_FROM; + if (module) + { + pm_source[i].module = module; + pm_source[i].funcs = funcs; + } if (newpri > oldpri) { //replacing an historic package should stomp on most of it, retaining only its enablement status. pm_source[i].flags &= ~SRCFLMASK_FROM; @@ -903,6 +992,8 @@ static void PM_AddSubList(const char *url, const char *prefix, unsigned int flag { Z_ReallocElements((void*)&pm_source, &pm_numsources, i+1, sizeof(*pm_source)); + pm_source[i].module = module; + pm_source[i].funcs = funcs; pm_source[i].status = SRCSTAT_UNTRIED; pm_source[i].flags = flags; @@ -915,6 +1006,10 @@ static void PM_AddSubList(const char *url, const char *prefix, unsigned int flag downloadablessequence++; } } +static void PM_AddSubList(const char *url, const char *prefix, unsigned int flags) +{ + PM_AddSubListModule(NULL, NULL, url, prefix, flags); +} #ifdef WEBCLIENT static void PM_RemSubList(const char *url) { @@ -962,52 +1057,6 @@ static const char *PM_ParsePackage(struct packagesourceinfo_s *source, const cha qboolean invariation = false; -#if 0 - if (version < 2) - { - char pathname[256]; - const char *fullname = Cmd_Argv(0); - if (argc > 5 || argc < 3) - { - Con_Printf("Package list is bad - %s\n", line); - continue; //but try the next line away - } - - p = Z_Malloc(sizeof(*p)); - if (*prefix) - Q_snprintfz(pathname, sizeof(pathname), "%s/%s", prefix, fullname); - else - Q_snprintfz(pathname, sizeof(pathname), "%s", fullname); - p->name = Z_StrDup(COM_SkipPath(pathname)); - p->title = Z_StrDup(p->name); - *COM_SkipPath(pathname) = 0; - p->category = Z_StrDup(pathname); - p->mirror[0] = Z_StrDup(p->name); - - p->priority = PM_DEFAULTPRIORITY; - p->flags = parseflags; - - p->mirror[0] = Z_StrDup(Cmd_Argv(1)); - PM_AddDep(p, DEP_FILE, Cmd_Argv(2)); - Q_strncpyz(p->version, Cmd_Argv(3), sizeof(p->version)); - Q_strncpyz(p->gamedir, Cmd_Argv(4), sizeof(p->gamedir)); - if (!strcmp(p->gamedir, "../")) - { - p->fsroot = FS_ROOT; - *p->gamedir = 0; - } - else - { - if (!*p->gamedir) - { - strcpy(p->gamedir, FS_GetGamedir(false)); -// p->fsroot = FS_GAMEONLY; - } - p->fsroot = FS_ROOT; - } - } - else -#endif { char pathname[256]; char *fullname = (source->version >= 3)?NULL:Z_StrDup(com_token); @@ -1107,6 +1156,8 @@ static const char *PM_ParsePackage(struct packagesourceinfo_s *source, const cha p->priority = atoi(val); else if (!strcmp(key, "qhash")) Z_StrDupPtr(&p->qhash, val); + else if (!strcmp(key, "packprefix")) + Z_StrDupPtr(&p->packprefix, val); else if (!strcmp(key, "desc") || !strcmp(key, "description")) { if (p->description) @@ -1132,6 +1183,12 @@ static const char *PM_ParsePackage(struct packagesourceinfo_s *source, const cha Z_StrDupPtr(&file, val); PM_AddDep(p, DEP_FILE, val); } + else if (!strcmp(key, "cachefile")) + { //installed file + if (!file) + Z_StrDupPtr(&file, val); + PM_AddDep(p, DEP_CACHEFILE, val); + } else if (!strcmp(key, "extract")) { if (!strcmp(val, "xz")) @@ -1145,6 +1202,8 @@ static const char *PM_ParsePackage(struct packagesourceinfo_s *source, const cha else Con_Printf("Unknown decompression method: %s\n", val); } + else if (!strcmp(key, "map")) + PM_AddDep(p, DEP_MAP, val); else if (!strcmp(key, "depend")) PM_AddDep(p, DEP_REQUIRE, val); else if (!strcmp(key, "conflict")) @@ -1227,7 +1286,15 @@ static const char *PM_ParsePackage(struct packagesourceinfo_s *source, const cha p->flags = flags; if (url && (!strncmp(url, "http://", 7) || !strncmp(url, "https://", 8))) + { p->mirror[0] = Z_StrDup(url); + + if (!file) + { + FS_PathURLCache(url, pathname, sizeof(pathname)); + PM_AddDep(p, DEP_CACHEFILE, pathname+10); + } + } else { int m; @@ -1589,6 +1656,42 @@ void PM_EnumeratePlugins(void (*callback)(const char *name)) } } #endif +void PM_EnumerateMaps(const char *partial, struct xcommandargcompletioncb_s *ctx) +{ + package_t *p; + struct packagedep_s *d; + size_t partiallen = strlen(partial); + char mname[256]; + const char *sep = strchr(partial, ':'); + size_t pkgpartiallen = sep?sep-partial:partiallen; + + PM_PreparePackageList(); + + for (p = availablepackages; p; p = p->next) + { + if (!Q_strncasecmp(p->name, partial, pkgpartiallen)) + { + for (d = p->deps; d; d = d->next) + { + if (d->dtype == DEP_MAP) + { + /*if (!strchr(partial, ':')) + { //try to expand to only one... + Q_snprintfz(mname, sizeof(mname), "%s:", p->name); + ctx->cb(mname, NULL, NULL, ctx); + break; + } + else*/ + { + Q_snprintfz(mname, sizeof(mname), "%s:%s", p->name, d->name); + if (!Q_strncasecmp(mname, partial, partiallen)) + ctx->cb(mname, NULL, NULL, ctx); + } + } + } + } + } +} static qboolean QDECL Host_StubClose (struct vfsfile_s *file) { @@ -1722,7 +1825,10 @@ static qboolean PM_FileInstalled_Internal(const char *package, const char *categ p->flags |= DPF_USERMARKED|DPF_ENABLED; if (PM_InsertPackage(p)) + { + PM_ResortPackages(); PM_WriteInstalledPackages(); + } return true; } @@ -1911,7 +2017,12 @@ void PM_LoadPackages(searchpath_t **oldpaths, const char *parent_pure, const cha if (d->dtype == DEP_FILE) { Q_snprintfz(temp, sizeof(temp), "%s/%s", p->gamedir, d->name); - FS_AddHashedPackage(oldpaths, parent_pure, parent_logical, search, loadstuff, temp, p->qhash, NULL, SPF_COPYPROTECTED|SPF_UNTRUSTED); + FS_AddHashedPackage(oldpaths, parent_pure, parent_logical, search, loadstuff, temp, *p->qhash?p->qhash:NULL, p->packprefix, SPF_COPYPROTECTED|SPF_UNTRUSTED); + } + else if (d->dtype == DEP_CACHEFILE) + { + Q_snprintfz(temp, sizeof(temp), "downloads/%s", d->name); + FS_AddHashedPackage(oldpaths, parent_pure, parent_logical, NULL, loadstuff, temp, *p->qhash?p->qhash:NULL, p->packprefix, SPF_COPYPROTECTED|SPF_UNTRUSTED); } } } @@ -1921,14 +2032,14 @@ void PM_LoadPackages(searchpath_t **oldpaths, const char *parent_pure, const cha void PM_Shutdown(qboolean soft) { + size_t i, pm_numoldsources = pm_numsources; //free everything... downloadablessequence++; - while(pm_numsources > 0) + pm_numsources = 0; + for (i = 0; i < pm_numoldsources; i++) { - size_t i = --pm_numsources; - #ifdef WEBCLIENT if (pm_source[i].curdl) { @@ -1937,13 +2048,24 @@ void PM_Shutdown(qboolean soft) } #endif pm_source[i].status = SRCSTAT_UNTRIED; - Z_Free(pm_source[i].url); - pm_source[i].url = NULL; - Z_Free(pm_source[i].prefix); - pm_source[i].prefix = NULL; + + if (pm_source[i].module && soft) + { //added via a plugin. reset rather than forget. + pm_source[pm_numsources++] = pm_source[i]; + } + else + { //forget it, oh noes. + Z_Free(pm_source[i].url); + pm_source[i].url = NULL; + Z_Free(pm_source[i].prefix); + pm_source[i].prefix = NULL; + } + } + if (!pm_numsources) + { + Z_Free(pm_source); + pm_source = NULL; } - Z_Free(pm_source); - pm_source = NULL; if (!soft) { @@ -2482,6 +2604,7 @@ static void PM_ListDownloaded(struct dl_download *dl) { pm_source[listidx].status = SRCSTAT_OBTAINED; PM_ParsePackageList(f, 0, dl->url, pm_source[listidx].prefix); + PM_ResortPackages(); } else if (dl->replycode == HTTP_DNSFAILURE) pm_source[listidx].status = SRCSTAT_FAILED_DNS; @@ -2560,6 +2683,22 @@ static void PM_ListDownloaded(struct dl_download *dl) } } } +static void PM_Plugin_Source_Finished(void *ctx, vfsfile_t *f) +{ + struct pm_source_s *src = ctx; + COM_AssertMainThread("PM_Plugin_Source_Finished"); + if (!src->curdl) + { + struct dl_download dl; + dl.file = f; + dl.status = DL_FINISHED; + dl.user_num = src-pm_source; + dl.url = src->url; + src->curdl = &dl; + PM_ListDownloaded(&dl); + } + VFS_CLOSE(f); +} #endif #if defined(HAVE_CLIENT) && defined(WEBCLIENT) static void PM_UpdatePackageList(qboolean autoupdate, int retry); @@ -2633,19 +2772,27 @@ static void PM_UpdatePackageList(qboolean autoupdate, int retry) if (pm_source[i].status == SRCSTAT_OBTAINED) return; //already successful once. no need to do it again. pm_source[i].flags &= ~SRCFL_ONCE; - pm_source[i].curdl = HTTP_CL_Get(pm_source[i].url, NULL, PM_ListDownloaded); - if (pm_source[i].curdl) - { - pm_source[i].curdl->user_num = i; - pm_source[i].curdl->file = VFSPIPE_Open(1, false); - pm_source[i].curdl->isquery = true; - DL_CreateThread(pm_source[i].curdl, NULL, NULL); + if (pm_source[i].funcs) + { + pm_source[i].funcs->Update(pm_source[i].url, VFS_OpenPipeCallback(PM_Plugin_Source_Finished, &pm_source[i])); } else { - Con_Printf("Could not contact updates server - %s\n", pm_source[i].url); - pm_source[i].status = SRCSTAT_FAILED_DNS; + pm_source[i].curdl = HTTP_CL_Get(pm_source[i].url, NULL, PM_ListDownloaded); + if (pm_source[i].curdl) + { + pm_source[i].curdl->user_num = i; + + pm_source[i].curdl->file = VFSPIPE_Open(1, false); + pm_source[i].curdl->isquery = true; + DL_CreateThread(pm_source[i].curdl, NULL, NULL); + } + else + { + Con_Printf("Could not contact updates server - %s\n", pm_source[i].url); + pm_source[i].status = SRCSTAT_FAILED_DNS; + } } } @@ -2665,7 +2812,24 @@ static void PM_UpdatePackageList(qboolean autoupdate, int retry) #endif } - +qboolean PM_RegisterUpdateSource(void *module, plugupdatesourcefuncs_t *funcs) +{ + size_t i; + if (!funcs) + { + for (i = 0; i < pm_numsources; i++) + { + if (pm_source[i].module == module) + { + pm_source[i].module = NULL; + pm_source[i].funcs = NULL; + } + } + } + else + PM_AddSubListModule(module, funcs, va("plug:%s", funcs->description), NULL, SRCFL_PLUGIN); + return true; +} static void COM_QuotedConcat(const char *cat, char *buf, size_t bufsize) { @@ -2798,11 +2962,17 @@ static void PM_WriteInstalledPackages(void) if (p->fsroot == FS_BINARYPATH) COM_QuotedKeyVal("root", "bin", buf, sizeof(buf)); + if (p->packprefix) + COM_QuotedKeyVal("packprefix", p->packprefix, buf, sizeof(buf)); for (dep = p->deps; dep; dep = dep->next) { if (dep->dtype == DEP_FILE) COM_QuotedKeyVal("file", dep->name, buf, sizeof(buf)); + else if (dep->dtype == DEP_CACHEFILE) + COM_QuotedKeyVal("cachefile", dep->name, buf, sizeof(buf)); + else if (dep->dtype == DEP_MAP) + COM_QuotedKeyVal("map", dep->name, buf, sizeof(buf)); else if (dep->dtype == DEP_REQUIRE) COM_QuotedKeyVal("depend", dep->name, buf, sizeof(buf)); else if (dep->dtype == DEP_CONFLICT) @@ -2910,6 +3080,11 @@ static void PM_WriteInstalledPackages(void) Q_strncatz(buf, " ", sizeof(buf)); COM_QuotedConcat("root=bin", buf, sizeof(buf)); } + if (p->packprefix) + { + Q_strncatz(buf, " ", sizeof(buf)); + COM_QuotedConcat(va("packprefix=%s", p->packprefix), buf, sizeof(buf)); + } for (dep = p->deps; dep; dep = dep->next) { @@ -2918,6 +3093,16 @@ static void PM_WriteInstalledPackages(void) Q_strncatz(buf, " ", sizeof(buf)); COM_QuotedConcat(va("file=%s", dep->name), buf, sizeof(buf)); } + else if (dep->dtype == DEP_CACHEFILE) + { + Q_strncatz(buf, " ", sizeof(buf)); + COM_QuotedConcat(va("cachefile=%s", dep->name), buf, sizeof(buf)); + } + else if (dep->dtype == DEP_MAP) + { + Q_strncatz(buf, " ", sizeof(buf)); + COM_QuotedConcat(va("map=%s", dep->name), buf, sizeof(buf)); + } else if (dep->dtype == DEP_REQUIRE) { Q_strncatz(buf, " ", sizeof(buf)); @@ -2984,10 +3169,10 @@ static void PM_PackageEnabled(package_t *p) FS_FlushFSHashFull(); for (dep = p->deps; dep; dep = dep->next) { - if (dep->dtype != DEP_FILE) + if (dep->dtype != DEP_FILE && dep->dtype != DEP_CACHEFILE) continue; COM_FileExtension(dep->name, ext, sizeof(ext)); - if (!stricmp(ext, "pak") || !stricmp(ext, "pk3")) + if (!stricmp(ext, "pak") || !stricmp(ext, "pk3") || !stricmp(ext, "zip")) { if (pm_packagesinstalled) { @@ -3172,33 +3357,47 @@ static void PM_Download_Got(int iarg, void *data) } #endif + + + + + + for (dep = p->deps; dep; dep = dep->next) { unsigned int nfl; - if (dep->dtype != DEP_FILE) + if (dep->dtype != DEP_FILE && dep->dtype != DEP_CACHEFILE) continue; COM_FileExtension(dep->name, ext, sizeof(ext)); - if (!stricmp(ext, "pak") || !stricmp(ext, "pk3")) + if (!stricmp(ext, "pak") || !stricmp(ext, "pk3") || !stricmp(ext, "zip")) FS_UnloadPackFiles(); //we reload them after #ifdef PLUGINS if ((!stricmp(ext, "dll") || !stricmp(ext, "so")) && !Q_strncmp(dep->name, PLUGINPREFIX, strlen(PLUGINPREFIX))) Cmd_ExecuteString(va("plug_close %s\n", dep->name), RESTRICT_LOCAL); //try to purge plugins so there's no files left open #endif - nfl = DPF_NATIVE; - if (*p->gamedir) + if (dep->dtype == DEP_CACHEFILE) { - char temp[MAX_OSPATH]; - destname = va("%s/%s", p->gamedir, dep->name); - if (PM_TryGenCachedName(destname, p, temp, sizeof(temp))) - { - nfl = DPF_CACHED; - destname = va("%s", temp); - } + nfl = DPF_CACHED; + destname = va("downloads/%s", dep->name); } else - destname = dep->name; + { + nfl = DPF_NATIVE; + if (!*p->gamedir) //basedir + destname = dep->name; + else + { + char temp[MAX_OSPATH]; + destname = va("%s/%s", p->gamedir, dep->name); + if (PM_TryGenCachedName(destname, p, temp, sizeof(temp))) + { + nfl = DPF_CACHED; + destname = va("%s", temp); + } + } + } if (p->flags & DPF_MARKED) nfl |= DPF_ENABLED; nfl |= (p->flags & ~(DPF_CACHED|DPF_NATIVE|DPF_CORRUPT)); @@ -3319,18 +3518,21 @@ static char *PM_GetTempName(package_t *p) //always favour the file so that we can rename safely without needing a copy. for (dep = p->deps, fdep = NULL; dep; dep = dep->next) { - if (dep->dtype != DEP_FILE) - continue; - if (fdep) + if (dep->dtype == DEP_FILE || dep->dtype == DEP_CACHEFILE) { - fdep = NULL; - break; + if (fdep) + { + fdep = NULL; + break; + } + fdep = dep; } - fdep = dep; } if (fdep) { - if (*p->gamedir) + if (fdep->dtype == DEP_CACHEFILE) + destname = va("downloads/%s.tmp", fdep->name); + else if (*p->gamedir) destname = va("%s/%s.tmp", p->gamedir, fdep->name); else destname = va("%s.tmp", fdep->name); @@ -3478,12 +3680,12 @@ static qboolean PM_SignatureOkay(package_t *p) for (dep = p->deps; dep; dep = dep->next) { - if (dep->dtype != DEP_FILE) + if (dep->dtype != DEP_FILE && dep->dtype != DEP_CACHEFILE) continue; //only allow .pak/.pk3/.zip without a signature, and only when they have a qhash specified (or the .fmf specified it without a qhash...). COM_FileExtension(dep->name, ext, sizeof(ext)); - if ((!stricmp(ext, "pak") || !stricmp(ext, "pk3") || !stricmp(ext, "zip")) && (p->qhash || (p->flags&DPF_MANIFEST))) + if ((!stricmp(ext, "pak") || !stricmp(ext, "pk3") || !stricmp(ext, "zip")) && (p->qhash || dep->dtype == DEP_CACHEFILE || (p->flags&DPF_MANIFEST))) ; else return false; @@ -3537,12 +3739,64 @@ int PM_IsApplying(qboolean listsonly) } #ifdef WEBCLIENT +static size_t PM_AddFilePackage(const char *packagename, struct gamepacks *gp, size_t numgp) +{ + size_t found = 0; + struct packagedep_s *dep; + package_t *p = PM_FindPackage(packagename); + if (!p) + return 0; + + if (found < numgp) + { + gp[found].path = NULL; + gp[found].url = p->mirror[0]; + for (dep = p->deps; dep; dep = dep->next) + { + if (dep->dtype == DEP_CACHEFILE) + gp[found].path = Z_StrDupf("downloads/%s", dep->name); + } + if (gp[found].path && gp[found].url) + { + gp[found].subpath = p->packprefix; + found++; + } + } + for (dep = p->deps; dep; dep = dep->next) + { + if (dep->dtype == DEP_REQUIRE) + found += PM_AddFilePackage(dep->name, gp+found, numgp); + } + return found; +} static void PM_DownloadsCompleted(int iarg, void *data) { //if something installed, then make sure everything is reconfigured properly. - if (pm_packagesinstalled) + if (pm_packagesinstalled || pm_onload.package || pm_onload.map) { pm_packagesinstalled = false; - FS_ChangeGame(fs_manifest, true, false); + + if (pm_onload.package) + { + package_t *p = PM_FindPackage(pm_onload.package); + char *map = pm_onload.map; + + struct gamepacks packs[64]; + size_t usedpacks; + pm_onload.map = NULL; + + usedpacks = PM_AddFilePackage(pm_onload.package, packs, countof(packs)-1); + packs[usedpacks].path = NULL; + packs[usedpacks].subpath = NULL; + packs[usedpacks].url = NULL; + + Z_Free(pm_onload.package); + pm_onload.package = NULL; + COM_Gamedir(p->gamedir, packs); + Cbuf_InsertText(va("map %s\n", map), RESTRICT_LOCAL, false); + Z_Free(map); + } + else + FS_ChangeGame(fs_manifest, true, false); } } @@ -3696,6 +3950,39 @@ static void PM_StartADownload(void) //clear the updating flag once there's no more activity needed pkg_updating = downloading; } +qboolean PM_LoadMapPackage(const char *package) +{ + struct packagedep_s *dep; + package_t *p = PM_FindPackage(package); + if (!p) + return false; + + if (p->flags & DPF_PRESENT) + return true; //okay, already installed. + if (p->extract == EXTRACT_ZIP) + return false; + + //refuse to use weird packages. + for (dep = p->deps; dep; dep = dep->next) + { + if (dep->dtype == DEP_FILE) + return false; + } + + //make sure its downloaded. fs code will be asked to explicitly use it, so no need to activate it. + p->trymirrors = ~0u; + return true; +} +void PM_LoadMap(const char *package, const char *map) +{ + if (!PM_LoadMapPackage(package)) + return; + pm_onload.package = Z_StrDup(package); + pm_onload.map = Z_StrDup(map); + + pkg_updating = true; + PM_StartADownload(); +} #endif //'just' starts doing all the things needed to remove/install selected packages void PM_ApplyChanges(void) @@ -3726,11 +4013,11 @@ void PM_ApplyChanges(void) for (dep = p->deps; dep; dep = dep->next) { - if (dep->dtype == DEP_FILE) + if (dep->dtype == DEP_FILE || dep->dtype == DEP_CACHEFILE) { char ext[8]; COM_FileExtension(dep->name, ext, sizeof(ext)); - if (!stricmp(ext, "pak") || !stricmp(ext, "pk3")) + if (!stricmp(ext, "pak") || !stricmp(ext, "pk3") || !stricmp(ext, "zip")) reloadpacks = true; #ifdef PLUGINS //when disabling/purging plugins, be sure to unload them first (unfortunately there might be some latency before this can actually happen). @@ -3748,7 +4035,13 @@ void PM_ApplyChanges(void) Con_Printf("Purging package %s\n", p->name); for (dep = p->deps; dep; dep = dep->next) { - if (dep->dtype == DEP_FILE) + if (dep->dtype == DEP_CACHEFILE) + { + char *f = va("downloads/%s", dep->name); + if (!FS_Remove(f, p->fsroot)) + p->flags |= DPF_CACHED; + } + else if (dep->dtype == DEP_FILE) { if (*p->gamedir) { @@ -3856,7 +4149,7 @@ void PM_ApplyChanges(void) if (p->mirror[i]) break; for (dep = p->deps; dep; dep=dep->next) - if (dep->dtype == DEP_FILE) + if (dep->dtype == DEP_FILE||dep->dtype == DEP_CACHEFILE) break; if (!dep && i == countof(p->mirror)) { //this appears to be a meta package with no download @@ -4047,25 +4340,6 @@ qboolean PM_CanInstall(const char *packagename) return false; } -static int QDECL sortpackages(const void *l, const void *r) -{ - const package_t *a=*(package_t*const*)l, *b=*(package_t*const*)r; - const char *ac, *bc; - int order; - - //sort by categories - ac = a->category?a->category:""; - bc = b->category?b->category:""; - order = strcmp(ac,bc); - if (order) - return order; - - //otherwise sort by title. - ac = a->title?a->title:a->name; - bc = b->title?b->title:b->name; - order = strcmp(ac,bc); - return order; -} void PM_Command_f(void) { package_t *p; @@ -4155,7 +4429,7 @@ void PM_Command_f(void) continue; sorted[count++] = p; } - qsort(sorted, count, sizeof(*sorted), sortpackages); + qsort(sorted, count, sizeof(*sorted), PM_PackageSortOrdering); for (i = 0; i < count; i++) { char quoted[8192]; @@ -4673,6 +4947,7 @@ void PM_AddManifestPackages(ftemanifest_t *man) continue; } + PM_ResortPackages(); PM_ApplyChanges(); } @@ -4821,6 +5096,10 @@ static void MD_Draw (int x, int y, struct menucustom_s *c, struct emenu_s *m) { package_t *p; char *n; + + if (y + 8 < 0 || y >= vid.height) //small optimisation. + return; + if (c->dint != downloadablessequence) return; //probably stale p = c->dptr; @@ -5027,18 +5306,21 @@ static qboolean MD_Key (struct menucustom_s *c, struct emenu_s *m, int key, unsi { if (p == p2) continue; + if (strcmp(p->gamedir, p2->gamedir)) + continue; //different gamedirs. don't screw up. for (dep = p->deps; dep; dep = dep->next) { - if (dep->dtype != DEP_FILE) - continue; - for (dep2 = p2->deps; dep2; dep2 = dep2->next) + if (dep->dtype == DEP_FILE) { - if (dep2->dtype != DEP_FILE) - continue; - if (!strcmp(dep->name, dep2->name)) + for (dep2 = p2->deps; dep2; dep2 = dep2->next) { - PM_UnmarkPackage(p2, DPF_MARKED); - break; + if (dep2->dtype != DEP_FILE) + continue; + if (!strcmp(dep->name, dep2->name)) + { + PM_UnmarkPackage(p2, DPF_MARKED); + break; + } } } } @@ -5242,6 +5524,8 @@ static int MD_AddItemsToDownloadMenu(emenu_t *m, int y, const char *pathprefix) continue; if ((p->flags & DPF_HIDDEN) && (p->arch || !(p->flags & DPF_ENABLED))) continue; + if ((p->flags & DPF_HIDEUNLESSPRESENT) && !(p->flags & DPF_PRESENT)) + continue; slash = strchr(p->category+prefixlen, '/'); if (!slash) { @@ -5317,6 +5601,8 @@ static int MD_AddItemsToDownloadMenu(emenu_t *m, int y, const char *pathprefix) continue; if ((p->flags & DPF_HIDDEN) && (p->arch || !(p->flags & DPF_ENABLED))) continue; + if ((p->flags & DPF_HIDEUNLESSPRESENT) && !(p->flags & DPF_PRESENT)) + continue; slash = strchr(p->category+prefixlen, '/'); if (slash) diff --git a/engine/common/common.h b/engine/common/common.h index 0a0e81ae..d44922d8 100644 --- a/engine/common/common.h +++ b/engine/common/common.h @@ -798,6 +798,7 @@ struct gamepacks char *subpath; //within the package (for zips) }; void COM_Gamedir (const char *dir, const struct gamepacks *packagespaths); +qboolean FS_PathURLCache(const char *url, char *path, size_t pathsize); //converts a url to something that can be shoved into a filesystem qboolean FS_GamedirIsOkay(const char *path); char *FS_GetGamedir(qboolean publicpathonly); char *FS_GetManifestArgs(void); diff --git a/engine/common/fs.c b/engine/common/fs.c index f115ca23..9be31d45 100644 --- a/engine/common/fs.c +++ b/engine/common/fs.c @@ -3139,12 +3139,19 @@ void FS_AddHashedPackage(searchpath_t **oldpaths, const char *parentpath, const continue; if ((loadstuff & (1<next) diff --git a/engine/common/fs.h b/engine/common/fs.h index d0fad4fe..aa71b4e1 100644 --- a/engine/common/fs.h +++ b/engine/common/fs.h @@ -76,6 +76,9 @@ void PM_LoadPackages(searchpath_t **oldpaths, const char *parent_pure, const cha void *PM_GeneratePackageFromMeta(vfsfile_t *file, char *fname, size_t fnamesize, enum fs_relative *fsroot); void PM_FileInstalled(const char *filename, enum fs_relative fsroot, void *metainfo, qboolean enable); //we finished installing a file via some other mechanism (drag+drop or from server. insert it into the updates menu. void PM_EnumeratePlugins(void (*callback)(const char *name)); +struct xcommandargcompletioncb_s; +void PM_EnumerateMaps(const char *partial, struct xcommandargcompletioncb_s *ctx); +void PM_LoadMap(const char *package, const char *map); int PM_IsApplying(qboolean listsonly); unsigned int PM_MarkUpdates (void); //mark new/updated packages as needing install. void PM_ApplyChanges(void); //for -install/-doinstall args @@ -85,6 +88,14 @@ qboolean PM_FindUpdatedEngine(char *syspath, size_t syspathsize); //names the en void PM_AddManifestPackages(ftemanifest_t *man); void Menu_Download_Update(void); +typedef struct +{ + char *description; + void (*Update) (const char *url, vfsfile_t *out); +#define plugupdatesourcefuncs_name "UpdateSource" +} plugupdatesourcefuncs_t; +qboolean PM_RegisterUpdateSource(void *module, plugupdatesourcefuncs_t *funcs); + int FS_EnumerateKnownGames(qboolean (*callback)(void *usr, ftemanifest_t *man), void *usr); struct modlist_s diff --git a/engine/common/plugin.c b/engine/common/plugin.c index 18ea771a..5d821ee0 100644 --- a/engine/common/plugin.c +++ b/engine/common/plugin.c @@ -406,6 +406,10 @@ static qboolean QDECL PlugBI_ExportInterface(const char *name, void *interfacept #ifdef HAVE_CLIENT if (!strcmp(name, plugvrfuncs_name)) return R_RegisterVRDriver(currentplug, interfaceptr); +#endif +#ifdef PACKAGEMANAGER + if (!strcmp(name, plugupdatesourcefuncs_name)) + return PM_RegisterUpdateSource(currentplug, interfaceptr); #endif return false; } @@ -1117,7 +1121,6 @@ static void QDECL Plug_Net_Close(qhandle_t handle) } #if defined(HAVE_SERVER) && defined(HAVE_CLIENT) -qboolean FS_PathURLCache(const char *url, char *path, size_t pathsize); static qboolean QDECL Plug_MapLog_Query(const char *packagename, const char *mapname, float *vals) { if (!strncmp(packagename, "http://", 7) || !strncmp(packagename, "https://", 8)) @@ -1532,6 +1535,9 @@ void Plug_Close(plugin_t *plug) S_UnregisterSoundInputModule(plug); #endif NET_RegisterCrypto(plug, NULL); +#ifdef PACKAGEMANAGER + PM_RegisterUpdateSource(currentplug, NULL); +#endif FS_UnRegisterFileSystemModule(plug); Mod_UnRegisterAllModelFormats(plug); @@ -1791,11 +1797,14 @@ static void *QDECL PlugBI_GetEngineInterface(const char *interfacename, size_t s { static plugcmdfuncs_t funcs = { - Plug_Cmd_AddCommand, + COM_QuotedString, + COM_ParseType, + COM_ParseTokenOut, Plug_Cmd_TokenizeString, Plug_Cmd_Args, Plug_Cmd_Argv, Plug_Cmd_Argc, + Plug_Cmd_AddCommand, Plug_Cmd_AddText, }; if (structsize == sizeof(funcs)) diff --git a/engine/http/httpclient.c b/engine/http/httpclient.c index d8aed149..6fbe1284 100644 --- a/engine/http/httpclient.c +++ b/engine/http/httpclient.c @@ -1798,6 +1798,9 @@ typedef struct void *mutex; int refs; qboolean terminate; //one end has closed, make the other report failures now that its no longer needed. + + void *ctx; + void (*callback) (void *ctx, vfsfile_t *pipe); } vfspipe_t; static qboolean QDECL VFSPIPE_Close(vfsfile_t *f) @@ -1814,6 +1817,8 @@ static qboolean QDECL VFSPIPE_Close(vfsfile_t *f) Sys_DestroyMutex(p->mutex); free(p); } + else if (p->callback) + p->callback(p->ctx, f); return true; } static qofs_t QDECL VFSPIPE_GetLen(vfsfile_t *f) @@ -1892,7 +1897,7 @@ static int QDECL VFSPIPE_WriteBytes(vfsfile_t *f, const void *buffer, int len) return len; } -vfsfile_t *VFSPIPE_Open(int refs, qboolean seekable) +vfsfile_t *VFS_OpenPipeInternal(int refs, qboolean seekable, void (*callback)(void *ctx, vfsfile_t *file), void *ctx) { vfspipe_t *newf; newf = malloc(sizeof(*newf)); @@ -1909,6 +1914,9 @@ vfsfile_t *VFSPIPE_Open(int refs, qboolean seekable) newf->funcs.ReadBytes = VFSPIPE_ReadBytes; newf->funcs.WriteBytes = VFSPIPE_WriteBytes; + newf->ctx = ctx; + newf->callback = callback; + if (seekable) { //if this is set, then we allow changing the readpos at the expense of buffering the ENTIRE file. no more fifo. newf->funcs.Seek = VFSPIPE_Seek; @@ -1924,3 +1932,12 @@ vfsfile_t *VFSPIPE_Open(int refs, qboolean seekable) return &newf->funcs; } +vfsfile_t *VFS_OpenPipeCallback(void (*callback)(void*ctx, vfsfile_t *file), void *ctx) +{ + return VFS_OpenPipeInternal(2, false, callback, ctx); +} + +vfsfile_t *VFSPIPE_Open(int refs, qboolean seekable) +{ + return VFS_OpenPipeInternal(refs, seekable, NULL, NULL); +} diff --git a/engine/http/iweb.h b/engine/http/iweb.h index b66937f0..355d4fd5 100644 --- a/engine/http/iweb.h +++ b/engine/http/iweb.h @@ -139,6 +139,7 @@ struct dl_download }; vfsfile_t *VFSPIPE_Open(int refs, qboolean seekable); //refs should be 1 or 2, to say how many times it must be closed before its actually closed, so both ends can close separately +vfsfile_t *VFS_OpenPipeCallback(void (*callback)(void*ctx, vfsfile_t *file), void *ctx); void HTTP_CL_Think(const char **fname, float *percent); void HTTP_CL_Terminate(void); //kills all active downloads unsigned int HTTP_CL_GetActiveDownloads(void); diff --git a/engine/server/sv_ccmds.c b/engine/server/sv_ccmds.c index ec418e4e..c66a485e 100644 --- a/engine/server/sv_ccmds.c +++ b/engine/server/sv_ccmds.c @@ -19,6 +19,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "quakedef.h" #include "pr_common.h" +#include "fs.h" #ifndef CLIENTONLY @@ -491,6 +492,10 @@ static void SV_Map_c(int argn, const char *partial, struct xcommandargcompletion COM_EnumerateFiles(va("maps/%s*.map.gz", partial), CompleteMapListExt, ctx); COM_EnumerateFiles(va("maps/%s*.cm", partial), CompleteMapList, ctx); COM_EnumerateFiles(va("maps/%s*.hmp", partial), CompleteMapList, ctx); + +#ifdef PACKAGEMANAGER + PM_EnumerateMaps(partial, ctx); +#endif } } @@ -534,6 +539,9 @@ quake2: quake: + is used in certain map names. * cannot be, but $ potentially could be. +fte: +'map package:mapname' should download the specified map package and load up its maps. + mvdsv: basemap#modifier.ent files @@ -616,6 +624,20 @@ void SV_Map_f (void) return; } +#ifdef PACKAGEMANAGER + if (Cmd_Argc() == 2) + { + char *mangled = Cmd_Argv(1); + char *sep = strchr(mangled, ':'); + if (sep) + { + *sep++ = 0; + PM_LoadMap(mangled, sep); + return; + } + } +#endif + Q_strncpyz (level, Cmd_Argv(1), sizeof(level)); startspot = ((Cmd_Argc() == 2)?NULL:Cmd_Argv(2)); } diff --git a/plugins/plugin.h b/plugins/plugin.h index ecc21adc..46275131 100644 --- a/plugins/plugin.h +++ b/plugins/plugin.h @@ -216,15 +216,21 @@ typedef struct //subconsole handling #define plugsubconsolefuncs_name "SubConsole" } plugsubconsolefuncs_t; +enum com_tokentype_e; typedef struct //console command/tokenizing/cbuf functions { - F(qboolean, AddCommand, (const char *buffer)); //Registers a console command. + F(const char *,QuotedString, (const char *string, char *buf, int buflen, qboolean omitquotes)); //generates a string with c-style markup and relevant quote types. + F(char *, ParseToken, (const char *data, char *token, size_t tokenlen, enum com_tokentype_e *tokentype)); //standard quake-style token parsing. + F(char *, ParsePunctuation, (const char *data, const char *punctuation, char *token, size_t tokenlen, enum com_tokentype_e *tokentype)); //use explicit punctuation. + F(void, TokenizeString, (const char *msg)); //tokenize a string. F(void, Args, (char *buffer, int bufsize)); //Gets the extra args F(void, Argv, (int argnum, char *buffer, size_t bufsize)); //Gets a 0-based token F(int, Argc, (void)); //gets the number of tokens available. + F(qboolean, AddCommand, (const char *cmdname)); //Registers a console command. + F(void, AddText, (const char *text, qboolean insert)); #define plugcmdfuncs_name "Cmd" } plugcmdfuncs_t; diff --git a/plugins/qi/qi.c b/plugins/qi/qi.c index 6dd9775d..8cea4409 100644 --- a/plugins/qi/qi.c +++ b/plugins/qi/qi.c @@ -1,3 +1,5 @@ +#include "quakedef.h" +#include "fs.h" #include "../plugin.h" static plugsubconsolefuncs_t *confuncs; static plugfsfuncs_t *filefuncs; @@ -8,6 +10,7 @@ static plugclientfuncs_t *clientfuncs; #define DATABASEURL "https://www.quaddicted.com/reviews/quaddicted_database.xml" #define FILEIMAGEURL "https://www.quaddicted.com/reviews/screenshots/%s_injector.jpg" #define FILEDOWNLOADURL "https://www.quaddicted.com/filebase/%s.zip" +#define FILEREVIEWURL "https://www.quaddicted.com/reviews/%s.html" #define WINDOWTITLE "Quaddicted Map+Mod Archive" #define WINDOWNAME "QI" @@ -30,6 +33,7 @@ static plugclientfuncs_t *clientfuncs; static xmltree_t *thedatabase; static qhandle_t dlcontext = -1; +static vfsfile_t *packagemanager; static struct { @@ -102,7 +106,7 @@ static qboolean QI_SetupWindow(const char *console, qboolean force) confuncs->SetActive(console); return true; } -static void QI_DeHTML(const char *in, char *out, size_t outsize) +static void QI_DeHTML(const char *in, qboolean escapes, char *out, size_t outsize) { outsize--; while(*in && outsize > 0) @@ -175,6 +179,12 @@ static void QI_DeHTML(const char *in, char *out, size_t outsize) outsize--; } } + else if ((*in == '\"') && escapes && outsize >= 2) + { + *out++ = '\\'; + *out++ = *in++; + outsize-=2; + } else { *out++ = *in++; @@ -339,7 +349,7 @@ static void QI_RefreshMapList(qboolean forcedisplay) year += 1900; Q_snprintf(descbuf, sizeof(descbuf), "^aId:^a %s\n^aAuthor(s):^a %s\n^aDate:^a %04u-%02u-%02u\n^aRating:^a %s\n\n", id, author, year, month, day, ratingtext); - QI_DeHTML(desc, descbuf + strlen(descbuf), sizeof(descbuf) - strlen(descbuf)); + QI_DeHTML(desc, false, descbuf + strlen(descbuf), sizeof(descbuf) - strlen(descbuf)); desc = descbuf; Con_SubPrintf(console, "%s %s ^[^4%s: ^1%s\\tip\\%s\\tipimg\\"FILEIMAGEURL"\\id\\%s^]", type, ratingtext, id, XML_GetChildBody(file, "title", ""), desc, id, id); @@ -526,6 +536,187 @@ static void QI_RunMap(xmltree_t *qifile, const char *map) cmdfuncs->AddText("\n", false); } + +void VARGS VFS_PRINTF(vfsfile_t *vf, const char *format, ...) +{ + va_list argptr; + char string[1024]; + + va_start (argptr, format); + vsnprintf (string,sizeof(string)-1, format,argptr); + va_end (argptr); + + VFS_PUTS(vf, string); +} +static void QI_WriteUpdateList(vfsfile_t *pminfo) +{ + xmltree_t *file; + char descbuf[1024], *d, *nl; + + if (thedatabase) + for (file = thedatabase->child; file; file = file->sibling) + { + const char *id = XML_GetParameter(file, "id", "unnamed"); + const char *rating = XML_GetParameter(file, "rating", ""); + int ratingnum = atoi(rating); + const char *author = XML_GetChildBody(file, "author", "unknown"); + const char *desc = XML_GetChildBody(file, "description", ""); + const char *type = XML_GetParameter(file, "type", ""); + const char *date = XML_GetChildBody(file, "date", "1.1.1990"); + xmltree_t *tech = XML_ChildOfTree(file, "techinfo", 0); + const char *cmdline = XML_GetChildBody(tech, "commandline", ""); + const char *zipbasedir = XML_GetChildBody(tech, "zipbasedir", ""); + int year, month, day; + int startmapnum, i; + char ratingtext[65]; + char gamedir[65]; + char prefix[MAX_QPATH]; + xmltree_t *startmap; + if (strcmp(file->name, "file")) + continue; //erk? + if (atoi(XML_GetParameter(file, "hide", "")) || atoi(XML_GetParameter(file, "fte_hide", ""))) + continue; + switch(atoi(type)) + { + case 1: + type = "map"; //'single map file(s)' + break; + case 2: + type = "mod"; //'Partial conversion' + break; + case 4: + type = "spd"; //'speedmapping' + break; + case 5: + type = "otr"; //'misc files' + break; + default: + type = "???"; //no idea + break; + } + + if (ratingnum > (sizeof(ratingtext)-5)/6) + ratingnum = (sizeof(ratingtext)-5)/6; + if (ratingnum) + { + Q_snprintf(ratingtext, sizeof(ratingtext), "^a"); + for (i = 0; i < ratingnum; i++) + Q_snprintf(ratingtext + i+2, sizeof(ratingtext)-i*2+2, "*"); + Q_snprintf(ratingtext + i+2, sizeof(ratingtext)-i*2+2, "^a"); + } + else if (*rating) + Q_snprintf(ratingtext, sizeof(ratingtext), "%s", rating); + else + Q_snprintf(ratingtext, sizeof(ratingtext), "%s", "unrated"); + + + day = atoi(date?date:"1"); + date = date?strchr(date, '.'):NULL; + month = atoi(date?date+1:"1"); + date = date?strchr(date+1, '.'):NULL; + year = atoi(date?date+1:"1990"); + if (year < 90) + year += 2000; + else if (year < 1900) + year += 1900; + + strcpy(gamedir, "id1"); + while (cmdline) + { + cmdline = cmdfuncs->ParseToken(cmdline, descbuf, sizeof(descbuf), NULL); + if (!strcmp(descbuf, "-game")) + cmdline = cmdfuncs->ParseToken(cmdline, gamedir, sizeof(gamedir), NULL); + else if (!strcmp(descbuf, "-hipnotic") || !strcmp(descbuf, "-rogue") || !strcmp(descbuf, "-quoth")) + { + if (!*gamedir) + strcpy(gamedir, descbuf+1); + } + } + if (!*gamedir) + continue; //bad package + + + *descbuf = 0; + QI_DeHTML(desc, true, descbuf + strlen(descbuf), sizeof(descbuf) - strlen(descbuf)); + desc = descbuf; + + VFS_PRINTF(pminfo, "{\n" + "\tpackage \"qi_%s\"\n" + "\ttitle \"%s %s\"\n" + "\tcategory \"Quaddicted - %i\"\n" + "\tlicense \"Unknown\"\n" + "\tauthor \"Unknown\"\n" + "\tqhash \"\"\n" + ,id, ratingtext, XML_GetChildBody(file, "title", ""), year); + + VFS_PRINTF(pminfo, "\tgamedir \"%s\"\n", gamedir); + + VFS_PRINTF(pminfo, "\tver %04u-%02u-%02u\n", year, month, day); + if (!strchr(author, '\"') && !strchr(author, '\n')) + VFS_PRINTF(pminfo, "\tauthor \"%s\"\n", author); + VFS_PRINTF(pminfo, "\twebsite \""FILEREVIEWURL"\"\n", id); //in lieu of an actual site, lets fill it with quaddicted's reviews. + + VFS_PRINTF(pminfo, "\turl \""FILEDOWNLOADURL"\"\n", id); + + + //skip any dodgy leading slashes + while (*zipbasedir == '/' || *zipbasedir == '\\') + zipbasedir++; + if (!*zipbasedir) + strcpy(prefix, ".."); //err, there wasn't a directory... we still need to 'strip' it though. + else + { + //skip the zip's gamedir + while (*zipbasedir && *zipbasedir != '/' && *zipbasedir != '\\') + zipbasedir++; + //skip any trailing + while (*zipbasedir == '/' || *zipbasedir == '\\') + zipbasedir++; + for (i = 0; *zipbasedir; i++) + { + if (i >= sizeof(prefix)-1) + break; + if (*zipbasedir == '\\') //sigh + prefix[i] = '/'; + else if (*zipbasedir == '\"' || *zipbasedir == '\n' || *zipbasedir == '\r') + break; //bad char... + else + prefix[i] = *zipbasedir; + zipbasedir++; + } + while (i > 0 && prefix[i-1] == '/') + i--; + prefix[i] = 0; + } + if (*prefix) + VFS_PRINTF(pminfo, "\tpackprefix \"%s\"\n", prefix); + + for(d = descbuf;;) + { + nl = strchr(d, '\n'); + if (nl) + *nl++ = 0; + VFS_PRINTF(pminfo, "\tdesc \"%s\"\n", d); + if (!nl) + break; + d = nl; + } + + //VFS_PRINTF(pminfo, "\tpreview \""FILEIMAGEURL"\"\n", id); + for (startmapnum = 0; ; startmapnum++) + { + startmap = XML_ChildOfTree(tech, "startmap", startmapnum); + if (!startmap) + break; + + VFS_PRINTF(pminfo, "\tmap \"%s\"\n", startmap->body); + } + if (!startmapnum) //if there's no start maps listed, use the package's id instead. these are not intended for anything else. + VFS_PRINTF(pminfo, "\tmap \"%s\"\n", id); + VFS_PRINTF(pminfo, "}\n"); + } +} + static unsigned int QI_GetDate(xmltree_t *file) { unsigned int day, month, year; @@ -685,8 +876,8 @@ static void QDECL QI_Tick(double realtime, double gametime) { if (dlcontext != -1) { - qofs_t flen; - if (filefuncs->GetLen(dlcontext, &flen)) + qofs_t flen=0; + if (dlcontext < 0 || filefuncs->GetLen(dlcontext, &flen)) { int ofs = 0; char *file; @@ -729,6 +920,22 @@ static void QDECL QI_Tick(double realtime, double gametime) // XML_ConPrintTree(thedatabase, "quadicted_xml", 0); } } + else if (packagemanager) + { + if (!thedatabase && dlcontext == -1) + { + dlcontext = -2; + //if (filefuncs->Open(DATABASEURL, &dlcontext, 1) >= 0) + return; + } + else + { + VFS_PRINTF(packagemanager, "version 3\n"); + QI_WriteUpdateList(packagemanager); + } + VFS_CLOSE(packagemanager); + packagemanager = NULL; + } } static int QDECL QI_ConExecuteCommand(qboolean isinsecure) @@ -774,12 +981,26 @@ static qboolean QI_ExecuteCommand(qboolean isinsecure) return false; } +void QI_GenPackages(const char *url, vfsfile_t *pipe) +{ + if (packagemanager) + VFS_CLOSE(packagemanager); + packagemanager = pipe; +} +static plugupdatesourcefuncs_t sourcefuncs = +{ + "Quaddicted Maps", + QI_GenPackages +}; + qboolean Plug_Init(void) { confuncs = plugfuncs->GetEngineInterface(plugsubconsolefuncs_name, sizeof(*confuncs)); filefuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*filefuncs)); clientfuncs = plugfuncs->GetEngineInterface(plugclientfuncs_name, sizeof(*clientfuncs)); + plugfuncs->ExportInterface(plugupdatesourcefuncs_name, &sourcefuncs, sizeof(sourcefuncs)); + if (confuncs && filefuncs && clientfuncs) { filters.minrating = filters.maxrating = -1;