/* Copyright (C) 2024 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "MapReader.h"

#include "graphics/Camera.h"
#include "graphics/CinemaManager.h"
#include "graphics/Entity.h"
#include "graphics/GameView.h"
#include "graphics/MapGenerator.h"
#include "graphics/Patch.h"
#include "graphics/Terrain.h"
#include "graphics/TerrainTextureEntry.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Loader.h"
#include "ps/Profiler2.h"
#include "ps/TaskManager.h"
#include "ps/World.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/PostprocManager.h"
#include "renderer/SkyManager.h"
#include "renderer/WaterManager.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpCinemaManager.h"
#include "simulation2/components/ICmpGarrisonHolder.h"
#include "simulation2/components/ICmpObstruction.h"
#include "simulation2/components/ICmpOwnership.h"
#include "simulation2/components/ICmpPlayer.h"
#include "simulation2/components/ICmpPlayerManager.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpTurretHolder.h"
#include "simulation2/components/ICmpVisual.h"
#include "simulation2/components/ICmpWaterManager.h"

#include <boost/algorithm/string/predicate.hpp>

extern bool IsQuitRequested();

#if defined(_MSC_VER) && _MSC_VER > 1900
#pragma warning(disable: 4456) // Declaration hides previous local declaration.
#pragma warning(disable: 4458) // Declaration hides class member.
#endif

// TODO: Maybe this should be optimized depending on the map size.
constexpr int MAP_GENERATION_CONTEXT_SIZE{96 * MiB};

CMapReader::CMapReader() = default;

// LoadMap: try to load the map from given file; reinitialise the scene to new data if successful
void CMapReader::LoadMap(const VfsPath& pathname, const ScriptContext& cx,  JS::HandleValue settings, CTerrain *pTerrain_,
						 WaterManager* pWaterMan_, SkyManager* pSkyMan_,
						 CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_,
						 CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities)
{
	pTerrain = pTerrain_;
	pLightEnv = pLightEnv_;
	pGameView = pGameView_;
	pWaterMan = pWaterMan_;
	pSkyMan = pSkyMan_;
	pCinema = pCinema_;
	pTrigMan = pTrigMan_;
	pPostproc = pPostproc_;
	pSimulation2 = pSimulation2_;
	pSimContext = pSimContext_;
	m_PlayerID = playerID_;
	m_SkipEntities = skipEntities;
	m_StartingCameraTarget = INVALID_ENTITY;
	m_ScriptSettings.init(cx.GetGeneralJSContext(), settings);

	filename_xml = pathname.ChangeExtension(L".xml");

	// In some cases (particularly tests) we don't want to bother storing a large
	// mostly-empty .pmp file, so we let the XML file specify basic terrain instead.
	// If there's an .xml file and no .pmp, then we're probably in this XML-only mode
	only_xml = false;
	if (!VfsFileExists(pathname) && VfsFileExists(filename_xml))
	{
		only_xml = true;
	}

	file_format_version = CMapIO::FILE_VERSION; // default if there's no .pmp

	if (!only_xml)
	{
		// [25ms]
		unpacker.Read(pathname, "PSMP");
		file_format_version = unpacker.GetVersion();
	}

	// check oldest supported version
	if (file_format_version < FILE_READ_VERSION)
		throw PSERROR_Game_World_MapLoadFailed("Could not load terrain file - too old version!");

	// delete all existing entities
	if (pSimulation2)
		pSimulation2->ResetState();

	// reset post effects
	if (pPostproc)
		pPostproc->SetPostEffect(L"default");

	// load map or script settings script
	if (settings.isUndefined())
		LDR_Register([this](const double)
		{
			return LoadScriptSettings();
		}, L"CMapReader::LoadScriptSettings", 50);
	else
		LDR_Register([this](const double)
		{
			return LoadRMSettings();
		}, L"CMapReader::LoadRMSettings", 50);

	// load player settings script (must be done before reading map)
	LDR_Register([this](const double)
	{
		return LoadPlayerSettings();
	}, L"CMapReader::LoadPlayerSettings", 50);

	// unpack the data
	if (!only_xml)
		LDR_Register([this](const double)
		{
			return UnpackTerrain();
		}, L"CMapReader::UnpackMap", 1200);

	// read the corresponding XML file
	LDR_Register([this](const double)
	{
		return ReadXML();
	}, L"CMapReader::ReadXML", 50);

	// apply terrain data to the world
	LDR_Register([this](const double)
	{
		return ApplyTerrainData();
	}, L"CMapReader::ApplyTerrainData", 5);

	// read entities
	LDR_Register([this](const double)
	{
		return ReadXMLEntities();
	}, L"CMapReader::ReadXMLEntities", 5800);

	// apply misc data to the world
	LDR_Register([this](const double)
	{
		return ApplyData();
	}, L"CMapReader::ApplyData", 5);

	// load map settings script (must be done after reading map)
	LDR_Register([this](const double)
	{
		return LoadMapSettings();
	}, L"CMapReader::LoadMapSettings", 5);
}

// LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful
void CMapReader::LoadRandomMap(const CStrW& scriptFile, const ScriptContext& cx, JS::HandleValue settings, CTerrain *pTerrain_,
						 WaterManager* pWaterMan_, SkyManager* pSkyMan_,
						 CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_,
						 CSimulation2 *pSimulation2_, int playerID_)
{
	pSimulation2 = pSimulation2_;
	pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL;
	m_ScriptSettings.init(cx.GetGeneralJSContext(), settings);
	pTerrain = pTerrain_;
	pLightEnv = pLightEnv_;
	pGameView = pGameView_;
	pWaterMan = pWaterMan_;
	pSkyMan = pSkyMan_;
	pCinema = pCinema_;
	pTrigMan = pTrigMan_;
	pPostproc = pPostproc_;
	m_PlayerID = playerID_;
	m_SkipEntities = false;
	m_StartingCameraTarget = INVALID_ENTITY;

	// delete all existing entities
	if (pSimulation2)
		pSimulation2->ResetState();

	only_xml = false;

	// copy random map settings (before entity creation)
	LDR_Register([this](const double)
	{
		return LoadRMSettings();
	}, L"CMapReader::LoadRMSettings", 50);

	// load player settings script (must be done before reading map)
	LDR_Register([this](const double)
	{
		return LoadPlayerSettings();
	}, L"CMapReader::LoadPlayerSettings", 50);

	// load map generator with random map script
	LDR_Register([this, scriptFile](const double)
	{
		return StartMapGeneration(scriptFile);
	}, L"CMapReader::StartMapGeneration", 1);

	LDR_Register([this](const double)
	{
		return PollMapGeneration();
	}, L"CMapReader::PollMapGeneration", 19999);

	// parse RMS results into terrain structure
	LDR_Register([this](const double)
	{
		return ParseTerrain();
	}, L"CMapReader::ParseTerrain", 500);

	// parse RMS results into environment settings
	LDR_Register([this](const double)
	{
		return ParseEnvironment();
	}, L"CMapReader::ParseEnvironment", 5);

	// parse RMS results into camera settings
	LDR_Register([this](const double)
	{
		return ParseCamera();
	}, L"CMapReader::ParseCamera", 5);

	// apply terrain data to the world
	LDR_Register([this](const double)
	{
		return ApplyTerrainData();
	}, L"CMapReader::ApplyTerrainData", 5);

	// parse RMS results into entities
	LDR_Register([this](const double)
	{
		return ParseEntities();
	}, L"CMapReader::ParseEntities", 1000);

	// apply misc data to the world
	LDR_Register([this](const double)
	{
		return ApplyData();
	}, L"CMapReader::ApplyData", 5);

	// load map settings script (must be done after reading map)
	LDR_Register([this](const double)
	{
		return LoadMapSettings();
	}, L"CMapReader::LoadMapSettings", 5);
}

// UnpackTerrain: unpack the terrain from the end of the input data stream
//		- data: map size, heightmap, list of textures used by map, texture tile assignments
int CMapReader::UnpackTerrain()
{
	// yield after this time is reached. balances increased progress bar
	// smoothness vs. slowing down loading.
	const double end_time = timer_Time() + 200e-3;

	// first call to generator (this is skipped after first call,
	// i.e. when the loop below was interrupted)
	if (cur_terrain_tex == 0)
	{
		m_PatchesPerSide = (ssize_t)unpacker.UnpackSize();

		// unpack heightmap [600us]
		size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1;
		m_Heightmap.resize(SQR(verticesPerSide));
		unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16));

		// unpack # textures
		num_terrain_tex = unpacker.UnpackSize();
		m_TerrainTextures.reserve(num_terrain_tex);
	}

	// unpack texture names; find handle for each texture.
	// interruptible.
	while (cur_terrain_tex < num_terrain_tex)
	{
		CStr texturename;
		unpacker.UnpackString(texturename);

		if (CTerrainTextureManager::IsInitialised())
		{
			CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename);
			m_TerrainTextures.push_back(texentry);
		}

		cur_terrain_tex++;
		LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex);
	}

	// unpack tile data [3ms]
	ssize_t tilesPerSide = m_PatchesPerSide*PATCH_SIZE;
	m_Tiles.resize(size_t(SQR(tilesPerSide)));
	unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size());

	// reset generator state.
	cur_terrain_tex = 0;

	return 0;
}

int CMapReader::ApplyTerrainData()
{
	if (m_PatchesPerSide == 0)
	{
		// we'll probably crash when trying to use this map later
		throw PSERROR_Game_World_MapLoadFailed("Error loading map: no terrain data.\nCheck application log for details.");
	}

	if (!only_xml)
	{
		// initialise the terrain
		pTerrain->Initialize(m_PatchesPerSide, &m_Heightmap[0]);

		if (CTerrainTextureManager::IsInitialised())
		{
			// setup the textures on the minipatches
			STileDesc* tileptr = &m_Tiles[0];
			for (ssize_t j=0; j<m_PatchesPerSide; j++) {
				for (ssize_t i=0; i<m_PatchesPerSide; i++) {
					for (ssize_t m=0; m<PATCH_SIZE; m++) {
						for (ssize_t k=0; k<PATCH_SIZE; k++) {
							CMiniPatch& mp = pTerrain->GetPatch(i,j)->m_MiniPatches[m][k];	// can't fail

							mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index];
							mp.Priority = tileptr->m_Priority;

							tileptr++;
						}
					}
				}
			}
		}
	}

	CmpPtr<ICmpTerrain> cmpTerrain(*pSimContext, SYSTEM_ENTITY);
	if (cmpTerrain)
		cmpTerrain->ReloadTerrain();

	return 0;
}

// ApplyData: take all the input data, and rebuild the scene from it
int CMapReader::ApplyData()
{
	// copy over the lighting parameters
	if (pLightEnv)
		*pLightEnv = m_LightEnv;

	CmpPtr<ICmpPlayerManager> cmpPlayerManager(*pSimContext, SYSTEM_ENTITY);

	if (pGameView && cmpPlayerManager)
	{
		// Default to global camera (with constraints)
		pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus());

		// TODO: Starting rotation?
		CmpPtr<ICmpPlayer> cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID));
		if (cmpPlayer && cmpPlayer->HasStartingCamera())
		{
			// Use player starting camera
			CFixedVector3D pos = cmpPlayer->GetStartingCameraPos();
			pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
		}
		else if (m_StartingCameraTarget != INVALID_ENTITY)
		{
			// Point camera at entity
			CmpPtr<ICmpPosition> cmpPosition(*pSimContext, m_StartingCameraTarget);
			if (cmpPosition)
			{
				CFixedVector3D pos = cmpPosition->GetPosition();
				pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
			}
		}
	}

	return 0;
}

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

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


PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname)
{
	VfsPath filename_xml = pathname.ChangeExtension(L".xml");

	CXeromyces xmb_file;
	if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK)
		return PSRETURN_File_ReadFailed;

	// Define all the relevant elements used in the XML file
	#define EL(x) int el_##x = xmb_file.GetElementID(#x)
	#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
	EL(scenario);
	EL(scriptsettings);
	#undef AT
	#undef EL

	XMBElement root = xmb_file.GetRoot();
	ENSURE(root.GetNodeName() == el_scenario);

	XERO_ITER_EL(root, child)
	{
		int child_name = child.GetNodeName();
		if (child_name == el_scriptsettings)
		{
			m_ScriptSettings = child.GetText();
		}
	}

	return PSRETURN_OK;
}

void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret)
{
	ScriptRequest rq(scriptInterface);

	Script::CreateObject(rq, ret);

	if (m_ScriptSettings.empty())
		return;

	JS::RootedValue scriptSettingsVal(rq.cx);
	Script::ParseJSON(rq, m_ScriptSettings, &scriptSettingsVal);
	Script::SetProperty(rq, ret, "settings", scriptSettingsVal, false);
}

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

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


// Holds various state data while reading maps, so that loading can be
// interrupted (e.g. to update the progress display) then later resumed.
class CXMLReader
{
	NONCOPYABLE(CXMLReader);
public:
	CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader)
		: m_MapReader(mapReader), nodes(NULL, 0, NULL)
	{
		Init(xml_filename);
	}

	CStr ReadScriptSettings();

	// read everything except for entities
	void ReadXML();

	// return semantics: see Loader.cpp!LoadFunc.
	int ProgressiveReadEntities();

private:
	CXeromyces xmb_file;

	CMapReader& m_MapReader;

	int el_entity;
	int el_tracks;
	int el_template, el_player;
	int el_position, el_orientation, el_obstruction;
	int el_garrison;
	int el_turrets;
	int el_actor;
	int at_x;
	int at_y;
	int at_z;
	int at_group, at_group2;
	int at_angle;
	int at_uid;
	int at_seed;
	int at_turret;

	XMBElementList nodes; // children of root

	// loop counters
	size_t node_idx;
	size_t entity_idx;

	// # entities+nonentities processed and total (for progress calc)
	int completed_jobs, total_jobs;

	// maximum used entity ID, so we can safely allocate new ones
	entity_id_t max_uid;

	void Init(const VfsPath& xml_filename);

	void ReadTerrain(XMBElement parent);
	void ReadEnvironment(XMBElement parent);
	void ReadCamera(XMBElement parent);
	void ReadPaths(XMBElement parent);
	void ReadTriggers(XMBElement parent);
	int ReadEntities(XMBElement parent, double end_time);
};


void CXMLReader::Init(const VfsPath& xml_filename)
{
	// must only assign once, so do it here
	node_idx = entity_idx = 0;

	if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK)
		throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!");

	// define the elements and attributes that are frequently used in the XML file,
	// so we don't need to do lots of string construction and comparison when
	// reading the data.
	// (Needs to be synchronised with the list in CXMLReader - ugh)
#define EL(x) el_##x = xmb_file.GetElementID(#x)
#define AT(x) at_##x = xmb_file.GetAttributeID(#x)
	EL(entity);
	EL(tracks);
	EL(template);
	EL(player);
	EL(position);
	EL(garrison);
	EL(turrets);
	EL(orientation);
	EL(obstruction);
	EL(actor);
	AT(x); AT(y); AT(z);
	AT(group); AT(group2);
	AT(angle);
	AT(uid);
	AT(seed);
	AT(turret);
#undef AT
#undef EL

	XMBElement root = xmb_file.GetRoot();
	ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
	nodes = root.GetChildNodes();

	// find out total number of entities+nonentities
	// (used when calculating progress)
	completed_jobs = 0;
	total_jobs = 0;
	for (XMBElement node : nodes)
		total_jobs += node.GetChildNodes().size();

	// Find the maximum entity ID, so we can safely allocate new IDs without conflicts

	max_uid = SYSTEM_ENTITY;

	XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities"));
	XERO_ITER_EL(ents, ent)
	{
		CStr uid = ent.GetAttributes().GetNamedItem(at_uid);
		max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt());
	}
}


CStr CXMLReader::ReadScriptSettings()
{
	XMBElement root = xmb_file.GetRoot();
	ENSURE(xmb_file.GetElementStringView(root.GetNodeName()) == "Scenario");
	nodes = root.GetChildNodes();

	XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings"));

	return settings.GetText();
}


void CXMLReader::ReadTerrain(XMBElement parent)
{
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
	AT(patches);
	AT(texture);
	AT(priority);
	AT(height);
#undef AT

	ssize_t patches = 9;
	CStr texture = "grass1_spring";
	int priority = 0;
	u16 height = 16384;

	XERO_ITER_ATTR(parent, attr)
	{
		if (attr.Name == at_patches)
			patches = attr.Value.ToInt();
		else if (attr.Name == at_texture)
			texture = attr.Value;
		else if (attr.Name == at_priority)
			priority = attr.Value.ToInt();
		else if (attr.Name == at_height)
			height = (u16)attr.Value.ToInt();
	}

	m_MapReader.m_PatchesPerSide = patches;

	// Load the texture
	CTerrainTextureEntry* texentry = nullptr;
	if (CTerrainTextureManager::IsInitialised())
		texentry = g_TexMan.FindTexture(texture);

	m_MapReader.pTerrain->Initialize(patches, NULL);

	// Fill the heightmap
	u16* heightmap = m_MapReader.pTerrain->GetHeightMap();
	ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide();
	for (ssize_t i = 0; i < SQR(verticesPerSide); ++i)
		heightmap[i] = height;

	// Fill the texture map
	for (ssize_t pz = 0; pz < patches; ++pz)
	{
		for (ssize_t px = 0; px < patches; ++px)
		{
			CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz);	// can't fail

			for (ssize_t z = 0; z < PATCH_SIZE; ++z)
			{
				for (ssize_t x = 0; x < PATCH_SIZE; ++x)
				{
					patch->m_MiniPatches[z][x].Tex = texentry;
					patch->m_MiniPatches[z][x].Priority = priority;
				}
			}
		}
	}
}

void CXMLReader::ReadEnvironment(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
	EL(posteffect);
	EL(skyset);
	EL(suncolor);
	EL(sunelevation);
	EL(sunrotation);
	EL(ambientcolor);
	EL(water);
	EL(waterbody);
	EL(type);
	EL(color);
	EL(tint);
	EL(height);
	EL(waviness);
	EL(murkiness);
	EL(windangle);
	EL(fog);
	EL(fogcolor);
	EL(fogfactor);
	EL(fogthickness);
	EL(postproc);
	EL(brightness);
	EL(contrast);
	EL(saturation);
	EL(bloom);
	AT(r); AT(g); AT(b);
#undef AT
#undef EL

	XERO_ITER_EL(parent, element)
	{
		int element_name = element.GetNodeName();

		XMBAttributeList attrs = element.GetAttributes();

		if (element_name == el_skyset)
		{
			if (m_MapReader.pSkyMan)
				m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8());
		}
		else if (element_name == el_suncolor)
		{
			m_MapReader.m_LightEnv.m_SunColor = RGBColor(
				attrs.GetNamedItem(at_r).ToFloat(),
				attrs.GetNamedItem(at_g).ToFloat(),
				attrs.GetNamedItem(at_b).ToFloat());
		}
		else if (element_name == el_sunelevation)
		{
			m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat();
		}
		else if (element_name == el_sunrotation)
		{
			m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat();
		}
		else if (element_name == el_ambientcolor)
		{
			m_MapReader.m_LightEnv.m_AmbientColor = RGBColor(
				attrs.GetNamedItem(at_r).ToFloat(),
				attrs.GetNamedItem(at_g).ToFloat(),
				attrs.GetNamedItem(at_b).ToFloat());
		}
		else if (element_name == el_fog)
		{
			XERO_ITER_EL(element, fog)
			{
				int fog_element_name = fog.GetNodeName();
				if (fog_element_name == el_fogcolor)
				{
					XMBAttributeList fogAttributes = fog.GetAttributes();
					m_MapReader.m_LightEnv.m_FogColor = RGBColor(
						fogAttributes.GetNamedItem(at_r).ToFloat(),
						fogAttributes.GetNamedItem(at_g).ToFloat(),
						fogAttributes.GetNamedItem(at_b).ToFloat());
				}
				else if (fog_element_name == el_fogfactor)
				{
					m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat();
				}
				else if (fog_element_name == el_fogthickness)
				{
					m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat();
				}
			}
		}
		else if (element_name == el_postproc)
		{
			XERO_ITER_EL(element, postproc)
			{
				int post_element_name = postproc.GetNodeName();
				if (post_element_name == el_brightness)
				{
					m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat();
				}
				else if (post_element_name == el_contrast)
				{
					m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat();
				}
				else if (post_element_name == el_saturation)
				{
					m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat();
				}
				else if (post_element_name == el_bloom)
				{
					m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat();
				}
				else if (post_element_name == el_posteffect)
				{
					if (m_MapReader.pPostproc)
						m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8());
				}
			}
		}
		else if (element_name == el_water)
		{
			XERO_ITER_EL(element, waterbody)
			{
				ENSURE(waterbody.GetNodeName() == el_waterbody);
				XERO_ITER_EL(waterbody, waterelement)
				{
					int water_element_name = waterelement.GetNodeName();
					if (water_element_name == el_height)
					{
						CmpPtr<ICmpWaterManager> cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
						ENSURE(cmpWaterManager);
						cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText()));
						continue;
					}

					// The rest are purely graphical effects, and should be ignored if
					// graphics are disabled
					if (!m_MapReader.pWaterMan)
						continue;

					if (water_element_name == el_type)
					{
						if (waterelement.GetText() == "default")
							m_MapReader.pWaterMan->m_WaterType = L"ocean";
						else
							m_MapReader.pWaterMan->m_WaterType =  waterelement.GetText().FromUTF8();
					}
#define READ_COLOR(el, out) \
					else if (water_element_name == el) \
					{ \
						XMBAttributeList colorAttrs = waterelement.GetAttributes(); \
						out = CColor( \
							colorAttrs.GetNamedItem(at_r).ToFloat(), \
							colorAttrs.GetNamedItem(at_g).ToFloat(), \
							colorAttrs.GetNamedItem(at_b).ToFloat(), \
							1.f); \
					}

#define READ_FLOAT(el, out) \
					else if (water_element_name == el) \
					{ \
						out = waterelement.GetText().ToFloat(); \
					} \

					READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor)
					READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint)
					READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness)
					READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness)
					READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle)

#undef READ_FLOAT
#undef READ_COLOR

					else
						debug_warn(L"Invalid map XML data");
				}

			}
		}
		else
			debug_warn(L"Invalid map XML data");
	}

	m_MapReader.m_LightEnv.CalculateSunDirection();
}

void CXMLReader::ReadCamera(XMBElement parent)
{
	// defaults if we don't find player starting camera
#define EL(x) int el_##x = xmb_file.GetElementID(#x)
#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)
	EL(declination);
	EL(rotation);
	EL(position);
	AT(angle);
	AT(x); AT(y); AT(z);
#undef AT
#undef EL

	float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
	CVector3D translation = CVector3D(100, 150, -100);

	XERO_ITER_EL(parent, element)
	{
		int element_name = element.GetNodeName();

		XMBAttributeList attrs = element.GetAttributes();
		if (element_name == el_declination)
		{
			declination = attrs.GetNamedItem(at_angle).ToFloat();
		}
		else if (element_name == el_rotation)
		{
			rotation = attrs.GetNamedItem(at_angle).ToFloat();
		}
		else if (element_name == el_position)
		{
			translation = CVector3D(
				attrs.GetNamedItem(at_x).ToFloat(),
				attrs.GetNamedItem(at_y).ToFloat(),
				attrs.GetNamedItem(at_z).ToFloat());
		}
		else
			debug_warn(L"Invalid map XML data");
	}

	if (m_MapReader.pGameView)
	{
		m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
		m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation);
		m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation);
		m_MapReader.pGameView->GetCamera()->UpdateFrustum();
	}
}

void CXMLReader::ReadPaths(XMBElement parent)
{
	#define EL(x) int el_##x = xmb_file.GetElementID(#x)
	#define AT(x) int at_##x = xmb_file.GetAttributeID(#x)

	EL(path);
	EL(rotation);
	EL(node);
	EL(position);
	EL(target);
	AT(name);
	AT(timescale);
	AT(orientation);
	AT(mode);
	AT(style);
	AT(x);
	AT(y);
	AT(z);
	AT(deltatime);

#undef EL
#undef AT

	CmpPtr<ICmpCinemaManager> cmpCinemaManager(*m_MapReader.pSimContext, SYSTEM_ENTITY);
	XERO_ITER_EL(parent, element)
	{
		int elementName = element.GetNodeName();

		if (elementName == el_path)
		{
			CCinemaData pathData;
			XMBAttributeList attrs = element.GetAttributes();
			CStrW pathName(attrs.GetNamedItem(at_name).FromUTF8());
			pathData.m_Name = pathName;
			pathData.m_Timescale = fixed::FromString(attrs.GetNamedItem(at_timescale));
			pathData.m_Orientation = attrs.GetNamedItem(at_orientation).FromUTF8();
			pathData.m_Mode = attrs.GetNamedItem(at_mode).FromUTF8();
			pathData.m_Style = attrs.GetNamedItem(at_style).FromUTF8();

			TNSpline positionSpline, targetSpline;
			fixed lastPositionTime = fixed::Zero();
			fixed lastTargetTime = fixed::Zero();

			XERO_ITER_EL(element, pathChild)
			{
				elementName = pathChild.GetNodeName();
				attrs = pathChild.GetAttributes();

				// Load node data used for spline
				if (elementName == el_node)
				{
					lastPositionTime += fixed::FromString(attrs.GetNamedItem(at_deltatime));
					lastTargetTime += fixed::FromString(attrs.GetNamedItem(at_deltatime));
					XERO_ITER_EL(pathChild, nodeChild)
					{
						elementName = nodeChild.GetNodeName();
						attrs = nodeChild.GetAttributes();

						if (elementName == el_position)
						{
							CFixedVector3D position(fixed::FromString(attrs.GetNamedItem(at_x)),
								fixed::FromString(attrs.GetNamedItem(at_y)),
								fixed::FromString(attrs.GetNamedItem(at_z)));

							positionSpline.AddNode(position, CFixedVector3D(), lastPositionTime);
							lastPositionTime = fixed::Zero();
						}
						else if (elementName == el_rotation)
						{
							// TODO: Implement rotation slerp/spline as another object
						}
						else if (elementName == el_target)
						{
							CFixedVector3D targetPosition(fixed::FromString(attrs.GetNamedItem(at_x)),
								fixed::FromString(attrs.GetNamedItem(at_y)),
								fixed::FromString(attrs.GetNamedItem(at_z)));

							targetSpline.AddNode(targetPosition, CFixedVector3D(), lastTargetTime);
							lastTargetTime = fixed::Zero();
						}
						else
							LOGWARNING("Invalid cinematic element for node child");
					}
				}
				else
					LOGWARNING("Invalid cinematic element for path child");
			}

			// Construct cinema path with data gathered
			CCinemaPath path(pathData, positionSpline, targetSpline);
			if (path.Empty())
			{
				LOGWARNING("Path with name '%s' is empty", pathName.ToUTF8());
				return;
			}

			if (!cmpCinemaManager)
				continue;
			if (!cmpCinemaManager->HasPath(pathName))
				cmpCinemaManager->AddPath(path);
			else
				LOGWARNING("Path with name '%s' already exists", pathName.ToUTF8());
		}
		else
			LOGWARNING("Invalid path child with name '%s'", element.GetText());
	}
}

void CXMLReader::ReadTriggers(XMBElement UNUSED(parent))
{
}

int CXMLReader::ReadEntities(XMBElement parent, double end_time)
{
	XMBElementList entities = parent.GetChildNodes();

	ENSURE(m_MapReader.pSimulation2);
	CSimulation2& sim = *m_MapReader.pSimulation2;
	CmpPtr<ICmpPlayerManager> cmpPlayerManager(sim, SYSTEM_ENTITY);

	while (entity_idx < entities.size())
	{
		// all new state at this scope and below doesn't need to be
		// wrapped, since we only yield after a complete iteration.

		XMBElement entity = entities[entity_idx++];
		ENSURE(entity.GetNodeName() == el_entity);

		XMBAttributeList attrs = entity.GetAttributes();
		CStr uid = attrs.GetNamedItem(at_uid);
		ENSURE(!uid.empty());
		int EntityUid = uid.ToInt();

		CStrW TemplateName;
		int PlayerID = 0;
		std::vector<entity_id_t> Garrison;
		std::vector<std::pair<std::string, entity_id_t>> Turrets;
		CFixedVector3D Position;
		CFixedVector3D Orientation;
		long Seed = -1;

		// Obstruction control groups.
		entity_id_t ControlGroup = INVALID_ENTITY;
		entity_id_t ControlGroup2 = INVALID_ENTITY;

		XERO_ITER_EL(entity, setting)
		{
			int element_name = setting.GetNodeName();

			// <template>
			if (element_name == el_template)
			{
				TemplateName = setting.GetText().FromUTF8();
			}
			// <player>
			else if (element_name == el_player)
			{
				PlayerID = setting.GetText().ToInt();
			}
			// <position>
			else if (element_name == el_position)
			{
				XMBAttributeList positionAttrs = setting.GetAttributes();
				Position = CFixedVector3D(
					fixed::FromString(positionAttrs.GetNamedItem(at_x)),
					fixed::FromString(positionAttrs.GetNamedItem(at_y)),
					fixed::FromString(positionAttrs.GetNamedItem(at_z)));
			}
			// <orientation>
			else if (element_name == el_orientation)
			{
				XMBAttributeList orientationAttrs = setting.GetAttributes();
				Orientation = CFixedVector3D(
					fixed::FromString(orientationAttrs.GetNamedItem(at_x)),
					fixed::FromString(orientationAttrs.GetNamedItem(at_y)),
					fixed::FromString(orientationAttrs.GetNamedItem(at_z)));
				// TODO: what happens if some attributes are missing?
			}
			// <obstruction>
			else if (element_name == el_obstruction)
			{
				XMBAttributeList obstructionAttrs = setting.GetAttributes();
				ControlGroup = obstructionAttrs.GetNamedItem(at_group).ToInt();
				ControlGroup2 = obstructionAttrs.GetNamedItem(at_group2).ToInt();
			}
			// <garrison>
			else if (element_name == el_garrison)
			{
				XMBElementList garrison = setting.GetChildNodes();
				Garrison.reserve(garrison.size());
				for (const XMBElement& garr_ent : garrison)
				{
					XMBAttributeList garrisonAttrs = garr_ent.GetAttributes();
					Garrison.push_back(garrisonAttrs.GetNamedItem(at_uid).ToInt());
				}
			}
			// <turrets>
			else if (element_name == el_turrets)
			{
				XMBElementList turrets = setting.GetChildNodes();
				Turrets.reserve(turrets.size());
				for (const XMBElement& turretPoint : turrets)
				{
					XMBAttributeList turretAttrs = turretPoint.GetAttributes();
					Turrets.emplace_back(
						turretAttrs.GetNamedItem(at_turret),
						turretAttrs.GetNamedItem(at_uid).ToInt()
					);
				}
			}
			// <actor>
			else if (element_name == el_actor)
			{
				XMBAttributeList attrs = setting.GetAttributes();
				CStr seedStr = attrs.GetNamedItem(at_seed);
				if (!seedStr.empty())
				{
					Seed = seedStr.ToLong();
					ENSURE(Seed >= 0);
				}
			}
			else
				debug_warn(L"Invalid map XML data");
		}

		entity_id_t ent = sim.AddEntity(TemplateName, EntityUid);
		entity_id_t player = cmpPlayerManager->GetPlayerByID(PlayerID);
		if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
		{	// Don't add entities with invalid player IDs
			LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(TemplateName));
		}
		else
		{
			CmpPtr<ICmpPosition> cmpPosition(sim, ent);
			if (cmpPosition)
			{
				cmpPosition->JumpTo(Position.X, Position.Z);
				cmpPosition->SetYRotation(Orientation.Y);
				// TODO: other parts of the position
			}

			if (!Garrison.empty())
			{
				CmpPtr<ICmpGarrisonHolder> cmpGarrisonHolder(sim, ent);
				if (cmpGarrisonHolder)
					cmpGarrisonHolder->SetInitEntities(std::move(Garrison));
				else
					LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no GarrisonHolder component and thus cannot garrison units.", ent, PlayerID);
			}

			// Needs to be before ownership changes to prevent initialising
			// subunits too soon.
			if (!Turrets.empty())
			{
				CmpPtr<ICmpTurretHolder> cmpTurretHolder(sim, ent);
				if (cmpTurretHolder)
					cmpTurretHolder->SetInitEntities(std::move(Turrets));
				else
					LOGERROR("CXMLMapReader::ReadEntities() entity '%d' of player '%d' has no TurretHolder component and thus cannot use turrets.", ent, PlayerID);
			}

			CmpPtr<ICmpOwnership> cmpOwnership(sim, ent);
			if (cmpOwnership)
				cmpOwnership->SetOwner(PlayerID);

			CmpPtr<ICmpObstruction> cmpObstruction(sim, ent);
			if (cmpObstruction)
			{
				if (ControlGroup != INVALID_ENTITY)
					cmpObstruction->SetControlGroup(ControlGroup);
				if (ControlGroup2 != INVALID_ENTITY)
					cmpObstruction->SetControlGroup2(ControlGroup2);

				cmpObstruction->ResolveFoundationCollisions();
			}

			CmpPtr<ICmpVisual> cmpVisual(sim, ent);
			if (cmpVisual)
			{
				if (Seed != -1)
					cmpVisual->SetActorSeed((u32)Seed);
				// TODO: variation/selection strings
			}

			if (PlayerID == m_MapReader.m_PlayerID && (boost::algorithm::ends_with(TemplateName, L"civil_centre") || m_MapReader.m_StartingCameraTarget == INVALID_ENTITY))
			{
				// Focus on civil centre or first entity owned by player
				m_MapReader.m_StartingCameraTarget = ent;
			}
		}

		completed_jobs++;
		LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
	}

	return 0;
}

void CXMLReader::ReadXML()
{
	for (XMBElement node : nodes)
	{
		CStr name = xmb_file.GetElementString(node.GetNodeName());
		if (name == "Terrain")
		{
			ReadTerrain(node);
		}
		else if (name == "Environment")
		{
			ReadEnvironment(node);
		}
		else if (name == "Camera")
		{
			ReadCamera(node);
		}
		else if (name == "ScriptSettings")
		{
			// Already loaded - this is to prevent an assertion
		}
		else if (name == "Entities")
		{
			// Handled by ProgressiveReadEntities instead
		}
		else if (name == "Paths")
		{
			ReadPaths(node);
		}
		else if (name == "Triggers")
		{
			ReadTriggers(node);
		}
		else if (name == "Script")
		{
			if (m_MapReader.pSimulation2)
				m_MapReader.pSimulation2->SetStartupScript(node.GetText());
		}
		else
		{
			debug_printf("Invalid XML element in map file: %s\n", name.c_str());
			debug_warn(L"Invalid map XML data");
		}
	}
}

int CXMLReader::ProgressiveReadEntities()
{
	// yield after this time is reached. balances increased progress bar
	// smoothness vs. slowing down loading.
	const double end_time = timer_Time() + 200e-3;

	int ret;

	while (node_idx < nodes.size())
	{
		XMBElement node = nodes[node_idx];
		CStr name = xmb_file.GetElementString(node.GetNodeName());
		if (name == "Entities")
		{
			if (!m_MapReader.m_SkipEntities)
			{
				ret = ReadEntities(node, end_time);
				if (ret != 0)	// error or timed out
					return ret;
			}
		}

		node_idx++;
	}

	return 0;
}

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

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


// load script settings from map
int CMapReader::LoadScriptSettings()
{
	if (!xml_reader)
		xml_reader = new CXMLReader(filename_xml, *this);

	// parse the script settings
	if (pSimulation2)
		pSimulation2->SetMapSettings(xml_reader->ReadScriptSettings());

	return 0;
}

// load player settings script
int CMapReader::LoadPlayerSettings()
{
	if (pSimulation2)
		pSimulation2->LoadPlayerSettings(true);
	return 0;
}

// load map settings script
int CMapReader::LoadMapSettings()
{
	if (pSimulation2)
		pSimulation2->LoadMapSettings();
	return 0;
}

int CMapReader::ReadXML()
{
	if (!xml_reader)
		xml_reader = new CXMLReader(filename_xml, *this);

	xml_reader->ReadXML();

	return 0;
}

// progressive
int CMapReader::ReadXMLEntities()
{
	if (!xml_reader)
		xml_reader = new CXMLReader(filename_xml, *this);

	int ret = xml_reader->ProgressiveReadEntities();
	// finished or failed
	if (ret <= 0)
	{
		SAFE_DELETE(xml_reader);
	}

	return ret;
}

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

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


int CMapReader::LoadRMSettings()
{
	// copy random map settings over to sim
	ENSURE(pSimulation2);
	pSimulation2->SetMapSettings(m_ScriptSettings);

	return 0;
}

struct CMapReader::GeneratorState
{
	std::atomic<int> progress{1};
	Future<Script::StructuredClone> task;
};

int CMapReader::StartMapGeneration(const CStrW& scriptFile)
{
	ScriptRequest rq(pSimulation2->GetScriptInterface());

	m_GeneratorState = std::make_unique<GeneratorState>();

	// The settings are stringified to pass them to the task.
	m_GeneratorState->task = g_TaskManager.PushTask(
		[&progress = m_GeneratorState->progress, scriptFile,
			settings = Script::StringifyJSON(rq, &m_ScriptSettings)](const StopToken stopToken)
		{
			PROFILE2("Map Generation");

			const CStrW scriptPath{scriptFile.empty() ? L"" : L"maps/random/" + scriptFile};

			const std::shared_ptr<ScriptContext> mapgenContext{ScriptContext::CreateContext(
				MAP_GENERATION_CONTEXT_SIZE)};
			ScriptInterface mapgenInterface{"Engine", "MapGenerator", mapgenContext};

			return RunMapGenerationScript(stopToken, progress, mapgenInterface, scriptPath, settings);
		});

	return 0;
}

[[noreturn]] void ThrowMapGenerationError()
{
	throw PSERROR_Game_World_MapLoadFailed{
		"Error generating random map.\nCheck application log for details."};
}

int CMapReader::PollMapGeneration()
{
	ENSURE(m_GeneratorState);

	if (IsQuitRequested())
	{
		LOGWARNING("Quit requested!");
		return -1;
	}

	if (!m_GeneratorState->task.IsDone())
		return m_GeneratorState->progress.load();

	const Script::StructuredClone results{m_GeneratorState->task.Get()};
	if (!results)
		ThrowMapGenerationError();

	// Parse data into simulation context
	ScriptRequest rq(pSimulation2->GetScriptInterface());
	JS::RootedValue data{rq.cx};
	Script::ReadStructuredClone(rq, results, &data);

	if (data.isUndefined())
		ThrowMapGenerationError();

	m_MapData.init(rq.cx, data);

	return 0;
};


int CMapReader::ParseTerrain()
{
	TIMER(L"ParseTerrain");
	ScriptRequest rq(pSimulation2->GetScriptInterface());

	// parse terrain from map data
	//	an error here should stop the loading process
#define GET_TERRAIN_PROPERTY(val, prop, out)\
if (!Script::GetProperty(rq, val, #prop, out))\
		{	LOGERROR("CMapReader::ParseTerrain() failed to get '%s' property", #prop);\
			throw PSERROR_Game_World_MapLoadFailed("Error parsing terrain data.\nCheck application log for details"); }

	u32 size;
	GET_TERRAIN_PROPERTY(m_MapData, size, size)

	m_PatchesPerSide = size / PATCH_SIZE;

	// flat heightmap of u16 data
	GET_TERRAIN_PROPERTY(m_MapData, height, m_Heightmap)

	// load textures
	std::vector<std::string> textureNames;
	GET_TERRAIN_PROPERTY(m_MapData, textureNames, textureNames)
	num_terrain_tex = textureNames.size();

	while (cur_terrain_tex < num_terrain_tex)
	{
		if (CTerrainTextureManager::IsInitialised())
		{
			CTerrainTextureEntry* texentry = g_TexMan.FindTexture(textureNames[cur_terrain_tex]);
			m_TerrainTextures.push_back(texentry);
		}

		cur_terrain_tex++;
	}

	// build tile data
	m_Tiles.resize(SQR(size));

	JS::RootedValue tileData(rq.cx);
	GET_TERRAIN_PROPERTY(m_MapData, tileData, &tileData)

	// parse tile data object into flat arrays
	std::vector<u16> tileIndex;
	std::vector<u16> tilePriority;
	GET_TERRAIN_PROPERTY(tileData, index, tileIndex);
	GET_TERRAIN_PROPERTY(tileData, priority, tilePriority);

	ENSURE(SQR(size) == tileIndex.size() && SQR(size) == tilePriority.size());

	// reorder by patches and store
	for (size_t x = 0; x < size; ++x)
	{
		size_t patchX = x / PATCH_SIZE;
		size_t offX = x % PATCH_SIZE;
		for (size_t y = 0; y < size; ++y)
		{
			size_t patchY = y / PATCH_SIZE;
			size_t offY = y % PATCH_SIZE;

			STileDesc tile;
			tile.m_Tex1Index = tileIndex[y*size + x];
			tile.m_Tex2Index = 0xFFFF;
			tile.m_Priority = tilePriority[y*size + x];

			m_Tiles[(patchY * m_PatchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)] = tile;
		}
	}

	// reset generator state
	cur_terrain_tex = 0;

#undef GET_TERRAIN_PROPERTY

	return 0;
}

int CMapReader::ParseEntities()
{
	TIMER(L"ParseEntities");
	ScriptRequest rq(pSimulation2->GetScriptInterface());

	// parse entities from map data
	std::vector<Entity> entities;

	if (!Script::GetProperty(rq, m_MapData, "entities", entities))
		LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");

	CSimulation2& sim = *pSimulation2;
	CmpPtr<ICmpPlayerManager> cmpPlayerManager(sim, SYSTEM_ENTITY);

	size_t entity_idx = 0;
	size_t num_entities = entities.size();

	Entity currEnt;

	while (entity_idx < num_entities)
	{
		// Get current entity struct
		currEnt = entities[entity_idx];

		entity_id_t ent = pSimulation2->AddEntity(currEnt.templateName, currEnt.entityID);
		entity_id_t player = cmpPlayerManager->GetPlayerByID(currEnt.playerID);
		if (ent == INVALID_ENTITY || player == INVALID_ENTITY)
		{	// Don't add entities with invalid player IDs
			LOGERROR("Failed to load entity template '%s'", utf8_from_wstring(currEnt.templateName));
		}
		else
		{
			CmpPtr<ICmpPosition> cmpPosition(sim, ent);
			if (cmpPosition)
			{
				cmpPosition->JumpTo(currEnt.position.X * (int)TERRAIN_TILE_SIZE, currEnt.position.Z * (int)TERRAIN_TILE_SIZE);
				cmpPosition->SetYRotation(currEnt.rotation.Y);
				// TODO: other parts of the position
			}

			CmpPtr<ICmpOwnership> cmpOwnership(sim, ent);
			if (cmpOwnership)
				cmpOwnership->SetOwner(currEnt.playerID);

			// Detect and fix collisions between foundation-blocking entities.
			// This presently serves to copy wall tower control groups to wall
			// segments, allowing players to expand RMS-generated walls.
			CmpPtr<ICmpObstruction> cmpObstruction(sim, ent);
			if (cmpObstruction)
				cmpObstruction->ResolveFoundationCollisions();

			if (currEnt.playerID == m_PlayerID && (boost::algorithm::ends_with(currEnt.templateName, L"civil_centre") || m_StartingCameraTarget == INVALID_ENTITY))
			{
				// Focus on civil centre or first entity owned by player
				m_StartingCameraTarget = currEnt.entityID;
			}
		}

		entity_idx++;
	}

	return 0;
}

int CMapReader::ParseEnvironment()
{
	// parse environment settings from map data
	ScriptRequest rq(pSimulation2->GetScriptInterface());

#define GET_ENVIRONMENT_PROPERTY(val, prop, out)\
	if (!Script::GetProperty(rq, val, #prop, out))\
		LOGWARNING("CMapReader::ParseEnvironment() failed to get '%s' property", #prop);

	JS::RootedValue envObj(rq.cx);
	GET_ENVIRONMENT_PROPERTY(m_MapData, Environment, &envObj)

	if (envObj.isUndefined())
	{
		LOGWARNING("CMapReader::ParseEnvironment(): Environment settings not found");
		return 0;
	}

	if (pPostproc)
		pPostproc->SetPostEffect(L"default");

	std::wstring skySet;
	GET_ENVIRONMENT_PROPERTY(envObj, SkySet, skySet)
	if (pSkyMan)
		pSkyMan->SetSkySet(skySet);

	CColor sunColor;
	GET_ENVIRONMENT_PROPERTY(envObj, SunColor, sunColor)
	m_LightEnv.m_SunColor = RGBColor(sunColor.r, sunColor.g, sunColor.b);

	GET_ENVIRONMENT_PROPERTY(envObj, SunElevation, m_LightEnv.m_Elevation)
	GET_ENVIRONMENT_PROPERTY(envObj, SunRotation, m_LightEnv.m_Rotation)

	CColor ambientColor;
	GET_ENVIRONMENT_PROPERTY(envObj, AmbientColor, ambientColor)
	m_LightEnv.m_AmbientColor = RGBColor(ambientColor.r, ambientColor.g, ambientColor.b);

	// Water properties
	JS::RootedValue waterObj(rq.cx);
	GET_ENVIRONMENT_PROPERTY(envObj, Water, &waterObj)

	JS::RootedValue waterBodyObj(rq.cx);
	GET_ENVIRONMENT_PROPERTY(waterObj, WaterBody, &waterBodyObj)

	// Water level - necessary
	float waterHeight;
	GET_ENVIRONMENT_PROPERTY(waterBodyObj, Height, waterHeight)

	CmpPtr<ICmpWaterManager> cmpWaterManager(*pSimulation2, SYSTEM_ENTITY);
	ENSURE(cmpWaterManager);
	cmpWaterManager->SetWaterLevel(entity_pos_t::FromFloat(waterHeight));

	// If we have graphics, get rest of settings
	if (pWaterMan)
	{
		GET_ENVIRONMENT_PROPERTY(waterBodyObj, Type, pWaterMan->m_WaterType)
		if (pWaterMan->m_WaterType == L"default")
			pWaterMan->m_WaterType = L"ocean";
		GET_ENVIRONMENT_PROPERTY(waterBodyObj, Color, pWaterMan->m_WaterColor)
		GET_ENVIRONMENT_PROPERTY(waterBodyObj, Tint, pWaterMan->m_WaterTint)
		GET_ENVIRONMENT_PROPERTY(waterBodyObj, Waviness, pWaterMan->m_Waviness)
		GET_ENVIRONMENT_PROPERTY(waterBodyObj, Murkiness, pWaterMan->m_Murkiness)
		GET_ENVIRONMENT_PROPERTY(waterBodyObj, WindAngle, pWaterMan->m_WindAngle)
	}

	JS::RootedValue fogObject(rq.cx);
	GET_ENVIRONMENT_PROPERTY(envObj, Fog, &fogObject);

	GET_ENVIRONMENT_PROPERTY(fogObject, FogFactor, m_LightEnv.m_FogFactor);
	GET_ENVIRONMENT_PROPERTY(fogObject, FogThickness, m_LightEnv.m_FogMax);

	CColor fogColor;
	GET_ENVIRONMENT_PROPERTY(fogObject, FogColor, fogColor);
	m_LightEnv.m_FogColor = RGBColor(fogColor.r, fogColor.g, fogColor.b);

	JS::RootedValue postprocObject(rq.cx);
	GET_ENVIRONMENT_PROPERTY(envObj, Postproc, &postprocObject);

	std::wstring postProcEffect;
	GET_ENVIRONMENT_PROPERTY(postprocObject, PostprocEffect, postProcEffect);

	if (pPostproc)
		pPostproc->SetPostEffect(postProcEffect);

	GET_ENVIRONMENT_PROPERTY(postprocObject, Brightness, m_LightEnv.m_Brightness);
	GET_ENVIRONMENT_PROPERTY(postprocObject, Contrast, m_LightEnv.m_Contrast);
	GET_ENVIRONMENT_PROPERTY(postprocObject, Saturation, m_LightEnv.m_Saturation);
	GET_ENVIRONMENT_PROPERTY(postprocObject, Bloom, m_LightEnv.m_Bloom);

	m_LightEnv.CalculateSunDirection();

#undef GET_ENVIRONMENT_PROPERTY

	return 0;
}

int CMapReader::ParseCamera()
{
	ScriptRequest rq(pSimulation2->GetScriptInterface());

	// parse camera settings from map data
	// defaults if we don't find player starting camera
	float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
	CVector3D translation = CVector3D(100, 150, -100);

#define GET_CAMERA_PROPERTY(val, prop, out)\
	if (!Script::GetProperty(rq, val, #prop, out))\
		LOGWARNING("CMapReader::ParseCamera() failed to get '%s' property", #prop);

	JS::RootedValue cameraObj(rq.cx);
	GET_CAMERA_PROPERTY(m_MapData, Camera, &cameraObj)

	if (!cameraObj.isUndefined())
	{	// If camera property exists, read values
		CFixedVector3D pos;
		GET_CAMERA_PROPERTY(cameraObj, Position, pos)
		translation = pos;

		GET_CAMERA_PROPERTY(cameraObj, Rotation, rotation)
		GET_CAMERA_PROPERTY(cameraObj, Declination, declination)
	}
#undef GET_CAMERA_PROPERTY

	if (pGameView)
	{
		pGameView->GetCamera()->m_Orientation.SetXRotation(declination);
		pGameView->GetCamera()->m_Orientation.RotateY(rotation);
		pGameView->GetCamera()->m_Orientation.Translate(translation);
		pGameView->GetCamera()->UpdateFrustum();
	}

	return 0;
}

CMapReader::~CMapReader()
{
	// Cleaup objects
	delete xml_reader;
}
