//-------------------------------------------------------------------------
/*
Copyright (C) 1996, 2003 - 3D Realms Entertainment
Copyright (C) 2020 - Christoph Oelckers

This file is part of Duke Nukem 3D version 1.5 - Atomic Edition

Duke Nukem 3D is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

Original Source: 1996 - Todd Replogle
Prepared for public release: 03/21/2003 - Charlie Wiederhold, 3D Realms
Modifications for JonoF's port by Jonathon Fowler (jf@jonof.id.au)
*/
//------------------------------------------------------------------------- 

#include "automap.h"
#include "c_dispatch.h"
#include "c_cvars.h"
#include "gstrings.h"
#include "printf.h"
#include "serializer.h"
#include "v_2ddrawer.h"
#include "earcut.hpp"
#include "buildtiles.h"
#include "d_event.h"
#include "c_bind.h"
#include "gamestate.h"
#include "gamecontrol.h"
#include "quotemgr.h"
#include "v_video.h"
#include "gamestruct.h"
#include "v_draw.h"
#include "sectorgeometry.h"
#include "gamefuncs.h"
#include "hw_sections.h"
#include "coreactor.h"
#include "texturemanager.h"

CVAR(Bool, am_followplayer, true, CVAR_ARCHIVE)
CVAR(Bool, am_rotate, true, CVAR_ARCHIVE)
CVAR(Float, am_linealpha, 1.0f, CVAR_ARCHIVE)
CVAR(Int, am_linethickness, 1, CVAR_ARCHIVE)
CVAR(Bool, am_textfont, false, CVAR_ARCHIVE)
CVAR(Bool, am_showlabel, false, CVAR_ARCHIVE)
CVAR(Bool, am_nameontop, false, CVAR_ARCHIVE)

static DVector2 min_bounds = { INT_MAX, 0 };;
static DVector2 max_bounds = { 0, 0 };
static DVector2 follow = { INT_MAX, INT_MAX };
static DAngle follow_a = DAngle::fromDeg(INT_MAX);
static double gZoom = 0.75;
static float am_zoomdir;
int automapMode;
bool automapping;
bool gFullMap;
BitArray show2dsector;
BitArray show2dwall;

CVAR(Color, am_twosidedcolor, 0xaaaaaa, CVAR_ARCHIVE)
CVAR(Color, am_onesidedcolor, 0xaaaaaa, CVAR_ARCHIVE)
CVAR(Color, am_playercolor, 0xaaaaaa, CVAR_ARCHIVE)
CVAR(Color, am_ovtwosidedcolor, 0xaaaaaa, CVAR_ARCHIVE)
CVAR(Color, am_ovonesidedcolor, 0xaaaaaa, CVAR_ARCHIVE)
CVAR(Color, am_ovplayercolor, 0xaaaaaa, CVAR_ARCHIVE)

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

CCMD(allmap)
{
	if (!CheckCheatmode(true, false))
	{
		gFullMap = !gFullMap;
		Printf("%s\n", GStrings(gFullMap ? "SHOW MAP: ON" : "SHOW MAP: OFF"));
	}
}

CCMD(togglemap)
{
	if (gamestate == GS_LEVEL)
	{
		automapMode++;
		if (automapMode == am_count) automapMode = am_off;
		if (isBlood() && automapMode == am_overlay) automapMode = am_full; // todo: investigate if this can be re-enabled
	}
}

CCMD(togglefollow)
{
	am_followplayer = !am_followplayer;
	auto msg = quoteMgr.GetQuote(am_followplayer ? 84 : 83);
	if (!msg || !*msg) msg = am_followplayer ? GStrings("FOLLOW MODE ON") : GStrings("FOLLOW MODE Off");
	Printf(PRINT_NOTIFY, "%s\n", msg);
	if (am_followplayer) follow.X = INT_MAX;
}

CCMD(togglerotate)
{
	am_rotate = !am_rotate;
	auto msg = am_rotate ? GStrings("TXT_ROTATE_ON") : GStrings("TXT_ROTATE_OFF");
	Printf(PRINT_NOTIFY, "%s\n", msg);
}

CCMD(am_zoom)
{
	if (argv.argc() >= 2)
	{
		am_zoomdir = (float)atof(argv[1]);
	}
}

//==========================================================================
//
// AM_Responder
// Handle automap exclusive bindings.
//
//==========================================================================

bool AM_Responder(event_t* ev, bool last)
{
	if (ev->type == EV_KeyDown || ev->type == EV_KeyUp)
	{
		if (am_followplayer)
		{
			// check for am_pan* and ignore in follow mode
			const char* defbind = AutomapBindings.GetBind(ev->data1);
			if (defbind && !strnicmp(defbind, "+am_pan", 7)) return false;
		}

		bool res = C_DoKey(ev, &AutomapBindings, nullptr);
		if (res && ev->type == EV_KeyUp && !last)
		{
			// If this is a release event we also need to check if it released a button in the main Bindings
			// so that that button does not get stuck.
			const char* defbind = Bindings.GetBind(ev->data1);
			return (!defbind || defbind[0] != '+'); // Let G_Responder handle button releases
		}
		return res;
	}
	return false;
}

//---------------------------------------------------------------------------
//
// 
//
//---------------------------------------------------------------------------

static void CalcMapBounds()
{
	min_bounds = { INT_MAX, INT_MAX };
	max_bounds = { INT_MIN, INT_MIN };

	for(auto& wal : wall)
	{
		// get map min and max coordinates
		if (wal.pos.X < min_bounds.X) min_bounds.X = wal.pos.X;
		if (wal.pos.Y < min_bounds.Y) min_bounds.Y = wal.pos.Y;
		if (wal.pos.X > max_bounds.X) max_bounds.X = wal.pos.X;
		if (wal.pos.Y > max_bounds.Y) max_bounds.Y = wal.pos.Y;
	}
}

//---------------------------------------------------------------------------
//
// 
//
//---------------------------------------------------------------------------

static void AutomapControl(const DVector2& cangvect)
{
	static double nonsharedtimer;
	double ms = (double)screen->FrameTime;
	double interval;

	if (nonsharedtimer > 0 || ms < nonsharedtimer)
	{
		interval = ms - nonsharedtimer;
	}
	else
	{
		interval = 0;
	}
	nonsharedtimer = ms;

	if (System_WantGuiCapture())
		return;

	if (automapMode != am_off)
	{
		if (am_zoomdir > 0)
		{
			gZoom = gZoom * am_zoomdir;
		}
		else if (am_zoomdir < 0)
		{
			gZoom = gZoom / -am_zoomdir;
		}
		am_zoomdir = 0;

		double j = interval * (35. / 65536.) / gZoom;
		gZoom += (buttonMap.ButtonDown(gamefunc_Enlarge_Screen) - buttonMap.ButtonDown(gamefunc_Shrink_Screen)) * j * max(gZoom, 0.25);
		gZoom = clamp(gZoom, 0.05, 2.);

		if (!am_followplayer)
		{
			const double zoomspeed = j * 512.;
			const auto panhorz = buttonMap.ButtonDown(gamefunc_AM_PanRight) - buttonMap.ButtonDown(gamefunc_AM_PanLeft);
			const auto panvert = buttonMap.ButtonDown(gamefunc_AM_PanUp) - buttonMap.ButtonDown(gamefunc_AM_PanDown);

			if (min_bounds.X == INT_MAX) CalcMapBounds();

			follow = clamp(follow + DVector2(panvert, panhorz).Rotated(cangvect.X, cangvect.Y) * zoomspeed, min_bounds, max_bounds);
		}
	}
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void SerializeAutomap(FSerializer& arc)
{
	if (arc.BeginObject("automap"))
	{
		arc("automapping", automapping)
			("fullmap", gFullMap)
			("mappedsectors", show2dsector)
			("mappedwalls", show2dwall)
			.EndObject();
	}
}


//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void ClearAutomap()
{
	show2dsector.Zero();
	show2dwall.Zero();
	min_bounds.X = INT_MAX;
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void MarkSectorSeen(sectortype* sec)
{
	if (sec) 
	{
		show2dsector.Set(sectindex(sec));
		for (auto& wal : sec->walls)
		{
			if (!wal.twoSided()) continue;
			const auto bits = (CSTAT_WALL_BLOCK | CSTAT_WALL_MASKED | CSTAT_WALL_1WAY | CSTAT_WALL_BLOCK_HITSCAN);
			if (wal.cstat & bits) continue;
			if (wal.nextWall()->cstat & bits) continue;
			auto osec = wal.nextSector();
			if (osec->lotag == 32767) continue;
			if (osec->ceilingz >= osec->floorz) continue;
			show2dsector.Set(sectindex(osec));
		}
	}
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void drawlinergb(const DVector2& v1, const DVector2& v2, PalEntry p)
{
	if (am_linethickness <= 1) 
	{
		twod->AddLine(v1, v2, &viewport3d, p, uint8_t(am_linealpha * 255));
	}
	else
	{
		twod->AddThickLine(v1, v2, am_linethickness, p, uint8_t(am_linealpha * 255));
	}
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

static inline PalEntry RedLineColor()
{
	// todo:
	// Blood uses palette index 12 (99,99,99)
	// Exhumed uses palette index 111 (roughly 170,170,170) but darkens the line in overlay mode the farther it is away from the player in vertical direction.
	// Shadow Warrior uses palette index 152 in overlay mode and index 12 in full map mode. (152: 84, 88, 40)
	return automapMode == am_overlay? *am_ovtwosidedcolor : *am_twosidedcolor;
}

static inline PalEntry WhiteLineColor()
{

	// todo:
	// Blood uses palette index 24
	// Exhumed uses palette index 111 (roughly 170,170,170) but darkens the line in overlay mode the farther it is away from the player in vertical direction.
	// Shadow Warrior uses palette index 24 (60,60,60)
	return automapMode == am_overlay ? *am_ovonesidedcolor : *am_onesidedcolor;
}

static inline PalEntry PlayerLineColor()
{
	return automapMode == am_overlay ? *am_ovplayercolor : *am_playercolor;
}


CCMD(printpalcol)
{
	if (argv.argc() < 2) return;

	int i = atoi(argv[1]);
	Printf("%d, %d, %d\n", GPalette.BaseColors[i].r, GPalette.BaseColors[i].g, GPalette.BaseColors[i].b);
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

bool ShowRedLine(int j, int i)
{
	auto wal = &wall[j];
	if (!isSWALL())
	{
		return !gFullMap && !show2dsector[wal->nextsector];
	}
	else
	{
		if (!gFullMap)
		{
			if (!show2dwall[j]) return false;
			int k = wal->nextwall;
			if (k > j && !show2dwall[k]) return false; //???
		}
		if (automapMode == am_full)
		{
			if (sector[i].floorz != sector[i].ceilingz)
				if (wal->nextSector()->floorz != wal->nextSector()->ceilingz)
					if (((wal->cstat | wal->nextWall()->cstat) & (CSTAT_WALL_MASKED | CSTAT_WALL_1WAY)) == 0)
						if (sector[i].floorz == wal->nextSector()->floorz)
							return false;
			if (sector[i].floortexture != wal->nextSector()->floortexture)
				return false;
			if (sector[i].floorshade != wal->nextSector()->floorshade)
				return false;
		}
		return true;
	}
}

//---------------------------------------------------------------------------
//
// two sided lines
//
//---------------------------------------------------------------------------

static void drawredlines(const DVector2& cpos, const DVector2& cangvect, const DVector2& xydim)
{
	for (unsigned i = 0; i < sector.Size(); i++)
	{
		if (!gFullMap && !show2dsector[i]) continue;

		double z1 = sector[i].ceilingz;
		double z2 = sector[i].floorz;

		for (auto& wal : sector[i].walls)
		{
			if (!wal.twoSided()) continue;

			auto osec = wal.nextSector();

			if (osec->ceilingz == z1 && osec->floorz == z2)
				if (((wal.cstat | wal.nextWall()->cstat) & (CSTAT_WALL_MASKED | CSTAT_WALL_1WAY)) == 0) continue;

			if (ShowRedLine(wallindex(&wal), i))
			{
				auto v1 = OutAutomapVector(wal.pos - cpos, cangvect, gZoom, xydim);
				auto v2 = OutAutomapVector(wal.point2Wall()->pos - cpos, cangvect, gZoom, xydim);
				drawlinergb(v1, v2, RedLineColor());
			}
		}
	}
}

//---------------------------------------------------------------------------
//
// one sided lines
//
//---------------------------------------------------------------------------

static void drawwhitelines(const DVector2& cpos, const DVector2& cangvect, const DVector2& xydim)
{
	for (int i = (int)sector.Size() - 1; i >= 0; i--)
	{
		if (!gFullMap && !show2dsector[i] && !isSWALL()) continue;

		for (auto& wal : sector[i].walls)
		{
			if (wal.nextwall >= 0) continue;
			if (!gFullMap && !wal.walltexture.isValid()) continue;

			if (isSWALL() && !gFullMap && !show2dwall[wallindex(&wal)])
				continue;

			auto v1 = OutAutomapVector(wal.pos - cpos, cangvect, gZoom, xydim);
			auto v2 = OutAutomapVector(wal.point2Wall()->pos - cpos, cangvect, gZoom, xydim);
			drawlinergb(v1, v2, WhiteLineColor());
		}
	}
}

//---------------------------------------------------------------------------
//
// player sprite fallback
//
//---------------------------------------------------------------------------

static void DrawPlayerArrow(const DVector2& cpos, const DAngle cang, const double czoom, const DAngle pl_angle)
{
#if 0
	static constexpr int arrow[] =
	{
		0, 65536, 0, -65536,
		0, 65536, -32768, 32878,
		0, 65536, 32768, 32878,
	};

	double xvect = -cang.Sin() * czoom;
	double yvect = -cang.Cos() * czoom;

	double pxvect = -pl_angle.Sin();
	double pyvect = -pl_angle.Cos();

	int width = screen->GetWidth();
	int height = screen->GetHeight();

	for (int i = 0; i < 12; i += 4)
	{
		// FIXME: This has been broken since before the floatification refactor.
		// Needs repair and changing out to backended vector function.
		double px1 = (arrow[i] * pxvect) - (arrow[i+1] * pyvect);
		double py1 = (arrow[i+1] * pxvect) + (arrow[i] * pyvect) + (height * 0.5);
		double px2 = (arrow[i+2] * pxvect) - (arrow[i+3] * pyvect);
		double py2 = (arrow[i+3] * pxvect) + (arrow[i+2] * pyvect) + (height * 0.5);

		auto oxy1 = DVector2(px1, py1) - cpos;
		auto oxy2 = DVector2(px2, py2) - cpos;

		double sx1 = (oxy1.X * xvect) - (oxy1.Y * yvect) + (width * 0.5);
		double sy1 = (oxy1.Y * xvect) + (oxy1.X * yvect) + (height * 0.5);
		double sx2 = (oxy2.X * xvect) - (oxy2.Y * yvect) + (width * 0.5);
		double sy2 = (oxy2.Y * xvect) + (oxy2.X * yvect) + (height * 0.5);

		drawlinergb(sx1, sy1, sx2, sy2, WhiteLineColor());
	}
#endif
}


//---------------------------------------------------------------------------
//
// floor textures
//
//---------------------------------------------------------------------------

static void renderDrawMapView(const DVector2& cpos, const DVector2& cangvect, const DVector2& xydim)
{
	TArray<FVector4> vertices;
	TArray<DCoreActor*> floorsprites;

	for (int i = (int)sector.Size() - 1; i >= 0; i--)
	{
		auto sect = &sector[i];
		if (!gFullMap && !show2dsector[i]) continue;

		//Collect floor sprites to draw
		TSectIterator<DCoreActor> it(sect);
		while (auto act = it.Next())
		{
			if (act->spr.cstat & CSTAT_SPRITE_INVISIBLE)
				continue;

			if (act->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR)	// floor and slope sprites
			{
				if ((act->spr.cstat & (CSTAT_SPRITE_ONE_SIDE | CSTAT_SPRITE_YFLIP)) == (CSTAT_SPRITE_ONE_SIDE | CSTAT_SPRITE_YFLIP))
					continue; // upside down
				floorsprites.Push(act);
			}
		}

		if (sect->floorstat & CSTAT_SECTOR_SKY) continue;

		auto flortex = sect->floortexture;
		if (!flortex.isValid()) continue;

		int translation = TRANSLATION(Translation_Remap + curbasepal, sector[i].floorpal);
		PalEntry light = shadeToLight(sector[i].floorshade);

		for (auto section : sectionsPerSector[i])
		{
			TArray<int>* indices;
			auto mesh = sectionGeometry.get(&sections[section], 0, { 0.f, 0.f }, &indices);
			vertices.Resize(mesh->vertices.Size());
			for (unsigned j = 0; j < mesh->vertices.Size(); j++)
			{
				auto v = OutAutomapVector(DVector2(mesh->vertices[j].X - cpos.X, -mesh->vertices[j].Y - cpos.Y), cangvect, gZoom, xydim);
				vertices[j] = { float(v.X), float(v.Y), mesh->texcoords[j].X, mesh->texcoords[j].Y };
			}

			twod->AddPoly(TexMan.GetGameTexture(flortex, true), vertices.Data(), vertices.Size(), (unsigned*)indices->Data(), indices->Size(), translation, light,
				LegacyRenderStyles[STYLE_Translucent], &viewport3d);
		}
	}
	qsort(floorsprites.Data(), floorsprites.Size(), sizeof(DCoreActor*), [](const void* a, const void* b)
		{
			auto A = *(DCoreActor**)a;
			auto B = *(DCoreActor**)b;
			if (A->spr.pos.Z < B->spr.pos.Z) return 1;
			if (A->spr.pos.Z > B->spr.pos.Z) return -1;
			return A->time - B->time; // ensures stable sort.
		});

	vertices.Resize(4);
	for (auto actor : floorsprites)
	{
		if (!gFullMap && !(actor->spr.cstat2 & CSTAT2_SPRITE_MAPPED)) continue;
		DVector2 pp[4];
		GetFlatSpritePosition(actor, actor->spr.pos.XY(), pp, nullptr, true);

		for (unsigned j = 0; j < 4; j++)
		{
			auto v = OutAutomapVector(pp[j] - cpos, cangvect, gZoom, xydim);
			vertices[j] = { float(v.X), float(v.Y), j == 1 || j == 2 ? 1.f : 0.f, j == 2 || j == 3 ? 1.f : 0.f };
		}
		int shade;
		if ((actor->sector()->ceilingstat & CSTAT_SECTOR_SKY)) shade = actor->sector()->ceilingshade;
		else shade = actor->sector()->floorshade;
		shade += actor->spr.shade;
		PalEntry color = shadeToLight(shade);
		FRenderStyle rs = LegacyRenderStyles[STYLE_Translucent];
		float alpha = 1;
		if (actor->spr.cstat & CSTAT_SPRITE_TRANSLUCENT)
		{
			rs = GetRenderStyle(0, !!(actor->spr.cstat & CSTAT_SPRITE_TRANS_FLIP));
			alpha = GetAlphaFromBlend((actor->spr.cstat & CSTAT_SPRITE_TRANS_FLIP) ? DAMETH_TRANS2 : DAMETH_TRANS1, 0);
			color.a = uint8_t(alpha * 255);
		}

		int translation = TRANSLATION(Translation_Remap + curbasepal, actor->spr.pal);
		const static unsigned indices[] = { 0, 1, 2, 0, 2, 3 };
		twod->AddPoly(TexMan.GetGameTexture(actor->spr.spritetexture(), true), vertices.Data(), vertices.Size(), indices, 6, translation, color, rs, &viewport3d);
	}
}

//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------

void DrawOverheadMap(const DVector2& plxy, const DAngle pl_angle, double const interpfrac)
{
	if (am_followplayer || follow.X == INT_MAX)
	{
		follow = plxy;
	}

	follow_a = am_rotate ? pl_angle : DAngle270;
	const DVector2 xydim = DVector2(screen->GetWidth(), screen->GetHeight()) * 0.5;
	const DVector2 avect = follow_a.ToVector();

	AutomapControl(avect);

	if (automapMode == am_full)
	{
		twod->ClearScreen();
		renderDrawMapView(follow, avect, xydim);
	}

	drawredlines(follow, avect, xydim);
	drawwhitelines(follow, avect, xydim);
	if (!gi->DrawAutomapPlayer(plxy, follow, follow_a, xydim, gZoom, interpfrac))
		DrawPlayerArrow(follow, follow_a, gZoom, pl_angle);

}

//---------------------------------------------------------------------------
//
// Draws lines for alls in Duke/SW when cstat is CSTAT_SPRITE_ALIGNMENT_FACING.
//
//---------------------------------------------------------------------------

void DrawAutomapAlignmentFacing(const spritetype& spr, const DVector2& bpos, const DVector2& cangvect, const double czoom, const DVector2& xydim, const PalEntry& col)
{
	auto v1 = OutAutomapVector(bpos, cangvect, czoom, xydim);
	auto v2 = OutAutomapVector(spr.Angles.Yaw.ToVector() * 8., cangvect, czoom);
	auto v3 = v2.Rotated90CW();
	auto v4 = v1 + v2;

	drawlinergb(v1 - v2, v4, col);
	drawlinergb(v1 - v3, v4, col);
	drawlinergb(v1 + v3, v4, col);
}

//---------------------------------------------------------------------------
//
// Draws lines for sprites in Duke/SW when cstat is CSTAT_SPRITE_ALIGNMENT_WALL.
//
//---------------------------------------------------------------------------

void DrawAutomapAlignmentWall(const spritetype& spr, const DVector2& bpos, const DVector2& cangvect, const double czoom, const DVector2& xydim, const PalEntry& col)
{
	auto tex = TexMan.GetGameTexture(spr.spritetexture());
	auto xrep = spr.scale.X;
	int xspan = (int)tex->GetDisplayWidth();
	int xoff = (int)tex->GetDisplayLeftOffset() + spr.xoffset;

	if ((spr.cstat & CSTAT_SPRITE_XFLIP) > 0) xoff = -xoff;

	auto sprvec = spr.Angles.Yaw.ToVector().Rotated90CW() * xrep;
	
	auto b1 = bpos - sprvec * ((xspan * 0.5) + xoff);
	auto b2 = b1 + sprvec * xspan;

	auto v1 = OutAutomapVector(b1, cangvect, czoom, xydim);
	auto v2 = OutAutomapVector(b2, cangvect, czoom, xydim);

	drawlinergb(v1, v2, col);
}


//---------------------------------------------------------------------------
//
// Draws lines for alls in Duke/SW when cstat is CSTAT_SPRITE_ALIGNMENT_FLOOR.
//
//---------------------------------------------------------------------------

void DrawAutomapAlignmentFloor(const spritetype& spr, const DVector2& bpos, const DVector2& cangvect, const double czoom, const DVector2& xydim, const PalEntry& col)
{
	auto tex = TexMan.GetGameTexture(spr.spritetexture());

	auto xrep = spr.scale.X;
	auto yrep = spr.scale.Y;
	int xspan = (int)tex->GetDisplayWidth();
	int yspan = (int)tex->GetDisplayHeight();
	int xoff = (int)tex->GetDisplayLeftOffset();
	int yoff = (int)tex->GetDisplayTopOffset();

	if (isSWALL() || (spr.cstat & CSTAT_SPRITE_ALIGNMENT_MASK) != CSTAT_SPRITE_ALIGNMENT_SLOPE)
	{
		xoff += spr.xoffset;
		yoff += spr.yoffset;
	}

	if ((spr.cstat & CSTAT_SPRITE_XFLIP) > 0) xoff = -xoff;
	if ((spr.cstat & CSTAT_SPRITE_YFLIP) > 0) yoff = -yoff;

	auto sprvec = spr.Angles.Yaw.ToVector();
	auto xscale = sprvec.Rotated90CW() * xspan * xrep;
	auto yscale = sprvec * yspan * yrep;
	auto xybase = DVector2(((xspan * 0.5) + xoff) * xrep, ((yspan * 0.5) + yoff) * yrep);

	auto b1 = bpos + (xybase * sprvec.Y) + (xybase.Rotated90CW() * sprvec.X);
	auto b2 = b1 - xscale;
	auto b3 = b2 - yscale;
	auto b4 = b1 - yscale;

	auto v1 = OutAutomapVector(b1, cangvect, czoom, xydim);
	auto v2 = OutAutomapVector(b2, cangvect, czoom, xydim);
	auto v3 = OutAutomapVector(b3, cangvect, czoom, xydim);
	auto v4 = OutAutomapVector(b4, cangvect, czoom, xydim);

	drawlinergb(v1, v2, col);
	drawlinergb(v2, v3, col);
	drawlinergb(v3, v4, col);
	drawlinergb(v4, v1, col);
}