// worldio.cpp: loading & saving of maps and savegames #include "worldio.hh" #include #include #include #include #include #include #include #include "blend.hh" #include "command.hh" // idents, identflags #include "console.hh" /* conoutf */ #include "main.hh" // clearmainmenu, renderbackground, multiplayer #include "octa.hh" #include "octaedit.hh" // texmru #include "octarender.hh" #include "pvs.hh" #include "rendermodel.hh" // loadmapmodel, getmminfo, flushpreloadedmodels #include "texture.hh" #include "world.hh" #define OCTAVERSION 33 struct octaheader { char magic[4]; // "OCTA" int version; // any >8bit quantity is little endian int headersize; // sizeof(header) int worldsize; int numents; int numpvs; int lightmaps; int blendmap; int numvars; int numvslots; }; #define MAPVERSION 1 // bump if map format changes, see worldio.cpp struct mapheader { char magic[4]; // "TMAP" int version; // any >8bit quantity is little endian int headersize; // sizeof(header) int worldsize; int numents; int numpvs; int blendmap; int numvars; int numvslots; }; VARR(mapversion, 1, MAPVERSION, 0); static void validmapname(char *dst, const char *src, const char *prefix = nullptr, const char *alt = "untitled", size_t maxlen = 100) { if(prefix) while(*prefix) *dst++ = *prefix++; const char *start = dst; if(src) loopi(maxlen) { char c = *src++; if(iscubealnum(c) || c == '_' || c == '-' || c == '/' || c == '\\') *dst++ = c; else break; } if(dst > start) *dst = '\0'; else if(dst != alt) copystring(dst, alt, maxlen); } static void fixent(entity &e, int version) { if(version <= 0) { if(e.type >= ET_DECAL) e.type++; } } static bool loadmapheader(stream *f, const char *ogzname, mapheader &hdr, octaheader &ohdr) { if(f->read(&hdr, 3*sizeof(int)) != 3*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); return false; } lilswap(&hdr.version, 2); if(!memcmp(hdr.magic, "TMAP", 4)) { if(hdr.version>MAPVERSION) { conoutf(CON_ERROR, "map %s requires a newer version of Tesseract", ogzname); return false; } if(f->read(&hdr.worldsize, 6*sizeof(int)) != 6*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); return false; } lilswap(&hdr.worldsize, 6); if(hdr.worldsize <= 0|| hdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); return false; } } else if(!memcmp(hdr.magic, "OCTA", 4)) { if(hdr.version!=OCTAVERSION) { conoutf(CON_ERROR, "map %s uses an unsupported map format version", ogzname); return false; } if(f->read(&ohdr.worldsize, 7*sizeof(int)) != 7*sizeof(int)) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); return false; } lilswap(&ohdr.worldsize, 7); if(ohdr.worldsize <= 0|| ohdr.numents < 0) { conoutf(CON_ERROR, "map %s has malformatted header", ogzname); return false; } memcpy(hdr.magic, "TMAP", 4); hdr.version = 0; hdr.headersize = sizeof(hdr); hdr.worldsize = ohdr.worldsize; hdr.numents = ohdr.numents; hdr.numpvs = ohdr.numpvs; hdr.blendmap = ohdr.blendmap; hdr.numvars = ohdr.numvars; hdr.numvslots = ohdr.numvslots; } else { conoutf(CON_ERROR, "map %s uses an unsupported map type", ogzname); return false; } return true; } static string ogzname, cfgname, picname; static void setmapfilenames(const char *fname, const char *cname = nullptr) { string name; validmapname(name, fname); formatstring(ogzname, "media/map/%s.ogz", name); formatstring(picname, "media/map/%s.png", name); validmapname(name, cname ? cname : fname); formatstring(cfgname, "media/map/%s.cfg", name); path(ogzname); path(cfgname); path(picname); } enum { OCTSAV_CHILDREN = 0, OCTSAV_EMPTY, OCTSAV_SOLID, OCTSAV_NORMAL }; #define LM_PACKW 512 #define LM_PACKH 512 #define LAYER_DUP (1<<7) struct polysurfacecompat { uchar lmid[2]; uchar verts, numverts; }; static int savemapprogress = 0; static void savec(cube *c, const ivec &o, int size, stream *f, bool nolms) { if((savemapprogress++&0xFFF)==0) renderprogress(float(savemapprogress)/allocnodes, "saving octree..."); loopi(8) { ivec co(i, o, size); if(c[i].children) { f->putchar(OCTSAV_CHILDREN); savec(c[i].children, co, size>>1, f, nolms); } else { int oflags = 0, surfmask = 0, totalverts = 0; if(c[i].material!=MAT_AIR) oflags |= 0x40; if(isempty(c[i])) f->putchar(oflags | OCTSAV_EMPTY); else { if(!nolms) { if(c[i].merged) oflags |= 0x80; if(c[i].ext) loopj(6) { const surfaceinfo &surf = c[i].ext->surfaces[j]; if(!surf.used()) continue; oflags |= 0x20; surfmask |= 1<putchar(oflags | OCTSAV_SOLID); else { f->putchar(oflags | OCTSAV_NORMAL); f->write(c[i].edges, 12); } } loopj(6) f->putlil(c[i].texture[j]); if(oflags&0x40) f->putlil(c[i].material); if(oflags&0x80) f->putchar(c[i].merged); if(oflags&0x20) { f->putchar(surfmask); f->putchar(totalverts); loopj(6) if(surfmask&(1<surfaces[j]; vertinfo *verts = c[i].ext->verts() + surf.verts; int layerverts = surf.numverts&MAXFACEVERTS, numverts = surf.totalverts(), vertmask = 0, vertorder = 0, dim = dimension(j), vc = C[dim], vr = R[dim]; if(numverts) { if(c[i].merged&(1<write(&surf, sizeof(surf)); bool hasxyz = (vertmask&0x04)!=0, hasnorm = (vertmask&0x80)!=0; if(layerverts == 4) { if(hasxyz && vertmask&0x01) { ivec v0 = verts[vertorder].getxyz(), v2 = verts[(vertorder+2)&3].getxyz(); f->putlil(v0[vc]); f->putlil(v0[vr]); f->putlil(v2[vc]); f->putlil(v2[vr]); hasxyz = false; } } if(hasnorm && vertmask&0x08) { f->putlil(verts[0].norm); hasnorm = false; } if(hasxyz || hasnorm) loopk(layerverts) { const vertinfo &v = verts[(k+vertorder)%layerverts]; if(hasxyz) { ivec xyz = v.getxyz(); f->putlil(xyz[vc]); f->putlil(xyz[vr]); } if(hasnorm) f->putlil(v.norm); } } } } } } static cube *loadchildren(stream *f, const ivec &co, int size, bool &failed); static void loadc(stream *f, cube &c, const ivec &co, int size, bool &failed) { int octsav = f->getchar(); switch(octsav&0x7) { case OCTSAV_CHILDREN: c.children = loadchildren(f, co, size>>1, failed); return; case OCTSAV_EMPTY: emptyfaces(c); break; case OCTSAV_SOLID: solidfaces(c); break; case OCTSAV_NORMAL: f->read(c.edges, 12); break; default: failed = true; return; } loopi(6) c.texture[i] = f->getlil(); if(octsav&0x40) c.material = f->getlil(); if(octsav&0x80) c.merged = f->getchar(); if(octsav&0x20) { int surfmask, totalverts; surfmask = f->getchar(); totalverts = max(f->getchar(), 0); newcubeext(c, totalverts, false); memset(c.ext->surfaces, 0, sizeof(c.ext->surfaces)); memset(c.ext->verts(), 0, totalverts*sizeof(vertinfo)); int offset = 0; loopi(6) if(surfmask&(1<surfaces[i]; if(mapversion <= 0) { polysurfacecompat psurf; f->read(&psurf, sizeof(polysurfacecompat)); surf.verts = psurf.verts; surf.numverts = psurf.numverts; } else f->read(&surf, sizeof(surf)); int vertmask = surf.verts, numverts = surf.totalverts(); if(!numverts) { surf.verts = 0; continue; } surf.verts = offset; vertinfo *verts = c.ext->verts() + offset; offset += numverts; ivec v[4], n, vo = ivec(co).mask(0xFFF).shl(3); int layerverts = surf.numverts&MAXFACEVERTS, dim = dimension(i), vc = C[dim], vr = R[dim], bias = 0; genfaceverts(c, i, v); bool hasxyz = (vertmask&0x04)!=0, hasuv = mapversion <= 0 && (vertmask&0x40)!=0, hasnorm = (vertmask&0x80)!=0; if(hasxyz) { ivec e1, e2, e3; n.cross((e1 = v[1]).sub(v[0]), (e2 = v[2]).sub(v[0])); if(n.iszero()) n.cross(e2, (e3 = v[3]).sub(v[0])); bias = -n.dot(ivec(v[0]).mul(size).add(vo)); } else { int vis = layerverts < 4 ? (vertmask&0x02 ? 2 : 1) : 3, order = vertmask&0x01 ? 1 : 0, k = 0; verts[k++].setxyz(v[order].mul(size).add(vo)); if(vis&1) verts[k++].setxyz(v[order+1].mul(size).add(vo)); verts[k++].setxyz(v[order+2].mul(size).add(vo)); if(vis&2) verts[k++].setxyz(v[(order+3)&3].mul(size).add(vo)); } if(layerverts == 4) { if(hasxyz && vertmask&0x01) { ushort c1 = f->getlil(), r1 = f->getlil(), c2 = f->getlil(), r2 = f->getlil(); ivec xyz; xyz[vc] = c1; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; verts[0].setxyz(xyz); xyz[vc] = c1; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; verts[1].setxyz(xyz); xyz[vc] = c2; xyz[vr] = r2; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; verts[2].setxyz(xyz); xyz[vc] = c2; xyz[vr] = r1; xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; verts[3].setxyz(xyz); hasxyz = false; } if(hasuv && vertmask&0x02) { loopk(4) f->getlil(); if(surf.numverts&LAYER_DUP) loopk(4) f->getlil(); hasuv = false; } } if(hasnorm && vertmask&0x08) { ushort norm = f->getlil(); loopk(layerverts) verts[k].norm = norm; hasnorm = false; } if(hasxyz || hasuv || hasnorm) loopk(layerverts) { vertinfo &v = verts[k]; if(hasxyz) { ivec xyz; xyz[vc] = f->getlil(); xyz[vr] = f->getlil(); xyz[dim] = n[dim] ? -(bias + n[vc]*xyz[vc] + n[vr]*xyz[vr])/n[dim] : vo[dim]; v.setxyz(xyz); } if(hasuv) { f->getlil(); f->getlil(); } if(hasnorm) v.norm = f->getlil(); } if(hasuv && surf.numverts&LAYER_DUP) loopk(layerverts) { f->getlil(); f->getlil(); } } } } static cube *loadchildren(stream *f, const ivec &co, int size, bool &failed) { cube *c = newcubes(); loopi(8) { loadc(f, c[i], ivec(i, co, size), size, failed); if(failed) break; } return c; } VAR(dbgvars, 0, 0, 1); static void savevslot(stream *f, VSlot &vs, int prev) { f->putlil(vs.changed); f->putlil(prev); if(vs.changed & (1<putlil(vs.params.length()); loopv(vs.params) { SlotShaderParam &p = vs.params[i]; f->putlil(strlen(p.name)); f->write(p.name, strlen(p.name)); loopk(4) f->putlil(p.val[k]); } } if(vs.changed & (1<putlil(vs.scale); if(vs.changed & (1<putlil(vs.rotation); if(vs.changed & (1<putlil(vs.offset[k]); } if(vs.changed & (1<putlil(vs.scroll[k]); } if(vs.changed & (1<putlil(vs.layer); if(vs.changed & (1<putlil(vs.alphafront); f->putlil(vs.alphaback); } if(vs.changed & (1<putlil(vs.colorscale[k]); } if(vs.changed & (1<putlil(vs.refractscale); loopk(3) f->putlil(vs.refractcolor[k]); } if(vs.changed & (1<putlil(vs.detail); } static void savevslots(stream *f, int numvslots) { if(vslots.empty()) return; int *prev = new int[numvslots]; memset(prev, -1, numvslots*sizeof(int)); loopi(numvslots) { VSlot *vs = vslots[i]; if(vs->changed) continue; for(;;) { VSlot *cur = vs; do vs = vs->next; while(vs && vs->index >= numvslots); if(!vs) break; prev[vs->index] = cur->index; } } int lastroot = 0; loopi(numvslots) { VSlot &vs = *vslots[i]; if(!vs.changed) continue; if(lastroot < i) f->putlil(-(i - lastroot)); savevslot(f, vs, prev[i]); lastroot = i+1; } if(lastroot < numvslots) f->putlil(-(numvslots - lastroot)); delete[] prev; } static void loadvslot(stream *f, VSlot &vs, int changed) { vs.changed = changed; if(vs.changed & (1<getlil(); string name; loopi(numparams) { SlotShaderParam &p = vs.params.add(); int nlen = f->getlil(); f->read(name, min(nlen, MAXSTRLEN-1)); name[min(nlen, MAXSTRLEN-1)] = '\0'; if(nlen >= MAXSTRLEN) f->seek(nlen - (MAXSTRLEN-1), SEEK_CUR); p.name = getshaderparamname(name); p.loc = -1; loopk(4) p.val[k] = f->getlil(); } } if(vs.changed & (1<getlil(); if(vs.changed & (1<getlil(), 0, 7); if(vs.changed & (1<getlil(); } if(vs.changed & (1<getlil(); } if(vs.changed & (1<getlil(); if(vs.changed & (1<getlil(); vs.alphaback = f->getlil(); } if(vs.changed & (1<getlil(); } if(vs.changed & (1<getlil(); loopk(3) vs.refractcolor[k] = f->getlil(); } if(vs.changed & (1<getlil(); } static void loadvslots(stream *f, int numvslots) { int *prev; try { prev = new int[numvslots]; } catch (...) { return; } memset(prev, -1, numvslots*sizeof(int)); while(numvslots > 0) { int changed = f->getlil(); if(changed < 0) { loopi(-changed) vslots.add(new VSlot(nullptr, vslots.length())); numvslots += changed; } else { prev[vslots.length()] = f->getlil(); loadvslot(f, *vslots.add(new VSlot(nullptr, vslots.length())), changed); numvslots--; } } loopv(vslots) if(vslots.inrange(prev[i])) vslots[prev[i]]->next = vslots[i]; delete[] prev; } static bool save_world(const char *mname, bool nolms) { if(!*mname) mname = game::getclientmap(); setmapfilenames(*mname ? mname : "untitled"); stream *f = opengzfile(ogzname, "wb"); if(!f) { conoutf(CON_WARN, "could not write map to %s", ogzname); return false; } int numvslots = vslots.length(); if(!nolms && !multiplayer(false)) { numvslots = compactvslots(); allchanged(); } savemapprogress = 0; renderprogress(0, "saving map..."); mapheader hdr; memcpy(hdr.magic, "TMAP", 4); hdr.version = MAPVERSION; hdr.headersize = sizeof(hdr); hdr.worldsize = worldsize; hdr.numents = 0; const vector &ents = entities::getents(); loopv(ents) if(ents[i]->type!=ET_EMPTY || nolms) hdr.numents++; hdr.numpvs = nolms ? 0 : getnumviewcells(); hdr.blendmap = shouldsaveblendmap(); hdr.numvars = 0; hdr.numvslots = numvslots; enumerate(idents, ident, id, { if((id.type == ID_VAR || id.type == ID_FVAR || id.type == ID_SVAR) && id.flags&IDF_OVERRIDE && !(id.flags&IDF_READONLY) && id.flags&IDF_OVERRIDDEN) hdr.numvars++; }); lilswap(&hdr.version, 8); f->write(&hdr, sizeof(hdr)); enumerate(idents, ident, id, { if((id.type!=ID_VAR && id.type!=ID_FVAR && id.type!=ID_SVAR) || !(id.flags&IDF_OVERRIDE) || id.flags&IDF_READONLY || !(id.flags&IDF_OVERRIDDEN)) continue; f->putchar(id.type); f->putlil(strlen(id.name)); f->write(id.name, strlen(id.name)); switch(id.type) { case ID_VAR: if(dbgvars) conoutf(CON_DEBUG, "wrote var %s: %d", id.name, *id.storage.i); f->putlil(*id.storage.i); break; case ID_FVAR: if(dbgvars) conoutf(CON_DEBUG, "wrote fvar %s: %f", id.name, *id.storage.f); f->putlil(*id.storage.f); break; case ID_SVAR: if(dbgvars) conoutf(CON_DEBUG, "wrote svar %s: %s", id.name, *id.storage.s); f->putlil(strlen(*id.storage.s)); f->write(*id.storage.s, strlen(*id.storage.s)); break; } }); if(dbgvars) conoutf(CON_DEBUG, "wrote %d vars", hdr.numvars); f->putchar((int)strlen(game::gameident())); f->write(game::gameident(), (int)strlen(game::gameident())+1); f->putlil(entities::extraentinfosize()); vector extras; game::writegamedata(extras); f->putlil(extras.length()); f->write(extras.getbuf(), extras.length()); f->putlil(texmru.length()); loopv(texmru) f->putlil(texmru[i]); char *ebuf = new char[entities::extraentinfosize()]; loopv(ents) { if(ents[i]->type!=ET_EMPTY || nolms) { entity tmp = *ents[i]; lilswap(&tmp.o.x, 3); lilswap(&tmp.attr1, 5); f->write(&tmp, sizeof(entity)); entities::writeent(*ents[i], ebuf); if(entities::extraentinfosize()) f->write(ebuf, entities::extraentinfosize()); } } delete[] ebuf; savevslots(f, numvslots); renderprogress(0, "saving octree..."); savec(worldroot, ivec(0, 0, 0), worldsize>>1, f, nolms); if(!nolms) { if(getnumviewcells()>0) { renderprogress(0, "saving pvs..."); savepvs(f); } } if(shouldsaveblendmap()) { renderprogress(0, "saving blendmap..."); saveblendmap(f); } delete f; conoutf("wrote map file %s", ogzname); return true; } static uint mapcrc = 0; uint getmapcrc() { return mapcrc; } void clearmapcrc() { mapcrc = 0; } bool load_world(const char *mname, const char *cname) // still supports all map formats that have existed since the earliest cube betas! { int loadingstart = SDL_GetTicks(); setmapfilenames(mname, cname); stream *f = opengzfile(ogzname, "rb"); if(!f) { conoutf(CON_ERROR, "could not read map %s", ogzname); return false; } mapheader hdr; octaheader ohdr; memset(&ohdr, 0, sizeof(ohdr)); if(!loadmapheader(f, ogzname, hdr, ohdr)) { delete f; return false; } resetmap(); Texture *mapshot = textureload(picname, 3, true, false); renderbackground("loading...", mapshot, mname, game::getmapinfo()); setvar("mapversion", hdr.version, true, false); renderprogress(0, "clearing world..."); freeocta(worldroot); worldroot = nullptr; int worldscale = 0; while(1<getchar(), ilen = f->getlil(); string name; f->read(name, min(ilen, MAXSTRLEN-1)); name[min(ilen, MAXSTRLEN-1)] = '\0'; if(ilen >= MAXSTRLEN) f->seek(ilen - (MAXSTRLEN-1), SEEK_CUR); ident *id = getident(name); tagval val; string str; switch(type) { case ID_VAR: val.setint(f->getlil()); break; case ID_FVAR: val.setfloat(f->getlil()); break; case ID_SVAR: { int slen = f->getlil(); f->read(str, min(slen, MAXSTRLEN-1)); str[min(slen, MAXSTRLEN-1)] = '\0'; if(slen >= MAXSTRLEN) f->seek(slen - (MAXSTRLEN-1), SEEK_CUR); val.setstr(str); break; } default: continue; } if(id && id->flags&IDF_OVERRIDE) switch(id->type) { case ID_VAR: { int i = val.getint(); if(id->minval <= id->maxval && i >= id->minval && i <= id->maxval) { setvar(name, i); if(dbgvars) conoutf(CON_DEBUG, "read var %s: %d", name, i); } break; } case ID_FVAR: { float f = val.getfloat(); if(id->minvalf <= id->maxvalf && f >= id->minvalf && f <= id->maxvalf) { setfvar(name, f); if(dbgvars) conoutf(CON_DEBUG, "read fvar %s: %f", name, f); } break; } case ID_SVAR: setsvar(name, val.getstr()); if(dbgvars) conoutf(CON_DEBUG, "read svar %s: %s", name, val.getstr()); break; } } if(dbgvars) conoutf(CON_DEBUG, "read %d vars", hdr.numvars); string gametype; bool samegame = true; int len = f->getchar(); if(len >= 0) f->read(gametype, len+1); gametype[max(len, 0)] = '\0'; if(strcmp(gametype, game::gameident())!=0) { samegame = false; conoutf(CON_WARN, "WARNING: loading map from %s game, ignoring entities except for lights/mapmodels", gametype); } int eif = f->getlil(); int extrasize = f->getlil(); vector extras; f->read(extras.pad(extrasize), extrasize); if(samegame) game::readgamedata(extras); texmru.shrink(0); ushort nummru = f->getlil(); loopi(nummru) texmru.add(f->getlil()); renderprogress(0, "loading entities..."); vector &ents = entities::getents(); int einfosize = entities::extraentinfosize(); char *ebuf = einfosize > 0 ? new char[einfosize] : nullptr; loopi(min(hdr.numents, MAXENTS)) { extentity &e = *entities::newentity(); ents.add(&e); f->read(&e, sizeof(entity)); lilswap(&e.o.x, 3); lilswap(&e.attr1, 5); fixent(e, hdr.version); if(samegame) { if(einfosize > 0) f->read(ebuf, einfosize); entities::readent(e, ebuf, mapversion); } else { if(eif > 0) f->seek(eif, SEEK_CUR); if(e.type>=ET_GAMESPECIFIC) { entities::deleteentity(ents.pop()); continue; } } if(!insideworld(e.o)) { if(e.type != ET_LIGHT && e.type != ET_SPOTLIGHT) { conoutf(CON_WARN, "warning: ent outside of world: enttype[%s] index %d (%f, %f, %f)", entities::entname(e.type), i, e.o.x, e.o.y, e.o.z); } } } if(ebuf) delete[] ebuf; if(hdr.numents > MAXENTS) { conoutf(CON_WARN, "warning: map has %d entities", hdr.numents); f->seek((hdr.numents-MAXENTS)*(samegame ? sizeof(entity) + einfosize : eif), SEEK_CUR); } renderprogress(0, "loading slots..."); loadvslots(f, hdr.numvslots); renderprogress(0, "loading octree..."); bool failed = false; worldroot = loadchildren(f, ivec(0, 0, 0), hdr.worldsize>>1, failed); if(failed) conoutf(CON_ERROR, "garbage in map"); renderprogress(0, "validating..."); validatec(worldroot, hdr.worldsize>>1); if(!failed) { if(mapversion <= 0) loopi(ohdr.lightmaps) { int type = f->getchar(); if(type&0x80) { f->getlil(); f->getlil(); } int bpp = 3; if(type&(1<<4) && (type&0x0F)!=2) bpp = 4; f->seek(bpp*LM_PACKW*LM_PACKH, SEEK_CUR); } if(hdr.numpvs > 0) loadpvs(f, hdr.numpvs); if(hdr.blendmap) loadblendmap(f, hdr.blendmap); } mapcrc = f->getcrc(); delete f; conoutf("read map %s (%.1f seconds)", ogzname, (SDL_GetTicks()-loadingstart)/1000.0f); clearmainmenu(); identflags |= IDF_OVERRIDDEN; execfile("config/default_map_settings.cfg", false); execfile(cfgname, false); identflags &= ~IDF_OVERRIDDEN; preloadusedmapmodels(true); game::preload(); flushpreloadedmodels(); //preloadmapsounds(); entitiesinoctanodes(); attachentities(); allchanged(true); renderbackground("loading...", mapshot, mname, game::getmapinfo()); startmap(cname ? cname : mname); return true; } static void savemap(char *mname) { save_world(mname, false); } COMMAND(savemap, "s"); static void writeobj(char *name) { defformatstring(fname, "%s.obj", name); stream *f = openfile(path(fname), "w"); if(!f) return; f->printf("# obj file of Cube 2 level\n\n"); defformatstring(mtlname, "%s.mtl", name); path(mtlname); f->printf("mtllib %s\n\n", mtlname); vector verts, texcoords; hashtable shareverts(1<<16), sharetc(1<<16); hashtable > mtls(1<<8); vector usedmtl; vec bbmin(1e16f, 1e16f, 1e16f), bbmax(-1e16f, -1e16f, -1e16f); loopv(valist) { vtxarray &va = *valist[i]; if(!va.edata || !va.vdata) continue; ushort *edata = va.edata + va.eoffset; vertex *vdata = va.vdata; ushort *idx = edata; loopj(va.texs) { elementset &es = va.texelems[j]; if(usedmtl.find(es.texture) < 0) usedmtl.add(es.texture); vector &keys = mtls[es.texture]; loopk(es.length) { const vertex &v = vdata[idx[k]]; const vec &pos = v.pos; const vec &tc = v.tc; ivec2 &key = keys.add(); key.x = shareverts.access(pos, verts.length()); if(key.x == verts.length()) { verts.add(pos); bbmin.min(pos); bbmax.max(pos); } key.y = sharetc.access(tc, texcoords.length()); if(key.y == texcoords.length()) texcoords.add(tc); } idx += es.length; } } vec center(-(bbmax.x + bbmin.x)/2, -(bbmax.y + bbmin.y)/2, -bbmin.z); loopv(verts) { vec v = verts[i]; v.add(center); if(v.y != floor(v.y)) f->printf("v %.3f ", -v.y); else f->printf("v %d ", int(-v.y)); if(v.z != floor(v.z)) f->printf("%.3f ", v.z); else f->printf("%d ", int(v.z)); if(v.x != floor(v.x)) f->printf("%.3f\n", v.x); else f->printf("%d\n", int(v.x)); } f->printf("\n"); loopv(texcoords) { const vec &tc = texcoords[i]; f->printf("vt %.6f %.6f\n", tc.x, 1-tc.y); } f->printf("\n"); usedmtl.sort(); loopv(usedmtl) { vector &keys = mtls[usedmtl[i]]; f->printf("g slot%d\n", usedmtl[i]); f->printf("usemtl slot%d\n\n", usedmtl[i]); for(int i = 0; i < keys.length(); i += 3) { f->printf("f"); loopk(3) f->printf(" %d/%d", keys[i+2-k].x+1, keys[i+2-k].y+1); f->printf("\n"); } f->printf("\n"); } delete f; f = openfile(mtlname, "w"); if(!f) return; f->printf("# mtl file of Cube 2 level\n\n"); loopv(usedmtl) { VSlot &vslot = lookupvslot(usedmtl[i], false); f->printf("newmtl slot%d\n", usedmtl[i]); f->printf("map_Kd %s\n", vslot.slot->sts.empty() ? notexture->name : path(makerelpath("media", vslot.slot->sts[0].name))); f->printf("\n"); } delete f; conoutf("generated model %s", fname); } COMMAND(writeobj, "s"); static void writecollideobj(char *name) { extern bool havesel; extern selinfo sel; if(!havesel) { conoutf(CON_ERROR, "geometry for collide model not selected"); return; } vector &ents = entities::getents(); extentity *mm = nullptr; loopv(entgroup) { extentity &e = *ents[entgroup[i]]; if(e.type != ET_MAPMODEL || !pointinsel(sel, e.o)) continue; mm = &e; break; } if(!mm) loopv(ents) { extentity &e = *ents[i]; if(e.type != ET_MAPMODEL || !pointinsel(sel, e.o)) continue; mm = &e; break; } if(!mm) { conoutf(CON_ERROR, "could not find map model in selection"); return; } model *m = loadmapmodel(mm->attr1); if(!m) { mapmodelinfo *mmi = getmminfo(mm->attr1); if(mmi) conoutf(CON_ERROR, "could not load map model: %s", mmi->name); else conoutf(CON_ERROR, "could not find map model: %d", mm->attr1); return; } matrix4x3 xform; m->calctransform(xform); float scale = mm->attr5 > 0 ? mm->attr5/100.0f : 1; int yaw = mm->attr2, pitch = mm->attr3, roll = mm->attr4; matrix3 orient; orient.identity(); if(scale != 1) orient.scale(scale); if(yaw) orient.rotate_around_z(sincosmod360(yaw)); if(pitch) orient.rotate_around_x(sincosmod360(pitch)); if(roll) orient.rotate_around_y(sincosmod360(-roll)); xform.mul(orient, mm->o, matrix4x3(xform)); xform.invert(); ivec selmin = sel.o, selmax = ivec(sel.s).mul(sel.grid).add(sel.o); vector verts; hashtable shareverts; vector tris; loopv(valist) { vtxarray &va = *valist[i]; if(va.geommin.x > selmax.x || va.geommin.y > selmax.y || va.geommin.z > selmax.z || va.geommax.x < selmin.x || va.geommax.y < selmin.y || va.geommax.z < selmin.z) continue; if(!va.edata || !va.vdata) continue; ushort *edata = va.edata + va.eoffset; vertex *vdata = va.vdata; ushort *idx = edata; loopj(va.texs) { elementset &es = va.texelems[j]; for(int k = 0; k < es.length; k += 3) { const vec &v0 = vdata[idx[k]].pos, &v1 = vdata[idx[k+1]].pos, &v2 = vdata[idx[k+2]].pos; if(!v0.insidebb(selmin, selmax) || !v1.insidebb(selmin, selmax) || !v2.insidebb(selmin, selmax)) continue; int i0 = shareverts.access(v0, verts.length()); if(i0 == verts.length()) verts.add(v0); tris.add(i0); int i1 = shareverts.access(v1, verts.length()); if(i1 == verts.length()) verts.add(v1); tris.add(i1); int i2 = shareverts.access(v2, verts.length()); if(i2 == verts.length()) verts.add(v2); tris.add(i2); } idx += es.length; } } defformatstring(fname, "%s.obj", name); stream *f = openfile(path(fname), "w"); if(!f) return; f->printf("# obj file of Cube 2 collide model\n\n"); loopv(verts) { vec v = xform.transform(verts[i]); if(v.y != floor(v.y)) f->printf("v %.3f ", -v.y); else f->printf("v %d ", int(-v.y)); if(v.z != floor(v.z)) f->printf("%.3f ", v.z); else f->printf("%d ", int(v.z)); if(v.x != floor(v.x)) f->printf("%.3f\n", v.x); else f->printf("%d\n", int(v.x)); } f->printf("\n"); for(int i = 0; i < tris.length(); i += 3) f->printf("f %d %d %d\n", tris[i+2]+1, tris[i+1]+1, tris[i]+1); f->printf("\n"); delete f; conoutf("generated collide model %s", fname); } COMMAND(writecollideobj, "s");