////////////////////////////////////////////////////////////////////////////////
// MechShell.cpp
// Mech shell window for MechVM
// Copyright by Bjoern Ganster
// Developed since 2008
////////////////////////////////////////////////////////////////////////////////

#include "BGCPString.h"
#include "Config.h"
#include "FileList.h"
#include "Font.h"
#include "GLTableView.h"
#include "LZdecode.h"
#include "MechLab.h"
#include "MechShell.h"
#include "MechSim.h"
#include "MechVM.h"
#include "MWDate.h"
#include "Texture.h"

#ifdef MECHVM
#include "MechWarrior3ZBD.h"
#endif

#include <iostream>

#define FILE_NUMBER FILE_NO_MECHSHELL_CPP

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

MechShell::MechShell(int _left, int _top, int _width, int _height)
: GLWindowContainer("MechShell", MechShell_ID, nullptr, _left, _top, _width, _height),
  #ifdef MECHVM
  archive (NULL),
  archive2 (NULL),
  mSmkViewer(nullptr),
  #endif
  mCurrentRoom(nullptr),
  mReturnScreen(nullptr),
  mVirtualScreenSizeX(_width - _left),
  mVirtualScreenSizeY(_height - _top)
{
	SET_SCOPE;
	mFont = getDefaultFont();
	BGCPString mechVMRootDir = getDataDir();
	setVar("mechVMRootDir", mechVMRootDir);
	setVar("nl", "\n"); // new line
	RETURN;
}

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

MechShell::~MechShell()
{
	SET_SCOPE;
	closeArchives();
	clearImages();
	RETURN;
}

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

void MechShell::closeArchives()
{
	SET_SCOPE;
	#ifdef MECHVM
	if (archive2 != archive) {
		safeDelete(archive2);
	} else {
		archive2 = nullptr;
	}
	safeDelete(archive);
	#endif
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Init

bool MechShell::loadCampaign(const PathChar* path)
{
	BGCPString campaignDir;
	getVar("levelDir", campaignDir);
	BGCPString campaignFN = campaignDir;
	campaignFN.addPathSep(BGCPString(path, CP_SYSTEM));
	return tree.loadFromFile(campaignFN.getAsPath());
}

bool MechShell::startCampaign (const BGCPString& gameName)
{
	SET_SCOPE;
	getMechVm()->setScreen(this);
	XMLTreeConstIterator treeIter(tree.getRoot());

	// gameName may be needed later to update persistent variables in MechVM.cfg
	setVar(BGCPString("GameName"), gameName);

	// If no archive2 was specified, use archive1 instead, for NetMech
	#ifdef MECHVM
	if (archive2 == NULL) {
		archive2 = archive;
	}
	#endif

	// Process variable assignments
	// todo: introduce new tag for loading fonts here ... additional tags for selecting fonts then ...
	SET_OP;
	for (size_t i = 0; i < treeIter.getAttributeCount(); ++i) {
		BGCPString command;
		treeIter.getAttributeName(i, command);

		if (command.equalsNoCase("setvar")) {
			BGCPString assignment;
			treeIter.getAttributeValueByIndex(i, assignment);
			processAssignment(assignment, true);
		} else if (command.equalsNoCase("initvar")) {
			BGCPString assignment;
			treeIter.getAttributeValueByIndex(i, assignment);
			initVar(assignment, true);
		} else if (command.equalsNoCase("setstr")) {
			BGCPString assignment;
			treeIter.getAttributeValueByIndex(i, assignment);
			processAssignment(assignment, false);
		} else if (command.equalsNoCase("exec")) {
			BGCPString command;
			treeIter.getAttributeValueByIndex(i, command);
			execute(command);
		}
	}

	SET_OP;
	if (isMercs()) {
		BGCPString mercsDataFN(getDataDir());
		mercsDataFN.addPathSep("campaigns");
		mercsDataFN.addPathSep("mw2-mercs");
		mercsDataFN.addPathSep("contracts.xml");
		XMLTree mercsDataXml;
		if (mercsDataXml.loadFromFile(mercsDataFN.getAsPath())) {
			XMLTreeConstIterator iter = mercsDataXml.getRoot();
			for (size_t i = 0; i < iter.getChildCount(); ++i) {
				iter.goToChild(i);
				if (iter.nodeNameEquals("contract")) {
					MW2MercsContract contract;
					BGCPString showDate, startDate, endDate;
					iter.getAttributeValueByName("showDate", showDate);
					iter.getAttributeValueByName("startDate", startDate);
					iter.getAttributeValueByName("endDate", endDate);
					iter.getAttributeValueByName("name", contract.contractName);
					iter.getAttributeValueByName("mission", contract.firstMission);
					iter.getAttributeValueByName("setvar", contract.setVars);

					contract.showDate = MWDate::fromStr(showDate);
					contract.startDate = MWDate::fromStr(startDate);
					contract.endDate = MWDate::fromStr(endDate);

					mContracts.push_back(contract);
				}
				iter.goToParent();
			}
		}
	}

	BGCPString newRoom;
	treeIter.getAttributeValueByName("startRoom", newRoom);

	if (newRoom.empty()) {
		newRoom = getRoom();
	}

	bool result = enterRoom(newRoom);

	RETURN result;
}

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

bool MechShell::loadArchive (const PathChar* path)
{
	SET_SCOPE;

	// The archive may be stated using a variable, so we need to evaluate
	BGCPString pathStr(path, CP_SYSTEM);
	evaluate(pathStr, false);

	if (archive == NULL) {
		archive = openArchive(pathStr.getAsPath());
		if (archive == NULL) {
			RETURN false;
		} else {
			RETURN true;
		}
	} else {
		archive2 = openArchive(pathStr.getAsPath());
		if (archive2 == NULL) {
			RETURN false;
		} else {
			RETURN true;
		}
	}
}

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

bool MechShell::loadImageFromArchive(const TCHAR* filePath, Texture& texture)
{
	#ifdef MECHVM
	if (archive == NULL) {
		return false;
	}
	#endif

	SET_SCOPE;
	BGCPString fn(filePath, CP_SYSTEM);
	MemoryBlock block;

	// Check if files are supposed to be loaded from an archive
	#ifdef MECHVM
	size_t fileSize;
	if (fn.startsWith("1|")) {
		size_t pos;
		size_t len = bgstrlen(filePath);
		if (bgstrrscan(filePath, '/', len-1, 2, pos)) {
			BGCPString dirName(filePath + 2, sizeof(TCHAR)*(pos-2), CP_SYSTEM);
			archive->leaveDir();
			dirName.convertToCP(CP_ASCII);
			if (!archive->enterSubDir(dirName.getPtr<char>()))
				printf ("Can't enter directory %s\n", dirName.getPtr<char>());
			fn = BGCPString (filePath + pos+1, sizeof(TCHAR)*(len-pos-1), CP_SYSTEM);
		} else {
			fn = BGCPString (filePath+2, CP_SYSTEM);
		}
		size_t fileNum = archive->getEntryNumberByName(fn);
		fileSize = archive->getFileSize(fileNum);
		block.resize(fileSize);

		if (!archive->getFileChunk(fileNum, 0, block.getPtr<unsigned char>(), fileSize)) {
			RETURN false;
		}
	} else if (fn.startsWith("2|")) {
		size_t pos;
		size_t len = bgstrlen(filePath);
		if (bgstrrscan(filePath, '/', 2, len, pos)) {
			BGCPString dirName(filePath+2, pos-2, CP_SYSTEM);
			archive2->leaveDir();
			dirName.convertToCP(CP_ASCII);
			if (!archive2->enterSubDir(dirName.getPtr<char>()))
				printf ("Can't enter directory %s\n", dirName.getPtr<char>());
			fn = BGCPString (filePath + pos+1, len-pos-1, CP_SYSTEM);
		}
		size_t fileNum = archive2->getEntryNumberByName(fn.getPtr<char>());
		fileSize = archive2->getFileSize(fileNum);
		block.resize(fileSize);

		if (!archive2->getFileChunk(fileNum, 0, block.getPtr<unsigned char>(), fileSize)) {
			RETURN false;
		}
	}
	#endif

	// Load image
	auto fileType = getFileTypeFromContent(&block);
	switch (fileType) {
		#ifdef MECHVM
		case FileType::EFA:
			{
				MemoryBlock* pcx = LZdecode(&block);
				bool result = MechWarriorIIPRJ::loadPCX(pcx->getPtr<unsigned char>(), pcx->getSize(), texture, mRoomPal);
				delete pcx;
				RETURN result;
			}
			break;
		case FileType::PCX:
			{
				bool result = MechWarriorIIPRJ::loadPCX(block.getPtr<unsigned char>(), block.getSize(), texture, mRoomPal);
				RETURN result;
			}
		case FileType::MW3_555:
			{
				bool result = Decode_MW3_555(block.getPtr<unsigned char>(), block.getSize(), texture, true);
				RETURN result;
			}
		#endif
		default:
			RETURN false;
	}
}

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

void MechShell::clearImages()
{
	for (size_t i = 0; i < mRegions.size(); ++i) {
		delete mRegions[i].img;
	}
	mRegions.clear();
	mTextEdits.clear();
}

// Help function: add mission results to scrollable text
void MechShell::addMissionResults(int& row, int& col)
{
	SET_SCOPE;

	// Find the table view
	if (getWindowCount() == 0) {
		RETURN;
	}
	GLStringTableView* table = dynamic_cast<GLStringTableView*>(getSubWin(0));
	if (table == nullptr) {
		RETURN;
	}

	BGCPString missionResults, line;
	if (getVar("missionResults", missionResults)) {
		missionResults.convertToCP(CP_ASCII);
		for (size_t i = 0; i < missionResults.getByteCount(); ++i) {
			char c = missionResults.get<char>(i);
			if (c == 0x0a || c == 0x0d) {
				table->setString(row, col, line);
				col = 0;
				++row;
				line.clear();
			} else {
				line += c;
			}
		}

		if (!line.empty()) {
			table->setString(row, col, line);
			col = 0;
			++row;
		}

		#ifdef MECHVM
		if (isMercs()) {
			BGCPString salvage;
			if (getVar("salvage", salvage)) {
				row += 2;
				if (!salvage.empty()) {
					table->setString(row, 0, BGCPString("Salvage: ") + salvage);
				} else {
					table->setString(row, 0, "Salvage: none");
				}
				++row;
			}
		}
		#endif
	}

	setVar("missionResults", ""); // only add mission results once ...
	RETURN;
}

#ifdef MECHVM
GLStringTableView* MechShell::buildScrollableTextFromBWD (MemoryBlock* mb, int x1, int y1, 
	int x2, int y2, const BGCPString& tagName)
{
	SET_SCOPE;
	size_t tagPos = 0x34;
	bool found = false;
	while (tagPos < mb->getSize() && !found) {
		BGCPString str(mb->getPtr<char>(tagPos), 4, CP_ASCII);
		if (str == tagName) {
			found = true;
		} else {
			++tagPos;
		}
	}

	SET_OP;
	//mTableStrings.clear();
	size_t textLen = mb->get<DWORD>(tagPos+4);
	if (tagPos + textLen > mb->getSize()) {
		if (tagPos < mb->getSize()) {
			textLen = mb->getSize() - tagPos;
		} else {
			textLen = 0;
		}
	}

	if (textLen == 0) {
		RETURN nullptr;
	}

	size_t i = tagPos + 8;
	BGCPString text(mb->getPtr<char>(tagPos+8), textLen, CP_ASCII);
	GLStringTableView* result = buildScrollableText(text, x1, y1, x2, y2);
	RETURN result;
}
#endif

GLStringTableView* MechShell::buildScrollableTextFromCSL(const BGCPString& text, int x1, int y1, int x2, int y2)
{
	SET_SCOPE;
	GLStringTableView* newTableView = new GLStringTableView(this);
	int tx1 = width * x1 / mVirtualScreenSizeX;
	int ty1 = height * y1 / mVirtualScreenSizeY;
	int tw = width * (x2 - x1) / mVirtualScreenSizeX;
	int th = height * (y2 - y1) / mVirtualScreenSizeY;
	newTableView->setGeometry(tx1, ty1, tw, th);
	newTableView->setDataSource(newTableView);
	newTableView->textColor = Color(1, 1, 1, 1);
	add(newTableView);

	SET_OP;
	int x = 0; // Screen position in line
	int charWidth = 9;
	int colWidth = width * (x2 - x1) / mVirtualScreenSizeX - 60;
	BGCPString line;
	bool addedMissionResults = false;
	int row = 0;
	int col = 0;
	size_t i = 0;

	SET_OP;
	while (i < text.getByteCount()) {
		char c = text.get<char>(i);
		if (c == ',') {
			newTableView->setString(row, col, line);
			++row;
			line.clear();
		} else {
			line += c;
		}
		++i;
	}

	// Add the last line
	if (!line.empty()) {
		SET_OP;
		newTableView->setString(row, col, line);
		++row;
	}

	SET_OP;
	newTableView->setColWidth(1, colWidth, 120);
	newTableView->setRowHeights(row, mFont->getCharHeight()+1);
	newTableView->setBackgroundColor(0, 0, 0, 0);
	RETURN newTableView;
}

GLStringTableView* MechShell::buildScrollableText(const BGCPString& text, int x1, int y1, int x2, int y2)
{
	SET_SCOPE;
	GLStringTableView* newTableView = new GLStringTableView(this);
	int tx1 = width * x1 / mVirtualScreenSizeX;
	int ty1 = height * y1 / mVirtualScreenSizeY;
	int tw = width * (x2 - x1) / mVirtualScreenSizeX;
	int th = height * (y2 - y1) / mVirtualScreenSizeY;
	newTableView->setGeometry(tx1, ty1, tw, th);
	newTableView->setDataSource(newTableView);
	newTableView->textColor = Color(1, 1, 1, 1);
	add(newTableView);

	// Scan input string, and keep track of the index where the current word starts.
	// As soon as we reach the width limit, use only the part of the line until where the current word starts.
	SET_OP;
	int x = 0; // Screen position in line
	int charWidth = 9;
	int colWidth = width * (x2 - x1) / mVirtualScreenSizeX - 60;
	BGCPString line, word;
	bool addedMissionResults = false;
	int row = 0;
	int col = 0;
	size_t i = 0;
	while (i < text.getByteCount()) {
		char c = text.get<char>(i);
		if (c == 0x0d || c == 0x0a) {
			line.appendIfNotEmpty(' ');
			line += word;
			newTableView->setString(row, col, line);
			col = 0;
			++row;
			x = 0;
			line.clear();
			word.clear();
			++i;

			if (i < text.getByteCount()) {
				char d = text.get<uint8_t>(i);
				if (c != d && (d == 0x0d || d == 0x0a)) {
					++i;
				}
			}

		} else if (c < 32) {
			++i; // ignore all other control characters, including 0x0a
		} else if (c == '\\') {
			char d= text.get<uint8_t>(i+1);
			/*if (d == 'n') { // safe to ignore since it is followed by a normal 0x0d 0x0a anyway?
				line.appendIfNotEmpty(' ');
				line += word;
				strings.push_back(line);
				x = 0;
				line.clear();
				word.clear();
			}*/
			if (d == 't') {
				// Tab, treat as space?
				line.appendIfNotEmpty(' ');
				line += word;
				newTableView->setString(row, col, line);
				++col;
				line.clear();
				word.clear();
				x = 0;
			}
			if (d == 'q') {
				addMissionResults(row, col);
				addedMissionResults = true;
			}
			if (d == 'p') {
				BGCPString playerName;
				getVar("PlayerName", playerName);
				if (!word.empty()) {
					line.appendIfNotEmpty(' ');
					line += word;
				}
				line.appendIfNotEmpty(' ');
				line += playerName;
				word.clear();
			}
			if (d == 'm') {
				BGCPString playerTeam;
				getVar("PlayerTeam", playerTeam);
				if (!word.empty()) {
					line.appendIfNotEmpty(' ');
					line += word;
				}
				line.appendIfNotEmpty(' ');
				line += playerTeam;
				word.clear();
			}
			i += 2;
		} else if (c == ' ') {
			line.appendIfNotEmpty(' ');
			line += word;
			word.clear();
			++i;
			x += charWidth;
		} else {
			word.add(&c);
			++i;
			x += charWidth;
		}

		if (x > colWidth) {
			newTableView->setString(row, col, line);
			col = 0;
			++row;
			line.clear();
			x = 0;
		}
	}

	// Add the rest
	if (!line.empty() || !word.empty()) {
		SET_OP;
		line.appendIfNotEmpty(' ');
		line += word;
		newTableView->setString(row, col, line);
		col = 0;
		++row;
	}

	if (!addedMissionResults) {
		addMissionResults(row, col);
	}

	SET_OP;
	size_t rowCount, colCount;
	newTableView->getDimensions(MechShell_ID, rowCount, colCount);
	newTableView->setColWidth(5, colWidth, 120);
	newTableView->setRowHeights(rowCount, mFont->getCharHeight()+1);
	newTableView->setBackgroundColor(0, 0, 0, 0);
	RETURN newTableView;
}

void MechShell::loadTexts()
{
	if (mCurrentRoom == nullptr) {
		return;
	}

	SET_SCOPE;
	XMLTreeConstIterator treeIter(mCurrentRoom);

	for (size_t i = 0; i < treeIter.getChildCount(); ++i) {
		treeIter.goToChild(i);
		if (treeIter.nodeNameEquals("scrollableText")) {
			BGCPString BWDFN;
			treeIter.getAttributeValueByName("bwd", BWDFN);
			evaluate(BWDFN);
			int x1 = (int)treeIter.getIntAttributeValueByName("x1");
			int y1 = (int)treeIter.getIntAttributeValueByName("y1");
			int x2 = (int)treeIter.getIntAttributeValueByName("x2");
			int y2 = (int)treeIter.getIntAttributeValueByName("y2");

			// Normally read text from ORDR tag, but for Mercs contracts, the tag may differ
			BGCPString tagName = "ORDR";
			treeIter.getAttributeValueByName("tag", tagName);

			#ifdef MECHVM
			MemoryBlock mb;
			GLStringTableView* tableView = nullptr;
			if (!BWDFN.empty() && archive2 != nullptr)
			{
				archive2->leaveDir();
				archive2->enterSubDir("BWD");
				auto fileNo = archive2->getEntryNumberByName(BWDFN);
				auto fileSize = archive2->getFileSize(fileNo);

				if (fileSize > 60) {
					mb.resize(fileSize);
					archive2->getFileChunk(fileNo, 0, mb.getPtr<unsigned char>(), fileSize);
					tableView = buildScrollableTextFromBWD(&mb, x1, y1, x2, y2, tagName);
				}
			}
			#endif

			BGCPString splitByCommaText;
			if (tableView == nullptr && treeIter.getAttributeValueByName("splitByComma", splitByCommaText)) {
				// Construct table view from a comma-separated list
				SET_OP;
				BGCPString evaluatedText = splitByCommaText;
				evaluate(evaluatedText, false);
				tableView = buildScrollableTextFromCSL(evaluatedText, x1, y1, x2, y2);
			}

			if (tableView == nullptr) {
				// See if we can extract from an XML file instead
				BGCPString xmlFN, nodeName;
				if (treeIter.getAttributeValueByName("xmlFile", xmlFN)
				&&  treeIter.getAttributeValueByName("nodeName", nodeName))
				{
					evaluate(xmlFN, true);
					BGCPString levelDir;
					getVar("levelDir", levelDir);
					BGCPString fullXmlFN = levelDir;
					fullXmlFN.addPathSep(xmlFN);
					XMLTree levelXml;
					if (levelXml.loadFromFile(fullXmlFN.getAsPath())) {
						XMLTreeConstIterator levelIter(levelXml.getRoot());
						size_t i = 0;
						bool foundNode = false;
						while (i < levelIter.getChildCount() && !foundNode) {
							levelIter.goToChild(i);
							BGCPString thisNodeName;
							levelIter.getNodeName(thisNodeName);
							if (thisNodeName == nodeName) {
								levelIter.goToChild(0);
								BGCPString text = levelIter.getText();
								evaluate(text, false);
								tableView = buildScrollableText(text, x1, y1, x2, y2);
								foundNode = true;
								levelIter.goToParent();
							} else {
								++i;
							}
							levelIter.goToParent();
						}
					}
				}
			}

			// Text to use if the BWD file is not found (used for user-generated missions, like the random missions)
			BGCPString altText;
			if (tableView == nullptr && treeIter.getAttributeValueByName("altText", altText)) {
				// If we can't get text from the BWD, use altText instead
				SET_OP;
				BGCPString evaluatedAltText = altText;
				evaluate(evaluatedAltText, false);
				tableView = buildScrollableText(evaluatedAltText, x1, y1, x2, y2);
			}
		}
		treeIter.goToParent();
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Store variable in config

void MechShell::writeBackVar (const BGCPString& varName)
{
	SET_SCOPE;
	int varIndex = findVar (varName);
	if (varIndex >= 0) {
		Config* config = getConfig();
		if (config != NULL) {
			BGCPString gameName = retrieveStr("GameName");

			size_t cfgLine = 0;
			bool found = false;

			// Find start of game definition in MechVM.cfg
			while (!found && cfgLine < config->getKeyCount()) {
				BGCPString key = config->getKey(cfgLine);
				BGCPString value = config->getValue(cfgLine);
				if (key == "GameName"
					&&  value == gameName)
				{
					found = true;
				} else {
					++cfgLine;
				}
			}

			// Find variable in game definition
			if (found) {
				found = false;

				while (!found && cfgLine < config->getKeyCount()) {
					BGCPString key = config->getKey(cfgLine);
					BGCPString value = config->getValue(cfgLine);

					BGCPString newVal = varName;
					newVal += "=";
					newVal += vars[varIndex+1];
					if (key == "SetShellVar"
						&&  value.startsWith(varName))
					{
						found = true;
						config->setKeyValuePair(cfgLine, "SetShellVar", newVal);
					} else {
						if (!key.empty()) {
							++cfgLine;
						} else {
							// Empty key marks start of next game.
							// If we get here, we need to add the key instead of updating it.
							config->insertKeyValuePair(cfgLine, "SetShellVar", newVal);
						}
					}
				}
			}
		}
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Get complete list of installed mechs (used for instant action)

void MechShell::getMechList()
{
	SET_SCOPE;
	BGCPString mechList;
	BGCPString mechDirPath(getDataDir());
	mechDirPath.addPathSep();
	mechDirPath += MECH_SUB_DIR;
	
	BGCPString dirMask = mechDirPath;
	dirMask.addPathSep("*");
	FileList fileList;
	if (fileList.start(dirMask.getAsPath())) {
		do {
			SET_OP;
			BGCPString fn = BGCPString(fileList.getFileName(), CP_SYSTEM);
			BGCPString mvmkMask = mechDirPath;
			mvmkMask.addPathSep(fn);
			mvmkMask.addPathSep("*.mvmk");
			FileList fileList2;
			if (fileList2.start(mvmkMask.getAsPath())) {
				if (!mechList.empty()) {
					mechList += ",";
				}
				mechList += fn;
			}
		} while (++fileList);
	}

	setVar("mechlist", mechList);
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Remove an element from a comma-separated list

void MechShell::removeFromList(const BGCPString& cmd)
{
	SET_SCOPE;
	auto pos1 = cmd.forward(0, 7);
	auto pos2 = cmd.scan(",", pos1, cmd.getByteCount());
	auto pos3 = cmd.scan(")", pos2, cmd.getByteCount());

	if (pos1 < pos2 && pos2 < pos3 && pos3 < cmd.getByteCount()) {
		BGCPString varName1 = cmd.copyPart(pos1, pos2-pos1);
		pos2 += cmd.getCharSize(pos2);
		BGCPString varName2 = cmd.copyPart(pos2, pos3-pos2);
		evaluate(varName1, true);
		size_t elementToRemove = (size_t)varName1.toInt();

		BGCPString listVar;
		if (!getVar(varName2, listVar)) {
			RETURN;
		}
		auto entries = listVar.split(",", false);

		if (entries.size() > 1) {
			if (elementToRemove < entries.size()) {
				BGCPString newListVar;
				for (size_t i = 0; i < elementToRemove; ++i) {
					if (newListVar.empty()) {
						newListVar = entries[i];
					} else {
						newListVar += BGCPString(",") + entries[i];
					}
				}
				for (size_t i = elementToRemove+1; i < entries.size(); ++i) {
					if (newListVar.empty()) {
						newListVar = entries[i];
					} else {
						newListVar += BGCPString(",") + entries[i];
					}
				}
				setVar(varName2, newListVar);
			} // else: no need to change anything ...
		} else {
			if (elementToRemove == 0) {
				setVar(varName2, "");
			}
		}
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Mercs: get a list of Mech that are available for purchase in a given year ...

struct MechOnSaleData {
	int tons, startYear, endYear;
	BGCPString chassis, config;

	bool operator<(const MechOnSaleData& other) 
	{
		return tons < other.tons;
	}
};

void MechShell::getMechsForSale()
{
	SET_SCOPE;
	BGCPString dateStr, chassisList, configList;
	getVar("date", dateStr);
	auto date = MWDate::fromStr(dateStr);

	// Read data file
	SET_OP;
	BGCPString MechSaleSheet(getDataDir());
	MechSaleSheet.addPathSep("Mech Sale Sheet.tsv");

	SET_OP;
	MemoryBlock mb;
	if (!mb.loadFromFile(MechSaleSheet.getAsPath())) {
		RETURN;
	}

	SET_OP;
	BGCPString linesStr(mb.getPtr<char>(), mb.getSize(), CP_ASCII);
	char endl[] = {0x0d, 0x0a, 0}; // just passing one character to split() will mess up the strings, preventing the OS from finding files etc.
	auto lines = linesStr.split(endl, false);

	// Parse data file
	SET_OP;
	std::vector<MechOnSaleData> mechData;
	for (size_t i = 1; i < lines.size(); ++i) {
		BGCPString& line = lines[i];
		auto cells = line.split("\t", false);

		if (cells.size() > 4) {
			SET_OP;
			MechOnSaleData data;
			data.tons      = (int)(cells[0].toInt());
			data.startYear = (int)(cells[1].toInt());
			data.endYear   = (int)(cells[2].toInt());
			data.chassis   = cells[3];
			data.config    = cells[4];

			SET_OP;
			BGCPString configFN(getDataDir());
			configFN.addPathSep(MECH_SUB_DIR);
			configFN.addPathSep(data.chassis);
			configFN.addPathSep(data.config);
			configFN += ".mek";

			// Reject data early ...
			SET_OP;
			if (data.startYear <= date.year
			&&  date.year <= data.endYear
			&&  fileExists(configFN.getAsPath()))
			{
				mechData.push_back(data);
			}
		}
	}

	// Randomly choose 10 Mechs from above dataset
	SET_OP;
	randomize();
	std::vector<MechOnSaleData> chosenMechData;
	while (mechData.size() > 0 && chosenMechData.size() < 10) {
		int j = irand<int>(0, mechData.size());
		auto iter = mechData.begin() + j;
		chosenMechData.push_back(*iter);
		mechData.erase(iter);
	}

	// Build the strings using that dataset
	SET_OP;
	if (chosenMechData.size() > 0) {
		std::sort(chosenMechData.begin(), chosenMechData.end());
		BGCPString chassisOnSale = chosenMechData[0].chassis;
		BGCPString configsOnSale = chosenMechData[0].config;
		for (size_t i = 1; i < chosenMechData.size(); ++i) {
			chassisOnSale += BGCPString(",") + chosenMechData[i].chassis;
			configsOnSale += BGCPString(",") + chosenMechData[i].config;
		}
		setVar("MechsOnSaleChassis", chassisOnSale);
		setVar("MechsOnSale", configsOnSale);
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Get a summary on a vehicle to display, e.g. for the MechLab

void MechShell::getVehicleSummary(const BGCPString& cmd)
{
	SET_SCOPE;
	size_t pos2 = cmd.forward(0, 18);
	size_t pos5 = cmd.scan(')', pos2, cmd.getByteCount());
	size_t pos3 = cmd.scan(',', pos2, pos5);
	size_t temp = pos3 + cmd.getCharSize(pos3);
	size_t pos4 = cmd.scan(',', temp, pos5);

	if (pos2 >= pos3 || pos3 >= pos4 || pos4 >= pos5 || pos5 > cmd.getByteCount()) {
		RETURN;
	}

	BGCPString mvmkPath   = cmd.copyPart(pos2, pos3-pos2);
	pos3 += cmd.getCharSize(pos3);
	BGCPString configPath = cmd.copyPart(pos3, pos4-pos3);
	pos4 += cmd.getCharSize(pos4);
	BGCPString targetVar = cmd.copyPart(pos4, pos5-pos4);

	evaluate(mvmkPath, false);
	evaluate(configPath, false);

	Vehicle* vehicle = loadVehicle2(mvmkPath, configPath, "", "");
	if (vehicle == nullptr) {
		RETURN;
	}

	BGCPString summary;
	vehicle->getSummary(summary);
	setVar(targetVar, summary);
	RETURN;
}

// Sell all Mechs, adding money as calculated by calcVehiclePrice
void MechShell::sellOut()
{
	BGCPString CBillsStr, configsStr, chassisStr;
	getVar("cbills", CBillsStr);
	getVar("configs", configsStr);
	getVar("mechlist", chassisStr);
	auto cbills = CBillsStr.toInt();
	auto configs = configsStr.split(",", false);
	auto chassis = chassisStr.split(",", false);

	for (size_t i = 0; i < std::min(configs.size(), chassis.size()); ++i) {
		BGCPString& chassisI = chassis[i];
		BGCPString configI = configs[i] + BGCPString(".mek");
		auto mechPrice = calcVehiclePrice(chassisI, configI);
		cbills += mechPrice;
	}

	setVar("cbills", BGCPString::fromInt(cbills));
}

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

void MechShell::execute(const BGCPString& cmdList)
{
	SET_SCOPE;
	auto cmds = cmdList.split(";", false);

	for (size_t i = 0; i < cmds.size(); ++i) {
		BGCPString& cmd = cmds[i];
		if (cmd.startsWith("FetchMEKs")) {
			fetchMEKs(cmd);
		} else if (cmd.startsWith("runSim")) {
			runSim(cmd);
		} else if (cmd.startsWith("MechLab")) {
			showMechLab(cmd);
		} else if (cmd == "exit") {
			auto* mechVM = getMechVm();
			mechVM->deleteLater(this);
			if (mReturnScreen != nullptr) {
				mechVM->setScreen(mReturnScreen);
			} else {
				mechVM->restore();
			}
		} else if (cmd.startsWith("FetchMEKComment")) {
			fetchMEKComment(cmd);
		} else if (cmd == "getMechList") {
			getMechList();
		} else if (cmd.startsWith("remove(")) {
			removeFromList(cmd);
		} else if (cmd.startsWith("append(")) {
			appendToList(cmd);
		} else if (cmd.startsWith("getMechsForSale")) {
			getMechsForSale();
		} else if (cmd.startsWith("getVehicleSummary")) {
			getVehicleSummary(cmd);
		} else if (cmd.startsWith("sellOut")) {
			sellOut();
		} else if (cmd == "updateMercsContracts") {
			updateMercsContracts();
		} else if (cmd == "getMercsContractMissions") {
			getMercsContractMissions();
		} else if (cmd.startsWith("LoadFontFromArchive")) {
			BGCPString fileNo = cmd.copyPart(20, 6);
			int commaPos = cmd.findChar(',');
			int divider = 8;
			if (commaPos > 0) {
				fileNo = cmd.copyPart(20, commaPos-20);
				BGCPString dividerStr = cmd.copyPart(commaPos+1,2);
				divider = (int)dividerStr.toInt();
			}
			loadFontFromArchive(fileNo, divider);
		}
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Return XML tree iterator for a room, given its name

XMLTreeNode* MechShell::findRoom(const BGCPString& roomName)
{
	// Find xml tree node for the new room
	SET_SCOPE;
	XMLTreeNode* treeRoot = tree.getRoot();
	if (treeRoot == nullptr) {
		RETURN nullptr;
	}

	SET_OP;
	size_t i = 0;
	while (i < treeRoot->getChildCount())
	{
		SET_OP;
		auto* child = treeRoot->getChild(i);
		XMLTreeConstIterator treeIter(treeRoot->getChild(i));
		if (treeIter.nodeNameEquals("room")) {
			BGCPString thisRoomName;
			treeIter.getAttributeValueByName("name", thisRoomName);
			if (roomName == thisRoomName) {
				RETURN child;
			} else {
				i++;
			}
		}
	}

	RETURN nullptr;
}

////////////////////////////////////////////////////////////////////////////////
// Enter next room

bool MechShell::enterRoom(const BGCPString& newRoom)
{
	if (newRoom.empty()) {
		return false;
	}

	SET_SCOPE;
	clearImages();
	removeAll(); // clear all child widgets of MechShell, including the table views
	//mTableStrings.clear();
	
	size_t i = 0;
	XMLTreeNode* nextRoomNode = findRoom(newRoom);

	if (nextRoomNode == nullptr) {
		COUT << "Room not found: ";
		newRoom.printLn();
		RETURN false;
	}

	SET_OP;
	mCurrentRoom = nextRoomNode;
	XMLTreeConstIterator treeIter(mCurrentRoom);
	setVar("room", newRoom);
	BGCPString backgroundInArchiveFN, animFN;
	if (treeIter.getAttributeValueByName("background", backgroundInArchiveFN)) {
		MechShellRegion img;
		img.img = new Texture();
		if (!loadImageFromArchive(backgroundInArchiveFN.getAsPath(), *(img.img))) {
			BGCPString backgroundFN;
			getVar("levelDir", backgroundFN);
			backgroundFN.addPathSep(backgroundInArchiveFN);
			if (!img.img->loadFromFile(backgroundFN.getAsPath())) {
				img.img->resize(640, 480, GL_RGB);
				img.img->fillRectI(0, 0, 640, 480, 0, 0, 0);
			}
		}
		img.x1 = 0;
		img.y1 = 0;
		img.x2 = img.img->getWidth();
		img.y2 = img.img->getHeight();
		mVirtualScreenSizeX = img.x2;
		mVirtualScreenSizeY = img.y2;
		mRegions.push_back(img);
	}

	#ifdef MECHVM
	SET_OP;
	if (treeIter.getAttributeValueByName("anim", animFN)) {
		evaluate(animFN, false);
		mSmkViewer = new SmackerView(this, getLeft(), getTop(), getWidth(), getHeight());
		BGCPString smkPath;
		if (getVar("GamePath", smkPath)) {
			smkPath.addPathSep();
			smkPath += animFN;
			smkPath.convertToCP(CP_UTF8);
			mSmkViewer->open(smkPath.getPtr<char>());
			add(mSmkViewer);
		}
		treeIter.getAttributeValueByName("next_room", mNextRoom);
	}
	#endif

	// Process variable assignments
	SET_OP;
	for (size_t i = 0; i < treeIter.getAttributeCount(); ++i) {
		BGCPString attrName, attrValue;
		if (treeIter.getAttributeName(i, attrName)
		&&  treeIter.getAttributeValueByIndex(i, attrValue)) 
		{
			if (attrName.equalsNoCase("setvar")) {
				processAssignment(attrValue, true);
			} else if (attrName.equalsNoCase("setstr")) {
				processAssignment(attrValue, false);
			} else if (attrName.equalsNoCase("exec")) {
				BGCPString cmd;
				treeIter.getAttributeValueByIndex(i, cmd);
				execute(cmd);
			}
		}
	}

	SET_OP;
	loadTexts();
	COUT << "Entered room ";
	getRoom().printLn();

	// Write back variables
	SET_OP;
	BGCPString persistentVars = retrieveStr("persistent");
	persistentVars.convertToCP(CP_ASCII);
	if (!persistentVars.empty()) {
		auto words = persistentVars.split(",", true);
		for (size_t i = 0; i < words.size(); ++i) {
			writeBackVar(words[i]);
		}
	}

	// Prepare vector of regions to display
	SET_OP;
	for (size_t i = 0; i < mCurrentRoom->getChildCount(); i++) {
		XMLTreeNode* child = mCurrentRoom->getChild(i);
		XMLTreeConstIterator treeIter(child);
		if (treeIter.nodeNameEquals("reg")
		||  treeIter.nodeNameEquals("textBtn"))
		{
			//mCurrentRoomRegions.emplace_back(child);
			MechShellRegion image;
			image.x1 = (int)treeIter.getIntAttributeValueByName("x1");
			image.y1 = (int)treeIter.getIntAttributeValueByName("y1");
			image.x2 = (int)treeIter.getIntAttributeValueByName("x2");
			image.y2 = (int)treeIter.getIntAttributeValueByName("y2");
			treeIter.getAttributeValueByName("hint", image.hint);
			treeIter.getAttributeValueByName("goto", image.go_to);
			treeIter.getAttributeValueByName("cnd", image.condition);
			treeIter.getAttributeValueByName("setstr", image.setstr);
			treeIter.getAttributeValueByName("setvar", image.setvar);
			treeIter.getAttributeValueByName("range", image.range);
			treeIter.getAttributeValueByName("exec", image.exec);
			treeIter.getAttributeValueByName("text", image.text);

			image.font = mFont;
			BGCPString fontNo;
			if (treeIter.getAttributeValueByName("font", fontNo)) {
				size_t no = (size_t) fontNo.toInt();
				if (no < mFonts.size()) {
					image.font = mFonts[no];
				}
			}

			BGCPString graphic;
			treeIter.getAttributeValueByName("graphic", graphic);

			if (!graphic.empty()) {
				MemoryBlock mb;
				#ifdef MECHVM
				if (graphic.startsWith("1|")) {
					graphic.convertToCP(CP_ASCII);
					size_t slashPos = graphic.scan("/", 2, graphic.getByteCount());
					if (slashPos < graphic.getByteCount()) {
						// Read from archive (NET.PRJ for NetMech uses paths)
						BGCPString dirName = graphic.copyPart(2, slashPos-2);
						BGCPString filename = graphic.copyPart(slashPos+1, graphic.getByteCount());
						archive->leaveDir();
						archive->enterSubDir(dirName.getPtr<char>());
						archive->getFile(filename.getPtr<char>(), mb);
					} else {
						// Read from archive (rimage.zbd for MW3 does not have subdirectories)
						BGCPString filename = graphic.copyPart(2, graphic.getByteCount());
						archive->getFile(filename.getPtr<char>(), mb);
					}
				} else {
				#endif
					// Read from file relative to game directory, usually a SHP file
					BGCPString fullFN;
					getVar("GamePath", fullFN);
					fullFN.addPathSep(graphic);
					mb.loadFromFile(fullFN.getAsPath());
				}

				image.img = new Texture();

				#ifdef MECHVM
				if (graphic.endsWith(".SHP")) {
					MechWarriorIIPRJ::loadSHP(mb, 0, mRoomPal, *image.img);
				} else if (graphic.endsWith(".PCX")) {
					if (mb.get<uint8_t>(65) > 1) {
						image.img->loadFromFile(mb.getPtr<char>(), mb.getSize());
					} else {
						MechWarriorIIPRJ::loadPCX(mb.getPtr<unsigned char>(), mb.getSize(), *image.img, mRoomPal);
					}
				} else {
					// MechWarrior 3 555?
					Decode_MW3_555(mb.getPtr<unsigned char>(), mb.getSize(), *image.img, true);
				}
				#endif

				image.x2 = image.x1 + image.img->getWidth()-1;
				image.y2 = image.y1 + image.img->getHeight()-1;
			}

			mRegions.emplace_back(image);
		} else if (treeIter.nodeNameEquals("textEdit")) {
			MechShellTextEdit textEdit;
			textEdit.x1 = (int)treeIter.getIntAttributeValueByName("x1");
			textEdit.y1 = (int)treeIter.getIntAttributeValueByName("y1");
			textEdit.x2 = (int)treeIter.getIntAttributeValueByName("x2");
			textEdit.y2 = (int)treeIter.getIntAttributeValueByName("y2");
			treeIter.getAttributeValueByName("var", textEdit.var);
			mTextEdits.emplace_back(textEdit);
		}
	}

	RETURN true;
}

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

void MechShell::receiveMessage (VirtualClass* /*sender*/, int /*MessageType*/, VirtualClass* data)
{
}

////////////////////////////////////////////////////////////////////////////////
// Update title with model graph name and framerate

void MechShell::updateTitle ()
{
}

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

void MechShell::render(int x, int y, int w, int h)
{
	SET_SCOPE;
	Config* config = getConfig();
	Color& backgroundColor = config->getViewerBackgroundColor();
	glClearColor(backgroundColor.r, backgroundColor.g, backgroundColor.b, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glViewport (0, 0, w, h);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glColor3d(1.0, 1.0, 1.0);

	// Reset matrices in case we return from the sim
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glOrtho(0, mVirtualScreenSizeX, mVirtualScreenSizeY, 0, 0, 1);

	// Render regions
	SET_OP;
	for (size_t i = 0; i < mRegions.size(); ++i) {
		MechShellRegion& region = mRegions[i];

		if (checkCondition(region.condition)) {
			// Region represented by a graphic?
			if (region.img != nullptr) {
				SET_OP;
				region.img->use();

				// Make sure we use the mid of the pixels for the uv coords
				double u1 = 0.5 / region.img->getWidth();
				double v1 = 0.5 / region.img->getHeight();
				double u2 = (((double)region.img->getWidth()) -1.5) / region.img->getWidth();
				double v2 = (((double)region.img->getHeight())-1.5) / region.img->getHeight();

				glEnable(GL_TEXTURE_2D);
				glEnable(GL_BLEND);
				glAlphaFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
				glBegin(GL_POLYGON);
					region.img->glTexCoord(u1, v2);
					glVertex2d (region.x1, region.y1);
					region.img->glTexCoord(u2, v2);
					glVertex2d (region.x2, region.y1);
					region.img->glTexCoord(u2, v1);
					glVertex2d (region.x2, region.y2);
					region.img->glTexCoord(u1, v1);
					glVertex2d (region.x1, region.y2);
				glEnd();
			}

			// Render text?
			if (!region.text.empty()) {
				SET_OP;
				BGCPString text = region.text;
				evaluate (text, false);
				int textWidth = mFont->textWidth(text);
				//int x1 = region.x1+region.x2)/2-textWidth/2;
				//int y1 = region.y1+region.y2)/2-mFont->getCharHeight()/2;
				region.font->renderText(text, region.x1, region.y1, region.x2);
			}

			// Display hint?
			if (!region.hint.empty()
			&&  mLastMouseX >= region.x1
			&&  mLastMouseY >= region.y1
			&&  mLastMouseX <  region.x2
			&&  mLastMouseY <  region.y2)
			{
				SET_OP;
				int hintWidth = mFont->textWidth(region.hint);
				int x1 = (region.x1+region.x2)/2-hintWidth/2;
				region.font->renderText(region.hint, region.x1, (region.y1+region.y2)/2, region.x1+hintWidth);
			}
		}
	}

	// Render text edit text
	SET_OP;
	for (size_t i = 0; i < mTextEdits.size(); ++i) {
		MechShellTextEdit& textEdit = mTextEdits[i];
		BGCPString val;
		getVar(textEdit.var, val);
		val.convertToCP(CP_ASCII);
		if (!val.empty()) {
			SET_OP;
			int textWidth = mFont->textWidth(val);
			int x1 = (textEdit.x1+textEdit.x2)/2-textWidth/2;
			int y1 = (textEdit.y1+textEdit.y2)/2-mFont->getCharHeight()/2;
			mFont->renderText(val, x1, y1, textEdit.x2);
		}
	}

	glDisable(GL_BLEND);

	#ifdef MECHVM
	if (mSmkViewer != NULL && mSmkViewer->done()) {
		SET_OP;
		endSmk();
	}
	#endif

	SET_OP;
	GLWindowContainer::render(x, y, w, h);
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// End Smacker playback

void MechShell::endSmk() 
{
	#ifdef MECHVM
	SET_SCOPE;
	if (mSmkViewer != NULL) {
		safeDelete(mSmkViewer);

		// Retiring first plays the retire video (rich or poor), followed by the intro, so two videos are played in a row.
		BGCPString nextRoomCopy = mNextRoom;

		enterRoom(mNextRoom);
		//mNextRoom.clear();
	}
	RETURN;
	#endif
}

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

void MechShell::keypressed (BGKey key, bool shiftHeld, bool controlHeld, bool down, bool repeat)
{
	// After ending a mission, QTRL-Q is still held, so we may get an additional
	// keystroke here. That keystroke should not prevent after-mission video playback.
	SET_SCOPE;
	#ifdef MECHVM
	if (mSmkViewer != NULL) {
		if (key == SDLK_SPACE || key == SDLK_RETURN) {
			endSmk();
		}
	}
	#endif

	if (key == 'q' && controlHeld && down && !repeat) {
		auto* mechVM = getMechVm();
		mechVM->deleteLater(this);
		if (mReturnScreen != nullptr) {
			mechVM->setScreen(mReturnScreen);
		} else {
			mechVM->restore();
		}
		RETURN;
	}

	if (mTextEdits.size() > 0 && down) {
		MechShellTextEdit& textEdit = mTextEdits[0];
		BGCPString val;
		getVar(textEdit.var, val);
		val.convertToCP(CP_ASCII);

		if ((key >= 'a' && key <= 'z')
		||  (key >= '0' && key <= '9')
		||  (key == ' ')
		||  (key == '_')
		||  (key == '-')
		||  (key == '.')
		||  (key == ',')
		||  (key == '!'))
		{
			if (shiftHeld)
				key = key - 'a' + 'A';
			val += BGCPString(key);
			setVar(textEdit.var, val);
		} else if (key == SDLK_BACKSPACE && !val.empty()) {
			val = val.copyPart(0, val.getByteCount()-1);
			setVar(textEdit.var, val);
		}
	}
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Button state changed over a window: returns child that was clicked

GLWindow* MechShell::buttonStateChanged (int button, bool down, int x, int y)
{
	if (mCurrentRoom == nullptr) {
		return nullptr;
	}

	SET_SCOPE;
	XMLTreeConstIterator treeIter(mCurrentRoom);
	int w = getWidth();
	int h = getHeight();

	if (button == 1 && !down && w > 0 && h > 0) {
		SET_OP;
		int tx = mVirtualScreenSizeX * x / getWidth();
		int ty = mVirtualScreenSizeY * y / getHeight();
		printf("x1=\"%i\" y1=\"%i\"\n", tx, ty);

		for (size_t i = 0; i < mRegions.size(); i++) {
			SET_OP;
			const MechShellRegion& image = mRegions[i];

			SET_OP;
			if (image.x1 <= tx && image.y1 <= ty && tx <= image.x2 && ty <= image.y2 && checkCondition(image.condition)) {
				SET_OP;
				if (!image.setvar.empty()) {
					SET_OP;
					processAssignment (image.setvar, true);
				}
				if (!image.setstr.empty()) {
					SET_OP;
					processAssignment (image.setstr, false);
				}
				if (!image.exec.empty()) {
					SET_OP;
					execute(image.exec);
				}
				if (!image.go_to.empty()) {
					SET_OP;
					BGCPString nextRoom = image.go_to;
					evaluate(nextRoom, true);
					enterRoom(nextRoom);
					RETURN this; // Prevent other rooms from being visited ...
				}
			}
			treeIter.goToParent();
		}
	}

	RETURN GLWindowContainer::buttonStateChanged(button, down, x, y);
}

void MechShell::mouseMoved (int x, int y)
{
	SET_SCOPE;
	mLastMouseX = mVirtualScreenSizeX * x / getWidth();
	mLastMouseY = mVirtualScreenSizeY * y / getHeight();
	RETURN;
}

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

void MechShell::getDosBoxGameDir (BGCPString& gameDir)
{
	SET_SCOPE;
	int index = findVar ("GamePathInDosBox");
	if (index >= 0) {
		gameDir = BGCPString(getDataDir());
		gameDir += "games";
		gameDir += vars[index+1];
	}
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Variables

bool MechShell::processAssignment (const BGCPString& assignmentList, bool doMath)
{
	SET_SCOPE;
	if (assignmentList.empty()) {
		RETURN true;
	}

	std::vector<BGCPString> assignments = assignmentList.split(";", false);
	bool allOkay = true;

	for (size_t i = 0; i < assignments.size(); ++i) {
		SET_OP;
		const BGCPString currAssignment = assignments[i];
		// Parse assignment
		size_t len = currAssignment.getByteCount();
		size_t pos = currAssignment.scan('=', 0, len);

		if (pos < len) {
			SET_OP;
			size_t clen = currAssignment.getCharSize(pos);
			BGCPString varName(currAssignment.getPtr<char>(), pos, currAssignment.getCP());
			BGCPString val(currAssignment.getPtr<char>()+pos+clen, len-pos-clen, currAssignment.getCP());
			evaluate(val, doMath);
			setVar(varName, val);

			#ifdef _DEBUG
			varName.print();
			COUT << "<-";
			val.printLn();
			#endif
		} else {
			SET_OP;
			#ifdef _DEBUG
			currAssignment.printLn();
			#endif
			execute(currAssignment);
		}
	}

	RETURN allOkay;
}

// Set variable, but only if it does not exist yet
// (should be used with persistent variables, these have been loaded from MechVM.cfg)
void MechShell::initVar (BGCPString& assignment, bool doEvaluate)
{
	// Parse assignment
	SET_SCOPE;
	size_t len = assignment.getByteCount();
	size_t pos = assignment.scan('=', 0, len);

	if (pos < len) {
		size_t clen = assignment.getCharSize(pos);
		BGCPString varName(assignment.getPtr<char>(), pos, assignment.getCP());
		BGCPString value;
		if (!getVar(varName, value)) {
			value = assignment.copyPart(pos+clen, len-pos-clen);
			if (doEvaluate) {
				evaluate(value, true);
			}
			setVar(varName, value);
		}
	}
	RETURN;
}

// Assuming that pos points to the character past a brace, find the matching brace
// Returns the position of the matching brace as a byte offset or the number of bytes in the string on failure
size_t findMatchingBrace(const BGCPString& str, size_t pos)
{
	SET_SCOPE;
	int braces = 1;

	while (pos < str.getByteCount()) {
		auto clen = str.getCharSize(pos);
		if (str.charAtEquals(pos,'(')) {
			++braces;
		} else if (str.charAtEquals(pos, ')')) {
			--braces;
			if (braces == 0) {
				// This is what we were looking for
				RETURN pos;
			}
		}
		pos += clen;
	}

	RETURN str.getByteCount();
}

bool MechShell::evaluate (BGCPString& str, bool opsAllowed) const
{
	// Substitute variables
	SET_SCOPE;
	size_t pos = 0, pos2 = 0;
	while (pos2 < str.getByteCount()) {
		pos = str.scan('%', 0, str.getByteCount());
		size_t clen = str.getCharSize(pos);
		pos2 = str.scan('%', pos+clen, str.getByteCount());
		if (pos < str.getByteCount() && pos2 < str.getByteCount()) {
			BGCPString newStr = str.copyPart(0, pos);
			BGCPString var = str.copyPart(pos+clen, pos2-pos-clen);
			BGCPString temp;
			getVar(var, temp);
			newStr += temp;
			newStr += str.copyPart(pos2+clen, str.getByteCount()-pos2-clen);
			str = newStr;
		}
	}

	// if_less function
	SET_OP;
	pos = str.scan("if_less(", 0, str.getByteCount());
	while (pos < str.getByteCount()) {
		size_t pos2 = str.forward(pos, 7);
		size_t pos3 = findMatchingBrace(str, pos2);
		pos2 += str.getCharSize(pos2);
		size_t argsEnd = pos3 - str.getCharSize(pos2);
		BGCPString argStrings = str.copyPart(pos2, argsEnd-pos2);
		auto args = argStrings.split(",", false);
		bool is_less = false;
		if (args.size() == 4) {
			evaluate(args[0], true);
			evaluate(args[1], true);
			evaluate(args[2], false); // false since we may have to process text as result ... calculation has to happen somewhere else then ...
			evaluate(args[3], false); // false since we may have to process text as result ... calculation has to happen somewhere else then ...
			double v1 = args[0].toDouble();
			double v2 = args[1].toDouble();
			BGCPString funcResult = args[2];
			if (v1 >= v2) {
				funcResult = args[3];
			}
			BGCPString before = str.copyPart(0, pos);
			pos3 += str.getCharSize(pos3);
			BGCPString after =  str.copyPart(pos3, str.getByteCount()-pos3);
			str = before + funcResult;
			pos = str.getByteCount();
			str += after;
			pos = str.scan("if_less(", pos, str.getByteCount());
		} else {
			pos = pos3 + str.getCharSize(pos3);
		}
	}

	// Question mark ("?") operator
	if (str.startsWith("?")) {
		auto pos = str.forward(0,1);
		str = str.copyPart(pos, str.getByteCount());
		auto args = str.split(":", false);
		if (args.size() == 3) {
			if (checkCondition(args[0])) {
				str = args[1];
			} else {
				str = args[2];
			}
		}
	}

	// count function
	SET_OP;
	bool found = true;
	do {
		size_t pos = str.scan("count(", 0, str.getByteCount());
		size_t pos2 = str.forward(pos, 6);
		size_t pos3 = str.scan(')', pos2, str.getByteCount());
		size_t clen = str.getCharSize(pos3);
		if (pos3 < str.getByteCount() && pos2 < str.getByteCount()) {
			BGCPString other = str.copyPart(0, pos);
			BGCPString var = str.copyPart(pos2, pos3-pos2);
			BGCPString temp = var;
			getVar(temp, var);
			auto words = var.split(",", true);
			auto count = words.size();
			other += BGCPString::fromInt(count);
			other += str.copyPart(pos3+clen, str.getByteCount()-pos3-clen);
			str = other;
		} else {
			found = false;
		}
	} while (found);

	// Calculate price of BattleMechs and vehicles
	SET_OP;
	found = true;
	do {
		auto len = str.getByteCount();
		size_t pos = str.scan("price(", 0, len);
		size_t pos2 = str.forward(pos, 6);
		size_t pos4 = str.scan(')', pos2, len);
		if (pos < len) {
			size_t pos3 = str.scan(',', pos2, pos4);
			size_t clen = str.getCharSize(pos3);
			if (pos2 < pos3 && pos3 < pos4 && pos4 < str.getByteCount()) {
				BGCPString before   = str.copyPart(0, pos);
				BGCPString mvmkfn   = str.copyPart(pos2, pos3-pos2);
				pos3 += str.getCharSize(pos3);
				BGCPString configfn = str.copyPart(pos3, pos4-pos3);
				pos4 += str.getCharSize(pos4);
				BGCPString after    = str.copyPart(pos4, str.getByteCount());
				int price = calcVehiclePrice(mvmkfn, configfn);
				str = before + BGCPString::fromInt(price) + after;
			} else {
				// Corrupt query?
				pos4 = str.forward(pos4, 1);
				str = BGCPString("0") + str.copyPart(pos4, len-pos4);
				found = false;
			}
		} else {
			found = false;
		}
	} while (found);

	// Check for select statement
	SET_OP;
	str.convertToCP(CP_UTF16_LE);
	pos = str.scan("select(", 0, str.getByteCount());
	if (pos < str.getByteCount()) {
		size_t clen = str.getCharSize(pos);
		size_t pos2a = str.advance(pos, 7);
		size_t pos2b = findMatchingBrace(str, pos2a);
		if (pos2b < str.getByteCount()) {
			BGCPString before = str.copyPart(0, pos);
			auto clenpos2 = str.getCharSize(pos2b);
			BGCPString after = str.copyPart(pos2b+clenpos2, str.getByteCount()-pos2b-clenpos2);
			BGCPString selectStatement = str.copyPart(pos+14, pos2b-pos2a);
			BGCPString selectResult;
			selectStr(selectStatement, selectResult);
			str = before + selectResult + after;
		}
	}

	// Check for random function
	SET_OP;
	str.convertToCP(CP_UTF16_LE);
	pos = str.scan("random(", 0, str.getByteCount());
	if (pos < str.getByteCount()) {
		size_t clen = str.getCharSize(pos);
		size_t pos2a = str.advance(pos, 7);
		size_t pos2b = findMatchingBrace(str, pos2a);
		if (pos2b < str.getByteCount()) {
			BGCPString before = str.copyPart(0, pos);
			auto clenpos2 = str.getCharSize(pos2b);
			BGCPString after = str.copyPart(pos2b+clenpos2, str.getByteCount()-pos2b-clenpos2);
			auto strings = str.copyPart(pos2a,pos2b-1);
			auto strList = strings.split(",",false);
			if (strList.size() > 0) {
				int opt = irand<int>(0, strList.size());
				str = before + strList[opt] + after;
			} else {
				str = before + after;
			}
		}
	}

	// Add variables?
	SET_OP;
	pos = str.scan('+', 0, str.getByteCount());
	if (opsAllowed
	&&  pos < str.getByteCount()
	&&  str.getByteCount() > 1) 
	{
		size_t clen = str.getCharSize(pos);
		BGCPString aStr(str.getPtr<char>(), pos, str.getCP());
		BGCPString bStr(str.getPtr<char>(pos+clen), str.getByteCount(), str.getCP());
		int64_t a = aStr.toInt();
		int64_t b = bStr.toInt();
		str = BGCPString::fromInt(a+b);
	}

	// Subtract, skip first char ...
	SET_OP;
	size_t clen = str.getCharSize(0);
	pos = str.scan('-', clen, str.getByteCount());
	if (opsAllowed
		&&  pos < str.getByteCount()
		&&  str.getByteCount() > 1) 
	{
		size_t clen = str.getCharSize(pos);
		BGCPString aStr = BGCPString(str.getPtr<char>(), pos, str.getCP());
		BGCPString bStr = BGCPString(str.getPtr<char>() +pos+clen, str.getCP());
		int64_t a = aStr.toInt();
		int64_t b = bStr.toInt();
		str = BGCPString::fromInt(a-b);
	}

	// length function
	SET_OP;
	pos = str.scan("length(", 0, str.getByteCount());
	if (pos < str.getByteCount()) {
		size_t pos2 = findMatchingBrace(str, pos);
		BGCPString var = str.copyPart(pos+14, pos2);
		auto len = var.getCharCount();
		str = str.copyPart(0, pos) + BGCPString::fromInt(len) + str.copyPart(pos2+2, str.getByteCount()-pos2);
	}

	//todo: multiplication, brackets ...
	RETURN true;
}

// Select function (access single array element)
// arg 1: index to select, arg2: the array from which an element is selected
bool MechShell::selectStr(const BGCPString& select, BGCPString& result) const
{
	SET_SCOPE;
	if (select.empty()) {
		return false;
	}

	size_t pos1 = select.scan(',', 0, select.getByteCount());
	if (pos1 < select.getByteCount()) {
		BGCPString selector(select.getPtr<char>(), pos1, select.getCP());
		evaluate(selector,true);
		int num = (int) selector.toInt();
		size_t clen = select.getCharSize(pos1);
		BGCPString selectionSet = select.copyPart(pos1 + clen, select.getByteCount()-pos1-clen);
		evaluate(selectionSet, false); // with ops allowed, hyphens in strings would be misunderstood as minus
		auto words = selectionSet.split(",", true);
		auto wc = words.size();

		if (wc > 0) {
			while (num < 0) {
				num += wc;
			}
			num = num % wc;
			result = words[num];
		}
		RETURN true;
	} else {
		result.clear();
		RETURN false;
	}
}

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

void MechShell::fetchMEKs(const BGCPString& args)
{
	SET_SCOPE;
	size_t pos1 = args.scan(',', 0, args.getByteCount());
	size_t clen = args.getCharSize(pos1);
	size_t pos2 = args.scan(',', pos1+clen, args.getByteCount());

	if (pos1 >= args.getByteCount() 
	||  pos2 >= args.getByteCount())
	{
		RETURN;
	}

	SET_OP;
	BGCPString chassisNumVar = BGCPString(args.getPtr<char>(10), pos1-10, args.getCP());
	BGCPString chassisListVar = BGCPString(args.getPtr<char>(pos1+clen), pos2-pos1-clen, args.getCP());
	BGCPString configVarName = BGCPString(args.getPtr<char>(pos2+clen), args.getByteCount()-pos2-2*clen, args.getCP());

	SET_OP;
	BGCPString chassis;
	BGCPString selStr = "%";
	selStr += chassisNumVar;
	selStr += "%,%";
	selStr += chassisListVar;
	selStr +="%";
	selectStr(selStr, chassis);

	// Check MEK files from chassis folder
	SET_OP;
	bool missionSuccessful = false;
	BGCPString configs;
	BGCPString mask(getDataDir());
	mask.addPathSep(MECH_SUB_DIR);
	mask.addPathSep(chassis);
	mask.addPathSep("*.mek");

	SET_OP;
	FileList fl;
	if (fl.start(mask.getAsPath())) {
		if (configs.getByteCount() > 0)
			configs += ",";
		BGCPString fn(fl.getFileName(), CP_SYSTEM);
		configs += fn;

		while (++fl) {
			configs += ",";
			BGCPString fn(fl.getFileName(), CP_SYSTEM);
			configs += fn;
		}
	}

	SET_OP;
	configs.convertToCP(CP_ASCII);
	setVar(configVarName, configs);
	RETURN;
}

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

void MechShell::fetchMEKComment(const BGCPString& cmd)
{
	SET_SCOPE;
	size_t cmdLen = cmd.getByteCount();
	BGCPString argsStr = cmd.copyPart(16, cmdLen-17);
	auto args = argsStr.split(",", false);
	if (args.size() < 5) {
		RETURN;
	}

	BGCPString chassis = args[0];
	BGCPString chassisList = args[1];
	BGCPString config = args[2];
	BGCPString configList = args[3];
	BGCPString commentVar = args[4];

	BGCPString chassisSelector = BGCPString("%") + chassis + BGCPString("%,") + chassisList;
	BGCPString configSelector  = BGCPString("%") + config  + BGCPString("%,") + configList;
	selectStr(chassisSelector, chassis);
	selectStr(configSelector,  config);

	SET_OP;
	BGCPString FullFN(getDataDir());
	FullFN.addPathSep(MECH_SUB_DIR);
	FullFN.addPathSep(chassis);
	FullFN.addPathSep(config);
	MemoryBlock mb;
	bool found = mb.loadFromFile(FullFN.getAsPath());

	if (mb.getSize() > 0x158) {
		DWORD weaponCount = mb.get<DWORD>(0x10);
		DWORD ammoCount = mb.get<DWORD>(0x14);
		const char* comment = mb.getPtr<char>(0x158+8*weaponCount+8*ammoCount);
		if (comment != nullptr) {
			setVar(commentVar, BGCPString(comment, CP_ASCII));
		}
	}
	RETURN;
}

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

Vehicle* MechShell::loadPlayerVehicle(const BGCPString& configFN, const BGCPString& chassisName, 
	const BGCPString& textureFN)
{
	SET_SCOPE;
	Vehicle* vehicle = loadVehicle2(chassisName, configFN, textureFN, "Mercenaries");

	if (vehicle == NULL) {
		printf("Player vehicle not loaded, giving up\n");
		RETURN nullptr;
	}
	
	vehicle->updateLoadoutGeometry();

	RETURN vehicle;
}

////////////////////////////////////////////////////////////////////////////////
// Show Mech Lab

void MechShell::showMechLab(const BGCPString& cmd)
{
	SET_SCOPE;
	int screens = MechLab_outfitMechButtonID | MechLab_equipmentButtonID | MechLab_weaponGroupsID | MechLab_exitButtonID;
	MechLab* lab = new MechLab(screens, this);
	lab->setGeometry(0, 0, getWidth(), getHeight());
	getMechVm()->setScreen(lab);
	lab->setReturnVis(this);

	BGCPString MEKname, chassisName, MEKprefix;
	BGCPString MEKdir = retrieveStr("MEKdir");
	BGCPString cfgFN = MEKdir;

	// See if there is a background to display
	SET_OP;
	BGCPString backgroundFN;
	getVar("MechLabBackground", backgroundFN);
	#ifdef MECHVM
	if (!backgroundFN.empty()) {
		backgroundFN.convertToCP(CP_ASCII);
		MemoryBlock mb;
		archive->getFile(backgroundFN.get<char>(), mb);
		Texture texture;
		MemoryBlock* pcx = LZdecode(&mb);
		if (pcx != nullptr) {
			if (MechWarriorIIPRJ::loadPCX(pcx->getPtr<unsigned char>(), pcx->getSize(), texture, mRoomPal)) {
				Texture resizedTexture;
				resizedTexture.resize(1024, 512, GL_RGB);
				resizedTexture.copy(&texture, 0, 0, texture.getWidth(), texture.getHeight(), 0, 0);
				//resizedTexture.saveToFile(L"c:\\temp\\test.bmp", 0, 0, resizedTexture.getWidth(), resizedTexture.getHeight());
				lab->setBackground(resizedTexture);
			}
			delete pcx;
		}
	}
	#endif

	// Retrieve arguments
	SET_OP;
	BGCPString args = cmd;
	args.convertToCP(CP_ASCII);
	size_t pos1 = args.scan('(', 0, args.getByteCount());
	size_t pos2 = args.scan(')', pos1, args.getByteCount());
	size_t c1 = args.scan(',', pos1, pos2);
	size_t c2 = args.scan(',', c1+1, pos2);
	size_t c3 = args.scan(',', c2+1, pos2);
	size_t c4 = args.scan(',', c3+1, pos2); 
	if(0 < pos1 && pos1 < c1 && c1 < c2 && c2 < c3 && c3 < c4 && c4 < pos2 && pos2 < args.getByteCount())
	{
		SET_OP;
		BGCPString qstr1("%");
		qstr1 += args.copyPart(pos1+1, c1-pos1-1);
		qstr1 += "%,%";
		qstr1 += BGCPString(args.getPtr<char>()+c1+1, c2-c1-1, CP_ASCII);
		qstr1 += "%";
		selectStr(qstr1, MEKname);

		BGCPString qstr2("%");
		qstr2 += args.copyPart(c2+1, c3-c2-1);
		qstr2 += "%,%";
		qstr2 += BGCPString(args.getPtr<char>()+c3+1, c4-c3-1, CP_ASCII);
		qstr2 += "%";
		selectStr(qstr2, MEKprefix);

		BGCPString qstr3("%");
		qstr3 += args.copyPart(c2+1, c3-c2-1);
		mMercsChassisNum = qstr3;
		qstr3 += "%,%";
		qstr3 += BGCPString(args.getPtr<char>()+c4+1, pos2-c4-1, CP_ASCII);
		qstr3 += "%";
		selectStr(qstr3, chassisName);
	}

	if (!MEKname.endsWith(".mek") && !MEKname.endsWith(".MEK")) {
		MEKname += BGCPString(".mek");
	}

	// Read data
	SET_OP;
	Vehicle* playerVehicle = loadPlayerVehicle (MEKname, chassisName);
	if (playerVehicle != NULL) {
		SET_OP;
		cfgFN += MEKname;
		BGCPString MVMKdir(getDataDir());
		MVMKdir.addPathSep(MECH_SUB_DIR);
		MVMKdir.addPathSep(chassisName);
		lab->showMech(playerVehicle);
		lab->setMechDir(MVMKdir);
		lab->showOutfitMechControls();
	}

	SET_OP;
	BGCPString dateStr;
	if (getVar("date", dateStr)) {
		MWDate date = MWDate::fromStr(dateStr);
		BGCPString mechLabType;
		getVar("weaponsLocker", mechLabType);
		lab->addWeapons(mechLabType, date.year);
	}
	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Called by MechLab to indicate that the user saved a new MEK file and
// intends to use it

void MechShell::useConfig(const PathChar* MEKFN)
{
	if (MEKFN == NULL) {
		return;
	}

	SET_SCOPE;
	size_t len = bgstrlen(MEKFN);
	if (len < 3) {
		RETURN;
	}

	size_t pos;
	BGCPString MEKFNpart(MEKFN, CP_SYSTEM);
	if (bgstrrscan<PathChar>(MEKFN, OSPathSep, len-1, 0, pos)) {
		MEKFNpart = BGCPString(MEKFN + pos+1, len, CP_SYSTEM);
	}

	auto configsVarIndex = findVar ("configs");
	if (configsVarIndex < 0 || configsVarIndex > (int) vars.size()) {
		RETURN;
	}
	BGCPString MEKlist = vars[configsVarIndex+1];

	if (isMercs()) {
		// For Mercs, the new config should replace an existing one
		SET_OP;
		auto allConfigs = MEKlist.split(",", true);
		size_t chassisNum = 0;
		auto chassisVarIndex = findVar(mMercsChassisNum);

		if (chassisVarIndex >= 0 && (size_t)(chassisVarIndex + 1) < vars.size()) {
			chassisNum = (size_t)vars[chassisVarIndex+1].toInt();

			// Special case: for Mercs lancemates, there is an additional config called "none"
			// Since we are updating the main configs list, we have to subtract 1 to skip that entry ...
			if (mMercsChassisNum != "chassis") {
				--chassisNum;
			}

			chassisNum %= allConfigs.size();
		}

		allConfigs[chassisNum] = MEKFNpart;
		BGCPString newConfigsStr;
		for (size_t i = 0; i < allConfigs.size(); ++i) {
			if (!newConfigsStr.empty()) {
				newConfigsStr += BGCPString(",");
			}
			newConfigsStr += allConfigs[i];
		}
		vars[configsVarIndex+1] = newConfigsStr;

		// Update lancemate config strings
		BGCPString lancemateConfigs = BGCPString("none,") +  newConfigsStr;
		setVar("sm1_configs", lancemateConfigs);
		setVar("sm2_configs", lancemateConfigs);
	} else {
		// For the other games, simply append the new config to the existing list
		SET_OP;
		MEKlist += BGCPString(",") + MEKFNpart;
		vars[configsVarIndex+1] = MEKlist;
		int MEKcount = (int) MEKlist.count(',')+1;
		auto configVarIndex = findVar ("config");
		vars[configVarIndex+1] = BGCPString(MEKcount-1);
	}

	RETURN;
}

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

bool MechShell::runInternalSim(const BGCPString& missionName, bool& missionSuccessful)
{
	// It's okay to store the pointer to sim in a local variable. It will mark itself for deletion 
	// upon completion or is deleted below
	SET_SCOPE;
	MechSim* mechSim = new MechSim();
	mechSim->loadShotPrototypes();
	mechSim->addArchive(archive2);
	mechSim->setShell(this);
	getVar("GamePath", mechSim->mGameDir);

	SET_OP;
	BGCPString levelDir;
	getVar("levelDir", levelDir);
	BGCPString levelFN = levelDir;
	levelFN.addPathSep(missionName + BGCPString(".xml"));
	bool readyToPlay = mechSim->loadLevel(levelDir.getAsPath(), levelFN.getAsPath());

	if (readyToPlay) {
		SET_OP;
		auto mechVM = getMechVm();
		mechVM->setScreen(mechSim);
		
		#ifndef _DEBUG
		if (!mechVM->isInFullscreenMode()) {
			mechVM->setFullscreenMode(true);
		}
		#endif

		mechSim->setReturnVis(this);
		mechSim->mPaused = false;
		RETURN true;
	} else {
		delete mechSim;
		RETURN false;
	}
}

void MechShell::enterPostMissionRoom(bool missionSuccessful)
{
	SET_SCOPE;
	BGCPString nextRoom;
	if (missionSuccessful) {
		getVar("SuccessRoom", nextRoom);
	} else {
		getVar("FailureRoom", nextRoom);
	}
	enterRoom(nextRoom);
	RETURN;
}

bool MechShell::runSim(const BGCPString& cmd)
{
	SET_SCOPE;
	const TCHAR* methodStr = NULL;
	int startMethod = 0;

	size_t pos1 = cmd.scan("(", 0, cmd.getByteCount());
	size_t pos2 = cmd.scan(")", 0, cmd.getByteCount());
	size_t clen = cmd.getCharSize(pos1);
	BGCPString missionName = cmd.copyPart(pos1+clen, pos2-pos1-clen);
	evaluate(missionName);

	COUT << "Running mission ";
	missionName.printLn();
	bool missionSuccessful = false;
	bool startCorrectly = false;
	BGCPString simType;
	if (getVar("SimType", simType)) {
		/*if (simType == "DosBox") {
			startCorrectly = runExternalSim (true, missionSuccessful, missionName);
		} else if (simType == "native") {
			startCorrectly = runExternalSim (false, missionSuccessful, missionName);
		} else*/ if (simType == "internal") {
			startCorrectly = runInternalSim(missionName, missionSuccessful);
		} else {
			COUT << "Unsupported SimType" << std::endl;
		}
	} else {
		COUT << "Missing SimType" << std::endl;
	}

	RETURN false;
}

bool MechShell::setVar (const BGCPString& varName, const BGCPString& value)
{
	if (varName.empty()) {
		return false;
	}
	
	// Check the stored variables
	SET_SCOPE;
	int i = findVar(varName);

	if (i >= 0) {
		vars[i+1] = value;
	} else {
		// Add new variable
		vars.push_back(varName);
		vars.push_back(value);
	}

	RETURN true;
}

// Return index of variable, or -1
int MechShell::findVar (const BGCPString& name) const
{
	SET_SCOPE;
	int i = 0;
	while (i < (int) vars.size()) {
		const BGCPString& var = vars[i];
		if (var == name) {
			RETURN i;
		} else {
			i += 2;
		}
	}

	RETURN -1;
}

BGCPString MechShell::retrieveStr (const BGCPString& varName) const 
{
	SET_SCOPE;
	BGCPString result;
	int index = findVar (varName);
	if (index >= 0) {
		result = vars[index+1];
	}
	RETURN result;
}

// Get variable value
bool MechShell::getVar(const BGCPString& varName, BGCPString& result) const
{
	SET_SCOPE;
	int index = findVar(varName);

	if (index >= 0) {
		result = vars[index+1];
		RETURN true;
	} else {
		result.clear();
		RETURN false;
	}
}

// GLTable implementation: get table dimensions
void MechShell::getDimensions (size_t /*senderID*/, size_t& rowCount, size_t& colCount)
{
/*	SET_SCOPE;
	colCount = 1;
	rowCount = mTableStrings.size();

	for (size_t i = 0; i < mTableStrings.size(); ++i) {
		if (mTableStrings[i].size() > colCount) {
			colCount = mTableStrings[i].size();
		}
	}

	RETURN;*/
}

// GLTable implementation: Get string for a cell
bool MechShell::getString (size_t /*senderID*/, int row, int col, BGCPString& str)
{
/*	SET_SCOPE;
	if (row < (int) mTableStrings.size()) {
		if (col < (int) mTableStrings[row].size()) {
			str = mTableStrings[row][col];
			RETURN true;
		}
	}

	RETURN false;*/
	return false;
}

// Request context menu
void MechShell::showContextMenu(VirtualClass* /*sender*/, int /*x*/, int /*y*/)
{
}

// Check for a cnd tag in the XML file and return the result of the evaluation
// If there is no tag, the return value defaults to true ...
// If there are several tags, they will be "anded"
// todo: allow + for "or" and * for "and" and evaluate (v1>0)*(v2<0)+(v3>0)
bool MechShell::checkCondition(const BGCPString& condition) const
{
	SET_SCOPE;
	if (condition.empty()) {
		RETURN true; // if there is no condition, treat it as condition fulfilled
	}

	bool result = true;

	size_t neqPos = condition.scan("!=", 0, condition.getByteCount());
	if (neqPos < condition.getByteCount()) {
		size_t clen = condition.getCharSize(neqPos);
		BGCPString leftStr = condition.copyPart(0, neqPos);
		BGCPString rightStr = condition.copyPart(neqPos+2*clen, condition.getByteCount()-neqPos-2*clen);
		evaluate(leftStr, true);
		evaluate(rightStr, true);
		result &= !leftStr.equalsNoCase(rightStr); // hopefully works for integers and strings ...
	} else {
		// Search for "=" only if "!=" is not found ... they're difficult to tell apart ...
		size_t eqPos = condition.scan("=", 0, condition.getByteCount());
		if (eqPos < condition.getByteCount()) {
			size_t clen = condition.getCharSize(eqPos);
			BGCPString leftStr = condition.copyPart(0, eqPos);
			BGCPString rightStr = condition.copyPart(eqPos+clen, condition.getByteCount()-eqPos-clen);
			evaluate(leftStr, true);
			evaluate(rightStr, true);
			result &= leftStr.equalsNoCase(rightStr); // hopefully works for integers and strings ...
		}
	}

	size_t ltPos = condition.scan("<", 0, condition.getByteCount());
	if (ltPos < condition.getByteCount()) {
		size_t clen = condition.getCharSize(ltPos);
		BGCPString leftStr = condition.copyPart(0, ltPos);
		BGCPString rightStr = condition.copyPart(ltPos+clen, condition.getByteCount()-ltPos-clen);
		evaluate(leftStr, true);
		evaluate(rightStr, true);
		double v1 = leftStr.toDouble();
		double v2 = rightStr.toDouble();
		result &= (v1 < v2);
	}

	size_t gtPos = condition.scan(">", 0, condition.getByteCount());
	if (gtPos < condition.getByteCount()) {
		size_t clen = condition.getCharSize(gtPos);
		BGCPString leftStr = condition.copyPart(0, gtPos);
		BGCPString rightStr = condition.copyPart(gtPos+clen, condition.getByteCount()-gtPos-clen);
		evaluate(leftStr, true);
		evaluate(rightStr, true);
		double v1 = leftStr.toDouble();
		double v2 = rightStr.toDouble();
		result &= (v1 > v2);
	}

	RETURN result;
}

// Update list of available contracts
void MechShell::updateMercsContracts()
{
	SET_SCOPE;
	BGCPString dateStr = retrieveStr("date");
	MWDate date = MWDate::fromStr(dateStr);
	BGCPString finishedContract = retrieveStr("contract").lowerStringASCII();

	BGCPString cbillsStr;
	getVar("cbills", cbillsStr);
	int cbills = (int) cbillsStr.toInt();

	// Advance date
	if (!finishedContract.empty()) {
		if (finishedContract.equalsNoCase("LuxuryVacation")) {
			// todo: booking fees should be applied immediately after booking ... introduce extra member bookingFees for that?
			// would be useful for vacation ... currently, you can book a vacation, buy a Mech and have a negative bank account
			date.addMonths(6);
			cbills -= 100000;
			setVar("cbills", BGCPString::fromInt(cbills));
		} else {
			bool found = false;
			for (size_t i = 0; i < mContracts.size(); ++i) {
				SET_OP;
				const MW2MercsContract& contract = mContracts[i];
				if (contract.contractName.lowerStringASCII() == finishedContract) {
					if (date < contract.endDate) {
						date = contract.endDate;
						found = true;
					}
				}
			}

			if (!found) {
				date.addMonths(2);
			}
		}
		setVar("date", date.toString());
	}

	BGCPString contracts;
	for (size_t i = 0; i < mContracts.size(); ++i) {
		SET_OP;
		const MW2MercsContract& contract = mContracts[i];
		if (contract.showDate <= date && date <= contract.startDate) {
			contracts.appendIfNotEmpty(",");
			contracts += contract.contractName;

			// Check for additional variable assignments - already needed to display the contract
			if (!contract.setVars.empty()) {
				processAssignment(contract.setVars, true);
			}
		}
	}

	// Add random contract ...
	int missionType = irand(0,3);
	int missionTerrain = irand(0,3);
	char* missionTypes[3] = {"randPatrol", "randDefense", "randEscort"};
	contracts.appendIfNotEmpty(",");
	contracts += missionTypes[missionType];
	setVar("randomMissionTerrain", BGCPString::fromInt(missionTerrain));
	if (date.year < 3047) {
		setVar("paymentPerGoal", "50000");
		setVar("enemyFaction", "DCMS");
		setVar("mechtype", "IS,30");
	} else if (date.year < 3049) {
		setVar("paymentPerGoal", "100000");
		setVar("enemyFaction", "DCMS");
		setVar("mechtype", "IS,50");
	} else if (date.year < 3050) {
		setVar("paymentPerGoal", "200000");
		setVar("enemyFaction", "DCMS");
		setVar("mechtype", "IS,100");
	} else if (date.year < 3060) {
		setVar("paymentPerGoal", "500000");
		setVar("enemyFaction", "Clan Smoke Jaguar");
		setVar("mechtype", "Clan,100");
	} else {
		setVar("paymentPerGoal", "500000");
		if (frand<float>() < 0.5) {
			setVar("enemyFaction", "Clan Jade Falcon");
		} else {
			setVar("enemyFaction", "Clan Ghost Bear");
		}
		setVar("mechtype", "Clan,100");
	}

	// Add special contract for vacation (post Luthien, if enough credits are available)
	if (date.year >= 3052 && date.month > 5 && cbills > 100000) {
		contracts.appendIfNotEmpty(",");
		contracts += "LuxuryVacation";
	}

	contracts.appendIfNotEmpty(",");
	contracts += "retire";

	SET_OP;
	setVar("contract", "");
	setVar("contracts", contracts);
	RETURN;
}

void MechShell::getMercsContractMissions()
{
	SET_SCOPE;
	BGCPString contract = retrieveStr("contract").lowerStringASCII();
	BGCPString lcContract = contract.lowerStringASCII();

	// Try to find the starting mission in the contracts file
	bool found = false;
	for (size_t i = 0; i < mContracts.size(); ++i) {
		SET_OP;
		const MW2MercsContract& scontract = mContracts[i];
		if (scontract.contractName.lowerStringASCII() == lcContract) {
			setVar("mission", scontract.firstMission);
			found = true;
		}
	}

	// If the contract does not specify a mission, use mission XML
	if (!found) {
		setVar("mission", lcContract);
	}

	RETURN;
}

// Set screen to return to when the shell is done
void MechShell::setReturnScreen(GLWindowContainer* returnScreen)
{
	mReturnScreen = returnScreen;
}

// Check if this shell is executed for Mercs ...
bool MechShell::isMercs() const
{
	// If a contracts variable is present, that indicates MW2-Mercs, otherwise, it's something else ...
	SET_SCOPE;
	BGCPString varName = "contracts";
	BGCPString dummy;
	bool result = getVar(varName, dummy);
	RETURN result;
}

// Return name of current room
BGCPString MechShell::getRoom() const
{
	SET_SCOPE;
	BGCPString room;
	getVar("room", room);
	RETURN room;
}

// Append element to list
void MechShell::appendToList(const BGCPString& cmd)
{
	SET_SCOPE;

	size_t pos = cmd.forward(0, 7);
	size_t pos2 = cmd.scan(",", pos, cmd.getByteCount());
	size_t pos3 = cmd.scan(")", pos2, cmd.getByteCount());

	if (pos >= pos2 || pos >= cmd.getByteCount()) {
		RETURN;
	}

	BGCPString listVarName = cmd.copyPart(pos, pos2-pos);
	pos2 += cmd.getCharSize(pos2);
	BGCPString strToAppend = cmd.copyPart(pos2, pos3-pos2);
	evaluate(strToAppend, false);

	int varIndex = findVar(listVarName);
	if (varIndex < 0 || vars[varIndex+1].empty()) {
		setVar(listVarName, strToAppend);
		RETURN;
	}

	vars[varIndex+1] += BGCPString(",") + strToAppend;

	RETURN;
}

// Load font from archive
void MechShell::loadFontFromArchive(const BGCPString& fn, int divider)
{
	if (fn.empty() || divider < 1) {
		return;
	}

	SET_SCOPE;
	MemoryBlock mb;
	/*size_t fileNum = archive->getEntryNumberByName(fn);
	size_t fileSize = archive->getFileSize(fileNum);
	mb.resize(fileSize);*/

	BGCPString fnAscii = fn;
	fnAscii.convertToCP(CP_ASCII);
	if (!archive->getFile(fnAscii.getPtr<char>(), mb)) {
		RETURN;
	}

	SET_OP;
	Texture texture;
	decodeFNT(mb, texture);
	int charW = texture.getWidth() / 16;
	int charH = texture.getHeight() / divider;
	Font* font = new Font(texture, charW, charH, 1);
	mFonts.push_back(font);
	RETURN;
}
