Cube  editing.m at [6b85eefc85]

File src/editing.m artifact f776852317 part of check-in 6b85eefc85


// editing.cpp: most map editing commands go here, entity editing commands are
// in world.cpp

#include "cube.h"

#import "DynamicEntity.h"
#import "Monster.h"
#import "OFString+Cube.h"

bool editmode = false;

// the current selection, used by almost all editing commands
// invariant: all code assumes that these are kept inside MINBORD distance of
// the edge of the map

struct block sel;

OF_CONSTRUCTOR()
{
	enqueueInit(^{
		sel = (struct block) {
			variable(@"selx", 0, 0, 4096, &sel.x, NULL, false),
			variable(@"sely", 0, 0, 4096, &sel.y, NULL, false),
			variable(@"selxs", 0, 0, 4096, &sel.xs, NULL, false),
			variable(@"selys", 0, 0, 4096, &sel.ys, NULL, false),
		};
	});
}

int selh = 0;
bool selset = false;

#define loopselxy(b)                                                       \
	{                                                                  \
		makeundo();                                                \
		for (int x = 0; x < sel->xs; x++) {                        \
			for (int y = 0; y < sel->ys; y++) {                \
				struct sqr *s = S(sel->x + x, sel->y + y); \
				b;                                         \
			}                                                  \
		}                                                          \
		remip(sel, 0);                                             \
	}

int cx, cy, ch;

int curedittex[] = { -1, -1, -1 };

bool dragging = false;
int lastx, lasty, lasth;

int lasttype = 0, lasttex = 0;
static struct sqr rtex;

VAR(editing, 0, 0, 1);

void
toggleedit()
{
	if (player1.state == CS_DEAD)
		return; // do not allow dead players to edit to avoid state
		        // confusion
	if (!editmode && !allowedittoggle())
		return; // not in most multiplayer modes
	if (!(editmode = !editmode)) {
		settagareas();     // reset triggers to allow quick playtesting
		entinmap(player1); // find spawn closest to current floating pos
	} else {
		resettagareas(); // clear trigger areas to allow them to be
		                 // edited
		player1.health = 100;
		if (m_classicsp)
			// all monsters back at their spawns for editing
			[Monster resetAll];
		projreset();
	}
	Cube.sharedInstance.repeatsKeys = editmode;
	selset = false;
	editing = editmode;
}
COMMANDN(edittoggle, toggleedit, ARG_NONE)

void
correctsel() // ensures above invariant
{
	selset = !OUTBORD(sel.x, sel.y);
	int bsize = ssize - MINBORD;
	if (sel.xs + sel.x > bsize)
		sel.xs = bsize - sel.x;
	if (sel.ys + sel.y > bsize)
		sel.ys = bsize - sel.y;
	if (sel.xs <= 0 || sel.ys <= 0)
		selset = false;
}

bool
noteditmode()
{
	correctsel();
	if (!editmode)
		conoutf(@"this function is only allowed in edit mode");
	return !editmode;
}

bool
noselection()
{
	if (!selset)
		conoutf(@"no selection");
	return !selset;
}

#define EDITSEL                             \
	if (noteditmode() || noselection()) \
		return;
#define EDITSELMP                                            \
	if (noteditmode() || noselection() || multiplayer()) \
		return;
#define EDITMP                              \
	if (noteditmode() || multiplayer()) \
		return;

void
selectpos(int x, int y, int xs, int ys)
{
	struct block s = { x, y, xs, ys };
	sel = s;
	selh = 0;
	correctsel();
}

void
makesel()
{
	struct block s = { min(lastx, cx), min(lasty, cy), abs(lastx - cx) + 1,
		abs(lasty - cy) + 1 };
	sel = s;
	selh = max(lasth, ch);
	correctsel();
	if (selset)
		rtex = *S(sel.x, sel.y);
}

VAR(flrceil, 0, 0, 2);

// finds out z height when cursor points at wall
float
sheight(struct sqr *s, struct sqr *t, float z)
{
	return !flrceil // z-s->floor<s->ceil-z
	    ? (s->type == FHF ? s->floor - t->vdelta / 4.0f : (float)s->floor)
	    : (s->type == CHF ? s->ceil + t->vdelta / 4.0f : (float)s->ceil);
}

void
cursorupdate() // called every frame from hud
{
	flrceil = ((int)(player1.pitch >= 0)) * 2;

	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))
		return;
	struct sqr *s = S(cx, cy);

	// selected wall
	if (fabs(sheight(s, s, z) - z) > 1) {
		// find right wall cube
		x += (x > player1.origin.x ? 0.5f : -0.5f);
		y += (y > player1.origin.y ? 0.5f : -0.5f);

		cx = (int)x;
		cy = (int)y;

		if (OUTBORD(cx, cy))
			return;
	}

	if (dragging)
		makesel();

	const int GRIDSIZE = 5;
	const float GRIDW = 0.5f;
	const float GRID8 = 2.0f;
	const float GRIDS = 2.0f;
	const int GRIDM = 0x7;

	// render editing grid

	for (int ix = cx - GRIDSIZE; ix <= cx + GRIDSIZE; ix++) {
		for (int iy = cy - GRIDSIZE; iy <= cy + GRIDSIZE; iy++) {
			if (OUTBORD(ix, iy))
				continue;
			struct sqr *s = S(ix, iy);
			if (SOLID(s))
				continue;
			float h1 = sheight(s, s, z);
			float h2 = sheight(s, SWS(s, 1, 0, ssize), z);
			float h3 = sheight(s, SWS(s, 1, 1, ssize), z);
			float h4 = sheight(s, SWS(s, 0, 1, ssize), z);
			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);
			struct 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);
		struct block b = { cx, cy, 1, 1 };
		box(&b, ih, sheight(s, SWS(s, 1, 0, ssize), z),
		    sheight(s, SWS(s, 1, 1, ssize), z),
		    sheight(s, SWS(s, 0, 1, ssize), z));
		linestyle(GRIDS, 0xFF, 0x00, 0x00);
		dot(cx, cy, ih);
		ch = (int)ih;
	}

	if (selset) {
		linestyle(GRIDS, 0xFF, 0x40, 0x40);
		box(&sel, (float)selh, (float)selh, (float)selh, (float)selh);
	}
}

static OFMutableData *undos; // unlimited undo
VARP(undomegs, 0, 1, 10);    // bounded by n megs

void
pruneundos(int maxremain) // bound memory
{
	int t = 0;
	for (ssize_t i = (ssize_t)undos.count - 1; i >= 0; i--) {
		struct block *undo = [undos mutableItemAtIndex:i];

		t += undo->xs * undo->ys * sizeof(struct sqr);
		if (t > maxremain) {
			OFFreeMemory(undo);
			[undos removeItemAtIndex:i];
		}
	}
}

void
makeundo()
{
	if (undos == nil)
		undos = [[OFMutableData alloc]
		    initWithItemSize:sizeof(struct block *)];

	struct block *copy = blockcopy(&sel);
	[undos addItem:&copy];
	pruneundos(undomegs << 20);
}

void
editundo()
{
	EDITMP;
	if (undos.count == 0) {
		conoutf(@"nothing more to undo");
		return;
	}
	struct block *p = undos.mutableLastItem;
	[undos removeLastItem];
	blockpaste(p);
	OFFreeMemory(p);
}

static struct block *copybuf = NULL;

void
copy()
{
	EDITSELMP;

	if (copybuf)
		OFFreeMemory(copybuf);

	copybuf = blockcopy(&sel);
}

void
paste()
{
	EDITMP;
	if (!copybuf) {
		conoutf(@"nothing to paste");
		return;
	}
	sel.xs = copybuf->xs;
	sel.ys = copybuf->ys;
	correctsel();
	if (!selset || sel.xs != copybuf->xs || sel.ys != copybuf->ys) {
		conoutf(@"incorrect selection");
		return;
	}
	makeundo();
	copybuf->x = sel.x;
	copybuf->y = sel.y;
	blockpaste(copybuf);
}

void
tofronttex() // maintain most recently used of the texture lists when applying
             // texture
{
	for (int i = 0; i < 3; i++) {
		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;
		}
	}
}

void
editdrag(bool isDown)
{
	if ((dragging = isDown)) {
		lastx = cx;
		lasty = cy;
		lasth = ch;
		selset = false;
		tofronttex();
	}
	makesel();
}

// 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, const struct block *sel)
{
	loopselxy(
	    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;
	editheightxy(isfloor, amount, &sel);
	addmsg(1, 7, SV_EDITH, sel.x, sel.y, sel.xs, sel.ys, isfloor, amount);
}
COMMAND(editheight, ARG_2INT)

void
edittexxy(int type, int t, const struct block *sel)
{
	loopselxy(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];
	edittexxy(type, t, &sel);
	addmsg(1, 7, SV_EDITT, sel.x, sel.y, sel.xs, sel.ys, type, t);
}

void
replace()
{
	EDITSELMP;

	for (int x = 0; x < ssize; x++) {
		for (int y = 0; y < ssize; y++) {
			struct 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;
			}
		}
	}

	struct block b = { 0, 0, ssize, ssize };
	remip(&b, 0);
}

void
edittypexy(int type, const struct block *sel)
{
	loopselxy(s->type = type);
}

void
edittype(int type)
{
	EDITSEL;

	if (type == CORNER &&
	    (sel.xs != sel.ys || sel.xs == 3 || (sel.xs > 4 && sel.xs != 8) ||
	        sel.x & ~-sel.xs || sel.y & ~-sel.ys)) {
		conoutf(@"corner selection must be power of 2 aligned");
		return;
	}

	edittypexy(type, &sel);
	addmsg(1, 6, SV_EDITS, sel.x, sel.y, sel.xs, sel.ys, type);
}

void
heightfield(int t)
{
	edittype(t == 0 ? FHF : CHF);
}
COMMAND(heightfield, ARG_1INT)

void
solid(int t)
{
	edittype(t == 0 ? SPACE : SOLID);
}
COMMAND(solid, ARG_1INT)

void
corner()
{
	edittype(CORNER);
}
COMMAND(corner, ARG_NONE)

void
editequalisexy(bool isfloor, const struct block *sel)
{
	int low = 127, hi = -128;
	loopselxy({
		if (s->floor < low)
			low = s->floor;
		if (s->ceil > hi)
			hi = s->ceil;
	});
	loopselxy({
		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;

	editequalisexy(isfloor, &sel);
	addmsg(1, 6, SV_EDITE, sel.x, sel.y, sel.xs, sel.ys, isfloor);
}
COMMAND(equalize, ARG_1INT)

void
setvdeltaxy(int delta, const struct block *sel)
{
	loopselxy(s->vdelta = max(s->vdelta + delta, 0));
	remipmore(sel, 0);
}

void
setvdelta(int delta)
{
	EDITSEL;

	setvdeltaxy(delta, &sel);
	addmsg(1, 6, SV_EDITD, sel.x, sel.y, sel.xs, sel.ys, delta);
}

#define MAXARCHVERT 50
int archverts[MAXARCHVERT][MAXARCHVERT];
bool archvinit = false;

void
archvertex(int span, int vert, int delta)
{
	if (!archvinit) {
		archvinit = true;
		for (int s = 0; s < MAXARCHVERT; s++)
			for (int v = 0; v < MAXARCHVERT; v++)
				archverts[s][v] = 0;
	}
	if (span >= MAXARCHVERT || vert >= MAXARCHVERT || span < 0 || vert < 0)
		return;
	archverts[span][vert] = delta;
}

void
arch(int sidedelta, int _a)
{
	EDITSELMP;

	sel.xs++;
	sel.ys++;

	if (sel.xs > MAXARCHVERT)
		sel.xs = MAXARCHVERT;
	if (sel.ys > MAXARCHVERT)
		sel.ys = MAXARCHVERT;

	struct block *sel_ = &sel;
	// Ugly hack to make the macro work.
	struct block *sel = sel_;
	loopselxy(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, 0);
}

void
slope(int xd, int yd)
{
	EDITSELMP;

	int off = 0;
	if (xd < 0)
		off -= xd * sel.xs;
	if (yd < 0)
		off -= yd * sel.ys;

	sel.xs++;
	sel.ys++;

	struct block *sel_ = &sel;
	// Ugly hack to make the macro work.
	struct block *sel = sel_;
	loopselxy(s->vdelta = xd * x + yd * y + off);
	remipmore(sel, 0);
}

void
perlin(int scale, int seed, int psize)
{
	EDITSELMP;

	sel.xs++;
	sel.ys++;

	makeundo();

	sel.xs--;
	sel.ys--;

	perlinarea(&sel, scale, seed, psize);

	sel.xs++;
	sel.ys++;

	remipmore(&sel, 0);

	sel.xs--;
	sel.ys--;
}

VARF(
    fullbright, 0, 0, 1, if (fullbright) {
	    if (noteditmode())
		    return;
	    for (int i = 0; i < mipsize; i++)
		    world[i].r = world[i].g = world[i].b = 176;
    });

void
edittag(int tag)
{
	EDITSELMP;

	struct block *sel_ = &sel;
	// Ugly hack to make the macro work.
	struct block *sel = sel_;
	loopselxy(s->tag = tag);
}

void
newent(OFString *what, OFString *a1, OFString *a2, OFString *a3, OFString *a4)
{
	EDITSEL;

	newentity(sel.x, sel.y, (int)player1.origin.z, what,
	    [a1 cube_intValueWithBase:0], [a2 cube_intValueWithBase:0],
	    [a3 cube_intValueWithBase:0], [a4 cube_intValueWithBase:0]);
}

COMMANDN(select, selectpos, ARG_4INT)
COMMAND(edittag, ARG_1INT)
COMMAND(replace, ARG_NONE)
COMMAND(archvertex, ARG_3INT)
COMMAND(arch, ARG_2INT)
COMMAND(slope, ARG_2INT)
COMMANDN(vdelta, setvdelta, ARG_1INT)
COMMANDN(undo, editundo, ARG_NONE)
COMMAND(copy, ARG_NONE)
COMMAND(paste, ARG_NONE)
COMMAND(edittex, ARG_2INT)
COMMAND(newent, ARG_5STR)
COMMAND(perlin, ARG_3INT)