From c3d471390407d240f6cd797e76e08ab98b6341ad Mon Sep 17 00:00:00 2001 From: ac-stef Date: Mon, 27 Jan 2014 17:16:53 +0100 Subject: [PATCH] clean up map header handling Older versions of ac did not obey the map header field "headersize". When a map was written, only the "version" field was updated, even if the new written header had a different size. Thanks to bigger headers in map versions 7 & 8 (and now smaller headers again in 9), the headersize of basically all official maps is bogus. This fix makes sure, only proper headersizes are written. It also makes the game tolerate different (bigger) headers - and keeps their content untouched. Future map format extensions can use this to add header information without breaking existing map parsers. This patch establishes a new map format 10, which is identical to format 9, with the exception that the headersize field is guaranteed to be correct. Format 10 also allows any amount of additional header data. This patch also cleans up the transformation routines for old map entities. --- source/src/cube.h | 2 ++ source/src/tools.cpp | 49 +++++++++++++++++++++++++++++++++++---- source/src/tools.h | 2 ++ source/src/world.cpp | 18 ++++++++------- source/src/world.h | 52 +++--------------------------------------- source/src/worldio.cpp | 41 ++++++++++++++++++++++++++------- 6 files changed, 94 insertions(+), 70 deletions(-) diff --git a/source/src/cube.h b/source/src/cube.h index 623081f1..0410e07c 100644 --- a/source/src/cube.h +++ b/source/src/cube.h @@ -34,6 +34,8 @@ extern sqr *world, *wmip[]; // map data, the mips are sequential 2D extern header hdr; // current map header extern int sfactor, ssize; // ssize = 2^sfactor extern int cubicsize, mipsize; // cubicsize = ssize^2 +extern uchar *headerextra; // special feature map data +extern int headerextrasize; extern physent *camera1; // camera representing perspective of player, usually player1 extern playerent *player1; // special client ent that receives input and acts as camera extern vector players; // all the other clients (in multiplayer) diff --git a/source/src/tools.cpp b/source/src/tools.cpp index a65c4628..4ac00657 100644 --- a/source/src/tools.cpp +++ b/source/src/tools.cpp @@ -24,6 +24,41 @@ const char *numtime() return numt; } +static const uchar transformenttab[] = { + /* mapformat 1..5 6..7 */ + /* 0 */ /* NOTUSED */ NOTUSED, /* NOTUSED */ NOTUSED, + /* 1 */ /* LIGHT */ LIGHT, /* LIGHT */ LIGHT, + /* 2 */ /* PLAYERSTART */ PLAYERSTART, /* PLAYERSTART */ PLAYERSTART, + /* 3 */ /* I_SHELLS */ I_AMMO, /* I_CLIPS */ I_CLIPS, + /* 4 */ /* I_BULLETS */ I_AMMO, /* I_AMMO */ I_AMMO, + /* 5 */ /* I_ROCKETS */ I_AMMO, /* I_GRENADE */ I_GRENADE, + /* 6 */ /* I_ROUNDS */ I_AMMO, /* I_HEALTH */ I_HEALTH, + /* 7 */ /* I_HEALTH */ I_HEALTH, /* I_ARMOUR */ I_ARMOUR, + /* 8 */ /* I_BOOST */ I_HEALTH, /* I_AKIMBO */ I_AKIMBO, + /* 9 */ /* I_GREENARMOUR */ I_HELMET, /* MAPMODEL */ MAPMODEL, + /* 10 */ /* I_YELLOWARMOUR */ I_ARMOUR, /* CARROT */ CARROT, + /* 11 */ /* I_QUAD */ I_AKIMBO, /* LADDER */ LADDER, + /* 12 */ /* TELEPORT */ NOTUSED, /* CTF_FLAG */ CTF_FLAG, + /* 13 */ /* TELEDEST */ NOTUSED, /* SOUND */ SOUND, + /* 14 */ /* MAPMODEL */ MAPMODEL, /* CLIP */ CLIP, + /* 15 */ /* MONSTER */ NOTUSED, /* PLCLIP */ PLCLIP, + /* 16 */ /* CARROT */ NOTUSED, 16, + /* 17 */ /* JUMPPAD */ NOTUSED, 17 }; + +void transformoldentities(int mapversion, uchar &enttype) +{ + const uchar *usetab = transformenttab + (mapversion > 5 ? 1 : 0); + if(mapversion < 8 && enttype < 18) enttype = usetab[enttype * 2]; +} + +int fixmapheadersize(int version, int headersize) // we can't trust hdr.headersize for file versions < 10 (thx flow) +{ + if(version < 4) return sizeof(header) - sizeof(int) * 16; + else if(version == 7 || version == 8) return sizeof(header) + sizeof(char) * 128; // mediareq + else if(version < 10 || headersize < int(sizeof(header))) return sizeof(header); + return headersize; +} + int mapdims[8]; // min/max X/Y and delta X/Y and min/max Z extern char *maplayout, *testlayout; @@ -33,6 +68,7 @@ extern int checkarea(int, char *); mapstats *loadmapstats(const char *filename, bool getlayout) { + const int sizeof_header = sizeof(header), sizeof_baseheader = sizeof_header - sizeof(int) * 16; static mapstats s; static uchar *enttypes = NULL; static short *entposs = NULL; @@ -44,11 +80,14 @@ mapstats *loadmapstats(const char *filename, bool getlayout) stream *f = opengzfile(filename, "rb"); if(!f) return NULL; - memset(&s.hdr, 0, sizeof(header)); - if(f->read(&s.hdr, sizeof(header)-sizeof(int)*16)!=sizeof(header)-sizeof(int)*16 || (strncmp(s.hdr.head, "CUBE", 4) && strncmp(s.hdr.head, "ACMP",4))) { delete f; return NULL; } + memset(&s.hdr, 0, sizeof_header); + if(f->read(&s.hdr, sizeof_baseheader) != sizeof_baseheader || (strncmp(s.hdr.head, "CUBE", 4) && strncmp(s.hdr.head, "ACMP",4))) { delete f; return NULL; } lilswap(&s.hdr.version, 4); - if(s.hdr.version>MAPVERSION || s.hdr.numents > MAXENTITIES || (s.hdr.version>=4 && f->read(&s.hdr.waterlevel, sizeof(int)*16)!=sizeof(int)*16)) { delete f; return NULL; } - if((s.hdr.version==7 || s.hdr.version==8) && !f->seek(sizeof(char)*128, SEEK_CUR)) { delete f; return NULL; } + s.hdr.headersize = fixmapheadersize(s.hdr.version, s.hdr.headersize); + int restofhead = min(s.hdr.headersize, sizeof_header) - sizeof_baseheader; + if(s.hdr.version > MAXMAPVERSION || s.hdr.numents > MAXENTITIES || + f->read(&s.hdr.waterlevel, restofhead) != restofhead || + !f->seek(clamp(s.hdr.headersize - sizeof_header, 0, MAXHEADEREXTRA), SEEK_CUR)) { delete f; return NULL; } if(s.hdr.version>=4) { lilswap(&s.hdr.waterlevel, 1); @@ -62,7 +101,7 @@ mapstats *loadmapstats(const char *filename, bool getlayout) { f->read(&e, sizeof(persistent_entity)); lilswap((short *)&e, 4); - TRANSFORMOLDENTITIES(s.hdr) + transformoldentities(s.hdr.version, e.type); if(e.type == PLAYERSTART && (e.attr2 == 0 || e.attr2 == 1 || e.attr2 == 100)) s.spawns[e.attr2 == 100 ? 2 : e.attr2]++; if(e.type == CTF_FLAG && (e.attr2 == 0 || e.attr2 == 1)) { s.flags[e.attr2]++; s.flagents[e.attr2] = i; } s.entcnt[e.type]++; diff --git a/source/src/tools.h b/source/src/tools.h index 4204a308..3074aed3 100644 --- a/source/src/tools.h +++ b/source/src/tools.h @@ -907,6 +907,8 @@ struct stream extern const char *timestring(bool local = false, const char *fmt = NULL); extern const char *asctime(); extern const char *numtime(); +extern void transformoldentities(int mapversion, uchar &enttype); +extern int fixmapheadersize(int version, int headersize); extern char *path(char *s); extern char *path(const char *s, bool copy); extern char *unixpath(char *s); diff --git a/source/src/world.cpp b/source/src/world.cpp index 47d5f705..42335820 100644 --- a/source/src/world.cpp +++ b/source/src/world.cpp @@ -8,6 +8,8 @@ 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, @@ -481,10 +483,6 @@ bool empty_world(int factor, bool force) // main empty world creation routine checkselections(); // assert no selection became invalid - strncpy(hdr.head, "ACMP", 4); - hdr.version = MAPVERSION; - hdr.headersize = sizeof(header); - hdr.sfactor = sfactor; if(copy) { ow->x = ow->y = shrink ? 0 : ssize/4; @@ -501,17 +499,21 @@ bool empty_world(int factor, bool force) // main empty world creation routine entinmap(player1); } else - { - copystring(hdr.maptitle, "Untitled Map by Unknown", 128); + { // all-new map + memset(&hdr, 0, sizeof(header)); + formatstring(hdr.maptitle)("Untitled Map by %s", player1->name); hdr.waterlevel = -100000; - hdr.ambient = 0; setwatercolor(); - loopi(sizeof(hdr.reserved)/sizeof(hdr.reserved[0])) hdr.reserved[i] = 0; loopk(3) loopi(256) hdr.texlists[k][i] = i; ents.shrink(0); block b = { 8, 8, ssize-16, ssize - 16}; edittypexy(SPACE, b); + DELETEA(headerextra); } + strncpy(hdr.head, "ACMP", 4); + hdr.version = MAPVERSION; + hdr.headersize = sizeof(header); + hdr.sfactor = sfactor; calclight(); resetmap(!copy); diff --git a/source/src/world.h b/source/src/world.h index 41e819ac..361cc73e 100644 --- a/source/src/world.h +++ b/source/src/world.h @@ -32,7 +32,8 @@ enum // hardcoded texture numbers DEFAULT_CEIL }; -#define MAPVERSION 9 // bump if map format changes, see worldio.cpp +#define MAXMAPVERSION 10 // defines the highest readable format +#define MAPVERSION 9 // default map format version to be written (bump if map format changes, see worldio.cpp) struct header // map file format header { @@ -66,54 +67,6 @@ struct mapstats bool hasflags; }; -#define TRANSFORMOLDENTITIES(headr) \ - if(e.type==LIGHT) \ - { \ - if(!e.attr2) e.attr2 = 255; /* needed for MAPVERSION<=2 */ \ - if(e.attr1>32) e.attr1 = 32; /* 12_03 and below */ \ - } \ - if(headr.version<6 && strncmp(headr.head,"CUBE",4)==0) /* only render lights, pl starts and map models on old maps // <6 was =6 && headr.version<8) { \ - if( e.type >= I_HELMET && e.type < (MAXENTTYPES - 1) ) { e.type += 1; } \ - } - #define SWS(w,x,y,s) (&(w)[((y)<<(s))+(x)]) #define SW(w,x,y) SWS(w,x,y,sfactor) #define S(x,y) SW(world,x,y) // convenient lookup of a lowest mip cube @@ -121,6 +74,7 @@ struct mapstats #define DEFAULT_FACTOR 8 #define LARGEST_FACTOR 11 // 10 is already insane #define MAXENTITIES 65535 +#define MAXHEADEREXTRA (1<<20) #define SOLID(x) ((x)->type==SOLID) #define MINBORD 2 // 2 cubes from the edge of the world are always solid #define OUTBORD(x,y) ((x)=ssize-MINBORD || (y)>=ssize-MINBORD) diff --git a/source/src/worldio.cpp b/source/src/worldio.cpp index d812d6d2..cc20f1b3 100644 --- a/source/src/worldio.cpp +++ b/source/src/worldio.cpp @@ -218,6 +218,7 @@ void save_world(char *mname) if(!f) { conoutf("could not write map to %s", cgzname); return; } strncpy(hdr.head, "ACMP", 4); // ensure map now declares itself as an AssaultCube map, even if imported as CUBE hdr.version = MAPVERSION; + hdr.headersize = sizeof(header); hdr.numents = 0; loopv(ents) if(ents[i].type!=NOTUSED) hdr.numents++; if(hdr.numents > MAXENTITIES) @@ -226,11 +227,15 @@ void save_world(char *mname) hdr.numents = MAXENTITIES; } header tmp = hdr; + 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 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); int ne = hdr.numents; loopv(ents) { @@ -315,6 +320,7 @@ extern float Mh; bool load_world(char *mname) // still supports all map formats that have existed since the earliest cube betas! { + const int sizeof_header = sizeof(header), sizeof_baseheader = sizeof_header - sizeof(int) * 16; stopwatch watch; watch.start(); @@ -335,14 +341,28 @@ 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; } header tmp; - memset(&tmp, 0, sizeof(header)); - if(f->read(&tmp, sizeof(header)-sizeof(int)*16)!=sizeof(header)-sizeof(int)*16) { conoutf("\f3while reading map: header malformatted"); delete f; return false; } - lilswap(&tmp.version, 4); - if(strncmp(tmp.head, "CUBE", 4)!=0 && strncmp(tmp.head, "ACMP",4)!=0) { conoutf("\f3while reading map: header malformatted"); delete f; return false; } - if(tmp.version>MAPVERSION) { conoutf("\f3this map requires a newer version of AssaultCube"); delete f; return false; } + memset(&tmp, 0, sizeof_header); + if(f->read(&tmp, sizeof_baseheader) != sizeof_baseheader || + (strncmp(tmp.head, "CUBE", 4)!=0 && strncmp(tmp.head, "ACMP",4)!=0)) { conoutf("\f3while reading map: header malformatted (1)"); delete f; return false; } + lilswap(&tmp.version, 4); // version, headersize, sfactor, numents + 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; } - if(tmp.version>=4 && f->read(&tmp.waterlevel, sizeof(int)*16)!=sizeof(int)*16) { conoutf("\f3while reading map: header malformatted"); delete f; return false; } - if((tmp.version==7 || tmp.version==8) && !f->seek(sizeof(char)*128, SEEK_CUR)) { conoutf("\f3while reading map: header malformatted"); 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; } + 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) + { // 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; } + } + f->seek(tmp.headersize - sizeof_header - headerextrasize, SEEK_CUR); // (equivalent to SEEK_SET to tmp.headersize) + } hdr = tmp; loadingscreen("%s", hdr.maptitle); resetmap(); @@ -367,7 +387,12 @@ bool load_world(char *mname) // still supports all map formats that have f->read(&e, sizeof(persistent_entity)); lilswap((short *)&e, 4); e.spawned = false; - TRANSFORMOLDENTITIES(hdr) + if(e.type == LIGHT) + { + if(!e.attr2) e.attr2 = 255; // needed for MAPVERSION<=2 + if(e.attr1>32) e.attr1 = 32; // 12_03 and below + } + transformoldentities(hdr.version, e.type); if(e.type == PLAYERSTART && (e.attr2 == 0 || e.attr2 == 1 || e.attr2 == 100)) { if(e.attr2 == 100)