From d3bda71212252cab8f0f1cb43d7173881564f64a Mon Sep 17 00:00:00 2001 From: ac-stef Date: Thu, 1 May 2014 11:41:22 +0200 Subject: [PATCH] map file addons: embedded config file; persistent undo/redo data This commit establishes an extensible way of storing additional data in map file headers. Those header extras can be permanent, like the embedded config file or be used only once after map load like the undo/redo data. Additional extra data can be easily added and will not break backward compatibility, so this map format should hold up for quite some time. To make things easier for mappers, maps can now be saved without invoking the various optimisations. That, in combination with the stored undo data, means, that editing a map can be picked up at exactly the same state, as the map was saved in. Maps will automatically be written in the appropriate fashion, depending on the situation: 1) for sendmap: no optimisation, no undo data 2) savemap during editing: no optimisation, with undo data 3) savemap when not editing: optimised, no undo data 4) savemapoptimised: optimised, no undo data (For savemap to save different maps during editing, the variable preserveundosonsave has to be "1". It is "0" by default.) All of the above map files can be played without problems - they only differ in file size. (However, maps with undo data may be too big to send) The new command "embedconfigfile" will load the config file, include it in the map header and rename the original text file. "extractconfigfile" will write an embedded config file to a separate file and remove it from the map header. Embedded config files take precedence over regular config files and therefore make it harder for cheaters to play with modified config files. further changes: cleaned up some debugging code; more ASSERTs --- source/src/client.cpp | 2 +- source/src/clients2c.cpp | 2 +- source/src/cube.h | 2 - source/src/editing.cpp | 81 ++++++++++++++++ source/src/protos.h | 7 +- source/src/server.cpp | 7 +- source/src/tools.h | 10 +- source/src/world.cpp | 4 +- source/src/worldio.cpp | 205 ++++++++++++++++++++++++++++++++++----- 9 files changed, 281 insertions(+), 39 deletions(-) diff --git a/source/src/client.cpp b/source/src/client.cpp index 816244ff..af5267d4 100644 --- a/source/src/client.cpp +++ b/source/src/client.cpp @@ -752,7 +752,7 @@ void sendmap(char *mapname) { if(!*mapname) mapname = getclientmap(); if(securemapcheck(mapname)) return; - if(gamemode == GMODE_COOPEDIT && !strcmp(getclientmap(), mapname)) save_world(mapname); + if(gamemode == GMODE_COOPEDIT && !strcmp(getclientmap(), mapname)) save_world(mapname, true, false); // skip optimisations, don't add undos int mapsize, cfgsize, cfgsizegz, revision; uchar *mapdata = readmap(path(mapname), &mapsize, &revision); diff --git a/source/src/clients2c.cpp b/source/src/clients2c.cpp index 94f06a48..7b0f6d73 100644 --- a/source/src/clients2c.cpp +++ b/source/src/clients2c.cpp @@ -410,7 +410,7 @@ void parsemessages(int cn, playerent *d, ucharbuf &p, bool demo = false) DEBUGVAR(d); ASSERT(type>=0 && type players; // all the other clients (in multiplayer) diff --git a/source/src/editing.cpp b/source/src/editing.cpp index 73a92c1b..6ca9c102 100644 --- a/source/src/editing.cpp +++ b/source/src/editing.cpp @@ -363,6 +363,87 @@ void editredo() freeblock(p); } +extern int worldiodebug; + +void restoreeditundo(ucharbuf &q) +{ + int type, len, explen; + while(!q.overread() && (type = getuint(q))) + { + int bx = getuint(q), by = getuint(q), bxs = getuint(q), bys = getuint(q); + short pp[5]; + loopi(5) pp[i] = getint(q); + len = getuint(q); + if((bx | by | (bx + bxs) | (by + bys)) & ~(ssize - 1)) continue; + explen = bxs * bys; + block *b = (block *)new uchar[sizeof(block) + explen * sizeof(sqr)]; + b->x = bx; b->y = by; b->xs = bxs; b->ys = bys; + loopi(5) b->p[i] = pp[i]; + ucharbuf p = q.subbuf(len); + rldecodecubes(p, (sqr *)(b+1), explen, 6, true); + switch(type) + { + case 10: undos.insert(0, b); break; + case 20: redos.insert(0, b); break; + default: freeblock(b); break; + } + #ifdef _DEBUG + if(worldiodebug) switch(type) + { + case 10: + case 20: + clientlogf(" got %sdo x %d, y %d, xs %d, ys %d, remaining %d, overread %d", type == 10 ? "un" : "re", b->x, b->y, b->xs, b->ys, q.remaining(), int(q.overread())); + break; + } + #endif + } + if(undos.length() || redos.length()) conoutf("restored editing history: %d undos and %d redos", undos.length(), redos.length()); +} + +int rlencodeundo(int type, vector &t, block *s) +{ + putuint(t, type); + putuint(t, s->x); + putuint(t, s->y); + putuint(t, s->xs); + putuint(t, s->ys); + loopi(5) putint(t, s->p[i]); + vector tmp; + rlencodecubes(tmp, (sqr *)(s+1), s->xs * s->ys, true); + putuint(t, tmp.length()); + t.put(tmp.getbuf(), tmp.length()); + #ifdef _DEBUG + if(worldiodebug) clientlogf(" compressing redo/undo x %d, y %d, xs %d, ys %d, compressed length %d, cubes %d", s->x, s->y, s->xs, s->ys, tmp.length(), s->xs * s->ys); + #endif + return t.length(); +} + +void backupeditundo(vector &buf, int undolimit, int redolimit) +{ + vector tmp; + loopvrev(undos) + { + tmp.setsize(0); + undolimit -= rlencodeundo(10, tmp, undos[i]); + if(undolimit < 0) break; + buf.put(tmp.getbuf(), tmp.length()); + #ifdef _DEBUG + if(worldiodebug) clientlogf(" written undo x %d, y %d, xs %d, ys %d, compressed length %d", undos[i]->x, undos[i]->y, undos[i]->xs, undos[i]->ys, tmp.length()); + #endif + } + loopvrev(redos) + { + tmp.setsize(0); + redolimit -= rlencodeundo(20, tmp, redos[i]); + if(redolimit < 0) break; + buf.put(tmp.getbuf(), tmp.length()); + #ifdef _DEBUG + if(worldiodebug) clientlogf(" written redo x %d, y %d, xs %d, ys %d, compressed length %d", redos[i]->x, redos[i]->y, redos[i]->xs, redos[i]->ys, tmp.length()); + #endif + } + putuint(buf, 0); +} + vector copybuffers; void resetcopybuffers() diff --git a/source/src/protos.h b/source/src/protos.h index dc3434e4..547dbed2 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -591,6 +591,8 @@ extern void editheightxy(bool isfloor, int amount, block &sel); //extern bool noteditmode(); extern bool noteditmode(const char* func = NULL); extern void pruneundos(int maxremain = 0); +extern void restoreeditundo(ucharbuf &q); +extern void backupeditundo(vector &buf, int undolimit, int redolimit); // renderhud enum @@ -669,12 +671,15 @@ extern void render_particles(int time, int typemask = ~0); // worldio extern int mapdims[8]; extern const char *setnames(const char *name); -extern void save_world(char *fname); +extern void save_world(char *mname, bool skipoptimise = false, bool addcomfort = false); extern bool load_world(char *mname); extern void writemap(char *name, int size, uchar *data); extern void writecfggz(char *name, int size, int sizegz, uchar *data); extern uchar *readmap(char *name, int *size, int *revision); extern uchar *readmcfggz(char *name, int *size, int *sizegz); +extern void rlencodecubes(vector &f, sqr *s, int len, bool preservesolids); +extern void rldecodecubes(ucharbuf &f, sqr *s, int len, int version, bool silent); +extern void clearheaderextras(); // physics extern float raycube(const vec &o, const vec &ray, vec &surface); diff --git a/source/src/server.cpp b/source/src/server.cpp index 1893dcbc..499b9d8c 100644 --- a/source/src/server.cpp +++ b/source/src/server.cpp @@ -3,7 +3,12 @@ #include "cube.h" +#ifdef STANDALONE #define DEBUGCOND (true) +#else +VARP(serverdebug, 0, 0, 1); +#define DEBUGCOND (serverdebug==1) +#endif #include "server.h" #include "servercontroller.h" @@ -2795,7 +2800,7 @@ void process(ENetPacket *packet, int sender, int chan) DEBUGVAR(cl->name); ASSERT(type>=0 && type struct vector bool inrange(size_t i) const { return i=0 && i 0); return buf[--ulen]; } + T &last() { ASSERT(ulen > 0); return buf[ulen-1]; } + void drop() { ASSERT(ulen > 0); buf[--ulen].~T(); } bool empty() const { return ulen==0; } int capacity() const { return alen; } @@ -450,8 +450,8 @@ template struct vector T &operator[](int i) { ASSERT(i>=0 && i= 0 && ii) drop(); } - void setsize(int i) { ASSERT(i<=ulen); ulen = i; } + void shrink(int i) { ASSERT(i>=0 && i<=ulen); while(ulen>i) drop(); } + void setsize(int i) { ASSERT(i>=0 && i<=ulen); ulen = i; } void deletecontents() { while(!empty()) delete pop(); } void deletearrays() { while(!empty()) delete[] pop(); } diff --git a/source/src/world.cpp b/source/src/world.cpp index eb2764d0..d7ede346 100644 --- a/source/src/world.cpp +++ b/source/src/world.cpp @@ -8,8 +8,6 @@ sqr *world = NULL; int sfactor, ssize, cubicsize, mipsize; header hdr; -uchar *headerextra = NULL; -int headerextrasize; // main geometric mipmapping routine, recursively rebuild mipmaps within block b. // tries to produce cube out of 4 lower level mips as well as possible, @@ -501,7 +499,7 @@ bool empty_world(int factor, bool force) // main empty world creation routine ents.shrink(0); block b = { 8, 8, ssize-16, ssize - 16}; edittypexy(SPACE, b); - DELETEA(headerextra); + clearheaderextras(); } strncpy(hdr.head, "ACMP", 4); hdr.version = MAPVERSION; diff --git a/source/src/worldio.cpp b/source/src/worldio.cpp index d5c30b60..e5f93a35 100644 --- a/source/src/worldio.cpp +++ b/source/src/worldio.cpp @@ -2,6 +2,9 @@ #include "cube.h" +#define DEBUGCOND (worldiodebug==1) +VARP(worldiodebug, 0, 0, 1); + void backup(char *name, char *backupname) { string backupfile; @@ -10,7 +13,7 @@ void backup(char *name, char *backupname) rename(findfile(name, "wb"), backupfile); } -static string cgzname, ocgzname, bakname, pcfname, mcfname, omcfname, mapname; +static string cgzname, ocgzname, bakname, cbakname, pcfname, mcfname, omcfname, mapname; const char *setnames(const char *name) { @@ -29,12 +32,14 @@ const char *setnames(const char *name) formatstring(cgzname)("packages/%s/%s.cgz", pakname, mapname); formatstring(ocgzname)("packages/maps/official/%s.cgz", mapname); formatstring(bakname)("packages/%s/%s_%s.BAK", pakname, mapname, numtime()); + formatstring(cbakname)("packages/%s/%s.cfg_%s.BAK", pakname, mapname, numtime()); formatstring(pcfname)("packages/%s/package.cfg", pakname); formatstring(mcfname)("packages/%s/%s.cfg", pakname, mapname); formatstring(omcfname)("packages/maps/official/%s.cfg", mapname); path(cgzname); path(bakname); + path(cbakname); return cgzname; } @@ -220,15 +225,11 @@ void rldecodecubes(ucharbuf &f, sqr *s, int len, int version, bool silent) // ru } case SOLID: { + sqrdefault(s); // takes care of ftex, ctex, floor, ceil and tag s->type = SOLID; s->utex = s->wtex = f.get(); s->vdelta = f.get(); if(version<=2) { f.get(); f.get(); } - s->ftex = DEFAULT_FLOOR; - s->ctex = DEFAULT_CEIL; - s->floor = 0; - s->ceil = 16; - s->tag = 0; break; } case 253: // SOLID with all textures during editing (undo) @@ -241,6 +242,7 @@ void rldecodecubes(ucharbuf &f, sqr *s, int len, int version, bool silent) // ru f.overread(); continue; } + sqrdefault(s); s->type = type; s->floor = f.get(); s->ceil = f.get(); @@ -260,21 +262,165 @@ void rldecodecubes(ucharbuf &f, sqr *s, int len, int version, bool silent) // ru } } +// headerextra stores additional data in a map file (support since format 10) +// data can be persistent or oneway +// the format and handling is explicitly designed to handle yet unknown header types to avoid further format version bumps + +struct headerextra +{ + int len, flags; + uchar *data; + headerextra() : len(0), flags(0), data(NULL) {} + headerextra(int l, int f, uchar *d) : len(l), flags(f), data(NULL) { if(d) { data = new uchar[len]; memcpy(data, d, len); } } + ~headerextra() { DELETEA(data); } +}; +vector headerextras; + +enum { HX_UNUSED = 0, HX_MAPINFO, HX_MODEINFO, HX_ARTIST, HX_EDITUNDO, HX_CONFIG, HX_NUM, HX_TYPEMASK = 0x3f, HX_FLAG_PERSIST = 0x40 }; +const char *hx_names[] = { "unused", "mapinfo", "modeinfo", "artist", "editundo", "config", "unknown" }; +#define addhxpacket(p, len, flags, buffer) { if(p.length() + len < MAXHEADEREXTRA) { putuint(p, len); putuint(p, flags); p.put(buffer, len); } } +#define hx_name(t) hx_names[min((t) & HX_TYPEMASK, int(HX_NUM))] + +void clearheaderextras() { loopvrev(headerextras) delete headerextras.remove(i); } + +void deleteheaderextra(int n) { if(headerextras.inrange(n)) delete headerextras.remove(n); } + +COMMANDF(listheaderextras, "", () +{ + loopv(headerextras) conoutf("extra header record %d: %s, %d bytes", i, hx_name(headerextras[i]->flags), headerextras[i]->len); + if(!headerextras.length()) conoutf("no extra header records found"); +}); + +int findheaderextra(int type) +{ + loopv(headerextras) if((headerextras[i]->flags & HX_TYPEMASK) == type) return i; + return -1; +} + +void unpackheaderextra(uchar *buf, int len) // break the extra data from the mapheader into its pieces +{ + ucharbuf p(buf, len); + DEBUG("unpacking " << len << " bytes"); + while(1) + { + int len = getuint(p), flags = getuint(p), type = flags & HX_TYPEMASK; + if(p.overread() || len > p.remaining()) break; + clientlogf(" found headerextra \"%s\", %d bytes%s", hx_name(type), len, flags & HX_FLAG_PERSIST ? "persistent" : ""); // debug info + headerextras.add(new headerextra(len, flags, p.subbuf(len).buf)); + } +} + +void parseheaderextra(bool clearnonpersist = true, int ignoretypes = 0) // parse all headerextra packets, delete the nonpersistent ones (like editundos) +{ + DEBUG("parsing " << headerextras.length() << " packets"); + loopv(headerextras) + { + ucharbuf q(headerextras[i]->data, headerextras[i]->len); + int type = headerextras[i]->flags & HX_TYPEMASK; + DEBUG("packet " << i << " type " << type); + bool deletethis = false; // (set deletethis for headers that persist outside headerextras and get reinserted by packheaderextras()) + if(!(ignoretypes & (1 << type))) switch(type) + { + case HX_EDITUNDO: + restoreeditundo(q); + break; + + case HX_CONFIG: + setcontext("map", "embedded"); + execute((const char *)q.buf); + resetcontext(); + break; + + case HX_MAPINFO: + case HX_MODEINFO: + case HX_ARTIST: + default: + break; + } + if(deletethis || (clearnonpersist && !(headerextras[i]->flags & HX_FLAG_PERSIST))) delete headerextras.remove(i--); + } +} + +ucharbuf packheaderextras(int ignoretypes = 0) // serialise all extra data packets to save with the map header +{ + vector buf, tmp; + loopv(headerextras) if(!(ignoretypes & (1 << (headerextras[i]->flags & HX_TYPEMASK)))) + { // copy existing persistent hx packets + addhxpacket(buf, headerextras[i]->len, headerextras[i]->flags, headerextras[i]->data); + } + // create non-persistent ones, if wanted + if(!(ignoretypes & (1 << HX_EDITUNDO))) + { + int limit = (MAXHEADEREXTRA - buf.length()) / 3; + backupeditundo(tmp, limit, limit); + addhxpacket(buf, tmp.length(), HX_EDITUNDO, tmp.getbuf()); + } + ucharbuf q(new uchar[buf.length()], buf.length()); + memcpy(q.buf, buf.getbuf(), q.maxlen); + DEBUG("packed " << q.maxlen << " bytes"); + return q; +} + +bool embedconfigfile() +{ + if(securemapcheck(getclientmap())) return false; + setnames(getclientmap()); + int len; + uchar *buf = (uchar *)loadfile(path(mcfname), &len); + if(buf) + { + for(int n; (n = findheaderextra(HX_CONFIG)) >= 0; ) deleteheaderextra(n); + headerextras.add(new headerextra(len + 1, HX_CONFIG|HX_FLAG_PERSIST, NULL))->data = buf; + backup(mcfname, cbakname); // don't delete the config file, just rename + conoutf("embedded map config file \"%s\"", mcfname); + } + else conoutf("\f3failed to load config file %s", mcfname); + return buf != NULL; +} +COMMANDF(embedconfigfile, "", () { if(embedconfigfile()) save_world(getclientmap()); }); + +void extractconfigfile() +{ + if(securemapcheck(getclientmap())) return; + setnames(getclientmap()); + int n = findheaderextra(HX_CONFIG); + if(n < 0) conoutf("no embedded config found"); + else + { + backup(mcfname, cbakname); // don't overwrite an existing config file, just rename it + stream *f = openfile(mcfname, "w"); + if(f) + { + f->write(headerextras[n]->data, headerextras[n]->len); + delete f; + conoutf("extracted embedded map config to file %s", mcfname); + deleteheaderextra(n); + } + else conoutf("\f3failed to write config to %s", mcfname); + } +} +COMMAND(extractconfigfile, ""); + VAR(advancemaprevision, 1, 1, 100); VARP(mapbackupsonsave, 0, 1, 1); -void save_world(char *mname) +void save_world(char *mname, bool skipoptimise, bool addcomfort) { if(!*mname) mname = getclientmap(); if(securemapcheck(mname)) return; + DEBUG("writing map \"" << mname << "\""); if(!validmapname(mname)) { conoutf("\f3Invalid map name. It must only contain letters, digits, '-', '_' and be less than %d characters long", MAXMAPNAMELEN); return; } - voptimize(); - mapmrproper(false); + if(!skipoptimise) + { + voptimize(); + mapmrproper(false); + addcomfort = false; // "optimized + undos" is not useful + } setnames(mname); if(mapbackupsonsave) backup(cgzname, bakname); stream *f = opengzfile(cgzname, "wb"); @@ -290,15 +436,18 @@ void save_world(char *mname) hdr.numents = MAXENTITIES; } header tmp = hdr; + ucharbuf hx = packheaderextras(addcomfort ? 0 : (1 << HX_EDITUNDO)); // if addcomfort -> add undos/redos int writeextra = 0; - if(headerextra) tmp.headersize += writeextra = clamp(headerextrasize, 0, MAXHEADEREXTRA); - if(writeextra) tmp.version = 10; // 9 and 10 are the same, but in 10 the headersize is reliable - if we don't need it, stick to 9 + if(hx.maxlen) tmp.headersize += writeextra = clamp(hx.maxlen, 0, MAXHEADEREXTRA); + if(writeextra || skipoptimise) tmp.version = 10; // 9 and 10 are the same, but in 10 the headersize is reliable - if we don't need it, stick to 9 + tmp.maprevision += advancemaprevision; + DEBUG("version " << tmp.version << " headersize " << tmp.headersize << " entities " << tmp.numents << " factor " << tmp.sfactor << " revision " << tmp.maprevision); lilswap(&tmp.version, 4); lilswap(&tmp.waterlevel, 1); - tmp.maprevision += advancemaprevision; lilswap(&tmp.maprevision, 2); f->write(&tmp, sizeof(header)); - if(writeextra) f->write(headerextra, writeextra); + if(writeextra) f->write(hx.buf, writeextra); + delete[] hx.buf; int ne = hdr.numents; loopv(ents) { @@ -312,11 +461,14 @@ void save_world(char *mname) } vector rawcubes; - rlencodecubes(rawcubes, world, cubicsize, false); + rlencodecubes(rawcubes, world, cubicsize, skipoptimise); // if skipoptimize -> keep properties of solid cubes (forces format 10) f->write(rawcubes.getbuf(), rawcubes.length()); delete f; - conoutf("wrote map file %s", cgzname); + conoutf("wrote map file %s %s%s", cgzname, skipoptimise ? "without optimisation" : "(optimised)", addcomfort ? " (including editing history)" : ""); } +VARP(preserveundosonsave, 0, 0, 1); +COMMANDF(savemap, "s", (char *name) { save_world(name, preserveundosonsave && editmode, preserveundosonsave && editmode); } ); +COMMANDF(savemapoptimised, "s", (char *name) { save_world(name, false, false); } ); extern int mapdims[8]; // min/max X/Y and delta X/Y and min-floor/max-ceil void showmapdims() @@ -376,6 +528,7 @@ bool load_world(char *mname) // still supports all map formats that have } stream *f = opengzfile(cgzname, "rb"); if(!f) { conoutf("\f3could not read map %s", cgzname); return false; } + DEBUG("reading map \"" << cgzname << "\""); header tmp; memset(&tmp, 0, sizeof_header); if(f->read(&tmp, sizeof_baseheader) != sizeof_baseheader || @@ -384,20 +537,23 @@ bool load_world(char *mname) // still supports all map formats that have if(tmp.version > MAXMAPVERSION) { conoutf("\f3this map requires a newer version of AssaultCube"); delete f; return false; } if(tmp.sfactorLARGEST_FACTOR || tmp.numents > MAXENTITIES) { conoutf("\f3illegal map size"); delete f; return false; } tmp.headersize = fixmapheadersize(tmp.version, tmp.headersize); - DELETEA(headerextra); int restofhead = min(tmp.headersize, sizeof_header) - sizeof_baseheader; if(f->read(&tmp.waterlevel, restofhead) != restofhead) { conoutf("\f3while reading map: header malformatted (2)"); delete f; return false; } + DEBUG("version " << tmp.version << " headersize " << tmp.headersize << " headerextrasize " << tmp.headersize - int(sizeof(header)) << " entities " << tmp.numents << " factor " << tmp.sfactor << " revision " << tmp.maprevision); + clearheaderextras(); if(tmp.headersize > sizeof_header) { - headerextrasize = tmp.headersize - sizeof_header; - if(tmp.version < 9) headerextrasize = 0; // throw away mediareq... - else if(headerextrasize > MAXHEADEREXTRA) headerextrasize = MAXHEADEREXTRA; - if(headerextrasize) + int extrasize = tmp.headersize - sizeof_header; + if(tmp.version < 9) extrasize = 0; // throw away mediareq... + else if(extrasize > MAXHEADEREXTRA) extrasize = MAXHEADEREXTRA; + if(extrasize) { // map file actually has extra header data that we want too preserve - headerextra = new uchar[headerextrasize]; - if(f->read(headerextra, headerextrasize) != headerextrasize) { conoutf("\f3while reading map: header malformatted (3)"); delete f; return false; } + uchar *extrabuf = new uchar[extrasize]; + if(f->read(extrabuf, extrasize) != extrasize) { conoutf("\f3while reading map: header malformatted (3)"); delete f; return false; } + unpackheaderextra(extrabuf, extrasize); + delete[] extrabuf; } - f->seek(tmp.headersize - sizeof_header - headerextrasize, SEEK_CUR); // (equivalent to SEEK_SET to tmp.headersize) + f->seek(tmp.headersize, SEEK_SET); } hdr = tmp; rebuildtexlists(); @@ -504,6 +660,7 @@ bool load_world(char *mname) // still supports all map formats that have execfile("config/default_map_settings.cfg"); execfile(pcfname); execfile(mcfname); + parseheaderextra(); neverpersist = false; per_idents = true; popscontext(); @@ -554,5 +711,3 @@ bool load_world(char *mname) // still supports all map formats that have startmap(mname); return true; } - -COMMANDN(savemap, save_world, "s");