From 03f974b2bc71be0e54bef2e69eac435c54a9a4f4 Mon Sep 17 00:00:00 2001 From: ac-stef Date: Mon, 20 Apr 2015 14:59:24 +0200 Subject: [PATCH] additional text attributes for mapmodels This adds a means to store additional attributes for mapmodels, including a caching mechanism, so the mapmodels don't have to be loaded to access the information. The goal is, to provide fully automatic mapmodel menus for map editing who also add information about downloaded mapmodels. Model attributes are extracted everytime, when a model is loaded. At game end, all available data is written to config/mapmodelattributes.cfg and restored at next startup. On release, a file with all (but only) official models should be included. Since autodownloaded models are usually "loaded" after download, those are automatically added to the database. If someone adds models manually to the directories (media pack, w/e), the command "loadallmapmodels" can be used to try to find all available models. The only way to delete something from the database, is to delete the file config/mapmodelattributes.cfg when the game is closed. The current list of attributes is: keywords List of one or more menu topics to list the model under. Check config/menus_edit.cfg for available keywords. Unknown keywords will be listed separately. desc Short name or description of the model. Will be used as menu entry. usage Important information for how to use that specific model. author Who created the mapmodel. If it's a derivative of another model, state the author of the mod (refer to the original author under "license", if you like). Always use the exact same spelling for the same author, to allow menus to automatically group models by author. license Free-text attribute for license info or info about additional contributors (or original authors for derivated works). distribution One of "official", "autodownload" or "manual". Other values default to "manual". This is used to determine, if a map contains only original media or media that can be download automatically. version Single numerical value to separate older from newer versions. Defaults to zero. Higher numbers mean "newer". Maybe someone manages to implement a versioning system for custom media in the future... New commands: mdlattribute To be used in model config files. Adds a value for a specified attribute to the model. mapmodelregister ... Adds values for all attributes to a specific mapmodel. This is used to restore the cached values from config/mapmodelattributes.cfg during game start. Does not actually verify of load the model. mapmodelattributes Returns the value of a specific attribute of a mapmodel. If modelpath is a number, it is interpreted as index for the list of models in the current map config. If no attribute name is specified, all attributes are printed to the console instead. getmapmodelattributes Returns a table of all known mapmodels. Each model is listed with the modelpath and the values for the requested attributes. To be used instead of a fixed table in menu_edit.cfg. In addition to attribute names, the function also allows the keywords "explodekeywords" and "sortby:" as arguments. If "explodekeywords" is given, all mapmodels with more than one keyword are listed multiple times, once for each keyword. If an attribute name is preceeded by "sortby:" the list will be sorted by this table column. If "sortby:" is used several times, this first sort will have highest priority. The list is always sorted by path by default. example: getmapmodelattributes sortby: author sortby: desc explodekeywords loadallmapmodels Tries to load mapmodels from /all/ paths below packages/models/mapmodels. Will throw a lot of error messages, because not all directories contain models to load. This command can be used to build or rebuild the complete list of all available mapmodels. It is only necessary, if models are added manually to the directories, since every mapmodel gets added to the list when it is loaded - which takes care of all automatically downloaded models. DOCREF mdlattribute mapmodelregister mapmodelattributes DOCREF getmapmodelattributes loadallmapmodels --- .gitignore | 1 + config/scontext.cfg | 2 +- source/src/command.cpp | 2 +- source/src/main.cpp | 2 + source/src/model.h | 10 +++ source/src/protocol.h | 1 + source/src/protos.h | 2 + source/src/rendermodel.cpp | 175 ++++++++++++++++++++++++++++++++++++- source/src/stream.cpp | 19 ++++ source/src/tools.h | 1 + 10 files changed, 211 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 698101bf..a8f82d93 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ config/init.cfg config/saved.cfg config/servers.cfg config/configtemplates.zip +config/mapmodelattributes.cfg packages/misc/checksums_md5.txt source/codeblocks/AssaultCube.layout source/codeblocks/AssaultCube_Linux.layout diff --git a/config/scontext.cfg b/config/scontext.cfg index 877084b7..5080e1da 100644 --- a/config/scontext.cfg +++ b/config/scontext.cfg @@ -12,7 +12,7 @@ const context_mdlcfg 4 // mdl configs, same // set allowed commands for the map config context mapcfgidents = [loadnotexture loadsky mapmodelreset mapmodel texturereset texture fog fogcolour mapsoundreset mapsound watercolour shadowyaw] -mdlcfgidents = [md2anim md2emit md2tag md3anim md3emit md3link md3load md3skin mdlalphatest mdlalphablend mdlcachelimit mdlcullface mdlscale mdlshadowdist mdltrans mdltranslucent mdlvertexlight] +mdlcfgidents = [md2anim md2emit md2tag md3anim md3emit md3link md3load md3skin mdlalphatest mdlalphablend mdlcachelimit mdlcullface mdlscale mdlshadowdist mdltrans mdltranslucent mdlvertexlight mdlattribute] loop i (listlen $mapcfgidents) [ scriptcontext $context_mapcfg (at $mapcfgidents $i) ] loop i (listlen $mdlcfgidents) [ scriptcontext $context_mdlcfg (at $mdlcfgidents $i) ] diff --git a/source/src/command.cpp b/source/src/command.cpp index b0d0dfb9..8544bd89 100644 --- a/source/src/command.cpp +++ b/source/src/command.cpp @@ -1209,7 +1209,7 @@ char *indexlist(const char *s, int pos) } COMMANDF(at, "si", (char *s, int *pos) { commandret = indexlist(s, *pos); }); -int listlen(char *s) +int listlen(const char *s) { int n = 0; whitespaceskip; diff --git a/source/src/main.cpp b/source/src/main.cpp index cc44025a..2566b1f2 100644 --- a/source/src/main.cpp +++ b/source/src/main.cpp @@ -42,6 +42,7 @@ void quit() // normal exit if(resetcfg) deletecfg(); else writecfg(); savehistory(); + writemapmodelattributes(); writeallxmaps(); cleanup(NULL); popscontext(); @@ -1263,6 +1264,7 @@ int main(int argc, char **argv) exec("config/scontext.cfg"); exec("config/locale.cfg"); exec("config/keymap.cfg"); + execfile("config/mapmodelattributes.cfg"); exec("config/menus.cfg"); exec("config/scripts.cfg"); exec("config/prefabs.cfg"); diff --git a/source/src/model.h b/source/src/model.h index ea840ae3..1db36b94 100644 --- a/source/src/model.h +++ b/source/src/model.h @@ -68,3 +68,13 @@ struct model }; struct mapmodelinfo { int rad, h, zoff; string name; model *m; }; + +enum { MMA_KEYWORDS = 0, MMA_DESC, MMA_DEFAULTPARAMS, MMA_USAGE, MMA_AUTHOR, MMA_LICENSE, MMA_DISTRIBUTION, MMA_VERSION, MMA_NUM }; + +struct mapmodelattributes +{ + string name; + const char *n[MMA_NUM]; + int tmp; + mapmodelattributes() { memset(this, 0, sizeof(*this)); } +}; diff --git a/source/src/protocol.h b/source/src/protocol.h index fa02b093..03128187 100644 --- a/source/src/protocol.h +++ b/source/src/protocol.h @@ -156,6 +156,7 @@ enum #define FTXT__SERVDESC (FTXT_NOWHITE | FTXT_ALLOWBLANKS | FTXT_TABTOBLANK) #define FTXT__MAPMSG (FTXT_NOWHITE | FTXT_ALLOWBLANKS | FTXT_TABTOBLANK) #define FTXT__MAPNAME (FTXT_NOCOLOR | FTXT_MAPNAME | FTXT_TOLOWER) +#define FTXT__MDLATTR (FTXT_NOWHITE | FTXT_ALLOWBLANKS | FTXT_TABTOBLANK | FTXT_NOCOLOR | FTXT_SAFECS) #define FTXT__DEMONAME (FTXT_NOCOLOR | FTXT_MAPNAME) #define FTXT__PLAYERNAME (FTXT_NOWHITE | FTXT_NOCOLOR) #define FTXT__SERVERINFO (FTXT_ALLOWTAB) diff --git a/source/src/protos.h b/source/src/protos.h index 863f3025..b1919d12 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -744,6 +744,7 @@ extern void renderclients(); extern void renderclient(playerent *d); extern void renderclient(playerent *d, const char *mdlname, const char *vwepname, int tex = 0); extern void updateclientname(playerent *d); +extern void writemapmodelattributes(); // weapon extern void shoot(playerent *d, vec &to); @@ -840,6 +841,7 @@ extern void floatret(float v); extern void result(const char *s); extern void exec(const char *cfgfile); extern bool execfile(const char *cfgfile); +extern int listlen(const char *s); extern void resetcomplete(); extern void complete(char *s, bool reversedirection); extern void push(const char *name, const char *action); diff --git a/source/src/rendermodel.cpp b/source/src/rendermodel.cpp index e4899cf1..bfe79b7e 100644 --- a/source/src/rendermodel.cpp +++ b/source/src/rendermodel.cpp @@ -3,6 +3,7 @@ VARP(animationinterpolationtime, 0, 150, 1000); model *loadingmodel = NULL; +mapmodelattributes loadingattributes; #include "tristrip.h" #include "modelcache.h" @@ -86,9 +87,26 @@ void mdlcachelimit(int *limit) COMMAND(mdlcachelimit, "i"); +const char *mdlattrnames[] = { "keywords", "desc", "defaults", "usage", "author", "license", "distribution", "version", "" }; + +void mdlattribute(char *attrname, char *val) +{ + checkmdl; + int i = getlistindex(attrname, mdlattrnames, true, MMA_NUM); + if(i < MMA_NUM) + { + DELSTRING(loadingattributes.n[i]); + filtertext(val, val, FTXT__MDLATTR); + if(*val) loadingattributes.n[i] = newstring(val); + } +} + +COMMAND(mdlattribute, "ss"); + VAR(mapmodelchanged, 0, 0, 1); vector mapmodels; +const char *mmpath = "mapmodels/"; void mapmodel(int *rad, int *h, int *zoff, char *snap, char *name) { @@ -97,7 +115,7 @@ void mapmodel(int *rad, int *h, int *zoff, char *snap, char *name) mmi.h = *h; mmi.zoff = *zoff; mmi.m = NULL; - formatstring(mmi.name)("mapmodels/%s", name); + formatstring(mmi.name)("%s%s", mmpath, name); mapmodelchanged = 1; } @@ -120,6 +138,43 @@ COMMANDF(mapmodelbyname, "s", (char *name) result(res); }); +hashtable mdlregistry; + +void setmodelattributes(const char *name, mapmodelattributes &ma) +{ + if(!strchr(name, ' ') && !strncmp(name, mmpath, strlen(mmpath))) name += strlen(mmpath); // for now: ignore mapmodels with spaces in the path (next release: ban them) + else return; // we only want mapmodels + mapmodelattributes **mrp = mdlregistry.access(name), *mr = mrp ? *mrp : NULL, *r = mr; + if(!r) + { + r = new mapmodelattributes; + copystring(r->name, name); + } + loopi(MMA_NUM) + { + if(ma.n[i]) + { + if(!r->n[i]) mapmodelchanged = 1; + DELSTRING(r->n[i]); // overwrite existing attributes + r->n[i] = ma.n[i]; + ma.n[i] = NULL; + } + } + if(!mr) mdlregistry.access(r->name, r); +} + +void mapmodelregister_(char **args, int numargs) // read model attributes without loading the model +{ + mapmodelattributes ma; + if(numargs > 0) // need a name at least + { + defformatstring(p)("%s%s", mmpath, args[0]); + loopirev(--numargs < MMA_NUM ? numargs : MMA_NUM) ma.n[i] = *args[i + 1] ? newstring(args[i + 1]) : NULL; + setmodelattributes(p, ma); + } +} +COMMANDN(mapmodelregister, mapmodelregister_, "v"); + hashtable mdllookup; hashtable mdlnotfound; @@ -141,6 +196,7 @@ model *loadmodel(const char *name, int i, bool trydl) // load model by name pushscontext(IEXC_MDLCFG); m = new md2(name); // try md2 loadingmodel = m; + loopi(MMA_NUM) DELSTRING(loadingattributes.n[i]); if(!m->load()) // md2 didn't load { delete m; @@ -163,7 +219,11 @@ model *loadmodel(const char *name, int i, bool trydl) // load model by name } } popscontext(); - if(loadingmodel && m) mdllookup.access(m->name(), m); + if(loadingmodel && m) + { + mdllookup.access(m->name(), m); + setmodelattributes(m->name(), loadingattributes); + } loadingmodel = NULL; } if(mapmodels.inrange(i) && !mapmodels[i].m) mapmodels[i].m = m; @@ -175,6 +235,101 @@ void cleanupmodels() enumerate(mdllookup, model *, m, m->cleanup()); } +void mapmodelattributes_(char *name, char *attr) // mostly for debugging purposes... +{ + const char *res = NULL; + mapmodelattributes **ap = mdlregistry.access(name), *a = ap ? *ap : NULL; + if(a) + { + int i = getlistindex(attr, mdlattrnames, true, MMA_NUM); + if(i < MMA_NUM) res = a->n[i]; + else + { + string s = ""; + loopi(MMA_NUM) if(a->n[i]) concatformatstring(s, " %s:\"%s\"", mdlattrnames[i], a->n[i]); + conoutf("%s:%s", a->name, *s ? s : " "); + } + } + result(res ? res : ""); +} +COMMANDN(mapmodelattributes, mapmodelattributes_, "is"); + +static int mmasortorder = 0; +int mmasort(mapmodelattributes **a, mapmodelattributes **b) { return strcmp((*a)->name, (*b)->name); } +int mmasort2(mapmodelattributes **a, mapmodelattributes **b) +{ + const char *aa = (*a)->n[mmasortorder], *bb = (*b)->n[mmasortorder], nn[2] = { 127, 0 }; + int i = strcmp(aa ? aa : nn, bb ? bb : nn); + return i ? i : (*a)->tmp - (*b)->tmp; +} + +void writemapmodelattributes() +{ + stream *f = openfile("config" PATHDIVS "mapmodelattributes.cfg", "w"); + vector mmas; + enumerate(mdlregistry, mapmodelattributes *, m, mmas.add(m)); + mmas.sort(mmasort); + f->printf("// automatically written on exit. this is cached information extracted from model configs. no point in editing it.\n// [path %s]\n", conc(mdlattrnames, MMA_NUM, true)); // memory leak, but w/e + loopv(mmas) + { + f->printf("\nmapmodelregister %s", mmas[i]->name); + loopj(MMA_NUM) f->printf(" %s", escapestring(mmas[i]->n[j])); // escapestring can handle NULL + } + f->printf("\n\n"); + delete f; +} + +void getmapmodelattributes(char **args, int numargs) // create a list of mapmodel paths and selected attributes +{ + // parse argument list + vector opts; + loopi(MMA_NUM) opts.add(mdlattrnames[i]); + opts.add("sortby:"); opts.add("explodekeywords"); opts.add(""); + vector a; + loopi(numargs) a.add(getlistindex(args[i], (const char **)opts.getbuf(), false, opts.length())); + bool explodekeywords = false; + loopi(numargs) if(a[i] == MMA_NUM + 1) explodekeywords = true; + + // build list of mapmodels (explode if necessary) + vector mmas, emmas; + enumerate(mdlregistry, mapmodelattributes *, m, + { + if(explodekeywords && m->n[MMA_KEYWORDS] && listlen(m->n[MMA_KEYWORDS]) > 1) + { + vector keys; + explodelist(m->n[MMA_KEYWORDS], keys); + loopv(keys) + { + mmas.add(new mapmodelattributes(*m)); + emmas.add(mmas.last())->n[MMA_KEYWORDS] = keys[i]; + } + } + else mmas.add(m); + }); + + // sort the list + mmas.sort(mmasort); + loopirev(numargs - 1) if(a[i] == MMA_NUM) // sortby: + { + loopvj(mmas) mmas[j]->tmp = j; // store last sort order, because quicksort is not stable + if((mmasortorder = a[i + 1]) < MMA_NUM) mmas.sort(mmasort2); + } + + // assemble output + vector res; + loopv(mmas) + { + cvecprintf(res, "%s", mmas[i]->name); + loopvj(a) if(a[j] < MMA_NUM) cvecprintf(res, " %s", escapestring(mmas[i]->n[a[j]])); + res.add('\n'); + } + if(res.length()) res.last() = '\0'; + else res.add('\0'); + loopv(emmas) { delstring(emmas[i]->n[MMA_KEYWORDS]); delete emmas[i]; } // only delete what was exploded + result(res.getbuf()); +} +COMMAND(getmapmodelattributes, "v"); + VARP(dynshadow, 0, 40, 100); VARP(dynshadowdecay, 0, 1000, 3000); @@ -772,3 +927,19 @@ void renderclients() loopv(players) if((d = players[i]) && d->state!=CS_SPAWNING && d->state!=CS_SPECTATE && (!player1->isspectating() || player1->spectatemode != SM_FOLLOW1ST || player1->followplayercn != i)) renderclient(d); if(player1->state==CS_DEAD || (reflecting && !refracting)) renderclient(player1); } + +void loadallmapmodels() // try to find all mapmodels in packages/models/mapmodels +{ + vector files; + listfilesrecursive("packages/models/mapmodels", files); + loopvrev(files) if(strchr(files[i], '.')) delstring(files.remove(i)); // poor man's solution to remove ordinary files from the list: remove everything containing '.' + files.sort(stringsort); + loopvrev(files) if(files.inrange(i + 1) && !strcmp(files[i], files[i + 1])) delstring(files.remove(i + 1)); // remove doubles + loopv(files) + { + const char *mn = files[i] + strlen("packages/models/"); + clientlogf("loadallmaps: %s %s", mn, loadmodel(mn) ? "loaded" : "failed to load"); + delstring(files[i]); + } +} +COMMAND(loadallmapmodels, ""); diff --git a/source/src/stream.cpp b/source/src/stream.cpp index cd3c59ca..6996f354 100644 --- a/source/src/stream.cpp +++ b/source/src/stream.cpp @@ -302,6 +302,25 @@ int listfiles(const char *dir, const char *ext, vector &files) return dirs; } +#ifndef STANDALONE +void listfilesrecursive(const char *dir, vector &files, int level) +{ + if(level > 8) return; // 8 levels is insane enough... + vector thisdir; + listfiles(dir, NULL, thisdir); + loopv(thisdir) + { + if(thisdir[i][0] != '.') // ignore "." and ".." (and also other directories starting with '.', like it is unix-convention - and doesn't hurt on windows) + { + defformatstring(name)("%s/%s", dir, thisdir[i]); + files.add(newstring(name)); + listfilesrecursive(name, files, level + 1); + } + delstring(thisdir[i]); + } +} +#endif + bool delfile(const char *path) { return !remove(path); diff --git a/source/src/tools.h b/source/src/tools.h index be825d31..cd1f1455 100644 --- a/source/src/tools.h +++ b/source/src/tools.h @@ -964,6 +964,7 @@ extern char *loadfile(const char *fn, int *size, const char *mode = NULL); extern void filerotate(const char *basename, const char *ext, int keepold, const char *oldformat = NULL); extern bool listdir(const char *dir, const char *ext, vector &files); extern int listfiles(const char *dir, const char *ext, vector &files); +extern void listfilesrecursive(const char *dir, vector &files, int level = 0); extern int listzipfiles(const char *dir, const char *ext, vector &files); extern bool delfile(const char *path); extern void backup(char *name, char *backupname);