Files
assaultcube-/source/src/editing.cpp
T
ac-stef 41fa9e1b34 add command for raw scripted editing
selectionwalk action beginsel endsel

  The command iterates over all cubes in all selections and executes
  "action" for each cube. The cubes of every selection are framed by
  executions of "beginsel" and "endsel". The action script is allowed
  to make changes to cube attributes, and if it does, an undo point is
  automatically added. "beginsel" and "endsel" are optional.

  While the scripts are executed, several aliases provide further info:

  sw_cursel
    Position and size of the current selection. Valid in all three
    scripts. Holds four integer values for x, y, xs, ys. See command
    "selection" for details.

  (all following aliases are only valid during the execution of the action
   script and provide the attributes of one cube of the map)

  sw_abs_x
  sw_abs_y
  sw_rel_x
  sw_rel_y
    Absolute and relative (within the current selection) coordinates of
    the current cube. Read-only values.

  sw_type
    Type of the current cube. Can be 0..4 for SOLID, CORNER, FHF, CHF and
    SPACE. May be changed to other numerical value or keyword.

  sw_floor
  sw_ceil
    Floor and ceiling heights of the current cube. Range -128..127

  sw_wtex
  sw_ftex
  sw_ctex
  sw_utex
    Texture slot indices for wall, floor, ceiling and upper wall of the
    current cube.

  sw_vdelta
  sw_tag
    Vdelta and tag values of current cube.

  sw_r
  sw_g
  sw_b
    Current light values of current cube. Can be edited, but only with
    temporary effect. Will be reset with the next light recalc.

The command is mostly useful for statistics scripts (like counting cubes
with certain attributes), but is can also be used for scripted editing,
as the following (useless) example shows:

  selectionwalk [ += sw_floor (rnd 2) ]

Further changes:

  The command listoptions now also can return the names of cube types:

    echo (listoptions cubetypes)

  The command countwalls is now implemented as a script instead of a
  builtin command. The script works better, because it doesn't create
  unnecessary undo points.

DOCREF selectionwalk listoptions countwalls
2016-08-27 21:58:02 +02:00

1184 lines
37 KiB
C++

// editing.cpp: most map editing commands go here, entity editing commands are in world.cpp
#include "cube.h"
bool editmode = false;
// the current selections, used by almost all editing commands
// invariant: all code assumes that these are kept inside MINBORD distance of the edge of the map
// => selections are checked when they are made or when the world is reloaded
vector<block> sels;
#define loopselxy(sel, b) { makeundo(sel); loop(x,(sel).xs) loop(y,(sel).ys) { sqr *s = S((sel).x+x, (sel).y+y); b; } remip(sel); }
#define loopselsxy(b) { loopv(sels) loopselxy(sels[i], b); }
int cx, cy, ch;
int curedittex[] = { -1, -1, -1 };
bool dragging = false;
int lastx, lasty, lasth;
int lasttype = 0, lasttex = 0;
sqr rtex;
VAR(editing, 1, 0, 0);
VAR(unsavededits, 1, 0, 0);
VAR(editmetakeydown, 1, 0, 0);
COMMANDF(editmeta, "d", (bool on) { editmetakeydown = on ? 1 : 0; } );
void toggleedit(bool force)
{
if(player1->state==CS_DEAD) return; // do not allow dead players to edit to avoid state confusion
if(!force && !editmode && !allowedittoggle()) return; // not in most multiplayer modes
if(player1->state == CS_SPECTATE)
{
if(watchingdemo)
{
if(player1->spectatemode != SM_FLY) spectatemode(SM_FLY); // release in a place of followed player
}
else return;
}
if(!(editmode = !editmode))
{
float oldz = player1->o.z;
entinmap(player1); // find spawn closest to current floating pos
player1->timeinair = player1->o.z == oldz ? 0: 300;
}
else
{
//put call to clear/restart gamemode
player1->attacking = false;
}
keyrepeat(editmode);
editing = editmode ? 1 : 0;
player1->state = editing ? CS_EDITING : (watchingdemo ? CS_SPECTATE : CS_ALIVE);
if(editing && player1->onladder) player1->onladder = false;
if(editing && (player1->weaponsel->type == GUN_SNIPER && ((sniperrifle *)player1->weaponsel)->scoped)) ((sniperrifle *)player1->weaponsel)->onownerdies(); // or ondeselecting()
if(editing && (player1->weaponsel->type == GUN_GRENADE)) ((grenades *)player1->weaponsel)->onownerdies();
if(!force) addmsg(SV_EDITMODE, "ri", editing);
}
void edittoggle() { toggleedit(false); }
COMMAND(edittoggle, "");
bool correctsel(block &s) // ensures above invariant
{
int bsize = ssize - MINBORD;
if(s.xs+s.x>bsize) s.xs = bsize-s.x;
if(s.ys+s.y>bsize) s.ys = bsize-s.y;
if(s.xs<=0 || s.ys<=0) return false;
else return !OUTBORD(s.x, s.y);
}
bool noteditmode(const char* func)
{
if(!editmode)
{
if(func && func[0]!='\0') conoutf("\f4[\f3%s\f4]\f5 is only allowed in edit mode", func);
else conoutf("this function is only allowed in edit mode");
}
return !editmode;
}
inline bool selset()
{
return (sels.length() > 0);
}
bool noselection()
{
if(!selset()) conoutf("no selection");
return !selset();
}
char *editinfo()
{
static string info;
if(!editmode) return NULL;
info[0] = '\0';
int e = closestent();
if(e >= 0)
{
entity &c = ents[e];
int t = c.type < MAXENTTYPES ? c.type : 0;
formatstring(info)("%s entity: %s (%s)", pinnedclosestent ? "\fs\f3pinned\fr" : "closest", entnames[t], formatentityattributes(c, true));
const char *slotinfo = "unassigned slot";
if(t == MAPMODEL)
{
mapmodelinfo *mmi = getmminfo(c.attr2);
if(mmi) slotinfo = mmshortname(mmi->name);
}
else if(t == SOUND)
{
if(mapconfigdata.mapsoundlines.inrange(c.attr1)) slotinfo = mapconfigdata.mapsoundlines[c.attr1].name;
}
else slotinfo = "";
if(*slotinfo) concatformatstring(info, " %s", slotinfo);
}
if(selset())
{
concatformatstring(info, "\n\tselection = (%d, %d)", (sels.last()).xs, (sels.last()).ys);
if(sels.length() > 1) concatformatstring(info, " and %d more", sels.length() - 1);
}
else concatformatstring(info, "\n\tno selection");
sqr *s;
if(!OUTBORD(cx, cy) && (s = S(cx,cy)) && !editfocusdetails(s) && !SOLID(s) && s->tag) concatformatstring(info, ", tag 0x%02X", s->tag);
return info;
}
#define EDITSEL if(noteditmode("EDITSEL") || noselection()) return
#define EDITSELMP if(noteditmode("EDITSELMP") || noselection() || multiplayer()) return
#define EDITMP if(noteditmode("EDITMP") || multiplayer()) return
// multiple sels
// add a selection to the list
void addselection(int x, int y, int xs, int ys, int h)
{
block &s = sels.add();
s.x = x; s.y = y; s.xs = xs; s.ys = ys; s.h = h;
if(!correctsel(s)) { sels.drop(); }
}
// reset all selections
void resetselections()
{
sels.shrink(0);
}
COMMANDF(select, "iiii", (int *x, int *y, int *xs, int *ys) { resetselections(); addselection(*x, *y, *xs, *ys, 0); });
COMMANDF(addselection, "iiii", (int *x, int *y, int *xs, int *ys) { addselection(*x, *y, *xs, *ys, 0); });
COMMAND(resetselections, "");
// reset all invalid selections
void checkselections()
{
loopv(sels) if(!correctsel(sels[i])) sels.remove(i);
}
// update current selection, or add a new one
void makesel(bool isnew)
{
if(isnew || sels.length() == 0) addselection(min(lastx, cx), min(lasty, cy), iabs(lastx-cx)+1, iabs(lasty-cy)+1, max(lasth, ch));
else
{
block &cursel = sels.last();
cursel.x = min(lastx, cx); cursel.y = min(lasty, cy);
cursel.xs = iabs(lastx-cx)+1; cursel.ys = iabs(lasty-cy)+1;
cursel.h = max(lasth, ch);
correctsel(cursel);
}
if(selset()) rtex = *S((sels.last()).x, (sels.last()).y);
}
#define SEL_ATTR(attr) { string buf = ""; loopv(sels) { concatformatstring(buf, "%d ", sels[i].attr); } result(buf); }
COMMANDF(selx, "", (void) { SEL_ATTR(x); });
COMMANDF(sely, "", (void) { SEL_ATTR(y); });
COMMANDF(selxs, "", (void) { SEL_ATTR(xs); });
COMMANDF(selys, "", (void) { SEL_ATTR(ys); });
#undef SEL_ATTR
VAR(flrceil,0,0,2);
VAR(editaxis,0,0,13);
VARP(showgrid,0,1,1);
// VC8 optimizer screws up rendering somehow if this is an actual function
#define sheight(s,t,z) (!flrceil ? (s->type==FHF ? s->floor-t->vdelta/4.0f : (float)s->floor) : (s->type==CHF ? s->ceil+t->vdelta/4.0f : (float)s->ceil))
// remove those two after playing with the values a bit :)
VAR(tagnum, 1, 14, 100);
VAR(tagnumfull, 0, 0, 100);
VAR(taglife, 1, 30, 1000);
// and have mercy with old graphics cards...
vector<int> tagclipcubes;
bool showtagclipfocus = false;
COMMANDF(showtagclipfocus, "d", (bool on) { showtagclipfocus = on; } );
VAR(showtagclips, 0, 1, 1);
FVAR(tagcliplinewidth, 0.2, 1, 3);
inline void forceinsideborders(int &xy)
{
if(xy < MINBORD) xy = MINBORD;
if(xy >= ssize - MINBORD) xy = ssize - MINBORD - 1;
}
void cursorupdate() // called every frame from hud
{
flrceil = ((int)(camera1->pitch>=0))*2;
int cyaw = ((int) camera1->yaw) % 180;
editaxis = editmode ? (fabs(camera1->pitch) > 65 ? 13 : (cyaw < 45 || cyaw > 135 ? 12 : 11)) : 0;
volatile float x = worldpos.x; // volatile needed to prevent msvc7 optimizer bug?
volatile float y = worldpos.y;
volatile float z = worldpos.z;
cx = (int)x;
cy = (int)y;
if(OUTBORD(cx, cy))
{
forceinsideborders(cx);
forceinsideborders(cy);
return;
}
sqr *s = S(cx,cy);
if(fabs(sheight(s,s,z)-z)>1) // selected wall
{
x += x>camera1->o.x ? 0.5f : -0.5f; // find right wall cube
y += y>camera1->o.y ? 0.5f : -0.5f;
cx = (int)x;
cy = (int)y;
if(OUTBORD(cx, cy))
{
forceinsideborders(cx);
forceinsideborders(cy);
return;
}
}
if(dragging) makesel(false);
const int GRIDSIZE = 5;
const float GRIDW = 0.5f;
const float GRID8 = 2.0f;
const float GRIDS = 2.0f;
const int GRIDM = 0x7;
static int lastsparkle = 0;
bool sparkletime = lastmillis - lastsparkle >= 20;
if(sparkletime) lastsparkle = lastmillis - (lastmillis%20); // clip adding sparklies at 50 fps
// render editing grid
extern int blankouthud;
if(showgrid && !blankouthud)
{
for(int ix = cx-GRIDSIZE; ix<=cx+GRIDSIZE; ix++) for(int iy = cy-GRIDSIZE; iy<=cy+GRIDSIZE; iy++)
{
if(OUTBORD(ix, iy)) continue;
sqr *s = S(ix,iy);
if(SOLID(s)) continue;
float h1 = sheight(s, s, z);
float h2 = sheight(s, SWS(s,1,0,sfactor), z);
float h3 = sheight(s, SWS(s,1,1,sfactor), z);
float h4 = sheight(s, SWS(s,0,1,sfactor), z);
if(sparkletime && showtagclips && showtagclipfocus && s->tag & TAGANYCLIP)
particle_cube(s->tag & TAGCLIP ? PART_EPICKUP : PART_EMODEL, tagnum, taglife, ix, iy);
if(s->tag) linestyle(GRIDW, 0xFF, 0x40, 0x40);
else if(s->type==FHF || s->type==CHF) linestyle(GRIDW, 0x80, 0xFF, 0x80);
else linestyle(GRIDW, 0x80, 0x80, 0x80);
block b = { ix, iy, 1, 1 };
box(b, h1, h2, h3, h4);
linestyle(GRID8, 0x40, 0x40, 0xFF);
if(!(ix&GRIDM)) line(ix, iy, h1, ix, iy+1, h4);
if(!((ix+1)&GRIDM)) line(ix+1, iy, h2, ix+1, iy+1, h3);
if(!(iy&GRIDM)) line(ix, iy, h1, ix+1, iy, h2);
if(!((iy+1)&GRIDM)) line(ix, iy+1, h4, ix+1, iy+1, h3);
}
if(!SOLID(s))
{
float ih = sheight(s, s, z);
linestyle(GRIDS, 0xFF, 0xFF, 0xFF);
block b = { cx, cy, 1, 1 };
box(b, ih, sheight(s, SWS(s,1,0,sfactor), z), sheight(s, SWS(s,1,1,sfactor), z), sheight(s, SWS(s,0,1,sfactor), z));
linestyle(GRIDS, 0xFF, 0x00, 0x00);
dot(cx, cy, ih);
ch = (int)ih;
}
}
else ch = (int) sheight(s, s, z);
if(selset() && !blankouthud)
{
linestyle(GRIDS, 0xFF, 0x40, 0x40);
loopv(sels) box(sels[i], (float)sels[i].h, (float)sels[i].h, (float)sels[i].h, (float)sels[i].h);
}
if(!showtagclipfocus && showtagclips && !blankouthud)
{
const int xo[] = { 0, 0, 1, 1, 0 }, yo[] = {0, 1, 1, 0, 0 };
loopv(tagclipcubes) // all non-solid & have clips
{
int x = tagclipcubes[i] & 0xFFFF, y = tagclipcubes[i] >> 16;
ASSERT(!OUTBORD(x, y));
sqr *s = S(x,y), *o[9];
if(s->tag & TAGCLIP) linestyle(tagcliplinewidth, 0xFF, 0xFF, 0); // yello
else linestyle(tagcliplinewidth, 0xFF, 0, 0xFF); // magenta
o[8] = SWS(s,-1,-1,sfactor);
o[3] = o[8] + 1; // 837
o[7] = o[3] + 1; // 0s2
o[0] = o[4] = SWS(s,-1,0,sfactor); // 516
o[2] = o[0] + 2;
o[5] = SWS(s,-1,1,sfactor);
o[1] = o[5] + 1;
o[6] = o[1] + 1;
bool clipped[9];
loopj(9) clipped[j] = !SOLID(o[j]) && (o[j]->tag & TAGANYCLIP) > 0;
int h = s->floor - (s->type == FHF ? (s->vdelta + 3) / 4 : 0), c = s->ceil + (s->type == CHF ? (s->vdelta + 3) / 4 : 0);
loopk(4) if((clipped[k] == clipped[k+1]) && !(clipped[k] && clipped[k + 5])) line(x + xo[k+1], y + yo[k+1], h, x + xo[k+1], y + yo[k+1], c);
for( ; h < c; h++)
{
int k = (h + x + y) & 3;
if(k < 2 && !clipped[k]) line(x + xo[k], y + yo[k], h + 1, x + xo[k+1], y + yo[k+1], h);
k = (h - x - y) & 3;
if(k > 1 && !clipped[k]) line(x + xo[k], y + yo[k], h + 1, x + xo[k+1], y + yo[k+1], h);
}
if(sparkletime && tagnumfull) particle_cube(s->tag & TAGCLIP ? PART_EPICKUP : PART_EMODEL, tagnumfull, 30, x, y);
}
}
glLineWidth(1);
}
vector<block *> undos, redos; // unlimited undo
VAR(undomegs, 0, 5, 50); // bounded by n megs
void pruneundos(int maxremain) // bound memory
{
int u = 0, r = 0;
maxremain /= sizeof(sqr);
loopvrev(undos)
{
u += undos[i]->xs * undos[i]->ys;
if(u > maxremain) freeblockp(undos.remove(i));
}
loopvrev(redos)
{
r += redos[i]->xs * redos[i]->ys;
if(r > maxremain) freeblockp(redos.remove(i));
}
}
void storeposition(short p[])
{
loopi(3) p[i] = player1->o.v[i] * DMF;
p[3] = player1->yaw;
p[4] = player1->pitch;
}
void makeundo(block &sel)
{
storeposition(sel.p);
undos.add(blockcopy(sel));
pruneundos(undomegs<<20);
unsavededits++;
}
void restoreposition(short p[])
{
loopi(3) player1->o.v[i] = float(p[i]) / DMF;
player1->yaw = p[3];
player1->pitch = p[4];
player1->resetinterp();
}
void restoreposition(block &sel)
{
restoreposition(sel.p);
resetselections();
addselection(sel.x, sel.y, sel.xs, sel.ys, sel.h); // select undone area
checkselections();
}
void gotoentity(int *n)
{
if(noteditmode("gotoentity")) return;
if(ents.inrange(*n) && ents[*n].type != NOTUSED)
{
player1->o.x = ents[*n].x;
player1->o.y = ents[*n].y;
player1->o.z = ents[*n].z;
player1->yaw = ((ents[*n].attr1 / ENTSCALE10) % 360 + 360) % 360; // works for most ;)
player1->pitch = 0;
player1->resetinterp();
}
}
COMMAND(gotoentity, "i");
void gotoposition(char *x, char *y, char *z, char *yaw, char *pitch)
{
if(noteditmode("gotoposition")) return;
if(*x) player1->o.x = atof(x);
if(*y) player1->o.y = atof(y);
if(*z) player1->o.z = atof(z);
if(*yaw) player1->yaw = (atoi(yaw) % 360 + 360) % 360;
if(*pitch) player1->pitch = atoi(pitch) % 90;
player1->resetinterp();
defformatstring(res)("%s %s %s %d %d", floatstr(player1->o.x), floatstr(player1->o.y), floatstr(player1->o.z), int(player1->yaw), int(player1->pitch));
result(res);
}
COMMAND(gotoposition, "sssss");
void editundo()
{
EDITMP;
if(undos.empty()) { conoutf("nothing more to undo"); return; }
block *p = undos.pop();
redos.add(blockcopy(*p));
if(editmetakeydown) restoreposition(*p);
blockpaste(*p);
freeblock(p);
unsavededits++;
}
COMMANDN(undo, editundo, "");
void editredo()
{
EDITMP;
if(redos.empty()) { conoutf("nothing more to redo"); return; }
block *p = redos.pop();
undos.add(blockcopy(*p));
if(editmetakeydown) restoreposition(*p);
blockpaste(*p);
freeblock(p);
unsavededits++;
}
COMMANDN(redo, editredo, "");
extern int worldiodebug;
void restoreeditundo(ucharbuf &q)
{
int type, len, explen;
while(!q.overread() && (type = getuint(q)))
{
int bx = getuint(q), by = getuint(q), bxs = getuint(q), bys = getuint(q);
short pp[5];
loopi(5) pp[i] = getint(q);
len = getuint(q);
if((bx | by | (bx + bxs) | (by + bys)) & ~(ssize - 1)) continue;
explen = bxs * bys;
block *b = (block *)new uchar[sizeof(block) + explen * sizeof(sqr)];
b->x = bx; b->y = by; b->xs = bxs; b->ys = bys;
loopi(5) b->p[i] = pp[i];
ucharbuf p = q.subbuf(len);
rldecodecubes(p, (sqr *)(b+1), explen, 6, true);
switch(type)
{
case 10: undos.insert(0, b); break;
case 20: redos.insert(0, b); break;
default: freeblock(b); break;
}
#ifdef _DEBUG
if(worldiodebug) switch(type)
{
case 10:
case 20:
clientlogf(" got %sdo x %d, y %d, xs %d, ys %d, remaining %d, overread %d", type == 10 ? "un" : "re", b->x, b->y, b->xs, b->ys, q.remaining(), int(q.overread()));
break;
}
#endif
}
if(undos.length() || redos.length()) conoutf("restored editing history: %d undos and %d redos", undos.length(), redos.length());
}
int rlencodeundo(int type, vector<uchar> &t, block *s)
{
putuint(t, type);
putuint(t, s->x);
putuint(t, s->y);
putuint(t, s->xs);
putuint(t, s->ys);
loopi(5) putint(t, s->p[i]);
vector<uchar> tmp;
rlencodecubes(tmp, (sqr *)(s+1), s->xs * s->ys, true);
putuint(t, tmp.length());
t.put(tmp.getbuf(), tmp.length());
#ifdef _DEBUG
if(worldiodebug) clientlogf(" compressing redo/undo x %d, y %d, xs %d, ys %d, compressed length %d, cubes %d", s->x, s->y, s->xs, s->ys, tmp.length(), s->xs * s->ys);
#endif
return t.length();
}
int backupeditundo(vector<uchar> &buf, int undolimit, int redolimit)
{
int numundo = 0;
vector<uchar> tmp;
loopvrev(undos)
{
tmp.setsize(0);
undolimit -= rlencodeundo(10, tmp, undos[i]);
if(undolimit < 0) break;
buf.put(tmp.getbuf(), tmp.length());
numundo++;
#ifdef _DEBUG
if(worldiodebug) clientlogf(" written undo x %d, y %d, xs %d, ys %d, compressed length %d", undos[i]->x, undos[i]->y, undos[i]->xs, undos[i]->ys, tmp.length());
#endif
}
loopvrev(redos)
{
tmp.setsize(0);
redolimit -= rlencodeundo(20, tmp, redos[i]);
if(redolimit < 0) break;
buf.put(tmp.getbuf(), tmp.length());
#ifdef _DEBUG
if(worldiodebug) clientlogf(" written redo x %d, y %d, xs %d, ys %d, compressed length %d", redos[i]->x, redos[i]->y, redos[i]->xs, redos[i]->ys, tmp.length());
#endif
}
putuint(buf, 0);
return numundo;
}
vector<block *> copybuffers;
void *copytexconfig = NULL;
void resetcopybuffers()
{
loopv(copybuffers) freeblock(copybuffers[i]);
copybuffers.shrink(0);
if(copytexconfig)
{
texconfig_delete(copytexconfig);
copytexconfig = NULL;
}
}
void copy()
{
EDITSELMP;
resetcopybuffers();
copytexconfig = texconfig_copy();
loopv(sels)
{
block *b = blockcopy(sels[i]);
copybuffers.add(b);
}
}
COMMAND(copy, "");
void paste()
{
EDITSELMP;
if(!copybuffers.length()) { conoutf("nothing to paste"); return; }
uchar usedslots[256] = { 0 }, *texmap = NULL;
loopv(copybuffers) blocktexusage(*copybuffers[i], usedslots);
if(sels.length() && copytexconfig) texmap = texconfig_paste(copytexconfig, usedslots);
loopv(sels)
{
block sel = sels[i];
int selx = sel.x;
int sely = sel.y;
loopvj(copybuffers)
{
block *copyblock = copybuffers[j];
int dx = copyblock->x - copybuffers[0]->x, dy = copyblock->y - copybuffers[0]->y;
sel.xs = copyblock->xs;
sel.ys = copyblock->ys;
sel.x = selx + dx;
sel.y = sely + dy;
if(!correctsel(sel) || sel.xs!=copyblock->xs || sel.ys!=copyblock->ys) { conoutf("incorrect selection"); return; }
makeundo(sel);
blockpaste(*copyblock, sel.x, sel.y, true, texmap);
}
}
}
COMMAND(paste, "");
void tofronttex() // maintain most recently used of the texture lists when applying texture
{
loopi(3)
{
int c = curedittex[i];
if(c>=0)
{
uchar *p = hdr.texlists[i];
int t = p[c];
for(int a = c-1; a>=0; a--) p[a+1] = p[a];
p[0] = t;
curedittex[i] = -1;
}
}
}
const char *texturestacktypes[] = { "floor", "wall", "ceiling", "" };
void edittexturestack(char *what, int *slot) // manually manipulate the "last-used" texture lists to put certain textures up front
{
tofronttex(); // keep laste edited texture
int n = getlistindex(what, texturestacktypes, true, -1);
if(n >= 0)
{
loopi(256) if(hdr.texlists[n][i] == *slot) curedittex[n] = i; // find stack index of wanted texture slot
}
tofronttex();
}
COMMAND(edittexturestack, "si");
void editdrag(bool isdown)
{
if((dragging = isdown))
{
lastx = cx;
lasty = cy;
lasth = ch;
tofronttex();
if(!editmetakeydown) resetselections();
}
makesel(isdown);
if(!isdown) for(int i = sels.length() - 2; i >= 0; i--)
{
block &a = sels.last(), &b = sels[i];
if(a.x == b.x && a.y == b.y && a.xs == b.xs && a.ys == b.ys)
{ // making a selection twice will deselect both of it
sels.drop();
sels.remove(i);
break;
}
}
}
// the core editing function. all the *xy functions perform the core operations
// and are also called directly from the network, the function below it is strictly
// triggered locally. They all have very similar structure.
void editheightxy(bool isfloor, int amount, block &sel)
{
loopselxy(sel, if(isfloor)
{
s->floor += amount;
if(s->floor>=s->ceil) s->floor = s->ceil-1;
}
else
{
s->ceil += amount;
if(s->ceil<=s->floor) s->ceil = s->floor+1;
});
}
void editheight(int *flr, int *amount)
{
EDITSEL;
bool isfloor = *flr==0;
loopv(sels)
{
editheightxy(isfloor, *amount, sels[i]);
addmsg(SV_EDITH, "ri6", sels[i].x, sels[i].y, sels[i].xs, sels[i].ys, isfloor, *amount);
}
}
COMMAND(editheight, "ii");
// texture type : 0 floor, 1 wall, 2 ceil, 3 upper wall
void renderhudtexturepreview(int slot, int pos, bool highlight)
{
Texture *pt = slot != DEFAULT_SKY ? lookupworldtexture(slot, false) : NULL;
int hs = (highlight ? 0 : (FONTH / 3) * 2), bs = VIRTW / 8 - hs, border = FONTH / (highlight ? 2 : 4), x = 2 * VIRTW - VIRTW / 8 - FONTH + hs / 2, y = VIRTH * (6 - pos) / 4, xs, ys;
blendbox(x - border, y - border, x + bs + border, y + bs + border, false);
if(pt)
{
if(pt->xs > pt->ys) xs = bs, ys = (xs * pt->ys) / pt->xs; // keep aspect ratio of texture
else ys = bs, xs = (ys * pt->xs) / pt->ys;
int xt = x + (bs - xs) / 2, yt = y + (bs - ys) / 2;
extern int fullbrightlevel;
framedquadtexture(pt->id, xt, yt, xs, ys, 1, fullbrightlevel);
}
else
{ // sky slot: just show blue instead of the texture
color c(0, 0, 0.6f, 1);
blendbox(x, y, x + bs, y + bs, false, -1, &c);
}
if(highlight)
{
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
border /= 2;
box2d(x - border, y - border, x + bs + border, y + bs + border, 200);
glEnable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
}
}
static int lastedittex = 0;
VARP(hudtexttl, 0, 2500, 10000);
void renderhudtexturepreviews()
{
if(lastedittex && hudtexttl && (lastmillis - lastedittex) < hudtexttl)
{
int atype = lasttype == 3 ? 1 : lasttype;
int idx = curedittex[atype];
if(idx < 0) idx = 0;
int startidx = clamp(idx - 2, 0, 251);
loopi(5) renderhudtexturepreview(hdr.texlists[atype][startidx + i], i, startidx + i == idx);
}
}
void edittexxy(int type, int t, block &sel)
{
loopselxy(sel, switch(type)
{
case 0: s->ftex = t; break;
case 1: s->wtex = t; break;
case 2: s->ctex = t; break;
case 3: s->utex = t; break;
});
}
void edittex(int *type, int *dir)
{
EDITSEL;
if(*type < 0 || *type > 3) return;
if(*type != lasttype) { tofronttex(); lasttype = *type; }
int atype = *type == 3 ? 1 : *type;
int i = curedittex[atype];
i = i < 0 ? 0 : i + *dir;
curedittex[atype] = i = min(max(i, 0), 255);
int t = lasttex = hdr.texlists[atype][i];
loopv(sels)
{
edittexxy(*type, t, sels[i]);
addmsg(SV_EDITT, "ri6", sels[i].x, sels[i].y, sels[i].xs, sels[i].ys, *type, t);
}
unsavededits++;
lastedittex = lastmillis;
}
COMMAND(edittex, "ii");
void settex(int *texture, int *type)
{
EDITSEL;
if(*type < 0 || *type > 3) return;
int atype = *type == 3 ? 1 : *type;
int t = -1;
loopi(256) if(*texture == (int)hdr.texlists[atype][i])
{
t = (int)hdr.texlists[atype][i];
break;
}
if(t < 0)
{
conoutf("invalid/unavaible texture");
return;
}
loopv(sels)
{
edittexxy(*type, t, sels[i]);
addmsg(SV_EDITT, "ri6", sels[i].x, sels[i].y, sels[i].xs, sels[i].ys, *type, t);
}
unsavededits++;
}
COMMAND(settex, "ii");
void replace()
{
EDITSELMP;
loop(x,ssize) loop(y,ssize)
{
sqr *s = S(x, y);
switch(lasttype)
{
case 0: if(s->ftex == rtex.ftex) s->ftex = lasttex; break;
case 1: if(s->wtex == rtex.wtex) s->wtex = lasttex; break;
case 2: if(s->ctex == rtex.ctex) s->ctex = lasttex; break;
case 3: if(s->utex == rtex.utex) s->utex = lasttex; break;
}
}
block b = { 0, 0, ssize, ssize };
remip(b);
unsavededits++;
}
COMMAND(replace, "");
void edittypexy(int type, block &sel)
{
loopselxy(sel, s->type = type);
}
void edittype(int type)
{
EDITSEL;
loopv(sels)
{
block &sel = sels[i];
if(type == CORNER && (sel.xs != sel.ys || (sel.xs != 1 && sel.xs != 2 && sel.xs != 4 && sel.xs != 8 && sel.xs != 16) || (sel.x | sel.y) & (sel.xs - 1)))
conoutf("corner selection must be power of 2 aligned");
else
{
edittypexy(type, sel);
addmsg(SV_EDITS, "ri5", sel.x, sel.y, sel.xs, sel.ys, type);
}
}
}
void heightfield(int *t) { edittype(*t==0 ? FHF : CHF); }
void solid(int *t) { edittype(*t==0 ? SPACE : SOLID); }
void corner() { edittype(CORNER); }
COMMAND(heightfield, "i");
COMMAND(solid, "i");
COMMAND(corner, "");
void editequalisexy(bool isfloor, block &sel)
{
int low = 127, hi = -128;
loopselxy(sel,
{
if(s->floor<low) low = s->floor;
if(s->ceil>hi) hi = s->ceil;
});
loopselxy(sel,
{
if(isfloor) s->floor = low; else s->ceil = hi;
if(s->floor>=s->ceil) s->floor = s->ceil-1;
});
}
void equalize(int *flr)
{
bool isfloor = *flr==0;
EDITSEL;
loopv(sels)
{
block &sel = sels[i];
editequalisexy(isfloor, sel);
addmsg(SV_EDITE, "ri5", sel.x, sel.y, sel.xs, sel.ys, isfloor);
}
}
COMMAND(equalize, "i");
void setvdeltaxy(int delta, block &sel)
{
loopselxy(sel, s->vdelta = max(s->vdelta+delta, 0));
remipmore(sel);
}
void setvdelta(int *delta)
{
EDITSEL;
loopv(sels)
{
setvdeltaxy(*delta, sels[i]);
addmsg(SV_EDITD, "ri5", sels[i].x, sels[i].y, sels[i].xs, sels[i].ys, *delta);
}
}
COMMANDN(vdelta, setvdelta, "i");
const int MAXARCHVERT = 50;
int archverts[MAXARCHVERT][MAXARCHVERT];
bool archvinit = false;
void archvertex(int *span, int *vert, int *delta)
{
if(!archvinit)
{
archvinit = true;
loop(s,MAXARCHVERT) loop(v,MAXARCHVERT) archverts[s][v] = 0;
}
if(*span>=MAXARCHVERT || *vert>=MAXARCHVERT || *span<0 || *vert<0) return;
archverts[*span][*vert] = *delta;
}
COMMAND(archvertex, "iii");
void arch(int *sidedelta)
{
EDITSELMP;
loopv(sels)
{
block &sel = sels[i];
sel.xs++;
sel.ys++;
if(sel.xs>MAXARCHVERT) sel.xs = MAXARCHVERT;
if(sel.ys>MAXARCHVERT) sel.ys = MAXARCHVERT;
loopselxy(sel, s->vdelta =
sel.xs>sel.ys
? (archverts[sel.xs-1][x] + (y==0 || y==sel.ys-1 ? *sidedelta : 0))
: (archverts[sel.ys-1][y] + (x==0 || x==sel.xs-1 ? *sidedelta : 0)));
remipmore(sel);
}
}
COMMAND(arch, "i");
void slope(int *xd, int *yd)
{
EDITSELMP;
loopv(sels)
{
block &sel = sels[i];
int off = 0;
if(*xd < 0) off -= *xd * sel.xs;
if(*yd < 0) off -= *yd * sel.ys;
sel.xs++;
sel.ys++;
loopselxy(sel, s->vdelta = *xd * x + *yd * y + off);
remipmore(sel);
}
}
COMMAND(slope, "ii");
void perlin(int *scale, int *seed, int *psize)
{
EDITSELMP;
loopv(sels)
{
block sel = sels[i];
sel.xs++;
sel.ys++;
makeundo(sel);
sel.xs--;
sel.ys--;
perlinarea(sel, *scale, *seed, *psize);
sel.xs++;
sel.ys++;
remipmore(sel);
}
}
COMMAND(perlin, "iii");
VARF(fullbright, 0, 0, 1,
if(fullbright)
{
if(noteditmode("fullbright")) return;
fullbrightlight();
}
else calclight();
);
void edittag(int *tag)
{
EDITSELMP;
loopselsxy(s->tag = *tag);
}
COMMAND(edittag, "i");
void edittagclip(char *tag)
{
int nt = ((int) strtol(tag, NULL, 0)) & TAGANYCLIP;
if(tolower(*tag) == 'n') nt = 0; // "none", "nil, "nop"
else if(!strncasecmp(tag, "pl", 2)) nt = TAGPLCLIP; // "playerclip", "plclip", "pl"
else if(isalpha(*tag)) nt = TAGCLIP; // "clip", "all", "full", "hippo"
EDITSELMP;
loopselsxy(s->tag = (s->tag & TAGTRIGGERMASK) | nt);
}
COMMAND(edittagclip, "s");
void newent(char *what, float *a1, float *a2, float *a3, float *a4)
{
EDITSEL;
loopv(sels) newentity(-1, sels[i].x, sels[i].y, (int)camera1->o.z, what, *a1, *a2, *a3, *a4);
}
COMMAND(newent, "sffff");
void movemap(int *xop, int *yop, int *zop) // move whole map
{
EDITMP;
int xo = *xop, yo = *yop, zo = *zop;
if(!worldbordercheck(MINBORD + max(-xo, 0), MINBORD + max(xo, 0), MINBORD + max(-yo, 0), MINBORD + max(yo, 0), max(zo, 0), max(-zo, 0)))
{
conoutf("not enough space to move the map");
return;
}
if(xo || yo)
{
block b = { max(-xo, 0), max(-yo, 0), ssize - iabs(xo), ssize - iabs(yo) }, *cp = blockcopy(b);
cp->x = max(xo, 0);
cp->y = max(yo, 0);
blockpaste(*cp);
freeblock(cp);
}
if(zo)
{
loop(x, ssize) loop(y, ssize)
{
S(x,y)->floor = max(-128, S(x,y)->floor + zo);
S(x,y)->ceil = min(127, S(x,y)->ceil + zo);
}
setfvar("waterlevel", waterlevel + zo, true);
}
loopv(ents)
{
ents[i].x += xo;
ents[i].y += yo;
ents[i].z += zo;
if(OUTBORD(ents[i].x, ents[i].y)) ents[i].type = NOTUSED;
}
player1->o.x += xo;
player1->o.y += yo;
entinmap(player1);
calclight();
resetmap(false);
unsavededits++;
}
COMMAND(movemap, "iii");
void selfliprotate(block &sel, int dir)
{
makeundo(sel);
block *org = blockcopy(sel);
const sqr *q = (const sqr *)(org + 1);
int x1 = sel.x, x2 = sel.x + sel.xs - 1, y1 = sel.y, y2 = sel.y + sel.ys - 1, x, y;
switch(dir)
{
case 1: // 90 degree clockwise
for(x = x2; x >= x1; x--) for(y = y1; y <= y2; y++) *S(x,y) = *q++;
break;
case 2: // 180 degree
for(y = y2; y >= y1; y--) for(x = x2; x >= x1; x--) *S(x,y) = *q++;
break;
case 3: // 90 degree counterclockwise
for(x = x1; x <= x2; x++) for(y = y2; y >= y1; y--) *S(x,y) = *q++;
break;
case 11: // flip x-axis
for(y = y1; y <= y2; y++) for(x = x2; x >= x1; x--) *S(x,y) = *q++;
break;
case 12: // flip y-axis
for(y = y2; y >= y1; y--) for(x = x1; x <= x2; x++) *S(x,y) = *q++;
break;
}
remipmore(sel);
freeblock(org);
}
void selectionrotate(int *dir)
{
EDITSELMP;
*dir &= 3;
if(!*dir) return;
loopv(sels)
{
block &sel = sels[i];
if(sel.xs == sel.ys || *dir == 2) selfliprotate(sel, *dir);
}
}
COMMAND(selectionrotate, "i");
void selectionflip(char *axis)
{
EDITSELMP;
char c = toupper(*axis);
if(c != 'X' && c != 'Y') return;
loopv(sels) selfliprotate(sels[i], c == 'X' ? 11 : 12);
}
COMMAND(selectionflip, "s");
void selectionwalk(char *action, char *beginsel, char *endsel)
{
EDITSELMP;
if(*action)
{
const char *localvars[] = { "sw_cursel", "sw_abs_x", "sw_abs_y", "sw_rel_x", "sw_rel_y", "sw_type", "sw_floor", "sw_ceil", "sw_wtex", "sw_ftex", "sw_ctex", "sw_utex", "sw_vdelta", "sw_tag", "sw_r", "sw_g", "sw_b", "" };
for(int i = 0; *localvars[i]; i++) push(localvars[i], "");
int maxsels = sels.length();
loopv(sels)
{
if(i >= maxsels) break; // don't allow the script to add selections
block sel = sels[i]; // (copy, not reference)
defformatstring(tmp)("%d %d %d %d", sel.x, sel.y, sel.xs, sel.ys);
alias("sw_cursel", tmp);
if(*beginsel) execute(beginsel);
bool haveundo = false;
loop(x, sel.xs) loop(y, sel.ys)
{
sqr *s = S(sel.x + x, sel.y + y), so = *s;
formatstring(tmp)("%d", sel.x + x); alias("sw_abs_x", tmp);
formatstring(tmp)("%d", sel.y + y); alias("sw_abs_y", tmp);
formatstring(tmp)("%d", x); alias("sw_rel_x", tmp);
formatstring(tmp)("%d", y); alias("sw_rel_y", tmp);
#define AATTR(x) formatstring(tmp)("%d", s->x); alias("sw_"#x, tmp)
AATTR(type); AATTR(floor); AATTR(ceil); AATTR(wtex); AATTR(ftex); AATTR(ctex); AATTR(utex); AATTR(vdelta); AATTR(tag); AATTR(r); AATTR(g); AATTR(b);
#undef AATTR
execute(action);
so.type = getlistindex(getalias("sw_type"), cubetypenames, true, int(SPACE));
#define GETA(x) so.x = ATOI(getalias("sw_"#x))
GETA(floor); GETA(ceil); GETA(wtex); GETA(ftex); GETA(ctex); GETA(utex); GETA(vdelta); GETA(tag); GETA(r); GETA(g); GETA(b);
#undef GETA
if(memcmp(s, &so, sizeof(sqr)))
{
if(!haveundo) makeundo(sel);
haveundo = true;
*s = so;
}
}
if(haveundo) remip(sel);
if(*endsel) execute(endsel);
}
for(int i = 0; *localvars[i]; i++) pop(localvars[i]);
}
}
COMMAND(selectionwalk, "sss");
void transformclipentities() // transforms all clip entities to tag clips, if they are big enough (so, that no player could be above or below them)
{ // (hardcoded factor ENTSCALE10 for attr1 and ENTSCALE5 for attr2-4)
EDITMP;
int total = 0, thisrun, bonus = 5;
do
{
thisrun = 0;
loopv(ents)
{
entity &e = ents[i];
if((e.type == CLIP || e.type == PLCLIP) && e.attr2 / ENTSCALE5 && e.attr3 / ENTSCALE5 && e.attr4 / ENTSCALE5)
{
int allowedspace = e.type == CLIP ? 1 : 4;
int clipmask = e.type == CLIP ? TAGCLIP : TAGPLCLIP;
int i2 = e.attr2 / ENTSCALE5, i3 = e.attr3 / ENTSCALE5; // floor values
int r2 = (e.attr2 % ENTSCALE5) > 0, r3 = (e.attr3 % ENTSCALE5) > 0; // fractions
int x1i = e.x - i2, x2i = e.x + i2 - 1, y1i = e.y - i3, y2i = e.y + i3 - 1; // inner rectangle (fully covered cubes)
int x1o = x1i - r2, x2o = x2i + r2, y1o = y1i - r3, y2o = y2i + r3; // outer rectangle (partially covered cubes)
float z1 = S(e.x, e.y)->floor + float(e.attr1) / ENTSCALE10, z2 = z1 + float(e.attr4) / ENTSCALE5;
bool bigenough = true, nodelete = false;
for(int xx = x1o; xx <= x2o; xx++) for(int yy = y1o; yy <= y2o; yy++) // loop over outer rectangle to check, if a clip has the required height
{
if(OUTBORD(xx,yy) || SOLID(S(xx,yy))) continue;
bool inner = xx >= x1i && xx <= x2i && yy >= y1i && yy <= y2i; // flag: xx|yy is inner rectangle
sqr *s[4] = { S(xx, yy), S(xx + 1, yy), S(xx, yy + 1), S(xx + 1, yy + 1) };
int vdeltamax = 0;
loopj(4) if(s[j]->vdelta > vdeltamax) vdeltamax = s[j]->vdelta;
int floor = s[0]->floor - (s[0]->type == FHF ? (vdeltamax + 3) / 4 : 0),
ceil = s[0]->ceil - (s[0]->type == CHF ? (vdeltamax + 3) / 4 : 0);
bool alreadytagged = (s[0]->tag & (TAGCLIP | clipmask)) != 0;
if((z1 - floor > allowedspace || ceil - z2 > allowedspace) && !alreadytagged) bigenough = false;
if(!inner && !alreadytagged) nodelete = true; // fractional part of the clip would not be covered: entity can not be deleted
}
if(bigenough)
{
for(int xx = x1i; xx <= x2i; xx++) for(int yy = y1i; yy <= y2i; yy++) // only convert inner rectangle to tag clips
{
if(!OUTBORD(xx,yy) && !SOLID(S(xx,yy))) S(xx, yy)->tag |= clipmask;
}
if(!nodelete)
{
deleted_ents.add(e);
e.type = NOTUSED; // only delete entity, if it is now fully covered in tag clips
thisrun++;
}
}
}
}
total += thisrun;
}
while(thisrun || bonus-- > 0);
loopi(ssize) loopj(ssize) { sqr *s = S(i,j); if(s->tag & TAGCLIP) s->tag &= ~TAGPLCLIP; }
conoutf("changed %d clip entities to tagged clip areas", total);
if(total) unsavededits++;
}
COMMAND(transformclipentities, "");
void reseteditor() // reset only stuff that would cause trouble editing the next map (don't reset selections, for example)
{
loopk(3) curedittex[k] = -1;
pruneundos(0);
pinnedclosestent = false;
}