////////////////////////////////////////////////////////////////////////////////
// MechSim.cpp
// Simulation screen for MechVM
// Copyright Bjoern Ganster
// Developed since 2007
////////////////////////////////////////////////////////////////////////////////

#include "MechSim.h"

#include "AeroTech.h"
#include "Audio.h"
#include "Config.h"
#include "Font.h"
#include "GameObject.h"
#include "intersections.h"
#include "MechSimOverlay.h"
#include "MechVM.h"
#include "Sound.h"
#include "Vehicles.h"
#include "VehicleAI.h"

#ifdef MECHVM
#include "ArchiveViewer.h"
#include "ISO9660.h"
#include "MechWarrior3GameZArchive.h"
#endif

#include "GL/glu.h"

#include <algorithm>
#include <iostream>

#define FILE_NUMBER FILE_NO_MECHSIM_CPP

double MAX_ZOOM = 60.0;
double MIN_ZOOM = 10.0;

class MechShell;

////////////////////////////////////////////////////////////////////////////////
// Access global MechSim variable ...

MechSim* gMechSim = nullptr;

MechSim* getMechSim()
{
	return gMechSim;
}

////////////////////////////////////////////////////////////////////////////////
// Constructor

MechSim::MechSim()
: GLWindow("MechSim", MechSim_ID),
  m_sceneObjects (new RenderableObject("MechSim scene", RenderableObjectID)),
  playerVehicleAI(NULL),
  returnVis(nullptr),
  selectedObject(1),
  frame(0),
  lastFrame(0),
  mSatelliteDist(500.0),
  textureBuffer(GL_RGB),
  mCurrentNavPoint(0),
  #ifdef MECHVM
  mArchive(nullptr),
  #endif
  mMissionSuccessful(false),
  mMfdMode(MFD_ShowWeapons),
  mShots("Shots", RenderableObjectID),
  mNow(0),
  mFreeViewTheta(0.0),
  mFreeViewDeltaTheta(0.0),
  mShell(nullptr),
  mEnvironment(normal),
  mSoundFrameDurationMS(100),
  music(nullptr),
  mSkybox(nullptr),
  mDisplayLight(GL_LIGHT0),
  mGravity(9.81),
  mMechSimDebug(false),
  mPaused(true),
  mZoom(MAX_ZOOM),
  mViewingMode(NormalView),
  mSuccessMission("Starport"),
  mFailureMission("Starport"),
  mPausedPic(nullptr),
  mFont(nullptr),
  mOverlayDivider(2),
  mAllowRenderShadows(true)
{
	SET_SCOPE;
	gMechSim = this;
	SDL_ShowCursor(SDL_DISABLE);
	mMissionStarted = SDL_GetTicks();
	
	mDisplayLight.mAmbientFactor = 0.5;
	mDisplayLight.color = Color(0.5, 0.5, 0.5);
	mDisplayLight.mConstantAttenuation = 1.0;
	mDisplayLight.mLinearAttenuation = 0.0;
	mDisplayLight.mQuadraticAttenuation = 0.0;

	SET_OP;
	Config* config = getConfig();
	
	SET_OP;
	Audio* audio = getAudio();
	audio->unpause();
	mSoundMutex = SDL_CreateMutex();
	setAudioProvider(this);

	// Retrieve rendering settings from the config ...
	BGCPString overlayDividerStr = config->getValue("overlayDivider");
	mOverlayDivider = (int) overlayDividerStr.toInt();
	BGCPString allowRenderShadowsStr = config->getValue("renderShadows");
	mAllowRenderShadows = (allowRenderShadowsStr.toInt() > 0);

	mFont = getDefaultFont();

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Destructor

MechSim::~MechSim()
{
	SET_SCOPE;

	// Make sure rendering is complete before deleting data
	glFinish();
	gMechSim = nullptr;
	safeDelete(m_sceneObjects);
	safeDelete(mPausedPic);


	SET_OP;
	for (size_t i = 0; i < mManagedObjects.size(); ++i) {
		ManagedObject& o = mManagedObjects[i];
		safeDelete(o.object);
	}

	mManagedObjects.clear();

	auto mechVM = getMechVm();
	mechVM->setFullscreenMode(false);

	for (size_t i = 0; i < mLightSources.size(); ++i) {
		safeDelete(mLightSources[i]);
	}

	SET_OP;
	if (mFont != getDefaultFont()) {
		safeDelete(mFont);
	}

	// Sound things have to be deleted with a locked mutex ...
	SET_OP;
	SDL_LockMutex(mSoundMutex);
	setAudioProvider(nullptr);
	safeDelete(music);
	for (auto iter = mQueuedSounds.begin(); iter != mQueuedSounds.end(); ++iter) {
		safeDelete(iter->sound);
	}
	SDL_UnlockMutex(mSoundMutex);
	SDL_DestroyMutex(mSoundMutex);
	SDL_ShowCursor(SDL_ENABLE);
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Receive messages

void MechSim::receiveMessage (VirtualClass* /*sender*/, int messageType, VirtualClass* data)
{
}

////////////////////////////////////////////////////////////////////////////////
// Update window title

void MechSim::updateTitle ()
{
}

////////////////////////////////////////////////////////////////////////////////
// Render map or target

void MechSim::renderMap(Vehicle* vehicle, Texture* texture, DWORD now, double u1, double v1, double u2, double v2)
{
	if (vehicle == nullptr || texture == nullptr) {
		return;
	}

	// Render map to texture
	SET_SCOPE;

	const auto& mapDisplayCoords = vehicle->mapDisplayCoords;
	auto tw = texture->getWidth();
	auto th = texture->getHeight();
	TextureDim x1 = (TextureDim)(tw * u1);
	TextureDim y1 = (TextureDim)(th * v1);
	TextureDim x2 = (TextureDim)(tw * u2);
	TextureDim y2 = (TextureDim)(th * v2);

	if (x2 < x1 || y2 < y1) {
		RETURN;
	}

	TextureDim stopx = x2-x1;
	TextureDim stopy = y2-y1;
	textureBuffer.prepare(stopx, stopy, true);

	SET_OP;
	glClearColor(mapGroundColor.r, mapGroundColor.g, mapGroundColor.b, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glViewport(0, 0, stopx, stopy);

	// Set viewing projection
	SET_OP;
	glEnable(GL_NORMALIZE);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	Point3D satelliteCenter = vehicle->getPosition();
	gluPerspective(60.0, ((double)stopx) / ((double)stopy), 0.1, satelliteCenter.y + 1.05 * mSatelliteDist);
	Point3D satellitePos = satelliteCenter + Point3D(0.0, mSatelliteDist, 0.0);
	gluLookAt(satellitePos.x, satellitePos.y, satellitePos.z,
		satelliteCenter.x, satelliteCenter.y, satelliteCenter.z,
		0, 0, 1);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	// Render scene
	SET_OP;
	mDisplayLight.deactivate();
	activateLightSources(false);
	glColor3d(1, 1, 1);
	if (m_sceneObjects != NULL) {
		m_sceneObjects->render();
	}
	deactivateLightSources();
	glDisable(GL_LIGHTING);

	// Render nav points
	glDisable(GL_TEXTURE_2D);
	glDisable(GL_DEPTH_TEST);
	if (mNavPoints.size() > 0) {
		SET_OP;
		double flicker = (now % 500) / 500.0;
		glPointSize(3.0);
		glLoadIdentity();
		glBegin(GL_POINTS);
			for (size_t i = 0; i < mNavPoints.size(); ++i) {
				SET_OP;
				if (mNavPoints[i].visited) {
					glColor3d(0.0, flicker, 0.0);
				} else {
					glColor3d(flicker, flicker, 0.0);
				}
				mNavPoints[i].pos.glVertex();
			}

			// Render a dotted line to the currently selected nav point
			SET_OP;
			if (mNavPoints[mCurrentNavPoint].visited) {
				glColor3d(0.0, flicker, 0.0);
			} else {
				glColor3d(flicker, flicker, 0.0);
			}
		glEnd();
		glPointSize(3.0);
		glBegin(GL_POINTS);
		SET_OP;
			double dist = (vehicle->getPosition() - mNavPoints[mCurrentNavPoint].pos).length();
			double pointDist = 100.0; // todo: determine this based on the radar viewing distance?
			size_t pointsToDraw = (size_t)(dist / pointDist);
			for (size_t i = 1; i < pointsToDraw; ++i) {
				Point3D dist = mNavPoints[mCurrentNavPoint].pos - vehicle->getPosition();
				Point3D p = vehicle->getPosition() + i * dist / pointsToDraw;
				p.glVertex();
			}
		glEnd();
	}

	// Show walking direction
	SET_OP;
	Point3D mechPos = vehicle->getPosition();
	Point3D walkingDir = mechPos + 1000.0 * vehicle->p_front;
	glColor3f(1.0f, 0.0f, 0.0f);
	glBegin(GL_LINES);
		mechPos.glVertex();
		walkingDir.glVertex();
	glEnd();

	SET_OP;
	textureBuffer.stopRenderingToTexture(false);
	texture->copy(&textureBuffer, 0, 0, stopx, stopy, x1, y1);
	if (mCurrentNavPoint < mNavPoints.size() && mFont != nullptr) {
		// Show nav point name and distance
		SET_OP;
		double dist = (vehicle->getPosition() - mNavPoints[mCurrentNavPoint].pos).length();
		BGCPString msg = mNavPoints[mCurrentNavPoint].name + BGCPString(": ") + BGCPString::fromInt((int64_t)dist) + BGCPString("m");
		mFont->renderText(texture, msg, x1, y1+2, x2, y1, 128, 128, 255);
	}

	RETURN;
}

void MechSim::renderTarget(Vehicle* vehicle, Texture* texture, DWORD now, double u1, double v1, double u2, double v2)
{
	// Render target to texture
	SET_SCOPE;

	GameObject* target =  playerVehicleAI->getTarget();
	if (target == nullptr || texture == nullptr) {
		RETURN;
	}

	auto tw = texture->getWidth();
	auto th = texture->getHeight();
	TextureDim x1 = (TextureDim)(tw * u1);
	TextureDim y1 = (TextureDim)(th * v1);
	TextureDim x2 = (TextureDim)(tw * u2);
	TextureDim y2 = (TextureDim)(th * v2);
	TextureDim stopx = x2-x1;
	TextureDim stopy = y2-y1;
	textureBuffer.prepare(stopx, stopy, true);

	SET_OP;
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glViewport(0, 0, stopx, stopy);

	SET_OP;
	activateLightSources(true);
	target->setWireframeColors();

	// Set viewing projection
	SET_OP;
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, ((double)stopx) / ((double)stopy), 0.1, 5000.0);

	Point3D minPos, maxPos;
	target->getMinCoords(minPos);
	target->getMaxCoords(maxPos);
	double targetSize = (minPos - maxPos).length();
	Point3D targetMid = 0.5 * (minPos + maxPos);
	Point3D dist = targetMid - vehicle->getPosition();
	dist.unitLength();
	Point3D eye = targetMid - 0.8 * targetSize * dist;
	gluLookAt(eye.x, eye.y, eye.z,
		targetMid.x, targetMid.y, targetMid.z,
		0, 1, 0);
	glMatrixMode(GL_MODELVIEW);

	glLoadIdentity();
	glColor3d(1, 1, 1);
	target->render();

	// Render wireframe ...
	glClear(GL_COLOR_BUFFER_BIT);
	glDisable(GL_LIGHTING);
	glDisable(GL_TEXTURE_2D);
	target->renderWireframe();

	SET_OP;
	textureBuffer.stopRenderingToTexture(false);

	//texture->fillRect(x1, y1, x2, y2, 0, 0, 0);
	texture->copy(&textureBuffer, 0, 0, stopx, stopy, x1, y1);

	// Display target designation
	SET_OP;
	if (mFont != nullptr) {
		BGCPString msg = target->getDescriptionText();
		int y = y1 + mFont->getCharHeight() + 2;
		if (target->getInspected()) {
			msg += BGCPString("\n") + target->getInspectionText();
			y += mFont->getCharHeight()+2;
		}

		// getPosition yields local coordinates, but we need coordinates in the top-hierarchy of coordinate systems
		SET_OP;
		Point3D min, max;
		target->getMinCoords(min);
		target->getMaxCoords(max);
		Point3D ownPos = vehicle->getPosition();
		Point3D targetPos = 0.5 * (min+max);
		int dist2 = (int) (ownPos-targetPos).length();
		msg += BGCPString("\n") + BGCPString::fromInt(dist2) + " m";
		mFont->renderText(texture, msg, x1, y, x2, y1, 128, 128, 255);
	}
	RETURN;
}

void MechSim::renderCockpitInstruments(Texture* renderTargetTexture, Vehicle* vehicle)
{
	if (mFont == nullptr) {
		return;
	}

	SET_SCOPE;
	auto tw = renderTargetTexture->getWidth();
	auto th = renderTargetTexture->getHeight();

	SET_OP;
	MechSimOverlay overlay(renderTargetTexture);
	overlay.mFont = mFont;
	const auto& heatDisplayCoords = vehicle->heatDisplayCoords;
	const auto& speedDisplayCoords = vehicle->speedDisplayCoords;
	const auto& jumpJetDisplayCoords = vehicle->jumpJetDisplayCoords;

	double speed = playerVehicleAI->getSpeed();
	double speedPercent = vehicle->getSpeedPercent();
	double heat = vehicle->getHeat();

	// Speedometer
	BGCPString text = BGCPString::fromInt((int)(3.6 * speed)) + BGCPString(" kph");
	if (speedPercent >= 0.0) {
		overlay.showHBar(speedPercent / 100.0, speedDisplayCoords, 0, 200, 0, text);
	} else {
		overlay.showHBar(-speedPercent / 100.0, speedDisplayCoords, 255, 0, 0, text);
	}

	// Heat display
	overlay.showHBar(heat / 30.0, heatDisplayCoords, 255, 0, 0, "Heat");

	// Jump jet display
	int jjs = vehicle->getInteger(Vehicle::Info_JumpJets);
	overlay.showHBar(vehicle->mJumpJetFuel / jjs, jumpJetDisplayCoords, 0, 0, 255, "Jump Jets");

	// Altimeter
	int totalVal = (int) vehicle->getPosition().y;
	BGCPString altimeterText = BGCPString::fromInt(totalVal) + BGCPString(" m");
	double altimeterBarPos = putInRange(0.0, totalVal / 500.0, 1.0);
	overlay.showHBar(altimeterBarPos, vehicle->altimeterDisplayCoords, 0, 0, 255, altimeterText);

	// Artificial horizon
	AeroTech* aeroTech = dynamic_cast<AeroTech*>(vehicle);
	if (getEnvironment() == space && aeroTech != nullptr) {
		SET_OP;
		Matrix M(-vehicle->p_left, vehicle->p_up, vehicle->p_front);
		if (aeroTech->mMovementVector.length() > 0.0) {
			Point3D dir1 = aeroTech->mMovementVector;
			AbstractVehicleAI* ai = vehicle->getAI();
			if (ai != nullptr) {
				GameObject* target = ai->getTarget();
				if (target != nullptr) {
					AeroTech* targetAero = dynamic_cast<AeroTech*>(target);
					if (targetAero != nullptr) {
						dir1 -= targetAero->mMovementVector;
					}
				}
			}
			dir1.unitLength();
			Point3D n1, n2;
			dir1.findPerps(n1, n2);
			M = M * Matrix(n1, n2, dir1);
		}
		mArrow.setMatrix(M);
		overlay.showInterceptCourse(vehicle->artificialHorizonDisplayCoords, textureBuffer, renderTargetTexture, &mArrow);
	} else {
		overlay.showArtificialHorizon(vehicle->artificialHorizonDisplayCoords, vehicle, textureBuffer, renderTargetTexture);
	}

	SET_OP;
	const auto& weaponCoords = vehicle->weaponDisplayCoords;
	int tu1 = (int) (tw * weaponCoords.u1);
	int tv1 = (int) (th * weaponCoords.v1);
	int tu2 = (int) (tw * weaponCoords.u2);
	int tv2 = (int) (th * weaponCoords.v2);
	SET_OP;
	renderTargetTexture->fillRectI(tu1, tv1, tu2, tv2, 0, 0, 0);

	switch (mMfdMode) {
		case MFD_ShowWeapons:
			// Show weapon status
			SET_OP;
			overlay.showWeapons(playerVehicleAI, tu1, tv2, mNow);
			break;
		case MFD_Goals:
			{
				// Render mission goals
				SET_OP;
				BGCPString missionGoalsState;
				mMissionGoalsTracker.showMissionGoals(missionGoalsState, false, "", false);
				mFont->renderText(renderTargetTexture, missionGoalsState, tu1, tv2-mFont->getCharHeight()-1, tu2, tv1, 0, 255, 0);
			}
			break;
		case MFD_Comms:
			SET_OP;
			overlay.showComms(tu1, tv1, tu2, tv2);
			break;
		case MFD_CommAll:
		case MFD_Comm1:
		case MFD_Comm2:
			SET_OP;
			overlay.showComms2(tu1, tv1, tu2, tv2, mMfdMode);
			break;
	}
	const auto& armorCoords = vehicle->armorDisplayCoords;
	overlay.showArmor(vehicle, armorCoords.u1,  armorCoords.v1, armorCoords.u2, armorCoords.v2);

	// Set viewing panels transparent (if sensors function)
	SET_OP;
	char a = (char)(255 * vehicle->getSensorDamage());
	for (size_t i = 0; i+3 < vehicle->mViewpanels.size(); i += 4) {
		int u1 = (int)(vehicle->mViewpanels[i]   * renderTargetTexture->getWidth());
		int v1 = (int)(vehicle->mViewpanels[i+1] * renderTargetTexture->getHeight());
		int u2 = (int)(vehicle->mViewpanels[i+2] * renderTargetTexture->getWidth());
		int v2 = (int)(vehicle->mViewpanels[i+3] * renderTargetTexture->getHeight());
		if (a == 0) {
			renderTargetTexture->fillRectI(u1, v1, u2, v2, 0, 0, 0, a);
		} else {
			for (int v = v1; v < v2; ++v) {
				for (int u = u1; u < u2; ++u) {
					unsigned pix = irand(0, 255);
					renderTargetTexture->setRGBA(u, v, pix, pix, pix, a);
				}
			}
		}
	}

	SET_OP;
	GameObject* target =  playerVehicleAI->getTarget();
	if (target != nullptr) {
		renderTarget(vehicle, renderTargetTexture, mNow, vehicle->mapDisplayCoords.u1, vehicle->mapDisplayCoords.v1,
			vehicle->mapDisplayCoords.u2, vehicle->mapDisplayCoords.v2);
	} else {
		vehicle->windshieldVisible(true);
		renderMap(vehicle, renderTargetTexture, mNow, vehicle->mapDisplayCoords.u1, vehicle->mapDisplayCoords.v1, 
			vehicle->mapDisplayCoords.u2, vehicle->mapDisplayCoords.v2);
	}

	// Ensure the updated texture is used
	SET_OP;
	renderTargetTexture->update();
	RETURN;
}

void MechSim::setRenderSceneMatrices(double fovy, const Point3D& viewerPos, const Point3D& viewCenter, const Point3D& viewUp)
{
	// Set viewing projection
	SET_SCOPE;
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	// Render with the far plane at infinity
	double a = ((double)width) / ((double)height);
	double zNear = 0.1;
	double f = 1.0/tan(M_PI * fovy / 360.0);
	double d[16] = {f/a, 0, 0, 0,
					0, f, 0, 0,
					0, 0, -1, -1.0,
					0, 0, -2*zNear, 0};
	Matrix M(d);
	M.glLoadMatrix();

	// old code that uses GLU to set the perspective matrix (left as a reference)
	//gluPerspective(fovy, a, 0.1, 10000.0);

	SET_OP;
	gluLookAt(viewerPos.x, viewerPos.y, viewerPos.z,
		viewCenter.x, viewCenter.y, viewCenter.z,
		viewUp.x, viewUp.y, viewUp.z);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Render

void MechSim::render(int x, int y, int w, int h)
{
	if (playerVehicleAI == NULL) {
		printf("No player vehicle -- rendering skipped\n");
		return;
	}

	SET_SCOPE;
	Vehicle* vehicle = playerVehicleAI->getVehicle();
	if (vehicle == nullptr) {
		RETURN;
	}
	if (vehicle->getStatus() == VehicleStatus::destroyed) {
		getMechVm()->setScreen(returnVis);
		getMechVm()->deleteLater(this);
		mMissionSuccessful = false;
		RETURN;
	}

	SET_OP;
	Texture* texture = vehicle->getTexture();
	double speed = playerVehicleAI->getSpeed();
	double speedPercent = vehicle->getSpeedPercent();
	double heat = vehicle->getHeat();
	mNow = SDL_GetTicks() - mMissionStarted;
	width = w;
	height = h;
	double deltaT = 0.0;
	if (frame > 0) {
		deltaT = (mNow-lastFrame) / 1000.0;
	}
	++frame;

	// Prevent large movements when the game is paused ...
	SET_OP;
	if (deltaT > 0.1) {
		deltaT = 0.1;
	}

	if (mPaused) {
		deltaT = 0.0;
	}

	if (mNow > lastFrame) {
		lastFrame = mNow;
	} else {
		COUT << "No time has passed since the last frame" << std::endl;
	}

	// Free view
	mFreeViewTheta += deltaT * mFreeViewDeltaTheta;

	Audio* audio = getAudio();
	if (audio->getRemainingQueuedAudioMS() < mSoundFrameDurationMS
	&&  audio->getAudioSpec().callback == nullptr)
	{
		calcSoundFrame();
	}

	// Check visited nav points
	SET_OP;
	for (size_t i = 0; i < mNavPoints.size(); ++i) {
		double dist = (vehicle->getPosition() - mNavPoints[i].pos).length();
		if (dist < 100.0 && !mNavPoints[i].visited) {
			mNavPoints[i].visited = true;
			updateObjectStatus(mNavPoints[i].name, SubObjectiveType::visit, false);
			if (mCurrentNavPoint == i && i+1 < mNavPoints.size()) {
				++mCurrentNavPoint;
			}
		}
	}

	// Check for special, distance-based subobjectives
	SET_OP;
	mMissionGoalsTracker.checkDistanceSubObjectives(vehicle->getPosition());

	// Update object positions
	if (!mPaused) {
		SET_OP;
		for (size_t i = 0; i < mAIVehicles.size(); ++i) {
			AbstractVehicleAI* vehicleAI = mAIVehicles[i];
			vehicleAI->planMovement(mNow);
			Vehicle* vehicle = vehicleAI->getVehicle();
			vehicle->calcNextFrameMovement(deltaT, m_sceneObjects, mNow, mGravity);
			vehicle->calcNextFrameHeat(mNow, deltaT);
		}
	}

	SET_OP;
	Matrix m;
	m_sceneObjects->cacheTransformedVertices(m);
	mShots.cacheTransformedVertices(m);

	SET_OP;
	Point3D viewerPos, viewUp, viewCenter;
	if (playerVehicleAI != NULL) {
		switch (playerVehicleAI->getPerspective()) {
			case 0:
				// Cockpit perspective
				{
					SET_OP;
					const Matrix& M = vehicle->getObject(Biped_Mech_Part_Torso)->getMatrix();
					viewerPos = M.Transform(vehicle->cockpitCoords);
					Point3D q = vehicle->cockpitCoords + Point3D(0.0, 0.0, -1.0); // q is used to determine the viewing direction ...
					q = M.Transform(q);
					viewCenter = viewerPos + viewerPos - q; // ??
					viewUp = vehicle->p_up;
				}
				break;
			case 1:
				// External perspective
				SET_OP;
				viewerPos = vehicle->getPosition() + 20 * vehicle->p_up
				            - 50 * sin(M_PI * mFreeViewTheta / 180.0) * vehicle->p_front 
				            - 50 * cos(M_PI * mFreeViewTheta / 180.0) * vehicle->p_left;
				viewCenter = vehicle->getPosition();
				viewUp = vehicle->p_up;
				break;
		}
	}

	SET_OP;
	Mesh* cockpitMesh = vehicle->getCockpitMesh();
	Texture* renderTargetTexture = nullptr;
	if (cockpitMesh != nullptr) {
		renderTargetTexture = cockpitMesh->getTexture();
		if (cockpitMesh != vehicle) {
			cockpitMesh->setFlag(RenderableObject::Flag_invisible);
		}
	}
	if (renderTargetTexture == nullptr) {
		renderTargetTexture = vehicle->getTexture();
	}

	// Render cockpit displays
	if (vehicle->useCockpitDisplays() 
	&& renderTargetTexture != nullptr 
	&& playerVehicleAI->getPerspective() == 0
	&& textureBuffer.canRenderToTexture()) 
	{
		renderCockpitInstruments(renderTargetTexture, vehicle);
	}

	// HUD for external views
	SET_OP;
	MechSimOverlay* externalHudOverlay = nullptr; // todo: use smart pointer here (no naked new)
	if (!vehicle->useCockpitDisplays() || playerVehicleAI->getPerspective() != 0) {
		int wdiv = w;
		int hdiv = h;
		if (mOverlayDivider > 0) {
			wdiv /= mOverlayDivider;
			hdiv /= mOverlayDivider;
		}
		externalHudOverlay = new MechSimOverlay(wdiv, hdiv);
		externalHudOverlay->mFont = mFont;
		externalHudOverlay->setSpeed(speed, speedPercent);
		externalHudOverlay->showHBar(heat / 30.0, DisplayComponentCoords(0.33, 0.05, 0.66, 0.1), 255, 0, 0, "Heat");

		// Jump jets
		SET_OP;
		int jjs = vehicle->getInteger(Vehicle::Info_JumpJets);
		if (jjs > 0) {
			externalHudOverlay->showVBar(100.0 * vehicle->mJumpJetFuel / jjs, 0.01, 0.15, 0.03, 0.6, 0, 0, 255);
			externalHudOverlay->showText("Jump Jets", 0.04, 0.5, 0, 0, 255);
		}
		BGCPString altStr = BGCPString("Altitude: ") + BGCPString::fromDouble(vehicle->getPosition().y) + BGCPString(" m");
		externalHudOverlay->showText(altStr, 0.04, 0.48, 0, 0, 255);

		SET_OP;
		Texture* overlayTexture = externalHudOverlay->getOverlayTexture();
		renderTarget(vehicle, overlayTexture, mNow, 0.01, 0.01, 0.21, 0.21);
		renderMap(vehicle, overlayTexture, mNow, 0.01, 0.79, 0.21, 0.99);
		externalHudOverlay->showArmor(vehicle, 0.9, 0.01, 0.99, 0.1);
		externalHudOverlay->showWeapons(playerVehicleAI, (int)(0.7 * overlayTexture->getWidth()), (int)(0.99f * overlayTexture->getHeight()), 
			mNow);

		SET_OP;
		double mfdU1 = 0.3;
		double mfdU2 = 0.7;
		double mfdV1 = 0.7;
		double mfdV2 = 0.3;
		int mfdX1 = (int) (mfdU1 * w);
		int mfdX2 = (int) (mfdU2 * w);
		int mfdY1 = (int) (mfdV1 * h);
		int mfdY2 = (int) (mfdV2 * h);

		SET_OP;
		switch (mMfdMode) {
			case MFD_ShowWeapons:
				// Show weapon status - no is placed somewhere else in this mode
				//overlay.showWeapons(playerVehicleAI, tu1, tv2, mNow);
				SET_OP;
				break;
			case MFD_Goals:
				{
					// Render mission goals
					SET_OP;
					BGCPString missionGoalsState;
					mMissionGoalsTracker.showMissionGoals(missionGoalsState, false, "", false);
					externalHudOverlay->showText(missionGoalsState, mfdU1, mfdV1, mfdU2, mfdV2, 0, 255, 0);
				}
				break;
			case MFD_Comms:
				SET_OP;
				externalHudOverlay->showComms(mfdX1, mfdY2, mfdX2, mfdY1);
				break;
			case MFD_CommAll:
			case MFD_Comm1:
			case MFD_Comm2:
				SET_OP;
				externalHudOverlay->showComms2(mfdX1, mfdY2, mfdX2, mfdY1, mMfdMode);
				break;
		}
	}

	SET_OP;
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
	glViewport(x, y, w, h);

	setRenderSceneMatrices(mZoom, viewerPos, viewCenter, viewUp);

	if (playerVehicleAI->getPerspective() == 0) {
		// Make windshield invisible/transparent for cockpit view
		vehicle->windshieldVisible(false);
	} else {
		// Windshield should be visible in external view
		vehicle->windshieldVisible(true);
	}

	SET_OP;
	glColor3d(1, 1, 1);
	if (mSkybox != nullptr && mViewingMode == NormalView) {
		SET_OP;
		Matrix skyboxMatrix = TranslationMatrix(viewerPos.x, viewerPos.y - 10.0, viewerPos.z) * ScaleMatrix(1000, 1000, 1000);
		mSkybox->setMatrix(skyboxMatrix);
		mSkybox->cacheTransformedVertices(skyboxMatrix);
		glDepthMask(GL_FALSE);
		glDisable(GL_LIGHTING);
		glDisable(GL_DEPTH_TEST);
		mSkybox->render();
		glDepthMask(GL_TRUE);
	}

	// Render scene
	glEnable(GL_DEPTH_TEST);
	glColor3d(1, 1, 1);
	bool renderShadows = false;
	if (mLightSources.size() > 0 && mAllowRenderShadows) {
		renderShadows = mLightSources[0]->mRenderShadows;
	}

	if (m_sceneObjects != NULL) {
		SET_OP;
		switch (mViewingMode) {
			case NormalView:
				glEnable(GL_LIGHTING);
				if (renderShadows && mLightSources.size() > 0) {
					// Render scene with light sources set up for shadows (also initialises the depth buffer)
					// We can't already initialize the stencil buffer since the front faces used for intitialising the
					// stencil buffer depend on the light source
					//deactivateLightSources();
					activateLightSources(true);
					m_sceneObjects->render();
					mShots.render();

					glDepthMask(GL_FALSE);
					glDisable(GL_BLEND);
					glEnable(GL_STENCIL_TEST);
					for (size_t i = 0; i < mLightSources.size(); ++i) {
						// Render shadow volumes to the stencil buffer
						glClear(GL_STENCIL_BUFFER_BIT);
						glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
						glEnable(GL_DEPTH_CLAMP);
						glStencilFunc(GL_ALWAYS, 0, 0xff);
						glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
						glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);
						Point3D& lightSourcePos = mLightSources[i]->mPos;
						m_sceneObjects->markFrontFacingPolygons(lightSourcePos);
						m_sceneObjects->renderShadowVolumes(lightSourcePos);
						glDisable(GL_DEPTH_CLAMP);

						// Render scene using the shadow information in the stencil buffer, this time non-shadowed areas
						glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
						glDepthFunc(GL_EQUAL);
						glStencilFunc(GL_EQUAL, 0, ~0);
						glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
						mLightSources[i]->activate(false);
						m_sceneObjects->render();
						mShots.render();
					}
					glDepthFunc(GL_LESS);
					glDepthMask(GL_TRUE);
					glDisable(GL_STENCIL_TEST);
				} else {
					activateLightSources(false);
					m_sceneObjects->render();
					mShots.render();
				}
				break;
			case WireframeView:
				m_sceneObjects->render();
				mShots.render();
				glFlush();
				glClear(GL_COLOR_BUFFER_BIT);
				glDisable(GL_LIGHTING);
				glDisable(GL_TEXTURE_2D);
				deactivateLightSources();
				m_sceneObjects->renderWireframe();
				mShots.renderWireframe();
				break;
		}
	}

	// Process weapon fire and display energy weapon rays
	// Since energy weapon fire is rendered here, this has to happen after glClear or we need to store the parameters
	// for energy weapons fire and render it separately ...
	if (!mPaused) {
		processWeaponFire(deltaT);
	}

	// Render the cockpit, possibly with a different zoom
	glClear(GL_DEPTH_BUFFER_BIT);
	deactivateLightSources();
	if (playerVehicleAI->getPerspective() == 0
	&&  cockpitMesh != vehicle) 
	{
		mDisplayLight.mPos = viewerPos;
		mDisplayLight.activate(false);

		// Set viewing projection
		SET_OP;
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		double a = ((double)width) / ((double)height);
		gluPerspective(MAX_ZOOM, a, 0.01, 50.0);

		SET_OP;
		gluLookAt(viewerPos.x, viewerPos.y, viewerPos.z,
			viewCenter.x, viewCenter.y, viewCenter.z,
			viewUp.x, viewUp.y, viewUp.z);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glEnable(GL_BLEND);

		if (cockpitMesh != nullptr) {
			cockpitMesh->clearFlag(RenderableObject::Flag_invisible);
			cockpitMesh->render();
		}
	}
	mDisplayLight.deactivate();
	glDisable(GL_LIGHTING);

	if (externalHudOverlay != nullptr) {
		externalHudOverlay->render();
		delete externalHudOverlay;
		externalHudOverlay = nullptr;
	}

	// Render reticle
	SET_OP;
	if (playerVehicleAI->getPerspective() == 0) {
		double u = 0.0;
		double v = 0.0;
		if (vehicle->reticleMovesHorizontally()) {
			// Bad formula for converting the cursor position into an aim
			u = mLastMouseX;
		}
		if (vehicle->reticleMovesVertically()) {
			// Bad formula for converting the cursor position into an aim
			v = mLastMouseY;
		}
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_TEXTURE_2D);
		mReticleColor.glColor();

		glBegin(GL_LINES);
			glVertex2d(u-0.02, v);
			glVertex2d(u-0.01, v);
			glVertex2d(u+0.01, v);
			glVertex2d(u+0.02, v);
			glVertex2d(u, v-0.02);
			glVertex2d(u, v-0.01);
			glVertex2d(u, v+0.01);
			glVertex2d(u, v+0.02);
		glEnd();

		glColor3d(0.0, 0.0, 0.0);
		glBegin(GL_LINES);
			glVertex2d(u-0.02, v-0.02);
			glVertex2d(u-0.01, v-0.01);
			glVertex2d(u+0.02, v+0.02);
			glVertex2d(u+0.01, v+0.01);
			glVertex2d(u-0.02, v+0.02);
			glVertex2d(u-0.01, v+0.01);
			glVertex2d(u+0.01, v-0.01);
			glVertex2d(u+0.02, v-0.02);
		glEnd();

		// Render enemy indicator
		glColor3d(1.0, 1.0, 1.0);
		GameObject* target = playerVehicleAI->getTarget();
		if (target != nullptr) {
			const Matrix& M = vehicle->getObject(Biped_Mech_Part_Torso)->getMatrix();
			viewerPos = M.Transform(vehicle->cockpitCoords);
			Point3D q = M.Transform(vehicle->cockpitCoords + Point3D(0.0, 0.0, -1.0));

			Point3D desiredPos = target->getPosition();
			double angleToEnemy = calcPolarCoord(viewerPos, q - viewerPos, vehicle->p_up, desiredPos);

			double u1 = 0.1 * sin(M_PI * angleToEnemy / 180.0 );
			double v1 = 0.1 * cos(M_PI * angleToEnemy / 180.0 );
			double u2 = 0.05 * sin(M_PI * angleToEnemy / 180.0 );
			double v2 = 0.05 * cos(M_PI * angleToEnemy / 180.0 );

			glDisable(GL_DEPTH_TEST);
			glDisable(GL_TEXTURE_2D);
			glBegin(GL_LINES);
				glVertex2d(u1, v1);
				glVertex2d(u2, v2);
			glEnd();
		}
	}

	// If the game is paused, show the "paused" sign
	if (mPaused && mPausedPic != nullptr) {
		glDisable(GL_DEPTH_TEST);
		glEnable(GL_TEXTURE_2D);
		glDisable(GL_LIGHTING);
		glEnable(GL_BLEND);
		glColor3d(1.0, 1.0, 1.0);
		mPausedPic->use();
		glBegin(GL_QUADS);
			mPausedPic->glTexCoord(0.0, 0.0);
			glVertex2d(-1.0, -1.0);

			mPausedPic->glTexCoord(1.0, 0.0);
			glVertex2d(1.0, -1.0);

			mPausedPic->glTexCoord(1.0, 1.0);
			glVertex2d(1.0, 1.0);

			mPausedPic->glTexCoord(0.0, 1.0);
			glVertex2d(-1.0, 1.0);
		glEnd();
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// End the sim, return to calling screen

void MechSim::endSim()
{
	// End sim
	SET_SCOPE;
	getMechVm()->setScreen(returnVis);
	getMechVm()->deleteLater(this);

	if (mShell == nullptr) {
		RETURN;
	}

	// Update mission goals that are evaluated only at the end of a mission
	updateObjectStatus("Player1", SubObjectiveType::sleep, true);

	BGCPString mechShellSalvage, mechShellMechList, mechShellConfigs;
	mShell->getVar("salvage", mechShellSalvage); // can be set by Mercs mission out3 level script
	if (mMissionSuccessful
	&&  mShell->getVar("mechlist", mechShellMechList)
	&&  mShell->getVar("configs", mechShellConfigs)) 
	{
		// Set hint for next mission
		mShell->setVar("nextMission", mSuccessMission);

		// Check for salvage
		for (size_t i = 0; i < mAIVehicles.size(); ++i) {
			if (mAIVehicles[i] != nullptr) {
				Vehicle* vehicle = mAIVehicles[i]->getVehicle();
				if (vehicle != nullptr && vehicle->mSalvage) {
					// If the situation allows a Mech to be salvaged, then the only circumstances that can prevent this
					// is if the Mech is completely destroyed either by artillery or an ammunition explosion
					// (If this is a possibility, simply do not allow salvaging in the mission XML)
					// We need to remove the file extension from the file name
					BGCPString configFN = vehicle->CFGFN;
					size_t dotPos = configFN.scan(".", 0, configFN.getByteCount());
					configFN = configFN.copyPart(0, dotPos);

					// Add vehicle to mechShell lists (chassis, configs, salvage)
					mechShellMechList += BGCPString(",") + vehicle->MVMKFN;
					mechShellConfigs += BGCPString(",") + configFN;
					if (!mechShellSalvage.empty()) {
						mechShellSalvage += BGCPString(",");
					}
					mechShellSalvage += configFN;
					mShell->setVar("mechlist", mechShellMechList);
					mShell->setVar("configs", mechShellConfigs);
				}
			}
		}
	} else {
		mShell->setVar("nextMission", mFailureMission);
	}

	mShell->setVar("salvage", mechShellSalvage);

	// Update MechShell vars
	BGCPString missionGoalsState, scoreUnit;
	if (mShell->isMercs()) {
		scoreUnit = " C-Bills";
	}
	int score = mMissionGoalsTracker.showMissionGoals(missionGoalsState, true, scoreUnit, true);
	mShell->setVar("missionResults", missionGoalsState);

	BGCPString missionScore = BGCPString::fromInt(score);
	mShell->setVar("missionScore", missionScore);
	mShell->enterPostMissionRoom(mMissionSuccessful);

	RETURN;
}

// Find the enemy or target closest to the viewing ray (recursive part)
RenderableObject* MechSim::findTarget(const Point3D& pos, const Point3D& dir, RenderableObject* ro, double& minAngle) 
{
	if (playerVehicleAI == nullptr || ro == nullptr) {
		return nullptr;
	}

	SET_SCOPE;
	Vehicle* vehicle = playerVehicleAI->getVehicle();
	if (ro == vehicle) {
		RETURN nullptr; // Player does not want to target himself normally ...
	}

	// Check this object against the ray
	GameObject* go = dynamic_cast<GameObject*>(ro);
	RenderableObject* result = nullptr;
	if (go != nullptr && !go->getDescriptionText().empty()) {
		SET_OP;
		Point3D min, max;
		if (!ro->getMinCoords(min) || !ro->getMaxCoords(max)) {
			RETURN nullptr;
		}
		Point3D mid = 0.5 * (min + max);
		Point3D v2 = mid - pos;
		v2.unitLength();
		double newAngle = 180.0 * acos(dir * v2) / M_PI;
		if (newAngle < minAngle) {
			minAngle = newAngle;
			result = ro;

			// Stop if it is a vehicle
			Vehicle* vehicle = dynamic_cast<Vehicle*>(ro);
			if (vehicle != nullptr) {
				RETURN result;
			}
		}
	}

	// Recurse for all child objects
	for (size_t i = 0; i < ro->getObjectCount(); ++i) {
		SET_OP;
		RenderableObject* child = ro->getObject(i);
		RenderableObject* candidate = findTarget(pos, dir, child, minAngle);
		if (candidate != nullptr) {
			result = candidate;
		}
	}

	RETURN result;
}


// Find the enemy or target closest to the viewing ray
GameObject* MechSim::findTarget()
{
	// Find viewer position and viewing direction
	SET_SCOPE;
	Vehicle* vehicle = playerVehicleAI->getVehicle();
	Matrix M;
	vehicle->getPartMatrix(Biped_Mech_Part_Torso, M);
	Point3D viewerPos = M.Transform(vehicle->cockpitCoords);
	Point3D q = vehicle->cockpitCoords + Point3D(0.0, 0.0, -1.0); // q is used to determine the viewing direction ...
	q = M.Transform(q);
	Point3D viewDir = viewerPos - q;

	// Find target to the object closest to the viewing ray
	double angle = 30; // Only angles smaller than this angle will be considered
	RenderableObject* candidate = findTarget(viewerPos, viewDir, m_sceneObjects, angle);
	
	RETURN dynamic_cast<GameObject*> (candidate);
}

////////////////////////////////////////////////////////////////////////////////
// Keyboard event handler

void MechSim::keypressed(BGKey key, bool shiftHeld, bool controlHeld, bool down, bool repeat)
{
	SET_SCOPE;
	switch (key) {
	case SDLK_ESCAPE:
		if (down) {
			mPaused = !mPaused;
		}
		break;
	case 'q':
		if (controlHeld) {
			endSim();
			RETURN;
		}
		break;
	case 'y':
		if (shiftHeld) {
			mZoom = std::min(mZoom+1, MAX_ZOOM);
		} else {
			mZoom = std::max(mZoom-1, MIN_ZOOM);
		}
		break;
	case SDLK_F11:
		{
			if (playerVehicleAI == NULL) {
				RETURN;
			}

			SET_OP;
			Vehicle* vehicle = playerVehicleAI->getVehicle();

			COUT << "Mech position: (" << vehicle->getPosition().x << ", " << vehicle->getPosition().y << ", " 
				 << vehicle->getPosition().z << ")" << std::endl;
			#ifdef _DEBUG
			if (down) {
				mMechSimDebug = true;

				// Code useful for simulating sensor damage ...
				/*Vehicle* playerVehicle = playerVehicleAI->getVehicle();
				BipedMech* mech = dynamic_cast<BipedMech*>(playerVehicle);
				if (mech != nullptr) {
					++mech->mSensorDamage;
				}*/
			} else {
				mMechSimDebug = false;
			}
			#endif
		}
		break;
	case SDLK_F12:
		if (down) {
			if (mMfdMode == MFD_ShowWeapons) {
				mMfdMode = MFD_Goals;
			} else {
				mMfdMode = MFD_ShowWeapons;
			}
		}
		break;
	case 'b':
		if(down) {
			if (mMfdMode == MFD_ShowWeapons || mMfdMode == MFD_Goals) {
				mMfdMode = MFD_Comms;
			} else {
				mMfdMode = MFD_ShowWeapons;
			}
		}
		break;
	case 'w':
		if (down) {
			if (mViewingMode == NormalView) {
				mViewingMode = WireframeView;
			} else {
				mViewingMode = NormalView;
			}
		}
		break;
	}

	if (down) {
		if (mMfdMode == MFD_Comms) {
			SET_OP;
			Vehicle* starmate1 = findVehicleByID("Starmate1");
			Vehicle* starmate2 = findVehicleByID("Starmate2");
			switch(key) {
				case '1':
					if (starmate1 != nullptr)
						mMfdMode = MFD_Comm1;
					break;
				case '2':
					if (starmate2 != nullptr) 
						mMfdMode = MFD_Comm2;
					break;
				case '3':
					if (starmate1 != nullptr || starmate2 != nullptr) 
						mMfdMode = MFD_CommAll;
					break;
			}
		} else if (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm1 || mMfdMode == MFD_Comm2) {
			SET_OP;
			switch(key) {
				case '1':
					setStarmateAction_Attack();
					break;
				case '2':
					setStarmateAction_JoinFormation();
					break;
				case '3':
					setStarmateAction_Shutdown();
					break;
			}
		}
	}

	if (playerVehicleAI != NULL) {
		SET_OP;
		#ifdef _DEBUG
		Vehicle* vehicle = playerVehicleAI->getVehicle();
		COUT << "Mech position: (" << vehicle->getPosition().x << ", " << vehicle->getPosition().y << ", " 
				<< vehicle->getPosition().z << ")" << std::endl;
		#endif
		playerVehicleAI->keypressed(key, shiftHeld, controlHeld, down, repeat);
	}

	RETURN;
}

// Button state changed over a window: returns child that was clicked
GLWindow* MechSim::buttonStateChanged (int button, bool down, int x, int y)
{
	if (playerVehicleAI != NULL) {
		playerVehicleAI->buttonStateChanged(button, down, x, y);
	}

	return nullptr;
}

// Simple debugging feature: by pressing F11, the variable below is set and can be queried wherever MechSim is available ...
bool MechSim::getMechSimDebug()
{
	return mMechSimDebug;
}

////////////////////////////////////////////////////////////////////////////////

void MechSim::mouseMoved (int x, int y) 
{
	mLastMouseX = 2.0 * x / width - 1.0;
	if (playerVehicleAI != nullptr && width > 0 && height > 0) {
		playerVehicleAI->setAim(((double)(x)) / width, ((double) y) / height);
	}
	mLastMouseY = 2.0 * y / height - 1.0;
}

////////////////////////////////////////////////////////////////////////////////

void MechSim::processObjectTag(RenderableObject* container, XMLTreeConstIterator& iter, 
	const Matrix& m, const PathChar* levelDir)
{
	SET_SCOPE;
	GameObject* gameObject = nullptr;
	BGCPString objFN, fullFN, textureFN, id;

	#ifdef MECHVM
	if (mArchive != nullptr) {
		// MW2
		BGCPString wtbFN, zbdMeshFN;
		if (iter.getAttributeValueByName("mw2prj", wtbFN)) {
			SET_OP;
			wtbFN.convertToCP(CP_ASCII);
			if (!mArchive->enterSubDir("POLY")) {
				RETURN;
			}

			MemoryBlock wtbFile;
			if (!mArchive->getFile(wtbFN.get<char>(), wtbFile)) {
				RETURN;
			}

			gameObject = new GameObject;
			if (MechWarriorIIPRJ::loadWTB(&wtbFile, gameObject, 0.01)) {
				gameObject->calculateNormals();
				gameObject->setName(wtbFN);
			} else {
				log("Error opening ", wtbFN);
				safeDelete(gameObject);
				RETURN;
			}
		}

		// Load mesh from MW3 gamez.zbd ...
		if (iter.getAttributeValueByName("zbdmesh", zbdMeshFN)) {
			SET_OP;
			MW3GameZArchive* gameZArchive = dynamic_cast <MW3GameZArchive*>(mArchive);
			if (gameZArchive != nullptr) {
				size_t zbdMeshNo = (size_t)zbdMeshFN.toInt();
				zbdMeshNo += 3; // account for the fake files ...
				//auto zbdMeshNo = mArchive->getEntryNumberByName(zbdMeshFN);

				bool decoded = false;
				gameObject = new GameObject;
				if (gameZArchive->isPiratesMoonFile()) {
					decoded = gameZArchive->decodeGameZPMMesh(zbdMeshNo, gameObject);
				} else {
					decoded = gameZArchive->decodeGameZMesh(zbdMeshNo, gameObject);
				}

				if (decoded) {
					gameObject->calculateNormals(-1.0);
					gameObject->setName(zbdMeshFN);
				} else {
					log("Error opening ZBD mesh ", zbdMeshNo);
					safeDelete(gameObject);
					RETURN;
				}
			}
		}
	}
	#endif

	SET_OP;
	if (iter.getAttributeValueByName("obj", objFN)) {
		fullFN = BGCPString(levelDir, CP_SYSTEM);
		fullFN.addPathSep(objFN);
		gameObject = new GameObject;
		if (gameObject->loadFromObj(fullFN.getAsPath())) {
			gameObject->setName(objFN);
		} else {
			COUT << "Failed to load mesh ";
			fullFN.printLn();
			safeDelete(gameObject);
			RETURN;
		}
	}

	if (gameObject == nullptr) {
		COUT << "Mesh not loaded" << std::endl;
		RETURN;
	}

	if (iter.getAttributeValueByName("id", id)) {
		gameObject->setID(id);
	}

	if (iter.getAttributeValueByName("texture", textureFN)) {
		SET_OP;
		BGCPString fullTextureFN(levelDir, CP_SYSTEM);
		fullTextureFN.addPathSep(textureFN);

		gameObject->color = Color(1.0, 1.0, 1.0);
		Texture* objTexture = find<Texture>(fullTextureFN);
		if (objTexture != nullptr) {
			gameObject->setTexture(objTexture);
		} else {
			objTexture = new Texture;
			if (objTexture->loadFromFile(fullTextureFN.getAsPath())) {
				gameObject->setTexture(objTexture);
				addManagedObject(objTexture, fullTextureFN);
			} else {
				COUT << "Failed to load texture ";
				fullTextureFN.printLn();
				delete objTexture;
			}
		}
	} else {
		gameObject->color = Color::random();
	}

	BGCPString destroyedGeometryFN;
	if (iter.getAttributeValueByName("destroyedGeometry", destroyedGeometryFN)) {
		SET_OP;
		Mesh* mesh = new Mesh;
		bool loaded = false;
		if (destroyedGeometryFN.endsWith(".obj")) {
			BGCPString destroyedFullFN = BGCPString(levelDir, CP_SYSTEM);
			destroyedFullFN.addPathSep(destroyedGeometryFN);
			loaded = mesh->loadFromObj(destroyedFullFN.getAsPath());
		} else {
			#ifdef MECHVM
			if (mArchive->enterSubDir("POLY")) {
				MemoryBlock wtbFile;
				destroyedGeometryFN.convertToCP(CP_ASCII);
				if (mArchive->getFile(destroyedGeometryFN.get<char>(), wtbFile)) {
					loaded = MechWarriorIIPRJ::loadWTB(&wtbFile, mesh, 0.01);
				}
			}
			#endif
		}

		if (loaded) {
			gameObject->calculateNormals();
			gameObject->setDestroyedGeometry(mesh);
		} else {
			BGCPString msg = BGCPString("Failed to load ") + destroyedGeometryFN;
			msg.printLn();
			delete mesh;
		}
	}

	// Objects without a health value should be hard to destroy ...
	SET_OP;
	double health = iter.getDoubleAttributeByName("health", 20000.0);
	gameObject->setHealth(health);

	BGCPString descriptionText;
	iter.getAttributeValueByName("description", descriptionText);
	gameObject->setDescriptionText(descriptionText);
	BGCPString inspectionText;
	iter.getAttributeValueByName("inspectionText", inspectionText);
	gameObject->setInspectionText(inspectionText);

	double tx = iter.getDoubleAttributeByName("tx");
	double ty = iter.getDoubleAttributeByName("ty");
	double tz = iter.getDoubleAttributeByName("tz");

	gameObject->setPosition(tx, ty, tz);

	double rx = iter.getDoubleAttributeByName("rx");
	double ry = iter.getDoubleAttributeByName("ry");
	double rz = iter.getDoubleAttributeByName("rz");

	double sx = iter.getDoubleAttributeByName("sx", 1.0);
	double sy = iter.getDoubleAttributeByName("sy", 1.0);
	double sz = iter.getDoubleAttributeByName("sz", 1.0);

	Matrix objMatrix = m * TranslationMatrix(tx, ty, tz);

	BGCPString colorStr;
	if (iter.getAttributeValueByName("color", colorStr)) {
		Color c = parseColor(colorStr);
		gameObject->color = c;
	}

	if (rx != 0) {
		objMatrix = objMatrix * RotationMatrix(M_PI * rx / 180.0, 1, 0, 0);
	}
	if (ry != 0) {
		objMatrix = objMatrix * RotationMatrix(M_PI * ry / 180.0, 0, 1, 0);
	}
	if (rz != 0) {
		objMatrix = objMatrix * RotationMatrix(M_PI * rz / 180.0, 0, 0, 1);
	}

	if (sx != 1 || sy != 1 || sz != 1) {
		objMatrix = objMatrix * ScaleMatrix(sx, sy, sz, 1);
	}

	// Shadow preprocessing
	SET_OP;
	if (iter.hasAttribute("shadowFix3Coords")) {
		BGCPString coords;
		iter.getAttributeValueByName("shadowFix3Coords", coords);
		auto coordsSplit = coords.split(",", true);
		if (coordsSplit.size() >= 3) {
			Point3D p (coordsSplit[0].toDouble(), coordsSplit[1].toDouble(), coordsSplit[2].toDouble());
			gameObject->findMeshEdges();
			bool waterTight = gameObject->fillHoles3(p);
			if (waterTight) {
				gameObject->setFlag(RenderableObject::Flag_waterTight);
			}
		}
	} else if (iter.hasAttribute("shadowFix")) {
		BGCPString algoStr;
		iter.getAttributeValueByName("shadowFix", algoStr);
		auto algo = algoStr.toInt();
		bool waterTight = false;
		switch(algo) {
			case 1:
				gameObject->findMeshEdges();
				waterTight = gameObject->fillHoles();
				break;
			case 2:
				gameObject->findMeshEdges();
				waterTight = gameObject->fillHoles2();
				break;
		}
		if (waterTight) {
			gameObject->setFlag(RenderableObject::Flag_waterTight);
		}
	}

	SET_OP;
	gameObject->setMatrix(objMatrix);
	Point3D meshMin, meshMax;
	container->addChild(gameObject);
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////

void MechSim::processHeightmapTag(RenderableObject* container, XMLTreeConstIterator& iter)
{
	// Get parameters from XML tag
	SET_SCOPE;
	int xsize = (int) iter.getIntAttributeValueByName("xsize");
	int ysize = (int) iter.getIntAttributeValueByName("ysize");
	int zsize = (int) iter.getIntAttributeValueByName("zsize");
	double noise = iter.getDoubleAttributeByName("noise");
	int smoothen = (int) iter.getIntAttributeValueByName("smoothen");
	BGCPString heightmapFN;
	if(!iter.getAttributeValueByName("filename", heightmapFN)) {
		COUT << "Obligatory heightmap tag not found" << std::endl;
		RETURN;
	}

	SET_OP;
	if (heightmapFN.empty()) {
		COUT << "No heightmap given for heightmap tag" << std::endl;
		RETURN;
	}

	SET_OP;
	BGCPString fullFN (getDataDir());
	fullFN.addPathSep(heightmapFN);
	if (!fileExists(fullFN.getAsPath())) {
		COUT << "File does not exist: ";
		fullFN.printLn();
		RETURN;
	}

	// Load heightmap texture
	SET_OP;
	Texture texture;
	if (!texture.loadFromFile(fullFN.getAsPath())) {
		COUT << "Failed to open ";
		heightmapFN.printLn();
		RETURN;
	}

	// todo: smoothing passes are still missing

	// Add vertices to mesh
	SET_OP;
	Mesh* mesh = new Mesh();
	std::vector<double> vertexCoords;
	TextureDim tw = texture.getWidth();
	TextureDim th = texture.getHeight();

	for (int z = 0; z < zsize; ++z) {
		for (int x = 0; x < xsize; ++x) {
			SET_OP;
			TextureDim tx = tw - x * tw / xsize - 1;
			TextureDim tz = z * th / zsize;
			unsigned char r, g, b;
			texture.getRGB(tx, tz, r, g, b);
			int th = (r + g + b) * ysize / 3;
			double y = th / 255.0 + frand(noise);
			//Point3D p(x, h, z);
			//mesh->addPoint(p);
			vertexCoords.emplace_back(x);
			vertexCoords.emplace_back(y);
			vertexCoords.emplace_back(z);
		}
	}

	for (int z = 0; z < zsize - 1; ++z) {
		for (int x = 0; x < xsize - 1; ++x) {
			SET_OP;
			size_t p0 = x + z * xsize;
			size_t p1 = p0 + 1;
			size_t p2 = p1 + xsize;
			size_t p3 = p0 + xsize;

			MeshPolygon* poly1 = mesh->addPolygon(3);
			poly1->setVertexIndex(0, p0);
			poly1->setVertexIndex(1, p1);
			poly1->setVertexIndex(2, p2);

			MeshPolygon* poly2 = mesh->addPolygon(3);
			poly2->setVertexIndex(0, p0);
			poly2->setVertexIndex(1, p2);
			poly2->setVertexIndex(2, p3);
		}
	}

	SET_OP;
	MeshPolygon* poly1 = mesh->addPolygon(3);
	mesh->calculateNormals();
	container->addChild(mesh);
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Based on a simple specification, select a random Mech

void MechSim::selectRandomMech(const BGCPString& randomMechSpec, BGCPString& chassis, BGCPString& config, BGCPString& description)
{
	BGCPString evaluatedMechSpec = evaluateMechShellVars(randomMechSpec);
	evaluatedMechSpec.convertToCP(CP_ASCII);
	int commaPos = evaluatedMechSpec.findChar(',');
	BGCPString faction = evaluatedMechSpec.copyPart(0, commaPos);
	BGCPString maxTonnageStr = evaluatedMechSpec.copyPart(commaPos+1, randomMechSpec.getByteCount()-commaPos-1);
	auto maxTonnage = maxTonnageStr.toInt();

	// Currently returns Mechs appropriate for random missions
	// Could be made more flexible, taking into account the year and the Mech List "Mech Sale Sheet.csv"
	if (faction == "IS") {
		if (maxTonnage == 30) {
			int val = irand(0, 5);
			switch(val) {
			case 0:
				chassis = "mw2+Commando";
				config = "COM-2D Commando";
				description = "Enemy Commando";
				return;
			case 1:
				chassis = "Whammy-Wasp";
				config = "WSP-1B Wasp";
				description = "Enemy Wasp";
				return;
			case 2:
				chassis = "MW2-Mongoose";
				config = "MON-67 Mongoose";
				description = "Enemy Mongoose";
				return;
			case 3:
				chassis = "mw2-Hornet";
				config = "HN-151 Hornet";
				description = "Enemy Hornet";
				return;
			default:
			case 4:
				chassis = "mw2-Urbanmech";
				config = "UM-R60 Urbanmech";
				description = "Enemy UrbanMech";
				return;
			}
		} else if (maxTonnage == 50) {
			int val = irand(0, 5);
			switch(val) {
			case 0:
				chassis = "mw2+Jenner";
				config = "JR7-D Jenner";
				description = "Enemy Jenner";
				return;
			case 1:
				chassis = "mw2-Whitworth";
				config = "WTH-1 Whitworth";
				description = "Enemy Whitworth";
				return;
			case 2:
				chassis = "mw2-Centurion";
				config = "CN9-A Centurion";
				description = "Enemy Centurion";
				return;
			case 3:
				chassis = "mw2+Clint";
				config = "CLNT-2-3T Clint";
				description = "Enemy Clint";
				return;
			case 4:
				chassis = "Whammy-Phoenix Hawk";
				config = "PXH-1 Phoenix Hawk";
				description = "Enemy Phoenix Hawk";
				return;
			}
		} else if (maxTonnage == 100) {
			int val = irand(0, 8);
			switch(val) {
			case 0:
				chassis = "mw2+Annihilator";
				config = "ANH-1A Annihilator";
				description = "Enemy Annihilator";
				return;
			case 1:
				chassis = "Titan";
				config = "Titan TI-1A";
				description = "Enemy Titan";
				return;
			case 2:
				chassis = "mw2-Cyclops";
				config = "CP-10-Z Cyclops";
				description = "Enemy Cyclops";
				return;
			case 3:
				chassis = "mw2+Stalker";
				config = "STK-3F Stalker";
				description = "Enemy Stalker";
				return;
			case 4:
				chassis = "Whammy-Highlander";
				config = "HGN-733 Highlander";
				description = "Enemy Highlander";
				return;
			}
		}
	} else {
		// todo: Clan Mechs should depend on faction?
		int val = irand(0, 5);
		switch(val) {
			case 0:
				chassis = "mw2+Gargoyle";
				config = "Gargoyle (Prime)";
				description = "Enemy Man'o'War";
				return;
			case 1:
				chassis = "mw2+Warhawk";
				config = "Warhawk (Prime)";
				description = "Enemy Masakari";
				return;
			case 2:
				chassis = "Kingfisher";
				config = "Kingfisher Prime";
				description = "Enemy Kingfisher";
				return;
			case 3:
				chassis = "mw2+Executioner";
				config = "Executioner (Prime)";
				description = "Enemy Gladiator";
				return;
			case 4:
				chassis = "mw2+Dire Wolf";
				config = "Dire Wolf (Prime)";
				description = "Enemy Daishi";
				return;
		}
	}
}

////////////////////////////////////////////////////////////////////////////////

void MechSim::processSpawn(XMLTreeConstIterator& iter)
{
	// todo: let AI read its data directly from XML?
	SET_SCOPE;
	Point3D pos;
	BGCPString chassisName, configFN, faction, id, target, description, inspectionText;
	std::vector<AIGoal> goals;

	pos.x = iter.getDoubleAttributeByName("x");
	pos.y = iter.getDoubleAttributeByName("y");
	pos.z = iter.getDoubleAttributeByName("z");
	double heading = iter.getDoubleAttributeByName("heading", 0.0);
	iter.getAttributeValueByName("chassis", chassisName);
	iter.getAttributeValueByName("config", configFN);
	iter.getAttributeValueByName("faction", faction);
	iter.getAttributeValueByName("id", id);
	double triggerRange = iter.getDoubleAttributeByName("triggerRange", 300.0);
	double triggerSpeed = iter.getDoubleAttributeByName("triggerSpeed", 1.0);
	double aimingAccuracy = iter.getDoubleAttributeByName("aimingAccuracy", 10.0);
	iter.getAttributeValueByName("description", description);
	iter.getAttributeValueByName("inspectionText", inspectionText);

	int gunnery = (int)(iter.getIntAttributeValueByName("gunnery", 4));
	int piloting = (int)(iter.getIntAttributeValueByName("piloting", 4));
	if (piloting < 0) {
		piloting = 0; // Higher than 6 means the AI is able only to follow its preset course
	}
	if (gunnery < 0) {
		gunnery = 0;
	}

	// See if we need to assign a random Mech
	BGCPString randomMechSpec;
	if (iter.getAttributeValueByName("randomMech", randomMechSpec)) {
		selectRandomMech(randomMechSpec, chassisName, configFN, description);
	}

	// See if the shell wants to evaluate some of the strings (used by Instant Action and could be used to configure mech stars)
	faction = evaluateMechShellVars(faction);
	chassisName = evaluateMechShellVars(chassisName);
	configFN = evaluateMechShellVars(configFN);

	int sleepSeconds = (int)iter.getIntAttributeValueByName("sleep", 0);
	if (sleepSeconds > 0) {
		AIGoal goal;
		goal.type = SubObjectiveType::sleep;
		goal.duration = sleepSeconds;
		goals.push_back(goal);
	}

	iter.getAttributeValueByName("attack", target);
	if (!target.empty()) {
		AIGoal goal;
		goal.type = SubObjectiveType::attack;
		goal.target = target;
		goals.push_back(goal);
	}

	iter.getAttributeValueByName("description", description);
	int health = (int) iter.getIntAttributeValueByName("health", 1000);
	int salvage = (int) iter.getIntAttributeValueByName("salvage", 0);

	SET_OP;
	for (size_t j = 0; j < iter.getChildCount(); ++j) {
		iter.goToChild(j);
		goals.push_back(AIGoal());
		AIGoal& goal = goals[goals.size()-1];

		if (iter.nodeNameEquals("visit")) {
			goal.type = SubObjectiveType::visit;
		} else if (iter.nodeNameEquals("attack") || iter.nodeNameEquals("destroy")) {
			goal.type = SubObjectiveType::attack;
		} else if (iter.nodeNameEquals("defend")) {
			goal.type = SubObjectiveType::defend;
		} else if (iter.nodeNameEquals("inspect")) {
			goal.type = SubObjectiveType::inspect;
		} else if (iter.nodeNameEquals("wait")) {
			goal.type = SubObjectiveType::sleep;
			iter.getAttributeValueByName("trigger", goal.trigger);
			goal.duration = (int)iter.getIntAttributeValueByName("duration", 9999); // if no duration is mentioned, the trigger should count instead
		} else if (iter.nodeNameEquals("event")) {
			goal.type = SubObjectiveType::event;
		} else if (iter.nodeNameEquals("joinFormation")) {
			goal.type = SubObjectiveType::joinFormation;
		} else {
			COUT << "Unknown goal type: ";
			BGCPString str;
			iter.getNodeName(str);
			str.printLn();
			RETURN;
		}

		SET_OP;
		goal.coords.x = iter.getDoubleAttributeByName("x");
		goal.coords.y = iter.getDoubleAttributeByName("y");
		goal.coords.z = iter.getDoubleAttributeByName("z");
		iter.getAttributeValueByName("target", goal.target);
		iter.getAttributeValueByName("trigger", goal.trigger);

		iter.goToParent();
	}

	// First spawn is reserved for the player, all additional spawns are added to the scene right away
	if (mAIVehicles.size() == 0) {
		if (chassisName.empty() && mShell != nullptr) {
			SET_OP;
			mShell->selectStr("%chassis%,%mechlist%", chassisName);
		}
		if (configFN.empty() && mShell!= nullptr) {
			SET_OP;
			mShell->selectStr("%config%,%configs%", configFN);
		}
	}

	SET_OP;
	if (!configFN.endsWith(".mek") && !configFN.endsWith(".MEK")) {
		configFN += ".mek";
	}
	Vehicle* vehicle = loadVehicle2(chassisName, configFN, mTextureFN, faction);
	if (vehicle == nullptr) {
		RETURN;
	}

	//addManagedObject(vehicle); // since the vehicle is added to the scene, it will be deleted along with the scene
	if (mAIVehicles.size() == 0) {
		SET_OP;
		playerVehicleAI = (PlayerVehicleAI*)vehicle->createAI(true, getEnvironment());
		addManagedObject(playerVehicleAI);
		mAIVehicles.push_back(playerVehicleAI);
		vehicle->setStatus(engineRunning);
	} else {
		SET_OP;
		AbstractVehicleAI* ai = vehicle->createAI(false, getEnvironment());
		addManagedObject(ai);
		mAIVehicles.push_back(ai);
		vehicle->setID(id);
		ai->setTriggerRange(triggerRange);
		//aiPlayer->addAttackGoal(target); // was already added to goals above ...
		vehicle->setName(description);
		ai->mTriggerSpeed = triggerSpeed;
		ai->mAimingAccuracy = aimingAccuracy;
		ai->addGoals(goals, false);
		vehicle->CFGFN = configFN; // Needed for salvage
		vehicle->MVMKFN = chassisName;
		ai->setPiloting(piloting);
		ai->setGunnery(gunnery);
	}
		
	vehicle->setName(description);
	vehicle->setPosition(pos);
	vehicle->mHeading = heading;
	m_sceneObjects->addChild(vehicle);
	vehicle->updateLoadoutGeometry();
	vehicle->setDescriptionText(description);
	vehicle->setInspectionText(inspectionText);
	vehicle->setID(id);
	vehicle->mSalvage = (salvage != 0);

	// If there is a metersPerSecond, use that to update the vehicle speed
	if (iter.hasAttribute("metersPerSecond")) {
		double newMPS = iter.getDoubleAttributeByName("metersPerSecond");
		vehicle->mMetersPerSecond = newMPS;
	}

	if (iter.hasAttribute("lift")) {
		vehicle->mLift = iter.getDoubleAttributeByName("lift", 0.0);
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////

void MechSim::addSceneObjectsRecursively(RenderableObject* container, 
	const PathChar* levelDir, XMLTreeConstIterator& iter, const Matrix& m)
{
	SET_SCOPE;
	size_t childCount = iter.getChildCount();

	for (size_t i = 0; i < childCount; ++i) {
		iter.goToChild(i);
		if (iter.nodeNameEquals("transform")) {
			SET_OP;
			processTransform(container, m, iter, levelDir);
		} else if (iter.nodeNameEquals("object")) {
			SET_OP;
			processObjectTag(container, iter, m, levelDir);
		} else if (iter.nodeNameEquals("heightmap")) {
			SET_OP;
			processHeightmapTag(container, iter);
		} else if (iter.nodeNameEquals("repeat")) {
			SET_OP;
			int count = (int)iter.getIntAttributeValueByName("count");
			double tx = iter.getDoubleAttributeByName("tx");
			double ty = iter.getDoubleAttributeByName("ty");
			double tz = iter.getDoubleAttributeByName("tz");
			for (int i = 0; i < count; ++i) {
				Matrix m2 = m * TranslationMatrix(i*tx, i*ty, i*tz);
				addSceneObjectsRecursively(container, levelDir, iter, m2);
			}
		} else if (iter.nodeNameEquals("random")) {
			SET_OP;
			// Select one random child node to instantiate and ignore all others
			size_t childCount = iter.getChildCount();
			size_t selectedChild = irand<size_t>(0, childCount);
			iter.goToChild(selectedChild);
			addSceneObjectsRecursively(container, levelDir, iter, m);
			iter.goToParent();
		} else if (iter.nodeNameEquals("lightSource")) {
			SET_OP;
			LightSource* ls = new LightSource(GL_LIGHT0 + mLightSources.size() + 1);
			ls->mPos.x = iter.getDoubleAttributeByName("x");
			ls->mPos.y = iter.getDoubleAttributeByName("y");
			ls->mPos.z = iter.getDoubleAttributeByName("z");
			ls->mPos.w = iter.getDoubleAttributeByName("w");
			ls->mAmbientFactor = (GLfloat)iter.getDoubleAttributeByName("ambient", 0.3);
			BGCPString colorStr;
			iter.getAttributeValueByName("color", colorStr);

			BGCPString renderShadowsStr;
			iter.getAttributeValueByName("renderShadows", renderShadowsStr);
			ls->mRenderShadows = (renderShadowsStr == "true");

			ls->color = parseColor(colorStr);
			mLightSources.push_back(ls);
			addSceneObjectsRecursively(container, levelDir, iter, m);
		} else if (iter.nodeNameEquals("nav")) {
			SET_OP;
			NavPoint navPoint;
			navPoint.pos.x = iter.getDoubleAttributeByName("x");
			navPoint.pos.y = iter.getDoubleAttributeByName("y");
			navPoint.pos.z = iter.getDoubleAttributeByName("z");
			iter.getAttributeValueByName("description", navPoint.name);
			navPoint.visited = false;
			mNavPoints.push_back(navPoint);
		} else if (iter.nodeNameEquals("spawn")) {
			SET_OP;
			processSpawn(iter);
		} else if (iter.nodeNameEquals("mission-goals")) {
			SET_OP;
			iter.getAttributeValueByName("success", mSuccessMission);
			iter.getAttributeValueByName("failure", mFailureMission);
			mMissionGoalsTracker.parseFromXml(iter, mShell);
		} else if (iter.nodeNameEquals("music")) {
			BGCPString isoFN;
			mShell->getVar("iso", isoFN);
			#ifdef MECHVM
			ISO9660 iso;
			if (iso.open(isoFN.getAsPath())) {
				BGCPString trackNo;
				// If the CD does not have track 20, it's probably Titanium. For Mercs, the tracks seem to be the same ...
				// If a DOS CD is used with a TT game, we have to use the tracks as defined by the DOS version,
				// therefore we decide what track number to use based on what CD we are given, not based on MW2.PRJ
				bool is_tt = (iso.getEntryNumberByName("TRACK 20.cda") > iso.getEntryCount());
				if (is_tt) {
					iter.getAttributeValueByName("tt_track", trackNo);
				} else {
					iter.getAttributeValueByName("track", trackNo);
				}
				BGCPString fn = BGCPString("TRACK ") + trackNo + BGCPString(".cda");
				fn.convertToCP(CP_ASCII);
				MemoryBlock mb;
				//if (mb.loadFromFile(fn.getAsPath())) {
				if (iso.getFile(fn.get<char>(), mb)) {
					music = new Sound();
					music->decodeCDDA(mb);
					Audio* audio = getAudio();
					queueSound(*music, true);
				}
			} else {
				COUT << "Failed to play music from ";
				isoFN.printLn();
			}
			#endif
		} else if (iter.nodeNameEquals("includeXML")) {
			SET_OP;
			BGCPString newXmlFN;
			for (size_t i = 0; i < iter.getChildCount(); ++i) {
				iter.goToChild(i);
				BGCPString textHere = iter.getText();
				BGCPString evaluatedText = evaluateMechShellVars(textHere);
				newXmlFN += evaluatedText;
				iter.goToParent();
			}
			BGCPString levelFN(levelDir, CP_SYSTEM);
			levelFN.addPathSep(newXmlFN + BGCPString(".xml"));
			loadLevel(levelDir, levelFN.getAsPath());
		} else {
			if (iter.getChildCount() > 0) {
				SET_OP;
				addSceneObjectsRecursively(container, levelDir, iter, m);
			}
		}
		iter.goToParent();
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////

void MechSim::processTransform(RenderableObject* container, const Matrix& m, XMLTreeConstIterator& iter, 
	const PathChar* levelDir)
{
	SET_SCOPE;
	Matrix localMatrix = m;
	double tx = iter.getDoubleAttributeByName("tx");
	double ty = iter.getDoubleAttributeByName("ty");
	double tz = iter.getDoubleAttributeByName("tz");
	localMatrix = TranslationMatrix(tx, ty, tz) * localMatrix;

	double ry = iter.getDoubleAttributeByName("ry");
	if (ry != 0.0) {
		localMatrix = RotationMatrix(M_PI * ry / 180.0, 0, 1, 0) * localMatrix;
	}

	double sx = iter.getDoubleAttributeByName("sx", 1.0);
	double sy = iter.getDoubleAttributeByName("sy", 1.0);
	double sz = iter.getDoubleAttributeByName("sz", 1.0);
	localMatrix = ScaleMatrix(sx, sy, sz) * localMatrix;

	addSceneObjectsRecursively(container, levelDir, iter, localMatrix);
	RETURN;
}

bool MechSim::loadLevel(const PathChar* levelDir, const PathChar* xmlFN)
{
	SET_SCOPE;
	randomize();
	COUT << "Launching level " << xmlFN << std::endl;
	XMLTree xml;
	BGCPString bettySpeech;
	if(!xml.loadFromFile(xmlFN)) {
		RETURN false;
	}

	SET_OP;
	XMLTreeConstIterator iter = xml.getRoot();
	iter.getAttributeValueByName("camo", mTextureFN);
	mTextureFN += ".png";

	SET_OP;
	mGravity = 9.81 * iter.getDoubleAttributeByName("gravity", 1.0);

	SET_OP;
	iter.getAttributeValueByName("Betty", bettySpeech);
	if (!bettySpeech.empty()) {
		enqueueVoice(bettySpeech.getPtr<char>());
	}

	SET_OP;
	BGCPString colorStr;
	iter.getAttributeValueByName("reticle", colorStr);
	mReticleColor = parseColor(colorStr);

	SET_OP;
	BGCPString envStr;
	iter.getAttributeValueByName("environment", envStr);
	if (envStr == "space") {
		mEnvironment = space;
	} else if (envStr == "underwater") {
		mEnvironment = underwater;
	}

	SET_OP;
	Matrix m;
	BGCPString skyboxFN;
	iter.getAttributeValueByName("skybox", skyboxFN);
	if (!skyboxFN.empty()) {
		Texture* skyboxTexture = new Texture();
		bool skyboxLoaded = false;
		BGCPString fullSkyboxFN(levelDir, CP_SYSTEM);
		fullSkyboxFN.addPathSep(skyboxFN);
		if (skyboxTexture->loadFromFile(fullSkyboxFN.getAsPath())) {
			mSkybox = new Mesh();
			BGCPString skyboxObjFN(levelDir, CP_SYSTEM);
			skyboxObjFN.addPathSep("hemi.obj");
			skyboxLoaded = mSkybox->loadFromObj(skyboxObjFN.getAsPath());
			if (skyboxLoaded) {
				mSkybox->setMatrix(ScaleMatrix(10000.0, 10000.0, 10000.0));
				mSkybox->setTexture(skyboxTexture);
				addManagedObject(skyboxTexture);
				addManagedObject(mSkybox);
			} else {
				safeDelete(mSkybox);
			}
		}
		if (!skyboxLoaded) {
			delete skyboxTexture;
		}
	}

	SET_OP;
	BGCPString mapGroundColorStr;
	iter.getAttributeValueByName("mapGroundColor", mapGroundColorStr);
	mapGroundColor = parseColor(mapGroundColorStr);

	// Parse the rest of the scene recursively
	SET_OP;
	addSceneObjectsRecursively(m_sceneObjects, levelDir, iter, m);

	mMissionStarted = SDL_GetTicks();
	RETURN true;
}

void MechSim::loadShotPrototypes()
{
	SET_SCOPE;
	BGCPString shotPrototypePath(getDataDir());
	shotPrototypePath.addPathSep("weapons");
	shotPrototypePath.addPathSep();

	BGCPString laserPrototypeFN = shotPrototypePath;
	laserPrototypeFN += "beam.obj";
	mLaserShotPrototype.loadFromObj(laserPrototypeFN.getAsPath());

	BGCPString bulletPrototypeFN = shotPrototypePath;
	bulletPrototypeFN += "bullet.obj";
	mBulletPrototype.loadFromObj(bulletPrototypeFN.getAsPath());
	mBulletPrototype.calculateNormals();
	
	BGCPString missilePrototypeFN = shotPrototypePath;
	missilePrototypeFN += "missile.obj";
	mMissilePrototype.loadFromObj(missilePrototypeFN.getAsPath());
	mMissilePrototype.calculateNormals();

	BGCPString arrowPrototypeFN = shotPrototypePath;
	arrowPrototypeFN += "arrow.obj";
	mArrow.loadFromObj(arrowPrototypeFN.getAsPath());

	BGCPString arrowTextureFN = shotPrototypePath;
	arrowTextureFN += "arrow.png";
	Texture* arrowTexture = new Texture();
	arrowTexture->loadFromFile(arrowTextureFN.getAsPath());
	mArrow.setTexture(arrowTexture);

	RETURN;
}

// Get prototype geometry for a shot
Mesh* MechSim::getShotPrototype(WeaponCategory whichOne)
{
	switch(whichOne) {
		case WeaponCategory::energy:  return &mLaserShotPrototype;
		case WeaponCategory::missile: return &mMissilePrototype;
		case WeaponCategory::bullet:  return &mBulletPrototype;
		default: return nullptr;
	}
}

// Add shot to the scene
void MechSim::addShot(Shot* shot)
{
	mShots.addChild(shot);
}

// Add object
void MechSim::addManagedObject(VirtualClass* o, const BGCPString name)
{
	ManagedObject mo(name, o);
	mManagedObjects.push_back(mo);
}

void MechSim::adjustSatelliteDist(double factor)
{
	if (factor > 1.0 && mSatelliteDist < 8000
	||  factor < 1.0 && mSatelliteDist > 75)
	{
		mSatelliteDist *= factor;
	}
}

void MechSim::selectNextNavPoint()
{
	if (mNavPoints.size() > 0) {
		mCurrentNavPoint = (mCurrentNavPoint + 1) % mNavPoints.size();
	}
}

// Play a sound from a MW2.PRJ archive
bool MechSim::playSound(const char* filename)
{
	SET_SCOPE;
	if (mArchive == nullptr
	||  bgstrlen(filename) == 0) 
	{
		return false;
	}

	SET_OP;
	Audio* audio = getAudio();
	Sound* archivedSound = audio->getSound(filename);
	if (archivedSound != nullptr) {
		queueSound(*archivedSound);
		RETURN true;
	}

	SET_OP;
	mArchive->leaveDir();
	if (!mArchive->enterSubDir("SNDS")) {
		RETURN false;
	}

	MemoryBlock sflFile;
	if (!mArchive->getFile(filename, sflFile)) {
		RETURN false;
	}

	SET_OP;
	Sound* sound = new Sound;
	sound->decodeSFL(sflFile);
	queueSound(*sound);
	audio->archiveSound(sound, filename);
	RETURN true;
}

// Check when the current voice ends
DWORD MechSim::getCurrentVoiceEnd()
{
	SET_SCOPE;
	DWORD result = mNow;
	for (size_t i = 0; i < mQueuedVoice.size(); ++i) {
		if (mQueuedVoice[i].sound != nullptr) {
			result = std::max(result, mQueuedVoice[i].startMS + mQueuedVoice[i].sound->getDurationMS());
		}
	}
	RETURN result;
}

// Schedule playing a voice file
void MechSim::scheduleSound(Sound* voice, DWORD startMS)
{
	SET_SCOPE;
	QueuedVoice voiceStruct;
	voiceStruct.sound = voice;
	voiceStruct.startMS = startMS;
	mQueuedVoice.emplace_back(voiceStruct);
	RETURN;
}

// Queue voice - no more than one person should be talking at once, even in war ...
// filenames may be a single file, or a comma-separated list of files
bool MechSim::enqueueVoice(const BGCPString& filenames)
{
	SET_SCOPE;
	#ifdef MECHVM
	if (mArchive == nullptr
	||  filenames.empty()) 
	{
		return false;
	}

	BGCPString filenamesAscii = filenames;
	filenamesAscii.convertToCP(CP_ASCII);
	auto filenameV = filenames.split(",", false);
	DWORD nextVoiceStart = getCurrentVoiceEnd();
	DWORD SilenceBetweenVoices = 500;
	bool completeSuccess = true;

	for (size_t i = 0; i < filenameV.size(); ++i) {
		SET_OP;
		const BGCPString& filename = filenameV[i];
		Audio* audio = getAudio();
		mArchive->leaveDir();
		if (!mArchive->enterSubDir("SNDS")) {
			completeSuccess = false;
		}

		MemoryBlock sflFile;
		if (mArchive->getFile(filename.get<char>(), sflFile)) {
			SET_OP;
			Sound* sound = new Sound;
			sound->decodeSFL(sflFile);
			scheduleSound(sound, nextVoiceStart);
			nextVoiceStart += sound->getDurationMS() + SilenceBetweenVoices;
		} else {
			// If the file is not stored in MW2.PRJ, try the keating folder ...
			SET_OP;
			MemoryBlock mb;
			BGCPString keatingFN = mGameDir;
			keatingFN.addPathSep("keating");
			keatingFN.addPathSep(filename);
			if (mb.loadFromFile(keatingFN.getAsPath())) {
				Sound* sound = new Sound;
				sound->decodeSFL(mb);
				scheduleSound(sound, nextVoiceStart);
				nextVoiceStart += sound->getDurationMS() + SilenceBetweenVoices;
			} else {
				completeSuccess = false;
			}
		}
	}
	RETURN completeSuccess;
	#else
	return false;
	#endif
}

void MechSim::setReturnVis(GLWindow* newVis)
{
	returnVis = newVis;
}

void MechSim::addVehicle(RenderableObject* vehicle)
{
	m_sceneObjects->addChild(vehicle);
}

#ifdef MECHVM
void MechSim::addArchive(MWArchive* archive)
{
	if (archive == nullptr) {
		return;
	}

	SET_SCOPE;
	mArchive = archive;

	// Load font for the sim from mw2.prj or net.prj
	mArchive->leaveDir();
	if (mArchive->enterSubDir("FONT")) {
		MemoryBlock mb;
		mArchive->getFile("BASE6x7k.FNT", mb);
		Texture texture;
		decodeFNT(mb, texture);
		//texture.saveToFile(L"c:\\temp\\BASE6x7k.FNT.bmp", 0, 0, texture.getWidth(), texture.getHeight());
		mFont = new Font(texture, 9, 14, 1);
	} else {
		mFont = Font::loadFromSystem("Arial", 16, 2);
	}

	// Get paused image - todo: locate MW3 alternative?
	MemoryBlock shpMB, palMB;
	mPausedPic = new Texture();
	mArchive->leaveDir();
	mArchive->enterSubDir("PAL");
	if (!mArchive->getFile("BROWN_DA.COL", palMB)) {
		mArchive->getFile("BROW0TDA.COL", palMB);
	}
	Palette pal;
	memcpy(pal.values, palMB.getPtr<char*>(), std::min<int>(palMB.getSize(), 768));
	mArchive->leaveDir();
	mArchive->enterSubDir("SHP");
	mArchive->getFile("PAUSED6.SHP", shpMB);
	MechWarriorIIPRJ::loadSHP(shpMB, 0, pal, *mPausedPic);

	RETURN;
}
#endif

// Notify mission goals tracker of an event. This may be a destroyed object (Vehicle or GameObject) or other type of event
void MechSim::updateObjectStatus(const BGCPString& id, SubObjectiveType newStatus, bool missionEnd)
{
	SET_SCOPE;
	mMissionGoalsTracker.updateObjectStatus(id, newStatus, missionEnd);
	if (std::find(mPreviousEvents.begin(), mPreviousEvents.end(), id) == mPreviousEvents.end()) {
		// New event ...
		mPreviousEvents.push_back(id);
	}

	// Check if any of the vehicles care about the event
	for (size_t i = 0; i < mAIVehicles.size(); ++i) {
		AbstractVehicleAI* ai = mAIVehicles[i];
		if (ai != nullptr) {
			ai->updateObjectStatus(id, newStatus);
		}
	}
	RETURN;
}

// Get a pointer to the entire scene ...
RenderableObject* MechSim::getScene()
{
	return m_sceneObjects;
}

// Get number of milliseconds spend on the current mission
DWORD MechSim::missionTime() const
{
	return mNow;
}

// Allow the shell to register with the sim so that the sim can ask variables from the shell
void MechSim::setShell(MechShell* shell)
{
	SET_SCOPE;
	mShell = shell;

	// Query font from the shell ...
	if (mShell != nullptr && mArchive != nullptr) {
		SET_OP;
		BGCPString mechSimFontFN;
		mShell->getVar("mechSimFont", mechSimFontFN);
		mechSimFontFN.convertToCP(CP_ASCII);
		int slashPos = mechSimFontFN.findChar('/');
		if (slashPos > 0) {
			BGCPString dir = mechSimFontFN.copyPart(0, slashPos);
			mArchive->enterSubDir(dir.getPtr<char>());
			mechSimFontFN = mechSimFontFN.copyPart(slashPos+1, mechSimFontFN.getByteCount()-slashPos-1);
		}

		SET_OP;
		MemoryBlock fontBlock;
		mArchive->getFile(mechSimFontFN.getPtr<char>(), fontBlock);

		SET_OP;
		Texture texture;
		decodeFNT(fontBlock, texture);
		size_t divider = 8;
		int charW = texture.getWidth() / 16;
		int charH = texture.getHeight() / divider;

		SET_OP;
		safeDelete(mFont);
		mFont = new Font(texture, charW, charH, 1);
	}

	RETURN;
}

BGCPString MechSim::evaluateMechShellVars(const BGCPString src)
{
	SET_SCOPE;
	MechShell* shell = (MechShell*)mShell;
	BGCPString result = src;
	if (shell != nullptr) {
		shell->evaluate(result, false);
	}
	RETURN result;
}

EnvironmentTypes MechSim::getEnvironment() const
{
	return mEnvironment;
}

// Activate/deactivate light sources
void MechSim::activateLightSources(bool renderingShadows) const
{
	SET_SCOPE;
	for (size_t i = 0; i < mLightSources.size(); ++i) {
		mLightSources[i]->activate(renderingShadows);
	}
	RETURN;
}

void MechSim::deactivateLightSources() const
{
	SET_SCOPE;
	for (size_t i = 0; i < mLightSources.size(); ++i) {
		mLightSources[i]->deactivate();
	}
	RETURN;
}

// Add sound to the sound queue
void MechSim::queueSound(const Sound& sound, bool repeat)
{
	SET_SCOPE;
	SDL_LockMutex(mSoundMutex);

	// Can't play back infinitely many sounds at once ...
	if (mQueuedSounds.size() < 10) {
		Audio* audio = getAudio();
		const SDL_AudioSpec& deviceAudioSpec = audio->getAudioSpec();

		// convert sound into a format compatible with the output device
		SET_OP;
		Sound* compatibleSound = new Sound();
		compatibleSound->copyConvert(sound, deviceAudioSpec);

		QueuedSound queuedSound;
		queuedSound.sound = compatibleSound;
		queuedSound.repeat = repeat;
		queuedSound.playOffset = 0;

		mQueuedSounds.push_back(queuedSound);
	}

	SDL_UnlockMutex(mSoundMutex);
	RETURN;
}

// Update the sound queue
// Used for sound playback without callback
// todo: limit number of sounds played concurrently? Mostly a problem with collisions ...
void MechSim::calcSoundFrame()
{
	// Get rid of all sounds that are over
	SET_SCOPE;
	auto soundIter = mQueuedSounds.begin();
	while (soundIter != mQueuedSounds.end()) {
		QueuedSound& sound = (*soundIter);
		if (sound.playOffset >= sound.sound->mData.getSize()) {
			if (sound.repeat) {
				sound.playOffset = 0;
			} else {
				delete (soundIter->sound);
				soundIter = mQueuedSounds.erase(soundIter);
			}
		} else {
			++soundIter;
		}
	}

	if (mQueuedSounds.size() == 0) {
		RETURN;
	}

	SET_OP;
	Audio* audio = getAudio();
	const SDL_AudioSpec& deviceAudioSpec = audio->getAudioSpec();
	auto bps = getBytesPerSample(deviceAudioSpec.format);
	size_t requiredBufferSize = mSoundFrameDurationMS * bps * deviceAudioSpec.channels * deviceAudioSpec.freq / 1000;

	if (mQueuedSounds.size() == 1) {
		SET_OP;
		// If there is just one sound to play, we do not need additional buffers or mount sounds
		QueuedSound& sound = mQueuedSounds[0];
		if (sound.playOffset < sound.sound->mData.getSize()) {
			size_t bytesToPlay = std::min(requiredBufferSize, sound.sound->mData.getSize() - sound.playOffset);
			char* p = sound.sound->mData.getPtr<char>(sound.playOffset);
			audio->queue(p, bytesToPlay);
			sound.playOffset += bytesToPlay;
		}
		RETURN;
	}

	// Prepare a sound for the sound frame
	SET_OP;
	Sound soundFrame;
	soundFrame.mAudioFormat = deviceAudioSpec;
	soundFrame.mData.resize(requiredBufferSize);
	soundFrame.mData.fill(0, requiredBufferSize, 0);

	// If there is only one sound playing, copy its data
	// Samples from the first sound can be copied directly
	soundIter = mQueuedSounds.begin();

	// Find start of current sound, decide how much data to copy, then copy
	size_t bytesToCopy = std::min(requiredBufferSize, soundIter->sound->mData.getSize() - mQueuedSounds[0].playOffset);
	soundFrame.mData.write(0, soundIter->sound->mData.getPtr<char>(soundIter->playOffset), bytesToCopy);
	mQueuedSounds[0].playOffset += bytesToCopy;

	++soundIter;

	// Further sounds have to be added
	SET_OP;
	size_t samplesToCopy = bytesToCopy / (bps * deviceAudioSpec.channels);
	while (soundIter != mQueuedSounds.end()) {
		// Read existing samples, add new sample, write result back
		size_t bytesToCopy = std::min(requiredBufferSize, soundIter->sound->mData.getSize() - soundIter->playOffset);

		for (size_t i = 0; i < samplesToCopy; ++i) {
			for (size_t c = 0; c < deviceAudioSpec.channels; ++c) {
				size_t startOffset = soundIter->playOffset / (bps * deviceAudioSpec.channels);
				float val = soundFrame.getSampleF(i, c) + soundIter->sound->getSampleF(i + startOffset, c);
				//val = putInRange<float>(-1.0f, val, 1.0f); // todo: apply clamp after all values have been added
				soundFrame.setSampleF(i, c, val);
			}
		}

		soundIter->playOffset += bytesToCopy;
		++soundIter;
	}

	for (size_t i = 0; i < samplesToCopy; ++i) {
		for (size_t c = 0; c < deviceAudioSpec.channels; ++c) {
			float val = soundFrame.getSampleF(i, c);
			val = putInRange<float>(-1.0f, val, 1.0f);
			soundFrame.setSampleF(i, c, val);
		}
	}

	audio->queue(soundFrame);

	RETURN;
}

// Provide audio via SDL callback
void MechSim::provideAudio(void* userdata, Uint8* stream, int len)
{
	if (mPaused) {
		SDL_memset(stream, 0, len);
		return;
	}

	// Check if a voice should be played
	if (mQueuedVoice.size() > 0) {
		if (mQueuedVoice[0].startMS <= mNow
		&&  mQueuedVoice[0].sound != nullptr) 
		{
			queueSound(*mQueuedVoice[0].sound);
			mQueuedVoice.erase(mQueuedVoice.begin());
		}
	}

	// Get rid of all sounds that are over
	SET_SCOPE;
	SDL_LockMutex(mSoundMutex);
	auto soundIter = mQueuedSounds.begin();
	while (soundIter != mQueuedSounds.end()) {
		SET_OP;
		QueuedSound& sound = (*soundIter);
		if (sound.playOffset >= sound.sound->mData.getSize()) {
			if (sound.repeat) {
				sound.playOffset = 0;
			} else {
				safeDelete(soundIter->sound);
				soundIter = mQueuedSounds.erase(soundIter);
			}
		} else {
			++soundIter;
		}
	}

	if (mQueuedSounds.size() == 0) {
		SET_OP;
		SDL_UnlockMutex(mSoundMutex);
		SDL_memset(stream, 0, len);
		RETURN;
	}

	SET_OP;
	Audio* audio = getAudio();
	const SDL_AudioSpec& deviceAudioSpec = audio->getAudioSpec();
	auto bps = getBytesPerSample(deviceAudioSpec.format);

	if (mQueuedSounds.size() == 1) {
		SET_OP;
		// If there is just one sound to play, we do not need additional buffers or mount sounds
		QueuedSound& sound = mQueuedSounds[0];
		if (sound.playOffset < sound.sound->mData.getSize()) {
			size_t bytesToPlay = std::min<size_t>(len, sound.sound->mData.getSize() - sound.playOffset);
			char* p = sound.sound->mData.getPtr<char>(sound.playOffset);
			//audio->queue(p, bytesToPlay);
			SDL_memcpy(stream, p, bytesToPlay);

			if (bytesToPlay < (size_t)len && len > 0) {
				SDL_memset(stream+bytesToPlay, 0, len-bytesToPlay);
			}

			sound.playOffset += bytesToPlay;
		}
		SDL_UnlockMutex(mSoundMutex);
		RETURN;
	}

	// Prepare a sound for the sound frame
	SET_OP;
	Sound soundFrame;
	soundFrame.mAudioFormat = deviceAudioSpec;
	soundFrame.mData.resize(len);
	soundFrame.mData.fill(0, len, 0);

	// If there is only one sound playing, copy its data
	// Samples from the first sound can be copied directly
	soundIter = mQueuedSounds.begin();

	// Find start of current sound, decide how much data to copy, then copy
	size_t bytesToCopy = std::min<size_t>(len, soundIter->sound->mData.getSize() - mQueuedSounds[0].playOffset);
	soundFrame.mData.write(0, soundIter->sound->mData.getPtr<char>(soundIter->playOffset), bytesToCopy);
	mQueuedSounds[0].playOffset += bytesToCopy;

	++soundIter;

	// Further sounds have to be added
	SET_OP;
	size_t samplesToCopy = bytesToCopy / (bps * deviceAudioSpec.channels);
	while (soundIter != mQueuedSounds.end()) {
		// Read existing samples, add new sample, write result back
		size_t bytesToCopy = std::min<size_t>(len, soundIter->sound->mData.getSize() - soundIter->playOffset);

		for (size_t i = 0; i < samplesToCopy; ++i) {
			for (size_t c = 0; c < deviceAudioSpec.channels; ++c) {
				size_t startOffset = soundIter->playOffset / (bps * deviceAudioSpec.channels);
				float val = soundFrame.getSampleF(i, c) + soundIter->sound->getSampleF(i + startOffset, c);
				//val = putInRange<float>(-1.0f, val, 1.0f); // todo: apply clamp after all values have been added
				soundFrame.setSampleF(i, c, val);
			}
		}

		soundIter->playOffset += bytesToCopy;
		++soundIter;
	}

	for (size_t i = 0; i < samplesToCopy; ++i) {
		for (size_t c = 0; c < deviceAudioSpec.channels; ++c) {
			float val = soundFrame.getSampleF(i, c);
			val = putInRange<float>(-1.0f, val, 1.0f);
			soundFrame.setSampleF(i, c, val);
		}
	}

	SDL_memcpy(stream, soundFrame.mData.getPtr<char>(0), len);
	SDL_UnlockMutex(mSoundMutex);
	RETURN;
}

// Find vehicle by name
Vehicle* MechSim::findVehicleByID(const BGCPString& vehicleName)
{
	SET_SCOPE;
	for (size_t i = 0; i < mAIVehicles.size(); ++i) {
		AbstractVehicleAI* vehicleAI = mAIVehicles[i];
		Vehicle* vehicle = vehicleAI->getVehicle();
		if (vehicle != nullptr && vehicle->getID().equalsNoCase(vehicleName)) {
			RETURN vehicle;
		}
	}

	RETURN nullptr;
}

// Trigger starmate/lancemate action via communications dialog
void MechSim::setStarmateAction_JoinFormation()
{
	SET_SCOPE;
	Vehicle* starmate1 = findVehicleByID("Starmate1");
	Vehicle* starmate2 = findVehicleByID("Starmate2");

	AIGoal joinGoal;
	joinGoal.type = SubObjectiveType::joinFormation;
	std::vector<AIGoal> newGoals;
	newGoals.emplace_back(joinGoal);

	if (starmate1 != nullptr 
	&& (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm1))
	{
		AbstractVehicleAI* vehicle1AI = starmate1->getAI();
		if (vehicle1AI != nullptr) {
			vehicle1AI->addGoals(newGoals, true);
		}
	}

	if (starmate2 != nullptr 
	&& (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm2))
	{
		AbstractVehicleAI* vehicle2AI = starmate2->getAI();
		if (vehicle2AI != nullptr) {
			vehicle2AI->addGoals(newGoals, true);
		}
	}
	RETURN;
}

void MechSim::setStarmateAction_Attack()
{
	SET_SCOPE;
	Vehicle* starmate1 = findVehicleByID("Starmate1");
	Vehicle* starmate2 = findVehicleByID("Starmate2");
	GameObject* target = playerVehicleAI->getTarget();

	AIGoal joinGoal;
	joinGoal.type = SubObjectiveType::joinFormation;
	std::vector<AIGoal> newGoals;
	newGoals.emplace_back(joinGoal);

	if (starmate1 != nullptr 
	&& (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm1))
	{
		AbstractVehicleAI* vehicle1AI = starmate1->getAI();
		if (vehicle1AI != nullptr) {
			vehicle1AI->setTarget(target);
			vehicle1AI->addGoals(newGoals, true);
		}
	}

	if (starmate2 != nullptr 
	&& (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm2))
	{
		AbstractVehicleAI* vehicle2AI = starmate2->getAI();
		if (vehicle2AI != nullptr) {
			vehicle2AI->setTarget(target);
			vehicle2AI->addGoals(newGoals, true);
		}
	}
	RETURN;
}

void MechSim::setStarmateAction_Shutdown()
{
	SET_SCOPE;
	Vehicle* starmate1 = findVehicleByID("Starmate1");
	Vehicle* starmate2 = findVehicleByID("Starmate2");

	AIGoal sleepGoal;
	sleepGoal.type = SubObjectiveType::sleep;
	std::vector<AIGoal> newGoals;
	newGoals.emplace_back(sleepGoal);

	if (starmate1 != nullptr 
	&& (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm1))
	{
		AbstractVehicleAI* vehicle1AI = starmate1->getAI();
		if (vehicle1AI != nullptr) {
			vehicle1AI->addGoals(newGoals, true);
		}
	}

	if (starmate2 != nullptr 
	&& (mMfdMode == MFD_CommAll || mMfdMode == MFD_Comm2))
	{
		AbstractVehicleAI* vehicle2AI = starmate2->getAI();
		if (vehicle2AI != nullptr) {
			vehicle2AI->addGoals(newGoals, true);
		}
	}
	RETURN;
}

Vehicle* MechSim::getPlayerVehicle()
{
	return playerVehicleAI->getVehicle();
}

const Vehicle* MechSim::getPlayerVehicle() const
{
	if (playerVehicleAI == nullptr) {
		return nullptr;
	}

	return playerVehicleAI->getVehicle();
}

void MechSim::mouseWheelMoved(int x, int y)
{
	mZoom = putInRange(MIN_ZOOM, mZoom-y, MAX_ZOOM);
}

void MechSim::processWeaponFire(double deltaT)
{
	// Check if new shots need to be created
	SET_SCOPE;
	for (size_t i = 0; i < m_sceneObjects->getObjectCount(); ++i) {
		Vehicle* vehicle = dynamic_cast<Vehicle*> (m_sceneObjects->getObject(i));
		if (vehicle != nullptr) {
			vehicle->processWeaponFire(mNow, m_sceneObjects, deltaT);
		}
	}

	size_t i = 0;
	SET_OP;
	while (i < mShots.getObjectCount() && deltaT > 0.0) {
		// Move shot
		SET_OP;
		Shot* shot = dynamic_cast<Shot*>(mShots.getObject(i));
		MountedWeapon* mountedWeapon = shot->weapon;
		WeaponType weaponType = mountedWeapon->weaponType;
		auto baseWeapon = getWeaponStats(weaponType);
		if (mNow > shot->triggeredWhen + 1000 * baseWeapon->defyGravityDuration) {
			shot->flightDir += Point3D(0, -mGravity * deltaT, 0);
		}

		// check for collisions before moving the shot to its new position, get rid of the shot after collision
		double minDist = 5000.0;
		Point3D dir = deltaT * shot->flightDir;
		Point3D newPos = shot->position + dir;
		double vectorLen = dir.length();
		dir.unitLength();
		Point3D intersection;

		SET_OP;
		RenderableObject* hitObject = traverseScene(shot->position, newPos, m_sceneObjects, minDist, intersection);
		if (hitObject != nullptr && minDist < vectorLen) {
			// Vehicles need special treatment, because for a vehicle, we need the mesh's parent
			SET_OP;
			Vehicle* vehicle = findVehicleParent(hitObject);
			if (vehicle != nullptr) {
				vehicle->receiveDamage(hitObject, shot->damageCarried, shot->shooter, intersection);
			} else {
				GameObject* gameObject = dynamic_cast<GameObject*>(hitObject);
				if (gameObject != nullptr && minDist < vectorLen) {
					gameObject->receiveDamage(hitObject, shot->damageCarried, shot->shooter, intersection);
				}
			}
			mShots.removeObject(i, true);
		} else {
			// Artillery hit something ... using increased damage and range to compensate for bad aim ...
			#ifdef MECHVM
			if (shot->weapon->weaponType == WeaponType::IS1_Long_Tom 
			&& (minDist < vectorLen || shot->position.y < 0.0)) 
			{
				applyAreaDamage(shot->position, 100, 100, shot->shooter);
			}
			#endif

			if (shot->position.y < 0.0) { // todo: support direct hits?
				SET_OP;
				mShots.removeObject(i, true);
			} else {
				SET_OP;
				shot->position = newPos;

				// For rotating the shot, z equals the moving direction of the shot, the other two are perpendicular to that ...
				Point3D zDir = shot->flightDir;
				zDir.unitLength();
				Point3D xDir, yDir;
				zDir.findPerps(xDir, yDir);
				Matrix M = TranslationMatrix(shot->position.x, shot->position.y, shot->position.z) * Matrix(xDir, yDir, zDir);
				shot->setMatrix(M);
				shot->cacheTransformedVertices(M);
				++i;
			}
		}
	}

	RETURN;
}

// Apply damage to Mechs and vehicles as an area effect
// todo: would be cool to have actual particles deal the damage ...
void MechSim::applyAreaDamage(const Point3D& explosionLocation, double baseDamage, double maxDist, Vehicle* shooter)
{
	SET_SCOPE;
	for (size_t i = 0; i < mAIVehicles.size(); ++i) {
		Vehicle* vehicle = mAIVehicles[i]->getVehicle();
		if (vehicle != nullptr) {
			double dist = (explosionLocation - vehicle->getPosition()).length();
			if (dist < maxDist) {
				double damage = baseDamage * (1.0 - dist / maxDist);
				for (int i = 0; i < damage; ++i) {
					const BoundingBox& bb = vehicle->getBoundingBox();
					Point3D pointOfImpact = Point3D(
						bb.min().x + frand<double>() * (bb.max().x - bb.min().x),
						bb.min().y + frand<double>() * (bb.max().y - bb.min().y),
						bb.min().z + frand<double>() * (bb.max().z - bb.min().z));
					vehicle->receiveDamage(nullptr, 1.0, shooter, pointOfImpact);
				}
			}
		}
	}
	RETURN;
}

// Check if an event already happened
bool MechSim::eventHappened(const BGCPString& eventName)
{
	SET_SCOPE;

	for (size_t i = 0; i < mPreviousEvents.size(); ++i) {
		if (mPreviousEvents[i] == eventName) {
			RETURN true;
		}
	}

	RETURN false;
}

MechShell* MechSim::getShell()
{
	return mShell;
}

void MechSim::setFreeViewDeltaTheta(double newVal)
{
	mFreeViewDeltaTheta = newVal;
}

void MechSim::setFreeViewTheta(double newVal)
{
	mFreeViewTheta = newVal;
}
