// worldio.cpp: loading & saving of maps and savegames #include "cube.h" #define DEBUGCOND (worldiodebug==1) VARP(worldiodebug, 0, 0, 1); static string cgzname, ocgzname, bakname, cbakname, mcfname, omcfname; const char *setnames(const char *name) { const char *mapname = behindpath(name), *nt = numtime(); formatstring(cgzname) ("packages" PATHDIVS "maps" PATHDIVS "%s.cgz", mapname); formatstring(mcfname) ("packages" PATHDIVS "maps" PATHDIVS "%s.cfg", mapname); formatstring(ocgzname)("packages" PATHDIVS "maps" PATHDIVS "official" PATHDIVS "%s.cgz", mapname); formatstring(omcfname)("packages" PATHDIVS "maps" PATHDIVS "official" PATHDIVS "%s.cfg", mapname); formatstring(bakname) ("packages" PATHDIVS "maps" PATHDIVS "%s_%s.BAK", mapname, nt); formatstring(cbakname)("packages" PATHDIVS "maps" PATHDIVS "%s.cfg_%s.BAK", mapname, nt); return cgzname; } // the optimize routines below are here to reduce the detrimental effects of messy mapping by // setting certain properties (vdeltas and textures) to neighbouring values wherever there is no // visible difference. This allows the mipmapper to generate more efficient mips. // the reason it is done on save is to reduce the amount spend in the mipmapper (as that is done // in realtime). inline bool nhf(sqr *s) { return s->type!=FHF && s->type!=CHF; } void voptimize() // reset vdeltas on non-hf cubes { loop(y, ssize) loop(x, ssize) { sqr *s = S(x, y); if(x && y) { if(nhf(s) && nhf(S(x-1, y)) && nhf(S(x-1, y-1)) && nhf(S(x, y-1))) s->vdelta = 0; } else s->vdelta = 0; } } // these two are used by getmap/sendmap.. transfers compressed maps directly void writemap(char *name, int msize, uchar *mdata) { setnames(name); backup(cgzname, bakname); stream *f = openfile(cgzname, "wb"); if(!f) { conoutf("\f3could not write map to %s", cgzname); return; } f->write(mdata, msize); delete f; conoutf("wrote map %s as file %s", name, cgzname); } uchar *readmap(char *name, int *size, int *revision) { setnames(name); uchar *data = (uchar *)loadfile(cgzname, size); if(!data) { conoutf("\f3could not read map %s", cgzname); return NULL; } mapstats *ms = loadmapstats(cgzname, false); if(revision) *revision = ms->hdr.maprevision; return data; } void writecfggz(char *name, int size, int sizegz, uchar *data) { if(size < 1 || !sizegz || size > MAXCFGFILESIZE) return; setnames(name); uchar *rawcfg = new uchar[size]; uLongf rawsize = size; if(rawcfg && uncompress(rawcfg, &rawsize, data, sizegz) == Z_OK && rawsize - size == 0) { stream *f = openfile(mcfname, "w"); if(f) { f->write(rawcfg, size); delete f; conoutf("wrote map config to %s", mcfname); } else { conoutf("\f3could not write config to %s", mcfname); } } DELETEA(rawcfg); } #define GZBUFSIZE ((MAXCFGFILESIZE * 11) / 10) uchar *readmcfggz(char *name, int *size, int *sizegz) { setnames(name); uchar *gzbuf = new uchar[GZBUFSIZE]; uchar *data = (uchar*)loadfile(mcfname, size, "r"); if(data && *size < MAXCFGFILESIZE) { uLongf gzbufsize = GZBUFSIZE; if(compress2(gzbuf, &gzbufsize, data, *size, 9) != Z_OK) { *size = 0; gzbufsize = 0; DELETEA(gzbuf); } *sizegz = (int) gzbufsize; } else { DELETEA(gzbuf); } DELETEA(data); return gzbuf; } // save map as .cgz file. uses 2 layers of compression: first does simple run-length // encoding and leaves out data for certain kinds of cubes, then zlib removes the // last bits of redundancy. Both passes contribute greatly to the miniscule map sizes. void rlencodecubes(vector &f, sqr *s, int len, bool preservesolids) // run-length encoding and serialisation of a series of cubes { sqr *t = NULL; int sc = 0; #define spurge while(sc) { f.add(255); if(sc>255) { f.add(255); sc -= 255; } else { f.add(sc); sc = 0; } } #define c(f) (s->f==t->f) while(len-- > 0) { // 4 types of blocks, to compress a bit: // 255 (2): same as previous block + count // 254 (3): same as previous, except light // deprecated // SOLID (5) // anything else (9) if(SOLID(s) && !preservesolids) { if(t && c(type) && c(wtex) && c(vdelta)) { sc++; } else { spurge; f.add(s->type); f.add(s->wtex); f.add(s->vdelta); } } else { if(t && c(type) && c(floor) && c(ceil) && c(ctex) && c(ftex) && c(utex) && c(wtex) && c(vdelta) && c(tag)) { sc++; } else { spurge; f.add(s->type == SOLID ? 253 : s->type); f.add(s->floor); f.add(s->ceil); f.add(s->wtex); f.add(s->ftex); f.add(s->ctex); f.add(s->vdelta); f.add(s->utex); f.add(s->tag); } } t = s; s++; } spurge; } bool rldecodecubes(ucharbuf &f, sqr *s, int len, int version, bool silent) // run-length decoding of a series of cubes (version is only relevant, if < 6) { sqr *t = NULL, *e = s + len; while(s < e) { int type = f.overread() ? -1 : f.get(); switch(type) { case -1: { if(!silent) conoutf("while reading map at %d: unexpected end of file", int(cubicsize - (e - s))); f.forceoverread(); silent = true; sqrdefault(s); break; } case 255: { if(!t) { f.forceoverread(); continue; } int n = f.get(); loopi(n) memcpy(s++, t, sizeof(sqr)); s--; break; } case 254: // only in MAPVERSION<=2 { if(!t) { f.forceoverread(); continue; } memcpy(s, t, sizeof(sqr)); f.get(); f.get(); break; } 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(); } break; } case 253: // SOLID with all textures during editing (undo) type = SOLID; default: { if(type<0 || type>=MAXTYPE) { if(!silent) conoutf("while reading map at %d: type %d out of range", int(cubicsize - (e - s)), type); f.forceoverread(); continue; } sqrdefault(s); s->type = type; s->floor = f.get(); s->ceil = f.get(); if(s->floor>=s->ceil) s->floor = s->ceil-1; // for pre 12_13 s->wtex = f.get(); s->ftex = f.get(); s->ctex = f.get(); if(version<=2) { f.get(); f.get(); } s->vdelta = f.get(); s->utex = (version>=2) ? f.get() : s->wtex; s->tag = (version>=5) ? f.get() : 0; } } s->defer = 0; t = s; s++; } return !f.overread(); // true: no problem } // 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); } headerextra *duplicate() { return new headerextra(len, flags, data); } }; vector headerextras; const char *hx_names[] = { "unused", "mapinfo", "modeinfo", "artist", "editundo", "config", "vantage point", "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); } void 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"); } COMMAND(listheaderextras, ""); 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); if(p.overread() || len > p.remaining()) break; DEBUGCODE(clientlogf(" found headerextra \"%s\", %d bytes%s", hx_name(flags & HX_TYPEMASK), 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); // needs to have '\0' at the end resetcontext(); break; case HX_MAPINFO: { string l, c; getstring(l, q, MAXSTRLEN - 1); getstring(c, q, MAXSTRLEN - 1); setmapinfo(l, c); break; } 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; // rewrite some headers every time if(!(ignoretypes & (1 << HX_MAPINFO))) { for(int n; (n = findheaderextra(HX_MAPINFO)) >= 0; ) deleteheaderextra(n); if(mapinfo_license[0]) { sendstring(mapinfo_license, tmp); sendstring(mapinfo_comment, tmp); headerextras.add(new headerextra(tmp.length(), HX_MAPINFO|HX_FLAG_PERSIST, tmp.getbuf())); tmp.setsize(0); } } 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; if(unsavededits) { conoutf("There are unsaved edits: they need to be saved or discarded before a config file can be embedded."); return false; } setnames(getclientmap()); int len; uchar *buf = (uchar *)loadfile(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; if(unsavededits) { conoutf("There are unsaved edits: they need to be saved or discarded before the config file can be extracted."); 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 > 0 ? headerextras[n]->len - 1 : 0); delete f; conoutf("extracted embedded map config to file %s", mcfname); deleteheaderextra(n); hdr.flags &= ~MHF_AUTOMAPCONFIG; // external config file, manually edited } else conoutf("\f3failed to write config to %s", mcfname); } } COMMAND(extractconfigfile, ""); void automapconfig() { conoutf("\"automatic embedded map config data\" %senabled", hdr.flags & MHF_AUTOMAPCONFIG ? "was already " : ""); hdr.flags |= MHF_AUTOMAPCONFIG; } COMMAND(automapconfig, ""); COMMANDF(getautomapconfig, "", () { intret((hdr.flags & MHF_AUTOMAPCONFIG) != 0); }); void flagmapconfigchange() { // if changes are tracked, because automapcfg is enabled and the change is not read from a config file -> set unsaved edits flag if(execcontext != IEXC_MAPCFG && hdr.flags & MHF_AUTOMAPCONFIG) unsavededits++; } int mapconfigerror = 0; void flagmapconfigerror(int what) { if(execcontext == IEXC_MAPCFG || execcontext == IEXC_MDLCFG) mapconfigerror |= what; } void getcurrentmapconfig(vector &f, bool onlysounds) { if(!onlysounds) { extern int fogcolour, shadowyaw; extern char *loadsky; if(fog != DEFAULT_FOG) cvecprintf(f, "fog %d\n", fog); if(fogcolour != DEFAULT_FOGCOLOUR) cvecprintf(f, "fogcolour 0x%06x\n", fogcolour); if(shadowyaw != DEFAULT_SHADOWYAW) cvecprintf(f, "shadowyaw %d\n", shadowyaw); if(*mapconfigdata.notexturename) cvecprintf(f, "loadnotexture \"%s\"\n", mapconfigdata.notexturename); if(*loadsky) cvecprintf(f, "loadsky \"%s\"\n", loadsky); cvecprintf(f, "%smapmodelreset\n", f.length() ? "\n" : ""); loopi(256) { mapmodelinfo *mmi = getmminfo(i); if(!mmi) break; cvecprintf(f, "mapmodel %d %d %d %s \"%s\"", mmi->rad, mmi->h, mmi->zoff, mmi->scale == 1.0f ? "0" : floatstr(mmi->scale, true), mmshortname(mmi->name)); if(mmi->flags & MMF_CONFIGMASK) cvecprintf(f, " %d", mmi->flags & MMF_CONFIGMASK); f.add('\n'); } cvecprintf(f, "\ntexturereset\n"); loopi(256) { const char *t = gettextureslot(i); if(!t) break; cvecprintf(f, "%s\n", t); } cvecprintf(f, "\n"); } cvecprintf(f, "mapsoundreset\n"); loopv(mapconfigdata.mapsoundlines) { mapsoundline &s = mapconfigdata.mapsoundlines[i]; cvecprintf(f, "mapsound \"ambience/%s\" %d\n", s.name, s.maxuses); } cvecprintf(f, "\n"); f.add('\0'); } #ifdef _DEBUG COMMANDF(dumpmapconfig, "", () { vector buf; updatemapmodeldependencies(); getcurrentmapconfig(buf, false); stream *f = openfile("dumpmapconfig.txt", "w"); if(f) f->write(buf.getbuf(), buf.length() - 1); DELETEP(f); hdr.flags |= MHF_AUTOMAPCONFIG; }); #endif bool clearvantagepoint() { bool yep = false; for(int n; (n = findheaderextra(HX_VANTAGEPOINT)) >= 0; yep = true) deleteheaderextra(n); unsavededits++; return yep; } COMMANDF(clearvantagepoint, "", () { if(!noteditmode("clearvantagepoint") && !multiplayer() && clearvantagepoint()) conoutf("cleared vantage point"); }); void setvantagepoint() { if(multiplayer("setvantagepoint") || (!player1->isspectating() && noteditmode("setvantagepoint"))) return; clearvantagepoint(); short p[5]; storeposition(p); vector buf; loopi(5) putint(buf, p[i]); physent d = *player1; d.radius = d.eyeheight = d.maxeyeheight = d.aboveeye = 0.1; if(collide(&d, false)) { conoutf("\f3collision with map: vantage point not set"); return; } if(!OUTBORD(p[0] / DMF, p[1] / DMF) && iabs(p[2]) < 127 * int(DMF) && p[3] >= 0 && p[3] < 360 && iabs(p[4]) <= 90) { headerextras.add(new headerextra(buf.length(), HX_VANTAGEPOINT|HX_FLAG_PERSIST, buf.getbuf())); conoutf("vantage point set"); } } COMMAND(setvantagepoint, ""); bool getvantagepoint(short p[5]) { int n = findheaderextra(HX_VANTAGEPOINT); if(n >= 0) { ucharbuf q(headerextras[n]->data, headerextras[n]->len); loopi(5) p[i] = getint(q); if(!OUTBORD(p[0] / DMF, p[1] / DMF) && iabs(p[2]) < 127 * int(DMF) && p[3] >= 0 && p[3] < 360 && iabs(p[4]) <= 90) return true; // check for sane values } return false; } COMMANDF(getvantagepoint, "", () { short p[5]; string res = ""; if(getvantagepoint(p)) formatstring(res)("%s %s %s %d %d", floatstr(p[0] / DMF), floatstr(p[1] / DMF), floatstr(p[2] / DMF), p[3], p[4]); result(res); }); bool gotovantagepoint() { short p[5]; if(getvantagepoint(p)) { restoreposition(p); physent d = *player1; d.radius = d.eyeheight = d.maxeyeheight = d.aboveeye = 0.1; if(!collide(&d, false)) return true; // don't use out-of-map vantage points } // if there is no (valid) vantage point set, we go to the first playerstart instead int s = findentity(PLAYERSTART, 0); if(ents.inrange(s)) gotoplayerstart(player1, &ents[s]); entinmap(player1); return false; } COMMANDF(gotovantagepoint, "", () { intret((editmode || (!multiplayer(NULL) && player1->isspectating())) && gotovantagepoint() ? 1 : 0); }); void setmapinfo(const char *newlicense, const char *newcomment) { string s; filtertext(s, newlicense ? newlicense : mapinfo_license, FTXT__MAPINFO_LICENSE, 20); mapinfo_license = exchangestr(mapinfo_license, s); filtertext(s, newcomment ? newcomment : mapinfo_comment, FTXT__MAPINFO_COMMENT, 60); mapinfo_comment = exchangestr(mapinfo_comment, s); } SVARF(mapinfo_license, "", setmapinfo(); ); SVARF(mapinfo_comment, "", setmapinfo(); ); VAR(advancemaprevision, 1, 1, 100); VARP(mapbackupsonsave, 0, 1, 1); void rebuildtexlists() // checks the texlists, if they still contain all possible textures { short h[256]; int mk[3], mt = 0; vector missing; loopk(3) { missing.setsize(0); uchar *p = hdr.texlists[k]; loopi(256) h[i] = 0; loopi(256) h[p[i]]++; loopi(256) if(h[i] == 0) missing.add(i); mt += mk[k] = missing.length(); loopi(256) if(h[p[i]] > 1) { h[p[i]]--; p[i] = missing.pop(); } } if(mt) conoutf("WARNING: rebuildtexlists() fixed %d|%d|%d missing entries", mk[0], mk[1], mk[2]); } static const uchar sortenttab[] = { // 1..9: used in network protocol, 10..19: sorted lights, 20..29: misc, 30..39: clipped ents at the end of the list 99, // NOTUSED // entity slot not in use in map (usually seen at deleted entities) 10, // LIGHT (colored: 11, shadow: 12) // lightsource, attr1 = radius, attr2 = intensity (or attr2..4 = r-g-b) 2, // PLAYERSTART // attr1 = angle, attr2 = team 1, 1, 1, // I_CLIPS, I_AMMO, I_GRENADE, // attr1 = elevation 1, 1, 1, 1, // I_HEALTH, I_HELMET, I_ARMOUR, I_AKIMBO, 30, // MAPMODEL (clipped: 31) // attr1 = angle, attr2 = idx, attr3 = elevation, attr4 = texture, attr5 = pitch, attr6 = roll 21, // CARROT // attr1 = tag, attr2 = type 3, // LADDER // attr1 = height 1, // CTF_FLAG // attr1 = angle, attr2 = red/blue 20, // SOUND // attr1 = idx, attr2 = radius, attr3 = size, attr4 = volume 32, // CLIP // attr1 = elevation, attr2 = xradius, attr3 = yradius, attr4 = height, attr6 = slope, attr7 = shape 33}; // PLCLIP // attr1 = elevation, attr2 = xradius, attr3 = yradius, attr4 = height, attr6 = slope, attr7 = shape struct numbered_persistent_entity { persistent_entity e; int n; }; int cmp_npe(const numbered_persistent_entity *a, const numbered_persistent_entity *b) { ASSERT(a->n != b->n); return a->n - b->n; } 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; } if(!skipoptimise) { // optimisation works on the real world geometry, not on a copy voptimize(); mapmrproper(false); addcomfort = false; // "optimized + undos" is not useful } // get target file ready setnames(mname); if(mapbackupsonsave) backup(cgzname, bakname); stream *f = opengzfile(cgzname, "wb"); if(!f) { conoutf("could not write map to %s", cgzname); return; } // update embedded config file (if used) updatemapmodeldependencies(); bool autoembedconfig = (hdr.flags & MHF_AUTOMAPCONFIG) != 0; if(autoembedconfig) { // write embedded map config backup(mcfname, cbakname); // rename possibly existing external map config file for(int n; (n = findheaderextra(HX_CONFIG)) >= 0; ) deleteheaderextra(n); // delete existing embedded config vector ec; getcurrentmapconfig(ec, false); uchar *ecu = new uchar[ec.length()]; memcpy(ecu, ec.getbuf(), ec.length()); headerextras.add(new headerextra(ec.length(), HX_CONFIG|HX_FLAG_PERSIST, NULL))->data = ecu; } // update (and fix) map header 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.timestamp = (int) time(NULL); rebuildtexlists(); // create sorted entity list vector sortedents; hdr.numents = 0; ASSERT(MAXENTITIES < (1 << 16)); numbered_persistent_entity npe; loopv(ents) if(ents[i].type != NOTUSED && ents[i].type != DUMMYENT) // omit deleted entities and dummies { hdr.numents++; npe.e = ents[i]; int t = npe.e.type, nt = t < MAXENTTYPES ? sortenttab[t] : 100 + t; if(t == LIGHT) { if(npe.e.attr3 || npe.e.attr4) nt++; // sort white lights before colored ones to fix light bug if(npe.e.attr1 < 0) nt += 2; // sort shadows after lights } else if(t == MAPMODEL) { mapmodelinfo *mmi = getmminfo(npe.e.attr2); if(mmi && mmi->h) nt++; // sort clipped mapmodels right after unclipped mapmodels, but before clips and plclips } npe.n = i | (nt << 16); // numbers have to be unique for the sort to be stable sortedents.add(npe); } sortedents.sort(cmp_npe); if(hdr.numents > MAXENTITIES) { conoutf("too many map entities (%d), only %d will be written to file", hdr.numents, MAXENTITIES); hdr.numents = MAXENTITIES; } // write header and extras to file header tmp = hdr; ucharbuf hx = packheaderextras(addcomfort ? 0 : (1 << HX_EDITUNDO)); // if addcomfort -> add undos/redos int writeextra = 0; if(hx.maxlen) tmp.headersize += writeextra = clamp(hx.maxlen, 0, MAXHEADEREXTRA); tmp.maprevision += advancemaprevision; DEBUG("version " << tmp.version << " headersize " << tmp.headersize << " entities " << tmp.numents << " factor " << tmp.sfactor << " revision " << tmp.maprevision); lilswap(&tmp.version, 4); // version, headersize, sfactor, numents lilswap(&tmp.waterlevel, 1); lilswap(&tmp.maprevision, 4); // maprevision, ambient, flags, timestamp f->write(&tmp, sizeof(header)); if(writeextra) f->write(hx.buf, writeextra); delete[] hx.buf; // write entities loopi(hdr.numents) { persistent_entity &tmp = sortedents[i].e; clampentityattributes(tmp); lilswap((short *)&tmp, 4); lilswap(&tmp.attr5, 1); f->write(&tmp, sizeof(persistent_entity)); } // fix world border if(editmode) { int nonsolid = 0; loop(y, ssize) loop(x, ssize) { if(OUTBORD(x, y)) { sqr *s = S(x, y); if(!SOLID(s)) { sqrdefault(s); s->type = SOLID; nonsolid++; } } else x = ssize - MINBORD - 1; } if(nonsolid) conoutf("\f3fixed %d non-solid world border cubes", nonsolid); } // write map geometry vector rawcubes; rlencodecubes(rawcubes, world, cubicsize, skipoptimise); // if skipoptimize -> keep properties of solid cubes (forces format 10) f->write(rawcubes.getbuf(), rawcubes.length()); delete f; unsavededits = 0; conoutf("wrote map file %s %s%s%s", cgzname, skipoptimise ? "without optimisation" : "(optimised)", addcomfort ? " (including editing history)" : "", autoembedconfig ? " (including map config)" : ""); } 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); } ); void save_world9(char *mname) { if(unsavededits) { conoutf("\f3There are unsaved changes to the map. Please save them to a lossless format first (\"savemap\")."); return; } if(!*mname) { conoutf("\f3You need to specify a map file name."); return; } if(securemapcheck(mname)) return; if(!validmapname(mname)) { conoutf("\f3Invalid map name. It must only contain letters, digits, '-', '_' and be less than %d characters long", MAXMAPNAMELEN); return; } if(findheaderextra(HX_CONFIG) >= 0) { // extract embedded config file first string clientmapbak; copystring(clientmapbak, getclientmap()); copystring(getclientmap(), mname); // hack! extractconfigfile(); copystring(getclientmap(), clientmapbak); } if(headerextras.length()) { conoutf("\f3not writing header extra information:"); listheaderextras(); } setnames(mname); backup(cgzname, bakname); stream *f = opengzfile(cgzname, "wb"); 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 = 9; hdr.headersize = sizeof(header); hdr.timestamp = (int) time(NULL); // non-zero timestamps in format 9 can be used to identify "exported" maps hdr.numents = 0; loopv(ents) if(ents[i].type != NOTUSED && ents[i].type != DUMMYENT) hdr.numents++; if(hdr.numents > MAXENTITIES) { conoutf("too many map entities (%d), only %d will be written to file", hdr.numents, MAXENTITIES); hdr.numents = MAXENTITIES; } header tmp = hdr; tmp.maprevision += advancemaprevision; tmp.flags = 0; tmp.waterlevel /= WATERLEVELSCALING; lilswap(&tmp.version, 4); // version, headersize, sfactor, numents lilswap(&tmp.waterlevel, 1); lilswap(&tmp.maprevision, 4); // maprevision, ambient, flags, timestamp f->write(&tmp, sizeof(header)); int ne = hdr.numents, ec = 0; loopv(ents) { if(ents[i].type != NOTUSED && ents[i].type != DUMMYENT) { if(!ne--) break; persistent_entity tmp = ents[i]; clampentityattributes(tmp); tmp.attr1 /= entscale[tmp.type][0]; tmp.attr2 /= entscale[tmp.type][1]; tmp.attr3 /= entscale[tmp.type][2]; tmp.attr4 /= entscale[tmp.type][3]; if(tmp.attr5 || tmp.attr6 || tmp.attr7) conoutf("\f3lost some attributes for entity #%d (%d)", i, ++ec); lilswap((short *)&tmp, 4); f->write(&tmp, 12); } } vector rawcubes; rlencodecubes(rawcubes, world, cubicsize, false); f->write(rawcubes.getbuf(), rawcubes.length()); delete f; conoutf("wrote map file %s with format 9", cgzname); } COMMANDN(savemap9, save_world9, "s"); void showmapdims() { conoutf(" min X|Y|Z: %3d : %3d : %3d", clmapdims.x1, clmapdims.y1, clmapdims.minfloor); conoutf(" max X|Y|Z: %3d : %3d : %3d", clmapdims.x2, clmapdims.y2, clmapdims.maxceil); conoutf("delta X|Y|Z: %3d : %3d : %3d", clmapdims.xspan, clmapdims.yspan, clmapdims.maxceil - clmapdims.minfloor); } COMMAND(showmapdims, ""); extern void preparectf(bool cleanonly = false); int maploaded = 0; VAR(curmaprevision, 1, 0, 0); COMMANDF(getmaptimestamp, "s", (char *fmt) { result(hdr.timestamp ? timestring((time_t)hdr.timestamp, *fmt != 'U', *fmt == 'U' ? fmt + 1 : fmt) : "no timestamp stored"); }); void changemapflag(int val, int mask) { int old = hdr.flags; if(val) hdr.flags |= mask; else hdr.flags &= ~mask; if(editmode && old != hdr.flags) unsavededits++; } VARF(mapoverride_nowaterreflect, 0, 0, 1, changemapflag(mapoverride_nowaterreflect, MHF_DISABLEWATERREFLECT)); VARF(mapoverride_limitwaveheight, 0, 0, 1, changemapflag(mapoverride_limitwaveheight, MHF_LIMITWATERWAVEHEIGHT)); VARF(mapoverride_nostencilshadows, 0, 0, 1, changemapflag(mapoverride_nostencilshadows, MHF_DISABLESTENCILSHADOWS)); VAR(_ignoreillegalpaths, 0, 0, 1); extern char *mlayout; extern int Mv, Ma, Hhits; extern float Mh; static string lastloadedconfigfile; int 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(); advancemaprevision = 1; setnames(mname); maploaded = getfilesize(ocgzname); if(maploaded > 0) { copystring(cgzname, ocgzname); copystring(mcfname, omcfname); } else maploaded = getfilesize(cgzname); if(!validmapname(mname)) { conoutf("\f3Invalid map name. It must only contain letters, digits, '-', '_' and be less than %d characters long", MAXMAPNAMELEN); return -1; } stream *f = opengzfile(cgzname, "rb"); if(!f) { conoutf("\f3could not read map %s", cgzname); return -2; } if(unsavededits) xmapbackup("load_map_", mname); unsavededits = 0; DEBUG("reading map \"" << cgzname << "\""); header tmp; 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 -3; } lilswap(&tmp.version, 4); // version, headersize, sfactor, numents if(tmp.version > MAPVERSION) { conoutf("\f3this map requires a newer version of AssaultCube"); delete f; return -4; } if(tmp.sfactorLARGEST_FACTOR || tmp.numents > MAXENTITIES) { conoutf("\f3illegal map size"); delete f; return -5; } tmp.headersize = fixmapheadersize(tmp.version, tmp.headersize); 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 -6; } 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) { 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 uchar *extrabuf = new uchar[extrasize]; if(f->read(extrabuf, extrasize) != extrasize) { conoutf("\f3while reading map: header malformatted (3)"); delete f; delete[] extrabuf; return -7; } unpackheaderextra(extrabuf, extrasize); delete[] extrabuf; } f->seek(tmp.headersize, SEEK_SET); } hdr = tmp; mapconfigdata.clear(); rebuildtexlists(); loadingscreen("%s", hdr.maptitle); resetmap(); int res = 0; if(hdr.version>=4) { lilswap(&hdr.waterlevel, 1); if(!hdr.watercolor[3]) setwatercolor(); lilswap(&hdr.maprevision, 4); // maprevision, ambient, flags, timestamp curmaprevision = hdr.maprevision; } else { hdr.waterlevel = -100000; hdr.ambient = 0; } if(hdr.version < 10) hdr.waterlevel *= WATERLEVELSCALING; setfvar("waterlevel", float(hdr.waterlevel) / WATERLEVELSCALING); mapoverride_nowaterreflect = (hdr.flags & MHF_DISABLEWATERREFLECT) ? 1 : 0; mapoverride_limitwaveheight = (hdr.flags & MHF_LIMITWATERWAVEHEIGHT) ? 1 : 0; mapoverride_nostencilshadows = (hdr.flags & MHF_DISABLESTENCILSHADOWS) ? 1 : 0; // read and convert all entities cleartodoentities(); persistent_entity *tempents = new persistent_entity[hdr.numents]; bool oldentityformat = hdr.version < 10; // version < 10 have only 4 attributes and no scaling loopi(hdr.numents) { persistent_entity &e = tempents[i]; f->read(&e, oldentityformat ? 12 : sizeof(persistent_entity)); lilswap((short *)&e, 4); if(oldentityformat) e.attr5 = e.attr6 = e.attr7 = 0; else lilswap(&e.attr5, 1); if(e.type == LIGHT && e.attr1 >= 0) { if(!e.attr2) e.attr2 = 255; // needed for MAPVERSION<=2 if(e.attr1 > 32) e.attr1 = 32; // 12_03 and below (but applied to _all_ files!) } transformoldentitytypes(hdr.version, e.type); if(oldentityformat && e.type < MAXENTTYPES) { if(e.type == CTF_FLAG || e.type == MAPMODEL) e.attr1 = e.attr1 + 7 - (e.attr1 + 7) % 15; // round the angle to the nearest 15-degree-step, like old versions did during rendering if(e.type == LIGHT && e.attr1 < 0) e.attr1 = 0; // negative lights had no meaning before version 10 int ov, ss; persistent_entity oe = e; #define SCALEATTR(x) \ if((ss = abs(entwraparound[e.type][x - 1] / entscale[e.type][x - 1]))) e.attr##x = (int(e.attr##x) % ss + ss) % ss; \ e.attr##x = ov = e.attr##x * entscale[e.type][x - 1]; \ if(ov != e.attr##x) { conoutf("overflow during conversion of attr%d of entity #%d (%s): value %d can no longer be represented - pls fix manually before saving the map",\ x, i, entnames[e.type], oe.attr##x); e.attr##x = oe.attr##x; res |= LWW_ENTATTROVERFLOW; \ defformatstring(desc)("%s: attr%d was %d, value can no longer be represented", entnames[e.type], x, oe.attr##x); addtodoentity(i, desc); } SCALEATTR(1); SCALEATTR(2); SCALEATTR(3); SCALEATTR(4); //SCALEATTR(5); // no need to scale zeros //SCALEATTR(6); //SCALEATTR(7); #undef SCALEATTR } } // create entities (and update some statistics) calcentitystats(clentstats, tempents, hdr.numents); ents.shrink(0); loopi(hdr.numents) { entity &e = ents.add(); memcpy(&e, &tempents[i], sizeof(persistent_entity)); e.spawned = false; } delete[] tempents; // read world geometry delete[] world; setupworld(hdr.sfactor); if(!mapinfo.numelems || (mapinfo.access(mname) && !cmpf(cgzname, mapinfo[mname]))) world = (sqr *)ents.getbuf(); c2skeepalive(); vector rawcubes; // fetch whole file into buffer loopi(9) { ucharbuf q = rawcubes.reserve(cubicsize); q.len = f->read(q.buf, cubicsize); rawcubes.addbuf(q); if(q.len < cubicsize) break; } delete f; ucharbuf uf(rawcubes.getbuf(), rawcubes.length()); res |= rldecodecubes(uf, world, cubicsize, hdr.version, false) ? 0 : LWW_DECODEERR; // decode file c2skeepalive(); // calculate map statistics servsqr *smallworld = createservworld(world, cubicsize); int we = calcmapdims(clmapdims, smallworld, ssize); if(we) conoutf("world error %d", we); res |= LWW_WORLDERROR * (iabs(we) & 0xf); delete[] smallworld; DELETEA(mlayout); mlayout = new char[cubicsize + 256]; memset(mlayout, 0, cubicsize * sizeof(char)); Mv = Ma = Hhits = 0; char texuse[256]; loopi(256) texuse[i] = 0; loopk(cubicsize) { sqr *s = &world[k]; if(SOLID(s)) mlayout[k] = 127; else { mlayout[k] = s->floor; // FIXME int diff = s->ceil - s->floor; if(diff > 6) { if(diff > MAXMHEIGHT) Hhits += diff - MAXMHEIGHT; Ma += 1; Mv += diff; } texuse[s->utex] = texuse[s->ftex] = texuse[s->ctex] = 1; } texuse[s->wtex] = 1; } Mh = Ma ? (float)Mv/Ma : 0; c2skeepalive(); calclight(); conoutf("read map %s rev %d (%d milliseconds)", cgzname, hdr.maprevision, watch.elapsed()); conoutf("%s", hdr.maptitle); mapconfigerror = 0; pushscontext(IEXC_MAPCFG); // untrusted altogether if(hdr.flags & MHF_AUTOMAPCONFIG) { // full featured embedded config: no need to read any other map config files copystring(lastloadedconfigfile, ""); } else { // map config from files execfile("config/default_map_settings.cfg"); execfile(mcfname); copystring(lastloadedconfigfile, mcfname); } parseheaderextra(); popscontext(); c2skeepalive(); watch.start(); loopi(256) if(texuse[i]) lookupworldtexture(i, autodownload ? true : false); int texloadtime = watch.elapsed(); c2skeepalive(); watch.start(); if(autodownload) resetmdlnotfound(); preload_mapmodels(autodownload ? true : false); int mdlloadtime = watch.elapsed(); c2skeepalive(); watch.start(); audiomgr.preloadmapsounds(autodownload ? true : false); int audioloadtime = watch.elapsed(); c2skeepalive(); watch.start(); int downloaded = downloadpackages(); if(downloaded > 0) clientlogf("downloaded content (%d KB in %d seconds)", downloaded/1024, watch.elapsed()/1000); c2skeepalive(); res |= loadskymap(true) ? 0 : LWW_MISSINGMEDIA * 1; watch.start(); loopi(256) if(texuse[i] && lookupworldtexture(i, false) == notexture && (!_ignoreillegalpaths || gettextureslot(i))) res |= LWW_MISSINGMEDIA * 2; clientlogf("loaded textures (%d milliseconds)", texloadtime+watch.elapsed()); c2skeepalive(); watch.start(); res |= preload_mapmodels(false) ? 0 : LWW_MISSINGMEDIA * 4; clientlogf("loaded mapmodels (%d milliseconds)", mdlloadtime+watch.elapsed()); c2skeepalive(); watch.start(); res |= audiomgr.preloadmapsounds(false) ? 0 : LWW_MISSINGMEDIA * 8; // also set, if soundvol == 0, because no sounds are loaded (or checked) in that case clientlogf("loaded mapsounds (%d milliseconds)", audioloadtime+watch.elapsed()); c2skeepalive(); // determine index of first clipped entity clentstats.firstclip = max(0, ents.length() - 1); // fallback for "no clipped entities at all" loopv(ents) { entity &e = ents[i]; mapmodelinfo *mmi; if(e.type == CLIP || e.type == PLCLIP || (e.type == MAPMODEL && (mmi = getmminfo(e.attr2)) && mmi->h)) { clentstats.firstclip = i; break; } } defformatstring(startmillis)("%d", totalmillis); alias("gametimestart", startmillis, true); startmap(mname); res |= mapconfigerror; if(res) conoutf("\f3unresolved problems occurred during load_world(), warning: 0x%x", res); return res; // negative: error (no map loaded), zero: no problem, positive: some problems (bitmask value) } COMMANDF(loadmap, "s", (char *mapname) { intret(!multiplayer("loadmap") ? load_world(mapname) : -42); }); char *getfiledesc(const char *dir, const char *name, const char *ext) // extract demo and map descriptions { if(!dir || !name || !ext) return NULL; defformatstring(fn)("%s/%s.%s", dir, name, ext); path(fn); string text, demodescalias; if(!strcmp(ext, "dmo")) { stream *f = opengzfile(fn, "rb"); if(!f) return NULL; demoheader hdr; if(f->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic))) { delete f; return NULL; } delete f; lilswap(&hdr.version, 1); lilswap(&hdr.protocol, 1); const char *tag = "(incompatible file) "; if(hdr.version == DEMO_VERSION) { if(hdr.protocol == PROTOCOL_VERSION) tag = ""; else if(hdr.protocol == -PROTOCOL_VERSION) tag = "(recorded on modded server) "; } formatstring(text)("%s%s", tag, hdr.desc); text[DHDR_DESCCHARS - 1] = '\0'; formatstring(demodescalias)("demodesc_%s", name); const char *customdesc = getalias(demodescalias); if(customdesc) { int textlen = strlen(text); concatformatstring(text, " \n\f4(Description: \f0%s\f4)", customdesc); ASSERT(MAXSTRLEN > 2 * DHDR_DESCCHARS); text[textlen + DHDR_DESCCHARS - 1] = '\0'; } return newstring(text); } else if(!strcmp(ext, "cgz")) { stream *f = opengzfile(fn, "rb"); if(!f) return NULL; header hdr; if(f->read(&hdr, sizeof(header))!=sizeof(header) || (strncmp(hdr.head, "CUBE", 4) && strncmp(hdr.head, "ACMP",4))) { delete f; return NULL; } delete f; lilswap(&hdr.version, 1); filtertext(hdr.maptitle, hdr.maptitle, FTXT__MAPMSG, 127); formatstring(text)("%s%s", (hdr.version>MAPVERSION) ? "(incompatible file) " : "", hdr.maptitle); text[DHDR_DESCCHARS - 1] = '\0'; return newstring(text); } return NULL; } void enumfiles(char *dir, char *ext) { vector res; vector files; if(!strcasecmp(ext, "dir")) listsubdirs(dir, files, stringsort); else listfiles(dir, *ext ? ext : NULL, files, stringsort); loopv(files) cvecprintf(res, "%s\n", escapestring(files[i])); files.deletearrays(); resultcharvector(res, -1); } COMMAND(enumfiles, "ss"); // support reading and writing binary data in config files static bool hexbinenabled = false; static vector hexbin; void hexbinwrite(stream *f, void *data, int len, bool ascii = true) // write block of binary data as hex values with up to 24 bytes per line { string asc; uchar *s = (uchar *) data; while(len > 0) { int chunk = min(len, 24); f->printf("hexbinchunk"); loopi(chunk) { asc[i] = isalnum(*s) ? *s : '.'; asc[i + 1] = '\0'; f->printf(" %02x", int(*s++)); } if(ascii) f->printf(" // %s\n", asc); else f->printf("\n"); len -= chunk; } } COMMANDF(hexbinchunk, "v", (char **args, int numargs) // read up to 24 bytes from the command's arguments to the hexbin buffer { if(!hexbinenabled) { conoutf("hexbinchunk out of context"); return; } loopi(numargs) hexbin.add(strtol(args[i], NULL, 16)); }); // multi-map editmode extensions (xmap) // // keep several (versions of) maps in memory at the same time // compare maps and visualise differences // // the general assumption is, that ram is cheap, and that a copy of a size-9 map should be below 10MB (including undo data) #define XMAPVERSION 1001001 VARP(persistentxmaps, 0, 1, 1); // save all xmaps on exit, restore them at game start struct xmap { string nick; // unique handle string name; vector headerextras; vector ents, delents; vector mapconfig; vector todoents; vector todoentdescs; sqr *world; header hdr; int ssize, cubicsize, numundo; string mcfname; short position[5]; xmap() : world(NULL), ssize(0), cubicsize(0), numundo(0) { *nick = *name = *mcfname = '\0'; } xmap(const char *nnick) // take the current map and store it in an xmap { copystring(nick, nnick); copystring(name, getclientmap()); hdr = ::hdr; ssize = ::ssize; cubicsize = ::cubicsize; loopv(::headerextras) headerextras.add(::headerextras[i]->duplicate()); world = new sqr[cubicsize]; memcpy(world, ::world, cubicsize * sizeof(sqr)); loopv(::ents) if(::ents[i].type != NOTUSED) ents.add() = ::ents[i]; loopv(deleted_ents) delents.add() = deleted_ents[i]; copystring(mcfname, lastloadedconfigfile); // may have been "official" or not if(hdr.flags & MHF_AUTOMAPCONFIG) getcurrentmapconfig(mapconfig, false); storeposition(position); vector tmp; // add undo/redo data in compressed form as temporary headerextra numundo = backupeditundo(tmp, MAXHEADEREXTRA, MAXHEADEREXTRA); headerextras.add(new headerextra(tmp.length(), HX_EDITUNDO, tmp.getbuf())); loopv(::todoents) { todoents.add(::todoents[i]); todoentdescs.add(newstring(::todoentdescs[i])); } } ~xmap() { headerextras.deletecontents(); delete[] world; ents.setsize(0); delents.setsize(0); mapconfig.setsize(0); todoents.setsize(0); todoentdescs.deletearrays(); } void restoreent(int i) { entity &e = ::ents.add(); memcpy(&e, &ents[i], sizeof(persistent_entity)); e.spawned = true; } void restore() // overwrite the current map with the contents of an xmap { setnames(name); copystring(lastloadedconfigfile, mcfname); ::hdr = hdr; clearheaderextras(); resetmap(true); curmaprevision = hdr.maprevision; setfvar("waterlevel", float(hdr.waterlevel) / WATERLEVELSCALING); ::ents.shrink(0); loopv(ents) restoreent(i); deleted_ents.setsize(0); loopv(delents) deleted_ents.add(delents[i]); delete[] ::world; setupworld(hdr.sfactor); memcpy(::world, world, cubicsize * sizeof(sqr)); calclight(); // includes full remip() loopv(headerextras) ::headerextras.add(headerextras[i]->duplicate()); pushscontext(IEXC_MAPCFG); // untrusted altogether if(mapconfig.length()) execute(mapconfig.getbuf()); else { execfile("config/default_map_settings.cfg"); execfile(lastloadedconfigfile); } parseheaderextra(true, mapconfig.length() ? (1<printf("restorexmap version %d %d %d\n", XMAPVERSION, isbigendian() ? 1 : 0, int(sizeof(world))); // it is a non-portable binary file format, so we have to check... f->printf("restorexmap names \"%s\" \"%s\"\n", name, mcfname); f->printf("restorexmap sizes %d %d %d\n", ssize, cubicsize, numundo); hexbinwrite(f, &hdr, sizeof(header)); f->printf("restorexmap header\n"); uLongf worldsize = cubicsize * sizeof(sqr), gzbufsize = (worldsize * 11) / 10; // gzip the world, so the file will only be big instead of huge uchar *gzbuf = new uchar[gzbufsize]; if(compress2(gzbuf, &gzbufsize, (uchar *)world, worldsize, 9) != Z_OK) gzbufsize = 0; hexbinwrite(f, gzbuf, gzbufsize, false); f->printf("restorexmap world\n"); DELETEA(gzbuf); loopv(headerextras) { hexbinwrite(f, headerextras[i]->data, headerextras[i]->len); f->printf("restorexmap headerextra %d // %s\n", headerextras[i]->flags, hx_name(headerextras[i]->flags)); } int el = ents.length(), all = el + delents.length(); loopi(all) // entities are stored as plain text - you may edit them { persistent_entity &e = i >= el ? delents[i - el] : ents[i]; f->printf("restorexmap %sent %d %d %d %d %d %d %d %d %d %d %d // %s\n", i >= el ? "del": "", e.type, e.x, e.y, e.z, e.attr1, e.attr2, e.attr3, e.attr4, e.attr5, e.attr6, e.attr7, e.type >= 0 && e.type < MAXENTTYPES ? entnames[e.type] : "unknown"); } if(mapconfig.length()) { char *t = newstring(mapconfig.getbuf()), *p, *l = t; while((p = strchr(l, '\n'))) { *p = '\0'; f->printf("restorexmap config %s\n", escapestring(l)); l = p + 1; } delstring(t); } loopv(todoents) f->printf("restorexmap todoent %d %s\n", todoents[i], escapestring(todoentdescs[i])); f->printf("restorexmap position %d %d %d %d %d // EOF, don't touch this\n\n", int(position[0]), int(position[1]), int(position[2]), int(position[3]), int(position[4])); } }; static vector xmaps; static xmap *bak, *xmjigsaw; // only bak needs to be deleted before reuse bool validxmapname(const char *nick) { if(validmapname(nick)) return true; conoutf("sry, %s is not a valid xmap nickname", nick); return false; } void xmapdelete(xmap *&xm) // make sure, we don't point to deleted xmaps { DELETEP(xm); } void xmapbackup(const char *nickprefix, const char *nick) // throw away existing backup and make a new one { xmapdelete(bak); defformatstring(text)("%s%s", nickprefix, nick); bak = new xmap(text); if(unsavededits) conoutf("\f3stored backup of unsaved edits on map '%s'; to restore, type \"/xmap_restore\"", bak->name); } const char *xmapdescstring(xmap *xm, bool shortform = false) { static string s[2]; static int toggle; toggle = !toggle; formatstring(s[toggle])("\"%s\": %s rev %d, %d ents%c size %d, hdrs %d, %d undo steps", xm->nick, xm->name, xm->hdr.maprevision, xm->ents.length(), shortform ? '\0' : ',', xm->hdr.sfactor, xm->headerextras.length() - 1, xm->numundo); return s[toggle]; } xmap *getxmapbynick(const char *nick, int *index = NULL, bool errmsg = true) { if(*nick) loopvrev(xmaps) { if(!strcmp(nick, xmaps[i]->nick)) { if(index) *index = i; return xmaps[i]; } } if(errmsg) conoutf("xmap \"%s\" not found", nick); return NULL; } COMMANDF(xmap_list, "", () // list xmaps (and status) { loopv(xmaps) conoutf("xmap %d %s", i, xmapdescstring(xmaps[i])); if(xmaps.length() == 0) conoutf("no xmaps in memory"); if(bak) conoutf("backup stored before %s", xmapdescstring(bak)); } ); COMMANDF(getxmaplist, "s", (char *what) // return list of xmap nicks and descriptions { vector res; if(strcasecmp(what, "bak")) loopv(xmaps) cvecprintf(res, "%s %s\n", escapestring(xmaps[i]->nick), escapestring(xmapdescstring(xmaps[i]))); else if(bak) cvecprintf(res, "%s %s", escapestring(bak->nick), escapestring(xmapdescstring(bak))); resultcharvector(res, -1); } ); COMMANDF(xmap_store, "s", (const char *nick) // store current map in an xmap buffer { if(noteditmode("xmap_store")) return; // (may also be used in coopedit) if(!*nick || !validxmapname(nick)) return; int i; if(getxmapbynick(nick, &i, false)) { xmap *xm = xmaps.remove(i); xmapdelete(xm); } // overwrite existing same nick xmaps.add(new xmap(nick)); unsavededits = 0; conoutf("stored xmap %s", xmapdescstring(xmaps.last())); } ); COMMANDF(xmap_delete, "s", (const char *nick) // delete xmap buffer { if(!*nick) return; int i; if(getxmapbynick(nick, &i)) { xmap *xm = xmaps.remove(i); xmapdelete(xm); conoutf("deleted xmap \"%s\"", nick); } } ); COMMANDF(xmap_delete_backup, "", () { if(bak) { xmapdelete(bak); conoutf("deleted backup xmap"); }} ); COMMANDF(xmap_keep_backup, "s", (const char *nick) // move backup xmap to more permanent position { if(!bak) { conoutf("no backup xmap available"); return ; } defformatstring(bakdesc)("%s", bak->nick); if(*nick) { if(validxmapname(nick)) copystring(bak->nick, nick); else return; } loopi(42) if(getxmapbynick(bak->nick, NULL, false)) concatstring(bak->nick, "_"); // evade existing nicknames xmaps.add(bak); bak = NULL; conoutf("stored backup xmap \"%s\" as xmap %s", bakdesc, xmapdescstring(xmaps.last())); } ); COMMANDF(xmap_rename, "ss", (const char *oldnick, const char *newnick) // rename xmap buffer { if(!*oldnick || !*newnick) return; xmap *xm = getxmapbynick(oldnick); if(!xm || !validxmapname(newnick) || getxmapbynick(newnick, NULL, false)) return; copystring(xm->nick, newnick); } ); COMMANDF(xmap_restore, "s", (const char *nick) // use xmap as current map { EDITMP("xmap_restore"); if(*nick) { xmap *xm = getxmapbynick(nick); if(!xm) return; xmapbackup("restore_xmap_", nick); // keep backup, so we can restore from the restore :) xm->restore(); unsavededits = 0; conoutf("restored xmap %s", xmapdescstring(xm)); } else if(bak) { bak->restore(); unsavededits = 0; conoutf("restored backup created before %s", xmapdescstring(bak)); } } ); void restorexmap(char **args, int numargs) // read an xmap from a cubescript file { const char *cmdnames[] = { "version", "names", "sizes", "header", "world", "headerextra", "ent", "delent", "config", "todoent", "position", "" }; const char cmdnumarg[] = { 3, 2, 3, 0, 0, 1, 11, 11, 1, 2, 5 }; if(!xmjigsaw || numargs < 1) return; // { conoutf("restorexmap out of context"); return; } bool abort = false; int cmd = getlistindex(args[0], cmdnames, false, -1); if(cmd < 0 || numargs != cmdnumarg[cmd] + 1) { conoutf("restorexmap error"); return; } switch(cmd) { case 0: // version if(ATOI(args[1]) != XMAPVERSION || ATOI(args[2]) != (isbigendian() ? 1 : 0) || ATOI(args[3]) != int(sizeof(world))) { conoutf("restorexmap: file is from different game version"); abort = true; } break; case 1: // names copystring(xmjigsaw->name, args[1]); copystring(xmjigsaw->mcfname, args[2]); break; case 2: // sizes xmjigsaw->ssize = ATOI(args[1]); xmjigsaw->cubicsize = ATOI(args[2]); xmjigsaw->numundo = ATOI(args[3]); break; case 3: // header if(hexbin.length() == sizeof(header)) memcpy(&xmjigsaw->hdr, hexbin.getbuf(), sizeof(header)); else abort = true; break; case 4: // world { int rawworldsize = xmjigsaw->cubicsize * sizeof(sqr); xmjigsaw->world = new sqr[rawworldsize]; uLongf rawsize = rawworldsize; if(uncompress((uchar *)xmjigsaw->world, &rawsize, hexbin.getbuf(), hexbin.length()) != Z_OK || rawsize - rawworldsize != 0) abort = true; break; } case 5: // headerextra xmjigsaw->headerextras.add(new headerextra(hexbin.length(), ATOI(args[1]), hexbin.getbuf())); break; case 6: // ent case 7: // delent { persistent_entity &e = cmd == 6 ? xmjigsaw->ents.add() : xmjigsaw->delents.add(); int a[11]; loopi(11) a[i] = ATOI(args[i + 1]); e.type = a[0]; e.x = a[1]; e.y = a[2]; e.z = a[3]; e.attr1 = a[4]; e.attr2 = a[5]; e.attr3 = a[6]; e.attr4 = a[7]; e.attr5 = a[8]; e.attr6 = a[9]; e.attr7 = a[10]; break; } case 8: // config cvecprintf(xmjigsaw->mapconfig, "%s\n", args[1]); break; case 9: // todoent xmjigsaw->todoents.add(ATOI(args[1])); xmjigsaw->todoentdescs.add(newstring(args[2])); break; case 10: // position - this is also the last command and will finish the xmap { loopi(5) xmjigsaw->position[i] = ATOI(args[i + 1]); int i; if(getxmapbynick(xmjigsaw->nick, &i, false)) { xmap *xm = xmaps.remove(i); xmapdelete(xm); } // overwrite existing same nick if(!xmjigsaw->world) abort = true; else { if(xmjigsaw->mapconfig.length()) xmjigsaw->mapconfig.add('\0'); xmaps.add(xmjigsaw); xmjigsaw = NULL; // no abort! } } } hexbin.setsize(0); if(abort) DELETEP(xmjigsaw); } COMMAND(restorexmap, "v"); static const char *bakprefix = "(backup)", *xmapspath = "mapediting/xmaps"; char *xmapfilename(const char *nick, const char *prefix = "") { static defformatstring(fn)("%s/%s%s.xmap", xmapspath, prefix, nick); return path(fn); } void writexmap(xmap *xm, const char *prefix = "") { stream *f = openfile(xmapfilename(xm->nick, prefix), "w"); if(f) { xm->write(f); delete f; } } void writeallxmaps() // at game exit, write all xmaps to cubescript files in mapediting/xmaps { if(!persistentxmaps) return; loopv(xmaps) writexmap(xmaps[i]); if(unsavededits) xmapbackup("gameend", ""); if(bak) writexmap(bak, bakprefix); } void loadxmap(const char *nick) { DELETEP(xmjigsaw); xmjigsaw = new xmap(); copystring(xmjigsaw->nick, nick); hexbinenabled = true; execfile(xmapfilename(nick)); hexbinenabled = false; hexbin.setsize(0); // just to make sure } int loadallxmaps() // at game start, load all xmaps from mapediting/xmaps { vector xmapnames; string filename; listfiles(xmapspath, "xmap", xmapnames, stringsort); loopv(xmapnames) { loadxmap(xmapnames[i]); copystring(filename, xmapfilename(xmapnames[i])); backup(filename, xmapfilename(xmapnames[i], "backups/")); // move to backup folder - we will write new files at exit delete[] xmapnames[i]; } loopv(xmaps) if(!strncmp(xmaps[i]->nick, bakprefix, strlen(bakprefix))) { xmapdelete(bak); bak = xmaps.remove(i); copystring(bak->nick, bak->nick + strlen(bakprefix)); break; } return xmapnames.length(); }