#include "octaedit.hh" #include #include #include #include #include #include #include #include #include "blend.hh" #include "command.hh" #include "console.hh" /* conoutf */ #include "light.hh" #include "main.hh" #include "material.hh" #include "octa.hh" #include "octarender.hh" #include "physics.hh" #include "pvs.hh" #include "rendergl.hh" #include "renderlights.hh" #include "renderva.hh" #include "texture.hh" #include "world.hh" extern int outline; bool boxoutline = false; void boxs(int orient, vec o, const vec &s, float size) { int d = dimension(orient), dc = dimcoord(orient); float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0; o[D[d]] += dc * s[D[d]] + f; vec r(0, 0, 0), c(0, 0, 0); r[R[d]] = s[R[d]]; c[C[d]] = s[C[d]]; vec v1 = o, v2 = vec(o).add(r), v3 = vec(o).add(r).add(c), v4 = vec(o).add(c); r[R[d]] = 0.5f*size; c[C[d]] = 0.5f*size; gle::defvertex(); gle::begin(GL_TRIANGLE_STRIP); gle::attrib(vec(v1).sub(r).sub(c)); gle::attrib(vec(v1).add(r).add(c)); gle::attrib(vec(v2).add(r).sub(c)); gle::attrib(vec(v2).sub(r).add(c)); gle::attrib(vec(v3).add(r).add(c)); gle::attrib(vec(v3).sub(r).sub(c)); gle::attrib(vec(v4).sub(r).add(c)); gle::attrib(vec(v4).add(r).sub(c)); gle::attrib(vec(v1).sub(r).sub(c)); gle::attrib(vec(v1).add(r).add(c)); xtraverts += gle::end(); } void boxs(int orient, vec o, const vec &s) { int d = dimension(orient), dc = dimcoord(orient); float f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0; o[D[d]] += dc * s[D[d]] + f; gle::defvertex(); gle::begin(GL_LINE_LOOP); gle::attrib(o); o[R[d]] += s[R[d]]; gle::attrib(o); o[C[d]] += s[C[d]]; gle::attrib(o); o[R[d]] -= s[R[d]]; gle::attrib(o); xtraverts += gle::end(); } void boxs3D(const vec &o, vec s, int g) { s.mul(g); loopi(6) boxs(i, o, s); } static void boxsgrid(int orient, vec o, vec s, int g) { int d = dimension(orient), dc = dimcoord(orient); float ox = o[R[d]], oy = o[C[d]], xs = s[R[d]], ys = s[C[d]], f = boxoutline ? (dc>0 ? 0.2f : -0.2f) : 0; o[D[d]] += dc * s[D[d]]*g + f; gle::defvertex(); gle::begin(GL_LINES); loop(x, xs) { o[R[d]] += g; gle::attrib(o); o[C[d]] += ys*g; gle::attrib(o); o[C[d]] = oy; } loop(y, ys) { o[C[d]] += g; o[R[d]] = ox; gle::attrib(o); o[R[d]] += xs*g; gle::attrib(o); } xtraverts += gle::end(); } selinfo sel; // FIXME: extern elsewhere static selinfo lastsel, savedsel; static int orient = 0; static int gridsize = 8; static ivec cor, lastcor; static ivec cur, lastcur; extern int entediting; bool editmode = false; bool havesel = false; static bool hmapsel = false; static int horient = 0; extern int entmoving; VARF(dragging, 0, 0, 1, if(!dragging || cor[0]<0) return; lastcur = cur; lastcor = cor; sel.grid = gridsize; sel.orient = orient; ); static int moving = 0; ICOMMAND(moving, "b", (int *n), { if(*n >= 0) { if(!*n || (moving<=1 && !pointinsel(sel, vec(cur).add(1)))) moving = 0; else if(!moving) moving = 1; } intret(moving); }); VARF(gridpower, 0, 3, 12, { if(dragging) return; gridsize = 1<=worldsize) gridsize = worldsize/2; cancelsel(); }); VAR(passthroughsel, 0, 0, 1); VAR(editing, 1, 0, 0); VAR(selectcorners, 0, 0, 1); VARF(hmapedit, 0, 0, 1, horient = sel.orient); static void forcenextundo() { lastsel.orient = -1; } namespace hmap { static void cancel(); } static void cubecancel() { havesel = false; moving = dragging = hmapedit = passthroughsel = 0; forcenextundo(); hmap::cancel(); } void cancelsel() { cubecancel(); entcancel(); } void toggleedit(bool force) { if(!force) { if(!isconnected()) return; if(player->state!=CS_ALIVE && player->state!=CS_DEAD && player->state!=CS_EDITING) return; // do not allow dead players to edit to avoid state confusion if(!game::allowedittoggle()) return; // not in most multiplayer modes } if(!(editmode = !editmode)) { player->state = player->editstate; player->o.z -= player->eyeheight; // entinmap wants feet pos entinmap(player); // find spawn closest to current floating pos } else { game::resetgamestate(); player->editstate = player->state; player->state = CS_EDITING; } cancelsel(); stoppaintblendmap(); keyrepeat(editmode, KR_EDITMODE); editing = entediting = editmode; if(!force) game::edittoggled(editmode); execident("edittoggled"); } VARP(editinview, 0, 1, 1); bool noedit(bool view, bool msg) { if(!editmode) { if(msg) conoutf(CON_ERROR, "operation only allowed in edit mode"); return true; } if(view || haveselent()) return false; vec o(sel.o), s(sel.s); s.mul(sel.grid / 2.0f); o.add(s); float r = max(s.x, s.y, s.z); bool viewable = (isvisiblesphere(r, o) != VFC_NOT_VISIBLE); if(viewable || !editinview) return false; if(msg) conoutf(CON_ERROR, "selection not in view"); return true; } static void reorient() { sel.cx = 0; sel.cy = 0; sel.cxs = sel.s[R[dimension(orient)]]*2; sel.cys = sel.s[C[dimension(orient)]]*2; sel.orient = orient; } static void selextend() { if(noedit(true)) return; loopi(3) { if(cur[i]=sel.o[i]+sel.s[i]*sel.grid) { sel.s[i] = (cur[i]-sel.o[i])/sel.grid+1; } } } ICOMMAND(edittoggle, "", (), toggleedit(false)); COMMAND(entcancel, ""); COMMAND(cubecancel, ""); COMMAND(cancelsel, ""); COMMAND(reorient, ""); COMMAND(selextend, ""); ICOMMAND(selmoved, "", (), { if(noedit(true)) return; intret(sel.o != savedsel.o ? 1 : 0); }); ICOMMAND(selsave, "", (), { if(noedit(true)) return; savedsel = sel; }); ICOMMAND(selrestore, "", (), { if(noedit(true)) return; sel = savedsel; }); ICOMMAND(selswap, "", (), { if(noedit(true)) return; swap(sel, savedsel); }); ICOMMAND(getselpos, "", (), { if(noedit(true)) return; defformatstring(pos, "%s %s %s", floatstr(sel.o.x), floatstr(sel.o.y), floatstr(sel.o.z)); result(pos); }); static void setselpos(int *x, int *y, int *z) { if(noedit(moving!=0)) return; havesel = true; sel.o = ivec(*x, *y, *z).mask(~(gridsize-1)); } COMMAND(setselpos, "iii"); static void movesel(int *dir, int *dim) { if(noedit(moving!=0)) return; if(*dim < 0 || *dim > 2) return; sel.o[*dim] += *dir * sel.grid; } COMMAND(movesel, "ii"); ///////// selection support ///////////// static cube &blockcube(int x, int y, int z, const block3 &b, int rgrid) // looks up a world cube, based on coordinates mapped by the block { int dim = dimension(b.orient), dc = dimcoord(b.orient); ivec s(dim, x*b.grid, y*b.grid, dc*(b.s[dim]-1)*b.grid); s.add(b.o); if(dc) s[dim] -= z*b.grid; else s[dim] += z*b.grid; return lookupcube(s, rgrid); } #define loopxy(b) loop(y,(b).s[C[dimension((b).orient)]]) loop(x,(b).s[R[dimension((b).orient)]]) #define loopxyz(b, r, f) { loop(z,(b).s[D[dimension((b).orient)]]) loopxy((b)) { cube &c = blockcube(x,y,z,b,r); f; } } #define loopselxyz(f) { if(local) makeundo(); loopxyz(sel, sel.grid, f); changed(sel); } #define selcube(x, y, z) blockcube(x, y, z, sel, sel.grid) ////////////// cursor /////////////// int selchildcount = 0, selchildmat = -1; // octa.hh ICOMMAND(havesel, "", (), intret(havesel ? selchildcount : 0)); ICOMMAND(selchildcount, "", (), { if(selchildcount < 0) result(tempformatstring("1/%d", -selchildcount)); else intret(selchildcount); }); ICOMMAND(selchildmat, "s", (char *prefix), { if(selchildmat > 0) result(getmaterialdesc(selchildmat, prefix)); }); static void countselchild(cube *c, const ivec &cor, int size) { ivec ss = ivec(sel.s).mul(sel.grid); loopoctaboxsize(cor, size, sel.o, ss) { ivec o(i, cor, size); if(c[i].children) countselchild(c[i].children, o, size/2); else { selchildcount++; if(c[i].material != MAT_AIR && selchildmat != MAT_AIR) { if(selchildmat < 0) selchildmat = c[i].material; else if(selchildmat != c[i].material) selchildmat = MAT_AIR; } } } } static void normalizelookupcube(const ivec &o) { if(lusize>gridsize) { lu.x += (o.x-lu.x)/gridsize*gridsize; lu.y += (o.y-lu.y)/gridsize*gridsize; lu.z += (o.z-lu.z)/gridsize*gridsize; } else if(gridsize>lusize) { lu.x &= ~(gridsize-1); lu.y &= ~(gridsize-1); lu.z &= ~(gridsize-1); } lusize = gridsize; } static void updateselection() { sel.o.x = min(lastcur.x, cur.x); sel.o.y = min(lastcur.y, cur.y); sel.o.z = min(lastcur.z, cur.z); sel.s.x = abs(lastcur.x-cur.x)/sel.grid+1; sel.s.y = abs(lastcur.y-cur.y)/sel.grid+1; sel.s.z = abs(lastcur.z-cur.z)/sel.grid+1; } bool editmoveplane(const vec &o, const vec &ray, int d, float off, vec &handle, vec &dest, bool first) { plane pl(d, off); float dist = 0.0f; if(!pl.rayintersect(player->o, ray, dist)) return false; dest = vec(ray).mul(dist).add(player->o); if(first) handle = vec(dest).sub(o); dest.sub(handle); return true; } namespace hmap { inline bool isheightmap(int orient, int d, bool empty, cube *c); } extern float rayent(const vec &o, const vec &ray, float radius, int mode, int size, int &orient, int &ent); VAR(gridlookup, 0, 0, 1); VAR(passthroughcube, 0, 1, 1); void rendereditcursor() { int d = dimension(sel.orient), od = dimension(orient), odc = dimcoord(orient); bool hidecursor = /*UI::hascursor() ||*/ blendpaintmode, hovering = false; hmapsel = false; if(moving) { static vec dest, handle; if(editmoveplane(vec(sel.o), camdir, od, sel.o[D[od]]+odc*sel.grid*sel.s[D[od]], handle, dest, moving==1)) { if(moving==1) { dest.add(handle); handle = vec(ivec(handle).mask(~(sel.grid-1))); dest.sub(handle); moving = 2; } ivec o = ivec(dest).mask(~(sel.grid-1)); sel.o[R[od]] = o[R[od]]; sel.o[C[od]] = o[C[od]]; } } else if(entmoving) { entdrag(camdir); } else { ivec w; float sdist = 0, wdist = 0, t; int entorient = 0, ent = -1; wdist = rayent(player->o, camdir, 1e16f, (editmode && showmat ? RAY_EDITMAT : 0) // select cubes first | (!dragging && entediting ? RAY_ENTS : 0) | RAY_SKIPFIRST | (passthroughcube==1 ? RAY_PASS : 0), gridsize, entorient, ent); if((havesel || dragging) && !passthroughsel && !hmapedit) // now try selecting the selection if(rayboxintersect(vec(sel.o), vec(sel.s).mul(sel.grid), player->o, camdir, sdist, orient)) { // and choose the nearest of the two if(sdist < wdist) { wdist = sdist; ent = -1; } } if((hovering = hoveringonent(hidecursor ? -1 : ent, entorient))) { if(!havesel) { selchildcount = 0; selchildmat = -1; sel.s = ivec(0, 0, 0); } } else { vec w = vec(camdir).mul(wdist+0.05f).add(player->o); if(!insideworld(w)) { loopi(3) wdist = min(wdist, ((camdir[i] > 0 ? worldsize : 0) - player->o[i]) / camdir[i]); w = vec(camdir).mul(wdist-0.05f).add(player->o); if(!insideworld(w)) { wdist = 0; loopi(3) w[i] = std::clamp(player->o[i], 0.0f, float(worldsize)); } } cube *c = &lookupcube(ivec(w)); if(gridlookup && !dragging && !moving && !havesel && hmapedit!=1) gridsize = lusize; int mag = lusize / gridsize; normalizelookupcube(ivec(w)); if(sdist == 0 || sdist > wdist) rayboxintersect(vec(lu), vec(gridsize), player->o, camdir, t=0, orient); // just getting orient cur = lu; cor = ivec(vec(w).mul(2).div(gridsize)); od = dimension(orient); d = dimension(sel.orient); if(hmapedit==1 && dimcoord(horient) == (camdir[dimension(horient)]<0)) { hmapsel = hmap::isheightmap(horient, dimension(horient), false, c); if(hmapsel) od = dimension(orient = horient); } if(dragging) { updateselection(); sel.cx = min(cor[R[d]], lastcor[R[d]]); sel.cy = min(cor[C[d]], lastcor[C[d]]); sel.cxs = max(cor[R[d]], lastcor[R[d]]); sel.cys = max(cor[C[d]], lastcor[C[d]]); if(!selectcorners) { sel.cx &= ~1; sel.cy &= ~1; sel.cxs &= ~1; sel.cys &= ~1; sel.cxs -= sel.cx-2; sel.cys -= sel.cy-2; } else { sel.cxs -= sel.cx-1; sel.cys -= sel.cy-1; } sel.cx &= 1; sel.cy &= 1; havesel = true; } else if(!havesel) { sel.o = lu; sel.s.x = sel.s.y = sel.s.z = 1; sel.cx = sel.cy = 0; sel.cxs = sel.cys = 2; sel.grid = gridsize; sel.orient = orient; d = od; } sel.corner = (cor[R[d]]-(lu[R[d]]*2)/gridsize)+(cor[C[d]]-(lu[C[d]]*2)/gridsize)*2; selchildcount = 0; selchildmat = -1; countselchild(worldroot, ivec(0, 0, 0), worldsize/2); if(mag>=1 && selchildcount==1) { selchildmat = c->material; if(mag>1) selchildcount = -mag; } } } glDisable(GL_CULL_FACE); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); // cursors ldrnotextureshader->set(); renderentselection(player->o, camdir, entmoving!=0); boxoutline = outline!=0; enablepolygonoffset(GL_POLYGON_OFFSET_LINE); if(!moving && !hovering && !hidecursor) { if(hmapedit==1) gle::colorub(0, hmapsel ? 255 : 40, 0); else gle::colorub(120,120,120); boxs(orient, vec(lu), vec(lusize)); } // selections if(havesel || moving) { d = dimension(sel.orient); gle::colorub(50,50,50); // grid boxsgrid(sel.orient, vec(sel.o), vec(sel.s), sel.grid); gle::colorub(200,0,0); // 0 reference boxs3D(vec(sel.o).sub(0.5f*min(gridsize*0.25f, 2.0f)), vec(min(gridsize*0.25f, 2.0f)), 1); gle::colorub(200,200,200);// 2D selection box vec co(sel.o.v), cs(sel.s.v); co[R[d]] += 0.5f*(sel.cx*gridsize); co[C[d]] += 0.5f*(sel.cy*gridsize); cs[R[d]] = 0.5f*(sel.cxs*gridsize); cs[C[d]] = 0.5f*(sel.cys*gridsize); cs[D[d]] *= gridsize; boxs(sel.orient, co, cs); if(hmapedit==1) // 3D selection box gle::colorub(0,120,0); else gle::colorub(0,0,120); boxs3D(vec(sel.o), vec(sel.s), sel.grid); } disablepolygonoffset(GL_POLYGON_OFFSET_LINE); boxoutline = false; glDisable(GL_BLEND); glEnable(GL_CULL_FACE); } void tryedit() { extern int hidehud; if(!editmode || hidehud || mainmenu) return; if(blendpaintmode) trypaintblendmap(); } //////////// ready changes to vertex arrays //////////// static bool haschanged = false; static void readychanges(const ivec &bbmin, const ivec &bbmax, cube *c, const ivec &cor, int size) { loopoctabox(cor, size, bbmin, bbmax) { ivec o(i, cor, size); if(c[i].ext) { if(c[i].ext->va) // removes va s so that octarender will recreate { int hasmerges = c[i].ext->va->hasmerges; destroyva(c[i].ext->va); c[i].ext->va = nullptr; if(hasmerges) invalidatemerges(c[i], o, size, true); } freeoctaentities(c[i]); c[i].ext->tjoints = -1; } if(c[i].children) { if(size<=1) { solidfaces(c[i]); discardchildren(c[i], true); brightencube(c[i]); } else readychanges(bbmin, bbmax, c[i].children, o, size/2); } else brightencube(c[i]); } } void commitchanges(bool force) { if(!force && !haschanged) return; haschanged = false; int oldlen = valist.length(); resetclipplanes(); entitiesinoctanodes(); inbetweenframes = false; octarender(); inbetweenframes = true; setupmaterials(oldlen); clearshadowcache(); updatevabbs(); } void changed(const ivec &bbmin, const ivec &bbmax, bool commit) { readychanges(bbmin, bbmax, worldroot, ivec(0, 0, 0), worldsize/2); haschanged = true; if(commit) commitchanges(); } void changed(const block3 &sel, bool commit) { if(sel.s.iszero()) return; readychanges(ivec(sel.o).sub(1), ivec(sel.s).mul(sel.grid).add(sel.o).add(1), worldroot, ivec(0, 0, 0), worldsize/2); haschanged = true; if(commit) commitchanges(); } //////////// copy and undo ///////////// static inline void copycube(const cube &src, cube &dst) { dst = src; dst.visible = 0; dst.merged = 0; dst.ext = nullptr; // src cube is responsible for va destruction if(src.children) { dst.children = newcubes(F_EMPTY); loopi(8) copycube(src.children[i], dst.children[i]); } } static inline void pastecube(const cube &src, cube &dst) { discardchildren(dst); copycube(src, dst); } static void blockcopy(const block3 &s, int rgrid, block3 *b) { *b = s; cube *q = b->c(); loopxyz(s, rgrid, copycube(c, *q++)); } static block3 *blockcopy(const block3 &s, int rgrid) { int bsize = sizeof(block3)+sizeof(cube)*s.size(); if(bsize <= 0 || bsize > (100<<20)) return nullptr; try { auto *b = (block3 *)new uchar[bsize]; blockcopy(s, rgrid, b); return b; } catch (...) {} return nullptr; } static void freeblock(block3 *b, bool alloced = true) { cube *q = b->c(); loopi(b->size()) discardchildren(*q++); if(alloced) delete[] b; } static void selgridmap(const selinfo &sel, uchar *g) // generates a map of the cube sizes at each grid point { loopxyz(sel, -sel.grid, (*g++ = bitscan(lusize), (void)c)); } static void freeundo(undoblock *u) { if(!u->numents) freeblock(u->block(), false); delete[] (uchar *)u; } static void pasteundoblock(block3 *b, uchar *g) { cube *s = b->c(); loopxyz(*b, 1<numents) pasteundoents(u); else pasteundoblock(u->block(), u->gridmap()); } static inline int undosize(undoblock *u) { if(u->numents) return u->numents*sizeof(undoent); else { block3 *b = u->block(); cube *q = b->c(); int size = b->size(), total = size; loopj(size) total += familysize(*q++)*sizeof(cube); return total; } } struct undolist { undoblock *first, *last; undolist() : first(nullptr), last(nullptr) {} bool empty() { return !first; } void add(undoblock *u) { u->next = nullptr; u->prev = last; if(!first) first = last = u; else { last->next = u; last = u; } } undoblock *popfirst() { undoblock *u = first; first = first->next; if(first) first->prev = nullptr; else last = nullptr; return u; } undoblock *poplast() { undoblock *u = last; last = last->prev; if(last) last->next = nullptr; else first = nullptr; return u; } }; static undolist undos, redos; VARP(undomegs, 0, 5, 100); // bounded by n megs static int totalundos = 0; void pruneundos(int maxremain) // bound memory { while(totalundos > maxremain && !undos.empty()) { undoblock *u = undos.popfirst(); totalundos -= u->size; freeundo(u); } //conoutf(CON_DEBUG, "undo: %d of %d(%%%d)", totalundos, undomegs<<20, totalundos*100/(undomegs<<20)); while(!redos.empty()) { undoblock *u = redos.popfirst(); totalundos -= u->size; freeundo(u); } } static void clearundos() { pruneundos(0); } COMMAND(clearundos, ""); static undoblock *newundocube(const selinfo &s) { int ssize = s.size(), selgridsize = ssize, blocksize = sizeof(block3)+ssize*sizeof(cube); if(blocksize <= 0 || blocksize > (undomegs<<20)) return nullptr; undoblock *u = nullptr; try { u = (undoblock *)new uchar[sizeof(undoblock) + blocksize + selgridsize]; } catch (...) { return nullptr; } u->numents = 0; block3 *b = u->block(); blockcopy(s, -s.grid, b); uchar *g = u->gridmap(); selgridmap(s, g); return u; } void addundo(undoblock *u) { u->size = undosize(u); u->timestamp = totalmillis; undos.add(u); totalundos += u->size; pruneundos(undomegs<<20); } VARP(nompedit, 0, 1, 1); static void makeundo(selinfo &s) { undoblock *u = newundocube(s); if(u) addundo(u); } static void makeundo() // stores state of selected cubes before editing { if(lastsel==sel || sel.s.iszero()) return; lastsel=sel; makeundo(sel); } static inline int countblock(cube *c, int n = 8) { int r = 0; loopi(n) if(c[i].children) r += countblock(c[i].children); else ++r; return r; } static int countblock(block3 *b) { return countblock(b->c(), b->size()); } static void swapundo(undolist &a, undolist &b, int op) { if(noedit()) return; if(a.empty()) { conoutf(CON_WARN, "nothing more to %s", op == EDIT_REDO ? "redo" : "undo"); return; } int ts = a.last->timestamp; if(multiplayer(false)) { int n = 0, ops = 0; for(undoblock *u = a.last; u && ts==u->timestamp; u = u->prev) { ++ops; n += u->numents ? u->numents : countblock(u->block()); if(ops > 10 || n > 2500) { conoutf(CON_WARN, "undo too big for multiplayer"); if(nompedit) { multiplayer(); return; } op = -1; break; } } } selinfo l = sel; while(!a.empty() && ts==a.last->timestamp) { if(op >= 0) game::edittrigger(sel, op); undoblock *u = a.poplast(), *r; if(u->numents) r = copyundoents(u); else { block3 *ub = u->block(); l.o = ub->o; l.s = ub->s; l.grid = ub->grid; l.orient = ub->orient; r = newundocube(l); } if(r) { r->size = u->size; r->timestamp = totalmillis; b.add(r); } pasteundo(u); if(!u->numents) changed(*u->block(), false); freeundo(u); } commitchanges(); if(!hmapsel) { sel = l; reorient(); } forcenextundo(); } static void editundo() { swapundo(undos, redos, EDIT_UNDO); } static void editredo() { swapundo(redos, undos, EDIT_REDO); } // guard against subdivision #define protectsel(f) { undoblock *_u = newundocube(sel); f; if(_u) { pasteundo(_u); freeundo(_u); } } static vector editinfos; editinfo *localedit = nullptr; template static void packcube(cube &c, B &buf) { if(c.children) { buf.put(0xFF); loopi(8) packcube(c.children[i], buf); } else { cube data = c; lilswap(data.texture, 6); buf.put(c.material&0xFF); buf.put(c.material>>8); buf.put(data.edges, sizeof(data.edges)); buf.put((uchar *)data.texture, sizeof(data.texture)); } } template static bool packblock(block3 &b, B &buf) { if(b.size() <= 0 || b.size() > (1<<20)) return false; block3 hdr = b; lilswap(hdr.o.v, 3); lilswap(hdr.s.v, 3); lilswap(&hdr.grid, 1); lilswap(&hdr.orient, 1); buf.put((const uchar *)&hdr, sizeof(hdr)); cube *c = b.c(); loopi(b.size()) packcube(c[i], buf); return true; } struct vslothdr { ushort index; ushort slot; }; static void packvslots(cube &c, vector &buf, vector &used) { if(c.children) { loopi(8) packvslots(c.children[i], buf, used); } else loopi(6) { ushort index = c.texture[i]; if(vslots.inrange(index) && vslots[index]->changed && used.find(index) < 0) { used.add(index); VSlot &vs = *vslots[index]; vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr)); hdr.index = index; hdr.slot = vs.slot->index; lilswap(&hdr.index, 2); packvslot(buf, vs); } } } static void packvslots(block3 &b, vector &buf) { vector used; cube *c = b.c(); loopi(b.size()) packvslots(c[i], buf, used); memset(buf.pad(sizeof(vslothdr)), 0, sizeof(vslothdr)); } template static void unpackcube(cube &c, B &buf) { int mat = buf.get(); if(mat == 0xFF) { c.children = newcubes(F_EMPTY); loopi(8) unpackcube(c.children[i], buf); } else { c.material = mat | (buf.get()<<8); buf.get(c.edges, sizeof(c.edges)); buf.get((uchar *)c.texture, sizeof(c.texture)); lilswap(c.texture, 6); } } template static bool unpackblock(block3 *&b, B &buf) { if(b) { freeblock(b); b = nullptr; } block3 hdr; if(buf.get((uchar *)&hdr, sizeof(hdr)) < int(sizeof(hdr))) return false; lilswap(hdr.o.v, 3); lilswap(hdr.s.v, 3); lilswap(&hdr.grid, 1); lilswap(&hdr.orient, 1); if(hdr.size() > (1<<20) || hdr.grid <= 0 || hdr.grid > (1<<12)) return false; try { b = (block3 *)new uchar[sizeof(block3)+hdr.size()*sizeof(cube)]; } catch (...) { return false; } *b = hdr; cube *c = b->c(); memset(c, 0, b->size()*sizeof(cube)); loopi(b->size()) unpackcube(c[i], buf); return true; } struct vslotmap { int index; VSlot *vslot; vslotmap() {} vslotmap(int index, VSlot *vslot) : index(index), vslot(vslot) {} }; static vector unpackingvslots; static void unpackvslots(cube &c, ucharbuf &buf) { if(c.children) { loopi(8) unpackvslots(c.children[i], buf); } else loopi(6) { ushort tex = c.texture[i]; loopvj(unpackingvslots) if(unpackingvslots[j].index == tex) { c.texture[i] = unpackingvslots[j].vslot->index; break; } } } static void unpackvslots(block3 &b, ucharbuf &buf) { while(buf.remaining() >= int(sizeof(vslothdr))) { vslothdr &hdr = *(vslothdr *)buf.pad(sizeof(vslothdr)); lilswap(&hdr.index, 2); if(!hdr.index) break; VSlot &vs = *lookupslot(hdr.slot, false).variants; VSlot ds; if(!unpackvslot(buf, ds, false)) break; if(vs.index < 0 || vs.index == DEFAULT_SKY) continue; VSlot *edit = editvslot(vs, ds); unpackingvslots.add(vslotmap(hdr.index, edit ? edit : &vs)); } cube *c = b.c(); loopi(b.size()) unpackvslots(c[i], buf); unpackingvslots.setsize(0); } static bool compresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen) { uLongf len = compressBound(inlen); if(len > (1<<20)) return false; try { outbuf = new uchar[len]; } catch (...) { return false; } if(compress2((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen, Z_BEST_COMPRESSION) != Z_OK || len > (1<<16)) { delete[] outbuf; outbuf = nullptr; return false; } outlen = len; return true; } static bool uncompresseditinfo(const uchar *inbuf, int inlen, uchar *&outbuf, int &outlen) { if(compressBound(outlen) > (1<<20)) return false; uLongf len = outlen; try { outbuf = new uchar[len]; } catch (...) { return false; } if(uncompress((Bytef *)outbuf, &len, (const Bytef *)inbuf, inlen) != Z_OK) { delete[] outbuf; outbuf = nullptr; return false; } outlen = len; return true; } bool packeditinfo(editinfo *e, int &inlen, uchar *&outbuf, int &outlen) { vector buf; if(!e || !e->copy || !packblock(*e->copy, buf)) return false; packvslots(*e->copy, buf); inlen = buf.length(); return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen); } bool unpackeditinfo(editinfo *&e, const uchar *inbuf, int inlen, int outlen) { if(e && e->copy) { freeblock(e->copy); e->copy = nullptr; } uchar *outbuf = nullptr; if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false; ucharbuf buf(outbuf, outlen); if(!e) e = editinfos.add(new editinfo); if(!unpackblock(e->copy, buf)) { delete[] outbuf; return false; } unpackvslots(*e->copy, buf); delete[] outbuf; return true; } void freeeditinfo(editinfo *&e) { if(!e) return; editinfos.removeobj(e); if(e->copy) freeblock(e->copy); delete e; e = nullptr; } static bool packundo(undoblock *u, int &inlen, uchar *&outbuf, int &outlen) { vector buf; buf.reserve(512); *(ushort *)buf.pad(2) = lilswap(ushort(u->numents)); if(u->numents) { undoent *ue = u->ents(); loopi(u->numents) { *(ushort *)buf.pad(2) = lilswap(ushort(ue[i].i)); entity &e = *(entity *)buf.pad(sizeof(entity)); e = ue[i].e; lilswap(&e.o.x, 3); lilswap(&e.attr1, 5); } } else { block3 &b = *u->block(); if(!packblock(b, buf)) return false; buf.put(u->gridmap(), b.size()); packvslots(b, buf); } inlen = buf.length(); return compresseditinfo(buf.getbuf(), buf.length(), outbuf, outlen); } bool unpackundo(const uchar *inbuf, int inlen, int outlen) { uchar *outbuf = nullptr; if(!uncompresseditinfo(inbuf, inlen, outbuf, outlen)) return false; ucharbuf buf(outbuf, outlen); if(buf.remaining() < 2) { delete[] outbuf; return false; } int numents = lilswap(*(const ushort *)buf.pad(2)); if(numents) { if(buf.remaining() < numents*int(2 + sizeof(entity))) { delete[] outbuf; return false; } loopi(numents) { int idx = lilswap(*(const ushort *)buf.pad(2)); entity &e = *(entity *)buf.pad(sizeof(entity)); lilswap(&e.o.x, 3); lilswap(&e.attr1, 5); pasteundoent(idx, e); } } else { block3 *b = nullptr; if(!unpackblock(b, buf) || b->grid >= worldsize || buf.remaining() < b->size()) { freeblock(b); delete[] outbuf; return false; } uchar *g = buf.pad(b->size()); unpackvslots(*b, buf); pasteundoblock(b, g); changed(*b, false); freeblock(b); } delete[] outbuf; commitchanges(); return true; } bool packundo(int op, int &inlen, uchar *&outbuf, int &outlen) { switch(op) { case EDIT_UNDO: return !undos.empty() && packundo(undos.last, inlen, outbuf, outlen); case EDIT_REDO: return !redos.empty() && packundo(redos.last, inlen, outbuf, outlen); default: return false; } } struct prefabheader { char magic[4]; int version; }; struct prefab : editinfo { char *name; GLuint ebo, vbo; int numtris, numverts; prefab() : name(nullptr), ebo(0), vbo(0), numtris(0), numverts(0) {} ~prefab() { DELETEA(name); if(copy) freeblock(copy); } void cleanup() { if(ebo) { glDeleteBuffers_(1, &ebo); ebo = 0; } if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; } numtris = numverts = 0; } }; static hashnameset prefabs; void cleanupprefabs() { enumerate(prefabs, prefab, p, p.cleanup()); } static void delprefab(char *name) { prefab *p = prefabs.access(name); if(p) { p->cleanup(); prefabs.remove(name); conoutf("deleted prefab %s", name); } } COMMAND(delprefab, "s"); static void saveprefab(char *name) { if(!name[0] || noedit(true) || (nompedit && multiplayer())) return; prefab *b = prefabs.access(name); if(!b) { b = &prefabs[name]; b->name = newstring(name); } if(b->copy) freeblock(b->copy); protectsel(b->copy = blockcopy(block3(sel), sel.grid)); changed(sel); defformatstring(filename, "media/prefab/%s.obr", name); path(filename); stream *f = opengzfile(filename, "wb"); if(!f) { conoutf(CON_ERROR, "could not write prefab to %s", filename); return; } prefabheader hdr; memcpy(hdr.magic, "OEBR", 4); hdr.version = 0; lilswap(&hdr.version, 1); f->write(&hdr, sizeof(hdr)); streambuf s(f); if(!packblock(*b->copy, s)) { delete f; conoutf(CON_ERROR, "could not pack prefab %s", filename); return; } delete f; conoutf("wrote prefab file %s", filename); } COMMAND(saveprefab, "s"); static void pasteblock(block3 &b, selinfo &sel, bool local) { sel.s = b.s; int o = sel.orient; sel.orient = b.orient; cube *s = b.c(); loopselxyz(if(!isempty(*s) || s->children || s->material != MAT_AIR) pastecube(*s, c); s++); // 'transparent'. old opaque by 'delcube; paste' sel.orient = o; } static prefab *loadprefab(const char *name, bool msg = true) { prefab *b = prefabs.access(name); if(b) return b; defformatstring(filename, "media/prefab/%s.obr", name); path(filename); stream *f = opengzfile(filename, "rb"); if(!f) { if(msg) conoutf(CON_ERROR, "could not read prefab %s", filename); return nullptr; } prefabheader hdr; if(f->read(&hdr, sizeof(hdr)) != sizeof(prefabheader) || memcmp(hdr.magic, "OEBR", 4)) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s has malformatted header", filename); return nullptr; } lilswap(&hdr.version, 1); if(hdr.version != 0) { delete f; if(msg) conoutf(CON_ERROR, "prefab %s uses unsupported version", filename); return nullptr; } streambuf s(f); block3 *copy = nullptr; if(!unpackblock(copy, s)) { delete f; if(msg) conoutf(CON_ERROR, "could not unpack prefab %s", filename); return nullptr; } delete f; b = &prefabs[name]; b->name = newstring(name); b->copy = copy; return b; } static void pasteprefab(char *name) { if(!name[0] || noedit() || (nompedit && multiplayer())) return; prefab *b = loadprefab(name, true); if(b) pasteblock(*b->copy, sel, true); } COMMAND(pasteprefab, "s"); struct prefabmesh { struct vertex { vec pos; bvec4 norm; }; static const int SIZE = 1<<9; int table[SIZE]; vector verts; vector chain; vector tris; prefabmesh() { memset(table, -1, sizeof(table)); } int addvert(const vertex &v) { uint h = hthash(v.pos)&(SIZE-1); for(int i = table[h]; i>=0; i = chain[i]) { const vertex &c = verts[i]; if(c.pos==v.pos && c.norm==v.norm) return i; } if(verts.length() >= USHRT_MAX) return -1; verts.add(v); chain.add(table[h]); return table[h] = verts.length()-1; } int addvert(const vec &pos, const bvec &norm) { vertex vtx; vtx.pos = pos; vtx.norm = norm; return addvert(vtx); } void setup(prefab &p) { if(tris.empty()) return; p.cleanup(); loopv(verts) verts[i].norm.flip(); if(!p.vbo) glGenBuffers_(1, &p.vbo); gle::bindvbo(p.vbo); glBufferData_(GL_ARRAY_BUFFER, verts.length()*sizeof(vertex), verts.getbuf(), GL_STATIC_DRAW); gle::clearvbo(); p.numverts = verts.length(); if(!p.ebo) glGenBuffers_(1, &p.ebo); gle::bindebo(p.ebo); glBufferData_(GL_ELEMENT_ARRAY_BUFFER, tris.length()*sizeof(ushort), tris.getbuf(), GL_STATIC_DRAW); gle::clearebo(); p.numtris = tris.length()/3; } }; static void genprefabmesh(prefabmesh &r, cube &c, const ivec &co, int size) { if(c.children) { neighbourstack[++neighbourdepth] = c.children; loopi(8) { ivec o(i, co, size/2); genprefabmesh(r, c.children[i], o, size/2); } --neighbourdepth; } else if(!isempty(c)) { int vis; loopi(6) if((vis = visibletris(c, i, co, size))) { ivec v[4]; genfaceverts(c, i, v); int convex = 0; if(!flataxisface(c, i)) convex = faceconvexity(v); int order = vis&4 || convex < 0 ? 1 : 0, numverts = 0; vec vo(co), pos[4], norm[4]; pos[numverts++] = vec(v[order]).mul(size/8.0f).add(vo); if(vis&1) pos[numverts++] = vec(v[order+1]).mul(size/8.0f).add(vo); pos[numverts++] = vec(v[order+2]).mul(size/8.0f).add(vo); if(vis&2) pos[numverts++] = vec(v[(order+3)&3]).mul(size/8.0f).add(vo); guessnormals(pos, numverts, norm); int index[4]; loopj(numverts) index[j] = r.addvert(pos[j], bvec(norm[j])); loopj(numverts-2) if(index[0]!=index[j+1] && index[j+1]!=index[j+2] && index[j+2]!=index[0]) { r.tris.add(index[0]); r.tris.add(index[j+1]); r.tris.add(index[j+2]); } } } } static void genprefabmesh(prefab &p) { block3 b = *p.copy; b.o = ivec(0, 0, 0); cube *oldworldroot = worldroot; int oldworldscale = worldscale, oldworldsize = worldsize; worldroot = newcubes(); worldscale = 1; worldsize = 2; while(worldsize < max(max(b.s.x, b.s.y), b.s.z)*b.grid) { worldscale++; worldsize *= 2; } cube *s = p.copy->c(); loopxyz(b, b.grid, if(!isempty(*s) || s->children) pastecube(*s, c); s++); prefabmesh r; neighbourstack[++neighbourdepth] = worldroot; loopi(8) genprefabmesh(r, worldroot[i], ivec(i, ivec(0, 0, 0), worldsize/2), worldsize/2); --neighbourdepth; r.setup(p); freeocta(worldroot); worldroot = oldworldroot; worldscale = oldworldscale; worldsize = oldworldsize; useshaderbyname("prefab"); } extern bvec outlinecolour; static void renderprefab(prefab &p, const vec &o, float yaw, float pitch, float roll, float size, const vec &color) { if(!p.numtris) { genprefabmesh(p); if(!p.numtris) return; } block3 &b = *p.copy; matrix4 m; m.identity(); m.settranslation(o); if(yaw) m.rotate_around_z(yaw*RAD); if(pitch) m.rotate_around_x(pitch*RAD); if(roll) m.rotate_around_y(-roll*RAD); matrix3 w(m); if(size > 0 && size != 1) m.scale(size); m.translate(vec(b.s).mul(-b.grid*0.5f)); gle::bindvbo(p.vbo); gle::bindebo(p.ebo); gle::enablevertex(); gle::enablenormal(); prefabmesh::vertex *v = (prefabmesh::vertex *)0; gle::vertexpointer(sizeof(prefabmesh::vertex), v->pos.v); gle::normalpointer(sizeof(prefabmesh::vertex), v->norm.v, GL_BYTE); matrix4 pm; pm.mul(camprojmatrix, m); GLOBALPARAM(prefabmatrix, pm); GLOBALPARAM(prefabworld, w); SETSHADER(prefab); gle::color(vec(color).mul(ldrscale)); glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); enablepolygonoffset(GL_POLYGON_OFFSET_LINE); pm.mul(camprojmatrix, m); GLOBALPARAM(prefabmatrix, pm); SETSHADER(prefab); gle::color((outlinecolour).tocolor().mul(ldrscale)); glDrawRangeElements_(GL_TRIANGLES, 0, p.numverts-1, p.numtris*3, GL_UNSIGNED_SHORT, (ushort *)0); disablepolygonoffset(GL_POLYGON_OFFSET_LINE); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); gle::disablevertex(); gle::disablenormal(); gle::clearebo(); gle::clearvbo(); } void renderprefab(const char *name, const vec &o, float yaw, float pitch, float roll, float size, const vec &color) { prefab *p = loadprefab(name, false); if(p) renderprefab(*p, o, yaw, pitch, roll, size, color); } void previewprefab(const char *name, const vec &color) { prefab *p = loadprefab(name, false); if(p) { block3 &b = *p->copy; float yaw; vec o = calcmodelpreviewpos(vec(b.s).mul(b.grid*0.5f), yaw); renderprefab(*p, o, yaw, 0, 0, 1, color); } } void mpcopy(editinfo *&e, selinfo &sel, bool local) { if(local) game::edittrigger(sel, EDIT_COPY); if(e==nullptr) e = editinfos.add(new editinfo); if(e->copy) freeblock(e->copy); e->copy = nullptr; protectsel(e->copy = blockcopy(block3(sel), sel.grid)); changed(sel); } void mppaste(editinfo *&e, selinfo &sel, bool local) { if(e==nullptr) return; if(local) game::edittrigger(sel, EDIT_PASTE); if(e->copy) pasteblock(*e->copy, sel, local); } static void copy() { if(noedit(true)) return; mpcopy(localedit, sel, true); } static void pastehilite() { if(!localedit) return; sel.s = localedit->copy->s; reorient(); havesel = true; } static void paste() { if(noedit(true)) return; mppaste(localedit, sel, true); } COMMAND(copy, ""); COMMAND(pastehilite, ""); COMMAND(paste, ""); COMMANDN(undo, editundo, ""); COMMANDN(redo, editredo, ""); static vector editingvslots; struct vslotref { vslotref(int &index) { editingvslots.add(&index); } ~vslotref() { editingvslots.pop(); } }; #define editingvslot(...) vslotref vslotrefs[] = { __VA_ARGS__ }; (void)vslotrefs; void compacteditvslots() { loopv(editingvslots) if(*editingvslots[i]) compactvslot(*editingvslots[i]); loopv(unpackingvslots) compactvslot(*unpackingvslots[i].vslot); loopv(editinfos) { editinfo *e = editinfos[i]; compactvslots(e->copy->c(), e->copy->size()); } for(undoblock *u = undos.first; u; u = u->next) if(!u->numents) compactvslots(u->block()->c(), u->block()->size()); for(undoblock *u = redos.first; u; u = u->next) if(!u->numents) compactvslots(u->block()->c(), u->block()->size()); } ///////////// height maps //////////////// namespace hmap { static vector textures; static void cancel() { textures.setsize(0); } ICOMMAND(hmapcancel, "", (), cancel()); ICOMMAND(hmapselect, "", (), int t = lookupcube(cur).texture[orient]; int i = textures.find(t); if(i<0) textures.add(t); else textures.remove(i); ); inline bool isheightmap(int o, int d, bool empty, cube *c) { return havesel || (empty && isempty(*c)) || textures.empty() || textures.find(c->texture[o]) >= 0; } #define MAXBRUSH 64 #define MAXBRUSHC 63 #define MAXBRUSH2 32 int brush[MAXBRUSH][MAXBRUSH]; VARN(hbrushx, brushx, 0, MAXBRUSH2, MAXBRUSH); VARN(hbrushy, brushy, 0, MAXBRUSH2, MAXBRUSH); bool paintbrush = 0; int brushmaxx = 0, brushminx = MAXBRUSH; int brushmaxy = 0, brushminy = MAXBRUSH; static void clearhbrush() { memset(brush, 0, sizeof brush); brushmaxx = brushmaxy = 0; brushminx = brushminy = MAXBRUSH; paintbrush = false; } COMMAND(clearhbrush, ""); static void hbrushvert(int *x, int *y, int *v) { *x += MAXBRUSH2 - brushx + 1; // +1 for automatic padding *y += MAXBRUSH2 - brushy + 1; if(*x<0 || *y<0 || *x>=MAXBRUSH || *y>=MAXBRUSH) return; brush[*x][*y] = std::clamp(*v, 0, 8); paintbrush = paintbrush || (brush[*x][*y] > 0); brushmaxx = min(MAXBRUSH-1, max(brushmaxx, *x+1)); brushmaxy = min(MAXBRUSH-1, max(brushmaxy, *y+1)); brushminx = max(0, min(brushminx, *x-1)); brushminy = max(0, min(brushminy, *y-1)); } COMMAND(hbrushvert, "iii"); #define PAINTED 1 #define NOTHMAP 2 #define MAPPED 16 static uchar flags[MAXBRUSH][MAXBRUSH]; static cube *cmap[MAXBRUSHC][MAXBRUSHC][4]; static int mapz[MAXBRUSHC][MAXBRUSHC]; static int map [MAXBRUSH][MAXBRUSH]; static selinfo changes; static bool selecting; static int d, dc, dr, dcr, biasup, br, hws, fg; static int gx, gy, gz, mx, my, mz, nx, ny, nz, bmx, bmy, bnx, bny; static uint fs; static selinfo hundo; static cube *getcube(ivec t, int f) { t[d] += dcr*f*gridsize; if(t[d] > nz || t[d] < mz) return nullptr; cube *c = &lookupcube(t, gridsize); if(c->children) forcemip(*c, false); discardchildren(*c, true); if(!isheightmap(sel.orient, d, true, c)) return nullptr; if (t.x < changes.o.x) changes.o.x = t.x; else if(t.x > changes.s.x) changes.s.x = t.x; if (t.y < changes.o.y) changes.o.y = t.y; else if(t.y > changes.s.y) changes.s.y = t.y; if (t.z < changes.o.z) changes.o.z = t.z; else if(t.z > changes.s.z) changes.s.z = t.z; return c; } static uint getface(cube *c, int d) { return 0x0f0f0f0f & ((dc ? c->faces[d] : 0x88888888 - c->faces[d]) >> fs); } static void pushside(cube &c, int d, int x, int y, int z) { ivec a; getcubevector(c, d, x, y, z, a); a[R[d]] = 8 - a[R[d]]; setcubevector(c, d, x, y, z, a); } static void addpoint(int x, int y, int z, int v) { if(!(flags[x][y] & MAPPED)) map[x][y] = v + (z*8); flags[x][y] |= MAPPED; } static void select(int x, int y, int z) { if((NOTHMAP & flags[x][y]) || (PAINTED & flags[x][y])) return; ivec t(d, x+gx, y+gy, dc ? z : hws-z); t.shl(gridpower); // selections may damage; must makeundo before hundo.o = t; hundo.o[D[d]] -= dcr*gridsize*2; makeundo(hundo); cube **c = cmap[x][y]; loopk(4) c[k] = nullptr; c[1] = getcube(t, 0); if(!c[1] || !isempty(*c[1])) { // try up c[2] = c[1]; c[1] = getcube(t, 1); if(!c[1] || isempty(*c[1])) { c[0] = c[1]; c[1] = c[2]; c[2] = nullptr; } else { z++; t[d]+=fg; } } else // drop down { z--; t[d]-= fg; c[0] = c[1]; c[1] = getcube(t, 0); } if(!c[1] || isempty(*c[1])) { flags[x][y] |= NOTHMAP; return; } flags[x][y] |= PAINTED; mapz [x][y] = z; if(!c[0]) c[0] = getcube(t, 1); if(!c[2]) c[2] = getcube(t, -1); c[3] = getcube(t, -2); c[2] = !c[2] || isempty(*c[2]) ? nullptr : c[2]; c[3] = !c[3] || isempty(*c[3]) ? nullptr : c[3]; uint face = getface(c[1], d); if(face == 0x08080808 && (!c[0] || !isempty(*c[0]))) { flags[x][y] |= NOTHMAP; return; } if(c[1]->faces[R[d]] == F_SOLID) // was single face += 0x08080808; else // was pair face += c[2] ? getface(c[2], d) : 0x08080808; face += 0x08080808; // c[3] uchar *f = (uchar*)&face; addpoint(x, y, z, f[0]); addpoint(x+1, y, z, f[1]); addpoint(x, y+1, z, f[2]); addpoint(x+1, y+1, z, f[3]); if(selecting) // continue to adjacent cubes { if(x>bmx) select(x-1, y, z); if(xbmy) select(x, y-1, z); if(y, <, 1, 0, -); else pullhmap(worldsize*8, <, >, 0, 8, +); cube **c = cmap[x][y]; int e[2][2]; int notempty = 0; loopk(4) if(c[k]) { loopi(2) loopj(2) { e[i][j] = min(8, map[x+i][y+j] - (mapz[x][y]+3-k)*8); notempty |= e[i][j] > 0; } if(notempty) { c[k]->texture[sel.orient] = c[1]->texture[sel.orient]; solidfaces(*c[k]); loopi(2) loopj(2) { int f = e[i][j]; if(f<0 || (f==0 && e[1-i][j]==0 && e[i][1-j]==0)) { f=0; pushside(*c[k], d, i, j, 0); pushside(*c[k], d, i, j, 1); } edgeset(cubeedge(*c[k], d, i, j), dc, dc ? f : 8-f); } } else emptyfaces(*c[k]); } if(!changed) return; if(x>mx) ripple(x-1, y, mapz[x][y], true); if(xmy) ripple(x, y-1, mapz[x][y], true); if(ymx && y>my)); // do diagonals because adjacents DIAGONAL_RIPPLE(-1, +1, (x>mx && ymy)); } #define loopbrush(i) for(int x=bmx; x<=bnx+i; x++) for(int y=bmy; y<=bny+i; y++) static void paint() { loopbrush(1) map[x][y] -= dr * brush[x][y]; } static void smooth() { int sum, div; loopbrush(-2) { sum = 0; div = 9; loopi(3) loopj(3) if(flags[x+i][y+j] & MAPPED) sum += map[x+i][y+j]; else div--; if(div) map[x+1][y+1] = sum / div; } } static void rippleandset() { loopbrush(0) ripple(x, y, gz, false); } static void run(int dir, int mode) { d = dimension(sel.orient); dc = dimcoord(sel.orient); dcr= dc ? 1 : -1; dr = dir>0 ? 1 : -1; br = dir>0 ? 0x08080808 : 0; // biasup = mode == dir<0; biasup = dir<0; bool paintme = paintbrush; int cx = (sel.corner&1 ? 0 : -1); int cy = (sel.corner&2 ? 0 : -1); hws= (worldsize>>gridpower); gx = (cur[R[d]] >> gridpower) + cx - MAXBRUSH2; gy = (cur[C[d]] >> gridpower) + cy - MAXBRUSH2; gz = (cur[D[d]] >> gridpower); fs = dc ? 4 : 0; fg = dc ? gridsize : -gridsize; mx = max(0, -gx); // ripple range my = max(0, -gy); nx = min(MAXBRUSH-1, hws-gx) - 1; ny = min(MAXBRUSH-1, hws-gy) - 1; if(havesel) { // selection range bmx = mx = max(mx, (sel.o[R[d]]>>gridpower)-gx); bmy = my = max(my, (sel.o[C[d]]>>gridpower)-gy); bnx = nx = min(nx, (sel.s[R[d]]+(sel.o[R[d]]>>gridpower))-gx-1); bny = ny = min(ny, (sel.s[C[d]]+(sel.o[C[d]]>>gridpower))-gy-1); } if(havesel && mode<0) // -ve means smooth selection paintme = false; else { // brush range bmx = max(mx, brushminx); bmy = max(my, brushminy); bnx = min(nx, brushmaxx-1); bny = min(ny, brushmaxy-1); } nz = worldsize-gridsize; mz = 0; hundo.s = ivec(d,1,1,5); hundo.orient = sel.orient; hundo.grid = gridsize; forcenextundo(); changes.grid = gridsize; changes.s = changes.o = cur; memset(map, 0, sizeof map); memset(flags, 0, sizeof flags); selecting = true; select(std::clamp(MAXBRUSH2-cx, bmx, bnx), std::clamp(MAXBRUSH2-cy, bmy, bny), dc ? gz : hws - gz); selecting = false; if(paintme) paint(); else smooth(); rippleandset(); // pull up points to cubify, and set changes.s.sub(changes.o).shr(gridpower).add(1); changed(changes); } } static void edithmap(int dir, int mode) { if((nompedit && multiplayer()) || !hmapsel) return; hmap::run(dir, mode); } ///////////// main cube edit //////////////// inline int bounded(int n) { return n<0 ? 0 : (n>8 ? 8 : n); } static void pushedge(uchar &edge, int dir, int dc) { int ne = bounded(edgeget(edge, dc)+dir); edgeset(edge, dc, ne); int oe = edgeget(edge, 1-dc); if((dir<0 && dc && oe>ne) || (dir>0 && dc==0 && oe0) == dc && h<=0) || ((dir<0) == dc && h>=worldsize)) return; if(dir<0) sel.o[d] += sel.grid * seldir; } if(dc) sel.o[d] += sel.us(d)-sel.grid; sel.s[d] = 1; loopselxyz( if(c.children) solidfaces(c); ushort mat = getmaterial(c); discardchildren(c, true); c.material = mat; if(mode==1) // fill command { if(dir<0) { solidfaces(c); cube &o = blockcube(x, y, 1, sel, -sel.grid); loopi(6) c.texture[i] = o.children ? DEFAULT_GEOM : o.texture[i]; } else emptyfaces(c); } else { uint bak = c.faces[d]; uchar *p = (uchar *)&c.faces[d]; if(mode==2) linkedpush(c, d, sel.corner&1, sel.corner>>1, dc, seldir); // corner command else { loop(mx,2) loop(my,2) // pull/push edges command { if(x==0 && mx==0 && sel.cx) continue; if(y==0 && my==0 && sel.cy) continue; if(x==sel.s[R[d]]-1 && mx==1 && (sel.cx+sel.cxs)&1) continue; if(y==sel.s[C[d]]-1 && my==1 && (sel.cy+sel.cys)&1) continue; if(p[mx+my*2] != ((uchar *)&bak)[mx+my*2]) continue; linkedpush(c, d, mx, my, dc, seldir); } } optiface(p, c); if(invalidcubeguard==1 && !isvalidcube(c)) { uint newbak = c.faces[d]; uchar *m = (uchar *)&bak; uchar *n = (uchar *)&newbak; loopk(4) if(n[k] != m[k]) // tries to find partial edit that is valid { c.faces[d] = bak; c.edges[d*4+k] = n[k]; if(isvalidcube(c)) m[k] = n[k]; } c.faces[d] = bak; } } ); if (mode==1 && dir>0) sel.o[d] += sel.grid * seldir; } static void editface(int *dir, int *mode) { if(noedit(moving!=0)) return; if(hmapedit!=1) mpeditface(*dir, *mode, sel, true); else edithmap(*dir, *mode); } VAR(selectionsurf, 0, 0, 1); static void pushsel(int *dir) { if(noedit(moving!=0)) return; int d = dimension(orient); int s = dimcoord(orient) ? -*dir : *dir; sel.o[d] += s*sel.grid; if(selectionsurf==1) { player->o[d] += s*sel.grid; player->resetinterp(); } } void mpdelcube(selinfo &sel, bool local) { if(local) game::edittrigger(sel, EDIT_DELCUBE); loopselxyz(discardchildren(c, true); emptyfaces(c)); } static void delcube() { if(noedit(true)) return; mpdelcube(sel, true); } COMMAND(pushsel, "i"); COMMAND(editface, "ii"); COMMAND(delcube, ""); /////////// texture editing ////////////////// static int curtexindex = -1, lasttex = 0, lasttexmillis = -1; static int texpaneltimer = 0; vector texmru; static void tofronttex() // maintain most recently used of the texture lists when applying texture { int c = curtexindex; if(texmru.inrange(c)) { texmru.insert(0, texmru.remove(c)); curtexindex = -1; } } static selinfo repsel; static int reptex = -1; static vector remappedvslots; static VSlot *remapvslot(int index, bool delta, const VSlot &ds) { loopv(remappedvslots) if(remappedvslots[i].index == index) return remappedvslots[i].vslot; VSlot &vs = lookupvslot(index, false); if(vs.index < 0 || vs.index == DEFAULT_SKY) return nullptr; VSlot *edit = nullptr; if(delta) { VSlot ms; mergevslot(ms, vs, ds); edit = ms.changed ? editvslot(vs, ms) : vs.slot->variants; } else edit = ds.changed ? editvslot(vs, ds) : vs.slot->variants; if(!edit) edit = &vs; remappedvslots.add(vslotmap(vs.index, edit)); return edit; } static void remapvslots(cube &c, bool delta, const VSlot &ds, int orient, bool &findrep, VSlot *&findedit) { if(c.children) { loopi(8) remapvslots(c.children[i], delta, ds, orient, findrep, findedit); return; } static VSlot ms; if(orient<0) loopi(6) { VSlot *edit = remapvslot(c.texture[i], delta, ds); if(edit) { c.texture[i] = edit->index; if(!findedit) findedit = edit; } } else { int i = visibleorient(c, orient); VSlot *edit = remapvslot(c.texture[i], delta, ds); if(edit) { if(findrep) { if(reptex < 0) reptex = c.texture[i]; else if(reptex != c.texture[i]) findrep = false; } c.texture[i] = edit->index; if(!findedit) findedit = edit; } } } static void edittexcube(cube &c, int tex, int orient, bool &findrep) { if(orient<0) loopi(6) c.texture[i] = tex; else { int i = visibleorient(c, orient); if(findrep) { if(reptex < 0) reptex = c.texture[i]; else if(reptex != c.texture[i]) findrep = false; } c.texture[i] = tex; } if(c.children) loopi(8) edittexcube(c.children[i], tex, orient, findrep); } void mpeditvslot(int delta, VSlot &ds, int allfaces, selinfo &sel, bool local) { if(local) { game::edittrigger(sel, EDIT_VSLOT, delta, allfaces, 0, &ds); if(!(lastsel==sel)) tofronttex(); if(allfaces || !(repsel == sel)) reptex = -1; repsel = sel; } bool findrep = local && !allfaces && reptex < 0; VSlot *findedit = nullptr; loopselxyz(remapvslots(c, delta != 0, ds, allfaces ? -1 : sel.orient, findrep, findedit)); remappedvslots.setsize(0); if(local && findedit) { lasttex = findedit->index; lasttexmillis = totalmillis; curtexindex = texmru.find(lasttex); if(curtexindex < 0) { curtexindex = texmru.length(); texmru.add(lasttex); } } } bool mpeditvslot(int delta, int allfaces, selinfo &sel, ucharbuf &buf) { VSlot ds; if(!unpackvslot(buf, ds, delta != 0)) return false; editingvslot(ds.layer, ds.detail); mpeditvslot(delta, ds, allfaces, sel, false); return true; } VAR(allfaces, 0, 0, 1); VAR(usevdelta, 1, 0, 0); static void vdelta(uint *body) { if(noedit()) return; usevdelta++; execute(body); usevdelta--; } COMMAND(vdelta, "e"); static void vrotate(int *n) { if(noedit()) return; VSlot ds; ds.changed = 1<changed && nompedit && multiplayer()) return; } editingvslot(ds.layer); mpeditvslot(usevdelta, ds, allfaces, sel, true); } COMMAND(vlayer, "i"); ICOMMAND(getvlayer, "i", (int *tex), intret(lookupvslot(*tex, false).layer)); static void vdetail(int *n) { if(noedit()) return; VSlot ds; ds.changed = 1<changed && nompedit && multiplayer()) return; } editingvslot(ds.detail); mpeditvslot(usevdelta, ds, allfaces, sel, true); } COMMAND(vdetail, "i"); ICOMMAND(getvdetail, "i", (int *tex), intret(lookupvslot(*tex, false).detail)); static void valpha(float *front, float *back) { if(noedit()) return; VSlot ds; ds.changed = 1< 0 && (*r > 0 || *g > 0 || *b > 0)) ds.refractcolor = vec(std::clamp(*r, 0.0f, 1.0f), std::clamp(*g, 0.0f, 1.0f), std::clamp(*b, 0.0f, 1.0f)); else ds.refractcolor = vec(1, 1, 1); mpeditvslot(usevdelta, ds, allfaces, sel, true); } COMMAND(vrefract, "ffff"); ICOMMAND(getvrefract, "i", (int *tex), { VSlot &vslot = lookupvslot(*tex, false); defformatstring(str, "%s %s %s %s", floatstr(vslot.refractscale), floatstr(vslot.refractcolor.r), floatstr(vslot.refractcolor.g), floatstr(vslot.refractcolor.b)); result(str); }); static void vreset() { if(noedit()) return; VSlot ds; mpeditvslot(usevdelta, ds, allfaces, sel, true); } COMMAND(vreset, ""); static void vshaderparam(const char *name, float *x, float *y, float *z, float *w) { if(noedit()) return; VSlot ds; ds.changed = 1< str; loopv(vslot.params) { SlotShaderParam &p = vslot.params[i]; if(i) str.put(' '); str.put(p.name, strlen(p.name)); } str.add('\0'); stringret(newstring(str.getbuf(), str.length()-1)); }); void mpedittex(int tex, int allfaces, selinfo &sel, bool local) { if(local) { game::edittrigger(sel, EDIT_TEX, tex, allfaces); if(allfaces || !(repsel == sel)) reptex = -1; repsel = sel; } bool findrep = local && !allfaces && reptex < 0; loopselxyz(edittexcube(c, tex, allfaces ? -1 : sel.orient, findrep)); } static int unpacktex(int &tex, ucharbuf &buf, bool insert = true) { if(tex < 0x10000) return true; VSlot ds; if(!unpackvslot(buf, ds, false)) return false; VSlot &vs = *lookupslot(tex & 0xFFFF, false).variants; if(vs.index < 0 || vs.index == DEFAULT_SKY) return false; VSlot *edit = insert ? editvslot(vs, ds) : findvslot(*vs.slot, vs, ds); if(!edit) return false; tex = edit->index; return true; } int shouldpacktex(int index) { if(vslots.inrange(index)) { VSlot &vs = *vslots[index]; if(vs.changed) return 0x10000 + vs.slot->index; } return 0; } bool mpedittex(int tex, int allfaces, selinfo &sel, ucharbuf &buf) { if(!unpacktex(tex, buf)) return false; mpedittex(tex, allfaces, sel, false); return true; } static void filltexlist() { if(texmru.length()!=vslots.length()) { loopvrev(texmru) if(texmru[i]>=vslots.length()) { if(curtexindex > i) curtexindex--; else if(curtexindex == i) curtexindex = -1; texmru.remove(i); } loopv(vslots) if(texmru.find(i)<0) texmru.add(i); } } void compactmruvslots() { remappedvslots.setsize(0); loopvrev(texmru) { if(vslots.inrange(texmru[i])) { VSlot &vs = *vslots[texmru[i]]; if(vs.index >= 0) { texmru[i] = vs.index; continue; } } if(curtexindex > i) curtexindex--; else if(curtexindex == i) curtexindex = -1; texmru.remove(i); } if(vslots.inrange(lasttex)) { VSlot &vs = *vslots[lasttex]; lasttex = vs.index >= 0 ? vs.index : 0; } else lasttex = 0; reptex = vslots.inrange(reptex) ? vslots[reptex]->index : -1; } static void edittex(int i, bool save = true) { lasttex = i; lasttexmillis = totalmillis; if(save) { loopvj(texmru) if(texmru[j]==lasttex) { curtexindex = j; break; } } mpedittex(i, allfaces, sel, true); } static void edittex_(int *dir) { if(noedit()) return; filltexlist(); if(texmru.empty()) return; texpaneltimer = 5000; if(!(lastsel==sel)) tofronttex(); curtexindex = std::clamp(curtexindex<0 ? 0 : curtexindex+*dir, 0, texmru.length()-1); edittex(texmru[curtexindex], false); } static void gettex() { if(noedit(true)) return; filltexlist(); int tex = -1; loopxyz(sel, sel.grid, tex = c.texture[sel.orient]); loopv(texmru) if(texmru[i]==tex) { curtexindex = i; tofronttex(); return; } } static void getcurtex() { if(noedit(true)) return; filltexlist(); int index = curtexindex < 0 ? 0 : curtexindex; if(!texmru.inrange(index)) return; intret(texmru[index]); } static void getseltex() { if(noedit(true)) return; cube &c = lookupcube(sel.o, -sel.grid); if(c.children || isempty(c)) return; intret(c.texture[sel.orient]); } static void gettexname(int *tex, int *subslot) { if(*tex<0) return; VSlot &vslot = lookupvslot(*tex, false); Slot &slot = *vslot.slot; if(!slot.sts.inrange(*subslot)) return; result(slot.sts[*subslot].name); } static void getslottex(int *idx) { if(*idx < 0 || !slots.inrange(*idx)) { intret(-1); return; } Slot &slot = lookupslot(*idx, false); intret(slot.variants->index); } COMMANDN(edittex, edittex_, "i"); ICOMMAND(settex, "i", (int *tex), { if(!vslots.inrange(*tex) || noedit()) return; filltexlist(); edittex(*tex); }); COMMAND(gettex, ""); COMMAND(getcurtex, ""); COMMAND(getseltex, ""); ICOMMAND(getreptex, "", (), { if(!noedit()) intret(vslots.inrange(reptex) ? reptex : -1); }); COMMAND(gettexname, "ii"); ICOMMAND(texmru, "b", (int *idx), { filltexlist(); intret(texmru.inrange(*idx) ? texmru[*idx] : texmru.length()); }); ICOMMAND(looptexmru, "re", (ident *id, uint *body), { loopstart(id, stack); filltexlist(); loopv(texmru) { loopiter(id, stack, texmru[i]); execute(body); } loopend(id, stack); }); ICOMMAND(numvslots, "", (), intret(vslots.length())); ICOMMAND(numslots, "", (), intret(slots.length())); COMMAND(getslottex, "i"); ICOMMAND(texloaded, "i", (int *tex), intret(slots.inrange(*tex) && slots[*tex]->loaded ? 1 : 0)); static void replacetexcube(cube &c, int oldtex, int newtex) { loopi(6) if(c.texture[i] == oldtex) c.texture[i] = newtex; if(c.children) loopi(8) replacetexcube(c.children[i], oldtex, newtex); } void mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, bool local) { if(local) game::edittrigger(sel, EDIT_REPLACE, oldtex, newtex, insel ? 1 : 0); if(insel) { loopselxyz(replacetexcube(c, oldtex, newtex)); } else { loopi(8) replacetexcube(worldroot[i], oldtex, newtex); } allchanged(); } bool mpreplacetex(int oldtex, int newtex, bool insel, selinfo &sel, ucharbuf &buf) { if(!unpacktex(oldtex, buf, false)) return false; editingvslot(oldtex); if(!unpacktex(newtex, buf)) return false; mpreplacetex(oldtex, newtex, insel, sel, false); return true; } static void replace(bool insel) { if(noedit()) return; if(reptex < 0) { conoutf(CON_ERROR, "can only replace after a texture edit"); return; } mpreplacetex(reptex, lasttex, insel, sel, true); } ICOMMAND(replace, "", (), replace(false)); ICOMMAND(replacesel, "", (), replace(true)); ////////// flip and rotate /////////////// static inline uint dflip(uint face) { return face==F_EMPTY ? face : 0x88888888 - (((face&0xF0F0F0F0)>>4) | ((face&0x0F0F0F0F)<<4)); } static inline uint cflip(uint face) { return ((face&0xFF00FF00)>>8) | ((face&0x00FF00FF)<<8); } static inline uint rflip(uint face) { return ((face&0xFFFF0000)>>16)| ((face&0x0000FFFF)<<16); } static inline uint mflip(uint face) { return (face&0xFF0000FF) | ((face&0x00FF0000)>>8) | ((face&0x0000FF00)<<8); } static void flipcube(cube &c, int d) { swap(c.texture[d*2], c.texture[d*2+1]); c.faces[D[d]] = dflip(c.faces[D[d]]); c.faces[C[d]] = cflip(c.faces[C[d]]); c.faces[R[d]] = rflip(c.faces[R[d]]); if(c.children) { loopi(8) if(i&octadim(d)) swap(c.children[i], c.children[i-octadim(d)]); loopi(8) flipcube(c.children[i], d); } } static inline void rotatequad(cube &a, cube &b, cube &c, cube &d) { cube t = a; a = b; b = c; c = d; d = t; } static void rotatecube(cube &c, int d) // rotates cube clockwise. see pics in cvs for help. { c.faces[D[d]] = cflip(mflip(c.faces[D[d]])); c.faces[C[d]] = dflip(mflip(c.faces[C[d]])); c.faces[R[d]] = rflip(mflip(c.faces[R[d]])); swap(c.faces[R[d]], c.faces[C[d]]); swap(c.texture[2*R[d]], c.texture[2*C[d]+1]); swap(c.texture[2*C[d]], c.texture[2*R[d]+1]); swap(c.texture[2*C[d]], c.texture[2*C[d]+1]); if(c.children) { int row = octadim(R[d]); int col = octadim(C[d]); for(int i=0; i<=octadim(d); i+=octadim(d)) rotatequad ( c.children[i+row], c.children[i], c.children[i+col], c.children[i+col+row] ); loopi(8) rotatecube(c.children[i], d); } } void mpflip(selinfo &sel, bool local) { if(local) { game::edittrigger(sel, EDIT_FLIP); makeundo(); } int zs = sel.s[dimension(sel.orient)]; loopxy(sel) { loop(z,zs) flipcube(selcube(x, y, z), dimension(sel.orient)); loop(z,zs/2) { cube &a = selcube(x, y, z); cube &b = selcube(x, y, zs-z-1); swap(a, b); } } changed(sel); } static void flip() { if(noedit()) return; mpflip(sel, true); } void mprotate(int cw, selinfo &sel, bool local) { if(local) game::edittrigger(sel, EDIT_ROTATE, cw); int d = dimension(sel.orient); if(!dimcoord(sel.orient)) cw = -cw; int m = sel.s[C[d]] < sel.s[R[d]] ? C[d] : R[d]; int ss = sel.s[m] = max(sel.s[R[d]], sel.s[C[d]]); if(local) makeundo(); loop(z,sel.s[D[d]]) loopi(cw>0 ? 1 : 3) { loopxy(sel) rotatecube(selcube(x,y,z), d); loop(y,ss/2) loop(x,ss-1-y*2) rotatequad ( selcube(ss-1-y, x+y, z), selcube(x+y, y, z), selcube(y, ss-1-x-y, z), selcube(ss-1-x-y, ss-1-y, z) ); } changed(sel); } static void rotate(int *cw) { if(noedit()) return; mprotate(*cw, sel, true); } COMMAND(flip, ""); COMMAND(rotate, "i"); enum { EDITMATF_EMPTY = 0x10000, EDITMATF_NOTEMPTY = 0x20000, EDITMATF_SOLID = 0x30000, EDITMATF_NOTSOLID = 0x40000 }; static const struct { const char *name; int filter; } editmatfilters[] = { { "empty", EDITMATF_EMPTY }, { "notempty", EDITMATF_NOTEMPTY }, { "solid", EDITMATF_SOLID }, { "notsolid", EDITMATF_NOTSOLID } }; static void setmat(cube &c, ushort mat, ushort matmask, ushort filtermat, ushort filtermask, int filtergeom) { if(c.children) loopi(8) setmat(c.children[i], mat, matmask, filtermat, filtermask, filtergeom); else if((c.material&filtermask) == filtermat) { switch(filtergeom) { case EDITMATF_EMPTY: if(isempty(c)) break; return; case EDITMATF_NOTEMPTY: if(!isempty(c)) break; return; case EDITMATF_SOLID: if(isentirelysolid(c)) break; return; case EDITMATF_NOTSOLID: if(!isentirelysolid(c)) break; return; } if(mat!=MAT_AIR) { c.material &= matmask; c.material |= mat; } else c.material = MAT_AIR; } } void mpeditmat(int matid, int filter, selinfo &sel, bool local) { if(local) game::edittrigger(sel, EDIT_MAT, matid, filter); ushort filtermat = 0, filtermask = 0, matmask; int filtergeom = 0; if(filter >= 0) { filtermat = filter&0xFF; filtermask = filtermat&(MATF_VOLUME|MATF_INDEX) ? MATF_VOLUME|MATF_INDEX : (filtermat&MATF_CLIP ? MATF_CLIP : filtermat); filtergeom = filter&~0xFF; } if(matid < 0) { matid = 0; matmask = filtermask; if(isclipped(filtermat&MATF_VOLUME)) matmask &= ~MATF_CLIP; if(isdeadly(filtermat&MATF_VOLUME)) matmask &= ~MAT_DEATH; } else { matmask = matid&(MATF_VOLUME|MATF_INDEX) ? 0 : (matid&MATF_CLIP ? ~MATF_CLIP : ~matid); if(isclipped(matid&MATF_VOLUME)) matid |= MAT_CLIP; if(isdeadly(matid&MATF_VOLUME)) matid |= MAT_DEATH; } loopselxyz(setmat(c, matid, matmask, filtermat, filtermask, filtergeom)); } static void editmat(char *name, char *filtername) { if(noedit()) return; int filter = -1; if(filtername[0]) { loopi(sizeof(editmatfilters)/sizeof(editmatfilters[0])) if(!strcmp(editmatfilters[i].name, filtername)) { filter = editmatfilters[i].filter; break; } if(filter < 0) filter = findmaterial(filtername); if(filter < 0) { conoutf(CON_ERROR, "unknown material \"%s\"", filtername); return; } } int id = -1; if(name[0] || filter < 0) { id = findmaterial(name); if(id<0) { conoutf(CON_ERROR, "unknown material \"%s\"", name); return; } } mpeditmat(id, filter, sel, true); } COMMAND(editmat, "ss"); void rendertexturepanel(int w, int h) { if((texpaneltimer -= curtime)>0 && editmode) { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); pushhudmatrix(); hudmatrix.scale(h/1800.0f, h/1800.0f, 1); flushhudmatrix(false); SETSHADER(hudrgb); int y = 50, gap = 10; gle::defvertex(2); gle::deftexcoord0(); loopi(7) { int s = (i == 3 ? 285 : 220), ti = curtexindex+i-3; if(texmru.inrange(ti)) { VSlot &vslot = lookupvslot(texmru[ti]), *layer = nullptr, *detail = nullptr; Slot &slot = *vslot.slot; Texture *tex = slot.sts.empty() ? notexture : slot.sts[0].t, *glowtex = nullptr, *layertex = nullptr, *detailtex = nullptr; if(slot.texmask&(1<slot->sts.empty() ? notexture : layer->slot->sts[0].t; } if(vslot.detail) { detail = &lookupvslot(vslot.detail); detailtex = detail->slot->sts.empty() ? notexture : detail->slot->sts[0].t; } float sx = min(1.0f, tex->xs/(float)tex->ys), sy = min(1.0f, tex->ys/(float)tex->xs); int x = w*1800/h-s-50, r = s; vec2 tc[4] = { vec2(0, 0), vec2(1, 0), vec2(1, 1), vec2(0, 1) }; float xoff = vslot.offset.x, yoff = vslot.offset.y; if(vslot.rotation) { const texrotation &r = texrotations[vslot.rotation]; if(r.swapxy) { swap(xoff, yoff); loopk(4) swap(tc[k].x, tc[k].y); } if(r.flipx) { xoff *= -1; loopk(4) tc[k].x *= -1; } if(r.flipy) { yoff *= -1; loopk(4) tc[k].y *= -1; } } loopk(4) { tc[k].x = tc[k].x/sx - xoff/tex->xs; tc[k].y = tc[k].y/sy - yoff/tex->ys; } glBindTexture(GL_TEXTURE_2D, tex->id); loopj(glowtex ? 3 : 2) { if(j < 2) gle::color(vec(vslot.colorscale).mul(j), texpaneltimer/1000.0f); else { glBindTexture(GL_TEXTURE_2D, glowtex->id); glBlendFunc(GL_SRC_ALPHA, GL_ONE); gle::color(vslot.glowcolor, texpaneltimer/1000.0f); } gle::begin(GL_TRIANGLE_STRIP); gle::attribf(x, y); gle::attrib(tc[0]); gle::attribf(x+r, y); gle::attrib(tc[1]); gle::attribf(x, y+r); gle::attrib(tc[3]); gle::attribf(x+r, y+r); gle::attrib(tc[2]); xtraverts += gle::end(); if(j==1 && detailtex) { glBindTexture(GL_TEXTURE_2D, detailtex->id); gle::begin(GL_TRIANGLE_STRIP); gle::attribf(x, y); gle::attrib(tc[0]); gle::attribf(x+r/2, y); gle::attrib(tc[1]); gle::attribf(x, y+r/2); gle::attrib(tc[3]); gle::attribf(x+r/2, y+r/2); gle::attrib(tc[2]); xtraverts += gle::end(); } if(j==1 && layertex) { gle::color(layer->colorscale, texpaneltimer/1000.0f); glBindTexture(GL_TEXTURE_2D, layertex->id); gle::begin(GL_TRIANGLE_STRIP); gle::attribf(x+r/2, y+r/2); gle::attrib(tc[0]); gle::attribf(x+r, y+r/2); gle::attrib(tc[1]); gle::attribf(x+r/2, y+r); gle::attrib(tc[3]); gle::attribf(x+r, y+r); gle::attrib(tc[2]); xtraverts += gle::end(); } if(!j) { r -= 10; x += 5; y += 5; } else if(j == 2) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } } y += s+gap; } pophudmatrix(true, false); resethudshader(); } } #define EDITSTAT(name, type, val) \ ICOMMAND(editstat##name, "", (), \ { \ extern int statrate; \ static int laststat = 0; \ static type prevstat = 0; \ static type curstat = 0; \ if(totalmillis - laststat >= statrate) \ { \ prevstat = curstat; \ laststat = totalmillis - (totalmillis%statrate); \ } \ if(prevstat == curstat) curstat = (val); \ type##ret(curstat); \ }); EDITSTAT(wtr, int, wtris/1024); EDITSTAT(vtr, int, (vtris*100)/max(wtris, 1)); EDITSTAT(wvt, int, wverts/1024); EDITSTAT(vvt, int, (vverts*100)/max(wverts, 1)); EDITSTAT(evt, int, xtraverts/1024); EDITSTAT(eva, int, xtravertsva/1024); EDITSTAT(octa, int, allocnodes*8); EDITSTAT(va, int, allocva); EDITSTAT(glde, int, glde); EDITSTAT(geombatch, int, gbatches); EDITSTAT(oq, int, getnumqueries()); EDITSTAT(pvs, int, getnumviewcells());