Index: source/graphics/Camera.cpp
===================================================================
--- source/graphics/Camera.cpp	(revision 24796)
+++ source/graphics/Camera.cpp	(working copy)
@@ -1,436 +1,436 @@
 /* Copyright (C) 2021 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 .
  */
 
 /*
  * CCamera holds a view and a projection matrix. It also has a frustum
  * which can be used to cull objects for rendering.
  */
 
 #include "precompiled.h"
 
 #include "Camera.h"
 
 #include "graphics/HFTracer.h"
 #include "graphics/Terrain.h"
 #include "lib/ogl.h"
 #include "maths/MathUtil.h"
 #include "maths/Vector4D.h"
 #include "ps/Game.h"
 #include "ps/World.h"
 #include "renderer/Renderer.h"
 #include "renderer/WaterManager.h"
 
 CCamera::CCamera()
 	: m_NearPlane(0.0f), m_FarPlane(0.0f), m_FOV(0.0f), m_ProjType(CUSTOM)
 {
 	// Set viewport to something anything should handle, but should be initialised
 	// to window size before use.
 	m_ViewPort.m_X = 0;
 	m_ViewPort.m_Y = 0;
 	m_ViewPort.m_Width = 800;
 	m_ViewPort.m_Height = 600;
 }
 
 CCamera::~CCamera() = default;
 
 void CCamera::SetProjection(const CMatrix3D& matrix)
 {
 	m_ProjType = CUSTOM;
 	m_ProjMat = matrix;
 }
 
 void CCamera::SetProjectionFromCamera(const CCamera& camera)
 {
 	m_ProjType = camera.m_ProjType;
 	m_NearPlane = camera.m_NearPlane;
 	m_FarPlane = camera.m_FarPlane;
 	if (m_ProjType == PERSPECTIVE)
 	{
 		m_FOV = camera.m_FOV;
 	}
 	m_ProjMat = camera.m_ProjMat;
 }
 
 void CCamera::SetPerspectiveProjection(float nearp, float farp, float fov)
 {
 	m_ProjType = PERSPECTIVE;
 	m_NearPlane = nearp;
 	m_FarPlane = farp;
 	m_FOV = fov;
 
 	m_ProjMat.SetPerspective(m_FOV, GetAspectRatio(), m_NearPlane, m_FarPlane);
 }
 
 // Updates the frustum planes. Should be called
 // everytime the view or projection matrices are
 // altered.
 void CCamera::UpdateFrustum(const CBoundingBoxAligned& scissor)
 {
 	CMatrix3D MatFinal;
 	CMatrix3D MatView;
 
 	m_Orientation.GetInverse(MatView);
 
 	MatFinal = m_ProjMat * MatView;
 
 	m_ViewFrustum.SetNumPlanes(6);
 
 	// get the RIGHT plane
-	m_ViewFrustum.m_aPlanes[0].m_Norm.X = scissor[1].X*MatFinal._41 - MatFinal._11;
-	m_ViewFrustum.m_aPlanes[0].m_Norm.Y = scissor[1].X*MatFinal._42 - MatFinal._12;
-	m_ViewFrustum.m_aPlanes[0].m_Norm.Z = scissor[1].X*MatFinal._43 - MatFinal._13;
-	m_ViewFrustum.m_aPlanes[0].m_Dist   = scissor[1].X*MatFinal._44 - MatFinal._14;
+	m_ViewFrustum.m_aPlanes[0].m_Norm.Xref() = scissor[1].getX()*MatFinal._41 - MatFinal._11;
+	m_ViewFrustum.m_aPlanes[0].m_Norm.Yref() = scissor[1].getX()*MatFinal._42 - MatFinal._12;
+	m_ViewFrustum.m_aPlanes[0].m_Norm.Zref() = scissor[1].getX()*MatFinal._43 - MatFinal._13;
+	m_ViewFrustum.m_aPlanes[0].m_Dist        = scissor[1].getX()*MatFinal._44 - MatFinal._14;
 
 	// get the LEFT plane
-	m_ViewFrustum.m_aPlanes[1].m_Norm.X = -scissor[0].X*MatFinal._41 + MatFinal._11;
-	m_ViewFrustum.m_aPlanes[1].m_Norm.Y = -scissor[0].X*MatFinal._42 + MatFinal._12;
-	m_ViewFrustum.m_aPlanes[1].m_Norm.Z = -scissor[0].X*MatFinal._43 + MatFinal._13;
-	m_ViewFrustum.m_aPlanes[1].m_Dist   = -scissor[0].X*MatFinal._44 + MatFinal._14;
+	m_ViewFrustum.m_aPlanes[1].m_Norm.Xref() = -scissor[0].getX()*MatFinal._41 + MatFinal._11;
+	m_ViewFrustum.m_aPlanes[1].m_Norm.Yref() = -scissor[0].getX()*MatFinal._42 + MatFinal._12;
+	m_ViewFrustum.m_aPlanes[1].m_Norm.Zref() = -scissor[0].getX()*MatFinal._43 + MatFinal._13;
+	m_ViewFrustum.m_aPlanes[1].m_Dist        = -scissor[0].getX()*MatFinal._44 + MatFinal._14;
 
 	// get the BOTTOM plane
-	m_ViewFrustum.m_aPlanes[2].m_Norm.X = -scissor[0].Y*MatFinal._41 + MatFinal._21;
-	m_ViewFrustum.m_aPlanes[2].m_Norm.Y = -scissor[0].Y*MatFinal._42 + MatFinal._22;
-	m_ViewFrustum.m_aPlanes[2].m_Norm.Z = -scissor[0].Y*MatFinal._43 + MatFinal._23;
-	m_ViewFrustum.m_aPlanes[2].m_Dist   = -scissor[0].Y*MatFinal._44 + MatFinal._24;
+	m_ViewFrustum.m_aPlanes[2].m_Norm.Xref() = -scissor[0].getY()*MatFinal._41 + MatFinal._21;
+	m_ViewFrustum.m_aPlanes[2].m_Norm.Yref() = -scissor[0].getY()*MatFinal._42 + MatFinal._22;
+	m_ViewFrustum.m_aPlanes[2].m_Norm.Zref() = -scissor[0].getY()*MatFinal._43 + MatFinal._23;
+	m_ViewFrustum.m_aPlanes[2].m_Dist        = -scissor[0].getY()*MatFinal._44 + MatFinal._24;
 
 	// get the TOP plane
-	m_ViewFrustum.m_aPlanes[3].m_Norm.X = scissor[1].Y*MatFinal._41 - MatFinal._21;
-	m_ViewFrustum.m_aPlanes[3].m_Norm.Y = scissor[1].Y*MatFinal._42 - MatFinal._22;
-	m_ViewFrustum.m_aPlanes[3].m_Norm.Z = scissor[1].Y*MatFinal._43 - MatFinal._23;
-	m_ViewFrustum.m_aPlanes[3].m_Dist   = scissor[1].Y*MatFinal._44 - MatFinal._24;
+	m_ViewFrustum.m_aPlanes[3].m_Norm.Xref() = scissor[1].getY()*MatFinal._41 - MatFinal._21;
+	m_ViewFrustum.m_aPlanes[3].m_Norm.Yref() = scissor[1].getY()*MatFinal._42 - MatFinal._22;
+	m_ViewFrustum.m_aPlanes[3].m_Norm.Zref() = scissor[1].getY()*MatFinal._43 - MatFinal._23;
+	m_ViewFrustum.m_aPlanes[3].m_Dist        = scissor[1].getY()*MatFinal._44 - MatFinal._24;
 
 	// get the FAR plane
-	m_ViewFrustum.m_aPlanes[4].m_Norm.X = scissor[1].Z*MatFinal._41 - MatFinal._31;
-	m_ViewFrustum.m_aPlanes[4].m_Norm.Y = scissor[1].Z*MatFinal._42 - MatFinal._32;
-	m_ViewFrustum.m_aPlanes[4].m_Norm.Z = scissor[1].Z*MatFinal._43 - MatFinal._33;
-	m_ViewFrustum.m_aPlanes[4].m_Dist   = scissor[1].Z*MatFinal._44 - MatFinal._34;
+	m_ViewFrustum.m_aPlanes[4].m_Norm.Xref() = scissor[1].getZ()*MatFinal._41 - MatFinal._31;
+	m_ViewFrustum.m_aPlanes[4].m_Norm.Yref() = scissor[1].getZ()*MatFinal._42 - MatFinal._32;
+	m_ViewFrustum.m_aPlanes[4].m_Norm.Zref() = scissor[1].getZ()*MatFinal._43 - MatFinal._33;
+	m_ViewFrustum.m_aPlanes[4].m_Dist        = scissor[1].getZ()*MatFinal._44 - MatFinal._34;
 
 	// get the NEAR plane
-	m_ViewFrustum.m_aPlanes[5].m_Norm.X = -scissor[0].Z*MatFinal._41 + MatFinal._31;
-	m_ViewFrustum.m_aPlanes[5].m_Norm.Y = -scissor[0].Z*MatFinal._42 + MatFinal._32;
-	m_ViewFrustum.m_aPlanes[5].m_Norm.Z = -scissor[0].Z*MatFinal._43 + MatFinal._33;
-	m_ViewFrustum.m_aPlanes[5].m_Dist   = -scissor[0].Z*MatFinal._44 + MatFinal._34;
+	m_ViewFrustum.m_aPlanes[5].m_Norm.Xref() = -scissor[0].getZ()*MatFinal._41 + MatFinal._31;
+	m_ViewFrustum.m_aPlanes[5].m_Norm.Yref() = -scissor[0].getZ()*MatFinal._42 + MatFinal._32;
+	m_ViewFrustum.m_aPlanes[5].m_Norm.Zref() = -scissor[0].getZ()*MatFinal._43 + MatFinal._33;
+	m_ViewFrustum.m_aPlanes[5].m_Dist        = -scissor[0].getZ()*MatFinal._44 + MatFinal._34;
 
 	for (size_t i = 0; i < 6; ++i)
 		m_ViewFrustum.m_aPlanes[i].Normalize();
 }
 
 void CCamera::ClipFrustum(const CPlane& clipPlane)
 {
 	CPlane normClipPlane = clipPlane;
 	normClipPlane.Normalize();
 	m_ViewFrustum.AddPlane(normClipPlane);
 }
 
 void CCamera::SetViewPort(const SViewPort& viewport)
 {
 	m_ViewPort.m_X = viewport.m_X;
 	m_ViewPort.m_Y = viewport.m_Y;
 	m_ViewPort.m_Width = viewport.m_Width;
 	m_ViewPort.m_Height = viewport.m_Height;
 }
 
 float CCamera::GetAspectRatio() const
 {
 	return static_cast(m_ViewPort.m_Width) / static_cast(m_ViewPort.m_Height);
 }
 
 void CCamera::GetViewQuad(float dist, Quad& quad) const
 {
 	ENSURE(m_ProjType == PERSPECTIVE);
 	const float y = dist * tanf(m_FOV * 0.5f);
 	const float x = y * GetAspectRatio();
 
-	quad[0].X = -x;
-	quad[0].Y = -y;
-	quad[0].Z = dist;
-	quad[1].X = x;
-	quad[1].Y = -y;
-	quad[1].Z = dist;
-	quad[2].X = x;
-	quad[2].Y = y;
-	quad[2].Z = dist;
-	quad[3].X = -x;
-	quad[3].Y = y;
-	quad[3].Z = dist;
+	quad[0].Xref() = -x;
+	quad[0].Yref() = -y;
+	quad[0].Zref() = dist;
+	quad[1].Xref() = x;
+	quad[1].Yref() = -y;
+	quad[1].Zref() = dist;
+	quad[2].Xref() = x;
+	quad[2].Yref() = y;
+	quad[2].Zref() = dist;
+	quad[3].Xref() = -x;
+	quad[3].Yref() = y;
+	quad[3].Zref() = dist;
 }
 
 void CCamera::BuildCameraRay(int px, int py, CVector3D& origin, CVector3D& dir) const
 {
 	// Coordinates relative to the camera plane.
 	const float dx = static_cast(px) / g_Renderer.GetWidth();
 	const float dy = 1.0f - static_cast(py) / g_Renderer.GetHeight();
 
 	Quad points;
 	GetViewQuad(m_FarPlane, points);
 
 	// Transform from camera space to world space.
 	for (CVector3D& point : points)
 		point = m_Orientation.Transform(point);
 
 	// Get world space position of mouse point at the far clipping plane.
 	CVector3D basisX = points[1] - points[0];
 	CVector3D basisY = points[3] - points[0];
 	CVector3D targetPoint = points[0] + (basisX * dx) + (basisY * dy);
 
 	origin = m_Orientation.GetTranslation();
 
 	// Build direction for the camera origin to the target point.
 	dir = targetPoint - origin;
 	dir.Normalize();
 }
 
 void CCamera::GetScreenCoordinates(const CVector3D& world, float& x, float& y) const
 {
 	CMatrix3D transform = m_ProjMat * m_Orientation.GetInverse();
 
-	CVector4D screenspace = transform.Transform(CVector4D(world.X, world.Y, world.Z, 1.0f));
+	CVector4D screenspace = transform.Transform(CVector4D(world.getX(), world.getY(), world.getZ(), 1.0f));
 
 	x = screenspace.X / screenspace.W;
 	y = screenspace.Y / screenspace.W;
 	x = (x + 1) * 0.5f * g_Renderer.GetWidth();
 	y = (1 - y) * 0.5f * g_Renderer.GetHeight();
 }
 
 CVector3D CCamera::GetWorldCoordinates(int px, int py, bool aboveWater) const
 {
 	CHFTracer tracer(g_Game->GetWorld()->GetTerrain());
 	int x, z;
 	CVector3D origin, dir, delta, terrainPoint, waterPoint;
 
 	BuildCameraRay(px, py, origin, dir);
 
 
 	bool gotTerrain = tracer.RayIntersect(origin, dir, x, z, terrainPoint);
 
 	if (!aboveWater)
 	{
 		if (gotTerrain)
 			return terrainPoint;
 
 		// Off the edge of the world?
 		// Work out where it /would/ hit, if the map were extended out to infinity with average height.
 		return GetWorldCoordinates(px, py, 50.0f);
 	}
 
 	CPlane plane;
 	plane.Set(CVector3D(0.f, 1.f, 0.f),										// upwards normal
 		CVector3D(0.f, g_Renderer.GetWaterManager()->m_WaterHeight, 0.f));	// passes through water plane
 
 	bool gotWater = plane.FindRayIntersection( origin, dir, &waterPoint );
 
 	// Clamp the water intersection to within the map's bounds, so that
 	// we'll always return a valid position on the map
 	ssize_t mapSize = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide();
 	if (gotWater)
 	{
-		waterPoint.X = Clamp(waterPoint.X, 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
-		waterPoint.Z = Clamp(waterPoint.Z, 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
+		waterPoint.Xref() = Clamp(waterPoint.getX(), 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
+		waterPoint.Zref() = Clamp(waterPoint.getZ(), 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
 	}
 
 	if (gotTerrain)
 	{
 		if (gotWater)
 		{
 			// Intersecting both heightmap and water plane; choose the closest of those
 			if ((origin - terrainPoint).LengthSquared() < (origin - waterPoint).LengthSquared())
 				return terrainPoint;
 			else
 				return waterPoint;
 		}
 		else
 		{
 			// Intersecting heightmap but parallel to water plane
 			return terrainPoint;
 		}
 	}
 	else
 	{
 		if (gotWater)
 		{
 			// Only intersecting water plane
 			return waterPoint;
 		}
 		else
 		{
 			// Not intersecting terrain or water; just return 0,0,0.
 			return CVector3D(0.f, 0.f, 0.f);
 		}
 	}
 
 }
 
 CVector3D CCamera::GetWorldCoordinates(int px, int py, float h) const
 {
 	CPlane plane;
 	plane.Set(CVector3D(0.f, 1.f, 0.f), CVector3D(0.f, h, 0.f)); // upwards normal, passes through h
 
 	CVector3D origin, dir, delta, currentTarget;
 
 	BuildCameraRay(px, py, origin, dir);
 
 	if (plane.FindRayIntersection(origin, dir, ¤tTarget))
 		return currentTarget;
 
 	// No intersection with the infinite plane - nothing sensible can be returned,
 	// so just choose an arbitrary point on the plane
 	return CVector3D(0.f, h, 0.f);
 }
 
 CVector3D CCamera::GetFocus() const
 {
 	// Basically the same as GetWorldCoordinates
 
 	CHFTracer tracer(g_Game->GetWorld()->GetTerrain());
 	int x, z;
 
 	CVector3D origin, dir, delta, terrainPoint, waterPoint;
 
 	origin = m_Orientation.GetTranslation();
 	dir = m_Orientation.GetIn();
 
 	bool gotTerrain = tracer.RayIntersect(origin, dir, x, z, terrainPoint);
 
 	CPlane plane;
 	plane.Set(CVector3D(0.f, 1.f, 0.f),										// upwards normal
 		CVector3D(0.f, g_Renderer.GetWaterManager()->m_WaterHeight, 0.f));	// passes through water plane
 
 	bool gotWater = plane.FindRayIntersection( origin, dir, &waterPoint );
 
 	// Clamp the water intersection to within the map's bounds, so that
 	// we'll always return a valid position on the map
 	ssize_t mapSize = g_Game->GetWorld()->GetTerrain()->GetVerticesPerSide();
 	if (gotWater)
 	{
-		waterPoint.X = Clamp(waterPoint.X, 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
-		waterPoint.Z = Clamp(waterPoint.Z, 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
+		waterPoint.Xref() = Clamp(waterPoint.getX(), 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
+		waterPoint.Zref() = Clamp(waterPoint.getZ(), 0.f, static_cast((mapSize - 1) * TERRAIN_TILE_SIZE));
 	}
 
 	if (gotTerrain)
 	{
 		if (gotWater)
 		{
 			// Intersecting both heightmap and water plane; choose the closest of those
 			if ((origin - terrainPoint).LengthSquared() < (origin - waterPoint).LengthSquared())
 				return terrainPoint;
 			else
 				return waterPoint;
 		}
 		else
 		{
 			// Intersecting heightmap but parallel to water plane
 			return terrainPoint;
 		}
 	}
 	else
 	{
 		if (gotWater)
 		{
 			// Only intersecting water plane
 			return waterPoint;
 		}
 		else
 		{
 			// Not intersecting terrain or water; just return 0,0,0.
 			return CVector3D(0.f, 0.f, 0.f);
 		}
 	}
 }
 
 void CCamera::LookAt(const CVector3D& camera, const CVector3D& target, const CVector3D& up)
 {
 	CVector3D delta = target - camera;
 	LookAlong(camera, delta, up);
 }
 
 void CCamera::LookAlong(const CVector3D& camera, CVector3D orientation, CVector3D up)
 {
 	orientation.Normalize();
 	up.Normalize();
 	CVector3D s = orientation.Cross(up);
 
-	m_Orientation._11 = -s.X;	m_Orientation._12 = up.X;	m_Orientation._13 = orientation.X;	m_Orientation._14 = camera.X;
-	m_Orientation._21 = -s.Y;	m_Orientation._22 = up.Y;	m_Orientation._23 = orientation.Y;	m_Orientation._24 = camera.Y;
-	m_Orientation._31 = -s.Z;	m_Orientation._32 = up.Z;	m_Orientation._33 = orientation.Z;	m_Orientation._34 = camera.Z;
+	m_Orientation._11 = -s.getX();	m_Orientation._12 = up.getX();	m_Orientation._13 = orientation.getX();	m_Orientation._14 = camera.getX();
+	m_Orientation._21 = -s.getY();	m_Orientation._22 = up.getY();	m_Orientation._23 = orientation.getY();	m_Orientation._24 = camera.getY();
+	m_Orientation._31 = -s.getZ();	m_Orientation._32 = up.getZ();	m_Orientation._33 = orientation.getZ();	m_Orientation._34 = camera.getZ();
 	m_Orientation._41 = 0.0f;	m_Orientation._42 = 0.0f;	m_Orientation._43 = 0.0f;			m_Orientation._44 = 1.0f;
 }
 
 // Render the camera's frustum
 void CCamera::Render(int intermediates) const
 {
 #if CONFIG2_GLES
 #warning TODO: implement camera frustum for GLES
 #else
 	Quad nearPoints;
 	Quad farPoints;
 
 	GetViewQuad(m_NearPlane, nearPoints);
 	GetViewQuad(m_FarPlane, farPoints);
 	for(int i = 0; i < 4; i++)
 	{
 		nearPoints[i] = m_Orientation.Transform(nearPoints[i]);
 		farPoints[i] = m_Orientation.Transform(farPoints[i]);
 	}
 
 	// near plane
 	glBegin(GL_POLYGON);
-		glVertex3fv(&nearPoints[0].X);
-		glVertex3fv(&nearPoints[1].X);
-		glVertex3fv(&nearPoints[2].X);
-		glVertex3fv(&nearPoints[3].X);
+		glVertex3fv(&nearPoints[0].Xref());
+		glVertex3fv(&nearPoints[1].Xref());
+		glVertex3fv(&nearPoints[2].Xref());
+		glVertex3fv(&nearPoints[3].Xref());
 	glEnd();
 
 	// far plane
 	glBegin(GL_POLYGON);
-		glVertex3fv(&farPoints[0].X);
-		glVertex3fv(&farPoints[1].X);
-		glVertex3fv(&farPoints[2].X);
-		glVertex3fv(&farPoints[3].X);
+		glVertex3fv(&farPoints[0].Xref());
+		glVertex3fv(&farPoints[1].Xref());
+		glVertex3fv(&farPoints[2].Xref());
+		glVertex3fv(&farPoints[3].Xref());
 	glEnd();
 
 	// connection lines
 	glBegin(GL_QUAD_STRIP);
-		glVertex3fv(&nearPoints[0].X);
-		glVertex3fv(&farPoints[0].X);
-		glVertex3fv(&nearPoints[1].X);
-		glVertex3fv(&farPoints[1].X);
-		glVertex3fv(&nearPoints[2].X);
-		glVertex3fv(&farPoints[2].X);
-		glVertex3fv(&nearPoints[3].X);
-		glVertex3fv(&farPoints[3].X);
-		glVertex3fv(&nearPoints[0].X);
-		glVertex3fv(&farPoints[0].X);
+		glVertex3fv(&nearPoints[0].Xref());
+		glVertex3fv(&farPoints[0].Xref());
+		glVertex3fv(&nearPoints[1].Xref());
+		glVertex3fv(&farPoints[1].Xref());
+		glVertex3fv(&nearPoints[2].Xref());
+		glVertex3fv(&farPoints[2].Xref());
+		glVertex3fv(&nearPoints[3].Xref());
+		glVertex3fv(&farPoints[3].Xref());
+		glVertex3fv(&nearPoints[0].Xref());
+		glVertex3fv(&farPoints[0].Xref());
 	glEnd();
 
 	// intermediate planes
 	CVector3D intermediatePoints[4];
 	for(int i = 0; i < intermediates; ++i)
 	{
 		float t = (i+1.0)/(intermediates+1.0);
 
 		for(int j = 0; j < 4; ++j)
 			intermediatePoints[j] = nearPoints[j]*t + farPoints[j]*(1.0-t);
 
 		glBegin(GL_POLYGON);
-			glVertex3fv(&intermediatePoints[0].X);
-			glVertex3fv(&intermediatePoints[1].X);
-			glVertex3fv(&intermediatePoints[2].X);
-			glVertex3fv(&intermediatePoints[3].X);
+			glVertex3fv(&intermediatePoints[0].Xref());
+			glVertex3fv(&intermediatePoints[1].Xref());
+			glVertex3fv(&intermediatePoints[2].Xref());
+			glVertex3fv(&intermediatePoints[3].Xref());
 		glEnd();
 	}
 #endif
 }
Index: source/graphics/CameraController.cpp
===================================================================
--- source/graphics/CameraController.cpp	(revision 24796)
+++ source/graphics/CameraController.cpp	(working copy)
@@ -1,719 +1,719 @@
 /* Copyright (C) 2021 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 .
 */
 
 #include "precompiled.h"
 
 #include "CameraController.h"
 
 #include "graphics/HFTracer.h"
 #include "graphics/Terrain.h"
 #include "graphics/scripting/JSInterface_GameView.h"
 #include "lib/input.h"
 #include "lib/timer.h"
 #include "maths/MathUtil.h"
 #include "maths/Matrix3D.h"
 #include "maths/Quaternion.h"
 #include "ps/ConfigDB.h"
 #include "ps/Game.h"
 #include "ps/Globals.h"
 #include "ps/Hotkey.h"
 #include "ps/Joystick.h"
 #include "ps/Pyrogenesis.h"
 #include "ps/TouchInput.h"
 #include "ps/World.h"
 #include "renderer/Renderer.h"
 #include "renderer/WaterManager.h"
 #include "simulation2/Simulation2.h"
 #include "simulation2/components/ICmpPosition.h"
 #include "simulation2/components/ICmpRangeManager.h"
 #include "simulation2/helpers/Los.h"
 
 extern int g_xres, g_yres;
 
 // Maximum distance outside the edge of the map that the camera's
 // focus point can be moved
 static const float CAMERA_EDGE_MARGIN = 2.0f * TERRAIN_TILE_SIZE;
 
 CCameraController::CCameraController(CCamera& camera)
 	: ICameraController(camera),
 	  m_ConstrainCamera(true),
 	  m_FollowEntity(INVALID_ENTITY),
 	  m_FollowFirstPerson(false),
 
 	  // Dummy values (these will be filled in by the config file)
 	  m_ViewScrollSpeed(0),
 	  m_ViewScrollSpeedModifier(1),
 	  m_ViewRotateXSpeed(0),
 	  m_ViewRotateXMin(0),
 	  m_ViewRotateXMax(0),
 	  m_ViewRotateXDefault(0),
 	  m_ViewRotateYSpeed(0),
 	  m_ViewRotateYSpeedWheel(0),
 	  m_ViewRotateYDefault(0),
 	  m_ViewRotateSpeedModifier(1),
 	  m_ViewDragSpeed(0),
 	  m_ViewZoomSpeed(0),
 	  m_ViewZoomSpeedWheel(0),
 	  m_ViewZoomMin(0),
 	  m_ViewZoomMax(0),
 	  m_ViewZoomDefault(0),
 	  m_ViewZoomSpeedModifier(1),
 	  m_ViewFOV(DEGTORAD(45.f)),
 	  m_ViewNear(2.f),
 	  m_ViewFar(4096.f),
 	  m_JoystickPanX(-1),
 	  m_JoystickPanY(-1),
 	  m_JoystickRotateX(-1),
 	  m_JoystickRotateY(-1),
 	  m_JoystickZoomIn(-1),
 	  m_JoystickZoomOut(-1),
 	  m_HeightSmoothness(0.5f),
 	  m_HeightMin(16.f),
 
 	  m_PosX(0, 0, 0.01f),
 	  m_PosY(0, 0, 0.01f),
 	  m_PosZ(0, 0, 0.01f),
 	  m_Zoom(0, 0, 0.1f),
 	  m_RotateX(0, 0, 0.001f),
 	  m_RotateY(0, 0, 0.001f)
 {
 	SViewPort vp;
 	vp.m_X = 0;
 	vp.m_Y = 0;
 	vp.m_Width = g_xres;
 	vp.m_Height = g_yres;
 	m_Camera.SetViewPort(vp);
 
 	SetCameraProjection();
 	SetupCameraMatrixSmooth(&m_Camera.m_Orientation);
 	m_Camera.UpdateFrustum();
 }
 
 CCameraController::~CCameraController() = default;
 
 void CCameraController::LoadConfig()
 {
 	CFG_GET_VAL("view.scroll.speed", m_ViewScrollSpeed);
 	CFG_GET_VAL("view.scroll.speed.modifier", m_ViewScrollSpeedModifier);
 	CFG_GET_VAL("view.rotate.x.speed", m_ViewRotateXSpeed);
 	CFG_GET_VAL("view.rotate.x.min", m_ViewRotateXMin);
 	CFG_GET_VAL("view.rotate.x.max", m_ViewRotateXMax);
 	CFG_GET_VAL("view.rotate.x.default", m_ViewRotateXDefault);
 	CFG_GET_VAL("view.rotate.y.speed", m_ViewRotateYSpeed);
 	CFG_GET_VAL("view.rotate.y.speed.wheel", m_ViewRotateYSpeedWheel);
 	CFG_GET_VAL("view.rotate.y.default", m_ViewRotateYDefault);
 	CFG_GET_VAL("view.rotate.speed.modifier", m_ViewRotateSpeedModifier);
 	CFG_GET_VAL("view.drag.speed", m_ViewDragSpeed);
 	CFG_GET_VAL("view.zoom.speed", m_ViewZoomSpeed);
 	CFG_GET_VAL("view.zoom.speed.wheel", m_ViewZoomSpeedWheel);
 	CFG_GET_VAL("view.zoom.min", m_ViewZoomMin);
 	CFG_GET_VAL("view.zoom.max", m_ViewZoomMax);
 	CFG_GET_VAL("view.zoom.default", m_ViewZoomDefault);
 	CFG_GET_VAL("view.zoom.speed.modifier", m_ViewZoomSpeedModifier);
 
 	CFG_GET_VAL("joystick.camera.pan.x", m_JoystickPanX);
 	CFG_GET_VAL("joystick.camera.pan.y", m_JoystickPanY);
 	CFG_GET_VAL("joystick.camera.rotate.x", m_JoystickRotateX);
 	CFG_GET_VAL("joystick.camera.rotate.y", m_JoystickRotateY);
 	CFG_GET_VAL("joystick.camera.zoom.in", m_JoystickZoomIn);
 	CFG_GET_VAL("joystick.camera.zoom.out", m_JoystickZoomOut);
 
 	CFG_GET_VAL("view.height.smoothness", m_HeightSmoothness);
 	CFG_GET_VAL("view.height.min", m_HeightMin);
 
 #define SETUP_SMOOTHNESS(CFG_PREFIX, SMOOTHED_VALUE) \
 	{ \
 		float smoothness = SMOOTHED_VALUE.GetSmoothness(); \
 		CFG_GET_VAL(CFG_PREFIX ".smoothness", smoothness); \
 		SMOOTHED_VALUE.SetSmoothness(smoothness); \
 	}
 
 	SETUP_SMOOTHNESS("view.pos", m_PosX);
 	SETUP_SMOOTHNESS("view.pos", m_PosY);
 	SETUP_SMOOTHNESS("view.pos", m_PosZ);
 	SETUP_SMOOTHNESS("view.zoom", m_Zoom);
 	SETUP_SMOOTHNESS("view.rotate.x", m_RotateX);
 	SETUP_SMOOTHNESS("view.rotate.y", m_RotateY);
 #undef SETUP_SMOOTHNESS
 
 	CFG_GET_VAL("view.near", m_ViewNear);
 	CFG_GET_VAL("view.far", m_ViewFar);
 	CFG_GET_VAL("view.fov", m_ViewFOV);
 
 	// Convert to radians
 	m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault));
 	m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault));
 	m_ViewFOV = DEGTORAD(m_ViewFOV);
 }
 
 void CCameraController::SetViewport(const SViewPort& vp)
 {
 	m_Camera.SetViewPort(vp);
 	SetCameraProjection();
 }
 
 void CCameraController::Update(const float deltaRealTime)
 {
 	// Calculate mouse movement
 	static int mouse_last_x = 0;
 	static int mouse_last_y = 0;
 	int mouse_dx = g_mouse_x - mouse_last_x;
 	int mouse_dy = g_mouse_y - mouse_last_y;
 	mouse_last_x = g_mouse_x;
 	mouse_last_y = g_mouse_y;
 
 	if (HotkeyIsPressed("camera.rotate.cw"))
 		m_RotateY.AddSmoothly(m_ViewRotateYSpeed * deltaRealTime);
 	if (HotkeyIsPressed("camera.rotate.ccw"))
 		m_RotateY.AddSmoothly(-m_ViewRotateYSpeed * deltaRealTime);
 	if (HotkeyIsPressed("camera.rotate.up"))
 		m_RotateX.AddSmoothly(-m_ViewRotateXSpeed * deltaRealTime);
 	if (HotkeyIsPressed("camera.rotate.down"))
 		m_RotateX.AddSmoothly(m_ViewRotateXSpeed * deltaRealTime);
 
 	float moveRightward = 0.f;
 	float moveForward = 0.f;
 
 	if (HotkeyIsPressed("camera.pan"))
 	{
 		moveRightward += m_ViewDragSpeed * mouse_dx;
 		moveForward += m_ViewDragSpeed * -mouse_dy;
 	}
 
 	if (g_mouse_active)
 	{
 		if (g_mouse_x >= g_xres - 2 && g_mouse_x < g_xres)
 			moveRightward += m_ViewScrollSpeed * deltaRealTime;
 		else if (g_mouse_x <= 3 && g_mouse_x >= 0)
 			moveRightward -= m_ViewScrollSpeed * deltaRealTime;
 
 		if (g_mouse_y >= g_yres - 2 && g_mouse_y < g_yres)
 			moveForward -= m_ViewScrollSpeed * deltaRealTime;
 		else if (g_mouse_y <= 3 && g_mouse_y >= 0)
 			moveForward += m_ViewScrollSpeed * deltaRealTime;
 	}
 
 	if (HotkeyIsPressed("camera.right"))
 		moveRightward += m_ViewScrollSpeed * deltaRealTime;
 	if (HotkeyIsPressed("camera.left"))
 		moveRightward -= m_ViewScrollSpeed * deltaRealTime;
 	if (HotkeyIsPressed("camera.up"))
 		moveForward += m_ViewScrollSpeed * deltaRealTime;
 	if (HotkeyIsPressed("camera.down"))
 		moveForward -= m_ViewScrollSpeed * deltaRealTime;
 
 	if (g_Joystick.IsEnabled())
 	{
 		// This could all be improved with extra speed and sensitivity settings
 		// (maybe use pow to allow finer control?), and inversion settings
 
 		moveRightward += g_Joystick.GetAxisValue(m_JoystickPanX) * m_ViewScrollSpeed * deltaRealTime;
 		moveForward -= g_Joystick.GetAxisValue(m_JoystickPanY) * m_ViewScrollSpeed * deltaRealTime;
 
 		m_RotateX.AddSmoothly(g_Joystick.GetAxisValue(m_JoystickRotateX) * m_ViewRotateXSpeed * deltaRealTime);
 		m_RotateY.AddSmoothly(-g_Joystick.GetAxisValue(m_JoystickRotateY) * m_ViewRotateYSpeed * deltaRealTime);
 
 		// Use a +1 bias for zoom because I want this to work with trigger buttons that default to -1
 		m_Zoom.AddSmoothly((g_Joystick.GetAxisValue(m_JoystickZoomIn) + 1.0f) / 2.0f * m_ViewZoomSpeed * deltaRealTime);
 		m_Zoom.AddSmoothly(-(g_Joystick.GetAxisValue(m_JoystickZoomOut) + 1.0f) / 2.0f * m_ViewZoomSpeed * deltaRealTime);
 	}
 
 	if (moveRightward || moveForward)
 	{
 		// Break out of following mode when the user starts scrolling
 		m_FollowEntity = INVALID_ENTITY;
 
 		float s = sin(m_RotateY.GetSmoothedValue());
 		float c = cos(m_RotateY.GetSmoothedValue());
 		m_PosX.AddSmoothly(c * moveRightward);
 		m_PosZ.AddSmoothly(-s * moveRightward);
 		m_PosX.AddSmoothly(s * moveForward);
 		m_PosZ.AddSmoothly(c * moveForward);
 	}
 
 	if (m_FollowEntity)
 	{
 		CmpPtr cmpPosition(*(g_Game->GetSimulation2()), m_FollowEntity);
 		CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY);
 		if (cmpPosition && cmpPosition->IsInWorld() &&
 			cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FollowEntity, g_Game->GetViewedPlayerID()) == LosVisibility::VISIBLE)
 		{
 			// Get the most recent interpolated position
 			float frameOffset = g_Game->GetSimulation2()->GetLastFrameOffset();
 			CMatrix3D transform = cmpPosition->GetInterpolatedTransform(frameOffset);
 			CVector3D pos = transform.GetTranslation();
 
 			if (m_FollowFirstPerson)
 			{
 				float x, z, angle;
 				cmpPosition->GetInterpolatedPosition2D(frameOffset, x, z, angle);
 				float height = 4.f;
 				m_Camera.m_Orientation.SetIdentity();
 				m_Camera.m_Orientation.RotateX(static_cast(M_PI) / 24.f);
 				m_Camera.m_Orientation.RotateY(angle);
-				m_Camera.m_Orientation.Translate(pos.X, pos.Y + height, pos.Z);
+				m_Camera.m_Orientation.Translate(pos.getX(), pos.getY() + height, pos.getZ());
 
 				m_Camera.UpdateFrustum();
 				return;
 			}
 			else
 			{
 				// Move the camera to match the unit
 				CCamera targetCam = m_Camera;
 				SetupCameraMatrixSmoothRot(&targetCam.m_Orientation);
 
 				CVector3D pivot = GetSmoothPivot(targetCam);
 				CVector3D delta = pos - pivot;
-				m_PosX.AddSmoothly(delta.X);
-				m_PosY.AddSmoothly(delta.Y);
-				m_PosZ.AddSmoothly(delta.Z);
+				m_PosX.AddSmoothly(delta.getX());
+				m_PosY.AddSmoothly(delta.getY());
+				m_PosZ.AddSmoothly(delta.getZ());
 			}
 		}
 		else
 		{
 			// The unit disappeared (died or garrisoned etc), so stop following it
 			m_FollowEntity = INVALID_ENTITY;
 		}
 	}
 
 	if (HotkeyIsPressed("camera.zoom.in"))
 		m_Zoom.AddSmoothly(-m_ViewZoomSpeed * deltaRealTime);
 	if (HotkeyIsPressed("camera.zoom.out"))
 		m_Zoom.AddSmoothly(m_ViewZoomSpeed * deltaRealTime);
 
 	if (m_ConstrainCamera)
 		m_Zoom.ClampSmoothly(m_ViewZoomMin, m_ViewZoomMax);
 
 	float zoomDelta = -m_Zoom.Update(deltaRealTime);
 	if (zoomDelta)
 	{
 		CVector3D forwards = m_Camera.GetOrientation().GetIn();
-		m_PosX.AddSmoothly(forwards.X * zoomDelta);
-		m_PosY.AddSmoothly(forwards.Y * zoomDelta);
-		m_PosZ.AddSmoothly(forwards.Z * zoomDelta);
+		m_PosX.AddSmoothly(forwards.getX() * zoomDelta);
+		m_PosY.AddSmoothly(forwards.getY() * zoomDelta);
+		m_PosZ.AddSmoothly(forwards.getZ() * zoomDelta);
 	}
 
 	if (m_ConstrainCamera)
 		m_RotateX.ClampSmoothly(DEGTORAD(m_ViewRotateXMin), DEGTORAD(m_ViewRotateXMax));
 
 	FocusHeight(true);
 
 	// Ensure the ViewCamera focus is inside the map with the chosen margins
 	// if not so - apply margins to the camera
 	if (m_ConstrainCamera)
 	{
 		CCamera targetCam = m_Camera;
 		SetupCameraMatrixSmoothRot(&targetCam.m_Orientation);
 
 		CTerrain* pTerrain = g_Game->GetWorld()->GetTerrain();
 
 		CVector3D pivot = GetSmoothPivot(targetCam);
 		CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot;
 
 		CVector3D desiredPivot = pivot;
 
 		CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY);
 		if (cmpRangeManager && cmpRangeManager->GetLosCircular())
 		{
 			// Clamp to a circular region around the center of the map
 			float r = pTerrain->GetMaxX() / 2;
-			CVector3D center(r, desiredPivot.Y, r);
+			CVector3D center(r, desiredPivot.getY(), r);
 			float dist = (desiredPivot - center).Length();
 			if (dist > r - CAMERA_EDGE_MARGIN)
 				desiredPivot = center + (desiredPivot - center).Normalized() * (r - CAMERA_EDGE_MARGIN);
 		}
 		else
 		{
 			// Clamp to the square edges of the map
-			desiredPivot.X = Clamp(desiredPivot.X, pTerrain->GetMinX() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxX() - CAMERA_EDGE_MARGIN);
-			desiredPivot.Z = Clamp(desiredPivot.Z, pTerrain->GetMinZ() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxZ() - CAMERA_EDGE_MARGIN);
+			desiredPivot.Xref() = Clamp(desiredPivot.getX(), pTerrain->GetMinX() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxX() - CAMERA_EDGE_MARGIN);
+			desiredPivot.Zref() = Clamp(desiredPivot.getZ(), pTerrain->GetMinZ() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxZ() - CAMERA_EDGE_MARGIN);
 		}
 
 		// Update the position so that pivot is within the margin
-		m_PosX.SetValueSmoothly(desiredPivot.X + delta.X);
-		m_PosZ.SetValueSmoothly(desiredPivot.Z + delta.Z);
+		m_PosX.SetValueSmoothly(desiredPivot.getX() + delta.getX());
+		m_PosZ.SetValueSmoothly(desiredPivot.getZ() + delta.getZ());
 	}
 
 	m_PosX.Update(deltaRealTime);
 	m_PosY.Update(deltaRealTime);
 	m_PosZ.Update(deltaRealTime);
 
 	// Handle rotation around the Y (vertical) axis
 	{
 		CCamera targetCam = m_Camera;
 		SetupCameraMatrixSmooth(&targetCam.m_Orientation);
 
 		float rotateYDelta = m_RotateY.Update(deltaRealTime);
 		if (rotateYDelta)
 		{
 			// We've updated RotateY, and need to adjust Pos so that it's still
 			// facing towards the original focus point (the terrain in the center
 			// of the screen).
 
 			CVector3D upwards(0.0f, 1.0f, 0.0f);
 
 			CVector3D pivot = GetSmoothPivot(targetCam);
 			CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot;
 
 			CQuaternion q;
 			q.FromAxisAngle(upwards, rotateYDelta);
 			CVector3D d = q.Rotate(delta) - delta;
 
-			m_PosX.Add(d.X);
-			m_PosY.Add(d.Y);
-			m_PosZ.Add(d.Z);
+			m_PosX.Add(d.getX());
+			m_PosY.Add(d.getY());
+			m_PosZ.Add(d.getZ());
 		}
 	}
 
 	// Handle rotation around the X (sideways, relative to camera) axis
 	{
 		CCamera targetCam = m_Camera;
 		SetupCameraMatrixSmooth(&targetCam.m_Orientation);
 
 		float rotateXDelta = m_RotateX.Update(deltaRealTime);
 		if (rotateXDelta)
 		{
 			CVector3D rightwards = targetCam.GetOrientation().GetLeft() * -1.0f;
 
 			CVector3D pivot = GetSmoothPivot(targetCam);
 			CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot;
 
 			CQuaternion q;
 			q.FromAxisAngle(rightwards, rotateXDelta);
 			CVector3D d = q.Rotate(delta) - delta;
 
-			m_PosX.Add(d.X);
-			m_PosY.Add(d.Y);
-			m_PosZ.Add(d.Z);
+			m_PosX.Add(d.getX());
+			m_PosY.Add(d.getY());
+			m_PosZ.Add(d.getZ());
 		}
 	}
 
 	/* This is disabled since it doesn't seem necessary:
 
 	// Ensure the camera's near point is never inside the terrain
 	if (m_ConstrainCamera)
 	{
 	CMatrix3D target;
 	target.SetIdentity();
 	target.RotateX(m_RotateX.GetValue());
 	target.RotateY(m_RotateY.GetValue());
 	target.Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue());
 
 	CVector3D nearPoint = target.GetTranslation() + target.GetIn() * defaultNear;
 	float ground = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z);
 	float limit = ground + 16.f;
 	if (nearPoint.Y < limit)
 	m_PosY.AddSmoothly(limit - nearPoint.Y);
 	}
 	*/
 
 	m_RotateY.Wrap(-static_cast(M_PI), static_cast(M_PI));
 
 	// Update the camera matrix
 	SetCameraProjection();
 	SetupCameraMatrixSmooth(&m_Camera.m_Orientation);
 	m_Camera.UpdateFrustum();
 }
 
 CVector3D CCameraController::GetSmoothPivot(CCamera& camera) const
 {
 	return camera.GetOrientation().GetTranslation() + camera.GetOrientation().GetIn() * m_Zoom.GetSmoothedValue();
 }
 
 CVector3D CCameraController::GetCameraPivot() const
 {
 	return GetSmoothPivot(m_Camera);
 }
 
 CVector3D CCameraController::GetCameraPosition() const
 {
 	return CVector3D(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue());
 }
 
 CVector3D CCameraController::GetCameraRotation() const
 {
 	// The angle of rotation around the Z axis is not used.
 	return CVector3D(m_RotateX.GetValue(), m_RotateY.GetValue(), 0.0f);
 }
 
 float CCameraController::GetCameraZoom() const
 {
 	return m_Zoom.GetValue();
 }
 
 void CCameraController::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom)
 {
-	m_PosX.SetValue(pos.X);
-	m_PosY.SetValue(pos.Y);
-	m_PosZ.SetValue(pos.Z);
+	m_PosX.SetValue(pos.getX());
+	m_PosY.SetValue(pos.getY());
+	m_PosZ.SetValue(pos.getZ());
 	m_RotateX.SetValue(rotX);
 	m_RotateY.SetValue(rotY);
 	m_Zoom.SetValue(zoom);
 
 	FocusHeight(false);
 
 	SetupCameraMatrixNonSmooth(&m_Camera.m_Orientation);
 	m_Camera.UpdateFrustum();
 
 	// Break out of following mode so the camera really moves to the target
 	m_FollowEntity = INVALID_ENTITY;
 }
 
 void CCameraController::MoveCameraTarget(const CVector3D& target)
 {
 	// Maintain the same orientation and level of zoom, if we can
 	// (do this by working out the point the camera is looking at, saving
 	//  the difference between that position and the camera point, and restoring
 	//  that difference to our new target)
 
 	CCamera targetCam = m_Camera;
 	SetupCameraMatrixNonSmooth(&targetCam.m_Orientation);
 
 	CVector3D pivot = GetSmoothPivot(targetCam);
 	CVector3D delta = target - pivot;
 
-	m_PosX.SetValueSmoothly(delta.X + m_PosX.GetValue());
-	m_PosZ.SetValueSmoothly(delta.Z + m_PosZ.GetValue());
+	m_PosX.SetValueSmoothly(delta.getX() + m_PosX.GetValue());
+	m_PosZ.SetValueSmoothly(delta.getZ() + m_PosZ.GetValue());
 
 	FocusHeight(false);
 
 	// Break out of following mode so the camera really moves to the target
 	m_FollowEntity = INVALID_ENTITY;
 }
 
 void CCameraController::ResetCameraTarget(const CVector3D& target)
 {
 	CMatrix3D orientation;
 	orientation.SetIdentity();
 	orientation.RotateX(DEGTORAD(m_ViewRotateXDefault));
 	orientation.RotateY(DEGTORAD(m_ViewRotateYDefault));
 
 	CVector3D delta = orientation.GetIn() * m_ViewZoomDefault;
-	m_PosX.SetValue(target.X - delta.X);
-	m_PosY.SetValue(target.Y - delta.Y);
-	m_PosZ.SetValue(target.Z - delta.Z);
+	m_PosX.SetValue(target.getX() - delta.getX());
+	m_PosY.SetValue(target.getY() - delta.getY());
+	m_PosZ.SetValue(target.getZ() - delta.getZ());
 	m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault));
 	m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault));
 	m_Zoom.SetValue(m_ViewZoomDefault);
 
 	FocusHeight(false);
 
 	SetupCameraMatrixSmooth(&m_Camera.m_Orientation);
 	m_Camera.UpdateFrustum();
 
 	// Break out of following mode so the camera really moves to the target
 	m_FollowEntity = INVALID_ENTITY;
 }
 
 void CCameraController::FollowEntity(entity_id_t entity, bool firstPerson)
 {
 	m_FollowEntity = entity;
 	m_FollowFirstPerson = firstPerson;
 }
 
 entity_id_t CCameraController::GetFollowedEntity()
 {
 	return m_FollowEntity;
 }
 
 void CCameraController::SetCameraProjection()
 {
 	m_Camera.SetPerspectiveProjection(m_ViewNear, m_ViewFar, m_ViewFOV);
 }
 
 void CCameraController::ResetCameraAngleZoom()
 {
 	CCamera targetCam = m_Camera;
 	SetupCameraMatrixNonSmooth(&targetCam.m_Orientation);
 
 	// Compute the zoom adjustment to get us back to the default
 	CVector3D forwards = targetCam.GetOrientation().GetIn();
 
 	CVector3D pivot = GetSmoothPivot(targetCam);
 	CVector3D delta = pivot - targetCam.GetOrientation().GetTranslation();
 	float dist = delta.Dot(forwards);
 	m_Zoom.AddSmoothly(m_ViewZoomDefault - dist);
 
 	// Reset orientations to default
 	m_RotateX.SetValueSmoothly(DEGTORAD(m_ViewRotateXDefault));
 	m_RotateY.SetValueSmoothly(DEGTORAD(m_ViewRotateYDefault));
 }
 
 void CCameraController::SetupCameraMatrixSmooth(CMatrix3D* orientation)
 {
 	orientation->SetIdentity();
 	orientation->RotateX(m_RotateX.GetSmoothedValue());
 	orientation->RotateY(m_RotateY.GetSmoothedValue());
 	orientation->Translate(m_PosX.GetSmoothedValue(), m_PosY.GetSmoothedValue(), m_PosZ.GetSmoothedValue());
 }
 
 void CCameraController::SetupCameraMatrixSmoothRot(CMatrix3D* orientation)
 {
 	orientation->SetIdentity();
 	orientation->RotateX(m_RotateX.GetSmoothedValue());
 	orientation->RotateY(m_RotateY.GetSmoothedValue());
 	orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue());
 }
 
 void CCameraController::SetupCameraMatrixNonSmooth(CMatrix3D* orientation)
 {
 	orientation->SetIdentity();
 	orientation->RotateX(m_RotateX.GetValue());
 	orientation->RotateY(m_RotateY.GetValue());
 	orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue());
 }
 
 void CCameraController::FocusHeight(bool smooth)
 {
 	/*
 	The camera pivot height is moved towards ground level.
 	To prevent excessive zoom when looking over a cliff,
 	the target ground level is the maximum of the ground level at the camera's near and pivot points.
 	The ground levels are filtered to achieve smooth camera movement.
 	The filter radius is proportional to the zoom level.
 	The camera height is clamped to prevent map penetration.
 	*/
 
 	if (!m_ConstrainCamera)
 		return;
 
 	CCamera targetCam = m_Camera;
 	SetupCameraMatrixSmoothRot(&targetCam.m_Orientation);
 
 	const CVector3D position = targetCam.GetOrientation().GetTranslation();
 	const CVector3D forwards = targetCam.GetOrientation().GetIn();
 
 	// horizontal view radius
-	const float radius = sqrtf(forwards.X * forwards.X + forwards.Z * forwards.Z) * m_Zoom.GetSmoothedValue();
+	const float radius = forwards.HorizontalLength() * m_Zoom.GetSmoothedValue();
 	const float near_radius = radius * m_HeightSmoothness;
 	const float pivot_radius = radius * m_HeightSmoothness;
 
 	const CVector3D nearPoint = position + forwards * m_ViewNear;
 	const CVector3D pivotPoint = position + forwards * m_Zoom.GetSmoothedValue();
 
 	const float ground = std::max(
-		g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z),
+		g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.getX(), nearPoint.getZ()),
 		g_Renderer.GetWaterManager()->m_WaterHeight);
 
 	// filter ground levels for smooth camera movement
-	const float filtered_near_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(nearPoint.X, nearPoint.Z, near_radius);
-	const float filtered_pivot_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(pivotPoint.X, pivotPoint.Z, pivot_radius);
+	const float filtered_near_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(nearPoint.getX(), nearPoint.getZ(), near_radius);
+	const float filtered_pivot_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(pivotPoint.getX(), pivotPoint.getZ(), pivot_radius);
 
 	// filtered maximum visible ground level in view
 	const float filtered_ground = std::max(
 		std::max(filtered_near_ground, filtered_pivot_ground),
 		g_Renderer.GetWaterManager()->m_WaterHeight);
 
 	// target camera height above pivot point
-	const float pivot_height = -forwards.Y * (m_Zoom.GetSmoothedValue() - m_ViewNear);
+	const float pivot_height = -forwards.getY() * (m_Zoom.GetSmoothedValue() - m_ViewNear);
 
 	// minimum camera height above filtered ground level
 	const float min_height = (m_HeightMin + ground - filtered_ground);
 
 	const float target_height = std::max(pivot_height, min_height);
-	const float height = (nearPoint.Y - filtered_ground);
+	const float height = (nearPoint.getY() - filtered_ground);
 	const float diff = target_height - height;
 
 	if (fabsf(diff) < 0.0001f)
 		return;
 
 	if (smooth)
 		m_PosY.AddSmoothly(diff);
 	else
 		m_PosY.Add(diff);
 }
 
 InReaction CCameraController::HandleEvent(const SDL_Event_* ev)
 {
 	switch (ev->ev.type)
 	{
 	case SDL_HOTKEYPRESS:
 	{
 		std::string hotkey = static_cast(ev->ev.user.data1);
 		if (hotkey == "camera.reset")
 		{
 			ResetCameraAngleZoom();
 			return IN_HANDLED;
 		}
 		return IN_PASS;
 	}
 
 	case SDL_HOTKEYDOWN:
 	{
 		std::string hotkey = static_cast(ev->ev.user.data1);
 
 		// Mouse wheel must be treated using events instead of polling,
 		// because SDL auto-generates a sequence of mousedown/mouseup events
 		// and we never get to see the "down" state inside Update().
 		if (hotkey == "camera.zoom.wheel.in")
 		{
 			m_Zoom.AddSmoothly(-m_ViewZoomSpeedWheel);
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.zoom.wheel.out")
 		{
 			m_Zoom.AddSmoothly(m_ViewZoomSpeedWheel);
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.rotate.wheel.cw")
 		{
 			m_RotateY.AddSmoothly(m_ViewRotateYSpeedWheel);
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.rotate.wheel.ccw")
 		{
 			m_RotateY.AddSmoothly(-m_ViewRotateYSpeedWheel);
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.scroll.speed.increase")
 		{
 			m_ViewScrollSpeed *= m_ViewScrollSpeedModifier;
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.scroll.speed.decrease")
 		{
 			m_ViewScrollSpeed /= m_ViewScrollSpeedModifier;
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.rotate.speed.increase")
 		{
 			m_ViewRotateXSpeed *= m_ViewRotateSpeedModifier;
 			m_ViewRotateYSpeed *= m_ViewRotateSpeedModifier;
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.rotate.speed.decrease")
 		{
 			m_ViewRotateXSpeed /= m_ViewRotateSpeedModifier;
 			m_ViewRotateYSpeed /= m_ViewRotateSpeedModifier;
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.zoom.speed.increase")
 		{
 			m_ViewZoomSpeed *= m_ViewZoomSpeedModifier;
 			return IN_HANDLED;
 		}
 		else if (hotkey == "camera.zoom.speed.decrease")
 		{
 			m_ViewZoomSpeed /= m_ViewZoomSpeedModifier;
 			return IN_HANDLED;
 		}
 		return IN_PASS;
 	}
 	}
 
 	return IN_PASS;
 }
Index: source/graphics/CinemaManager.cpp
===================================================================
--- source/graphics/CinemaManager.cpp	(revision 24796)
+++ source/graphics/CinemaManager.cpp	(working copy)
@@ -1,256 +1,256 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include 
 #include 
 
 #include "graphics/CinemaManager.h"
-
+#include "graphics/Color.h"
 #include "graphics/Camera.h"
 #include "graphics/GameView.h"
 #include "lib/ogl.h"
 #include "maths/MathUtil.h"
 #include "maths/Quaternion.h"
 #include "maths/Vector3D.h"
 #include "maths/Vector4D.h"
 #include "ps/CLogger.h"
 #include "ps/ConfigDB.h"
 #include "ps/CStr.h"
 #include "ps/Game.h"
 #include "ps/GameSetup/Config.h"
 #include "ps/Hotkey.h"
 #include "ps/World.h"
 #include "simulation2/components/ICmpCinemaManager.h"
 #include "simulation2/components/ICmpOverlayRenderer.h"
 #include "simulation2/components/ICmpRangeManager.h"
 #include "simulation2/components/ICmpSelectable.h"
 #include "simulation2/components/ICmpTerritoryManager.h"
 #include "simulation2/MessageTypes.h"
 #include "simulation2/system/ComponentManager.h"
 #include "simulation2/Simulation2.h"
 #include "renderer/Renderer.h"
 
 
 CCinemaManager::CCinemaManager()
 	: m_DrawPaths(false)
 {
 }
 
 void CCinemaManager::Update(const float deltaRealTime) const
 {
 	CmpPtr cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity());
 	if (!cmpCinemaManager)
 		return;
 
 	if (IsPlaying())
 		cmpCinemaManager->PlayQueue(deltaRealTime, g_Game->GetView()->GetCamera());
 }
 
 void CCinemaManager::Render() const
 {
 	if (IsEnabled())
 		DrawBars();
 	else if (m_DrawPaths)
 		DrawPaths();
 }
 
 void CCinemaManager::DrawPaths() const
 {
 	CmpPtr cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity());
 	if (!cmpCinemaManager)
 		return;
 
 	for (const std::pair& p : cmpCinemaManager->GetPaths())
 	{
-		DrawSpline(p.second, CColor(0.2f, 0.2f, 1.f, 0.9f), 128, true);
-		DrawNodes(p.second, CColor(0.1f, 1.f, 0.f, 1.f));
+		DrawSpline(p.second, RGBAcolor(0.2f, 0.2f, 1.f, 0.9f), 128, true);
+		DrawNodes(p.second, RGBAcolor(0.1f, 1.f, 0.f, 1.f));
 
 		if (p.second.GetTargetSpline().GetAllNodes().empty())
 			continue;
 
-		DrawSpline(p.second.GetTargetSpline(), CColor(1.f, 0.3f, 0.4f, 0.9f), 128, true);
-		DrawNodes(p.second.GetTargetSpline(), CColor(1.f, 0.1f, 0.f, 1.f));
+		DrawSpline(p.second.GetTargetSpline(), RGBAcolor(1.f, 0.3f, 0.4f, 0.9f), 128, true);
+		DrawNodes(p.second.GetTargetSpline(), RGBAcolor(1.f, 0.1f, 0.f, 1.f));
 	}
 }
 
-void CCinemaManager::DrawSpline(const RNSpline& spline, const CColor& splineColor, int smoothness, bool lines) const
+void CCinemaManager::DrawSpline(const RNSpline& spline, const RGBAcolor& splineColor, int smoothness, bool lines) const
 {
 	if (spline.GetAllNodes().size() < 2)
 		return;
 	if (spline.GetAllNodes().size() == 2 && lines)
 		smoothness = 2;
 
 	float start = spline.MaxDistance.ToFloat() / smoothness;
 	float time = 0;
 
 #if CONFIG2_GLES
 	#warning TODO : implement CCinemaPath on GLES
 #else
 
 	glDisable(GL_DEPTH_TEST);
 	glEnable(GL_BLEND);
-	glColor4f(splineColor.r, splineColor.g, splineColor.b, splineColor.a);
+	glColor4f(splineColor.getR(), splineColor.getG(), splineColor.getB(), splineColor.getA());
 	if (lines)
 	{
 		glLineWidth(1.8f);
 		glEnable(GL_LINE_SMOOTH);
 		glBegin(GL_LINE_STRIP);
 		for (int i = 0; i <= smoothness; ++i)
 		{
 			time = start * i / spline.MaxDistance.ToFloat();
 			CVector3D tmp = spline.GetPosition(time);
-			glVertex3f(tmp.X, tmp.Y, tmp.Z);
+			glVertex3f(tmp.getX(), tmp.getY(), tmp.getZ());
 		}
 		glEnd();
 
 		// Height indicator
 		if (g_Game && g_Game->GetWorld() && g_Game->GetWorld()->GetTerrain())
 		{
 			glLineWidth(1.1f);
 			glBegin(GL_LINES);
 			for (int i = 0; i <= smoothness; ++i)
 			{
 				time = start * i / spline.MaxDistance.ToFloat();
 				CVector3D tmp = spline.GetPosition(time);
-				float groundY = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(tmp.X, tmp.Z);
-				glVertex3f(tmp.X, tmp.Y, tmp.Z);
-				glVertex3f(tmp.X, groundY, tmp.Z);
+				float groundY = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(tmp.getX(), tmp.getZ());
+				glVertex3f(tmp.getX(), tmp.getY(), tmp.getZ());
+				glVertex3f(tmp.getX(), groundY, tmp.getZ());
 			}
 			glEnd();
 		}
 
 		glDisable(GL_LINE_SMOOTH);
 		glLineWidth(1.0f);
 	}
 	else
 	{
 		smoothness /= 2;
 		start = spline.MaxDistance.ToFloat() / smoothness;
 		glEnable(GL_POINT_SMOOTH);
 		glPointSize(3.0f);
 		glBegin(GL_POINTS);
 		for (int i = 0; i <= smoothness; ++i)
 		{
 			time = start * i / spline.MaxDistance.ToFloat();
 			CVector3D tmp = spline.GetPosition(time);
-			glVertex3f(tmp.X, tmp.Y, tmp.Z);
+			glVertex3f(tmp.getX(), tmp.getY(), tmp.getZ());
 		}
 		glEnd();
 		glPointSize(1.0f);
 		glDisable(GL_POINT_SMOOTH);
 	}
 	glDisable(GL_BLEND);
 	glEnable(GL_DEPTH_TEST);
 
 #endif
 }
 
-void CCinemaManager::DrawNodes(const RNSpline& spline, const CColor& nodeColor) const
+void CCinemaManager::DrawNodes(const RNSpline& spline, const RGBAcolor& nodeColor) const
 {
 #if CONFIG2_GLES
 	#warning TODO : implement CCinemaPath on GLES
 #else
 
 	glDisable(GL_DEPTH_TEST);
 	glEnable(GL_POINT_SMOOTH);
 	glPointSize(7.0f);
-	glColor4f(nodeColor.r, nodeColor.g, nodeColor.b, nodeColor.a);
+	glColor4f(nodeColor.getR(), nodeColor.getG(), nodeColor.getB(), nodeColor.getA());
 	glBegin(GL_POINTS);
 
 	for (const SplineData& node : spline.GetAllNodes())
-		glVertex3f(node.Position.X.ToFloat(), node.Position.Y.ToFloat(), node.Position.Z.ToFloat());
+		glVertex3f(node.Position.getX().ToFloat(), node.Position.getY().ToFloat(), node.Position.getZ().ToFloat());
 
 	glEnd();
 	glPointSize(1.0f);
 	glDisable(GL_POINT_SMOOTH);
 	glEnable(GL_DEPTH_TEST);
 #endif
 }
 
 void CCinemaManager::DrawBars() const
 {
 	int height = (float)g_xres / 2.39f;
 	int shift = (g_yres - height) / 2;
 	if (shift <= 0)
 		return;
 
 #if CONFIG2_GLES
 	#warning TODO : implement bars for GLES
 #else
 	// Set up transform for GL bars
 	glMatrixMode(GL_PROJECTION);
 	glPushMatrix();
 	glLoadIdentity();
 	glMatrixMode(GL_MODELVIEW);
 	glPushMatrix();
 	glLoadIdentity();
 	CMatrix3D transform;
 	transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
 	glLoadMatrixf(&transform._11);
 
 	glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
 
 	glEnable(GL_BLEND);
 	glDisable(GL_DEPTH_TEST);
 
 	glBegin(GL_QUADS);
 	glVertex2i(0, 0);
 	glVertex2i(g_xres, 0);
 	glVertex2i(g_xres, shift);
 	glVertex2i(0, shift);
 	glEnd();
 
 	glBegin(GL_QUADS);
 	glVertex2i(0, g_yres - shift);
 	glVertex2i(g_xres, g_yres - shift);
 	glVertex2i(g_xres, g_yres);
 	glVertex2i(0, g_yres);
 	glEnd();
 
 	glDisable(GL_BLEND);
 	glEnable(GL_DEPTH_TEST);
 
 	// Restore transform
 	glMatrixMode(GL_PROJECTION);
 	glPopMatrix();
 	glMatrixMode(GL_MODELVIEW);
 	glPopMatrix();
 #endif
 }
 
 bool CCinemaManager::IsEnabled() const
 {
 	CmpPtr cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity());
 	return cmpCinemaManager && cmpCinemaManager->IsEnabled();
 }
 
 bool CCinemaManager::IsPlaying() const
 {
 	return IsEnabled() && g_Game && !g_Game->m_Paused;
 }
 
 bool CCinemaManager::GetPathsDrawing() const
 {
 	return m_DrawPaths;
 }
 
 void CCinemaManager::SetPathsDrawing(const bool drawPath)
 {
 	m_DrawPaths = drawPath;
 }
Index: source/graphics/CinemaManager.h
===================================================================
--- source/graphics/CinemaManager.h	(revision 24796)
+++ source/graphics/CinemaManager.h	(working copy)
@@ -1,61 +1,61 @@
 /* Copyright (C) 2021 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 .
  */
 
 #ifndef INCLUDED_CINEMAMANAGER
 #define INCLUDED_CINEMAMANAGER
 
 #include "lib/input.h" // InReaction - can't forward-declare enum
-#include "graphics/Color.h"
 #include "ps/CStr.h"
 #include "simulation2/helpers/CinemaPath.h"
 
+class RGBAcolor;
 
 /**
  * Class for in game playing of cinematics. Should only be instantiated in CGameView.
  */
 class CCinemaManager
 {
 public:
 	CCinemaManager();
 	~CCinemaManager() {}
 
 	/**
 	 * Renders black bars and paths (if enabled)
 	 */
 	void Render() const;
 	void DrawBars() const;
 	void DrawPaths() const;
-	void DrawSpline(const RNSpline& spline, const CColor& splineColor, int smoothness, bool lines) const;
-	void DrawNodes(const RNSpline& spline, const CColor& nodesColor) const;
+	void DrawSpline(const RNSpline& spline, const RGBAcolor& splineColor, int smoothness, bool lines) const;
+	void DrawNodes(const RNSpline& spline, const RGBAcolor& nodesColor) const;
 
 	bool IsPlaying() const;
 	bool IsEnabled() const;
 
 	/**
 	 * Updates CCinemManager and current path
 	 * @param deltaRealTime Elapsed real time since the last frame.
 	 */
 	void Update(const float deltaRealTime) const;
 
 	bool GetPathsDrawing() const;
 	void SetPathsDrawing(const bool drawPath);
 
 private:
 	bool m_DrawPaths;
 };
 
 #endif
Index: source/graphics/Color.cpp
===================================================================
--- source/graphics/Color.cpp	(revision 24796)
+++ source/graphics/Color.cpp	(working copy)
@@ -1,145 +1,158 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include "graphics/Color.h"
 
 #include "graphics/SColor.h"
 #include "maths/MathUtil.h"
+#include "maths/Vector4D.h"
 #include "lib/sse.h"
 #include "ps/CLogger.h"
 #include "ps/CStr.h"
 
 #if COMPILER_HAS_SSE
 #include 
 #endif
 
-static SColor4ub ConvertRGBColorTo4ubFallback(const RGBColor& src)
+static SColor4ub ConvertRGBcolorTo4ubFallback(const RGBcolor& src)
 {
 	SColor4ub result;
-	result.R = Clamp(static_cast(src.X * 255), 0, 255);
-	result.G = Clamp(static_cast(src.Y * 255), 0, 255);
-	result.B = Clamp(static_cast(src.Z * 255), 0, 255);
+	result.R = static_cast(Clamp(static_cast(src.getR() * 256.0f), 0, 255));
+	result.G = static_cast(Clamp(static_cast(src.getG() * 256.0f), 0, 255));
+	result.B = static_cast(Clamp(static_cast(src.getB() * 256.0f), 0, 255));
 	result.A = 255;
 	return result;
 }
 
 // on IA32, this is replaced by an SSE assembly version in ia32.cpp
-SColor4ub (*ConvertRGBColorTo4ub)(const RGBColor& src) = ConvertRGBColorTo4ubFallback;
+SColor4ub (*ConvertRGBcolorTo4ub)(const RGBcolor& src) = ConvertRGBcolorTo4ubFallback;
 
 
 // Assembler-optimized function for color conversion
 #if COMPILER_HAS_SSE
-static SColor4ub ConvertRGBColorTo4ubSSE(const RGBColor& src)
+static SColor4ub ConvertRGBcolorTo4ubSSE(const RGBcolor& src)
 {
 	const __m128 zero = _mm_setzero_ps();
 	const __m128 _255 = _mm_set_ss(255.0f);
-	__m128 r = _mm_load_ss(&src.X);
-	__m128 g = _mm_load_ss(&src.Y);
-	__m128 b = _mm_load_ss(&src.Z);
+	__m128 r = _mm_load_ss(src.pR());
+	__m128 g = _mm_load_ss(src.pG());
+	__m128 b = _mm_load_ss(src.pB());
 
 	// C = min(255, 255*max(C, 0)) ( == Clamp(255*C, 0, 255) )
 	r = _mm_max_ss(r, zero);
 	g = _mm_max_ss(g, zero);
 	b = _mm_max_ss(b, zero);
 
 	r = _mm_mul_ss(r, _255);
 	g = _mm_mul_ss(g, _255);
 	b = _mm_mul_ss(b, _255);
 
 	r = _mm_min_ss(r, _255);
 	g = _mm_min_ss(g, _255);
 	b = _mm_min_ss(b, _255);
 
 	// convert to integer and combine channels using bit logic
 	int ri = _mm_cvtss_si32(r);
 	int gi = _mm_cvtss_si32(g);
 	int bi = _mm_cvtss_si32(b);
 
 	return SColor4ub(ri, gi, bi, 0xFF);
 }
 #endif
 
 void ColorActivateFastImpl()
 {
 #if COMPILER_HAS_SSE
 	if (HostHasSSE())
 	{
-		ConvertRGBColorTo4ub = ConvertRGBColorTo4ubSSE;
+		ConvertRGBcolorTo4ub = ConvertRGBcolorTo4ubSSE;
 		return;
 	}
 #endif
 	debug_printf("No SSE available. Slow fallback routines will be used.\n");
 }
 
 /**
  * Important: This function does not modify the value if parsing fails.
  */
-bool CColor::ParseString(const CStr8& value, int defaultAlpha)
+bool RGBAcolor::ParseString(const CStr8& value, int defaultAlpha)
 {
 	const size_t NUM_VALS = 4;
 	int values[NUM_VALS] = { 0, 0, 0, defaultAlpha };
 	std::stringstream stream;
 	stream.str(value);
 	// Parse each value
 	size_t i;
 	for (i = 0; i < NUM_VALS; ++i)
 	{
 		if (stream.eof())
 			break;
 
 		stream >> values[i];
 		if ((stream.rdstate() & std::stringstream::failbit) != 0)
 		{
-			LOGWARNING("Unable to parse CColor parameters. Your input: '%s'", value.c_str());
+			LOGWARNING("Unable to parse RGBAcolor parameters. Your input: '%s'", value.c_str());
 			return false;
 		}
 		if (values[i] < 0 || values[i] > 255)
 		{
-			LOGWARNING("Invalid value (<0 or >255) when parsing CColor parameters. Your input: '%s'", value.c_str());
+			LOGWARNING("Invalid value (<0 or >255) when parsing RGBAcolor parameters. Your input: '%s'", value.c_str());
 			return false;
 		}
 	}
 
 	if (i < 3)
 	{
-		LOGWARNING("Not enough parameters when parsing as CColor. Your input: '%s'", value.c_str());
+		LOGWARNING("Not enough parameters when parsing as RGBAcolor. Your input: '%s'", value.c_str());
 		return false;
 	}
 	if (!stream.eof())
 	{
-		LOGWARNING("Too many parameters when parsing as CColor. Your input: '%s'", value.c_str());
+		LOGWARNING("Too many parameters when parsing as RGBAcolor. Your input: '%s'", value.c_str());
 		return false;
 	}
 
-	r = values[0] / 255.f;
-	g = values[1] / 255.f;
-	b = values[2] / 255.f;
-	a = values[3] / 255.f;
+	R = values[0] / 255.f;
+	G = values[1] / 255.f;
+	B = values[2] / 255.f;
+	A = values[3] / 255.f;
 
 	return true;
 }
 
-bool CColor::operator==(const CColor& color) const
+bool RGBAcolor::operator==(const RGBAcolor& color) const
 {
 	return
-		r == color.r &&
-		g == color.g &&
-		b == color.b &&
-		a == color.a;
+		R == color.R &&
+		G == color.G &&
+		B == color.B &&
+		A == color.A;
 }
+
+RGBAcolor::operator CVector4D() const { return CVector4D(R,G,B,A); }
+
+#if 0
+#include "lib/code_annotation.h"
+PRINT_SIZEOF_AS_ERROR(RGBcolor);
+#endif
+#if 0
+#include "lib/code_annotation.h"
+PRINT_SIZEOF_AS_ERROR(RGBAcolor);
+#endif
+
Index: source/graphics/Color.h
===================================================================
--- source/graphics/Color.h	(revision 24796)
+++ source/graphics/Color.h	(working copy)
@@ -1,86 +1,189 @@
 /* Copyright (C) 2021 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 .
  */
-
+// (Note there's also a Color struct for the messaging system, somewhere...)
 #ifndef INCLUDED_COLOR
 #define INCLUDED_COLOR
 
+struct Color;
 #include "graphics/SColor.h"
 #include "maths/Vector3D.h"
 #include "maths/Vector4D.h"
+#include "maths/MathUtil.h"
 
 // Simple defines for 3 and 4 component floating point colors - just map to
 // corresponding vector types.
-typedef CVector3D RGBColor;
-typedef CVector4D RGBAColor;
+class RGBcolor
+{
+	float R, G, B;
+public:
+	///constructors, etc
+	RGBcolor() {}
+	RGBcolor(float x, float y, float z) : R(x), G(y), B(z) { }
+	///get functions
+	float getR() const { return R; }
+	float getG() const { return G; }
+	float getB() const { return B; }
+	float MaxComponent() { return std::max({R, G, B}); }
+	float MinComponent() { return std::min({R, G, B}); }
+	u8 getR8() const { return static_cast( Clamp( int(R*256.0f), 0, 255 ) ); }
+	u8 getG8() const { return static_cast( Clamp( int(G*256.0f), 0, 255 ) ); }
+	u8 getB8() const { return static_cast( Clamp( int(B*256.0f), 0, 255 ) ); }
+	///Not quite "set functions"; "get non-const ref" functions to minimize changes
+	float& Rref(){ return R; }
+	float& Gref(){ return G; }
+	float& Bref(){ return B; }
+	///pointers for the stuuupid SSE library
+	float const * pR() const { return (((float*)this)+0); }
+	float const * pG() const { return (((float*)this)+1); }
+	float const * pB() const { return (((float*)this)+2); }
+	void clamp0to1()
+	{
+		if( R < 0.0f ) R = 0.0f;
+		if( 0.996f < R ) R = 0.996f;
+		if( G < 0.0f ) G = 0.0f;
+		if( 0.996f < G ) G = 0.996f;
+		if( B < 0.0f ) B = 0.0f;
+		if( 0.996f < B ) B = 0.996f;
+	}
+	RGBcolor clamped0to1() const
+	{
+		RGBcolor result(*this);
+		result.clamp0to1();
+		return result;
+	}
+	bool operator==(const RGBcolor& p) const
+	{
+		return (R == p.R && G == p.G && B == p.B);
+	}
+	bool operator!=(const RGBcolor& p) const
+	{
+		return (R != p.R || G != p.G || B != p.B);
+	}
+	RGBcolor operator+(const RGBcolor& c) const
+	{
+		return RGBcolor(R + c.getR(), G + c.getG(), B + c.getB());
+	}
+	RGBcolor operator-(const RGBcolor& c) const
+	{
+		return RGBcolor(R - c.getR(), G - c.getG(), B - c.getB());
+	}
+	RGBcolor const & operator+=(const RGBcolor& v)
+	{
+		R += v.getR();  G += v.getG();  B += v.getB();
+		return *this;
+	}
+	RGBcolor const & operator-=(const RGBcolor& v)
+	{
+		R -= v.getR();  G -= v.getG();  B -= v.getB();
+		return *this;
+	}
+	RGBcolor operator*(float value) const
+	{
+		return RGBcolor(R * value, G * value, B * value);
+	}
+	RGBcolor& operator*=(float value)
+	{
+		R *= value;
+		G *= value;
+		B *= value;
+		return *this;
+	}
+};
+//typedef CVector4D RGBAColor;
 
 // Convert float RGB(A) colors to unsigned byte.
 // Exposed as function pointer because it is set at init-time to
 // one of several implementations depending on CPU caps.
-extern SColor4ub (*ConvertRGBColorTo4ub)(const RGBColor& src);
+extern SColor4ub (*ConvertRGBcolorTo4ub)(const RGBcolor& src);
 
 /**
  * Detects CPU caps and activates the best possible codepath.
  */
 extern void ColorActivateFastImpl();
 
 class CStr8;
 
-struct CColor
+class RGBAcolor
 {
-	CColor() : r(-1.f), g(-1.f), b(-1.f), a(1.f) {}
-	CColor(float cr, float cg, float cb, float ca) : r(cr), g(cg), b(cb), a(ca) {}
+protected: //CGUIColor inherits RGBAcolor and wants to manipulate the variables
+	float R, G, B, A;
+public:
+	RGBAcolor() : R(-1.f), G(-1.f), B(-1.f), A(1.f) {}
+	RGBAcolor(float cr, float cg, float cb, float ca) : R(cr), G(cg), B(cb), A(ca) {}
+	///get functions
+	float getR() const { return R; }
+	float getG() const { return G; }
+	float getB() const { return B; }
+	float getA() const { return A; }
+	float MaxComponent() { return std::max({R, G, B}); } //alpha channel's component?
+	float MinComponent() { return std::min({R, G, B}); } //alpha channel's component?
+	u8 getR8() const { return static_cast( Clamp( int(R*256.0f), 0, 255 ) ); }
+	u8 getG8() const { return static_cast( Clamp( int(G*256.0f), 0, 255 ) ); }
+	u8 getB8() const { return static_cast( Clamp( int(B*256.0f), 0, 255 ) ); }
+	u8 getA8() const { return static_cast( Clamp( int(A*256.0f), 0, 255 ) ); }
+	///Conversion to CVector4D is needed for the shader uniforms
+	operator CVector4D() const;
+	///Not quite "set functions"; "get non-const ref" functions to minimize changes
+	float& Rref(){ return R; }
+	float& Gref(){ return G; }
+	float& Bref(){ return B; }
+	float& Aref(){ return A; }
 
 	/**
 	 * Returns whether this has been set to a valid color.
+	 * DanW58:  I suppose values greater than 1 are valid in the case that
+	 * color is being used for a light source, like the Sun, and not just
+	 * reflective material color --which can never be greater than 1.0f
+	 * It's funny then that the default constructor puts RGB at -1.0f...
+	 * I've no idea why...
 	 */
 	operator bool() const
 	{
-		return r >= 0 && g >= 0 && b >= 0 && a >= 0;
+		return R >= 0 && G >= 0 && B >= 0 && A >= 0;
 	}
 
 	/**
 	 * Try to parse @p Value as a color. Returns true on success, false otherwise.
 	 * Leaves the color unchanged if it failed.
-	 * @param value Should be "r g b" or "r g b a" where each value is an integer in [0,255].
-	 * @param defaultAlpha The alpha value that is used if the format of @p Value is "r g b".
+	 * @param value Should be "R G B" or "R G B A" where each value is an integer in [0,255].
+	 * @param defaultAlpha The alpha value that is used if the format of @p Value is "R G B".
 	 */
 	bool ParseString(const CStr8& value, int defaultAlpha = 255);
 
-	bool operator==(const CColor& color) const;
-	bool operator!=(const CColor& color) const
+	bool operator==(const RGBAcolor& color) const;
+	bool operator!=(const RGBAcolor& color) const
 	{
 		return !(*this == color);
 	}
 
 	// For passing to glColor[34]fv:
-	const float* FloatArray() const { return &r; }
+	const float* FloatArray() const { return &R; }
 
 	// For passing to CRenderer:
 	SColor4ub AsSColor4ub() const
 	{
 		return SColor4ub(
-			static_cast(r * 255.f),
-			static_cast(g * 255.f),
-			static_cast(b * 255.f),
-			static_cast(a * 255.f)
+			static_cast(Clamp(R * 256.0f, 0, 255)),
+			static_cast(Clamp(G * 255.0f, 0, 255)),
+			static_cast(Clamp(B * 255.0f, 0, 255)),
+			static_cast(Clamp(A * 255.0f, 0, 255))
 		);
 	}
 
-	float r, g, b, a;
 };
 
 #endif // INCLUDED_COLOR
Index: source/graphics/Decal.cpp
===================================================================
--- source/graphics/Decal.cpp	(revision 24796)
+++ source/graphics/Decal.cpp	(working copy)
@@ -1,115 +1,115 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include "Decal.h"
 
 #include "graphics/Terrain.h"
 #include "maths/MathUtil.h"
 
 CModelAbstract* CModelDecal::Clone() const
 {
 	CModelDecal* clone = new CModelDecal(m_Terrain, m_Decal);
 	return clone;
 }
 
 void CModelDecal::CalcVertexExtents(ssize_t& i0, ssize_t& j0, ssize_t& i1, ssize_t& j1)
 {
 	CVector3D corner0(m_Decal.m_OffsetX + m_Decal.m_SizeX/2, 0, m_Decal.m_OffsetZ + m_Decal.m_SizeZ/2);
 	CVector3D corner1(m_Decal.m_OffsetX + m_Decal.m_SizeX/2, 0, m_Decal.m_OffsetZ - m_Decal.m_SizeZ/2);
 	CVector3D corner2(m_Decal.m_OffsetX - m_Decal.m_SizeX/2, 0, m_Decal.m_OffsetZ - m_Decal.m_SizeZ/2);
 	CVector3D corner3(m_Decal.m_OffsetX - m_Decal.m_SizeX/2, 0, m_Decal.m_OffsetZ + m_Decal.m_SizeZ/2);
 
 	corner0 = GetTransform().Transform(corner0);
 	corner1 = GetTransform().Transform(corner1);
 	corner2 = GetTransform().Transform(corner2);
 	corner3 = GetTransform().Transform(corner3);
 
-	i0 = floor(std::min(std::min(corner0.X, corner1.X), std::min(corner2.X, corner3.X)) / TERRAIN_TILE_SIZE);
-	j0 = floor(std::min(std::min(corner0.Z, corner1.Z), std::min(corner2.Z, corner3.Z)) / TERRAIN_TILE_SIZE);
-	i1 = ceil(std::max(std::max(corner0.X, corner1.X), std::max(corner2.X, corner3.X)) / TERRAIN_TILE_SIZE);
-	j1 = ceil(std::max(std::max(corner0.Z, corner1.Z), std::max(corner2.Z, corner3.Z)) / TERRAIN_TILE_SIZE);
+	i0 = floor(std::min(std::min(corner0.getX(), corner1.getX()), std::min(corner2.getX(), corner3.getX())) / TERRAIN_TILE_SIZE);
+	j0 = floor(std::min(std::min(corner0.getZ(), corner1.getZ()), std::min(corner2.getZ(), corner3.getZ())) / TERRAIN_TILE_SIZE);
+	i1 = ceil(std::max(std::max(corner0.getX(), corner1.getX()), std::max(corner2.getX(), corner3.getX())) / TERRAIN_TILE_SIZE);
+	j1 = ceil(std::max(std::max(corner0.getZ(), corner1.getZ()), std::max(corner2.getZ(), corner3.getZ())) / TERRAIN_TILE_SIZE);
 
 	i0 = Clamp(i0, static_cast(0), m_Terrain->GetVerticesPerSide() - 1);
 	j0 = Clamp(j0, static_cast(0), m_Terrain->GetVerticesPerSide() - 1);
 	i1 = Clamp(i1, static_cast(0), m_Terrain->GetVerticesPerSide() - 1);
 	j1 = Clamp(j1, static_cast(0), m_Terrain->GetVerticesPerSide() - 1);
 }
 
 void CModelDecal::CalcBounds()
 {
 	ssize_t i0, j0, i1, j1;
 	CalcVertexExtents(i0, j0, i1, j1);
 	m_WorldBounds = m_Terrain->GetVertexesBound(i0, j0, i1, j1);
 }
 
 void CModelDecal::SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1)
 {
 	// Check if there's no intersection between the dirty range and this decal
 	ssize_t bi0, bj0, bi1, bj1;
 	CalcVertexExtents(bi0, bj0, bi1, bj1);
 	if (bi1 < i0 || bi0 > i1 || bj1 < j0 || bj0 > j1)
 		return;
 
 	SetDirty(RENDERDATA_UPDATE_VERTICES);
 }
 
 void CModelDecal::InvalidatePosition()
 {
 	m_PositionValid = false;
 }
 
 void CModelDecal::ValidatePosition()
 {
 	if (m_PositionValid)
 	{
 		ENSURE(!m_Parent || m_Parent->m_PositionValid);
 		return;
 	}
 
 	if (m_Parent && !m_Parent->m_PositionValid)
 	{
 		// Make sure we don't base our calculations on
 		// a parent animation state that is out of date.
 		m_Parent->ValidatePosition();
 
 		// Parent will recursively call our validation.
 		ENSURE(m_PositionValid);
 		return;
 	}
 
 	m_PositionValid = true;
 }
 
 void CModelDecal::SetTransform(const CMatrix3D& transform)
 {
 	// Since decals are assumed to be horizontal and projected downwards
 	// onto the terrain, use just the Y-axis rotation and the translation
 	CMatrix3D newTransform;
 	newTransform.SetYRotation(transform.GetYRotation() + m_Decal.m_Angle);
 	newTransform.Translate(transform.GetTranslation());
 
 	CRenderableObject::SetTransform(newTransform);
 	InvalidatePosition();
 }
 
 void CModelDecal::RemoveShadows()
 {
 	m_Decal.m_Material.AddShaderDefine(str_DISABLE_RECEIVE_SHADOWS, str_1);
 	m_Decal.m_Material.RecomputeCombinedShaderDefines();
 }
Index: source/graphics/Frustum.cpp
===================================================================
--- source/graphics/Frustum.cpp	(revision 24796)
+++ source/graphics/Frustum.cpp	(working copy)
@@ -1,160 +1,160 @@
 /* Copyright (C) 2009 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 .
  */
 
 /*
  * CFrustum is a collection of planes which define a viewing space.
  */
 
 /*
 Usually associated with the camera, there are 6 planes which define the
 view pyramid. But we allow more planes per frustum which may be used for
 portal rendering, where a portal may have 3 or more edges.
 */
 
 #include "precompiled.h"
 
 #include "Frustum.h"
 #include "maths/BoundingBoxAligned.h"
 #include "maths/MathUtil.h"
 #include "maths/Matrix3D.h"
 
 CFrustum::CFrustum ()
 {
 	m_NumPlanes = 0;
 }
 
 CFrustum::~CFrustum ()
 {
 }
 
 void CFrustum::SetNumPlanes (size_t num)
 {
 	m_NumPlanes = num;
 
 	//clip it
 	if (m_NumPlanes >= MAX_NUM_FRUSTUM_PLANES)
 	{
 		debug_warn(L"CFrustum::SetNumPlanes: Too many planes");
 		m_NumPlanes = MAX_NUM_FRUSTUM_PLANES-1;
 	}
 }
 
 void CFrustum::AddPlane(const CPlane& plane)
 {
 	if (m_NumPlanes >= MAX_NUM_FRUSTUM_PLANES)
 	{
 		debug_warn(L"CFrustum::AddPlane: Too many planes");
 		return;
 	}
 
 	m_aPlanes[m_NumPlanes++] = plane;
 }
 
 void CFrustum::Transform(CMatrix3D& m)
 {
 	for (size_t i = 0; i < m_NumPlanes; ++i)
 	{
 		CVector3D n = m.Rotate(m_aPlanes[i].m_Norm);
 		CVector3D p = m.Transform(m_aPlanes[i].m_Norm * -m_aPlanes[i].m_Dist);
 		m_aPlanes[i].Set(n, p);
 		m_aPlanes[i].Normalize();
 	}
 }
 
 bool CFrustum::IsPointVisible(const CVector3D& point) const
 {
 	for (size_t i=0; i radius)
 			return false;
 	}
 
 	return true;
 }
 
 bool CFrustum::IsBoxVisible(const CVector3D& position, const CBoundingBoxAligned& bounds) const
 {
 	//basically for every plane we calculate the furthest point
 	//in the box to that plane. If that point is beyond the plane
 	//then the box is not visible
 	CVector3D FarPoint;
 	CVector3D Min = position+bounds[0];
 	CVector3D Max = position+bounds[1];
 
 	for (size_t i=0; i 0.0f ? Max.X : Min.X;
-		FarPoint.Y = m_aPlanes[i].m_Norm.Y > 0.0f ? Max.Y : Min.Y;
-		FarPoint.Z = m_aPlanes[i].m_Norm.Z > 0.0f ? Max.Z : Min.Z;
+		FarPoint.Xref() = m_aPlanes[i].m_Norm.getX() > 0.0f ? Max.getX() : Min.getX();
+		FarPoint.Yref() = m_aPlanes[i].m_Norm.getY() > 0.0f ? Max.getY() : Min.getY();
+		FarPoint.Zref() = m_aPlanes[i].m_Norm.getZ() > 0.0f ? Max.getZ() : Min.getZ();
 
 		if (m_aPlanes[i].IsPointOnBackSide(FarPoint))
 			return false;
 	}
 
 	return true;
 }
 
 bool CFrustum::IsBoxVisible(const CBoundingBoxAligned& bounds) const
 {
 	//Same as the previous one, but with the position = (0, 0, 0)
 	CVector3D FarPoint;
 
 	for (size_t i=0; i 0.0f ? bounds[1].X : bounds[0].X;
-		FarPoint.Y = m_aPlanes[i].m_Norm.Y > 0.0f ? bounds[1].Y : bounds[0].Y;
-		FarPoint.Z = m_aPlanes[i].m_Norm.Z > 0.0f ? bounds[1].Z : bounds[0].Z;
+		FarPoint.Xref() = m_aPlanes[i].m_Norm.getX() > 0.0f ? bounds[1].getX() : bounds[0].getX();
+		FarPoint.Yref() = m_aPlanes[i].m_Norm.getY() > 0.0f ? bounds[1].getY() : bounds[0].getY();
+		FarPoint.Zref() = m_aPlanes[i].m_Norm.getZ() > 0.0f ? bounds[1].getZ() : bounds[0].getZ();
 
 		if (m_aPlanes[i].IsPointOnBackSide(FarPoint))
 			return false;
 	}
 
 	return true;
 }
Index: source/graphics/GameView.cpp
===================================================================
--- source/graphics/GameView.cpp	(revision 24796)
+++ source/graphics/GameView.cpp	(working copy)
@@ -1,415 +1,415 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include "GameView.h"
 
 #include "graphics/CameraController.h"
 #include "graphics/CinemaManager.h"
 #include "graphics/ColladaManager.h"
 #include "graphics/HFTracer.h"
 #include "graphics/LOSTexture.h"
 #include "graphics/LightEnv.h"
 #include "graphics/Model.h"
 #include "graphics/ObjectManager.h"
 #include "graphics/Patch.h"
 #include "graphics/SkeletonAnimManager.h"
 #include "graphics/SmoothedValue.h"
 #include "graphics/Terrain.h"
 #include "graphics/TerrainTextureManager.h"
 #include "graphics/TerritoryTexture.h"
 #include "graphics/Unit.h"
 #include "graphics/UnitManager.h"
 #include "graphics/scripting/JSInterface_GameView.h"
 #include "lib/input.h"
 #include "lib/timer.h"
 #include "lobby/IXmppClient.h"
 #include "maths/BoundingBoxAligned.h"
 #include "maths/MathUtil.h"
 #include "maths/Matrix3D.h"
 #include "maths/Quaternion.h"
 #include "ps/ConfigDB.h"
 #include "ps/Filesystem.h"
 #include "ps/Game.h"
 #include "ps/Globals.h"
 #include "ps/Hotkey.h"
 #include "ps/Joystick.h"
 #include "ps/Loader.h"
 #include "ps/LoaderThunks.h"
 #include "ps/Profile.h"
 #include "ps/Pyrogenesis.h"
 #include "ps/TouchInput.h"
 #include "ps/World.h"
 #include "renderer/Renderer.h"
 #include "renderer/WaterManager.h"
 #include "simulation2/Simulation2.h"
 #include "simulation2/components/ICmpPosition.h"
 #include "simulation2/components/ICmpRangeManager.h"
 
 #include 
 
 class CGameViewImpl
 {
 	NONCOPYABLE(CGameViewImpl);
 public:
 	CGameViewImpl(CGame* game)
 		: Game(game),
 		ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager),
 		ObjectManager(MeshManager, SkeletonAnimManager, *game->GetSimulation2()),
 		LOSTexture(*game->GetSimulation2()),
 		TerritoryTexture(*game->GetSimulation2()),
 		ViewCamera(),
 		CullCamera(),
 		LockCullCamera(false),
 		Culling(true),
 		CameraController(new CCameraController(ViewCamera))
 	{
 	}
 
 	CGame* Game;
 	CColladaManager ColladaManager;
 	CMeshManager MeshManager;
 	CSkeletonAnimManager SkeletonAnimManager;
 	CObjectManager ObjectManager;
 	CLOSTexture LOSTexture;
 	CTerritoryTexture TerritoryTexture;
 
 	/**
 	 * this camera controls the eye position when rendering
 	 */
 	CCamera ViewCamera;
 
 	/**
 	 * this camera controls the frustum that is used for culling
 	 * and shadow calculations
 	 *
 	 * Note that all code that works with camera movements should only change
 	 * m_ViewCamera. The render functions automatically sync the cull camera to
 	 * the view camera depending on the value of m_LockCullCamera.
 	 */
 	CCamera CullCamera;
 
 	/**
 	 * When @c true, the cull camera is locked in place.
 	 * When @c false, the cull camera follows the view camera.
 	 *
 	 * Exposed to JS as gameView.lockCullCamera
 	 */
 	bool LockCullCamera;
 
 	/**
 	 * When @c true, culling is enabled so that only models that have a chance of
 	 * being visible are sent to the renderer.
 	 * Otherwise, the entire world is sent to the renderer.
 	 *
 	 * Exposed to JS as gameView.culling
 	 */
 	bool Culling;
 
 	/**
 	 * Cache global lighting environment. This is used  to check whether the
 	 * environment has changed during the last frame, so that vertex data can be updated etc.
 	 */
 	CLightEnv CachedLightEnv;
 
 	CCinemaManager CinemaManager;
 
 	/**
 	 * Controller of the view's camera. We use a std::unique_ptr for an easy
 	 * on the fly replacement. It's guaranteed that the pointer is never nulllptr.
 	 */
 	std::unique_ptr CameraController;
 };
 
 #define IMPLEMENT_BOOLEAN_SETTING(NAME) \
 bool CGameView::Get##NAME##Enabled() const \
 { \
 	return m->NAME; \
 } \
 \
 void CGameView::Set##NAME##Enabled(bool Enabled) \
 { \
 	m->NAME = Enabled; \
 }
 
 IMPLEMENT_BOOLEAN_SETTING(Culling);
 IMPLEMENT_BOOLEAN_SETTING(LockCullCamera);
 
 bool CGameView::GetConstrainCameraEnabled() const
 {
 	return m->CameraController->GetConstrainCamera();
 }
 
 void CGameView::SetConstrainCameraEnabled(bool enabled)
 {
 	m->CameraController->SetConstrainCamera(enabled);
 }
 
 #undef IMPLEMENT_BOOLEAN_SETTING
 
 CGameView::CGameView(CGame *pGame):
 	m(new CGameViewImpl(pGame))
 {
 	m->CullCamera = m->ViewCamera;
 	g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera);
 }
 
 CGameView::~CGameView()
 {
 	UnloadResources();
 
 	delete m;
 }
 
 void CGameView::SetViewport(const SViewPort& vp)
 {
 	m->CameraController->SetViewport(vp);
 }
 
 CObjectManager& CGameView::GetObjectManager()
 {
 	return m->ObjectManager;
 }
 
 CCamera* CGameView::GetCamera()
 {
 	return &m->ViewCamera;
 }
 
 CCinemaManager* CGameView::GetCinema()
 {
 	return &m->CinemaManager;
 };
 
 CLOSTexture& CGameView::GetLOSTexture()
 {
 	return m->LOSTexture;
 }
 
 CTerritoryTexture& CGameView::GetTerritoryTexture()
 {
 	return m->TerritoryTexture;
 }
 
 int CGameView::Initialize()
 {
 	m->CameraController->LoadConfig();
 	return 0;
 }
 
 void CGameView::RegisterInit()
 {
 	// CGameView init
 	RegMemFun(this, &CGameView::Initialize, L"CGameView init", 1);
 
 	RegMemFun(g_TexMan.GetSingletonPtr(), &CTerrainTextureManager::LoadTerrainTextures, L"LoadTerrainTextures", 60);
 	RegMemFun(g_Renderer.GetSingletonPtr(), &CRenderer::LoadAlphaMaps, L"LoadAlphaMaps", 5);
 }
 
 void CGameView::BeginFrame()
 {
 	if (m->LockCullCamera == false)
 	{
 		// Set up cull camera
 		m->CullCamera = m->ViewCamera;
 	}
 	g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera);
 
 	CheckLightEnv();
 
 	m->Game->CachePlayerColors();
 }
 
 void CGameView::Render()
 {
 	g_Renderer.RenderScene(*this);
 }
 
 ///////////////////////////////////////////////////////////
 // This callback is part of the Scene interface
 // Submit all objects visible in the given frustum
 void CGameView::EnumerateObjects(const CFrustum& frustum, SceneCollector* c)
 {
 	{
 	PROFILE3("submit terrain");
 
 	CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain();
 	float waterHeight = g_Renderer.GetWaterManager()->m_WaterHeight + 0.001f;
 	const ssize_t patchesPerSide = pTerrain->GetPatchesPerSide();
 
 	// find out which patches will be drawn
 	for (ssize_t j=0; jGetPatch(i,j);	// can't fail
 
 			// If the patch is underwater, calculate a bounding box that also contains the water plane
 			CBoundingBoxAligned bounds = patch->GetWorldBounds();
-			if(bounds[1].Y < waterHeight)
-				bounds[1].Y = waterHeight;
+			if(bounds[1].getY() < waterHeight)
+				bounds[1].Yref() = waterHeight;
 
 			if (!m->Culling || frustum.IsBoxVisible(bounds))
 				c->Submit(patch);
 		}
 	}
 	}
 
 	m->Game->GetSimulation2()->RenderSubmit(*c, frustum, m->Culling);
 }
 
 
 void CGameView::CheckLightEnv()
 {
 	if (m->CachedLightEnv == g_LightEnv)
 		return;
 
 	m->CachedLightEnv = g_LightEnv;
 	CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain();
 
 	if (!pTerrain)
 		return;
 
 	PROFILE("update light env");
 	pTerrain->MakeDirty(RENDERDATA_UPDATE_COLOR);
 
 	const std::vector& units = m->Game->GetWorld()->GetUnitManager().GetUnits();
 	for (size_t i = 0; i < units.size(); ++i)
 		units[i]->GetModel().SetDirtyRec(RENDERDATA_UPDATE_COLOR);
 }
 
 
 void CGameView::UnloadResources()
 {
 	g_TexMan.UnloadTerrainTextures();
 	g_Renderer.UnloadAlphaMaps();
 	g_Renderer.GetWaterManager()->UnloadWaterTextures();
 }
 
 void CGameView::Update(const float deltaRealTime)
 {
 	// If camera movement is being handled by the touch-input system,
 	// then we should stop to avoid conflicting with it
 	if (g_TouchInput.IsEnabled())
 		return;
 
 	if (!g_app_has_focus)
 		return;
 
 	m->CinemaManager.Update(deltaRealTime);
 	if (m->CinemaManager.IsEnabled())
 		return;
 
 	m->CameraController->Update(deltaRealTime);
 }
 
 CVector3D CGameView::GetCameraPivot() const
 {
 	return m->CameraController->GetCameraPivot();
 }
 
 CVector3D CGameView::GetCameraPosition() const
 {
 	return m->CameraController->GetCameraPosition();
 }
 
 CVector3D CGameView::GetCameraRotation() const
 {
 	return m->CameraController->GetCameraRotation();
 }
 
 float CGameView::GetCameraZoom() const
 {
 	return m->CameraController->GetCameraZoom();
 }
 
 void CGameView::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom)
 {
 	m->CameraController->SetCamera(pos, rotX, rotY, zoom);
 }
 
 void CGameView::MoveCameraTarget(const CVector3D& target)
 {
 	m->CameraController->MoveCameraTarget(target);
 }
 
 void CGameView::ResetCameraTarget(const CVector3D& target)
 {
 	m->CameraController->ResetCameraTarget(target);
 }
 
 void CGameView::FollowEntity(entity_id_t entity, bool firstPerson)
 {
 	m->CameraController->FollowEntity(entity, firstPerson);
 }
 
 entity_id_t CGameView::GetFollowedEntity()
 {
 	return m->CameraController->GetFollowedEntity();
 }
 
 InReaction game_view_handler(const SDL_Event_* ev)
 {
 	// put any events that must be processed even if inactive here
 	if (!g_app_has_focus || !g_Game || !g_Game->IsGameStarted() || g_Game->GetView()->GetCinema()->IsEnabled())
 		return IN_PASS;
 
 	CGameView *pView=g_Game->GetView();
 
 	return pView->HandleEvent(ev);
 }
 
 InReaction CGameView::HandleEvent(const SDL_Event_* ev)
 {
 	switch(ev->ev.type)
 	{
 	case SDL_HOTKEYPRESS:
 	{
 		std::string hotkey = static_cast(ev->ev.user.data1);
 		if (hotkey == "wireframe")
 		{
 			if (g_XmppClient && g_rankedGame == true)
 				break;
 			else if (g_Renderer.GetModelRenderMode() == SOLID)
 			{
 				g_Renderer.SetTerrainRenderMode(EDGED_FACES);
 				g_Renderer.SetWaterRenderMode(EDGED_FACES);
 				g_Renderer.SetModelRenderMode(EDGED_FACES);
 				g_Renderer.SetOverlayRenderMode(EDGED_FACES);
 			}
 			else if (g_Renderer.GetModelRenderMode() == EDGED_FACES)
 			{
 				g_Renderer.SetTerrainRenderMode(WIREFRAME);
 				g_Renderer.SetWaterRenderMode(WIREFRAME);
 				g_Renderer.SetModelRenderMode(WIREFRAME);
 				g_Renderer.SetOverlayRenderMode(WIREFRAME);
 			}
 			else
 			{
 				g_Renderer.SetTerrainRenderMode(SOLID);
 				g_Renderer.SetWaterRenderMode(SOLID);
 				g_Renderer.SetModelRenderMode(SOLID);
 				g_Renderer.SetOverlayRenderMode(SOLID);
 			}
 			return IN_HANDLED;
 		}
 	}
 	}
 
 	return m->CameraController->HandleEvent(ev);
 }
Index: source/graphics/HFTracer.cpp
===================================================================
--- source/graphics/HFTracer.cpp	(revision 24796)
+++ source/graphics/HFTracer.cpp	(working copy)
@@ -1,360 +1,360 @@
 /* Copyright (C) 2021 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 .
  */
 
 /*
  * Determine intersection of rays with a heightfield.
  */
 
 #include "precompiled.h"
 
 #include "HFTracer.h"
 
 #include "graphics/Patch.h"
 #include "graphics/Terrain.h"
 #include "maths/BoundingBoxAligned.h"
 #include "maths/MathUtil.h"
 #include "maths/Vector3D.h"
 
 #include 
 
 // To cope well with points that are slightly off the edge of the map,
 // we act as if there's an N-tile margin around the edges of the heightfield.
 // (N shouldn't be too huge else it'll hurt performance a little when
 // RayIntersect loops through it all.)
 // CTerrain::CalcPosition implements clamp-to-edge behaviour so the tracer
 // will have that behaviour.
 static const int MARGIN_SIZE = 64;
 
 ///////////////////////////////////////////////////////////////////////////////
 // CHFTracer constructor
 CHFTracer::CHFTracer(CTerrain *pTerrain):
 	m_pTerrain(pTerrain),
 	m_Heightfield(m_pTerrain->GetHeightMap()),
 	m_MapSize(m_pTerrain->GetVerticesPerSide()),
 	m_CellSize((float)TERRAIN_TILE_SIZE),
 	m_HeightScale(HEIGHT_SCALE)
 {
 }
 
 
 ///////////////////////////////////////////////////////////////////////////////
 // RayTriIntersect: intersect a ray with triangle defined by vertices
 // v0,v1,v2; return true if ray hits triangle at distance less than dist,
 // or false otherwise
 static bool RayTriIntersect(const CVector3D& v0, const CVector3D& v1, const CVector3D& v2,
 								const CVector3D& origin, const CVector3D& dir, float& dist)
 {
 	const float EPSILON=0.00001f;
 
 	// calculate edge vectors
 	CVector3D edge0=v1-v0;
 	CVector3D edge1=v2-v0;
 
     // begin calculating determinant - also used to calculate U parameter
     CVector3D pvec=dir.Cross(edge1);
 
     // if determinant is near zero, ray lies in plane of triangle
     float det = edge0.Dot(pvec);
     if (fabs(det)1.01f)
         return false;
 
     // prepare to test V parameter
     CVector3D qvec=tvec.Cross(edge0);
 
     // calculate V parameter and test bounds
     float v=dir.Dot(qvec)*inv_det;
     if (v<0.0f || u+v>1.0f)
         return false;
 
     // calculate distance to intersection point from ray origin
     float d=edge1.Dot(qvec)*inv_det;
     if (d>=0 && dCalcPosition(cx,cz,vpos[0]);
 	m_pTerrain->CalcPosition(cx+1,cz,vpos[1]);
 	m_pTerrain->CalcPosition(cx+1,cz+1,vpos[2]);
 	m_pTerrain->CalcPosition(cx,cz+1,vpos[3]);
 
 	dist=1.0e30f;
 	if (RayTriIntersect(vpos[0],vpos[1],vpos[2],origin,dir,dist)) {
 		res=true;
 	}
 
 	if (RayTriIntersect(vpos[0],vpos[2],vpos[3],origin,dir,dist)) {
 		res=true;
 	}
 
 	return res;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
 // RayIntersect: intersect ray with this heightfield; return true if
 // intersection occurs (and fill in grid coordinates of intersection), or false
 // otherwise
 bool CHFTracer::RayIntersect(const CVector3D& origin, const CVector3D& dir, int& x, int& z, CVector3D& ipt) const
 {
 	// If the map is empty (which should never happen),
 	// return early before we crash when reading zero-sized heightmaps
 	if (!m_MapSize)
 	{
 		debug_warn(L"CHFTracer::RayIntersect called with zero-size map");
 		return false;
 	}
 
 	// intersect first against bounding box
 	CBoundingBoxAligned bound;
 	bound[0] = CVector3D(-MARGIN_SIZE * m_CellSize, 0, -MARGIN_SIZE * m_CellSize);
 	bound[1] = CVector3D((m_MapSize + MARGIN_SIZE) * m_CellSize, 65535 * m_HeightScale, (m_MapSize + MARGIN_SIZE) * m_CellSize);
 
 	float tmin,tmax;
 	if (!bound.RayIntersect(origin,dir,tmin,tmax)) {
 		// ray missed world bounds; no intersection
 		return false;
 	}
 
 	// project origin onto grid, if necessary, to get starting point for traversal
 	CVector3D traversalPt;
 	if (tmin>0) {
 		traversalPt=origin+dir*tmin;
 	} else {
 		traversalPt=origin;
 	}
 
 	// setup traversal variables
-	int sx=dir.X<0 ? -1 : 1;
-	int sz=dir.Z<0 ? -1 : 1;
+	int sx=dir.getX()<0 ? -1 : 1;
+	int sz=dir.getZ()<0 ? -1 : 1;
 
 	float invCellSize=1.0f/float(m_CellSize);
 
-	float fcx=traversalPt.X*invCellSize;
+	float fcx=traversalPt.getX()*invCellSize;
 	int cx=(int)floor(fcx);
 
-	float fcz=traversalPt.Z*invCellSize;
+	float fcz=traversalPt.getZ()*invCellSize;
 	int cz=(int)floor(fcz);
 
 	float invdx = 1.0e20f;
 	float invdz = 1.0e20f;
 
-	if (fabs(dir.X) > 1.0e-20)
-		invdx = float(1.0/fabs(dir.X));
-	if (fabs(dir.Z) > 1.0e-20)
-		invdz = float(1.0/fabs(dir.Z));
+	if (fabs(dir.getX()) > 1.0e-20)
+		invdx = float(1.0/fabs(dir.getX()));
+	if (fabs(dir.getZ()) > 1.0e-20)
+		invdz = float(1.0/fabs(dir.getZ()));
 
 	do {
 		// test current cell
 		if (cx >= -MARGIN_SIZE && cx < int(m_MapSize + MARGIN_SIZE - 1) && cz >= -MARGIN_SIZE && cz < int(m_MapSize + MARGIN_SIZE - 1))
 		{
 			float dist;
 
 			if (CellIntersect(cx,cz,origin,dir,dist)) {
 				x=cx;
 				z=cz;
 				ipt=origin+dir*dist;
 				return true;
 			}
 		}
 		else
 		{
 			// Degenerate case: y close to zero
 			// catch travelling off the map
 			if ((cx < -MARGIN_SIZE) && (sx < 0))
 				return false;
 			if ((cx >= (int)(m_MapSize + MARGIN_SIZE - 1)) && (sx > 0))
 				return false;
 			if ((cz < -MARGIN_SIZE) && (sz < 0))
 				return false;
 			if ((cz >= (int)(m_MapSize + MARGIN_SIZE - 1)) && (sz > 0))
 				return false;
 		}
 
 		// get coords of current cell
-		fcx=traversalPt.X*invCellSize;
-		fcz=traversalPt.Z*invCellSize;
+		fcx=traversalPt.getX()*invCellSize;
+		fcz=traversalPt.getZ()*invCellSize;
 
 		// get distance to next cell in x,z
 		float dx=(sx==-1) ? fcx-float(cx) : 1-(fcx-float(cx));
 		dx*=invdx;
 		float dz=(sz==-1) ? fcz-float(cz) : 1-(fcz-float(cz));
 		dz*=invdz;
 
 		// advance ..
 		float dist;
 		if (dx=0);
+	} while (traversalPt.getY()>=0);
 
 	// fell off end of heightmap with no intersection; return a miss
 	return false;
 }
 
 static bool TestTile(u16* heightmap, int stride, int i, int j, const CVector3D& pos, const CVector3D& dir, CVector3D& isct)
 {
 	u16 y00 = heightmap[i + j*stride];
 	u16 y10 = heightmap[i+1 + j*stride];
 	u16 y01 = heightmap[i + (j+1)*stride];
 	u16 y11 = heightmap[i+1 + (j+1)*stride];
 
 	CVector3D p00(    i * TERRAIN_TILE_SIZE, y00 * HEIGHT_SCALE,     j * TERRAIN_TILE_SIZE);
 	CVector3D p10((i+1) * TERRAIN_TILE_SIZE, y10 * HEIGHT_SCALE,     j * TERRAIN_TILE_SIZE);
 	CVector3D p01(    i * TERRAIN_TILE_SIZE, y01 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE);
 	CVector3D p11((i+1) * TERRAIN_TILE_SIZE, y11 * HEIGHT_SCALE, (j+1) * TERRAIN_TILE_SIZE);
 
 	int mid1 = y00+y11;
 	int mid2 = y01+y10;
 	int triDir = (mid1 < mid2);
 
 	float dist = FLT_MAX;
 
 	if (triDir)
 	{
 		if (RayTriIntersect(p00, p10, p01, pos, dir, dist) || // lower-left triangle
 		    RayTriIntersect(p11, p01, p10, pos, dir, dist))   // upper-right triangle
 		{
 			isct = pos + dir * dist;
 			return true;
 		}
 	}
 	else
 	{
 		if (RayTriIntersect(p00, p11, p01, pos, dir, dist) || // upper-left triangle
 		    RayTriIntersect(p00, p10, p11, pos, dir, dist))   // lower-right triangle
 		{
 			isct = pos + dir * dist;
 			return true;
 		}
 	}
 	return false;
 }
 
 bool CHFTracer::PatchRayIntersect(CPatch* patch, const CVector3D& origin, const CVector3D& dir, CVector3D* out)
 {
 	// (TODO: This largely duplicates RayIntersect - some refactoring might be
 	// nice in the future.)
 
 	// General approach:
 	// Given the ray defined by origin + dir * t, we increase t until it
 	// enters the patch's bounding box. The x,z coordinates identify which
 	// tile it is currently above/below. Do an intersection test vs the tile's
 	// two triangles. If it doesn't hit, do a 2D line rasterisation to find
 	// the next tiles the ray will pass through, and test each of them.
 
 	// Start by jumping to the point where the ray enters the bounding box
 	CBoundingBoxAligned bound = patch->GetWorldBounds();
 	float tmin, tmax;
 	if (!bound.RayIntersect(origin, dir, tmin, tmax))
 	{
 		// Ray missed patch; no intersection
 		return false;
 	}
 
 	int heightmapStride = patch->m_Parent->GetVerticesPerSide();
 
 	// Get heightmap, offset to start at this patch
 	u16* heightmap = patch->m_Parent->GetHeightMap() +
 			patch->m_X * PATCH_SIZE +
 			patch->m_Z * PATCH_SIZE * heightmapStride;
 
 	// Get patch-space position of ray origin and bbox entry point
 	CVector3D patchPos(
 			patch->m_X * PATCH_SIZE * TERRAIN_TILE_SIZE,
 			0.0f,
 			patch->m_Z * PATCH_SIZE * TERRAIN_TILE_SIZE);
 	CVector3D originPatch = origin - patchPos;
 	CVector3D entryPatch = originPatch + dir * tmin;
 
 	// We want to do a simple 2D line rasterisation (with the 3D ray projected
 	// down onto the Y plane). That will tell us which cells are intersected
 	// in 2D dimensions, then we can do a more precise 3D intersection test.
 	//
 	// WLOG, assume the ray has direction dir.x > 0, dir.z > 0, and starts in
 	// cell (i,j). The next cell intersecting the line must be either (i+1,j)
 	// or (i,j+1). To tell which, just check whether the point (i+1,j+1) is
 	// above or below the ray. Advance into that cell and repeat.
 	//
 	// (If the ray passes precisely through (i+1,j+1), we can pick either.
 	// If the ray is parallel to Y, only the first cell matters, then we can
 	// carry on rasterising in any direction (a bit of a waste of time but
 	// should be extremely rare, and it's safe and simple).)
 
 	// Work out which tile we're starting in
-	int i = Clamp(static_cast(entryPatch.X / TERRAIN_TILE_SIZE), 0, static_cast(PATCH_SIZE) - 1);
-	int j = Clamp(static_cast(entryPatch.Z / TERRAIN_TILE_SIZE), 0, static_cast(PATCH_SIZE) - 1);
+	int i = Clamp(static_cast(entryPatch.getX() / TERRAIN_TILE_SIZE), 0, static_cast(PATCH_SIZE) - 1);
+	int j = Clamp(static_cast(entryPatch.getZ() / TERRAIN_TILE_SIZE), 0, static_cast(PATCH_SIZE) - 1);
 
 	// Work out which direction the ray is going in
-	int di = (dir.X >= 0 ? 1 : 0);
-	int dj = (dir.Z >= 0 ? 1 : 0);
+	int di = (dir.getX() >= 0 ? 1 : 0);
+	int dj = (dir.getZ() >= 0 ? 1 : 0);
 
 	do
 	{
 		CVector3D isct;
 		if (TestTile(heightmap, heightmapStride, i, j, originPatch, dir, isct))
 		{
 			if (out)
 				*out = isct + patchPos;
 			return true;
 		}
 
 		// Get the vertex between the two possible next cells
 		float nx = (i + di) * (int)TERRAIN_TILE_SIZE;
 		float nz = (j + dj) * (int)TERRAIN_TILE_SIZE;
 
 		// Test which side of the ray the vertex is on, and advance into the
 		// appropriate cell, using a test that works for all 4 combinations
 		// of di,dj
-		float dot = dir.Z * (nx - originPatch.X) - dir.X * (nz - originPatch.Z);
+		float dot = dir.getZ() * (nx - originPatch.getX()) - dir.getX() * (nz - originPatch.getZ());
 		if ((di == dj) == (dot > 0.0f))
 			j += dj*2-1;
 		else
 			i += di*2-1;
 	}
 	while (i >= 0 && j >= 0 && i < PATCH_SIZE && j < PATCH_SIZE);
 
 	// Ran off the edge of the patch, so no intersection
 	return false;
 }
Index: source/graphics/LightEnv.cpp
===================================================================
--- source/graphics/LightEnv.cpp	(revision 24796)
+++ source/graphics/LightEnv.cpp	(working copy)
@@ -1,57 +1,57 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include "graphics/LightEnv.h"
 
 #include "maths/MathUtil.h"
-
+#include "maths/Vector3D.h"
 
 CLightEnv::CLightEnv()
 	: m_Elevation(DEGTORAD(45)),
 	m_Rotation(DEGTORAD(315)),
 	m_SunColor(1.5, 1.5, 1.5),
 	m_AmbientColor(0x50/255.f, 0x60/255.f, 0x85/255.f),
 	m_FogColor(0xCC/255.f, 0xCC/255.f, 0xE5/255.f),
 	m_FogFactor(0.000f),
 	m_FogMax(0.5f),
 	m_Brightness(0.0f), m_Contrast(1.0f), m_Saturation(0.99f), m_Bloom(0.1999f)
 {
 	CalculateSunDirection();
 }
 
 void CLightEnv::SetElevation(float f)
 {
 	m_Elevation = f;
 	CalculateSunDirection();
 }
 
 void CLightEnv::SetRotation(float f)
 {
 	m_Rotation = f;
 	CalculateSunDirection();
 }
 
 void CLightEnv::CalculateSunDirection()
 {
-	m_SunDir.Y = -sinf(m_Elevation);
-	float scale = 1 + m_SunDir.Y;
-	m_SunDir.X = scale * sinf(m_Rotation);
-	m_SunDir.Z = scale * cosf(m_Rotation);
+	m_SunDir.Yref() = -sinf(m_Elevation);
+	float scale = 1 + m_SunDir.getY();
+	m_SunDir.Xref() = scale * sinf(m_Rotation);
+	m_SunDir.Zref() = scale * cosf(m_Rotation);
 	m_SunDir.Normalize();
 }
Index: source/graphics/LightEnv.h
===================================================================
--- source/graphics/LightEnv.h	(revision 24796)
+++ source/graphics/LightEnv.h	(working copy)
@@ -1,125 +1,124 @@
 /* Copyright (C) 2021 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 .
  */
 
 /*
  * CLightEnv, a class describing the current lights
  */
 
 #ifndef INCLUDED_LIGHTENV
 #define INCLUDED_LIGHTENV
 
-#include "graphics/Color.h"
-#include "maths/MathUtil.h"
-#include "maths/Vector3D.h"
+#include "graphics/Color.h"  // HAS to be here, used in structs
 
 class CMapWriter;
 class CMapReader;
+class CVector3D;
 
 /**
  * Class CLightEnv: description of a lighting environment - contains all the
  * necessary parameters for representation of the lighting within a scenario
  */
 class CLightEnv
 {
 public:
-	RGBColor m_SunColor;
-	RGBColor m_AmbientColor;
-	RGBColor m_FogColor;
+	RGBcolor m_SunColor;
+	RGBcolor m_AmbientColor;
+	RGBcolor m_FogColor;
 
 	float m_FogFactor;
 	float m_FogMax;
 
 	float m_Brightness, m_Contrast, m_Saturation, m_Bloom;
 
 	CLightEnv();
 
 	float GetElevation() const { return m_Elevation; }
 	float GetRotation() const { return m_Rotation; }
 	const CVector3D& GetSunDir() const { return m_SunDir; }
 
 	void SetElevation(float f);
 	void SetRotation(float f);
 
 	/**
 	 * Calculate brightness of a point of a unit with the given normal vector,
 	 * for rendering with CPU lighting.
 	 * The resulting color contains both ambient and diffuse light.
 	 * To cope with sun overbrightness, the color is scaled by 0.5.
 	 *
 	 * @param normal normal vector (must have length 1)
 	 */
-	RGBColor EvaluateUnitScaled(const CVector3D& normal) const
+	RGBcolor EvaluateUnitScaled(const CVector3D& normal) const
 	{
 		float dot = -normal.Dot(m_SunDir);
 
-		RGBColor color = m_AmbientColor;
+		RGBcolor color = m_AmbientColor;
 		if (dot > 0)
 			color += m_SunColor * dot;
 
 		return color * 0.5f;
 	}
 
 	// Comparison operators
 	bool operator==(const CLightEnv& o) const
 	{
 		return m_Elevation == o.m_Elevation &&
 			m_Rotation == o.m_Rotation &&
 			m_SunColor == o.m_SunColor &&
 			m_AmbientColor == o.m_AmbientColor &&
 			m_FogColor == o.m_FogColor &&
 			m_FogFactor == o.m_FogFactor &&
 			m_FogMax == o.m_FogMax &&
 			m_Brightness == o.m_Brightness &&
 			m_Contrast == o.m_Contrast &&
 			m_Saturation == o.m_Saturation &&
 			m_Bloom == o.m_Bloom;
 	}
 
 	bool operator!=(const CLightEnv& o) const
 	{
 		return !(*this == o);
 	}
 
 private:
 	friend class CMapWriter;
 	friend class CMapReader;
 	friend class CXMLReader;
 
 	/**
 	* Height of sun above the horizon, in radians.
 	* For example, an elevation of M_PI/2 means the sun is straight up.
 	*/
 	float m_Elevation;
 
 	/**
 	* Direction of sun on the compass, in radians.
 	* For example, a rotation of zero means the sun is in the direction (0,0,-1)
 	* and a rotation of M_PI/2 means the sun is in the direction (1,0,0) (not taking
 	* elevation into account).
 	*/
 	float m_Rotation;
 
 	/**
 	* Vector corresponding to m_Elevation and m_Rotation.
 	* Updated by CalculateSunDirection.
 	*/
 	CVector3D m_SunDir;
 
 	void CalculateSunDirection();
 };
 
 #endif // INCLUDED_LIGHTENV
Index: source/graphics/MapReader.cpp
===================================================================
--- source/graphics/MapReader.cpp	(revision 24796)
+++ source/graphics/MapReader.cpp	(working copy)
@@ -1,1603 +1,1603 @@
 /* Copyright (C) 2021 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 .
  */
 
 #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 "lib/external_libraries/libsdl.h"
 #include "maths/MathUtil.h"
 #include "ps/CLogger.h"
 #include "ps/Loader.h"
 #include "ps/LoaderThunks.h"
 #include "ps/World.h"
 #include "ps/XML/Xeromyces.h"
 #include "renderer/PostprocManager.h"
 #include "renderer/SkyManager.h"
 #include "renderer/WaterManager.h"
 #include "scriptinterface/ScriptContext.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 
 
 #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
 
 CMapReader::CMapReader()
 	: xml_reader(0), m_PatchesPerSide(0), m_MapGen(0)
 {
 	cur_terrain_tex = 0;	// important - resets generator state
 }
 
 // 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())
 		RegMemFun(this, &CMapReader::LoadScriptSettings, L"CMapReader::LoadScriptSettings", 50);
 	else
 		RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50);
 
 	// load player settings script (must be done before reading map)
 	RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50);
 
 	// unpack the data
 	if (!only_xml)
 		RegMemFun(this, &CMapReader::UnpackMap, L"CMapReader::UnpackMap", 1200);
 
 	// read the corresponding XML file
 	RegMemFun(this, &CMapReader::ReadXML, L"CMapReader::ReadXML", 50);
 
 	// apply terrain data to the world
 	RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5);
 
 	// read entities
 	RegMemFun(this, &CMapReader::ReadXMLEntities, L"CMapReader::ReadXMLEntities", 5800);
 
 	// apply misc data to the world
 	RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5);
 
 	// load map settings script (must be done after reading map)
 	RegMemFun(this, &CMapReader::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_)
 {
 	m_ScriptFile = scriptFile;
 	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)
 	RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50);
 
 	// load player settings script (must be done before reading map)
 	RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50);
 
 	// load map generator with random map script
 	RegMemFun(this, &CMapReader::GenerateMap, L"CMapReader::GenerateMap", 20000);
 
 	// parse RMS results into terrain structure
 	RegMemFun(this, &CMapReader::ParseTerrain, L"CMapReader::ParseTerrain", 500);
 
 	// parse RMS results into environment settings
 	RegMemFun(this, &CMapReader::ParseEnvironment, L"CMapReader::ParseEnvironment", 5);
 
 	// parse RMS results into camera settings
 	RegMemFun(this, &CMapReader::ParseCamera, L"CMapReader::ParseCamera", 5);
 
 	// apply terrain data to the world
 	RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5);
 
 	// parse RMS results into entities
 	RegMemFun(this, &CMapReader::ParseEntities, L"CMapReader::ParseEntities", 1000);
 
 	// apply misc data to the world
 	RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5);
 
 	// load map settings script (must be done after reading map)
 	RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5);
 }
 
 // UnpackMap: unpack the given data from the raw data stream into local variables
 int CMapReader::UnpackMap()
 {
 	return UnpackTerrain();
 }
 
 // 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);
 
 		ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled)
 		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]);
 
 		// setup the textures on the minipatches
 		STileDesc* tileptr = &m_Tiles[0];
 		for (ssize_t j=0; jGetPatch(i,j)->m_MiniPatches[m][k];	// can't fail
 
 						mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index];
 						mp.Priority = tileptr->m_Priority;
 
 						tileptr++;
 					}
 				}
 			}
 		}
 	}
 
 	CmpPtr 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 cmpPlayerManager(*pSimContext, SYSTEM_ENTITY);
 
 	if (pGameView && cmpPlayerManager)
 	{
 		// Default to global camera (with constraints)
 		pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus());
 
 		// TODO: Starting rotation?
 		CmpPtr 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()));
+			pGameView->ResetCameraTarget(CVector3D(pos.getX().ToFloat(), pos.getY().ToFloat(), pos.getZ().ToFloat()));
 		}
 		else if (m_StartingCameraTarget != INVALID_ENTITY)
 		{
 			// Point camera at entity
 			CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget);
 			if (cmpPosition)
 			{
 				CFixedVector3D pos = cmpPosition->GetPosition();
-				pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat()));
+				pGameView->ResetCameraTarget(CVector3D(pos.getX().ToFloat(), pos.getY().ToFloat(), pos.getZ().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);
 
 	ScriptInterface::CreateObject(rq, ret);
 
 	if (m_ScriptSettings.empty())
 		return;
 
 	JS::RootedValue scriptSettingsVal(rq.cx);
 	scriptInterface.ParseJSON(m_ScriptSettings, &scriptSettingsVal);
 	scriptInterface.SetProperty(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.GetElementString(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.GetElementString(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
 	ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled)
 	CTerrainTextureEntry* 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(
+			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(
+			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(
+					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 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( \
+						out = RGBAcolor( \
 							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 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 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 Garrison;
 		std::vector> 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();
 
 			// 
 			if (element_name == el_template)
 			{
 				TemplateName = setting.GetText().FromUTF8();
 			}
 			// 
 			else if (element_name == el_player)
 			{
 				PlayerID = setting.GetText().ToInt();
 			}
 			// 
 			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)));
 			}
 			// 
 			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?
 			}
 			// 
 			else if (element_name == el_obstruction)
 			{
 				XMBAttributeList obstructionAttrs = setting.GetAttributes();
 				ControlGroup = obstructionAttrs.GetNamedItem(at_group).ToInt();
 				ControlGroup2 = obstructionAttrs.GetNamedItem(at_group2).ToInt();
 			}
 			// 
 			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());
 				}
 			}
 			// 
 			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()
 					);
 				}
 			}
 			// 
 			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 cmpPosition(sim, ent);
 			if (cmpPosition)
 			{
-				cmpPosition->JumpTo(Position.X, Position.Z);
-				cmpPosition->SetYRotation(Orientation.Y);
+				cmpPosition->JumpTo(Position.getX(), Position.getZ());
+				cmpPosition->SetYRotation(Orientation.getY());
 				// TODO: other parts of the position
 			}
 
 			CmpPtr cmpOwnership(sim, ent);
 			if (cmpOwnership)
 				cmpOwnership->SetOwner(PlayerID);
 
 			if (!Garrison.empty())
 			{
 				CmpPtr 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);
 			}
 
 			if (!Turrets.empty())
 			{
 				CmpPtr 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 cmpObstruction(sim, ent);
 			if (cmpObstruction)
 			{
 				if (ControlGroup != INVALID_ENTITY)
 					cmpObstruction->SetControlGroup(ControlGroup);
 				if (ControlGroup2 != INVALID_ENTITY)
 					cmpObstruction->SetControlGroup2(ControlGroup2);
 
 				cmpObstruction->ResolveFoundationCollisions();
 			}
 
 			CmpPtr 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;
 }
 
 int CMapReader::GenerateMap()
 {
 	ScriptRequest rq(pSimulation2->GetScriptInterface());
 
 	if (!m_MapGen)
 	{
 		// Initialize map generator
 		m_MapGen = new CMapGenerator();
 
 		VfsPath scriptPath;
 
 		if (m_ScriptFile.length())
 			scriptPath = L"maps/random/"+m_ScriptFile;
 
 		// Stringify settings to pass across threads
 		std::string scriptSettings = pSimulation2->GetScriptInterface().StringifyJSON(&m_ScriptSettings);
 
 		// Try to generate map
 		m_MapGen->GenerateMap(scriptPath, scriptSettings);
 	}
 
 	// Check status
 	int progress = m_MapGen->GetProgress();
 	if (progress < 0)
 	{
 		// RMS failed - return to main menu
 		throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details.");
 	}
 	else if (progress == 0)
 	{
 		// Finished, get results as StructuredClone object, which must be read to obtain the JS::Value
 		ScriptInterface::StructuredClone results = m_MapGen->GetResults();
 
 		// Parse data into simulation context
 		JS::RootedValue data(rq.cx);
 		pSimulation2->GetScriptInterface().ReadStructuredClone(results, &data);
 
 		if (data.isUndefined())
 		{
 			// RMS failed - return to main menu
 			throw PSERROR_Game_World_MapLoadFailed("Error generating random map.\nCheck application log for details.");
 		}
 		else
 		{
 			m_MapData.init(rq.cx, data);
 		}
 	}
 	else
 	{
 		// Still working
 
 		// Sleep for a while, slowing down the rendering thread
 		// to allow more CPU for the map generator thread
 		SDL_Delay(100);
 	}
 
 	// return progress
 	return progress;
 };
 
 
 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 (!pSimulation2->GetScriptInterface().GetProperty(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 textureNames;
 	GET_TERRAIN_PROPERTY(m_MapData, textureNames, textureNames)
 	num_terrain_tex = textureNames.size();
 
 	while (cur_terrain_tex < num_terrain_tex)
 	{
 		ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled)
 		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 tileIndex;
 	std::vector 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 entities;
 
 	if (!pSimulation2->GetScriptInterface().GetProperty(m_MapData, "entities", entities))
 		LOGWARNING("CMapReader::ParseEntities() failed to get 'entities' property");
 
 	CSimulation2& sim = *pSimulation2;
 	CmpPtr 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 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);
+				cmpPosition->JumpTo(currEnt.position.getX() * (int)TERRAIN_TILE_SIZE, currEnt.position.getZ() * (int)TERRAIN_TILE_SIZE);
+				cmpPosition->SetYRotation(currEnt.rotation.getY());
 				// TODO: other parts of the position
 			}
 
 			CmpPtr 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 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 (!pSimulation2->GetScriptInterface().GetProperty(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;
+	RGBAcolor sunColor;
 	GET_ENVIRONMENT_PROPERTY(envObj, SunColor, sunColor)
-	m_LightEnv.m_SunColor = RGBColor(sunColor.r, sunColor.g, sunColor.b);
+	m_LightEnv.m_SunColor = RGBcolor(sunColor.getR(), sunColor.getG(), sunColor.getB());
 
 	GET_ENVIRONMENT_PROPERTY(envObj, SunElevation, m_LightEnv.m_Elevation)
 	GET_ENVIRONMENT_PROPERTY(envObj, SunRotation, m_LightEnv.m_Rotation)
 
-	CColor ambientColor;
+	RGBAcolor ambientColor;
 	GET_ENVIRONMENT_PROPERTY(envObj, AmbientColor, ambientColor)
-	m_LightEnv.m_AmbientColor = RGBColor(ambientColor.r, ambientColor.g, ambientColor.b);
+	m_LightEnv.m_AmbientColor = RGBcolor(ambientColor.getR(), ambientColor.getG(), ambientColor.getB());
 
 	// 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 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;
+	RGBAcolor fogColor;
 	GET_ENVIRONMENT_PROPERTY(fogObject, FogColor, fogColor);
-	m_LightEnv.m_FogColor = RGBColor(fogColor.r, fogColor.g, fogColor.b);
+	m_LightEnv.m_FogColor = RGBcolor(fogColor.getR(), fogColor.getG(), fogColor.getB());
 
 	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 (!pSimulation2->GetScriptInterface().GetProperty(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;
 	delete m_MapGen;
 }
Index: source/graphics/MapWriter.cpp
===================================================================
--- source/graphics/MapWriter.cpp	(revision 24796)
+++ source/graphics/MapWriter.cpp	(working copy)
@@ -1,505 +1,505 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include "Camera.h"
 #include "CinemaManager.h"
 #include "GameView.h"
 #include "LightEnv.h"
 #include "MapReader.h"
 #include "MapWriter.h"
 #include "Patch.h"
 #include "Terrain.h"
 #include "TerrainTextureEntry.h"
 #include "TerrainTextureManager.h"
 
 #include "maths/MathUtil.h"
 #include "maths/NUSpline.h"
 #include "ps/CLogger.h"
 #include "ps/Loader.h"
 #include "ps/Filesystem.h"
 #include "ps/XML/XMLWriter.h"
 #include "renderer/PostprocManager.h"
 #include "renderer/SkyManager.h"
 #include "renderer/WaterManager.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/ICmpPosition.h"
 #include "simulation2/components/ICmpTemplateManager.h"
 #include "simulation2/components/ICmpTurretHolder.h"
 #include "simulation2/components/ICmpVisual.h"
 #include "simulation2/components/ICmpWaterManager.h"
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // CMapWriter constructor: nothing to do at the minute
 CMapWriter::CMapWriter()
 {
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // SaveMap: try to save the current map to the given file
 void CMapWriter::SaveMap(const VfsPath& pathname, CTerrain* pTerrain,
 						 WaterManager* pWaterMan, SkyManager* pSkyMan,
 						 CLightEnv* pLightEnv, CCamera* pCamera, CCinemaManager* UNUSED(pCinema),
 						 CPostprocManager* pPostproc,
 						 CSimulation2* pSimulation2)
 {
 	CFilePacker packer(FILE_VERSION, "PSMP");
 
 	// build necessary data
 	PackMap(packer, pTerrain);
 
 	try
 	{
 		// write it out
 		packer.Write(pathname);
 	}
 	catch (PSERROR_File_WriteFailed&)
 	{
 		LOGERROR("Failed to write map '%s'", pathname.string8());
 		return;
 	}
 
 	VfsPath pathnameXML = pathname.ChangeExtension(L".xml");
 	WriteXML(pathnameXML, pWaterMan, pSkyMan, pLightEnv, pCamera, pPostproc, pSimulation2);
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // GetHandleIndex: return the index of the given handle in the given list; or 0xFFFF if
 // handle isn't in list
 static u16 GetEntryIndex(const CTerrainTextureEntry* entry, const std::vector& entries)
 {
 	const size_t limit = std::min(entries.size(), size_t(0xFFFEu));	// paranoia
 	for (size_t i=0;i& textures,
 									 std::vector& tiles)
 {
 	// the list of all handles in use
 	std::vector entries;
 
 	// resize tile array to required size
 	tiles.resize(SQR(pTerrain->GetVerticesPerSide()-1));
 	STileDesc* tileptr=&tiles[0];
 
 	// now iterate through all the tiles
 	const ssize_t patchesPerSide=pTerrain->GetPatchesPerSide();
 	for (ssize_t j=0;jGetPatch(i,j)->m_MiniPatches[m][k];	// can't fail
 					u16 index=u16(GetEntryIndex(mp.GetTextureEntry(),entries));
 					if (index==0xFFFF) {
 						index=(u16)entries.size();
 						entries.push_back(mp.GetTextureEntry());
 					}
 
 					tileptr->m_Tex1Index=index;
 					tileptr->m_Tex2Index=0xFFFF;
 					tileptr->m_Priority=mp.GetPriority();
 					tileptr++;
 				}
 			}
 		}
 	}
 
 	// now find the texture names for each handle
 	for (size_t i=0;iGetTag();
 		}
 		textures.push_back(texturename);
 	}
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PackMap: pack the current world into a raw data stream
 void CMapWriter::PackMap(CFilePacker& packer, CTerrain* pTerrain)
 {
 	// now pack everything up
 	PackTerrain(packer, pTerrain);
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PackTerrain: pack the terrain onto the end of the output data stream
 //		- data: map size, heightmap, list of textures used by map, texture tile assignments
 void CMapWriter::PackTerrain(CFilePacker& packer, CTerrain* pTerrain)
 {
 	// pack map size
 	const ssize_t mapsize = pTerrain->GetPatchesPerSide();
 	packer.PackSize(mapsize);
 
 	// pack heightmap
 	packer.PackRaw(pTerrain->GetHeightMap(),sizeof(u16)*SQR(pTerrain->GetVerticesPerSide()));
 
 	// the list of textures used by map
 	std::vector terrainTextures;
 	// descriptions of each tile
 	std::vector tiles;
 
 	// build lists by scanning through the terrain
 	EnumTerrainTextures(pTerrain, terrainTextures, tiles);
 
 	// pack texture names
 	const size_t numTextures = terrainTextures.size();
 	packer.PackSize(numTextures);
 	for (size_t i=0;i(FILE_VERSION));
 
 		ENSURE(pSimulation2);
 		CSimulation2& sim = *pSimulation2;
 
 		if (!sim.GetStartupScript().empty())
 		{
 			XMLWriter_Element scriptTag(xmlMapFile, "Script");
 			scriptTag.Text(sim.GetStartupScript().c_str(), true);
 		}
 
 		{
 			XMLWriter_Element environmentTag(xmlMapFile, "Environment");
 			environmentTag.Setting("SkySet", pSkyMan->GetSkySet());
 			{
 				XMLWriter_Element sunColorTag(xmlMapFile, "SunColor");
-				sunColorTag.Attribute("r", pLightEnv->m_SunColor.X); // yes, it's X/Y/Z...
-				sunColorTag.Attribute("g", pLightEnv->m_SunColor.Y);
-				sunColorTag.Attribute("b", pLightEnv->m_SunColor.Z);
+				sunColorTag.Attribute("r", pLightEnv->m_SunColor.getR()); // yes, it's X/Y/Z...
+				sunColorTag.Attribute("g", pLightEnv->m_SunColor.getG());
+				sunColorTag.Attribute("b", pLightEnv->m_SunColor.getB());
 			}
 			{
 				XMLWriter_Element SunElevationTag(xmlMapFile, "SunElevation");
 				SunElevationTag.Attribute("angle", pLightEnv->m_Elevation);
 			}
 			{
 				XMLWriter_Element sunRotationTag(xmlMapFile, "SunRotation");
 				sunRotationTag.Attribute("angle", pLightEnv->m_Rotation);
 			}
 			{
 				XMLWriter_Element ambientColorTag(xmlMapFile, "AmbientColor");
-				ambientColorTag.Attribute("r", pLightEnv->m_AmbientColor.X);
-				ambientColorTag.Attribute("g", pLightEnv->m_AmbientColor.Y);
-				ambientColorTag.Attribute("b", pLightEnv->m_AmbientColor.Z);
+				ambientColorTag.Attribute("r", pLightEnv->m_AmbientColor.getR());
+				ambientColorTag.Attribute("g", pLightEnv->m_AmbientColor.getG());
+				ambientColorTag.Attribute("b", pLightEnv->m_AmbientColor.getB());
 			}
 			{
 				XMLWriter_Element fogTag(xmlMapFile, "Fog");
 				fogTag.Setting("FogFactor", pLightEnv->m_FogFactor);
 				fogTag.Setting("FogThickness", pLightEnv->m_FogMax);
 				{
 					XMLWriter_Element fogColorTag(xmlMapFile, "FogColor");
-					fogColorTag.Attribute("r", pLightEnv->m_FogColor.X);
-					fogColorTag.Attribute("g", pLightEnv->m_FogColor.Y);
-					fogColorTag.Attribute("b", pLightEnv->m_FogColor.Z);
+					fogColorTag.Attribute("r", pLightEnv->m_FogColor.getR());
+					fogColorTag.Attribute("g", pLightEnv->m_FogColor.getG());
+					fogColorTag.Attribute("b", pLightEnv->m_FogColor.getB());
 				}
 			}
 
 			{
 				XMLWriter_Element waterTag(xmlMapFile, "Water");
 				{
 					XMLWriter_Element waterBodyTag(xmlMapFile, "WaterBody");
 					CmpPtr cmpWaterManager(sim, SYSTEM_ENTITY);
 					ENSURE(cmpWaterManager);
 					waterBodyTag.Setting("Type", pWaterMan->m_WaterType);
 					{
 						XMLWriter_Element colorTag(xmlMapFile, "Color");
-						colorTag.Attribute("r", pWaterMan->m_WaterColor.r);
-						colorTag.Attribute("g", pWaterMan->m_WaterColor.g);
-						colorTag.Attribute("b", pWaterMan->m_WaterColor.b);
+						colorTag.Attribute("r", pWaterMan->m_WaterColor.getR());
+						colorTag.Attribute("g", pWaterMan->m_WaterColor.getG());
+						colorTag.Attribute("b", pWaterMan->m_WaterColor.getB());
 					}
 					{
 						XMLWriter_Element tintTag(xmlMapFile, "Tint");
-						tintTag.Attribute("r", pWaterMan->m_WaterTint.r);
-						tintTag.Attribute("g", pWaterMan->m_WaterTint.g);
-						tintTag.Attribute("b", pWaterMan->m_WaterTint.b);
+						tintTag.Attribute("r", pWaterMan->m_WaterTint.getR());
+						tintTag.Attribute("g", pWaterMan->m_WaterTint.getG());
+						tintTag.Attribute("b", pWaterMan->m_WaterTint.getB());
 					}
 					waterBodyTag.Setting("Height", cmpWaterManager->GetExactWaterLevel(0, 0));
 					waterBodyTag.Setting("Waviness", pWaterMan->m_Waviness);
 					waterBodyTag.Setting("Murkiness", pWaterMan->m_Murkiness);
 					waterBodyTag.Setting("WindAngle", pWaterMan->m_WindAngle);
 				}
 			}
 
 			{
 				XMLWriter_Element postProcTag(xmlMapFile, "Postproc");
 				{
 					postProcTag.Setting("Brightness", pLightEnv->m_Brightness);
 					postProcTag.Setting("Contrast", pLightEnv->m_Contrast);
 					postProcTag.Setting("Saturation", pLightEnv->m_Saturation);
 					postProcTag.Setting("Bloom", pLightEnv->m_Bloom);
 					postProcTag.Setting("PostEffect", pPostproc->GetPostEffect());
 				}
 			}
 		}
 
 		{
 			XMLWriter_Element cameraTag(xmlMapFile, "Camera");
 			{
 				XMLWriter_Element positionTag(xmlMapFile, "Position");
 				CVector3D pos = pCamera->GetOrientation().GetTranslation();
-				positionTag.Attribute("x", pos.X);
-				positionTag.Attribute("y", pos.Y);
-				positionTag.Attribute("z", pos.Z);
+				positionTag.Attribute("x", pos.getX());
+				positionTag.Attribute("y", pos.getY());
+				positionTag.Attribute("z", pos.getZ());
 			}
 
 			CVector3D in = pCamera->GetOrientation().GetIn();
 			// Convert to spherical coordinates
-			float rotation = atan2(in.X, in.Z);
-			float declination = atan2(sqrt(in.X*in.X + in.Z*in.Z), in.Y) - static_cast(M_PI / 2);
+			float rotation = atan2(in.getX(), in.getZ());
+			float declination = atan2(sqrt(in.getX()*in.getX() + in.getZ()*in.getZ()), in.getY()) - static_cast(M_PI / 2);
 
 			{
 				XMLWriter_Element rotationTag(xmlMapFile, "Rotation");
 				rotationTag.Attribute("angle", rotation);
 			}
 			{
 				XMLWriter_Element declinationTag(xmlMapFile, "Declination");
 				declinationTag.Attribute("angle", declination);
 			}
 		}
 
 		{
 			std::string settings = sim.GetMapSettingsString();
 			if (!settings.empty())
 			{
 				XMLWriter_Element scriptSettingsTag(xmlMapFile, "ScriptSettings");
 				scriptSettingsTag.Text(("\n" + settings + "\n").c_str(), true);
 			}
 		}
 
 		{
 			XMLWriter_Element entitiesTag(xmlMapFile, "Entities");
 			CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY);
 			ENSURE(cmpTemplateManager);
 
 			// This will probably need to be changed in the future, but for now we'll
 			// just save all entities that have a position
 			CSimulation2::InterfaceList ents = sim.GetEntitiesWithInterface(IID_Position);
 			for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it)
 			{
 				entity_id_t ent = it->first;
 
 				// Don't save local entities (placement previews etc)
 				if (ENTITY_IS_LOCAL(ent))
 					continue;
 
 				XMLWriter_Element entityTag(xmlMapFile, "Entity");
 				entityTag.Attribute("uid", ent);
 
 				entityTag.Setting("Template", cmpTemplateManager->GetCurrentTemplateName(ent));
 
 				CmpPtr cmpOwnership(sim, ent);
 				if (cmpOwnership)
 					entityTag.Setting("Player", static_cast(cmpOwnership->GetOwner()));
 
 				CmpPtr cmpGarrisonHolder(sim, ent);
 				if (cmpGarrisonHolder)
 				{
 					std::vector garrison = cmpGarrisonHolder->GetEntities();
 					if (!garrison.empty())
 					{
 						XMLWriter_Element garrisonTag(xmlMapFile, "Garrison");
 						for (const entity_id_t garr_ent_id : garrison)
 						{
 							XMLWriter_Element garrisonedEntityTag(xmlMapFile, "GarrisonedEntity");
 							garrisonedEntityTag.Attribute("uid", static_cast(garr_ent_id));
 						}
 					}
 				}
 
 				CmpPtr cmpTurretHolder(sim, ent);
 				if (cmpTurretHolder)
 				{
 					std::vector > turrets = cmpTurretHolder->GetTurrets();
 					if (!turrets.empty())
 					{
 						XMLWriter_Element turretTag(xmlMapFile, "Turrets");
 						for (const std::pair& turret : turrets)
 						{
 							XMLWriter_Element turretedEntityTag(xmlMapFile, "Turret");
 							turretedEntityTag.Attribute("turret", turret.first);
 							turretedEntityTag.Attribute("uid", static_cast(turret.second));
 						}
 					}
 				}
 
 				CmpPtr cmpPosition(sim, ent);
 				if (cmpPosition)
 				{
 					CFixedVector3D pos;
 					if (cmpPosition->IsInWorld())
 						pos = cmpPosition->GetPosition();
 
 					CFixedVector3D rot = cmpPosition->GetRotation();
 					{
 						XMLWriter_Element positionTag(xmlMapFile, "Position");
-						positionTag.Attribute("x", pos.X);
-						positionTag.Attribute("z", pos.Z);
+						positionTag.Attribute("x", pos.getX());
+						positionTag.Attribute("z", pos.getZ());
 						// TODO: height offset etc
 					}
 					{
 						XMLWriter_Element orientationTag(xmlMapFile, "Orientation");
-						orientationTag.Attribute("y", rot.Y);
+						orientationTag.Attribute("y", rot.getY());
 						// TODO: X, Z maybe
 					}
 				}
 
 				CmpPtr cmpObstruction(sim, ent);
 				if (cmpObstruction)
 				{
 					// TODO: Currently only necessary because Atlas
 					// does not set up control groups for its walls.
 					cmpObstruction->ResolveFoundationCollisions();
 
 					entity_id_t group = cmpObstruction->GetControlGroup();
 					entity_id_t group2 = cmpObstruction->GetControlGroup2();
 
 					// Don't waste space writing the default control groups.
 					if (group != ent || group2 != INVALID_ENTITY)
 					{
 						XMLWriter_Element obstructionTag(xmlMapFile, "Obstruction");
 						if (group != ent)
 							obstructionTag.Attribute("group", group);
 						if (group2 != INVALID_ENTITY)
 							obstructionTag.Attribute("group2", group2);
 					}
 				}
 
 				CmpPtr cmpVisual(sim, ent);
 				if (cmpVisual)
 				{
 					entity_id_t seed = static_cast(cmpVisual->GetActorSeed());
 					if (seed != ent)
 					{
 						XMLWriter_Element actorTag(xmlMapFile, "Actor");
 						actorTag.Attribute("seed",seed);
 					}
 					// TODO: variation/selection strings
 				}
 			}
 		}
 
 
 		CmpPtr cmpCinemaManager(sim, SYSTEM_ENTITY);
 		if (cmpCinemaManager)
 		{
 			const std::map& paths = cmpCinemaManager->GetPaths();
 			std::map::const_iterator it = paths.begin();
 			XMLWriter_Element pathsTag(xmlMapFile, "Paths");
 
 			for ( ; it != paths.end(); ++it )
 			{
 				fixed timescale = it->second.GetTimescale();
 				const std::vector& position_nodes = it->second.GetAllNodes();
 				const std::vector& target_nodes = it->second.GetTargetSpline().GetAllNodes();
 				const CCinemaData* data = it->second.GetData();
 
 				XMLWriter_Element pathTag(xmlMapFile, "Path");
 				pathTag.Attribute("name", data->m_Name);
 				pathTag.Attribute("timescale", timescale);
 				pathTag.Attribute("orientation", data->m_Orientation);
 				pathTag.Attribute("mode", data->m_Mode);
 				pathTag.Attribute("style", data->m_Style);
 
 				struct SEvent
 				{
 					fixed time;
 					const char* type;
 					CFixedVector3D value;
 					SEvent(fixed time, const char* type, CFixedVector3D value)
 						: time(time), type(type), value(value)
 					{}
 					bool operator<(const SEvent& another) const
 					{
 						return time < another.time;
 					}
 				};
 
 				// All events of a manipulating of camera (position/rotation/target)
 				std::vector events;
 				events.reserve(position_nodes.size() + target_nodes.size());
 
 				fixed last_position = fixed::Zero();
 				for (size_t i = 0; i < position_nodes.size(); ++i)
 				{
 					fixed distance = i > 0 ? position_nodes[i - 1].Distance : fixed::Zero();
 					last_position += distance;
 					events.emplace_back(last_position, "Position", position_nodes[i].Position);
 				}
 
 				fixed last_target = fixed::Zero();
 				for (size_t i = 0; i < target_nodes.size(); ++i)
 				{
 					fixed distance = i > 0 ? target_nodes[i - 1].Distance : fixed::Zero();
 					last_target += distance;
 					events.emplace_back(last_target, "Target", target_nodes[i].Position);
 				}
 
 				std::sort(events.begin(), events.end());
 				for (size_t i = 0; i < events.size();)
 				{
 					XMLWriter_Element nodeTag(xmlMapFile, "Node");
 					fixed deltatime = i > 0 ? (events[i].time - events[i - 1].time) : fixed::Zero();
 					nodeTag.Attribute("deltatime", deltatime);
 					size_t j = i;
 					for (; j < events.size() && events[j].time == events[i].time; ++j)
 					{
 						// Types: Position/Rotation/Target
 						XMLWriter_Element eventTypeTag(xmlMapFile, events[j].type);
-						eventTypeTag.Attribute("x", events[j].value.X);
-						eventTypeTag.Attribute("y", events[j].value.Y);
-						eventTypeTag.Attribute("z", events[j].value.Z);
+						eventTypeTag.Attribute("x", events[j].value.getX());
+						eventTypeTag.Attribute("y", events[j].value.getY());
+						eventTypeTag.Attribute("z", events[j].value.getZ());
 					}
 					i = j;
 				}
 			}
 		}
 	}
 	if (!xmlMapFile.StoreVFS(g_VFS, filename))
 		LOGERROR("Failed to write map '%s'", filename.string8());
 }
Index: source/graphics/Material.cpp
===================================================================
--- source/graphics/Material.cpp	(revision 24796)
+++ source/graphics/Material.cpp	(working copy)
@@ -1,95 +1,97 @@
 /* Copyright (C) 2012 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 .
  */
 
 #include "precompiled.h"
 
+#include "graphics/Color.h"
 #include "Material.h"
+#include "ps/CStr.h"
 
-static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f);
+static RGBAcolor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f);
 
 CMaterial::CMaterial() :
 	m_AlphaBlending(false)
 {
 }
 
 void CMaterial::SetShaderEffect(const CStr& effect)
 {
 	m_ShaderEffect = CStrIntern(effect);
 }
 
 void CMaterial::AddShaderDefine(CStrIntern key, CStrIntern value)
 {
 	m_ShaderDefines.Add(key, value);
 	m_CombinedShaderDefines.clear();
 }
 
 void CMaterial::AddConditionalDefine(const char* defname, const char* defvalue, int type, std::vector &args)
 {
 	m_ConditionalDefines.Add(defname, defvalue, type, args);
 	m_CombinedShaderDefines.clear();
 }
 
 void CMaterial::AddStaticUniform(const char* key, const CVector4D& value)
 {
 	m_StaticUniforms.Add(key, value);
 }
 
 void CMaterial::AddSampler(const TextureSampler& texture)
 {
 	m_Samplers.push_back(texture);
 	if (texture.Name == str_baseTex)
 		m_DiffuseTexture = texture.Sampler;
 }
 
 void CMaterial::AddRenderQuery(const char* key)
 {
 	m_RenderQueries.Add(key);
 }
 
 void CMaterial::AddRequiredSampler(const CStr& samplerName)
 {
 	CStrIntern string(samplerName);
 	m_RequiredSamplers.push_back(string);
 }
 
 
 // Set up m_CombinedShaderDefines so that index i contains m_ShaderDefines, plus
 // the extra defines from m_ConditionalDefines[j] for all j where bit j is set in i.
 // This lets GetShaderDefines() cheaply return the defines for any combination of conditions.
 //
 // (This might scale badly if we had a large number of conditional defines per material,
 // but currently we don't expect to have many.)
 void CMaterial::RecomputeCombinedShaderDefines()
 {
 	m_CombinedShaderDefines.clear();
 	int size = m_ConditionalDefines.GetSize();
 
 	// Loop over all 2^n combinations of flags
 	for (int i = 0; i < (1 << size); i++)
 	{
 		CShaderDefines defs = m_ShaderDefines;
 		for (int j = 0; j < size; j++)
 		{
 			if (i & (1 << j))
 			{
 				const CShaderConditionalDefines::CondDefine& def = m_ConditionalDefines.GetItem(j);
 				defs.Add(def.m_DefName, def.m_DefValue);
 			}
 		}
 		m_CombinedShaderDefines.push_back(defs);
 	}
 }
Index: source/graphics/Material.h
===================================================================
--- source/graphics/Material.h	(revision 24796)
+++ source/graphics/Material.h	(working copy)
@@ -1,103 +1,103 @@
 /* Copyright (C) 2021 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 .
  */
 
 #ifndef INCLUDED_MATERIAL
 #define INCLUDED_MATERIAL
 
-#include "graphics/Color.h"
 #include "graphics/ShaderDefines.h"
-#include "graphics/Texture.h"
-#include "ps/CStr.h"
-#include "ps/CStrIntern.h"
+#include "graphics/Texture.h"  // HAS to be here; used structurally
+class CStr;
+class CVector4D;
+#include "ps/CStrIntern.h"  // HAS to be here; used structurally
 #include "simulation2/helpers/Player.h"
 
 class CMaterial
 {
 public:
 
 	struct TextureSampler
 	{
 		TextureSampler(const CStr &n, CTexturePtr t) : Name(n), Sampler(t) {}
 		TextureSampler(const CStrIntern &n, CTexturePtr t) : Name(n), Sampler(t) {}
 
 		CStrIntern Name;
 		CTexturePtr Sampler;
 	};
 
 	typedef std::vector SamplersVector;
 
 	CMaterial();
 
 	// Whether this material's shaders use alpha blending, in which case
 	// models using this material need to be rendered in a special order
 	// relative to the alpha-blended water plane
 	void SetUsesAlphaBlending(bool flag) { m_AlphaBlending = flag; }
  	bool UsesAlphaBlending() const { return m_AlphaBlending; }
 
 	const CTexturePtr& GetDiffuseTexture() const { return m_DiffuseTexture; }
 
 	void SetShaderEffect(const CStr& effect);
 	CStrIntern GetShaderEffect() const { return m_ShaderEffect; }
 
 	// Must call RecomputeCombinedShaderDefines after this, before rendering with this material
 	void AddShaderDefine(CStrIntern key, CStrIntern value);
 
 	// conditionFlags is a bitmask representing which indexes of the
 	// GetConditionalDefines() list are currently matching.
 	// Use 0 if you don't care about conditional defines.
 	const CShaderDefines& GetShaderDefines(uint32_t conditionFlags) const { return m_CombinedShaderDefines.at(conditionFlags); }
 
 	// Must call RecomputeCombinedShaderDefines after this, before rendering with this material
 	void AddConditionalDefine(const char* defname, const char* defvalue, int type, std::vector &args);
 
 	const CShaderConditionalDefines& GetConditionalDefines() const { return m_ConditionalDefines; }
 
 	void AddStaticUniform(const char* key, const CVector4D& value);
 	const CShaderUniforms& GetStaticUniforms() const { return m_StaticUniforms; }
 
 	void AddSampler(const TextureSampler& texture);
 	const SamplersVector& GetSamplers() const { return m_Samplers; }
 
 	void AddRenderQuery(const char* key);
 	const CShaderRenderQueries& GetRenderQueries() const { return m_RenderQueries; }
 
 	void AddRequiredSampler(const CStr& samplerName);
 	const std::vector& GetRequiredSampler() const { return m_RequiredSamplers; }
 
 	// Must be called after all AddShaderDefine and AddConditionalDefine
 	void RecomputeCombinedShaderDefines();
 
 private:
 
 	// This pointer is kept to make it easier for the fixed pipeline to
 	// access the only texture it's interested in.
 	CTexturePtr m_DiffuseTexture;
 
 	SamplersVector m_Samplers;
 	std::vector m_RequiredSamplers;
 
 	CStrIntern m_ShaderEffect;
 	CShaderDefines m_ShaderDefines;
 	CShaderConditionalDefines m_ConditionalDefines;
 	std::vector m_CombinedShaderDefines;
 	CShaderUniforms m_StaticUniforms;
 	CShaderRenderQueries m_RenderQueries;
 
 	bool m_AlphaBlending;
 };
 
 #endif
Index: source/graphics/Model.cpp
===================================================================
--- source/graphics/Model.cpp	(revision 24796)
+++ source/graphics/Model.cpp	(working copy)
@@ -1,676 +1,676 @@
 /* Copyright (C) 2021 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 .
  */
 
 /*
  * Mesh object with texture and skinning information
  */
 
 #include "precompiled.h"
 
 #include "Model.h"
 
 #include "Decal.h"
 #include "ModelDef.h"
 #include "maths/Quaternion.h"
 #include "maths/BoundingBoxAligned.h"
 #include "SkeletonAnim.h"
 #include "SkeletonAnimDef.h"
 #include "SkeletonAnimManager.h"
 #include "MeshManager.h"
 #include "ObjectEntry.h"
 #include "lib/res/graphics/ogl_tex.h"
 #include "lib/res/h_mgr.h"
 #include "lib/sysdep/rtl.h"
 #include "ps/Profile.h"
 #include "ps/CLogger.h"
 #include "renderer/RenderingOptions.h"
 #include "simulation2/Simulation2.h"
 #include "simulation2/components/ICmpTerrain.h"
 #include "simulation2/components/ICmpWaterManager.h"
 
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // Constructor
 CModel::CModel(CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation)
 	: m_Flags(0), m_Anim(NULL), m_AnimTime(0), m_Simulation(simulation),
 	m_BoneMatrices(NULL), m_AmmoPropPoint(NULL), m_AmmoLoadedProp(0),
 	m_SkeletonAnimManager(skeletonAnimManager)
 {
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // Destructor
 CModel::~CModel()
 {
 	ReleaseData();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // ReleaseData: delete anything allocated by the model
 void CModel::ReleaseData()
 {
 	rtl_FreeAligned(m_BoneMatrices);
 
 	for (size_t i = 0; i < m_Props.size(); ++i)
 		delete m_Props[i].m_Model;
 	m_Props.clear();
 
 	m_pModelDef = CModelDefPtr();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // InitModel: setup model from given geometry
 bool CModel::InitModel(const CModelDefPtr& modeldef)
 {
 	// clean up any existing data first
 	ReleaseData();
 
 	m_pModelDef = modeldef;
 
 	size_t numBones = modeldef->GetNumBones();
 	if (numBones != 0)
 	{
 		size_t numBlends = modeldef->GetNumBlends();
 
 		// allocate matrices for bone transformations
 		// (one extra matrix is used for the special case of bind-shape relative weighting)
 		m_BoneMatrices = (CMatrix3D*)rtl_AllocateAligned(sizeof(CMatrix3D) * (numBones + 1 + numBlends), 16);
 		for (size_t i = 0; i < numBones + 1 + numBlends; ++i)
 		{
 			m_BoneMatrices[i].SetIdentity();
 		}
 	}
 
 	m_PositionValid = true;
 
 	return true;
 }
 
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // CalcBound: calculate the world space bounds of this model
 void CModel::CalcBounds()
 {
 	// Need to calculate the object bounds first, if that hasn't already been done
 	if (! (m_Anim && m_Anim->m_AnimDef))
 	{
 		if (m_ObjectBounds.IsEmpty())
 			CalcStaticObjectBounds();
 	}
 	else
 	{
 		if (m_Anim->m_ObjectBounds.IsEmpty())
 			CalcAnimatedObjectBounds(m_Anim->m_AnimDef, m_Anim->m_ObjectBounds);
 		ENSURE(! m_Anim->m_ObjectBounds.IsEmpty()); // (if this happens, it'll be recalculating the bounds every time)
 		m_ObjectBounds = m_Anim->m_ObjectBounds;
 	}
 
 	// Ensure the transform is set correctly before we use it
 	ValidatePosition();
 
 	// Now transform the object-space bounds to world-space bounds
 	m_ObjectBounds.Transform(GetTransform(), m_WorldBounds);
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // CalcObjectBounds: calculate object space bounds of this model, based solely on vertex positions
 void CModel::CalcStaticObjectBounds()
 {
 	m_ObjectBounds.SetEmpty();
 
 	size_t numverts=m_pModelDef->GetNumVertices();
 	SModelVertex* verts=m_pModelDef->GetVertices();
 
 	for (size_t i=0;im_AnimDef)
 	{
 		CSkeletonAnim dummyanim;
 		dummyanim.m_AnimDef=anim;
 		if (!SetAnimation(&dummyanim)) return;
 	}
 
 	size_t numverts=m_pModelDef->GetNumVertices();
 	SModelVertex* verts=m_pModelDef->GetVertices();
 
 	// Remove any transformations, so that we calculate the bounding box
 	// at the origin. The box is later re-transformed onto the object, without
 	// having to recalculate the size of the box.
 	CMatrix3D transform, oldtransform = GetTransform();
 	CModelAbstract* oldparent = m_Parent;
 
 	m_Parent = 0;
 	transform.SetIdentity();
 	CRenderableObject::SetTransform(transform);
 
 	// Following seems to stomp over the current animation time - which, unsurprisingly,
 	// introduces artefacts in the currently playing animation. Save it here and restore it
 	// at the end.
 	float AnimTime = m_AnimTime;
 
 	// iterate through every frame of the animation
 	for (size_t j=0;jGetNumFrames();j++) {
 		m_PositionValid = false;
 		ValidatePosition();
 
 		// extend bounds by vertex positions at the frame
 		for (size_t i=0;iGetFrameTime();
 	}
 
 	m_PositionValid = false;
 	m_Parent = oldparent;
 	SetTransform(oldtransform);
 	m_AnimTime = AnimTime;
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 const CBoundingBoxAligned CModel::GetWorldBoundsRec()
 {
 	CBoundingBoxAligned bounds = GetWorldBounds();
 	for (size_t i = 0; i < m_Props.size(); ++i)
 		bounds += m_Props[i].m_Model->GetWorldBoundsRec();
 	return bounds;
 }
 
 const CBoundingBoxAligned CModel::GetObjectSelectionBoundsRec()
 {
 	CBoundingBoxAligned objBounds = GetObjectBounds();		// updates the (children-not-included) object-space bounds if necessary
 
 	// now extend these bounds to include the props' selection bounds (if any)
 	for (size_t i = 0; i < m_Props.size(); ++i)
 	{
 		const Prop& prop = m_Props[i];
 		if (prop.m_Hidden || !prop.m_Selectable)
 			continue; // prop is hidden from rendering, so it also shouldn't be used for selection
 
 		CBoundingBoxAligned propSelectionBounds = prop.m_Model->GetObjectSelectionBoundsRec();
 		if (propSelectionBounds.IsEmpty())
 			continue;	// submodel does not wish to participate in selection box, exclude it
 
 		// We have the prop's bounds in its own object-space; now we need to transform them so they can be properly added
 		// to the bounds in our object-space. For that, we need the transform of the prop attachment point.
 		//
 		// We have the prop point information; however, it's not trivial to compute its exact location in our object-space
 		// since it may or may not be attached to a bone (see SPropPoint), which in turn may or may not be in the middle of
 		// an animation. The bone matrices might be of interest, but they're really only meant to be used for the animation
 		// system and are quite opaque to use from the outside (see @ref ValidatePosition).
 		//
 		// However, a nice side effect of ValidatePosition is that it also computes the absolute world-space transform of
 		// our props and sets it on their respective models. In particular, @ref ValidatePosition will compute the prop's
 		// world-space transform as either
 		//
 		// T' = T x	B x O
 		// or
 		// T' = T x O
 		//
 		// where T' is the prop's world-space transform, T is our world-space transform, O is the prop's local
 		// offset/rotation matrix, and B is an optional transformation matrix of the bone the prop is attached to
 		// (taking into account animation and everything).
 		//
 		// From this, it is clear that either O or B x O is the object-space transformation matrix of the prop. So,
 		// all we need to do is apply our own inverse world-transform T^(-1) to T' to get our desired result. Luckily,
 		// this is precomputed upon setting the transform matrix (see @ref SetTransform), so it is free to fetch.
 
 		CMatrix3D propObjectTransform = prop.m_Model->GetTransform(); // T'
 		propObjectTransform.Concatenate(GetInvTransform()); // T^(-1) x T'
 
 		// Transform the prop's bounds into our object coordinate space
 		CBoundingBoxAligned transformedPropSelectionBounds;
 		propSelectionBounds.Transform(propObjectTransform, transformedPropSelectionBounds);
 
 		objBounds += transformedPropSelectionBounds;
 	}
 
 	return objBounds;
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // BuildAnimation: load raw animation frame animation from given file, and build a
 // animation specific to this model
 CSkeletonAnim* CModel::BuildAnimation(const VfsPath& pathname, const CStr& name, const CStr& ID, int frequency, float speed, float actionpos, float actionpos2, float soundpos)
 {
 	CSkeletonAnimDef* def = m_SkeletonAnimManager.GetAnimation(pathname);
 	if (!def)
 		return NULL;
 
 	CSkeletonAnim* anim = new CSkeletonAnim();
 	anim->m_Name = name;
 	anim->m_ID = ID;
 	anim->m_Frequency = frequency;
 	anim->m_AnimDef = def;
 	anim->m_Speed = speed;
 
 	if (actionpos == -1.f)
 		anim->m_ActionPos = -1.f;
 	else
 		anim->m_ActionPos = actionpos * anim->m_AnimDef->GetDuration();
 
 	if (actionpos2 == -1.f)
 		anim->m_ActionPos2 = -1.f;
 	else
 		anim->m_ActionPos2 = actionpos2 * anim->m_AnimDef->GetDuration();
 
 	if (soundpos == -1.f)
 		anim->m_SoundPos = -1.f;
 	else
 		anim->m_SoundPos = soundpos * anim->m_AnimDef->GetDuration();
 
 	anim->m_ObjectBounds.SetEmpty();
 	InvalidateBounds();
 
 	return anim;
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // Update: update this model to the given time, in msec
 void CModel::UpdateTo(float time)
 {
 	// update animation time, but don't calculate bone matrices - do that (lazily) when
 	// something requests them; that saves some calculation work for offscreen models,
 	// and also assures the world space, inverted bone matrices (required for normal
 	// skinning) are up to date with respect to m_Transform
 	m_AnimTime = time;
 
 	// mark vertices as dirty
 	SetDirty(RENDERDATA_UPDATE_VERTICES);
 
 	// mark matrices as dirty
 	InvalidatePosition();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // InvalidatePosition
 void CModel::InvalidatePosition()
 {
 	m_PositionValid = false;
 
 	for (size_t i = 0; i < m_Props.size(); ++i)
 		m_Props[i].m_Model->InvalidatePosition();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // ValidatePosition: ensure that current transform and bone matrices are both uptodate
 void CModel::ValidatePosition()
 {
 	if (m_PositionValid)
 	{
 		ENSURE(!m_Parent || m_Parent->m_PositionValid);
 		return;
 	}
 
 	if (m_Parent && !m_Parent->m_PositionValid)
 	{
 		// Make sure we don't base our calculations on
 		// a parent animation state that is out of date.
 		m_Parent->ValidatePosition();
 
 		// Parent will recursively call our validation.
 		ENSURE(m_PositionValid);
 		return;
 	}
 
 	if (m_Anim && m_BoneMatrices)
 	{
 //		PROFILE( "generating bone matrices" );
 
 		ENSURE(m_pModelDef->GetNumBones() == m_Anim->m_AnimDef->GetNumKeys());
 
 		m_Anim->m_AnimDef->BuildBoneMatrices(m_AnimTime, m_BoneMatrices, !(m_Flags & MODELFLAG_NOLOOPANIMATION));
 	}
 	else if (m_BoneMatrices)
 	{
 		// Bones but no animation - probably a buggy actor forgot to set up the animation,
 		// so just render it in its bind pose
 
 		for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++)
 		{
 			m_BoneMatrices[i].SetIdentity();
 			m_BoneMatrices[i].Rotate(m_pModelDef->GetBones()[i].m_Rotation);
 			m_BoneMatrices[i].Translate(m_pModelDef->GetBones()[i].m_Translation);
 		}
 	}
 
 	// For CPU skinning, we precompute as much as possible so that the only
 	// per-vertex work is a single matrix*vec multiplication.
 	// For GPU skinning, we try to minimise CPU work by doing most computation
 	// in the vertex shader instead.
 	// Using g_RenderingOptions to detect CPU vs GPU is a bit hacky,
 	// and this doesn't allow the setting to change at runtime, but there isn't
 	// an obvious cleaner way to determine what data needs to be computed,
 	// and GPU skinning is a rarely-used experimental feature anyway.
 	bool worldSpaceBoneMatrices = !g_RenderingOptions.GetGPUSkinning();
 	bool computeBlendMatrices = !g_RenderingOptions.GetGPUSkinning();
 
 	if (m_BoneMatrices && worldSpaceBoneMatrices)
 	{
 		// add world-space transformation to m_BoneMatrices
 		const CMatrix3D transform = GetTransform();
 		for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++)
 			m_BoneMatrices[i].Concatenate(transform);
 	}
 
 	// our own position is now valid; now we can safely update our props' positions without fearing
 	// that doing so will cause a revalidation of this model (see recursion above).
 	m_PositionValid = true;
 
 	CMatrix3D translate;
 	CVector3D objTranslation = m_Transform.GetTranslation();
 	float objectHeight = 0.0f;
 
 	CmpPtr cmpTerrain(m_Simulation, SYSTEM_ENTITY);
 	if (cmpTerrain)
-		objectHeight = cmpTerrain->GetExactGroundLevel(objTranslation.X, objTranslation.Z);
+		objectHeight = cmpTerrain->GetExactGroundLevel(objTranslation.getX(), objTranslation.getZ());
 
 	// Object height is incorrect for floating objects. We use water height instead.
 	CmpPtr cmpWaterManager(m_Simulation, SYSTEM_ENTITY);
 	if (cmpWaterManager)
 	{
-		float waterHeight = cmpWaterManager->GetExactWaterLevel(objTranslation.X, objTranslation.Z);
+		float waterHeight = cmpWaterManager->GetExactWaterLevel(objTranslation.getX(), objTranslation.getZ());
 		if (waterHeight >= objectHeight && m_Flags & MODELFLAG_FLOATONWATER)
 			objectHeight = waterHeight;
 	}
 
 	// re-position and validate all props
 	for (const Prop& prop : m_Props)
 	{
 		CMatrix3D proptransform = prop.m_Point->m_Transform;
 		if (prop.m_Point->m_BoneIndex != 0xff)
 		{
 			CMatrix3D boneMatrix = m_BoneMatrices[prop.m_Point->m_BoneIndex];
 			if (!worldSpaceBoneMatrices)
 				boneMatrix.Concatenate(GetTransform());
 			proptransform.Concatenate(boneMatrix);
 		}
 		else
 		{
 			// not relative to any bone; just apply world-space transformation (i.e. relative to object-space origin)
 			proptransform.Concatenate(m_Transform);
 		}
 
 		// Adjust prop height to terrain level when needed
 		if (cmpTerrain && (prop.m_MaxHeight != 0.f || prop.m_MinHeight != 0.f))
 		{
 			const CVector3D& propTranslation = proptransform.GetTranslation();
-			const float propTerrain = cmpTerrain->GetExactGroundLevel(propTranslation.X, propTranslation.Z);
+			const float propTerrain = cmpTerrain->GetExactGroundLevel(propTranslation.getX(), propTranslation.getZ());
 			const float translateHeight = std::min(prop.m_MaxHeight, std::max(prop.m_MinHeight, propTerrain - objectHeight));
 			translate.SetTranslation(0.f, translateHeight, 0.f);
 			proptransform.Concatenate(translate);
 		}
 
 		prop.m_Model->SetTransform(proptransform);
 		prop.m_Model->ValidatePosition();
 	}
 
 	if (m_BoneMatrices)
 	{
 		for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++)
 		{
 			m_BoneMatrices[i] = m_BoneMatrices[i] * m_pModelDef->GetInverseBindBoneMatrices()[i];
 		}
 
 		// Note: there is a special case of joint influence, in which the vertex
 		//	is influenced by the bind-shape transform instead of a particular bone,
 		//	which we indicate with the blending bone ID set to the total number
 		//	of bones. But since we're skinning in world space, we use the model's
 		//	world space transform and store that matrix in this special index.
 		//	(see http://trac.wildfiregames.com/ticket/1012)
 		m_BoneMatrices[m_pModelDef->GetNumBones()] = m_Transform;
 
 		if (computeBlendMatrices)
 			m_pModelDef->BlendBoneMatrices(m_BoneMatrices);
 	}
 }
 
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // SetAnimation: set the given animation as the current animation on this model;
 // return false on error, else true
 bool CModel::SetAnimation(CSkeletonAnim* anim, bool once)
 {
 	m_Anim = nullptr; // in case something fails
 
 	if (anim)
 	{
 		m_Flags &= ~MODELFLAG_NOLOOPANIMATION;
 
 		if (once)
 			m_Flags |= MODELFLAG_NOLOOPANIMATION;
 
 		// Not rigged or animation is not valid.
 		if (!m_BoneMatrices || !anim->m_AnimDef)
 			return false;
 
 		if (anim->m_AnimDef->GetNumKeys() != m_pModelDef->GetNumBones())
 		{
 			LOGERROR("Mismatch between model's skeleton and animation's skeleton (%s.dae has %lu model bones while the animation %s has %lu animation keys.)",
 				m_pModelDef->GetName().string8().c_str() ,
 				static_cast(m_pModelDef->GetNumBones()), 
 				anim->m_Name.c_str(),
 				static_cast(anim->m_AnimDef->GetNumKeys()));
 			return false;
 		}
 
 		// Reset the cached bounds when the animation is changed.
 		m_ObjectBounds.SetEmpty();
 		InvalidateBounds();
 
 		// Start anim from beginning.
 		m_AnimTime = 0;
 	}
 
 	m_Anim = anim;
 
 	return true;
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // CopyAnimation
 void CModel::CopyAnimationFrom(CModel* source)
 {
 	m_Anim = source->m_Anim;
 	m_AnimTime = source->m_AnimTime;
 
 	m_Flags &= ~MODELFLAG_CASTSHADOWS;
 	if (source->m_Flags & MODELFLAG_CASTSHADOWS)
 		m_Flags |= MODELFLAG_CASTSHADOWS;
 
 	m_ObjectBounds.SetEmpty();
 	InvalidateBounds();
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // AddProp: add a prop to the model on the given point
 void CModel::AddProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry, float minHeight, float maxHeight, bool selectable)
 {
 	// position model according to prop point position
 
 	// this next call will invalidate the bounds of "model", which will in turn also invalidate the selection box
 	model->SetTransform(point->m_Transform);
 	model->m_Parent = this;
 
 	Prop prop;
 	prop.m_Point = point;
 	prop.m_Model = model;
 	prop.m_ObjectEntry = objectentry;
 	prop.m_MinHeight = minHeight;
 	prop.m_MaxHeight = maxHeight;
 	prop.m_Selectable = selectable;
 	m_Props.push_back(prop);
 }
 
 void CModel::AddAmmoProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry)
 {
 	AddProp(point, model, objectentry);
 	m_AmmoPropPoint = point;
 	m_AmmoLoadedProp = m_Props.size() - 1;
 	m_Props[m_AmmoLoadedProp].m_Hidden = true;
 
 	// we only need to invalidate the selection box here if it is based on props and their visibilities
 	if (!m_CustomSelectionShape)
 		m_SelectionBoxValid = false;
 }
 
 void CModel::ShowAmmoProp()
 {
 	if (m_AmmoPropPoint == NULL)
 		return;
 
 	// Show the ammo prop, hide all others on the same prop point
 	for (size_t i = 0; i < m_Props.size(); ++i)
 		if (m_Props[i].m_Point == m_AmmoPropPoint)
 			m_Props[i].m_Hidden = (i != m_AmmoLoadedProp);
 
 	//  we only need to invalidate the selection box here if it is based on props and their visibilities
 	if (!m_CustomSelectionShape)
 		m_SelectionBoxValid = false;
 }
 
 void CModel::HideAmmoProp()
 {
 	if (m_AmmoPropPoint == NULL)
 		return;
 
 	// Hide the ammo prop, show all others on the same prop point
 	for (size_t i = 0; i < m_Props.size(); ++i)
 		if (m_Props[i].m_Point == m_AmmoPropPoint)
 			m_Props[i].m_Hidden = (i == m_AmmoLoadedProp);
 
 	//  we only need to invalidate here if the selection box is based on props and their visibilities
 	if (!m_CustomSelectionShape)
 		m_SelectionBoxValid = false;
 }
 
 CModelAbstract* CModel::FindFirstAmmoProp()
 {
 	if (m_AmmoPropPoint)
 		return m_Props[m_AmmoLoadedProp].m_Model;
 
 	for (size_t i = 0; i < m_Props.size(); ++i)
 	{
 		CModel* propModel = m_Props[i].m_Model->ToCModel();
 		if (propModel)
 		{
 			CModelAbstract* model = propModel->FindFirstAmmoProp();
 			if (model)
 				return model;
 		}
 	}
 
 	return NULL;
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // Clone: return a clone of this model
 CModelAbstract* CModel::Clone() const
 {
 	CModel* clone = new CModel(m_SkeletonAnimManager, m_Simulation);
 	clone->m_ObjectBounds = m_ObjectBounds;
 	clone->InitModel(m_pModelDef);
 	clone->SetMaterial(m_Material);
 	clone->SetAnimation(m_Anim);
 	clone->SetFlags(m_Flags);
 
 	for (size_t i = 0; i < m_Props.size(); i++)
 	{
 		// eek!  TODO, RC - need to investigate shallow clone here
 		if (m_AmmoPropPoint && i == m_AmmoLoadedProp)
 			clone->AddAmmoProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry);
 		else
 			clone->AddProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry, m_Props[i].m_MinHeight, m_Props[i].m_MaxHeight, m_Props[i].m_Selectable);
 	}
 
 	return clone;
 }
 
 
 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
 // SetTransform: set the transform on this object, and reorientate props accordingly
 void CModel::SetTransform(const CMatrix3D& transform)
 {
 	// call base class to set transform on this object
 	CRenderableObject::SetTransform(transform);
 	InvalidatePosition();
 }
 
 //////////////////////////////////////////////////////////////////////////
 
 void CModel::AddFlagsRec(int flags)
 {
 	m_Flags |= flags;
 
 	if (flags & MODELFLAG_IGNORE_LOS)
 	{
 		m_Material.AddShaderDefine(str_IGNORE_LOS, str_1);
 		m_Material.RecomputeCombinedShaderDefines();
 	}
 
 	for (size_t i = 0; i < m_Props.size(); ++i)
 		if (m_Props[i].m_Model->ToCModel())
 			m_Props[i].m_Model->ToCModel()->AddFlagsRec(flags);
 }
 
 void CModel::RemoveShadowsRec()
 {
 	m_Flags &= ~MODELFLAG_CASTSHADOWS;
 
 	m_Material.AddShaderDefine(str_DISABLE_RECEIVE_SHADOWS, str_1);
 	m_Material.RecomputeCombinedShaderDefines();
 
 	for (size_t i = 0; i < m_Props.size(); ++i)
 	{
 		if (m_Props[i].m_Model->ToCModel())
 			m_Props[i].m_Model->ToCModel()->RemoveShadowsRec();
 		else if (m_Props[i].m_Model->ToCModelDecal())
 			m_Props[i].m_Model->ToCModelDecal()->RemoveShadows();
 	}
 }
 
 void CModel::SetMaterial(const CMaterial &material)
 {
 	m_Material = material;
 }
 
 void CModel::SetPlayerID(player_id_t id)
 {
 	CModelAbstract::SetPlayerID(id);
 
 	for (std::vector::iterator it = m_Props.begin(); it != m_Props.end(); ++it)
 		it->m_Model->SetPlayerID(id);
 }
 
-void CModel::SetShadingColor(const CColor& color)
+void CModel::SetShadingColor(const RGBAcolor& color)
 {
 	CModelAbstract::SetShadingColor(color);
 
 	for (std::vector::iterator it = m_Props.begin(); it != m_Props.end(); ++it)
 		it->m_Model->SetShadingColor(color);
 }
Index: source/graphics/Model.h
===================================================================
--- source/graphics/Model.h	(revision 24796)
+++ source/graphics/Model.h	(working copy)
@@ -1,307 +1,307 @@
 /* Copyright (C) 2021 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 .
  */
 
 /*
  * Mesh object with texture and skinning information
  */
 
 #ifndef INCLUDED_MODEL
 #define INCLUDED_MODEL
 
 #include 
 
 #include "graphics/Texture.h"
 #include "graphics/Material.h"
 #include "graphics/MeshManager.h"
 #include "graphics/ModelAbstract.h"
 
 struct SPropPoint;
 class CObjectEntry;
 class CSkeletonAnim;
 class CSkeletonAnimDef;
 class CSkeletonAnimManager;
 class CSimulation2;
 
 #define MODELFLAG_CASTSHADOWS		(1<<0)
 #define MODELFLAG_NOLOOPANIMATION	(1<<1)
 #define MODELFLAG_SILHOUETTE_DISPLAY	(1<<2)
 #define MODELFLAG_SILHOUETTE_OCCLUDER	(1<<3)
 #define MODELFLAG_IGNORE_LOS		(1<<4)
 #define MODELFLAG_FLOATONWATER		(1<<5)
 ///////////////////////////////////////////////////////////////////////////////
 // CModel: basically, a mesh object - holds the texturing and skinning
 // information for a model in game
 class CModel : public CModelAbstract
 {
 	NONCOPYABLE(CModel);
 
 public:
 	struct Prop
 	{
 		Prop() : m_MinHeight(0.f), m_MaxHeight(0.f), m_Point(0), m_Model(0), m_ObjectEntry(0), m_Hidden(false), m_Selectable(true) {}
 
 		float m_MinHeight;
 		float m_MaxHeight;
 
 		/**
 		 * Location of the prop point within its parent model, relative to either a bone in the parent model or to the
 		 * parent model's origin. See the documentation for @ref SPropPoint for more details.
 		 * @see SPropPoint
 		 */
 		const SPropPoint* m_Point;
 
 		/**
 		 * Pointer to the model associated with this prop. Note that the transform matrix held by this model is the full object-to-world
 		 * space transform, taking into account all parent model positioning (see @ref CModel::ValidatePosition for positioning logic).
 		 * @see CModel::ValidatePosition
 		 */
 		CModelAbstract* m_Model;
 		CObjectEntry* m_ObjectEntry;
 
 		bool m_Hidden; ///< Should this prop be temporarily removed from rendering?
 		bool m_Selectable; /// < should this prop count in the selection size?
 	};
 
 public:
 	// constructor
 	CModel(CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation);
 	// destructor
 	~CModel();
 
 
 	/// Dynamic cast
 	virtual CModel* ToCModel()
 	{
 		return this;
 	}
 
 	// setup model from given geometry
 	bool InitModel(const CModelDefPtr& modeldef);
 	// update this model's state; 'time' is the absolute time since the start of the animation, in MS
 	void UpdateTo(float time);
 
 	// get the model's geometry data
 	const CModelDefPtr& GetModelDef() { return m_pModelDef; }
 
 	// set the model's material
 	void SetMaterial(const CMaterial &material);
 	// set the model's player ID, recursively through props
 	void SetPlayerID(player_id_t id);
 	// set the models mod color
-	virtual void SetShadingColor(const CColor& color);
+	virtual void SetShadingColor(const RGBAcolor& color);
 	// get the model's material
 	CMaterial& GetMaterial() { return m_Material; }
 
 	// set the given animation as the current animation on this model
 	bool SetAnimation(CSkeletonAnim* anim, bool once = false);
 
 	// get the currently playing animation, if any
 	CSkeletonAnim* GetAnimation() const { return m_Anim; }
 
 	// set the animation state to be the same as from another; both models should
 	// be compatible types (same type of skeleton)
 	void CopyAnimationFrom(CModel* source);
 
 	// set object flags
 	void SetFlags(int flags) { m_Flags=flags; }
 	// get object flags
 	int GetFlags() const { return m_Flags; }
 	// add object flags, recursively through props
 	void AddFlagsRec(int flags);
 	// remove shadow casting and receiving, recursively through props
 	// TODO: replace with more generic shader define + flags setting
 	void RemoveShadowsRec();
 
 	// recurse down tree setting dirty bits
 	virtual void SetDirtyRec(int dirtyflags) {
 		SetDirty(dirtyflags);
 		for (size_t i=0;iSetDirtyRec(dirtyflags);
 		}
 	}
 
 	virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1)
 	{
 		for (size_t i = 0; i < m_Props.size(); ++i)
 			m_Props[i].m_Model->SetTerrainDirty(i0, j0, i1, j1);
 	}
 
 	virtual void SetEntityVariable(const std::string& name, float value)
 	{
 		for (size_t i = 0; i < m_Props.size(); ++i)
 			m_Props[i].m_Model->SetEntityVariable(name, value);
 	}
 
 	// --- WORLD/OBJECT SPACE BOUNDS -----------------------------------------------------------------
 
 	/// Overridden to calculate both the world-space and object-space bounds of this model, and stores the result in
 	/// m_Bounds and m_ObjectBounds, respectively.
 	virtual void CalcBounds();
 
 	/// Returns the object-space bounds for this model, excluding its children.
 	const CBoundingBoxAligned& GetObjectBounds()
 	{
 		RecalculateBoundsIfNecessary();				// recalculates both object-space and world-space bounds if necessary
 		return m_ObjectBounds;
 	}
 
 	virtual const CBoundingBoxAligned GetWorldBoundsRec();		// reimplemented here
 
 	/// Auxiliary method; calculates object space bounds of this model, based solely on vertex positions, and stores
 	/// the result in m_ObjectBounds. Called by CalcBounds (instead of CalcAnimatedObjectBounds) if it has been determined
 	/// that the object-space bounds are static.
 	void CalcStaticObjectBounds();
 
 	/// Auxiliary method; calculate object-space bounds encompassing all vertex positions for given animation, and stores
 	/// the result in m_ObjectBounds. Called by CalcBounds (instead of CalcStaticBounds) if it has been determined that the
 	/// object-space bounds need to take animations into account.
 	void CalcAnimatedObjectBounds(CSkeletonAnimDef* anim,CBoundingBoxAligned& result);
 
 	// --- SELECTION BOX/BOUNDS ----------------------------------------------------------------------
 
 	/// Reimplemented here since proper models should participate in selection boxes.
 	virtual const CBoundingBoxAligned GetObjectSelectionBoundsRec();
 
 	/**
 	 * Set transform of this object.
 	 *
 	 * @note In order to ensure that all child props are updated properly,
 	 * you must call ValidatePosition().
 	 */
 	virtual void SetTransform(const CMatrix3D& transform);
 
 	/**
 	 * Return whether this is a skinned/skeletal model. If it is, Get*BoneMatrices()
 	 * will return valid non-NULL arrays.
 	 */
 	bool IsSkinned() { return (m_BoneMatrices != NULL); }
 
 	// return the models bone matrices; 16-byte aligned for SSE reads
 	const CMatrix3D* GetAnimatedBoneMatrices() {
 		ENSURE(m_PositionValid);
 		return m_BoneMatrices;
 	}
 
 	/**
 	 * Load raw animation frame animation from given file, and build an
 	 * animation specific to this model.
 	 * @param pathname animation file to load
 	 * @param name animation name (e.g. "idle")
 	 * @param ID specific ID of the animation, to sync with props
 	 * @param frequency influences the random choices
 	 * @param speed animation speed as a factor of the default animation speed
 	 * @param actionpos offset of 'action' event, in range [0, 1]
 	 * @param actionpos2 offset of 'action2' event, in range [0, 1]
 	 * @param sound offset of 'sound' event, in range [0, 1]
 	 * @return new animation, or NULL on error
 	 */
 	CSkeletonAnim* BuildAnimation(const VfsPath& pathname, const CStr& name, const CStr& ID, int frequency, float speed, float actionpos, float actionpos2, float soundpos);
 
 	/**
 	 * Add a prop to the model on the given point.
 	 */
 	void AddProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry, float minHeight = 0.f, float maxHeight = 0.f, bool selectable = true);
 
 	/**
 	 * Add a prop to the model on the given point, and treat it as the ammo prop.
 	 * The prop will be hidden by default.
 	 */
 	void AddAmmoProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry);
 
 	/**
 	 * Show the ammo prop (if any), and hide any other props on that prop point.
 	 */
 	void ShowAmmoProp();
 
 	/**
 	 * Hide the ammo prop (if any), and show any other props on that prop point.
 	 */
 	void HideAmmoProp();
 
 	/**
 	 * Find the first prop used for ammo, by this model or its own props.
 	 */
 	CModelAbstract* FindFirstAmmoProp();
 
 	// return prop list
 	std::vector& GetProps() { return m_Props; }
 	const std::vector& GetProps() const { return m_Props; }
 
 	// return a clone of this model
 	virtual CModelAbstract* Clone() const;
 
 	/**
 	 * Ensure that both the transformation and the bone
 	 * matrices are correct for this model and all its props.
 	 */
 	virtual void ValidatePosition();
 
 	/**
 	 * Mark this model's position and bone matrices,
 	 * and all props' positions as invalid.
 	 */
 	virtual void InvalidatePosition();
 
 private:
 	// delete anything allocated by the model
 	void ReleaseData();
 
 	// Needed for terrain aligned props
 	CSimulation2& m_Simulation;
 
 	// object flags
 	int m_Flags;
 	// model's material
 	CMaterial m_Material;
 	// pointer to the model's raw 3d data
 	CModelDefPtr m_pModelDef;
 	// object space bounds of model - accounts for bounds of all possible animations
 	// that can play on a model. Not always up-to-date - currently CalcBounds()
 	// updates it when necessary.
 	CBoundingBoxAligned m_ObjectBounds;
 	// animation currently playing on this model, if any
 	CSkeletonAnim* m_Anim;
 	// time (in MS) into the current animation
 	float m_AnimTime;
 
 	/**
 	 * Current state of all bones on this model; null if associated modeldef isn't skeletal.
 	 * Props may attach to these bones by means of the SPropPoint::m_BoneIndex field; in this case their
 	 * transformation matrix held is relative to the bone transformation (see @ref SPropPoint and
 	 * @ref CModel::ValidatePosition).
 	 *
 	 * @see SPropPoint
 	 */
 	CMatrix3D* m_BoneMatrices;
 	// list of current props on model
 	std::vector m_Props;
 
 	/**
 	 * The prop point to which the ammo prop is attached, or NULL if none
 	 */
 	const SPropPoint* m_AmmoPropPoint;
 
 	/**
 	 * If m_AmmoPropPoint is not NULL, then the index in m_Props of the ammo prop
 	 */
 	size_t m_AmmoLoadedProp;
 
 	// manager object which can load animations for us
 	CSkeletonAnimManager& m_SkeletonAnimManager;
 };
 
 #endif
Index: source/graphics/ModelAbstract.cpp
===================================================================
--- source/graphics/ModelAbstract.cpp	(revision 24796)
+++ source/graphics/ModelAbstract.cpp	(working copy)
@@ -1,92 +1,92 @@
 /* Copyright (C) 2011 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 .
  */
 
 #include "precompiled.h"
 
 #include "ModelAbstract.h"
 
 #include "ps/CLogger.h"
 
 const CBoundingBoxOriented& CModelAbstract::GetSelectionBox()
 {
 	if (!m_SelectionBoxValid)
 	{
 		CalcSelectionBox();
 		m_SelectionBoxValid = true;
 	}
 	return m_SelectionBox;
 }
 
 void CModelAbstract::CalcSelectionBox()
 {
 	if (m_CustomSelectionShape)
 	{
 		// custom shape
 		switch(m_CustomSelectionShape->m_Type)
 		{
 		case CustomSelectionShape::BOX:
 			{
 				// create object-space bounds according to the information in the descriptor, and transform them to world-space.
 				// the box is centered on the X and Z axes, but extends from 0 to its height on the Y axis.
 				const float width = m_CustomSelectionShape->m_Size0;
 				const float depth = m_CustomSelectionShape->m_Size1;
 				const float height = m_CustomSelectionShape->m_Height;
 
 				CBoundingBoxAligned bounds;
 				bounds += CVector3D(-width/2.f, 0,     -depth/2.f);
 				bounds += CVector3D( width/2.f, height, depth/2.f);
 
 				bounds.Transform(GetTransform(), m_SelectionBox);
 			}
 			break;
 		case CustomSelectionShape::CYLINDER:
 			{
 				// TODO: unimplemented
 				m_SelectionBox.SetEmpty();
 				LOGWARNING("[ModelAbstract] TODO: Cylinder selection boxes are not yet implemented. Use BOX or BOUNDS selection shapes instead.");
 			}
 			break;
 		default:
 			{
 				m_SelectionBox.SetEmpty();
 				//LOGWARNING("[ModelAbstract] Unrecognized selection shape type: %ld", m_CustomSelectionShape->m_Type);
 				debug_warn("[ModelAbstract] Unrecognized selection shape type");
 			}
 			break;
 		}
 	}
 	else
 	{
 		// standard method
 
 		// Get the object-space bounds that should be used to construct this model (and its children)'s selection box
 		CBoundingBoxAligned objBounds = GetObjectSelectionBoundsRec();
 		if (objBounds.IsEmpty())
 		{
 			m_SelectionBox.SetEmpty(); // model does not wish to participate in selection
 			return;
 		}
 
 		// Prevent the bounding box from extending through the terrain; clip the lower plane at Y=0 in object space.
-		if (objBounds[1].Y > 0.f) // should always be the case, unless the models are defined really weirdly
-			objBounds[0].Y = std::max(0.f, objBounds[0].Y);
+		if (objBounds[1].getY() > 0.f) // should always be the case, unless the models are defined really weirdly
+			objBounds[0].Yref() = std::max(0.f, objBounds[0].getY());
 
 		// transform object-space axis-aligned bounds to world-space arbitrary-aligned box
 		objBounds.Transform(GetTransform(), m_SelectionBox);
 	}
 
 }
Index: source/graphics/ModelAbstract.h
===================================================================
--- source/graphics/ModelAbstract.h	(revision 24796)
+++ source/graphics/ModelAbstract.h	(working copy)
@@ -1,191 +1,191 @@
 /* Copyright (C) 2021 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 .
  */
 
 #ifndef INCLUDED_MODELABSTRACT
 #define INCLUDED_MODELABSTRACT
 
-#include "graphics/Color.h"
-#include "graphics/RenderableObject.h"
-#include "maths/BoundingBoxOriented.h"
+#include "graphics/Color.h"  // HAS to be here; used structurally
+#include "graphics/RenderableObject.h"  // HAS to be here; inherited
+#include "maths/BoundingBoxOriented.h"  // HAS to be here; used structurally
 #include "simulation2/helpers/Player.h"
 
 class CModel;
 class CModelDecal;
 class CModelParticleEmitter;
 
 /**
  * Abstract base class for graphical objects that are used by units,
  * or as props attached to other CModelAbstract objects.
  * This includes meshes, terrain decals, and sprites.
  * These objects exist in a tree hierarchy.
  */
 class CModelAbstract : public CRenderableObject
 {
 	NONCOPYABLE(CModelAbstract);
 
 public:
 
 	/**
 	 * Describes a custom selection shape to be used for a model's selection box instead of the default
 	 * recursive bounding boxes.
 	 */
 	struct CustomSelectionShape
 	{
 		enum EType {
 			/// The selection shape is determined by an oriented box of custom, user-specified size.
 			BOX,
 			/// The selection shape is determined by a cylinder of custom, user-specified size.
 			CYLINDER
 		};
 
 		EType m_Type; ///< Type of shape.
 		float m_Size0; ///< Box width if @ref BOX, or radius if @ref CYLINDER
 		float m_Size1; ///< Box depth if @ref BOX, or radius if @ref CYLINDER
 		float m_Height; ///< Box height if @ref BOX, cylinder height if @ref CYLINDER
 	};
 
 public:
 
 	CModelAbstract()
 		: m_Parent(NULL), m_PositionValid(false), m_ShadingColor(1, 1, 1, 1), m_PlayerID(INVALID_PLAYER),
 		  m_SelectionBoxValid(false), m_CustomSelectionShape(NULL)
 	{ }
 
 	~CModelAbstract()
 	{
 		delete m_CustomSelectionShape; // allocated and set externally by CCmpVisualActor, but our responsibility to clean up
 	}
 
 	virtual CModelAbstract* Clone() const = 0;
 
 	/// Dynamic cast
 	virtual CModel* ToCModel() { return NULL; }
 
 	/// Dynamic cast
 	virtual CModelDecal* ToCModelDecal() { return NULL; }
 
 	/// Dynamic cast
 	virtual CModelParticleEmitter* ToCModelParticleEmitter() { return NULL; }
 
 	// (This dynamic casting is a bit ugly, but we won't have many subclasses
 	// and this seems the easiest way to integrate with other code that wants
 	// type-specific processing)
 
 	/// Calls SetDirty on this model and all child objects.
 	virtual void SetDirtyRec(int dirtyflags) = 0;
 
 	/// Returns world space bounds of this object and all child objects.
 	virtual const CBoundingBoxAligned GetWorldBoundsRec() { return GetWorldBounds(); } // default implementation
 
 	/**
 	 * Returns the world-space selection box of this model. Used primarily for hittesting against against a selection ray. The
 	 * returned selection box may be empty to indicate that it does not wish to participate in the selection process.
 	 */
 	virtual const CBoundingBoxOriented& GetSelectionBox();
 
 	virtual void InvalidateBounds()
 	{
 		m_BoundsValid = false;
 		// a call to this method usually means that the model's transform has changed, i.e. it has moved or rotated, so we'll also
 		// want to update the selection box accordingly regardless of the shape it is built from.
 		m_SelectionBoxValid = false;
 	}
 
 	/// Sets a custom selection shape as described by a @p descriptor. Argument may be NULL
 	/// if you wish to keep the default behaviour of using the recursively-calculated bounding boxes.
 	void SetCustomSelectionShape(CustomSelectionShape* descriptor)
 	{
 		if (m_CustomSelectionShape != descriptor)
 		{
 			m_CustomSelectionShape = descriptor;
 			m_SelectionBoxValid = false; // update the selection box when it is next requested
 		}
 	}
 
 	/**
 	 * Returns the (object-space) bounds that should be used to construct a selection box for this model and its children.
 	 * May return an empty bound to indicate that this model and its children should not be selectable themselves, or should
 	 * not be included in its parent model's selection box. This method is used for constructing the default selection boxes,
 	 * as opposed to any boxes of custom shape specified by @ref m_CustomSelectionShape.
 	 *
 	 * If you wish your model type to be included in selection boxes, override this method and have it return the object-space
 	 * bounds of itself, augmented recursively (via this method) with the object-space selection bounds of its children.
 	 */
 	virtual const CBoundingBoxAligned GetObjectSelectionBoundsRec() { return CBoundingBoxAligned::EMPTY; }
 
 	/**
 	 * Called when terrain has changed in the given inclusive bounds.
 	 * Might call SetDirty if the change affects this model.
 	 */
 	virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) = 0;
 
 	/**
 	 * Called when the entity tries to set some variable to affect the display of this model
 	 * and/or its child objects.
 	 */
 	virtual void SetEntityVariable(const std::string& UNUSED(name), float UNUSED(value)) { }
 
 	/**
 	 * Ensure that both the transformation and the bone matrices are correct for this model and all its props.
 	 */
 	virtual void ValidatePosition() = 0;
 
 	/**
 	 * Mark this model's position and bone matrices, and all props' positions as invalid.
 	 */
 	virtual void InvalidatePosition() = 0;
 
 	virtual void SetPlayerID(player_id_t id) { m_PlayerID = id; }
 
 	// get the model's player ID; initial default is INVALID_PLAYER
 	virtual player_id_t GetPlayerID() const { return m_PlayerID; }
 
-	virtual void SetShadingColor(const CColor& color) { m_ShadingColor = color; }
-	virtual CColor GetShadingColor() const { return m_ShadingColor; }
+	virtual void SetShadingColor(const RGBAcolor& color) { m_ShadingColor = color; }
+	virtual RGBAcolor GetShadingColor() const { return m_ShadingColor; }
 
 protected:
 	void CalcSelectionBox();
 
 public:
 	/// If non-null, points to the model that we are attached to.
 	CModelAbstract* m_Parent;
 
 	/// True if both transform and and bone matrices are valid.
 	bool m_PositionValid;
 
 	player_id_t m_PlayerID;
 
 	/// Modulating color
-	CColor m_ShadingColor;
+	RGBAcolor m_ShadingColor;
 
 protected:
 
 	/// Selection box for this model.
 	CBoundingBoxOriented m_SelectionBox;
 
 	/// Is the current selection box valid?
 	bool m_SelectionBoxValid;
 
 	/// Pointer to a descriptor for a custom-defined selection box shape. If no custom selection box is required, this is NULL
 	/// and the standard recursive-bounding-box-based selection box is used. Otherwise, a custom selection box described by this
 	/// field will be used.
 	/// @see SetCustomSelectionShape
 	CustomSelectionShape* m_CustomSelectionShape;
 
 };
 
 #endif // INCLUDED_MODELABSTRACT
Index: source/graphics/ModelDef.cpp
===================================================================
--- source/graphics/ModelDef.cpp	(revision 24796)
+++ source/graphics/ModelDef.cpp	(working copy)
@@ -1,495 +1,495 @@
 /* Copyright (C) 2021 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 .
  */
 
 /*
  * Defines a raw 3d model.
  */
 
 #include "precompiled.h"
 
 #include "ModelDef.h"
 #include "graphics/SkeletonAnimDef.h"
 #include "lib/sse.h"
 #include "ps/FileIo.h"
 #include "maths/Vector4D.h"
 
 #if COMPILER_HAS_SSE
 # include 
 #endif
 
 CVector3D CModelDef::SkinPoint(const SModelVertex& vtx,
 							   const CMatrix3D newPoseMatrices[])
 {
 	CVector3D result (0, 0, 0);
 
 	for (int i = 0; i < SVertexBlend::SIZE && vtx.m_Blend.m_Bone[i] != 0xff; ++i)
 	{
 		result += newPoseMatrices[vtx.m_Blend.m_Bone[i]].Transform(vtx.m_Coords) * vtx.m_Blend.m_Weight[i];
 	}
 
 	return result;
 }
 
 CVector3D CModelDef::SkinNormal(const SModelVertex& vtx,
 								const CMatrix3D newPoseMatrices[])
 {
 	// To be correct, the normal vectors apparently need to be multiplied by the
 	// inverse of the transpose. Unfortunately inverses are slow.
 	// If a matrix is orthogonal, M * M^T = I and so the inverse of the transpose
 	// is the original matrix. But that's not entirely relevant here, because
 	// the bone matrices include translation components and so they're not
 	// orthogonal.
 	// But that's okay because we have
 	//   M = T * R
 	// and want to find
 	//   n' = (M^T^-1) * n
 	//      = (T * R)^T^-1 * n
 	//      = (R^T * T^T)^-1 * n
 	//      = (T^T^-1 * R^T^-1) * n
 	// R is indeed orthogonal so R^T^-1 = R. T isn't orthogonal at all.
 	// But n is only a 3-vector, and from the forms of T and R (which have
 	// lots of zeroes) I can convince myself that replacing T with T^T^-1 has no
 	// effect on anything but the fourth component of M^T^-1 - and the fourth
 	// component is discarded since it has no effect on n', and so we can happily
 	// use n' = M*n.
 	//
 	// (This isn't very good as a proof, but it's better than assuming M is
 	// orthogonal when it's clearly not.)
 
 	CVector3D result (0, 0, 0);
 
 	for (int i = 0; i < SVertexBlend::SIZE && vtx.m_Blend.m_Bone[i] != 0xff; ++i)
 	{
 		result += newPoseMatrices[vtx.m_Blend.m_Bone[i]].Rotate(vtx.m_Norm) * vtx.m_Blend.m_Weight[i];
 	}
 
 	// If there was more than one influence, the result is probably not going
 	// to be of unit length (since it's a weighted sum of several independent
 	// unit vectors), so we need to normalise it.
 	// (It's fairly common to only have one influence, so it seems sensible to
 	// optimise that case a bit.)
 	if (vtx.m_Blend.m_Bone[1] != 0xff) // if more than one influence
 		result.Normalize();
 
 	return result;
 }
 
 void(*CModelDef::SkinPointsAndNormals)(
 	size_t numVertices,
 	const VertexArrayIterator& Position,
 	const VertexArrayIterator& Normal,
 	const SModelVertex* vertices,
 	const size_t* blendIndices,
 	const CMatrix3D newPoseMatrices[]) {};
 
 
 static void SkinPointsAndNormalsFallback(
 		size_t numVertices,
 		const VertexArrayIterator& Position,
 		const VertexArrayIterator& Normal,
 		const SModelVertex* vertices,
 		const size_t* blendIndices,
 		const CMatrix3D newPoseMatrices[])
 {
 	// To avoid some performance overhead, get the raw vertex array pointers
 	char* PositionData = Position.GetData();
 	size_t PositionStride = Position.GetStride();
 	char* NormalData = Normal.GetData();
 	size_t NormalStride = Normal.GetStride();
 
 	for (size_t j = 0; j < numVertices; ++j)
 	{
 		const SModelVertex& vtx = vertices[j];
 
 		CVector3D pos = newPoseMatrices[blendIndices[j]].Transform(vtx.m_Coords);
 		CVector3D norm = newPoseMatrices[blendIndices[j]].Rotate(vtx.m_Norm);
 
 		// If there was more than one influence, the result is probably not going
 		// to be of unit length (since it's a weighted sum of several independent
 		// unit vectors), so we need to normalise it.
 		// (It's fairly common to only have one influence, so it seems sensible to
 		// optimise that case a bit.)
 		if (vtx.m_Blend.m_Bone[1] != 0xff) // if more than one influence
 			norm.Normalize();
 
-		memcpy(PositionData + PositionStride*j, &pos.X, 3*sizeof(float));
-		memcpy(NormalData + NormalStride*j, &norm.X, 3*sizeof(float));
+		memcpy(PositionData + PositionStride*j, &pos, 3*sizeof(float)); //DanW58, was &pos.X
+		memcpy(NormalData + NormalStride*j, &norm, 3*sizeof(float)); //DanW58, was &norm.X
 	}
 }
 
 #if COMPILER_HAS_SSE
 static void SkinPointsAndNormalsSSE(
 		size_t numVertices,
 		const VertexArrayIterator& Position,
 		const VertexArrayIterator& Normal,
 		const SModelVertex* vertices,
 		const size_t* blendIndices,
 		const CMatrix3D newPoseMatrices[])
 {
 	// To avoid some performance overhead, get the raw vertex array pointers
 	char* PositionData = Position.GetData();
 	size_t PositionStride = Position.GetStride();
 	char* NormalData = Normal.GetData();
 	size_t NormalStride = Normal.GetStride();
 
 	// Must be aligned correctly for SSE
 	ASSERT((intptr_t)newPoseMatrices % 16 == 0);
 	ASSERT((intptr_t)PositionData % 16 == 0);
 	ASSERT((intptr_t)PositionStride % 16 == 0);
 	ASSERT((intptr_t)NormalData % 16 == 0);
 	ASSERT((intptr_t)NormalStride % 16 == 0);
 
 	__m128 col0, col1, col2, col3, vec0, vec1, vec2;
 
 	for (size_t j = 0; j < numVertices; ++j)
 	{
 		const SModelVertex& vtx = vertices[j];
 		const CMatrix3D& mtx = newPoseMatrices[blendIndices[j]];
 
 		// Loads matrix to xmm registers.
 		col0 = _mm_load_ps(mtx._data);
 		col1 = _mm_load_ps(mtx._data + 4);
 		col2 = _mm_load_ps(mtx._data + 8);
 		col3 = _mm_load_ps(mtx._data + 12);
 
 		// Loads and computes vertex coordinates.
-		vec0 = _mm_load1_ps(&vtx.m_Coords.X);	// v0 = [x, x, x, x]
+		vec0 = _mm_load1_ps(vtx.m_Coords.pX());	// v0 = [x, x, x, x] //DanW58, was &&vtx.m_Coords.X
 		vec0 = _mm_mul_ps(col0, vec0);			// v0 = [_11*x, _21*x, _31*x, _41*x]
-		vec1 = _mm_load1_ps(&vtx.m_Coords.Y);	// v1 = [y, y, y, y]
+		vec1 = _mm_load1_ps(vtx.m_Coords.pY());	// v1 = [y, y, y, y] //DanW58, was &&vtx.m_Coords.X
 		vec1 = _mm_mul_ps(col1, vec1);			// v1 = [_12*y, _22*y, _32*y, _42*y]
 		vec0 = _mm_add_ps(vec0, vec1);			// v0 = [_11*x + _12*y, ...]
-		vec1 = _mm_load1_ps(&vtx.m_Coords.Z);	// v1 = [z, z, z, z]
+		vec1 = _mm_load1_ps(vtx.m_Coords.pZ());	// v1 = [z, z, z, z] //DanW58, was &&vtx.m_Coords.X
 		vec1 = _mm_mul_ps(col2, vec1);			// v1 = [_13*z, _23*z, _33*z, _43*z]
 		vec1 = _mm_add_ps(vec1, col3);			// v1 = [_13*z + _14, ...]
 		vec0 = _mm_add_ps(vec0, vec1);			// v0 = [_11*x + _12*y + _13*z + _14, ...]
 		_mm_store_ps((float*)(PositionData + PositionStride*j), vec0);
 
 		// Loads and computes normal vectors.
-		vec0 = _mm_load1_ps(&vtx.m_Norm.X);		// v0 = [x, x, x, x]
+		vec0 = _mm_load1_ps(vtx.m_Norm.pX());	// v0 = [x, x, x, x]
 		vec0 = _mm_mul_ps(col0, vec0);			// v0 = [_11*x, _21*x, _31*x, _41*x]
-		vec1 = _mm_load1_ps(&vtx.m_Norm.Y);		// v1 = [y, y, y, y]
+		vec1 = _mm_load1_ps(vtx.m_Norm.pY());	// v1 = [y, y, y, y]
 		vec1 = _mm_mul_ps(col1, vec1);			// v1 = [_12*y, _22*y, _32*y, _42*y]
 		vec0 = _mm_add_ps(vec0, vec1);			// v0 = [_11*x + _12*y, ...]
-		vec1 = _mm_load1_ps(&vtx.m_Norm.Z);		// v1 = [z, z, z, z]
+		vec1 = _mm_load1_ps(vtx.m_Norm.pZ());	// v1 = [z, z, z, z]
 		vec1 = _mm_mul_ps(col2, vec1);			// v1 = [_13*z, _23*z, _33*z, _43*z]
 		vec0 = _mm_add_ps(vec0, vec1);			// v0 = [_11*x + _12*y + _13*z, ...]
 
 		// If there was more than one influence, the result is probably not going
 		// to be of unit length (since it's a weighted sum of several independent
 		// unit vectors), so we need to normalise it.
 		// (It's fairly common to only have one influence, so it seems sensible to
 		// optimise that case a bit.)
 		if (vtx.m_Blend.m_Bone[1] != 0xff) // if more than one influence
 		{
 			// Normalization.
 			// vec1 = [x*x, y*y, z*z, ?*?]
 			vec1 = _mm_mul_ps(vec0, vec0);
 			// vec2 = [y*y, z*z, x*x, ?*?]
 			vec2 = _mm_shuffle_ps(vec1, vec1, _MM_SHUFFLE(3, 0, 2, 1));
 			vec1 = _mm_add_ps(vec1, vec2);
 			// vec2 = [z*z, x*x, y*y, ?*?]
 			vec2 = _mm_shuffle_ps(vec2, vec2, _MM_SHUFFLE(3, 0, 2, 1));
 			vec1 = _mm_add_ps(vec1, vec2);
 			// rsqrt(a) = 1 / sqrt(a)
 			vec1 = _mm_rsqrt_ps(vec1);
 			vec0 = _mm_mul_ps(vec0, vec1);
 		}
 		_mm_store_ps((float*)(NormalData + NormalStride*j), vec0);
 	}
 }
 #endif
 
 void CModelDef::BlendBoneMatrices(
 		CMatrix3D boneMatrices[])
 {
 	for (size_t i = 0; i < m_NumBlends; ++i)
 	{
 		const SVertexBlend& blend = m_pBlends[i];
 		CMatrix3D& boneMatrix = boneMatrices[m_NumBones + 1 + i];
 
 		// Note: there is a special case of joint influence, in which the vertex
 		//	is influenced by the bind-shape matrix instead of a particular bone,
 		//	which we indicate by setting the bone ID to the total number of bones.
 		//	It should be blended with the world space transform and we have already
 		//	set up this matrix in boneMatrices.
 		//	(see http://trac.wildfiregames.com/ticket/1012)
 
 		boneMatrix.Blend(boneMatrices[blend.m_Bone[0]], blend.m_Weight[0]);
 		for (size_t j = 1; j < SVertexBlend::SIZE && blend.m_Bone[j] != 0xFF; ++j)
 			boneMatrix.AddBlend(boneMatrices[blend.m_Bone[j]], blend.m_Weight[j]);
 	}
 }
 
 // CModelDef Constructor
 CModelDef::CModelDef() :
 	m_NumVertices(0), m_NumUVsPerVertex(0), m_pVertices(0), m_NumFaces(0), m_pFaces(0),
 	m_NumBones(0), m_Bones(0), m_InverseBindBoneMatrices(NULL),
 	m_NumBlends(0), m_pBlends(0), m_pBlendIndices(0),
 	m_Name(L"[not loaded]")
 {
 }
 
 // CModelDef Destructor
 CModelDef::~CModelDef()
 {
 	for(RenderDataMap::iterator it = m_RenderData.begin(); it != m_RenderData.end(); ++it)
 		delete it->second;
 	delete[] m_pVertices;
 	delete[] m_pFaces;
 	delete[] m_Bones;
 	delete[] m_InverseBindBoneMatrices;
 	delete[] m_pBlends;
 	delete[] m_pBlendIndices;
 }
 
 // FindPropPoint: find and return pointer to prop point matching given name;
 // return null if no match (case insensitive search)
 const SPropPoint* CModelDef::FindPropPoint(const char* name) const
 {
 	for (size_t i = 0; i < m_PropPoints.size(); ++i)
 		if (m_PropPoints[i].m_Name == name)
 			return &m_PropPoints[i];
 
 	return 0;
 }
 
 // Load: read and return a new CModelDef initialised with data from given file
 CModelDef* CModelDef::Load(const VfsPath& filename, const VfsPath& name)
 {
 	CFileUnpacker unpacker;
 
 	// read everything in from file
 	unpacker.Read(filename,"PSMD");
 
 	// check version
 	if (unpacker.GetVersion() mdef (new CModelDef());
 	mdef->m_Name = name;
 
 	// now unpack everything
 	mdef->m_NumVertices = unpacker.UnpackSize();
 
 	// versions prior to 4 only support 1 UV set, 4 and later store it here
 	if (unpacker.GetVersion() <= 3)
 	{
 		mdef->m_NumUVsPerVertex = 1;
 	}
 	else
 	{
 		mdef->m_NumUVsPerVertex = unpacker.UnpackSize();
 	}
 
 	mdef->m_pVertices=new SModelVertex[mdef->m_NumVertices];
 
 	for (size_t i = 0; i < mdef->m_NumVertices; ++i)
 	{
 		unpacker.UnpackRaw(&mdef->m_pVertices[i].m_Coords, 12);
 		unpacker.UnpackRaw(&mdef->m_pVertices[i].m_Norm, 12);
 
 		for (size_t s = 0; s < mdef->m_NumUVsPerVertex; ++s)
 		{
 			float uv[2];
 			unpacker.UnpackRaw(&uv[0], 8);
 			mdef->m_pVertices[i].m_UVs.push_back(uv[0]);
 			mdef->m_pVertices[i].m_UVs.push_back(uv[1]);
 		}
 
 		unpacker.UnpackRaw(&mdef->m_pVertices[i].m_Blend, sizeof(SVertexBlend));
 	}
 
 	mdef->m_NumFaces = unpacker.UnpackSize();
 	mdef->m_pFaces=new SModelFace[mdef->m_NumFaces];
 	unpacker.UnpackRaw(mdef->m_pFaces,sizeof(SModelFace)*mdef->m_NumFaces);
 
 	mdef->m_NumBones = unpacker.UnpackSize();
 	if (mdef->m_NumBones)
 	{
 		mdef->m_Bones=new CBoneState[mdef->m_NumBones];
 		unpacker.UnpackRaw(mdef->m_Bones,mdef->m_NumBones*sizeof(CBoneState));
 
 		mdef->m_pBlendIndices = new size_t[mdef->m_NumVertices];
 		std::vector blends;
 		for (size_t i = 0; i < mdef->m_NumVertices; i++)
 		{
 			const SVertexBlend &blend = mdef->m_pVertices[i].m_Blend;
 			if (blend.m_Bone[1] == 0xFF)
 			{
 				mdef->m_pBlendIndices[i] = blend.m_Bone[0];
 			}
 			else
 			{
 				// If there's already a vertex using the same blend as this, then
 				// reuse its entry from blends; otherwise add the new one to blends
 				size_t j;
 				for (j = 0; j < blends.size(); j++)
 				{
 					if (blend == blends[j]) break;
 				}
 				if (j >= blends.size())
 					blends.push_back(blend);
 				// This index is offset by one to allow the special case of a
 				//	weighted influence relative to the bind-shape rather than
 				//	a particular bone. See comment in BlendBoneMatrices.
 				mdef->m_pBlendIndices[i] = mdef->m_NumBones + 1 + j;
 			}
 		}
 
 		mdef->m_NumBlends = blends.size();
 		mdef->m_pBlends = new SVertexBlend[mdef->m_NumBlends];
 		std::copy(blends.begin(), blends.end(), mdef->m_pBlends);
 	}
 
 	if (unpacker.GetVersion() >= 2)
 	{
 		// versions >=2 also have prop point data
 		size_t numPropPoints = unpacker.UnpackSize();
 		mdef->m_PropPoints.resize(numPropPoints);
 		if (numPropPoints)
 		{
 			for (size_t i = 0; i < numPropPoints; i++)
 			{
 				unpacker.UnpackString(mdef->m_PropPoints[i].m_Name);
-				unpacker.UnpackRaw(&mdef->m_PropPoints[i].m_Position.X, sizeof(mdef->m_PropPoints[i].m_Position));
-				unpacker.UnpackRaw(&mdef->m_PropPoints[i].m_Rotation.m_V.X, sizeof(mdef->m_PropPoints[i].m_Rotation));
+				unpacker.UnpackRaw(mdef->m_PropPoints[i].m_Position.ncpX(), sizeof(mdef->m_PropPoints[i].m_Position));
+				unpacker.UnpackRaw(mdef->m_PropPoints[i].m_Rotation.m_V.ncpX(), sizeof(mdef->m_PropPoints[i].m_Rotation));
 				unpacker.UnpackRaw(&mdef->m_PropPoints[i].m_BoneIndex, sizeof(mdef->m_PropPoints[i].m_BoneIndex));
 
 				// build prop point transform
 				mdef->m_PropPoints[i].m_Transform.SetIdentity();
 				mdef->m_PropPoints[i].m_Transform.Rotate(mdef->m_PropPoints[i].m_Rotation);
 				mdef->m_PropPoints[i].m_Transform.Translate(mdef->m_PropPoints[i].m_Position);
 			}
 		}
 	}
 
 	if (unpacker.GetVersion() <= 2)
 	{
 		// Versions <=2 don't include the default 'root' prop point, so add it here
 
 		SPropPoint prop;
 		prop.m_Name = "root";
 		prop.m_Transform.SetIdentity();
 		prop.m_BoneIndex = 0xFF;
 
 		mdef->m_PropPoints.push_back(prop);
 	}
 
 	if (unpacker.GetVersion() <= 2)
 	{
 		// Versions <=2 store the vertexes relative to the bind pose. That
 		// isn't useful when you want to do correct skinning, so later versions
 		// store them in world space. So, fix the old models by skinning each
 		// vertex:
 
 		if (mdef->m_NumBones) // only do skinned models
 		{
 			std::vector bindPose (mdef->m_NumBones);
 
 			for (size_t i = 0; i < mdef->m_NumBones; ++i)
 			{
 				bindPose[i].SetIdentity();
 				bindPose[i].Rotate(mdef->m_Bones[i].m_Rotation);
 				bindPose[i].Translate(mdef->m_Bones[i].m_Translation);
 			}
 
 			for (size_t i = 0; i < mdef->m_NumVertices; ++i)
 			{
 				mdef->m_pVertices[i].m_Coords = SkinPoint(mdef->m_pVertices[i], &bindPose[0]);
 				mdef->m_pVertices[i].m_Norm = SkinNormal(mdef->m_pVertices[i], &bindPose[0]);
 			}
 		}
 	}
 
 	// Compute the inverse bind-pose matrices, needed by the skinning code
 	mdef->m_InverseBindBoneMatrices = new CMatrix3D[mdef->m_NumBones];
 	CBoneState* defpose = mdef->GetBones();
 	for (size_t i = 0; i < mdef->m_NumBones; ++i)
 	{
 		mdef->m_InverseBindBoneMatrices[i].SetIdentity();
 		mdef->m_InverseBindBoneMatrices[i].Translate(-defpose[i].m_Translation);
 		mdef->m_InverseBindBoneMatrices[i].Rotate(defpose[i].m_Rotation.GetInverse());
 	}
 
 	return mdef.release();
 }
 
 // Save: write the given CModelDef to the given file
 void CModelDef::Save(const VfsPath& filename, const CModelDef* mdef)
 {
 	CFilePacker packer(FILE_VERSION, "PSMD");
 
 	// pack everything up
 	const size_t numVertices = mdef->GetNumVertices();
 	packer.PackSize(numVertices);
 	packer.PackRaw(mdef->GetVertices(), sizeof(SModelVertex) * numVertices);
 
 	const size_t numFaces = mdef->GetNumFaces();
 	packer.PackSize(numFaces);
 	packer.PackRaw(mdef->GetFaces(), sizeof(SModelFace) * numFaces);
 
 	const size_t numBones = mdef->m_NumBones;
 	packer.PackSize(numBones);
 	if (numBones)
 		packer.PackRaw(mdef->m_Bones, sizeof(CBoneState) * numBones);
 
 	const size_t numPropPoints = mdef->m_PropPoints.size();
 	packer.PackSize(numPropPoints);
 	for (size_t i = 0; i < numPropPoints; i++)
 	{
 		packer.PackString(mdef->m_PropPoints[i].m_Name);
-		packer.PackRaw(&mdef->m_PropPoints[i].m_Position.X, sizeof(mdef->m_PropPoints[i].m_Position));
-		packer.PackRaw(&mdef->m_PropPoints[i].m_Rotation.m_V.X, sizeof(mdef->m_PropPoints[i].m_Rotation));
+		packer.PackRaw(mdef->m_PropPoints[i].m_Position.pX(), sizeof(mdef->m_PropPoints[i].m_Position));
+		packer.PackRaw(mdef->m_PropPoints[i].m_Rotation.m_V.pX(), sizeof(mdef->m_PropPoints[i].m_Rotation));
 		packer.PackRaw(&mdef->m_PropPoints[i].m_BoneIndex, sizeof(mdef->m_PropPoints[i].m_BoneIndex));
 	}
 
 	// flush everything out to file
 	packer.Write(filename);
 }
 
 // SetRenderData: Set the render data object for the given key,
 void CModelDef::SetRenderData(const void* key, CModelDefRPrivate* data)
 {
 	delete m_RenderData[key];
 	m_RenderData[key] = data;
 }
 
 // GetRenderData: Get the render data object for the given key,
 // or 0 if no such object exists.
 // Reference count of the render data object is automatically increased.
 CModelDefRPrivate* CModelDef::GetRenderData(const void* key) const
 {
 	RenderDataMap::const_iterator it = m_RenderData.find(key);
 
 	if (it != m_RenderData.end())
 		return it->second;
 
 	return 0;
 }
 
 void ModelDefActivateFastImpl()
 {
 #if COMPILER_HAS_SSE
 	if (HostHasSSE())
 	{
 		CModelDef::SkinPointsAndNormals = SkinPointsAndNormalsSSE;
 		return;
 	}
 #endif
 	CModelDef::SkinPointsAndNormals = SkinPointsAndNormalsFallback;
 }
Index: source/graphics/ObjectEntry.cpp
===================================================================
--- source/graphics/ObjectEntry.cpp	(revision 24796)
+++ source/graphics/ObjectEntry.cpp	(working copy)
@@ -1,307 +1,309 @@
 /* Copyright (C) 2021 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 .
  */
 
 #include "precompiled.h"
 
 #include "ObjectEntry.h"
 
 #include "graphics/Decal.h"
 #include "graphics/Material.h"
 #include "graphics/MaterialManager.h"
 #include "graphics/MeshManager.h"
 #include "graphics/Model.h"
 #include "graphics/ModelDef.h"
 #include "graphics/ObjectBase.h"
 #include "graphics/ObjectManager.h"
 #include "graphics/ParticleManager.h"
 #include "graphics/SkeletonAnim.h"
 #include "graphics/TextureManager.h"
 #include "lib/rand.h"
 #include "ps/CLogger.h"
+#include "ps/CStr.h"
 #include "ps/Game.h"
 #include "ps/World.h"
 #include "renderer/Renderer.h"
 #include "simulation2/Simulation2.h"
 
 #include 
 
 CObjectEntry::CObjectEntry(CObjectBase* base, CSimulation2& simulation) :
 	m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Model(NULL), m_Outdated(false), m_Simulation(simulation)
 {
 }
 
 CObjectEntry::~CObjectEntry()
 {
 	for (const std::pair& anim : m_Animations)
 		delete anim.second;
 
 	delete m_Model;
 }
 
 
 bool CObjectEntry::BuildVariation(const std::vector >& selections,
 								  const std::vector& variationKey,
 								  CObjectManager& objectManager)
 {
 	CObjectBase::Variation variation = m_Base->BuildVariation(variationKey);
 
 	// Copy the chosen data onto this model:
 
 	for (std::multimap::iterator it = variation.samplers.begin(); it != variation.samplers.end(); ++it)
 		m_Samplers.push_back(it->second);
 
 	m_ModelName = variation.model;
 
 	if (! variation.color.empty())
 	{
 		std::stringstream str;
 		str << variation.color;
 		int r, g, b;
 		if (! (str >> r >> g >> b)) // Any trailing data is ignored
 			LOGERROR("Actor '%s' has invalid RGB color '%s'", utf8_from_wstring(m_Base->m_ShortName), variation.color);
 		else
-			m_Color = CColor(r/255.0f, g/255.0f, b/255.0f, 1.0f);
+			m_Color = RGBAcolor(r/255.0f, g/255.0f, b/255.0f, 1.0f);
 	}
 
 	if (variation.decal.m_SizeX && variation.decal.m_SizeZ)
 	{
 		CMaterial material = g_Renderer.GetMaterialManager().LoadMaterial(m_Base->m_Material);
 
 		for (const CObjectBase::Samp& samp : m_Samplers)
 		{
 			CTextureProperties textureProps(samp.m_SamplerFile);
 			textureProps.SetWrap(GL_CLAMP_TO_BORDER);
 			CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
 			// TODO: Should check which renderpath is selected and only preload the necessary textures.
 			texture->Prefetch();
 			material.AddSampler(CMaterial::TextureSampler(samp.m_SamplerName, texture));
 		}
 
 		SDecal decal(material,
 			variation.decal.m_SizeX, variation.decal.m_SizeZ,
 			variation.decal.m_Angle, variation.decal.m_OffsetX, variation.decal.m_OffsetZ,
 			m_Base->m_Properties.m_FloatOnWater);
 		m_Model = new CModelDecal(objectManager.GetTerrain(), decal);
 
 		return true;
 	}
 
 	if (!variation.particles.empty())
 	{
 		m_Model = new CModelParticleEmitter(g_Renderer.GetParticleManager().LoadEmitterType(variation.particles));
 		return true;
 	}
 
 	std::vector props;
 
 	for (std::multimap::iterator it = variation.props.begin(); it != variation.props.end(); ++it)
 		props.push_back(it->second);
 
 	// Build the model:
 
 	// try and create a model
 	CModelDefPtr modeldef (objectManager.GetMeshManager().GetMesh(m_ModelName));
 	if (!modeldef)
 	{
 		LOGERROR("CObjectEntry::BuildVariation(): Model %s failed to load", m_ModelName.string8());
 		return false;
 	}
 
 	// delete old model, create new
 	CModel* model = new CModel(objectManager.GetSkeletonAnimManager(), m_Simulation);
 	delete m_Model;
 	m_Model = model;
 	model->SetMaterial(g_Renderer.GetMaterialManager().LoadMaterial(m_Base->m_Material));
-	model->GetMaterial().AddStaticUniform("objectColor", CVector4D(m_Color.r, m_Color.g, m_Color.b, m_Color.a));
+//	model->GetMaterial().AddStaticUniform("objectColor", CVector4D(m_Color.getR(), m_Color.getG(), m_Color.getB(), m_Color.getA()));
+	model->GetMaterial().AddStaticUniform("objectColor", m_Color);
 	model->InitModel(modeldef);
 
 	if (m_Samplers.empty())
 		LOGERROR("Actor '%s' has no textures.", utf8_from_wstring(m_Base->m_ShortName));
 
 	for (const CObjectBase::Samp& samp : m_Samplers)
 	{
 		CTextureProperties textureProps(samp.m_SamplerFile);
 		textureProps.SetWrap(GL_CLAMP_TO_EDGE);
 		CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps);
 		// if we've loaded this model we're probably going to render it soon, so prefetch its texture.
 		// All textures are prefetched even in the fixed pipeline, including the normal maps etc.
 		// TODO: Should check which renderpath is selected and only preload the necessary textures.
 		texture->Prefetch();
 		model->GetMaterial().AddSampler(CMaterial::TextureSampler(samp.m_SamplerName, texture));
 	}
 
 	for (const CStrIntern& requSampName : model->GetMaterial().GetRequiredSampler())
 	{
 		if (std::find_if(m_Samplers.begin(), m_Samplers.end(),
 		                 [&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end())
 			LOGERROR("Actor %s: required texture sampler %s not found (material %s)", utf8_from_wstring(m_Base->m_ShortName), requSampName.string().c_str(), m_Base->m_Material.string8().c_str());
 	}
 
 	// calculate initial object space bounds, based on vertex positions
 	model->CalcStaticObjectBounds();
 
 	// load the animations
 	for (std::multimap::iterator it = variation.anims.begin(); it != variation.anims.end(); ++it)
 	{
 		CStr name = it->first.LowerCase();
 
 		if (it->second.m_FileName.empty())
 			continue;
 		CSkeletonAnim* anim = model->BuildAnimation(
 			it->second.m_FileName,
 			name,
 			it->second.m_ID,
 			it->second.m_Frequency,
 			it->second.m_Speed,
 			it->second.m_ActionPos,
 			it->second.m_ActionPos2,
 			it->second.m_SoundPos);
 		if (anim)
 			m_Animations.insert(std::make_pair(name, anim));
 	}
 
 	// ensure there's always an idle animation
 	if (m_Animations.find("idle") == m_Animations.end())
 	{
 		CSkeletonAnim* anim = new CSkeletonAnim();
 		anim->m_Name = "idle";
 		anim->m_ID = "";
 		anim->m_AnimDef = NULL;
 		anim->m_Frequency = 0;
 		anim->m_Speed = 0.f;
 		anim->m_ActionPos = 0.f;
 		anim->m_ActionPos2 = 0.f;
 		anim->m_SoundPos = 0.f;
 		m_Animations.insert(std::make_pair("idle", anim));
 
 		// Ignore errors, since they're probably saying this is a non-animated model
 		model->SetAnimation(anim);
 	}
 	else
 	{
 		// start up idling
 		if (!model->SetAnimation(GetRandomAnimation("idle")))
 			LOGERROR("Failed to set idle animation in model \"%s\"", m_ModelName.string8());
 	}
 
 	// build props - TODO, RC - need to fix up bounds here
 	// TODO: Make sure random variations get handled correctly when a prop fails
 	for (const CObjectBase::Prop& prop : props)
 	{
 		// Pluck out the special attachpoint 'projectile'
 		if (prop.m_PropPointName == "projectile")
 		{
 			m_ProjectileModelName = prop.m_ModelName;
 			continue;
 		}
 
 		CObjectEntry* oe = objectManager.FindObjectVariation(prop.m_ModelName.c_str(), selections);
 		if (!oe)
 		{
 			LOGERROR("Failed to build prop model \"%s\" on actor \"%s\"", utf8_from_wstring(prop.m_ModelName), utf8_from_wstring(m_Base->m_ShortName));
 			continue;
 		}
 
 		// If we don't have a projectile but this prop does (e.g. it's our rider), then
 		// use that as our projectile too
 		if (m_ProjectileModelName.empty() && !oe->m_ProjectileModelName.empty())
 			m_ProjectileModelName = oe->m_ProjectileModelName;
 
 		CStr ppn = prop.m_PropPointName;
 		bool isAmmo = false;
 
 		// Handle the special attachpoint 'loaded-'
 		if (ppn.Find("loaded-") == 0)
 		{
 			ppn = prop.m_PropPointName.substr(7);
 			isAmmo = true;
 		}
 
 		const SPropPoint* proppoint = modeldef->FindPropPoint(ppn.c_str());
 		if (proppoint)
 		{
 			CModelAbstract* propmodel = oe->m_Model->Clone();
 			if (isAmmo)
 				model->AddAmmoProp(proppoint, propmodel, oe);
 			else
 				model->AddProp(proppoint, propmodel, oe, prop.m_minHeight, prop.m_maxHeight, prop.m_selectable);
 			if (propmodel->ToCModel())
 				propmodel->ToCModel()->SetAnimation(oe->GetRandomAnimation("idle"));
 		}
 		else
 			LOGERROR("Failed to find matching prop point called \"%s\" in model \"%s\" for actor \"%s\"", ppn, m_ModelName.string8(), utf8_from_wstring(m_Base->m_ShortName));
 	}
 
 	// Setup flags.
 	if (m_Base->m_Properties.m_CastShadows)
 	{
 		model->SetFlags(model->GetFlags() | MODELFLAG_CASTSHADOWS);
 	}
 
 	if (m_Base->m_Properties.m_FloatOnWater)
 	{
 		model->SetFlags(model->GetFlags() | MODELFLAG_FLOATONWATER);
 	}
 
 	return true;
 }
 
 CSkeletonAnim* CObjectEntry::GetRandomAnimation(const CStr& animationName, const CStr& ID) const
 {
 	std::vector anims = GetAnimations(animationName, ID);
 
 	int totalFreq = 0;
 	for (CSkeletonAnim* anim : anims)
 		totalFreq += anim->m_Frequency;
 
 	if (totalFreq == 0)
 		return anims[rand(0, anims.size())];
 
 	int r = rand(0, totalFreq);
 	for (CSkeletonAnim* anim : anims)
 	{
 		r -= anim->m_Frequency;
 		if (r < 0)
 			return anim;
 	}
 	return NULL;
 }
 
 std::vector CObjectEntry::GetAnimations(const CStr& animationName, const CStr& ID) const
 {
 	std::vector anims;
 
 	SkeletonAnimMap::const_iterator lower = m_Animations.lower_bound(animationName);
 	SkeletonAnimMap::const_iterator upper = m_Animations.upper_bound(animationName);
 
 	for (SkeletonAnimMap::const_iterator it = lower; it != upper; ++it)
 	{
 		if (ID.empty() || it->second->m_ID == ID)
 			anims.push_back(it->second);
 	}
 
 	if (anims.empty())
 	{
 		lower = m_Animations.lower_bound("idle");
 		upper = m_Animations.upper_bound("idle");
 		for (SkeletonAnimMap::const_iterator it = lower; it != upper; ++it)
 			anims.push_back(it->second);
 	}
 
 	ENSURE(!anims.empty());
 	return anims;
 }
Index: source/graphics/ObjectEntry.h
===================================================================
--- source/graphics/ObjectEntry.h	(revision 24796)
+++ source/graphics/ObjectEntry.h	(working copy)
@@ -1,97 +1,97 @@
 /* Copyright (C) 2021 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 .
  */
 
 #ifndef INCLUDED_OBJECTENTRY
 #define INCLUDED_OBJECTENTRY
 
 class CModelAbstract;
 class CSkeletonAnim;
 class CObjectBase;
 class CObjectManager;
 class CSimulation2;
 struct SPropPoint;
 
 #include