// Generated by Construct 3, the game and app creator :: https://www.construct.net
"use strict";
// common_prelude.js
// ECMAScript 5 strict mode

// Create the cr namespace for all runtime names
window.cr = {};
cr.plugins_ = {};
cr.behaviors = {};

// Add Object.getPrototypeOf if missing (not in Opera)
if (typeof Object.getPrototypeOf !== "function")
{
	if (typeof "test".__proto__ === "object")
	{
		Object.getPrototypeOf = function(object) {
			return object.__proto__;
		};
	}
	else
	{
		Object.getPrototypeOf = function(object) {
			// May break if the constructor has been tampered with
			return object.constructor.prototype;
		};
	}
}

(function(){

	// Log something even after export, vs. the log() function which is stripped out when exporting
	cr.logexport = function (msg)
	{
		if (window.console && window.console.log)
			window.console.log(msg);
	};
	
	cr.logerror = function (msg)
	{
		if (window.console && window.console.error)
			window.console.error(msg);
	};
	
	// ECMAScript 5 helpers
	cr.seal = function(x)
	{
		// This just causes too big a perf hit on iOS
		// /**PREVIEWONLY**/ if (Object.seal) return Object.seal(x);
		
		return x;
	};
	
	cr.freeze = function(x)
	{
		// /**PREVIEWONLY**/ if (Object.freeze) return Object.freeze(x);
		
		return x;
	};
	
	cr.is_undefined = function (x)
	{
		return typeof x === "undefined";
	};
	
	cr.is_number = function (x)
	{
		return typeof x === "number";
	};
	
	cr.is_string = function (x)
	{
		return typeof x === "string";
	};
	
	cr.isPOT = function (x)
	{
		return x > 0 && ((x - 1) & x) === 0;
	};
	
	cr.nextHighestPowerOfTwo = function(x) {
		--x;
		for (var i = 1; i < 32; i <<= 1) {
			x = x | x >> i;
		}
		return x + 1;
	}
	
	cr.abs = function (x)
	{
		// Faster than Math.abs
		return (x < 0 ? -x : x);
	};
	
	cr.max = function (a, b)
	{
		// Faster than Math.max
		return (a > b ? a : b);
	};
	
	cr.min = function (a, b)
	{
		// Faster than Math.min
		return (a < b ? a : b);
	};
	
	cr.PI = Math.PI;
	
	cr.round = function (x)
	{
		// Faster than Math.round
		return (x + 0.5) | 0;
	};
	
	cr.floor = function (x)
	{
		if (x >= 0)
			return x | 0;
		else
			return (x | 0) - 1;		// correctly round down when negative
	};
	
	cr.ceil = function (x)
	{
		var f = x | 0;
		return (f === x ? f : f + 1);
	};
	
	// Vector types
	function Vector2(x, y)
	{
		this.x = x;
		this.y = y;
		cr.seal(this);
	};

	Vector2.prototype.offset = function (px, py)
	{
		this.x += px;
		this.y += py;
		return this;
	};
	
	Vector2.prototype.mul = function (px, py)
	{
		this.x *= px;
		this.y *= py;
		return this;
	};
	
	cr.vector2 = Vector2;

	// Segment intersection
	cr.segments_intersect = function(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)
	{
		var max_ax, min_ax, max_ay, min_ay, max_bx, min_bx, max_by, min_by;
		
		// Long-hand code since this is a performance hotspot and this type of
		// code minimises the number of conditional tests necessary.
		if (a1x < a2x)
		{
			min_ax = a1x;
			max_ax = a2x;
		}
		else
		{
			min_ax = a2x;
			max_ax = a1x;
		}
		
		if (b1x < b2x)
		{
			min_bx = b1x;
			max_bx = b2x;
		}
		else
		{
			min_bx = b2x;
			max_bx = b1x;
		}
		
		if (max_ax < min_bx || min_ax > max_bx)
			return false;
		
		if (a1y < a2y)
		{
			min_ay = a1y;
			max_ay = a2y;
		}
		else
		{
			min_ay = a2y;
			max_ay = a1y;
		}
		
		if (b1y < b2y)
		{
			min_by = b1y;
			max_by = b2y;
		}
		else
		{
			min_by = b2y;
			max_by = b1y;
		}
		
		if (max_ay < min_by || min_ay > max_by)
			return false;
			
		var dpx = b1x - a1x + b2x - a2x;
		var dpy = b1y - a1y + b2y - a2y;
		var qax = a2x - a1x;
		var qay = a2y - a1y;
		var qbx = b2x - b1x;
		var qby = b2y - b1y;

		var d = cr.abs(qay * qbx - qby * qax);
		var la = qbx * dpy - qby * dpx;
		
		if (cr.abs(la) > d)
			return false;
		
		var lb = qax * dpy - qay * dpx;
		
		return cr.abs(lb) <= d;
	};

	function Rect(left, top, right, bottom)
	{
		this.set(left, top, right, bottom);
		cr.seal(this);
	};
	
	Rect.prototype.set = function (left, top, right, bottom)
	{
		this.left = left;
		this.top = top;
		this.right = right;
		this.bottom = bottom;
	};
	
	Rect.prototype.copy = function (r)
	{
		this.left = r.left;
		this.top = r.top;
		this.right = r.right;
		this.bottom = r.bottom;
	};

	Rect.prototype.width = function ()
	{
		return this.right - this.left;
	};

	Rect.prototype.height = function ()
	{
		return this.bottom - this.top;
	};
	
	Rect.prototype.offset = function (px, py)
	{
		this.left += px;
		this.top += py;
		this.right += px;
		this.bottom += py;
		return this;
	};
	
	Rect.prototype.normalize = function ()
	{
		var temp = 0;
		
		if (this.left > this.right)
		{
			temp = this.left;
			this.left = this.right;
			this.right = temp;
		}
		
		if (this.top > this.bottom)
		{
			temp = this.top;
			this.top = this.bottom;
			this.bottom = temp;
		}
	};

	Rect.prototype.intersects_rect = function (rc)
	{
		return !(rc.right < this.left || rc.bottom < this.top || rc.left > this.right || rc.top > this.bottom);
	};
	
	Rect.prototype.intersects_rect_off = function (rc, ox, oy)
	{
		return !(rc.right + ox < this.left || rc.bottom + oy < this.top || rc.left + ox > this.right || rc.top + oy > this.bottom);
	};
	
	Rect.prototype.contains_pt = function (x, y)
	{
		return (x >= this.left && x <= this.right) && (y >= this.top && y <= this.bottom);
	};
	
	Rect.prototype.equals = function (r)
	{
		return this.left === r.left && this.top === r.top && this.right === r.right && this.bottom === r.bottom;
	};
	
	cr.rect = Rect;

	function Quad()
	{
		this.tlx = 0;
		this.tly = 0;
		this.trx = 0;
		this.try_ = 0;	// is a keyword otherwise!
		this.brx = 0;
		this.bry = 0;
		this.blx = 0;
		this.bly = 0;
		cr.seal(this);
	};
	
	Quad.prototype.set_from_rect = function (rc)
	{
		this.tlx = rc.left;
		this.tly = rc.top;
		this.trx = rc.right;
		this.try_ = rc.top;
		this.brx = rc.right;
		this.bry = rc.bottom;
		this.blx = rc.left;
		this.bly = rc.bottom;
	};
	
	Quad.prototype.set_from_rotated_rect = function (rc, a)
	{
		if (a === 0)
		{
			this.set_from_rect(rc);
		}
		else
		{
			var sin_a = Math.sin(a);
			var cos_a = Math.cos(a);

			var left_sin_a = rc.left * sin_a;
			var top_sin_a = rc.top * sin_a;
			var right_sin_a = rc.right * sin_a;
			var bottom_sin_a = rc.bottom * sin_a;

			var left_cos_a = rc.left * cos_a;
			var top_cos_a = rc.top * cos_a;
			var right_cos_a = rc.right * cos_a;
			var bottom_cos_a = rc.bottom * cos_a;
			
			this.tlx = left_cos_a - top_sin_a;
			this.tly = top_cos_a + left_sin_a;
			this.trx = right_cos_a - top_sin_a;
			this.try_ = top_cos_a + right_sin_a;
			this.brx = right_cos_a - bottom_sin_a;
			this.bry = bottom_cos_a + right_sin_a;
			this.blx = left_cos_a - bottom_sin_a;
			this.bly = bottom_cos_a + left_sin_a;
		}
	};

	Quad.prototype.offset = function (px, py)
	{
		this.tlx += px;
		this.tly += py;
		this.trx += px;
		this.try_ += py;
		this.brx += px;
		this.bry += py;
		this.blx += px;
		this.bly += py;
		return this;
	};
	
	var minresult = 0;
	var maxresult = 0;
	
	function minmax4(a, b, c, d)
	{
		if (a < b)
		{
			if (c < d)
			{
				// sort order: (a, c) (b, d)
				if (a < c)
					minresult = a;
				else
					minresult = c;
				
				if (b > d)
					maxresult = b;
				else
					maxresult = d;
			}
			else
			{
				// sort order: (a, d) (b, c)
				if (a < d)
					minresult = a;
				else
					minresult = d;
				
				if (b > c)
					maxresult = b;
				else
					maxresult = c;
			}
		}
		else
		{
			if (c < d)
			{
				// sort order: (b, c) (a, d)
				if (b < c)
					minresult = b;
				else
					minresult = c;
				
				if (a > d)
					maxresult = a;
				else
					maxresult = d;
			}
			else
			{
				// sort order: (b, d) (a, c)
				if (b < d)
					minresult = b;
				else
					minresult = d;
				
				if (a > c)
					maxresult = a;
				else
					maxresult = c;
			}
		}
	};

	Quad.prototype.bounding_box = function (rc)
	{
		minmax4(this.tlx, this.trx, this.brx, this.blx);
		rc.left = minresult;
		rc.right = maxresult;
		
		minmax4(this.tly, this.try_, this.bry, this.bly);
		rc.top = minresult;
		rc.bottom = maxresult;
	};

	Quad.prototype.contains_pt = function (x, y)
	{
		var tlx = this.tlx;
		var tly = this.tly;
		
		// p lies inside either triangles tl, tr, br or tl, bl, br
		var v0x = this.trx - tlx;
		var v0y = this.try_ - tly;
		var v1x = this.brx - tlx;
		var v1y = this.bry - tly;
		var v2x = x - tlx;
		var v2y = y - tly;

		var dot00 = v0x * v0x + v0y * v0y
		var dot01 = v0x * v1x + v0y * v1y
		var dot02 = v0x * v2x + v0y * v2y
		var dot11 = v1x * v1x + v1y * v1y
		var dot12 = v1x * v2x + v1y * v2y

		var invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01);
		var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
		var v = (dot00 * dot12 - dot01 * dot02) * invDenom;

		// Point is in first triangle
		if ((u >= 0.0) && (v > 0.0) && (u + v < 1))
			return true;

		// For second triangle, only v0 changes, so only recompute what that changes
		v0x = this.blx - tlx;
		v0y = this.bly - tly;

		var dot00 = v0x * v0x + v0y * v0y
		var dot01 = v0x * v1x + v0y * v1y
		var dot02 = v0x * v2x + v0y * v2y

		invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01);
		u = (dot11 * dot02 - dot01 * dot12) * invDenom;
		v = (dot00 * dot12 - dot01 * dot02) * invDenom;

		// Point is in second triangle
		return (u >= 0.0) && (v > 0.0) && (u + v < 1);
	};

	// Get point at index i (ordered: tl, tr, br, bl)
	Quad.prototype.at = function (i, xory)
	{
		// Returning X pos
		if (xory)
		{
			switch (i)
			{
				case 0: return this.tlx;
				case 1: return this.trx;
				case 2: return this.brx;
				case 3: return this.blx;
				case 4: return this.tlx;
				default: return this.tlx;
			}
		}
		// Returning Y pos
		else
		{
			switch (i)
			{
				case 0: return this.tly;
				case 1: return this.try_;
				case 2: return this.bry;
				case 3: return this.bly;
				case 4: return this.tly;
				default: return this.tly;
			}
		}
	};
	
	Quad.prototype.midX = function ()
	{
		return (this.tlx + this.trx  + this.brx + this.blx) / 4;
	};
	
	Quad.prototype.midY = function ()
	{
		return (this.tly + this.try_ + this.bry + this.bly) / 4;
	};

	Quad.prototype.intersects_segment = function (x1, y1, x2, y2)
	{
		// Contained segments count as intersecting
		if (this.contains_pt(x1, y1) || this.contains_pt(x2, y2))
			return true;
			
		var a1x, a1y, a2x, a2y;

		// Otherwise check all 4 combinations of segment intersects
		var i;
		for (i = 0; i < 4; i++)
		{
			a1x = this.at(i, true);
			a1y = this.at(i, false);
			a2x = this.at(i + 1, true);
			a2y = this.at(i + 1, false);
			
			if (cr.segments_intersect(x1, y1, x2, y2, a1x, a1y, a2x, a2y))
				return true;
		}
		
		return false;
	};
	
	Quad.prototype.intersects_quad = function (rhs)
	{
		// If rhs is completely contained by this quad, none of its segments intersect, but its
		// mid point will be inside this quad.  Test for this first.
		var midx = rhs.midX();
		var midy = rhs.midY();
								
		if (this.contains_pt(midx, midy))
			return true;

		// Alternatively rhs may completely contain this quad
		midx = this.midX();
		midy = this.midY();
		
		if (rhs.contains_pt(midx, midy))
			return true;
			
		var a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y;

		// Otherwise check all 16 combinations of segment intersects
		var i, j;
		for (i = 0; i < 4; i++)
		{
			for (j = 0; j < 4; j++)
			{
				a1x = this.at(i, true);
				a1y = this.at(i, false);
				a2x = this.at(i + 1, true);
				a2y = this.at(i + 1, false);
				b1x = rhs.at(j, true);
				b1y = rhs.at(j, false);
				b2x = rhs.at(j + 1, true);
				b2y = rhs.at(j + 1, false);
				
				if (cr.segments_intersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y))
					return true;
			}
		}

		return false;
	};
	
	cr.quad = Quad;
	
	// Return red, green and blue values in COLORREF format
	cr.RGB = function (red, green, blue)
	{
		return Math.max(Math.min(red, 255), 0)
			 | (Math.max(Math.min(green, 255), 0) << 8)
			 | (Math.max(Math.min(blue, 255), 0) << 16);
	};

	// Extended range RGB value
	var RGBEX_SHIFT = 131072;	// 2^17
	var RGBEX_MAX = 65535;		// 2^16 - 1
	var RGBEX_MIN = -65536;		// -(2^16)
	cr.RGBEx = function RGBEx(red, green, blue)
	{
		red = cr.clamp(Math.floor(red * 1024), RGBEX_MIN, RGBEX_MAX);
		green = cr.clamp(Math.floor(green * 1024), RGBEX_MIN, RGBEX_MAX);
		blue = cr.clamp(Math.floor(blue * 1024), RGBEX_MIN, RGBEX_MAX);

		// Shift negative numbers in to upper positive range
		if (red < 0)
			red += RGBEX_SHIFT;
		if (green < 0)
			green += RGBEX_SHIFT;
		if (blue < 0)
			blue += RGBEX_SHIFT;

		// Combine arithmetically since bitwise operators will truncate to 32-bit
		return -(
			red * RGBEX_SHIFT * RGBEX_SHIFT +
			green * RGBEX_SHIFT +
			blue
		);
	};
	
	cr.GetRValue = function (rgb)
	{
		if (rgb >= 0)
		{
			return (rgb & 0xFF) / 255;
		}
		else
		{
			var v = Math.floor(-rgb / (RGBEX_SHIFT * RGBEX_SHIFT));
			if (v > RGBEX_MAX)
				v -= RGBEX_SHIFT;
			return v / 1024;
		}
	};
	
	cr.GetGValue = function (rgb)
	{
		if (rgb >= 0)
		{
			return ((rgb & 0xFF00) >> 8) / 255;
		}
		else
		{
			var v = Math.floor((-rgb % (RGBEX_SHIFT * RGBEX_SHIFT)) / RGBEX_SHIFT);
			if (v > RGBEX_MAX)
				v -= RGBEX_SHIFT;
			return v / 1024;
		}
	};
	
	cr.GetBValue = function (rgb)
	{
		if (rgb >= 0)
		{
			return ((rgb & 0xFF0000) >> 16) / 255;
		}
		else
		{
			var v = Math.floor(-rgb % RGBEX_SHIFT);
			if (v > RGBEX_MAX)
				v -= RGBEX_SHIFT;
			return v / 1024;
		}
	};

	// Merge attributes of b in to a, where a does not have an attribute in b.
	// Does not overwrite attributes in a; this is considered an error case (unless allowOverwrite is true)
	cr.shallowCopy = function (a, b, allowOverwrite)
	{
		var attr;
		for (attr in b)
		{
			if (b.hasOwnProperty(attr))
			{
;

				a[attr] = b[attr];
			}
		}
		
		return a;
	};

	// Remove item at integer index from arr
	cr.arrayRemove = function (arr, index)
	{
		var i, len;
		index = cr.floor(index);
		
		if (index < 0 || index >= arr.length)
			return;							// index out of bounds

		for (i = index, len = arr.length - 1; i < len; i++)
			arr[i] = arr[i + 1];
			
		cr.truncateArray(arr, len);
	};
	
	cr.truncateArray = function (arr, index)
	{
		//arr.splice(index);
		arr.length = index;
	};
	
	cr.clearArray = function (arr)
	{
		cr.truncateArray(arr, 0);
	};
	
	// Make array 'dest' look the same as 'src' (make same length and shallow copy contents)
	// Can help avoid garbage created by slice(0)
	cr.shallowAssignArray = function (dest, src)
	{
		cr.clearArray(dest);
		
		var i, len;
		for (i = 0, len = src.length; i < len; ++i)
			dest[i] = src[i];
	};
	
	// Append the whole of b to the end of a
	cr.appendArray = function (a, b)
	{
		a.push.apply(a, b);
	};
	
	cr.fastIndexOf = function (arr, item)
	{
		// faster in JS land
		var i, len;
		for (i = 0, len = arr.length; i < len; ++i)
		{
			if (arr[i] === item)
				return i;
		}
		
		return -1;
	};

	// Find the object 'item' in arr and remove it
	cr.arrayFindRemove = function (arr, item)
	{
		var index = cr.fastIndexOf(arr, item);
		
		if (index !== -1)
			cr.arrayRemove(arr, index);
	};

	// Helpers
	cr.clamp = function(x, a, b)
	{
		if (x < a)
			return a;
		else if (x > b)
			return b;
		else
			return x;
	};

	cr.to_radians = function(x)
	{
		return x / (180.0 / cr.PI);
	};

	cr.to_degrees = function(x)
	{
		return x * (180.0 / cr.PI);
	};

	cr.clamp_angle_degrees = function (a)
	{
		// Clamp in degrees
		a %= 360;       // now in (-360, 360) range

		if (a < 0)
			a += 360;   // now in [0, 360) range

		return a;
	};

	cr.clamp_angle = function (a)
	{
		// Clamp in radians
		a %= 2 * cr.PI;       // now in (-2pi, 2pi) range

		if (a < 0)
			a += 2 * cr.PI;   // now in [0, 2pi) range

		return a;
	};

	cr.to_clamped_degrees = function (x)
	{
		// Convert x from radians to [0, 360) range
		return cr.clamp_angle_degrees(cr.to_degrees(x));
	};

	cr.to_clamped_radians = function (x)
	{
		// Convert x from radians to [0, 2pi) range
		return cr.clamp_angle(cr.to_radians(x));
	};
	
	cr.angleTo = function(x1, y1, x2, y2)
	{
		var dx = x2 - x1;
        var dy = y2 - y1;
		return Math.atan2(dy, dx);
	};

	cr.angleDiff = function (a1, a2)
	{
		if (a1 === a2)
			return 0;

		var s1 = Math.sin(a1);
		var c1 = Math.cos(a1);
		var s2 = Math.sin(a2);
		var c2 = Math.cos(a2);
		var n = s1 * s2 + c1 * c2;
		
		// Prevent NaN results
		if (n >= 1)
			return 0;
		if (n <= -1)
			return cr.PI;
			
		return Math.acos(n);
	};

	cr.angleRotate = function (start, end, step)
	{
		var ss = Math.sin(start);
		var cs = Math.cos(start);
		var se = Math.sin(end);
		var ce = Math.cos(end);

		if (Math.acos(ss * se + cs * ce) > step)
		{
			if (cs * se - ss * ce > 0)
				return cr.clamp_angle(start + step);
			else
				return cr.clamp_angle(start - step);
		}
		else
			return cr.clamp_angle(end);
	};

	// test if a1 is clockwise of a2
	cr.angleClockwise = function (a1, a2)
	{
		var s1 = Math.sin(a1);
		var c1 = Math.cos(a1);
		var s2 = Math.sin(a2);
		var c2 = Math.cos(a2);
		return c1 * s2 - s1 * c2 <= 0;
	};
	
	cr.rotatePtAround = function (px, py, a, ox, oy, getx)
	{
		if (a === 0)
			return getx ? px : py;
		
		var sin_a = Math.sin(a);
		var cos_a = Math.cos(a);
		
		px -= ox;
		py -= oy;

		var left_sin_a = px * sin_a;
		var top_sin_a = py * sin_a;
		var left_cos_a = px * cos_a;
		var top_cos_a = py * cos_a;
		
		px = left_cos_a - top_sin_a;
		py = top_cos_a + left_sin_a;
		
		px += ox;
		py += oy;
		
		return getx ? px : py;
	}
	
	cr.distanceTo = function(x1, y1, x2, y2)
	{
		var dx = x2 - x1;
        var dy = y2 - y1;
		return Math.sqrt(dx*dx + dy*dy);
	};

	cr.xor = function (x, y)
	{
		return !x !== !y;
	};
	
	cr.lerp = function (a, b, x)
	{
		return a + (b - a) * x;
	};
	
	cr.unlerp = function (a, b, c)
	{
		if (a === b)
			return 0;		// avoid divide by 0
		
		return (c - a) / (b - a);
	};
	
	cr.anglelerp = function (a, b, x)
	{
		var diff = cr.angleDiff(a, b);
		
		// b clockwise from a
		if (cr.angleClockwise(b, a))
		{
			return a + diff * x;
		}
		else
		{
			return a - diff * x;
		}
	};
	
	cr.qarp = function (a, b, c, x)
	{
		return cr.lerp(cr.lerp(a, b, x), cr.lerp(b, c, x), x);
	};
	
	cr.cubic = function (a, b, c, d, x)
	{
		return cr.lerp(cr.qarp(a, b, c, x), cr.qarp(b, c, d, x), x);
	};
	
	cr.cosp = function (a, b, x)
	{
		return (a + b + (a - b) * Math.cos(x * Math.PI)) / 2;
	};
	
	cr.hasAnyOwnProperty = function (o)
	{
		var p;
		for (p in o)
		{
			if (o.hasOwnProperty(p))
				return true;
		}
		
		return false;
	};
	
	// remove all own properties on obj, effectively reverting it to a new object
	// use with care! probably reverts object to dictionary mode which is slower
	cr.wipe = function (obj)
	{
		var p;
		for (p in obj)
		{
			if (obj.hasOwnProperty(p))
				delete obj[p];
		}
	};
	
	var startup_time = +(new Date());
	
	cr.performance_now = function()
	{
		if (typeof window["performance"] !== "undefined")
		{
			var winperf = window["performance"];
			
			if (typeof winperf.now !== "undefined")
				return winperf.now();
			else if (typeof winperf["webkitNow"] !== "undefined")
				return winperf["webkitNow"]();
			else if (typeof winperf["mozNow"] !== "undefined")
				return winperf["mozNow"]();
			else if (typeof winperf["msNow"] !== "undefined")
				return winperf["msNow"]();
		}
		
		return Date.now() - startup_time;
	};
	
	var isChrome = false;
	var isSafari = false;
	var isiOS = false;
	
	if (typeof window !== "undefined")		// not c2 editor
	{
		isChrome = /chrome/i.test(navigator.userAgent) || /chromium/i.test(navigator.userAgent);
		isSafari = !isChrome && /safari/i.test(navigator.userAgent);
		isiOS = /(iphone|ipod|ipad)/i.test(navigator.userAgent);
	}
	
	var supports_set = (typeof Set !== "undefined" && typeof Set.prototype["forEach"] !== "undefined");

	// Set of objects.  Requires a .toString() overload to distinguish objects where Set is not supported.
	function ObjectSet_()
	{
		this.s = null;
		this.items = null;			// lazy allocated (hopefully results in better GC performance)
		this.item_count = 0;
		
		if (supports_set)
		{
			this.s = new Set();
		}

		// Caches its items as an array for fast repeated calls to .valuesRef()
		this.values_cache = [];
		this.cache_valid = true;
		
		cr.seal(this);
	};

	ObjectSet_.prototype.contains = function (x)
	{
		if (this.isEmpty())
			return false;
		
		if (supports_set)
			return this.s["has"](x);
		else
			return (this.items && this.items.hasOwnProperty(x));
	};

	ObjectSet_.prototype.add = function (x)
	{
		if (supports_set)
		{
			if (!this.s["has"](x))
			{
				this.s["add"](x);
				this.cache_valid = false;
			}
		}
		else
		{
			var str = x.toString();
			var items = this.items;
			
			// not yet created 'items': create it and add one item
			if (!items)
			{
				this.items = {};
				this.items[str] = x;
				this.item_count = 1;
				this.cache_valid = false;
			}
			// already created 'items': add if not already added
			// don't use contains(), it would call toString() again
			else if (!items.hasOwnProperty(str))
			{
				items[str] = x;
				this.item_count++;
				this.cache_valid = false;
			}
		}
	};

	ObjectSet_.prototype.remove = function (x)
	{
		if (this.isEmpty())
			return;
		
		if (supports_set)
		{
			if (this.s["has"](x))
			{
				this.s["delete"](x);
				this.cache_valid = false;
			}
		}
		else if (this.items)
		{
			var str = x.toString();
			var items = this.items;
			
			if (items.hasOwnProperty(str))
			{
				delete items[str];
				this.item_count--;
				this.cache_valid = false;
			}
		}
	};

	ObjectSet_.prototype.clear = function (/*wipe_*/)
	{
		if (this.isEmpty())
			return;
		
		if (supports_set)
		{
			this.s["clear"]();			// best!
		}
		else
		{
			//if (wipe_)
			//	cr.wipe(this.items);	// is slower
			//else
				this.items = null;		// creates garbage; will lazy allocate on next add()
			
			this.item_count = 0;
		}

		// Reset cache to known empty state
		cr.clearArray(this.values_cache);
		this.cache_valid = true;
	};

	ObjectSet_.prototype.isEmpty = function ()
	{
		return this.count() === 0;
	};

	ObjectSet_.prototype.count = function ()
	{
		if (supports_set)
			return this.s["size"];
		else
			return this.item_count;
	};
	
	var current_arr = null;
	var current_index = 0;
	
	function set_append_to_arr(x)
	{
		current_arr[current_index++] = x;
	};
	
	ObjectSet_.prototype.update_cache = function ()
	{
		if (this.cache_valid)
			return;

		if (supports_set)
		{
			cr.clearArray(this.values_cache);
			
			current_arr = this.values_cache;
			current_index = 0;
			
			this.s["forEach"](set_append_to_arr);
			
;
			
			current_arr = null;
			current_index = 0;
		}
		else
		{
			var values_cache = this.values_cache;
			cr.clearArray(values_cache);
			var p, n = 0, items = this.items;
		
			if (items)
			{
				for (p in items)
				{
					if (items.hasOwnProperty(p))
						values_cache[n++] = items[p];
				}
			}
			
;
		}
		
		// Cache now up to date
		this.cache_valid = true;
	};
	
	ObjectSet_.prototype.valuesRef = function ()
	{
		this.update_cache();
		
		// Return reference to the cache
		return this.values_cache;
	};
	
	cr.ObjectSet = ObjectSet_;
	
	// Remove all duplicates from an array
	var tmpSet = new cr.ObjectSet();
	
	cr.removeArrayDuplicates = function (arr)
	{
		var i, len;
		for (i = 0, len = arr.length; i < len; ++i)
		{
			tmpSet.add(arr[i]);
		}
		
		cr.shallowAssignArray(arr, tmpSet.valuesRef());
		tmpSet.clear();
	};

	// Remove every item in an ObjectSet 'remset' from 'arr'.
	// Avoids repeated expensive calls to remove one item at a time.
	cr.arrayRemoveAllFromObjectSet = function (arr, remset)
	{
		if (supports_set)
			cr.arrayRemoveAll_set(arr, remset.s);
		else
			cr.arrayRemoveAll_arr(arr, remset.valuesRef());
	};
	
	cr.arrayRemoveAll_set = function (arr, s)
	{
		var i, j, len, item;
		
		for (i = 0, j = 0, len = arr.length; i < len; ++i)
		{
			item = arr[i];
			
			if (!s["has"](item))					// not an item to remove
				arr[j++] = item;					// keep it
		}
		
		cr.truncateArray(arr, j);
	};
	
	cr.arrayRemoveAll_arr = function (arr, rem)
	{
		var i, j, len, item;
		
		for (i = 0, j = 0, len = arr.length; i < len; ++i)
		{
			item = arr[i];
			
			if (cr.fastIndexOf(rem, item) === -1)	// not an item to remove
				arr[j++] = item;					// keep it
		}
		
		cr.truncateArray(arr, j);
	};
	

    // Kahan adder.  At the mercy of optimising javascript engines which may wipe out its effect entirely
    // - hopefully they use the equivalent of precise mode.
	function KahanAdder_()
	{
		this.c = 0;
        this.y = 0;
        this.t = 0;
        this.sum = 0;
		cr.seal(this);
	};

	KahanAdder_.prototype.add = function (v)
	{
		this.y = v - this.c;
	    this.t = this.sum + this.y;
	    this.c = (this.t - this.sum) - this.y;
	    this.sum = this.t;
	};

    KahanAdder_.prototype.reset = function ()
    {
        this.c = 0;
        this.y = 0;
        this.t = 0;
        this.sum = 0;
    };
	
	cr.KahanAdder = KahanAdder_;
	
	cr.regexp_escape = function(text)
	{
		return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
	};
	
	// Collision polys
	function CollisionPoly_(pts_array_)
	{
		this.pts_cache = [];
		this.bboxLeft = 0;
		this.bboxTop = 0;
		this.bboxRight = 0;
		this.bboxBottom = 0;
		this.convexpolys = null;		// for physics behavior to cache separated polys
		this.set_pts(pts_array_);
		cr.seal(this);
	};
	
	CollisionPoly_.prototype.set_pts = function(pts_array_)
	{
		this.pts_array = pts_array_;
		this.pts_count = pts_array_.length / 2;			// x, y, x, y... in array
		this.pts_cache.length = pts_array_.length;
		
		// invalidate cache
		this.cache_width = -1;
		this.cache_height = -1;
		this.cache_angle = 0;
	};
	
	CollisionPoly_.prototype.is_empty = function()
	{
		return !this.pts_array.length;
	};
	
	CollisionPoly_.prototype.update_bbox = function ()
	{
		var myptscache = this.pts_cache;
		
		var bboxLeft_ = myptscache[0];
		var bboxRight_ = bboxLeft_;
		var bboxTop_ = myptscache[1];
		var bboxBottom_ = bboxTop_;
		
		var x, y, i = 1, i2, len = this.pts_count;
		
		for ( ; i < len; ++i)
		{
			i2 = i*2;
			x = myptscache[i2];
			y = myptscache[i2+1];
			
			if (x < bboxLeft_)
				bboxLeft_ = x;
			if (x > bboxRight_)
				bboxRight_ = x;
			if (y < bboxTop_)
				bboxTop_ = y;
			if (y > bboxBottom_)
				bboxBottom_ = y;
		}
		
		this.bboxLeft = bboxLeft_;
		this.bboxRight = bboxRight_;
		this.bboxTop = bboxTop_;
		this.bboxBottom = bboxBottom_;
	};
	
	CollisionPoly_.prototype.set_from_rect = function(rc, offx, offy)
	{
		this.pts_cache.length = 8;
		this.pts_count = 4;
		var myptscache = this.pts_cache;
		myptscache[0] = rc.left - offx;
		myptscache[1] = rc.top - offy;
		myptscache[2] = rc.right - offx;
		myptscache[3] = rc.top - offy;
		myptscache[4] = rc.right - offx;
		myptscache[5] = rc.bottom - offy;
		myptscache[6] = rc.left - offx;
		myptscache[7] = rc.bottom - offy;
		this.cache_width = rc.right - rc.left;
		this.cache_height = rc.bottom - rc.top;
		this.update_bbox();
	};
	
	CollisionPoly_.prototype.set_from_quad = function(q, offx, offy, w, h)
	{
		this.pts_cache.length = 8;
		this.pts_count = 4;
		var myptscache = this.pts_cache;
		myptscache[0] = q.tlx - offx;
		myptscache[1] = q.tly - offy;
		myptscache[2] = q.trx - offx;
		myptscache[3] = q.try_ - offy;
		myptscache[4] = q.brx - offx;
		myptscache[5] = q.bry - offy;
		myptscache[6] = q.blx - offx;
		myptscache[7] = q.bly - offy;
		this.cache_width = w;
		this.cache_height = h;
		this.update_bbox();
	};
	
	CollisionPoly_.prototype.set_from_poly = function (r)
	{
		this.pts_count = r.pts_count;
		cr.shallowAssignArray(this.pts_cache, r.pts_cache);
		this.bboxLeft = r.bboxLeft;
		this.bboxTop - r.bboxTop;
		this.bboxRight = r.bboxRight;
		this.bboxBottom = r.bboxBottom;
	};
	
	CollisionPoly_.prototype.cache_poly = function(w, h, a)
	{
		if (this.cache_width === w && this.cache_height === h && this.cache_angle === a)
			return;		// cache up-to-date
			
		// Set the points cache to the scaled and rotated poly
		this.cache_width = w;
		this.cache_height = h;
		this.cache_angle = a;
		
		var i, i2, i21, len, x, y;
		var sina = 0;
		var cosa = 1;
		var myptsarray = this.pts_array;
		var myptscache = this.pts_cache;
		
		if (a !== 0)
		{
			sina = Math.sin(a);
			cosa = Math.cos(a);
		}
		
		for (i = 0, len = this.pts_count; i < len; i++)
		{
			// get scaled
			i2 = i*2;
			i21 = i2+1;
			x = myptsarray[i2] * w;
			y = myptsarray[i21] * h;
			
			// rotate by angle and save in cache
			myptscache[i2] = (x * cosa) - (y * sina);
			myptscache[i21] = (y * cosa) + (x * sina);
		}
		
		this.update_bbox();
	};
	
	// (px, py) is relative to polygon origin.  Poly must be cached beforehand.
	CollisionPoly_.prototype.contains_pt = function (a2x, a2y)
	{
		var myptscache = this.pts_cache;
		
		// Special case: first point is always contained (so exactly overlapping identical polys register collision)
		if (a2x === myptscache[0] && a2y === myptscache[1])
			return true;
		
		// Determine start coordinates outside of poly.
		// Test 2 different start coordinates to ensure segments passing exactly
		// through vertices don't give false negatives.
		// Also to guarantee the start points are outside of this poly, take the
		// poly's bounding box and set the positions outside of that box.
		var i, i2, imod, len = this.pts_count;
		
		var a1x = this.bboxLeft - 110;
		var a1y = this.bboxTop - 101;
		var a3x = this.bboxRight + 131
		var a3y = this.bboxBottom + 120;
		var b1x, b1y, b2x, b2y;
		
		// count segments intersecting from given start points
		var count1 = 0, count2 = 0;
		
		for (i = 0; i < len; i++)
		{
			i2 = i*2;
			imod = ((i+1)%len)*2;
			b1x = myptscache[i2];
			b1y = myptscache[i2+1];
			b2x = myptscache[imod];
			b2y = myptscache[imod+1];
			
			if (cr.segments_intersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y))
				count1++;
			if (cr.segments_intersect(a3x, a3y, a2x, a2y, b1x, b1y, b2x, b2y))
				count2++;
		}
		
		// In theory both counts should always be even or odd at the same time.
		// However, if one of the segments passes exactly through a vertex then the count will incorrectly be even.
		// In that case the other line should still have an odd count.  Therefore, return
		// true if either count was odd.
		return (count1 % 2 === 1) || (count2 % 2 === 1);
	};
	
	CollisionPoly_.prototype.intersects_poly = function (rhs, offx, offy)
	{
		var rhspts = rhs.pts_cache;
		var mypts = this.pts_cache;
		
		// Determine if this contains rhs
		if (this.contains_pt(rhspts[0] + offx, rhspts[1] + offy))
			return true;
		// Determine if rhs contains this
		if (rhs.contains_pt(mypts[0] - offx, mypts[1] - offy))
			return true;
			
		// Check all combinations of segment intersection
		// TODO: could be faster with a sweep
		var i, i2, imod, leni, j, j2, jmod, lenj;
		var a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y;
		
		for (i = 0, leni = this.pts_count; i < leni; i++)
		{
			i2 = i*2;
			imod = ((i+1)%leni)*2;
			a1x = mypts[i2];
			a1y = mypts[i2+1];
			a2x = mypts[imod];
			a2y = mypts[imod+1];
			
			for (j = 0, lenj = rhs.pts_count; j < lenj; j++)
			{
				j2 = j*2;
				jmod = ((j+1)%lenj)*2;
				b1x = rhspts[j2] + offx;
				b1y = rhspts[j2+1] + offy;
				b2x = rhspts[jmod] + offx;
				b2y = rhspts[jmod+1] + offy;
				
				if (cr.segments_intersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y))
					return true;
			}
		}
		
		return false;
	};
	
	CollisionPoly_.prototype.intersects_segment = function (offx, offy, x1, y1, x2, y2)
	{
		var mypts = this.pts_cache;
		
		// Determine if this contains either end of the segment
		if (this.contains_pt(x1 - offx, y1 - offy))
			return true;
			
		// Check all combinations of segment intersection
		var i, leni, i2, imod;
		var a1x, a1y, a2x, a2y;
		
		for (i = 0, leni = this.pts_count; i < leni; i++)
		{
			i2 = i*2;
			imod = ((i+1)%leni)*2;
			a1x = mypts[i2] + offx;
			a1y = mypts[i2+1] + offy;
			a2x = mypts[imod] + offx;
			a2y = mypts[imod+1] + offy;
			
			if (cr.segments_intersect(x1, y1, x2, y2, a1x, a1y, a2x, a2y))
				return true;
		}
		
		return false;
	};
	
	CollisionPoly_.prototype.mirror = function (px)
	{
		var i, leni, i2;
		for (i = 0, leni = this.pts_count; i < leni; ++i)
		{
			i2 = i*2;
			this.pts_cache[i2] = px * 2 - this.pts_cache[i2];
		}
	};
	
	CollisionPoly_.prototype.flip = function (py)
	{
		var i, leni, i21;
		for (i = 0, leni = this.pts_count; i < leni; ++i)
		{
			i21 = i*2+1;
			this.pts_cache[i21] = py * 2 - this.pts_cache[i21];
		}
	};
	
	CollisionPoly_.prototype.diag = function ()
	{
		var i, leni, i2, i21, temp;
		for (i = 0, leni = this.pts_count; i < leni; ++i)
		{
			i2 = i*2;
			i21 = i2+1;
			temp = this.pts_cache[i2];
			this.pts_cache[i2] = this.pts_cache[i21];
			this.pts_cache[i21] = temp;
		}
	};
	
	cr.CollisionPoly = CollisionPoly_;
	
	function SparseGrid_(cellwidth_, cellheight_)
	{
		this.cellwidth = cellwidth_;
		this.cellheight = cellheight_;
		
		// Using string properties derived from numbers to store if a grid cell
		// is present, e.g. this.cells["-6"]["4"]
		this.cells = {};
	};
	
	SparseGrid_.prototype.totalCellCount = 0;
	
	SparseGrid_.prototype.getCell = function (x_, y_, create_if_missing)
	{
		var ret;
		var col = this.cells[x_];
		
		if (!col)
		{
			if (create_if_missing)
			{
				ret = allocGridCell(this, x_, y_);
				this.cells[x_] = {};
				this.cells[x_][y_] = ret;
				return ret;
			}
			else
				return null;
		}
		
		ret = col[y_];
		
		if (ret)
			return ret;
		else if (create_if_missing)
		{
			ret = allocGridCell(this, x_, y_);
			this.cells[x_][y_] = ret;
			return ret;
		}
		else
			return null;
	};
	
	SparseGrid_.prototype.XToCell = function (x_)
	{
		return cr.floor(x_ / this.cellwidth);
	};
	
	SparseGrid_.prototype.YToCell = function (y_)
	{
		return cr.floor(y_ / this.cellheight);
	};
	
	SparseGrid_.prototype.update = function (inst, oldrange, newrange)
	{
		var x, lenx, y, leny, cell;
		
		// If no old range provided, must be new object, so just insert it across
		// the new range.
		if (oldrange)
		{
			// Iterate old range removing this instance (where old range does not overlap new range)
			// Note ranges are inclusive!
			for (x = oldrange.left, lenx = oldrange.right; x <= lenx; ++x)
			{
				for (y = oldrange.top, leny = oldrange.bottom; y <= leny; ++y)
				{
					if (newrange && newrange.contains_pt(x, y))
						continue;	// is still in this cell
					
					cell = this.getCell(x, y, false);	// don't create if missing
					
					if (!cell)
						continue;	// cell does not exist yet
					
					cell.remove(inst);
					
					// recycle the cell if it's now empty
					if (cell.isEmpty())
					{
						freeGridCell(cell);
						this.cells[x][y] = null;
					}
				}
			}
		}
		
		// If no new range provided, must be being destroyed, so remove across the old range.
		if (newrange)
		{
			// Iterate the new range inserting this instance (where new range does not
			// overlap old range)
			for (x = newrange.left, lenx = newrange.right; x <= lenx; ++x)
			{
				for (y = newrange.top, leny = newrange.bottom; y <= leny; ++y)
				{
					if (oldrange && oldrange.contains_pt(x, y))
						continue;	// is still in this cell
					
					// create the cell if missing and insert the object.
					// note if already in the cell, does nothing.
					this.getCell(x, y, true).insert(inst);
				}
			}
		}
	};
	
	SparseGrid_.prototype.queryRange = function (rc, result)
	{
		var x, lenx, ystart, y, leny, cell;
		
		x = this.XToCell(rc.left);
		ystart = this.YToCell(rc.top);
		lenx = this.XToCell(rc.right);
		leny = this.YToCell(rc.bottom);
		
		for ( ; x <= lenx; ++x)
		{
			for (y = ystart; y <= leny; ++y)
			{
				cell = this.getCell(x, y, false);
				
				if (!cell)
					continue;
				
				cell.dump(result);
			}
		}
	};
	
	cr.SparseGrid = SparseGrid_;
	
	function RenderGrid_(cellwidth_, cellheight_)
	{
		this.cellwidth = cellwidth_;
		this.cellheight = cellheight_;
		
		// Using string properties derived from numbers to store if a grid cell
		// is present, e.g. this.cells["-6"]["4"]
		this.cells = {};
	};
	
	RenderGrid_.prototype.totalCellCount = 0;
	
	RenderGrid_.prototype.getCell = function (x_, y_, create_if_missing)
	{
		var ret;
		var col = this.cells[x_];
		
		if (!col)
		{
			if (create_if_missing)
			{
				ret = allocRenderCell(this, x_, y_);
				this.cells[x_] = {};
				this.cells[x_][y_] = ret;
				return ret;
			}
			else
				return null;
		}
		
		ret = col[y_];
		
		if (ret)
			return ret;
		else if (create_if_missing)
		{
			ret = allocRenderCell(this, x_, y_);
			this.cells[x_][y_] = ret;
			return ret;
		}
		else
			return null;
	};
	
	RenderGrid_.prototype.XToCell = function (x_)
	{
		return cr.floor(x_ / this.cellwidth);
	};
	
	RenderGrid_.prototype.YToCell = function (y_)
	{
		return cr.floor(y_ / this.cellheight);
	};
	
	RenderGrid_.prototype.update = function (inst, oldrange, newrange)
	{
		var x, lenx, y, leny, cell;
		
		// If no old range provided, must be new object, so just insert it across
		// the new range.
		if (oldrange)
		{
			// Iterate old range removing this instance (where old range does not overlap new range)
			// Note ranges are inclusive!
			for (x = oldrange.left, lenx = oldrange.right; x <= lenx; ++x)
			{
				for (y = oldrange.top, leny = oldrange.bottom; y <= leny; ++y)
				{
					if (newrange && newrange.contains_pt(x, y))
						continue;	// is still in this cell
					
					cell = this.getCell(x, y, false);	// don't create if missing
					
					if (!cell)
						continue;	// cell does not exist yet
					
					cell.remove(inst);
					
					// recycle the cell if it's now empty
					if (cell.isEmpty())
					{
						freeRenderCell(cell);
						this.cells[x][y] = null;
					}
				}
			}
		}
		
		// If no new range provided, must be being destroyed, so remove across the old range.
		if (newrange)
		{
			// Iterate the new range inserting this instance (where new range does not
			// overlap old range)
			for (x = newrange.left, lenx = newrange.right; x <= lenx; ++x)
			{
				for (y = newrange.top, leny = newrange.bottom; y <= leny; ++y)
				{
					if (oldrange && oldrange.contains_pt(x, y))
						continue;	// is still in this cell
					
					// create the cell if missing and insert the object.
					// note if already in the cell, does nothing.
					this.getCell(x, y, true).insert(inst);
				}
			}
		}
	};
	
	RenderGrid_.prototype.queryRange = function (left, top, right, bottom, result)
	{
		var x, lenx, ystart, y, leny, cell;
		
		x = this.XToCell(left);
		ystart = this.YToCell(top);
		lenx = this.XToCell(right);
		leny = this.YToCell(bottom);
		
		for ( ; x <= lenx; ++x)
		{
			for (y = ystart; y <= leny; ++y)
			{
				cell = this.getCell(x, y, false);
				
				if (!cell)
					continue;
				
				cell.dump(result);
			}
		}
	};
	
	RenderGrid_.prototype.markRangeChanged = function (rc)
	{
		var x, lenx, ystart, y, leny, cell;
		
		x = rc.left;
		ystart = rc.top;
		lenx = rc.right;
		leny = rc.bottom;
		
		for ( ; x <= lenx; ++x)
		{
			for (y = ystart; y <= leny; ++y)
			{
				cell = this.getCell(x, y, false);
				
				if (!cell)
					continue;
				
				cell.is_sorted = false;
			}
		}
	};
	
	cr.RenderGrid = RenderGrid_;
	
	var gridcellcache = [];
	
	function allocGridCell(grid_, x_, y_)
	{
		var ret;
		
		SparseGrid_.prototype.totalCellCount++;
		
		if (gridcellcache.length)
		{
			ret = gridcellcache.pop();
			ret.grid = grid_;
			ret.x = x_;
			ret.y = y_;
			return ret;
		}
		else
			return new cr.GridCell(grid_, x_, y_);
	};
	
	function freeGridCell(c)
	{
		SparseGrid_.prototype.totalCellCount--;
		
		c.objects.clear();
		
		if (gridcellcache.length < 1000)
			gridcellcache.push(c);
	};
	
	function GridCell_(grid_, x_, y_)
	{
		this.grid = grid_;
		this.x = x_;
		this.y = y_;
		this.objects = new cr.ObjectSet();
	};
	
	GridCell_.prototype.isEmpty = function ()
	{
		return this.objects.isEmpty();
	};
	
	GridCell_.prototype.insert = function (inst)
	{
		this.objects.add(inst);
	};
	
	GridCell_.prototype.remove = function (inst)
	{
		this.objects.remove(inst);
	};
	
	GridCell_.prototype.dump = function (result)
	{
		cr.appendArray(result, this.objects.valuesRef());
	};
	
	cr.GridCell = GridCell_;
	
	var rendercellcache = [];
	
	function allocRenderCell(grid_, x_, y_)
	{
		var ret;
		
		RenderGrid_.prototype.totalCellCount++;
		
		if (rendercellcache.length)
		{
			ret = rendercellcache.pop();
			ret.grid = grid_;
			ret.x = x_;
			ret.y = y_;
			return ret;
		}
		else
			return new cr.RenderCell(grid_, x_, y_);
	};
	
	function freeRenderCell(c)
	{
		RenderGrid_.prototype.totalCellCount--;
		
		c.reset();
		
		if (rendercellcache.length < 1000)
			rendercellcache.push(c);
	};
	
	function RenderCell_(grid_, x_, y_)
	{
		this.grid = grid_;
		this.x = x_;
		this.y = y_;
		this.objects = [];		// array which needs to be sorted by Z order
		this.is_sorted = true;	// whether array is in correct sort order or not
		
		// instances pending removal from 'objects' array. Try to batch these removals
		// best performance, since lots of single removals has poor efficiency.
		this.pending_removal = new cr.ObjectSet();
		this.any_pending_removal = false;
	};
	
	RenderCell_.prototype.isEmpty = function ()
	{
		// 'Empty' state is a little non-trivial since there is the set of objects pending_removal
		// to take in to consideration. First of all if objects is empty then we know the cell is empty.
		if (!this.objects.length)
		{
;
;
			return true;
		}
		
		// 'objects' is not empty. However if there are fewer instances in the removal queue, then
		// even if we called flush_pending we know there would still be instances left.
		// So we can safely indicate that the cell is not empty.
		if (this.objects.length > this.pending_removal.count())
			return false;
		
		// Otherwise every item in objects must be in the pending removal set.
		// The set will be empty if we update it. Use this opportunity to clear the state
		// and indicate empty.
;
		this.flush_pending();		// takes fast path and just resets state
		return true;
	};
	
	RenderCell_.prototype.insert = function (inst)
	{
		// If the instance being inserted is in the pending_removal queue
		// then it is actually still in the objects array. In this case simply
		// remove the entry from the pending_removal queue.
		if (this.pending_removal.contains(inst))
		{
			this.pending_removal.remove(inst);
			
			// Unset the flag if there is no longer anything pending, to
			// avoid unnecessary work
			if (this.pending_removal.isEmpty())
				this.any_pending_removal = false;
			
			// 'inst' is still in objects array
			//assert2(this.objects.indexOf(inst) >= 0, "expected instance to be in objects list");
			return;
		}
		
		if (this.objects.length)
		{
			//assert2(this.objects.indexOf(inst) === -1, "instance already in render cell");
			
			// Simply append the instance to the end of the objects array. We could do a binary
			// search for the right place to insert, but then batch inserts will have poor efficiency.
			// It is probably better to sort the whole list later when it's needed if it has changed.
			// Note that if the inserted instance has a higher Z index than the previous top item,
			// then the sorted state is preserved so we can avoid marking it as needing a sort.
			var top = this.objects[this.objects.length - 1];
			
			if (top.get_zindex() > inst.get_zindex())
				this.is_sorted = false;		// 'inst' should be somewhere beneath 'top'
			
			this.objects.push(inst);
		}
		else
		{
			// Array is empty: add instance and leave in sorted mode (no need to sort one element)
			this.objects.push(inst);
			this.is_sorted = true;
		}
		
		//assert2(this.objects.indexOf(inst) >= 0, "expected instance to be in objects list");
;
	};
	
	RenderCell_.prototype.remove = function (inst)
	{
		//assert2(this.objects.indexOf(inst) >= 0, "removing instance not in render cell");
		
		// Add to objects pending removal, for batching
		this.pending_removal.add(inst);
		this.any_pending_removal = true;
		
		// Distant and rarely updated cells could end up getting very long pending_removal queues.
		// To avoid a memory-leak like buildup, force a flush if it reaches 30 items.
		if (this.pending_removal.count() >= 30)
			this.flush_pending();
	};
	
	// Batch remove all instances pending removal, ensuring the 'objects'
	// array is up-to-date with correct instances, but not necessarily sorted
	RenderCell_.prototype.flush_pending = function ()
	{
;
		
		if (!this.any_pending_removal)
			return;		// not changed
		
		// Special case: if every instance in the objects array is pending removal, then
		// simply clear everything.
		if (this.pending_removal.count() === this.objects.length)
		{
			// Expect every instance in 'objects' to be present in 'pending_removal'
			//for (var i = 0, len = this.objects.length; i < len; ++i)
			//{
			//	assert2(this.pending_removal.contains(this.objects[i]), "render cell invalid pending state");
			//}
			
			this.reset();
			return;
		}
		
		// remove all pending removal in one pass for best efficiency
		cr.arrayRemoveAllFromObjectSet(this.objects, this.pending_removal);
		
		this.pending_removal.clear();
		this.any_pending_removal = false;
	};
	
	function sortByInstanceZIndex(a, b)
	{
		// only called by ensure_sorted which comes after the layer updates its zindices itself,
		// so no need to call get_zindex, all direct zindex properties are up to date
		return a.zindex - b.zindex;
	};
	
	RenderCell_.prototype.ensure_sorted = function ()
	{
		if (this.is_sorted)
			return;		// already sorted
		
		this.objects.sort(sortByInstanceZIndex);
		this.is_sorted = true;
	};
	
	RenderCell_.prototype.reset = function ()
	{
		cr.clearArray(this.objects);
		this.is_sorted = true;
		this.pending_removal.clear();
		this.any_pending_removal = false;
	};
	
	RenderCell_.prototype.dump = function (result)
	{
		// append the updated, sorted object list to the result array
		this.flush_pending();
		this.ensure_sorted();
		
		// don't append anything if the list is empty, just creates more merge work
		if (this.objects.length)
			result.push(this.objects);
	};
	
	cr.RenderCell = RenderCell_;
	
	var fxNames = [ "lighter",
					"xor",
					"copy",
					"destination-over",
					"source-in",
					"destination-in",
					"source-out",
					"destination-out",
					"source-atop",
					"destination-atop"];

	cr.effectToCompositeOp = function(effect)
	{
		// (none) = source-over
		if (effect <= 0 || effect >= 11)
			return "source-over";
		
		return fxNames[effect - 1];	// not including "none" so offset by 1
	};
	
	cr.setGLBlend = function(this_, effect, gl)
	{
		if (!gl)
			return;
			
		// default alpha blend
		this_.srcBlend = gl.ONE;
		this_.destBlend = gl.ONE_MINUS_SRC_ALPHA;
		
		switch (effect) {
		case 1:		// lighter (additive)
			this_.srcBlend = gl.ONE;
			this_.destBlend = gl.ONE;
			break;
		case 2:		// xor
			break;	// todo
		case 3:		// copy
			this_.srcBlend = gl.ONE;
			this_.destBlend = gl.ZERO;
			break;
		case 4:		// destination-over
			this_.srcBlend = gl.ONE_MINUS_DST_ALPHA;
			this_.destBlend = gl.ONE;
			break;
		case 5:		// source-in
			this_.srcBlend = gl.DST_ALPHA;
			this_.destBlend = gl.ZERO;
			break;
		case 6:		// destination-in
			this_.srcBlend = gl.ZERO;
			this_.destBlend = gl.SRC_ALPHA;
			break;
		case 7:		// source-out
			this_.srcBlend = gl.ONE_MINUS_DST_ALPHA;
			this_.destBlend = gl.ZERO;
			break;
		case 8:		// destination-out
			this_.srcBlend = gl.ZERO;
			this_.destBlend = gl.ONE_MINUS_SRC_ALPHA;
			break;
		case 9:		// source-atop
			this_.srcBlend = gl.DST_ALPHA;
			this_.destBlend = gl.ONE_MINUS_SRC_ALPHA;
			break;
		case 10:	// destination-atop
			this_.srcBlend = gl.ONE_MINUS_DST_ALPHA;
			this_.destBlend = gl.SRC_ALPHA;
			break;
		}	
	};
	
	cr.round6dp = function (x)
	{
		return Math.round(x * 1000000) / 1000000;
	};
	
	// Compare two strings case insensitively. Use localeCompare where supported to avoid
	// creating garbage by using toLowerCase (which return new strings).
	/*
	var localeCompare_options = {
		"usage": "search",
		"sensitivity": "accent"
	};
	
	var has_localeCompare = !!"a".localeCompare;
	var localeCompare_works1 = (has_localeCompare && "a".localeCompare("A", undefined, localeCompare_options) === 0);
	var localeCompare_works2 = (has_localeCompare && "a".localeCompare("á", undefined, localeCompare_options) !== 0);
	var supports_localeCompare = (has_localeCompare && localeCompare_works1 && localeCompare_works2);
	*/
	
	cr.equals_nocase = function (a, b)
	{
		if (typeof a !== "string" || typeof b !== "string")
			return false;
			
		// Try to skip the possibly garbage-creating or slow checks lower down
		// by returning false early if different lengths or true early if the strings
		// are identical (no case conversion needed)
		if (a.length !== b.length)
			return false;
		if (a === b)
			return true;
		
		// Otherwise do an actual case-insensitive check
		/*
		if (supports_localeCompare)
		{
			return (a.localeCompare(b, undefined, localeCompare_options) === 0);
		}
		else
		{
		*/
			// Can't avoid creating garbage
			return a.toLowerCase() === b.toLowerCase();
		//}
	};
	
	// Convert CSS property to camel case, e.g. "font-size" -> "fontSize". Useful for writing JS styles from CSS properties.
	cr.cssToCamelCase = function (str)
	{
		var ret = "";
		var i, len, c;
		var isAfterHyphen = false;
		for (i = 0, len = str.length; i < len; ++i)
		{
			c = str.charAt(i);
			
			if (c === "-")
			{
				isAfterHyphen = true;
			}
			else
			{
				if (isAfterHyphen)
				{
					ret += c.toUpperCase();
					isAfterHyphen = false;
				}
				else
				{
					ret += c;
				}
			}
		}
		
		return ret;
	};
	
	// Returns true if input event (e.g. touch) is aimed at canvas; false if somewhere else
	// e.g. an input control.
	cr.isCanvasInputEvent = function (e)
	{
		var target = e.target;
		
		if (!target)
			return true;
		
		if (target === document || target === window)
			return true;
		
		if (document && document.body && target === document.body)
			return true;
		
		if (cr.equals_nocase(target.tagName, "canvas"))
			return true;
		
		return false;
	};
	
}());

// glwrap.js
// ECMAScript 5 strict mode

// gl-matrix 1.0.1 - https://github.com/toji/gl-matrix/blob/master/LICENSE.md
var MatrixArray=typeof Float32Array!=="undefined"?Float32Array:Array,glMatrixArrayType=MatrixArray,vec3={},mat3={},mat4={},quat4={};vec3.create=function(a){var b=new MatrixArray(3);a&&(b[0]=a[0],b[1]=a[1],b[2]=a[2]);return b};vec3.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];return b};vec3.add=function(a,b,c){if(!c||a===c)return a[0]+=b[0],a[1]+=b[1],a[2]+=b[2],a;c[0]=a[0]+b[0];c[1]=a[1]+b[1];c[2]=a[2]+b[2];return c};
vec3.subtract=function(a,b,c){if(!c||a===c)return a[0]-=b[0],a[1]-=b[1],a[2]-=b[2],a;c[0]=a[0]-b[0];c[1]=a[1]-b[1];c[2]=a[2]-b[2];return c};vec3.negate=function(a,b){b||(b=a);b[0]=-a[0];b[1]=-a[1];b[2]=-a[2];return b};vec3.scale=function(a,b,c){if(!c||a===c)return a[0]*=b,a[1]*=b,a[2]*=b,a;c[0]=a[0]*b;c[1]=a[1]*b;c[2]=a[2]*b;return c};
vec3.normalize=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=Math.sqrt(c*c+d*d+e*e);if(g){if(g===1)return b[0]=c,b[1]=d,b[2]=e,b}else return b[0]=0,b[1]=0,b[2]=0,b;g=1/g;b[0]=c*g;b[1]=d*g;b[2]=e*g;return b};vec3.cross=function(a,b,c){c||(c=a);var d=a[0],e=a[1],a=a[2],g=b[0],f=b[1],b=b[2];c[0]=e*b-a*f;c[1]=a*g-d*b;c[2]=d*f-e*g;return c};vec3.length=function(a){var b=a[0],c=a[1],a=a[2];return Math.sqrt(b*b+c*c+a*a)};vec3.dot=function(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]};
vec3.direction=function(a,b,c){c||(c=a);var d=a[0]-b[0],e=a[1]-b[1],a=a[2]-b[2],b=Math.sqrt(d*d+e*e+a*a);if(!b)return c[0]=0,c[1]=0,c[2]=0,c;b=1/b;c[0]=d*b;c[1]=e*b;c[2]=a*b;return c};vec3.lerp=function(a,b,c,d){d||(d=a);d[0]=a[0]+c*(b[0]-a[0]);d[1]=a[1]+c*(b[1]-a[1]);d[2]=a[2]+c*(b[2]-a[2]);return d};vec3.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+"]"};
mat3.create=function(a){var b=new MatrixArray(9);a&&(b[0]=a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3],b[4]=a[4],b[5]=a[5],b[6]=a[6],b[7]=a[7],b[8]=a[8]);return b};mat3.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];return b};mat3.identity=function(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=1;a[5]=0;a[6]=0;a[7]=0;a[8]=1;return a};
mat3.transpose=function(a,b){if(!b||a===b){var c=a[1],d=a[2],e=a[5];a[1]=a[3];a[2]=a[6];a[3]=c;a[5]=a[7];a[6]=d;a[7]=e;return a}b[0]=a[0];b[1]=a[3];b[2]=a[6];b[3]=a[1];b[4]=a[4];b[5]=a[7];b[6]=a[2];b[7]=a[5];b[8]=a[8];return b};mat3.toMat4=function(a,b){b||(b=mat4.create());b[15]=1;b[14]=0;b[13]=0;b[12]=0;b[11]=0;b[10]=a[8];b[9]=a[7];b[8]=a[6];b[7]=0;b[6]=a[5];b[5]=a[4];b[4]=a[3];b[3]=0;b[2]=a[2];b[1]=a[1];b[0]=a[0];return b};
mat3.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+", "+a[4]+", "+a[5]+", "+a[6]+", "+a[7]+", "+a[8]+"]"};mat4.create=function(a){var b=new MatrixArray(16);a&&(b[0]=a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3],b[4]=a[4],b[5]=a[5],b[6]=a[6],b[7]=a[7],b[8]=a[8],b[9]=a[9],b[10]=a[10],b[11]=a[11],b[12]=a[12],b[13]=a[13],b[14]=a[14],b[15]=a[15]);return b};
mat4.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=a[12];b[13]=a[13];b[14]=a[14];b[15]=a[15];return b};mat4.identity=function(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=0;a[5]=1;a[6]=0;a[7]=0;a[8]=0;a[9]=0;a[10]=1;a[11]=0;a[12]=0;a[13]=0;a[14]=0;a[15]=1;return a};
mat4.transpose=function(a,b){if(!b||a===b){var c=a[1],d=a[2],e=a[3],g=a[6],f=a[7],h=a[11];a[1]=a[4];a[2]=a[8];a[3]=a[12];a[4]=c;a[6]=a[9];a[7]=a[13];a[8]=d;a[9]=g;a[11]=a[14];a[12]=e;a[13]=f;a[14]=h;return a}b[0]=a[0];b[1]=a[4];b[2]=a[8];b[3]=a[12];b[4]=a[1];b[5]=a[5];b[6]=a[9];b[7]=a[13];b[8]=a[2];b[9]=a[6];b[10]=a[10];b[11]=a[14];b[12]=a[3];b[13]=a[7];b[14]=a[11];b[15]=a[15];return b};
mat4.determinant=function(a){var b=a[0],c=a[1],d=a[2],e=a[3],g=a[4],f=a[5],h=a[6],i=a[7],j=a[8],k=a[9],l=a[10],n=a[11],o=a[12],m=a[13],p=a[14],a=a[15];return o*k*h*e-j*m*h*e-o*f*l*e+g*m*l*e+j*f*p*e-g*k*p*e-o*k*d*i+j*m*d*i+o*c*l*i-b*m*l*i-j*c*p*i+b*k*p*i+o*f*d*n-g*m*d*n-o*c*h*n+b*m*h*n+g*c*p*n-b*f*p*n-j*f*d*a+g*k*d*a+j*c*h*a-b*k*h*a-g*c*l*a+b*f*l*a};
mat4.inverse=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=a[3],f=a[4],h=a[5],i=a[6],j=a[7],k=a[8],l=a[9],n=a[10],o=a[11],m=a[12],p=a[13],r=a[14],s=a[15],A=c*h-d*f,B=c*i-e*f,t=c*j-g*f,u=d*i-e*h,v=d*j-g*h,w=e*j-g*i,x=k*p-l*m,y=k*r-n*m,z=k*s-o*m,C=l*r-n*p,D=l*s-o*p,E=n*s-o*r,q=1/(A*E-B*D+t*C+u*z-v*y+w*x);b[0]=(h*E-i*D+j*C)*q;b[1]=(-d*E+e*D-g*C)*q;b[2]=(p*w-r*v+s*u)*q;b[3]=(-l*w+n*v-o*u)*q;b[4]=(-f*E+i*z-j*y)*q;b[5]=(c*E-e*z+g*y)*q;b[6]=(-m*w+r*t-s*B)*q;b[7]=(k*w-n*t+o*B)*q;b[8]=(f*D-h*z+j*x)*q;
b[9]=(-c*D+d*z-g*x)*q;b[10]=(m*v-p*t+s*A)*q;b[11]=(-k*v+l*t-o*A)*q;b[12]=(-f*C+h*y-i*x)*q;b[13]=(c*C-d*y+e*x)*q;b[14]=(-m*u+p*B-r*A)*q;b[15]=(k*u-l*B+n*A)*q;return b};mat4.toRotationMat=function(a,b){b||(b=mat4.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];b[4]=a[4];b[5]=a[5];b[6]=a[6];b[7]=a[7];b[8]=a[8];b[9]=a[9];b[10]=a[10];b[11]=a[11];b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b};
mat4.toMat3=function(a,b){b||(b=mat3.create());b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[4];b[4]=a[5];b[5]=a[6];b[6]=a[8];b[7]=a[9];b[8]=a[10];return b};mat4.toInverseMat3=function(a,b){var c=a[0],d=a[1],e=a[2],g=a[4],f=a[5],h=a[6],i=a[8],j=a[9],k=a[10],l=k*f-h*j,n=-k*g+h*i,o=j*g-f*i,m=c*l+d*n+e*o;if(!m)return null;m=1/m;b||(b=mat3.create());b[0]=l*m;b[1]=(-k*d+e*j)*m;b[2]=(h*d-e*f)*m;b[3]=n*m;b[4]=(k*c-e*i)*m;b[5]=(-h*c+e*g)*m;b[6]=o*m;b[7]=(-j*c+d*i)*m;b[8]=(f*c-d*g)*m;return b};
mat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],g=a[2],f=a[3],h=a[4],i=a[5],j=a[6],k=a[7],l=a[8],n=a[9],o=a[10],m=a[11],p=a[12],r=a[13],s=a[14],a=a[15],A=b[0],B=b[1],t=b[2],u=b[3],v=b[4],w=b[5],x=b[6],y=b[7],z=b[8],C=b[9],D=b[10],E=b[11],q=b[12],F=b[13],G=b[14],b=b[15];c[0]=A*d+B*h+t*l+u*p;c[1]=A*e+B*i+t*n+u*r;c[2]=A*g+B*j+t*o+u*s;c[3]=A*f+B*k+t*m+u*a;c[4]=v*d+w*h+x*l+y*p;c[5]=v*e+w*i+x*n+y*r;c[6]=v*g+w*j+x*o+y*s;c[7]=v*f+w*k+x*m+y*a;c[8]=z*d+C*h+D*l+E*p;c[9]=z*e+C*i+D*n+E*r;c[10]=z*g+C*
j+D*o+E*s;c[11]=z*f+C*k+D*m+E*a;c[12]=q*d+F*h+G*l+b*p;c[13]=q*e+F*i+G*n+b*r;c[14]=q*g+F*j+G*o+b*s;c[15]=q*f+F*k+G*m+b*a;return c};mat4.multiplyVec3=function(a,b,c){c||(c=b);var d=b[0],e=b[1],b=b[2];c[0]=a[0]*d+a[4]*e+a[8]*b+a[12];c[1]=a[1]*d+a[5]*e+a[9]*b+a[13];c[2]=a[2]*d+a[6]*e+a[10]*b+a[14];return c};
mat4.multiplyVec4=function(a,b,c){c||(c=b);var d=b[0],e=b[1],g=b[2],b=b[3];c[0]=a[0]*d+a[4]*e+a[8]*g+a[12]*b;c[1]=a[1]*d+a[5]*e+a[9]*g+a[13]*b;c[2]=a[2]*d+a[6]*e+a[10]*g+a[14]*b;c[3]=a[3]*d+a[7]*e+a[11]*g+a[15]*b;return c};
mat4.translate=function(a,b,c){var d=b[0],e=b[1],b=b[2],g,f,h,i,j,k,l,n,o,m,p,r;if(!c||a===c)return a[12]=a[0]*d+a[4]*e+a[8]*b+a[12],a[13]=a[1]*d+a[5]*e+a[9]*b+a[13],a[14]=a[2]*d+a[6]*e+a[10]*b+a[14],a[15]=a[3]*d+a[7]*e+a[11]*b+a[15],a;g=a[0];f=a[1];h=a[2];i=a[3];j=a[4];k=a[5];l=a[6];n=a[7];o=a[8];m=a[9];p=a[10];r=a[11];c[0]=g;c[1]=f;c[2]=h;c[3]=i;c[4]=j;c[5]=k;c[6]=l;c[7]=n;c[8]=o;c[9]=m;c[10]=p;c[11]=r;c[12]=g*d+j*e+o*b+a[12];c[13]=f*d+k*e+m*b+a[13];c[14]=h*d+l*e+p*b+a[14];c[15]=i*d+n*e+r*b+a[15];
return c};mat4.scale=function(a,b,c){var d=b[0],e=b[1],b=b[2];if(!c||a===c)return a[0]*=d,a[1]*=d,a[2]*=d,a[3]*=d,a[4]*=e,a[5]*=e,a[6]*=e,a[7]*=e,a[8]*=b,a[9]*=b,a[10]*=b,a[11]*=b,a;c[0]=a[0]*d;c[1]=a[1]*d;c[2]=a[2]*d;c[3]=a[3]*d;c[4]=a[4]*e;c[5]=a[5]*e;c[6]=a[6]*e;c[7]=a[7]*e;c[8]=a[8]*b;c[9]=a[9]*b;c[10]=a[10]*b;c[11]=a[11]*b;c[12]=a[12];c[13]=a[13];c[14]=a[14];c[15]=a[15];return c};
mat4.rotate=function(a,b,c,d){var e=c[0],g=c[1],c=c[2],f=Math.sqrt(e*e+g*g+c*c),h,i,j,k,l,n,o,m,p,r,s,A,B,t,u,v,w,x,y,z;if(!f)return null;f!==1&&(f=1/f,e*=f,g*=f,c*=f);h=Math.sin(b);i=Math.cos(b);j=1-i;b=a[0];f=a[1];k=a[2];l=a[3];n=a[4];o=a[5];m=a[6];p=a[7];r=a[8];s=a[9];A=a[10];B=a[11];t=e*e*j+i;u=g*e*j+c*h;v=c*e*j-g*h;w=e*g*j-c*h;x=g*g*j+i;y=c*g*j+e*h;z=e*c*j+g*h;e=g*c*j-e*h;g=c*c*j+i;d?a!==d&&(d[12]=a[12],d[13]=a[13],d[14]=a[14],d[15]=a[15]):d=a;d[0]=b*t+n*u+r*v;d[1]=f*t+o*u+s*v;d[2]=k*t+m*u+A*
v;d[3]=l*t+p*u+B*v;d[4]=b*w+n*x+r*y;d[5]=f*w+o*x+s*y;d[6]=k*w+m*x+A*y;d[7]=l*w+p*x+B*y;d[8]=b*z+n*e+r*g;d[9]=f*z+o*e+s*g;d[10]=k*z+m*e+A*g;d[11]=l*z+p*e+B*g;return d};mat4.rotateX=function(a,b,c){var d=Math.sin(b),b=Math.cos(b),e=a[4],g=a[5],f=a[6],h=a[7],i=a[8],j=a[9],k=a[10],l=a[11];c?a!==c&&(c[0]=a[0],c[1]=a[1],c[2]=a[2],c[3]=a[3],c[12]=a[12],c[13]=a[13],c[14]=a[14],c[15]=a[15]):c=a;c[4]=e*b+i*d;c[5]=g*b+j*d;c[6]=f*b+k*d;c[7]=h*b+l*d;c[8]=e*-d+i*b;c[9]=g*-d+j*b;c[10]=f*-d+k*b;c[11]=h*-d+l*b;return c};
mat4.rotateY=function(a,b,c){var d=Math.sin(b),b=Math.cos(b),e=a[0],g=a[1],f=a[2],h=a[3],i=a[8],j=a[9],k=a[10],l=a[11];c?a!==c&&(c[4]=a[4],c[5]=a[5],c[6]=a[6],c[7]=a[7],c[12]=a[12],c[13]=a[13],c[14]=a[14],c[15]=a[15]):c=a;c[0]=e*b+i*-d;c[1]=g*b+j*-d;c[2]=f*b+k*-d;c[3]=h*b+l*-d;c[8]=e*d+i*b;c[9]=g*d+j*b;c[10]=f*d+k*b;c[11]=h*d+l*b;return c};
mat4.rotateZ=function(a,b,c){var d=Math.sin(b),b=Math.cos(b),e=a[0],g=a[1],f=a[2],h=a[3],i=a[4],j=a[5],k=a[6],l=a[7];c?a!==c&&(c[8]=a[8],c[9]=a[9],c[10]=a[10],c[11]=a[11],c[12]=a[12],c[13]=a[13],c[14]=a[14],c[15]=a[15]):c=a;c[0]=e*b+i*d;c[1]=g*b+j*d;c[2]=f*b+k*d;c[3]=h*b+l*d;c[4]=e*-d+i*b;c[5]=g*-d+j*b;c[6]=f*-d+k*b;c[7]=h*-d+l*b;return c};
mat4.frustum=function(a,b,c,d,e,g,f){f||(f=mat4.create());var h=b-a,i=d-c,j=g-e;f[0]=e*2/h;f[1]=0;f[2]=0;f[3]=0;f[4]=0;f[5]=e*2/i;f[6]=0;f[7]=0;f[8]=(b+a)/h;f[9]=(d+c)/i;f[10]=-(g+e)/j;f[11]=-1;f[12]=0;f[13]=0;f[14]=-(g*e*2)/j;f[15]=0;return f};mat4.perspective=function(a,b,c,d,e){a=c*Math.tan(a*Math.PI/360);b*=a;return mat4.frustum(-b,b,-a,a,c,d,e)};
mat4.ortho=function(a,b,c,d,e,g,f){f||(f=mat4.create());var h=b-a,i=d-c,j=g-e;f[0]=2/h;f[1]=0;f[2]=0;f[3]=0;f[4]=0;f[5]=2/i;f[6]=0;f[7]=0;f[8]=0;f[9]=0;f[10]=-2/j;f[11]=0;f[12]=-(a+b)/h;f[13]=-(d+c)/i;f[14]=-(g+e)/j;f[15]=1;return f};
mat4.lookAt=function(a,b,c,d){d||(d=mat4.create());var e,g,f,h,i,j,k,l,n=a[0],o=a[1],a=a[2];g=c[0];f=c[1];e=c[2];c=b[1];j=b[2];if(n===b[0]&&o===c&&a===j)return mat4.identity(d);c=n-b[0];j=o-b[1];k=a-b[2];l=1/Math.sqrt(c*c+j*j+k*k);c*=l;j*=l;k*=l;b=f*k-e*j;e=e*c-g*k;g=g*j-f*c;(l=Math.sqrt(b*b+e*e+g*g))?(l=1/l,b*=l,e*=l,g*=l):g=e=b=0;f=j*g-k*e;h=k*b-c*g;i=c*e-j*b;(l=Math.sqrt(f*f+h*h+i*i))?(l=1/l,f*=l,h*=l,i*=l):i=h=f=0;d[0]=b;d[1]=f;d[2]=c;d[3]=0;d[4]=e;d[5]=h;d[6]=j;d[7]=0;d[8]=g;d[9]=i;d[10]=k;d[11]=
0;d[12]=-(b*n+e*o+g*a);d[13]=-(f*n+h*o+i*a);d[14]=-(c*n+j*o+k*a);d[15]=1;return d};mat4.fromRotationTranslation=function(a,b,c){c||(c=mat4.create());var d=a[0],e=a[1],g=a[2],f=a[3],h=d+d,i=e+e,j=g+g,a=d*h,k=d*i;d*=j;var l=e*i;e*=j;g*=j;h*=f;i*=f;f*=j;c[0]=1-(l+g);c[1]=k+f;c[2]=d-i;c[3]=0;c[4]=k-f;c[5]=1-(a+g);c[6]=e+h;c[7]=0;c[8]=d+i;c[9]=e-h;c[10]=1-(a+l);c[11]=0;c[12]=b[0];c[13]=b[1];c[14]=b[2];c[15]=1;return c};
mat4.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+", "+a[4]+", "+a[5]+", "+a[6]+", "+a[7]+", "+a[8]+", "+a[9]+", "+a[10]+", "+a[11]+", "+a[12]+", "+a[13]+", "+a[14]+", "+a[15]+"]"};quat4.create=function(a){var b=new MatrixArray(4);a&&(b[0]=a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3]);return b};quat4.set=function(a,b){b[0]=a[0];b[1]=a[1];b[2]=a[2];b[3]=a[3];return b};
quat4.calculateW=function(a,b){var c=a[0],d=a[1],e=a[2];if(!b||a===b)return a[3]=-Math.sqrt(Math.abs(1-c*c-d*d-e*e)),a;b[0]=c;b[1]=d;b[2]=e;b[3]=-Math.sqrt(Math.abs(1-c*c-d*d-e*e));return b};quat4.inverse=function(a,b){if(!b||a===b)return a[0]*=-1,a[1]*=-1,a[2]*=-1,a;b[0]=-a[0];b[1]=-a[1];b[2]=-a[2];b[3]=a[3];return b};quat4.length=function(a){var b=a[0],c=a[1],d=a[2],a=a[3];return Math.sqrt(b*b+c*c+d*d+a*a)};
quat4.normalize=function(a,b){b||(b=a);var c=a[0],d=a[1],e=a[2],g=a[3],f=Math.sqrt(c*c+d*d+e*e+g*g);if(f===0)return b[0]=0,b[1]=0,b[2]=0,b[3]=0,b;f=1/f;b[0]=c*f;b[1]=d*f;b[2]=e*f;b[3]=g*f;return b};quat4.multiply=function(a,b,c){c||(c=a);var d=a[0],e=a[1],g=a[2],a=a[3],f=b[0],h=b[1],i=b[2],b=b[3];c[0]=d*b+a*f+e*i-g*h;c[1]=e*b+a*h+g*f-d*i;c[2]=g*b+a*i+d*h-e*f;c[3]=a*b-d*f-e*h-g*i;return c};
quat4.multiplyVec3=function(a,b,c){c||(c=b);var d=b[0],e=b[1],g=b[2],b=a[0],f=a[1],h=a[2],a=a[3],i=a*d+f*g-h*e,j=a*e+h*d-b*g,k=a*g+b*e-f*d,d=-b*d-f*e-h*g;c[0]=i*a+d*-b+j*-h-k*-f;c[1]=j*a+d*-f+k*-b-i*-h;c[2]=k*a+d*-h+i*-f-j*-b;return c};quat4.toMat3=function(a,b){b||(b=mat3.create());var c=a[0],d=a[1],e=a[2],g=a[3],f=c+c,h=d+d,i=e+e,j=c*f,k=c*h;c*=i;var l=d*h;d*=i;e*=i;f*=g;h*=g;g*=i;b[0]=1-(l+e);b[1]=k+g;b[2]=c-h;b[3]=k-g;b[4]=1-(j+e);b[5]=d+f;b[6]=c+h;b[7]=d-f;b[8]=1-(j+l);return b};
quat4.toMat4=function(a,b){b||(b=mat4.create());var c=a[0],d=a[1],e=a[2],g=a[3],f=c+c,h=d+d,i=e+e,j=c*f,k=c*h;c*=i;var l=d*h;d*=i;e*=i;f*=g;h*=g;g*=i;b[0]=1-(l+e);b[1]=k+g;b[2]=c-h;b[3]=0;b[4]=k-g;b[5]=1-(j+e);b[6]=d+f;b[7]=0;b[8]=c+h;b[9]=d-f;b[10]=1-(j+l);b[11]=0;b[12]=0;b[13]=0;b[14]=0;b[15]=1;return b};
quat4.slerp=function(a,b,c,d){d||(d=a);var e=a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3],g,f;if(Math.abs(e)>=1)return d!==a&&(d[0]=a[0],d[1]=a[1],d[2]=a[2],d[3]=a[3]),d;g=Math.acos(e);f=Math.sqrt(1-e*e);if(Math.abs(f)<0.001)return d[0]=a[0]*0.5+b[0]*0.5,d[1]=a[1]*0.5+b[1]*0.5,d[2]=a[2]*0.5+b[2]*0.5,d[3]=a[3]*0.5+b[3]*0.5,d;e=Math.sin((1-c)*g)/f;c=Math.sin(c*g)/f;d[0]=a[0]*e+b[0]*c;d[1]=a[1]*e+b[1]*c;d[2]=a[2]*e+b[2]*c;d[3]=a[3]*e+b[3]*c;return d};
quat4.str=function(a){return"["+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+"]"};

(function()
{
	var MAX_VERTICES = 8000;						// equates to 2500 objects being drawn
	var MAX_INDICES = (MAX_VERTICES / 2) * 3;		// 6 indices for every 4 vertices
	var MAX_POINTS = 8000;
	
	var MULTI_BUFFERS = 4;							// cycle 4 buffers to try and avoid blocking
	
	var BATCH_NULL = 0;
	var BATCH_QUAD = 1;
	var BATCH_SETTEXTURE = 2;
	var BATCH_SETOPACITY = 3;
	var BATCH_SETBLEND = 4;
	var BATCH_UPDATEMODELVIEW = 5;
	var BATCH_RENDERTOTEXTURE = 6;
	var BATCH_CLEAR = 7;
	var BATCH_POINTS = 8;
	var BATCH_SETPROGRAM = 9;
	var BATCH_SETPROGRAMPARAMETERS = 10;
	var BATCH_SETTEXTURE1 = 11;
	var BATCH_SETCOLOR = 12;
	var BATCH_SETDEPTHTEST = 13;
	var BATCH_SETEARLYZMODE = 14;
	
	// for testing lost contexts
	/*
	var lose_ext = null;
	
	window.lose_context = function ()
	{
		if (!lose_ext)
		{
			console.log("WEBGL_lose_context not supported");
			return;
		}
		
		lose_ext.loseContext();
	};
	
	window.restore_context = function ()
	{
		if (!lose_ext)
		{
			console.log("WEBGL_lose_context not supported");
			return;
		}
		
		lose_ext.restoreContext();
	};
	*/
	
	var tempMat4 = mat4.create();
	
	function GLWrap_(gl, isMobile, enableFrontToBack)
	{
		// Work around crappiness in IE's WebGL implementation
		this.isIE = /msie/i.test(navigator.userAgent) || /trident/i.test(navigator.userAgent);
		
		this.width = 0;		// not yet known, wait for call to setSize()
		this.height = 0;
		
		this.enableFrontToBack = !!enableFrontToBack;
		this.isEarlyZPass = false;
		this.isBatchInEarlyZPass = false;
		this.currentZ = 0;
		this.zNear = 1;
		this.zFar = 1000;
		this.zIncrement = ((this.zFar - this.zNear) / 32768);
		
		// For calculating incrementing Z positions
		this.zA = this.zFar / (this.zFar - this.zNear);
		this.zB = this.zFar * this.zNear / (this.zNear - this.zFar);
		this.kzA = 65536 * this.zA;
		this.kzB = 65536 * this.zB;

		this.cam = vec3.create([0, 0, 100]);			// camera position
		this.look = vec3.create([0, 0, 0]);				// lookat position
		this.up = vec3.create([0, 1, 0]);				// up vector
		this.worldScale = vec3.create([1, 1, 1]);		// world scaling factor
		
		this.enable_mipmaps = true;
		
		this.matP = mat4.create();						// perspective matrix
		this.matMV = mat4.create();						// model view matrix
		this.lastMV = mat4.create();
		this.currentMV = mat4.create();
		
		this.gl = gl;
		
		// Identify WebGL version number
		this.version = (this.gl.getParameter(this.gl.VERSION).indexOf("WebGL 2") === 0 ? 2 : 1);

		// Get EXT_disjoint_timer_query or EXT_disjoint_timer_query_webgl2 depending on the WebGL version
		this.timerExt = null;
		this.isTiming = false;

		if (this.version === 2)
		{
			// Note Firefox with WebGL 2 still returns the WebGL 1 extension
			this.timerExt = this.gl.getExtension("EXT_disjoint_timer_query_webgl2") ||
							this.gl.getExtension("EXT_disjoint_timer_query");
		}
		else
		{
			this.timerExt = this.gl.getExtension("EXT_disjoint_timer_query");
		}

		this.frameNumber = 0;		// increments every present()
		this.timerResults = [];		// array of last timer queries { frameNumber, query, result }
		
		this.initState();
		
		// for testing lost contexts
		//lose_ext = this.gl.getExtension("WEBGL_lose_context");
	};
	
	GLWrap_.prototype.initState = function ()
	{
		var gl = this.gl;
		var i, len;
		
		// For filtering redundant setXXX() calls
		this.lastOpacity = 1;
		this.lastTexture0 = null;			// last bound to TEXTURE0
		this.lastTexture1 = null;			// last bound to TEXTURE1
		
		this.currentOpacity = 1;
		
		// One-time creation initialisation
		gl.clearColor(0, 0, 0, 0);
		gl.clear(gl.COLOR_BUFFER_BIT);
		gl.enable(gl.BLEND);
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
		gl.disable(gl.CULL_FACE);
		gl.disable(gl.STENCIL_TEST);
		gl.disable(gl.DITHER);
		
		if (this.enableFrontToBack)
		{
			gl.enable(gl.DEPTH_TEST);
			gl.depthFunc(gl.LEQUAL);
		}
		else
		{
			gl.disable(gl.DEPTH_TEST);
		}
		
		this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
		
		this.lastSrcBlend = gl.ONE;
		this.lastDestBlend = gl.ONE_MINUS_SRC_ALPHA;
		
		// Typed arrays for buffer data
		this.vertexData = new Float32Array(MAX_VERTICES * (this.enableFrontToBack ? 3 : 2));
		this.texcoordData = new Float32Array(MAX_VERTICES * 2);
		this.pointData = new Float32Array(MAX_POINTS * 4);
		
		// Create buffers
		this.pointBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, this.pointBuffer);
		gl.bufferData(gl.ARRAY_BUFFER, this.pointData.byteLength, gl.DYNAMIC_DRAW);
		
		this.vertexBuffers = new Array(MULTI_BUFFERS);
		this.texcoordBuffers = new Array(MULTI_BUFFERS);
		
		for (i = 0; i < MULTI_BUFFERS; i++)
		{
			this.vertexBuffers[i] = gl.createBuffer();
			gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffers[i]);
			gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW);
			
			this.texcoordBuffers[i] = gl.createBuffer();
			gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffers[i]);
			gl.bufferData(gl.ARRAY_BUFFER, this.texcoordData.byteLength, gl.DYNAMIC_DRAW);
		}
		
		this.curBuffer = 0;
		
		this.indexBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
		
		// Pre-generate the index data since it never changes
		var indexData = new Uint16Array(MAX_INDICES);
		
		i = 0, len = MAX_INDICES;
		var fv = 0;
		
		while (i < len)
		{
			indexData[i++] = fv;		// top left
			indexData[i++] = fv + 1;	// top right
			indexData[i++] = fv + 2;	// bottom right (first tri)
			indexData[i++] = fv;		// top left
			indexData[i++] = fv + 2;	// bottom right
			indexData[i++] = fv + 3;	// bottom left
			fv += 4;
		}
		
		gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
		
		// pointer in to vertex buffer
		this.vertexPtr = 0;
		this.texPtr = 0;
		
		// pointer in to point buffer
		this.pointPtr = 0;
		
		var fsSource, vsSource;
		
		// Array of all shader programs
		this.shaderPrograms = [];
		
		/////////////////////////////////////////////////////
		// CREATE DEFAULT SHADER PROGRAM
		
		// Create shaders
		fsSource = [
			"varying mediump vec2 vTex;",
			"uniform lowp float opacity;",
			"uniform lowp sampler2D samplerFront;",

			"void main(void) {",
			"	gl_FragColor = texture2D(samplerFront, vTex);",
			"	gl_FragColor *= opacity;",
			"}"
		].join("\n");
		
		if (this.enableFrontToBack)
		{
			// note aPos is a vec3, Z values are used
			vsSource = [
				"attribute highp vec3 aPos;",
				"attribute mediump vec2 aTex;",
				"varying mediump vec2 vTex;",
				"uniform highp mat4 matP;",
				"uniform highp mat4 matMV;",

				"void main(void) {",
				"	gl_Position = matP * matMV * vec4(aPos.x, aPos.y, aPos.z, 1.0);",
				"	vTex = aTex;",
				"}"
			].join("\n");
		}
		else
		{
			// note aPos is a vec2, no Z values are used
			vsSource = [
				"attribute highp vec2 aPos;",
				"attribute mediump vec2 aTex;",
				"varying mediump vec2 vTex;",
				"uniform highp mat4 matP;",
				"uniform highp mat4 matMV;",

				"void main(void) {",
				"	gl_Position = matP * matMV * vec4(aPos.x, aPos.y, 0.0, 1.0);",
				"	vTex = aTex;",
				"}"
			].join("\n");
		}
		
		var shaderProg = this.createShaderProgram({src: fsSource}, vsSource, "<default>");
;
		this.shaderPrograms.push(shaderProg);		// Default shader is always shader 0
		
		// Create points shader program
		fsSource = [
			"uniform mediump sampler2D samplerFront;",
			"varying lowp float opacity;",

			"void main(void) {",
			"	gl_FragColor = texture2D(samplerFront, gl_PointCoord);",
			"	gl_FragColor *= opacity;",
			"}"
		].join("\n");
		
		var pointVsSource = [
			"attribute vec4 aPos;",
			"varying float opacity;",
			"uniform mat4 matP;",
			"uniform mat4 matMV;",

			"void main(void) {",
			"	gl_Position = matP * matMV * vec4(aPos.x, aPos.y, 0.0, 1.0);",
			"	gl_PointSize = aPos.z;",
			"	opacity = aPos.w;",
			"}"
		].join("\n");
		
		shaderProg = this.createShaderProgram({src: fsSource}, pointVsSource, "<point>");
;
		this.shaderPrograms.push(shaderProg);		// Point shader is always shader 1
		
		fsSource = [
			"varying mediump vec2 vTex;",
			"uniform lowp sampler2D samplerFront;",

			"void main(void) {",
			"	if (texture2D(samplerFront, vTex).a < 1.0)",
			"		discard;",						// discarding non-opaque fragments
			"}"
		].join("\n");

		var shaderProg = this.createShaderProgram({src: fsSource}, vsSource, "<earlyz>");
;
		this.shaderPrograms.push(shaderProg);		// Early-Z shader is always shader 2
		
		fsSource = [
			"uniform lowp vec4 colorFill;",

			"void main(void) {",
			"	gl_FragColor = colorFill;",
			"}"
		].join("\n");

		var shaderProg = this.createShaderProgram({src: fsSource}, vsSource, "<fill>");
;
		this.shaderPrograms.push(shaderProg);		// Fill-color shader is always shader 3
		
		// Create all other shaders bundled with the project
		for (var shader_name in cr.shaders)
		{
			if (cr.shaders.hasOwnProperty(shader_name))
				this.shaderPrograms.push(this.createShaderProgram(cr.shaders[shader_name], vsSource, shader_name));
		}
		
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, null);
		
		// The job batch
		this.batch = [];
		this.batchPtr = 0;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
		this.lastProgram = -1;				// start -1 so first switchProgram can do work
		this.currentProgram = -1;			// current program during batch execution
		this.currentShader = null;
		
		this.fbo = gl.createFramebuffer();
		this.renderToTex = null;
		
		this.depthBuffer = null;
		this.attachedDepthBuffer = false;	// wait until first size call to attach, otherwise it has no storage
		
		if (this.enableFrontToBack)
		{
			this.depthBuffer = gl.createRenderbuffer();
		}

		this.tmpVec3 = vec3.create([0, 0, 0]);
		
;
		var pointsizes = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE);
		this.minPointSize = pointsizes[0];
		this.maxPointSize = pointsizes[1];
		
		// Chrome and Firefox's WebGL implementations (ANGLE) seem to have bugs with very large point sizes
		// on some systems. Cap the max point size at 2048 to work around, forcing the Particles object to
		// fall back to quads for very large particles.
		if (this.maxPointSize > 2048)
			this.maxPointSize = 2048;
		
;
		
	
		this.switchProgram(0);
		
		cr.seal(this);
	};
	
	function GLShaderProgram(gl, shaderProgram, name)
	{
		this.gl = gl;
		this.shaderProgram = shaderProgram;
		this.name = name;
		
		// Get shader variable locations
		this.locAPos = gl.getAttribLocation(shaderProgram, "aPos");
		this.locATex = gl.getAttribLocation(shaderProgram, "aTex");
		
		this.locMatP = gl.getUniformLocation(shaderProgram, "matP");
		this.locMatMV = gl.getUniformLocation(shaderProgram, "matMV");
		this.locOpacity = gl.getUniformLocation(shaderProgram, "opacity");
		this.locColorFill = gl.getUniformLocation(shaderProgram, "colorFill");
		this.locSamplerFront = gl.getUniformLocation(shaderProgram, "samplerFront");
		
		// Optional parameters
		this.locSamplerBack = gl.getUniformLocation(shaderProgram, "samplerBack");
		this.locSrcStart = gl.getUniformLocation(shaderProgram, "srcStart");
		this.locSrcEnd = gl.getUniformLocation(shaderProgram, "srcEnd");
		this.locSrcOriginStart = gl.getUniformLocation(shaderProgram, "srcOriginStart");
		this.locSrcOriginEnd = gl.getUniformLocation(shaderProgram, "srcOriginEnd");
		this.locLayoutStart = gl.getUniformLocation(shaderProgram, "layoutStart");
		this.locLayoutEnd = gl.getUniformLocation(shaderProgram, "layoutEnd");
		this.locDestStart = gl.getUniformLocation(shaderProgram, "destStart");
		this.locDestEnd = gl.getUniformLocation(shaderProgram, "destEnd");
		this.locSeconds = gl.getUniformLocation(shaderProgram, "seconds");
		this.locPixelSize = gl.getUniformLocation(shaderProgram, "pixelSize");
		this.locLayerScale = gl.getUniformLocation(shaderProgram, "layerScale");
		this.locLayerAngle = gl.getUniformLocation(shaderProgram, "layerAngle");
		
		this.hasAnyOptionalUniforms = !!(this.locPixelSize || this.locSeconds || this.locSamplerBack || this.locSrcStart || this.locSrcEnd || this.locSrcOriginStart || this.locSrcOriginEnd || this.locLayoutStart || this.locLayoutEnd || this.locDestStart || this.locDestEnd || this.locLayerScale || this.locLayerAngle);
		
		// Store the last set shader parameters in JS, and only assign them when they change,
		// since these GL calls are expensive.
		this.lpPixelWidth = -999;		// set to something unlikely so never counts as cached on first set
		this.lpPixelHeight = -999;
		this.lpOpacity = 1;
		this.lpSrcStartX = 0.0;
		this.lpSrcStartY = 0.0;
		this.lpSrcEndX = 0.0;
		this.lpSrcEndY = 0.0;
		this.lpSrcOriginStartX = 0.0;
		this.lpSrcOriginStartY = 0.0;
		this.lpSrcOriginEndX = 0.0;
		this.lpSrcOriginEndY = 0.0;
		this.lpLayoutStartX = 0.0;
		this.lpLayoutStartY = 0.0;
		this.lpLayoutEndX = 0.0;
		this.lpLayoutEndY = 0.0;
		this.lpDestStartX = 0.0;
		this.lpDestStartY = 0.0;
		this.lpDestEndX = 1.0;
		this.lpDestEndY = 1.0;
		this.lpLayerScale = 1.0;
		this.lpLayerAngle = 0.0;
		this.lpSeconds = 0.0;
		this.lastCustomParams = [];
		this.lpMatMV = mat4.create();
		
		// Assign defaults
		if (this.locOpacity)
			gl.uniform1f(this.locOpacity, 1);
		
		if (this.locColorFill)
			gl.uniform4f(this.locColorFill, 1.0, 1.0, 1.0, 1.0);

		if (this.locSamplerFront)
			gl.uniform1i(this.locSamplerFront, 0);
			
		if (this.locSamplerBack)
			gl.uniform1i(this.locSamplerBack, 1);
		
		if (this.locSrcStart)
			gl.uniform2f(this.locSrcStart, 0.0, 0.0);
		
		if (this.locSrcEnd)
			gl.uniform2f(this.locSrcEnd, 1.0, 1.0);
		
		if (this.locSrcOriginStart)
			gl.uniform2f(this.locSrcOriginStart, 0.0, 0.0);
		
		if (this.locSrcOriginEnd)
			gl.uniform2f(this.locSrcOriginEnd, 0.0, 0.0);
	
		if (this.locLayoutStart)
			gl.uniform2f(this.locLayoutStart, 0.0, 0.0);
		
		if (this.locLayoutEnd)
			gl.uniform2f(this.locLayoutEnd, 0.0, 0.0);
		
		if (this.locDestStart)
			gl.uniform2f(this.locDestStart, 0.0, 0.0);
			
		if (this.locDestEnd)
			gl.uniform2f(this.locDestEnd, 1.0, 1.0);
			
		if (this.locLayerScale)
			gl.uniform1f(this.locLayerScale, 1.0);
		
		if (this.locLayerAngle)
			gl.uniform1f(this.locLayerAngle, 0.0);
		
		if (this.locSeconds)
			gl.uniform1f(this.locSeconds, 0.0);
		
		this.hasCurrentMatMV = false;		// matMV needs updating
	};
	
	function areMat4sEqual(a, b)
	{
		return a[0]===b[0]&&a[1]===b[1]&&a[2]===b[2]&&a[3]===b[3]&&
			   a[4]===b[4]&&a[5]===b[5]&&a[6]===b[6]&&a[7]===b[7]&&
			   a[8]===b[8]&&a[9]===b[9]&&a[10]===b[10]&&a[11]===b[11]&&
			   a[12]===b[12]&&a[13]===b[13]&&a[14]===b[14]&&a[15]===b[15];
	};
	
	GLShaderProgram.prototype.updateMatMV = function (mv)
	{
		if (areMat4sEqual(this.lpMatMV, mv))
			return;		// no change, save the expensive GL call
		
		mat4.set(mv, this.lpMatMV);
		this.gl.uniformMatrix4fv(this.locMatMV, false, mv);
	};
	
	GLWrap_.prototype.createShaderProgram = function(shaderEntry, vsSource, name)
	{
		var gl = this.gl;
		
		// Create fragment shader
		var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
		gl.shaderSource(fragmentShader, shaderEntry.src);
		gl.compileShader(fragmentShader);
		
		if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS))
		{
			// Failed to compile fragment shader
;
			gl.deleteShader(fragmentShader);
			return null;
		}
		
		// Create vertex shader
		// TODO: share vertex shaders where possible?
		var vertexShader = gl.createShader(gl.VERTEX_SHADER);
		gl.shaderSource(vertexShader, vsSource);
		gl.compileShader(vertexShader);
		
		if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS))
		{
			// Failed to compile vertex shader
;
			gl.deleteShader(fragmentShader);
			gl.deleteShader(vertexShader);
			return null;
		}
		
		// Assemble shaders in to shader program
		var shaderProgram = gl.createProgram();
		gl.attachShader(shaderProgram, fragmentShader);
		gl.attachShader(shaderProgram, vertexShader);
		gl.linkProgram(shaderProgram);
		
		if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS))
		{
;
			gl.deleteShader(fragmentShader);
			gl.deleteShader(vertexShader);
			gl.deleteProgram(shaderProgram);
			return null;
		}
		
		gl.useProgram(shaderProgram);
		// not supported in IE and more or less a redundant call anyway
		//gl.validateProgram(shaderProgram);
		
		
		// Shaders no longer necessary; linked in to the program already
		gl.deleteShader(fragmentShader);
		gl.deleteShader(vertexShader);
		
		var ret = new GLShaderProgram(gl, shaderProgram, name);
		
		ret.extendBoxHorizontal = shaderEntry.extendBoxHorizontal || 0;
		ret.extendBoxVertical = shaderEntry.extendBoxVertical || 0;
		ret.crossSampling = !!shaderEntry.crossSampling;
		ret.preservesOpaqueness = !!shaderEntry.preservesOpaqueness;
		ret.animated = !!shaderEntry.animated;
		ret.parameters = shaderEntry.parameters || [];
		
		// Look up all parameter uniform locations
		var i, len;
		for (i = 0, len = ret.parameters.length; i < len; i++)
		{
			ret.parameters[i][1] = gl.getUniformLocation(shaderProgram, ret.parameters[i][0]);
			ret.lastCustomParams.push(0);
			
			// Note does not correctly handle color parameters which are vec3. Leave this and assume uniforms are zero-initialised.
			//gl.uniform1f(ret.parameters[i][1], 0);
		}
		
		cr.seal(ret);
		return ret;
	};
	
	GLWrap_.prototype.getShaderIndex = function(name_)
	{
		var i, len;
		for (i = 0, len = this.shaderPrograms.length; i < len; i++)
		{
			if (this.shaderPrograms[i].name === name_)
				return i;
		}
		
		return -1;
	};
	
	GLWrap_.prototype.project = function (x, y, out)
	{
		var mv = this.matMV;
		var proj = this.matP;
		
		// Based on http://www.opengl.org/wiki/GluProject_and_gluUnProject_code
		var fTempo = [0, 0, 0, 0, 0, 0, 0, 0];
		
		fTempo[0] = mv[0]*x+mv[4]*y+mv[12];
		fTempo[1] = mv[1]*x+mv[5]*y+mv[13];
		fTempo[2] = mv[2]*x+mv[6]*y+mv[14];
		fTempo[3] = mv[3]*x+mv[7]*y+mv[15];

		fTempo[4] = proj[0]*fTempo[0]+proj[4]*fTempo[1]+proj[8]*fTempo[2]+proj[12]*fTempo[3];
		fTempo[5] = proj[1]*fTempo[0]+proj[5]*fTempo[1]+proj[9]*fTempo[2]+proj[13]*fTempo[3];
		fTempo[6] = proj[2]*fTempo[0]+proj[6]*fTempo[1]+proj[10]*fTempo[2]+proj[14]*fTempo[3];
		fTempo[7] = -fTempo[2];
		
		// The result normalizes between -1 and 1
		if(fTempo[7]===0.0)	//The w value
			return;
		 
		fTempo[7]=1.0/fTempo[7];

		fTempo[4]*=fTempo[7];
		fTempo[5]*=fTempo[7];
		fTempo[6]*=fTempo[7];

		out[0]=(fTempo[4]*0.5+0.5)*this.width;
		out[1]=(fTempo[5]*0.5+0.5)*this.height;
	};
	
	GLWrap_.prototype.setSize = function(w, h, force)
	{
		if (this.width === w && this.height === h && !force)
			return;
			
		this.endBatch();
		
		var gl = this.gl;
		
		this.width = w;
		this.height = h;
		gl.viewport(0, 0, w, h);
		
		mat4.lookAt(this.cam, this.look, this.up, this.matMV);
		
		if (this.enableFrontToBack)
		{
			mat4.ortho(-w/2, w/2, h/2, -h/2, this.zNear, this.zFar, this.matP);
			this.worldScale[0] = 1;
			this.worldScale[1] = 1;
		}
		else
		{
			mat4.perspective(45, w / h, this.zNear, this.zFar, this.matP);
			var tl = [0, 0];
			var br = [0, 0];
			this.project(0, 0, tl);
			this.project(1, 1, br);
			this.worldScale[0] = 1 / (br[0] - tl[0]);
			this.worldScale[1] = -1 / (br[1] - tl[1]);
		}
		
		// Write updated matP to all shaders and mark all needing a MV update
		var i, len, s;
		for (i = 0, len = this.shaderPrograms.length; i < len; i++)
		{
			s = this.shaderPrograms[i];
			s.hasCurrentMatMV = false;
			
			if (s.locMatP)
			{
				gl.useProgram(s.shaderProgram);
				gl.uniformMatrix4fv(s.locMatP, false, this.matP);
			}
		}
		
		// Switch back to last set program
		gl.useProgram(this.shaderPrograms[this.lastProgram].shaderProgram);
		
		// Forget current texture to ensure next setTexture call isn't dropped.  Sometimes switching in to
		// fullscreen mode loses the current texture and we need to make sure GLWrap's state matches that.
		gl.bindTexture(gl.TEXTURE_2D, null);
		
		gl.activeTexture(gl.TEXTURE1);
		gl.bindTexture(gl.TEXTURE_2D, null);
		gl.activeTexture(gl.TEXTURE0);
		
		this.lastTexture0 = null;
		this.lastTexture1 = null;
		
		// Resize depth buffer if any
		if (this.depthBuffer)
		{
			gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
			
			gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthBuffer);
			gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, this.width, this.height);
			
			if (!this.attachedDepthBuffer)
			{
				// Permanently attach the depth buffer to the framebuffer object. To disable use of the depth buffer
				// we disable depth tests instead of detaching it, to minimise framebuffer state changes. Note we must
				// do this in the first setSize call, since we cannot set a depth attachment with no storage.
				gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.depthBuffer);
				this.attachedDepthBuffer = true;
			}
			
			gl.bindRenderbuffer(gl.RENDERBUFFER, null);
			
			gl.bindFramebuffer(gl.FRAMEBUFFER, null);
			this.renderToTex = null;
		}
	};
	
	GLWrap_.prototype.resetModelView = function ()
	{
		mat4.lookAt(this.cam, this.look, this.up, this.matMV);
		mat4.scale(this.matMV, this.worldScale);
	};
	
	GLWrap_.prototype.translate = function (x, y)
	{
		if (x === 0 && y === 0)
			return;
			
		this.tmpVec3[0] = x;// * this.worldScale[0];
		this.tmpVec3[1] = y;// * this.worldScale[1];
		this.tmpVec3[2] = 0;
		mat4.translate(this.matMV, this.tmpVec3);
	};
	
	GLWrap_.prototype.scale = function (x, y)
	{
		if (x === 1 && y === 1)
			return;
		
		this.tmpVec3[0] = x;
		this.tmpVec3[1] = y;
		this.tmpVec3[2] = 1;
		mat4.scale(this.matMV, this.tmpVec3);
	};
	
	GLWrap_.prototype.rotateZ = function (a)
	{
		if (a === 0)
			return;
		
		mat4.rotateZ(this.matMV, a);
	};
	
	GLWrap_.prototype.updateModelView = function()
	{
		// Matches existing transform: discard call
		if (areMat4sEqual(this.lastMV, this.matMV))
			return;
		
		var b = this.pushBatch();
		b.type = BATCH_UPDATEMODELVIEW;
		
		if (b.mat4param)
			mat4.set(this.matMV, b.mat4param);
		else
			b.mat4param = mat4.create(this.matMV);
		
		mat4.set(this.matMV, this.lastMV);
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	///////////////////////////////
	/*
	var debugBatch = false;
	
	document.addEventListener("mousedown", function (info)
	{
		if (info.which === 2)
			debugBatch = true;
	});
	*/
	///////////////////////////////
	
	// Choose a Z value that corresponds to a depth buffer value of 'i'. Note that Z buffer value calculations
	// are non-linear. Also note that this calculation is susceptible to floating point rounding errors, and conversion
	// back to a Z buffer value can result in close-but-inexact sequences like 3.00000001, 3.99999999. We don't know what
	// the driver will do with this: it could round them and we get the values we want, or it could simply truncate and then
	// two separate Z indices end up sharing the same Z buffer value of 3, which will allow overdraw at that index.
	// To avoid this, a cautious approach is used where the Z index is doubled (the i * 2 in the calculation), therefore
	// forcing the two indices to separate Z buffer values regardless of what rounding mechanism is used. This reduces
	// the full 65536 possible values to "only" 32768 which should still be plenty, given that indices are only assigned for
	// objects actually in the viewport.
	// Also note that with extreme sprite counts the index must be clamped to prevent objects disappearing as they go past
	// the far plane. However this is not the end of the world, since if a lot of objects at the back share the same Z buffer
	// value, they will simply all overdraw each other in the back-to-front pass (with the LEQUAL test) and still appear
	// correctly, despite wasting fillrate, but there's not much that can be done once Z buffer resolution is exhausted.
	//this.currentZ = this.cam[2] - (this.kzB / (i * 2 - this.kzA));			// still overdraws on some devices
	//this.currentZ = this.cam[2] - (this.zB / ((i / 32768) - this.zA));		// still overdraws on some devices
	GLWrap_.prototype.setEarlyZIndex = function (i)
	{
		if (!this.enableFrontToBack)
			return;
		
		if (i > 32760)
			i = 32760;
		
		this.currentZ = this.cam[2] - this.zNear - i * this.zIncrement;
	};
	
	function GLBatchJob(type_, glwrap_)
	{
		this.type = type_;
		this.glwrap = glwrap_;
		this.gl = glwrap_.gl;
		this.opacityParam = 0;		// for setOpacity()
		this.startIndex = 0;		// for quad()
		this.indexCount = 0;		// "
		this.texParam = null;		// for setTexture()
		this.mat4param = null;		// for updateModelView()
		this.layoutRect = quat4.create();		// note no vec4 in this version of glmatrix
		this.srcOriginRect = quat4.create();	// note no vec4 in this version of glmatrix
		this.shaderParams = [];		// for user parameters
		
		cr.seal(this);
	};
	
	GLBatchJob.prototype.doSetEarlyZPass = function ()
	{
		var gl = this.gl;
		var glwrap = this.glwrap;
		
		if (this.startIndex !== 0)		// enable
		{
			//if (debugBatch)
			//	console.log("[WebGL batch] === Early Z pass (front-to-back) === ");
			
			gl.depthMask(true);			// enable depth writes
			gl.colorMask(false, false, false, false);	// disable color writes
			gl.disable(gl.BLEND);		// no color writes so disable blend
			
			// Unbind color attachment: color writes are disabled so no point having one
			gl.bindFramebuffer(gl.FRAMEBUFFER, glwrap.fbo);
			gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0);
			gl.clear(gl.DEPTH_BUFFER_BIT);		// auto-clear depth buffer
			gl.bindFramebuffer(gl.FRAMEBUFFER, null);
			
			glwrap.isBatchInEarlyZPass = true;
		}
		else
		{
			//if (debugBatch)
			//	console.log("[WebGL batch] === Color pass (back-to-front) === ");
			
			gl.depthMask(false);		// disable depth writes, only test existing depth values
			gl.colorMask(true, true, true, true);		// enable color writes
			gl.enable(gl.BLEND);		// turn blending back on
			
			glwrap.isBatchInEarlyZPass = false;
		}
	};
	
	GLBatchJob.prototype.doSetTexture = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] " + (this.texParam ? "Setting a texture" : "Setting no texture"));
		
		this.gl.bindTexture(this.gl.TEXTURE_2D, this.texParam);
	};
	
	GLBatchJob.prototype.doSetTexture1 = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] " + (this.texParam ? "Setting texture1" : "Setting no texture1"));
		
		var gl = this.gl;
		gl.activeTexture(gl.TEXTURE1);
		gl.bindTexture(gl.TEXTURE_2D, this.texParam);
		gl.activeTexture(gl.TEXTURE0);
	};
	
	GLBatchJob.prototype.doSetOpacity = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Setting opacity to " + this.opacityParam);
		
		var o = this.opacityParam;
		var glwrap = this.glwrap;
		glwrap.currentOpacity = o;
		var curProg = glwrap.currentShader;
		
		if (curProg.locOpacity && curProg.lpOpacity !== o)
		{
			curProg.lpOpacity = o;
			this.gl.uniform1f(curProg.locOpacity, o);
		}
	};
	
	GLBatchJob.prototype.doQuad = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Drawing " + (this.indexCount / 6) + " quads");
			
		this.gl.drawElements(this.gl.TRIANGLES, this.indexCount, this.gl.UNSIGNED_SHORT, this.startIndex);
	};
	
	GLBatchJob.prototype.doSetBlend = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Changing blend mode (" + this.startIndex + ", " + this.indexCount + ")");
			
		// recycled params to save memory
		this.gl.blendFunc(this.startIndex, this.indexCount);
	};
	
	GLBatchJob.prototype.doUpdateModelView = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Updating model view");
		
		// Mark all shaders needing a model view update, and write the model view to the current shader now
		var i, len, s, shaderPrograms = this.glwrap.shaderPrograms, currentProgram = this.glwrap.currentProgram;
		
		for (i = 0, len = shaderPrograms.length; i < len; i++)
		{
			s = shaderPrograms[i];
			
			// This is the current program: send the updated model view now
			if (i === currentProgram && s.locMatMV)
			{
				s.updateMatMV(this.mat4param);
				s.hasCurrentMatMV = true;
			}
			// Otherwise mark as needing an update next time we switch to it
			else
				s.hasCurrentMatMV = false;
		}
		
		// Write the current model view
		mat4.set(this.mat4param, this.glwrap.currentMV);
	};
	
	GLBatchJob.prototype.doRenderToTexture = function ()
	{		
		var gl = this.gl;
		var glwrap = this.glwrap;
		
		if (this.texParam)
		{
			//if (debugBatch)
			//	log("[WebGL batch] Rendering to texture");
			
			// Verify the texture to render to is unbound from TEXTURE1 so the renderer
			// does not think we might be rendering to the same texture.
			if (glwrap.lastTexture1 === this.texParam)
			{
				//if (debugBatch)
				//	log("[WebGL batch] Unbinding texture1 since it is being set as rendertarget");
				
				gl.activeTexture(gl.TEXTURE1);
				gl.bindTexture(gl.TEXTURE_2D, null);
				glwrap.lastTexture1 = null;
				gl.activeTexture(gl.TEXTURE0);
			}
			
			gl.bindFramebuffer(gl.FRAMEBUFFER, glwrap.fbo);
			
			// Don't modify color attachments in early Z pass mode: color writes are disabled and the color attachment is unbound
			if (!glwrap.isBatchInEarlyZPass)
			{
				gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texParam, 0);
			}
			
			// very CPU intensive check, turn off for now
			//assert2(gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE, "Rendering to texture: framebuffer not complete");
		}
		else
		{
			//if (debugBatch)
			//	log("[WebGL batch] Rendering to backbuffer");
			
			if (!glwrap.enableFrontToBack)
			{
				gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0);
			}
			
			gl.bindFramebuffer(gl.FRAMEBUFFER, null);
		}
	};
	
	GLBatchJob.prototype.doClear = function ()
	{
		var gl = this.gl;
		var mode = this.startIndex;
		
		if (mode === 0)			// clear whole surface
		{
			//if (debugBatch)
			//	log("[WebGL batch] Clearing whole surface");
		
			gl.clearColor(this.mat4param[0], this.mat4param[1], this.mat4param[2], this.mat4param[3]);
			gl.clear(gl.COLOR_BUFFER_BIT);
		}
		else if (mode === 1)	// clear rectangle
		{
			//if (debugBatch)
			//	log("[WebGL batch] Clearing " + this.mat4param[2] + " x " + this.mat4param[3] + " rectangle");
			
			gl.enable(gl.SCISSOR_TEST);
			gl.scissor(this.mat4param[0], this.mat4param[1], this.mat4param[2], this.mat4param[3]);
			gl.clearColor(0, 0, 0, 0);
			gl.clear(gl.COLOR_BUFFER_BIT);
			gl.disable(gl.SCISSOR_TEST);
		}
		else					// clear depth
		{
			//if (debugBatch)
			//	log("[WebGL batch] Clearing depth buffer");
		
			gl.clear(gl.DEPTH_BUFFER_BIT);
		}
	};
	
	GLBatchJob.prototype.doSetDepthTestEnabled = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Setting depth test enabled: " + !!this.startIndex);
	
		var gl = this.gl;
		var enable = this.startIndex;
		
		if (enable !== 0)
		{
			gl.enable(gl.DEPTH_TEST);
		}
		else
		{
			gl.disable(gl.DEPTH_TEST);
		}
	};
	
	GLBatchJob.prototype.doPoints = function ()
	{
		var gl = this.gl;
		var glwrap = this.glwrap;
		
		//if (debugBatch)
		//	log("[WebGL batch] Drawing " + this.indexCount + " points");
		
		// In front-to-back mode, disable the depth test. The point shader does not send any Z position at all,
		// so to make sure they render the depth test must be disabled for the points.
		if (glwrap.enableFrontToBack)
			gl.disable(gl.DEPTH_TEST);
		
		var s = glwrap.shaderPrograms[1];
		gl.useProgram(s.shaderProgram);
		
		if (!s.hasCurrentMatMV && s.locMatMV)
		{
			s.updateMatMV(glwrap.currentMV);
			s.hasCurrentMatMV = true;
		}
		
		gl.enableVertexAttribArray(s.locAPos);
		gl.bindBuffer(gl.ARRAY_BUFFER, glwrap.pointBuffer);
		gl.vertexAttribPointer(s.locAPos, 4, gl.FLOAT, false, 0, 0);
			
		gl.drawArrays(gl.POINTS, this.startIndex / 4, this.indexCount);
		
		s = glwrap.currentShader;
		gl.useProgram(s.shaderProgram);
		
		// Update vertex attribute array bindings
		if (s.locAPos >= 0)
		{
			gl.enableVertexAttribArray(s.locAPos);
			gl.bindBuffer(gl.ARRAY_BUFFER, glwrap.vertexBuffers[glwrap.curBuffer]);
			gl.vertexAttribPointer(s.locAPos, glwrap.enableFrontToBack ? 3 : 2, gl.FLOAT, false, 0, 0);
		}
		
		if (s.locATex >= 0)
		{
			gl.enableVertexAttribArray(s.locATex);
			gl.bindBuffer(gl.ARRAY_BUFFER, glwrap.texcoordBuffers[glwrap.curBuffer]);
			gl.vertexAttribPointer(s.locATex, 2, gl.FLOAT, false, 0, 0);
		}
		
		// Re-enable depth test in front-to-back mode.
		if (glwrap.enableFrontToBack)
			gl.enable(gl.DEPTH_TEST);
	};
	
	GLBatchJob.prototype.doSetProgram = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Setting program " + this.startIndex);
		
		var gl = this.gl;
		var glwrap = this.glwrap;
		
		var s = glwrap.shaderPrograms[this.startIndex];		// recycled param to save memory
		glwrap.currentProgram = this.startIndex;			// current batch program
		glwrap.currentShader = s;
		
		gl.useProgram(s.shaderProgram);						// switch to
		
		// Update model view if stale
		if (!s.hasCurrentMatMV && s.locMatMV)
		{
			s.updateMatMV(glwrap.currentMV);
			s.hasCurrentMatMV = true;
		}
		
		// Write current opacity
		if (s.locOpacity && s.lpOpacity !== glwrap.currentOpacity)
		{
			s.lpOpacity = glwrap.currentOpacity;
			gl.uniform1f(s.locOpacity, glwrap.currentOpacity);
		}
		
		// Update vertex attribute array bindings
		if (s.locAPos >= 0)
		{
			gl.enableVertexAttribArray(s.locAPos);
			gl.bindBuffer(gl.ARRAY_BUFFER, glwrap.vertexBuffers[glwrap.curBuffer]);
			gl.vertexAttribPointer(s.locAPos, glwrap.enableFrontToBack ? 3 : 2, gl.FLOAT, false, 0, 0);
		}
		
		if (s.locATex >= 0)
		{
			gl.enableVertexAttribArray(s.locATex);
			gl.bindBuffer(gl.ARRAY_BUFFER, glwrap.texcoordBuffers[glwrap.curBuffer]);
			gl.vertexAttribPointer(s.locATex, 2, gl.FLOAT, false, 0, 0);
		}
	}
	
	GLBatchJob.prototype.doSetColor = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Updating color uniform");
	
		var s = this.glwrap.currentShader;
		var mat4param = this.mat4param;
		
		this.gl.uniform4f(s.locColorFill, mat4param[0], mat4param[1], mat4param[2], mat4param[3]);
	};
	
	GLBatchJob.prototype.doSetProgramParameters = function ()
	{
		//if (debugBatch)
		//	log("[WebGL batch] Setting program parameters");
		
		var i, len, s = this.glwrap.currentShader;
		var gl = this.gl;
		var mat4param = this.mat4param;
		var layoutRect = this.layoutRect;
		var srcOriginRect = this.srcOriginRect;
		
		if (s.locSamplerBack && this.glwrap.lastTexture1 !== this.texParam)
		{
			gl.activeTexture(gl.TEXTURE1);
			gl.bindTexture(gl.TEXTURE_2D, this.texParam);
			this.glwrap.lastTexture1 = this.texParam;
			gl.activeTexture(gl.TEXTURE0);
		}
		
		var v = mat4param[0];
		var v2 = mat4param[1];
		
		if (s.locPixelSize && (v !== s.lpPixelWidth || v2 !== s.lpPixelHeight))
		{
			s.lpPixelWidth = v;
			s.lpPixelHeight = v2;
			gl.uniform2f(s.locPixelSize, v, v2);
		}
		
		v = mat4param[2];
		v2 = mat4param[3];
		
		if (s.locDestStart && (v !== s.lpDestStartX || v2 !== s.lpDestStartY))
		{
			s.lpDestStartX = v;
			s.lpDestStartY = v2;
			gl.uniform2f(s.locDestStart, v, v2);
		}
		
		v = mat4param[4];
		v2 = mat4param[5];
		
		if (s.locDestEnd && (v !== s.lpDestEndX || v2 !== s.lpDestEndY))
		{
			s.lpDestEndX = v;
			s.lpDestEndY = v2;
			gl.uniform2f(s.locDestEnd, v, v2);
		}
		
		v = mat4param[6];
		
		if (s.locLayerScale && v !== s.lpLayerScale)
		{
			s.lpLayerScale = v;
			gl.uniform1f(s.locLayerScale, v);
		}
		
		v = mat4param[7];
		
		if (s.locLayerAngle && v !== s.lpLayerAngle)
		{
			s.lpLayerAngle = v;
			gl.uniform1f(s.locLayerAngle, v);
		}
		
		v = mat4param[12];
		v2 = mat4param[13];
		
		if (s.locSrcStart && (v !== s.lpSrcStartX || v2 !== s.lpSrcStartY))
		{
			s.lpSrcStartX = v;
			s.lpSrcStartY = v2;
			gl.uniform2f(s.locSrcStart, v, v2);
		}
		
		v = mat4param[14];
		v2 = mat4param[15];
		
		if (s.locSrcEnd && (v !== s.lpSrcEndX || v2 !== s.lpSrcEndY))
		{
			s.lpSrcEndX = v;
			s.lpSrcEndY = v2;
			gl.uniform2f(s.locSrcEnd, v, v2);
		}
		
		v = srcOriginRect[0];
		v2 = srcOriginRect[1];
		
		if (s.locSrcOriginStart && (v !== s.lpSrcOriginStartX || v2 !== s.lpSrcOriginStartY))
		{
			s.lpSrcOriginStartX = v;
			s.lpSrcOriginStartY = v2;
			gl.uniform2f(s.locSrcOriginStart, v, v2);
		}
		
		v = srcOriginRect[2];
		v2 = srcOriginRect[3];
		
		if (s.locSrcOriginEnd && (v !== s.lpSrcOriginEndX || v2 !== s.lpSrcOriginEndY))
		{
			s.lpSrcOriginEndX = v;
			s.lpSrcOriginEndY = v2;
			gl.uniform2f(s.locSrcOriginEnd, v, v2);
		}
		
		v = layoutRect[0];
		v2 = layoutRect[1];
		
		if (s.locLayoutStart && (v !== s.lpLayoutStartX || v2 !== s.lpLayoutStartY))
		{
			s.lpLayoutStartX = v;
			s.lpLayoutStartY = v2;
			gl.uniform2f(s.locLayoutStart, v, v2);
		}
		
		v = layoutRect[2];
		v2 = layoutRect[3];
		
		if (s.locLayoutEnd && (v !== s.lpLayoutEndX || v2 !== s.lpLayoutEndY))
		{
			s.lpLayoutEndX = v;
			s.lpLayoutEndY = v2;
			gl.uniform2f(s.locLayoutEnd, v, v2);
		}
		
		v = this.startIndex;		// time parameter stowed here
		if (s.locSeconds && v !== s.lpSeconds)
		{
			s.lpSeconds = v;
			gl.uniform1f(s.locSeconds, v);
		}
			
		if (s.parameters.length)
		{
			for (i = 0, len = s.parameters.length; i < len; i++)
			{
				v = this.shaderParams[i];
				
				// Array indicates a color parameter (vec3) in [r, g, b] array.
				// TODO: better optimise this path.
				if (Array.isArray(v))
				{
					gl.uniform3fv(s.parameters[i][1], v);
				}
				else if (v !== s.lastCustomParams[i])
				{
					s.lastCustomParams[i] = v;
					gl.uniform1f(s.parameters[i][1], v);
				}
			}
		}
	};
	
	GLWrap_.prototype.pushBatch = function ()
	{
		// Create new if reached end of array
		if (this.batchPtr === this.batch.length)
			this.batch.push(new GLBatchJob(BATCH_NULL, this));
		
		// Otherwise recycle the batch
		return this.batch[this.batchPtr++];
	};
	
	GLWrap_.prototype.endBatch = function ()
	{
		if (this.batchPtr === 0)
			return;
		if (this.gl.isContextLost())
			return;
		
		this.maybeStartTiming();

		// Write the buffers to VRAM.
		var gl = this.gl;
		
		if (this.pointPtr > 0)
		{
			gl.bindBuffer(gl.ARRAY_BUFFER, this.pointBuffer);
			gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.pointData.subarray(0, this.pointPtr));
			
			if (s && s.locAPos >= 0 && s.name === "<point>")
				gl.vertexAttribPointer(s.locAPos, 4, gl.FLOAT, false, 0, 0);
		}
		
		//if (debugBatch)
		//	log("[WebGL batch] Running batch (" + this.batchPtr + " batch items, " + this.vertexPtr + " vertices)");
		
		if (this.vertexPtr > 0)
		{
			// Send buffer data and make sure current program is using the new buffer
			var s = this.currentShader;
			
			gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffers[this.curBuffer]);
			gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertexData.subarray(0, this.vertexPtr));
			
			if (s && s.locAPos >= 0 && s.name !== "<point>")
				gl.vertexAttribPointer(s.locAPos, this.enableFrontToBack ? 3 : 2, gl.FLOAT, false, 0, 0);
			
			gl.bindBuffer(gl.ARRAY_BUFFER, this.texcoordBuffers[this.curBuffer]);
			gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.texcoordData.subarray(0, this.texPtr));
			
			if (s && s.locATex >= 0 && s.name !== "<point>")
				gl.vertexAttribPointer(s.locATex, 2, gl.FLOAT, false, 0, 0);
		}
		
		// Execute the batch. Note V8 won't optimise this with references to the vars (e.g. BATCH_QUAD) so they have
		// been replaced with literals.
		var i, len, b;
		for (i = 0, len = this.batchPtr; i < len; i++)
		{
			b = this.batch[i];
			
			switch (b.type) {
			case 1:
				b.doQuad();
				break;
			case 2:
				b.doSetTexture();
				break;
			case 3:
				b.doSetOpacity();
				break;
			case 4:
				b.doSetBlend();
				break;
			case 5:
				b.doUpdateModelView();
				break;
			case 6:
				b.doRenderToTexture();
				break;
			case 7:
				b.doClear();
				break;
			case 8:
				b.doPoints();
				break;
			case 9:
				b.doSetProgram();
				break;
			case 10:
				b.doSetProgramParameters();
				break;
			case 11:
				b.doSetTexture1();
				break;
			case 12:
				b.doSetColor();
				break;
			case 13:
				b.doSetDepthTestEnabled();
				break;
			case 14:
				b.doSetEarlyZPass();
				break;
			}
		}
		
		// reset cursors and recycle buffers
		this.batchPtr = 0;
		this.vertexPtr = 0;
		this.texPtr = 0;
		this.pointPtr = 0;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
		this.isBatchInEarlyZPass = false;
		
		this.curBuffer++;
		
		if (this.curBuffer >= MULTI_BUFFERS)
			this.curBuffer = 0;
	};

	GLWrap_.prototype.maybeStartTiming = function()
	{
		if (this.isTiming)
			return;		// already started a timer for this frame

		if (!this.timerExt)
			return;		// timing not supported

		this.isTiming = true;
		var o = {
			frameNumber: this.frameNumber,
			query: null,
			result: 0
		};

		if (this.version === 2)
		{
			o.query = this.gl["createQuery"]();
			this.gl["beginQuery"](this.timerExt["TIME_ELAPSED_EXT"], o.query);
		}
		else
		{
			o.query = this.timerExt["createQueryEXT"]();
			this.timerExt["beginQueryEXT"](this.timerExt["TIME_ELAPSED_EXT"], o.query);
		}

		this.timerResults.push(o);

		// If spilled over 1000 timer results, ensure last one is destroyed.
		// The runtime deletes old results it doesn't need any more after taking measurements,
		// so this is just a sanity check.
		if (this.timerResults.length > 1000)
		{
			var oldest = this.timerResults.shift();

			// destroy the query object if it's been left behind
			if (oldest.query)
			{
				if (this.version === 2)
					this.gl["deleteQuery"](oldest.query);
				else
					this.timerExt["deleteQueryEXT"](oldest.query);
			}
		}
	};

	GLWrap_.prototype.maybeFinishTiming = function()
	{
		if (!this.isTiming || !this.timerResults.length)
			return;		// did not start a timer this frame

		// Current frame's timer at end of results list
		var o = this.timerResults[this.timerResults.length - 1];

		// End the query. The result won't be available at least until the next tick.
		if (this.version === 2)
		{
			this.gl["endQuery"](this.timerExt["TIME_ELAPSED_EXT"]);
		}
		else
		{
			this.timerExt["endQueryEXT"](this.timerExt["TIME_ELAPSED_EXT"]);
		}

		this.isTiming = false;
	}

	GLWrap_.prototype.checkForTimerResults = function (frameNum)
	{
		// Check for any timer results up to but not including the given frame number
		var i, len, o, available, disjoint;
		for (i = 0, len = this.timerResults.length; i < len; ++i)
		{
			o = this.timerResults[i];

			// Gone beyond frame number range to check: stop checking and return
			if (o.frameNumber >= frameNum)
				return;

			// Query object is left behind until it gets a result
			if (o.query)
			{
				if (this.version === 2)
					available = this.gl["getQueryParameter"](o.query, this.gl.QUERY_RESULT_AVAILABLE);
				else
					available = this.timerExt["getQueryObjectEXT"](o.query, this.timerExt["QUERY_RESULT_AVAILABLE_EXT"]);

				disjoint = this.gl.getParameter(this.timerExt["GPU_DISJOINT_EXT"]);

				if (available && !disjoint)
				{
					if (this.version === 2)
						o.result = this.gl["getQueryParameter"](o.query, this.gl.QUERY_RESULT);
					else
						o.result = this.timerExt["getQueryObjectEXT"](o.query, this.timerExt["QUERY_RESULT_EXT"]);

					o.result /= 1e9;		// convert from nanoseconds to seconds
				}

				if (available || disjoint)
				{
					if (this.version === 2)
						this.gl["deleteQuery"](o.query);
					else
						this.timerExt["deleteQueryEXT"](o.query);

					o.query = null;
				}
			}
		}
	};

	GLWrap_.prototype.getFrameRangeTimerResults = function (startFrame, endFrame)
	{
		if (endFrame <= startFrame)
			return -1;		// invalid range

		// Return total result time for all frames from startFrame up to but not including endFrame.
		// If any results still have pending queries or are otherwise unavailable, returns -1.
		var i, len, o, sum = 0, count = 0;
		for (i = 0, len = this.timerResults.length; i < len; ++i)
		{
			o = this.timerResults[i];

			// Gone past end of range we're interested in
			if (o.frameNumber >= endFrame)
				break;

			// Result is within range
			if (o.frameNumber >= startFrame)
			{
				// Still got a pending query: result is not yet ready
				if (o.query)
					return -1;

				// Otherwise count up the result
				sum += o.result;
				count++;
			}
		}

		// If we did not collect enough results, then some frames are missing so return result not available
		if (count < endFrame - startFrame)
			return -1;

		// Otherwise return final sum
		return sum;
	};

	// Note assuming called after getFrameRangeTimerResults(), we know no query objects are still pending
	// so there is no need for to check for them or release them
	GLWrap_.prototype.deleteTimerResultsToFrame = function (frameNum)
	{
		var i, len, o;
		for (i = 0, len = this.timerResults.length; i < len; ++i)
		{
			o = this.timerResults[i];

			if (o.frameNumber >= frameNum)
			{
				if (i > 0)
					this.timerResults.splice(0, i);

				return;
			}
		}
	};
	
	GLWrap_.prototype.setOpacity = function (op)
	{
		if (op === this.lastOpacity)
			return;
		
		if (this.isEarlyZPass)
			return;		// ignore
		
		var b = this.pushBatch();
		b.type = BATCH_SETOPACITY;
		b.opacityParam = op;
		
		this.lastOpacity = op;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.setTexture = function (tex)
	{
		if (tex === this.lastTexture0)
			return;
		
;
		
		var b = this.pushBatch();
		b.type = BATCH_SETTEXTURE;
		b.texParam = tex;
		
		this.lastTexture0 = tex;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.setBlend = function (s, d)
	{
		if (s === this.lastSrcBlend && d === this.lastDestBlend)
			return;
		
		if (this.isEarlyZPass)
			return;		// ignore
		
		var b = this.pushBatch();
		b.type = BATCH_SETBLEND;
		b.startIndex = s;		// recycle params to save memory
		b.indexCount = d;
		
		this.lastSrcBlend = s;
		this.lastDestBlend = d;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.isPremultipliedAlphaBlend = function ()
	{
		return (this.lastSrcBlend === this.gl.ONE && this.lastDestBlend === this.gl.ONE_MINUS_SRC_ALPHA);
	};
	
	GLWrap_.prototype.setAlphaBlend = function ()
	{
		this.setBlend(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
	};
	
	GLWrap_.prototype.setNoPremultiplyAlphaBlend = function ()
	{
		this.setBlend(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
	};
	
	var LAST_VERTEX = MAX_VERTICES * 2 - 8;
	
	GLWrap_.prototype.quad = function(tlx, tly, trx, try_, brx, bry, blx, bly)
	{
		// End the batch if the vertex buffer is full
		if (this.vertexPtr >= LAST_VERTEX)
			this.endBatch();
		
		var v = this.vertexPtr;			// vertex cursor
		var t = this.texPtr;
		var vd = this.vertexData;		// vertex data array
		var td = this.texcoordData;		// texture coord data array
		var currentZ = this.currentZ;
		
		// Try to extend a previous quad batch job
		if (this.hasQuadBatchTop)
		{
			this.batch[this.batchPtr - 1].indexCount += 6;
		}
		else
		{
			// No quad batch already in progress: start a new one
			var b = this.pushBatch();
			b.type = BATCH_QUAD;
			
			// Math note: the start index is in bytes in to the index array. There are 6 indices per quad,
			// and each index is 2 bytes, so there are 12 bytes per quad. In front-to-back mode v happens
			// to increment by 12 per quad, because 3 components x 4 vertices = 12. In back-to-front mode
			// there are only 2 components x 4 vertices = 8 per quad, which is bumped up to 12 by (v/2)*3.
			b.startIndex = this.enableFrontToBack ? v : (v / 2) * 3;
			b.indexCount = 6;
			this.hasQuadBatchTop = true;
			this.hasPointBatchTop = false;
		}
		
		if (this.enableFrontToBack)
		{
			// 3-component vertex buffer (x, y, z)
			vd[v++] = tlx;
			vd[v++] = tly;
			vd[v++] = currentZ;
			vd[v++] = trx;
			vd[v++] = try_;
			vd[v++] = currentZ;
			vd[v++] = brx;
			vd[v++] = bry;
			vd[v++] = currentZ;
			vd[v++] = blx;
			vd[v++] = bly;
			vd[v++] = currentZ;
		}
		else
		{
			// 2-component vertex buffer (x, y)
			vd[v++] = tlx;
			vd[v++] = tly;
			vd[v++] = trx;
			vd[v++] = try_;
			vd[v++] = brx;
			vd[v++] = bry;
			vd[v++] = blx;
			vd[v++] = bly;
		}
		
		td[t++] = 0;
		td[t++] = 0;
		td[t++] = 1;
		td[t++] = 0;
		td[t++] = 1;
		td[t++] = 1;
		td[t++] = 0;
		td[t++] = 1;
		
		// Update cursor
		this.vertexPtr = v;
		this.texPtr = t;
	};
	
	GLWrap_.prototype.quadTex = function(tlx, tly, trx, try_, brx, bry, blx, bly, rcTex)
	{
		// End the batch if the vertex buffer is full
		if (this.vertexPtr >= LAST_VERTEX)
			this.endBatch();
		
		var v = this.vertexPtr;			// vertex cursor
		var t = this.texPtr;
		var vd = this.vertexData;		// vertex data array
		var td = this.texcoordData;		// texture coord data array
		var currentZ = this.currentZ;
		
		// Try to extend a previous quad batch job
		if (this.hasQuadBatchTop)
		{
			this.batch[this.batchPtr - 1].indexCount += 6;
		}
		else
		{
			// No quad batch already in progress: start a new one
			var b = this.pushBatch();
			b.type = BATCH_QUAD;
			b.startIndex = this.enableFrontToBack ? v : (v / 2) * 3;
			b.indexCount = 6;
			this.hasQuadBatchTop = true;
			this.hasPointBatchTop = false;
		}
		
		var rc_left = rcTex.left;
		var rc_top = rcTex.top;
		var rc_right = rcTex.right;
		var rc_bottom = rcTex.bottom;
		
		if (this.enableFrontToBack)
		{
			// 3-component vertex buffer (x, y, z)
			vd[v++] = tlx;
			vd[v++] = tly;
			vd[v++] = currentZ;
			vd[v++] = trx;
			vd[v++] = try_;
			vd[v++] = currentZ;
			vd[v++] = brx;
			vd[v++] = bry;
			vd[v++] = currentZ;
			vd[v++] = blx;
			vd[v++] = bly;
			vd[v++] = currentZ;
		}
		else
		{
			vd[v++] = tlx;
			vd[v++] = tly;
			vd[v++] = trx;
			vd[v++] = try_;
			vd[v++] = brx;
			vd[v++] = bry;
			vd[v++] = blx;
			vd[v++] = bly;
		}
		
		td[t++] = rc_left;
		td[t++] = rc_top;
		td[t++] = rc_right;
		td[t++] = rc_top;
		td[t++] = rc_right;
		td[t++] = rc_bottom;
		td[t++] = rc_left;
		td[t++] = rc_bottom;
		
		// Update cursor
		this.vertexPtr = v;
		this.texPtr = t;
	};
	
	GLWrap_.prototype.quadTexUV = function(tlx, tly, trx, try_, brx, bry, blx, bly, tlu, tlv, tru, trv, bru, brv, blu, blv)
	{
		// End the batch if the vertex buffer is full
		if (this.vertexPtr >= LAST_VERTEX)
			this.endBatch();
		
		var v = this.vertexPtr;			// vertex cursor
		var t = this.texPtr;
		var vd = this.vertexData;		// vertex data array
		var td = this.texcoordData;		// texture coord data array
		var currentZ = this.currentZ;
		
		// Try to extend a previous quad batch job
		if (this.hasQuadBatchTop)
		{
			this.batch[this.batchPtr - 1].indexCount += 6;
		}
		else
		{
			// No quad batch already in progress: start a new one
			var b = this.pushBatch();
			b.type = BATCH_QUAD;
			b.startIndex = this.enableFrontToBack ? v : (v / 2) * 3;
			b.indexCount = 6;
			this.hasQuadBatchTop = true;
			this.hasPointBatchTop = false;
		}
		
		if (this.enableFrontToBack)
		{
			// 3-component vertex buffer (x, y, z)
			vd[v++] = tlx;
			vd[v++] = tly;
			vd[v++] = currentZ;
			vd[v++] = trx;
			vd[v++] = try_;
			vd[v++] = currentZ;
			vd[v++] = brx;
			vd[v++] = bry;
			vd[v++] = currentZ;
			vd[v++] = blx;
			vd[v++] = bly;
			vd[v++] = currentZ;
		}
		else
		{
			vd[v++] = tlx;
			vd[v++] = tly;
			vd[v++] = trx;
			vd[v++] = try_;
			vd[v++] = brx;
			vd[v++] = bry;
			vd[v++] = blx;
			vd[v++] = bly;
		}
		
		td[t++] = tlu;
		td[t++] = tlv;
		td[t++] = tru;
		td[t++] = trv;
		td[t++] = bru;
		td[t++] = brv;
		td[t++] = blu;
		td[t++] = blv;
		
		// Update cursor
		this.vertexPtr = v;
		this.texPtr = t;
	};
	
	GLWrap_.prototype.convexPoly = function(pts)
	{
		var pts_count = pts.length / 2;
;
		
		var tris = pts_count - 2;	// 3 points = 1 tri, 4 points = 2 tris, 5 points = 3 tris etc.
		var last_tri = tris - 1;
		
		// The batching system is already very efficiently geared towards issuing quads, since
		// 2D games consist mainly of rectangular sprites. Issuing a poly "properly" with a vertex
		// per poly point and indices to break it up in to triangles introduces some complications
		// with having to track index counts and vertex counts separately. To avoid this, we simply
		// issue a quad per two adjacent triangles in the poly. The quad pipeline is already highly
		// optimised so this should still be fast and batch well. If there is a single triangle to
		// draw, a degenerate quad is issued (with the bl and br points the same).
		var p0x = pts[0];
		var p0y = pts[1];
		
		var i, i2, p1x, p1y, p2x, p2y, p3x, p3y;
		
		for (i = 0; i < tris; i += 2)		// draw 2 triangles at a time
		{
			// Render as a triangle fan from point 0.
			// Tri N renders with a triangle of points 0, N + 1, N + 2
			i2 = i * 2;
			p1x = pts[i2 + 2];
			p1y = pts[i2 + 3];
			p2x = pts[i2 + 4];
			p2y = pts[i2 + 5];
			
			// Last triangle when odd number of triangles
			if (i === last_tri)
			{
				// issue degenerate quad
				this.quad(p0x, p0y, p1x, p1y, p2x, p2y, p2x, p2y);
			}
			// Otherwise draw 2 triangles via a quad
			else
			{
				p3x = pts[i2 + 6];
				p3y = pts[i2 + 7];
				this.quad(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
			}
		}
	};
	
	var LAST_POINT = MAX_POINTS - 4;
	
	GLWrap_.prototype.point = function(x_, y_, size_, opacity_)
	{
		// End the batch if the point buffer is full
		if (this.pointPtr >= LAST_POINT)
			this.endBatch();
		
		var p = this.pointPtr;			// point cursor
		var pd = this.pointData;		// point data array
		
		// Try to extend a previous point batch job
		if (this.hasPointBatchTop)
		{
			this.batch[this.batchPtr - 1].indexCount++;
		}
		else
		{
			// No quad batch already in progress: start a new one
			var b = this.pushBatch();
			b.type = BATCH_POINTS;
			b.startIndex = p;
			b.indexCount = 1;
			this.hasPointBatchTop = true;
			this.hasQuadBatchTop = false;
		}
		
		pd[p++] = x_;
		pd[p++] = y_;
		pd[p++] = size_;
		pd[p++] = opacity_;
		
		// Update cursor
		this.pointPtr = p;
	};
	
	GLWrap_.prototype.switchProgram = function (progIndex)
	{
		if (this.lastProgram === progIndex)
			return;			// no change
		
		var shaderProg = this.shaderPrograms[progIndex];
		
		// Shader is null if failed to compile
		if (!shaderProg)
		{
			if (this.lastProgram === 0)
				return;								// already on default shader
			
			// switch to default shader instead
			progIndex = 0;
			shaderProg = this.shaderPrograms[0];
		}
		
		// Push batch job to switch program
		var b = this.pushBatch();
		b.type = BATCH_SETPROGRAM;
		b.startIndex = progIndex;
		
		this.lastProgram = progIndex;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.programUsesDest = function (progIndex)
	{
		var s = this.shaderPrograms[progIndex];
		return !!(s.locDestStart || s.locDestEnd);
	};
	
	GLWrap_.prototype.programUsesCrossSampling = function (progIndex)
	{
		var s = this.shaderPrograms[progIndex];
		
		// NOTE: to avoid possible undefined behavior, any sampler also using background blending
		// is also regarded as cross-sampling. This is to prevent simultaneous input-output by
		// having texture1 bound to the current render target.
		return !!(s.locDestStart || s.locDestEnd || s.crossSampling);
	};
	
	GLWrap_.prototype.programPreservesOpaqueness = function (progIndex)
	{
		return this.shaderPrograms[progIndex].preservesOpaqueness;
	};
	
	GLWrap_.prototype.programExtendsBox = function (progIndex)
	{
		var s = this.shaderPrograms[progIndex];
		return s.extendBoxHorizontal !== 0 || s.extendBoxVertical !== 0;
	};
	
	GLWrap_.prototype.getProgramBoxExtendHorizontal = function (progIndex)
	{
		return this.shaderPrograms[progIndex].extendBoxHorizontal;
	};
	
	GLWrap_.prototype.getProgramBoxExtendVertical = function (progIndex)
	{
		return this.shaderPrograms[progIndex].extendBoxVertical;
	};
	
	GLWrap_.prototype.getProgramParameterType = function (progIndex, paramIndex)
	{
		return this.shaderPrograms[progIndex].parameters[paramIndex][2];
	};
	
	GLWrap_.prototype.programIsAnimated = function (progIndex)
	{
		return this.shaderPrograms[progIndex].animated;
	};
	
	GLWrap_.prototype.setProgramParameters = function (backTex, pixelWidth, pixelHeight, srcStartX, srcStartY, srcEndX, srcEndY, srcOriginStartX, srcOriginStartY, srcOriginEndX, srcOriginEndY, layoutStartX, layoutStartY, layoutEndX, layoutEndY, destStartX, destStartY, destEndX, destEndY, layerScale, layerAngle, seconds, params)
	{
		var i, len;
		var s = this.shaderPrograms[this.lastProgram];
		var b, mat4param, shaderParams, srcOriginRect, lrect;
		
		if (s.hasAnyOptionalUniforms || params.length)
		{
			// Push batch job to set parameters
			b = this.pushBatch();
			b.type = BATCH_SETPROGRAMPARAMETERS;
			
			if (b.mat4param)
				mat4.set(this.matMV, b.mat4param);
			else
				b.mat4param = mat4.create();
			
			mat4param = b.mat4param;
			mat4param[0] = pixelWidth;
			mat4param[1] = pixelHeight;
			mat4param[2] = destStartX;
			mat4param[3] = destStartY;
			mat4param[4] = destEndX;
			mat4param[5] = destEndY;
			mat4param[6] = layerScale;
			mat4param[7] = layerAngle;
			//mat4param[8] = layoutStartX;							// formerly viewOrigin
			//mat4param[9] = layoutStartY;
			//mat4param[10] = (layoutStartX + layoutEndX) / 2;		// formerly scrollPos
			//mat4param[11] = (layoutStartY + layoutEndY) / 2;
			mat4param[12] = srcStartX;
			mat4param[13] = srcStartY;
			mat4param[14] = srcEndX;
			mat4param[15] = srcEndY;
			
			srcOriginRect = b.srcOriginRect;
			srcOriginRect[0] = srcOriginStartX;
			srcOriginRect[1] = srcOriginStartY;
			srcOriginRect[2] = srcOriginEndX;
			srcOriginRect[3] = srcOriginEndY;
			
			lrect = b.layoutRect;
			lrect[0] = layoutStartX;
			lrect[1] = layoutEndY;			// note layout Y co-ordinates deliberately flipped to follow texture co-ordinates
			lrect[2] = layoutEndX;
			lrect[3] = layoutStartY;
			
			// Stow time in the startIndex parameter
			b.startIndex = seconds;
			
			if (s.locSamplerBack)
			{
;
				b.texParam = backTex;
			}
			else
				b.texParam = null;
			
			// Copy in shader parameters if any
			if (params.length)
			{
				shaderParams = b.shaderParams;
				shaderParams.length = params.length;
				
				for (i = 0, len = params.length; i < len; i++)
					shaderParams[i] = params[i];
			}
			
			this.hasQuadBatchTop = false;
			this.hasPointBatchTop = false;
		}
	};
	
	GLWrap_.prototype.clear = function (r, g, b_, a)
	{
		var b = this.pushBatch();
		b.type = BATCH_CLEAR;
		b.startIndex = 0;					// clear all mode
		
		if (!b.mat4param)
			b.mat4param = mat4.create();
		
		b.mat4param[0] = r;
		b.mat4param[1] = g;
		b.mat4param[2] = b_;
		b.mat4param[3] = a;
		
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.clearRect = function (x, y, w, h)
	{
		if (w < 0 || h < 0)
			return;							// invalid clear area
		
		var b = this.pushBatch();
		b.type = BATCH_CLEAR;
		b.startIndex = 1;					// clear rect mode
		
		if (!b.mat4param)
			b.mat4param = mat4.create();
		
		b.mat4param[0] = x;
		b.mat4param[1] = y;
		b.mat4param[2] = w;
		b.mat4param[3] = h;
		
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.clearDepth = function ()
	{
		var b = this.pushBatch();
		b.type = BATCH_CLEAR;
		b.startIndex = 2;					// clear depth mode
		
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.setEarlyZPass = function (e)
	{
		if (!this.enableFrontToBack)
			return;		// no depth buffer in use
		
		e = !!e;
		
		if (this.isEarlyZPass === e)
			return;		// no change
		
		var b = this.pushBatch();
		b.type = BATCH_SETEARLYZMODE;
		b.startIndex = (e ? 1 : 0);
		
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
		
		this.isEarlyZPass = e;
		this.renderToTex = null;
		
		if (this.isEarlyZPass)
		{
			this.switchProgram(2);		// early Z program
		}
		else
		{
			this.switchProgram(0);		// normal rendering
		}
	};
	
	GLWrap_.prototype.setDepthTestEnabled = function (e)
	{
		if (!this.enableFrontToBack)
			return;		// no depth buffer in use
		
		var b = this.pushBatch();
		b.type = BATCH_SETDEPTHTEST;
		b.startIndex = (e ? 1 : 0);
		
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.fullscreenQuad = function ()
	{
		// save existing model view
		mat4.set(this.lastMV, tempMat4);
		
		// switch to default transform
		this.resetModelView();
		this.updateModelView();
		var halfw = this.width / 2;
		var halfh = this.height / 2;
		this.quad(-halfw, halfh, halfw, halfh, halfw, -halfh, -halfw, -halfh);
		
		// restore previous model view
		mat4.set(tempMat4, this.matMV);
		this.updateModelView();
	};
	
	GLWrap_.prototype.setColorFillMode = function (r_, g_, b_, a_)
	{
		// switch to fill program
		this.switchProgram(3);
		
		// set fill color
		var b = this.pushBatch();
		b.type = BATCH_SETCOLOR;
		
		if (!b.mat4param)
			b.mat4param = mat4.create();
		
		b.mat4param[0] = r_;
		b.mat4param[1] = g_;
		b.mat4param[2] = b_;
		b.mat4param[3] = a_;
		
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	GLWrap_.prototype.setTextureFillMode = function ()
	{
;
		
		// restore default program
		this.switchProgram(0);
	};
	
	GLWrap_.prototype.restoreEarlyZMode = function ()
	{
;
		
		// restore early Z program
		this.switchProgram(2);
	};

	GLWrap_.prototype.getFrameNumber = function ()
	{
		return this.frameNumber;
	};
	
	GLWrap_.prototype.present = function ()
	{
		this.endBatch();

		// Finish timing this frame
		this.maybeFinishTiming();
		
		this.gl.flush();
		
		/*
		if (debugBatch)
		{
;
			debugBatch = false;
		}
		*/

		// Check for results becoming available in previous frames
		this.checkForTimerResults(this.frameNumber);

		this.frameNumber++;
	};
	
	GLWrap_.prototype.supportsGpuProfiling = function ()
	{
		return !!this.timerExt;
	};
	
	function nextHighestPowerOfTwo(x) {
		--x;
		for (var i = 1; i < 32; i <<= 1) {
			x = x | x >> i;
		}
		return x + 1;
	}
	
	var all_textures = [];
	var textures_by_src = {};
	
	GLWrap_.prototype.contextLost = function ()
	{
		// on context lost we need to ditch the texture cache since they'll all have been destroyed now
		cr.clearArray(all_textures);
		textures_by_src = {};
	};
	
	var BF_RGBA8 = 0;
	var BF_RGB8 = 1;
	var BF_RGBA4 = 2;
	var BF_RGB5_A1 = 3;
	var BF_RGB565 = 4;
	
	GLWrap_.prototype.loadTexture = function (img, tiling, linearsampling, pixelformat, tiletype, nomip)
	{
		// Try to find a texture already created from this image. If one exists, return the same texture
		// and increment its reference count.
		tiling = !!tiling;
		linearsampling = !!linearsampling;
		var tex_key = img.src + "," + tiling + "," + linearsampling + (tiling ? ("," + tiletype) : "");
		var webGL_texture = null;
		
		// Canvases can be passed to loadTexture, which have the src property missing, and in that case we want to skip this check.
		if (typeof img.src !== "undefined" && textures_by_src.hasOwnProperty(tex_key))
		{
			webGL_texture = textures_by_src[tex_key];
			webGL_texture.c2refcount++;
			return webGL_texture;
		}
		
		this.endBatch();
		
;
		
		var gl = this.gl;
		var isPOT = (cr.isPOT(img.width) && cr.isPOT(img.height));
		
		webGL_texture = gl.createTexture();
		gl.bindTexture(gl.TEXTURE_2D, webGL_texture);
		gl.pixelStorei(gl["UNPACK_PREMULTIPLY_ALPHA_WEBGL"], true);
		
		var internalformat = gl.RGBA;
		var format = gl.RGBA;
		var type = gl.UNSIGNED_BYTE;
		
		// Change type if a pixel format is provided to hint GL to use less memory
		// Note: IE's WebGL doesn't seem to support this so just use RGBA in that case
		if (pixelformat && !this.isIE)
		{
			switch (pixelformat) {
			case BF_RGB8:
				internalformat = gl.RGB;
				format = gl.RGB;
				break;
			case BF_RGBA4:
				type = gl.UNSIGNED_SHORT_4_4_4_4;
				break;
			case BF_RGB5_A1:
				type = gl.UNSIGNED_SHORT_5_5_5_1;
				break;
			case BF_RGB565:
				internalformat = gl.RGB;
				format = gl.RGB;
				type = gl.UNSIGNED_SHORT_5_6_5;
				break;
			}
		}
		
		// Tiled non-power-of-two textures are not supported in WebGL 1 - have to stretch the image to fit a POT
		if (this.version === 1 && !isPOT && tiling)
		{
			var canvas = document.createElement("canvas");
			canvas.width = cr.nextHighestPowerOfTwo(img.width);
			canvas.height = cr.nextHighestPowerOfTwo(img.height);
			var ctx = canvas.getContext("2d");
			ctx.imageSmoothingEnabled = linearsampling;		// respect sampling mode when stretching
			ctx.drawImage(img,
						  0, 0, img.width, img.height,
						  0, 0, canvas.width, canvas.height);
			gl.texImage2D(gl.TEXTURE_2D, 0, internalformat, format, type, canvas);
		}
		else
			gl.texImage2D(gl.TEXTURE_2D, 0, internalformat, format, type, img);
		
		if (tiling)
		{
			if (tiletype === "repeat-x")
			{
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
			}
			else if (tiletype === "repeat-y")
			{
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
			}
			else
			{
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
			}
		}
		else
		{
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
		}
		
		if (linearsampling)
		{
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
			
			// WebGL 2 can mipmap non-power-of-two textures
			if ((isPOT || this.version >= 2) && this.enable_mipmaps && !nomip)
			{
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
				gl.generateMipmap(gl.TEXTURE_2D);
			}
			else
				gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
		}
		else
		{
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
		}

		gl.bindTexture(gl.TEXTURE_2D, null);
		this.lastTexture0 = null;
		
		webGL_texture.c2width = img.width;
		webGL_texture.c2height = img.height;
		webGL_texture.c2refcount = 1;
		webGL_texture.c2texkey = tex_key;
		
		all_textures.push(webGL_texture);
		textures_by_src[tex_key] = webGL_texture;
		
		return webGL_texture;
	};
	
	GLWrap_.prototype.createEmptyTexture = function (w, h, linearsampling, _16bit, tiling)
	{
		this.endBatch();
		
		var gl = this.gl;
		
		// IE's WebGL doesn't support 16-bit textures
		if (this.isIE)
			_16bit = false;
		
		var webGL_texture = gl.createTexture();
		gl.bindTexture(gl.TEXTURE_2D, webGL_texture);
		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, _16bit ? gl.UNSIGNED_SHORT_4_4_4_4 : gl.UNSIGNED_BYTE, null);
		
		if (tiling)
		{
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
		}
		else
		{
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
			gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
		}
		
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linearsampling ? gl.LINEAR : gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linearsampling ? gl.LINEAR : gl.NEAREST);
		
		gl.bindTexture(gl.TEXTURE_2D, null);
		this.lastTexture0 = null;
		
		webGL_texture.c2width = w;
		webGL_texture.c2height = h;
		
		all_textures.push(webGL_texture);
		
		return webGL_texture;
	};
	
	GLWrap_.prototype.videoToTexture = function (video_, texture_, _16bit)
	{
		this.endBatch();
		
		var gl = this.gl;
		
		// IE's WebGL doesn't support 16-bit textures
		if (this.isIE)
			_16bit = false;

		gl.bindTexture(gl.TEXTURE_2D, texture_);
		gl.pixelStorei(gl["UNPACK_PREMULTIPLY_ALPHA_WEBGL"], true);
		
		// Cross-domain videos sometimes throw a security error here
		try {
			gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, _16bit ? gl.UNSIGNED_SHORT_4_4_4_4 : gl.UNSIGNED_BYTE, video_);
		}
		catch (e)
		{
			if (console && console.error)
				console.error("Error updating WebGL texture: ", e);
		}

		gl.bindTexture(gl.TEXTURE_2D, null);
		this.lastTexture0 = null;
	};
	
	GLWrap_.prototype.deleteTexture = function (tex)
	{
		if (!tex)
			return;
			
		if (typeof tex.c2refcount !== "undefined" && tex.c2refcount > 1)
		{
			tex.c2refcount--;
			return;
		}
		
		this.endBatch();
		
		if (tex === this.lastTexture0)
		{
			this.gl.bindTexture(this.gl.TEXTURE_2D, null);
			this.lastTexture0 = null;
		}
		
		if (tex === this.lastTexture1)
		{
			this.gl.activeTexture(this.gl.TEXTURE1);
			this.gl.bindTexture(this.gl.TEXTURE_2D, null);
			this.gl.activeTexture(this.gl.TEXTURE0);
			this.lastTexture1 = null;
		}
		
		cr.arrayFindRemove(all_textures, tex);
		
		if (typeof tex.c2texkey !== "undefined")
			delete textures_by_src[tex.c2texkey];

		this.gl.deleteTexture(tex);
	};
	
	GLWrap_.prototype.estimateVRAM = function ()
	{
		// Assume usage of frontbuffer and backbuffer
		var total = this.width * this.height * 4 * 2;
		
		var i, len, t;
		for (i = 0, len = all_textures.length; i < len; i++)
		{
			t = all_textures[i];
			total += (t.c2width * t.c2height * 4);
		}
		
		return total;
	};
	
	GLWrap_.prototype.textureCount = function ()
	{
		return all_textures.length;
	};
	
	GLWrap_.prototype.setRenderingToTexture = function (tex)
	{
		if (tex === this.renderToTex)
			return;
			
;
		
		// Ignore this assert: if we do set the render-to-texture to the same thing as texture1, then
		// doRenderToTexture will unbind it to ensure correct rendering.
		//assert2(!tex || tex !== this.lastTexture1, "Warning: set rendering to current texture 1");

		var b = this.pushBatch();
		b.type = BATCH_RENDERTOTEXTURE;
		b.texParam = tex;
		
		this.renderToTex = tex;
		this.hasQuadBatchTop = false;
		this.hasPointBatchTop = false;
	};
	
	cr.GLWrap = GLWrap_;

}());

// preview.js
// ECMAScript 5 strict mode

;

(function()
{
	// Use requestAnimationFrame when available
	var raf = window["requestAnimationFrame"] ||
	  window["mozRequestAnimationFrame"]    ||
	  window["webkitRequestAnimationFrame"] ||
	  window["msRequestAnimationFrame"]     ||
	  window["oRequestAnimationFrame"];
	
	// Determine supported audio formats for implementing the audio fallback
	var tmpAudio = new Audio();
	var supportedAudioFormats = {
		"audio/webm; codecs=opus": !!tmpAudio.canPlayType("audio/webm; codecs=opus"),
		"audio/ogg; codecs=opus": !!tmpAudio.canPlayType("audio/ogg; codecs=opus"),
		"audio/webm; codecs=vorbis": !!tmpAudio.canPlayType("audio/webm; codecs=vorbis"),
		"audio/ogg; codecs=vorbis": !!tmpAudio.canPlayType("audio/ogg; codecs=vorbis"),
		"audio/mp4": !!tmpAudio.canPlayType("audio/mp4"),
		"audio/mpeg": !!tmpAudio.canPlayType("audio/mpeg")		// MP3
	};
	tmpAudio = null;
	
	// Runtime class
	function Runtime(opts)
	{
		var canvas = opts["canvas"];
		var projectDataUrl = opts["projectDataUrl"];
		
		// No canvas provided in options: create one and add it to the DOM
		if (!canvas)
		{
			canvas = document.createElement("canvas");
			document.body.appendChild(canvas);
		}
		
		// No canvas support: fail silently
		if (!canvas || !canvas.getContext)
			return;
		
		window["c3runtime"] = this;
		window["c3canvas"] = canvas;
			
		var self = this;
		
		this.exportType = opts["exportType"];
		this.isPreview = (this.exportType === "preview");
		this.isRemotePreview = !!opts["isRemotePreview"];
		
		// Detect wrapper platforms
		this.isCrosswalk = /crosswalk/i.test(navigator.userAgent) || /xwalk/i.test(navigator.userAgent) || !!(typeof window["c2isCrosswalk"] !== "undefined" && window["c2isCrosswalk"]);
		
		this.isCordova = (this.exportType === "cordova");
		
		// for backwards compatibility with any plugins checking for the old isPhoneGap variable
		this.isPhoneGap = this.isCordova;
		
		// Detect the platform that we're running on by looking at the user agent string.
		// Note IE uses MSIE (<IE11), Trident (IE11), or IEMobile (Windows Phone).
		this.isMicrosoftEdge = /edge\//i.test(navigator.userAgent);
		this.isIE = (/msie/i.test(navigator.userAgent) || /trident/i.test(navigator.userAgent) || /iemobile/i.test(navigator.userAgent)) && !this.isMicrosoftEdge;
		this.isTizen = /tizen/i.test(navigator.userAgent);
		this.isAndroid = /android/i.test(navigator.userAgent) && !this.isTizen && !this.isIE && !this.isMicrosoftEdge;		// IE mobile and Tizen masquerade as Android
		this.isiPhone = (/iphone/i.test(navigator.userAgent) || /ipod/i.test(navigator.userAgent)) && !this.isIE && !this.isMicrosoftEdge;	// treat ipod as an iphone; IE mobile masquerades as iPhone
		this.isiPad = /ipad/i.test(navigator.userAgent);
		this.isiOS = this.isiPhone || this.isiPad;
		this.isChrome = (/chrome/i.test(navigator.userAgent) || /chromium/i.test(navigator.userAgent)) && !this.isIE && !this.isMicrosoftEdge;	// note true on Chromium-based webview on Android 4.4+; IE 'Edge' mode also pretends to be Chrome
		this.isAmazonWebApp = /amazonwebappplatform/i.test(navigator.userAgent);
		this.isFirefox = /firefox/i.test(navigator.userAgent);
		this.isSafari = /safari/i.test(navigator.userAgent) && !this.isChrome && !this.isIE && !this.isMicrosoftEdge;		// Chrome and IE Mobile masquerade as Safari
		this.isWindows = /windows/i.test(navigator.userAgent);
		this.isNWjs = (this.exportType === "nwjs" || /nodewebkit/i.test(navigator.userAgent) || /nwjs/i.test(navigator.userAgent));
		this.isNodeWebkit = this.isNWjs;		// old name for backwards compat
		this.isArcade = (this.exportType === "scirra-arcade");
		this.isWindows8App = !!(typeof window["c2isWindows8"] !== "undefined" && window["c2isWindows8"]);
		this.isWindowsPhone8 = !!(typeof window["c2isWindowsPhone8"] !== "undefined" && window["c2isWindowsPhone8"]);
		this.isWindowsPhone81 = !!(typeof window["c2isWindowsPhone81"] !== "undefined" && window["c2isWindowsPhone81"]);
		this.isWindows10 = (this.exportType === "windows-uwp");
		this.isWinJS = (this.isWindows8App || this.isWindowsPhone81 || this.isWindows10);	// note not WP8.0
		this.isBlackberry10 = !!(typeof window["c2isBlackberry10"] !== "undefined" && window["c2isBlackberry10"]);
		this.isAndroidStockBrowser = (this.isAndroid && !this.isChrome && !this.isCrosswalk && !this.isFirefox && !this.isAmazonWebApp);
		this.devicePixelRatio = 1;
		
		// Determine if running on a mobile: always true in mobile wrapper
		this.isMobile = (this.isCordova || this.isCrosswalk || this.isAndroid || this.isiOS || this.isWindowsPhone8 || this.isWindowsPhone81 || this.isBlackberry10 || this.isTizen);
		
		// Check for some other common mobile manufacturers
		if (!this.isMobile)
		{
			this.isMobile = /(blackberry|bb10|playbook|palm|symbian|nokia|windows\s+ce|phone|mobile|tablet|kindle|silk)/i.test(navigator.userAgent);
		}
		
		// Identify WKWebView. C3 no longer supports UIWebView so we can assume Cordova on iOS is always WKWebView.
		this.isWKWebView = !!(this.isiOS && this.isCordova);
		
		// NW.js detects as Chrome in preview mode. To work around this, allow a ?nw query string
		// to force it to detect as NW.js, or check for "nodewebkit" or "nwjs" in the user agent.
		if (this.isPreview && !this.isNWjs && (window.location.search === "?nw" || /nodewebkit/i.test(navigator.userAgent) || /nwjs/i.test(navigator.userAgent)))
		{
			this.isNWjs = true;
		}
		
		this.isDebug = (this.isPreview && window.location.search.indexOf("debug") > -1);

		// Renderer variables
		this.canvas = canvas;
		this.gl = null;
		this.glwrap = null;
		this.glUnmaskedRenderer = "(unavailable)";
		this.enableFrontToBack = false;
		this.earlyz_index = 0;
		this.ctx = null;
		this.firstInFullscreen = false;
		this.oldWidth = 0;		// for restoring non-fullscreen canvas after fullscreen
		this.oldHeight = 0;
		
		// Prevent selections and context menu on the canvas
		this.canvas.oncontextmenu = function (e) { if (e.preventDefault) e.preventDefault(); return false; };
		this.canvas.onselectstart = function (e) { if (e.preventDefault) e.preventDefault(); return false; };
			
		// In NW.js, prevent a drag-drop navigating the browser window.
		// Note if present the NW.js plugin will override ondrop.
		if (this.isNWjs)
		{
			window["ondragover"] = function(e) { e.preventDefault(); return false; };
			window["ondrop"] = function(e) { e.preventDefault(); return false; };
			
			// Also clear the cache since it will keep leaking files in the Cache folder otherwise.
			// (Note in debug mode window["nwgui"] is not available)
			if (window["nwgui"] && window["nwgui"]["App"]["clearCache"])
				window["nwgui"]["App"]["clearCache"]();
		}
		
		// Save the project data URL if one is provided. (Used in preview mode.)
		this.projectDataUrl = projectDataUrl || "";

		this.width = canvas.width;
		this.height = canvas.height;
		this.draw_width = this.width;
		this.draw_height = this.height;
		this.cssWidth = this.width;
		this.cssHeight = this.height;
		this.lastWindowWidth = window.innerWidth;
		this.lastWindowHeight = window.innerHeight;
		this.forceCanvasAlpha = false;		// note: now unused, left for backwards compat since plugins could modify it

		this.redraw = true;
		this.isSuspended = false;
		
		// Ensure now method present
		if (!Date.now) {
		  Date.now = function now() {
			return +new Date();
		  };
		}
		
		// Model
		this.plugins = [];
		this.types = {};
		this.types_lowercase = {};
		this.types_by_index = [];
		this.behaviors = [];
		this.layouts = {};
		this.layouts_by_index = [];
		this.eventsheets = {};
		this.eventsheets_by_index = [];
		this.wait_for_textures = [];        // for blocking until textures loaded
		this.triggers_to_postinit = [];
		this.all_global_vars = [];
		this.all_local_vars = [];
		
		this.solidBehavior = null;
		this.jumpthruBehavior = null;
		this.shadowcasterBehavior = null;

		// Death row (for objects pending full destroy) - mapped by type to cr.ObjectSet
		this.deathRow = {};
		this.hasPendingInstances = false;		// true if anything exists in create row or death row
		this.isInClearDeathRow = false;
		this.isInOnDestroy = 0;					// needs to support recursion so increments and decrements and is true if > 0
		this.isRunningEvents = false;
		this.isEndingLayout = false;
		
		// Creation row (for objects pending a full create)
		this.createRow = [];

		// save/load state variables (only handled at end of tick)
		this.isLoadingState = false;
		this.saveToSlot = "";
		this.loadFromSlot = "";
		this.loadFromJson = null;
		this.lastSaveJson = "";
		this.signalledContinuousPreview = false;
		this.suspendDrawing = false;		// for hiding display until continuous preview loads
		this.fireOnCreateAfterLoad = [];	// for delaying "On create" triggers until loading complete
		
        // dt1 = delta time by wall clock; dt = delta time after timescaling
		this.dt = 0;
        this.dt1 = 0;
		this.minimumFramerate = 30;
		this.logictime = 0;			// used to calculate CPUUtilisation
		this.cpuutilisation = 0;
        this.timescale = 1.0;
        this.kahanTime = new cr.KahanAdder();
		this.wallTime = new cr.KahanAdder();
		this.last_tick_time = 0;
		this.fps = 0;
		this.last_fps_time = 0;
		this.tickcount = 0;
		this.execcount = 0;
		this.framecount = 0;        	// for fps
		this.objectcount = 0;
		this.gpuTimeStartFrame = 0;		// for gpuutilisation - frame range for 1 second
		this.gpuTimeEndFrame = 0;
		this.gpuCurUtilisation = -1;	// current frame range's measurement, -1 if still pending
		this.gpuLastUtilisation = 0;	// value used for gpuutilisation
		this.changelayout = null;
		this.destroycallbacks = [];
		this.event_stack = [];
		this.event_stack_index = -1;
		this.localvar_stack = [[]];
		this.localvar_stack_index = 0;
		this.trigger_depth = 0;		// recursion depth for triggers
		this.pushEventStack(null);
		this.loop_stack = [];
		this.loop_stack_index = -1;
		this.next_uid = 0;
		this.next_puid = 0;		// permanent unique ids
		this.layout_first_tick = true;
		this.family_count = 0;
		this.suspend_events = [];
		this.raf_id = -1;
		this.timeout_id = -1;
		this.isloading = true;
		this.loadingprogress = 0;
		this.isNodeFullscreen = false;
		this.stackLocalCount = 0;	// number of stack-based local vars for recursion
		
		// For audio preloading
		this.audioInstance = null;
		
		// Games in iframes never get keyboard focus unless we allow at least
		// one click without calling preventDefault().
		this.had_a_click = false;
		this.isInUserInputEvent = false;

        // Instances requesting that they be ticked
		this.objects_to_pretick = new cr.ObjectSet();
        this.objects_to_tick = new cr.ObjectSet();
		this.objects_to_tick2 = new cr.ObjectSet();
		
		this.registered_collisions = [];
		this.temp_poly = new cr.CollisionPoly([]);
		this.temp_poly2 = new cr.CollisionPoly([]);

		this.allGroups = [];				// array of all event groups
        this.groups_by_name = {};
		this.cndsBySid = {};
		this.actsBySid = {};
		this.varsBySid = {};
		this.blocksBySid = {};
		
		this.running_layout = null;			// currently running layout
		this.layer_canvas = null;			// for layers "render-to-texture"
		this.layer_ctx = null;
		this.layer_tex = null;
		this.layout_tex = null;
		this.layout_canvas = null;
		this.layout_ctx = null;
		this.is_WebGL_context_lost = false;
		this.uses_background_blending = false;	// if any shader uses background blending, so entire layout renders to texture
		this.fx_tex = [null, null];
		this.fullscreen_scaling = 0;
		this.files_subfolder = "";			// path with project files
		this.objectsByUid = {};				// maps every in-use UID (as a string) to its instance
		
		this.loaderlogos = null;
		this.webFontNames = null;
		this.enhancedAccelerationPrecision = true;
		
		this.snapshotCanvas = null;
		this.snapshotData = "";
		
		this.objectRefTable = [];
		
		// Issue a warning if trying to preview html5 export on disk
		if (this.exportType === "html5" && location.protocol.substr(0, 4) === "file")
		{
			alert("Exported games won't work until you upload them. (When running on the file: protocol, browsers block many features from working for security reasons.)");
		}
		
		// kick off AJAX request for data.js
		this.requestProjectData();
	};
	
	Runtime.prototype.requestProjectData = function ()
	{
		var self = this;
		
		// WKWebView in Cordova breaks AJAX to local files. However the Cordova file API works, so use that as a workaround.
		if (this.isWKWebView)
		{
			this.fetchLocalFileViaCordovaAsText("data.js", function (str)
			{
				self.loadProject(JSON.parse(str));
				
			}, function (err)
			{
				alert("Error fetching data.js");
			});
			return;
		}
		
		var xhr;
		
		// Windows Phone 8 can't AJAX local files using the standards-based API, but
		// can if we use the old-school ActiveXObject. So use ActiveX on WP8 only.
		if (this.isWindowsPhone8)
			xhr = new ActiveXObject("Microsoft.XMLHTTP");
		else
			xhr = new XMLHttpRequest();
		
		// Use the project data URL if provided (used in preview mode), otherwise default to data.js or data.json depending on the platform.
		var datajs_filename = this.projectDataUrl || "data.js";
		
		// Work around stupid WACK limitation
		if (this.isWindows8App || this.isWindowsPhone8 || this.isWindowsPhone81 || this.isWindows10)
			datajs_filename = "data.json";
		
		xhr.open("GET", datajs_filename, true);
		
		// Check if responseType "json" is supported, and use it if so
		var supportsJsonResponse = false;
		
		if (("response" in xhr) && ("responseType" in xhr))
		{
			try {
				xhr["responseType"] = "json";
				supportsJsonResponse = (xhr["responseType"] === "json");
			}
			catch (e) {
				supportsJsonResponse = false;
			}
		}
		
		// If JSON not supported, try to force text mode instead
		if (!supportsJsonResponse && ("responseType" in xhr))
		{
			try {
				xhr["responseType"] = "text";
			}
			catch (e) {}
		}
		
		// Who knows what the server MIME type/charset configuration is, so try to force the browser to
		// correctly interpret the response as UTF-8 JSON data
		if ("overrideMimeType" in xhr)
		{
			try {
				xhr["overrideMimeType"]("application/json; charset=utf-8");
			}
			catch (e) {}
		}
		
		if (this.isWindowsPhone8)
		{
			// WP8 ActiveX doesn't support normal onload/onerror - use onreadystatechange instead.
			// Note WP8 doesn't support json response so skip that.
			xhr.onreadystatechange = function ()
			{
				if (xhr.readyState !== 4)
					return;

				self.loadProject(JSON.parse(xhr["responseText"]));
			};
		}
		else
		{
			xhr.onload = function ()
			{
				if (supportsJsonResponse)
				{
					self.loadProject(xhr["response"]);					// already parsed by browser
				}
				else
				{
					self.loadProject(JSON.parse(xhr["responseText"]));	// forced to sync parse JSON
				}
			};
			
			xhr.onerror = function (e)
			{
				cr.logerror("Error requesting " + datajs_filename + ":");
				cr.logerror(e);
			};
		}
		
		xhr.send();
	};
	
	// JSON data is loaded; init renderer and kick off asset loader
	Runtime.prototype.initRendererAndLoader = function ()
	{
		var self = this;
		var i, len, j, lenj, k, lenk, t, s, l, y;
		
		// Enable hi-dpi support (called Retina mode for legacy reasons). Note Android stock browser
		// reports devicePixelRatio incorrectly so disable on there.
		this.isRetina = (this.useHighDpi && !this.isAndroidStockBrowser);
		
		// iOS 8 bug: Safari apparently tries to auto-scale WebGL canvases, and does it wrong, so we can't size high-DPI non-fullscreen
		// canvases properly. To work around this, in non-fullscreen mode on iOS, disable retina mode (high-DPI display).
		if (this.fullscreen_mode === 0 && this.isiOS)
			this.isRetina = false;
		
		this.devicePixelRatio = (this.isRetina ? (window["devicePixelRatio"] || window["webkitDevicePixelRatio"] || window["mozDevicePixelRatio"] || window["msDevicePixelRatio"] || 1) : 1);
		
		// In case any singleglobal plugins destroy themselves on startup
		this.ClearDeathRow();
		
		var attribs;
		
		// Call setSize before getting context to avoid wastefully creating a canvas at the wrong size which
		// will immediately be resized
		if (this.fullscreen_mode > 0)
			this["setSize"](window.innerWidth, window.innerHeight, true);
		
		// Create renderer - check for WebGL support
		try {
			if (this.enableWebGL)
			{
				// No need for depth buffer or antialiasing. Use failIfMajorPerformanceCaveat to indicate
				// in Chrome that we would rather not get a software-rendered WebGL - it's better for us
				// to fall back to canvas2d in that case.
				attribs = {
					"alpha": true,
					"depth": false,
					"antialias": false,
					"failIfMajorPerformanceCaveat": true
				};
				
				this.gl = (this.canvas.getContext("webgl2", attribs) ||
						   this.canvas.getContext("webgl", attribs) ||
						   this.canvas.getContext("experimental-webgl", attribs));
			}
		}
		catch (e) {
		}
		
		// Using WebGL (was enabled and browser supports it)
		if (this.gl)
		{
			// Identify WebGL 2 and also try to identify unmasked renderer info
			var isWebGL2 = (this.gl.getParameter(this.gl.VERSION).indexOf("WebGL 2") === 0);
			var debug_ext = this.gl.getExtension("WEBGL_debug_renderer_info");
			
			if (debug_ext)
			{
				var unmasked_vendor = this.gl.getParameter(debug_ext.UNMASKED_VENDOR_WEBGL);
				var unmasked_renderer = this.gl.getParameter(debug_ext.UNMASKED_RENDERER_WEBGL);
				this.glUnmaskedRenderer = unmasked_renderer + " [" + unmasked_vendor + "]";
			}
			
			if (this.enableFrontToBack)
				this.glUnmaskedRenderer += " [front-to-back enabled]";
			
;
			
			this.overlay_canvas = document.createElement("canvas");
			this.canvas.parentNode.appendChild(this.overlay_canvas);
			this.overlay_canvas.oncontextmenu = function (e) { return false; };
			this.overlay_canvas.onselectstart = function (e) { return false; };
			this.overlay_canvas.width = Math.round(this.cssWidth * this.devicePixelRatio);
			this.overlay_canvas.height = Math.round(this.cssHeight * this.devicePixelRatio);
			this.overlay_canvas.style.width = this.cssWidth + "px";
			this.overlay_canvas.style.height = this.cssHeight + "px";
			this.overlay_canvas.style.position = "absolute";
			this.overlay_ctx = this.overlay_canvas.getContext("2d");
			
			this.glwrap = new cr.GLWrap(this.gl, this.isMobile, this.enableFrontToBack);
			this.glwrap.setSize(this.canvas.width, this.canvas.height);
			this.glwrap.enable_mipmaps = (this.downscalingQuality !== 0);
			
			this.ctx = null;
			
			this.canvas.addEventListener("webglcontextlost", function (ev) {
				ev.preventDefault();
				self.onContextLost();
				cr.logexport("[Construct 2] WebGL context lost");
				window["cr_setSuspended"](true);		// stop rendering
			}, false);
			
			this.canvas.addEventListener("webglcontextrestored", function (ev) {
				self.glwrap.initState();
				self.glwrap.setSize(self.glwrap.width, self.glwrap.height, true);
				self.layer_tex = null;
				self.layout_tex = null;
				self.fx_tex[0] = null;
				self.fx_tex[1] = null;
				self.onContextRestored();
				self.redraw = true;
				cr.logexport("[Construct 2] WebGL context restored");
				window["cr_setSuspended"](false);		// resume rendering
			}, false);
			
			// If no GPU profiling supported, leave the GPU utilisation at NaN
			if (!this.glwrap.supportsGpuProfiling())
				this.gpuLastUtilisation = NaN;
			
			// Look up all object type shader indices
			for (i = 0, len = this.types_by_index.length; i < len; i++)
			{
				t = this.types_by_index[i];
				
				for (j = 0, lenj = t.effect_types.length; j < lenj; j++)
				{
					s = t.effect_types[j];
					s.shaderindex = this.glwrap.getShaderIndex(s.id);
					s.preservesOpaqueness = this.glwrap.programPreservesOpaqueness(s.shaderindex);
					this.uses_background_blending = this.uses_background_blending || this.glwrap.programUsesDest(s.shaderindex);
				}
			}
			
			// Look up all layout shader indices
			for (i = 0, len = this.layouts_by_index.length; i < len; i++)
			{
				l = this.layouts_by_index[i];
				
				for (j = 0, lenj = l.effect_types.length; j < lenj; j++)
				{
					s = l.effect_types[j];
					s.shaderindex = this.glwrap.getShaderIndex(s.id);
					s.preservesOpaqueness = this.glwrap.programPreservesOpaqueness(s.shaderindex);
				}
				
				l.updateActiveEffects();		// update preserves opaqueness flag
				
				// Look up all layer shader indices
				for (j = 0, lenj = l.layers.length; j < lenj; j++)
				{
					y = l.layers[j];
					
					for (k = 0, lenk = y.effect_types.length; k < lenk; k++)
					{
						s = y.effect_types[k];
						s.shaderindex = this.glwrap.getShaderIndex(s.id);
						s.preservesOpaqueness = this.glwrap.programPreservesOpaqueness(s.shaderindex);
						this.uses_background_blending = this.uses_background_blending || this.glwrap.programUsesDest(s.shaderindex);
					}
					
					y.updateActiveEffects();		// update preserves opaqueness flag
				}
			}
		}
		else
		{
;
			
			attribs = {
				"alpha": true
			};
			this.ctx = this.canvas.getContext("2d", attribs);
			
			// Set image smoothing according to the project property
			this.ctx.imageSmoothingEnabled = this.linearSampling;
			
			this.overlay_canvas = null;
			this.overlay_ctx = null;
			
			// Mark GPU usage unavailable since measurements require WebGL
			this.gpuLastUtilisation = NaN;
		}
		
		this.tickFunc = function (timestamp) { self.tick(false, timestamp); };
		
		// In framed games, capture click and touch events and focus the game, so keyboard and other inputs work
		if (window != window.top && !this.isWinJS && !this.isWindowsPhone8)
		{
			document.addEventListener("mousedown", function () {
				window.focus();
			}, true);
			document.addEventListener("touchstart", function () {
				window.focus();
			}, true);
		}
		
		// Preview-only code
		if (this.isPreview)
		{
			// Load continuous preview
			if (window.location.search.indexOf("continuous") > -1)
			{
				cr.logexport("Reloading for continuous preview");
				this.loadFromSlot = "__c2_continuouspreview";
				this.suspendDrawing = true;
			}
			
			// Pause on blur (unfocus)
			if (this.pauseOnBlur && !this.isMobile)
			{
				window.addEventListener("focus", function ()
				{
					self["setSuspended"](false);
				});
				
				window.addEventListener("blur", function ()
				{
					// In debug mode, if the debug frame gets focus then this frame gets a blur event.
					// Don't let that cause suspending - we only want to suspend if neither this frame
					// nor the debugger have focus.
					var parent = window.parent;
					
					if (!parent || !parent.document.hasFocus())
						self["setSuspended"](true);
				});
			}
		}
		
		// Inform all plugins and behaviors of blur events so they can reset any keyboard key flags
		window.addEventListener("blur", function () {
			self.onWindowBlur();
		});
		
		// Unfocus form controls when a canvas touch event is made or mouse clicks in space.
		var unfocusFormControlFunc = function (e) {
			// Block middle-clicks creating the pan cursor
			if (e.type === "mousedown" && e.button === 1)
				e.preventDefault();
			
			// IE9 quirk: document.activeElement can point to the body tag, and calling blur() on the body
			// tag sends the window in to the background, which is annoying. Make sure blur is not called
			// on the body element.
			if (cr.isCanvasInputEvent(e) && document["activeElement"] && document["activeElement"] !== document.getElementsByTagName("body")[0] && document["activeElement"].blur)
			{
				// IE11 sometimes throws here with "incorrect function", which is kind of mysterious,
				// so just ignore that.
				try {
					document["activeElement"].blur();
				}
				catch (e) {}
			}
		}
		
		if (typeof PointerEvent !== "undefined")
		{
			document.addEventListener("pointerdown", unfocusFormControlFunc);
		}
		else if (window.navigator["msPointerEnabled"])
		{
			document.addEventListener("MSPointerDown", unfocusFormControlFunc);
		}
		else
		{
			document.addEventListener("touchstart", unfocusFormControlFunc);
		}
		
		document.addEventListener("mousedown", unfocusFormControlFunc);
		
		// Non-fullscreen games on retina displays never call setSize to enable hi-dpi display.
		// Do this now if the device has hi-dpi support.
		if (this.fullscreen_mode === 0 && this.isRetina && this.devicePixelRatio > 1)
		{
			this["setSize"](this.original_width, this.original_height, true);
		}
		else
		{
			// Otherwise update the size for the overlay canvas.
			this["setSize"](window.innerWidth, window.innerHeight, true);
		}
		
		this.tryLockOrientation();
		
		this.getready();	// determine things to preload
		this.go();			// run loading screen
		
		this.extra = {};
		cr.seal(this);
	};
	
	var webkitRepaintFlag = false;
	
	Runtime.prototype["setSize"] = function (w, h, force)
	{
		var offx = 0, offy = 0;
		var neww = 0, newh = 0, intscale = 0;
		
		// Ignore redundant events
		if (this.lastWindowWidth === w && this.lastWindowHeight === h && !force)
			return;
		
		this.lastWindowWidth = w;
		this.lastWindowHeight = h;
		
		var mode = this.fullscreen_mode;
		var orig_aspect, cur_aspect;
		
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || !!document["msFullscreenElement"] || document["fullScreen"] || this.isNodeFullscreen) && !this.isCordova;
		
		if (!isfullscreen && this.fullscreen_mode === 0 && !force)
			return;			// ignore size events when not fullscreen and not using a fullscreen-in-browser mode
		
		if (isfullscreen && this.fullscreen_scaling > 0)
			mode = this.fullscreen_scaling;
		
		var dpr = this.devicePixelRatio;
		
		// Letterbox or letterbox integer scale modes: adjust width and height and offset canvas accordingly
		if (mode >= 4)
		{
			orig_aspect = this.original_width / this.original_height;
			cur_aspect = w / h;
			
			// too wide: scale to fit height
			if (cur_aspect > orig_aspect)
			{
				neww = h * orig_aspect;
				
				if (mode === 5)	// integer scaling
				{
					// integer scale by device pixels, not CSS pixels, since DPR may be non-integral
					intscale = (neww * dpr) / this.original_width;
					if (intscale > 1)
						intscale = Math.floor(intscale);
					else if (intscale < 1)
						intscale = 1 / Math.ceil(1 / intscale);
					neww = this.original_width * intscale / dpr;
					newh = this.original_height * intscale / dpr;
					offx = (w - neww) / 2;
					offy = (h - newh) / 2;
					w = neww;
					h = newh;
				}
				else
				{
					offx = (w - neww) / 2;
					w = neww;
				}
			}
			// otherwise scale to fit width
			else
			{
				newh = w / orig_aspect;
				
				if (mode === 5)	// integer scaling
				{
					intscale = (newh * dpr) / this.original_height;
					if (intscale > 1)
						intscale = Math.floor(intscale);
					else if (intscale < 1)
						intscale = 1 / Math.ceil(1 / intscale);
					neww = this.original_width * intscale / dpr;
					newh = this.original_height * intscale / dpr;
					offx = (w - neww) / 2;
					offy = (h - newh) / 2;
					w = neww;
					h = newh;
				}
				else
				{
					offy = (h - newh) / 2;
					h = newh;
				}
			}
		}
		// Centered mode in NW.js: keep canvas size the same and just center it
		else if (this.isNWjs && this.isNodeFullscreen && this.fullscreen_mode_set === 0)
		{
			offx = Math.floor((w - this.original_width) / 2);
			offy = Math.floor((h - this.original_height) / 2);
			w = this.original_width;
			h = this.original_height;
		}
		
		if (mode < 2)
			this.aspect_scale = dpr;
		
		// hacks for iOS retina
		this.cssWidth = Math.round(w);
		this.cssHeight = Math.round(h);
		this.width = Math.round(w * dpr);
		this.height = Math.round(h * dpr);
		this.redraw = true;
		
		if (this.wantFullscreenScalingQuality)
		{
			this.draw_width = this.width;
			this.draw_height = this.height;
			this.fullscreenScalingQuality = true;
		}
		else
		{
			// Render directly even in low-res scale mode if the display area is smaller than the window size area,
			// or in crop mode (since no engine scaling happens)
			if ((this.width < this.original_width && this.height < this.original_height) || mode === 1)
			{
				this.draw_width = this.width;
				this.draw_height = this.height;
				this.fullscreenScalingQuality = true;
			}
			else
			{
				this.draw_width = this.original_width;
				this.draw_height = this.original_height;
				this.fullscreenScalingQuality = false;
				
				/*var orig_aspect = this.original_width / this.original_height;
				var cur_aspect = this.width / this.height;
				
				// note mode 2 (scale inner) inverts this logic and will use window width when width wider.
				if ((this.fullscreen_mode !== 2 && cur_aspect > orig_aspect) || (this.fullscreen_mode === 2 && cur_aspect < orig_aspect))
					this.aspect_scale = this.height / this.original_height;
				else
					this.aspect_scale = this.width / this.original_width;*/
				
				// Scale inner or scale outer mode: adjust the draw size to be proportional
				// to the window size, since the draw size is simply stretched-to-fit in the window
				if (mode === 2)		// scale inner
				{
					orig_aspect = this.original_width / this.original_height;
					cur_aspect = this.lastWindowWidth / this.lastWindowHeight;
					
					if (cur_aspect < orig_aspect)
						this.draw_width = this.draw_height * cur_aspect;
					else if (cur_aspect > orig_aspect)
						this.draw_height = this.draw_width / cur_aspect;
				}
				else if (mode === 3)
				{
					orig_aspect = this.original_width / this.original_height;
					cur_aspect = this.lastWindowWidth / this.lastWindowHeight;
					
					if (cur_aspect > orig_aspect)
						this.draw_width = this.draw_height * cur_aspect;
					else if (cur_aspect < orig_aspect)
						this.draw_height = this.draw_width / cur_aspect;
				}
			}
		}
		
		if (this.canvas)
		{
			this.canvas.width = Math.round(w * dpr);
			this.canvas.height = Math.round(h * dpr);
			
			if (this.isRetina)
			{
				this.canvas.style.width = Math.round(w) + "px";
				this.canvas.style.height = Math.round(h) + "px";
			}
			
			this.canvas.style.marginLeft = Math.floor(offx) + "px";
			this.canvas.style.marginTop = Math.floor(offy) + "px";
		}
		
		if (this.overlay_canvas)
		{
			this.overlay_canvas.width = Math.round(w * dpr);
			this.overlay_canvas.height = Math.round(h * dpr);
			
			this.overlay_canvas.style.width = this.cssWidth + "px";
			this.overlay_canvas.style.height = this.cssHeight + "px";
			
			this.overlay_canvas.style.left = Math.floor(offx) + "px";
			this.overlay_canvas.style.top = Math.floor(offy) + "px";
		}

		if (this.glwrap)
		{
			this.glwrap.setSize(Math.round(w * dpr), Math.round(h * dpr));
		}
		
		if (this.ctx)
		{
			// Re-apply the image smoothing property, since resizing the canvas resets its state
			this.ctx.imageSmoothingEnabled = this.linearSampling;
		}
		
		// Try to lock orientation to the project setting
		this.tryLockOrientation();
		
		// Work around iPhone landscape orientation weirdness. In some cases Safari on iPhone in landscape will
		// bring the address bar down over the page with a scroll offset. This means that despite reporting the
		// correct viewport size and getting the right canvas size, it appears offset under the address bar with
		// an empty space beneath. Forcing the scroll position back to 0 appears to correct this.
		if (this.isiPhone && !this.isCordova)
		{
			window.scrollTo(0, 0);
		}
	};
	
	Runtime.prototype.tryLockOrientation = function ()
	{
		if (!this.autoLockOrientation || this.orientations === 0)
			return;
		
		var orientation = "portrait";
		
		if (this.orientations === 2)
			orientation = "landscape";
		
		// Note IE/Edge can throw exceptions here if in an iframe (WrongDocumentError), which also affects the debugger.
		try {
			// latest API
			if (screen["orientation"] && screen["orientation"]["lock"])
				screen["orientation"]["lock"](orientation).catch(function(){});
			// legacy method names
			else if (screen["lockOrientation"])
				screen["lockOrientation"](orientation);
			else if (screen["webkitLockOrientation"])
				screen["webkitLockOrientation"](orientation);
			else if (screen["mozLockOrientation"])
				screen["mozLockOrientation"](orientation);
			else if (screen["msLockOrientation"])
				screen["msLockOrientation"](orientation);
		}
		catch (e)
		{
			if (console && console.warn)
				console.warn("Failed to lock orientation: ", e);
		}
	};
	
	Runtime.prototype.onContextLost = function ()
	{
		this.glwrap.contextLost();
		
		this.is_WebGL_context_lost = true;
		
		var i, len, t;
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onLostWebGLContext)
				t.onLostWebGLContext();
		}
	};
	
	Runtime.prototype.onContextRestored = function ()
	{
		this.is_WebGL_context_lost = false;
		
		// Recreate all object textures
		var i, len, t;
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onRestoreWebGLContext)
				t.onRestoreWebGLContext();
		}
	};
	
	var caf = window["cancelAnimationFrame"] ||
	  window["mozCancelAnimationFrame"]    ||
	  window["webkitCancelAnimationFrame"] ||
	  window["msCancelAnimationFrame"]     ||
	  window["oCancelAnimationFrame"];
	
	Runtime.prototype["setSuspended"] = function (s)
	{
		var i, len;
		var self = this;
		
		if (s && !this.isSuspended)
		{
			cr.logexport("[Construct 2] Suspending");
			this.isSuspended = true;			// next tick will be last
			
			if (this.raf_id !== -1)
				caf(this.raf_id);
			if (this.timeout_id !== -1)
				clearTimeout(this.timeout_id);
			
			for (i = 0, len = this.suspend_events.length; i < len; i++)
				this.suspend_events[i](true);
				
		}
		else if (!s && this.isSuspended)
		{
			cr.logexport("[Construct 2] Resuming");
			this.isSuspended = false;
			this.last_tick_time = cr.performance_now();	// ensure first tick is a zero-dt one
			this.last_fps_time = cr.performance_now();	// reset FPS counter
			this.framecount = 0;
			this.logictime = 0;
			
			for (i = 0, len = this.suspend_events.length; i < len; i++)
				this.suspend_events[i](false);
				
			
			this.tick(false);						// kick off runtime again
		}
	};
	
	Runtime.prototype.addSuspendCallback = function (f)
	{
		this.suspend_events.push(f);
	};
	
	Runtime.prototype.GetObjectReference = function (i)
	{
;
		return this.objectRefTable[i];
	};
	
	// Load the runtime scripts and data
	Runtime.prototype.loadProject = function (data_response)
	{
;
		if (!data_response || !data_response["project"])
			cr.logerror("Project model unavailable");
		
		var pm = data_response["project"];
		
		this.name = pm[0];
		this.first_layout = pm[1];
		
		// Determine fullscreen mode - needed before object creation
		this.fullscreen_mode = pm[12];	// 0 = off, 1 = crop, 2 = scale inner, 3 = scale outer, 4 = letterbox scale, 5 = integer letterbox scale
		this.fullscreen_mode_set = pm[12];
		
		this.original_width = pm[10];
		this.original_height = pm[11];
		
		this.parallax_x_origin = this.original_width / 2;
		this.parallax_y_origin = this.original_height / 2;
		
		// Determine loader layout - also needed before object creation
		this.uses_loader_layout = pm[18];
		
		// Determine loader style - need to start loading logo before anything else if it's to be shown
		this.loaderstyle = pm[19];
		this.iconsFolder = pm[28];
		this.webFontNames = pm[29];
		
		if (this.loaderstyle === 0)
		{
			var loaderImage = new Image();
			loaderImage.crossOrigin = "anonymous";
			
			this.loaderlogos = {
				logo: loaderImage
			};
			
			// clear the loader logo image if an error occurs so we don't try to draw it
			var loaderlogos = this.loaderlogos;
			loaderImage.addEventListener("error", function () {
				loaderlogos.logo = null;
			});
				
			// In preview mode allow for requesting loading-logo.png from blob project files list
			var loadingLogoUrl;
			if (this.isPreview)
				loadingLogoUrl = this.getProjectFileUrl("loading-logo.png");
			else
				loadingLogoUrl = this.iconsFolder + "loading-logo.png";
			this.setImageSrc(loaderImage, loadingLogoUrl);
		}
		else if (this.loaderstyle === 4)	// c3 splash
		{
			var loaderC3logo = new Image();
			loaderC3logo.src = "data:image/svg+xml;base64,[encoded data, 11744 characters]";
			
			//var loaderPowered_1024 = new Image();
			//loaderPowered_1024.src = /*{splash}*/"splash-images/splash-poweredby-1024.png";
			var loaderPowered_512 = new Image();
			loaderPowered_512.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAABABAMAAACekdKMAAAAMFBMVEUAAAByfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYYgo7vbAAAAD3RSTlMAmd137hFVqjO7zCKIRGZ881JRAAAFY0lEQVR42u2aPW/bVhSGn1iiPizZ8D+QgSJBNglFmiboQA0NUKAD1XotYA0BOspAkZnqx24vnTrIQNCpg4QkQMcYKDoW9j9w5y6qLNqWFDlvh3tJUa6ddHBhAuS7SKBIQffhuee851CQKVOmTJkyZcqUKXmqa8O+uyNlABKsFRmdP0o5AOle2gHoWdoBzFrpBHACwPeetJNmAFSlt6kGwGH0LqUAVqRuqgHkpEaqAVRkfvOWr+Dh9Rc4j6VvwnrhPNAbgK2ezj+yx7Z6GjUuASi4Ch4lHQAGwBeRK1rROQAdUx5K0hE4NUkaAdS02ZEuoms2wkCSgvYSgFJfkn6CobkSPG0mEcAOrHnGFOxAToHNjk/MygLgO/PxL0BN9yRdQKlufMQAoCdJGi8BOJYknXTJ269cu9HtdqNb4Ni6oimUbFb0dAZQ1DmULJ+TFtT0qQHwg73mLlCMfFUMQPR5VWrbmttNHICC1KDiSZ+w3pMOwFPDhP4IYFtv4ZX0nIorbUBNmjWAiqdZg5fmNFd6jvPhJQDjAV9LI0r28IpmycsBZalNWfoRWJcuoKY9c7cCkwpOwdcIqNT1BmrSgQkNbQAvpAGOF+aHOIBpCxhKR/TVBNjVOHkAOgrg2N6aoU7sklmVidtD7bNm/fIrjaBmk+Sxea142iBnTyguAfjZZscNXLOdXFM8EgVgXRpDz/6yonTEts6AXdV1APTVoKigFSXEmj3XN0kSV01WFQwAHC8O4Mhagye8NgHS137SALzsS3dxwhtXkQ4oaw64eqomOFKXbc2xJbFLTX8DOHYnsKsJu2Gdu8IIuTqjrMB8eyM5ACIFXQo2S5t7lNMM6AdFTaCgADqahHW8EQIohBk9rzEdE+JXAtjVmHWpDTmplUAA96Ea/bJDnVKSWjgaFTSHqubgmjVDXQchgFy4wBXNcXV6LYC8RjbEypomcCAyBopRShxqAn21yenM8WawqjOoKVbmLYBFndeImsnyVwJY1RR8NW1FTRiA6UNTCsP6fKwLONQBZTXx1WVbT0KbtwygvAxg81oAZU0xe2QYxkmyrDDx2DzWBXTUZFs7dNQwHUEMwN4VAKb47wOQ1wj8Gx0+/Y8A8powVJtt7XOoxn8A8N4IqCrA8cJUmzgAl3JAUWN8tSjqDX0NwI/XbwugGLe178oBK7bBaBesm0gggOUqwLrOHe8c1jV2NAMOw/XFAFRth2dr/burANS1cSc0C8kDsB7zAZvgKCjoLTiaFqwpmvwLQC7e2b3PB4CrZj48KXkAKktOEPr6VqeAr680udzEWACluK0LreJ1ThBe6+w4HkfJAhD1AtbAu3qgPcDVX2oCK9bqxwFQ1+fRsVW7va/uBZpAWaND652TCGBoM1rHHN6VmQlsq6cdE+4mC75oLAC49prKB1zTDd6NukHIKahbIkkEsGrmAQWZbZqXNLClrg04dTP4KnjnCwBfmgkZT7WD45k94C8BmA3MPGBgeqcbfQBxwwDsRMi3N7Iqc3dzsuVhV5ofkevpJOoGKUn6Ax5L92F49USoayZCls08uQDiM0ETCWPb8k7DMmH02SICGC7ayVhnsABwEpsZQkfxUpI4ALGpsFn4xCbHcVjnwgnvAkBh0U6CbzqrXgzA1E6FB9YOJGcacgUAfl36t0Ro/SIDUDEL/D1WBcJr5q0oRoJ2LQZgZJ4LPIu2RCPJAJafDLm2Yr2OHmQ4H0vzBksA2PKl38L3fY0aLAFYejK0ZpJhelVKzkj8dlRNzkj8dpRP0DTkVjTUXroB9PVnqtefS9I05BZU8NOdAyUl5q8RtwUg6KYcwP1Up8CanpMpU6ZMmTLdgP4BRYsi23xEdOAAAAAASUVORK5CYII=";
			var loaderPowered_256 = new Image();
			loaderPowered_256.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAgCAYAAAD9qabkAAAGsElEQVR42u1cLVQcVxSOqKmoqKipqKioQVRUYCoqImoqEBU1EYiIiIT94SQ9O8tw2p0Zcg4VERGImAhERAyiAoGJiEBgEAgEBoGIwCAQ7bvv3fve3TdvZndhQWy+75wR7My+P+797u/sgwcAAAAAAAAAAAAAAAAAMGd0svJ5Z1hsdvPyh7bn6D49RxdODbg39Iblsghe6jL3n6xlW0s4qZuhOyw+dYflf2vD0W9tz9F9eo4unBpwfwKaFc9E8NouQwaHnaz6EScGAgAWkACMq3qV8gC6WfXO3LtmErgECYAAgIX0AIpPjcKZV9+b+2csoEc4NRAA8BkRgH1uo/pdBBQ5ARAA8JkRQKfzz5eeACJhXsv+WupkxVtz74LDiWsTLuzFz0nCkRKLTSRD92m8+N76+suvJCx58aL6Wj7P8/wLGo9yFD5fkRUfU3P0htVjnn+5u1H+YZ474fXupPfjlJf2Zf9uyOQ/zbe+i54/pzH/LIpvbkIAtNfusNoy/5tTOU8KxWjd4azM+u1eqsfNpF0OZL+QdGBuBNAZjlbC96tVyREkr6zYCYq1tSQCTUKesJan/J3XtfkH5a+iXJoUzN8HLYnLPSIIPwaThJn/w9hzRnnHSMjkQxrGu+wPqocxWSjFj/d+KmPNQgAUZiXnN2P1zTnYvWSjR3KWRDQJsvsJHhswNwLo59VDESgSLm/RQ4JwTyzkev7yW2vBgoL1lJKfpRSCkotB2Iuz+jrLHTd3+UZZuPfe4g6rFSEqa+lF8cz+YgIQkiHLHSuN348hLrlP+1JEc05ziPchXoReA33OIdN5k9fUTgCGUIhYeZ7eoPpFz0MeEHtEl24vo9UaYQ6LEXI2wFwIgBTaKMYxK8ZJUGZRimI/6TWwEJIyitsuityN3G5bceBnWdGXxwmC52cl6w7+/tmHG4nKBClQTCbeAzBk1RCv7zM5vEt5QF4JWeFoLbIGIcWY1IRQZiGADlv5epghZ+PcfrOe3abz916ECQMg5cBEAmgqA5KQiaUhYRYX1Fl5tlhGGZvCBm+ljEXUgk4KHQusVRYjsG7caksTUBw6eCIxXkCStMhCivKxC6wIYDNFcsFjSZc6vVdj4nFWwLdtBDjvJCDNqwmsn7uwKA4D2GNxnoep4EDKgSk8gAmXiWd1/KsU+UrH2S1W9ZVK5llSIKWzY9kyo1Mk9jauNUGECkTxr/IIJI4/aOxkZIvZyTlubiGAtUEIcZrGo/klyajX0Na+O08CWAtkfexDDU686jCgZ0IuPvOPkHDgVh4A3U+7ty4JlYrXx59zVnIs0eYVafRIC6y4thJaiOVW8f+TWsJwmou9jzYCCPuZoiuSFdCs4YgV7dl9EIA681P1/3sVeyFdIceWdQHATEnA2vdMGSrOyre7rSF5R4quXWkRWPEI/Jo4fpX4X5fhfAnPuMNt7zI4T8C59G0EIPux3Y4TxhPFClWFkOS8SwLw56YIQJKnEgYoD+pazhMA5k8AnITTipu2Wqy8WUhGPTcJLSEPEVgigfg+KZiK/+OcwUGcK5iENgKgTHtbibJhvL1UQvPOcgCcg+iosxo/49GqvSbkJQDg1gTg+gJYuBtcTV3ai8MIVevetvcjK0pxtnyeUvRQYShOpFx2GwLgpOEVeyarU41nXvUVi5zKg9gyIec7ZiEA3eiUVnSXT/Gfb/h17AspSXgFAHdCAKzE2+I2xwpO7mgo3RWHLdbsOnbvxxJZTDJkoccUxiQOfZKPKhURCXAX3r4mjjYC0LkGq7SJyobrHiyPpVOR1yBNUNuJs319oz6ArHwf7yeQTb1KIUlUdv2vdNkVAO6MALjMp1twd12MbBXpQsZNtfbadlzV+VZTCF8ZcGOkLGyIiV0ykiyji9PLN6o770Bc+kkE4LwA33AT5xcOQ5tztaJIZVPvg58deWs9IwEEL8QmObd5/INU12I6JGoujQLAXAmAQJZGdeTVfkMgpfziHvt3BxoU0pMLJwvTbrHNjF+k5qeuQR3PTyIA77lIbF+7irN+oklHdd3pSgE1KPWECGYggGPak++fGG9Z3m0Kd0LsH5qlAGAi/C8CGRfzNuOQxeaXcjYpe58qHaaU177809CsQvX7aV5msbH2wD3rPJDRatzmKx6DHS8KJybtZ5rvuHCg6MkLOjK/zDntT4JJKdSFUO586Dwn9fM/DYnTy2lzIgAALAjCy0HpEAEAgEUmAA5v+nk9RAEAYEHhqh1SUSlO2tqyAQBYIMRJx/i3CgAAWGwCkN8bOILyAwAAAAAAAAAA1PA/vFyuZA1j3XwAAAAASUVORK5CYII=";
			var loaderPowered_128 = new Image();
			loaderPowered_128.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAQCAYAAADeWHeIAAAD30lEQVRo3u1YW0hUQRi2C4UV0RWCLtBDUVBRPQRFRD1UUBSBiWRhUlBUqHvOqu26c3aqPTNnTSWkwJTIyK5GWQ/dCJKgC0RGBBVdoQtFpEZRdHnIvn/OrnvUVdKXBGdgOGdm/rmc+b75/n9OSopOOumk039PuUExyx9yFsdzASue0pvXa1riqhmKTPTW+S3BTGZnazR7tqGXTSZempasp2ww8RF117ZzPqw3rtdg8ofJ5dS2deKEYYldGs0eEgCburN1Mw0jFURo8FtOWBOgDxJAbaglbJDgpGpn9ibDkm9g90epBDY/n+8dR1KcXls7gGx8lr0KKrIvAYhdgJxF7QCnBON9NZn8gnmclpaWfsreEodRfw/5fqxPFmzeYpzfeJ4LBJyR7vxyNdWj7y/MX6OeSQjgfod4SuuEzSWDl41CuYzGbf3WsNiB9oBGvQsCcM4HYeNu+5iM+EORRdj8JoNH51E93iMA7BneB4IUr/ItOd8dQ55B2/cczoe7oInn8NMLMXYRxr5p8tIxOTw6AX0f+5hYC3XZgvdGg+2ZqexhS66H4pEtnA/B+yn0OxAMirGw+4ZyJhGHnoqISQkgG4wie3we5yPIhaGuAoBvw1rueNTjkWE5aRr1LmIA5Pe0abETVEmnyEOO/ih/IuBhsx+ghwyjLBXvr1F/zAzLjBjQje7pl0/IJh5ggjTVGP8QEYDm8gBYQSTy2PmQX2C8dXGF6I4L8IXkMlpTICBHk2Lk8ugk6qNUCOvVqHdQAFEZ33yTi2mJNnkW2WxrLx9ic9eYlr0cIN0giSZgAepKtJ0GaOuJDDElaCKZ95CrHva72xOA5lGAe+3C8rwRFlsB4JXuEsBkzhx8V7PrzmQd2v0oFyIf0Yj/QwzgAaYUm3c0XqabAUmyPyhm5JSXD1buAXJNJFBlS74DMWpICWJjPzCYk9l+3A4EYKIc5aokAd8KUieSfyrTE+WfnRDATvh6mUFy7xLASVOKxsQtUgaNeDcIQP8EAO5nOj1KVi1xkYK/RLCI04UAj/x2jDDH6YQmAjg7m3w7+m6MKUadGXbS2xOgkEWnxwJF5S4wRjG5GJJrijXIRaB9Cdqr0K8lKQEwD8i2QamT69Ly3VsNjSGa0f6BYheNeMdTVky+trP2vCJ7tjrVTF4nmfX7S4YmCIDTZYmoh0xLYXew7Y3CSUO/CyTlCADzKDZof2tQfpvbc9U8ygWIKMUgSi1CkclELCKeERKb6YaQ7EeQCizhymidBD7FKx6iVhOhNNp9luTirsmjC/RO9LGkglr8z6BrbTyO0KkPJXVdhTuIxyQ66dRr0l+bhK3bGIpz1QAAAABJRU5ErkJggg==";
			
			//var loaderWebsite_1024 = new Image();
			//loaderWebsite_1024.src = /*{splash}*/"splash-images/splash-website-1024.png";
			var loaderWebsite_512 = new Image();
			loaderWebsite_512.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAABABAMAAACekdKMAAAAMFBMVEUAAAByfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYZyfYYgo7vbAAAAD3RSTlMAdxHdu4hmVZnuRMwzqiLYE4y2AAAF2klEQVR42u2Yv28jaRnHP/bG8SVxvA4ncRw6sCVoVlc4R8FJCOHlGkq7olrJy18QFzRUWSqkE1KibZAQOkc6Ua+5htJGW12BEihpHAqKE0XWcZzEcXIfinf8a8zunsTNoZPmW80zfsbvzGfmfX5BqlSpUqVKlSpVqlSpUqVKlSrV/1EHPv4SfN7wMgWQAkgBpABSAF+2HngOsG8PyDpsWwHy3gJU/UHM3o0uyxw4/G44zJf1LQC8INP3ugXAejOcrqn6EIf80puiQwDqHgF8S987nfsAcN+HfEd/M/v34TtwT9WLBABsewXQtAHkHfVtATlHAUsjboer1lX9O8BmV/V9AIf5tnpRATZrqj9aBLBmHMCe6l0lDmD3V6ofABS6YaHkAOTDo7XtADmvwjNuOAlYWnE7XNVXdXgKlFW1BDjcU7UzP/14AUA/DmA7+PwkDuCHqmHFvWih5ABknQAFvQW2fFq1AxxrD+iOK3E72o8+ItP2E8joz8jXHQDqnyj0vYRN/ZBMzdvZ/lYfwRKAA0c93nZSWYoB93VS4q/6GPL6WzJtOwkGwboVyOkAqHqy5jnQ1BJkvSNuE7bC74F/O4BjB0BGj0DvgJwTWHMAvOvFAoAblgHkHR9BsWspDuAEaNqBz7wB3nWQIICyLdiyPQH2LWW8ArpjO5B3QNwGoGYLKDquUA9hoekuhDvPaoWqh0BRK3MApRiAeyH8nPk0BuAuBNxzOIgWGiYI4NgGHLtnBZqeFhxBwUvPIefnxO0QNy6i3NUqGHbFPW9ATwG69jgL+aIbvvQAgBiA/bDr3/AqBuBzgC1vKUYL9e0lB+CeHShPNixBdwK1CWTstAewZWfVBnLRl7BvKUoPZBxNn5G6R+H9wS92Tl8OoGkp4BzEALwAWPOGTLRQ2VJyAHLeQnew7UOyjqBpjy1L/Quo2li1o5cD8M+d1lb4jCk4WQKw5qS3XOSsAujaAyjuvPUyANvRQs98khyAvAOK3uY9D+XOviXO7O1bCdVR3AY2wusNh2FXoEsACjr59msAhBprpRJcABCSn+pJcgCy7TsydrLtS3IewgNPKE944GOaY1bt+S0uHdY8XQTAx+rwnVcCiNxfAeD+DMDDBHuB/pg1G/Tv2PIEtj2kPmLbE7p3rNpfEABvq/756wCgbK9qj71xpWoLMt5mvWXdp1mvWLW/KAD+0jfkxf8FwIuvoBs8s7Q3hKqtfStQdLDuLlmv8p6zagMb8/uahYNYDAhBsub1K2PA6wAsBJsEAWx50h/Btk+aE4Du3bYN6F/nQtaL2/MsAGx5E2WB4SoAMtp7bRZ4BYBpjkkWQM6P2jew7mF7ANAcf+YR7I03Qp6O2/M64JmNeR1wvQRgZyc6LC0DyK7WAZuOXgYg5/VXAKDgtYeQdRBe7LEHY6BqObyhuD2vBMuW5pXg1RKAWvAtxwFMc99CJZh5eSGUj2rLhCdCbX0C1MehfN3Qa2DN9pD/Zs96gWzNWS9QDr3ADMCBT0KLUILmIoCurdB9znqB73k+9YkDoBYx+gByyQHoayt08I0wIomif/R1L9vPhz3Y9w/A9x1Nu8H1qBucATj2Eii07UHZxhxA2Q7wqaEb7EG2bmPq83zYWwawHxqj5rAXiu1ktB8+42rgQF6fhqbuilW7HnpzH5Gp2ZnOA/qOWAKQ039QLHsHPPN3VKY/nnlxxB91Ng9408nMp25nGUBO3yO4bDpuZZMBUA2Y16LtnA3Zm7pPWbVrvphNhCaV+einsQyAg3D6E8I8b3f6Yyac7y5MhOY+NV8sA6A5d6npMKk8eBluLSr0umE/lsNzx+y6u7OZYIPZ8O99YgDW2xptokJ7AUAA9uOD+UxwwafubgxAvjZz+TQxAJlQbhSn445yCNRn06y3ZD8fHkVT4YtvzKfC3yQOgExT/xa6nX9pYwag+Kb+nOZsKvzTBZ/nw6MYAPJl/XW4sJlcFEiVKlWqVKlSpUqVKlWqVKlSpUqV6uuu/wDv59f7eYDABQAAAABJRU5ErkJggg==";
			var loaderWebsite_256 = new Image();
			loaderWebsite_256.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAAgCAYAAAD9qabkAAAHF0lEQVR42u1aLXBcVRSuwCAQCAwCgcBUVCBiKhAVGEREBQYRUVFmyP40kzC7m83A+0lngkAgEDURFRE1iIgIDKIiIiYCERETERERE1FRzrn3nPfOve/uJm93szDD983stHnv3f9zvvN3HzwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/pfojvLj3qh43x+VX2I+NTqj7BueB83nD0gJAAIAAQAACAAEAMyN/qh4zpvbGxa/x+96o/zQbfywfGSfb4xffuqf53/Tvxf8f34W9bvi+t0u3jT6pbF8++y7edrTu28nrYv7646KV9p/d1jc0Hxfx2thbG2VH/dG5W5vmJ+5fkXYWPCae+LeX/yY5590/Twu9Rm1ycbj8QeNvof5r7SX57bv7qD42uzzlb6zP96fesz8iudOa/9L9n5/q+R5+3epPaDnbj2dcfl58Jz2rSv9yN6cUn99nftt85lAFjtCFs+6o3KV+ntbt82PUiSysfHyI7vvfEa9YXmgZ0TP11Pz4HVBcxeEWpmL0/hw6Nk7EdidUIDKp5408t9qZSyfhsJHB+vbXnNfgbA4gSveubHnbJ8U/HHxRaWYRCA8fyYDbsNC1h+UX+m3P4x3P6sUn5TCCTKvS5VgWAxiAhAyYfI7EcHfM0SwFym/kEp+aOZxo8oi69mUfi6UjN23qgiqHDxGtZ5ydRYC8GTk2jAh7flx3FocMTEJ3DafaQQge3Lt1umeiRHhczTn5Qm0Hjc+o97g58dsBGR+r9XgyPmsQ3PvwdWzB6RKLiRwkrTgZMXUPYs9CFVSf8DlasJ7eGvdu1nbT1sPWZO1wJUcl09UkIy1O0qRnCMGsdqhtRYrRCSRIB0n6Np3n6yq7GGwNiYgVVzrMUxyuXXMTiT4bQmg2ms6TyanatzuLx9W+2CsfJsQwBDAZWf408PwLFWBi81avvIDS4Lx3liDhBDg/gkgi62wc8VImL01DK2IU05meRIc8RRuggMb7j5Ua+gZPd9veA/b3rLO2z4R0qyIsBxPUIojFWrnUotLGbvuAQkawavcYVp7om9HGEwe8u1eyovQ/aV3R98b0r2NABqhS0sCUGucDG3I4sZrnY0Ayt3mWrM133fxKiLL5Blp+MByAAJYTh5gRSzmgVoEr/zlgTIyWzNrgW1sXgmWHBgrp/5N//5pLV0d/9fu5LztAyuvcSO5uhPDg8HuY3ZB9VsVzHSM6q36bcqYVLhaqa5vc6GXQQBdCWus9Q+8ANoXO/YsBBB7Ul6hXa7nvRJ5TQj5sXf9wx97KP59tgoCWAJYuSTeuwhcRbKA1TuKj61VtK6bJhKrmJYPViy6vnsxKJ+o96DjLKr9XQVxlm81F8DK1pYA3BgUPmisGyS6xnVYsTwPYPLc560CtCEAEy5M/Wk4AgJYRhggcRpb3b4kqjT5Jn8HSbsgXyAuHQv2poud3WFl/E7/5lCi8h6imHje9lGGe3BXAjDfZpP3xSfs1OVvSwA2rGEy85UI32fspdy7ByA5lX+bAO6y7ygDLhlq2cUtvrCbzdbXCyy7bk6oThKK4iyzutVWaHzpKj+vBMEk9RbVvlmh8OFMU1CpREVCykqRivFjpRVFPZstBNh9zL/4O0kwXtmwZxYC0BClBQGcxmPaioXPsGdr908AckaUjwEB/Edgyn4XquxhiMB16KpskzWFzZftfPv8PHDxq2w4lXGMZ7HI9mF5iS1sfsX/j0OdSglIKbwC8bpo3eSFNJNXPgFqS3utCEBKgCkF8rkN6ssQhCa/7koANq6PS6K6tigE2Jt4fnQ3wCrptPnMSwD1GXHfxUqKIINcBIVLIIBleAEilKkaOx+eCmLq0OrSVrNE5qxtfYnjMJmInLG9qyFHtf3KxSQB1sSb1PsP4v7ri1D52YuxzzM4AdXKCJGRTZq1JID16sIUJQQ12eZDAUe2l5bMyNN5477f9gnXulQ5hQDqGvm+ztNXOOqLODofXpfz7nhsmpuO7ZWfiJCSleGdgfR8UnvehgDcs+1iU8uGQfXJeQeumlLtja3W8BpSFRtgIXkAORRJ+KXcMBag1AE468qVA5OwS8eTxfNJichZ2pubfpvR91kyuUTKH8+fw44qLrdJKBqzcYuubRLQZf997B3dZjuPiZTXHXwrdf9pYwYXmYJ10qUm8djsfLhGb5OSM86nsedtCUBJILXvqblUHtOUkAeYEyxMfIBJBfSlwZ2p10HpHX+TIgh24/id1sgX1V6EdDNV2hJX8pneHksl58LQwY/Pv5SXUyn0hAQjj8Hv4rl4Jc3WqlIXVQYmWTGnoPKdKd/tTEtquhIeWc6qjCbeho6Z2psO75uOQ+TeZj6pPWdvwO2b8Qrqcy0f6e3F2/a9k5C9ao2yHhueAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3Cv+AbbV0PbhRsAiAAAAAElFTkSuQmCC";
			var loaderWebsite_128 = new Image();
			loaderWebsite_128.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAQCAYAAADeWHeIAAAEfElEQVRo3u1XWYgcRRiOiQohnlGMxFs0IopG8HpQ44WiohhNFI8HBROPuDvd0052J101te50Vc+a7INgYBcERRLFVaKCeEYlICFREqOCiQ+iEAkKEiHgg3is31dd1dNOMrtBfMhA/9B0Vf9HVf3HV3/PmFFSSSWVVFJJJZVUUk5VafbU1Mj8Q8FOKPTL1Ya5ryccF65KTolkujx3QCPte0KpozgOYnNTKE3AN+d9Sh1TbegVXpZ6oUwWT8WP1OqT/FwpdWQYm4erwshImsvyNZVZEAozgLWeHBw0J1jdOL02lHoR5cGrByo929k4PBTpg+ANkW/1hYkx3ofgralrPY/zQOqrscZjQZzeWI2bV7WDg3XC0dlZsJObaScS+q7JycnDOu14nYpKz2RAM3/ooaK9mhg5lza5Fv0GuQeg/xVsvY5vVxzyCcCgYMM/8f2UeuZkHOYvOGRJ5iD9YSB0yHeWHOZe8P/or+t5Tm8PAzUVn+92del34cCEjgdvmw2Aal0OnZ1MJDrRjtXoXBtgob/lXsKGfrQq9PcuSGOQec7bAP+eKG5eA/lfQpnezSDwDJgPByq5tCrTFgNU2MPewcH0eH7Dft/E/AbYeQVz3WknT+QsGffCFxWsfwfPFSpzTkUlCzH+IpDJ7bCzEgm8CWe/CLY2Qq7Zp1qn9gZ8CrPBBrKhH8eBnsez3gZQ6O+Wj48fgYP9EEWr5+D7q+SHsV7GKgB/YunExKyp+H6NlaJ1PoObV6JqXWnRAM6viuShwl7GuA9X4cOF779WlDoO395ipTJAgRi+wCNJEboxnmwHvFsC6J/r9azKOadctysgSwCzKd+7NG/gbHfCxkuwZ8i3/hMsiOYZPXUFZM5NH2FlwlHvrVBqPg61Awe6Hgd9wd1p6wDnt+GAWwnFkHuHQfCBm46fOU0vgnM/OUDyfQzedQWIrtOpLgGG9gsc0CEQ5mkGBLIfsBL/SwLg/ffB9gAZyrX3juBuAEos5t5h50W/Vz68OnouASz0S/M1D+QCOopnM/qBpTm0S72dMOn4m+GQz/tdBU3H970G7O8morhA38rrhXAeiqRWrC7o398tAaCztlYbOdqtE0F/3AXuxwMlQCR1wyNJhlZ6n00kqXdFdX1hvjcEstPOtAkgzRjsi3Yvs+ZE11us76kEcAffxnvMjgHfcOzvAwOtY3OIFPpP3td50yX0Fq87FZ+NIGy/5lBgCGt8BNsp707b/Fnnmx3Qf5aVg+dt2+h1SYDMtvmUQYXel0Qe36/Y6ytunvYvBBDpJewfLMJZ6Da/0U4kW7cQ6YAmTaz5Ge/9/ewIvZt3ercE6Fet0yH/DeTW2kfq95lkbo8b2Yj2TAKgmz2PjnGN4czODpZzNmHZ/T06l/IHw7dV6zr4rJqaZ9kEc504iahAfTq72HnzyZ0OHSaG5zEo/o/B2kUPYu/qMJzNd3FvRCJ+Y0Kz9/B2bCIwuK5yO+3gb+NiyrL3qKxKFuYyQA6v4/fORCs01jO5DpG1/EEvqaSSSiqppJL+D/oHA2Vc/OAUKTgAAAAASUVORK5CYII=";
			
			this.loaderlogos = {
				logo: loaderC3logo,
				powered: [/*loaderPowered_1024,*/ loaderPowered_512, loaderPowered_256, loaderPowered_128],
				website: [/*loaderWebsite_1024,*/ loaderWebsite_512, loaderWebsite_256, loaderWebsite_128]
			};
		}
		
		this.next_uid = pm[21];

		// First load of the object reference table. Note some common ACEs are not added yet and will return undefined -
		// this is just to get access to plugin/behavior ctors
		this.objectRefTable = cr.getObjectRefTable();
		
		// Create the system object
		this.system = new cr.system_object(this);

		// For each plugin, create a new plugin instance.
		var i, len, j, lenj, k, lenk, idstr, m, b, t, f, p, d;
		var plugin, plugin_ctor;
		
		for (i = 0, len = pm[2].length; i < len; i++)
		{
			m = pm[2][i];
			p = this.GetObjectReference(m[0]);
;
			
			// Add common ACEs for the plugin
			cr.add_common_aces(m, p.prototype);

			// Create new plugin instance
			plugin = new p(this);
			plugin.singleglobal = m[1];
			plugin.is_world = m[2];
			plugin.is_rotatable = m[5];
			plugin.must_predraw = m[9];

			if (plugin.onCreate)
				plugin.onCreate();  // opportunity to override default ACEs

			cr.seal(plugin);
			this.plugins.push(plugin);
		}
		
		// Common ACEs have been added above. To ensure references to common ACEs resolve correctly,
		// reload the object reference table: some undefined references will now return the common ACE.
		this.objectRefTable = cr.getObjectRefTable();

		// Create object types
		for (i = 0, len = pm[3].length; i < len; i++)
		{
			m = pm[3][i];
			
			plugin_ctor = this.GetObjectReference(m[1]);
;
			plugin = null;
			
			// find the plugin instance matching the ctor
			for (j = 0, lenj = this.plugins.length; j < lenj; j++)
			{
				if (this.plugins[j] instanceof plugin_ctor)
				{
					plugin = this.plugins[j];
					break;
				}
			}
			
			// Create a new object type from the plugin
;
;
			
			var type_inst = new plugin.Type(plugin);
			
;

			// Merge the model data in to the instance
			type_inst.name = m[0];
			type_inst.is_family = m[2];
			type_inst.instvar_sids = m[3].slice(0);
			type_inst.vars_count = m[3].length;
			type_inst.behs_count = m[4];
			type_inst.fx_count = m[5];
			type_inst.sid = m[11];
			
			if (type_inst.is_family)
			{
				type_inst.members = [];				// types in this family
				type_inst.family_index = this.family_count++;
				type_inst.families = null;
			}
			else
			{
				type_inst.members = null;
				type_inst.family_index = -1;
				type_inst.families = [];			// families this type belongs to
			}
			
			type_inst.family_var_map = null;
			type_inst.family_beh_map = null;
			type_inst.family_fx_map = null;
			
			// Container variables - assigned after this loop as created all object types
			type_inst.is_contained = false;
			type_inst.container = null;
			
			// Texture
			if (m[6])
			{
				type_inst.texture_file = m[6][0];
				type_inst.texture_filesize = m[6][1];
				type_inst.texture_pixelformat = m[6][2];
				type_inst.texture_data = m[6];
			}
			else
			{
				type_inst.texture_file = null;
				type_inst.texture_filesize = 0;
				type_inst.texture_pixelformat = 0;		// rgba8
				type_inst.texture_data = null;
			}
			
			// Animations
			if (m[7])
			{
				type_inst.animations = m[7];
			}
			else
			{
				type_inst.animations = null;
			}
			
			// Add in runtime-provided object type features
			type_inst.index = i;                                // save index in to types array in type
			type_inst.instances = [];                           // all instances of this type
			type_inst.deadCache = [];							// destroyed instances to recycle next create
			type_inst.solstack = [new cr.selection(type_inst)]; // initialise SOL stack with one empty SOL
			type_inst.cur_sol = 0;
			type_inst.default_instance = null;
			type_inst.default_layerindex = 0;
			type_inst.stale_iids = true;
			type_inst.updateIIDs = cr.type_updateIIDs;
			type_inst.getFirstPicked = cr.type_getFirstPicked;
			type_inst.getPairedInstance = cr.type_getPairedInstance;
			type_inst.getCurrentSol = cr.type_getCurrentSol;
			type_inst.pushCleanSol = cr.type_pushCleanSol;
			type_inst.pushCopySol = cr.type_pushCopySol;
			type_inst.popSol = cr.type_popSol;
			type_inst.getBehaviorByName = cr.type_getBehaviorByName;
			type_inst.getBehaviorIndexByName = cr.type_getBehaviorIndexByName;
			type_inst.getEffectIndexByName = cr.type_getEffectIndexByName;
			type_inst.applySolToContainer = cr.type_applySolToContainer;
			type_inst.getInstanceByIID = cr.type_getInstanceByIID;
			type_inst.collision_grid = new cr.SparseGrid(this.original_width, this.original_height);
			type_inst.any_cell_changed = true;
			type_inst.any_instance_parallaxed = false;
			type_inst.extra = {};
			type_inst.toString = cr.type_toString;

			// Create each of the type's behaviors
			type_inst.behaviors = [];

			for (j = 0, lenj = m[8].length; j < lenj; j++)
			{
				b = m[8][j];
				var behavior_ctor = this.GetObjectReference(b[1]);
				var behavior_plugin = null;
				
				// Try to find a created plugin matching ctor
				for (k = 0, lenk = this.behaviors.length; k < lenk; k++)
				{
					if (this.behaviors[k] instanceof behavior_ctor)
					{
						behavior_plugin = this.behaviors[k];
						break;
					}
				}

				// Is behavior-plugin not yet created?
				if (!behavior_plugin)
				{
					// Create new behavior-plugin instance
					behavior_plugin = new behavior_ctor(this);
					behavior_plugin.my_types = [];						// types using this behavior
					behavior_plugin.my_instances = new cr.ObjectSet(); 	// instances of this behavior

					if (behavior_plugin.onCreate)
						behavior_plugin.onCreate();
						
					cr.seal(behavior_plugin);

					// Save the behavior
					this.behaviors.push(behavior_plugin);
					
					if (cr.behaviors.solid && behavior_plugin instanceof cr.behaviors.solid)
						this.solidBehavior = behavior_plugin;
					
					if (cr.behaviors.jumpthru && behavior_plugin instanceof cr.behaviors.jumpthru)
						this.jumpthruBehavior = behavior_plugin;
					
					if (cr.behaviors.shadowcaster && behavior_plugin instanceof cr.behaviors.shadowcaster)
						this.shadowcasterBehavior = behavior_plugin;
				}
				
				// Record all types that make use of the behavior
				if (behavior_plugin.my_types.indexOf(type_inst) === -1)
					behavior_plugin.my_types.push(type_inst);

				// Create the behavior-type
				var behavior_type = new behavior_plugin.Type(behavior_plugin, type_inst);
				behavior_type.name = b[0];
				behavior_type.sid = b[2];
				behavior_type.onCreate();
				cr.seal(behavior_type);

				type_inst.behaviors.push(behavior_type);
			}
			
			// Global setting
			type_inst.global = m[9];
			
			// Is on loader layout
			type_inst.isOnLoaderLayout = m[10];
			
			// Assign shaders
			type_inst.effect_types = [];
			
			for (j = 0, lenj = m[12].length; j < lenj; j++)
			{
				type_inst.effect_types.push({
					id: m[12][j][0],
					name: m[12][j][1],
					shaderindex: -1,
					preservesOpaqueness: false,
					active: true,
					index: j
				});
			}
			
			// Store tilemap collision polys (if any)
			type_inst.tile_poly_data = m[13];
			
			// Create and seal.  However note when using loader layouts, defer creation of
			// object types not on the loader layout at this point.  Images are loaded in
			// type creation so we want to wait until the loader layout finishes loading, then
			// create all the remaining types and wait for loading to finish again.
			// Also create all non-world plugins immediately, since they are already ready.
			if (!this.uses_loader_layout || type_inst.is_family || type_inst.isOnLoaderLayout || !plugin.is_world)
			{
				type_inst.onCreate();
				cr.seal(type_inst);
			}

			// Add to the types object and types by index list.  Sometimes names are not exported
			if (type_inst.name)
			{
				this.types[type_inst.name] = type_inst;
				this.types_lowercase[type_inst.name.toLowerCase()] = type_inst;
			}
				
			this.types_by_index.push(type_inst);

			// If a single-global, create the instance now
			if (plugin.singleglobal)
			{
				var instance = new plugin.Instance(type_inst);

				instance.uid = this.next_uid++;
				instance.puid = this.next_puid++;
				instance.iid = 0;
				instance.get_iid = cr.inst_get_iid;
				instance.toString = cr.inst_toString;
				instance.properties = m[14];

				instance.onCreate();
				cr.seal(instance);

				type_inst.instances.push(instance);
				this.objectsByUid[instance.uid.toString()] = instance;
			}
		}
		
		// Set families
		for (i = 0, len = pm[4].length; i < len; i++)
		{
			var familydata = pm[4][i];
			var familytype = this.types_by_index[familydata[0]];
			var familymember;
			
			for (j = 1, lenj = familydata.length; j < lenj; j++)
			{
				familymember = this.types_by_index[familydata[j]];
				familymember.families.push(familytype);
				familytype.members.push(familymember);
			}
		}
		
		// Assemble containers
		for (i = 0, len = pm[27].length; i < len; i++)
		{
			var containerdata = pm[27][i];
			var containertypes = [];
			
			for (j = 0, lenj = containerdata.length; j < lenj; j++)
				containertypes.push(this.types_by_index[containerdata[j]]);
			
			for (j = 0, lenj = containertypes.length; j < lenj; j++)
			{
				containertypes[j].is_contained = true;
				containertypes[j].container = containertypes;
			}
		}
		
		// Map instance variables, behaviors and effects for families
		if (this.family_count > 0)
		{
			for (i = 0, len = this.types_by_index.length; i < len; i++)
			{
				t = this.types_by_index[i];
				
				if (t.is_family || !t.families.length)
					continue;
					
				t.family_var_map = new Array(this.family_count);
				t.family_beh_map = new Array(this.family_count);
				t.family_fx_map = new Array(this.family_count);
				var all_fx = [];
					
				var varsum = 0;
				var behsum = 0;
				var fxsum = 0;
				
				for (j = 0, lenj = t.families.length; j < lenj; j++)
				{
					f = t.families[j];
					t.family_var_map[f.family_index] = varsum;
					varsum += f.vars_count;
					t.family_beh_map[f.family_index] = behsum;
					behsum += f.behs_count;
					t.family_fx_map[f.family_index] = fxsum;
					fxsum += f.fx_count;
					
					// Build list of all effect types including inherited for this object type.
					// Make shallow copies so we can set the correct index for each type
					for (k = 0, lenk = f.effect_types.length; k < lenk; k++)
						all_fx.push(cr.shallowCopy({}, f.effect_types[k]));
				}
				
				// Update effect types array to include inherited
				t.effect_types = all_fx.concat(t.effect_types);
				
				for (j = 0, lenj = t.effect_types.length; j < lenj; j++)
					t.effect_types[j].index = j;
			}
		}

		// Create layouts
		for (i = 0, len = pm[5].length; i < len; i++)
		{
			m = pm[5][i];
			
			var layout = new cr.layout(this, m);
			cr.seal(layout);

			// Add by name and index
			this.layouts[layout.name] = layout;
			this.layouts_by_index.push(layout);
		}

		// Create event sheets
		for (i = 0, len = pm[6].length; i < len; i++)
		{
			m = pm[6][i];
			
			var sheet = new cr.eventsheet(this, m);
			cr.seal(sheet);

			// Add by name and index
			this.eventsheets[sheet.name] = sheet;
			this.eventsheets_by_index.push(sheet);
		}
		
		// Post-initialise the event system, now that all variables are available
		for (i = 0, len = this.eventsheets_by_index.length; i < len; i++)
			this.eventsheets_by_index[i].postInit();
		
		for (i = 0, len = this.eventsheets_by_index.length; i < len; i++)
			this.eventsheets_by_index[i].updateDeepIncludes();

		for (i = 0, len = this.triggers_to_postinit.length; i < len; i++)
			this.triggers_to_postinit[i].postInit();
			
		// Done with trigger postinit
		cr.clearArray(this.triggers_to_postinit)
		
		// Get list of audio files and arrange in to a map
		this.audio_files_info = pm[7];
		this.audio_files_map = {};		// TODO: use Map
		for (i = 0, len = this.audio_files_info.length; i < len; ++i)
		{
			d = this.audio_files_info[i];
			this.audio_files_map[d[0]] = d[1];
		}
		
		// Set files subfolder
		this.files_subfolder = pm[8];
		
		// Set pixel rounding mode
		this.pixel_rounding = pm[9];
		
		this.aspect_scale = 1.0;
		
		// determined before object creation
		//this.fullscreen_mode = pm[11];
		
		this.enableWebGL = pm[13];
		this.linearSampling = pm[14];
		this.clearBackground = pm[15];
		this.versionstr = pm[16];
		
		this.useHighDpi = pm[17];
		
		this.orientations = pm[20];		// 0 = any, 1 = portrait, 2 = landscape
		this.autoLockOrientation = (this.orientations > 0);
		this.pauseOnBlur = pm[22];
		this.wantFullscreenScalingQuality = pm[23];		// false = low quality, true = high quality
		this.fullscreenScalingQuality = this.wantFullscreenScalingQuality;
		
		this.downscalingQuality = pm[24];	// 0 = low (mips off), 1 = medium (mips on, dense spritesheet), 2 = high (mips on, sparse spritesheet)
		
		this.preloadSounds = pm[25];		// 0 = no, 1 = yes
		this.enableFrontToBack = pm[26] && !this.isIE;		// front-to-back renderer disabled in IE (but not Edge)
		
		this.enhancedAccelerationPrecision = pm[30];
		
		// Get the start time of the application in ms for loading screen
		this.start_time = Date.now();
		
		// Done with the object reference table, can recycle it now
		cr.clearArray(this.objectRefTable);
		
		// If preload sounds is disabled, the browser doesn't natively support WebM Opus, and we have a software decoder
		// available, initialise it now. This helps minimise the latency for spinning up the worker when playing the first sound.
		// We don't definitely know it will be needed - there could be AAC equivalents for everything - but we initialise anyway
		// to be on the safe side.
		if (!this.preloadSounds && !supportedAudioFormats["audio/webm; codecs=opus"] && window["OpusDecoder"])
		{
			window["OpusDecoder"]["Initialise"]();		// defaults to navigator.hardwareConcurrency || 2
		}
		
		// Next loading phase
		this.initRendererAndLoader();
	};
	
	var anyImageHadError = false;
	
	Runtime.prototype.waitForImageLoad = function (img_, src_)
	{
		// In cross-origin local preview mode, or remote preview mode, substitute the src for a blob URL in the image files map.
		if (window.cr_remotePreviewImageFiles && src_ && src_.substr(0, 5) !== "blob:")		// note ignore if already a blob URL (may have come from getLocalFileUrl())
		{
			src_ = window.cr_remotePreviewImageFiles[src_];
;
		}
		
		// Attach error handler before assigning src to ensure errors are caught.
		// If any image has an error loading, the progress bar turns red and the game will fail to load.
		img_.onerror = function (e)
		{
			img_.c2error = true;
			anyImageHadError = true;
			
			if (console && console.error)
				console.error("Error loading image '" + img_.src + "': ", e);
		};
		
		// Backwards compatibility with old plugins: previously there was no src parameter to this function
		// and callers set img_.src before this call. Don't handle the new behavior if the src has already
		// been set.
		if (!img_.src)
		{
			// Expansion APK reader is available
			if (typeof XAPKReader !== "undefined")
			{
				// Expand the src file, await completion, then assign the resulting src
				XAPKReader.get(src_, function (expanded_url)
				{
					// start loading image from expanded path
					img_.src = expanded_url;
				
				}, function (e)
				{
					// error expanding file
					img_.c2error = true;
					anyImageHadError = true;
					
					if (console && console.error)
						console.error("Error extracting image '" + src_ + "' from expansion file: ", e);
				});
			}
			else
			{
				// load normally
				img_.crossOrigin = "anonymous";			// required for Arcade sandbox compatibility
				this.setImageSrc(img_, src_);			// work around WKWebView problems
			}
		}
		
		this.wait_for_textures.push(img_);
	};
	
	Runtime.prototype.findWaitingTexture = function (src_)
	{
		var i, len;
		for (i = 0, len = this.wait_for_textures.length; i < len; i++)
		{
			if (this.wait_for_textures[i].cr_src === src_)
				return this.wait_for_textures[i];
		}
		
		return null;
	};
	
	var audio_preload_totalsize = 0;
	var audio_preload_started = false;
	
	Runtime.prototype.getready = function ()
	{
		// No audio instance or preload disabled: no point trying to preload any audio
		if (!this.audioInstance || !this.preloadSounds)
			return;
		
		var preloadList = [];
		var i, len, info, namepart, r, isMusic;
		for (i = 0, len = this.audio_files_info.length; i < len; ++i)
		{
			info = this.audio_files_info[i];
			namepart = info[0];
			isMusic = info[2];
			r = this.getPreferredAudioFile(namepart);
			
			if (r && !isMusic)		// don't preload music tracks
			{
				preloadList.push({
					filename: namepart + r[1],
					size: r[2],
					type: r[0]
				});
			}
		}
		
		audio_preload_totalsize = this.audioInstance.setPreloadList(preloadList);
	};

	Runtime.prototype.areAllTexturesAndSoundsLoaded = function ()
	{
		var totalsize = audio_preload_totalsize;
		var completedsize = 0;
		var audiocompletedsize = 0;
		var ret = true;

		var i, len, img;
		for (i = 0, len = this.wait_for_textures.length; i < len; i++)
		{
			img = this.wait_for_textures[i];
			
			var filesize = img.cr_filesize;

			// No filesize provided - oops, plugin dev messed up.
			// Assume 50kb so some progress happens.
			if (!filesize || filesize <= 0)
				filesize = 50000;

			totalsize += filesize;

			// Image finished loading?
			// Note images with no src may be awaiting decompression from expansion APK
			// so don't count them as completed
			if (!!img.src && img.complete && !img.c2error)
				completedsize += filesize;
			else
				ret = false;    // not all textures loaded
		}
		
		// All images finished loading: preload sounds if enabled
		if (ret && this.preloadSounds && this.audioInstance)
		{
			if (!audio_preload_started)
			{
				this.audioInstance.startPreloads();
				audio_preload_started = true;
			}
			
			audiocompletedsize = this.audioInstance.getPreloadedSize();
			
			completedsize += audiocompletedsize;
			
			if (audiocompletedsize < audio_preload_totalsize)
				ret = false;		// not done yet
		}

		if (totalsize == 0)
			this.progress = 1;		// indicate to C3 splash loader that it can finish now
		else
			this.progress = (completedsize / totalsize);

		return ret;
	};
	
	var isC3SplashDone = false;

	// Start the runtime running
	Runtime.prototype.go = function ()
	{
		// No canvas support
		if (!this.ctx && !this.glwrap)
			return;
			
		// Use either 2D context or WebGL overlay context to draw progress bar -
		// both are 2D contexts so the code can be recycled
		var ctx = this.ctx || this.overlay_ctx;
		
		// Respond to resizes while loading.
		if (this.fullscreen_mode > 0)
		{
			var curwidth = window.innerWidth;
			var curheight = window.innerHeight;
			if (this.lastWindowWidth !== curwidth || this.lastWindowHeight !== curheight)
			{
				this["setSize"](curwidth, curheight);
			}
		}

		this.progress = 0;
		this.last_progress = -1;
		var self = this;

		// Wait for any pending textures or audio to finish loading then forward to go_loading_finished
		if (this.areAllTexturesAndSoundsLoaded() && (this.loaderstyle !== 4 || isC3SplashDone))
		{
			this.go_loading_finished();
		}
		else
		{
			// Post progress to debugger if present
			
			// Draw loading screen on canvas.  areAllTexturesAndSoundsLoaded set this.progress.
			// Don't display anything for the first 500ms, so quick loads don't distractingly flash a progress message.
			// Loader styles: 0 = progress bar & logo; 1 = progress bar only; 2 = percentage text; 3 = nothing
			var ms_elapsed = Date.now() - this.start_time;

			if (ctx)
			{
				var overlay_width = this.width;
				var overlay_height = this.height;
				var dpr = this.devicePixelRatio;
				
				if (this.loaderstyle < 3 && (ms_elapsed >= 500 && this.last_progress != this.progress))
				{
					ctx.clearRect(0, 0, overlay_width, overlay_height);
					var mx = overlay_width / 2;
					var my = overlay_height / 2;
					var haslogo = (this.loaderstyle === 0 && this.loaderlogos.logo && this.loaderlogos.logo.complete);
					var hlw = 40 * dpr;
					var hlh = 0;
					var logowidth = 80 * dpr;
					var logoheight;
					
					if (haslogo)
					{
						var loaderLogoImage = this.loaderlogos.logo;
						logowidth = loaderLogoImage.width * dpr;
						logoheight = loaderLogoImage.height * dpr;
						hlw = logowidth / 2;
						hlh = logoheight / 2;
						ctx.drawImage(loaderLogoImage, cr.floor(mx - hlw), cr.floor(my - hlh), logowidth, logoheight);
					}
					
					// draw progress bar
					if (this.loaderstyle <= 1)
					{
						my += hlh + (haslogo ? 12 * dpr : 0);
						mx -= hlw;
						mx = cr.floor(mx) + 0.5;
						my = cr.floor(my) + 0.5;
						
						// make bar go red if error occurs
						ctx.fillStyle = anyImageHadError ? "red" : "DodgerBlue";
						ctx.fillRect(mx, my, Math.floor(logowidth * this.progress), 6 * dpr);
						ctx.strokeStyle = "black";
						ctx.strokeRect(mx, my, logowidth, 6 * dpr);
						ctx.strokeStyle = "white";
						ctx.strokeRect(mx - 1 * dpr, my - 1 * dpr, logowidth + 2 * dpr, 8 * dpr);
					}
					// draw percentage text
					else if (this.loaderstyle === 2)
					{
						ctx.font = "12pt Arial";
						ctx.fillStyle = anyImageHadError ? "#f00" : "#999";
						ctx.textBaseLine = "middle";
						var percent_text = Math.round(this.progress * 100) + "%";
						var text_dim = ctx.measureText ? ctx.measureText(percent_text) : null;
						var text_width = text_dim ? text_dim.width : 0;
						ctx.fillText(percent_text, mx - (text_width / 2), my);
					}
					
					this.last_progress = this.progress;
				}
				else if (this.loaderstyle === 4)
				{
					this.draw_c3_splash_loader(ctx);
					
					// use raf-driven animation instead of slow timer
					if (raf)
						raf(function() { self.go(); });
					else
						setTimeout(function() { self.go(); }, 16);
					
					return;
				}
			}

			// Call again after 100ms
			setTimeout(function() { self.go(); }, 100);
		}
	};
	
	var splashStartTime = -1;
	var splashFadeInDuration = 300;
	var splashFadeOutDuration = 300;
	var splashIsFadeIn = true;
	var splashIsFadeOut = false;
	var splashFadeInFinish = 0;
	var splashFadeOutStart = 0;
	var renderViaCanvas = null;
	var renderViaCtx = null;
	var splashFrameNumber = 0;
	
	function maybeCreateRenderViaCanvas(w, h)
	{
		if (!renderViaCanvas || renderViaCanvas.width !== w || renderViaCanvas.height !== h)
		{
			renderViaCanvas = document.createElement("canvas");
			renderViaCanvas.width = w;
			renderViaCanvas.height = h;
			renderViaCtx = renderViaCanvas.getContext("2d");
		}
	};
	
	function mipImage(arr, size)
	{
		if (size <= 128)
			return arr[2];
		else if (size <= 256)
			return arr[1];
		else// if (size <= 512)
			return arr[0];
		//else
		//	return arr[0];
	};
	
	Runtime.prototype.draw_c3_splash_loader = function(ctx)
	{
		if (isC3SplashDone)
			return;
		
		var splashAfterFadeOutWait = (this.isPreview ? 0 : 200);
		var splashMinDisplayTime = (this.isPreview ? 0 : 3000);
		
		var w = Math.ceil(this.width);
		var h = Math.ceil(this.height);
		var dpr = this.devicePixelRatio;
		
		var logoimage = this.loaderlogos.logo;
		var poweredimages = this.loaderlogos.powered;
		var websiteimages = this.loaderlogos.website;
		
		// If any images are not ready, return and wait until they are
		if (!logoimage.complete)
			return;
		
		for (var i = 0; i < 3; ++i)
		{
			if (!poweredimages[i].complete || !websiteimages[i].complete)
				return;
		}
		
		// First draw
		if (splashFrameNumber === 0)
			splashStartTime = Date.now();
		
		var nowTime = Date.now();
		var isRenderingVia = false;
		var renderToCtx = ctx;
		var drawW, drawH;
		
		if (splashIsFadeIn || splashIsFadeOut)
		{
			ctx.clearRect(0, 0, w, h);
		
			maybeCreateRenderViaCanvas(w, h);
			renderToCtx = renderViaCtx;
			isRenderingVia = true;
			
			// Note we only really start the fade-in from the second frame. This is to avoid any jank on the first
			// frame (which has to create an extra canvas and possibly load GPU textures) and ensure the actual
			// fade-in transition itself can run smoothly.
			if (splashIsFadeIn && splashFrameNumber === 1)
				splashStartTime = Date.now();
		}
		else
		{
			ctx.globalAlpha = 1;
		}
		
		renderToCtx.fillStyle = "#3B4045";
		renderToCtx.fillRect(0, 0, w, h);
		
		// Ignore exceptions when drawing splash images. This is in case any splash image fails to load. The easiest
		// way to detect this is letting drawImage throw an exception and simply skipping. This allows the timing
		// code to continue to run and still end the splash when it's done.
		try {
			// Style 1: full height
			if (this.cssHeight > 256)
			{
				// 'Powered by' text
				drawW = cr.clamp(h * 0.22, 105, w * 0.6) * 1.5;
				drawH = drawW / 8;
				renderToCtx.drawImage(mipImage(poweredimages, drawW), w * 0.5 - drawW/2, h * 0.2 - drawH/2, drawW, drawH);
				
				// C3 logo (SVG)
				drawW = Math.min(h * 0.395, w * 0.95);
				drawH = drawW;
				renderToCtx.drawImage(logoimage, w * 0.5 - drawW/2, h * 0.485 - drawH/2, drawW, drawH);
				
				// Website text
				drawW = cr.clamp(h * 0.22, 105, w * 0.6) * 1.5;
				drawH = drawW / 8;
				renderToCtx.drawImage(mipImage(websiteimages, drawW), w * 0.5 - drawW/2, h * 0.868 - drawH/2, drawW, drawH);
				
				// Progress bar background
				renderToCtx.fillStyle = "#4D565D";
				drawW = w;
				drawH = Math.max(h * 0.005, 2);
				renderToCtx.fillRect(0, h * 0.8 - drawH/2, drawW, drawH);
				
				// Progress bar fill
				renderToCtx.fillStyle = anyImageHadError ? "red" : "#29F3D0";
				drawW = w * this.progress;
				renderToCtx.fillRect(w * 0.5 - drawW/2, h * 0.8 - drawH/2, drawW, drawH);
			}
			// Style 2: narrow height
			else
			{
				// C3 logo (SVG)
				drawW = h * 0.55;
				drawH = drawW;
				renderToCtx.drawImage(logoimage, w * 0.5 - drawW/2, h * 0.45 - drawH/2, drawW, drawH);
				
				// Progress bar background
				renderToCtx.fillStyle = "#4D565D";
				drawW = w;
				drawH = Math.max(h * 0.005, 2);
				renderToCtx.fillRect(0, h * 0.85 - drawH/2, drawW, drawH);
				
				// Progress bar fill
				renderToCtx.fillStyle = anyImageHadError ? "red" : "#29F3D0";
				drawW = w * this.progress;
				renderToCtx.fillRect(w * 0.5 - drawW/2, h * 0.85 - drawH/2, drawW, drawH);
			}
			
			
			if (isRenderingVia)
			{
				if (splashIsFadeIn)
				{
					if (splashFrameNumber === 0)
						ctx.globalAlpha = 0;
					else
						ctx.globalAlpha = Math.min((nowTime - splashStartTime) / splashFadeInDuration, 1);
				}
				else if (splashIsFadeOut)
				{
					ctx.globalAlpha = Math.max(1 - (nowTime - splashFadeOutStart) / splashFadeOutDuration, 0);
				}
					
				ctx.drawImage(renderViaCanvas, 0, 0, w, h);
			}
		}
		catch(e)
		{
			// Ignore the exception, presumably a splash image failed to load and couldn't be drawn
		}
		
		if (splashIsFadeIn && nowTime - splashStartTime >= splashFadeInDuration && splashFrameNumber >= 2)
		{
			splashIsFadeIn = false;
			splashFadeInFinish = nowTime;
		}
		
		if (!splashIsFadeIn && nowTime - splashFadeInFinish >= splashMinDisplayTime && !splashIsFadeOut && this.progress >= 1)
		{
			splashIsFadeOut = true;
			splashFadeOutStart = nowTime;
		}
		
		// Note there are two ways we end the C3 splash:
		// 1) normally, after the splash fade out and wait
		// 2) in preview mode, if it loads quicker than 500ms, just skip straight to the game to avoid getting in the way
		if ((splashIsFadeOut && nowTime - splashFadeOutStart >= splashFadeOutDuration + splashAfterFadeOutWait) ||
			(this.isPreview && this.progress >= 1 && Date.now() - splashStartTime < 500))
		{
			isC3SplashDone = true;
			splashIsFadeIn = false;
			splashIsFadeOut = false;
			renderViaCanvas = null;
			renderViaCtx = null;
			this.loaderlogos = null;
		}
		
		++splashFrameNumber;
	};
	
	// Run once textures have all completed
	Runtime.prototype.go_loading_finished = function ()
	{
		// If there are any web fonts used in the project, and the browser supports loading them with document.fonts.load,
		// wait for them all to finish loading before starting. Otherwise just start right away.
		if (this.webFontNames.length && document.fonts && document.fonts.load)
		{
			var self = this;
			
			Promise.all(this.webFontNames.map(function (f)
			{
				return document.fonts.load("1em '" + f + "'");
			}))
			.then(function ()
			{
				self.start_first_layout();
			});
		}
		else
		{
			this.start_first_layout();
		}
	};
	
	Runtime.prototype.start_first_layout = function ()
	{
		// Remove overlay canvas if any
		if (this.overlay_canvas)
		{
			this.canvas.parentNode.removeChild(this.overlay_canvas);
			this.overlay_ctx = null;
			this.overlay_canvas = null;
		}
		
		// Reset the start time
		this.start_time = Date.now();
		this.last_fps_time = cr.performance_now();       // for counting framerate
		
		// Initialise debugger
		
		var i, len, t;
		
		// Create the rest of the types in the project if using loader layout
		if (this.uses_loader_layout)
		{
			for (i = 0, len = this.types_by_index.length; i < len; i++)
			{
				t = this.types_by_index[i];
				
				if (!t.is_family && !t.isOnLoaderLayout && t.plugin.is_world)
				{
					t.onCreate();
					cr.seal(t);
				}
			}
			
			// Now wait_for_textures has extra images waiting to be loaded...
		}
		else
			this.isloading = false;
			
		// Create all global non-world instances in all layouts
		for (i = 0, len = this.layouts_by_index.length; i < len; i++)
		{
			this.layouts_by_index[i].createGlobalNonWorlds();
		}
		
		// make sure aspect scale is correctly set in advance of first tick
		if (this.fullscreen_mode >= 2)
		{
			var orig_aspect = this.original_width / this.original_height;
			var cur_aspect = this.width / this.height;
			
			// note mode 2 (scale inner) inverts this logic and will use window width when width wider.
			if ((this.fullscreen_mode !== 2 && cur_aspect > orig_aspect) || (this.fullscreen_mode === 2 && cur_aspect < orig_aspect))
				this.aspect_scale = this.height / this.original_height;
			else
				this.aspect_scale = this.width / this.original_width;
		}
		
		// Find the first layout and start it running
		if (this.first_layout)
			this.layouts[this.first_layout].startRunning();
		else
			this.layouts_by_index[0].startRunning();

;
		
		// Is not using a loader layout: fire 'On loaded' now anyway
		if (!this.uses_loader_layout)
		{
			this.loadingprogress = 1;
			this.trigger(cr.system_object.prototype.cnds.OnLoadFinished, null);
			
			// Register the Service Worker so it can begin caching resources for use offline.
			// This is done after loading has finished so it doesn't delay loading due to requesting all resources (including audio),
			// and also to help avoid it making duplicate requests, since the first installation may use results from the HTTP cache.
			// Note if a loader layout is in use, this is postponed until the loader layout itself finishes, for the same reasons.
			if (window["C3_RegisterSW"])		// note not all platforms use a SW
				window["C3_RegisterSW"]();
		}
		
		// Hide splash screen in Crosswalk
		if (navigator["splashscreen"] && navigator["splashscreen"]["hide"])
			navigator["splashscreen"]["hide"]();
			
		// Trigger onAppBegin for any plugins with a handler
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onAppBegin)
				t.onAppBegin();
		}
		
		// Add event listeners to suspend the engine when the page becomes invisible
		var onVisibilityChanged = function ()
		{
			if (document["hidden"] || document["webkitHidden"] || document["mozHidden"] || document["msHidden"])
				window["cr_setSuspended"](true);
			else
				window["cr_setSuspended"](false);
		};
		document.addEventListener("visibilitychange", onVisibilityChanged, false);
		document.addEventListener("mozvisibilitychange", onVisibilityChanged, false);
		document.addEventListener("webkitvisibilitychange", onVisibilityChanged, false);
		document.addEventListener("msvisibilitychange", onVisibilityChanged, false);
		
		// In Cordova apps, page visibility events don't seem to fire when pressing the power button, so listen
		// for Cordova's pause and resume events as well.
		if (this.isCordova)
		{
			document.addEventListener("pause", function ()
			{
				window["cr_setSuspended"](true);
			});
			document.addEventListener("resume", function ()
			{
				window["cr_setSuspended"](false);
			});
		}
		
		// Starting up in minimised/background window: suspend engine
		if (document["hidden"] || document["webkitHidden"] || document["mozHidden"] || document["msHidden"])
		{
			window["cr_setSuspended"](true);		// stop rendering
		}
		else
		{
			// Initial tick
			this.tick(false);
		}
	};
		  
	// Timer tick (process one frame)
	Runtime.prototype.tick = function (background_wake, timestamp, debug_step)
	{
		// Some platforms fire suspend/resume events before the runtime is ready, and resuming
		// calls tick() to kick off the game loop again. If the runtime is not ready just ignore this.
		if (!this.running_layout)
			return;
			
		var nowtime = cr.performance_now();
		var logic_start = nowtime;
		
		// App suspended: stop ticking so code stops executing. Also in background wakes
		// make sure we don't try to kick off any new timers/callbacks.
		// However if the debugger is stepping, allow one step while suspended.
		if (!debug_step && this.isSuspended && !background_wake)
			return;
		
		// Schedule the next frame ASAP, to minimise any latency in the browser frame scheduler,
		// except in background wake mode.
		if (!background_wake)
		{
			if (raf)
				this.raf_id = raf(this.tickFunc);
			else
			{
				// Some mobile browsers without RAF run at sub-optimal framerates with a timeout of
				// 16, presumably because they actually wait for a whole 16 ms until doing another tick.
				// Having a timeout of 1 improves performance by eliminating the wait until the next tick.
				// However, desktop browsers which have no RAF support are optimised for V-syncing with 16ms intervals,
				// otherwise framerates run up in to the hundreds.
				this.timeout_id = setTimeout(this.tickFunc, this.isMobile ? 1 : 16);
			}
		}
		
		// If provided, use the timestamp from requestAnimationFrame since it is probably aligned with
		// the browser v-sync events. Otherwise if not provided (as in a few edge cases like first tick
		// after resume, background wake, or old browsers) then just use the performance_now() time.
		var raf_time = timestamp || nowtime;
		
		
		// In some circumstances resize events are either not fired at all, or are buggy
		// and fire at the wrong times. To avoid any such issues, we check the window
		// size every tick and if it's changed since we last saw it, fire setSize.
		var fsmode = this.fullscreen_mode;
	
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || document["fullScreen"] || !!document["msFullscreenElement"]) && !this.isCordova;
		
		if ((isfullscreen || this.isNodeFullscreen) && this.fullscreen_scaling > 0)
			fsmode = this.fullscreen_scaling;
		
		// Disable this workaround on iOS. Due to what looks like Safari bugs,
		// on iPhone it mis-aligns the canvas when changing orientation and this workaround
		// is in effect, and the iPad browser scrolling goes haywire when using textboxes.
		//if (fsmode > 0 && (!this.isiOS || window.self !== window.top))
		if (fsmode > 0)	// r222: experimentally enabling this workaround for all platforms
		{
			var curwidth = window.innerWidth;
			var curheight = window.innerHeight;
			if (this.lastWindowWidth !== curwidth || this.lastWindowHeight !== curheight)
			{
				this["setSize"](curwidth, curheight);
			}
		}
	
		// In fullscreen mode, make sure canvas appears aligned center. Note Chrome/nodewebkit does this anyway.
		if (isfullscreen)
		{
			if (!this.firstInFullscreen)
				this.firstInFullscreen = true;
		}
		// First tick coming out of fullscreen mode: restore previous margins
		else
		{
			if (this.firstInFullscreen)
			{
				this.firstInFullscreen = false;
				
				if (this.fullscreen_mode === 0)
				{
					this["setSize"](Math.round(this.oldWidth / this.devicePixelRatio), Math.round(this.oldHeight / this.devicePixelRatio), true);
				}
			}
			else
			{
				this.oldWidth = this.width;
				this.oldHeight = this.height;
			}
		}
		
		// If a loader layout loading, update the progress
		if (this.isloading)
		{
			var done = this.areAllTexturesAndSoundsLoaded();		// updates this.progress
			this.loadingprogress = this.progress;
			
			if (done)
			{
				this.isloading = false;
				this.progress = 1;
				this.trigger(cr.system_object.prototype.cnds.OnLoadFinished, null);
				
				// Register the SW now the loader layout has finished.
				if (window["C3_RegisterSW"])
					window["C3_RegisterSW"]();
			}
		}
		

		// Execute logic
		this.logic(raf_time);

		// Canvas needs updating?  Don't bother redrawing if page is not visible.
		if (this.redraw && !this.is_WebGL_context_lost && !this.suspendDrawing && !background_wake)
		{
			// Clear draw flag before render, since rendering some animated effects will
			// flag another redraw
			this.redraw = false;
			
			// Render
			
			if (this.glwrap)
				this.drawGL();
			else
				this.draw();
			
			
			// Snapshot the canvas if enabled
			if (this.snapshotCanvas)
			{
				if (this.canvas && this.canvas.toDataURL)
				{
					this.snapshotData = this.canvas.toDataURL(this.snapshotCanvas[0], this.snapshotCanvas[1]);
					
					if (window["cr_onSnapshot"])
						window["cr_onSnapshot"](this.snapshotData);
					
					this.trigger(cr.system_object.prototype.cnds.OnCanvasSnapshot, null);
				}
					
				this.snapshotCanvas = null;
			}
		}

		if (!this.hit_breakpoint)
		{
			this.tickcount++;
			this.execcount++;
			this.framecount++;
		}
		
		this.logictime += cr.performance_now() - logic_start;
	};
	

	// Process application logic
	Runtime.prototype.logic = function (cur_time)
	{
		var i, leni, j, lenj, k, lenk, type, inst, binst;
		

		// Test if enough time has passed to update the framerate
		if (cur_time - this.last_fps_time >= 1000)  // every 1 second
		{
			this.last_fps_time += 1000;
			
			// still more than 1 second behind: must have had some kind of hang/pause, just bring up to date
			if (cur_time - this.last_fps_time >= 1000)
				this.last_fps_time = cur_time;
			
			this.fps = this.framecount;
			this.framecount = 0;
			this.cpuutilisation = Math.min(this.logictime, 1000);		// clamp to 100%
			this.logictime = 0;

			// GPU profiling
			if (this.glwrap)
			{
				// If the end frame is 0, put the current frame number as the end of the 1s frame range.
				if (this.gpuTimeEndFrame === 0)
				{
					this.gpuTimeEndFrame = this.glwrap.getFrameNumber();
					this.gpuCurUtilisation = -1;
				}
			}
			
		}

		// Check for gpuutilisation measurement becoming available.
		if (this.glwrap && this.gpuCurUtilisation === -1 && this.glwrap.supportsGpuProfiling())
		{
			this.gpuCurUtilisation = this.glwrap.getFrameRangeTimerResults(this.gpuTimeStartFrame, this.gpuTimeEndFrame);

			if (this.gpuCurUtilisation !== -1)
			{
				this.glwrap.deleteTimerResultsToFrame(this.gpuTimeEndFrame);

				this.gpuLastUtilisation = Math.min(this.gpuCurUtilisation, 1);		// clamp to 100%
				this.gpuTimeStartFrame = this.gpuTimeEndFrame;
				this.gpuTimeEndFrame = 0;
			}
		}

		// Measure dt
		// Don't measure dt on first tick
		if (this.last_tick_time !== 0)
		{
			// Calculate dt difference in ms
			var ms_diff = cur_time - this.last_tick_time;
			
			// Ensure not negative in case of mismatch between performance_now() and rAF timestamp
			if (ms_diff < 0)
				ms_diff = 0;
			
			this.dt1 = ms_diff / 1000.0; // dt measured in seconds

			// If tab inactive, browser caps timers at 1 Hz.  If this has happened (test by dt being over 0.5),
			// just pause the game.  Also pause if the page is hidden.
			if (this.dt1 > 0.5)
				this.dt1 = 0;
			// Cap at a max dt of 0.1 (min framerate 10fps).
			else if (this.dt1 > 1 / this.minimumFramerate)
				this.dt1 = 1 / this.minimumFramerate;
		}

		this.last_tick_time = cur_time;

        // Set dt to the timescaled dt1 (wall clock delta time)
        this.dt = this.dt1 * this.timescale;

        // Sum the kahan time
        this.kahanTime.add(this.dt);
		this.wallTime.add(this.dt1);
		
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || document["fullScreen"] || !!document["msFullscreenElement"] || this.isNodeFullscreen) && !this.isCordova;
		
		// Calculate the project-wide zoom for fullscreen-scale mode
		if (this.fullscreen_mode >= 2 /* scale */ || (isfullscreen && this.fullscreen_scaling > 0))
		{
			var orig_aspect = this.original_width / this.original_height;
			var cur_aspect = this.width / this.height;
			
			var mode = this.fullscreen_mode;
					
			if (isfullscreen && this.fullscreen_scaling > 0)
				mode = this.fullscreen_scaling;
			
			// window width wider: zoom to fit height.
			// note mode 2 (scale inner) inverts this logic and will use window width when width wider.
			if ((mode !== 2 && cur_aspect > orig_aspect) || (mode === 2 && cur_aspect < orig_aspect))
			{
				this.aspect_scale = this.height / this.original_height;
			}
			// window height taller: zoom to fit width
			else
			{
				// zoom to fit width
				this.aspect_scale = this.width / this.original_width;
			}
			
			// Scroll layout to itself so it bounds again
			if (this.running_layout)
			{
				this.running_layout.scrollToX(this.running_layout.scrollX);
				this.running_layout.scrollToY(this.running_layout.scrollY);
			}
		}
		else
			this.aspect_scale = (this.isRetina ? this.devicePixelRatio : 1);

		// Destroy any objects queued for removal
		this.ClearDeathRow();
		
		// Run any events scheduled with the Wait action
		this.isInOnDestroy++;
		
		this.system.runWaits();		// prevent instance list changing
		
		this.isInOnDestroy--;
		
		this.ClearDeathRow();		// allow instance list changing
		
		this.isInOnDestroy++;
		
		// Tick objects-to-pre-tick
        var tickarr = this.objects_to_pretick.valuesRef();

        for (i = 0, leni = tickarr.length; i < leni; i++)
            tickarr[i].pretick();

		// Tick behaviors
		for (i = 0, leni = this.types_by_index.length; i < leni; i++)
		{
			type = this.types_by_index[i];
			
			// don't bother iterating types without behaviors. Types in a family
			// should still be iterated in case the type inherits a family behavior.
			if (type.is_family || (!type.behaviors.length && !type.families.length))
				continue;

			// For each instance in type
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];

				// For each behavior-instance in instance
				for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
				{
					inst.behavior_insts[k].tick();
				}
			}
		}
		
		// Call posttick on behaviors
		for (i = 0, leni = this.types_by_index.length; i < leni; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family || (!type.behaviors.length && !type.families.length))
				continue;	// type doesn't have any behaviors

			// For each instance in type
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];

				// For each behavior-instance in instance
				for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
				{
					binst = inst.behavior_insts[k];
					
					if (binst.posttick)
						binst.posttick();
				}
			}
		}
		
		// Tick objects-to-tick
        tickarr = this.objects_to_tick.valuesRef();

        for (i = 0, leni = tickarr.length; i < leni; i++)
            tickarr[i].tick();
			
		this.isInOnDestroy--;		// end preventing instance lists from being changed
		
		this.handleSaveLoad();		// save/load now if queued
			
		// Switch layout if one set.  Keep switching in case 'on start of layout' sets another layout,
		// but to prevent infinite loops, do this a maximum of ten times.
		i = 0;
		
		while (this.changelayout && i++ < 10)
		{
			this.doChangeLayout(this.changelayout);
		}

        // Reset all 'hasRun' flags on all event sheets to prevent event sheet cyclic inclusions
        for (i = 0, leni = this.eventsheets_by_index.length; i < leni; i++)
            this.eventsheets_by_index[i].hasRun = false;

		// If the running layout has an event sheet, run it
		
		
		if (this.running_layout.event_sheet)
			this.running_layout.event_sheet.run();
		
			
		// Reset the registered collisions
		cr.clearArray(this.registered_collisions);
			
		// Reset the first tick this layout flag
		this.layout_first_tick = false;
		
		this.isInOnDestroy++;		// prevent instance lists from being changed
		
		// Post-event ticking (tick2)
		for (i = 0, leni = this.types_by_index.length; i < leni; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family || (!type.behaviors.length && !type.families.length))
				continue;	// type doesn't have any behaviors

			// For each instance in type
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				var inst = type.instances[j];

				// For each behavior-instance in instance
				for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
				{
					binst = inst.behavior_insts[k];
					
					if (binst.tick2)
						binst.tick2();
				}
			}
		}
		
		// Tick objects-to-tick2
        tickarr = this.objects_to_tick2.valuesRef();

        for (i = 0, leni = tickarr.length; i < leni; i++)
            tickarr[i].tick2();
			
		this.isInOnDestroy--;		// end preventing instance lists from being changed
	};
	
	Runtime.prototype.onWindowBlur = function ()
	{
		var i, leni, j, lenj, k, lenk, type, inst, binst;
		
		for (i = 0, leni = this.types_by_index.length; i < leni; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family)
				continue;

			// For each instance in type
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];
				
				if (inst.onWindowBlur)
					inst.onWindowBlur();

				if (!inst.behavior_insts)
					continue;	// single-globals don't have behavior_insts
				
				// For each behavior-instance in instance
				for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
				{
					binst = inst.behavior_insts[k];
					
					if (binst.onWindowBlur)
						binst.onWindowBlur();
				}
			}
		}
	};
	
	Runtime.prototype.doChangeLayout = function (changeToLayout)
	{
		var prev_layout = this.running_layout;
		this.running_layout.stopRunning();
		
		var i, len, j, lenj, k, lenk, type, inst, binst;
		
		// WebGL renderer: clean up all textures for all types not on the next layout
		if (this.glwrap)
		{
			for (i = 0, len = this.types_by_index.length; i < len; i++)
			{
				type = this.types_by_index[i];
				
				if (type.is_family)
					continue;
				
				// This type not used on next layout
				if (type.unloadTextures && (!type.global || type.instances.length === 0) && changeToLayout.initial_types.indexOf(type) === -1)
				{
					type.unloadTextures();
				}
			}
		}
		
		// If restarting the same layout, cancel all pending waits
		if (prev_layout == changeToLayout)
			cr.clearArray(this.system.waits);
		
		// Reset the registered collisions
		cr.clearArray(this.registered_collisions);
		
		// trigger 'onBeforeLayoutChange' for all global objects
		this.runLayoutChangeMethods(true);
		
		changeToLayout.startRunning();
		
		// trigger 'onLayoutChange' for all global objects
		this.runLayoutChangeMethods(false);
		
		this.redraw = true;
		this.layout_first_tick = true;
		
		// Destroy any objects queued for removal on 'Start of layout'
		this.ClearDeathRow();
	};
	
	Runtime.prototype.runLayoutChangeMethods = function (isBeforeChange)
	{
		var i, len, beh, type, j, lenj, inst, k, lenk, binst;
		
		// Call every Behavior plugin first (pathfinding uses this to update the map)
		for (i = 0, len = this.behaviors.length; i < len; i++)
		{
			beh = this.behaviors[i];
			
			if (isBeforeChange)
			{
				if (beh.onBeforeLayoutChange)
					beh.onBeforeLayoutChange();
			}
			else
			{
				if (beh.onLayoutChange)
					beh.onLayoutChange();
			}
		}
		
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			if (!type.global && !type.plugin.singleglobal)
				continue;

			// For each instance in type
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];

				if (isBeforeChange)
				{
					if (inst.onBeforeLayoutChange)
						inst.onBeforeLayoutChange();
				}
				else
				{
					if (inst.onLayoutChange)
						inst.onLayoutChange();
				}
				
				if (inst.behavior_insts)
				{
					for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
					{
						binst = inst.behavior_insts[k];
						
						if (isBeforeChange)
						{
							if (binst.onBeforeLayoutChange)
								binst.onBeforeLayoutChange();
						}
						else
						{
							if (binst.onLayoutChange)
								binst.onLayoutChange();
						}
					}
				}
			}
		}
	};

	Runtime.prototype.pretickMe = function (inst)
    {
        this.objects_to_pretick.add(inst);
    };
	
	Runtime.prototype.unpretickMe = function (inst)
	{
		this.objects_to_pretick.remove(inst);
	};
	
    Runtime.prototype.tickMe = function (inst)
    {
        this.objects_to_tick.add(inst);
    };
	
	Runtime.prototype.untickMe = function (inst)
	{
		this.objects_to_tick.remove(inst);
	};
	
	Runtime.prototype.tick2Me = function (inst)
    {
        this.objects_to_tick2.add(inst);
    };
	
	Runtime.prototype.untick2Me = function (inst)
	{
		this.objects_to_tick2.remove(inst);
	};

    // Get dt for a given instance (in case it has its own time scale set)
    Runtime.prototype.getDt = function (inst)
    {
        // -1 indicates no object time scale set; use game timescale
        if (!inst || inst.my_timescale === -1.0)
            return this.dt;

        // Otherwise return wall-clock dt scaled by instance timescale
        return this.dt1 * inst.my_timescale;
    };

	// Render to canvas
	Runtime.prototype.draw = function ()
	{
		// Draw the running layout
		this.running_layout.draw(this.ctx);
		
	};
	
	Runtime.prototype.drawGL = function ()
	{
		if (this.enableFrontToBack)
		{
			this.earlyz_index = 1;		// start from front, 1-based to avoid exactly equalling near plane Z value
			this.running_layout.drawGL_earlyZPass(this.glwrap);
		}
		
		this.running_layout.drawGL(this.glwrap);
		
		
		this.glwrap.present();
	};

	Runtime.prototype.addDestroyCallback = function (f)
	{
		if (f)
			this.destroycallbacks.push(f);
	};
	
	Runtime.prototype.removeDestroyCallback = function (f)
	{
		cr.arrayFindRemove(this.destroycallbacks, f);
	};
	
	Runtime.prototype.getObjectByUID = function (uid_)
	{
;
		var uidstr = uid_.toString();
		
		if (this.objectsByUid.hasOwnProperty(uidstr))
			return this.objectsByUid[uidstr];
		else
			return null;
	};
	
	var objectset_cache = [];
	
	function alloc_objectset()
	{
		if (objectset_cache.length)
			return objectset_cache.pop();
		else
			return new cr.ObjectSet();
	};
	
	function free_objectset(s)
	{
		s.clear();
		objectset_cache.push(s);
	};

	Runtime.prototype.DestroyInstance = function (inst)
	{
		var i, len;
		var type = inst.type;
		var typename = type.name;
		var has_typename = this.deathRow.hasOwnProperty(typename);
		var obj_set = null;
		
		if (has_typename)
		{
			obj_set = this.deathRow[typename];
			
			if (obj_set.contains(inst))
				return;		// already had DestroyInstance called
		}
		else
		{
			// If no objectset record for this type name yet, add one
			obj_set = alloc_objectset();
			this.deathRow[typename] = obj_set;
		}
		
		inst.isDestroyed = true;
		
		// Add to death row to destroy later
		obj_set.add(inst);
		this.hasPendingInstances = true;
		
		// Also destroy all siblings if in container
		if (inst.is_contained)
		{
			for (i = 0, len = inst.siblings.length; i < len; i++)
			{
				this.DestroyInstance(inst.siblings[i]);
			}
		}
		
		// Is in the middle of ClearDeathRow: update values cache directly so this instance is also iterated
		if (this.isInClearDeathRow)
			obj_set.values_cache.push(inst);
		
		// Don't fire "On destroy" when ending a layout. This makes it tricky to know what to do with objects
		// that are created in "On destroy" events, and generally is confusing to users.
		if (!this.isEndingLayout)
		{
			this.isInOnDestroy++;		// support recursion
			this.trigger(Object.getPrototypeOf(inst.type.plugin).cnds.OnDestroyed, inst);
			this.isInOnDestroy--;
		}
	};

	Runtime.prototype.ClearDeathRow = function ()
	{
		if (!this.hasPendingInstances)
			return;
		
		var inst, type, instances;
		var i, j, leni, lenj, obj_set;
		this.isInClearDeathRow = true;
		
		// Flush creation row
		for (i = 0, leni = this.createRow.length; i < leni; ++i)
		{
			inst = this.createRow[i];
			type = inst.type;
			type.instances.push(inst);
			
			// Add to the type's family's instances
			for (j = 0, lenj = type.families.length; j < lenj; ++j)
			{
				type.families[j].instances.push(inst);
				type.families[j].stale_iids = true;
			}
		}
		
		cr.clearArray(this.createRow);
		
		this.IterateDeathRow();		// moved to separate function so for-in performance doesn't hobble entire function

		cr.wipe(this.deathRow);		// all objectsets have already been recycled
		this.isInClearDeathRow = false;
		this.hasPendingInstances = false;
	};
	
	Runtime.prototype.IterateDeathRow = function ()
	{
		// V8 sometimes complains "Not optimized: ForInStatement is not fast case".
		// This punishes the entire containing function, so we have this code in
		// its own absolutely minimal function.
		for (var p in this.deathRow)
		{
			if (this.deathRow.hasOwnProperty(p))
			{
				this.ClearDeathRowForType(this.deathRow[p]);
			}
		}
	};
	
	Runtime.prototype.ClearDeathRowForType = function (obj_set)
	{
		var arr = obj_set.valuesRef();			// get array of items from set
;
		
		var type = arr[0].type;
		
;
;
		
		var i, len, j, lenj, w, f, layer_instances, inst;
		
		// Remove all destroyed instances from the type instance list
		cr.arrayRemoveAllFromObjectSet(type.instances, obj_set);
		
		// Make sure IIDs reindex
		type.stale_iids = true;
		
		// Type is now empty: reset parallax flag so effect is not permanent
		if (type.instances.length === 0)
			type.any_instance_parallaxed = false;
		
		// Remove from the type's families
		for (i = 0, len = type.families.length; i < len; ++i)
		{
			f = type.families[i];
			cr.arrayRemoveAllFromObjectSet(f.instances, obj_set);
			f.stale_iids = true;
		}
		
		// Remove from any events scheduled with the wait action
		for (i = 0, len = this.system.waits.length; i < len; ++i)
		{
			w = this.system.waits[i];
			
			if (w.sols.hasOwnProperty(type.index))
				cr.arrayRemoveAllFromObjectSet(w.sols[type.index].insts, obj_set);
			
			// Also remove from type's families with wait records
			if (!type.is_family)
			{
				for (j = 0, lenj = type.families.length; j < lenj; ++j)
				{
					f = type.families[j];
				
					if (w.sols.hasOwnProperty(f.index))
						cr.arrayRemoveAllFromObjectSet(w.sols[f.index].insts, obj_set);
				}
			}
		}
		
		// Speculative optimisation: the instances to destroy for this type can belong to any layer, so
		// every instance must find itself in the layer instance list and remove itself. However in some
		// cases all the objects will be on the same layer. When destroying a large number of instances
		// on a layer with many instances, the continual find-remove creates n^2 (in)efficient removals in
		// a large array. However if we assume they all belong to the same layer and pre-emptively do a
		// faster remove-all using the full instance list, then we optimise for this case. It doesn't matter
		// if they are found or not: if found now then the instance will not remove itself later (saving the
		// per-instance removal cost), or if not found now then the instance will remove itself later (invoking
		// the per-instance removal cost, but only when objects are on different layers).
		var first_layer = arr[0].layer;
		
		if (first_layer)
		{
			// Remove each instance from the render grid in render cells mode
			if (first_layer.useRenderCells)
			{
				layer_instances = first_layer.instances;
				
				for (i = 0, len = layer_instances.length; i < len; ++i)
				{
					inst = layer_instances[i];
					
					if (!obj_set.contains(inst))
						continue;		// not destroying this instance
					
					// Remove from render grid and set rendercells to invalid initial state to indicate not in grid
					inst.update_bbox();
					first_layer.render_grid.update(inst, inst.rendercells, null);
					inst.rendercells.set(0, 0, -1, -1);
				}
			}
			
			cr.arrayRemoveAllFromObjectSet(first_layer.instances, obj_set);
			first_layer.setZIndicesStaleFrom(0);
		}
		
		// Per-instance removal tasks
		for (i = 0; i < arr.length; ++i)		// check array length every time in case it changes
		{
			this.ClearDeathRowForSingleInstance(arr[i], type);
		}
		
		// recycle the object set - note we wipe the whole deathRow object later
		free_objectset(obj_set);
				
		// make sure if anything is destroyed a redraw happens
		this.redraw = true;
	};
	
	Runtime.prototype.ClearDeathRowForSingleInstance = function (inst, type)
	{
		var i, len, binst;
		
		// Call all the 'instance destroyed' callbacks
		for (i = 0, len = this.destroycallbacks.length; i < len; ++i)
			this.destroycallbacks[i](inst);
		
		// Erase from collision cells object is added to
		if (inst.collcells)
		{
			// no new range provided - will remove only
			type.collision_grid.update(inst, inst.collcells, null);
		}

		// Delete from layer instances if on a layer. Note ClearDeathRowForType
		// may or may not have pre-emtively removed it; see the comments in that function.
		var layer = inst.layer;
		
		if (layer)
		{
			layer.removeFromInstanceList(inst, true);		// remove from both instance list and render grid
		}
		
		// Remove from all behavior-plugin's instances
		if (inst.behavior_insts)
		{
			for (i = 0, len = inst.behavior_insts.length; i < len; ++i)
			{
				binst = inst.behavior_insts[i];
				
				if (binst.onDestroy)
					binst.onDestroy();
					
				binst.behavior.my_instances.remove(inst);
			}
		}

		// Remove from objects-to-tick
		this.objects_to_pretick.remove(inst);
		this.objects_to_tick.remove(inst);
		this.objects_to_tick2.remove(inst);

		if (inst.onDestroy)
			inst.onDestroy();
			
		// Remove from the global UID map
		if (this.objectsByUid.hasOwnProperty(inst.uid.toString()))
			delete this.objectsByUid[inst.uid.toString()];
			
		this.objectcount--;
		
		// Cache up to 100 dead instances of this type to recycle for next create
		if (type.deadCache.length < 100)
			type.deadCache.push(inst);
		
		// Notify debugger of destroyed instance
	};

	Runtime.prototype.createInstance = function (type, layer, sx, sy)
	{
		// If passed a family, pick a random type in the family and create that instead
		if (type.is_family)
		{
			var i = cr.floor(Math.random() * type.members.length);
			return this.createInstance(type.members[i], layer, sx, sy);
		}
		
		if (!type.default_instance)
		{
			return null;
		}
		
		return this.createInstanceFromInit(type.default_instance, layer, false, sx, sy, false);
	};
	
	var all_behaviors = [];

	Runtime.prototype.createInstanceFromInit = function (initial_inst, layer, is_startup_instance, sx, sy, skip_siblings)
	{
		var i, len, j, lenj, p, effect_fallback, x, y;
		
		if (!initial_inst)
			return null;
		
		var type = this.types_by_index[initial_inst[1]];
;
;
		
		var is_world = type.plugin.is_world;
;
		
		// Fail if on a loader layout and this type is not loaded yet
		if (this.isloading && is_world && !type.isOnLoaderLayout)
			return null;
			
		// Fail to create if no WebGL renderer available and effect fallback is 'destroy'
		if (is_world && !this.glwrap && initial_inst[0][11] === 11)
			return null;
		
		// Using "System - create object" non-world objects can be created, but they get a valid layer
		// passed from the action.  The layer should be cleared to null otherwise a non-world object
		// gets pushed on to the layer.
		// On the other hand we also want to preserve the originally passed layer to pass along again
		// when creating siblings for a container.
		var original_layer = layer;
		
		if (!is_world)
			layer = null;
		
		// Either recycle a previously destroyed instance if any, else create a new object.
		var inst;
		
		if (type.deadCache.length)
		{
			inst = type.deadCache.pop();
			inst.recycled = true;
			
			// re-run ctor on recycled inst
			type.plugin.Instance.call(inst, type);
		}
		else
		{
			inst = new type.plugin.Instance(type);
			inst.recycled = false;
		}
		
		// Assign unique id: startup instances use the ID from the project model,
		// otherwise assign incrementing UIDs.
		// Note global objects on global layers may try to use the same UIDs; make sure the UID is
		// not already in use before using the saved UID.
		if (is_startup_instance && !skip_siblings && !this.objectsByUid.hasOwnProperty(initial_inst[2].toString()))
			inst.uid = initial_inst[2];
		else
			inst.uid = this.next_uid++;
		
		this.objectsByUid[inst.uid.toString()] = inst;
		
		inst.puid = this.next_puid++;
		
		// Assign instance id in advance. Must also check create row to get correct IIDs for
		// batch created instances.
		inst.iid = type.instances.length;
		
		for (i = 0, len = this.createRow.length; i < len; ++i)
		{
			if (this.createRow[i].type === type)
				inst.iid++;
		}
		
		inst.get_iid = cr.inst_get_iid;
		
		inst.toString = cr.inst_toString;
		inst.isDestroyed = false;

		// Slice the instance variables array to create a copy, rather than editing the initial instance variables,
		// or if the instance was recycled just copy over the initial values.
		var initial_vars = initial_inst[3];
		
		if (inst.recycled)
		{
			// clear the 'extra' object
			cr.wipe(inst.extra);
		}
		else
		{
			inst.extra = {};
			
			// In preview mode, store the names of the instance variables for the debugger
			if (this.isPreview)
			{
				inst.instance_var_names = [];
				inst.instance_var_names.length = initial_vars.length;
				
				for (i = 0, len = initial_vars.length; i < len; i++)
					inst.instance_var_names[i] = initial_vars[i][1];
			}
			
			inst.instance_vars = [];
			inst.instance_vars.length = initial_vars.length;
		}
		
		for (i = 0, len = initial_vars.length; i < len; i++)
			inst.instance_vars[i] = initial_vars[i][0];

		if (is_world)
		{
			// Copy world info from the data model
			var wm = initial_inst[0];
;
			
			inst.x = cr.is_undefined(sx) ? wm[0] : sx;
			inst.y = cr.is_undefined(sy) ? wm[1] : sy;
			inst.z = wm[2];
			inst.width = wm[3];
			inst.height = wm[4];
			inst.depth = wm[5];
			inst.angle = wm[6];
			inst.opacity = wm[7];
			inst.hotspotX = wm[8];
			inst.hotspotY = wm[9];
			inst.blend_mode = wm[10];
			
			// Set the blend mode if fallback requires
			effect_fallback = wm[11];
			
			if (!this.glwrap && type.effect_types.length)	// no WebGL renderer and shaders used
				inst.blend_mode = effect_fallback;			// use fallback blend mode - destroy mode was handled above
			
			// Set the blend mode variables
			inst.compositeOp = cr.effectToCompositeOp(inst.blend_mode);
			
			if (this.gl)
				cr.setGLBlend(inst, inst.blend_mode, this.gl);
			
			// Runtime members
			if (inst.recycled)
			{
				for (i = 0, len = wm[12].length; i < len; i++)
				{
					for (j = 0, lenj = wm[12][i].length; j < lenj; j++)
					{
						inst.effect_params[i][j] = wm[12][i][j];
						
						// see below for comments
						if (Array.isArray(inst.effect_params[i][j]))
							inst.effect_params[i][j] = inst.effect_params[i][j].slice(0);
					}
				}
				
				inst.bbox.set(0, 0, 0, 0);
				inst.collcells.set(0, 0, -1, -1);
				inst.rendercells.set(0, 0, -1, -1);
				inst.bquad.set_from_rect(inst.bbox);
				cr.clearArray(inst.bbox_changed_callbacks);
			}
			else
			{
				// Copy list of all effect type entries
				inst.effect_params = wm[12].slice(0);
				
				for (i = 0, len = inst.effect_params.length; i < len; i++)
				{
					// Copy list of effect type's parameters
					inst.effect_params[i] = wm[12][i].slice(0);
					
					// Make copies of any parameters that are arrays (i.e. color parameters), to prevent them being shared across instances.
					for (j = 0, lenj = inst.effect_params[i].length; j < lenj; ++j)
					{
						if (Array.isArray(inst.effect_params[i][j]))
							inst.effect_params[i][j] = inst.effect_params[i][j].slice(0);
					}
				}
				
				inst.active_effect_types = [];
				inst.active_effect_flags = [];
				inst.active_effect_flags.length = type.effect_types.length;
				
				inst.bbox = new cr.rect(0, 0, 0, 0);
				inst.collcells = new cr.rect(0, 0, -1, -1);
				inst.rendercells = new cr.rect(0, 0, -1, -1);
				inst.bquad = new cr.quad();
				inst.bbox_changed_callbacks = [];
				inst.set_bbox_changed = cr.set_bbox_changed;
				inst.add_bbox_changed_callback = cr.add_bbox_changed_callback;
				inst.contains_pt = cr.inst_contains_pt;
				inst.update_bbox = cr.update_bbox;
				inst.update_render_cell = cr.update_render_cell;
				inst.update_collision_cell = cr.update_collision_cell;
				inst.get_zindex = cr.inst_get_zindex;
			}
			
			inst.tilemap_exists = false;
			inst.tilemap_width = 0;
			inst.tilemap_height = 0;
			inst.tilemap_data = null;
			
			if (wm.length === 14)
			{
				inst.tilemap_exists = true;
				inst.tilemap_width = wm[13][0];
				inst.tilemap_height = wm[13][1];
				inst.tilemap_data = wm[13][2];
			}
			
			// Reset all effects to active
			for (i = 0, len = type.effect_types.length; i < len; i++)
				inst.active_effect_flags[i] = true;
			
			inst.shaders_preserve_opaqueness = true;
			inst.updateActiveEffects = cr.inst_updateActiveEffects;
			inst.updateActiveEffects();
			
			inst.uses_shaders = !!inst.active_effect_types.length;
			inst.bbox_changed = true;
			inst.cell_changed = true;
			type.any_cell_changed = true;
			inst.visible = true;
			
            // Local timescale of -1 means use game timescale
            inst.my_timescale = -1.0;
			inst.layer = layer;
			inst.zindex = layer.instances.length;	// will be placed at top of current layer
			inst.earlyz_index = 0;
			
			// Note: don't overwrite Sprite collision poly created in ctor rather than onCreate
			if (typeof inst.collision_poly === "undefined")
				inst.collision_poly = null;
			
			inst.collisionsEnabled = true;
			
			this.redraw = true;
		}
		
		var initial_props, binst;
		
		// Determine the list of all behavior types including those inherited from families
		cr.clearArray(all_behaviors);
		
		for (i = 0, len = type.families.length; i < len; i++)
		{
			all_behaviors.push.apply(all_behaviors, type.families[i].behaviors);
		}
		
		all_behaviors.push.apply(all_behaviors, type.behaviors);

		// Create behavior instances or recycle old ones
		if (inst.recycled)
		{
			for (i = 0, len = all_behaviors.length; i < len; i++)
			{
				var btype = all_behaviors[i];
				binst = inst.behavior_insts[i];
				binst.recycled = true;
				
				// re-run ctor on behavior inst
				btype.behavior.Instance.call(binst, btype, inst);
				
				// Copy in behavior instance properties
				initial_props = initial_inst[4][i];
				
				for (j = 0, lenj = initial_props.length; j < lenj; j++)
					binst.properties[j] = initial_props[j];

				binst.onCreate();
				
				// Add object instance to behavior-plugin's instances
				btype.behavior.my_instances.add(inst);
			}
		}
		else
		{
			inst.behavior_insts = [];

			for (i = 0, len = all_behaviors.length; i < len; i++)
			{
				var btype = all_behaviors[i];
				var binst = new btype.behavior.Instance(btype, inst);
				binst.recycled = false;
				
				// Copy in behavior instance properties
				binst.properties = initial_inst[4][i].slice(0);

				binst.onCreate();
				cr.seal(binst);
				
				inst.behavior_insts.push(binst);
				
				// Add object instance to behavior-plugin's instances
				btype.behavior.my_instances.add(inst);
			}
		}
		
		// Copy in properties
		initial_props = initial_inst[5];
		
		if (inst.recycled)
		{
			for (i = 0, len = initial_props.length; i < len; i++)
				inst.properties[i] = initial_props[i];
		}
		else
			inst.properties = initial_props.slice(0);

		// Don't add to type's instances yet - can break events looping over the instances.
		// Instead add it to creation row to be fully created at next top-level event.
		this.createRow.push(inst);
		this.hasPendingInstances = true;

		// Add to the layer instances
		if (layer)
		{
;
			layer.appendToInstanceList(inst, true);
			
			// If this layer is parallaxed, mark type as having parallaxed instances
			if (layer.parallaxX !== 1 || layer.parallaxY !== 1)
				type.any_instance_parallaxed = true;
		}
			
		this.objectcount++;
		
		// Create all siblings if in a container
		if (type.is_contained)
		{
			inst.is_contained = true;
			
			if (inst.recycled)
				cr.clearArray(inst.siblings);
			else
				inst.siblings = [];			// note: should not include self in siblings
			
			if (!is_startup_instance && !skip_siblings)	// layout links initial instances
			{
				for (i = 0, len = type.container.length; i < len; i++)
				{
					if (type.container[i] === type)
						continue;
					
					if (!type.container[i].default_instance)
					{
						return null;
					}

					// pass skip_siblings as true to prevent recursing infinitely
					inst.siblings.push(this.createInstanceFromInit(type.container[i].default_instance, original_layer, false, is_world ? inst.x : sx, is_world ? inst.y : sy, true));
				}
				
				// Make sure all the created siblings also have the right sibling arrays
				for (i = 0, len = inst.siblings.length; i < len; i++)
				{
					inst.siblings[i].siblings.push(inst);
					
					for (j = 0; j < len; j++)
					{
						if (i !== j)
							inst.siblings[i].siblings.push(inst.siblings[j]);
					}
				}
			}
		}
		else
		{
			inst.is_contained = false;
			inst.siblings = null;
		}
		
		inst.onCreate();
		
		if (!inst.recycled)
			cr.seal(inst);
			
		// postCreate all behaviors
		for (i = 0, len = inst.behavior_insts.length; i < len; i++)
		{
			if (inst.behavior_insts[i].postCreate)
				inst.behavior_insts[i].postCreate();
		}
		
		// Notify debugger of a new instance

		return inst;
	};

	Runtime.prototype.getLayerByName = function (layer_name)
	{
		var i, len;
		for (i = 0, len = this.running_layout.layers.length; i < len; i++)
		{
			var layer = this.running_layout.layers[i];

			if (cr.equals_nocase(layer.name, layer_name))
				return layer;
		}
		
		return null;
	};

	Runtime.prototype.getLayerByNumber = function (index)
	{
		index = cr.floor(index);
		
		if (index < 0)
			index = 0;
		if (index >= this.running_layout.layers.length)
			index = this.running_layout.layers.length - 1;

		return this.running_layout.layers[index];
	};
	
	Runtime.prototype.getLayer = function (l)
	{
		if (cr.is_number(l))
			return this.getLayerByNumber(l);
		else
			return this.getLayerByName(l.toString());
	};
	
	Runtime.prototype.clearSol = function (solModifiers)
	{
		// Iterate types in list  and reset their SOLs to select all
		var i, len;
		for (i = 0, len = solModifiers.length; i < len; i++)
		{
			solModifiers[i].getCurrentSol().select_all = true;
		}
	};

	Runtime.prototype.pushCleanSol = function (solModifiers)
	{
		// Iterate types in list and push a new, empty SOL
		var i, len;
		for (i = 0, len = solModifiers.length; i < len; i++)
		{
			solModifiers[i].pushCleanSol();
		}
	};

	Runtime.prototype.pushCopySol = function (solModifiers)
	{
		// Iterate types in list and push a cloned SOL
		var i, len;
		for (i = 0, len = solModifiers.length; i < len; i++)
		{
			solModifiers[i].pushCopySol();
		}
	};

	Runtime.prototype.popSol = function (solModifiers)
	{
		// Iterate types in list and pop back a SOL
		var i, len;
		for (i = 0, len = solModifiers.length; i < len; i++)
		{
			solModifiers[i].popSol();
		}
	};
	
	Runtime.prototype.updateAllCells = function (type)
	{
		if (!type.any_cell_changed)
			return;		// all instances must already be up-to-date
		
		var i, len, instances = type.instances;
		for (i = 0, len = instances.length; i < len; ++i)
		{
			instances[i].update_collision_cell();
		}
		
		// include anything on createrow
		var createRow = this.createRow;
		
		for (i = 0, len = createRow.length; i < len; ++i)
		{
			if (createRow[i].type === type)
				createRow[i].update_collision_cell();
		}
		
		type.any_cell_changed = false;
	};
	
	// Collect candidates for grid cell collisions
	Runtime.prototype.getCollisionCandidates = function (layer, rtype, bbox, candidates)
	{
		var i, len, t;
		var is_parallaxed = (layer ? (layer.parallaxX !== 1 || layer.parallaxY !== 1) : false);
		
		// Need to update bounding boxes first so all objects get updated to their correct buckets
		if (rtype.is_family)
		{
			for (i = 0, len = rtype.members.length; i < len; ++i)
			{
				t = rtype.members[i];
				
				if (is_parallaxed || t.any_instance_parallaxed)
				{
					cr.appendArray(candidates, t.instances);
				}
				else
				{
					this.updateAllCells(t);
					t.collision_grid.queryRange(bbox, candidates);
				}
			}
		}
		else
		{
			// When parallaxing is used, the collision grid cells no longer line up.
			// So we have no choice but to disable the optimisation and return all instances.
			if (is_parallaxed || rtype.any_instance_parallaxed)
			{
				cr.appendArray(candidates, rtype.instances);
			}
			else
			{
				this.updateAllCells(rtype);
				rtype.collision_grid.queryRange(bbox, candidates);
			}
		}
	};
	
	Runtime.prototype.getTypesCollisionCandidates = function (layer, types, bbox, candidates)
	{
		var i, len;
		
		for (i = 0, len = types.length; i < len; ++i)
		{
			this.getCollisionCandidates(layer, types[i], bbox, candidates);
		}
	};
	
	Runtime.prototype.getSolidCollisionCandidates = function (layer, bbox, candidates)
	{
		var solid = this.getSolidBehavior();

		if (!solid)
			return null;
		
		this.getTypesCollisionCandidates(layer, solid.my_types, bbox, candidates);
	};
	
	Runtime.prototype.getJumpthruCollisionCandidates = function (layer, bbox, candidates)
	{
		var jumpthru = this.getJumpthruBehavior();

		if (!jumpthru)
			return null;
		
		this.getTypesCollisionCandidates(layer, jumpthru.my_types, bbox, candidates);
	};
	
	// Test point overlap and pick relevant objects, with pt in canvas coords
	Runtime.prototype.testAndSelectCanvasPointOverlap = function (type, ptx, pty, inverted)
	{
		// Get the current SOL
		var sol = type.getCurrentSol();
		var i, j, inst, len;
		var orblock = this.getCurrentEventStack().current_event.orblock;
		
		// Point translated from canvas to given layer
		var lx, ly, arr;

		// All selected - filter in to the SOL instances
		if (sol.select_all)
		{
			if (!inverted)
			{
				sol.select_all = false;
				cr.clearArray(sol.instances);   // clear contents
			}

			for (i = 0, len = type.instances.length; i < len; i++)
			{
				inst = type.instances[i];
				inst.update_bbox();
				
				// Transform point from canvas to instance's layer
				lx = inst.layer.canvasToLayer(ptx, pty, true);
				ly = inst.layer.canvasToLayer(ptx, pty, false);
				
				if (inst.contains_pt(lx, ly))
				{
					// found one overlapping, negated test is now false
					if (inverted)
						return false;
					// otherwise pick as usual
					else
						sol.instances.push(inst);
				}
				else if (orblock)
					sol.else_instances.push(inst);
			}
		}
		else
		{
			// Otherwise filter the existing SOL
			j = 0;
			arr = (orblock ? sol.else_instances : sol.instances);
			
			for (i = 0, len = arr.length; i < len; i++)
			{
				inst = arr[i];
				inst.update_bbox();
				
				// Transform point from canvas to instance's layer
				lx = inst.layer.canvasToLayer(ptx, pty, true);
				ly = inst.layer.canvasToLayer(ptx, pty, false);
				
				if (inst.contains_pt(lx, ly))
				{
					// found one overlapping, negated test is now false
					if (inverted)
						return false;
					// iterating else_instances: push back to main instance list
					else if (orblock)
						sol.instances.push(inst);
					// otherwise pick as usual
					else
					{
						sol.instances[j] = sol.instances[i];
						j++;
					}
				}
			}

			// Truncate to those filtered
			if (!inverted)
				arr.length = j;
		}
		
		type.applySolToContainer();

		if (inverted)
			return true;		// did not find anything overlapping
		else
			return sol.hasObjects();
	};

	// Test if instances 'a' and 'b' overlap each other
	Runtime.prototype.testOverlap = function (a, b)
	{
		// Instances don't overlap themselves.  Also return false early if either object has collisions disabled.
		if (!a || !b || a === b || !a.collisionsEnabled || !b.collisionsEnabled)
			return false;

		a.update_bbox();
		b.update_bbox();
		
		// Check if testing collision with objects on different layers with different
		// positioning settings (e.g. parallax, scale, etc).  If so, we need to translate
		// both object's polys to screen co-ordinates before collision checking.
		var layera = a.layer;
		var layerb = b.layer;
		var different_layers = (layera !== layerb && (layera.parallaxX !== layerb.parallaxX || layerb.parallaxY !== layerb.parallaxY || layera.scale !== layerb.scale || layera.angle !== layerb.angle || layera.zoomRate !== layerb.zoomRate));
		var i, len, i2, i21, x, y, haspolya, haspolyb, polya, polyb;
		
		if (!different_layers)	// same layers: easy check
		{
			// Reject via bounding boxes first (fastest)
			if (!a.bbox.intersects_rect(b.bbox))
				return false;


			// Reject via bounding quads second (presumably next fastest)
			if (!a.bquad.intersects_quad(b.bquad))
				return false;
				
			// Both objects are a tilemap: ignore, this condition is not supported
			if (a.tilemap_exists && b.tilemap_exists)
				return false;
			
			// Either object is a tilemap: run a separate tilemap collision test instead
			if (a.tilemap_exists)
				return this.testTilemapOverlap(a, b);
			if (b.tilemap_exists)
				return this.testTilemapOverlap(b, a);
				
			haspolya = (a.collision_poly && !a.collision_poly.is_empty());
			haspolyb = (b.collision_poly && !b.collision_poly.is_empty());
			
			// Neither have collision polys: in bounding quad overlap
			if (!haspolya && !haspolyb)
				return true;
				
			// Test collision polys if any.  Use temp_poly if
			// it only has a bouding quad and no poly (at most one object will have bounding quad).
			if (haspolya)
			{
				a.collision_poly.cache_poly(a.width, a.height, a.angle);
				polya = a.collision_poly;
			}
			else
			{
				this.temp_poly.set_from_quad(a.bquad, a.x, a.y, a.width, a.height);
				polya = this.temp_poly;
			}
			
			if (haspolyb)
			{
				b.collision_poly.cache_poly(b.width, b.height, b.angle);
				polyb = b.collision_poly;
			}
			else
			{
				this.temp_poly.set_from_quad(b.bquad, b.x, b.y, b.width, b.height);
				polyb = this.temp_poly;
			}
			
			return polya.intersects_poly(polyb, b.x - a.x, b.y - a.y);
		}
		else	// different layers: need to do full translated check
		{
			haspolya = (a.collision_poly && !a.collision_poly.is_empty());
			haspolyb = (b.collision_poly && !b.collision_poly.is_empty());
			
			// Make sure both polya and polyb are cached in to temp_poly and temp_poly2,
			// since we need to change them by translating to the screen
			if (haspolya)
			{
				a.collision_poly.cache_poly(a.width, a.height, a.angle);
				this.temp_poly.set_from_poly(a.collision_poly);
			}
			else
			{
				this.temp_poly.set_from_quad(a.bquad, a.x, a.y, a.width, a.height);
			}
			
			polya = this.temp_poly;
			
			if (haspolyb)
			{
				b.collision_poly.cache_poly(b.width, b.height, b.angle);
				this.temp_poly2.set_from_poly(b.collision_poly);
			}
			else
			{
				this.temp_poly2.set_from_quad(b.bquad, b.x, b.y, b.width, b.height);
			}
			
			polyb = this.temp_poly2;
			
			// Both polya and polyb are relative to their own object co-ordinates.
			// Offset by the object co-ordinates and translate to screen.
			for (i = 0, len = polya.pts_count; i < len; i++)
			{
				i2 = i * 2;
				i21 = i2 + 1;
				x = polya.pts_cache[i2];
				y = polya.pts_cache[i21];

				polya.pts_cache[i2] = layera.layerToCanvas(x + a.x, y + a.y, true);
				polya.pts_cache[i21] = layera.layerToCanvas(x + a.x, y + a.y, false);
			}
			
			polya.update_bbox();

			for (i = 0, len = polyb.pts_count; i < len; i++)
			{
				i2 = i * 2;
				i21 = i2 + 1;
				x = polyb.pts_cache[i2];
				y = polyb.pts_cache[i21];
				
				polyb.pts_cache[i2] = layerb.layerToCanvas(x + b.x, y + b.y, true);
				polyb.pts_cache[i21] = layerb.layerToCanvas(x + b.x, y + b.y, false);
			}
			
			polyb.update_bbox();
			
			// Now they're both in screen co-ords, check for intersection
			return polya.intersects_poly(polyb, 0, 0);
		}
	};
	
	var tmpQuad = new cr.quad();
	var tmpRect = new cr.rect(0, 0, 0, 0);
	var collrect_candidates = [];
	
	Runtime.prototype.testTilemapOverlap = function (tm, a)
	{
		var i, len, c, rc;
		var bbox = a.bbox;
		var tmx = tm.x;
		var tmy = tm.y;
		
		tm.getCollisionRectCandidates(bbox, collrect_candidates);
		
		var collrects = collrect_candidates;
		
		var haspolya = (a.collision_poly && !a.collision_poly.is_empty());
		
		for (i = 0, len = collrects.length; i < len; ++i)
		{
			c = collrects[i];
			rc = c.rc;
			
			// First bounding box check
			if (bbox.intersects_rect_off(rc, tmx, tmy))
			{
				// Bounding box overlaps with this quad. Check bounding quad overlaps
				tmpQuad.set_from_rect(rc);
				tmpQuad.offset(tmx, tmy);
				
				// Bounding quads overlap
				if (tmpQuad.intersects_quad(a.bquad))
				{
					// a has a poly: now check the collision poly against tmpQuad
					if (haspolya)
					{
						a.collision_poly.cache_poly(a.width, a.height, a.angle);
						
						// tile has a poly: run a poly-poly check
						if (c.poly)
						{
							if (c.poly.intersects_poly(a.collision_poly, a.x - (tmx + rc.left), a.y - (tmy + rc.top)))
							{
								cr.clearArray(collrect_candidates);
								return true;
							}
						}
						// tile has no poly: check the object poly against the temp quad
						else
						{
							this.temp_poly.set_from_quad(tmpQuad, 0, 0, rc.right - rc.left, rc.bottom - rc.top);
							
							if (this.temp_poly.intersects_poly(a.collision_poly, a.x, a.y))
							{
								cr.clearArray(collrect_candidates);
								return true;
							}
						}
					}
					// otherwise a has no poly
					else
					{
						// tile has a poly: test quad-poly intersection
						if (c.poly)
						{
							this.temp_poly.set_from_quad(a.bquad, 0, 0, a.width, a.height);
							
							if (c.poly.intersects_poly(this.temp_poly, -(tmx + rc.left), -(tmy + rc.top)))
							{
								cr.clearArray(collrect_candidates);
								return true;
							}
						}
						// tile has no poly: proved quad-rect intersection already
						else
						{
							cr.clearArray(collrect_candidates);
							return true;
						}
					}
				}
			}
		}
		
		// Didn't find overlapping any quad
		cr.clearArray(collrect_candidates);
		return false;
	};
	
	Runtime.prototype.testRectOverlap = function (r, b)
	{
		// Instances don't overlap themselves.  Also return false early if either object has collisions disabled.
		if (!b || !b.collisionsEnabled)
			return false;

		
		b.update_bbox();
		
		var layerb = b.layer;
		var haspolyb, polyb;
		
		// Reject via bounding boxes first (fastest)
		if (!b.bbox.intersects_rect(r))
			return false;
		
		// Test rect against tilemap
		if (b.tilemap_exists)
		{
			b.getCollisionRectCandidates(r, collrect_candidates);
			
			var collrects = collrect_candidates;
			var i, len, c, tilerc;
			var tmx = b.x;
			var tmy = b.y;
			
			for (i = 0, len = collrects.length; i < len; ++i)
			{
				c = collrects[i];
				tilerc = c.rc;
				
				if (r.intersects_rect_off(tilerc, tmx, tmy))
				{
					// Check against tile poly if present
					if (c.poly)
					{
						
						this.temp_poly.set_from_rect(r, 0, 0);
						
						if (c.poly.intersects_poly(this.temp_poly, -(tmx + tilerc.left), -(tmy + tilerc.top)))
						{
							cr.clearArray(collrect_candidates);
							return true;
						}
					}
					// No poly: bounding boxes overlap so register a collision
					else
					{
						cr.clearArray(collrect_candidates);
						return true;
					}
				}
			}
			
			cr.clearArray(collrect_candidates);
			return false;
		}
		// Test rect against object
		else
		{
				
			tmpQuad.set_from_rect(r);

			// Reject via bounding quads second (presumably next fastest)
			if (!b.bquad.intersects_quad(tmpQuad))
				return false;
			
			haspolyb = (b.collision_poly && !b.collision_poly.is_empty());
			
			// Does not have collision poly: must be in bounding quad overlap
			if (!haspolyb)
				return true;
				
			b.collision_poly.cache_poly(b.width, b.height, b.angle);
			tmpQuad.offset(-r.left, -r.top);
			this.temp_poly.set_from_quad(tmpQuad, 0, 0, 1, 1);
			
			return b.collision_poly.intersects_poly(this.temp_poly, r.left - b.x, r.top - b.y);
		}
	};
	
	Runtime.prototype.testSegmentOverlap = function (x1, y1, x2, y2, b)
	{
		if (!b || !b.collisionsEnabled)
			return false;

		b.update_bbox();
		
		var layerb = b.layer;
		var haspolyb, polyb;
		
		// Reject via bounding boxes first (fastest). Create temporary bounding box around the segment.
		tmpRect.set(cr.min(x1, x2), cr.min(y1, y2), cr.max(x1, x2), cr.max(y1, y2));
		
		if (!b.bbox.intersects_rect(tmpRect))
			return false;
		
		// Test segment against tilemap
		if (b.tilemap_exists)
		{
			b.getCollisionRectCandidates(tmpRect, collrect_candidates);
			var collrects = collrect_candidates;
			var i, len, c, tilerc;
			var tmx = b.x;
			var tmy = b.y;
			
			for (i = 0, len = collrects.length; i < len; ++i)
			{
				c = collrects[i];
				tilerc = c.rc;
				
				// Segment bounding box intersects this tile collision rectangle
				if (tmpRect.intersects_rect_off(tilerc, tmx, tmy))
				{
					
					// Test real segment intersection
					tmpQuad.set_from_rect(tilerc);
					tmpQuad.offset(tmx, tmy);
					
					if (tmpQuad.intersects_segment(x1, y1, x2, y2))
					{
						// Check against tile collision poly if any
						if (c.poly)
						{
							if (c.poly.intersects_segment(tmx + tilerc.left, tmy + tilerc.top, x1, y1, x2, y2))
							{
								cr.clearArray(collrect_candidates);
								return true;
							}
						}
						// Otherwise is intersecting tile box
						else
						{
							cr.clearArray(collrect_candidates);
							return true;
						}
					}
				}
			}
			
			cr.clearArray(collrect_candidates);
			return false;
		}
		else
		{

			// Reject via bounding quads second (presumably next fastest)
			if (!b.bquad.intersects_segment(x1, y1, x2, y2))
				return false;
			
			haspolyb = (b.collision_poly && !b.collision_poly.is_empty());
			
			// Does not have collision poly: must be in bounding quad intersection
			if (!haspolyb)
				return true;
				
			b.collision_poly.cache_poly(b.width, b.height, b.angle);
			
			return b.collision_poly.intersects_segment(b.x, b.y, x1, y1, x2, y2);
		}
	};
	
	Runtime.prototype.typeHasBehavior = function (t, b)
	{
		if (!b)
			return false;
		
		var i, len, j, lenj, f;
		for (i = 0, len = t.behaviors.length; i < len; i++)
		{
			if (t.behaviors[i].behavior instanceof b)
				return true;
		}
		
		// Also check family behaviors
		if (!t.is_family)
		{
			for (i = 0, len = t.families.length; i < len; i++)
			{
				f = t.families[i];
				
				for (j = 0, lenj = f.behaviors.length; j < lenj; j++)
				{
					if (f.behaviors[j].behavior instanceof b)
						return true;
				}
			}
		}
		
		return false;
	};
	
	Runtime.prototype.typeHasNoSaveBehavior = function (t)
	{
		return this.typeHasBehavior(t, cr.behaviors.NoSave);
	};
	
	Runtime.prototype.typeHasPersistBehavior = function (t)
	{
		return this.typeHasBehavior(t, cr.behaviors.Persist);
	};
	
	Runtime.prototype.getSolidBehavior = function ()
	{
		return this.solidBehavior;
	};
	
	Runtime.prototype.getJumpthruBehavior = function ()
	{
		return this.jumpthruBehavior;
	};
	
	var candidates = [];

	Runtime.prototype.testOverlapSolid = function (inst)
	{
		var i, len, s;

		inst.update_bbox();
		this.getSolidCollisionCandidates(inst.layer, inst.bbox, candidates);

		for (i = 0, len = candidates.length; i < len; ++i)
		{
			s = candidates[i];
			
			if (!s.extra["solidEnabled"])
				continue;
			
			if (this.testOverlap(inst, s))
			{
				cr.clearArray(candidates);
				return s;
			}
		}

		cr.clearArray(candidates);
		return null;
	};
	
	Runtime.prototype.testRectOverlapSolid = function (r)
	{
		var i, len, s;
		this.getSolidCollisionCandidates(null, r, candidates);

		for (i = 0, len = candidates.length; i < len; ++i)
		{
			s = candidates[i];
			
			if (!s.extra["solidEnabled"])
				continue;
			
			if (this.testRectOverlap(r, s))
			{
				cr.clearArray(candidates);
				return s;
			}
		}

		cr.clearArray(candidates);
		return null;
	};
	
	var jumpthru_array_ret = [];
	
	Runtime.prototype.testOverlapJumpThru = function (inst, all)
	{
		var ret = null;
		
		if (all)
		{
			ret = jumpthru_array_ret;
			cr.clearArray(ret);
		}

		inst.update_bbox();
		this.getJumpthruCollisionCandidates(inst.layer, inst.bbox, candidates);
		var i, len, j;
		
		for (i = 0, len = candidates.length; i < len; ++i)
		{
			j = candidates[i];
			
			if (!j.extra["jumpthruEnabled"])
				continue;
			
			if (this.testOverlap(inst, j))
			{
				if (all)
					ret.push(j);
				else
				{
					cr.clearArray(candidates);
					return j;
				}
			}
		}

		cr.clearArray(candidates);
		return ret;
	};

	// Push to try and move out of solid.  Pass -1, 0 or 1 for xdir and ydir to specify a push direction.
	Runtime.prototype.pushOutSolid = function (inst, xdir, ydir, dist, include_jumpthrus, specific_jumpthru)
	{
		var push_dist = dist || 50;

		var oldx = inst.x
		var oldy = inst.y;

		var i;
		var last_overlapped = null, secondlast_overlapped = null;

		for (i = 0; i < push_dist; i++)
		{
			inst.x = (oldx + (xdir * i));
			inst.y = (oldy + (ydir * i));
			inst.set_bbox_changed();
			
			// Test if we've cleared the last instance we were overlapping
			if (!this.testOverlap(inst, last_overlapped))
			{
				// See if we're still overlapping a different solid
				last_overlapped = this.testOverlapSolid(inst);
				
				if (last_overlapped)
					secondlast_overlapped = last_overlapped;
				
				// We're clear of all solids - check jumpthrus
				if (!last_overlapped)
				{
					if (include_jumpthrus)
					{
						if (specific_jumpthru)
							last_overlapped = (this.testOverlap(inst, specific_jumpthru) ? specific_jumpthru : null);
						else
							last_overlapped = this.testOverlapJumpThru(inst);
							
						if (last_overlapped)
							secondlast_overlapped = last_overlapped;
					}
					
					// Clear of both - completed push out.  Adjust fractionally to 1/16th of a pixel.
					if (!last_overlapped)
					{
						if (secondlast_overlapped)
							this.pushInFractional(inst, xdir, ydir, secondlast_overlapped, 16);
						
						return true;
					}
				}
			}
		}

		// Didn't get out a solid: oops, we're stuck.
		// Restore old position.
		inst.x = oldx;
		inst.y = oldy;
		inst.set_bbox_changed();
		return false;
	};
	
	Runtime.prototype.pushOut = function (inst, xdir, ydir, dist, otherinst)
	{
		var push_dist = dist || 50;

		var oldx = inst.x
		var oldy = inst.y;

		var i;

		for (i = 0; i < push_dist; i++)
		{
			inst.x = (oldx + (xdir * i));
			inst.y = (oldy + (ydir * i));
			inst.set_bbox_changed();
			
			// Test if we've cleared the last instance we were overlapping
			if (!this.testOverlap(inst, otherinst))
				return true;
		}

		// Didn't get out a solid: oops, we're stuck.
		// Restore old position.
		inst.x = oldx;
		inst.y = oldy;
		inst.set_bbox_changed();
		return false;
	};
	
	Runtime.prototype.pushInFractional = function (inst, xdir, ydir, obj, limit)
	{
		var divisor = 2;
		var frac;
		var forward = false;
		var overlapping = false;
		var bestx = inst.x;
		var besty = inst.y;
		
		while (divisor <= limit)
		{
			frac = 1 / divisor;
			divisor *= 2;
			
			inst.x += xdir * frac * (forward ? 1 : -1);
			inst.y += ydir * frac * (forward ? 1 : -1);
			inst.set_bbox_changed();
			
			if (this.testOverlap(inst, obj))
			{
				// Overlapped something: try going forward again
				forward = true;
				overlapping = true;
			}
			else
			{
				// Didn't overlap anything: keep going back
				forward = false;
				overlapping = false;
				bestx = inst.x;
				besty = inst.y;
			}
		}
		
		// If left overlapping, move back to last place not overlapping
		if (overlapping)
		{
			inst.x = bestx;
			inst.y = besty;
			inst.set_bbox_changed();
		}
	};
	
	// Find nearest position not overlapping a solid
	Runtime.prototype.pushOutSolidNearest = function (inst, max_dist_)
	{
		var max_dist = (cr.is_undefined(max_dist_) ? 100 : max_dist_);
		var dist = 0;
		var oldx = inst.x
		var oldy = inst.y;

		var dir = 0;
		var dx = 0, dy = 0;
		var last_overlapped = this.testOverlapSolid(inst);
		
		if (!last_overlapped)
			return true;		// already clear of solids
		
		// 8-direction spiral scan
		while (dist <= max_dist)
		{
			switch (dir) {
			case 0:		dx = 0; dy = -1; dist++; break;
			case 1:		dx = 1; dy = -1; break;
			case 2:		dx = 1; dy = 0; break;
			case 3:		dx = 1; dy = 1; break;
			case 4:		dx = 0; dy = 1; break;
			case 5:		dx = -1; dy = 1; break;
			case 6:		dx = -1; dy = 0; break;
			case 7:		dx = -1; dy = -1; break;
			}
			
			dir = (dir + 1) % 8;
			
			inst.x = cr.floor(oldx + (dx * dist));
			inst.y = cr.floor(oldy + (dy * dist));
			inst.set_bbox_changed();
			
			// Test if we've cleared the last instance we were overlapping
			if (!this.testOverlap(inst, last_overlapped))
			{
				// See if we're still overlapping a different solid
				last_overlapped = this.testOverlapSolid(inst);
				
				// We're clear of all solids
				if (!last_overlapped)
					return true;
			}
		}
		
		// Didn't get pushed out: restore old position and return false
		inst.x = oldx;
		inst.y = oldy;
		inst.set_bbox_changed();
		return false;
	};
	
	// For behaviors to register that a collision happened
	Runtime.prototype.registerCollision = function (a, b)
	{
		// Ignore if either instance has disabled collisions
		if (!a.collisionsEnabled || !b.collisionsEnabled)
			return;
		
		this.registered_collisions.push([a, b]);
	};
	
	Runtime.prototype.checkRegisteredCollision = function (a, b)
	{
		var i, len, x;
		for (i = 0, len = this.registered_collisions.length; i < len; i++)
		{
			x = this.registered_collisions[i];
			
			if ((x[0] == a && x[1] == b) || (x[0] == b && x[1] == a))
				return true;
		}
		
		return false;
	};
	
	Runtime.prototype.calculateSolidBounceAngle = function(inst, startx, starty, obj)
	{
		var objx = inst.x;
		var objy = inst.y;
		var radius = cr.max(10, cr.distanceTo(startx, starty, objx, objy));
		var startangle = cr.angleTo(startx, starty, objx, objy);
		var firstsolid = obj || this.testOverlapSolid(inst);
		
		// Not overlapping a solid: function used wrong, return inverse of object angle (so it bounces back in reverse direction)
		if (!firstsolid)
			return cr.clamp_angle(startangle + cr.PI);
			
		var cursolid = firstsolid;
		
		// Rotate anticlockwise in 5 degree increments until no longer overlapping
		// Don't search more than 175 degrees around (36 * 5 = 180)
		var i, curangle, anticlockwise_free_angle, clockwise_free_angle;
		var increment = cr.to_radians(5);	// 5 degree increments
		
		for (i = 1; i < 36; i++)
		{
			curangle = startangle - i * increment;
			inst.x = startx + Math.cos(curangle) * radius;
			inst.y = starty + Math.sin(curangle) * radius;
			inst.set_bbox_changed();
			
			// No longer overlapping current solid
			if (!this.testOverlap(inst, cursolid))
			{
				// Search for any other solid
				cursolid = obj ? null : this.testOverlapSolid(inst);
				
				// Not overlapping any other solid: we've now reached the anticlockwise free angle
				if (!cursolid)
				{
					anticlockwise_free_angle = curangle;
					break;
				}
			}
		}
		
		// Did not manage to free up in anticlockwise direction: use reverse angle
		if (i === 36)
			anticlockwise_free_angle = cr.clamp_angle(startangle + cr.PI);
			
		var cursolid = firstsolid;
			
		// Now search in clockwise direction
		for (i = 1; i < 36; i++)
		{
			curangle = startangle + i * increment;
			inst.x = startx + Math.cos(curangle) * radius;
			inst.y = starty + Math.sin(curangle) * radius;
			inst.set_bbox_changed();
			
			// No longer overlapping current solid
			if (!this.testOverlap(inst, cursolid))
			{
				// Search for any other solid
				cursolid = obj ? null : this.testOverlapSolid(inst);
				
				// Not overlapping any other solid: we've now reached the clockwise free angle
				if (!cursolid)
				{
					clockwise_free_angle = curangle;
					break;
				}
			}
		}
		
		// Did not manage to free up in clockwise direction: use reverse angle
		if (i === 36)
			clockwise_free_angle = cr.clamp_angle(startangle + cr.PI);
			
		// Put the object back to its original position
		inst.x = objx;
		inst.y = objy;
		inst.set_bbox_changed();
			
		// Both angles match: can only be if object completely contained by solid and both searches went all
		// the way round to backwards.  Just return the back angle.
		if (clockwise_free_angle === anticlockwise_free_angle)
			return clockwise_free_angle;
		
		// We now have the first anticlockwise and first clockwise angles that are free.
		// Calculate the normal.
		var half_diff = cr.angleDiff(clockwise_free_angle, anticlockwise_free_angle) / 2;
		var normal;
		
		// Acute angle
		if (cr.angleClockwise(clockwise_free_angle, anticlockwise_free_angle))
		{
			normal = cr.clamp_angle(anticlockwise_free_angle + half_diff + cr.PI);
		}
		// Obtuse angle
		else
		{
			normal = cr.clamp_angle(clockwise_free_angle + half_diff);
		}
		
;
		
		// Reflect startangle about normal (r = v - 2 (v . n) n)
		var vx = Math.cos(startangle);
		var vy = Math.sin(startangle);
		var nx = Math.cos(normal);
		var ny = Math.sin(normal);
		var v_dot_n = vx * nx + vy * ny;
		var rx = vx - 2 * v_dot_n * nx;
		var ry = vy - 2 * v_dot_n * ny;
		return cr.angleTo(0, 0, rx, ry);
	};

	var triggerSheetIndex = -1;
	
	// Runtime: trigger an event
	Runtime.prototype.trigger = function (method, inst, value /* for fast triggers */)
	{
;
		
		// Keyboard etc. can fire events on loading screen, with no running layout.
		if (!this.running_layout)
			return false;
			
		var sheet = this.running_layout.event_sheet;

		if (!sheet)
			return false;     // no event sheet active; nothing to trigger

		var ret = false;
		var r, i, len;
		
		triggerSheetIndex++;
		
		// Trigger through all includes recursively (stored in deep_includes)
		var deep_includes = sheet.deep_includes;
		for (i = 0, len = deep_includes.length; i < len; ++i)
		{
			r = this.triggerOnSheet(method, inst, deep_includes[i], value);
			ret = ret || r;
		}
		
		// Trigger on current sheet last (not included in deep_includes), since
		// this is the shallowest level of include
		r = this.triggerOnSheet(method, inst, sheet, value);
		ret = ret || r;
		
		triggerSheetIndex--;
		
		return ret;
    };

    Runtime.prototype.triggerOnSheet = function (method, inst, sheet, value)
    {
        // Recurse by also triggering on first-level includes of the current sheet.
        var ret = false;
		var i, leni, r, families;

		// Get the type name from the instance (assume null means system).
		// For instances, re-trigger for each family type too.
		if (!inst)
		{
			r = this.triggerOnSheetForTypeName(method, inst, "system", sheet, value);
			ret = ret || r;
		}
		else
		{
			r = this.triggerOnSheetForTypeName(method, inst, inst.type.name, sheet, value);
			ret = ret || r;
			
			families = inst.type.families;
			
			for (i = 0, leni = families.length; i < leni; ++i)
			{
				r = this.triggerOnSheetForTypeName(method, inst, families[i].name, sheet, value);
				ret = ret || r;
			}
		}

		return ret;             // true if anything got triggered
	};
	
	Runtime.prototype.triggerOnSheetForTypeName = function (method, inst, type_name, sheet, value)
	{
		var i, leni;
		var ret = false, ret2 = false;
		var trig, index;
		
		// If a value is provided, treat it as a fast trigger
		var fasttrigger = (typeof value !== "undefined");
		
		var triggers = (fasttrigger ? sheet.fasttriggers : sheet.triggers);
		var obj_entry = triggers[type_name];
		
		// No triggers for this object type in the event sheet
		if (!obj_entry)
			return ret;

		var triggers_list = null;
		
		for (i = 0, leni = obj_entry.length; i < leni; ++i)
		{
			// Found matching method
			if (obj_entry[i].method == method)
			{
				triggers_list = obj_entry[i].evs;
				break;
			}
		}

		// No triggers of this method in the event sheet
		if (!triggers_list)
			return ret;
			
		var triggers_to_fire;

		if (fasttrigger)
		{
			// Look up the specific value in the trigger map
			triggers_to_fire = triggers_list[value];
		}
		else
		{
			triggers_to_fire = triggers_list;
		}
		
		if (!triggers_to_fire)
			return null;
		
		// Trigger every event in the triggers list
		for (i = 0, leni = triggers_to_fire.length; i < leni; i++)
		{
			trig = triggers_to_fire[i][0];
			index = triggers_to_fire[i][1];

			ret2 = this.executeSingleTrigger(inst, type_name, trig, index);
			ret = ret || ret2;
		}
		
		return ret;
	};
	
	Runtime.prototype.executeSingleTrigger = function (inst, type_name, trig, index)
	{
		var i, leni;
		var ret = false;
		
		this.trigger_depth++;
		
		// Each trigger should have a clean SOL for objects it references.
		// Need to also push previous event's SOL to ensure passive references aren't used
		var current_event = this.getCurrentEventStack().current_event;
		
		if (current_event)
			this.pushCleanSol(current_event.solModifiersIncludingParents);
		
		var isrecursive = (this.trigger_depth > 1);		// calling trigger from inside another trigger
		
		this.pushCleanSol(trig.solModifiersIncludingParents);
		
		if (isrecursive)
			this.pushLocalVarStack();
		
		var event_stack = this.pushEventStack(trig);
		event_stack.current_event = trig;

		// Pick the triggering instance, if any
		if (inst)
		{
			var sol = this.types[type_name].getCurrentSol();
			sol.select_all = false;
			cr.clearArray(sol.instances);
			sol.instances[0] = inst;
			this.types[type_name].applySolToContainer();
		}

		// If the event has a parent, we need to run all its parent subevents
		// in conditions-only mode (not running actions) to set up the SOL
		var ok_to_run = true;

		if (trig.parent)
		{
			// Run up tree to top collecting parent blocks
			var temp_parents_arr = event_stack.temp_parents_arr;

			var cur_parent = trig.parent;

			while (cur_parent)
			{
				temp_parents_arr.push(cur_parent);
				cur_parent = cur_parent.parent;
			}

			// Run the parent conditions, but in reverse, to start from the top.
			temp_parents_arr.reverse();

			// Run the conditions above the trigger.
			for (i = 0, leni = temp_parents_arr.length; i < leni; i++)
			{
				if (!temp_parents_arr[i].run_pretrigger())   // parent event failed
				{
					// Prevent trigger running and stop this loop
					ok_to_run = false;
					break;
				}
			}
		}

		// At last, run the trigger event, if none of the parents failed.
		if (ok_to_run)
		{
			this.execcount++;
			
			if (trig.orblock)
				trig.run_orblocktrigger(index);
			else
				trig.run();
			
			// Return true if at least one event successfully ran
			ret = ret || event_stack.last_event_true;
		}
		
		this.popEventStack();
		
		if (isrecursive)
			this.popLocalVarStack();

		// Pop the trigger's SOLs
		this.popSol(trig.solModifiersIncludingParents);
		
		if (current_event)
			this.popSol(current_event.solModifiersIncludingParents);
		
		// Clear death row between top-level events which were not triggered
		// during event interpretation (to avoid mangling SOL/instance lists during execution)
		if (this.hasPendingInstances && this.isInOnDestroy === 0 && triggerSheetIndex === 0 && !this.isRunningEvents)
		{
			this.ClearDeathRow();
		}
		
		this.trigger_depth--;
		return ret;
	};
	
	Runtime.prototype.getCurrentCondition = function ()
	{
		var evinfo = this.getCurrentEventStack();
		return evinfo.current_event.conditions[evinfo.cndindex];
	};
	
	Runtime.prototype.getCurrentConditionObjectType = function ()
	{
		var cnd = this.getCurrentCondition();
		return cnd.type;
	};
	
	Runtime.prototype.isCurrentConditionFirst = function ()
	{
		var evinfo = this.getCurrentEventStack();
		return evinfo.cndindex === 0;
	};
	
	Runtime.prototype.getCurrentAction = function ()
	{
		var evinfo = this.getCurrentEventStack();
		return evinfo.current_event.actions[evinfo.actindex];
	};
	
	Runtime.prototype.pushLocalVarStack = function ()
	{
		this.localvar_stack_index++;
		
		if (this.localvar_stack_index >= this.localvar_stack.length)
			this.localvar_stack.push([]);
	};
	
	Runtime.prototype.popLocalVarStack = function ()
	{
;
		
		this.localvar_stack_index--;
	};
	
	Runtime.prototype.getCurrentLocalVarStack = function ()
	{
		return this.localvar_stack[this.localvar_stack_index];
	};

	Runtime.prototype.pushEventStack = function (cur_event)
	{
		this.event_stack_index++;
		
		// Create a new stack frame if necessary, else recycling old one
		if (this.event_stack_index >= this.event_stack.length)
			this.event_stack.push(new cr.eventStackFrame());
		
		var ret = this.getCurrentEventStack();
		ret.reset(cur_event);
		return ret;
	};

	Runtime.prototype.popEventStack = function ()
	{
;

		this.event_stack_index--;
	};

	Runtime.prototype.getCurrentEventStack = function ()
	{
		return this.event_stack[this.event_stack_index];
	};

	Runtime.prototype.pushLoopStack = function (name_)
	{
		this.loop_stack_index++;
		
		if (this.loop_stack_index >= this.loop_stack.length)
		{
			this.loop_stack.push(cr.seal({ name: name_, index: 0, stopped: false }));
		}
			
		var ret = this.getCurrentLoop();
		ret.name = name_;
		ret.index = 0;
		ret.stopped = false;
		return ret;
	};

	Runtime.prototype.popLoopStack = function ()
	{
;

		this.loop_stack_index--;
	};

	Runtime.prototype.getCurrentLoop = function ()
	{
		return this.loop_stack[this.loop_stack_index];
	};

	Runtime.prototype.getEventVariableByName = function (name, scope)
	{
		var i, leni, j, lenj, sheet, e;

		// Search upwards through scope
		while (scope)
		{
			for (i = 0, leni = scope.subevents.length; i < leni; i++)
			{
				e = scope.subevents[i];

				if (e instanceof cr.eventvariable && cr.equals_nocase(name, e.name))
					return e;
			}

			scope = scope.parent;
		}

		// Check global scope (all variables in all event sheets at root level)
		for (i = 0, leni = this.eventsheets_by_index.length; i < leni; i++)
		{
			sheet = this.eventsheets_by_index[i];

			for (j = 0, lenj = sheet.events.length; j < lenj; j++)
			{
				e = sheet.events[j];

				if (e instanceof cr.eventvariable && cr.equals_nocase(name, e.name))
					return e;
			}
		}

		return null;
	};
	
	Runtime.prototype.getLayoutBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.layouts_by_index.length; i < len; i++)
		{
			if (this.layouts_by_index[i].sid === sid_)
				return this.layouts_by_index[i];
		}
		
		return null;
	};
	
	Runtime.prototype.getObjectTypeBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			if (this.types_by_index[i].sid === sid_)
				return this.types_by_index[i];
		}
		
		return null;
	};
	
	Runtime.prototype.getGroupBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.allGroups.length; i < len; i++)
		{
			if (this.allGroups[i].sid === sid_)
				return this.allGroups[i];
		}
		
		return null;
	};
	
	Runtime.prototype.doCanvasSnapshot = function (format_, quality_)
	{
		this.snapshotCanvas = [format_, quality_];
		this.redraw = true;		// force redraw so snapshot is always taken
	};
	
	
	function IsIndexedDBAvailable()
	{
		// Firefox is terrible and throws even trying to check if IndexedDB exists in some modes.
		try {
			return !!window.indexedDB;
		}
		catch (e)
		{
			return false;
		}
	};
	
	function makeSaveDb(e)
	{
		var db = e.target.result;
		db.createObjectStore("saves", { keyPath: "slot" });
	};
	
	function IndexedDB_WriteSlot(slot_, data_, oncomplete_, onerror_)
	{
		// some platforms throw when storage not allowed
		try {
			var request = indexedDB.open("_C2SaveStates");
			request.onupgradeneeded = makeSaveDb;
			
			request.onerror = onerror_;
			request.onsuccess = function (e)
			{
				var db = e.target.result;
				db.onerror = onerror_;
				
				var transaction = db.transaction(["saves"], "readwrite");
				var objectStore = transaction.objectStore("saves");
				var putReq = objectStore.put({"slot": slot_, "data": data_ });
				putReq.onsuccess = oncomplete_;
			};
		}
		catch (err)
		{
			onerror_(err);
		}
	};
	
	function IndexedDB_ReadSlot(slot_, oncomplete_, onerror_)
	{
		try {
			var request = indexedDB.open("_C2SaveStates");
			request.onupgradeneeded = makeSaveDb;
			request.onerror = onerror_;
			request.onsuccess = function (e)
			{
				var db = e.target.result;
				db.onerror = onerror_;
				
				var transaction = db.transaction(["saves"]);
				var objectStore = transaction.objectStore("saves");
				var readReq = objectStore.get(slot_);
				readReq.onsuccess = function (e)
				{
					if (readReq.result)
						oncomplete_(readReq.result["data"]);
					else
						oncomplete_(null);
				};
			};
		}
		catch (err)
		{
			onerror_(err);
		}
	};
	
	Runtime.prototype.signalContinuousPreview = function ()
	{
		this.signalledContinuousPreview = true;
	};
	
	function doContinuousPreviewReload()
	{
		cr.logexport("Reloading for continuous preview");
		
		if (window.location.search.indexOf("continuous") > -1)
			window.location.reload(true);
		else
			window.location = window.location + "?continuous";
	};
	
	
	Runtime.prototype.handleSaveLoad = function ()
	{
		var self = this;
		var savingToSlot = this.saveToSlot;
		var savingJson = this.lastSaveJson;
		var loadingFromSlot = this.loadFromSlot;
		var continuous = false;
		
		if (this.signalledContinuousPreview)
		{
			continuous = true;
			savingToSlot = "__c2_continuouspreview";
			this.signalledContinuousPreview = false;
		}
		
		// Save or load game state if set
		if (savingToSlot.length)
		{
			this.ClearDeathRow();
			
			savingJson = this.saveToJSONString();
			
			// Try saving to indexedDB first if available
			if (IsIndexedDBAvailable())
			{
				IndexedDB_WriteSlot(savingToSlot, savingJson, function ()
				{
					cr.logexport("Saved state to IndexedDB storage (" + savingJson.length + " bytes)");
					
					// Trigger 'On save complete'
					self.lastSaveJson = savingJson;
					self.trigger(cr.system_object.prototype.cnds.OnSaveComplete, null);
					self.lastSaveJson = "";
					
					if (continuous)
						doContinuousPreviewReload();
					
				}, function (e)
				{
					// Saving to indexedDB failed: try saving back to WebStorage instead
					try {
						localStorage.setItem("__c2save_" + savingToSlot, savingJson);
						
						cr.logexport("Saved state to WebStorage (" + savingJson.length + " bytes)");
						
						// Trigger 'On save complete'
						self.lastSaveJson = savingJson;
						self.trigger(cr.system_object.prototype.cnds.OnSaveComplete, null);
						self.lastSaveJson = "";
						
						if (continuous)
							doContinuousPreviewReload();
					}
					catch (f)
					{
						cr.logexport("Failed to save game state: " + e + "; " + f);
						self.trigger(cr.system_object.prototype.cnds.OnSaveFailed, null);
					}
				});
			}
			else
			{
				// IndexedDB not supported - just dump to WebStorage
				try {
					localStorage.setItem("__c2save_" + savingToSlot, savingJson);
					
					cr.logexport("Saved state to WebStorage (" + savingJson.length + " bytes)");
					
					// Trigger 'On save complete'
					self.lastSaveJson = savingJson;
					this.trigger(cr.system_object.prototype.cnds.OnSaveComplete, null);
					self.lastSaveJson = "";
					
					if (continuous)
						doContinuousPreviewReload();
				}
				catch (e)
				{
					cr.logexport("Error saving to WebStorage: " + e);
					self.trigger(cr.system_object.prototype.cnds.OnSaveFailed, null);
				}
			}
			
			this.saveToSlot = "";
			this.loadFromSlot = "";
			this.loadFromJson = null;
		}
		
		if (loadingFromSlot.length)
		{
			if (IsIndexedDBAvailable())
			{
				IndexedDB_ReadSlot(loadingFromSlot, function (result_)
				{
					// Load the result data. If no record was found, check if WebStorage contains it instead
					if (result_)
					{
						self.loadFromJson = result_;
						cr.logexport("Loaded state from IndexedDB storage (" + self.loadFromJson.length + " bytes)");
					}
					else
					{
						self.loadFromJson = localStorage.getItem("__c2save_" + loadingFromSlot) || "";
						cr.logexport("Loaded state from WebStorage (" + self.loadFromJson.length + " bytes)");
					}
					self.suspendDrawing = false;
					
					// If empty trigger 'On load failed'
					if (!self.loadFromJson)
					{
						self.loadFromJson = null;
						self.trigger(cr.system_object.prototype.cnds.OnLoadFailed, null);
					}
					
					
				}, function (e)
				{
					// Check if WebStorage contains this slot instead
					self.loadFromJson = localStorage.getItem("__c2save_" + loadingFromSlot) || "";
					cr.logexport("Loaded state from WebStorage (" + self.loadFromJson.length + " bytes)");
					self.suspendDrawing = false;
					
					// If empty trigger 'On load failed'
					if (!self.loadFromJson)
					{
						self.loadFromJson = null;
						self.trigger(cr.system_object.prototype.cnds.OnLoadFailed, null);
					}
					
				});
			}
			else
			{
				try {
					// Read JSON string from WebStorage then continue to load that
					this.loadFromJson = localStorage.getItem("__c2save_" + loadingFromSlot) || "";
					cr.logexport("Loaded state from WebStorage (" + this.loadFromJson.length + " bytes)");
				}
				catch (e)
				{
					this.loadFromJson = null;
				}
				
				this.suspendDrawing = false;
				
				// If empty trigger 'On load failed'
				if (!self.loadFromJson)
				{
					self.loadFromJson = null;
					self.trigger(cr.system_object.prototype.cnds.OnLoadFailed, null);
				}
			}
			
			this.loadFromSlot = "";
			this.saveToSlot = "";
		}
		
		if (this.loadFromJson !== null)
		{
			this.ClearDeathRow();
			
			var ok = this.loadFromJSONString(this.loadFromJson);
			
			if (ok)
			{
				// Trigger 'On load complete'
				this.lastSaveJson = this.loadFromJson;
				this.trigger(cr.system_object.prototype.cnds.OnLoadComplete, null);
				this.lastSaveJson = "";
			}
			else
			{
				// Trigger 'On load failed'
				self.trigger(cr.system_object.prototype.cnds.OnLoadFailed, null);
			}
			
			this.loadFromJson = null;
		}
	};
	
	function CopyExtraObject(extra)
	{
		// Some objects store various non-JSON compatible objects in their 'extra' fields.
		// This function copies fields to a new object skipping those which are known not to work.
		var p, ret = {};
		for (p in extra)
		{
			if (extra.hasOwnProperty(p))
			{
				// Don't save ObjectSets, since they need proper constructing
				if (extra[p] instanceof cr.ObjectSet)
					continue;
				// Don't save references to Box2D bodies, identified by their c2userdata member
				if (extra[p] && typeof extra[p].c2userdata !== "undefined")
					continue;
				
				// Don't save a key called "spriteCreatedDestroyCallback", Sprite uses it to know
				// whether to create a destroy callback
				if (p === "spriteCreatedDestroyCallback")
					continue;
					
				ret[p] = extra[p];
			}
		}
		
		return ret;
	};
	
	Runtime.prototype.saveToJSONString = function()
	{
		var i, len, j, lenj, type, layout, typeobj, g, c, a, v, p;
		
		var o = {
			"c2save":				true,
			"version":				1,
			"rt": {
				"time":				this.kahanTime.sum,
				"walltime":			this.wallTime.sum,
				"timescale":		this.timescale,
				"tickcount":		this.tickcount,
				"execcount":		this.execcount,
				"next_uid":			this.next_uid,
				"running_layout":	this.running_layout.sid,
				"start_time_offset": (Date.now() - this.start_time)
			},
			"types": {},
			"layouts": {},
			"events": {
				"groups": {},
				"cnds": {},
				"acts": {},
				"vars": {}
			}
		};
		
		// Save types - skip families since they have duplicate references to the real instances
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family || this.typeHasNoSaveBehavior(type))
				continue;
			
			typeobj = {
				"instances": []
			};
			
			if (cr.hasAnyOwnProperty(type.extra))
				typeobj["ex"] = CopyExtraObject(type.extra);
			
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				typeobj["instances"].push(this.saveInstanceToJSON(type.instances[j]));
			}
			
			o["types"][type.sid.toString()] = typeobj;
		}
		
		// Save layouts
		for (i = 0, len = this.layouts_by_index.length; i < len; i++)
		{
			layout = this.layouts_by_index[i];
			o["layouts"][layout.sid.toString()] = layout.saveToJSON();
		}
		
		// Save event data: group activation states
		var ogroups = o["events"]["groups"];
		for (i = 0, len = this.allGroups.length; i < len; i++)
		{
			g = this.allGroups[i];
			ogroups[g.sid.toString()] = this.groups_by_name[g.group_name].group_active;
		}
		
		// condition extras
		var ocnds = o["events"]["cnds"];
		for (p in this.cndsBySid)
		{
			if (this.cndsBySid.hasOwnProperty(p))
			{
				c = this.cndsBySid[p];
				if (cr.hasAnyOwnProperty(c.extra))
					ocnds[p] = { "ex": CopyExtraObject(c.extra) };
			}
		}
		
		// action extras
		var oacts = o["events"]["acts"];
		for (p in this.actsBySid)
		{
			if (this.actsBySid.hasOwnProperty(p))
			{
				a = this.actsBySid[p];
				if (cr.hasAnyOwnProperty(a.extra))
					oacts[p] = { "ex": CopyExtraObject(a.extra) };
			}
		}
		
		// variable extras
		var ovars = o["events"]["vars"];
		for (p in this.varsBySid)
		{
			if (this.varsBySid.hasOwnProperty(p))
			{
				v = this.varsBySid[p];
				
				// Save global or static local vars
				if (!v.is_constant && (!v.parent || v.is_static))
					ovars[p] = v.data;
			}
		}
		
		o["system"] = this.system.saveToJSON();
		
		return JSON.stringify(o);
	};
	
	Runtime.prototype.refreshUidMap = function ()
	{
		var i, len, type, j, lenj, inst;
		this.objectsByUid = {};
		
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family)
				continue;
			
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];
				this.objectsByUid[inst.uid.toString()] = inst;
			}
		}
	};
	
	Runtime.prototype.loadFromJSONString = function (str)
	{
		var o;
		
		try {
			o = JSON.parse(str);
		}
		catch (e) {
			return false;
		}
		
		if (!o["c2save"])
			return false;		// probably not a c2 save state
		
		if (o["version"] > 1)
			return false;		// from future version of c2; assume not compatible
		
		this.isLoadingState = true;
		
		var rt = o["rt"];
		this.kahanTime.reset();
		this.kahanTime.sum = rt["time"];
		this.wallTime.reset();
		this.wallTime.sum = rt["walltime"] || 0;
		this.timescale = rt["timescale"];
		this.tickcount = rt["tickcount"];
		this.execcount = rt["execcount"];
		this.start_time = Date.now() - rt["start_time_offset"];
		
		var layout_sid = rt["running_layout"];
		
		// Need to change to different layout
		if (layout_sid !== this.running_layout.sid)
		{
			var changeToLayout = this.getLayoutBySid(layout_sid);
			
			if (changeToLayout)
				this.doChangeLayout(changeToLayout);
			else
				return;		// layout that was saved on has gone missing (deleted?)
		}
		
		// Load all types
		var i, len, j, lenj, k, lenk, p, type, existing_insts, load_insts, inst, binst, layout, layer, g, iid, t;
		var otypes = o["types"];
		
		for (p in otypes)
		{
			if (otypes.hasOwnProperty(p))
			{
				type = this.getObjectTypeBySid(parseInt(p, 10));
				
				if (!type || type.is_family || this.typeHasNoSaveBehavior(type))
					continue;
				
				if (otypes[p]["ex"])
					type.extra = otypes[p]["ex"];
				else
					cr.wipe(type.extra);
				
				// Recycle any existing objects if possible
				existing_insts = type.instances;
				load_insts = otypes[p]["instances"];
				
				for (i = 0, len = cr.min(existing_insts.length, load_insts.length); i < len; i++)
				{
					// Can load directly in to existing instance
					this.loadInstanceFromJSON(existing_insts[i], load_insts[i]);
				}
				
				// Destroy the rest of the existing instances if there are too many
				for (i = load_insts.length, len = existing_insts.length; i < len; i++)
					this.DestroyInstance(existing_insts[i]);
					
				// Create additional instances if there are not enough existing instances
				for (i = existing_insts.length, len = load_insts.length; i < len; i++)
				{
					layer = null;
					
					if (type.plugin.is_world)
					{
						layer = this.running_layout.getLayerBySid(load_insts[i]["w"]["l"]);
						
						// layer's gone missing - just skip creating this instance
						if (!layer)
							continue;
					}
					
					// create an instance then load the state in to it
					// skip creating siblings; they will have been saved as well, we'll link them up later
					inst = this.createInstanceFromInit(type.default_instance, layer, false, 0, 0, true);
					this.loadInstanceFromJSON(inst, load_insts[i]);
				}
				
				type.stale_iids = true;
			}
		}
		
		this.ClearDeathRow();
		
		// Rebuild the objectsByUid map, since some objects will have loaded a different UID to the one
		// they were created with
		this.refreshUidMap();
		
		// Load all layouts
		var olayouts = o["layouts"];
		
		for (p in olayouts)
		{
			if (olayouts.hasOwnProperty(p))
			{
				layout = this.getLayoutBySid(parseInt(p, 10));
				
				if (!layout)
					continue;		// must've gone missing
					
				layout.loadFromJSON(olayouts[p]);
			}
		}
		
		// Load event states
		var ogroups = o["events"]["groups"];
		for (p in ogroups)
		{
			if (ogroups.hasOwnProperty(p))
			{
				g = this.getGroupBySid(parseInt(p, 10));
				
				if (g && this.groups_by_name[g.group_name])
					this.groups_by_name[g.group_name].setGroupActive(ogroups[p]);
			}
		}
		
		var ocnds = o["events"]["cnds"];
		for (p in this.cndsBySid)
		{
			if (this.cndsBySid.hasOwnProperty(p))
			{
				if (ocnds.hasOwnProperty(p))
				{
					this.cndsBySid[p].extra = ocnds[p]["ex"];
				}
				else
				{
					// Reset extra object to clear as it must have been upon saving if the save record is missing.
					this.cndsBySid[p].extra = {};
				}
			}
		}
		
		var oacts = o["events"]["acts"];
		for (p in this.actsBySid)
		{
			if (this.actsBySid.hasOwnProperty(p))
			{
				if (oacts.hasOwnProperty(p))
				{
					this.actsBySid[p].extra = oacts[p]["ex"];
				}
				else
				{
					this.actsBySid[p].extra = {};
				}
			}
		}
		
		var ovars = o["events"]["vars"];
		for (p in ovars)
		{
			if (ovars.hasOwnProperty(p) && this.varsBySid.hasOwnProperty(p))
			{
				this.varsBySid[p].data = ovars[p];
			}
		}
		
		this.next_uid = rt["next_uid"];
		this.isLoadingState = false;
		
		// Fire "On created" for any instances created during loading
		for (i = 0, len = this.fireOnCreateAfterLoad.length; i < len; ++i)
		{
			inst = this.fireOnCreateAfterLoad[i];
			this.trigger(Object.getPrototypeOf(inst.type.plugin).cnds.OnCreated, inst);
		}
		
		cr.clearArray(this.fireOnCreateAfterLoad);
		
		this.system.loadFromJSON(o["system"]);
		
		// Loop again and call afterLoad() on everything now that UIDs and all states are available
		// Also link together containers now that all objects are created
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			// note don't call afterLoad() on types with the No Save behavior - they completely ignore
			// the whole save/load cycle.
			if (type.is_family || this.typeHasNoSaveBehavior(type))
				continue;
			
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];
				
				// Link container
				if (type.is_contained)
				{
					iid = inst.get_iid();
					cr.clearArray(inst.siblings);
					
					for (k = 0, lenk = type.container.length; k < lenk; k++)
					{
						t = type.container[k];
						
						if (type === t)
							continue;
							
;
						inst.siblings.push(t.instances[iid]);
					}
				}
				
				if (inst.afterLoad)
					inst.afterLoad();
				
				if (inst.behavior_insts)
				{
					for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
					{
						binst = inst.behavior_insts[k];
						
						if (binst.afterLoad)
							binst.afterLoad();
					}
				}
			}
		}
		
		this.redraw = true;
		return true;
	};
	
	Runtime.prototype.saveInstanceToJSON = function(inst, state_only)
	{
		var i, len, world, behinst, et;
		var type = inst.type;
		var plugin = type.plugin;
		
		var o = {};
		
		if (state_only)
			o["c2"] = true;		// mark as known json data from Construct 2
		else
			o["uid"] = inst.uid;
		
		if (cr.hasAnyOwnProperty(inst.extra))
			o["ex"] = CopyExtraObject(inst.extra);
		
		// Save instance variables
		if (inst.instance_vars && inst.instance_vars.length)
		{
			o["ivs"] = {};
			
			for (i = 0, len = inst.instance_vars.length; i < len; i++)
			{
				o["ivs"][inst.type.instvar_sids[i].toString()] = inst.instance_vars[i];
			}
		}
		
		// Save world data
		if (plugin.is_world)
		{
			world = {
				"x": inst.x,
				"y": inst.y,
				"w": inst.width,
				"h": inst.height,
				"l": inst.layer.sid,
				"zi": inst.get_zindex()
			};
			
			if (inst.angle !== 0)
				world["a"] = inst.angle;
				
			if (inst.opacity !== 1)
				world["o"] = inst.opacity;
				
			if (inst.hotspotX !== 0.5)
				world["hX"] = inst.hotspotX;
				
			if (inst.hotspotY !== 0.5)
				world["hY"] = inst.hotspotY;
				
			if (inst.blend_mode !== 0)
				world["bm"] = inst.blend_mode;
				
			if (!inst.visible)
				world["v"] = inst.visible;
				
			if (!inst.collisionsEnabled)
				world["ce"] = inst.collisionsEnabled;
				
			if (inst.my_timescale !== -1)
				world["mts"] = inst.my_timescale;
			
			if (type.effect_types.length)
			{
				world["fx"] = [];
				
				for (i = 0, len = type.effect_types.length; i < len; i++)
				{
					et = type.effect_types[i];
					world["fx"].push({"name": et.name,
									  "active": inst.active_effect_flags[et.index],
									  "params": inst.effect_params[et.index] });
				}
			}
			
			o["w"] = world;
		}
		
		// Save behaviors
		if (inst.behavior_insts && inst.behavior_insts.length)
		{
			o["behs"] = {};
			
			for (i = 0, len = inst.behavior_insts.length; i < len; i++)
			{
				behinst = inst.behavior_insts[i];
				
				if (behinst.saveToJSON)
					o["behs"][behinst.type.sid.toString()] = behinst.saveToJSON();
			}
		}
		
		// Save plugin own data
		if (inst.saveToJSON)
			o["data"] = inst.saveToJSON();
		
		return o;
	};
	
	Runtime.prototype.getInstanceVarIndexBySid = function (type, sid_)
	{
		var i, len;
		for (i = 0, len = type.instvar_sids.length; i < len; i++)
		{
			if (type.instvar_sids[i] === sid_)
				return i;
		}
		
		return -1;
	};
	
	Runtime.prototype.getBehaviorIndexBySid = function (inst, sid_)
	{
		var i, len;
		for (i = 0, len = inst.behavior_insts.length; i < len; i++)
		{
			if (inst.behavior_insts[i].type.sid === sid_)
				return i;
		}
		
		return -1;
	};
	
	Runtime.prototype.loadInstanceFromJSON = function(inst, o, state_only)
	{
		var p, i, len, iv, oivs, world, fxindex, obehs, behindex;
		var oldlayer;
		var type = inst.type;
		var plugin = type.plugin;
		
		// state_only is used for saving/loading individual object states, in which
		// case we don't want to change the permanent UID
		if (state_only)
		{
			// check json data looks like it came from Construct 2
			if (!o["c2"])
				return;
		}
		else
			inst.uid = o["uid"];
		
		if (o["ex"])
			inst.extra = o["ex"];
		else
			cr.wipe(inst.extra);
		
		// Load instance variables
		oivs = o["ivs"];
		
		if (oivs)
		{
			for (p in oivs)
			{
				if (oivs.hasOwnProperty(p))
				{
					iv = this.getInstanceVarIndexBySid(type, parseInt(p, 10));
					
					if (iv < 0 || iv >= inst.instance_vars.length)
						continue;		// must've gone missing
						
					inst.instance_vars[iv] = oivs[p];
				}
			}
		}
		
		// Load world data
		if (plugin.is_world)
		{
			world = o["w"];
			
			// If instance is not on its intended layer, move it there now
			if (inst.layer.sid !== world["l"])
			{
				oldlayer = inst.layer;
				inst.layer = this.running_layout.getLayerBySid(world["l"]);
				
				if (inst.layer)
				{
					oldlayer.removeFromInstanceList(inst, true);
					inst.layer.appendToInstanceList(inst, true);
					
					inst.set_bbox_changed();
					inst.layer.setZIndicesStaleFrom(0);
				}
				else
				{
					// Object's layer has gone missing. Destroy this instance if full-state loading,
					// otherwise (e.g. 'load from json string' action) just leave it where it is.
					inst.layer = oldlayer;
					
					if (!state_only)
						this.DestroyInstance(inst);
				}
			}
			
			inst.x = world["x"];
			inst.y = world["y"];
			inst.width = world["w"];
			inst.height = world["h"];
			inst.zindex = world["zi"];
			inst.angle = world.hasOwnProperty("a") ? world["a"] : 0;
			inst.opacity = world.hasOwnProperty("o") ? world["o"] : 1;
			inst.hotspotX = world.hasOwnProperty("hX") ? world["hX"] : 0.5;
			inst.hotspotY = world.hasOwnProperty("hY") ? world["hY"] : 0.5;
			inst.visible = world.hasOwnProperty("v") ? world["v"] : true;
			inst.collisionsEnabled = world.hasOwnProperty("ce") ? world["ce"] : true;
			inst.my_timescale = world.hasOwnProperty("mts") ? world["mts"] : -1;
			
			inst.blend_mode = world.hasOwnProperty("bm") ? world["bm"] : 0;;
			inst.compositeOp = cr.effectToCompositeOp(inst.blend_mode);
			
			if (this.gl)
				cr.setGLBlend(inst, inst.blend_mode, this.gl);
						
			inst.set_bbox_changed();
			
			if (world.hasOwnProperty("fx"))
			{
				// Load effects and effect parameters
				for (i = 0, len = world["fx"].length; i < len; i++)
				{
					fxindex = type.getEffectIndexByName(world["fx"][i]["name"]);
					
					if (fxindex < 0)
						continue;		// must've gone missing
						
					inst.active_effect_flags[fxindex] = world["fx"][i]["active"];
					inst.effect_params[fxindex] = world["fx"][i]["params"];
				}
			}
			
			inst.updateActiveEffects();
		}
		
		// Load behaviors
		obehs = o["behs"];
		
		if (obehs)
		{
			for (p in obehs)
			{
				if (obehs.hasOwnProperty(p))
				{
					behindex = this.getBehaviorIndexBySid(inst, parseInt(p, 10));
					
					if (behindex < 0)
						continue;		// must've gone missing
					
					inst.behavior_insts[behindex].loadFromJSON(obehs[p]);
				}
			}
		}
		
		// Load plugin own data
		if (o["data"])
			inst.loadFromJSON(o["data"]);
	};
	
	Runtime.prototype.getProjectFileUrl = function (filename)
	{
		if (this.isPreview)
		{
			// Preview mode: need to look in the project file map to find what the blob URL is for this filename.
			// Keys are all lowercased to make it case insensitive.
			if (!window.cr_previewProjectFiles.hasOwnProperty(filename.toLowerCase()))
				return filename;
			
			return window.cr_previewProjectFiles[filename.toLowerCase()];
		}
		else
		{
			// Export mode: simply return the file appended to the files subfolder
			return this.files_subfolder + filename;
		}
	};
	
	// Hack for Safari blob video playback workaround in Remote Preview. This allows access to the original blob for a project file
	// which helps with the workaround in the Video plugin.
	Runtime.prototype.getProjectFileBlob = function (filename)
	{
		if (this.isPreview)
		{
			if (!window.cr_previewProjectFileBlobs || !window.cr_previewProjectFiles.hasOwnProperty(filename.toLowerCase()))
				return null;
			
			return window.cr_previewProjectFileBlobs[filename.toLowerCase()];
		}
		else
		{
			return null;
		}
	};
	
	Runtime.prototype.getLocalFileUrl = function (filename)
	{
		// As with getProjectFileUrl(), but relative to the root folder (not including files_subfolder)
		if (this.isPreview)
		{
			if (!window.cr_previewProjectFiles.hasOwnProperty(filename.toLowerCase()))
				return filename;
			
			return window.cr_previewProjectFiles[filename.toLowerCase()];
		}
		else
		{
			return filename;
		}
	};
	
	// Cordova loading utilities
	Runtime.prototype.fetchLocalFileViaCordova = function (filename, successCallback, errorCallback)
	{
		var path = window["cordova"]["file"]["applicationDirectory"] + "www/" + filename;
		
		window["resolveLocalFileSystemURL"](path, function (entry)
		{
			entry["file"](successCallback, errorCallback);
		}, errorCallback);
	};
	
	Runtime.prototype.fetchLocalFileViaCordovaAsText = function (filename, successCallback, errorCallback)
	{
		this.fetchLocalFileViaCordova(filename, function (file)
		{
			var reader = new FileReader();
			
			reader.onload = function (e)
			{
				successCallback(e.target.result);
			};
			
			reader.onerror = errorCallback;
			reader.readAsText(file);
			
		}, errorCallback);
	};
	
	var queuedArrayBufferReads = [];
	var activeArrayBufferReads = 0;
	var MAX_ARRAYBUFFER_READS = 8;
	
	Runtime.prototype.maybeStartNextArrayBufferRead = function()
	{
		if (!queuedArrayBufferReads.length)
			return;		// none left
		
		if (activeArrayBufferReads >= MAX_ARRAYBUFFER_READS)
			return;		// already got maximum number in-flight
		
		activeArrayBufferReads++;
		var job = queuedArrayBufferReads.shift();
		this.doFetchLocalFileViaCordovaAsArrayBuffer(job.filename, job.successCallback, job.errorCallback);
	};
	
	Runtime.prototype.fetchLocalFileViaCordovaAsArrayBuffer = function (filename, successCallback_, errorCallback_)
	{
		// This method creates an ArrayBuffer and is called in batches during startup. Since ArrayBuffer must be held
		// fully in RAM, this can spike memory use hard on startup. To avoid this, throttle the method to only have a maximum
		// number of requests in-flight at any given time.
		var self = this;
		
		queuedArrayBufferReads.push({
			filename: filename,
			successCallback: function (result)
			{
				activeArrayBufferReads--;
				self.maybeStartNextArrayBufferRead();
				successCallback_(result);
			},
			errorCallback: function (err)
			{
				activeArrayBufferReads--;
				self.maybeStartNextArrayBufferRead();
				errorCallback_(err);
			}
		});
		
		this.maybeStartNextArrayBufferRead();
	};
	
	Runtime.prototype.doFetchLocalFileViaCordovaAsArrayBuffer = function (filename, successCallback, errorCallback)
	{
		this.fetchLocalFileViaCordova(filename, function (file)
		{
			var reader = new FileReader();
			
			reader.onload = function (e)
			{
				successCallback(e.target.result);
			};
			
			reader.readAsArrayBuffer(file);
		}, errorCallback);
	};
	
	Runtime.prototype.fetchLocalFileViaCordovaAsURL = function (filename, successCallback, errorCallback)
	{
		// HACK: Safari is fussy about playing media elements from Blobs unless they have the correct type.
		// Assign a few common media types depending on the file extension.
		var blobType = "";
		var lowername = filename.toLowerCase();
		var ext3 = lowername.substr(lowername.length - 4);
		var ext4 = lowername.substr(lowername.length - 5);
		
		if (ext3 === ".mp4")
			blobType = "video/mp4";
		else if (ext4 === ".webm")
			blobType = "video/webm";		// use video type but hopefully works with audio too
		else if (ext3 === ".m4a")
			blobType = "audio/mp4";
		else if (ext3 === ".mp3")
			blobType = "audio/mpeg";
		
		// Convert fake Cordova file to a real Blob object, which we can create a URL to.
		this.fetchLocalFileViaCordovaAsArrayBuffer(filename, function (arrayBuffer)
		{
			var blob = new Blob([arrayBuffer], { type: blobType });
			var url = URL.createObjectURL(blob);
			successCallback(url);
		}, errorCallback);
	};
	
	Runtime.prototype.isAbsoluteUrl = function (url)
	{
		// e.g. true for "http://example.com", "data:foo", "blob:foo", false for "local.txt"
		return /^(?:[a-z]+:)?\/\//.test(url) || url.substr(0, 5) === "data:"  || url.substr(0, 5) === "blob:";
	};
	
	Runtime.prototype.setImageSrc = function (img, src)
	{
		// In WKWebView mode, load relative URLs as local files. Otherwise request normally.
		if (this.isWKWebView && !this.isAbsoluteUrl(src))
		{
			this.fetchLocalFileViaCordovaAsURL(src, function (url)
			{
				img.src = url;
			}, function (err)
			{
				alert("Failed to load image: " + err);
			});
		}
		else if (this.isPreview && !this.isAbsoluteUrl(src))
		{
			img.src = this.getLocalFileUrl(src);
		}
		else
		{
			// Export lowercases all local project files, so if it is a relative request also lowercase the name
			if (!this.isAbsoluteUrl(src))
				src = src.toLowerCase();
			
			img.src = src;
		}
	};
	
	Runtime.prototype.getPreferredAudioFile = function (namepart)
	{
		var lowername = namepart.toLowerCase();
		
		if (!this.audio_files_map.hasOwnProperty(lowername))
			return null;		// unknown audio file
		
		var info = this.audio_files_map[lowername];
		var webMOpusFile = null;
		
		// info is array of file data sorted by preference. Find the first entry that is supported
		var i, len, r, type;
		for (i = 0, len = info.length; i < len; ++i)
		{
			r = info[i];
			type = r[0];
			
			if (!webMOpusFile && type === "audio/webm; codecs=opus")
				webMOpusFile = r;
			
			if (supportedAudioFormats[type])
				return r;		// return whole record: [mimeType, extension, size]
		}
		
		// No supported format: return the WebM Opus file if there was one, which will be sent to the ffmpeg decoder.
		// If there wasn't one, it will just return null.
		return webMOpusFile;
	};
	
	Runtime.prototype.getProjectAudioFileUrl = function (namepart)
	{
		var lowername = namepart.toLowerCase();
		
		var info = this.getPreferredAudioFile(namepart);
		if (!info)
			return null;		// no supported audio file available
		
		// get full name by appending extension from preferred audio file
		var filename = lowername + info[1];
		
		// return correct URL to this filename, and include the type so we can figure out how to decode it
		return {
			url: this.getProjectFileUrl(filename),
			type: info[0]
		};
	};

	Runtime.prototype.getStatusInfo = function ()
	{
		return {
			"fps": this.fps,
			"cpu": this.cpuutilisation / 1000,
			"gpu": this.gpuLastUtilisation,
			"layout": this.running_layout ? this.running_layout.name : "",
			"renderer": this.glUnmaskedRenderer
		};
	};
	
	Runtime.prototype.accelerate = function (velocity, min_speed, max_speed, acceleration, dt)
	{
		// Construct 2 accidentally omits the effect of the acceleration during a tick when calculating movement.
		// This can lead to errors on the order of several pixels when calculating things like jumps for the Platform movement.
		// Construct 3 can use the correct formula for more accurate movement. To fix this while maintaining backwards compatibility,
		// new projects all opt in to the enhanced precision, but it can be disabled to get C2's behavior back.
		if (this.enhancedAccelerationPrecision)
		{
			// To further complicate things, users often use extremely high acceleration for semi-instant acceleration. Some behaviors
			// clamp the max speed and then calculate the amount to move with this function, which in enhanced precision mode takes
			// in to account a very high acceleration during the tick without any max speed clamping. Additionally some movements like
			// Platform have different speed limits in either direction (e.g. -infinity up, max fall speed down). So to ensure movements
			// are always correctly clamped to the max speed, this method passes both the min and max speed for clamping the offset.
			var min = min_speed * dt;
			var max = max_speed * dt;
			
			// v * t + (1/2) * a * t^2
			return cr.clamp(velocity * dt + 0.5 * acceleration * dt * dt, min, max);
		}
		else
		{
			// v * t (ignores acceleration)
			return velocity * dt;
		}
	};
	
	cr.runtime = Runtime;
	
	cr.createRuntime = function (opts)
	{
		return new Runtime(opts);
	};
	
	window["cr_createRuntime"] = cr.createRuntime;
	
}());

window["cr_getSnapshot"] = function (format_, quality_)
{
	var runtime = window["c3runtime"];
	
	if (runtime)
		runtime.doCanvasSnapshot(format_, quality_);
}

window["cr_sizeCanvas"] = function(w, h)
{
	if (w === 0 || h === 0)
		return;
		
	var runtime = window["c3runtime"];
	
	if (runtime)
		runtime["setSize"](w, h);
}

window["cr_setSuspended"] = function(s)
{
	var runtime = window["c3runtime"];
	
	if (runtime)
		runtime["setSuspended"](s);
}


// layer.js
// ECMAScript 5 strict mode

;

(function()
{
	function sort_by_zindex(a, b)
	{
		return a.zindex - b.zindex;
	};
	
	// Layer class
	function Layer(layout, m)
	{
		// Runtime members
		this.layout = layout;
		this.runtime = layout.runtime;
		this.instances = [];        // running instances
		this.scale = 1.0;
		this.angle = 0;
		this.disableAngle = false;
		
		this.tmprect = new cr.rect(0, 0, 0, 0);
		this.tmpquad = new cr.quad();
		
		this.viewLeft = 0;
		this.viewRight = 0;
		this.viewTop = 0;
		this.viewBottom = 0;
		
		//this.number assigned by layout when created
		
		// Lazy-assigned instance Z indices
		this.zindices_stale = false;
		this.zindices_stale_from = -1;		// first index that has changed, or -1 if no bound
		
		this.clear_earlyz_index = 0;
		
		// Data model values
		this.name = m[0];
		this.index = m[1];
		this.sid = m[2];
		this.visible = m[3];		// initially visible
		this.background_color = m[4];
		this.transparent = m[5];
		this.parallaxX = m[6];
		this.parallaxY = m[7];
		this.opacity = m[8];
		this.forceOwnTexture = m[9];
		this.useRenderCells = m[10];
		this.zoomRate = m[11];
		this.blend_mode = m[12];
		this.effect_fallback = m[13];
		this.compositeOp = "source-over";
		this.srcBlend = 0;
		this.destBlend = 0;
		
		// If using render cells, create a RenderGrid to sort instances in to and a set of instances
		// needing bounding box updates
		this.render_grid = null;
		
		// Last render list in case not changed
		this.last_render_list = alloc_arr();
		this.render_list_stale = true;
		this.last_render_cells = new cr.rect(0, 0, -1, -1);
		this.cur_render_cells = new cr.rect(0, 0, -1, -1);
		
		if (this.useRenderCells)
		{
			this.render_grid = new cr.RenderGrid(this.runtime.original_width, this.runtime.original_height);
		}
		
		this.render_offscreen = false;
		
		// Initialise initial instances
		var im = m[14];
		var i, len;
		this.startup_initial_instances = [];		// for restoring initial_instances after load
		this.initial_instances = [];
		this.created_globals = [];		// global object UIDs already created - for save/load to avoid recreating
		
		for (i = 0, len = im.length; i < len; i++)
		{
			var inst = im[i];
			var type = this.runtime.types_by_index[inst[1]];
;
			
			// If type has no default instance properties, make it this one
			if (!type.default_instance)
			{
				type.default_instance = inst;
				type.default_layerindex = this.index;
			}
				
			this.initial_instances.push(inst);
			
			if (this.layout.initial_types.indexOf(type) === -1)
				this.layout.initial_types.push(type);
		}
		
		cr.shallowAssignArray(this.startup_initial_instances, this.initial_instances);
		
		// Assign shaders
		this.effect_types = [];
		this.active_effect_types = [];
		this.shaders_preserve_opaqueness = true;
		this.effect_params = [];
		
		for (i = 0, len = m[15].length; i < len; i++)
		{
			this.effect_types.push({
				id: m[15][i][0],
				name: m[15][i][1],
				shaderindex: -1,
				preservesOpaqueness: false,
				active: true,
				index: i
			});
			
			this.effect_params.push(m[15][i][2].slice(0));
		}
		
		this.updateActiveEffects();
		
		this.rcTexBounce = new cr.rect(0, 0, 1, 1);
		this.rcTexDest = new cr.rect(0, 0, 1, 1);
		this.rcTexOrigin = new cr.rect(0, 0, 1, 1);
	};
	
	Layer.prototype.updateActiveEffects = function ()
	{
		cr.clearArray(this.active_effect_types);
		
		this.shaders_preserve_opaqueness = true;
		
		var i, len, et;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			
			if (et.active)
			{
				this.active_effect_types.push(et);
				
				if (!et.preservesOpaqueness)
					this.shaders_preserve_opaqueness = false;
			}
		}
	};
	
	Layer.prototype.getEffectByName = function (name_)
	{
		var i, len, et;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			
			if (et.name === name_)
				return et;
		}
		
		return null;
	};

	Layer.prototype.createInitialInstances = function (created_instances)
	{
		var i, k, len, inst, initial_inst, type, keep, hasPersistBehavior;
		for (i = 0, k = 0, len = this.initial_instances.length; i < len; i++)
		{
			initial_inst = this.initial_instances[i];
			type = this.runtime.types_by_index[initial_inst[1]];
;
			
			hasPersistBehavior = this.runtime.typeHasPersistBehavior(type);
			keep = true;
			
			// Only create objects with the persist behavior on the first visit
			if (!hasPersistBehavior || this.layout.first_visit)
			{
				inst = this.runtime.createInstanceFromInit(initial_inst, this, true);
				
				if (!inst)
					continue;		// may have skipped creation due to fallback effect "destroy"
				
				created_instances.push(inst);
				
				// Remove global objects from the initial instances list
				if (inst.type.global)
				{
					keep = false;
					this.created_globals.push(inst.uid);
				}
			}
			
			if (keep)
			{
				this.initial_instances[k] = this.initial_instances[i];
				k++;
			}
		}
		
		this.initial_instances.length = k;
		
		this.runtime.ClearDeathRow();		// flushes creation row so IIDs will be correct
		
		// Set the blend mode if fallback requires
		if (!this.runtime.glwrap && this.effect_types.length)	// no WebGL renderer and shaders used
			this.blend_mode = this.effect_fallback;				// use fallback blend mode
		
		// Set the blend mode variables
		this.compositeOp = cr.effectToCompositeOp(this.blend_mode);
		
		if (this.runtime.gl)
			cr.setGLBlend(this, this.blend_mode, this.runtime.gl);
		
		this.render_list_stale = true;
	};
	
	Layer.prototype.recreateInitialObjects = function (only_type, rc)
	{
		var i, len, initial_inst, type, wm, x, y, inst, j, lenj, s;
		var types_by_index = this.runtime.types_by_index;
		var only_type_is_family = only_type.is_family;
		var only_type_members = only_type.members;
		
		for (i = 0, len = this.initial_instances.length; i < len; ++i)
		{
			initial_inst = this.initial_instances[i];
			
			// Check initial_inst origin is within rectangle
			wm = initial_inst[0];
			x = wm[0];
			y = wm[1];
			
			if (!rc.contains_pt(x, y))
				continue;		// not in the given area
			
			type = types_by_index[initial_inst[1]];
			
			if (type !== only_type)
			{
				if (only_type_is_family)
				{
					// 'type' is not in the family 'only_type'
					if (only_type_members.indexOf(type) < 0)
						continue;
				}
				else
					continue;		// only_type is not a family, and the initial inst type does not match
			}
			
			// OK to create it
			inst = this.runtime.createInstanceFromInit(initial_inst, this, false);
			
			// Fire 'On created' for this instance
			this.runtime.isInOnDestroy++;
		
			this.runtime.trigger(Object.getPrototypeOf(type.plugin).cnds.OnCreated, inst);
			
			if (inst.is_contained)
			{
				for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
				{
					s = inst.siblings[i];
					this.runtime.trigger(Object.getPrototypeOf(s.type.plugin).cnds.OnCreated, s);
				}
			}
			
			this.runtime.isInOnDestroy--;
		}
	};
	
	Layer.prototype.removeFromInstanceList = function (inst, remove_from_grid)
	{
		var index = cr.fastIndexOf(this.instances, inst);
		
		if (index < 0)
			return;		// not found
		
		// When using render cells, if remove_from_grid is specified then also remove it from
		// the layer render grid. Skip this if right < left, since that means it's not in the grid.
		if (remove_from_grid && this.useRenderCells && inst.rendercells && inst.rendercells.right >= inst.rendercells.left)
		{
			inst.update_bbox();											// make sure actually in its current rendercells
			this.render_grid.update(inst, inst.rendercells, null);		// no new range provided - remove only
			inst.rendercells.set(0, 0, -1, -1);							// set to invalid state to indicate not inserted
		}
		
		// If instance is at top of list, we can pop it off without making the Z indices stale
		if (index === this.instances.length - 1)
			this.instances.pop();
		else
		{	
			// otherwise have to splice it out
			cr.arrayRemove(this.instances, index);
			this.setZIndicesStaleFrom(index);
		}
		
		this.render_list_stale = true;
	};
	
	Layer.prototype.appendToInstanceList = function (inst, add_to_grid)
	{
;
		
		// Since we know the instance is going to the top we can assign its Z index
		// without making all Z indices stale
		inst.zindex = this.instances.length;
		this.instances.push(inst);
		
		if (add_to_grid && this.useRenderCells && inst.rendercells)
		{
			inst.set_bbox_changed();		// will cause immediate update and new insertion to grid
		}
		
		this.render_list_stale = true;
	};
	
	Layer.prototype.prependToInstanceList = function (inst, add_to_grid)
	{
;
		
		this.instances.unshift(inst);
		this.setZIndicesStaleFrom(0);
		
		if (add_to_grid && this.useRenderCells && inst.rendercells)
		{
			inst.set_bbox_changed();		// will cause immediate update and new insertion to grid
		}
	};
	
	Layer.prototype.moveInstanceAdjacent = function (inst, other, isafter)
	{
;
		
		// Now both objects are definitely on the same layer: move in the Z order.
		var myZ = inst.get_zindex();
		var insertZ = other.get_zindex();
		
		cr.arrayRemove(this.instances, myZ);
		
		// if myZ is lower than insertZ, insertZ will have shifted down one index
		if (myZ < insertZ)
			insertZ--;
			
		// if inserting after object, increment the insert index
		if (isafter)
			insertZ++;
			
		// insertZ may now be pointing at the end of the array. If so, push instead of splice
		if (insertZ === this.instances.length)
			this.instances.push(inst);
		else
			this.instances.splice(insertZ, 0, inst);
			
		this.setZIndicesStaleFrom(myZ < insertZ ? myZ : insertZ);
	};
	
	Layer.prototype.setZIndicesStaleFrom = function (index)
	{
		// Keep track of the lowest index zindices are stale from
		if (this.zindices_stale_from === -1)			// not yet set
			this.zindices_stale_from = index;
		else if (index < this.zindices_stale_from)		// determine minimum z index affected
			this.zindices_stale_from = index;
		
		this.zindices_stale = true;
		this.render_list_stale = true;
	};
	
	Layer.prototype.updateZIndices = function ()
	{
		if (!this.zindices_stale)
			return;
		
		if (this.zindices_stale_from === -1)
			this.zindices_stale_from = 0;
		
		var i, len, inst;
		
		// When using render cells, this instance's Z index has changed and therefore is probably
		// no longer in the correct sort order in its render cell. Make sure the render cell
		// knows it needs sorting.
		if (this.useRenderCells)
		{
			for (i = this.zindices_stale_from, len = this.instances.length; i < len; ++i)
			{
				inst = this.instances[i];
				inst.zindex = i;
				this.render_grid.markRangeChanged(inst.rendercells);
			}
		}
		else
		{
			for (i = this.zindices_stale_from, len = this.instances.length; i < len; ++i)
			{
				this.instances[i].zindex = i;
			}
		}
		
		this.zindices_stale = false;
		this.zindices_stale_from = -1;
	};
	
	Layer.prototype.getScale = function (include_aspect)
	{
		return this.getNormalScale() * (this.runtime.fullscreenScalingQuality || include_aspect ? this.runtime.aspect_scale : 1);
	};
	
	Layer.prototype.getNormalScale = function ()
	{
		return ((this.scale * this.layout.scale) - 1) * this.zoomRate + 1;
	};
	
	Layer.prototype.getAngle = function ()
	{
		if (this.disableAngle)
			return 0;
			
		return cr.clamp_angle(this.layout.angle + this.angle);
	};
	
	var arr_cache = [];

	function alloc_arr()
	{
		if (arr_cache.length)
			return arr_cache.pop();
		else
			return [];
	}

	function free_arr(a)
	{
		cr.clearArray(a);
		arr_cache.push(a);
	};
	
	function mergeSortedZArrays(a, b, out)
	{
		var i = 0, j = 0, k = 0, lena = a.length, lenb = b.length, ai, bj;
		out.length = lena + lenb;
		
		for ( ; i < lena && j < lenb; ++k)
		{
			ai = a[i];
			bj = b[j];
			
			if (ai.zindex < bj.zindex)
			{
				out[k] = ai;
				++i;
			}
			else
			{
				out[k] = bj;
				++j;
			}
		}
		
		// Finish last run of either array if not done yet
		for ( ; i < lena; ++i, ++k)
			out[k] = a[i];
		
		for ( ; j < lenb; ++j, ++k)
			out[k] = b[j];
	};
	
	var next_arr = [];
	
	function mergeAllSortedZArrays_pass(arr, first_pass)
	{
		var i, len, arr1, arr2, out;
		
		for (i = 0, len = arr.length; i < len - 1; i += 2)
		{
			arr1 = arr[i];
			arr2 = arr[i+1];
			out = alloc_arr();
			mergeSortedZArrays(arr1, arr2, out);
			
			// On all but the first pass, the arrays in arr are locally allocated
			// and can be recycled.
			if (!first_pass)
			{
				free_arr(arr1);
				free_arr(arr2);
			}
			
			next_arr.push(out);
		}
		
		// if odd number of items then last one wasn't collapsed - append in to result again
		if (len % 2 === 1)
		{
			// The first pass uses direct reference to render cell arrays, so we can't just
			// pass through the odd array - it must be allocated and copied so it's recyclable.
			if (first_pass)
			{
				arr1 = alloc_arr();
				cr.shallowAssignArray(arr1, arr[len - 1]);
				next_arr.push(arr1);
			}
			else
			{
				next_arr.push(arr[len - 1]);
			}
		}
		
		cr.shallowAssignArray(arr, next_arr);
		cr.clearArray(next_arr);
	};

	function mergeAllSortedZArrays(arr)
	{
		var first_pass = true;
		
		while (arr.length > 1)
		{
			mergeAllSortedZArrays_pass(arr, first_pass);
			first_pass = false;
		}
		
		return arr[0];
	};
	
	var render_arr = [];
	
	Layer.prototype.getRenderCellInstancesToDraw = function ()
	{
;
		
		// Ensure all Z indices up-to-date for sorting.
		this.updateZIndices();
		
		// Now render cells are up to date, collect all the sorted instance lists from the render cells
		// inside the viewport to an array of arrays to merge.
		this.render_grid.queryRange(this.viewLeft, this.viewTop, this.viewRight, this.viewBottom, render_arr);
		
		// If there were no render cells returned at all, return a dummy empty array, otherwise the below
		// sort returns undefined.
		if (!render_arr.length)
			return alloc_arr();
		
		// If there is just one list returned, it will be holding a direct reference to the render cell's contents.
		// The caller will try to free this array. So make sure it gets copied to an allocated array that
		// can be freed.
		if (render_arr.length === 1)
		{
			var a = alloc_arr();
			cr.shallowAssignArray(a, render_arr[0]);
			cr.clearArray(render_arr);
			return a;
		}
		
		// render_arr.length is >= 2. Merge the result in to a single Z-sorted list.
		var draw_list = mergeAllSortedZArrays(render_arr);
		
		// Caller recycles returned draw_list.
		cr.clearArray(render_arr);
		
		return draw_list;
	};

	Layer.prototype.draw = function (ctx)
	{
		// Needs own texture
		this.render_offscreen = (this.forceOwnTexture || this.opacity !== 1.0 || this.blend_mode !== 0);
		var layer_canvas = this.runtime.canvas;
		var layer_ctx = ctx;
		var ctx_changed = false;

		if (this.render_offscreen)
		{
			// Need another canvas to render to.  Ensure it is created.
			if (!this.runtime.layer_canvas)
			{
				this.runtime.layer_canvas = document.createElement("canvas");
;
				layer_canvas = this.runtime.layer_canvas;
				layer_canvas.width = this.runtime.draw_width;
				layer_canvas.height = this.runtime.draw_height;
				this.runtime.layer_ctx = layer_canvas.getContext("2d");
;
				ctx_changed = true;
			}

			layer_canvas = this.runtime.layer_canvas;
			layer_ctx = this.runtime.layer_ctx;

			// Window size has changed (browser fullscreen mode)
			if (layer_canvas.width !== this.runtime.draw_width)
			{
				layer_canvas.width = this.runtime.draw_width;
				ctx_changed = true;
			}
			if (layer_canvas.height !== this.runtime.draw_height)
			{
				layer_canvas.height = this.runtime.draw_height;
				ctx_changed = true;
			}
			
			if (ctx_changed)
			{
				layer_ctx.imageSmoothingEnabled = this.runtime.linearSampling;
			}

			// If transparent, there's no fillRect to clear it - so clear it transparent now
			if (this.transparent)
				layer_ctx.clearRect(0, 0, this.runtime.draw_width, this.runtime.draw_height);
		}
		
		layer_ctx.globalAlpha = 1;
		layer_ctx.globalCompositeOperation = "source-over";
		
		// Not transparent: fill with background
		if (!this.transparent)
		{
			layer_ctx.fillStyle = "rgb(" + this.background_color[0] + "," + this.background_color[1] + "," + this.background_color[2] + ")";
			layer_ctx.fillRect(0, 0, this.runtime.draw_width, this.runtime.draw_height);
		}

		layer_ctx.save();

		// Calculate the top-left point of the currently scrolled and scaled view (but not rotated)
		this.disableAngle = true;
		var px = this.canvasToLayer(0, 0, true, true);
		var py = this.canvasToLayer(0, 0, false, true);
		this.disableAngle = false;
		
		if (this.runtime.pixel_rounding)
		{
			px = Math.round(px);
			py = Math.round(py);
		}
		
		this.rotateViewport(px, py, layer_ctx);
		
		// Scroll the layer to the new top-left point and also scale
		var myscale = this.getScale();
		layer_ctx.scale(myscale, myscale);
		layer_ctx.translate(-px, -py);

		// Get instances to render. In render cells mode, this will be derived from the on-screen cells,
		// otherwise it just returns this.instances. If possible in render cells mode re-use the last
		// display list.
		var instances_to_draw;
		
		if (this.useRenderCells)
		{
			this.cur_render_cells.left = this.render_grid.XToCell(this.viewLeft);
			this.cur_render_cells.top = this.render_grid.YToCell(this.viewTop);
			this.cur_render_cells.right = this.render_grid.XToCell(this.viewRight);
			this.cur_render_cells.bottom = this.render_grid.YToCell(this.viewBottom);
			
			if (this.render_list_stale || !this.cur_render_cells.equals(this.last_render_cells))
			{
				free_arr(this.last_render_list);
				instances_to_draw = this.getRenderCellInstancesToDraw();
				this.render_list_stale = false;
				this.last_render_cells.copy(this.cur_render_cells);
			}
			else
				instances_to_draw = this.last_render_list;
		}
		else
			instances_to_draw = this.instances;
		
		var i, len, inst, last_inst = null;
		
		for (i = 0, len = instances_to_draw.length; i < len; ++i)
		{
			inst = instances_to_draw[i];
			
			// Render cells are allowed to return a sorted list with duplicates. In this case the same instance
			// may appear multiple times consecutively. To avoid multiple draws, skip consecutive entries.
			if (inst === last_inst)
				continue;
			
			this.drawInstance(inst, layer_ctx);
			last_inst = inst;
		}
		
		// If used render cells, instances_to_draw is temporary and should be recycled
		if (this.useRenderCells)
			this.last_render_list = instances_to_draw;

		layer_ctx.restore();

		// If rendered to texture, paste to main display now
		if (this.render_offscreen)
		{
			// Drawing at layer opacity with layer blend mode
			ctx.globalCompositeOperation = this.compositeOp;
			ctx.globalAlpha = this.opacity;

			ctx.drawImage(layer_canvas, 0, 0);
		}
	};
	
	Layer.prototype.drawInstance = function(inst, layer_ctx)
	{
		// Skip if invisible or zero sized
		if (!inst.visible || inst.width === 0 || inst.height === 0)
			return;

		// Skip if not in the viewable area
		inst.update_bbox();
		var bbox = inst.bbox;
		
		if (bbox.right < this.viewLeft || bbox.bottom < this.viewTop || bbox.left > this.viewRight || bbox.top > this.viewBottom)
			return;

		// Draw the instance
		layer_ctx.globalCompositeOperation = inst.compositeOp;
		inst.draw(layer_ctx);
	};
	
	Layer.prototype.updateViewport = function (ctx)
	{
		this.disableAngle = true;
		var px = this.canvasToLayer(0, 0, true, true);
		var py = this.canvasToLayer(0, 0, false, true);
		this.disableAngle = false;
		
		if (this.runtime.pixel_rounding)
		{
			px = Math.round(px);
			py = Math.round(py);
		}
		
		this.rotateViewport(px, py, ctx);
	};
	
	Layer.prototype.rotateViewport = function (px, py, ctx)
	{
		var myscale = this.getScale();
		
		this.viewLeft = px;
		this.viewTop = py;
		this.viewRight = px + (this.runtime.draw_width * (1 / myscale));
		this.viewBottom = py + (this.runtime.draw_height * (1 / myscale));
		
		var myAngle = this.getAngle();
		
		if (myAngle !== 0)
		{
			if (ctx)
			{
				ctx.translate(this.runtime.draw_width / 2, this.runtime.draw_height / 2);
				ctx.rotate(-myAngle);
				ctx.translate(this.runtime.draw_width / -2, this.runtime.draw_height / -2);
			}
			
			// adjust viewport bounds
			this.tmprect.set(this.viewLeft, this.viewTop, this.viewRight, this.viewBottom);
			this.tmprect.offset((this.viewLeft + this.viewRight) / -2, (this.viewTop + this.viewBottom) / -2);
			this.tmpquad.set_from_rotated_rect(this.tmprect, myAngle);
			this.tmpquad.bounding_box(this.tmprect);
			this.tmprect.offset((this.viewLeft + this.viewRight) / 2, (this.viewTop + this.viewBottom) / 2);
			this.viewLeft = this.tmprect.left;
			this.viewTop = this.tmprect.top;
			this.viewRight = this.tmprect.right;
			this.viewBottom = this.tmprect.bottom;
		}
	}
	
	Layer.prototype.drawGL_earlyZPass = function (glw)
	{
		var windowWidth = this.runtime.draw_width;
		var windowHeight = this.runtime.draw_height;
		var shaderindex = 0;
		var etindex = 0;
		
		// In early Z mode, this layer will only need its own texture in force own texture mode.
		// Early Z is skipped if the blend mode or opacity have changed, or if there are any effects.
		this.render_offscreen = this.forceOwnTexture;

		if (this.render_offscreen)
		{
			// Need another canvas to render to.  Ensure it is created.
			if (!this.runtime.layer_tex)
			{
				this.runtime.layer_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
			}

			// Window size has changed (browser fullscreen mode)
			if (this.runtime.layer_tex.c2width !== this.runtime.draw_width || this.runtime.layer_tex.c2height !== this.runtime.draw_height)
			{
				glw.deleteTexture(this.runtime.layer_tex);
				this.runtime.layer_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
			}
			
			glw.setRenderingToTexture(this.runtime.layer_tex);
		}

		// Calculate the top-left point of the currently scrolled and scaled view (but not rotated)
		this.disableAngle = true;
		var px = this.canvasToLayer(0, 0, true, true);
		var py = this.canvasToLayer(0, 0, false, true);
		this.disableAngle = false;
		
		if (this.runtime.pixel_rounding)
		{
			px = Math.round(px);
			py = Math.round(py);
		}
		
		this.rotateViewport(px, py, null);
		
		// Scroll the layer to the new top-left point and also scale
		var myscale = this.getScale();
		glw.resetModelView();
		glw.scale(myscale, myscale);
		glw.rotateZ(-this.getAngle());
		glw.translate((this.viewLeft + this.viewRight) / -2, (this.viewTop + this.viewBottom) / -2);
		glw.updateModelView();

		// Get instances to render. In render cells mode, this will be derived from the on-screen cells,
		// otherwise it just returns this.instances. If possible in render cells mode re-use the last
		// display list.
		var instances_to_draw;
		
		if (this.useRenderCells)
		{
			this.cur_render_cells.left = this.render_grid.XToCell(this.viewLeft);
			this.cur_render_cells.top = this.render_grid.YToCell(this.viewTop);
			this.cur_render_cells.right = this.render_grid.XToCell(this.viewRight);
			this.cur_render_cells.bottom = this.render_grid.YToCell(this.viewBottom);
			
			if (this.render_list_stale || !this.cur_render_cells.equals(this.last_render_cells))
			{
				free_arr(this.last_render_list);
				instances_to_draw = this.getRenderCellInstancesToDraw();
				this.render_list_stale = false;
				this.last_render_cells.copy(this.cur_render_cells);
			}
			else
				instances_to_draw = this.last_render_list;
		}
		else
			instances_to_draw = this.instances;
		
		// Render instances in front-to-back order
		var i, inst, last_inst = null;
		
		for (i = instances_to_draw.length - 1; i >= 0; --i)
		{
			inst = instances_to_draw[i];
			
			// Render cells are allowed to return a sorted list with duplicates. In this case the same instance
			// may appear multiple times consecutively. To avoid multiple draws, skip consecutive entries.
			if (inst === last_inst)
				continue;
			
			this.drawInstanceGL_earlyZPass(instances_to_draw[i], glw);
			last_inst = inst;
		}
		
		// If used render cells, cache the last display list in case it can be re-used again
		if (this.useRenderCells)
			this.last_render_list = instances_to_draw;
		
		// Not transparent: fill with background
		if (!this.transparent)
		{
			this.clear_earlyz_index = this.runtime.earlyz_index++;
			glw.setEarlyZIndex(this.clear_earlyz_index);
			
			// fill color does not matter, simply exists to fill depth buffer
			glw.setColorFillMode(1, 1, 1, 1);
			glw.fullscreenQuad();		// fill remaining space in depth buffer with current Z value
			glw.restoreEarlyZMode();
		}
	};
	
	Layer.prototype.drawGL = function (glw)
	{
		var windowWidth = this.runtime.draw_width;
		var windowHeight = this.runtime.draw_height;
		var shaderindex = 0;
		var etindex = 0;
		
		// Needs own texture
		this.render_offscreen = (this.forceOwnTexture || this.opacity !== 1.0 || this.active_effect_types.length > 0 || this.blend_mode !== 0);

		if (this.render_offscreen)
		{
			// Need another canvas to render to.  Ensure it is created.
			if (!this.runtime.layer_tex)
			{
				this.runtime.layer_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
			}

			// Window size has changed (browser fullscreen mode)
			if (this.runtime.layer_tex.c2width !== this.runtime.draw_width || this.runtime.layer_tex.c2height !== this.runtime.draw_height)
			{
				glw.deleteTexture(this.runtime.layer_tex);
				this.runtime.layer_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
			}
			
			glw.setRenderingToTexture(this.runtime.layer_tex);

			// If transparent, there's no fillRect to clear it - so clear it transparent now
			if (this.transparent)
				glw.clear(0, 0, 0, 0);
		}
		
		// Not transparent: fill with background
		if (!this.transparent)
		{
			if (this.runtime.enableFrontToBack)
			{
				// front-to-back rendering: use fullscreen quad to take advantage of depth buffer
				glw.setEarlyZIndex(this.clear_earlyz_index);
			
				glw.setColorFillMode(this.background_color[0] / 255, this.background_color[1] / 255, this.background_color[2] / 255, 1);
				glw.fullscreenQuad();
				glw.setTextureFillMode();
			}
			else
			{
				// back-to-front rendering: normal clear
				glw.clear(this.background_color[0] / 255, this.background_color[1] / 255, this.background_color[2] / 255, 1);
			}
		}

		// Calculate the top-left point of the currently scrolled and scaled view (but not rotated)
		this.disableAngle = true;
		var px = this.canvasToLayer(0, 0, true, true);
		var py = this.canvasToLayer(0, 0, false, true);
		this.disableAngle = false;
		
		if (this.runtime.pixel_rounding)
		{
			px = Math.round(px);
			py = Math.round(py);
		}
		
		this.rotateViewport(px, py, null);
		
		// Scroll the layer to the new top-left point and also scale
		var myscale = this.getScale();
		glw.resetModelView();
		glw.scale(myscale, myscale);
		glw.rotateZ(-this.getAngle());
		glw.translate((this.viewLeft + this.viewRight) / -2, (this.viewTop + this.viewBottom) / -2);
		glw.updateModelView();

		// Get instances to render. In render cells mode, this will be derived from the on-screen cells,
		// otherwise it just returns this.instances. If possible in render cells mode re-use the last
		// display list.
		var instances_to_draw;
		
		if (this.useRenderCells)
		{
			this.cur_render_cells.left = this.render_grid.XToCell(this.viewLeft);
			this.cur_render_cells.top = this.render_grid.YToCell(this.viewTop);
			this.cur_render_cells.right = this.render_grid.XToCell(this.viewRight);
			this.cur_render_cells.bottom = this.render_grid.YToCell(this.viewBottom);
			
			if (this.render_list_stale || !this.cur_render_cells.equals(this.last_render_cells))
			{
				free_arr(this.last_render_list);
				instances_to_draw = this.getRenderCellInstancesToDraw();
				this.render_list_stale = false;
				this.last_render_cells.copy(this.cur_render_cells);
			}
			else
				instances_to_draw = this.last_render_list;
		}
		else
			instances_to_draw = this.instances;
		
		var i, len, inst, last_inst = null;
		
		for (i = 0, len = instances_to_draw.length; i < len; ++i)
		{
			inst = instances_to_draw[i];
			
			// Render cells are allowed to return a sorted list with duplicates. In this case the same instance
			// may appear multiple times consecutively. To avoid multiple draws, skip consecutive entries.
			if (inst === last_inst)
				continue;
			
			this.drawInstanceGL(instances_to_draw[i], glw);
			last_inst = inst;
		}
		
		// If used render cells, cache the last display list in case it can be re-used again
		if (this.useRenderCells)
			this.last_render_list = instances_to_draw;

		// If rendered to texture, paste to main display now
		if (this.render_offscreen)
		{
			// Note some of the single-shader rendering limitations also apply to layers
			//if (inst.type.effect_types.length === 1 && !glw.programUsesCrossSampling(shaderindex) &&
			//		!glw.programExtendsBox(shaderindex) && (!inst.angle || !glw.programUsesDest(shaderindex)) &&
			//		inst.opacity === 1)
			shaderindex = this.active_effect_types.length ? this.active_effect_types[0].shaderindex : 0;
			etindex = this.active_effect_types.length ? this.active_effect_types[0].index : 0;
			
			if (this.active_effect_types.length === 0 || (this.active_effect_types.length === 1 &&
				!glw.programUsesCrossSampling(shaderindex) && this.opacity === 1))
			{				
				if (this.active_effect_types.length === 1)
				{
					glw.switchProgram(shaderindex);
					glw.setProgramParameters(this.layout.getRenderTarget(),		// backTex
											 1.0 / this.runtime.draw_width,		// pixelWidth
											 1.0 / this.runtime.draw_height,	// pixelHeight
											 0.0, 0.0,							// srcStart
											 1.0, 1.0,							// srcEnd
											 0.0, 0.0,							// srcOriginStart
											 1.0, 1.0,							// srcOriginEnd
											 this.viewLeft, this.viewTop,		// layoutStart
											 this.viewRight, this.viewBottom,	// layoutEnd
											 0.0, 0.0,							// destStart
											 1.0, 1.0,							// destEnd
											 myscale,							// layerScale
											 this.getAngle(),
											 this.runtime.kahanTime.sum,
											 this.effect_params[etindex]);		// fx parameters
											 
					if (glw.programIsAnimated(shaderindex))
						this.runtime.redraw = true;
				}
				else
					glw.switchProgram(0);
					
				glw.setRenderingToTexture(this.layout.getRenderTarget());
				glw.setOpacity(this.opacity);
				glw.setTexture(this.runtime.layer_tex);
				glw.setBlend(this.srcBlend, this.destBlend);
				glw.resetModelView();
				glw.updateModelView();
				var halfw = this.runtime.draw_width / 2;
				var halfh = this.runtime.draw_height / 2;
				glw.quad(-halfw, halfh, halfw, halfh, halfw, -halfh, -halfw, -halfh);
				glw.setTexture(null);
			}
			else
			{
				this.layout.renderEffectChain(glw, this, null, this.layout.getRenderTarget());
			}
		}
	};
	
	Layer.prototype.drawInstanceGL = function (inst, glw)
	{
;
		
		// Skip if invisible or zero sized
		if (!inst.visible || inst.width === 0 || inst.height === 0)
			return;

		// Skip if not in the viewable area
		inst.update_bbox();
		var bbox = inst.bbox;
		
		if (bbox.right < this.viewLeft || bbox.bottom < this.viewTop || bbox.left > this.viewRight || bbox.top > this.viewBottom)
			return;

		glw.setEarlyZIndex(inst.earlyz_index);
		
		// Draw using shaders
		if (inst.uses_shaders)
		{
			this.drawInstanceWithShadersGL(inst, glw);
		}
		// Draw normally without any special shaders
		else
		{
			glw.switchProgram(0);		// un-set any previously set shader
			glw.setBlend(inst.srcBlend, inst.destBlend);
			inst.drawGL(glw);
		}
	};
	
	Layer.prototype.drawInstanceGL_earlyZPass = function (inst, glw)
	{
;
		
		// As per normal rendering, skip if invisible or zero sized
		if (!inst.visible || inst.width === 0 || inst.height === 0)
			return;

		// As per normal rendering, skip if not in the viewable area
		inst.update_bbox();
		var bbox = inst.bbox;
		
		if (bbox.right < this.viewLeft || bbox.bottom < this.viewTop || bbox.left > this.viewRight || bbox.top > this.viewBottom)
			return;
		
		// Write the distance-increasing early Z index to the instance to reuse later.
		// Note this is done after the same checks as normal rendering, so we only Z index the objects that are
		// actually going to have draw calls made. Later when the real draw call is made, its Z position is based
		// off this value.
		inst.earlyz_index = this.runtime.earlyz_index++;
		
		// Don't actually make an early Z pass if the object does not preserve opaqueness, or if
		// it doesn't support the drawGL_earlyZPass method.
		if (inst.blend_mode !== 0 || inst.opacity !== 1 || !inst.shaders_preserve_opaqueness || !inst.drawGL_earlyZPass)
			return;
		
		glw.setEarlyZIndex(inst.earlyz_index);
		inst.drawGL_earlyZPass(glw);
	};
	
	Layer.prototype.drawInstanceWithShadersGL = function (inst, glw)
	{
		// Where possible, draw an instance using a single shader direct to display for
		// maximum efficiency.  This can only be done if:
		// 1) The shader does not use cross-sampling.  If it does it has to render to an intermediate
		//    texture to prevent glitching, which is done via renderEffectChain.
		// 2) The shader does not use background blending, or the object is not rotated (at 0 degrees).
		//    Since the background is sampled linearly as a bounding box, it only works when the object
		//    is not rotated, otherwise the background gets rotated as well.  To fix this rotated objects
		//	  are pre-drawn to an offscreen surface in renderEffectChain.
		// 3) The shader does not extend the bounding box.  In this case as per 2) it also needs
		//    pre-drawing to an offscreen surface for the bounds to be enlarged.
		// 4) The object has 100% opacity.  If it has a different opacity, the opacity must be processed
		//    by pre-drawing.
		// Consider a screen blend for an unrotated object at 100% opacity on a mobile device.  While the
		// restrictions are fairly complicated, this allows the device to simply switch program, set
		// parameters and render without having to do any of the GPU-intensive swapping done in renderEffectChain.
		var shaderindex = inst.active_effect_types[0].shaderindex;
		var etindex = inst.active_effect_types[0].index;
		var myscale = this.getScale();
		
		if (inst.active_effect_types.length === 1 && !glw.programUsesCrossSampling(shaderindex) &&
			!glw.programExtendsBox(shaderindex) && ((!inst.angle && !inst.layer.getAngle()) || !glw.programUsesDest(shaderindex)) &&
			inst.opacity === 1 && !inst.type.plugin.must_predraw)
		{
			// Set the shader program to use
			glw.switchProgram(shaderindex);
			glw.setBlend(inst.srcBlend, inst.destBlend);
			
			if (glw.programIsAnimated(shaderindex))
				this.runtime.redraw = true;
			
			var destStartX = 0, destStartY = 0, destEndX = 0, destEndY = 0;
			
			// Skip screen co-ord calculations if shader doesn't use them
			if (glw.programUsesDest(shaderindex))
			{
				// Set the shader parameters
				var bbox = inst.bbox;
				var screenleft = this.layerToCanvas(bbox.left, bbox.top, true, true);
				var screentop = this.layerToCanvas(bbox.left, bbox.top, false, true);
				var screenright = this.layerToCanvas(bbox.right, bbox.bottom, true, true);
				var screenbottom = this.layerToCanvas(bbox.right, bbox.bottom, false, true);
				
				destStartX = screenleft / windowWidth;
				destStartY = 1 - screentop / windowHeight;
				destEndX = screenright / windowWidth;
				destEndY = 1 - screenbottom / windowHeight;
			}
			
			var srcStartX = 0, srcStartY = 0, srcEndX = 1, srcEndY = 1;
			var pxWidth = 1 / inst.width;
			var pxHeight = 1 / inst.height;
				
			// HACK for Sprite plugin: if we can find spritesheet co-ordinates for this instance, use them as the source rectangle.
			if (inst.curFrame && inst.curFrame.sheetTex)
			{
				srcStartX = inst.curFrame.sheetTex.left;
				srcStartY = inst.curFrame.sheetTex.top;
				srcEndX = inst.curFrame.sheetTex.right;
				srcEndY = inst.curFrame.sheetTex.bottom;
				
				if (inst.curFrame.texture_img)
				{
					pxWidth = 1 / inst.curFrame.texture_img.width;
					pxHeight = 1 / inst.curFrame.texture_img.height;
				}
			}
	
			glw.setProgramParameters(this.render_offscreen ? this.runtime.layer_tex : this.layout.getRenderTarget(), // backTex
									 pxWidth,							// pixelWidth
									 pxHeight,							// pixelHeight
									 srcStartX, srcStartY,				// srcStart
									 srcEndX, srcEndY,					// srcEnd
									 srcStartX, srcStartY,				// srcOriginStart
									 srcEndX, srcEndY,					// srcOriginEnd
									 inst.bbox.left, inst.bbox.top,		// layoutStart
									 inst.bbox.right, inst.bbox.bottom,	// layoutEnd
									 destStartX, destStartY,
									 destEndX, destEndY,
									 myscale,
									 this.getAngle(),
									 this.runtime.kahanTime.sum,
									 inst.effect_params[etindex]);
			
			// Draw instance
			inst.drawGL(glw);
		}
		// Draw using offscreen surfaces
		else
		{
			this.layout.renderEffectChain(glw, this, inst, this.render_offscreen ? this.runtime.layer_tex : this.layout.getRenderTarget());
			
			// Reset model view
			glw.resetModelView();
			glw.scale(myscale, myscale);
			glw.rotateZ(-this.getAngle());
			glw.translate((this.viewLeft + this.viewRight) / -2, (this.viewTop + this.viewBottom) / -2);
			glw.updateModelView();
		}
	};
	
	// Translate point in canvas coords to layer coords
	Layer.prototype.canvasToLayer = function (ptx, pty, getx, using_draw_area)
	{
		// Take in to account retina displays which map css to canvas pixels differently
		var multiplier = this.runtime.devicePixelRatio;
		
		if (this.runtime.isRetina)
		{
			ptx *= multiplier;
			pty *= multiplier;
		}
		
		// Apply parallax
		var ox = this.runtime.parallax_x_origin;
		var oy = this.runtime.parallax_y_origin;
		var par_x = ((this.layout.scrollX - ox) * this.parallaxX) + ox;
		var par_y = ((this.layout.scrollY - oy) * this.parallaxY) + oy;
		var x = par_x;
		var y = par_y;
		
		// Move to top-left of visible area
		var invScale = 1 / this.getScale(!using_draw_area);
		
		if (using_draw_area)
		{
			x -= (this.runtime.draw_width * invScale) / 2;
			y -= (this.runtime.draw_height * invScale) / 2;
		}
		else
		{
			x -= (this.runtime.width * invScale) / 2;
			y -= (this.runtime.height * invScale) / 2;
		}
		
		x += ptx * invScale;
		y += pty * invScale;
		
		// Rotate about scroll center
		var a = this.getAngle();
		if (a !== 0)
		{
			x -= par_x;
			y -= par_y;
			var cosa = Math.cos(a);
			var sina = Math.sin(a);
			var x_temp = (x * cosa) - (y * sina);
			y = (y * cosa) + (x * sina);
			x = x_temp;
			x += par_x;
			y += par_y;
		}
		
		// Return point in layer coords
		return getx ? x : y;
	};
	
	// If ignore_aspect is passed, converts layer to draw area instead
	Layer.prototype.layerToCanvas = function (ptx, pty, getx, using_draw_area)
	{
		var ox = this.runtime.parallax_x_origin;
		var oy = this.runtime.parallax_y_origin;
		var par_x = ((this.layout.scrollX - ox) * this.parallaxX) + ox;
		var par_y = ((this.layout.scrollY - oy) * this.parallaxY) + oy;
		var x = par_x;
		var y = par_y;
		
		// Rotate about canvas center
		var a = this.getAngle();
		
		if (a !== 0)
		{
			ptx -= par_x;
			pty -= par_y;
			var cosa = Math.cos(-a);
			var sina = Math.sin(-a);
			var x_temp = (ptx * cosa) - (pty * sina);
			pty = (pty * cosa) + (ptx * sina);
			ptx = x_temp;
			ptx += par_x;
			pty += par_y;
		}
		
		var invScale = 1 / this.getScale(!using_draw_area);
		
		if (using_draw_area)
		{
			x -= (this.runtime.draw_width * invScale) / 2;
			y -= (this.runtime.draw_height * invScale) / 2;
		}
		else
		{
			x -= (this.runtime.width * invScale) / 2;
			y -= (this.runtime.height * invScale) / 2;
		}
		
		x = (ptx - x) / invScale;
		y = (pty - y) / invScale;
	
		// Take in to account retina displays which map css to canvas pixels differently
		var multiplier = this.runtime.devicePixelRatio;
		
		if (this.runtime.isRetina && !using_draw_area)
		{
			x /= multiplier;
			y /= multiplier;
		}
		
		return getx ? x : y;
	};
	
	Layer.prototype.rotatePt = function (x_, y_, getx)
	{
		if (this.getAngle() === 0)
			return getx ? x_ : y_;
		
		var nx = this.layerToCanvas(x_, y_, true);
		var ny = this.layerToCanvas(x_, y_, false);
		
		this.disableAngle = true;
		var px = this.canvasToLayer(nx, ny, true);
		var py = this.canvasToLayer(nx, ny, true);
		this.disableAngle = false;
		
		return getx ? px : py;
	};
	
	Layer.prototype.saveToJSON = function ()
	{
		var i, len, et;
		
		var o = {
			"s": this.scale,
			"a": this.angle,
			"vl": this.viewLeft,
			"vt": this.viewTop,
			"vr": this.viewRight,
			"vb": this.viewBottom,
			"v": this.visible,
			"bc": this.background_color,
			"t": this.transparent,
			"px": this.parallaxX,
			"py": this.parallaxY,
			"o": this.opacity,
			"zr": this.zoomRate,
			"fx": [],
			"cg": this.created_globals,		// added r197; list of global UIDs already created
			"instances": []
		};
		
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			o["fx"].push({"name": et.name, "active": et.active, "params": this.effect_params[et.index] });
		}
		
		return o;
	};
	
	Layer.prototype.loadFromJSON = function (o)
	{
		var i, j, len, p, inst, fx;
		
		this.scale = o["s"];
		this.angle = o["a"];
		this.viewLeft = o["vl"];
		this.viewTop = o["vt"];
		this.viewRight = o["vr"];
		this.viewBottom = o["vb"];
		this.visible = o["v"];
		this.background_color = o["bc"];
		this.transparent = o["t"];
		this.parallaxX = o["px"];
		this.parallaxY = o["py"];
		this.opacity = o["o"];
		this.zoomRate = o["zr"];
		this.created_globals = o["cg"] || [];		// added r197
		
		// If we are loading a state that has already created global objects, they need to be removed
		// from initial_instances again. Restore all the original initial instances (startup_initial_instances) 
		// then run through the initial_instances list and remove any instances that have a UID in the created_globals list.
		cr.shallowAssignArray(this.initial_instances, this.startup_initial_instances);
		
		var temp_set = new cr.ObjectSet();
		for (i = 0, len = this.created_globals.length; i < len; ++i)
			temp_set.add(this.created_globals[i]);
		
		for (i = 0, j = 0, len = this.initial_instances.length; i < len; ++i)
		{
			if (!temp_set.contains(this.initial_instances[i][2]))		// UID in element 2
			{
				this.initial_instances[j] = this.initial_instances[i];
				++j;
			}
		}
		
		cr.truncateArray(this.initial_instances, j);
		
		// Load active effects and effect parameters
		var ofx = o["fx"];
		
		for (i = 0, len = ofx.length; i < len; i++)
		{
			fx = this.getEffectByName(ofx[i]["name"]);
			
			if (!fx)
				continue;		// must've gone missing
				
			fx.active = ofx[i]["active"];
			this.effect_params[fx.index] = ofx[i]["params"];
		}
		
		this.updateActiveEffects();
		
		// Load instances.
		// Before this step, all instances were created on the correct layers. So we have the right
		// instances on this layer, but they need to be updated so their Z order is correct given their
		// zindex properties that were loaded. So sort the instances list now.
		this.instances.sort(sort_by_zindex);
		
		// There could be duplicate or missing Z indices, so re-index all the Z indices again anyway.
		this.zindices_stale = true;
	};
	
	cr.layer = Layer;
}());


// layout.js
// ECMAScript 5 strict mode

;

(function()
{
	// Layout class
	function Layout(runtime, m)
	{
		// Runtime members
		this.runtime = runtime;
		this.event_sheet = null;
		this.scrollX = (this.runtime.original_width / 2);
		this.scrollY = (this.runtime.original_height / 2);
		this.scale = 1.0;
		this.angle = 0;
		this.first_visit = true;
		
		// Data model values
		this.name = m[0];
		this.originalWidth = m[1];
		this.originalHeight = m[2];
		this.width = m[1];
		this.height = m[2];
		this.unbounded_scrolling = m[3];
		this.sheetname = m[4];
		this.sid = m[5];
		
		// Create layers from layers model
		var lm = m[6];
		var i, len;
		this.layers = [];
		this.initial_types = [];
		
		for (i = 0, len = lm.length; i < len; i++)
		{
			// Create real layer
			var layer = new cr.layer(this, lm[i]);
			layer.number = i;
			cr.seal(layer);
			this.layers.push(layer);
		}
		
		// Initialise nonworld instances from model
		var im = m[7];
		this.initial_nonworld = [];
		
		for (i = 0, len = im.length; i < len; i++)
		{
			var inst = im[i];

			// Lookup type index
			var type = this.runtime.types_by_index[inst[1]];
;

			// If type has no default instance, make it this one
			if (!type.default_instance)
				type.default_instance = inst;
				
			this.initial_nonworld.push(inst);
			
			if (this.initial_types.indexOf(type) === -1)
				this.initial_types.push(type);
		}
		
		// Assign shaders
		this.effect_types = [];
		this.active_effect_types = [];
		this.shaders_preserve_opaqueness = true;
		this.effect_params = [];
		
		for (i = 0, len = m[8].length; i < len; i++)
		{
			this.effect_types.push({
				id: m[8][i][0],
				name: m[8][i][1],
				shaderindex: -1,
				preservesOpaqueness: false,
				active: true,
				index: i
			});
			
			this.effect_params.push(m[8][i][2].slice(0));
		}
		
		this.updateActiveEffects();
		
		this.rcTexBounce = new cr.rect(0, 0, 1, 1);
		this.rcTexDest = new cr.rect(0, 0, 1, 1);
		this.rcTexOrigin = new cr.rect(0, 0, 1, 1);
		
		// Viewport of layout, calculated as a layer with default settings. Used for effect parameters.
		this.viewRect = new cr.rect(0, 0, 0, 0);
		
		// For persist behavior: {"type_sid": [inst, inst, inst...] }
		this.persist_data = {};
	};
	
	Layout.prototype.saveObjectToPersist = function (inst)
	{
		var sidStr = inst.type.sid.toString();
		
		if (!this.persist_data.hasOwnProperty(sidStr))
			this.persist_data[sidStr] = [];
			
		var type_persist = this.persist_data[sidStr];		
		type_persist.push(this.runtime.saveInstanceToJSON(inst));
	};
	
	Layout.prototype.hasOpaqueBottomLayer = function ()
	{
		var layer = this.layers[0];
		return !layer.transparent && layer.opacity === 1.0 && !layer.forceOwnTexture && layer.visible;
	};
	
	Layout.prototype.updateActiveEffects = function ()
	{
		cr.clearArray(this.active_effect_types);
		
		this.shaders_preserve_opaqueness = true;
		
		var i, len, et;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			
			if (et.active)
			{
				this.active_effect_types.push(et);
				
				if (!et.preservesOpaqueness)
					this.shaders_preserve_opaqueness = false;
			}
		}
	};
	
	Layout.prototype.getEffectByName = function (name_)
	{
		var i, len, et;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			
			if (et.name === name_)
				return et;
		}
		
		return null;
	};
	
	var created_instances = [];
	
	function sort_by_zindex(a, b)
	{
		return a.zindex - b.zindex;
	};
	
	var first_layout = true;

	Layout.prototype.startRunning = function ()
	{
		// Find event sheet
		if (this.sheetname)
		{
			this.event_sheet = this.runtime.eventsheets[this.sheetname];
;
			
			this.event_sheet.updateDeepIncludes();
		}

		// Mark this layout as running
		this.runtime.running_layout = this;
		
		// Restore the original layout size, since the savegame system may have changed it
		this.width = this.originalWidth;
		this.height = this.originalHeight;

		// Scroll to top left
		this.scrollX = (this.runtime.original_width / 2);
		this.scrollY = (this.runtime.original_height / 2);
		
		// Shift all leftover global objects with a layer to this layout's layers instead
		var i, k, len, lenk, type, type_instances, initial_inst, inst, iid, t, s, p, q, type_data, layer;
		
		for (i = 0, len = this.runtime.types_by_index.length; i < len; i++)
		{
			type = this.runtime.types_by_index[i];
			
			if (type.is_family)
				continue;		// instances are only transferred for their real type
			
			type_instances = type.instances;
			
			for (k = 0, lenk = type_instances.length; k < lenk; k++)
			{
				inst = type_instances[k];
				
				if (inst.layer)
				{
					var num = inst.layer.number;
					if (num >= this.layers.length)
						num = this.layers.length - 1;
					inst.layer = this.layers[num];
					
					// Instances created when destroying objects from leaving the last layout
					// may still reside in the layer instance list. Be sure not to add twice.
					if (inst.layer.instances.indexOf(inst) === -1)
						inst.layer.instances.push(inst);
					
					inst.layer.zindices_stale = true;
				}
			}
		}
		
		// All the transferred global instances are now in whatever order they sit in their
		// instance lists, which could be jumbled up compared to their previous Z index.
		// Sort every layer's instances by their old Z indices to make an effort to preserve
		// global object's relative Z orders between layouts.
		// Don't do this on the very first layout run though, only when coming from another layout.
		if (!first_layout)
		{
			for (i = 0, len = this.layers.length; i < len; ++i)
			{
				this.layers[i].instances.sort(sort_by_zindex);
			}
		}
		
		var layer;
		cr.clearArray(created_instances);
		
		this.boundScrolling();

		// Create all the initial instances on layers
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			layer = this.layers[i];
			layer.createInitialInstances(created_instances);		// fills created_instances
			
			// Also reset layer view area (will not be set until next draw(), but it's better
			// than leaving it where it was from the last layout, which could be a different place)
			// Calculate the starting position, since otherwise 'Is on screen' is false for first tick
			// even for objects which are initially visible
			layer.updateViewport(null);
		}
		
		var uids_changed = false;
		
		// On second run and after, create persisted objects that were saved
		if (!this.first_visit)
		{
			for (p in this.persist_data)
			{
				if (this.persist_data.hasOwnProperty(p))
				{
					type = this.runtime.getObjectTypeBySid(parseInt(p, 10));
					
					if (!type || type.is_family || !this.runtime.typeHasPersistBehavior(type))
						continue;
					
					type_data = this.persist_data[p];
					
					for (i = 0, len = type_data.length; i < len; i++)
					{
						layer = null;
					
						if (type.plugin.is_world)
						{
							layer = this.getLayerBySid(type_data[i]["w"]["l"]);
							
							// layer's gone missing - just skip creating this instance
							if (!layer)
								continue;
						}
						
						// create an instance then load the state in to it
						// skip creating siblings; we'll link them up later
						inst = this.runtime.createInstanceFromInit(type.default_instance, layer, false, 0, 0, true);
						this.runtime.loadInstanceFromJSON(inst, type_data[i]);
						
						// createInstanceFromInit may have assigned a different UID to the one
						// loaded by loadInstanceFromJSON, so the runtime UID map may be wrong.
						// Make sure we rebuild the UID map from scratch in this case.
						uids_changed = true;
						
						created_instances.push(inst);
					}
					
					cr.clearArray(type_data);
				}
			}
			
			// Sort all layer indices to ensure Z order is restored
			for (i = 0, len = this.layers.length; i < len; i++)
			{
				this.layers[i].instances.sort(sort_by_zindex);
				this.layers[i].zindices_stale = true;		// in case of duplicates/holes
			}
		}
		
		if (uids_changed)
		{
			this.runtime.ClearDeathRow();
			this.runtime.refreshUidMap();
		}
		
		// createInstanceFromInit (via layer.createInitialInstance()s) does not create siblings for
		// containers when is_startup_instance is true, because all the instances are already in the layout.
		// Link them together now.
		for (i = 0; i < created_instances.length; i++)
		{
			inst = created_instances[i];
			
			if (!inst.type.is_contained)
				continue;
				
			iid = inst.get_iid();
				
			for (k = 0, lenk = inst.type.container.length; k < lenk; k++)
			{
				t = inst.type.container[k];
				
				if (inst.type === t)
					continue;
					
				if (t.instances.length > iid)
					inst.siblings.push(t.instances[iid]);
				else
				{
					// No initial paired instance in layout: create one
					if (!t.default_instance)
					{
					}
					else
					{
						s = this.runtime.createInstanceFromInit(t.default_instance, inst.layer, true, inst.x, inst.y, true);
						this.runtime.ClearDeathRow();
						t.updateIIDs();
						inst.siblings.push(s);
						created_instances.push(s);		// come back around and link up its own instances too
					}
				}
			}
		}
		
		// Create all initial non-world instances
		for (i = 0, len = this.initial_nonworld.length; i < len; i++)
		{
			initial_inst = this.initial_nonworld[i];
			type = this.runtime.types_by_index[initial_inst[1]];
			
			// If the type is in a container, don't create it; it should only be created along
			// with its container instances.
			if (!type.is_contained)
			{
				inst = this.runtime.createInstanceFromInit(this.initial_nonworld[i], null, true);
			}
			
			// Globals should not be in list; should have been created in createGlobalNonWorlds
;
		}		

		this.runtime.changelayout = null;
		
		// Create queued objects
		this.runtime.ClearDeathRow();
		
		// Canvas 2D renderer: attempt to preload all images that are used by types on this layout.
		// Since some canvas 2D browsers load images on demand, games can jank during playback as textures
		// are upload before first draw.  By drawing everything once on startup we can try to avoid this.
		// This may increase the chance devices run out of memory, but that's a problem with canvas 2D anyway.
		if (this.runtime.ctx)
		{
			for (i = 0, len = this.runtime.types_by_index.length; i < len; i++)
			{
				t = this.runtime.types_by_index[i];
				
				// Don't preload images for family types or when no instances used
				if (t.is_family || !t.instances.length || !t.preloadCanvas2D)
					continue;
					
				t.preloadCanvas2D(this.runtime.ctx);
			}
		}
		
		/*
		// Print VRAM
		if (this.runtime.glwrap)
		{
			console.log("Estimated VRAM at layout start: " + this.runtime.glwrap.textureCount() + " textures, approx. " + Math.round(this.runtime.glwrap.estimateVRAM() / 1024) + " kb");
		}
		*/
		
		// Now every container object is created and linked, run through them all firing 'On created'.
		// Note if we are loading a savegame, this must be deferred until loading is complete.
		if (this.runtime.isLoadingState)
		{
			cr.shallowAssignArray(this.runtime.fireOnCreateAfterLoad, created_instances);
		}
		else
		{
			// Not loading: fire all "On Created" now
			for (i = 0, len = created_instances.length; i < len; i++)
			{
				inst = created_instances[i];
				this.runtime.trigger(Object.getPrototypeOf(inst.type.plugin).cnds.OnCreated, inst);
			}
		}
		
		// Clear array to drop references
		cr.clearArray(created_instances);
		
		// Trigger 'start of layout', unless we are changing layout because a savegame is loading
		if (!this.runtime.isLoadingState)
		{
			this.runtime.trigger(cr.system_object.prototype.cnds.OnLayoutStart, null);
		}
		
		// Mark persisted objects to be loaded instead of initial objects next time around
		this.first_visit = false;
	};
	
	Layout.prototype.createGlobalNonWorlds = function ()
	{
		var i, k, len, initial_inst, inst, type;
		
		// Create all initial global non-world instances
		for (i = 0, k = 0, len = this.initial_nonworld.length; i < len; i++)
		{
			initial_inst = this.initial_nonworld[i];
			type = this.runtime.types_by_index[initial_inst[1]];
			
			if (type.global)
			{
				// If the type is in a container, don't create it; it should only be created along
				// with its container instances.
				if (!type.is_contained)
				{
					inst = this.runtime.createInstanceFromInit(initial_inst, null, true);
				}
			}
			else
			{			
				// Remove globals from list
				this.initial_nonworld[k] = initial_inst;
				k++;
			}
		}
		
		cr.truncateArray(this.initial_nonworld, k);
	};

	Layout.prototype.stopRunning = function ()
	{
;
		
		/*
		// Print VRAM
		if (this.runtime.glwrap)
		{
			console.log("Estimated VRAM at layout end: " + this.runtime.glwrap.textureCount() + " textures, approx. " + Math.round(this.runtime.glwrap.estimateVRAM() / 1024) + " kb");
		}
		*/

		// Trigger 'end of layout'
		if (!this.runtime.isLoadingState)
		{
			this.runtime.trigger(cr.system_object.prototype.cnds.OnLayoutEnd, null);
		}
		
		this.runtime.isEndingLayout = true;
		
		// Clear all 'wait'-scheduled events
		cr.clearArray(this.runtime.system.waits);

		var i, leni, j, lenj;
		var layer_instances, inst, type;
		
		// Save any objects with the persist behavior. We have to do this before destroying non-global
		// objects in case objects are in a container and destroying an instance will destroy a
		// linked instance further up with the persist behavior before we get to it.
		// Also skip if the first_visit flag is set, since this is set by the "reset persistent objects"
		// system action and means we don't want to save this layout's state.
		if (!this.first_visit)
		{
			for (i = 0, leni = this.layers.length; i < leni; i++)
			{
				// ensure Z indices up to date so next layout can try to preserve relative
				// order of globals
				this.layers[i].updateZIndices();
				
				layer_instances = this.layers[i].instances;
				
				for (j = 0, lenj = layer_instances.length; j < lenj; j++)
				{
					inst = layer_instances[j];
					
					if (!inst.type.global)
					{
						if (this.runtime.typeHasPersistBehavior(inst.type))
							this.saveObjectToPersist(inst);
					}
				}
			}
		}
		
		// Destroy all non-globals
		for (i = 0, leni = this.layers.length; i < leni; i++)
		{
			layer_instances = this.layers[i].instances;
			
			for (j = 0, lenj = layer_instances.length; j < lenj; j++)
			{
				inst = layer_instances[j];
				
				if (!inst.type.global)
				{
					this.runtime.DestroyInstance(inst);
				}
			}
			
			this.runtime.ClearDeathRow();
			
			// Clear layer instances.  startRunning() picks up global objects and moves them to the new layout's layers.
			cr.clearArray(layer_instances);
			this.layers[i].zindices_stale = true;
		}
		
		// Destroy all non-global, non-world object type instances
		for (i = 0, leni = this.runtime.types_by_index.length; i < leni; i++)
		{
			type = this.runtime.types_by_index[i];
			
			// note we don't do this for families, we iterate the non-family types anyway
			if (type.global || type.plugin.is_world || type.plugin.singleglobal || type.is_family)
				continue;
				
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
				this.runtime.DestroyInstance(type.instances[j]);
				
			this.runtime.ClearDeathRow();
		}
		
		first_layout = false;
		this.runtime.isEndingLayout = false;
	};
	
	var temp_rect = new cr.rect(0, 0, 0, 0);
	
	Layout.prototype.recreateInitialObjects = function (type, x1, y1, x2, y2)
	{
		temp_rect.set(x1, y1, x2, y2);
		
		var i, len;
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			this.layers[i].recreateInitialObjects(type, temp_rect);
		}
	};

	Layout.prototype.draw = function (ctx)
	{
		var layout_canvas;
		var layout_ctx = ctx;
		var ctx_changed = false;
		
		// Must render to off-screen canvas when using low-res fullscreen mode, then stretch back up
		var render_offscreen = !this.runtime.fullscreenScalingQuality;
		
		if (render_offscreen)
		{
			// Need another canvas to render to.  Ensure it is created.
			if (!this.runtime.layout_canvas)
			{
				this.runtime.layout_canvas = document.createElement("canvas");
				layout_canvas = this.runtime.layout_canvas;
				layout_canvas.width = this.runtime.draw_width;
				layout_canvas.height = this.runtime.draw_height;
				this.runtime.layout_ctx = layout_canvas.getContext("2d");
				ctx_changed = true;
			}

			layout_canvas = this.runtime.layout_canvas;
			layout_ctx = this.runtime.layout_ctx;

			// Window size has changed (browser fullscreen mode)
			if (layout_canvas.width !== this.runtime.draw_width)
			{
				layout_canvas.width = this.runtime.draw_width;
				ctx_changed = true;
			}
			if (layout_canvas.height !== this.runtime.draw_height)
			{
				layout_canvas.height = this.runtime.draw_height;
				ctx_changed = true;
			}
			
			if (ctx_changed)
			{
				layout_ctx.imageSmoothingEnabled = this.runtime.linearSampling;
			}
		}
		
		layout_ctx.globalAlpha = 1;
		layout_ctx.globalCompositeOperation = "source-over";
		
		// Clear canvas with transparent
		if (this.runtime.clearBackground && !this.hasOpaqueBottomLayer())
			layout_ctx.clearRect(0, 0, this.runtime.draw_width, this.runtime.draw_height);

		// Draw each layer
		var i, len, l;
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			l = this.layers[i];
			
			// Blend mode 11 means effect fallback is 'hide layer'
			// Note: transparent layers with zero instances are skipped
			if (l.visible && l.opacity > 0 && l.blend_mode !== 11 && (l.instances.length || !l.transparent))
				l.draw(layout_ctx);
			else
				l.updateViewport(null);		// even if not drawing, keep viewport up to date
		}
		
		// If rendered to texture, paste to main display now at full size
		if (render_offscreen)
		{
			ctx.drawImage(layout_canvas, 0, 0, this.runtime.width, this.runtime.height);
		}
	};
	
	Layout.prototype.drawGL_earlyZPass = function (glw)
	{
		glw.setEarlyZPass(true);
		
		// render_to_texture is implied true. Ensure the texture to render to is created.
		if (!this.runtime.layout_tex)
		{
			this.runtime.layout_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
		}

		// Window size has changed (browser fullscreen mode)
		if (this.runtime.layout_tex.c2width !== this.runtime.draw_width || this.runtime.layout_tex.c2height !== this.runtime.draw_height)
		{
			glw.deleteTexture(this.runtime.layout_tex);
			this.runtime.layout_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
		}
		
		glw.setRenderingToTexture(this.runtime.layout_tex);
		
		if (!this.runtime.fullscreenScalingQuality)
		{
			glw.setSize(this.runtime.draw_width, this.runtime.draw_height);
		}
		
		// Early-pass layers in front-to-back order
		var i, l;
		for (i = this.layers.length - 1; i >= 0; --i)
		{
			l = this.layers[i];
			
			// Only early-pass render layers which preserve opaqueness
			if (l.visible && l.opacity === 1 && l.shaders_preserve_opaqueness &&
				l.blend_mode === 0 && (l.instances.length || !l.transparent))
			{
				l.drawGL_earlyZPass(glw);
			}
			else
			{
				l.updateViewport(null);		// even if not drawing, keep viewport up to date
			}
		}
		
		glw.setEarlyZPass(false);
	};
	
	Layout.prototype.drawGL = function (glw)
	{
		// Render whole layout to texture if:
		// 1) layout has effects (needs post-process)
		// 2) any background blending effects are in use (need to sample from texture during rendering)
		// 3) "Fullscreen scaling quality" is "Low" (need to render at low-res and scale up after)
		// 4) we're using front-to-back rendering mode, where we must attach the depth buffer while rendering
		//    the display to a texture
		var render_to_texture = (this.active_effect_types.length > 0 ||
								 this.runtime.uses_background_blending ||
								 !this.runtime.fullscreenScalingQuality ||
								 this.runtime.enableFrontToBack);
		
		if (render_to_texture)
		{
			// Need another canvas to render to.  Ensure it is created.
			if (!this.runtime.layout_tex)
			{
				this.runtime.layout_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
			}

			// Window size has changed (browser fullscreen mode)
			if (this.runtime.layout_tex.c2width !== this.runtime.draw_width || this.runtime.layout_tex.c2height !== this.runtime.draw_height)
			{
				glw.deleteTexture(this.runtime.layout_tex);
				this.runtime.layout_tex = glw.createEmptyTexture(this.runtime.draw_width, this.runtime.draw_height, this.runtime.linearSampling);
			}
			
			glw.setRenderingToTexture(this.runtime.layout_tex);
			
			if (!this.runtime.fullscreenScalingQuality)
			{
				glw.setSize(this.runtime.draw_width, this.runtime.draw_height);
			}
		}
		// Not rendering to texture any more. Clean up layout_tex to save memory.
		else
		{
			if (this.runtime.layout_tex)
			{
				glw.setRenderingToTexture(null);
				glw.deleteTexture(this.runtime.layout_tex);
				this.runtime.layout_tex = null;
			}
		}
		
		if (this.runtime.clearBackground && !this.hasOpaqueBottomLayer())
			glw.clear(0, 0, 0, 0);

		// Draw each layer
		var i, len, l;
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			l = this.layers[i];
			
			if (l.visible && l.opacity > 0 && (l.instances.length || !l.transparent))
				l.drawGL(glw);
			else
				l.updateViewport(null);		// even if not drawing, keep viewport up to date
		}
		
		// If rendered to texture, paste to main display now
		if (render_to_texture)
		{
			// With one effect, it still must be post-drawn in low-res fullscreen mode otherwise
			// it may use the full resolution of the backbuffer
			if (this.active_effect_types.length === 0 ||
				(this.active_effect_types.length === 1 && this.runtime.fullscreenScalingQuality))
			{
				if (this.active_effect_types.length === 1)
				{
					var etindex = this.active_effect_types[0].index;
					
					var viewRect = this.calculateViewRect();
					
					glw.switchProgram(this.active_effect_types[0].shaderindex);
					glw.setProgramParameters(null,								// backTex
											 1.0 / this.runtime.draw_width,		// pixelWidth
											 1.0 / this.runtime.draw_height,	// pixelHeight
											 0.0, 0.0,							// srcStart
											 1.0, 1.0,							// srcEnd
											 0.0, 0.0,							// srcOriginStart
											 1.0, 1.0,							// srcOriginEnd
											 viewRect.left, viewRect.top,		// layoutStart
											 viewRect.right, viewRect.bottom,	// layoutEnd
											 0.0, 0.0,							// destStart
											 1.0, 1.0,							// destEnd
											 this.scale,						// layerScale
											 this.angle,						// layerAngle
											 this.runtime.kahanTime.sum,		// seconds
											 this.effect_params[etindex]);		// fx parameters
											 
					if (glw.programIsAnimated(this.active_effect_types[0].shaderindex))
						this.runtime.redraw = true;
				}
				else
					glw.switchProgram(0);
				
				if (!this.runtime.fullscreenScalingQuality)
				{
					glw.setSize(this.runtime.width, this.runtime.height);
				}
					
				glw.setRenderingToTexture(null);				// to backbuffer
				glw.setDepthTestEnabled(false);					// ignore depth buffer, copy full texture
				glw.setOpacity(1);
				glw.setTexture(this.runtime.layout_tex);
				glw.setAlphaBlend();
				glw.resetModelView();
				glw.updateModelView();
				var halfw = this.runtime.width / 2;
				var halfh = this.runtime.height / 2;
				glw.quad(-halfw, halfh, halfw, halfh, halfw, -halfh, -halfw, -halfh);
				glw.setTexture(null);
				glw.setDepthTestEnabled(true);					// turn depth test back on
			}
			else
			{
				this.renderEffectChain(glw, null, null, null);
			}
		}
	};
	
	Layout.prototype.getRenderTarget = function()
	{
		if (this.active_effect_types.length > 0 ||
				this.runtime.uses_background_blending ||
				!this.runtime.fullscreenScalingQuality ||
				this.runtime.enableFrontToBack)
		{
			return this.runtime.layout_tex;
		}
		else
		{
			return null;
		}
	};
	
	Layout.prototype.getMinLayerScale = function ()
	{
		var m = this.layers[0].getScale();
		var i, len, l;
		
		for (i = 1, len = this.layers.length; i < len; i++)
		{
			l = this.layers[i];
			
			if (l.parallaxX === 0 && l.parallaxY === 0)
				continue;
			
			if (l.getScale() < m)
				m = l.getScale();
		}
		
		return m;
	};

	Layout.prototype.scrollToX = function (x)
	{
		// Apply bounding
		if (!this.unbounded_scrolling)
		{
			var widthBoundary = (this.runtime.draw_width * (1 / this.getMinLayerScale()) / 2);
			
			if (x > this.width - widthBoundary)
				x = this.width - widthBoundary;
				
			// Note window width may be larger than layout width for browser fullscreen mode,
			// so prefer clamping to left
			if (x < widthBoundary)
				x = widthBoundary;
		}

		if (this.scrollX !== x)
		{
			this.scrollX = x;
			this.runtime.redraw = true;
		}
	};

	Layout.prototype.scrollToY = function (y)
	{		
		// Apply bounding
		if (!this.unbounded_scrolling)
		{
			var heightBoundary = (this.runtime.draw_height * (1 / this.getMinLayerScale()) / 2);
			
			if (y > this.height - heightBoundary)
				y = this.height - heightBoundary;
				
			// Note window width may be larger than layout width for browser fullscreen mode,
			// so prefer clamping to top
			if (y < heightBoundary)
				y = heightBoundary;
		}

		if (this.scrollY !== y)
		{
			this.scrollY = y;
			this.runtime.redraw = true;
		}
	};
	
	Layout.prototype.boundScrolling = function ()
	{
		this.scrollToX(this.scrollX);
		this.scrollToY(this.scrollY);
	};
	
	Layout.prototype.calculateViewRect = function ()
	{
		var px = this.scrollX;
		var py = this.scrollY;
		var invScale = 1 / (this.scale * this.runtime.aspect_scale);
		px -= (this.runtime.width * invScale) / 2;
		py -= (this.runtime.height * invScale) / 2;
		this.viewRect.set(
			px,
			py,
			px + this.runtime.draw_width * invScale,
			py + this.runtime.draw_height * invScale
		);
		return this.viewRect;
	};
	
	Layout.prototype.renderEffectChain = function (glw, layer, inst, rendertarget)
	{
		var active_effect_types = inst ?
							inst.active_effect_types :
							layer ?
								layer.active_effect_types :
								this.active_effect_types;
		
		var layerScale = 1, layerAngle = 0, viewOriginLeft = 0, viewOriginTop = 0, viewOriginRight = this.runtime.draw_width, viewOriginBottom = this.runtime.draw_height;
		
		if (inst)
		{
			layerScale = inst.layer.getScale();
			layerAngle = inst.layer.getAngle();
			viewOriginLeft = inst.layer.viewLeft;
			viewOriginTop = inst.layer.viewTop;
			viewOriginRight = inst.layer.viewRight;
			viewOriginBottom = inst.layer.viewBottom;
		}
		else if (layer)
		{
			layerScale = layer.getScale();
			layerAngle = layer.getAngle();
			viewOriginLeft = layer.viewLeft;
			viewOriginTop = layer.viewTop;
			viewOriginRight = layer.viewRight;
			viewOriginBottom = layer.viewBottom;
		}
		else
		{
			layerScale = this.scale;
			layerAngle = this.angle;
			var viewRect = this.calculateViewRect();
			viewOriginLeft = viewRect.left;
			viewOriginTop = viewRect.top;
			viewOriginRight = viewRect.right;
			viewOriginBottom = viewRect.bottom;
		}
		
		var fx_tex = this.runtime.fx_tex;
		var i, len, last, temp, fx_index = 0, other_fx_index = 1;
		var y, h;
		var windowWidth = this.runtime.draw_width;
		var windowHeight = this.runtime.draw_height;
		var halfw = windowWidth / 2;
		var halfh = windowHeight / 2;
		var rcTexBounce = layer ? layer.rcTexBounce : this.rcTexBounce;
		var rcTexDest = layer ? layer.rcTexDest : this.rcTexDest;
		var rcTexOrigin = layer ? layer.rcTexOrigin : this.rcTexOrigin;
		
		var screenleft = 0, clearleft = 0;
		var screentop = 0, cleartop = 0;
		var screenright = windowWidth, clearright = windowWidth;
		var screenbottom = windowHeight, clearbottom = windowHeight;
		
		var boxExtendHorizontal = 0;
		var boxExtendVertical = 0;
		var inst_layer_angle = inst ? inst.layer.getAngle() : 0;
		
		if (inst)
		{
			// Determine total box extension
			for (i = 0, len = active_effect_types.length; i < len; i++)
			{
				boxExtendHorizontal += glw.getProgramBoxExtendHorizontal(active_effect_types[i].shaderindex);
				boxExtendVertical += glw.getProgramBoxExtendVertical(active_effect_types[i].shaderindex);
			}
		
			// Project instance to screen
			var bbox = inst.bbox;
			screenleft = layer.layerToCanvas(bbox.left, bbox.top, true, true);
			screentop = layer.layerToCanvas(bbox.left, bbox.top, false, true);
			screenright = layer.layerToCanvas(bbox.right, bbox.bottom, true, true);
			screenbottom = layer.layerToCanvas(bbox.right, bbox.bottom, false, true);
			
			// Take in to account layer rotation if any
			if (inst_layer_angle !== 0)
			{
				var screentrx = layer.layerToCanvas(bbox.right, bbox.top, true, true);
				var screentry = layer.layerToCanvas(bbox.right, bbox.top, false, true);
				var screenblx = layer.layerToCanvas(bbox.left, bbox.bottom, true, true);
				var screenbly = layer.layerToCanvas(bbox.left, bbox.bottom, false, true);
				temp = Math.min(screenleft, screenright, screentrx, screenblx);
				screenright = Math.max(screenleft, screenright, screentrx, screenblx);
				screenleft = temp;
				temp = Math.min(screentop, screenbottom, screentry, screenbly);
				screenbottom = Math.max(screentop, screenbottom, screentry, screenbly);
				screentop = temp;
			}
			
			// Get unclamped, unextended source texture co-ordinates (the original box)
			rcTexOrigin.left = screenleft / windowWidth;
			rcTexOrigin.top = 1 - screentop / windowHeight;
			rcTexOrigin.right = screenright / windowWidth;
			rcTexOrigin.bottom = 1 - screenbottom / windowHeight;
			
			screenleft -= boxExtendHorizontal;
			screentop -= boxExtendVertical;
			screenright += boxExtendHorizontal;
			screenbottom += boxExtendVertical;
			
			// Unclamped texture coords
			rcTexDest.left = screenleft / windowWidth;
			rcTexDest.top = 1 - screentop / windowHeight;
			rcTexDest.right = screenright / windowWidth;
			rcTexDest.bottom = 1 - screenbottom / windowHeight;
			
			clearleft = screenleft = cr.floor(screenleft);
			cleartop = screentop = cr.floor(screentop);
			clearright = screenright = cr.ceil(screenright);
			clearbottom = screenbottom = cr.ceil(screenbottom);
			
			// Extend clear area by box extension again to prevent sampling nonzero pixels outside the box area
			// (especially for blur).
			clearleft -= boxExtendHorizontal;
			cleartop -= boxExtendVertical;
			clearright += boxExtendHorizontal;
			clearbottom += boxExtendVertical;
			
			if (screenleft < 0)					screenleft = 0;
			if (screentop < 0)					screentop = 0;
			if (screenright > windowWidth)		screenright = windowWidth;
			if (screenbottom > windowHeight)	screenbottom = windowHeight;
			if (clearleft < 0)					clearleft = 0;
			if (cleartop < 0)					cleartop = 0;
			if (clearright > windowWidth)		clearright = windowWidth;
			if (clearbottom > windowHeight)		clearbottom = windowHeight;
			
			// Clamped texture coords
			rcTexBounce.left = screenleft / windowWidth;
			rcTexBounce.top = 1 - screentop / windowHeight;
			rcTexBounce.right = screenright / windowWidth;
			rcTexBounce.bottom = 1 - screenbottom / windowHeight;
		}
		else
		{
			rcTexBounce.left = rcTexDest.left = rcTexOrigin.left = 0;
			rcTexBounce.top = rcTexDest.top = rcTexOrigin.top = 0;
			rcTexBounce.right = rcTexDest.right = rcTexOrigin.right = 1;
			rcTexBounce.bottom = rcTexDest.bottom = rcTexDest.bottom = 1;
		}
		
		// Check if we need to pre-draw the object to the first render surface, with no effect.
		// This is to allow:
		// - rotated or spritesheeted objects using blending to properly blend with the background
		// - bounding boxes to be extended when the effect requires it
		// - instance or layer opacity to be taken in to account if not 100%
		var pre_draw = (inst && (glw.programUsesDest(active_effect_types[0].shaderindex) || boxExtendHorizontal !== 0 || boxExtendVertical !== 0 || inst.opacity !== 1 || inst.type.plugin.must_predraw)) || (layer && !inst && layer.opacity !== 1);
		
		// Save composite mode until last draw
		glw.setAlphaBlend();
		
		if (pre_draw)
		{
			// Not yet created this effect surface
			if (!fx_tex[fx_index])
			{
				fx_tex[fx_index] = glw.createEmptyTexture(windowWidth, windowHeight, this.runtime.linearSampling);
			}

			// Window size has changed (browser fullscreen mode)
			if (fx_tex[fx_index].c2width !== windowWidth || fx_tex[fx_index].c2height !== windowHeight)
			{
				glw.deleteTexture(fx_tex[fx_index]);
				fx_tex[fx_index] = glw.createEmptyTexture(windowWidth, windowHeight, this.runtime.linearSampling);
			}
			
			glw.switchProgram(0);
			glw.setRenderingToTexture(fx_tex[fx_index]);
			
			// Clear target rectangle
			h = clearbottom - cleartop;
			y = (windowHeight - cleartop) - h;
			glw.clearRect(clearleft, y, clearright - clearleft, h);
			
			// Draw the inst or layer
			if (inst)
			{
				inst.drawGL(glw);
			}
			else
			{
				glw.setTexture(this.runtime.layer_tex);
				glw.setOpacity(layer.opacity);
				glw.resetModelView();
				glw.translate(-halfw, -halfh);
				glw.updateModelView();
				glw.quadTex(screenleft, screenbottom, screenright, screenbottom, screenright, screentop, screenleft, screentop, rcTexBounce);
			}
			
			// Set destination range to entire surface
			rcTexDest.left = rcTexDest.top = 0;
			rcTexDest.right = rcTexDest.bottom = 1;
			
			if (inst)
			{
				temp = rcTexBounce.top;
				rcTexBounce.top = rcTexBounce.bottom;
				rcTexBounce.bottom = temp;
			}
			
			// Exchange the fx surfaces
			fx_index = 1;
			other_fx_index = 0;
		}
		
		glw.setOpacity(1);
		
		var last = active_effect_types.length - 1;
		
		// If last effect uses cross-sampling or needs pre-drawing it cannot be rendered direct to target -
		// must render one more time to offscreen then copy in afterwards. Additionally, layout effects in
		// low-res fullscreen mode must post draw so they render at the draw size, then stretch up to the
		// backbuffer size afterwards.
		var post_draw = glw.programUsesCrossSampling(active_effect_types[last].shaderindex) ||
						(!layer && !inst && !this.runtime.fullscreenScalingQuality);
		
		var etindex = 0;
		
		// For each effect to render
		for (i = 0, len = active_effect_types.length; i < len; i++)
		{
			// Not yet created this effect surface
			if (!fx_tex[fx_index])
			{
				fx_tex[fx_index] = glw.createEmptyTexture(windowWidth, windowHeight, this.runtime.linearSampling);
			}

			// Window size has changed (browser fullscreen mode)
			if (fx_tex[fx_index].c2width !== windowWidth || fx_tex[fx_index].c2height !== windowHeight)
			{
				glw.deleteTexture(fx_tex[fx_index]);
				fx_tex[fx_index] = glw.createEmptyTexture(windowWidth, windowHeight, this.runtime.linearSampling);
			}
			
			// Set the shader program to use
			glw.switchProgram(active_effect_types[i].shaderindex);
			etindex = active_effect_types[i].index;
			
			if (glw.programIsAnimated(active_effect_types[i].shaderindex))
				this.runtime.redraw = true;
			
			// First effect and not pre-drawn: render instance to first effect surface
			if (i == 0 && !pre_draw)
			{
				glw.setRenderingToTexture(fx_tex[fx_index]);
				
				// Clear target rectangle
				h = clearbottom - cleartop;
				y = (windowHeight - cleartop) - h;
				glw.clearRect(clearleft, y, clearright - clearleft, h);
				
				if (inst)
				{
					var srcStartX = 0, srcStartY = 0, srcEndX = 1, srcEndY = 1;
					var pxWidth = 1 / inst.width;
					var pxHeight = 1 / inst.height;
						
					// HACK for Sprite plugin: if we can find spritesheet co-ordinates for this instance, use them as the source rectangle.
					if (inst.curFrame && inst.curFrame.sheetTex)
					{
						srcStartX = inst.curFrame.sheetTex.left;
						srcStartY = inst.curFrame.sheetTex.top;
						srcEndX = inst.curFrame.sheetTex.right;
						srcEndY = inst.curFrame.sheetTex.bottom;
						
						if (inst.curFrame.texture_img)
						{
							pxWidth = 1 / inst.curFrame.texture_img.width;
							pxHeight = 1 / inst.curFrame.texture_img.height;
						}
					}
					
					glw.setProgramParameters(rendertarget,						// backTex
											 pxWidth,							// pixelWidth
											 pxHeight,							// pixelHeight
											 srcStartX, srcStartY,				// srcStart
											 srcEndX, srcEndY,					// srcEnd
											 srcStartX, srcStartY,				// srcOriginStart
											 srcEndX, srcEndY,					// srcOriginEnd
											 inst.bbox.left, inst.bbox.top,		// layoutStart
											 inst.bbox.right, inst.bbox.bottom,	// layoutEnd
											 rcTexDest.left, rcTexDest.top,		// destStart
											 rcTexDest.right, rcTexDest.bottom,	// destEnd
											 layerScale,
											 layerAngle,
											 this.runtime.kahanTime.sum,
											 inst.effect_params[etindex]);		// fx params
					
					inst.drawGL(glw);
				}
				else
				{
					glw.setProgramParameters(rendertarget,						// backTex
											 1.0 / windowWidth,					// pixelWidth
											 1.0 / windowHeight,				// pixelHeight
											 0.0, 0.0,							// srcStart
											 1.0, 1.0,							// srcEnd
											 0.0, 0.0,							// srcOriginStart
											 1.0, 1.0,							// srcOriginEnd
											 viewOriginLeft, viewOriginTop,		// layoutStart
											 viewOriginRight, viewOriginBottom,	// layoutEnd
											 0.0, 0.0,							// destStart
											 1.0, 1.0,							// destEnd
											 layerScale,
											 layerAngle,
											 this.runtime.kahanTime.sum,
											 layer ?						// fx params
												layer.effect_params[etindex] :
												this.effect_params[etindex]);
					
					glw.setTexture(layer ? this.runtime.layer_tex : this.runtime.layout_tex);
					glw.resetModelView();
					glw.translate(-halfw, -halfh);
					glw.updateModelView();
					glw.quadTex(screenleft, screenbottom, screenright, screenbottom, screenright, screentop, screenleft, screentop, rcTexBounce);
				}
				
				// Destination range now takes in to account entire surface
				rcTexDest.left = rcTexDest.top = 0;
				rcTexDest.right = rcTexDest.bottom = 1;
				
				if (inst && !post_draw)
				{
					temp = screenbottom;
					screenbottom = screentop;
					screentop = temp;
				}
			}
			// Not first effect
			else
			{
				glw.setProgramParameters(rendertarget,							// backTex
										 1.0 / windowWidth,						// pixelWidth
										 1.0 / windowHeight,					// pixelHeight
										 0.0, 0.0,								// srcStart
										 1.0, 1.0,								// srcEnd
										 rcTexOrigin.left, rcTexOrigin.top,		// srcOriginStart
										 rcTexOrigin.right, rcTexOrigin.bottom,	// srcOriginEnd
										 viewOriginLeft, viewOriginTop,			// layoutStart
										 viewOriginRight, viewOriginBottom,		// layoutEnd
										 rcTexDest.left, rcTexDest.top,			// destStart
										 rcTexDest.right, rcTexDest.bottom,		// destEnd
										 layerScale,
										 layerAngle,
										 this.runtime.kahanTime.sum,
										 inst ?								// fx params
											inst.effect_params[etindex] :
											layer ? 
												layer.effect_params[etindex] :
												this.effect_params[etindex]);
				
				// Avoid having the render target and current texture set at same time
				glw.setTexture(null);
										 
				// The last effect renders direct to display.  Otherwise render to the current effect surface
				if (i === last && !post_draw)
				{
					// Use instance or layer blend mode for last step
					if (inst)
						glw.setBlend(inst.srcBlend, inst.destBlend);
					else if (layer)
						glw.setBlend(layer.srcBlend, layer.destBlend);
						
					glw.setRenderingToTexture(rendertarget);
				}
				else
				{
					glw.setRenderingToTexture(fx_tex[fx_index]);
					
					// Clear target rectangle
					h = clearbottom - cleartop;
					y = (windowHeight - cleartop) - h;
					glw.clearRect(clearleft, y, clearright - clearleft, h);
				}
				
				// Render with the shader
				glw.setTexture(fx_tex[other_fx_index]);
				glw.resetModelView();
				glw.translate(-halfw, -halfh);
				glw.updateModelView();
				glw.quadTex(screenleft, screenbottom, screenright, screenbottom, screenright, screentop, screenleft, screentop, rcTexBounce);
				
				if (i === last && !post_draw)
					glw.setTexture(null);
			}
			
			// Alternate fx_index between 0 and 1
			fx_index = (fx_index === 0 ? 1 : 0);
			other_fx_index = (fx_index === 0 ? 1 : 0);		// will be opposite to fx_index since it was just assigned
		}
		
		// If the last effect needs post-drawing, it is still on an effect surface and not yet drawn
		// to display.  Copy it to main display now.
		if (post_draw)
		{
			glw.switchProgram(0);
			
			// Use instance or layer blend mode for last step
			if (inst)
				glw.setBlend(inst.srcBlend, inst.destBlend);
			else if (layer)
				glw.setBlend(layer.srcBlend, layer.destBlend);
			else
			{
				// Post-drawing layout effect to backbuffer: restore full viewport and stretch up last texture
				if (!this.runtime.fullscreenScalingQuality)
				{
					glw.setSize(this.runtime.width, this.runtime.height);
					halfw = this.runtime.width / 2;
					halfh = this.runtime.height / 2;
					screenleft = 0;
					screentop = 0;
					screenright = this.runtime.width;
					screenbottom = this.runtime.height;
				}
			}
			
			glw.setRenderingToTexture(rendertarget);
			glw.setTexture(fx_tex[other_fx_index]);
			glw.resetModelView();
			glw.translate(-halfw, -halfh);
			glw.updateModelView();
			
			if (inst && active_effect_types.length === 1 && !pre_draw)
				glw.quadTex(screenleft, screentop, screenright, screentop, screenright, screenbottom, screenleft, screenbottom, rcTexBounce);
			else
				glw.quadTex(screenleft, screenbottom, screenright, screenbottom, screenright, screentop, screenleft, screentop, rcTexBounce);
			
			glw.setTexture(null);
		}
	};
	
	Layout.prototype.getLayerBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			if (this.layers[i].sid === sid_)
				return this.layers[i];
		}
		
		return null;
	};
	
	Layout.prototype.saveToJSON = function ()
	{
		var i, len, layer, et;
		
		var o = {
			"sx": this.scrollX,
			"sy": this.scrollY,
			"s": this.scale,
			"a": this.angle,
			"w": this.width,
			"h": this.height,
			"fv": this.first_visit,			// added r127
			"persist": this.persist_data,
			"fx": [],
			"layers": {}
		};
		
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			o["fx"].push({"name": et.name, "active": et.active, "params": this.effect_params[et.index] });
		}
		
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			layer = this.layers[i];
			o["layers"][layer.sid.toString()] = layer.saveToJSON();
		}
		
		return o;
	};
	
	Layout.prototype.loadFromJSON = function (o)
	{
		var i, j, len, fx, p, layer;
		
		this.scrollX = o["sx"];
		this.scrollY = o["sy"];
		this.scale = o["s"];
		this.angle = o["a"];
		this.width = o["w"];
		this.height = o["h"];
		this.persist_data = o["persist"];
		
		// first visit added r127, check it exists before loading
		if (typeof o["fv"] !== "undefined")
			this.first_visit = o["fv"];
		
		// Load active effects and effect parameters
		var ofx = o["fx"];
		
		for (i = 0, len = ofx.length; i < len; i++)
		{
			fx = this.getEffectByName(ofx[i]["name"]);
			
			if (!fx)
				continue;		// must've gone missing
				
			fx.active = ofx[i]["active"];
			this.effect_params[fx.index] = ofx[i]["params"];
		}
		
		this.updateActiveEffects();
		
		// Load layers
		var olayers = o["layers"];
		
		for (p in olayers)
		{
			if (olayers.hasOwnProperty(p))
			{
				layer = this.getLayerBySid(parseInt(p, 10));
				
				if (!layer)
					continue;		// must've gone missing
					
				layer.loadFromJSON(olayers[p]);
			}
		}
	};
	
	cr.layout = Layout;
	
}());


// eveng.js
// ECMAScript 5 strict mode

;

(function()
{
	// For optimising memory use with sol modifiers
	var allUniqueSolModifiers = [];
	
	function testSolsMatch(arr1, arr2)
	{
		var i, len = arr1.length;
		
		switch (len) {
		case 0:
			return true;
		case 1:
			return arr1[0] === arr2[0];
		case 2:
			return arr1[0] === arr2[0] && arr1[1] === arr2[1];
		default:
			for (i = 0; i < len; i++)
			{
				if (arr1[i] !== arr2[i])
					return false;
			}
			return true;
		}
	};
	
	function solArraySorter(t1, t2)
	{
		return t1.index - t2.index;
	};
	
	function findMatchingSolModifier(arr)
	{
		var i, len, u, temp, subarr;
		
		if (arr.length === 2)
		{
			if (arr[0].index > arr[1].index)
			{
				temp = arr[0];
				arr[0] = arr[1];
				arr[1] = temp;
			}
		}
		else if (arr.length > 2)
			arr.sort(solArraySorter);		// so testSolsMatch compares in same order
			
		if (arr.length >= allUniqueSolModifiers.length)
			allUniqueSolModifiers.length = arr.length + 1;
			
		if (!allUniqueSolModifiers[arr.length])
			allUniqueSolModifiers[arr.length] = [];
		
		subarr = allUniqueSolModifiers[arr.length];
		
		for (i = 0, len = subarr.length; i < len; i++)
		{
			u = subarr[i];
			
			// Matched: recycle existing sol
			if (testSolsMatch(arr, u))
				return u;
		}
		
		// Otherwise add new unique sol
		subarr.push(arr);
		return arr;
	};
	
	// Event sheet class
	function EventSheet(runtime, m)
	{
		// Runtime members
		this.runtime = runtime;
		this.triggers = {};
		this.fasttriggers = {};
        this.hasRun = false;
        this.includes = new cr.ObjectSet(); 	// all event sheets included by this sheet, at first-level indirection only
		this.deep_includes = [];				// all includes from this sheet recursively, in trigger order
		this.already_included_sheets = [];		// used while building deep_includes
		
		// Data model members
		this.name = m[0];
		var em = m[1];		// events model
		
		// For profiling
		
		// Iterate events and initialise them
		this.events = [];       // triggers won't make it to this array

		var i, len;
		for (i = 0, len = em.length; i < len; i++)
			this.init_event(em[i], null, this.events);
	};

    EventSheet.prototype.toString = function ()
    {
        return this.name;
    };

	EventSheet.prototype.init_event = function (m, parent, nontriggers)
	{
		switch (m[0]) {
		case 0:	// event block
		{
			// Create a new event block object
			var block = new cr.eventblock(this, parent, m);
			cr.seal(block);

			// Treat OR blocks as ordinary events, but register each triggered condition separately
			if (block.orblock)
			{
				nontriggers.push(block);
				
				var i, len;
				for (i = 0, len = block.conditions.length; i < len; i++)
				{
					if (block.conditions[i].trigger)
						this.init_trigger(block, i);
				}
			}
			else
			{
				// Initialise and store triggers separately
				if (block.is_trigger())
					this.init_trigger(block, 0);
				else
					nontriggers.push(block);
			}
			break;
		}
		case 1: // variable
		{
			var v = new cr.eventvariable(this, parent, m);
			cr.seal(v);
			nontriggers.push(v);
			break;
		}
        case 2:	// include
        {
            var inc = new cr.eventinclude(this, parent, m);
			cr.seal(inc);
            nontriggers.push(inc);
			break;
        }
		default:
;
		}
	};

	EventSheet.prototype.postInit = function ()
	{
		var i, len;
		for (i = 0, len = this.events.length; i < len; i++)
		{
			this.events[i].postInit(i < len - 1 && this.events[i + 1].is_else_block);
		}
	};
	
	EventSheet.prototype.updateDeepIncludes = function ()
	{
		cr.clearArray(this.deep_includes);
		cr.clearArray(this.already_included_sheets);
		
		this.addDeepIncludes(this);
		
		cr.clearArray(this.already_included_sheets);
	};
	
	EventSheet.prototype.addDeepIncludes = function (root_sheet)
	{
		var i, len, inc, sheet;
		var deep_includes = root_sheet.deep_includes;
		var already_included_sheets = root_sheet.already_included_sheets;
		var arr = this.includes.valuesRef();
		
		for (i = 0, len = arr.length; i < len; ++i)
		{
			inc = arr[i];
			sheet = inc.include_sheet;
			
			if (!inc.isActive() || root_sheet === sheet || already_included_sheets.indexOf(sheet) > -1)
				continue;
			
			// The order trigger sheets appear must be deepest-include first, but we still must
			// not add each include more than once. To maintain both requirements, we add to the
			// already-included list immediately, but wait until after recursing to add it to the
			// deep_includes list.
			already_included_sheets.push(sheet);
			sheet.addDeepIncludes(root_sheet);
			deep_includes.push(sheet);
		}
	};

	// Run event sheet
	EventSheet.prototype.run = function (from_include)
	{
		
		if (!this.runtime.resuming_breakpoint)
		{
			this.hasRun = true;
			
			if (!from_include)
				this.runtime.isRunningEvents = true;
		}

		// Run each event
		var i, len;
		for (i = 0, len = this.events.length; i < len; i++)
		{
			var ev = this.events[i];

			ev.run();
			
			// Note revert hasRun on breakpoint so we can resume when inside an included sheet

			
				// Clear modified SOLs between every event
				this.runtime.clearSol(ev.solModifiers);
				
				// Clear death row between top-level events
				if (this.runtime.hasPendingInstances)
					this.runtime.ClearDeathRow();
			
		}
		

			if (!from_include)
				this.runtime.isRunningEvents = false;

		
	};
	
	function isPerformanceSensitiveTrigger(method)
	{
		// 'On frame changed' trigger has turned up in profiles as performance sensitive
		if (cr.plugins_.Sprite && method === cr.plugins_.Sprite.prototype.cnds.OnFrameChanged)
		{
			return true;
		}
		
		return false;
	};
	
	// Add trigger to the sheet map
	EventSheet.prototype.init_trigger = function (trig, index)
	{
		if (!trig.orblock)
			this.runtime.triggers_to_postinit.push(trig);	// needs to be postInit'd later

		var i, len;
		var cnd = trig.conditions[index];
		
		// Add to the trigger map organised as such:
		// this.triggers[object name][n] = {method, [[ev1, 0], [ev2, 0]... [evN, x]]}
		
		// If a fast trigger, add in this format instead:
		// this.fasttriggers[object name][n] = {method, {"param1": [[ev1, 0]...], "param2": [[ev1, 0]...], ...}}

		// Get object type name
		var type_name;

		if (cnd.type)
			type_name = cnd.type.name;
		else
			type_name = "system";
			
		var fasttrigger = cnd.fasttrigger;
		
		var triggers = (fasttrigger ? this.fasttriggers : this.triggers);

		// Ensure object name entry created
		if (!triggers[type_name])
			triggers[type_name] = [];

		var obj_entry = triggers[type_name];

		// Ensure method name entry created
		var method = cnd.func;
		
		if (fasttrigger)
		{
			// Check this condition is fast-triggerable
			if (!cnd.parameters.length)				// no parameters
				return;
			
			var firstparam = cnd.parameters[0];
			
			if (firstparam.type !== 1 ||			// not a string param
				firstparam.expression.type !== 2)	// not a string literal node
			{
				return;
			}
			
			var fastevs;
			var firstvalue = firstparam.expression.value.toLowerCase();

			var i, len;
			for (i = 0, len = obj_entry.length; i < len; i++)
			{
				// found matching method
				if (obj_entry[i].method == method)
				{
					fastevs = obj_entry[i].evs;
					
					if (!fastevs[firstvalue])
						fastevs[firstvalue] = [[trig, index]];
					else
						fastevs[firstvalue].push([trig, index]);
					
					return;
				}
			}
			
			// Wasn't found: add new entry
			fastevs = {};
			fastevs[firstvalue] = [[trig, index]];
			obj_entry.push({ method: method, evs: fastevs });
		}
		else
		{
			for (i = 0, len = obj_entry.length; i < len; i++)
			{
				// found matching method
				if (obj_entry[i].method == method)
				{
					obj_entry[i].evs.push([trig, index]);
					return;
				}
			}
			
			// Wasn't found: add new entry. If performance sensitive, add to beginning
			// of array so it is always found first.
			if (isPerformanceSensitiveTrigger(method))
				obj_entry.unshift({ method: method, evs: [[trig, index]]});
			else
				obj_entry.push({ method: method, evs: [[trig, index]]});
		}
	};
	
	cr.eventsheet = EventSheet;

	// SOL
	function Selection(type)
	{
		this.type = type;
		this.instances = [];        // subset of picked instances
		this.else_instances = [];	// subset of unpicked instances
		this.select_all = true;
	};

	Selection.prototype.hasObjects = function ()
	{
		if (this.select_all)
			return this.type.instances.length;
		else
			return this.instances.length;
	};

	Selection.prototype.getObjects = function ()
	{
		if (this.select_all)
			return this.type.instances;
		else
			return this.instances;
	};

	/*
	Selection.prototype.ensure_picked = function (inst, skip_siblings)
	{
		var i, len;
		var orblock = inst.runtime.getCurrentEventStack().current_event.orblock;

		if (this.select_all)
		{
			this.select_all = false;
			
			if (orblock)
			{
				cr.shallowAssignArray(this.else_instances, inst.type.instances);
				cr.arrayFindRemove(this.else_instances, inst);
			}

			this.instances.length = 1;
			this.instances[0] = inst;
		}
		else
		{
			if (orblock)
			{
				i = this.else_instances.indexOf(inst);
				
				if (i !== -1)
				{
					this.instances.push(this.else_instances[i]);
					this.else_instances.splice(i, 1);
				}
			}
			else
			{
				if (this.instances.indexOf(inst) === -1)
					this.instances.push(inst);
			}
		}
		
		// Also pick instances
		if (!skip_siblings)
		{
			// todo...
		}
	};
	*/
	
	// for static conditions to set the selection to a single instance, but also
	// supports appending the object to the current selection if an OR block
	Selection.prototype.pick_one = function (inst)
	{
		if (!inst)
			return;
			
		// is or block: add to SOL if not already in it (and remove from else_instances)
		if (inst.runtime.getCurrentEventStack().current_event.orblock)
		{
			if (this.select_all)
			{
				// Dump all instances in to else_instances, following code will pluck out our one instance
				cr.clearArray(this.instances);
				cr.shallowAssignArray(this.else_instances, inst.type.instances);
				this.select_all = false;
			}
			
			// Find instance in else_instances and add it to instances
			// If not found, assume it's already in instances
			var i = this.else_instances.indexOf(inst);
			
			if (i !== -1)
			{
				this.instances.push(this.else_instances[i]);
				this.else_instances.splice(i, 1);
			}
		}
		// otherwise just set the selection to this instance
		else
		{
			this.select_all = false;
			cr.clearArray(this.instances);
			this.instances[0] = inst;
		}
	};
	
	cr.selection = Selection;

	// Event class
	function EventBlock(sheet, parent, m)
	{
		// Runtime members
		this.sheet = sheet;
		this.parent = parent;
		this.runtime = sheet.runtime;
		this.solModifiers = [];
		this.solModifiersIncludingParents = [];
		this.solWriterAfterCnds = false;	// block does not change SOL after running its conditions
		this.group = false;					// is group of events
		this.initially_activated = false;	// if a group, is active on startup
		this.toplevelevent = false;			// is an event block parented only by a top-level group
		this.toplevelgroup = false;			// is parented only by other groups or is top-level (i.e. not in a subevent)
		this.has_else_block = false;		// is followed by else
		//this.prev_block = null;			// reference to previous sibling block if any (for else)
		//this.is_logical = false;			// contains a logic condition (for else)
		//this.cndReferences = [];			// like solModifiers but only based on types referenced in conditions (for else)
		
		// Data members + initialisation
;
		this.conditions = [];
		this.actions = [];
		this.subevents = [];
		
		
		this.group_name = "";
		this.group = false;
		this.initially_activated = false;
		this.group_active = false;
		this.contained_includes = null;
		

        // Is a group
        if (m[1])
        {
			// [active_on_start, "name"]
			this.group_name = m[1][1].toLowerCase();
			this.group = true;
			this.initially_activated = !!m[1][0];
			this.contained_includes = [];
			this.group_active = this.initially_activated;
			
			this.runtime.allGroups.push(this);
            this.runtime.groups_by_name[this.group_name] = this;
			
			// For performance profiling
        }
		
		this.orblock = m[2];
		this.sid = m[4];
		
		if (!this.group)
			this.runtime.blocksBySid[this.sid.toString()] = this;

		// Initialise conditions
		var i, len;
		var cm = m[5];
		
		for (i = 0, len = cm.length; i < len; i++)
		{
			var cnd = new cr.condition(this, cm[i]);
			cnd.index = i;
			cr.seal(cnd);
			this.conditions.push(cnd);
			
			/*
			if (cnd.is_logical())
				this.is_logical = true;
				
			if (cnd.type && !cnd.type.plugin.singleglobal && this.cndReferences.indexOf(cnd.type) === -1)
				this.cndReferences.push(cnd.type);
			*/

			// Add condition's type to SOL modifiers
			this.addSolModifier(cnd.type);
		}

		// Initialise actions
		var am = m[6];
		
		for (i = 0, len = am.length; i < len; i++)
		{
			var act = new cr.action(this, am[i]);
			act.index = i;
			cr.seal(act);
			this.actions.push(act);
		}

		// Initialise subevents if any (item 4)
		if (m.length === 8)
		{
			var em = m[7];
			
			for (i = 0, len = em.length; i < len; i++)
				this.sheet.init_event(em[i], this, this.subevents);
		}
		
		this.is_else_block = false;
		
		if (this.conditions.length)
		{
			this.is_else_block = (this.conditions[0].type == null && this.conditions[0].func == cr.system_object.prototype.cnds.Else);
		}
	};
	
	
	
	EventBlock.prototype.postInit = function (hasElse/*, prevBlock_*/)
	{
		var i, len;
		
		// Work out if this is a top-level group
		var p = this.parent;
		
		if (this.group)
		{
			this.toplevelgroup = true;
			
			while (p)
			{
				if (!p.group)
				{
					this.toplevelgroup = false;
					break;
				}
				
				p = p.parent;
			}
		}
		
		// Don't count triggers as top level events - handle ClearDeathRow in the trigger function instead
		this.toplevelevent = !this.is_trigger() && (!this.parent || (this.parent.group && this.parent.toplevelgroup));
		
		this.has_else_block = !!hasElse;
		//this.prev_block = prevBlock_;
		
		// Determine SOL modifier list including all parent blocks (used for triggers). Note this is skipped if an objectname
		// parameter de-optimised this event and pointed its SOL modifiers at the project's full object type list.
		var allObjectTypes = this.runtime.types_by_index;
		
		if (this.solModifiers === allObjectTypes)
		{
			this.solModifiersIncludingParents = allObjectTypes;
		}
		else
		{
			this.solModifiersIncludingParents = this.solModifiers.slice(0);
			
			p = this.parent;
			
			while (p)
			{
				for (i = 0, len = p.solModifiers.length; i < len; i++)
					this.addParentSolModifier(p.solModifiers[i]);
				
				p = p.parent;
			}
			
			this.solModifiers = findMatchingSolModifier(this.solModifiers);
			this.solModifiersIncludingParents = findMatchingSolModifier(this.solModifiersIncludingParents);
		}
		
		var i, len/*, s*/;
		for (i = 0, len = this.conditions.length; i < len; i++)
			this.conditions[i].postInit();

		for (i = 0, len = this.actions.length; i < len; i++)
			this.actions[i].postInit();

		for (i = 0, len = this.subevents.length; i < len; i++)
		{
			this.subevents[i].postInit(i < len - 1 && this.subevents[i + 1].is_else_block);
		}
			
		/*
		// If this is an else block, merge the previous block's SOL modifiers
		if (this.is_else_block && this.prev_block)
		{
			for (i = 0, len = this.prev_block.solModifiers.length; i < len; i++)
			{
				s = this.prev_block.solModifiers[i];
				
				if (this.solModifiers.indexOf(s) === -1)
					this.solModifiers.push(s);
			}
		}
		*/
	};
	
	EventBlock.prototype.setGroupActive = function (a)
	{
		if (this.group_active === !!a)
			return;		// same state
			
		this.group_active = !!a;
		
		// Update all include states
		var i, len;
		for (i = 0, len = this.contained_includes.length; i < len; ++i)
		{
			this.contained_includes[i].updateActive();
		}
		
		// Pre-computed deep includes list may have changed if any includes
		// contained in this group
		if (len > 0 && this.runtime.running_layout.event_sheet)
			this.runtime.running_layout.event_sheet.updateDeepIncludes();
	};
	
	function addSolModifierToList(type, arr)
	{
		var i, len, t;
		
		if (!type)
			return;
			
		// Add to list if not already present
		if (arr.indexOf(type) === -1)
			arr.push(type);
		
		// Add any container types
		if (type.is_contained)
		{
			for (i = 0, len = type.container.length; i < len; i++)
			{
				t = type.container[i];
				
				if (type === t)
					continue;		// already handled
					
				// Add if not already present
				if (arr.indexOf(t) === -1)
					arr.push(t);
			}
		}
	};

	EventBlock.prototype.addSolModifier = function (type)
	{
		addSolModifierToList(type, this.solModifiers);
	};
	
	EventBlock.prototype.addParentSolModifier = function (type)
	{
		addSolModifierToList(type, this.solModifiersIncludingParents);
	};
	
	// De-optimised case for objectname parameters: have to use project's full object type list as SOL modifiers
	EventBlock.prototype.setAllSolModifiers = function ()
	{
		this.solModifiers = this.runtime.types_by_index;
	};
	
	EventBlock.prototype.setSolWriterAfterCnds = function ()
	{
		this.solWriterAfterCnds = true;
		
		// Recurse up chain
		if (this.parent)
			this.parent.setSolWriterAfterCnds();
	};

	EventBlock.prototype.is_trigger = function ()
	{
		if (!this.conditions.length)    // no conditions
			return false;
		else
			return this.conditions[0].trigger;
	};

	EventBlock.prototype.run = function ()
	{
		var i, len, c, any_true = false, cnd_result;
		
		var runtime = this.runtime;
		
		var evinfo = this.runtime.getCurrentEventStack();
		evinfo.current_event = this;
		
		var conditions = this.conditions;
		
		
		
			if (!this.is_else_block)
				evinfo.else_branch_ran = false;
				

		if (this.orblock)
		{
			if (conditions.length === 0)
				any_true = true;		// be sure to run if empty block
			
			
				evinfo.cndindex = 0
				
			
			for (len = conditions.length; evinfo.cndindex < len; evinfo.cndindex++)
			{
				c = conditions[evinfo.cndindex];
				
				if (c.trigger)		// skip triggers when running OR block
					continue;
				
				cnd_result = c.run();
				
				// Need to back up state of any_true in to the event stack frame so it can be resumed
				
				if (cnd_result)			// make sure all conditions run and run if any were true
					any_true = true;
			}
			
			evinfo.last_event_true = any_true;
			
			if (any_true)
				this.run_actions_and_subevents();
		}
		else
		{
			
				evinfo.cndindex = 0
				
			
			// Run each condition (keep the index in the event stack so the current condition can be found)
			for (len = conditions.length; evinfo.cndindex < len; evinfo.cndindex++)
			{
				cnd_result = conditions[evinfo.cndindex].run();
				
				
				if (!cnd_result)    // condition failed
				{
					evinfo.last_event_true = false;
					
					// Clear death row between top-level events
					// Check even if the condition failed so loops in groups correctly create objects
					if (this.toplevelevent && runtime.hasPendingInstances)
						runtime.ClearDeathRow();
			
					
					return;		// bail out now
				}
			}
			
			evinfo.last_event_true = true;
			this.run_actions_and_subevents();
		}
		
		
		this.end_run(evinfo);
		
	};
	
	EventBlock.prototype.end_run = function (evinfo)
	{
		// If has an else block, make sure else branch marked as ran
		if (evinfo.last_event_true && this.has_else_block)
			evinfo.else_branch_ran = true;
			
		// Clear death row between top-level events
		if (this.toplevelevent && this.runtime.hasPendingInstances)
			this.runtime.ClearDeathRow();
	};
	
	EventBlock.prototype.run_orblocktrigger = function (index)
	{
		var evinfo = this.runtime.getCurrentEventStack();
		evinfo.current_event = this;
		
		// Execute the triggered condition only, and if true, run the event
		if (this.conditions[index].run())
		{
			this.run_actions_and_subevents();
			
			this.runtime.getCurrentEventStack().last_event_true = true;
		}
	};

	EventBlock.prototype.run_actions_and_subevents = function ()
	{
		var evinfo = this.runtime.getCurrentEventStack();
		var len;
		
		// Run each action
		for (evinfo.actindex = 0, len = this.actions.length; evinfo.actindex < len; evinfo.actindex++)
		{
			if (this.actions[evinfo.actindex].run())
				return;
		}

		this.run_subevents();
	};
	
	// used by the wait action to call a scheduled event
	EventBlock.prototype.resume_actions_and_subevents = function ()
	{
		var evinfo = this.runtime.getCurrentEventStack();
		var len;
		
		// Run each action.  Don't set evinfo.actindex, it's been set already.
		for (len = this.actions.length; evinfo.actindex < len; evinfo.actindex++)
		{
			if (this.actions[evinfo.actindex].run())
				return;
		}

		this.run_subevents();
	};
	
	EventBlock.prototype.run_subevents = function ()
	{
		if (!this.subevents.length)
			return;
			
		var i, len, subev, pushpop/*, skipped_pop = false, pop_modifiers = null*/;
		var last = this.subevents.length - 1;
		
		
			this.runtime.pushEventStack(this);
		
		
		if (this.solWriterAfterCnds)
		{
			for (i = 0, len = this.subevents.length; i < len; i++)
			{
				subev = this.subevents[i];

				// Pushing and popping SOLs is relatively expensive.  However, top-level groups (i.e. either at the root level
				// or with only other groups as parents) only need to clear SOLs between subevents since there is nothing to
				// inherit, which helps avoid the push/pop overhead.  Also, the last subevent of a block can re-use the
				// parent SOL directly (without push/pop to copy), since nothing else needs to re-use it, which is another
				// way to avoid the push/pop overhead.
				// (But!) Else events cause two exceptions: [not yet implemented]
				// - if this (parent) event has an else event, it must push/pop for every subevent, to ensure the SOL
				//   is properly preserved for the following else event.
				// - if one of the subevents has an else event, it must not pop until after the following else event,
				//   for the same reason (keep the SOL intact).
				//   Since else events merge their solModifiers with the previous event, its solModifiers are the ones
				//   that must be pushed and popped.  (Otherwise different solModifiers could be popped, causing a leak.)
				
				
					pushpop = (!this.toplevelgroup || (!this.group && i < last));
					
					if (pushpop)
						this.runtime.pushCopySol(subev.solModifiers);
				
				
				subev.run();
				
				// If hit a breakpoint, return all the way back to the event loop and exit in to suspend
				
				
					if (pushpop)
						this.runtime.popSol(subev.solModifiers);
					else
						this.runtime.clearSol(subev.solModifiers);
						
			}
		}
		else
		{
			// This event block never modifies the SOL after running its conditions.
			// The conditions have already been run, so we know there are guaranteed
			// to be no SOL changes from here on.  This means we can save the overhead
			// of ever pushing, popping or clearing the SOL.
			for (i = 0, len = this.subevents.length; i < len; i++)
			{
				this.subevents[i].run();
				
				// If hit a breakpoint, return all the way back to the event loop and exit in to suspend
			}
		}
		
		
			this.runtime.popEventStack();
		
	};

	EventBlock.prototype.run_pretrigger = function ()
	{
		// When an event is triggered, the parent events preceding the trigger are run
		// in this mode, in top to bottom.
		// All that's necessary is to run the conditions alone, to set up the SOL.
		var evinfo = this.runtime.getCurrentEventStack();
		evinfo.current_event = this;
		var any_true = false;
		
		var i, len;
		for (evinfo.cndindex = 0, len = this.conditions.length; evinfo.cndindex < len; evinfo.cndindex++)
		{
			// Don't run looping conditions - they don't work pre-trigger
;

			if (this.conditions[evinfo.cndindex].run())
				any_true = true;
			else if (!this.orblock)			// condition failed (let OR blocks run all conditions anyway)
				return false;               // bail out
		}

		// No need to run subevents - trigger has worked out all parents
		return this.orblock ? any_true : true;
	};

	// Running retriggered for a looping condition
	EventBlock.prototype.retrigger = function ()
	{
		// Hack for Sprite's Spawn an Object to pick properly.
		this.runtime.execcount++;
		
		// Start iterating one beyond the current condition
		var prevcndindex = this.runtime.getCurrentEventStack().cndindex;
		var len;

		// This is recursing, so push to the event stack
		var evinfo = this.runtime.pushEventStack(this);

		// Running from the condition routine of a trigger.  Continue the event from
		// the condition immediately following the current condition, unless this is an
		// or block, in which case just skip to running the actions and subevents.
		if (!this.orblock)
		{
			for (evinfo.cndindex = prevcndindex + 1, len = this.conditions.length; evinfo.cndindex < len; evinfo.cndindex++)
			{
				if (!this.conditions[evinfo.cndindex].run())    // condition failed
				{
					this.runtime.popEventStack();               // moving up level of recursion
					return false;                               // bail out
				}
			}
		}

		this.run_actions_and_subevents();

		// Done with this level of recursion
		this.runtime.popEventStack();
		
		return true;		// ran an iteration
	};
	
	EventBlock.prototype.isFirstConditionOfType = function (cnd)
	{
		var cndindex = cnd.index;
		
		if (cndindex === 0)
			return true;
		
		--cndindex;
		
		for ( ; cndindex >= 0; --cndindex)
		{
			if (this.conditions[cndindex].type === cnd.type)
				return false;
		}
		
		return true;
	};
	
	cr.eventblock = EventBlock;

	// Event condition class
	function Condition(block, m)
	{
		// Runtime members
		this.block = block;
		this.sheet = block.sheet;
		this.runtime = block.runtime;
		this.parameters = [];
		this.results = [];
		this.extra = {};		// for plugins to stow away some custom info
		this.index = -1;
		
		this.anyParamVariesPerInstance = false;
		
		// Data model & initialisation
		this.func = this.runtime.GetObjectReference(m[1]);
;

		this.trigger = (m[3] > 0);
		this.fasttrigger = (m[3] === 2);
		this.looping = m[4];
		this.inverted = m[5];
		this.isstatic = m[6];
		this.sid = m[7];
		this.runtime.cndsBySid[this.sid.toString()] = this;
		
		if (m[0] === -1)		// system object
		{
			this.type = null;
			this.run = this.run_system;
			this.behaviortype = null;
			this.beh_index = -1;
		}
		else
		{
			// Get object type
			this.type = this.runtime.types_by_index[m[0]];
;

			if (this.isstatic)
				this.run = this.run_static;
			else
				this.run = this.run_object;

			// Behavior condition
			if (m[2])
			{
				this.behaviortype = this.type.getBehaviorByName(m[2]);
;
				
				this.beh_index = this.type.getBehaviorIndexByName(m[2]);
;
			}
			// Ordinary object condition
			else
			{
				this.behaviortype = null;
				this.beh_index = -1;
			}
			
			// Since this is an object condition and therefore changes the SOL,
			// make sure the parent is aware it writes to the SOL after its own conditions.
			if (this.block.parent)
				this.block.parent.setSolWriterAfterCnds();
		}
		
		// If a fast trigger just set the run function to return true; the fact
		// the condition is running implies it has already met the condition
		if (this.fasttrigger)
			this.run = this.run_true;

		// Initialise each parameter (if any)
		if (m.length === 10)
		{
			var i, len;
			var em = m[9];
			
			for (i = 0, len = em.length; i < len; i++)
			{
				var param = new cr.parameter(this, em[i]);
				cr.seal(param);
				this.parameters.push(param);
			}

			// For evaluating parameters
			this.results.length = em.length;
		}
	};

	Condition.prototype.postInit = function ()
	{
		var i, len, p;
		for (i = 0, len = this.parameters.length; i < len; i++)
		{
			p = this.parameters[i];
			p.postInit();
			
			if (p.variesPerInstance)
				this.anyParamVariesPerInstance = true;
		}
	};
	
	/*
	Condition.prototype.is_logical = function ()
	{
		// Logical conditions are system or singleglobal object conditions
		return !this.type || this.type.plugin.singleglobal;
	};
	*/
	
	// for fast triggers
	Condition.prototype.run_true = function ()
	{
		return true;
	};

	Condition.prototype.run_system = function ()
	{
		
		var i, len;

		// Evaluate all parameters
		for (i = 0, len = this.parameters.length; i < len; i++)
			this.results[i] = this.parameters[i].get();

		// Apply method with this = system object.
		return cr.xor(this.func.apply(this.runtime.system, this.results), this.inverted);
	};

	Condition.prototype.run_static = function ()
	{
		
		var i, len;

		// Evaluate all parameters
		for (i = 0, len = this.parameters.length; i < len; i++)
			this.results[i] = this.parameters[i].get();

		// Apply object method, but with the object type as 'this'.  Don't process invert
		// on the result!  Trust that the method takes in to account invert.
		var ret = this.func.apply(this.behaviortype ? this.behaviortype : this.type, this.results);
		this.type.applySolToContainer();
		return ret;
	};

	Condition.prototype.run_object = function ()
	{		
		
		var i, j, k, leni, lenj, p, ret, met, inst, s, sol2;
		//var has_else = this.block.has_else_block;
		
		var type = this.type;
		var sol = type.getCurrentSol();
		var is_orblock = this.block.orblock && !this.trigger;		// triggers in OR blocks need to work normally
		var offset = 0;
		var is_contained = type.is_contained;
		var is_family = type.is_family;
		var family_index = type.family_index;
		var beh_index = this.beh_index;
		var is_beh = (beh_index > -1);
		var params_vary = this.anyParamVariesPerInstance;
		var parameters = this.parameters;
		var results = this.results;
		var inverted = this.inverted;
		var func = this.func;
		var arr, container;
		
		if (params_vary)
		{
			// If parameters could vary per-instance, we can still evaluate any individual parameters that
			// don't vary up-front, and only evaluate the varying parameters per-instance.
			for (j = 0, lenj = parameters.length; j < lenj; ++j)
			{
				p = parameters[j];
				
				if (!p.variesPerInstance)
					results[j] = p.get(0);
			}
		}
		else
		{
			// Parameters for this action can be fully evaluated in advance without needing per-instance
			// re-evaluation.
			for (j = 0, lenj = parameters.length; j < lenj; ++j)
				results[j] = parameters[j].get(0);
		}

		// All selected: iterate instances and push results to selection
		if (sol.select_all) {
			cr.clearArray(sol.instances);       // clear contents
			cr.clearArray(sol.else_instances);
			arr = type.instances;

			for (i = 0, leni = arr.length; i < leni; ++i)
			{
				inst = arr[i];
;

				// Evaluate parameters
				if (params_vary)
				{
					for (j = 0, lenj = parameters.length; j < lenj; ++j)
					{
						p = parameters[j];
						
						if (p.variesPerInstance)
							results[j] = p.get(i);        // default SOL index is current object
					}
				}

				// Behavior condition
				if (is_beh)
				{
					// Offset if using family behaviors
					offset = 0;
					
					if (is_family)
					{
						offset = inst.type.family_beh_map[family_index];
					}
				
					ret = func.apply(inst.behavior_insts[beh_index + offset], results);
				}
				// Else ordinary condition
				else
					ret = func.apply(inst, results);

				// Apply invert and select
				met = cr.xor(ret, inverted);
				
				if (met)
					sol.instances.push(inst);
				else if (is_orblock)					// in OR blocks, keep the instances not meeting the condition for subsequent testing
					sol.else_instances.push(inst);
			}
			
			if (type.finish)
				type.finish(true);
			
			sol.select_all = false;
			type.applySolToContainer();
			return sol.hasObjects();
		}
		else {
			k = 0;
			
			// Note: don't use applySolToContainer() here, because its use could be inefficient when
			// lots of conditions are used in a cascade (it will clear and re-fill the entire array every time).
			
			// OR blocks only need to test instances not meeting any prior condition, stored in else_instances.
			// Note if this is the first condition in an OR block which is a sub-event to an ordinary block,
			// we still must look in instances instead of else_instances.
			var using_else_instances = (is_orblock && !this.block.isFirstConditionOfType(this));
			arr = (using_else_instances ? sol.else_instances : sol.instances);
			var any_true = false;

			// Not all selected: filter those which meet the condition
			for (i = 0, leni = arr.length; i < leni; ++i)
			{
				inst = arr[i];
;

				// Evaluate parameters
				if (params_vary)
				{
					for (j = 0, lenj = parameters.length; j < lenj; ++j)
					{
						p = parameters[j];
						
						if (p.variesPerInstance)
							results[j] = p.get(i);        // default SOL index is current object
					}
				}

				// Behavior condition
				if (is_beh)
				{
					// Offset if using family behaviors
					offset = 0;
					
					if (is_family)
					{
						offset = inst.type.family_beh_map[family_index];
					}
					
					ret = func.apply(inst.behavior_insts[beh_index + offset], results);
				}
				// Else ordinary condition
				else
					ret = func.apply(inst, results);

				// Test if condition true for this instance
				if (cr.xor(ret, inverted))
				{
					any_true = true;
					
					// OR block: erase from arr (by not incrementing k) and append to picked instances
					if (using_else_instances)
					{
						sol.instances.push(inst);
						
						// Apply to container
						if (is_contained)
						{
							for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
							{
								s = inst.siblings[j];
								s.type.getCurrentSol().instances.push(s);
							}
						}
					}
					// Otherwise keep this instance (by incrementing k)
					else
					{
						arr[k] = inst;
						
						// Apply to container
						if (is_contained)
						{
							for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
							{
								s = inst.siblings[j];
								s.type.getCurrentSol().instances[k] = s;
							}
						}
						
						k++;
					}
				}
				// Condition not true
				else
				{
					// In an OR block, we're iterating else_instances.  So make sure we leave the instance there (by incrementing k).
					if (using_else_instances)
					{
						arr[k] = inst;
						
						// Apply to container
						if (is_contained)
						{
							for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
							{
								s = inst.siblings[j];
								s.type.getCurrentSol().else_instances[k] = s;
							}
						}
						
						k++;
					}
					else if (is_orblock)
					{
						sol.else_instances.push(inst);
						
						// Apply to container
						if (is_contained)
						{
							for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
							{
								s = inst.siblings[j];
								s.type.getCurrentSol().else_instances.push(s);
							}
						}
					}
				}
			}

			// Truncate array to only those meeting condition.
			cr.truncateArray(arr, k);
			
			// Apply same to container
			if (is_contained)
			{
				container = type.container;
				
				for (i = 0, leni = container.length; i < leni; i++)
				{
					sol2 = container[i].getCurrentSol();
					
					if (using_else_instances)
						cr.truncateArray(sol2.else_instances, k);
					else
						cr.truncateArray(sol2.instances, k);
				}
			}
			
			var pick_in_finish = any_true;		// don't pick in finish() if we're only doing the logic test below
			
			// If an OR block and any_true is false, we only checked else_instances.
			// We still need to flag the event as true if any instances in the main
			// instances list meet the condition.  So to a non-picking logic only
			// test.
			if (using_else_instances && !any_true)
			{
				for (i = 0, leni = sol.instances.length; i < leni; i++)
				{
					inst = sol.instances[i];

					// Evaluate parameters
					if (params_vary)
					{
						for (j = 0, lenj = parameters.length; j < lenj; j++)
						{
							p = parameters[j];
							
							if (p.variesPerInstance)
								results[j] = p.get(i);
						}
					}

					// Behavior condition
					if (is_beh)
						ret = func.apply(inst.behavior_insts[beh_index], results);
					// Else ordinary condition
					else
						ret = func.apply(inst, results);

					// Test if condition true for this instance
					if (cr.xor(ret, inverted))
					{
						any_true = true;
						break;		// got our flag, don't need to test any more
					}
				}
			}
			
			if (type.finish)
				type.finish(pick_in_finish || is_orblock);
			
			// Return true if any objects in SOL, but 'OR' blocks need to return false
			// if no conditions ran even if there are instances in the SOL
			return is_orblock ? any_true : sol.hasObjects();
		}
	};
	
	cr.condition = Condition;

	// Event action class
	function Action(block, m)
	{
		// Runtime members
		this.block = block;
		this.sheet = block.sheet;
		this.runtime = block.runtime;
		this.parameters = [];
		this.results = [];
		this.extra = {};		// for plugins to stow away some custom info
		this.index = -1;
		
		this.anyParamVariesPerInstance = false;
		
		// Data model & initialisation
		this.func = this.runtime.GetObjectReference(m[1]);
;
		
		if (m[0] === -1)	// system
		{
			this.type = null;
			this.run = this.run_system;
			this.behaviortype = null;
			this.beh_index = -1;
		}
		else
		{
			this.type = this.runtime.types_by_index[m[0]];
;
			this.run = this.run_object;

			// Behavior action
			if (m[2])
			{
				this.behaviortype = this.type.getBehaviorByName(m[2]);
;
				
				this.beh_index = this.type.getBehaviorIndexByName(m[2]);
;
			}
			else
			{
				this.behaviortype = null;
				this.beh_index = -1;
			}
		}
		
		this.sid = m[3];
		this.runtime.actsBySid[this.sid.toString()] = this;

		// Initialise parameters
		if (m.length === 6)
		{
			var i, len;
			var em = m[5];
			
			for (i = 0, len = em.length; i < len; i++)
			{
				var param = new cr.parameter(this, em[i]);
				cr.seal(param);
				this.parameters.push(param);
			}

			// For evaluating parameters
			this.results.length = em.length;
		}
	};

	Action.prototype.postInit = function ()
	{
		var i, len, p;
		for (i = 0, len = this.parameters.length; i < len; i++)
		{
			p = this.parameters[i];
			p.postInit();
			
			if (p.variesPerInstance)
				this.anyParamVariesPerInstance = true;
		}
	};

	Action.prototype.run_system = function ()
	{
		var runtime = this.runtime;
		
		
		var i, len;
		var parameters = this.parameters;
		var results = this.results;

		// Evaluate parameters
		for (i = 0, len = parameters.length; i < len; ++i)
			results[i] = parameters[i].get();

		return this.func.apply(runtime.system, results);
	};

	Action.prototype.run_object = function ()
	{
		
		// Get the instances to execute on
		var type = this.type;
		var beh_index = this.beh_index;
		var family_index = type.family_index;
		var params_vary = this.anyParamVariesPerInstance;
		var parameters = this.parameters;
		var results = this.results;
		var func = this.func;
		var instances = type.getCurrentSol().getObjects();
		var is_family = type.is_family;
		var is_beh = (beh_index > -1);
		
		var i, j, leni, lenj, p, inst, offset;
		
		if (params_vary)
		{
			// If parameters could vary per-instance, we can still evaluate any individual parameters that
			// don't vary up-front, and only evaluate the varying parameters per-instance.
			for (j = 0, lenj = parameters.length; j < lenj; ++j)
			{
				p = parameters[j];
				
				if (!p.variesPerInstance)
					results[j] = p.get(0);
			}
		}
		else
		{
			// Parameters for this action can be fully evaluated in advance without needing per-instance
			// re-evaluation.
			for (j = 0, lenj = parameters.length; j < lenj; ++j)
				results[j] = parameters[j].get(0);
		}

		for (i = 0, leni = instances.length; i < leni; ++i)
		{
			inst = instances[i];
			
			// Evaluate parameters for this instance if we couldn't evaluate them all up-front
			if (params_vary)
			{
				for (j = 0, lenj = parameters.length; j < lenj; ++j)
				{
					// Only evaluate the specific parameters that vary per-instance. If 
					p = parameters[j];
					
					if (p.variesPerInstance)
						results[j] = p.get(i);    // pass i to use as default SOL index
				}
			}

			// Behavior action: call routine on corresponding behavior instance
			if (is_beh)
			{
				// Offset if using family behaviors
				offset = 0;
				
				if (is_family)
				{
					offset = inst.type.family_beh_map[family_index];
				}
			
				func.apply(inst.behavior_insts[beh_index + offset], results);
			}
			// Otherwise ordinary action call
			else
				func.apply(inst, results);
		}
		
		return false;
	};
	
	cr.action = Action;
	
	// temporary return values to support expression evaluation in recursive functions
	var tempValues = [];
	var tempValuesPtr = -1;
	
	function pushTempValue()
	{
		tempValuesPtr++;
		
		if (tempValues.length === tempValuesPtr)
			tempValues.push(new cr.expvalue());
			
		return tempValues[tempValuesPtr];
	};
	
	function popTempValue()
	{
		tempValuesPtr--;
	};

	// Parameter class
	function Parameter(owner, m)
	{
		// Runtime members
		this.owner = owner;
		this.block = owner.block;
		this.sheet = owner.sheet;
		this.runtime = owner.runtime;
		
		// Data members & initialisation
		this.type = m[0];
		
		this.expression = null;
		this.solindex = 0;
		this.get = null;
		this.combosel = 0;
		this.layout = null;
		this.key = 0;
		this.object = null;
		this.index = 0;
		this.varname = null;
		this.eventvar = null;
		this.fileinfo = null;
		this.subparams = null;
		this.variadicret = null;
		this.subparams = null;
		this.variadicret = null;
		
		// Indicates evaluated value could change between instances. If true, parameter must be
		// repeatedly evaluated for each instance, but if false, can be evaluated once and the value shared
		// for all instances, which is faster.
		this.variesPerInstance = false;
		
		var i, len, param;
		
		switch (this.type)
		{
			case 0:		// number
			case 7:		// any
				this.expression = new cr.expNode(this, m[1]);
				this.solindex = 0;
				this.get = this.get_exp;
				break;
			case 1:		// string
			case 14:	// objectname
				this.expression = new cr.expNode(this, m[1]);
				this.solindex = 0;
				this.get = this.get_exp_str;
				
				// objectname parameters could refer to anything, which could cause any SOL modification. Unfortunately
				// this means we have to deoptimise and set the entire project's object types as the SOL modifiers.
				if (this.type === 14)
					this.block.setAllSolModifiers();
				
				break;
			case 5:		// layer
				// As with the expression, but automatically converts result to a layer
				this.expression = new cr.expNode(this, m[1]);
				this.solindex = 0;
				this.get = this.get_layer;
				break;
			case 3:		// combo
			case 8:		// cmp
				this.combosel = m[1];
				this.get = this.get_combosel;
				break;
			case 6:		// layout
				// Get the layout by name
				this.layout = this.runtime.layouts[m[1]];
;
				this.get = this.get_layout;
				break;
			case 9:		// keyb
				this.key = m[1];
				this.get = this.get_key;
				break;
			case 4:		// object
				// Get the object by index
				this.object = this.runtime.types_by_index[m[1]];
;
				this.get = this.get_object;

				// To allow SOL modifications on object parameters, add to block's SOL modifiers.
				this.block.addSolModifier(this.object);
				
				// For actions, set the owner block as a SOL writer after conditions
				if (this.owner instanceof cr.action)
					this.block.setSolWriterAfterCnds();
				// For conditions, make sure any parent blocks are aware the SOL is written to after conditions
				else if (this.block.parent)
					this.block.parent.setSolWriterAfterCnds();

				break;
			case 10:	// instvar
				this.index = m[1];
				
				// Note object instance var parameters on the system object have a null owner type.
				if (owner.type && owner.type.is_family)
				{
					// Getting a family variable relies on the sol index and picked
					// objects, so always evaluate per-instance for this parameter type.
					this.get = this.get_familyvar;
					this.variesPerInstance = true;
				}
				else
					this.get = this.get_instvar;
				
				break;
			case 11:	// eventvar
				// Still in the middle of initialising the application event sheets
				// Wait until postInit() after all event variables are initialised to look up
				// the event variable we want.
				this.varname = m[1];
				this.eventvar = null;
				this.get = this.get_eventvar;
				break;
			case 2:		// audiofile	["name", ismusic]
			case 12:	// fileinfo		"name"
				this.fileinfo = m[1];
				this.get = this.get_audiofile;
				break;
			case 13:	// variadic
				this.get = this.get_variadic;
				this.subparams = [];
				this.variadicret = [];
				for (i = 1, len = m.length; i < len; i++)
				{
					param = new cr.parameter(this.owner, m[i]);
					cr.seal(param);
					this.subparams.push(param);
					this.variadicret.push(0);
				}
				break;
			default:
;
		}
	};

	Parameter.prototype.postInit = function ()
	{
		var i, len;
		
		if (this.type === 11)		// eventvar
		{
			// All variables have now been init()'d, so we can safely look up and cache
			// the variable by its name.
			this.eventvar = this.runtime.getEventVariableByName(this.varname, this.block.parent);
;
		}
		else if (this.type === 13)	// variadic, postInit all sub-params
		{
			for (i = 0, len = this.subparams.length; i < len; i++)
				this.subparams[i].postInit();
		}
		
		// To also look up event variables in any expressions, post-init any expression
		if (this.expression)
			this.expression.postInit();
	};
	
	Parameter.prototype.maybeVaryForType = function (t)
	{
		if (this.variesPerInstance)
			return;				// already varies per instance, no need to check again
		
		if (!t)
			return;				// never vary for system type
		
		// Any reference to an object type that could have more than one instance will
		// vary per instance, since each evaluation passes the solindex and requests
		// a value from a different instance.
		if (!t.plugin.singleglobal)
		{
			this.variesPerInstance = true;
			return;
		}
	};
	
	Parameter.prototype.setVaries = function ()
	{
		this.variesPerInstance = true;
	};

	Parameter.prototype.get_exp = function (solindex)
	{
		this.solindex = solindex || 0;   // default SOL index to use
		var temp = pushTempValue();
		this.expression.get(temp);
		popTempValue();
		return temp.data;      			// return actual JS value, not expvalue
	};
	
	Parameter.prototype.get_exp_str = function (solindex)
	{
		this.solindex = solindex || 0;   // default SOL index to use
		var temp = pushTempValue();
		this.expression.get(temp);
		popTempValue();
		
		// On rare occasions non-existent objects and other corner cases can return the default integer 0
		// from what would otherwise be a string expression.  This can result in 0 being passed for a string
		// parameter, where it then throws an error (e.g. str.substr() calls 0.substr()).  To ensure this
		// never happens, check the result is really a string and return an empty string if not.
		if (cr.is_string(temp.data))
			return temp.data;
		else
			return "";
	};

	Parameter.prototype.get_object = function ()
	{
		return this.object;
	};

	Parameter.prototype.get_combosel = function ()
	{
		return this.combosel;
	};

	Parameter.prototype.get_layer = function (solindex)
	{
		// As with get expression but automatically convert result to layer
		this.solindex = solindex || 0;   // default SOL index to use
		var temp = pushTempValue();
		this.expression.get(temp);
		popTempValue();

		if (temp.is_number())
			return this.runtime.getLayerByNumber(temp.data);
		else
			return this.runtime.getLayerByName(temp.data);
	}

	Parameter.prototype.get_layout = function ()
	{
		return this.layout;
	};

	Parameter.prototype.get_key = function ()
	{
		return this.key;
	};

	Parameter.prototype.get_instvar = function ()
	{
		return this.index;
	};
	
	Parameter.prototype.get_familyvar = function (solindex_)
	{
		// Find the offset for the family variable index for the specific instance's type
		var solindex = solindex_ || 0;
		var familytype = this.owner.type;
		var realtype = null;
		var sol = familytype.getCurrentSol();
		var objs = sol.getObjects();
		
		// In OR blocks, sols can be empty - resort to else_instances if need be
		if (objs.length)
			realtype = objs[solindex % objs.length].type;
		else if (sol.else_instances.length)
			realtype = sol.else_instances[solindex % sol.else_instances.length].type;
		else if (familytype.instances.length)
			realtype = familytype.instances[solindex % familytype.instances.length].type;
		else
			return 0;

		return this.index + realtype.family_var_map[familytype.family_index];
	};

	Parameter.prototype.get_eventvar = function ()
	{
		// Return actual event variable object in the event tree.
		// This was looked up and cached in postInit()
		return this.eventvar;
	};
	
	Parameter.prototype.get_audiofile = function ()
	{
		return this.fileinfo;
	};
	
	Parameter.prototype.get_variadic = function ()
	{
		var i, len;
		for (i = 0, len = this.subparams.length; i < len; i++)
		{
			this.variadicret[i] = this.subparams[i].get();
		}
		
		return this.variadicret;
	};
	
	cr.parameter = Parameter;

	// Event variable class
	function EventVariable(sheet, parent, m)
	{
		// Runtime members
		this.sheet = sheet;
		this.parent = parent;
		this.runtime = sheet.runtime;
		this.solModifiers = [];
		
		// Data model members
		this.name = m[1];
		this.vartype = m[2];
		this.initial = m[3];
		this.is_static = !!m[4];
		this.is_constant = !!m[5];
		this.sid = m[6];
		this.runtime.varsBySid[this.sid.toString()] = this;
		this.data = this.initial;	// note: also stored in event stack frame for local nonstatic nonconst vars
		
		if (this.parent)			// local var
		{
			if (this.is_static || this.is_constant)
				this.localIndex = -1;
			else
				this.localIndex = this.runtime.stackLocalCount++;
			
			this.runtime.all_local_vars.push(this);
		}
		else						// global var
		{
			this.localIndex = -1;
			this.runtime.all_global_vars.push(this);
		}
	};

	EventVariable.prototype.postInit = function ()
	{
		this.solModifiers = findMatchingSolModifier(this.solModifiers);
	};
	
	EventVariable.prototype.setValue = function (x)
	{
;
		
		var lvs = this.runtime.getCurrentLocalVarStack();
		
		// global, static local variable, or no event stack: just use this.data
		if (!this.parent || this.is_static || !lvs)
			this.data = x;
		else	// local nonstatic variable: use event stack to keep value at this level of recursion
		{
			// ensure enough array entries
			if (this.localIndex >= lvs.length)
				lvs.length = this.localIndex + 1;
			
			// store in current stack frame so recursive functions get their own value
			lvs[this.localIndex] = x;
		}
	};
	
	EventVariable.prototype.getValue = function ()
	{
		var lvs = this.runtime.getCurrentLocalVarStack();
		
		// global, static local variable, or no event stack: just use this.data
		if (!this.parent || this.is_static || !lvs || this.is_constant)
			return this.data;
		else	// local nonstatic variable
		{
			// if not enough array entries, or array value is undefined, no value has been assigned yet at this stack level - just return initial
			if (this.localIndex >= lvs.length)
			{
				//log("Check: local var stack not big enough");
				return this.initial;
			}
			
			if (typeof lvs[this.localIndex] === "undefined")
			{
				//log("Check: local var stack holds undefined value");
				return this.initial;
			}
			
			return lvs[this.localIndex];
		}
	};

	EventVariable.prototype.run = function ()
	{
		
			// If not global, static or constant, reset initial value
			if (this.parent && !this.is_static && !this.is_constant)
				this.setValue(this.initial);
		
	};
	
	cr.eventvariable = EventVariable;

    // Event include class
	function EventInclude(sheet, parent, m)
	{
		// Runtime members
		this.sheet = sheet;
		this.parent = parent;
		this.runtime = sheet.runtime;
		this.solModifiers = [];
		this.include_sheet = null;		// determined in postInit
		
		// Data model members
		this.include_sheet_name = m[1];
		
		this.active = true;
		
	};
	
	EventInclude.prototype.toString = function ()
	{
		return "include:" + this.include_sheet.toString();
	};

	EventInclude.prototype.postInit = function ()
	{
		// Look up the event sheet by name, which isn't available in init() but is in postInit()
        this.include_sheet = this.runtime.eventsheets[this.include_sheet_name];
;
;

        // Add to event sheet's list of first-level includes
        this.sheet.includes.add(this);
		
		this.solModifiers = findMatchingSolModifier(this.solModifiers);
		
		// Add to any groups that contain this
		var p = this.parent;
		
		while (p)
		{
			if (p.group)
				p.contained_includes.push(this);
			
			p = p.parent;
		}
		
		this.updateActive();
	};

	EventInclude.prototype.run = function ()
	{
		// Event sheets ought not be in subevents, but support this anyway.
        // When they're a subevent, they'll need a whole new clean SOL level for
        // all object types in the application, to ensure it won't trash the current SOL.
			if (this.parent)
				this.runtime.pushCleanSol(this.runtime.types_by_index);

        // To prevent cyclic includes, don't run an event sheet more than once per tick.
        if (!this.include_sheet.hasRun)
            this.include_sheet.run(true);			// from include
		
		// If hit a breakpoint, return all the way back to the event loop and exit in to suspend

			if (this.parent)
				this.runtime.popSol(this.runtime.types_by_index);
	};
	
	EventInclude.prototype.updateActive = function ()
	{
		// Check is not in a disabled group
		var p = this.parent;
		
		while (p)
		{
			if (p.group && !p.group_active)
			{
				this.active = false;
				return;
			}
			
			p = p.parent;
		}
		
		this.active = true;
	};
	
	EventInclude.prototype.isActive = function ()
	{
		return this.active;
	};
	
	cr.eventinclude = EventInclude;
	
	function EventStackFrame()
	{
		this.temp_parents_arr = [];
		this.reset(null);
		cr.seal(this);
	};
	
	EventStackFrame.prototype.reset = function (cur_event)
	{
		this.current_event = cur_event;
		this.cndindex = 0;
		this.actindex = 0;
		cr.clearArray(this.temp_parents_arr);
		this.last_event_true = false;
		this.else_branch_ran = false;
		this.any_true_state = false;
	};
	
	EventStackFrame.prototype.isModifierAfterCnds = function ()
	{
		// Event's flag takes priority if set
		if (this.current_event.solWriterAfterCnds)
			return true;
		
		// Is not currently on the last condition: assume the conditions
		// that follow are SOL modifiers if the event has any SOL modifiers at all
		if (this.cndindex < this.current_event.conditions.length - 1)
			return !!this.current_event.solModifiers.length;
			
		// Otherwise we may safely assume nothing is modified from here
		return false;
	};
	
	cr.eventStackFrame = EventStackFrame;
	
}());

// expressions.js
// ECMAScript 5 strict mode

(function()
{
	function ExpNode(owner_, m)
	{
		this.owner = owner_;
		this.runtime = owner_.runtime;
		this.type = m[0];
		
;
		
		this.get = [this.eval_int,
					this.eval_float,
					this.eval_string,
					this.eval_unaryminus,
					this.eval_add,
					this.eval_subtract,
					this.eval_multiply,
					this.eval_divide,
					this.eval_mod,
					this.eval_power,
					this.eval_and,
					this.eval_or,
					this.eval_equal,
					this.eval_notequal,
					this.eval_less,
					this.eval_lessequal,
					this.eval_greater,
					this.eval_greaterequal,
					this.eval_conditional,
					this.eval_system_exp,
					this.eval_object_exp,
					this.eval_instvar_exp,
					this.eval_behavior_exp,
					this.eval_eventvar_exp][this.type];
					
		// if expnode has parameters, points to the model array for them
		var paramsModel = null;
		
		this.value = null;
		this.first = null;
		this.second = null;
		this.third = null;
		this.func = null;
		this.results = null;
		this.parameters = null;
		this.object_type = null;
		this.beh_index = -1;
		this.instance_expr = null;
		this.varindex = -1;
		this.behavior_type = null;
		this.varname = null;
		this.eventvar = null;
		this.return_string = false;
					
		switch (this.type) {
		case 0:		// int
		case 1:		// float
		case 2:		// string
			this.value = m[1];
			break;
		case 3:		// unaryminus
			this.first = new cr.expNode(owner_, m[1]);
			break;
		// 4-17 are binary ops, handled outside of switch case as range
		case 18:	// conditional
			this.first = new cr.expNode(owner_, m[1]);
			this.second = new cr.expNode(owner_, m[2]);
			this.third = new cr.expNode(owner_, m[3]);
			break;
		case 19:	// system_exp
			this.func = this.runtime.GetObjectReference(m[1]);
;
			
			// The random() and choose() expressions always vary per instance since
			// they always return a different value
			if (this.func === cr.system_object.prototype.exps.random
			 || this.func === cr.system_object.prototype.exps.choose)
			{
				this.owner.setVaries();
			}

			// Prepare an array to store arguments - first will be the 'ret' argument
			this.results = [];
			this.parameters = [];
			
			// Has parameters
			if (m.length === 3)
			{
				paramsModel = m[2];
				this.results.length = paramsModel.length + 1;	// must also fit 'ret'
			}
			else
				this.results.length = 1;      // to fit 'ret'
				
			break;
		case 20:	// object_exp
			// Locate the object
			this.object_type = this.runtime.types_by_index[m[1]];
;
			
			this.beh_index = -1;
			this.func = this.runtime.GetObjectReference(m[2]);
			this.return_string = m[3];
			
			// If this is the Function object's 'Call' expression, we must set the
			// parameter as varying per-instance, since we don't know if the called
			// function will return different values.
			if (cr.plugins_.Function && this.func === cr.plugins_.Function.prototype.exps.Call)
			{
				this.owner.setVaries();
			}
			
			// has instance expression
			if (m[4])
				this.instance_expr = new cr.expNode(owner_, m[4]);
			else
				this.instance_expr = null;
				
			this.results = [];
			this.parameters = [];
				
			// has parameters
			if (m.length === 6)
			{
				paramsModel = m[5];
				this.results.length = paramsModel.length + 1;
			}
			else
				this.results.length = 1;	// to fit 'ret'
			
			break;
		case 21:		// instvar_exp
			// Locate the object type by name
			this.object_type = this.runtime.types_by_index[m[1]];
;
			this.return_string = m[2];

			// has instance expression
			if (m[3])
				this.instance_expr = new cr.expNode(owner_, m[3]);
			else
				this.instance_expr = null;
				
			this.varindex = m[4];

			break;
		case 22:		// behavior_exp
			// Locate the object
			this.object_type = this.runtime.types_by_index[m[1]];
;
			
			// Locate behavior type and index
			this.behavior_type = this.object_type.getBehaviorByName(m[2]);
;
			
			this.beh_index = this.object_type.getBehaviorIndexByName(m[2]);
			this.func = this.runtime.GetObjectReference(m[3]);
			this.return_string = m[4];
			
			// has instance expression
			if (m[5])
				this.instance_expr = new cr.expNode(owner_, m[5]);
			else
				this.instance_expr = null;
				
			this.results = [];
			this.parameters = [];
				
			// has parameters
			if (m.length === 7)
			{
				paramsModel = m[6];
				this.results.length = paramsModel.length + 1;
			}
			else
				this.results.length = 1;	// to fit 'ret'
			
			break;
		case 23:		// eventvar_exp
			this.varname = m[1];
			this.eventvar = null;	// assigned in postInit
			break;
		}
		
		this.owner.maybeVaryForType(this.object_type);
		
		// Initialise binary operators
		if (this.type >= 4 && this.type <= 17)
		{
			this.first = new cr.expNode(owner_, m[1]);
			this.second = new cr.expNode(owner_, m[2]);
		}

		// Initialise any parameters
		if (paramsModel)
		{
			var i, len;
			for (i = 0, len = paramsModel.length; i < len; i++)
				this.parameters.push(new cr.expNode(owner_, paramsModel[i]));
		}
		
		cr.seal(this);
	};
	
	ExpNode.prototype.postInit = function ()
	{
		if (this.type === 23)	// eventvar_exp
		{
			// Look up event variable name
			this.eventvar = this.owner.runtime.getEventVariableByName(this.varname, this.owner.block.parent);
;
		}
		
		if (this.first)
			this.first.postInit();
		if (this.second)
			this.second.postInit();
		if (this.third)
			this.third.postInit();
		if (this.instance_expr)
			this.instance_expr.postInit();
		if (this.parameters)
		{
			var i, len;
			for (i = 0, len = this.parameters.length; i < len; i++)
				this.parameters[i].postInit();
		}
	};
	
	var tempValues = [];
	var tempValuesPtr = -1;
	
	function pushTempValue()
	{
		++tempValuesPtr;
		
		if (tempValues.length === tempValuesPtr)
			tempValues.push(new cr.expvalue());
			
		return tempValues[tempValuesPtr];
	};
	
	function popTempValue()
	{
		--tempValuesPtr;
	};
	
	function eval_params(parameters, results, temp)
	{
		var i, len;
		for (i = 0, len = parameters.length; i < len; ++i)
		{
			parameters[i].get(temp);
			results[i + 1] = temp.data;   // passing actual javascript value as argument instead of expvalue
		}
	}

	// Expression node evaluation functions
	ExpNode.prototype.eval_system_exp = function (ret)
	{
		// Use func.apply to call the following method with system object as 'this':
		// function expression(ret [, arg1, arg2... argN])

		var parameters = this.parameters;
		
		// First argument is ret
		var results = this.results;
		results[0] = ret;
		
		var temp = pushTempValue();

		// Evaluate all parameters to the rest of the results
		eval_params(parameters, results, temp);
		
		popTempValue();

		// Invoke the system expression
		this.func.apply(this.runtime.system, results);
	};

	// Expression node evaluation functions
	ExpNode.prototype.eval_object_exp = function (ret)
	{
		// Use func.apply to call the following method with correct instance as 'this':
		// function expression(ret [, arg1, arg2... argN])

		var object_type = this.object_type;
		var results = this.results;
		var parameters = this.parameters;
		var instance_expr = this.instance_expr;
		var func = this.func;
		var index = this.owner.solindex;			// default to parameter's intended SOL index
		var sol = object_type.getCurrentSol();
		var instances = sol.getObjects();

		// No instances available: try else_instances instead, otherwise return either 0 or an empty string
		if (!instances.length)
		{
			if (sol.else_instances.length)
				instances = sol.else_instances;
			else
			{
				if (this.return_string)
					ret.set_string("");
				else
					ret.set_int(0);
				return;
			}
		}

		// First argument is ret
		results[0] = ret;
		ret.object_class = object_type;		// so expression can access family type if need be
		
		var temp = pushTempValue();

		// Evaluate all parameters to the rest of the results
		eval_params(parameters, results, temp);

		// If there is an instance expression, evaluate it to get a new index
		if (instance_expr) {
			instance_expr.get(temp);

			// If the result was a number, use it for an absolute instance index - not via the SOL
			if (temp.is_number()) {
				index = temp.data;
				instances = object_type.instances;    // pick from all instances, not SOL
			}
		}
		
		popTempValue();

		// Bound to available instances
		var len = instances.length;
		
		if (index >= len || index <= -len)
			index %= len;      // wraparound

		// If negative, offset such that -1 is the last selected
		if (index < 0)
			index += len;

		// Invoke method (returned_val is for plugin developer hint only)
		var returned_val = func.apply(instances[index], results);

;
	};
	
	ExpNode.prototype.eval_behavior_exp = function (ret)
	{
		// Use func.apply to call the following method with correct instance as 'this':
		// function expression(ret [, arg1, arg2... argN])

		var object_type = this.object_type;
		var results = this.results;
		var parameters = this.parameters;
		var instance_expr = this.instance_expr;
		var beh_index = this.beh_index;
		var func = this.func;
		var index = this.owner.solindex;			// default to parameter's intended SOL index
		var sol = object_type.getCurrentSol();
		var instances = sol.getObjects();

		// No instances available: try else_instances instead, otherwise return either 0 or an empty string
		if (!instances.length)
		{
			if (sol.else_instances.length)
				instances = sol.else_instances;
			else
			{
				if (this.return_string)
					ret.set_string("");
				else
					ret.set_int(0);
				return;
			}
		}

		// First argument is ret
		results[0] = ret;
		ret.object_class = object_type;		// so expression can access family type if need be
		
		var temp = pushTempValue();

		// Evaluate all parameters to the rest of the results
		eval_params(parameters, results, temp);
		
		// If there is an instance expression, evaluate it to get a new index
		if (instance_expr) {
			instance_expr.get(temp);

			// If the result was a number, use it for an absolute instance index - not via the SOL
			if (temp.is_number()) {
				index = temp.data;
				instances = object_type.instances;    // pick from all instances, not SOL
			}
		}
		
		popTempValue();

		// Bound to available instances
		var len = instances.length;
		
		if (index >= len || index <= -len)
			index %= len;      // wraparound

		// If negative, offset such that -1 is the last selected
		if (index < 0)
			index += len;

		// Invoke method (returned_val is for plugin developer hint only)
		var inst = instances[index];

		// Invoke as behavior expression.
		// Offset if using family behaviors
		var offset = 0;
		
		if (object_type.is_family)
		{
			offset = inst.type.family_beh_map[object_type.family_index];
		}
	
		var returned_val = func.apply(inst.behavior_insts[beh_index + offset], results);

;
	};

	ExpNode.prototype.eval_instvar_exp = function (ret)
	{
		var instance_expr = this.instance_expr;
		var object_type = this.object_type;
		var varindex = this.varindex;
		var index = this.owner.solindex;		// default to parameter's intended SOL index
		var sol = object_type.getCurrentSol();
		var instances = sol.getObjects();
		var inst;

		// No instances available: try else_instances instead, otherwise return either 0 or an empty string
		if (!instances.length)
		{
			if (sol.else_instances.length)
				instances = sol.else_instances;
			else
			{
				if (this.return_string)
					ret.set_string("");
				else
					ret.set_int(0);
				return;
			}
		}
		
		// If there is an instance expression, evaluate it to get the new index
		if (instance_expr)
		{
			var temp = pushTempValue();
			
			instance_expr.get(temp);

			// If the result was a number, use it for an absolute instance index - not via the SOL
			if (temp.is_number())
			{
				index = temp.data;

				var type_instances = object_type.instances;
				
				if (type_instances.length !== 0)		// avoid NaN result with %
				{
					index %= type_instances.length;     // wraparound

					if (index < 0)                      // offset
						index += type_instances.length;
				}

				// Return nth instance's instance variable
				inst = object_type.getInstanceByIID(index);
				var to_ret = inst.instance_vars[varindex];

				if (cr.is_string(to_ret))
					ret.set_string(to_ret);
				else
					ret.set_float(to_ret);

				popTempValue();
				return;         // done
			}
			
			popTempValue();
		}

		// Bound to available instances
		var len = instances.length;
		
		if (index >= len || index <= -len)
			index %= len;		// wraparound
		
		// If negative, offset such that -1 is the last selected
		if (index < 0)
			index += len;
		
		inst = instances[index];
		
		// Offset if using family variables
		var offset = 0;
		
		if (object_type.is_family)
		{
			offset = inst.type.family_var_map[object_type.family_index];
		}

		// Return nth instance's instance variable
		var to_ret = inst.instance_vars[varindex + offset];

		if (cr.is_string(to_ret))
			ret.set_string(to_ret);
		else
			ret.set_float(to_ret);
	};

	ExpNode.prototype.eval_int = function (ret)
	{
		ret.type = cr.exptype.Integer;
		ret.data = this.value;
	};

	ExpNode.prototype.eval_float = function (ret)
	{
		ret.type = cr.exptype.Float;
		ret.data = this.value;
	};

	ExpNode.prototype.eval_string = function (ret)
	{
		ret.type = cr.exptype.String;
		ret.data = this.value;
	};

	ExpNode.prototype.eval_unaryminus = function (ret)
	{
		this.first.get(ret);                // retrieve operand

		if (ret.is_number())
			ret.data = -ret.data;
	};

	ExpNode.prototype.eval_add = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			ret.data += temp.data;          // both operands numbers: add

			// Right operand was float: result must be typed float as well
			if (temp.is_float())
				ret.make_float();
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_subtract = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			ret.data -= temp.data;          // both operands numbers: subtract

			// Right operand was float: result must be typed float as well
			if (temp.is_float())
				ret.make_float();
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_multiply = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			ret.data *= temp.data;          // both operands numbers: multiply

			// Right operand was float: result must be typed float as well
			if (temp.is_float())
				ret.make_float();
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_divide = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			ret.data /= temp.data;          // both operands numbers: divide

			// Division always returns float, even with integer operands
			ret.make_float();
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_mod = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			ret.data %= temp.data;          // both operands numbers: modulo

			// Right operand was float: result must be typed float as well
			if (temp.is_float())
				ret.make_float();
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_power = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			ret.data = Math.pow(ret.data, temp.data);   // both operands numbers: raise to power

			// Right operand was float: result must be typed float as well
			if (temp.is_float())
				ret.make_float();
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_and = function (ret)
	{
		this.first.get(ret);			// left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand
		
		if (temp.is_string() || ret.is_string())
			this.eval_and_stringconcat(ret, temp);
		else
			this.eval_and_logical(ret, temp);
		
		popTempValue();
	};
	
	ExpNode.prototype.eval_and_stringconcat = function (ret, temp)
	{
		if (ret.is_string() && temp.is_string())
			this.eval_and_stringconcat_str_str(ret, temp);
		else
			this.eval_and_stringconcat_num(ret, temp);
	};
	
	ExpNode.prototype.eval_and_stringconcat_str_str = function (ret, temp)
	{
		ret.data += temp.data;
	};
	
	ExpNode.prototype.eval_and_stringconcat_num = function (ret, temp)
	{
		if (ret.is_string())
		{
			// Left operand is string. Based on caller, we know temp is not a string: concatenate number.
			// Note we round to avoid floating point errors appearing in the string.
			ret.data += (Math.round(temp.data * 1e10) / 1e10).toString();
		}
		else
		{
			// Right operand string but 'ret' is a number: set to a string with the number appended.
			ret.set_string(ret.data.toString() + temp.data);
		}
	};
	
	ExpNode.prototype.eval_and_logical = function (ret, temp)
	{
		// Both operands number: perform logical AND
		ret.set_int(ret.data && temp.data ? 1 : 0);
	};

	ExpNode.prototype.eval_or = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		if (ret.is_number() && temp.is_number())
		{
			if (ret.data || temp.data)
				ret.set_int(1);
			else
				ret.set_int(0);
		}
		
		popTempValue();
	};

	ExpNode.prototype.eval_conditional = function (ret)
	{
		this.first.get(ret);                // condition operand

		if (ret.data)                       // is true
			this.second.get(ret);           // evaluate second operand to ret
		else
			this.third.get(ret);            // evaluate third operand to ret
	};

	ExpNode.prototype.eval_equal = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		ret.set_int(ret.data === temp.data ? 1 : 0);
		popTempValue();
	};

	ExpNode.prototype.eval_notequal = function (ret)
	{
		this.first.get(ret);                // left operand
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		ret.set_int(ret.data !== temp.data ? 1 : 0);
		popTempValue();
	};

	ExpNode.prototype.eval_less = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		ret.set_int(ret.data < temp.data ? 1 : 0);
		popTempValue();
	};

	ExpNode.prototype.eval_lessequal = function (ret)
	{
		this.first.get(ret);                // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		ret.set_int(ret.data <= temp.data ? 1 : 0);
		popTempValue();
	};

	ExpNode.prototype.eval_greater = function (ret)
	{
		this.first.get(ret);            // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		ret.set_int(ret.data > temp.data ? 1 : 0);
		popTempValue();
	};

	ExpNode.prototype.eval_greaterequal = function (ret)
	{
		this.first.get(ret);            // left operand
		
		var temp = pushTempValue();
		this.second.get(temp);			// right operand

		ret.set_int(ret.data >= temp.data ? 1 : 0);
		popTempValue();
	};

	ExpNode.prototype.eval_eventvar_exp = function (ret)
	{
		var val = this.eventvar.getValue();
		
		if (typeof val === "number")
			ret.set_float(val);
		else if (typeof val === "boolean")
			ret.set_int(val ? 1 : 0);		// return boolean as 1 or 0
		else
			ret.set_string(val);
	};
	
	cr.expNode = ExpNode;

	// Expression value class
	function ExpValue(type, data)
	{
		this.type = type || cr.exptype.Integer;
		this.data = data || 0;
		this.object_class = null;

;
;
;

		// Since integers are emulated, ensure value is rounded for int
		if (this.type == cr.exptype.Integer)
			this.data = Math.floor(this.data);
			
		cr.seal(this);
	};

	ExpValue.prototype.is_int = function ()
	{
		return this.type === cr.exptype.Integer;
	};

	ExpValue.prototype.is_float = function ()
	{
		return this.type === cr.exptype.Float;
	};

	ExpValue.prototype.is_number = function ()
	{
		return this.type === cr.exptype.Integer || this.type === cr.exptype.Float;
	};

	ExpValue.prototype.is_string = function ()
	{
		return this.type === cr.exptype.String;
	};

	ExpValue.prototype.make_int = function ()
	{
		if (!this.is_int())
		{
			if (this.is_float())
				this.data = Math.floor(this.data);      // truncate float
			else if (this.is_string())
				this.data = parseInt(this.data, 10);

			this.type = cr.exptype.Integer;
		}
	};

	ExpValue.prototype.make_float = function ()
	{
		if (!this.is_float())
		{
			if (this.is_string())
				this.data = parseFloat(this.data);
			// else data is already a JS number

			this.type = cr.exptype.Float;
		}
	};

	ExpValue.prototype.make_string = function ()
	{
		if (!this.is_string())
		{
			this.data = this.data.toString();
			this.type = cr.exptype.String;
		}
	};

	ExpValue.prototype.set_int = function (val)
	{
;

		this.type = cr.exptype.Integer;
		this.data = Math.floor(val);
	};

	ExpValue.prototype.set_float = function (val)
	{
;

		this.type = cr.exptype.Float;
		this.data = val;
	};

	ExpValue.prototype.set_string = function (val)
	{
;

		this.type = cr.exptype.String;
		this.data = val;
	};
	
	ExpValue.prototype.set_any = function (val)
	{
		if (cr.is_number(val))
		{
			this.type = cr.exptype.Float;
			this.data = val;
		}
		else if (cr.is_string(val))
		{
			this.type = cr.exptype.String;
			this.data = val.toString();
		}
		// null/undefined/an object for some reason
		else
		{
			this.type = cr.exptype.Integer;
			this.data = 0;
		}
	};
	
	cr.expvalue = ExpValue;

	// Enum values for expvalue
	cr.exptype = {
		Integer: 0,     // emulated; no native integer support in javascript
		Float: 1,
		String: 2
	};
}());

// system.js
// ECMAScript 5 strict mode

;

// System object
cr.system_object = function (runtime)
{
    this.runtime = runtime;
	
	// Scheduled events set by the Wait action
	this.waits = [];
};

cr.system_object.prototype.saveToJSON = function ()
{
	var o = {};
	var i, len, j, lenj, p, w, t, sobj;
	
	// save scheduled waits
	o["waits"] = [];
	var owaits = o["waits"];
	var waitobj;
	
	for (i = 0, len = this.waits.length; i < len; i++)
	{
		w = this.waits[i];
		waitobj = {
			"t": w.time,
			"st": w.signaltag,
			"s": w.signalled,
			"ev": w.ev.sid,
			"sm": [],
			"sols": {}
		};
		
		if (w.ev.actions[w.actindex])
			waitobj["act"] = w.ev.actions[w.actindex].sid;
			
		for (j = 0, lenj = w.solModifiers.length; j < lenj; j++)
			waitobj["sm"].push(w.solModifiers[j].sid);
			
		for (p in w.sols)
		{
			if (w.sols.hasOwnProperty(p))
			{
				t = this.runtime.types_by_index[parseInt(p, 10)];
;
				
				sobj = {
					"sa": w.sols[p].sa,
					"insts": []
				};
				
				for (j = 0, lenj = w.sols[p].insts.length; j < lenj; j++)
					sobj["insts"].push(w.sols[p].insts[j].uid);
				
				waitobj["sols"][t.sid.toString()] = sobj;
			}
		}
		
		owaits.push(waitobj);
	}
	
	return o;
};

cr.system_object.prototype.loadFromJSON = function (o)
{
	var owaits = o["waits"];
	var i, len, j, lenj, p, w, addWait, e, aindex, t, savedsol, nusol, inst;
	
	cr.clearArray(this.waits);
	
	for (i = 0, len = owaits.length; i < len; i++)
	{
		w = owaits[i];
		
		e = this.runtime.blocksBySid[w["ev"].toString()];
		
		if (!e)
			continue;	// event must've gone missing
			
		// Find the action it was pointing at in this event
		aindex = -1;
		
		for (j = 0, lenj = e.actions.length; j < lenj; j++)
		{
			if (e.actions[j].sid === w["act"])
			{
				aindex = j;
				break;
			}
		}
		
		if (aindex === -1)
			continue;	// action must've gone missing
		
		addWait = {};
		addWait.sols = {};
		addWait.solModifiers = [];
		addWait.deleteme = false;
		addWait.time = w["t"];
		addWait.signaltag = w["st"] || "";
		addWait.signalled = !!w["s"];
		addWait.ev = e;
		addWait.actindex = aindex;
		
		for (j = 0, lenj = w["sm"].length; j < lenj; j++)
		{
			t = this.runtime.getObjectTypeBySid(w["sm"][j]);
			
			if (t)
				addWait.solModifiers.push(t);
		}
		
		for (p in w["sols"])
		{
			if (w["sols"].hasOwnProperty(p))
			{
				t = this.runtime.getObjectTypeBySid(parseInt(p, 10));
				
				if (!t)
					continue;		// type must've been deleted
				
				savedsol = w["sols"][p];
				nusol = {
					sa: savedsol["sa"],
					insts: []
				};
				
				for (j = 0, lenj = savedsol["insts"].length; j < lenj; j++)
				{
					inst = this.runtime.getObjectByUID(savedsol["insts"][j]);
					
					if (inst)
						nusol.insts.push(inst);
				}
				
				addWait.sols[t.index.toString()] = nusol;
			}
		}
		
		this.waits.push(addWait);
	}
};


(function ()
{
	var sysProto = cr.system_object.prototype;
	
	function SysCnds() {};

	//////////////////////////////
	// System conditions
    SysCnds.prototype.EveryTick = function()
    {
        return true;
    };

    SysCnds.prototype.OnLayoutStart = function()
    {
        return true;
    };

    SysCnds.prototype.OnLayoutEnd = function()
    {
        return true;
    };

    SysCnds.prototype.Compare = function(x, cmp, y)
    {
        return cr.do_cmp(x, cmp, y);
    };

    SysCnds.prototype.CompareTime = function (cmp, t)
    {
        var elapsed = this.runtime.kahanTime.sum;

        // Handle 'time equals X' separately, basically as "on first tick where time is over X"
        if (cmp === 0)
        {
            var cnd = this.runtime.getCurrentCondition();

            if (!cnd.extra["CompareTime_executed"])
            {
                // First occasion that time has elapsed
                if (elapsed >= t)
                {
                    cnd.extra["CompareTime_executed"] = true;
                    return true;
                }
            }

            return false;
        }

        // Otherwise do ordinary comparison
        return cr.do_cmp(elapsed, cmp, t);
    };

    SysCnds.prototype.LayerVisible = function (layer)
    {
        if (!layer)
            return false;
        else
            return layer.visible;
    };
	
	SysCnds.prototype.LayerEmpty = function (layer)
    {
        if (!layer)
            return false;
        else
            return !layer.instances.length;
    };
	
	SysCnds.prototype.LayerCmpOpacity = function (layer, cmp, opacity_)
	{
		if (!layer)
			return false;
		
		return cr.do_cmp(layer.opacity * 100, cmp, opacity_);
	};

    SysCnds.prototype.Repeat = function (count)
    {
		var current_frame = this.runtime.getCurrentEventStack();
        var current_event = current_frame.current_event;
		var solModifierAfterCnds = current_frame.isModifierAfterCnds();
        var current_loop = this.runtime.pushLoopStack();

        var i;
		
		if (solModifierAfterCnds)
		{
			for (i = 0; i < count && !current_loop.stopped; i++)
			{
				this.runtime.pushCopySol(current_event.solModifiers);

				current_loop.index = i;
				current_event.retrigger();
				

				this.runtime.popSol(current_event.solModifiers);
			}
		}
		else
		{
			for (i = 0; i < count && !current_loop.stopped; i++)
			{
				current_loop.index = i;
				current_event.retrigger();
				
			}
		}

        this.runtime.popLoopStack();
		return false;
    };
	
	SysCnds.prototype.While = function (count)
    {
		var current_frame = this.runtime.getCurrentEventStack();
        var current_event = current_frame.current_event;
		var solModifierAfterCnds = current_frame.isModifierAfterCnds();
        var current_loop = this.runtime.pushLoopStack();

        var i;
		
		if (solModifierAfterCnds)
		{
			for (i = 0; !current_loop.stopped; i++)
			{
				this.runtime.pushCopySol(current_event.solModifiers);

				current_loop.index = i;
				
				if (!current_event.retrigger())		// one of the other conditions returned false
					current_loop.stopped = true;	// break

				this.runtime.popSol(current_event.solModifiers);
			}
		}
		else
		{
			for (i = 0; !current_loop.stopped; i++)
			{
				current_loop.index = i;
				
				if (!current_event.retrigger())
					current_loop.stopped = true;
			}
		}

        this.runtime.popLoopStack();
		return false;
    };

    SysCnds.prototype.For = function (name, start, end)
    {
        var current_frame = this.runtime.getCurrentEventStack();
        var current_event = current_frame.current_event;
		var solModifierAfterCnds = current_frame.isModifierAfterCnds();
        var current_loop = this.runtime.pushLoopStack(name);

        var i;
		
		// running backwards
		if (end < start)
		{
			if (solModifierAfterCnds)
			{
				for (i = start; i >= end && !current_loop.stopped; --i)  // inclusive to end
				{
					this.runtime.pushCopySol(current_event.solModifiers);

					current_loop.index = i;
					current_event.retrigger();

					this.runtime.popSol(current_event.solModifiers);
				}
			}
			else
			{
				for (i = start; i >= end && !current_loop.stopped; --i)  // inclusive to end
				{
					current_loop.index = i;
					current_event.retrigger();
				}
			}
		}
		else
		{
			if (solModifierAfterCnds)
			{
				for (i = start; i <= end && !current_loop.stopped; ++i)  // inclusive to end
				{
					this.runtime.pushCopySol(current_event.solModifiers);

					current_loop.index = i;
					current_event.retrigger();

					this.runtime.popSol(current_event.solModifiers);
				}
			}
			else
			{
				for (i = start; i <= end && !current_loop.stopped; ++i)  // inclusive to end
				{
					current_loop.index = i;
					current_event.retrigger();
				}
			}
		}

        this.runtime.popLoopStack();
		return false;
    };

	// For recycling arrays and avoiding garbage in foreach conditions
	var foreach_instancestack = [];
	var foreach_instanceptr = -1;
	
    SysCnds.prototype.ForEach = function (obj)
    {
        // Copy instances to iterate
        var sol = obj.getCurrentSol();
		
		// Push to foreach stack if necessary
		foreach_instanceptr++;
		if (foreach_instancestack.length === foreach_instanceptr)
			foreach_instancestack.push([]);
		
		var instances = foreach_instancestack[foreach_instanceptr];
		cr.shallowAssignArray(instances, sol.getObjects());

        var current_frame = this.runtime.getCurrentEventStack();
        var current_event = current_frame.current_event;
		var solModifierAfterCnds = current_frame.isModifierAfterCnds();
        var current_loop = this.runtime.pushLoopStack();

        var i, len, j, lenj, inst, s, sol2;
		var is_contained = obj.is_contained;
		
		if (solModifierAfterCnds)
		{
			for (i = 0, len = instances.length; i < len && !current_loop.stopped; i++)
			{
				this.runtime.pushCopySol(current_event.solModifiers);
				
				inst = instances[i];

				// Pick the current instance (note sol was pushed above, don't move this out the loop)
				sol = obj.getCurrentSol();
				sol.select_all = false;
				cr.clearArray(sol.instances);
				sol.instances[0] = inst;
				
				if (is_contained)
				{
					for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
					{
						s = inst.siblings[j];
						sol2 = s.type.getCurrentSol();
						sol2.select_all = false;
						cr.clearArray(sol2.instances);
						sol2.instances[0] = s;
					}
				}

				current_loop.index = i;
				current_event.retrigger();

				this.runtime.popSol(current_event.solModifiers);
			}
		}
		else
		{
			sol.select_all = false;
			cr.clearArray(sol.instances);
			
			for (i = 0, len = instances.length; i < len && !current_loop.stopped; i++)
			{
				inst = instances[i];
				sol.instances[0] = inst;
				
				if (is_contained)
				{
					for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
					{
						s = inst.siblings[j];
						sol2 = s.type.getCurrentSol();
						sol2.select_all = false;
						cr.clearArray(sol2.instances);
						sol2.instances[0] = s;
					}
				}

				current_loop.index = i;
				current_event.retrigger();
			}
		}

		cr.clearArray(instances);
        this.runtime.popLoopStack();
		foreach_instanceptr--;
		return false;
    };
	
	function foreach_sortinstances(a, b)
	{
		var va = a.extra["c2_feo_val"];
		var vb = b.extra["c2_feo_val"];
		
		if (cr.is_number(va) && cr.is_number(vb))
			return va - vb;
		else
		{
			va = "" + va;
			vb = "" + vb;
			
			if (va < vb)
				return -1;
			else if (va > vb)
				return 1;
			else
				return 0;
		}
	};
	
	SysCnds.prototype.ForEachOrdered = function (obj, exp, order)
    {
        // Copy instances to iterate
        var sol = obj.getCurrentSol();
        
		// Push to foreach stack if necessary
		foreach_instanceptr++;
		if (foreach_instancestack.length === foreach_instanceptr)
			foreach_instancestack.push([]);
		
		var instances = foreach_instancestack[foreach_instanceptr];
		cr.shallowAssignArray(instances, sol.getObjects());

        var current_frame = this.runtime.getCurrentEventStack();
        var current_event = current_frame.current_event;
		var current_condition = this.runtime.getCurrentCondition();
		var solModifierAfterCnds = current_frame.isModifierAfterCnds();
        var current_loop = this.runtime.pushLoopStack();

		// Re-calculate the expression evaluated for each individual instance in the SOL
		var i, len, j, lenj, inst, s, sol2;
		for (i = 0, len = instances.length; i < len; i++)
		{
			instances[i].extra["c2_feo_val"] = current_condition.parameters[1].get(i);
		}
		
		// Sort instances by the calculated values in ascending order (and reverse if descending)
		instances.sort(foreach_sortinstances);

		if (order === 1)
			instances.reverse();
			
		var is_contained = obj.is_contained;
		
		// From here, same as for-each
		if (solModifierAfterCnds)
		{
			for (i = 0, len = instances.length; i < len && !current_loop.stopped; i++)
			{
				this.runtime.pushCopySol(current_event.solModifiers);

				inst = instances[i];
				sol = obj.getCurrentSol();
				sol.select_all = false;
				cr.clearArray(sol.instances);
				sol.instances[0] = inst;
				
				if (is_contained)
				{
					for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
					{
						s = inst.siblings[j];
						sol2 = s.type.getCurrentSol();
						sol2.select_all = false;
						cr.clearArray(sol2.instances);
						sol2.instances[0] = s;
					}
				}

				current_loop.index = i;
				current_event.retrigger();

				this.runtime.popSol(current_event.solModifiers);
			}
		}
		else
		{
			sol.select_all = false;
			cr.clearArray(sol.instances);
			
			for (i = 0, len = instances.length; i < len && !current_loop.stopped; i++)
			{
				inst = instances[i];
				sol.instances[0] = inst;
				
				if (is_contained)
				{
					for (j = 0, lenj = inst.siblings.length; j < lenj; j++)
					{
						s = inst.siblings[j];
						sol2 = s.type.getCurrentSol();
						sol2.select_all = false;
						cr.clearArray(sol2.instances);
						sol2.instances[0] = s;
					}
				}

				current_loop.index = i;
				current_event.retrigger();
			}
		}

		cr.clearArray(instances);
        this.runtime.popLoopStack();
		foreach_instanceptr--;
		return false;
    };
	
	SysCnds.prototype.PickByComparison = function (obj_, exp_, cmp_, val_)
	{
		var i, len, k, inst;
		
		if (!obj_)
			return;
		
		// Re-use foreach stack for temp arrays (expression to evaluate could call functions)
		foreach_instanceptr++;
		if (foreach_instancestack.length === foreach_instanceptr)
			foreach_instancestack.push([]);
		
		var tmp_instances = foreach_instancestack[foreach_instanceptr];
		
		// Copy the instances to process to tmp_instances.
		var sol = obj_.getCurrentSol();
		cr.shallowAssignArray(tmp_instances, sol.getObjects());
		
		if (sol.select_all)
			cr.clearArray(sol.else_instances);
		
		// All instances to process are now in tmp_instances. Filter them down to only
		// those meeting the condition; if any don't meet the condition, add them to else_instances.		
		var current_condition = this.runtime.getCurrentCondition();
		
		for (i = 0, k = 0, len = tmp_instances.length; i < len; i++)
		{
			inst = tmp_instances[i];
			tmp_instances[k] = inst;
			
			exp_ = current_condition.parameters[1].get(i);
			val_ = current_condition.parameters[3].get(i);
			
			if (cr.do_cmp(exp_, cmp_, val_))
			{
				k++;
			}
			else
			{
				sol.else_instances.push(inst);
			}
		}
		
		cr.truncateArray(tmp_instances, k);
		
		sol.select_all = false;
		cr.shallowAssignArray(sol.instances, tmp_instances);
		cr.clearArray(tmp_instances);
		
		foreach_instanceptr--;
		
		obj_.applySolToContainer();
		
		return !!sol.instances.length;
	};
	
	SysCnds.prototype.PickByEvaluate = function (obj_, exp_)
	{
		var i, len, k, inst;
		
		if (!obj_)
			return;
		
		// Re-use foreach stack for temp arrays (expression to evaluate could call functions)
		foreach_instanceptr++;
		if (foreach_instancestack.length === foreach_instanceptr)
			foreach_instancestack.push([]);
		
		var tmp_instances = foreach_instancestack[foreach_instanceptr];
		
		// Copy the instances to process to tmp_instances.
		var sol = obj_.getCurrentSol();
		cr.shallowAssignArray(tmp_instances, sol.getObjects());
		
		if (sol.select_all)
			cr.clearArray(sol.else_instances);
		
		// All instances to process are now in tmp_instances. Filter them down to only
		// those meeting the condition; if any don't meet the condition, add them to else_instances.		
		var current_condition = this.runtime.getCurrentCondition();
		
		for (i = 0, k = 0, len = tmp_instances.length; i < len; i++)
		{
			inst = tmp_instances[i];
			tmp_instances[k] = inst;
			
			exp_ = current_condition.parameters[1].get(i);
			
			if (exp_)
			{
				k++;
			}
			else
			{
				sol.else_instances.push(inst);
			}
		}
		
		cr.truncateArray(tmp_instances, k);
		
		sol.select_all = false;
		cr.shallowAssignArray(sol.instances, tmp_instances);
		cr.clearArray(tmp_instances);
		
		foreach_instanceptr--;
		
		obj_.applySolToContainer();
		
		return !!sol.instances.length;
	};

    SysCnds.prototype.TriggerOnce = function ()
    {
        // Store state in the owner condition
        var cndextra = this.runtime.getCurrentCondition().extra;

        // Get the last tick time that the condition was reached
		if (typeof cndextra["TriggerOnce_lastTick"] === "undefined")
			cndextra["TriggerOnce_lastTick"] = -1;
		
        var last_tick = cndextra["TriggerOnce_lastTick"];
        var cur_tick = this.runtime.tickcount;

        cndextra["TriggerOnce_lastTick"] = cur_tick;

        // If the last true tick was last tick, filter this call by returning false.
		// Always return true on the first tick of a layout, else restarting the current layout
		// doesn't re-run conditions filtered by 'trigger once'.
        return this.runtime.layout_first_tick || last_tick !== cur_tick - 1;
    };

    SysCnds.prototype.Every = function (seconds)
    {
        // Store state in the owner condition
        var cnd = this.runtime.getCurrentCondition();

        // Get the last time that the event ran
        var last_time = cnd.extra["Every_lastTime"] || 0;
        var cur_time = this.runtime.kahanTime.sum;
		
		// Only use the parameter every time the event runs
		if (typeof cnd.extra["Every_seconds"] === "undefined")
			cnd.extra["Every_seconds"] = seconds;
			
		var this_seconds = cnd.extra["Every_seconds"];

        // Delay has elapsed
        if (cur_time >= last_time + this_seconds)
        {
            cnd.extra["Every_lastTime"] = last_time + this_seconds;
			
			// If it's still over 40ms behind, just bring it up to date
			if (cur_time >= cnd.extra["Every_lastTime"] + 0.04)
			{
				cnd.extra["Every_lastTime"] = cur_time;
			}
			
			// Update the next time based on new parameter
			cnd.extra["Every_seconds"] = seconds;
			
            return true;
        }
		// Last triggered time is in the future (possible when using save/load): just bring timer up to date.
		else if (cur_time < last_time - 0.1)
		{
			cnd.extra["Every_lastTime"] = cur_time;
		}
        
		return false;
    };

    SysCnds.prototype.PickNth = function (obj, index)
    {
        if (!obj)
            return false;

        // Get the current sol
        var sol = obj.getCurrentSol();
        var instances = sol.getObjects();
		
		index = cr.floor(index);

        // Index out of range: condition false (no wraparound)
        if (index < 0 || index >= instances.length)
            return false;
			
		var inst = instances[index];

        // Set just the nth instance picked
        sol.pick_one(inst);
		obj.applySolToContainer();
        return true;
    };
	
	SysCnds.prototype.PickRandom = function (obj)
    {
        if (!obj)
            return false;

        // Get the current sol
        var sol = obj.getCurrentSol();
        var instances = sol.getObjects();
		
		var index = cr.floor(Math.random() * instances.length);

        // Index out of range: condition false (no wraparound)
        if (index >= instances.length)
            return false;
			
		var inst = instances[index];

        // Set just the nth instance picked
        sol.pick_one(inst);
		obj.applySolToContainer();
        return true;
    };
	
	SysCnds.prototype.CompareVar = function (v, cmp, val)
    {
        return cr.do_cmp(v.getValue(), cmp, val);
    };
	
	SysCnds.prototype.CompareBoolVar = function (v)
    {
        return v.getValue();
    };

    SysCnds.prototype.IsGroupActive = function (group)
    {
		var g = this.runtime.groups_by_name[group.toLowerCase()];
        return g && g.group_active;
    };
	
	SysCnds.prototype.IsPreview = function ()
	{
		return this.runtime.isPreview;
	};
	
	SysCnds.prototype.PickAll = function (obj)
    {
        if (!obj)
            return false;
			
		if (!obj.instances.length)
			return false;

        // Get the current sol and reset the select_all flag
        var sol = obj.getCurrentSol();
        sol.select_all = true;
		obj.applySolToContainer();
        return true;
    };
	
	SysCnds.prototype.IsMobile = function ()
	{
		return this.runtime.isMobile;
	};
	
	SysCnds.prototype.CompareBetween = function (x, a, b)
	{
		return x >= a && x <= b;
	};
	
	SysCnds.prototype.Else = function ()
	{
		var current_frame = this.runtime.getCurrentEventStack();
	
		if (current_frame.else_branch_ran)
			return false;		// another event in this else-if chain has run
		else
			return !current_frame.last_event_true;
		
		// TODO: picking Else implementation
		/*
		var current_frame = this.runtime.getCurrentEventStack();
        var current_event = current_frame.current_event;
		var prev_event = current_event.prev_block;
		
		if (!prev_event)
			return false;
			
		// If previous event is "logical" (e.g. is purely system conditions),
		// run solely based on if the last event did not run.
		if (prev_event.is_logical)
			return !this.runtime.last_event_true;
	
		// Otherwise, invert the picked object's SOLs (swap with the else_instances)
		// and run with those instances.
		var i, len, j, lenj, s, sol, temp, inst, any_picked = false;
		for (i = 0, len = prev_event.cndReferences.length; i < len; i++)
		{
			s = prev_event.cndReferences[i];
			sol = s.getCurrentSol();
			
			// All picked: pick none
			if (sol.select_all || sol.instances.length === s.instances.length)
			{
				sol.select_all = false;
				sol.instances.length = 0;
			}
			// Some picked: swap the pick lists to invert
			else
			{
				// Static conditions sometimes set one instance without filling else_instances.
				// In this case copy in all instances except the one picked.
				if (sol.instances.length === 1 && sol.else_instances.length === 0 && s.instances.length >= 2)
				{
					inst = sol.instances[0];
					sol.instances.length = 0;
					
					for (j = 0, lenj = s.instances.length; j < lenj; j++)
					{
						if (s.instances[j] != inst)
							sol.instances.push(s.instances[j]);
					}
					
					any_picked = true;
				}
				else
				{
					// Swap the picked instances with else_instances set by the preceding event
					temp = sol.instances;
					sol.instances = sol.else_instances;
					sol.else_instances = temp;
					any_picked = true;
				}
			}
		}
		
		// Nothing picked at all (all SOLs reverted to empty): event will do nothing, do not run
		return any_picked;
		*/
	};
	
	SysCnds.prototype.OnLoadFinished = function ()
	{
		return true;
	};
	
	SysCnds.prototype.OnCanvasSnapshot = function ()
	{
		return true;
	};
	
	SysCnds.prototype.EffectsSupported = function ()
	{
		return !!this.runtime.glwrap;
	};
	
	SysCnds.prototype.OnSaveComplete = function ()
	{
		return true;
	};
	
	SysCnds.prototype.OnSaveFailed = function ()
	{
		return true;
	};
	
	SysCnds.prototype.OnLoadComplete = function ()
	{
		return true;
	};
	
	SysCnds.prototype.OnLoadFailed = function ()
	{
		return true;
	};
	
	SysCnds.prototype.ObjectUIDExists = function (u)
	{
		return !!this.runtime.getObjectByUID(u);
	};
	
	SysCnds.prototype.IsOnPlatform = function (p)
	{
		var rt = this.runtime;
		
		switch (p) {
		case 0:		// HTML5 website
			return !rt.isNodeWebkit && !rt.isCordova && !rt.isWinJS && !rt.isWindowsPhone8 && !rt.isBlackberry10 && !rt.isAmazonWebApp;
		case 1:		// iOS
			return rt.isiOS;
		case 2:		// Android
			return rt.isAndroid;
		case 3:		// Windows 8
			return rt.isWindows8App;
		case 4:		// Windows Phone 8
			return rt.isWindowsPhone8;
		case 5:		// Blackberry 10
			return rt.isBlackberry10;
		case 6:		// Tizen
			return rt.isTizen;
		case 7:		// CocoonJS
			return false;		// removed
		case 8:		// Cordova
			return rt.isCordova;
		case 9:	// Scirra Arcade
			return rt.isArcade;
		case 10:	// node-webkit
			return rt.isNodeWebkit;
		case 11:	// crosswalk
			return rt.isCrosswalk;
		case 12:	// amazon webapp
			return rt.isAmazonWebApp;
		case 13:	// windows 10 app
			return rt.isWindows10;
		default:	// should not be possible
			return false;
		}
	};
	
	var cacheRegex = null;
	var lastRegex = "";
	var lastFlags = "";
	
	function getRegex(regex_, flags_)
	{
		// To allow RegExp objects to be compiled efficiently, keep re-using the same RegExp
		// object until the regex or flags change
		if (!cacheRegex || regex_ !== lastRegex || flags_ !== lastFlags)
		{
			cacheRegex = new RegExp(regex_, flags_);
			lastRegex = regex_;
			lastFlags = flags_;
		}
		
		cacheRegex.lastIndex = 0;		// reset
		return cacheRegex;
	};
	
	SysCnds.prototype.RegexTest = function (str_, regex_, flags_)
	{
		var regex = getRegex(regex_, flags_);
		
		return regex.test(str_);
	};
	
	var tmp_arr = [];
	
	SysCnds.prototype.PickOverlappingPoint = function (obj_, x_, y_)
	{
		if (!obj_)
            return false;

        // Get the current sol
        var sol = obj_.getCurrentSol();
        var instances = sol.getObjects();
		var current_event = this.runtime.getCurrentEventStack().current_event;
		var orblock = current_event.orblock;
		
		var cnd = this.runtime.getCurrentCondition();
		var i, len, inst, pick;
		
		if (sol.select_all)
		{
			cr.shallowAssignArray(tmp_arr, instances);
			cr.clearArray(sol.else_instances);
			sol.select_all = false;
			cr.clearArray(sol.instances);
		}
		else
		{
			if (orblock)
			{
				cr.shallowAssignArray(tmp_arr, sol.else_instances);
				cr.clearArray(sol.else_instances);
			}
			else
			{
				cr.shallowAssignArray(tmp_arr, instances);
				cr.clearArray(sol.instances);
			}
		}
		
		for (i = 0, len = tmp_arr.length; i < len; ++i)
		{
			inst = tmp_arr[i];
			inst.update_bbox();
			
			pick = cr.xor(inst.contains_pt(x_, y_), cnd.inverted);
			
			if (pick)
				sol.instances.push(inst);
			else
				sol.else_instances.push(inst);
		}
		
		obj_.applySolToContainer();
		
		return cr.xor(!!sol.instances.length, cnd.inverted);
	};
	
	SysCnds.prototype.PickLastCreated = function (obj_)
	{
		if (!obj_)
            return false;
		
		// Look for queued instances in create row, since those are the most recently created.
		// Search backwards for any instance matching the given type, or belonging to the family.
		var createRow = this.runtime.createRow;
		var i, inst, pick = null;
		var isFamily = obj_.is_family;
		for (i = createRow.length - 1; i >= 0; --i)
		{
			inst = createRow[i];
			
			if (isFamily)
			{
				if (inst.type.families.indexOf(obj_) >= 0)
				{
					pick = inst;
					break;
				}
			}
			else
			{
				if (inst.type === obj_)
				{
					pick = inst;
					break;
				}
			}
		}
		
		// Nothing on create row: pick the last instance in the type's instance list
		if (!pick && obj_.instances.length)
		{
			pick = obj_.instances[obj_.instances.length - 1];
		}
		
		// nothing to pick
		if (!pick)
			return false;
		
		// pick the last created inst
		var sol = obj_.getCurrentSol();
		sol.pick_one(pick);
		obj_.applySolToContainer();
        return true;
	};
	
	SysCnds.prototype.IsNaN = function (n)
	{
		return !!isNaN(n);
	};
	
	SysCnds.prototype.AngleWithin = function (a1, within, a2)
	{
		return cr.angleDiff(cr.to_radians(a1), cr.to_radians(a2)) <= cr.to_radians(within);
	};
	
	SysCnds.prototype.IsClockwiseFrom = function (a1, a2)
	{
		return cr.angleClockwise(cr.to_radians(a1), cr.to_radians(a2));
	};
	
	SysCnds.prototype.IsBetweenAngles = function (a, la, ua)
	{
		var angle = cr.to_clamped_radians(a);
		var lower = cr.to_clamped_radians(la);
		var upper = cr.to_clamped_radians(ua);
		var obtuse = (!cr.angleClockwise(upper, lower));
		
		// Handle differently when angle range is over 180 degrees, since angleClockwise only tests if within
		// 180 degrees clockwise of the angle
		if (obtuse)
			return !(!cr.angleClockwise(angle, lower) && cr.angleClockwise(angle, upper));
		else
			return cr.angleClockwise(angle, lower) && !cr.angleClockwise(angle, upper);
	};
	
	SysCnds.prototype.IsValueType = function (x, t)
	{
		if (typeof x === "number")
			return t === 0;
		else		// string
			return t === 1;
	};
	
	sysProto.cnds = new SysCnds();

	//////////////////////////////
	// System actions
    function SysActs() {};

    SysActs.prototype.GoToLayout = function (to)
    {
		if (this.runtime.isloading)
			return;		// cannot change layout while loading on loader layout
			
		if (this.runtime.changelayout)
			return;		// already changing to a different layout
		
;
        this.runtime.changelayout = to;
    };
	
	SysActs.prototype.NextPrevLayout = function (prev)
    {
		if (this.runtime.isloading)
			return;		// cannot change layout while loading on loader layout
			
		if (this.runtime.changelayout)
			return;		// already changing to a different layout
		
		var index = this.runtime.layouts_by_index.indexOf(this.runtime.running_layout);
		
		if (prev && index === 0)	
			return;		// cannot go to previous layout from first layout
		
		if (!prev && index === this.runtime.layouts_by_index.length - 1)
			return;		// cannot go to next layout from last layout
		
		var to = this.runtime.layouts_by_index[index + (prev ? -1 : 1)];
		
;
        this.runtime.changelayout = to;
    };

    SysActs.prototype.CreateObject = function (obj, layer, x, y)
    {
        if (!layer || !obj)
            return;

        var inst = this.runtime.createInstance(obj, layer, x, y);
		
		if (!inst)
			return;
		
		this.runtime.isInOnDestroy++;
		
		var i, len, s;
		this.runtime.trigger(Object.getPrototypeOf(obj.plugin).cnds.OnCreated, inst);
		
		if (inst.is_contained)
		{
			for (i = 0, len = inst.siblings.length; i < len; i++)
			{
				s = inst.siblings[i];
				this.runtime.trigger(Object.getPrototypeOf(s.type.plugin).cnds.OnCreated, s);
			}
		}
		
		this.runtime.isInOnDestroy--;

        // Pick just this instance
        var sol = obj.getCurrentSol();
        sol.select_all = false;
		cr.clearArray(sol.instances);
		sol.instances[0] = inst;
		
		// Siblings aren't in instance lists yet, pick them manually
		if (inst.is_contained)
		{
			for (i = 0, len = inst.siblings.length; i < len; i++)
			{
				s = inst.siblings[i];
				sol = s.type.getCurrentSol();
				sol.select_all = false;
				cr.clearArray(sol.instances);
				sol.instances[0] = s;
			}
		}
    };
	
	SysActs.prototype.CreateObjectByName = function (objName, layer, x, y)
    {
        if (!layer || !objName)
            return;

        // Look up object type by name
		var objectClass = this.runtime.types_lowercase[objName.toLowerCase()];
		
		if (!objectClass)
			return;
		
		// Invoke ordinary create action
		SysActs.prototype.CreateObject.call(this, objectClass, layer, x, y);
    };

    SysActs.prototype.SetLayerVisible = function (layer, visible_)
    {
        if (!layer)
            return;

		if (layer.visible !== visible_)
		{
			layer.visible = visible_;
			this.runtime.redraw = true;
		}
    };
	
	SysActs.prototype.SetLayerOpacity = function (layer, opacity_)
	{
		if (!layer)
			return;
			
		opacity_ = cr.clamp(opacity_ / 100, 0, 1);
		
		if (layer.opacity !== opacity_)
		{
			layer.opacity = opacity_;
			this.runtime.redraw = true;
		}
	};
	
	SysActs.prototype.SetLayerScaleRate = function (layer, sr)
	{
		if (!layer)
			return;
			
		if (layer.zoomRate !== sr)
		{
			layer.zoomRate = sr;
			this.runtime.redraw = true;
		}
	};
	
	SysActs.prototype.SetLayerForceOwnTexture = function (layer, f)
	{
		if (!layer)
			return;
		
		f = !!f;
		
		if (layer.forceOwnTexture !== f)
		{
			layer.forceOwnTexture = f;
			this.runtime.redraw = true;
		}
	};
	
	SysActs.prototype.SetLayoutScale = function (s)
	{
		if (!this.runtime.running_layout)
			return;
			
		if (this.runtime.running_layout.scale !== s)
		{
			this.runtime.running_layout.scale = s;
			this.runtime.running_layout.boundScrolling();
			this.runtime.redraw = true;
		}
	};

    SysActs.prototype.ScrollX = function(x)
    {
        this.runtime.running_layout.scrollToX(x);
    };

    SysActs.prototype.ScrollY = function(y)
    {
        this.runtime.running_layout.scrollToY(y);
    };

    SysActs.prototype.Scroll = function(x, y)
    {
        this.runtime.running_layout.scrollToX(x);
        this.runtime.running_layout.scrollToY(y);
    };

    SysActs.prototype.ScrollToObject = function(obj)
    {
        var inst = obj.getFirstPicked();

        if (inst)
        {
            this.runtime.running_layout.scrollToX(inst.x);
            this.runtime.running_layout.scrollToY(inst.y);
        }
    };
	
	SysActs.prototype.SetVar = function(v, x)
	{
;
		
		// Number
		if (v.vartype === 0)
		{
			if (cr.is_number(x))
				v.setValue(x);
			else
				v.setValue(parseFloat(x));
		}
		// String
		else if (v.vartype === 1)
			v.setValue(x.toString());
	};
	
	SysActs.prototype.AddVar = function(v, x)
	{
;
		
		// Number
		if (v.vartype === 0)
		{
			if (cr.is_number(x))
				v.setValue(v.getValue() + x);
			else
				v.setValue(v.getValue() + parseFloat(x));
		}
		// String
		else if (v.vartype === 1)
			v.setValue(v.getValue() + x.toString());
	};
	
	SysActs.prototype.SubVar = function(v, x)
	{
;
		
		// Number
		if (v.vartype === 0)
		{
			if (cr.is_number(x))
				v.setValue(v.getValue() - x);
			else
				v.setValue(v.getValue() - parseFloat(x));
		}
	};
	
	SysActs.prototype.SetBoolVar = function(v, x)
	{
;
;
		
		v.setValue(x !== 0);
	};
	
	SysActs.prototype.ToggleBoolVar = function(v)
	{
;
;
		
		v.setValue(!v.getValue());
	};

    SysActs.prototype.SetGroupActive = function (group, active)
    {
		var g = this.runtime.groups_by_name[group.toLowerCase()];
		
		if (!g)
			return;
		
		switch (active) {
		// Disable
		case 0:
			g.setGroupActive(false);
			break;
		// Enable
		case 1:
			g.setGroupActive(true);
			break;
		// Toggle
		case 2:
			g.setGroupActive(!g.group_active);
			break;
		}
    };

    SysActs.prototype.SetTimescale = function (ts_)
    {
        var ts = ts_;

        if (ts < 0)
            ts = 0;

        this.runtime.timescale = ts;
    };

    SysActs.prototype.SetObjectTimescale = function (obj, ts_)
    {
        var ts = ts_;

        if (ts < 0)
            ts = 0;

        if (!obj)
            return;

        // Get the current sol
        var sol = obj.getCurrentSol();
        var instances = sol.getObjects();

        // Set all timescales
        var i, len;
        for (i = 0, len = instances.length; i < len; i++)
        {
            instances[i].my_timescale = ts;
        }
    };

    SysActs.prototype.RestoreObjectTimescale = function (obj)
    {
        if (!obj)
            return false;

        // Get the current sol
        var sol = obj.getCurrentSol();
        var instances = sol.getObjects();

        // Set all timescales to -1, to indicate game time
        var i, len;
        for (i = 0, len = instances.length; i < len; i++)
        {
            instances[i].my_timescale = -1.0;
        }
    };
	
	var waitobjrecycle = [];
	
	function allocWaitObject()
	{
		var w;
		
		if (waitobjrecycle.length)
			w = waitobjrecycle.pop();
		else
		{
			w = {};
			w.sols = {};
			w.solModifiers = [];
		}
		
		w.deleteme = false;
		return w;
	};
	
	function freeWaitObject(w)
	{
		cr.wipe(w.sols);
		cr.clearArray(w.solModifiers);
		waitobjrecycle.push(w);
	};
	
	var solstateobjects = [];
	
	function allocSolStateObject()
	{
		var s;
		
		if (solstateobjects.length)
			s = solstateobjects.pop();
		else
		{
			s = {};
			s.insts = [];
		}
		
		s.sa = false;
		return s;
	};
	
	function freeSolStateObject(s)
	{
		cr.clearArray(s.insts);
		solstateobjects.push(s);
	};
	
	SysActs.prototype.Wait = function (seconds)
	{
		if (seconds < 0)
			return;
		
		var i, len, s, t, ss;
		var evinfo = this.runtime.getCurrentEventStack();
		
		// Add a new wait record with the current SOL state and scheduled time
		var waitobj = allocWaitObject();
		waitobj.time = this.runtime.kahanTime.sum + seconds;
		waitobj.signaltag = "";
		waitobj.signalled = false;
		waitobj.ev = evinfo.current_event;
		waitobj.actindex = evinfo.actindex + 1;	// pointing at next action
		
		for (i = 0, len = this.runtime.types_by_index.length; i < len; i++)
		{
			t = this.runtime.types_by_index[i];
			s = t.getCurrentSol();
			
			if (s.select_all && evinfo.current_event.solModifiers.indexOf(t) === -1)
				continue;
				
			// Copy selected instances
			waitobj.solModifiers.push(t);
			ss = allocSolStateObject();
			ss.sa = s.select_all;
			cr.shallowAssignArray(ss.insts, s.instances);
			waitobj.sols[i.toString()] = ss;
		}
		
		this.waits.push(waitobj);
		
		// Return true so the current event cancels in run_actions_and_subevents()
		return true;
	};
	
	SysActs.prototype.WaitForSignal = function (tag)
	{
		var i, len, s, t, ss;
		var evinfo = this.runtime.getCurrentEventStack();
		
		// Add a new wait record with the current SOL state and scheduled time
		var waitobj = allocWaitObject();
		waitobj.time = -1;
		waitobj.signaltag = tag.toLowerCase();
		waitobj.signalled = false;
		waitobj.ev = evinfo.current_event;
		waitobj.actindex = evinfo.actindex + 1;	// pointing at next action
		
		for (i = 0, len = this.runtime.types_by_index.length; i < len; i++)
		{
			t = this.runtime.types_by_index[i];
			s = t.getCurrentSol();
			
			if (s.select_all && evinfo.current_event.solModifiers.indexOf(t) === -1)
				continue;
				
			// Copy selected instances
			waitobj.solModifiers.push(t);
			ss = allocSolStateObject();
			ss.sa = s.select_all;
			cr.shallowAssignArray(ss.insts, s.instances);
			waitobj.sols[i.toString()] = ss;
		}
		
		this.waits.push(waitobj);
		
		// Return true so the current event cancels in run_actions_and_subevents()
		return true;
	};
	
	SysActs.prototype.Signal = function (tag)
	{
		// Mark all waiting events with the same tag as signalled
		var lowertag = tag.toLowerCase();
		
		var i, len, w;
		
		for (i = 0, len = this.waits.length; i < len; ++i)
		{
			w = this.waits[i];
			
			if (w.time !== -1)
				continue;					// timer wait, ignore
			
			if (w.signaltag === lowertag)	// waiting for this signal
				w.signalled = true;			// will run on next check
		}
	};
	
	SysActs.prototype.SetLayerScale = function (layer, scale)
    {
        if (!layer)
            return;

		if (layer.scale === scale)
			return;
			
        layer.scale = scale;
        this.runtime.redraw = true;
    };
	
	SysActs.prototype.ResetGlobals = function ()
	{
		var i, len, g;
		for (i = 0, len = this.runtime.all_global_vars.length; i < len; i++)
		{
			g = this.runtime.all_global_vars[i];
			g.data = g.initial;
		}
	};
	
	SysActs.prototype.SetLayoutAngle = function (a)
	{
		a = cr.to_radians(a);
		a = cr.clamp_angle(a);
		
		if (this.runtime.running_layout)
		{
			if (this.runtime.running_layout.angle !== a)
			{
				this.runtime.running_layout.angle = a;
				this.runtime.redraw = true;
			}
		}
	};
	
	SysActs.prototype.SetLayerAngle = function (layer, a)
    {
        if (!layer)
            return;
			
		a = cr.to_radians(a);
		a = cr.clamp_angle(a);

		if (layer.angle === a)
			return;
			
        layer.angle = a;
        this.runtime.redraw = true;
    };
	
	SysActs.prototype.SetLayerParallax = function (layer, px, py)
    {
        if (!layer)
            return;
			
		if (layer.parallaxX === px / 100 && layer.parallaxY === py / 100)
			return;
			
        layer.parallaxX = px / 100;
		layer.parallaxY = py / 100;
		
		// If layer is now parallaxed, update types any_instance_parallaxed flag
		if (layer.parallaxX !== 1 || layer.parallaxY !== 1)
		{
			var i, len, instances = layer.instances;
			for (i = 0, len = instances.length; i < len; ++i)
			{
				instances[i].type.any_instance_parallaxed = true;
			}
		}
		
        this.runtime.redraw = true;
    };
	
	SysActs.prototype.SetLayerBackground = function (layer, rgb)
    {
        if (!layer)
            return;
			
		var r = cr.clamp(Math.floor(cr.GetRValue(rgb) * 255), 0, 255);
		var g = cr.clamp(Math.floor(cr.GetGValue(rgb) * 255), 0, 255);
		var b = cr.clamp(Math.floor(cr.GetBValue(rgb) * 255), 0, 255);
			
		if (layer.background_color[0] === r && layer.background_color[1] === g && layer.background_color[2] === b)
			return;
			
        layer.background_color[0] = r;
		layer.background_color[1] = g;
		layer.background_color[2] = b;
        this.runtime.redraw = true;
    };
	
	SysActs.prototype.SetLayerTransparent = function (layer, t)
    {
        if (!layer)
            return;
			
		if (!!t === !!layer.transparent)
			return;
			
		layer.transparent = !!t;
        this.runtime.redraw = true;
    };
	
	SysActs.prototype.SetLayerBlendMode = function (layer, bm)
    {
        if (!layer)
            return;
			
		if (layer.blend_mode === bm)
			return;
			
		layer.blend_mode = bm;
		layer.compositeOp = cr.effectToCompositeOp(layer.blend_mode);
		
		if (this.runtime.gl)
			cr.setGLBlend(layer, layer.blend_mode, this.runtime.gl);
		
        this.runtime.redraw = true;
    };
	
	SysActs.prototype.StopLoop = function ()
	{
		if (this.runtime.loop_stack_index < 0)
			return;		// no loop currently running
			
		// otherwise mark loop stopped
		this.runtime.getCurrentLoop().stopped = true;
	};
	
	SysActs.prototype.GoToLayoutByName = function (layoutname)
	{
		if (this.runtime.isloading)
			return;		// cannot change layout while loading on loader layout
			
		if (this.runtime.changelayout)
			return;		// already changing to different layout
		
;
		
		// Find layout name with correct case
		var l;
		for (l in this.runtime.layouts)
		{
			if (this.runtime.layouts.hasOwnProperty(l) && cr.equals_nocase(l, layoutname))
			{
				this.runtime.changelayout = this.runtime.layouts[l];
				return;
			}
		}
	};
	
	SysActs.prototype.RestartLayout = function (layoutname)
	{
		if (this.runtime.isloading)
			return;		// cannot restart loader layouts
			
		if (this.runtime.changelayout)
			return;		// already changing to a different layout
		
;
		
		if (!this.runtime.running_layout)
			return;
			
		// Change to current layout - will restart the layout
		this.runtime.changelayout = this.runtime.running_layout;
		
		// Reset all group initial activations
		var i, len, g;
		for (i = 0, len = this.runtime.allGroups.length; i < len; i++)
		{
			g = this.runtime.allGroups[i];
			g.setGroupActive(g.initially_activated);
		}
	};
	
	SysActs.prototype.SnapshotCanvas = function (format_, quality_)
	{
		this.runtime.doCanvasSnapshot(format_ === 0 ? "image/png" : "image/jpeg", quality_ / 100);
	};
	
	SysActs.prototype.SetCanvasSize = function (w, h)
	{
		if (w <= 0 || h <= 0)
			return;
		
		var mode = this.runtime.fullscreen_mode;
		
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || !!document["msFullscreenElement"] || document["fullScreen"] || this.runtime.isNodeFullscreen);
		
		if (isfullscreen && this.runtime.fullscreen_scaling > 0)
			mode = this.runtime.fullscreen_scaling;
		
		if (mode === 0)
		{
			this.runtime["setSize"](w, h, true);
		}
		else
		{
			// fullscreen mode: effectively change the project 'window size' property
			this.runtime.original_width = w;
			this.runtime.original_height = h;
			this.runtime["setSize"](this.runtime.lastWindowWidth, this.runtime.lastWindowHeight, true);
		}
	};
	
	SysActs.prototype.SetLayoutEffectEnabled = function (enable_, effectname_)
	{
		if (!this.runtime.running_layout || !this.runtime.glwrap)
			return;
			
		var et = this.runtime.running_layout.getEffectByName(effectname_);
		
		if (!et)
			return;		// effect name not found
			
		var enable = (enable_ === 1);
		
		if (et.active == enable)
			return;		// no change
			
		et.active = enable;
		this.runtime.running_layout.updateActiveEffects();
		this.runtime.redraw = true;
	};
	
	SysActs.prototype.SetLayerEffectEnabled = function (layer, enable_, effectname_)
	{
		if (!layer || !this.runtime.glwrap)
			return;
			
		var et = layer.getEffectByName(effectname_);
		
		if (!et)
			return;		// effect name not found
			
		var enable = (enable_ === 1);
		
		if (et.active == enable)
			return;		// no change
			
		et.active = enable;
		layer.updateActiveEffects();
		this.runtime.redraw = true;
	};
	
	SysActs.prototype.SetLayoutEffectParam = function (effectname_, index_, value_)
	{
		if (!this.runtime.running_layout || !this.runtime.glwrap)
			return;
			
		var et = this.runtime.running_layout.getEffectByName(effectname_);
		
		if (!et)
			return;		// effect name not found
			
		var params = this.runtime.running_layout.effect_params[et.index];
			
		index_ = Math.floor(index_);
		
		if (index_ < 0 || index_ >= params.length)
			return;		// effect index out of bounds
			
		var paramType = this.runtime.glwrap.getProgramParameterType(et.shaderindex, index_);
		if (paramType === 1)		// percent param: divide by 100
			value_ /= 100.0;
		else if (paramType === 2)	// color param: break in to array
			value_ = [cr.GetRValue(value_), cr.GetGValue(value_), cr.GetBValue(value_)];
		
		if (params[index_] === value_)
			return;		// no change
			
		params[index_] = value_;
		
		if (et.active)
			this.runtime.redraw = true;
	};
	
	SysActs.prototype.SetLayerEffectParam = function (layer, effectname_, index_, value_)
	{
		if (!layer || !this.runtime.glwrap)
			return;
			
		var et = layer.getEffectByName(effectname_);
		
		if (!et)
			return;		// effect name not found
			
		var params = layer.effect_params[et.index];
			
		index_ = Math.floor(index_);
		
		if (index_ < 0 || index_ >= params.length)
			return;		// effect index out of bounds
			
		var paramType = this.runtime.glwrap.getProgramParameterType(et.shaderindex, index_);
		if (paramType === 1)		// percent param: divide by 100
			value_ /= 100.0;
		else if (paramType === 2)	// color param: break in to array
			value_ = [cr.GetRValue(value_), cr.GetGValue(value_), cr.GetBValue(value_)];
		
		if (params[index_] === value_)
			return;		// no change
			
		params[index_] = value_;
		
		if (et.active)
			this.runtime.redraw = true;
	};
	
	SysActs.prototype.SaveState = function (slot_)
	{
		this.runtime.saveToSlot = slot_;
	};
	
	SysActs.prototype.LoadState = function (slot_)
	{
		this.runtime.loadFromSlot = slot_;
	};
	
	SysActs.prototype.LoadStateJSON = function (jsonstr_)
	{
		this.runtime.loadFromJson = jsonstr_;
	};
	
	SysActs.prototype.SetHalfFramerateMode = function (set_)
	{
		this.runtime.halfFramerateMode = (set_ !== 0);
	};
	
	SysActs.prototype.SetFullscreenQuality = function (q)
	{
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || !!document["msFullscreenElement"] || document["fullScreen"] || this.isNodeFullscreen);
		
		if (!isfullscreen && this.runtime.fullscreen_mode === 0)
			return;
		
		this.runtime.wantFullscreenScalingQuality = (q !== 0);
		this.runtime["setSize"](this.runtime.lastWindowWidth, this.runtime.lastWindowHeight, true);
	};
	
	SysActs.prototype.ResetPersisted = function ()
	{
		var i, len;
		for (i = 0, len = this.runtime.layouts_by_index.length; i < len; ++i)
		{
			this.runtime.layouts_by_index[i].persist_data = {};
			this.runtime.layouts_by_index[i].first_visit = true;
		}
	};
	
	SysActs.prototype.RecreateInitialObjects = function (obj, x1, y1, x2, y2)
	{
		if (!obj)
			return;
		
		this.runtime.running_layout.recreateInitialObjects(obj, x1, y1, x2, y2);
	};
	
	SysActs.prototype.SetPixelRounding = function (m)
	{
		this.runtime.pixel_rounding = (m !== 0);
		this.runtime.redraw = true;
	};
	
	SysActs.prototype.SetMinimumFramerate = function (f)
	{
		if (f < 1)
			f = 1;
		if (f > 120)
			f = 120;
		
		this.runtime.minimumFramerate = f;
	};
	
	function SortZOrderList(a, b)
	{
		// Sort first by layer index
		var layerA = a[0];
		var layerB = b[0];
		var diff = layerA - layerB;
		
		if (diff !== 0)
			return diff;
		
		// Sort second by Z index if on the same layer
		var indexA = a[1];
		var indexB = b[1];
		return indexA - indexB;
	};
	
	function SortInstancesByValue(a, b)
	{
		return a[1] - b[1];
	};
	
	SysActs.prototype.SortZOrderByInstVar = function (obj, iv)
	{
		if (!obj)
			return;
		
		var i, len, inst, value, r, layer, toZ;
		
		var sol = obj.getCurrentSol();
		var pickedInstances = sol.getObjects();
		var zOrderList = [];
		var instValues = [];
		var layout = this.runtime.running_layout;
		var isFamily = obj.is_family;
		var familyIndex = obj.family_index;
		
		// Run through the SOL instances for this type. Note the order here is undefined and not necessarily in Z order.
		// Create two lists from the SOL: one of Z locations to fill as [layer, zindex], and the other of instances and their
		// instance variable value, as [inst, value].
		for (i = 0, len = pickedInstances.length; i < len; ++i)
		{
			inst = pickedInstances[i];
			
			if (!inst.layer)
				continue;		// not a world instance
			
			if (isFamily)
				value = inst.instance_vars[iv + inst.type.family_var_map[familyIndex]];
			else
				value = inst.instance_vars[iv];
			
			zOrderList.push([
				inst.layer.index,
				inst.get_zindex()
			]);
			
			instValues.push([
				inst,
				value
			]);
		}
		
		if (!zOrderList.length)
			return;				// no instances were world instances
		
		// Sort both the list of Z indices to fill (so it is in ascending order, not undefined SOL order),
		// and also sort the instances by their instance variable value. This makes both lists in ascending order.
		zOrderList.sort(SortZOrderList);
		instValues.sort(SortInstancesByValue);
		
		// Now write the list of instances sorted by their instance variable value to their corresponding Z orders.
		for (i = 0, len = zOrderList.length; i < len; ++i)
		{
			inst = instValues[i][0];					// instance in the order we want
			layer = layout.layers[zOrderList[i][0]];	// layer to put it on
			toZ = zOrderList[i][1];						// Z index on that layer to put it
			
			if (layer.instances[toZ] !== inst)			// not already got this instance there
			{
				layer.instances[toZ] = inst;			// update instance
				layer.setZIndicesStaleFrom(toZ);		// mark Z indices stale from this point since they have changed
			}
		}
	};
	
	sysProto.acts = new SysActs();

	//////////////////////////////
	// System expressions
    function SysExps() {};

    SysExps.prototype["int"] = function(ret, x)
    {
        if (cr.is_string(x))
        {
            ret.set_int(parseInt(x, 10));

            // Don't allow invalid conversions to return NaN
            if (isNaN(ret.data))
                ret.data = 0;
        }
        else
            ret.set_int(x);
    };

    SysExps.prototype["float"] = function(ret, x)
    {
        if (cr.is_string(x))
        {
            ret.set_float(parseFloat(x));

            // Don't allow invalid conversions to return NaN
            if (isNaN(ret.data))
                ret.data = 0;
        }
        else
            ret.set_float(x);
    };

    SysExps.prototype.str = function(ret, x)
    {
        if (cr.is_string(x))
            ret.set_string(x);
        else
            ret.set_string(x.toString());
    };

    SysExps.prototype.len = function(ret, x)
    {
        ret.set_int(x.length || 0);
    };

    SysExps.prototype.random = function (ret, a, b)
    {
        // b not provided: random number from 0 to a
        if (b === undefined)
        {
            ret.set_float(Math.random() * a);
        }
        else
        {
            // Return random number between a and b
            ret.set_float(Math.random() * (b - a) + a);
        }
    };

    SysExps.prototype.sqrt = function(ret, x)
    {
        ret.set_float(Math.sqrt(x));
    };

    SysExps.prototype.abs = function(ret, x)
    {
        ret.set_float(Math.abs(x));
    };

    SysExps.prototype.round = function(ret, x)
    {
        ret.set_int(Math.round(x));
    };

    SysExps.prototype.floor = function(ret, x)
    {
        ret.set_int(Math.floor(x));
    };

    SysExps.prototype.ceil = function(ret, x)
    {
        ret.set_int(Math.ceil(x));
	};
	
	SysExps.prototype.sign = function(ret, x)
    {
        ret.set_float(Math.sign(x));
    };

    SysExps.prototype.sin = function(ret, x)
    {
        ret.set_float(Math.sin(cr.to_radians(x)));
    };

    SysExps.prototype.cos = function(ret, x)
    {
        ret.set_float(Math.cos(cr.to_radians(x)));
    };

    SysExps.prototype.tan = function(ret, x)
    {
        ret.set_float(Math.tan(cr.to_radians(x)));
    };

    SysExps.prototype.asin = function(ret, x)
    {
        ret.set_float(cr.to_degrees(Math.asin(x)));
    };

    SysExps.prototype.acos = function(ret, x)
    {
        ret.set_float(cr.to_degrees(Math.acos(x)));
    };

    SysExps.prototype.atan = function(ret, x)
    {
        ret.set_float(cr.to_degrees(Math.atan(x)));
    };

    SysExps.prototype.exp = function(ret, x)
    {
        ret.set_float(Math.exp(x));
    };

    SysExps.prototype.ln = function(ret, x)
    {
        ret.set_float(Math.log(x));
    };

    SysExps.prototype.log10 = function(ret, x)
    {
        ret.set_float(Math.log(x) / Math.LN10);
    };

    SysExps.prototype.max = function(ret)
    {
		var max_ = arguments[1];
		
		if (typeof max_ !== "number")
			max_ = 0;
		
		var i, len, a;
		for (i = 2, len = arguments.length; i < len; i++)
		{
			a = arguments[i];
			
			if (typeof a !== "number")
				continue;		// ignore non-numeric types
			
			if (max_ < a)
				max_ = a;
		}
		
		ret.set_float(max_);
    };

    SysExps.prototype.min = function(ret)
    {
        var min_ = arguments[1];
		
		if (typeof min_ !== "number")
			min_ = 0;
		
		var i, len, a;
		for (i = 2, len = arguments.length; i < len; i++)
		{
			a = arguments[i];
			
			if (typeof a !== "number")
				continue;		// ignore non-numeric types
			
			if (min_ > a)
				min_ = a;
		}
		
		ret.set_float(min_);
    };

    SysExps.prototype.dt = function(ret)
    {
        ret.set_float(this.runtime.dt);
    };

    SysExps.prototype.timescale = function(ret)
    {
        ret.set_float(this.runtime.timescale);
    };

    SysExps.prototype.wallclocktime = function(ret)
    {
        ret.set_float((Date.now() - this.runtime.start_time) / 1000.0);
	};
	
	SysExps.prototype.unixtime = function(ret)
    {
        ret.set_float(Date.now());
    };

    SysExps.prototype.time = function(ret)
    {
        // Use the sum of dt's so far, so timescale is taken in to account
        ret.set_float(this.runtime.kahanTime.sum);
    };

    SysExps.prototype.tickcount = function(ret)
    {
        ret.set_int(this.runtime.tickcount);
    };

    SysExps.prototype.objectcount = function(ret)
    {
        ret.set_int(this.runtime.objectcount);
    };

    SysExps.prototype.fps = function(ret)
    {
        ret.set_int(this.runtime.fps);
    };

    SysExps.prototype.loopindex = function(ret, name_)
    {
		var loop, i, len;
		
        // No loop running: return 0
        if (!this.runtime.loop_stack.length)
        {
            ret.set_int(0);
            return;
        }

        // Name provided: find in loop stack
        if (name_)
        {
			// Note search from the top of the stack downwards, since if there are two nested loops both using the same name
			// (either by accident or e.g. due to function calls where there are two loops in use with index "i") then the
			// named loopindex expression should return the topmost loop's index only.
            for (i = this.runtime.loop_stack_index; i >= 0; --i)
            {
                loop = this.runtime.loop_stack[i];

                if (loop.name === name_)
                {
                    ret.set_int(loop.index);
                    return;
                }
            }

            // Not found
            ret.set_int(0);
        }
        // Name not provided: use top of loop stack
        else
        {
			loop = this.runtime.getCurrentLoop();
			ret.set_int(loop ? loop.index : -1);
        }
    };

    SysExps.prototype.distance = function(ret, x1, y1, x2, y2)
    {
        ret.set_float(cr.distanceTo(x1, y1, x2, y2));
    };

    SysExps.prototype.angle = function(ret, x1, y1, x2, y2)
    {
        ret.set_float(cr.to_degrees(cr.angleTo(x1, y1, x2, y2)));
    };

    SysExps.prototype.scrollx = function(ret)
    {
        ret.set_float(this.runtime.running_layout.scrollX);
    };

    SysExps.prototype.scrolly = function(ret)
    {
        ret.set_float(this.runtime.running_layout.scrollY);
    };

    SysExps.prototype.newline = function(ret)
    {
        ret.set_string("\n");
    };

    SysExps.prototype.lerp = function(ret, a, b, x)
    {
        ret.set_float(cr.lerp(a, b, x));
    };
	
	SysExps.prototype.qarp = function(ret, a, b, c, x)
    {
        ret.set_float(cr.qarp(a, b, c, x));
    };
	
	SysExps.prototype.cubic = function(ret, a, b, c, d, x)
    {
        ret.set_float(cr.cubic(a, b, c, d, x));
    };
	
	SysExps.prototype.cosp = function(ret, a, b, x)
    {
        ret.set_float(cr.cosp(a, b, x));
    };

    SysExps.prototype.windowwidth = function(ret)
    {
        ret.set_int(this.runtime.width);
    };

    SysExps.prototype.windowheight = function(ret)
    {
        ret.set_int(this.runtime.height);
    };
	
	SysExps.prototype.uppercase = function(ret, str)
	{
		ret.set_string(cr.is_string(str) ? str.toUpperCase() : "");
	};
	
	SysExps.prototype.lowercase = function(ret, str)
	{
		ret.set_string(cr.is_string(str) ? str.toLowerCase() : "");
	};
	
	SysExps.prototype.clamp = function(ret, x, l, u)
	{
		if (x < l)
			ret.set_float(l);
		else if (x > u)
			ret.set_float(u);
		else
			ret.set_float(x);
	};
	
	SysExps.prototype.layerscale = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_float(0);
		else
			ret.set_float(layer.scale);
	};
	
	SysExps.prototype.layeropacity = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_float(0);
		else
			ret.set_float(layer.opacity * 100);
	};
	
	SysExps.prototype.layerscalerate = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_float(0);
		else
			ret.set_float(layer.zoomRate);
	};
	
	SysExps.prototype.layerparallaxx = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_float(0);
		else
			ret.set_float(layer.parallaxX * 100);
	};
	
	SysExps.prototype.layerparallaxy = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_float(0);
		else
			ret.set_float(layer.parallaxY * 100);
	};
	
	SysExps.prototype.layerindex = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_int(-1);
		else
			ret.set_int(layer.index);
	};
	
	SysExps.prototype.layoutscale = function (ret)
	{
		if (this.runtime.running_layout)
			ret.set_float(this.runtime.running_layout.scale);
		else
			ret.set_float(0);
	};
	
	SysExps.prototype.layoutangle = function (ret)
	{
		ret.set_float(cr.to_degrees(this.runtime.running_layout.angle));
	};
	
	SysExps.prototype.layerangle = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);
		
		if (!layer)
			ret.set_float(0);
		else
			ret.set_float(cr.to_degrees(layer.angle));
	};
	
	SysExps.prototype.layoutwidth = function (ret)
	{
		ret.set_int(this.runtime.running_layout.width);
	};
	
	SysExps.prototype.layoutheight = function (ret)
	{
		ret.set_int(this.runtime.running_layout.height);
	};

	SysExps.prototype.find = function (ret, text, searchstr)
	{
		if (cr.is_string(text) && cr.is_string(searchstr))
			ret.set_int(text.search(new RegExp(cr.regexp_escape(searchstr), "i")));
		else
			ret.set_int(-1);
	};
	
	SysExps.prototype.findcase = function (ret, text, searchstr)
	{
		if (cr.is_string(text) && cr.is_string(searchstr))
			ret.set_int(text.search(new RegExp(cr.regexp_escape(searchstr), "")));
		else
			ret.set_int(-1);
	};
	
	SysExps.prototype.left = function (ret, text, n)
	{
		ret.set_string(cr.is_string(text) ? text.substr(0, n) : "");
	};
	
	SysExps.prototype.right = function (ret, text, n)
	{
		ret.set_string(cr.is_string(text) ? text.substr(text.length - n) : "");
	};
	
	SysExps.prototype.mid = function (ret, text, index_, length_)
	{
		ret.set_string(cr.is_string(text) ? text.substr(index_, length_) : "");
	};
	
	SysExps.prototype.tokenat = function (ret, text, index_, sep)
	{
		if (cr.is_string(text) && cr.is_string(sep))
		{
			var arr = text.split(sep);
			var i = cr.floor(index_);
			
			if (i < 0 || i >= arr.length)
				ret.set_string("");
			else
				ret.set_string(arr[i]);
		}
		else
			ret.set_string("");
	};
	
	SysExps.prototype.tokencount = function (ret, text, sep)
	{
		if (cr.is_string(text) && text.length)
			ret.set_int(text.split(sep).length);
		else
			ret.set_int(0);
	};
	
	SysExps.prototype.replace = function (ret, text, find_, replace_)
	{
		if (cr.is_string(text) && cr.is_string(find_) && cr.is_string(replace_))
			ret.set_string(text.replace(new RegExp(cr.regexp_escape(find_), "gi"), replace_));
		else
			ret.set_string(cr.is_string(text) ? text : "");
	};
	
	SysExps.prototype.trim = function (ret, text)
	{
		ret.set_string(cr.is_string(text) ? text.trim() : "");
	};
	
	SysExps.prototype.pi = function (ret)
	{
		ret.set_float(cr.PI);
	};
	
	SysExps.prototype.layoutname = function (ret)
	{
		if (this.runtime.running_layout)
			ret.set_string(this.runtime.running_layout.name);
		else
			ret.set_string("");
	};
	
	SysExps.prototype.renderer = function (ret)
	{
		ret.set_string(this.runtime.gl ? "webgl" : "canvas2d");
	};
	
	SysExps.prototype.rendererdetail = function (ret)
	{
		ret.set_string(this.runtime.glUnmaskedRenderer);
	};
	
	SysExps.prototype.anglediff = function (ret, a, b)
	{
		ret.set_float(cr.to_degrees(cr.angleDiff(cr.to_radians(a), cr.to_radians(b))));
	};
	
	SysExps.prototype.choose = function (ret)
	{
		var index = cr.floor(Math.random() * (arguments.length - 1));
		ret.set_any(arguments[index + 1]);
	};
	
	SysExps.prototype.rgb = function (ret, r, g, b)
	{
		ret.set_int(cr.RGB(r, g, b));
	};

	SysExps.prototype.rgbex = function (ret, r, g, b)
	{
		ret.set_float(cr.RGBEx(r / 100, g / 100, b / 100));
	};
	
	SysExps.prototype.projectversion = function (ret)
	{
		ret.set_string(this.runtime.versionstr);
	};
	
	SysExps.prototype.projectname = function (ret)
	{
		ret.set_string(this.runtime.name);
	};
	
	SysExps.prototype.anglelerp = function (ret, a, b, x)
	{
		a = cr.to_radians(a);
		b = cr.to_radians(b);
		var diff = cr.angleDiff(a, b);
		
		// b clockwise from a
		if (cr.angleClockwise(b, a))
		{
			ret.set_float(cr.to_clamped_degrees(a + diff * x));
		}
		// b anticlockwise from a
		else
		{
			ret.set_float(cr.to_clamped_degrees(a - diff * x));
		}
	};
	
	SysExps.prototype.anglerotate = function (ret, a, b, c)
	{
		a = cr.to_radians(a);
		b = cr.to_radians(b);
		c = cr.to_radians(c);
		
		ret.set_float(cr.to_clamped_degrees(cr.angleRotate(a, b, c)));
	};
	
	SysExps.prototype.zeropad = function (ret, n, d)
	{
		var s = (n < 0 ? "-" : "");
		if (n < 0) n = -n;
		var zeroes = d - n.toString().length;
		for (var i = 0; i < zeroes; i++)
			s += "0";
		ret.set_string(s + n.toString());
	};
	
	SysExps.prototype.cpuutilisation = function (ret)
	{
		ret.set_float(this.runtime.cpuutilisation / 1000);
	};
	
	SysExps.prototype.gpuutilisation = function (ret)
	{
		ret.set_float(this.runtime.gpuLastUtilisation);
	};
	
	SysExps.prototype.viewportleft = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);

		ret.set_float(layer ? layer.viewLeft : 0);
	};
	
	SysExps.prototype.viewporttop = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);

		ret.set_float(layer ? layer.viewTop : 0);
	};
	
	SysExps.prototype.viewportright = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);

		ret.set_float(layer ? layer.viewRight : 0);
	};
	
	SysExps.prototype.viewportbottom = function (ret, layerparam)
	{
		var layer = this.runtime.getLayer(layerparam);

		ret.set_float(layer ? layer.viewBottom : 0);
	};
	
	SysExps.prototype.loadingprogress = function (ret)
	{
		ret.set_float(this.runtime.loadingprogress);
	};
	
	SysExps.prototype.unlerp = function(ret, a, b, y)
    {
        ret.set_float(cr.unlerp(a, b, y));
    };
	
	SysExps.prototype.canvassnapshot = function (ret)
	{
		ret.set_string(this.runtime.snapshotData);
	};
	
	SysExps.prototype.urlencode = function (ret, s)
	{
		ret.set_string(encodeURIComponent(s));
	};
	
	SysExps.prototype.urldecode = function (ret, s)
	{
		ret.set_string(decodeURIComponent(s));
	};
	
	SysExps.prototype.canvastolayerx = function (ret, layerparam, x, y)
	{
		var layer = this.runtime.getLayer(layerparam);
		ret.set_float(layer ? layer.canvasToLayer(x, y, true) : 0);
	};
	
	SysExps.prototype.canvastolayery = function (ret, layerparam, x, y)
	{
		var layer = this.runtime.getLayer(layerparam);
		ret.set_float(layer ? layer.canvasToLayer(x, y, false) : 0);
	};
	
	SysExps.prototype.layertocanvasx = function (ret, layerparam, x, y)
	{
		var layer = this.runtime.getLayer(layerparam);
		ret.set_float(layer ? layer.layerToCanvas(x, y, true) : 0);
	};
	
	SysExps.prototype.layertocanvasy = function (ret, layerparam, x, y)
	{
		var layer = this.runtime.getLayer(layerparam);
		ret.set_float(layer ? layer.layerToCanvas(x, y, false) : 0);
	};
	
	SysExps.prototype.savestatejson = function (ret)
	{
		ret.set_string(this.runtime.lastSaveJson);
	};
	
	SysExps.prototype.imagememoryusage = function (ret)
	{
		if (this.runtime.glwrap)
			// round to 2 dp
			ret.set_float(Math.round(100 * this.runtime.glwrap.estimateVRAM() / (1024 * 1024)) / 100);
		else
			ret.set_float(0);
	};
	
	SysExps.prototype.regexsearch = function (ret, str_, regex_, flags_)
	{
		var regex = getRegex(regex_, flags_);
		
		ret.set_int(str_ ? str_.search(regex) : -1);
	};
	
	SysExps.prototype.regexreplace = function (ret, str_, regex_, flags_, replace_)
	{
		var regex = getRegex(regex_, flags_);
		
		ret.set_string(str_ ? str_.replace(regex, replace_) : "");
	};
	
	var regexMatches = [];
	var lastMatchesStr = "";
	var lastMatchesRegex = "";
	var lastMatchesFlags = "";
	
	function updateRegexMatches(str_, regex_, flags_)
	{
		// Same request as last time: skip running the match again
		if (str_ === lastMatchesStr && regex_ === lastMatchesRegex && flags_ === lastMatchesFlags)
			return;
		
		var regex = getRegex(regex_, flags_);
		regexMatches = str_.match(regex);
		
		lastMatchesStr = str_;
		lastMatchesRegex = regex_;
		lastMatchesFlags = flags_;
	};
	
	SysExps.prototype.regexmatchcount = function (ret, str_, regex_, flags_)
	{
		var regex = getRegex(regex_, flags_);
		updateRegexMatches(str_, regex_, flags_);
		ret.set_int(regexMatches ? regexMatches.length : 0);
	};
	
	SysExps.prototype.regexmatchat = function (ret, str_, regex_, flags_, index_)
	{
		index_ = Math.floor(index_);
		var regex = getRegex(regex_, flags_);
		updateRegexMatches(str_, regex_, flags_);
		
		if (!regexMatches || index_ < 0 || index_ >= regexMatches.length)
			ret.set_string("");
		else
			ret.set_string(regexMatches[index_]);
	};
	
	SysExps.prototype.infinity = function (ret)
	{
		ret.set_float(Infinity);
	};
	
	SysExps.prototype.setbit = function (ret, n, b, v)
	{
		// cast params to ints
		n = n | 0;
		b = b | 0;
		v = (v !== 0 ? 1 : 0);
		
		// return an int with the bit set
		ret.set_int((n & ~(1 << b)) | (v << b));
	};
	
	SysExps.prototype.togglebit = function (ret, n, b)
	{
		// cast params to ints
		n = n | 0;
		b = b | 0;
		
		// return an int with the bit toggled
		ret.set_int(n ^ (1 << b));
	};
	
	SysExps.prototype.getbit = function (ret, n, b)
	{
		// cast params to ints
		n = n | 0;
		b = b | 0;
		
		// return 0 or 1 depending on bit at that position
		ret.set_int((n & (1 << b)) ? 1 : 0);
	};
	
	SysExps.prototype.originalwindowwidth = function (ret)
	{
		ret.set_int(this.runtime.original_width);
	};
	
	SysExps.prototype.originalwindowheight = function (ret)
	{
		ret.set_int(this.runtime.original_height);
	};
	
	SysExps.prototype.originalviewportwidth = function (ret)
	{
		ret.set_int(this.runtime.original_width);
	};
	
	SysExps.prototype.originalviewportheight = function (ret)
	{
		ret.set_int(this.runtime.original_height);
	};
	
	sysProto.exps = new SysExps();
	
	sysProto.runWaits = function ()
	{
		var i, j, len, w, k, s, ss;
		var evinfo = this.runtime.getCurrentEventStack();
		
		for (i = 0, len = this.waits.length; i < len; i++)
		{
			w = this.waits[i];
			
			if (w.time === -1)		// signalled wait
			{
				if (!w.signalled)
					continue;		// not yet signalled
			}
			else					// timer wait
			{
				if (w.time > this.runtime.kahanTime.sum)
					continue;		// timer not yet expired
			}
			
			// Scheduled time has arrived. Restore event context
			evinfo.current_event = w.ev;
			evinfo.actindex = w.actindex;
			evinfo.cndindex = 0;
			
			for (k in w.sols)
			{
				if (w.sols.hasOwnProperty(k))
				{
					s = this.runtime.types_by_index[parseInt(k, 10)].getCurrentSol();
					ss = w.sols[k];
					s.select_all = ss.sa;
					cr.shallowAssignArray(s.instances, ss.insts);
					// this wait record is about to be released, so recycle the sol state now
					freeSolStateObject(ss);
				}
			}
			
			// Resume the event in this context
			w.ev.resume_actions_and_subevents();
			
			// Clean up the SOL we changed (subevents that ran will have cleaned themselves up)
			this.runtime.clearSol(w.solModifiers);
			w.deleteme = true;
		}
		
		// Remove expired entries
		for (i = 0, j = 0, len = this.waits.length; i < len; i++)
		{
			w = this.waits[i];
			this.waits[j] = w;
			
			if (w.deleteme)
				freeWaitObject(w);
			else
				j++;
		}
		
		cr.truncateArray(this.waits, j);
	};

}());


// commonace.js
// ECMAScript 5 strict mode

;

(function () {

	// Common ACE definitions
	cr.add_common_aces = function (m, pluginProto)
	{
		var singleglobal_ = m[1];
		//var is_world = m[2];
		var position_aces = m[3];
		var size_aces = m[4];
		var angle_aces = m[5];
		var appearance_aces = m[6];
		var zorder_aces = m[7];
		var effects_aces = m[8];
				
		if (!pluginProto.cnds)
			pluginProto.cnds = {};
		if (!pluginProto.acts)
			pluginProto.acts = {};
		if (!pluginProto.exps)
			pluginProto.exps = {};

		var cnds = pluginProto.cnds;
		var acts = pluginProto.acts;
		var exps = pluginProto.exps;

		if (position_aces)
		{
			cnds.CompareX = function (cmp, x)
			{
				return cr.do_cmp(this.x, cmp, x);
			};

			cnds.CompareY = function (cmp, y)
			{
				return cr.do_cmp(this.y, cmp, y);
			};

			cnds.IsOnScreen = function ()
			{
				var layer = this.layer;				
				this.update_bbox();
				var bbox = this.bbox;

				return !(bbox.right < layer.viewLeft || bbox.bottom < layer.viewTop || bbox.left > layer.viewRight || bbox.top > layer.viewBottom);
			};

			cnds.IsOutsideLayout = function ()
			{
				this.update_bbox();
				var bbox = this.bbox;
				var layout = this.runtime.running_layout;

				return (bbox.right < 0 || bbox.bottom < 0 || bbox.left > layout.width || bbox.top > layout.height);
			};
			
			// static condition
			cnds.PickDistance = function (which, x, y)
			{
				var sol = this.getCurrentSol();
				var instances = sol.getObjects();
				
				if (!instances.length)
					return false;
					
				var inst = instances[0];
				var pickme = inst;
				var dist = cr.distanceTo(inst.x, inst.y, x, y);
				
				var i, len, d;
				for (i = 1, len = instances.length; i < len; i++)
				{
					inst = instances[i];
					d = cr.distanceTo(inst.x, inst.y, x, y);
					
					if ((which === 0 && d < dist) || (which === 1 && d > dist))
					{
						dist = d;
						pickme = inst;
					}
				}
				
				// select the resulting instance
				sol.pick_one(pickme);
				return true;
			};

			acts.SetX = function (x)
			{
				if (this.x !== x)
				{
					this.x = x;
					this.set_bbox_changed();
				}
			};

			acts.SetY = function (y)
			{
				if (this.y !== y)
				{
					this.y = y;
					this.set_bbox_changed();
				}
			};

			acts.SetPos = function (x, y)
			{
				if (this.x !== x || this.y !== y)
				{
					this.x = x;
					this.y = y;
					this.set_bbox_changed();
				}
			};

			acts.SetPosToObject = function (obj, imgpt)
			{
				var inst = obj.getPairedInstance(this);

				if (!inst)
					return;
					
				var newx, newy;
					
				if (inst.getImagePoint)
				{
					newx = inst.getImagePoint(imgpt, true);
					newy = inst.getImagePoint(imgpt, false);
				}
				else
				{
					newx = inst.x;
					newy = inst.y;
				}
					
				if (this.x !== newx || this.y !== newy)
				{
					this.x = newx;
					this.y = newy;
					this.set_bbox_changed();
				}
			};

			acts.MoveForward = function (dist)
			{
				if (dist !== 0)
				{
					this.x += Math.cos(this.angle) * dist;
					this.y += Math.sin(this.angle) * dist;
					this.set_bbox_changed();
				}
			};

			acts.MoveAtAngle = function (a, dist)
			{
				if (dist !== 0)
				{
					this.x += Math.cos(cr.to_radians(a)) * dist;
					this.y += Math.sin(cr.to_radians(a)) * dist;
					this.set_bbox_changed();
				}
			};

			exps.X = function (ret)
			{
				ret.set_float(this.x);
			};

			exps.Y = function (ret)
			{
				ret.set_float(this.y);
			};

			// dt thrown in with position aces...
			exps.dt = function (ret)
			{
				ret.set_float(this.runtime.getDt(this));
			};
		}

		if (size_aces)
		{
			cnds.CompareWidth = function (cmp, w)
			{
				return cr.do_cmp(this.width, cmp, w);
			};

			cnds.CompareHeight = function (cmp, h)
			{
				return cr.do_cmp(this.height, cmp, h);
			};

			acts.SetWidth = function (w)
			{
				if (this.width !== w)
				{
					this.width = w;
					this.set_bbox_changed();
				}
			};

			acts.SetHeight = function (h)
			{
				if (this.height !== h)
				{
					this.height = h;
					this.set_bbox_changed();
				}
			};

			acts.SetSize = function (w, h)
			{
				if (this.width !== w || this.height !== h)
				{
					this.width = w;
					this.height = h;
					this.set_bbox_changed();
				}
			};

			exps.Width = function (ret)
			{
				ret.set_float(this.width);
			};

			exps.Height = function (ret)
			{
				ret.set_float(this.height);
			};
			
			exps.BBoxLeft = function (ret)
			{
				this.update_bbox();
				ret.set_float(this.bbox.left);
			};
			
			exps.BBoxTop = function (ret)
			{
				this.update_bbox();
				ret.set_float(this.bbox.top);
			};
			
			exps.BBoxRight = function (ret)
			{
				this.update_bbox();
				ret.set_float(this.bbox.right);
			};
			
			exps.BBoxBottom = function (ret)
			{
				this.update_bbox();
				ret.set_float(this.bbox.bottom);
			};
		}

		if (angle_aces)
		{
			cnds.AngleWithin = function (within, a)
			{
				return cr.angleDiff(this.angle, cr.to_radians(a)) <= cr.to_radians(within);
			};

			cnds.IsClockwiseFrom = function (a)
			{
				return cr.angleClockwise(this.angle, cr.to_radians(a));
			};
			
			cnds.IsBetweenAngles = function (a, b)
			{
				var lower = cr.to_clamped_radians(a);
				var upper = cr.to_clamped_radians(b);
				var angle = cr.clamp_angle(this.angle);
				var obtuse = (!cr.angleClockwise(upper, lower));
				
				// Handle differently when angle range is over 180 degrees, since angleClockwise only tests if within
				// 180 degrees clockwise of the angle
				if (obtuse)
					return !(!cr.angleClockwise(angle, lower) && cr.angleClockwise(angle, upper));
				else
					return cr.angleClockwise(angle, lower) && !cr.angleClockwise(angle, upper);
			};

			acts.SetAngle = function (a)
			{
				var newangle = cr.to_radians(cr.clamp_angle_degrees(a));

				if (isNaN(newangle))
					return;

				if (this.angle !== newangle)
				{
					this.angle = newangle;
					this.set_bbox_changed();
				}
			};

			acts.RotateClockwise = function (a)
			{
				if (a !== 0 && !isNaN(a))
				{
					this.angle += cr.to_radians(a);
					this.angle = cr.clamp_angle(this.angle);
					this.set_bbox_changed();
				}
			};

			acts.RotateCounterclockwise = function (a)
			{
				if (a !== 0 && !isNaN(a))
				{
					this.angle -= cr.to_radians(a);
					this.angle = cr.clamp_angle(this.angle);
					this.set_bbox_changed();
				}
			};

			acts.RotateTowardAngle = function (amt, target)
			{
				var newangle = cr.angleRotate(this.angle, cr.to_radians(target), cr.to_radians(amt));

				if (isNaN(newangle))
					return;

				if (this.angle !== newangle)
				{
					this.angle = newangle;
					this.set_bbox_changed();
				}
			};

			acts.RotateTowardPosition = function (amt, x, y)
			{
				var dx = x - this.x;
				var dy = y - this.y;
				var target = Math.atan2(dy, dx);
				var newangle = cr.angleRotate(this.angle, target, cr.to_radians(amt));

				if (isNaN(newangle))
					return;

				if (this.angle !== newangle)
				{
					this.angle = newangle;
					this.set_bbox_changed();
				}
			};

			acts.SetTowardPosition = function (x, y)
			{
				// Calculate angle towards position
				var dx = x - this.x;
				var dy = y - this.y;
				var newangle = Math.atan2(dy, dx);

				if (isNaN(newangle))
					return;

				if (this.angle !== newangle)
				{
					this.angle = newangle;
					this.set_bbox_changed();
				}
			};

			exps.Angle = function (ret)
			{
				ret.set_float(cr.to_clamped_degrees(this.angle));
			};
		}

		if (!singleglobal_)
		{
			cnds.CompareInstanceVar = function (iv, cmp, val)
			{
				return cr.do_cmp(this.instance_vars[iv], cmp, val);
			};

			cnds.IsBoolInstanceVarSet = function (iv)
			{
				return this.instance_vars[iv];
			};
			
			// static condition
			cnds.PickInstVarHiLow = function (which, iv)
			{
				var sol = this.getCurrentSol();
				var instances = sol.getObjects();
				
				if (!instances.length)
					return false;
					
				var inst = instances[0];
				var pickme = inst;
				var val = inst.instance_vars[iv];
				
				var i, len, v;
				for (i = 1, len = instances.length; i < len; i++)
				{
					inst = instances[i];
					v = inst.instance_vars[iv];
					
					if ((which === 0 && v < val) || (which === 1 && v > val))
					{
						val = v;
						pickme = inst;
					}
				}
				
				// select the resulting instance
				sol.pick_one(pickme);
				return true;
			};
			
			// static condition
			cnds.PickByUID = function (u)
			{
				var i, len, j, inst, families, instances, sol;
				var cnd = this.runtime.getCurrentCondition();
				
				// If inverted, reduce SOL to those instances not matching the UID
				if (cnd.inverted)
				{
					sol = this.getCurrentSol();
					
					if (sol.select_all)
					{
						sol.select_all = false;
						cr.clearArray(sol.instances);
						cr.clearArray(sol.else_instances);
						
						instances = this.instances;
						for (i = 0, len = instances.length; i < len; i++)
						{
							inst = instances[i];
							
							if (inst.uid === u)
								sol.else_instances.push(inst);
							else
								sol.instances.push(inst);
						}
						
						this.applySolToContainer();
						return !!sol.instances.length;
					}
					else
					{
						for (i = 0, j = 0, len = sol.instances.length; i < len; i++)
						{
							inst = sol.instances[i];
							sol.instances[j] = inst;
							
							if (inst.uid === u)
							{
								sol.else_instances.push(inst);
							}
							else
								j++;
						}
						
						cr.truncateArray(sol.instances, j);
						
						this.applySolToContainer();
						return !!sol.instances.length;
					}
				}
				else
				{
					// Not inverted (ordinary pick of single instance with matching UID)
					// Use the runtime's getObjectByUID() function to look up
					// efficiently, and also support pending creation objects.
					inst = this.runtime.getObjectByUID(u);
					
					if (!inst)
						return false;
						
					// Verify this instance is already picked. We should not be able to
					// pick instances already filtered out by prior conditions.
					sol = this.getCurrentSol();
					
					if (!sol.select_all && sol.instances.indexOf(inst) === -1)
						return false;		// not picked
					
					// If this type is a family, verify the inst belongs to this family.
					// Otherwise verify the inst is of the same type as this.
					if (this.is_family)
					{
						families = inst.type.families;
						
						for (i = 0, len = families.length; i < len; i++)
						{
							if (families[i] === this)
							{
								sol.pick_one(inst);
								this.applySolToContainer();
								return true;
							}
						}
					}
					else if (inst.type === this)
					{
						sol.pick_one(inst);
						this.applySolToContainer();
						return true;
					}
					
					// Instance is from wrong family or type
					return false;
				}
			};
			
			cnds.OnCreated = function ()
			{
				return true;
			};
			
			cnds.OnDestroyed = function ()
			{
				return true;
			};

			acts.SetInstanceVar = function (iv, val)
			{
				var myinstvars = this.instance_vars;
				
				// Keep the type of the instance var
				if (cr.is_number(myinstvars[iv]))
				{
					if (cr.is_number(val))
						myinstvars[iv] = val;
					else
						myinstvars[iv] = parseFloat(val);
				}
				else if (cr.is_string(myinstvars[iv]))
				{
					if (cr.is_string(val))
						myinstvars[iv] = val;
					else
						myinstvars[iv] = val.toString();
				}
				else
;
			};

			acts.AddInstanceVar = function (iv, val)
			{
				var myinstvars = this.instance_vars;
				
				// Keep the type of the instance var
				if (cr.is_number(myinstvars[iv]))
				{
					if (cr.is_number(val))
						myinstvars[iv] += val;
					else
						myinstvars[iv] += parseFloat(val);
				}
				else if (cr.is_string(myinstvars[iv]))
				{
					if (cr.is_string(val))
						myinstvars[iv] += val;
					else
						myinstvars[iv] += val.toString();
				}
				else
;
			};

			acts.SubInstanceVar = function (iv, val)
			{
				var myinstvars = this.instance_vars;
				
				// Keep the type of the instance var
				if (cr.is_number(myinstvars[iv]))
				{
					if (cr.is_number(val))
						myinstvars[iv] -= val;
					else
						myinstvars[iv] -= parseFloat(val);
				}
				else
;
			};

			acts.SetBoolInstanceVar = function (iv, val)
			{
				this.instance_vars[iv] = val ? 1 : 0;
			};

			acts.ToggleBoolInstanceVar = function (iv)
			{
				this.instance_vars[iv] = 1 - this.instance_vars[iv];
			};

			acts.Destroy = function ()
			{
				this.runtime.DestroyInstance(this);
			};
			
			if (!acts.LoadFromJsonString)
			{
				acts.LoadFromJsonString = function (str_)
				{
					var o, i, len, binst;
					
					try {
						o = JSON.parse(str_);
					}
					catch (e) {
						return;
					}
					
					this.runtime.loadInstanceFromJSON(this, o, true);
					
					if (this.afterLoad)
						this.afterLoad();
					
					if (this.behavior_insts)
					{
						for (i = 0, len = this.behavior_insts.length; i < len; ++i)
						{
							binst = this.behavior_insts[i];
							
							if (binst.afterLoad)
								binst.afterLoad();
						}
					}
				};
			}

			exps.Count = function (ret)
			{
				var count = ret.object_class.instances.length;
				
				// Include objects on creation row of same type
				var i, len, inst;
				for (i = 0, len = this.runtime.createRow.length; i < len; i++)
				{
					inst = this.runtime.createRow[i];
					
					if (ret.object_class.is_family)
					{
						if (inst.type.families.indexOf(ret.object_class) >= 0)
							count++;
					}
					else
					{
						if (inst.type === ret.object_class)
							count++;
					}
				}
				
				ret.set_int(count);
			};
			
			exps.PickedCount = function (ret)
			{
				ret.set_int(ret.object_class.getCurrentSol().getObjects().length);
			};
			
			exps.UID = function (ret)
			{
				ret.set_int(this.uid);
			};
			
			exps.IID = function (ret)
			{
				ret.set_int(this.get_iid());
			};
			
			// Don't override Array/Dictionary expressions (oops, shipped this in a beta so have to live with it)
			if (!exps.AsJSON)
			{
				exps.AsJSON = function (ret)
				{
					ret.set_string(JSON.stringify(this.runtime.saveInstanceToJSON(this, true)));
				};
			}
		}

		if (appearance_aces)
		{
			cnds.IsVisible = function ()
			{
				return this.visible;
			};

			acts.SetVisible = function (v)
			{
				if (!v !== !this.visible)
				{
					this.visible = !!v;
					this.runtime.redraw = true;
				}
			};

			cnds.CompareOpacity = function (cmp, x)
			{
				return cr.do_cmp(cr.round6dp(this.opacity * 100), cmp, x);
			};

			acts.SetOpacity = function (x)
			{
				var new_opacity = x / 100.0;

				if (new_opacity < 0)
					new_opacity = 0;
				else if (new_opacity > 1)
					new_opacity = 1;

				if (new_opacity !== this.opacity)
				{
					this.opacity = new_opacity;
					this.runtime.redraw = true;
				}
			};

			exps.Opacity = function (ret)
			{
				ret.set_float(cr.round6dp(this.opacity * 100.0));
			};
		}
		
		if (zorder_aces)
		{
			cnds.IsOnLayer = function (layer_)
			{
				if (!layer_)
					return false;
					
				return this.layer === layer_;
			};
			
			cnds.PickTopBottom = function (which_)
			{
				var sol = this.getCurrentSol();
				var instances = sol.getObjects();
				
				if (!instances.length)
					return false;
					
				var inst = instances[0];
				var pickme = inst;
				
				var i, len;
				for (i = 1, len = instances.length; i < len; i++)
				{
					inst = instances[i];
					
					// Testing if above
					if (which_ === 0)
					{
						if (inst.layer.index > pickme.layer.index || (inst.layer.index === pickme.layer.index && inst.get_zindex() > pickme.get_zindex()))
						{
							pickme = inst;
						}
					}
					// Testing if below
					else
					{
						if (inst.layer.index < pickme.layer.index || (inst.layer.index === pickme.layer.index && inst.get_zindex() < pickme.get_zindex()))
						{
							pickme = inst;
						}
					}
				}
				
				// select the resulting instance
				sol.pick_one(pickme);
				return true;
			};
			
			acts.MoveToTop = function ()
			{
				var layer = this.layer;
				var layer_instances = layer.instances;
				
				if (layer_instances.length && layer_instances[layer_instances.length - 1] === this)
					return;		// is already at top
				
				// remove and re-insert at top
				layer.removeFromInstanceList(this, false);
				layer.appendToInstanceList(this, false);
				this.runtime.redraw = true;
			};
			
			acts.MoveToBottom = function ()
			{
				var layer = this.layer;
				var layer_instances = layer.instances;
				
				if (layer_instances.length && layer_instances[0] === this)
					return;		// is already at bottom
				
				// remove and re-insert at bottom
				layer.removeFromInstanceList(this, false);
				layer.prependToInstanceList(this, false);
				this.runtime.redraw = true;
			};
			
			acts.MoveToLayer = function (layerMove)
			{
				// no layer or same layer: don't do anything
				if (!layerMove || layerMove == this.layer)
					return;
					
				// otherwise remove from current layer...
				this.layer.removeFromInstanceList(this, true);
				
				// ...and add to the top of the new layer (which can be done without making zindices stale)
				this.layer = layerMove;
				layerMove.appendToInstanceList(this, true);
				
				this.runtime.redraw = true;
			};
			
			acts.ZMoveToObject = function (where_, obj_)
			{
				var isafter = (where_ === 0);
				
				if (!obj_)
					return;
				
				var other = obj_.getFirstPicked(this);
				
				if (!other || other.uid === this.uid)
					return;
				
				// First move to same layer as other object if different
				if (this.layer.index !== other.layer.index)
				{
					this.layer.removeFromInstanceList(this, true);
					
					this.layer = other.layer;
					other.layer.appendToInstanceList(this, true);
				}
				
				this.layer.moveInstanceAdjacent(this, other, isafter);				
				this.runtime.redraw = true;
			};
			
			exps.LayerNumber = function (ret)
			{
				ret.set_int(this.layer.number);
			};
			
			exps.LayerName = function (ret)
			{
				ret.set_string(this.layer.name);
			};
			
			exps.ZIndex = function (ret)
			{
				ret.set_int(this.get_zindex());
			};
		}
		
		if (effects_aces)
		{
			acts.SetEffectEnabled = function (enable_, effectname_)
			{
				if (!this.runtime.glwrap)
					return;
					
				var i = this.type.getEffectIndexByName(effectname_);
				
				if (i < 0)
					return;		// effect name not found
					
				var enable = (enable_ === 1);
				
				if (this.active_effect_flags[i] === enable)
					return;		// no change
					
				this.active_effect_flags[i] = enable;
				this.updateActiveEffects();
				this.runtime.redraw = true;
			};
			
			acts.SetEffectParam = function (effectname_, index_, value_)
			{
				if (!this.runtime.glwrap)
					return;
					
				var i = this.type.getEffectIndexByName(effectname_);
				
				if (i < 0)
					return;		// effect name not found
				
				var et = this.type.effect_types[i];
				var params = this.effect_params[i];
				var r, g, b;
					
				index_ = Math.floor(index_);
				
				if (index_ < 0 || index_ >= params.length)
					return;		// effect index out of bounds
				
				var paramType = this.runtime.glwrap.getProgramParameterType(et.shaderindex, index_);
				if (paramType === 1)		// percent param: divide by 100
					value_ /= 100.0;
				
				// Update parameter
				if (paramType === 2)		// color param: break in to components
				{
					r = cr.GetRValue(value_);
					g = cr.GetGValue(value_);
					b = cr.GetBValue(value_);
					
					var arr = params[index_];
					if (arr[0] === r && arr[1] === g && arr[2] === b)
						return;		// no change
					
					arr[0] = r;
					arr[1] = g;
					arr[2] = b;
					
					if (et.active)
						this.runtime.redraw = true;
				}
				else
				{
					if (params[index_] === value_)
						return;		// no change
						
					params[index_] = value_;
					
					if (et.active)
						this.runtime.redraw = true;
				}
			};
		}
	};

	// For instances: mark bounding box stale
	cr.set_bbox_changed = function ()
	{
		this.bbox_changed = true;      		// will recreate next time box requested
		this.cell_changed = true;
		this.type.any_cell_changed = true;	// avoid unnecessary updateAllBBox() calls
		this.runtime.redraw = true;     	// assume runtime needs to redraw
		
		// call bbox changed callbacks
		var i, len, callbacks = this.bbox_changed_callbacks;
		for (i = 0, len = callbacks.length; i < len; ++i)
		{
			callbacks[i](this);
		}
		
		// If layer is using render cells, update the bounding box right away to
		// update its render cells. This appears to be faster than trying to maintain
		// a queue of objects to update later.
		if (this.layer.useRenderCells)
			this.update_bbox();
	};

	cr.add_bbox_changed_callback = function (f)
	{
		if (f)
		{
			this.bbox_changed_callbacks.push(f);
		}
	};

	// For instances: updates the bounding box (if it changed)
	cr.update_bbox = function ()
	{
		if (!this.bbox_changed)
			return;                 // bounding box not changed
		
		var bbox = this.bbox;
		var bquad = this.bquad;

		// Get unrotated box
		bbox.set(this.x, this.y, this.x + this.width, this.y + this.height);
		bbox.offset(-this.hotspotX * this.width, -this.hotspotY * this.height);

		// Not rotated
		if (!this.angle)
		{
			bquad.set_from_rect(bbox);    // make bounding quad from box
		}
		else
		{
			// Rotate to a quad and store bounding quad
			bbox.offset(-this.x, -this.y);       			// translate to origin
			bquad.set_from_rotated_rect(bbox, this.angle);	// rotate around origin
			bquad.offset(this.x, this.y);      				// translate back to original position

			// Generate bounding box from rotated quad
			bquad.bounding_box(bbox);
		}
		
		// Normalize bounding box in case of mirror/flip
		bbox.normalize();

		this.bbox_changed = false;  // bounding box up to date
		
		// Ensure render cell also up-to-date
		this.update_render_cell();
	};
	
	var tmprc = new cr.rect(0, 0, 0, 0);
	
	cr.update_render_cell = function ()
	{
		// ignore if layer not using render cells
		if (!this.layer.useRenderCells)
			return;
		
		var mygrid = this.layer.render_grid;
		var bbox = this.bbox;
		tmprc.set(mygrid.XToCell(bbox.left), mygrid.YToCell(bbox.top), mygrid.XToCell(bbox.right), mygrid.YToCell(bbox.bottom));
		
		// No change: no need to update anything
		if (this.rendercells.equals(tmprc))
			return;
		
		// Update in sparse grid
		if (this.rendercells.right < this.rendercells.left)
			mygrid.update(this, null, tmprc);		// first insertion with invalid rect: don't provide old range
		else
			mygrid.update(this, this.rendercells, tmprc);
		
		// Update my current collision cells
		this.rendercells.copy(tmprc);
		
		// Mark the render list as having changed
		this.layer.render_list_stale = true;
		
	};
	
	cr.update_collision_cell = function ()
	{		
		// Determine new set of collision cells and check if changed.
		// Note collcells is initialised to an invalid rect (0, 0, -1, -1)
		// In this case it's the first insertion, so don't provide the old range.
		if (!this.cell_changed || !this.collisionsEnabled)
			return;
		
		this.update_bbox();
		
		var mygrid = this.type.collision_grid;
		var bbox = this.bbox;
		tmprc.set(mygrid.XToCell(bbox.left), mygrid.YToCell(bbox.top), mygrid.XToCell(bbox.right), mygrid.YToCell(bbox.bottom));
		
		// No change: no need to update anything
		if (this.collcells.equals(tmprc))
			return;
		
		// Update in sparse grid
		if (this.collcells.right < this.collcells.left)
			mygrid.update(this, null, tmprc);		// first insertion with invalid rect: don't provide old range
		else
			mygrid.update(this, this.collcells, tmprc);
		
		// Update my current collision cells
		this.collcells.copy(tmprc);
		
		
		this.cell_changed = false;
	};

	// For instances: point in box test
	cr.inst_contains_pt = function (x, y)
	{
		
		// Reject via bounding box (fastest)
		if (!this.bbox.contains_pt(x, y))
			return false;
		// Reject via bounding quad (next fastest)
		if (!this.bquad.contains_pt(x, y))
			return false;
		
		// If has a tilemap, use a special runtime function to run the test
		if (this.tilemap_exists)
			return this.testPointOverlapTile(x, y);
			
		// Test via collision poly if present (slowest)
		if (this.collision_poly && !this.collision_poly.is_empty())
		{
			this.collision_poly.cache_poly(this.width, this.height, this.angle);
			return this.collision_poly.contains_pt(x - this.x, y - this.y);
		}
		// No poly - bounding quad contains point
		else
			return true;
	};

	cr.inst_get_iid = function ()
	{
		this.type.updateIIDs();
		return this.iid;
	};

	cr.inst_get_zindex = function ()
	{
		this.layer.updateZIndices();
		return this.zindex;
	};

	cr.inst_updateActiveEffects = function ()
	{
		cr.clearArray(this.active_effect_types);
			
		var i, len, et;
		var preserves_opaqueness = true;
		for (i = 0, len = this.active_effect_flags.length; i < len; i++)
		{
			if (this.active_effect_flags[i])
			{
				et = this.type.effect_types[i];
				this.active_effect_types.push(et);
				
				if (!et.preservesOpaqueness)
					preserves_opaqueness = false;
			}
		}
		
		this.uses_shaders = !!this.active_effect_types.length;
		this.shaders_preserve_opaqueness = preserves_opaqueness;
	};

	// For instances: toString overload
	cr.inst_toString = function ()
	{
		// e.g. "Inst2" or "Inst974"
		// Note: the UID is deliberatey not used here, since saving and loading
		// can change object UIDs. ObjectSets store objects by their toString conversion,
		// and if toString returns something including the UID which then changes, the
		// ObjectSets become corrupt. Instead, the PUID was invented (permanently unique
		// ID), which is not exposed or used anywhere except for the purposes of toString here.
		return "Inst" + this.puid;
	};

	// For types: gets the first picked instance
	cr.type_getFirstPicked = function (frominst)
	{
		// If a 'from' instance is provided, check if this type is in
		// a container with that instance. If so, pick the associated
		// container instance.
		if (frominst && frominst.is_contained && frominst.type != this)
		{
			var i, len, s;
			for (i = 0, len = frominst.siblings.length; i < len; i++)
			{
				s = frominst.siblings[i];
				
				if (s.type == this)
					return s;
			}
		}
		
		var instances = this.getCurrentSol().getObjects();

		if (instances.length)
			return instances[0];
		else
			return null;
	};

	cr.type_getPairedInstance = function (inst)
	{
		var instances = this.getCurrentSol().getObjects();
		
		if (instances.length)
			return instances[inst.get_iid() % instances.length];
		else
			return null;
	};

	cr.type_updateIIDs = function ()
	{
		if (!this.stale_iids || this.is_family)
			return;		// up to date or is family - don't want family to overwrite IIDs
			
		var i, len;
		for (i = 0, len = this.instances.length; i < len; i++)
			this.instances[i].iid = i;
		
		// Continue to assign IIDs even in to instances waiting on create row
		var next_iid = i;
		
		var createRow = this.runtime.createRow;
		
		for (i = 0, len = createRow.length; i < len; ++i)
		{
			if (createRow[i].type === this)
				createRow[i].iid = next_iid++;
		}
			
		this.stale_iids = false;
	};

	// also looks in createRow, not just type.instances
	cr.type_getInstanceByIID = function (i)
	{
		if (i < this.instances.length)
			return this.instances[i];
		
		// Look through creation row for additional instances
		i -= this.instances.length;
		
		var createRow = this.runtime.createRow;
		
		var j, lenj;
		for (j = 0, lenj = createRow.length; j < lenj; ++j)
		{
			if (createRow[j].type === this)
			{			
				if (i === 0)
					return createRow[j];
				
				--i;
			}
		}
		
;
		return null;
	};

	// For types: get current SOL
	cr.type_getCurrentSol = function ()
	{
		return this.solstack[this.cur_sol];
	};

	cr.type_pushCleanSol = function ()
	{
		this.cur_sol++;

		// Stack not yet big enough - create new SOL
		if (this.cur_sol === this.solstack.length)
		{
			this.solstack.push(new cr.selection(this));
		}
		else
		{
			this.solstack[this.cur_sol].select_all = true;  // else clear next SOL
			
			// Make sure any leftover else_instances are cleared; not always reset by next event
			cr.clearArray(this.solstack[this.cur_sol].else_instances);
		}
	};

	cr.type_pushCopySol = function ()
	{
		this.cur_sol++;

		// Stack not yet big enough - create new SOL
		if (this.cur_sol === this.solstack.length)
			this.solstack.push(new cr.selection(this));

		// Copy the previous sol in to the new sol
		var clonesol = this.solstack[this.cur_sol];
		var prevsol = this.solstack[this.cur_sol - 1];

		if (prevsol.select_all)
		{
			clonesol.select_all = true;
			
			// Make sure any leftover else_instances are cleared; not always reset by next event
			cr.clearArray(clonesol.else_instances);
		}
		else
		{
			clonesol.select_all = false;
			cr.shallowAssignArray(clonesol.instances, prevsol.instances);
			cr.shallowAssignArray(clonesol.else_instances, prevsol.else_instances);
		}
	};

	cr.type_popSol = function ()
	{
		// Simply decrement cur_sol - will start using previous SOL.
		// The SOL we left behind is left in undefined state, we just don't care about it.
		// It'll be overwritten next time it's used.
;
		this.cur_sol--;
	};

	cr.type_getBehaviorByName = function (behname)
	{
		var i, len, j, lenj, f, index = 0;
		
		// Check family behaviors first
		if (!this.is_family)
		{
			for (i = 0, len = this.families.length; i < len; i++)
			{
				f = this.families[i];
				
				for (j = 0, lenj = f.behaviors.length; j < lenj; j++)
				{
					if (behname === f.behaviors[j].name)
					{
						this.extra["lastBehIndex"] = index;
						return f.behaviors[j];
					}
						
					index++;
				}
			}
		}
		
		// Now check my behaviors
		for (i = 0, len = this.behaviors.length; i < len; i++) {
			if (behname === this.behaviors[i].name)
			{
				this.extra["lastBehIndex"] = index;
				return this.behaviors[i];
			}
				
			index++;
		}
		return null;
	};

	cr.type_getBehaviorIndexByName = function (behname)
	{
		var b = this.getBehaviorByName(behname);
		
		if (b)
			return this.extra["lastBehIndex"];
		else
			return -1;
	};

	cr.type_getEffectIndexByName = function (name_)
	{
		var i, len;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			if (this.effect_types[i].name === name_)
				return i;
		}
		
		return -1;
	};

	cr.type_applySolToContainer = function ()
	{
		if (!this.is_contained || this.is_family)
			return;
		
		var i, len, j, lenj, t, sol, sol2;
		this.updateIIDs();
		sol = this.getCurrentSol();
		var select_all = sol.select_all;
		var es = this.runtime.getCurrentEventStack();
		var orblock = es && es.current_event && es.current_event.orblock;
		
		for (i = 0, len = this.container.length; i < len; i++)
		{
			t = this.container[i];
			
			if (t === this)
				continue;
			
			t.updateIIDs();
				
			sol2 = t.getCurrentSol();
			sol2.select_all = select_all;
			
			if (!select_all)
			{
				cr.clearArray(sol2.instances);
				
				for (j = 0, lenj = sol.instances.length; j < lenj; ++j)
					sol2.instances[j] = t.getInstanceByIID(sol.instances[j].iid);
				
				if (orblock)
				{
					cr.clearArray(sol2.else_instances);
					
					for (j = 0, lenj = sol.else_instances.length; j < lenj; ++j)
						sol2.else_instances[j] = t.getInstanceByIID(sol.else_instances[j].iid);
				}
			}
		}
	};

	cr.type_toString = function ()
	{
		return "Type" + this.sid;
	};

	// For comparison conditions
	cr.do_cmp = function (x, cmp, y)
	{
		if (typeof x === "undefined" || typeof y === "undefined")
			return false;
			
		switch (cmp)
		{
			case 0:     // equal
				return x === y;
			case 1:     // not equal
				return x !== y;
			case 2:     // less
				return x < y;
			case 3:     // less/equal
				return x <= y;
			case 4:     // greater
				return x > y;
			case 5:     // greater/equal
				return x >= y;
			default:
;
				return false;
		}
	};

})();

cr.shaders = {};


// Sprite
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Plugin class
cr.plugins_.Sprite = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var pluginProto = cr.plugins_.Sprite.prototype;
		
	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;
	
	function frame_getDataUri()
	{
		if (this.datauri.length === 0)
		{		
			// Get Sprite image as data URI
			var tmpcanvas = document.createElement("canvas");
			tmpcanvas.width = this.width;
			tmpcanvas.height = this.height;
			var tmpctx = tmpcanvas.getContext("2d");
			
			if (this.spritesheeted)
			{
				tmpctx.drawImage(this.texture_img, this.offx, this.offy, this.width, this.height,
										 0, 0, this.width, this.height);
			}
			else
			{
				tmpctx.drawImage(this.texture_img, 0, 0, this.width, this.height);
			}
			
			this.datauri = tmpcanvas.toDataURL("image/png");
		}
		
		return this.datauri;
	};

	typeProto.onCreate = function()
	{
		if (this.is_family)
			return;
			
		var i, leni, j, lenj;
		var anim, frame, animobj, frameobj, wt, uv;
		
		this.all_frames = [];
		this.has_loaded_textures = false;
		
		// Load all animation frames
		for (i = 0, leni = this.animations.length; i < leni; i++)
		{
			anim = this.animations[i];
			animobj = {};
			animobj.name = anim[0];
			animobj.speed = anim[1];
			animobj.loop = anim[2];
			animobj.repeatcount = anim[3];
			animobj.repeatto = anim[4];
			animobj.pingpong = anim[5];
			animobj.sid = anim[6];
			animobj.frames = [];
			
			for (j = 0, lenj = anim[7].length; j < lenj; j++)
			{
				frame = anim[7][j];
				frameobj = {};
				frameobj.texture_file = frame[0];
				frameobj.texture_filesize = frame[1];
				frameobj.offx = frame[2];
				frameobj.offy = frame[3];
				frameobj.width = frame[4];
				frameobj.height = frame[5];
				frameobj.duration = frame[6];
				frameobj.hotspotX = frame[7];
				frameobj.hotspotY = frame[8];
				frameobj.image_points = frame[9];
				frameobj.poly_pts = frame[10];
				frameobj.pixelformat = frame[11];
				frameobj.spritesheeted = (frameobj.width !== 0);
				frameobj.datauri = "";		// generated on demand and cached
				frameobj.getDataUri = frame_getDataUri;
				
				uv = {};
				uv.left = 0;
				uv.top = 0;
				uv.right = 1;
				uv.bottom = 1;
				frameobj.sheetTex = uv;
				
				frameobj.webGL_texture = null;
				
				// Sprite sheets may mean multiple frames reference one image
				// Ensure image is not created in duplicate
				wt = this.runtime.findWaitingTexture(frame[0]);
				
				if (wt)
				{
					frameobj.texture_img = wt;
				}
				else
				{
					frameobj.texture_img = new Image();
					frameobj.texture_img.cr_src = frame[0];
					frameobj.texture_img.cr_filesize = frame[1];
					frameobj.texture_img.c2webGL_texture = null;
					
					// Tell runtime to wait on this texture
					this.runtime.waitForImageLoad(frameobj.texture_img, frame[0]);
				}
				
				cr.seal(frameobj);
				animobj.frames.push(frameobj);
				this.all_frames.push(frameobj);
			}
			
			cr.seal(animobj);
			this.animations[i] = animobj;		// swap array data for object
		}
	};
	
	typeProto.updateAllCurrentTexture = function ()
	{
		var i, len, inst;
		for (i = 0, len = this.instances.length; i < len; i++)
		{
			inst = this.instances[i];
			inst.curWebGLTexture = inst.curFrame.webGL_texture;
		}
	};
	
	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;
			
		var i, len, frame;
		
		// Release all animation frames
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			frame.texture_img.c2webGL_texture = null;
			frame.webGL_texture = null;
		}
		
		this.has_loaded_textures = false;
		
		this.updateAllCurrentTexture();
	};
	
	typeProto.onRestoreWebGLContext = function ()
	{
		// No need to create textures if no instances exist, will create on demand
		if (this.is_family || !this.instances.length)
			return;
			
		var i, len, frame;
		
		// Re-load all animation frames
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			
			frame.webGL_texture = this.runtime.glwrap.loadTexture(frame.texture_img, false, this.runtime.linearSampling, frame.pixelformat);
		}
		
		this.updateAllCurrentTexture();
	};
	
	typeProto.loadTextures = function ()
	{
		if (this.is_family || this.has_loaded_textures || !this.runtime.glwrap)
			return;
			
		var i, len, frame;
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			
			frame.webGL_texture = this.runtime.glwrap.loadTexture(frame.texture_img, false, this.runtime.linearSampling, frame.pixelformat);
		}
		
		this.has_loaded_textures = true;
	};
	
	typeProto.unloadTextures = function ()
	{
		// Don't release textures if any instances still exist, they are probably using them
		if (this.is_family || this.instances.length || !this.has_loaded_textures)
			return;
			
		var i, len, frame;
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			
			this.runtime.glwrap.deleteTexture(frame.webGL_texture);
			frame.webGL_texture = null;
		}
		
		this.has_loaded_textures = false;
	};
	
	var already_drawn_images = [];
	
	typeProto.preloadCanvas2D = function (ctx)
	{
		var i, len, frameimg;
		cr.clearArray(already_drawn_images);
		
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frameimg = this.all_frames[i].texture_img;
			
			if (already_drawn_images.indexOf(frameimg) !== -1)
					continue;
				
			// draw to preload, browser should lazy load the texture
			ctx.drawImage(frameimg, 0, 0);
			already_drawn_images.push(frameimg);
		}
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
		
		// Physics needs to see the collision poly before onCreate
		var poly_pts = this.type.animations[0].frames[0].poly_pts;
		
		if (this.recycled)
			this.collision_poly.set_pts(poly_pts);
		else
			this.collision_poly = new cr.CollisionPoly(poly_pts);
	};
	
	var instanceProto = pluginProto.Instance.prototype;

	instanceProto.onCreate = function()
	{
		this.visible = this.properties[0];
		this.isTicking = false;
		this.inAnimTrigger = false;
		this.collisionsEnabled = this.properties[3];
		
		this.cur_animation = this.getAnimationByName(this.properties[1]) || this.type.animations[0];
		this.cur_frame = this.properties[2];
		
		if (this.cur_frame < 0)
			this.cur_frame = 0;
		if (this.cur_frame >= this.cur_animation.frames.length)
			this.cur_frame = this.cur_animation.frames.length - 1;
			
		// Update poly and hotspot for the starting frame.
		var curanimframe = this.cur_animation.frames[this.cur_frame];
		this.collision_poly.set_pts(curanimframe.poly_pts);
		this.hotspotX = curanimframe.hotspotX;
		this.hotspotY = curanimframe.hotspotY;
		
		this.animForwards = (this.cur_animation.speed >= 0);
		this.cur_anim_speed = Math.abs(this.cur_animation.speed);
		this.cur_anim_repeatto = this.cur_animation.repeatto;
		
		// Tick this object to change animation frame, but never tick single-animation, single-frame objects.
		// Also don't tick zero speed animations until the speed or animation is changed, which saves ticking
		// on tile sprites.
		if (!(this.type.animations.length === 1 && this.type.animations[0].frames.length === 1) && this.cur_anim_speed !== 0)
		{
			this.runtime.tickMe(this);
			this.isTicking = true;
		}
		
		if (this.recycled)
			this.animTimer.reset();
		else
			this.animTimer = new cr.KahanAdder();
		
		this.frameStart = this.getNowTime();
		this.animPlaying = true;
		this.animRepeats = 0;
		this.animTriggerName = "";
		
		this.changeAnimName = "";
		this.changeAnimFrom = 0;
		this.changeAnimFrame = -1;
		
		// Ensure type has textures loaded
		this.type.loadTextures();
		
		// Iterate all animations and frames ensuring WebGL textures are loaded and sizes are set
		var i, leni, j, lenj;
		var anim, frame, uv, maintex;
		
		for (i = 0, leni = this.type.animations.length; i < leni; i++)
		{
			anim = this.type.animations[i];
			
			for (j = 0, lenj = anim.frames.length; j < lenj; j++)
			{
				frame = anim.frames[j];
				
				// If size is zero, image is not on a sprite sheet.  Determine size now.
				if (frame.width === 0)
				{
					frame.width = frame.texture_img.width;
					frame.height = frame.texture_img.height;
				}
				
				// If frame is spritesheeted update its uv coords
				if (frame.spritesheeted)
				{
					maintex = frame.texture_img;
					uv = frame.sheetTex;
					uv.left = frame.offx / maintex.width;
					uv.top = frame.offy / maintex.height;
					uv.right = (frame.offx + frame.width) / maintex.width;
					uv.bottom = (frame.offy + frame.height) / maintex.height;

					// Check if frame is in fact a complete-frame spritesheet
					if (frame.offx === 0 && frame.offy === 0 && frame.width === maintex.width && frame.height === maintex.height)
					{
						frame.spritesheeted = false;
					}
				}
			}
		}
		
		this.curFrame = this.cur_animation.frames[this.cur_frame];
		this.curWebGLTexture = this.curFrame.webGL_texture;
	};
	
	instanceProto.saveToJSON = function ()
	{
		var o = {
			"a": this.cur_animation.sid,
			"f": this.cur_frame,
			"cas": this.cur_anim_speed,
			"fs": this.frameStart,
			"ar": this.animRepeats,
			"at": this.animTimer.sum,
			"rt": this.cur_anim_repeatto
		};
		
		if (!this.animPlaying)
			o["ap"] = this.animPlaying;
			
		if (!this.animForwards)
			o["af"] = this.animForwards;
		
		return o;
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		var anim = this.getAnimationBySid(o["a"]);
		
		if (anim)
			this.cur_animation = anim;
		
		this.cur_frame = o["f"];
		
		if (this.cur_frame < 0)
			this.cur_frame = 0;
		if (this.cur_frame >= this.cur_animation.frames.length)
			this.cur_frame = this.cur_animation.frames.length - 1;
		
		this.cur_anim_speed = o["cas"];
		this.frameStart = o["fs"];
		this.animRepeats = o["ar"];
		this.animTimer.reset();
		this.animTimer.sum = o["at"];
		this.animPlaying = o.hasOwnProperty("ap") ? o["ap"] : true;
		this.animForwards = o.hasOwnProperty("af") ? o["af"] : true;
		
		if (o.hasOwnProperty("rt"))
			this.cur_anim_repeatto = o["rt"];
		else
			this.cur_anim_repeatto = this.cur_animation.repeatto;
			
		this.curFrame = this.cur_animation.frames[this.cur_frame];
		this.curWebGLTexture = this.curFrame.webGL_texture;
		this.collision_poly.set_pts(this.curFrame.poly_pts);
		this.hotspotX = this.curFrame.hotspotX;
		this.hotspotY = this.curFrame.hotspotY;
	};
	
	instanceProto.animationFinish = function (reverse)
	{
		// stop
		this.cur_frame = reverse ? 0 : this.cur_animation.frames.length - 1;
		this.animPlaying = false;
		
		// trigger finish events
		this.animTriggerName = this.cur_animation.name;
		
		this.inAnimTrigger = true;
		this.runtime.trigger(cr.plugins_.Sprite.prototype.cnds.OnAnyAnimFinished, this);
		this.runtime.trigger(cr.plugins_.Sprite.prototype.cnds.OnAnimFinished, this);
		this.inAnimTrigger = false;
			
		this.animRepeats = 0;
	};
	
	instanceProto.getNowTime = function()
	{
		return this.animTimer.sum;
	};
	
	instanceProto.tick = function()
	{
		this.animTimer.add(this.runtime.getDt(this));
		
		// Change any animation or frame that was queued
		if (this.changeAnimName.length)
			this.doChangeAnim();
		if (this.changeAnimFrame >= 0)
			this.doChangeAnimFrame();
		
		var now = this.getNowTime();
		var cur_animation = this.cur_animation;
		var prev_frame = cur_animation.frames[this.cur_frame];
		var next_frame;
		var cur_frame_time = prev_frame.duration / this.cur_anim_speed;
		
		if (this.animPlaying && now >= this.frameStart + cur_frame_time)
		{			
			// Next frame
			if (this.animForwards)
			{
				this.cur_frame++;
				//log("Advancing animation frame forwards");
			}
			else
			{
				this.cur_frame--;
				//log("Advancing animation frame backwards");
			}
				
			this.frameStart += cur_frame_time;
			
			// Reached end of frames
			if (this.cur_frame >= cur_animation.frames.length)
			{
				//log("At end of frames");
				
				if (cur_animation.pingpong)
				{
					this.animForwards = false;
					this.cur_frame = cur_animation.frames.length - 2;
					//log("Ping pong looping from end");
				}
				// Looping: wind back to repeat-to frame
				else if (cur_animation.loop)
				{
					this.cur_frame = this.cur_anim_repeatto;
				}
				else
				{					
					this.animRepeats++;
					
					if (this.animRepeats >= cur_animation.repeatcount)
					{
						//log("Number of repeats reached; ending animation");
						
						this.animationFinish(false);
					}
					else
					{
						//log("Repeating");
						this.cur_frame = this.cur_anim_repeatto;
					}
				}
			}
			// Ping-ponged back to start
			if (this.cur_frame < 0)
			{
				if (cur_animation.pingpong)
				{
					this.cur_frame = 1;
					this.animForwards = true;
					//log("Ping ponging back forwards");
					
					if (!cur_animation.loop)
					{
						this.animRepeats++;
							
						if (this.animRepeats >= cur_animation.repeatcount)
						{
							//log("Number of repeats reached; ending animation");
							
							this.animationFinish(true);
						}
					}
				}
				// animation running backwards
				else
				{
					if (cur_animation.loop)
					{
						this.cur_frame = this.cur_anim_repeatto;
					}
					else
					{
						this.animRepeats++;
						
						// Reached number of repeats
						if (this.animRepeats >= cur_animation.repeatcount)
						{
							//log("Number of repeats reached; ending animation");
							
							this.animationFinish(true);
						}
						else
						{
							//log("Repeating");
							this.cur_frame = this.cur_anim_repeatto;
						}
					}
				}
			}
			
			// Don't go out of bounds
			if (this.cur_frame < 0)
				this.cur_frame = 0;
			else if (this.cur_frame >= cur_animation.frames.length)
				this.cur_frame = cur_animation.frames.length - 1;
				
			// If frameStart is still more than a whole frame away, we must've fallen behind.  Instead of
			// going catch-up (cycling one frame per tick), reset the frame timer to now.
			if (now > this.frameStart + (cur_animation.frames[this.cur_frame].duration / this.cur_anim_speed))
			{
				//log("Animation can't keep up, resetting timer");
				this.frameStart = now;
			}
				
			next_frame = cur_animation.frames[this.cur_frame];
			this.OnFrameChanged(prev_frame, next_frame);
				
			this.runtime.redraw = true;
		}
	};
	
	instanceProto.getAnimationByName = function (name_)
	{
		var i, len, a;
		for (i = 0, len = this.type.animations.length; i < len; i++)
		{
			a = this.type.animations[i];
			
			if (cr.equals_nocase(a.name, name_))
				return a;
		}
		
		return null;
	};
	
	instanceProto.getAnimationBySid = function (sid_)
	{
		var i, len, a;
		for (i = 0, len = this.type.animations.length; i < len; i++)
		{
			a = this.type.animations[i];
			
			if (a.sid === sid_)
				return a;
		}
		
		return null;
	};
	
	instanceProto.doChangeAnim = function ()
	{
		var prev_frame = this.cur_animation.frames[this.cur_frame];
		
		// Find the animation by name
		var anim = this.getAnimationByName(this.changeAnimName);
		
		this.changeAnimName = "";
		
		// couldn't find by name
		if (!anim)
			return;
			
		// don't change if setting same animation and the animation is already playing
		if (cr.equals_nocase(anim.name, this.cur_animation.name) && this.animPlaying)
			return;
			
		this.cur_animation = anim;
		this.animForwards = (anim.speed >= 0);
		this.cur_anim_speed = Math.abs(anim.speed);
		this.cur_anim_repeatto = anim.repeatto;
		
		if (this.cur_frame < 0)
			this.cur_frame = 0;
		if (this.cur_frame >= this.cur_animation.frames.length)
			this.cur_frame = this.cur_animation.frames.length - 1;
			
		// from beginning
		if (this.changeAnimFrom === 1)
			this.cur_frame = 0;
			
		this.animPlaying = true;
		this.frameStart = this.getNowTime();
		
		this.OnFrameChanged(prev_frame, this.cur_animation.frames[this.cur_frame]);
		
		this.runtime.redraw = true;
	};
	
	instanceProto.doChangeAnimFrame = function ()
	{
		var prev_frame = this.cur_animation.frames[this.cur_frame];
		var prev_frame_number = this.cur_frame;
		
		this.cur_frame = cr.floor(this.changeAnimFrame);
		
		if (this.cur_frame < 0)
			this.cur_frame = 0;
		if (this.cur_frame >= this.cur_animation.frames.length)
			this.cur_frame = this.cur_animation.frames.length - 1;
			
		if (prev_frame_number !== this.cur_frame)
		{
			this.OnFrameChanged(prev_frame, this.cur_animation.frames[this.cur_frame]);
			this.frameStart = this.getNowTime();
			this.runtime.redraw = true;
		}
		
		this.changeAnimFrame = -1;
	};
	
	instanceProto.OnFrameChanged = function (prev_frame, next_frame)
	{
		// Has the frame size changed?  Resize the object proportionally
		var oldw = prev_frame.width;
		var oldh = prev_frame.height;
		var neww = next_frame.width;
		var newh = next_frame.height;
		
		if (oldw != neww)
			this.width *= (neww / oldw);
		if (oldh != newh)
			this.height *= (newh / oldh);
			
		// Update hotspot, collision poly and bounding box
		this.hotspotX = next_frame.hotspotX;
		this.hotspotY = next_frame.hotspotY;
		this.collision_poly.set_pts(next_frame.poly_pts);
		this.set_bbox_changed();
		
		// Update webGL texture if any
		this.curFrame = next_frame;
		this.curWebGLTexture = next_frame.webGL_texture;
		
		// Notify behaviors
		var i, len, b;
		for (i = 0, len = this.behavior_insts.length; i < len; i++)
		{
			b = this.behavior_insts[i];
			
			if (b.onSpriteFrameChanged)
				b.onSpriteFrameChanged(prev_frame, next_frame);
		}
		
		// Trigger 'on frame changed'
		this.runtime.trigger(cr.plugins_.Sprite.prototype.cnds.OnFrameChanged, this);
	};

	instanceProto.draw = function(ctx)
	{
		ctx.globalAlpha = this.opacity;
			
		// The current animation frame to draw
		var cur_frame = this.curFrame;
		var spritesheeted = cur_frame.spritesheeted;
		var cur_image = cur_frame.texture_img;
		
		var myx = this.x;
		var myy = this.y;
		var w = this.width;
		var h = this.height;
		
		// Object not rotated: can draw without transformation.
		if (this.angle === 0 && w >= 0 && h >= 0)
		{
			myx -= this.hotspotX * w;
			myy -= this.hotspotY * h;
			
			if (this.runtime.pixel_rounding)
			{
				myx = Math.round(myx);
				myy = Math.round(myy);
			}
			
			if (spritesheeted)
			{
				ctx.drawImage(cur_image, cur_frame.offx, cur_frame.offy, cur_frame.width, cur_frame.height,
										 myx, myy, w, h);
			}
			else
			{
				ctx.drawImage(cur_image, myx, myy, w, h);
			}
		}
		else
		{
			// Only pixel round the x/y position, otherwise objects don't rotate smoothly
			if (this.runtime.pixel_rounding)
			{
				myx = Math.round(myx);
				myy = Math.round(myy);
			}
			
			// Angle applied; we need to transform the canvas.  Save state.
			ctx.save();
			
			var widthfactor = w > 0 ? 1 : -1;
			var heightfactor = h > 0 ? 1 : -1;
			
			// Translate to object's position, then rotate by its angle.
			ctx.translate(myx, myy);
			
			if (widthfactor !== 1 || heightfactor !== 1)
				ctx.scale(widthfactor, heightfactor);
			
			ctx.rotate(this.angle * widthfactor * heightfactor);
			
			var drawx = 0 - (this.hotspotX * cr.abs(w))
			var drawy = 0 - (this.hotspotY * cr.abs(h));
			
			// Draw the object; canvas origin is at hot spot.
			if (spritesheeted)
			{
				ctx.drawImage(cur_image, cur_frame.offx, cur_frame.offy, cur_frame.width, cur_frame.height,
										 drawx, drawy, cr.abs(w), cr.abs(h));
			}
			else
			{
				ctx.drawImage(cur_image, drawx, drawy, cr.abs(w), cr.abs(h));
			}
			
			// Restore previous state.
			ctx.restore();
		}
			
		//////////////////////////////////////////
		// Draw collision poly (for debug)
		/*
		ctx.strokeStyle = "#f00";
		ctx.lineWidth = 3;
		ctx.beginPath();
		this.collision_poly.cache_poly(this.width, this.height, this.angle);
		var i, len, ax, ay, bx, by;
		for (i = 0, len = this.collision_poly.pts_count; i < len; i++)
		{
			ax = this.collision_poly.pts_cache[i*2] + this.x;
			ay = this.collision_poly.pts_cache[i*2+1] + this.y;
			bx = this.collision_poly.pts_cache[((i+1)%len)*2] + this.x;
			by = this.collision_poly.pts_cache[((i+1)%len)*2+1] + this.y;
			
			ctx.moveTo(ax, ay);
			ctx.lineTo(bx, by);
		}
		
		ctx.stroke();
		ctx.closePath();
		*/
		// Draw physics polys (for debug)
		/*
		if (this.behavior_insts.length >= 1 && this.behavior_insts[0].draw)
		{
			this.behavior_insts[0].draw(ctx);
		}
		*/
		//////////////////////////////////////////
	};
	
	instanceProto.drawGL_earlyZPass = function(glw)
	{
		this.drawGL(glw);
	};
	
	instanceProto.drawGL = function(glw)
	{
		glw.setTexture(this.curWebGLTexture);
		glw.setOpacity(this.opacity);
		var cur_frame = this.curFrame;
		
		var q = this.bquad;
		
		if (this.runtime.pixel_rounding)
		{
			var ox = Math.round(this.x) - this.x;
			var oy = Math.round(this.y) - this.y;
			
			if (cur_frame.spritesheeted)
				glw.quadTex(q.tlx + ox, q.tly + oy, q.trx + ox, q.try_ + oy, q.brx + ox, q.bry + oy, q.blx + ox, q.bly + oy, cur_frame.sheetTex);
			else
				glw.quad(q.tlx + ox, q.tly + oy, q.trx + ox, q.try_ + oy, q.brx + ox, q.bry + oy, q.blx + ox, q.bly + oy);
		}
		else
		{
			if (cur_frame.spritesheeted)
				glw.quadTex(q.tlx, q.tly, q.trx, q.try_, q.brx, q.bry, q.blx, q.bly, cur_frame.sheetTex);
			else
				glw.quad(q.tlx, q.tly, q.trx, q.try_, q.brx, q.bry, q.blx, q.bly);
		}
	};
	
	instanceProto.getImagePointIndexByName = function(name_)
	{
		var cur_frame = this.curFrame;
		
		var i, len;
		for (i = 0, len = cur_frame.image_points.length; i < len; i++)
		{
			if (cr.equals_nocase(name_, cur_frame.image_points[i][0]))
				return i;
		}
		
		return -1;
	};
	
	instanceProto.getImagePoint = function(imgpt, getX)
	{
		var cur_frame = this.curFrame;
		var image_points = cur_frame.image_points;
		var index;
		
		if (cr.is_string(imgpt))
			index = this.getImagePointIndexByName(imgpt);
		else
			index = imgpt - 1;	// 0 is origin
			
		index = cr.floor(index);
		if (index < 0 || index >= image_points.length)
			return getX ? this.x : this.y;	// return origin
			
		// get position scaled and relative to origin in pixels
		var x = (image_points[index][1] - cur_frame.hotspotX) * this.width;
		var y = image_points[index][2];
		
		y = (y - cur_frame.hotspotY) * this.height;
		
		// rotate by object angle
		var cosa = Math.cos(this.angle);
		var sina = Math.sin(this.angle);
		var x_temp = (x * cosa) - (y * sina);
		y = (y * cosa) + (x * sina);
		x = x_temp;
		x += this.x;
		y += this.y;
		return getX ? x : y;
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};

	// For the collision memory in 'On collision'.
	var arrCache = [];
	
	function allocArr()
	{
		if (arrCache.length)
			return arrCache.pop();
		else
			return [0, 0, 0];
	};
	
	function freeArr(a)
	{
		a[0] = 0;
		a[1] = 0;
		a[2] = 0;
		arrCache.push(a);
	};
	
	function makeCollKey(a, b)
	{
		// comma separated string with lowest value first
		if (a < b)
			return "" + a + "," + b;
		else
			return "" + b + "," + a;
	};
	
	function collmemory_add(collmemory, a, b, tickcount)
	{
		var a_uid = a.uid;
		var b_uid = b.uid;

		var key = makeCollKey(a_uid, b_uid);
		
		if (collmemory.hasOwnProperty(key))
		{
			// added already; just update tickcount
			collmemory[key][2] = tickcount;
			return;
		}
		
		var arr = allocArr();
		arr[0] = a_uid;
		arr[1] = b_uid;
		arr[2] = tickcount;
		collmemory[key] = arr;
	};
	
	function collmemory_remove(collmemory, a, b)
	{
		var key = makeCollKey(a.uid, b.uid);
		
		if (collmemory.hasOwnProperty(key))
		{
			freeArr(collmemory[key]);
			delete collmemory[key];
		}
	};
	
	function collmemory_removeInstance(collmemory, inst)
	{
		var uid = inst.uid;
		var p, entry;
		for (p in collmemory)
		{
			if (collmemory.hasOwnProperty(p))
			{
				entry = collmemory[p];
				
				// Referenced in either UID: must be removed
				if (entry[0] === uid || entry[1] === uid)
				{
					freeArr(collmemory[p]);
					delete collmemory[p];
				}
			}
		}
	};
	
	var last_coll_tickcount = -2;
	
	function collmemory_has(collmemory, a, b)
	{
		var key = makeCollKey(a.uid, b.uid);
		
		if (collmemory.hasOwnProperty(key))
		{
			last_coll_tickcount = collmemory[key][2];
			return true;
		}
		else
		{
			last_coll_tickcount = -2;
			return false;
		}
	};
	
	var candidates1 = [];
	
	Cnds.prototype.OnCollision = function (rtype)
	{	
		if (!rtype)
			return false;
			
		var runtime = this.runtime;
			
		// Static condition: perform picking manually.
		// Get the current condition.  This is like the 'is overlapping' condition
		// but with a built in 'trigger once' for the l instances.
		var cnd = runtime.getCurrentCondition();
		var ltype = cnd.type;
		var collmemory = null;
		
		// Create the collision memory, which remembers pairs of collisions that
		// are already overlapping
		if (cnd.extra["collmemory"])
		{
			collmemory = cnd.extra["collmemory"];
		}
		else
		{
			collmemory = {};
			cnd.extra["collmemory"] = collmemory;
		}
		
		// Once per condition, add a destroy callback to remove destroyed instances from collision memory
		// which helps avoid a memory leak. Note the spriteCreatedDestroyCallback property is not saved
		// to savegames, so loading a savegame will still cause a callback to be created, as intended.
		if (!cnd.extra["spriteCreatedDestroyCallback"])
		{
			cnd.extra["spriteCreatedDestroyCallback"] = true;
			
			runtime.addDestroyCallback(function(inst) {
				collmemory_removeInstance(cnd.extra["collmemory"], inst);
			});
		}
		
		// Get the currently active SOLs for both objects involved in the overlap test
		var lsol = ltype.getCurrentSol();
		var rsol = rtype.getCurrentSol();
		var linstances = lsol.getObjects();
		var rinstances;
		
		// Iterate each combination of instances
		var l, linst, r, rinst;
		var curlsol, currsol;
		
		var tickcount = this.runtime.tickcount;
		var lasttickcount = tickcount - 1;
		var exists, run;
		
		var current_event = runtime.getCurrentEventStack().current_event;
		var orblock = current_event.orblock;
		
		// Note: don't cache lengths of linstances or rinstances. They can change if objects get destroyed in the event
		// retriggering.
		for (l = 0; l < linstances.length; l++)
		{
			linst = linstances[l];
			
			if (rsol.select_all)
			{
				linst.update_bbox();
				this.runtime.getCollisionCandidates(linst.layer, rtype, linst.bbox, candidates1);
				rinstances = candidates1;
			}
			else
				rinstances = rsol.getObjects();
			
			for (r = 0; r < rinstances.length; r++)
			{
				rinst = rinstances[r];
				
				if (runtime.testOverlap(linst, rinst) || runtime.checkRegisteredCollision(linst, rinst))
				{
					exists = collmemory_has(collmemory, linst, rinst);
					run = (!exists || (last_coll_tickcount < lasttickcount));
					
					// objects are still touching so update the tickcount
					collmemory_add(collmemory, linst, rinst, tickcount);
					
					if (run)
					{						
						runtime.pushCopySol(current_event.solModifiers);
						curlsol = ltype.getCurrentSol();
						currsol = rtype.getCurrentSol();
						curlsol.select_all = false;
						currsol.select_all = false;
						
						// If ltype === rtype, it's the same object (e.g. Sprite collides with Sprite)
						// In which case, pick both instances
						if (ltype === rtype)
						{
							curlsol.instances.length = 2;	// just use lsol, is same reference as rsol
							curlsol.instances[0] = linst;
							curlsol.instances[1] = rinst;
							ltype.applySolToContainer();
						}
						else
						{
							// Pick each instance in its respective SOL
							curlsol.instances.length = 1;
							currsol.instances.length = 1;
							curlsol.instances[0] = linst;
							currsol.instances[0] = rinst;
							ltype.applySolToContainer();
							rtype.applySolToContainer();
						}
						
						current_event.retrigger();
						runtime.popSol(current_event.solModifiers);
					}
				}
				else
				{
					// Pair not overlapping: ensure any record removed (mainly to save memory)
					collmemory_remove(collmemory, linst, rinst);
				}
			}
			
			cr.clearArray(candidates1);
		}
		
		// We've aleady run the event by now.
		return false;
	};
	
	var rpicktype = null;
	var rtopick = new cr.ObjectSet();
	var needscollisionfinish = false;
	
	var candidates2 = [];
	var temp_bbox = new cr.rect(0, 0, 0, 0);
	
	function DoOverlapCondition(rtype, offx, offy)
	{
		if (!rtype)
			return false;
			
		var do_offset = (offx !== 0 || offy !== 0);
		var oldx, oldy, ret = false, r, lenr, rinst;
		var cnd = this.runtime.getCurrentCondition();
		var ltype = cnd.type;
		var inverted = cnd.inverted;
		var rsol = rtype.getCurrentSol();
		var orblock = this.runtime.getCurrentEventStack().current_event.orblock;
		var rinstances;
		
		if (rsol.select_all)
		{
			this.update_bbox();
			
			// Make sure queried box is offset the same as the collision offset so we look in
			// the right cells
			temp_bbox.copy(this.bbox);
			temp_bbox.offset(offx, offy);
			this.runtime.getCollisionCandidates(this.layer, rtype, temp_bbox, candidates2);
			rinstances = candidates2;
		}
		else if (orblock)
		{
			// Normally the instances to process are in the else_instances array. However if a parent normal block
			// already picked from rtype, it will have select_all off, no else_instances, and just some content
			// in 'instances'. Look for this case in the first condition only.
			if (this.runtime.isCurrentConditionFirst() && !rsol.else_instances.length && rsol.instances.length)
				rinstances = rsol.instances;
			else
				rinstances = rsol.else_instances;
		}
		else
		{
			rinstances = rsol.instances;
		}
		
		rpicktype = rtype;
		needscollisionfinish = (ltype !== rtype && !inverted);
		
		if (do_offset)
		{
			oldx = this.x;
			oldy = this.y;
			this.x += offx;
			this.y += offy;
			this.set_bbox_changed();
		}
		
		for (r = 0, lenr = rinstances.length; r < lenr; r++)
		{
			rinst = rinstances[r];
			
			// objects overlap: true for this instance, ensure both are picked
			// (if ltype and rtype are same, e.g. "Sprite overlaps Sprite", don't pick the other instance,
			// it will be picked when it gets iterated to itself)
			if (this.runtime.testOverlap(this, rinst))
			{
				ret = true;
				
				// Inverted condition: just bail out now, don't pick right hand instance -
				// also note we still return true since the condition invert flag makes that false
				if (inverted)
					break;
					
				if (ltype !== rtype)
					rtopick.add(rinst);
			}
		}
		
		if (do_offset)
		{
			this.x = oldx;
			this.y = oldy;
			this.set_bbox_changed();
		}
		
		cr.clearArray(candidates2);
		return ret;
	};
	
	typeProto.finish = function (do_pick)
	{
		if (!needscollisionfinish)
			return;
		
		if (do_pick)
		{
			var orblock = this.runtime.getCurrentEventStack().current_event.orblock;
			var sol = rpicktype.getCurrentSol();
			var topick = rtopick.valuesRef();
			var i, len, inst;
			
			if (sol.select_all)
			{
				// All selected: filter down to just those in topick
				sol.select_all = false;
				cr.clearArray(sol.instances);
			
				for (i = 0, len = topick.length; i < len; ++i)
				{
					sol.instances[i] = topick[i];
				}
				
				// In OR blocks, else_instances must also be filled with objects not in topick
				if (orblock)
				{
					cr.clearArray(sol.else_instances);
					
					for (i = 0, len = rpicktype.instances.length; i < len; ++i)
					{
						inst = rpicktype.instances[i];
						
						if (!rtopick.contains(inst))
							sol.else_instances.push(inst);
					}
				}
			}
			else
			{
				if (orblock)
				{
					var initsize = sol.instances.length;
				
					for (i = 0, len = topick.length; i < len; ++i)
					{
						sol.instances[initsize + i] = topick[i];
						cr.arrayFindRemove(sol.else_instances, topick[i]);
					}
				}
				else
				{
					cr.shallowAssignArray(sol.instances, topick);
				}
			}
			
			rpicktype.applySolToContainer();
		}
		
		rtopick.clear();
		needscollisionfinish = false;
	};
	
	Cnds.prototype.IsOverlapping = function (rtype)
	{
		return DoOverlapCondition.call(this, rtype, 0, 0);
	};
	
	Cnds.prototype.IsOverlappingOffset = function (rtype, offx, offy)
	{
		return DoOverlapCondition.call(this, rtype, offx, offy);
	};
	
	Cnds.prototype.IsAnimPlaying = function (animname)
	{
		// If awaiting a change of animation to really happen next tick, compare to that now
		if (this.changeAnimName.length)
			return cr.equals_nocase(this.changeAnimName, animname);
		else
			return cr.equals_nocase(this.cur_animation.name, animname);
	};
	
	Cnds.prototype.CompareFrame = function (cmp, framenum)
	{
		return cr.do_cmp(this.cur_frame, cmp, framenum);
	};
	
	Cnds.prototype.CompareAnimSpeed = function (cmp, x)
	{
		var s = (this.animForwards ? this.cur_anim_speed : -this.cur_anim_speed);
		return cr.do_cmp(s, cmp, x);
	};
	
	Cnds.prototype.OnAnimFinished = function (animname)
	{
		return cr.equals_nocase(this.animTriggerName, animname);
	};
	
	Cnds.prototype.OnAnyAnimFinished = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnFrameChanged = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsMirrored = function ()
	{
		return this.width < 0;
	};
	
	Cnds.prototype.IsFlipped = function ()
	{
		return this.height < 0;
	};
	
	Cnds.prototype.OnURLLoaded = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsCollisionEnabled = function ()
	{
		return this.collisionsEnabled;
	};
	
	pluginProto.cnds = new Cnds();

	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.Spawn = function (obj, layer, imgpt)
	{
		if (!obj || !layer)
			return;
			
		var inst = this.runtime.createInstance(obj, layer, this.getImagePoint(imgpt, true), this.getImagePoint(imgpt, false));
		
		if (!inst)
			return;
		
		if (typeof inst.angle !== "undefined")
		{
			inst.angle = this.angle;
			inst.set_bbox_changed();
		}
		
		this.runtime.isInOnDestroy++;
		
		var i, len, s;
		this.runtime.trigger(Object.getPrototypeOf(obj.plugin).cnds.OnCreated, inst);
		
		if (inst.is_contained)
		{
			for (i = 0, len = inst.siblings.length; i < len; i++)
			{
				s = inst.siblings[i];
				this.runtime.trigger(Object.getPrototypeOf(s.type.plugin).cnds.OnCreated, s);
			}
		}
		
		this.runtime.isInOnDestroy--;
		
		// This action repeats for all picked instances.  We want to set the current
		// selection to all instances that are created by this action.  Therefore,
		// reset the SOL only for the first instance.  Determine this by the last tick count run.
		// HOWEVER loops and the 'on collision' event re-triggers events, re-running the action
		// with the same tickcount.  To get around this, triggers and re-triggering events increment
		// the 'execcount', so each execution of the action has a different execcount even if not
		// the same tickcount.
		var cur_act = this.runtime.getCurrentAction();
		var reset_sol = false;
		
		if (cr.is_undefined(cur_act.extra["Spawn_LastExec"]) || cur_act.extra["Spawn_LastExec"] < this.runtime.execcount)
		{
			reset_sol = true;
			cur_act.extra["Spawn_LastExec"] = this.runtime.execcount;
		}
		
		var sol;
		
		// Pick just this instance, as long as it's a different type (else the SOL instances array is
		// potentially modified while in use)
		if (obj != this.type)
		{
			sol = obj.getCurrentSol();
			sol.select_all = false;
			
			if (reset_sol)
			{
				cr.clearArray(sol.instances);
				sol.instances[0] = inst;
			}
			else
				sol.instances.push(inst);
				
			// Siblings aren't in instance lists yet, pick them manually
			if (inst.is_contained)
			{
				for (i = 0, len = inst.siblings.length; i < len; i++)
				{
					s = inst.siblings[i];
					sol = s.type.getCurrentSol();
					sol.select_all = false;
					
					if (reset_sol)
					{
						cr.clearArray(sol.instances);
						sol.instances[0] = s;
					}
					else
						sol.instances.push(s);
				}
			}
		}
	};
	
	Acts.prototype.SetEffect = function (effect)
	{
		this.blend_mode = effect;
		this.compositeOp = cr.effectToCompositeOp(effect);
		cr.setGLBlend(this, effect, this.runtime.gl);
		this.runtime.redraw = true;
	};
	
	Acts.prototype.StopAnim = function ()
	{
		this.animPlaying = false;
		//log("Stopping animation");
	};
	
	Acts.prototype.StartAnim = function (from)
	{
		this.animPlaying = true;
		this.frameStart = this.getNowTime();
		//log("Starting animation");
		
		// from beginning
		if (from === 1 && this.cur_frame !== 0)
		{
			this.changeAnimFrame = 0;
			
			if (!this.inAnimTrigger)
				this.doChangeAnimFrame();
		}
		
		// start ticking if not already
		if (!this.isTicking)
		{
			this.runtime.tickMe(this);
			this.isTicking = true;
		}
	};
	
	Acts.prototype.SetAnim = function (animname, from)
	{
		this.changeAnimName = animname;
		this.changeAnimFrom = from;
		
		// start ticking if not already
		if (!this.isTicking)
		{
			this.runtime.tickMe(this);
			this.isTicking = true;
		}
		
		// not in trigger: apply immediately
		if (!this.inAnimTrigger)
			this.doChangeAnim();
	};
	
	Acts.prototype.SetAnimFrame = function (framenumber)
	{
		this.changeAnimFrame = framenumber;
		
		// start ticking if not already
		if (!this.isTicking)
		{
			this.runtime.tickMe(this);
			this.isTicking = true;
		}
		
		// not in trigger: apply immediately
		if (!this.inAnimTrigger)
			this.doChangeAnimFrame();
	};
	
	Acts.prototype.SetAnimSpeed = function (s)
	{
		this.cur_anim_speed = cr.abs(s);
		this.animForwards = (s >= 0);
		
		//this.frameStart = this.runtime.kahanTime.sum;
		
		// start ticking if not already
		if (!this.isTicking)
		{
			this.runtime.tickMe(this);
			this.isTicking = true;
		}
	};
	
	Acts.prototype.SetAnimRepeatToFrame = function (s)
	{
		s = Math.floor(s);
		
		if (s < 0)
			s = 0;
		if (s >= this.cur_animation.frames.length)
			s = this.cur_animation.frames.length - 1;
		
		this.cur_anim_repeatto = s;
	};
	
	Acts.prototype.SetMirrored = function (m)
	{
		var neww = cr.abs(this.width) * (m === 0 ? -1 : 1);
		
		if (this.width === neww)
			return;
			
		this.width = neww;
		this.set_bbox_changed();
	};
	
	Acts.prototype.SetFlipped = function (f)
	{
		var newh = cr.abs(this.height) * (f === 0 ? -1 : 1);
		
		if (this.height === newh)
			return;
			
		this.height = newh;
		this.set_bbox_changed();
	};
	
	Acts.prototype.SetScale = function (s)
	{
		var cur_frame = this.curFrame;
		var mirror_factor = (this.width < 0 ? -1 : 1);
		var flip_factor = (this.height < 0 ? -1 : 1);
		var new_width = cur_frame.width * s * mirror_factor;
		var new_height = cur_frame.height * s * flip_factor;
		
		if (this.width !== new_width || this.height !== new_height)
		{
			this.width = new_width;
			this.height = new_height;
			this.set_bbox_changed();
		}
	};
	
	Acts.prototype.LoadURL = function (url_, resize_, crossOrigin_)
	{
		var img = new Image();
		var self = this;
		var curFrame_ = this.curFrame;
		
		img.onload = function ()
		{
			// If this action was used on multiple instances, they will each try to create a
			// separate image or texture, which is a waste of memory. So if the same image has
			// already been loaded, ignore this callback.
			if (curFrame_.texture_img.src === img.src)
			{
				// Still may need to switch to using the image's texture in WebGL renderer
				if (self.runtime.glwrap && self.curFrame === curFrame_)
					self.curWebGLTexture = curFrame_.webGL_texture;
				
				// Still may need to update object size
				if (resize_ === 0)		// resize to image size
				{
					self.width = img.width;
					self.height = img.height;
					self.set_bbox_changed();
				}
				
				// Still need to trigger 'On loaded'
				self.runtime.redraw = true;
				self.runtime.trigger(cr.plugins_.Sprite.prototype.cnds.OnURLLoaded, self);
			
				return;
			}
			
			curFrame_.texture_img = img;
			curFrame_.offx = 0;
			curFrame_.offy = 0;
			curFrame_.width = img.width;
			curFrame_.height = img.height;
			curFrame_.spritesheeted = false;
			curFrame_.datauri = "";
			curFrame_.pixelformat = 0;	// reset to RGBA, since we don't know what type of image will have come in
										// and it could be different to what the exporter set for the original image
			
			// WebGL renderer: need to create texture (canvas2D just draws with img directly)
			if (self.runtime.glwrap)
			{
				if (curFrame_.webGL_texture)
					self.runtime.glwrap.deleteTexture(curFrame_.webGL_texture);
					
				curFrame_.webGL_texture = self.runtime.glwrap.loadTexture(img, false, self.runtime.linearSampling);
				
				if (self.curFrame === curFrame_)
					self.curWebGLTexture = curFrame_.webGL_texture;
				
				// Need to update other instance's curWebGLTexture
				self.type.updateAllCurrentTexture();
			}
			
			// Set size if necessary
			if (resize_ === 0)		// resize to image size
			{
				self.width = img.width;
				self.height = img.height;
				self.set_bbox_changed();
			}
			
			self.runtime.redraw = true;
			self.runtime.trigger(cr.plugins_.Sprite.prototype.cnds.OnURLLoaded, self);
		};
		
		if (url_.substr(0, 5) !== "data:" && crossOrigin_ === 0)
			img["crossOrigin"] = "anonymous";
		
		// use runtime function to work around WKWebView permissions
		this.runtime.setImageSrc(img, url_);
	};
	
	Acts.prototype.SetCollisions = function (set_)
	{
		if (this.collisionsEnabled === (set_ !== 0))
			return;		// no change
		
		this.collisionsEnabled = (set_ !== 0);
		
		if (this.collisionsEnabled)
			this.set_bbox_changed();		// needs to be added back to cells
		else
		{
			// remove from any current cells and restore to uninitialised state
			if (this.collcells.right >= this.collcells.left)
				this.type.collision_grid.update(this, this.collcells, null);
			
			this.collcells.set(0, 0, -1, -1);
		}
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.AnimationFrame = function (ret)
	{
		ret.set_int(this.cur_frame);
	};
	
	Exps.prototype.AnimationFrameCount = function (ret)
	{
		ret.set_int(this.cur_animation.frames.length);
	};
	
	Exps.prototype.AnimationName = function (ret)
	{
		ret.set_string(this.cur_animation.name);
	};
	
	Exps.prototype.AnimationSpeed = function (ret)
	{
		ret.set_float(this.animForwards ? this.cur_anim_speed : -this.cur_anim_speed);
	};
	
	Exps.prototype.ImagePointX = function (ret, imgpt)
	{
		ret.set_float(this.getImagePoint(imgpt, true));
	};
	
	Exps.prototype.ImagePointY = function (ret, imgpt)
	{
		ret.set_float(this.getImagePoint(imgpt, false));
	};
	
	Exps.prototype.ImagePointCount = function (ret)
	{
		ret.set_int(this.curFrame.image_points.length);
	};
	
	Exps.prototype.ImageWidth = function (ret)
	{
		ret.set_float(this.curFrame.width);
	};
	
	Exps.prototype.ImageHeight = function (ret)
	{
		ret.set_float(this.curFrame.height);
	};
	
	pluginProto.exps = new Exps();

}());

// Mouse
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Plugin class
cr.plugins_.Mouse = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var pluginProto = cr.plugins_.Mouse.prototype;
		
	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
		
		this.buttonMap = new Array(4);		// mouse down states
		this.mouseXcanvas = 0;				// mouse position relative to canvas
		this.mouseYcanvas = 0;
		
		this.triggerButton = 0;
		this.triggerType = 0;
		this.triggerDir = 0;
		this.handled = false;
	};

	var instanceProto = pluginProto.Instance.prototype;

	instanceProto.onCreate = function()
	{
		var self = this;
		
		// Bind mouse events.
		document.addEventListener("mousemove", function(info) {
			self.onMouseMove(info);
		});
		
		document.addEventListener("mousedown", function(info) {
			self.onMouseDown(info);
		});
		
		document.addEventListener("mouseup", function(info) {
			self.onMouseUp(info);
		});
		
		document.addEventListener("dblclick", function(info) {
			self.onDoubleClick(info);
		});
		
		var wheelevent = function(info) {
							self.onWheel(info);
						};
						
		document.addEventListener("mousewheel", wheelevent, false);
		document.addEventListener("DOMMouseScroll", wheelevent, false);
	};
	
	instanceProto.onMouseMove = function(info)
	{
		this.mouseXcanvas = info.pageX - this.runtime.canvas.offsetLeft;
		this.mouseYcanvas = info.pageY - this.runtime.canvas.offsetTop;
	};
	
	instanceProto.mouseInGame = function ()
	{
		if (this.runtime.fullscreen_mode > 0)
			return true;
			
		return this.mouseXcanvas >= 0 && this.mouseYcanvas >= 0
		    && this.mouseXcanvas < this.runtime.width && this.mouseYcanvas < this.runtime.height;
	};

	instanceProto.onMouseDown = function(info)
	{
		// Ignore mousedowns outside the canvas
		if (!this.mouseInGame())
			return;
		
		// Update button state
		this.buttonMap[info.which] = true;
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger OnAnyClick
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnAnyClick, this);
		
		// Trigger OnClick & OnObjectClicked
		this.triggerButton = info.which - 1;	// 1-based
		this.triggerType = 0;					// single click
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnClick, this);
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnObjectClicked, this);
		
		this.runtime.isInUserInputEvent = false;
	};

	instanceProto.onMouseUp = function(info)
	{
		// Ignore mouseup if didn't see a corresponding mousedown
		if (!this.buttonMap[info.which])
			return;
		
		if (this.runtime.had_a_click && !this.runtime.isMobile)
			info.preventDefault();
			
		this.runtime.had_a_click = true;
		
		// Update button state
		this.buttonMap[info.which] = false;
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger OnRelease
		this.triggerButton = info.which - 1;	// 1-based
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnRelease, this);
		
		this.runtime.isInUserInputEvent = false;
	};

	instanceProto.onDoubleClick = function(info)
	{
		// Ignore doubleclicks outside the canvas
		if (!this.mouseInGame())
			return;
			
		info.preventDefault();
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger OnClick & OnObjectClicked
		this.triggerButton = info.which - 1;	// 1-based
		this.triggerType = 1;					// double click
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnClick, this);
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnObjectClicked, this);
		
		this.runtime.isInUserInputEvent = false;
	};
	
	instanceProto.onWheel = function (info)
	{
		var delta = info.wheelDelta ? info.wheelDelta : info.detail ? -info.detail : 0;
		
		this.triggerDir = (delta < 0 ? 0 : 1);
		this.handled = false;
		
		this.runtime.isInUserInputEvent = true;
		
		this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnWheel, this);
		
		this.runtime.isInUserInputEvent = false;
		
		if (this.handled && cr.isCanvasInputEvent(info))
			info.preventDefault();
	};
	
	instanceProto.onWindowBlur = function ()
	{
		// Fire "On button up" for any buttons held down, to prevent stuck buttons
		var i, len;
		for (i = 0, len = this.buttonMap.length; i < len; ++i)
		{
			// Not down
			if (!this.buttonMap[i])
				continue;
			
			// Update button state
			this.buttonMap[i] = false;
			
			// Trigger OnRelease
			this.triggerButton = i - 1;
			this.runtime.trigger(cr.plugins_.Mouse.prototype.cnds.OnRelease, this);
		}
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};

	Cnds.prototype.OnClick = function (button, type)
	{
		return button === this.triggerButton && type === this.triggerType;
	};
	
	Cnds.prototype.OnAnyClick = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsButtonDown = function (button)
	{
		return this.buttonMap[button + 1];
	};
	
	Cnds.prototype.OnRelease = function (button)
	{
		return button === this.triggerButton;
	};
	
	Cnds.prototype.IsOverObject = function (obj)
	{
		// We need to handle invert manually.  If inverted, turn invert off on the condition,
		// and instead pass it to testAndSelectCanvasPointOverlap() which does SOL picking
		// based on the invert status.
		var cnd = this.runtime.getCurrentCondition();

		var mx = this.mouseXcanvas;
		var my = this.mouseYcanvas;
		
		return cr.xor(this.runtime.testAndSelectCanvasPointOverlap(obj, mx, my, cnd.inverted), cnd.inverted);
	};
	
	Cnds.prototype.OnObjectClicked = function (button, type, obj)
	{
		if (button !== this.triggerButton || type !== this.triggerType)
			return false;	// wrong click type
		
		return this.runtime.testAndSelectCanvasPointOverlap(obj, this.mouseXcanvas, this.mouseYcanvas, false);
	};
	
	Cnds.prototype.OnWheel = function (dir)
	{
		this.handled = true;
		return dir === this.triggerDir;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	// Either string or sprite animation frame of last set cursor, to skip redundant settings
	var lastSetCursor = null;
	
	Acts.prototype.SetCursor = function (c)
	{
		var cursor_style = ["auto", "pointer", "text", "crosshair", "move", "help", "wait", "none"][c];
		
		if (lastSetCursor === cursor_style)
			return;		// redundant
		
		lastSetCursor = cursor_style;
		document.body.style.cursor = cursor_style;
	};
	
	Acts.prototype.SetCursorSprite = function (obj)
	{
		if (this.runtime.isMobile || !obj)
			return;
			
		var inst = obj.getFirstPicked();
		
		if (!inst || !inst.curFrame)
			return;
			
		var frame = inst.curFrame;
		
		if (lastSetCursor === frame)
			return;		// already set this frame
		
		lastSetCursor = frame;
		var datauri = frame.getDataUri();
		
		var cursor_style = "url(" + datauri + ") " + Math.round(frame.hotspotX * frame.width) + " " + Math.round(frame.hotspotY * frame.height) + ", auto";
		
		// Work around a bug in Blink: changing the cursor between different data URLs does not have any effect.
		// First clearing the cursor then setting it again causes the cursor to take effect.
		document.body.style.cursor = "";
		document.body.style.cursor = cursor_style;
	};
	
	pluginProto.acts = new Acts();

	//////////////////////////////////////
	// Expressions
	function Exps() {};

	Exps.prototype.X = function (ret, layerparam)
	{
		var layer, oldScale, oldZoomRate, oldParallaxX, oldAngle;
		
		if (cr.is_undefined(layerparam))
		{
			// calculate X position on bottom layer as if its scale were 1.0
			layer = this.runtime.getLayerByNumber(0);
			oldScale = layer.scale;
			oldZoomRate = layer.zoomRate;
			oldParallaxX = layer.parallaxX;
			oldAngle = layer.angle;
			layer.scale = 1;
			layer.zoomRate = 1.0;
			layer.parallaxX = 1.0;
			layer.angle = 0;
			ret.set_float(layer.canvasToLayer(this.mouseXcanvas, this.mouseYcanvas, true));
			layer.scale = oldScale;
			layer.zoomRate = oldZoomRate;
			layer.parallaxX = oldParallaxX;
			layer.angle = oldAngle;
		}
		else
		{
			// use given layer param
			if (cr.is_number(layerparam))
				layer = this.runtime.getLayerByNumber(layerparam);
			else
				layer = this.runtime.getLayerByName(layerparam);
				
			if (layer)
				ret.set_float(layer.canvasToLayer(this.mouseXcanvas, this.mouseYcanvas, true));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.Y = function (ret, layerparam)
	{
		var layer, oldScale, oldZoomRate, oldParallaxY, oldAngle;
		
		if (cr.is_undefined(layerparam))
		{
			// calculate X position on bottom layer as if its scale were 1.0
			layer = this.runtime.getLayerByNumber(0);
			oldScale = layer.scale;
			oldZoomRate = layer.zoomRate;
			oldParallaxY = layer.parallaxY;
			oldAngle = layer.angle;
			layer.scale = 1;
			layer.zoomRate = 1.0;
			layer.parallaxY = 1.0;
			layer.angle = 0;
			ret.set_float(layer.canvasToLayer(this.mouseXcanvas, this.mouseYcanvas, false));
			layer.scale = oldScale;
			layer.zoomRate = oldZoomRate;
			layer.parallaxY = oldParallaxY;
			layer.angle = oldAngle;
		}
		else
		{
			// use given layer param
			if (cr.is_number(layerparam))
				layer = this.runtime.getLayerByNumber(layerparam);
			else
				layer = this.runtime.getLayerByName(layerparam);
				
			if (layer)
				ret.set_float(layer.canvasToLayer(this.mouseXcanvas, this.mouseYcanvas, false));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.AbsoluteX = function (ret)
	{
		ret.set_float(this.mouseXcanvas);
	};
	
	Exps.prototype.AbsoluteY = function (ret)
	{
		ret.set_float(this.mouseYcanvas);
	};
	
	pluginProto.exps = new Exps();
	
}());


// Audio
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Plugin class
cr.plugins_.Audio = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var pluginProto = cr.plugins_.Audio.prototype;
		
	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
	};

	var audRuntime = null;
	var audInst = null;
	var audTag = "";
	var appPath = "";			// for Cordova only
	
	var API_HTML5 = 0;
	var API_WEBAUDIO = 1;
	var API_CORDOVA = 2;
	var api = API_HTML5;
	var context = null;
	var audioBuffers = [];		// cache of buffers
	var audioInstances = [];	// cache of instances
	var lastAudio = null;
	var timescale_mode = 0;
	var silent = false;
	var masterVolume = 1;
	var listenerX = 0;
	var listenerY = 0;
	var isContextSuspended = false;
	var supportsWebMOpus = !!(new Audio().canPlayType("audio/webm; codecs=opus"));
	
	// Web Audio API positioned audio settings
	var panningModel = 1;		// HRTF
	var distanceModel = 1;		// Inverse
	var refDistance = 10;
	var maxDistance = 10000;
	var rolloffFactor = 1;
	
	// Mic input from the User Media object
	var micSource = null;
	var micTag = "";
	
	// Workarounds for browser playback limitations
	var useNextTouchWorkaround = false;			// heuristic in case play() does not return a promise and we have to guess if the play was blocked
	var playOnNextInput = [];					// C2AudioInstances with HTMLAudioElements to play on next input event
	var playMusicAsSoundWorkaround = false;		// play music tracks with Web Audio API
	var hasAnySoftwareDecodedMusic = false;		// set true upon first time playing a music track using software WebM Opus decoder
	var hasPlayedDummyBuffer = false;			// dummy buffer played to unblock AudioContext on some platforms
	
	function addAudioToPlayOnNextInput(a)
	{
		var i = playOnNextInput.indexOf(a);
		
		if (i === -1)
			playOnNextInput.push(a);
	};
	
	function tryPlayAudioElement(a)
	{
		var audioElem = a.instanceObject;
		
		// Try to play the video immediately. On modern browsers, this returns a promise that rejects if the playback is not allowed
		// at this time. On older browsers it will not return a Promise so we have to fall back to heuristics.
		var playRet;
		try {
			playRet = audioElem.play();
		}
		catch (err) {
			// Synchronous exception in play() call: queue for next input event
			addAudioToPlayOnNextInput(a);
			return;
		}
		
		if (playRet)		// promise was returned
		{
			// Rejects if can't play at this time
			playRet.catch(function (err)
			{
				addAudioToPlayOnNextInput(a);
			});
		}
		// Did not return promise and the play() call was made outside of a user input event on platforms that block autoplay:
		// assume the call did not work; queue for playback on next touch
		else if (useNextTouchWorkaround && !audRuntime.isInUserInputEvent)
		{
			addAudioToPlayOnNextInput(a);
		}
	};
	
	function playQueuedAudio()
	{
		var i, len, m;
		
		// On first call, play a dummy buffer to unblock the Web Audio API
		if (!hasPlayedDummyBuffer && !isContextSuspended && context)
		{
			playDummyBuffer();
			hasPlayedDummyBuffer = true;
		}
		
		// If not in silent mode, make play() calls for any queued audio
		if (!silent)
		{
			for (i = 0, len = playOnNextInput.length; i < len; ++i)
			{
				m = playOnNextInput[i];
				
				if (!m.stopped && !m.is_paused)
					m.instanceObject.play();
			}
		}
		
		cr.clearArray(playOnNextInput);
	};
	
	function playDummyBuffer()
	{
		if (!context["createBuffer"])
			return;
		
		// play empty buffer to unmute audio
		var buffer = context["createBuffer"](1, 220, 22050);
		var source = context["createBufferSource"]();
		source["buffer"] = buffer;
		source["connect"](context["destination"]);
		startSource(source);
	};
	
	// Listen for input events on both mobile and desktop to play any queued music in
	document.addEventListener("touchend", playQueuedAudio, true);
	document.addEventListener("click", playQueuedAudio, true);
	document.addEventListener("keydown", playQueuedAudio, true);
	
	function dbToLinear(x)
	{
		var v = dbToLinear_nocap(x);
		
		if (!isFinite(v))	// accidentally passing a string can result in NaN; set volume to 0 if so
			v = 0;
		
		if (v < 0)
			v = 0;
		if (v > 1)
			v = 1;
		return v;
	};
	
	function linearToDb(x)
	{
		if (x < 0)
			x = 0;
		if (x > 1)
			x = 1;
		return linearToDb_nocap(x);
	};
	
	function dbToLinear_nocap(x)
	{
		return Math.pow(10, x / 20);
	};
	
	function linearToDb_nocap(x)
	{
		return (Math.log(x) / Math.log(10)) * 20;
	};
	
	// for web audio API effects: a map of tags to an array of effects to process in order
	// e.g. effects["mytag"] = [node, node, node...]
	var effects = {};
	
	// return first effect node to connect to, or the destination
	function getDestinationForTag(tag)
	{
		tag = tag.toLowerCase();
		
		if (effects.hasOwnProperty(tag))
		{
			if (effects[tag].length)
				return effects[tag][0].getInputNode();
		}
		
		return context["destination"];
	};
	
	// work around older web audio api versions
	function createGain()
	{
		if (context["createGain"])
			return context["createGain"]();
		else
			return context["createGainNode"]();
	};
	
	function createDelay(d)
	{
		if (context["createDelay"])
			return context["createDelay"](d);
		else
			return context["createDelayNode"](d);
	};
	
	function startSource(s, scheduledTime)
	{
		if (s["start"])
			s["start"](scheduledTime || 0);
		else
			s["noteOn"](scheduledTime || 0);
	};
	
	function startSourceAt(s, x, d, scheduledTime)
	{
		if (s["start"])
			s["start"](scheduledTime || 0, x);
		else
			s["noteGrainOn"](scheduledTime || 0, x, d - x);
	};
	
	function stopSource(s)
	{
		// work around a tizen bug where this sometimes mysteriously fails
		try {
			if (s["stop"])
				s["stop"](0);
			else
				s["noteOff"](0);
		}
		catch (e) {}
	};
	
	function setAudioParam(ap, value, ramp, time)
	{
		if (!ap)
			return;		// iOS is missing some parameters
			
		ap["cancelScheduledValues"](0);
		
		// set immediately if time is zero
		if (time === 0)
		{
			ap["value"] = value;
			return;
		}
		
		var curTime = context["currentTime"];
		time += curTime;
		
		// otherwise use the chosen ramp
		switch (ramp) {
		case 0:		// step
			ap["setValueAtTime"](value, time);
			break;
		case 1:		// linear
			ap["setValueAtTime"](ap["value"], curTime);		// to set what to ramp from
			ap["linearRampToValueAtTime"](value, time);
			break;
		case 2:		// exponential
			ap["setValueAtTime"](ap["value"], curTime);		// to set what to ramp from
			ap["exponentialRampToValueAtTime"](value, time);
			break;
		}
	};
	
	var filterTypes = ["lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "peaking", "notch", "allpass"];
	
	function FilterEffect(type, freq, detune, q, gain, mix)
	{
		this.type = "filter";
		this.params = [type, freq, detune, q, gain, mix];
		
		this.inputNode = createGain();
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix;
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - mix;
		
		// backwards-compat with older spec
		this.filterNode = context["createBiquadFilter"]();
		
		if (typeof this.filterNode["type"] === "number")
			this.filterNode["type"] = type;
		else
			this.filterNode["type"] = filterTypes[type];
		
		this.filterNode["frequency"]["value"] = freq;
		
		if (this.filterNode["detune"])		// iOS 6 doesn't have detune yet
			this.filterNode["detune"]["value"] = detune;
			
		this.filterNode["Q"]["value"] = q;
		this.filterNode["gain"]["value"] = gain;
		
		this.inputNode["connect"](this.filterNode);
		this.inputNode["connect"](this.dryNode);
		this.filterNode["connect"](this.wetNode);
	};
	
	FilterEffect.prototype.connectTo = function (node)
	{
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node);
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node);
	};
	
	FilterEffect.prototype.remove = function ()
	{
		this.inputNode["disconnect"]();
		this.filterNode["disconnect"]();
		this.wetNode["disconnect"]();
		this.dryNode["disconnect"]();
	};
	
	FilterEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	FilterEffect.prototype.setParam = function(param, value, ramp, time)
	{
		// this.params = [freq, detune, q, gain, mix];
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[5] = value;
			setAudioParam(this.wetNode["gain"], value, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - value, ramp, time);
			break;
		case 1:		// filter frequency
			this.params[1] = value;
			setAudioParam(this.filterNode["frequency"], value, ramp, time);
			break;
		case 2:		// filter detune
			this.params[2] = value;
			setAudioParam(this.filterNode["detune"], value, ramp, time);
			break;
		case 3:		// filter Q
			this.params[3] = value;
			setAudioParam(this.filterNode["Q"], value, ramp, time);
			break;
		case 4:		// filter/delay gain (note value is in dB here)
			this.params[4] = value;
			setAudioParam(this.filterNode["gain"], value, ramp, time);
			break;
		}
	};
	
	function DelayEffect(delayTime, delayGain, mix)
	{
		this.type = "delay";
		this.params = [delayTime, delayGain, mix];
		
		this.inputNode = createGain();
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix;
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - mix;
		
		// Use a gain node to route the audio in a loop around a delay and another gain node
		this.mainNode = createGain();
		
		this.delayNode = createDelay(delayTime);
		this.delayNode["delayTime"]["value"] = delayTime;
		
		this.delayGainNode = createGain();
		this.delayGainNode["gain"]["value"] = delayGain;
		
		this.inputNode["connect"](this.mainNode);
		this.inputNode["connect"](this.dryNode);
		this.mainNode["connect"](this.wetNode);
		this.mainNode["connect"](this.delayNode);
		this.delayNode["connect"](this.delayGainNode);
		this.delayGainNode["connect"](this.mainNode);
	};
	
	DelayEffect.prototype.connectTo = function (node)
	{
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node);
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node);
	};
	
	DelayEffect.prototype.remove = function ()
	{
		this.inputNode["disconnect"]();
		this.mainNode["disconnect"]();
		this.delayNode["disconnect"]();
		this.delayGainNode["disconnect"]();
		this.wetNode["disconnect"]();
		this.dryNode["disconnect"]();
	};
	
	DelayEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	DelayEffect.prototype.setParam = function(param, value, ramp, time)
	{
		//this.params = [delayTime, delayGain, mix];
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[2] = value;
			setAudioParam(this.wetNode["gain"], value, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - value, ramp, time);
			break;
		case 4:		// filter/delay gain (note value is passed in dB but needs to be linear here)
			this.params[1] = dbToLinear(value);
			setAudioParam(this.delayGainNode["gain"], dbToLinear(value), ramp, time);
			break;
		case 5:		// delay time
			this.params[0] = value;
			setAudioParam(this.delayNode["delayTime"], value, ramp, time);
			break;
		}
	};
	
	function ConvolveEffect(buffer, normalize, mix, src)
	{
		this.type = "convolve";
		this.params = [normalize, mix, src];

		this.inputNode = createGain();
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix;
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - mix;
		
		this.convolveNode = context["createConvolver"]();
		
		if (buffer)
		{
			this.convolveNode["normalize"] = normalize;
			this.convolveNode["buffer"] = buffer;
		}
		
		this.inputNode["connect"](this.convolveNode);
		this.inputNode["connect"](this.dryNode);
		this.convolveNode["connect"](this.wetNode);
	};
	
	ConvolveEffect.prototype.connectTo = function (node)
	{
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node);
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node);
	};
	
	ConvolveEffect.prototype.remove = function ()
	{
		this.inputNode["disconnect"]();
		this.convolveNode["disconnect"]();
		this.wetNode["disconnect"]();
		this.dryNode["disconnect"]();
	};
	
	ConvolveEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	ConvolveEffect.prototype.setParam = function(param, value, ramp, time)
	{
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[1] = value;
			setAudioParam(this.wetNode["gain"], value, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - value, ramp, time);
			break;
		}
	};
	
	function FlangerEffect(delay, modulation, freq, feedback, mix)
	{
		this.type = "flanger";
		this.params = [delay, modulation, freq, feedback, mix];
		
		// Note the flanger at 100% mix is actually shared 50/50 between the wet and dry nodes,
		// and at 0% mix is all dry. So wet->dry goes 100 -> 50 for dry node and 0-50 for wet node.
		this.inputNode = createGain();
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - (mix / 2);
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix / 2;
		this.feedbackNode = createGain();
		this.feedbackNode["gain"]["value"] = feedback;
		
		this.delayNode = createDelay(delay + modulation);
		this.delayNode["delayTime"]["value"] = delay;
		
		// use oscillator -> gain to LFO on the delay time
		this.oscNode = context["createOscillator"]();
		this.oscNode["frequency"]["value"] = freq;
		this.oscGainNode = createGain();
		this.oscGainNode["gain"]["value"] = modulation;
		
		this.inputNode["connect"](this.delayNode);
		this.inputNode["connect"](this.dryNode);
		this.delayNode["connect"](this.wetNode);
		this.delayNode["connect"](this.feedbackNode);
		this.feedbackNode["connect"](this.delayNode);
		this.oscNode["connect"](this.oscGainNode);
		this.oscGainNode["connect"](this.delayNode["delayTime"]);
		startSource(this.oscNode);
	};
	
	FlangerEffect.prototype.connectTo = function (node)
	{
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node);
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node);
	};
	
	FlangerEffect.prototype.remove = function ()
	{
		this.inputNode["disconnect"]();
		this.delayNode["disconnect"]();
		this.oscNode["disconnect"]();
		this.oscGainNode["disconnect"]();
		this.dryNode["disconnect"]();
		this.wetNode["disconnect"]();
		this.feedbackNode["disconnect"]();
	};
	
	FlangerEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	FlangerEffect.prototype.setParam = function(param, value, ramp, time)
	{
		// this.params = [delay, modulation, freq, feedback, mix];
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[4] = value;
			// note flanger goes to 50/50 mix for 100% wet
			setAudioParam(this.wetNode["gain"], value / 2, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - (value / 2), ramp, time);
			break;
		case 6:		// modulation
			this.params[1] = value / 1000;
			setAudioParam(this.oscGainNode["gain"], value / 1000, ramp, time);
			break;
		case 7:		// modulation frequency
			this.params[2] = value;
			setAudioParam(this.oscNode["frequency"], value, ramp, time);
			break;
		case 8:		// feedback
			this.params[3] = value / 100;
			setAudioParam(this.feedbackNode["gain"], value / 100, ramp, time);
			break;
		}
	};
	
	function PhaserEffect(freq, detune, q, modulation, modfreq, mix)
	{
		this.type = "phaser";
		this.params = [freq, detune, q, modulation, modfreq, mix];
		
		// Note the phaser at 100% mix is actually shared 50/50 between the wet and dry nodes,
		// and at 0% mix is all dry. So wet->dry goes 100 -> 50 for dry node and 0-50 for wet node.
		this.inputNode = createGain();
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - (mix / 2);
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix / 2;
		
		this.filterNode = context["createBiquadFilter"]();
		
		if (typeof this.filterNode["type"] === "number")
			this.filterNode["type"] = 7;	// all-pass
		else
			this.filterNode["type"] = "allpass";
	
		this.filterNode["frequency"]["value"] = freq;
		
		if (this.filterNode["detune"])		// iOS 6 doesn't have detune yet
			this.filterNode["detune"]["value"] = detune;
		
		this.filterNode["Q"]["value"] = q;
	
		// use oscillator -> gain to LFO on the frequency
		this.oscNode = context["createOscillator"]();
		this.oscNode["frequency"]["value"] = modfreq;
		this.oscGainNode = createGain();
		this.oscGainNode["gain"]["value"] = modulation;
		
		this.inputNode["connect"](this.filterNode);
		this.inputNode["connect"](this.dryNode);
		this.filterNode["connect"](this.wetNode);
		this.oscNode["connect"](this.oscGainNode);
		this.oscGainNode["connect"](this.filterNode["frequency"]);
		startSource(this.oscNode);
	};
	
	PhaserEffect.prototype.connectTo = function (node)
	{
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node);
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node);
	};
	
	PhaserEffect.prototype.remove = function ()
	{
		this.inputNode["disconnect"]();
		this.filterNode["disconnect"]();
		this.oscNode["disconnect"]();
		this.oscGainNode["disconnect"]();
		this.dryNode["disconnect"]();
		this.wetNode["disconnect"]();
	};
	
	PhaserEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	PhaserEffect.prototype.setParam = function(param, value, ramp, time)
	{
		// this.params = [freq, detune, q, modulation, modfreq, mix];
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[5] = value;
			// note phaser goes to 50/50 mix for 100% wet
			setAudioParam(this.wetNode["gain"], value / 2, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - (value / 2), ramp, time);
			break;
		case 1:		// filter frequency
			this.params[0] = value;
			setAudioParam(this.filterNode["frequency"], value, ramp, time);
			break;
		case 2:		// filter detune
			this.params[1] = value;
			setAudioParam(this.filterNode["detune"], value, ramp, time);
			break;
		case 3:		// filter Q
			this.params[2] = value;
			setAudioParam(this.filterNode["Q"], value, ramp, time);
			break;
		case 6:		// modulation
			this.params[3] = value;
			setAudioParam(this.oscGainNode["gain"], value, ramp, time);
			break;
		case 7:		// modulation frequency
			this.params[4] = value;
			setAudioParam(this.oscNode["frequency"], value, ramp, time);
			break;
		}
	};
	
	function GainEffect(g)
	{
		this.type = "gain";
		this.params = [g];
		
		this.node = createGain();
		this.node["gain"]["value"] = g;
	};
	
	GainEffect.prototype.connectTo = function (node_)
	{
		this.node["disconnect"]();
		this.node["connect"](node_);
	};
	
	GainEffect.prototype.remove = function ()
	{
		this.node["disconnect"]();
	};
	
	GainEffect.prototype.getInputNode = function ()
	{
		return this.node;
	};
	
	GainEffect.prototype.setParam = function(param, value, ramp, time)
	{
		switch (param) {
		case 4:		// gain
			this.params[0] = dbToLinear(value);
			setAudioParam(this.node["gain"], dbToLinear(value), ramp, time);
			break;
		}
	};
	
	function TremoloEffect(freq, mix)
	{
		this.type = "tremolo";
		this.params = [freq, mix];
		
		// note the mix goes from 0 (gain of 1, modulating by 0) to 1 (gain of 0.5, modulating by 0.5)
		this.node = createGain();
		this.node["gain"]["value"] = 1 - (mix / 2);
		
		// modulate the gain value with an oscillator
		this.oscNode = context["createOscillator"]();
		this.oscNode["frequency"]["value"] = freq;
		this.oscGainNode = createGain();
		this.oscGainNode["gain"]["value"] = mix / 2;
		
		this.oscNode["connect"](this.oscGainNode);
		this.oscGainNode["connect"](this.node["gain"]);
		startSource(this.oscNode);
	};
	
	TremoloEffect.prototype.connectTo = function (node_)
	{
		this.node["disconnect"]();
		this.node["connect"](node_);
	};
	
	TremoloEffect.prototype.remove = function ()
	{
		this.oscNode["disconnect"]();
		this.oscGainNode["disconnect"]();
		this.node["disconnect"]();
	};
	
	TremoloEffect.prototype.getInputNode = function ()
	{
		return this.node;
	};
	
	TremoloEffect.prototype.setParam = function(param, value, ramp, time)
	{
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[1] = value;
			setAudioParam(this.node["gain"]["value"], 1 - (value / 2), ramp, time);
			setAudioParam(this.oscGainNode["gain"]["value"], value / 2, ramp, time);
			break;
		case 7:		// modulation frequency
			this.params[0] = value;
			setAudioParam(this.oscNode["frequency"], value, ramp, time);
			break;
		}
	};
	
	function RingModulatorEffect(freq, mix)
	{
		this.type = "ringmod";
		this.params = [freq, mix];
		
		this.inputNode = createGain();
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix;
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - mix;
		this.ringNode = createGain();
		this.ringNode["gain"]["value"] = 0;
		
		this.oscNode = context["createOscillator"]();
		this.oscNode["frequency"]["value"] = freq;
		this.oscNode["connect"](this.ringNode["gain"]);
		startSource(this.oscNode);
		
		this.inputNode["connect"](this.ringNode);
		this.inputNode["connect"](this.dryNode);
		this.ringNode["connect"](this.wetNode);
	};
	
	RingModulatorEffect.prototype.connectTo = function (node_)
	{
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node_);
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node_);
	};
	
	RingModulatorEffect.prototype.remove = function ()
	{
		this.oscNode["disconnect"]();
		this.ringNode["disconnect"]();
		this.inputNode["disconnect"]();
		this.wetNode["disconnect"]();
		this.dryNode["disconnect"]();
	};
	
	RingModulatorEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	RingModulatorEffect.prototype.setParam = function(param, value, ramp, time)
	{
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[1] = value;
			setAudioParam(this.wetNode["gain"], value, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - value, ramp, time);
			break;
		case 7:		// modulation frequency
			this.params[0] = value;
			setAudioParam(this.oscNode["frequency"], value, ramp, time);
			break;
		}
	};
	
	// Distortion based on some code by Google Inc. under BSD license
	// http://webaudiodemos.appspot.com/input/js/waveshaper.js
	function DistortionEffect(threshold, headroom, drive, makeupgain, mix)
	{
		this.type = "distortion";
		this.params = [threshold, headroom, drive, makeupgain, mix];
		
		this.inputNode = createGain();
		this.preGain = createGain();
		this.postGain = createGain();
		this.setDrive(drive, dbToLinear_nocap(makeupgain));
		this.wetNode = createGain();
		this.wetNode["gain"]["value"] = mix;
		this.dryNode = createGain();
		this.dryNode["gain"]["value"] = 1 - mix;
		
		this.waveShaper = context["createWaveShaper"]();
		this.curve = new Float32Array(65536);
		this.generateColortouchCurve(threshold, headroom);
		this.waveShaper.curve = this.curve;
		
		this.inputNode["connect"](this.preGain);
		this.inputNode["connect"](this.dryNode);
		this.preGain["connect"](this.waveShaper);
		this.waveShaper["connect"](this.postGain);
		this.postGain["connect"](this.wetNode);
	};
	
	DistortionEffect.prototype.setDrive = function (drive, makeupgain)
	{
		if (drive < 0.01)
			drive = 0.01;
		
		this.preGain["gain"]["value"] = drive;
		this.postGain["gain"]["value"] = Math.pow(1 / drive, 0.6) * makeupgain;
	};
	
	function e4(x, k)
	{
		return 1.0 - Math.exp(-k * x);
	}
	
	DistortionEffect.prototype.shape = function (x, linearThreshold, linearHeadroom)
	{		
		var maximum = 1.05 * linearHeadroom * linearThreshold;
		var kk = (maximum - linearThreshold);
		
		var sign = x < 0 ? -1 : +1;
		var absx = x < 0 ? -x : x;
		
		var shapedInput = absx < linearThreshold ? absx : linearThreshold + kk * e4(absx - linearThreshold, 1.0 / kk);
		shapedInput *= sign;
		
		return shapedInput;
	};
	
	DistortionEffect.prototype.generateColortouchCurve = function (threshold, headroom)
	{
		var linearThreshold = dbToLinear_nocap(threshold);
		var linearHeadroom = dbToLinear_nocap(headroom);
		
		var n = 65536;
		var n2 = n / 2;
		var x = 0;
		
		for (var i = 0; i < n2; ++i) {
			x = i / n2;
			x = this.shape(x, linearThreshold, linearHeadroom);
			
			this.curve[n2 + i] = x;
			this.curve[n2 - i - 1] = -x;
		}
	};
	
	DistortionEffect.prototype.connectTo = function (node)
	{
		this.wetNode["disconnect"]();
		this.wetNode["connect"](node);
		this.dryNode["disconnect"]();
		this.dryNode["connect"](node);
	};
	
	DistortionEffect.prototype.remove = function ()
	{
		this.inputNode["disconnect"]();
		this.preGain["disconnect"]();
		this.waveShaper["disconnect"]();
		this.postGain["disconnect"]();
		this.wetNode["disconnect"]();
		this.dryNode["disconnect"]();
	};
	
	DistortionEffect.prototype.getInputNode = function ()
	{
		return this.inputNode;
	};
	
	DistortionEffect.prototype.setParam = function(param, value, ramp, time)
	{
		// this.params = [threshold, headroom, drive, makeupgain, mix];
		switch (param) {
		case 0:		// mix
			value = value / 100;
			if (value < 0) value = 0;
			if (value > 1) value = 1;
			this.params[4] = value;
			setAudioParam(this.wetNode["gain"], value, ramp, time);
			setAudioParam(this.dryNode["gain"], 1 - value, ramp, time);
			break;
		}
	};
	
	function CompressorEffect(threshold, knee, ratio, attack, release)
	{
		this.type = "compressor";
		this.params = [threshold, knee, ratio, attack, release];
		
		this.node = context["createDynamicsCompressor"]();
		
		try {
			this.node["threshold"]["value"] = threshold;
			this.node["knee"]["value"] = knee;
			this.node["ratio"]["value"] = ratio;
			this.node["attack"]["value"] = attack;
			this.node["release"]["value"] = release;
		}
		catch (e) {}
	};
	
	CompressorEffect.prototype.connectTo = function (node_)
	{
		this.node["disconnect"]();
		this.node["connect"](node_);
	};
	
	CompressorEffect.prototype.remove = function ()
	{
		this.node["disconnect"]();
	};
	
	CompressorEffect.prototype.getInputNode = function ()
	{
		return this.node;
	};
	
	CompressorEffect.prototype.setParam = function(param, value, ramp, time)
	{
		// not supported
	};
	
	function AnalyserEffect(fftSize, smoothing)
	{
		this.type = "analyser";
		this.params = [fftSize, smoothing];
		
		this.node = context["createAnalyser"]();
		this.node["fftSize"] = fftSize;
		this.node["smoothingTimeConstant"] = smoothing;
		
		this.freqBins = new Float32Array(this.node["frequencyBinCount"]);
		this.signal = new Uint8Array(fftSize);
		this.peak = 0;
		this.rms = 0;
	};
	
	AnalyserEffect.prototype.tick = function ()
	{
		this.node["getFloatFrequencyData"](this.freqBins);
		this.node["getByteTimeDomainData"](this.signal);
		
		var fftSize = this.node["fftSize"];
		var i = 0;
		this.peak = 0;
		var rmsSquaredSum = 0;
		var s = 0;
		
		for ( ; i < fftSize; i++)
		{
			// get signal as absolute value from 0 to 1 then convert to dB
			s = (this.signal[i] - 128) / 128;
			if (s < 0)
				s = -s;
			
			if (this.peak < s)
				this.peak = s;
			
			rmsSquaredSum += s * s;			
		}
		
		this.peak = linearToDb(this.peak);
		this.rms = linearToDb(Math.sqrt(rmsSquaredSum / fftSize));
	};
	
	AnalyserEffect.prototype.connectTo = function (node_)
	{
		this.node["disconnect"]();
		this.node["connect"](node_);
	};
	
	AnalyserEffect.prototype.remove = function ()
	{
		this.node["disconnect"]();
	};
	
	AnalyserEffect.prototype.getInputNode = function ()
	{
		return this.node;
	};
	
	AnalyserEffect.prototype.setParam = function(param, value, ramp, time)
	{
		// not supported
	};
	
	// ObjectTracker used to track velocities for doppler effects, but doppler effects were removed.
	// TODO: refactor away ObjectTracker entirely.
	function ObjectTracker()
	{
		this.obj = null;
		this.loadUid = 0;
	};
	
	ObjectTracker.prototype.setObject = function (obj_)
	{
		this.obj = obj_;
	};
	
	ObjectTracker.prototype.hasObject = function ()
	{
		return !!this.obj;
	};
	
	ObjectTracker.prototype.tick = function (dt)
	{
	};
	
	var iOShadtouchstart = false;	// has had touch start input on iOS <=8 to work around web audio API muting
	var iOShadtouchend = false;		// has had touch end input on iOS 9+ to work around web audio API muting
	
	function C2AudioBuffer(src_, type_, is_music)
	{
		this.src = src_;
		this.type = type_;
		this.myapi = api;
		this.is_music = is_music;
		this.added_end_listener = false;
		var self = this;
		this.outNode = null;
		this.mediaSourceNode = null;
		this.panWhenReady = [];		// for web audio API positioned sounds
		this.seekWhenReady = 0;
		this.pauseWhenReady = false;
		this.supportWebAudioAPI = false;
		this.failedToLoad = false;
		this.needsSoftwareDecode = (this.type === "audio/webm; codecs=opus" && !supportsWebMOpus);
		this.wasEverReady = false;	// if a buffer is ever marked as ready, it's permanently considered ready after then.
									// this works around browsers changing the 'readyState' back when it previously indicated
									// that it could play to the end.
		
		// Set the flag indicating software-decoded music is in use if necessary.
		if (this.is_music && this.needsSoftwareDecode)
			hasAnySoftwareDecodedMusic = true;
		
		// If using the Web Audio API, still play music as HTML5 audio since it streams.
		// Otherwise AJAXing the music will not play it until it is completely downloaded.
		// Note some platforms are really stupid and don't let us start HTML5 audio until
		// a touch event, so we might not play the HTML5 audio until then when useNextTouchWorkaround set.
		// Also avoid doing this if we want to play music as sound via Web Audio (e.g. WKWebView), or if we have to
		// software decode the sound with Web Audio (e.g. playing WebM Opus in browsers without native support).
		if (api === API_WEBAUDIO && is_music && !playMusicAsSoundWorkaround && !this.needsSoftwareDecode)
		{
			this.myapi = API_HTML5;
			
			// due to perculiarities in media source nodes in the Web Audio API, create
			// a gain node to connect out to other instances. Only when the audio is ready
			// do we then create a media source and then connect that to the gain node.
			this.outNode = createGain();
		}
		
		// may be null until Web Audio API ajax completes
		this.bufferObject = null;			// actual audio object
		this.audioData = null;				// web audio api: ajax request result (compressed audio that needs decoding)
		var request;
		
		switch (this.myapi) {
		case API_HTML5:
		
			this.bufferObject = new Audio();
			this.bufferObject.crossOrigin = "anonymous";
			
			this.bufferObject.addEventListener("canplaythrough", function () {
				self.wasEverReady = true;	// update loaded state so preload is considered complete
			});
			
			// when using MediaElementAudioSourceNode for Web Audio API,
			// connect it only on the "canplay" event, otherwise it doesn't seem to work properly
			// in Chrome. We can happily connect up outNode before this fires though.
			// Note: WKWebView treats as cross-origin, so we can't route it this way.
			if (api === API_WEBAUDIO && context["createMediaElementSource"] && !/wiiu/i.test(navigator.userAgent))
			{
				this.supportWebAudioAPI = true;		// can be routed through web audio api
				
				this.bufferObject.addEventListener("canplay", function ()
				{
					// protect against this event firing twice; also verify buffer wasn't released in time it took for "canplay" event to fire
					if (!self.mediaSourceNode && self.bufferObject)
					{
						self.mediaSourceNode = context["createMediaElementSource"](self.bufferObject);
						self.mediaSourceNode["connect"](self.outNode);
					}
				});
			}
			
			this.bufferObject.autoplay = false;	// this is only a source buffer, not an instance
			this.bufferObject.preload = "auto";
			this.bufferObject.src = src_;
			break;
			
		case API_WEBAUDIO:
			if (audRuntime.isWKWebView)
			{
				audRuntime.fetchLocalFileViaCordovaAsArrayBuffer(src_, function (arrayBuffer)
				{
					self.audioData = arrayBuffer;
					self.decodeAudioBuffer();
				}, function (err)
				{
					self.failedToLoad = true;
				});
			}
			else
			{
				request = new XMLHttpRequest();
				request.open("GET", src_, true);
				request.responseType = "arraybuffer";
				
				request.onload = function () {
					self.audioData = request.response;
					self.decodeAudioBuffer();
				};
				
				request.onerror = function () {
					self.failedToLoad = true;
				};
				
				request.send();
			}
			break;
			
		case API_CORDOVA:
			// Just refer to src instead
			this.bufferObject = true;
			break;
		}
	};
	
	C2AudioBuffer.prototype.release = function ()
	{
		// Remove any audio instances from this buffer
		var i, len, j, a;
		
		for (i = 0, j = 0, len = audioInstances.length; i < len; ++i)
		{
			a = audioInstances[i];
			audioInstances[j] = a;
			
			if (a.buffer === this)
				a.stop();
			else
				++j;		// keep
		}
		
		audioInstances.length = j;
		
		// clean up nodes created for media element playback
		if (this.mediaSourceNode)
		{
			this.mediaSourceNode["disconnect"]();
			this.mediaSourceNode = null;
		}
		
		if (this.outNode)
		{
			this.outNode["disconnect"]();
			this.outNode = null;
		}
		
		// release data for GC
		this.bufferObject = null;
		this.audioData = null;
	};
	
	C2AudioBuffer.prototype.decodeAudioBuffer = function ()
	{
		if (this.bufferObject || !this.audioData)
			return;		// audio already decoded or AJAX request not yet complete
		
		var self = this;
		
		// If we got given a WebM Opus file when the browser doesn't support it, invoke the ffmpeg decoder.
		if (this.needsSoftwareDecode)
		{
			var buffer = this.audioData;
			
			this.audioData = null;
			
			window["OpusDecoder"](buffer, function (err, rawAudio)
			{
				if (err)
				{
					self.onDecodeError(err);
					return;
				}
				
				var audioBuffer = context["createBuffer"](1, rawAudio.length, 48000);
				var channelBuffer = audioBuffer["getChannelData"](0);
				channelBuffer.set(rawAudio);
				
				self.onDecodeComplete(audioBuffer);
			});
		}
		else
		{
			context["decodeAudioData"](this.audioData, function (buffer)
			{
				self.onDecodeComplete(buffer);
				
			}, function (e) {
				self.onDecodeError(e);
			});
		}
	};
	
	C2AudioBuffer.prototype.onDecodeComplete = function (buffer)
	{
		this.bufferObject = buffer;
		this.audioData = null;		// clear AJAX response to allow GC and save memory, only need the bufferObject now
		var p, i, len, a;
		
		if (!cr.is_undefined(this.playTagWhenReady) && !silent)
		{								
			if (this.panWhenReady.length)
			{
				for (i = 0, len = this.panWhenReady.length; i < len; i++)
				{
					p = this.panWhenReady[i];
					
					a = new C2AudioInstance(this, p.thistag);
					a.setPannerEnabled(true);
					
					if (typeof p.objUid !== "undefined")
					{
						p.obj = audRuntime.getObjectByUID(p.objUid);
						if (!p.obj)
							continue;
					}
					
					if (p.obj)
					{
						var px = cr.rotatePtAround(p.obj.x, p.obj.y, -p.obj.layer.getAngle(), listenerX, listenerY, true);
						var py = cr.rotatePtAround(p.obj.x, p.obj.y, -p.obj.layer.getAngle(), listenerX, listenerY, false);
						a.setPan(px, py, cr.to_degrees(p.obj.angle - p.obj.layer.getAngle()), p.ia, p.oa, p.og);
						a.setObject(p.obj);
					}
					else
					{
						a.setPan(p.x, p.y, p.a, p.ia, p.oa, p.og);
					}
					
					a.play(this.loopWhenReady, this.volumeWhenReady, this.seekWhenReady);
					
					if (this.pauseWhenReady)
						a.pause();
					
					audioInstances.push(a);
				}
				
				cr.clearArray(this.panWhenReady);
			}
			else
			{
				a = new C2AudioInstance(this, this.playTagWhenReady || "");		// sometimes playTagWhenReady is not set - TODO: why?
				a.play(this.loopWhenReady, this.volumeWhenReady, this.seekWhenReady);
				
				if (this.pauseWhenReady)
					a.pause();
				
				audioInstances.push(a);
			}
		}
		else if (!cr.is_undefined(this.convolveWhenReady))
		{
			var convolveNode = this.convolveWhenReady.convolveNode;
			convolveNode["normalize"] = this.normalizeWhenReady;
			convolveNode["buffer"] = buffer;
		}
	};
	
	C2AudioBuffer.prototype.onDecodeError = function (err)
	{
		// error decoding audio buffer
		self.failedToLoad = true;
	};
	
	C2AudioBuffer.prototype.isLoaded = function ()
	{
		switch (this.myapi) {
		case API_HTML5:
			// Only indicates can play through to end, assume this is good enough to assume preloaded
			var ret = this.bufferObject["readyState"] >= 4;	// HAVE_ENOUGH_DATA
			
			// Some browsers (at least Firefox) change the readyState back lower even after it reaches HAVE_ENOUGH_DATA.
			// To avoid this hanging 'All preloads complete', consider audio permanently preloaded after it first
			// reaches HAVE_ENOUGH_DATA.
			if (ret)
				this.wasEverReady = true;
			
			return ret || this.wasEverReady;
			
		case API_WEBAUDIO:
			// either AJAX request completed and audio decode pending, or audio finished decoding (and AJAX data cleared)
			return !!this.audioData || !!this.bufferObject;
			
		case API_CORDOVA:
			// Does not support preloading
			return true;
		}
		
		// Should not reach here
		return false;
	};
	
	C2AudioBuffer.prototype.isLoadedAndDecoded = function ()
	{
		switch (this.myapi) {
		case API_HTML5:
			return this.isLoaded();		// no distinction between loaded and decoded in HTML5 audio, just rely on ready state
			
		case API_WEBAUDIO:
			// decodeAudioData has completed (implies AJAX request also finished but the response was cleared to save memory)
			return !!this.bufferObject;
			
		case API_CORDOVA:
			// Does not support preloading
			return true;
		}
		
		// Should not reach here
		return false;
	};
	
	C2AudioBuffer.prototype.hasFailedToLoad = function ()
	{
		switch (this.myapi) {
		case API_HTML5:
			return !!this.bufferObject["error"];
			
		case API_WEBAUDIO:
			return this.failedToLoad;
		}
		
		return false;
	};
	
	function C2AudioInstance(buffer_, tag_)
	{
		var self = this;
		
		this.tag = tag_;
		this.fresh = true;
		this.stopped = true;
		this.src = buffer_.src;
		this.buffer = buffer_;
		this.myapi = api;
		this.is_music = buffer_.is_music;
		this.playbackRate = 1;
		this.hasPlaybackEnded = true;	// ended flag
		this.resume_me = false;			// make sure resumes when leaving suspend
		this.is_paused = false;
		this.resume_position = 0;		// for web audio api to resume from correct playback position
		this.looping = false;
		this.is_muted = false;
		this.is_silent = false;
		this.volume = 1;
		
		// for Web Audio API onended event
		this.onended_handler = function (e)
		{
			// Web Audio fires "onended" when pausing a sound, but we don't want to consider
			// that as playback ending. So ignore this event if is_paused is set.
			// resume_me is also set if it was paused due to suspending.
			if (self.is_paused || self.resume_me)
				return;
			
			// NOTE: due to a bug in iOS 8, onended fires with a null 'this'. It is supposed to be
			// the AudioBufferSourceNode that ended, and the active_buffer check will fail if it is
			// not correct. To work around the iOS 8 bug, if 'this' is not set, look for the buffer
			// in the event target instead, which does appear to be correctly set in iOS 8.
			var bufferThatEnded = this;
			if (!bufferThatEnded)
				bufferThatEnded = e.target;
			
			if (bufferThatEnded !== self.active_buffer)
				return;
			
			self.hasPlaybackEnded = true;
			self.stopped = true;
			audTag = self.tag;
			audRuntime.trigger(cr.plugins_.Audio.prototype.cnds.OnEnded, audInst);
		};
		
		// Seeking is implemented as a pause, change resume position, then resume.
		// The browser fires its "ended" event next tick after it ends, which
		// means it misses the "paused" flag and decides the sound has ended. To avoid
		// this, we keep a reference to the only buffer we're interested in ended events for.
		this.active_buffer = null;
		
		this.isTimescaled = ((timescale_mode === 1 && !this.is_music) || timescale_mode === 2);
		
		// Web Audio API only
		this.mutevol = 1;
		this.startTime = (this.isTimescaled ? audRuntime.kahanTime.sum : audRuntime.wallTime.sum);
		this.gainNode = null;
		this.pannerNode = null;
		this.pannerEnabled = false;
		this.objectTracker = null;
		this.panX = 0;
		this.panY = 0;
		this.panAngle = 0;
		this.panConeInner = 0;
		this.panConeOuter = 0;
		this.panConeOuterGain = 0;
		
		this.instanceObject = null;
		var add_end_listener = false;
		
		// If this is using Web Audio API but the buffer is not routing through Web Audio API,
		// fall back to HTML5 audio
		if (this.myapi === API_WEBAUDIO && this.buffer.myapi === API_HTML5 && !this.buffer.supportWebAudioAPI)
			this.myapi = API_HTML5;
		
		switch (this.myapi) {
		case API_HTML5:
			// For music recycle the buffer audio object
			if (this.is_music)
			{
				this.instanceObject = buffer_.bufferObject;
				add_end_listener = !buffer_.added_end_listener;
				buffer_.added_end_listener = true;
			}
			else
			{
				// Just make a new audio object
				this.instanceObject = new Audio();
				this.instanceObject.crossOrigin = "anonymous";
				this.instanceObject.autoplay = false;
				this.instanceObject.src = buffer_.bufferObject.src;
				add_end_listener = true;
			}
			
			if (add_end_listener)
			{
				this.instanceObject.addEventListener('ended', function () {
						audTag = self.tag;
						self.stopped = true;
						audRuntime.trigger(cr.plugins_.Audio.prototype.cnds.OnEnded, audInst);
				});
			}
			
			break;
		case API_WEBAUDIO:
			this.gainNode = createGain();
			this.gainNode["connect"](getDestinationForTag(tag_));
			
			// Buffer is also web audio API (for sfx)
			if (this.buffer.myapi === API_WEBAUDIO)
			{					
				// If the buffer is ready, make a new sound instance
				if (buffer_.bufferObject)
				{
					this.instanceObject = context["createBufferSource"]();
					this.instanceObject["buffer"] = buffer_.bufferObject;
					this.instanceObject["connect"](this.gainNode);
				}
			}
			// Buffer is HTMLMediaElement (for music)
			else
			{
				this.instanceObject = this.buffer.bufferObject;		// reference the audio element
				this.buffer.outNode["connect"](this.gainNode);
				
				// Ensure ended event added for the underlying media element
				if (!this.buffer.added_end_listener)
				{
					this.buffer.added_end_listener = true;
					
					this.buffer.bufferObject.addEventListener('ended', function () {
							audTag = self.tag;
							self.stopped = true;
							audRuntime.trigger(cr.plugins_.Audio.prototype.cnds.OnEnded, audInst);
					});
				}
			}
			break;
		case API_CORDOVA:
			
			// Create new Media object.  Include full path to the media file.		
			this.instanceObject = new window["Media"](appPath + this.src, null, null, function (status) {
					if (status === window["Media"]["MEDIA_STOPPED"])
					{
						self.hasPlaybackEnded = true;
						self.stopped = true;
						audTag = self.tag;
						audRuntime.trigger(cr.plugins_.Audio.prototype.cnds.OnEnded, audInst);
					}
			});
			
			break;
		}
	};
	
	C2AudioInstance.prototype.hasEnded = function ()
	{
		var time;
		
		// Only HTML5 audio has an actual ended property, rest have to go by duration.
		switch (this.myapi) {
		case API_HTML5:
			return this.instanceObject.ended;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				// looping - won't end
				if (!this.fresh && !this.stopped && this.instanceObject["loop"])
					return false;
				
				// paused - hasn't ended yet
				if (this.is_paused)
					return false;
				
				// Wait until onended fires
				return this.hasPlaybackEnded;
			}
			else
				return this.instanceObject.ended;
		
		case API_CORDOVA:
			return this.hasPlaybackEnded;
		}
		
		// should not reach here
		return true;
	};
	
	C2AudioInstance.prototype.canBeRecycled = function ()
	{
		if (this.fresh || this.stopped)
			return true;		// not yet used or is not playing
			
		return this.hasEnded();
	};
	
	C2AudioInstance.prototype.setPannerEnabled = function (enable_)
	{
		if (api !== API_WEBAUDIO)
			return;
		
		// when not enabled, route just goes: source -> gain -> dest
		// when enabled, we insert a panner node: source -> gain -> panner -> dest
		
		// disabled and switching to enabled: reconnect the gain node via the panner node
		if (!this.pannerEnabled && enable_)
		{
			if (!this.gainNode)
				return;
			
			if (!this.pannerNode)
			{
				this.pannerNode = context["createPanner"]();
				
				// support older API versions
				if (typeof this.pannerNode["panningModel"] === "number")
					this.pannerNode["panningModel"] = panningModel;
				else
					this.pannerNode["panningModel"] = ["equalpower", "HRTF", "soundfield"][panningModel];
					
				if (typeof this.pannerNode["distanceModel"] === "number")
					this.pannerNode["distanceModel"] = distanceModel;
				else
					this.pannerNode["distanceModel"] = ["linear", "inverse", "exponential"][distanceModel];
					
				this.pannerNode["refDistance"] = refDistance;
				this.pannerNode["maxDistance"] = maxDistance;
				this.pannerNode["rolloffFactor"] = rolloffFactor;
			}
			
			this.gainNode["disconnect"]();
			this.gainNode["connect"](this.pannerNode);
			this.pannerNode["connect"](getDestinationForTag(this.tag));
			
			this.pannerEnabled = true;
		}
		
		// enabled and switching to disabled: leave the pannerNode, but route around it
		else if (this.pannerEnabled && !enable_)
		{
			if (!this.gainNode)
				return;
			
			this.pannerNode["disconnect"]();
			this.gainNode["disconnect"]();
			this.gainNode["connect"](getDestinationForTag(this.tag));
			
			this.pannerEnabled = false;
		}
	};
	
	C2AudioInstance.prototype.setPan = function (x, y, angle, innerangle, outerangle, outergain)
	{
		if (!this.pannerEnabled || api !== API_WEBAUDIO)
			return;
			
		this.pannerNode["setPosition"](x, y, 0);
		this.pannerNode["setOrientation"](Math.cos(cr.to_radians(angle)), Math.sin(cr.to_radians(angle)), 0);
		this.pannerNode["coneInnerAngle"] = innerangle;
		this.pannerNode["coneOuterAngle"] = outerangle;
		this.pannerNode["coneOuterGain"] = outergain;
		
		this.panX = x;
		this.panY = y;
		this.panAngle = angle;
		this.panConeInner = innerangle;
		this.panConeOuter = outerangle;
		this.panConeOuterGain = outergain;
	};
	
	C2AudioInstance.prototype.setObject = function (o)
	{
		if (!this.pannerEnabled || api !== API_WEBAUDIO)
			return;
			
		if (!this.objectTracker)
			this.objectTracker = new ObjectTracker();
			
		this.objectTracker.setObject(o);
	};
	
	C2AudioInstance.prototype.tick = function (dt)
	{
		if (!this.pannerEnabled || api !== API_WEBAUDIO || !this.objectTracker || !this.objectTracker.hasObject() || !this.isPlaying())
		{
			return;
		}
		
		this.objectTracker.tick(dt);
		var inst = this.objectTracker.obj;
		var px = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, true);
		var py = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, false);
		this.pannerNode["setPosition"](px, py, 0);
		
		var a = 0;
		
		if (typeof this.objectTracker.obj.angle !== "undefined")
		{
			a = inst.angle - inst.layer.getAngle();
			this.pannerNode["setOrientation"](Math.cos(a), Math.sin(a), 0);
		}
	};
	
	C2AudioInstance.prototype.play = function (looping, vol, fromPosition, scheduledTime)
	{
		var instobj = this.instanceObject;
		this.looping = looping;
		this.volume = vol;
		var seekPos = fromPosition || 0;
		scheduledTime = scheduledTime || 0;
		
		switch (this.myapi) {
		case API_HTML5:
			
			// restore defaults
			
			if (instobj.playbackRate !== 1.0)
				instobj.playbackRate = 1.0;
				
			if (instobj.volume !== vol * masterVolume)
				instobj.volume = vol * masterVolume;
				
			if (instobj.loop !== looping)
				instobj.loop = looping;
				
			if (instobj.muted)
				instobj.muted = false;
			
			if (instobj.currentTime !== seekPos)
			{
				// no idea why this sometimes throws
				try {
					instobj.currentTime = seekPos;
				}
				catch (err)
				{
;
				}
			}
			
			tryPlayAudioElement(this);
				
			break;
		case API_WEBAUDIO:
			this.muted = false;
			this.mutevol = 1;
			
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				this.gainNode["gain"]["value"] = vol * masterVolume;
				
				// buffer sources are one-shot - make a new object second time around
				if (!this.fresh)
				{
					this.instanceObject = context["createBufferSource"]();
					this.instanceObject["buffer"] = this.buffer.bufferObject;
					this.instanceObject["connect"](this.gainNode);
				}
				
				this.instanceObject["onended"] = this.onended_handler;
				this.active_buffer = this.instanceObject;
				this.instanceObject.loop = looping;
				this.hasPlaybackEnded = false;
				
				if (seekPos === 0)
					startSource(this.instanceObject, scheduledTime);
				else
					startSourceAt(this.instanceObject, seekPos, this.getDuration(), scheduledTime);
			}
			else
			{
				if (instobj.playbackRate !== 1.0)
					instobj.playbackRate = 1.0;
				
				if (instobj.loop !== looping)
					instobj.loop = looping;
			
				instobj.volume = vol * masterVolume;
				
				if (instobj.currentTime !== seekPos)
				{
					// no idea why this sometimes throws
					try {
						instobj.currentTime = seekPos;
					}
					catch (err)
					{
;
					}
				}
				
				tryPlayAudioElement(this);
			}
			break;
		case API_CORDOVA:

			if ((!this.fresh && this.stopped) || seekPos !== 0)
				instobj["seekTo"](seekPos);
				
			instobj["play"]();
			this.hasPlaybackEnded = false;
			
			break;
		}
		
		this.playbackRate = 1;
		this.startTime = (this.isTimescaled ? audRuntime.kahanTime.sum : audRuntime.wallTime.sum) - seekPos;
		this.fresh = false;
		this.stopped = false;
		this.is_paused = false;
	};
	
	C2AudioInstance.prototype.stop = function ()
	{
		switch (this.myapi) {
		case API_HTML5:
			if (!this.instanceObject.paused)
				this.instanceObject.pause();
			break;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
				stopSource(this.instanceObject);
			else
			{
				if (!this.instanceObject.paused)
					this.instanceObject.pause();
			}
			break;
		case API_CORDOVA:
			this.instanceObject["stop"](); 
			break;
		}
		
		this.stopped = true;
		this.is_paused = false;
	};
	
	C2AudioInstance.prototype.pause = function ()
	{
		if (this.fresh || this.stopped || this.hasEnded() || this.is_paused)
			return;
		
		switch (this.myapi) {
		case API_HTML5:
			if (!this.instanceObject.paused)
				this.instanceObject.pause();
			break;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				// if buffer is looping, playback time may exceed the duration, and we
				// can't resume from somewhere after the duration. So wrap it back inside the duration.
				this.resume_position = this.getPlaybackTime(true);
				
				if (this.looping)
					this.resume_position = this.resume_position % this.getDuration();
				
				// Web Audio fires its "ended" event when stopped, but we want to ignore this when
				// pausing, so the is_paused flag is set first and the ended event ignores itself based on this.
				this.is_paused = true;
				stopSource(this.instanceObject);
			}
			else
			{
				if (!this.instanceObject.paused)
					this.instanceObject.pause();
			}
			break;
		case API_CORDOVA:
			this.instanceObject["pause"](); 
			break;
		}
		
		this.is_paused = true;
	};
	
	C2AudioInstance.prototype.resume = function ()
	{
		if (this.fresh || this.stopped || this.hasEnded() || !this.is_paused)
			return;
		
		switch (this.myapi) {
		case API_HTML5:
			tryPlayAudioElement(this);
			break;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				// resume by creating a new buffer source and playing from where it got paused
				this.instanceObject = context["createBufferSource"]();
				this.instanceObject["buffer"] = this.buffer.bufferObject;
				this.instanceObject["connect"](this.gainNode);
				this.instanceObject["onended"] = this.onended_handler;
				this.active_buffer = this.instanceObject;
				this.instanceObject.loop = this.looping;
				this.gainNode["gain"]["value"] = masterVolume * this.volume * this.mutevol;
				this.updatePlaybackRate();
				
				// set the start time back in time so it can still calculate the end time properly
				this.startTime = (this.isTimescaled ? audRuntime.kahanTime.sum : audRuntime.wallTime.sum) - (this.resume_position / (this.playbackRate || 0.001));
				startSourceAt(this.instanceObject, this.resume_position, this.getDuration());
			}
			else
			{
				tryPlayAudioElement(this);
			}
			break;
		case API_CORDOVA:
			this.instanceObject["play"](); 
			break;
		}
		
		this.is_paused = false;
	};
	
	C2AudioInstance.prototype.seek = function (pos)
	{
		if (this.fresh || this.stopped || this.hasEnded())
			return;
		
		switch (this.myapi) {
		case API_HTML5:
			try {
				this.instanceObject.currentTime = pos;
			}
			catch (e) {}
			break;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				if (this.is_paused)
					this.resume_position = pos;
				else
				{
					// easiest thing to do is pause, change the resume position then resume
					this.pause();
					this.resume_position = pos;
					this.resume();
				}
			}
			else
			{
				try {
					this.instanceObject.currentTime = pos;
				}
				catch (e) {}
			}
			break;
		case API_CORDOVA:
			// not supported
			break;
		}
	};
	
	C2AudioInstance.prototype.reconnect = function (toNode)
	{
		if (this.myapi !== API_WEBAUDIO)
			return;
			
		if (this.pannerEnabled)
		{
			this.pannerNode["disconnect"]();
			this.pannerNode["connect"](toNode);
		}
		else
		{
			this.gainNode["disconnect"]();
			this.gainNode["connect"](toNode);
		}
	};
	
	C2AudioInstance.prototype.getDuration = function (applyPlaybackRate)
	{
		var ret = 0;
		
		switch (this.myapi) {
		case API_HTML5:
			if (typeof this.instanceObject.duration !== "undefined")
				ret = this.instanceObject.duration;
			break;
		case API_WEBAUDIO:
			ret = this.buffer.bufferObject["duration"];
			break;
		case API_CORDOVA:
			ret = this.instanceObject["getDuration"]();
			break;
		}
		
		if (applyPlaybackRate)
			ret /= (this.playbackRate || 0.001);		// avoid divide-by-zero
		
		return ret;
	};
	
	C2AudioInstance.prototype.getPlaybackTime = function (applyPlaybackRate)
	{
		var duration = this.getDuration();
		var ret = 0;
		
		switch (this.myapi) {
		case API_HTML5:
			if (typeof this.instanceObject.currentTime !== "undefined")
				ret = this.instanceObject.currentTime;
			break;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				if (this.is_paused)
					return this.resume_position;
				else
					ret = (this.isTimescaled ? audRuntime.kahanTime.sum : audRuntime.wallTime.sum) - this.startTime;
			}
			else if (typeof this.instanceObject.currentTime !== "undefined")
				ret = this.instanceObject.currentTime;
			break;
		case API_CORDOVA:
			// not supported synchronously
			break;
		}
		
		// compensate by current playback rate (note: does not take in to account past changes)
		if (applyPlaybackRate)
			ret *= this.playbackRate;
		
		// clamp to buffer duration if not looping, otherwise keep counting up
		if (!this.looping && ret > duration)
			ret = duration;
		
		return ret;
	};
	
	C2AudioInstance.prototype.isPlaying = function ()
	{
		return !this.is_paused && !this.fresh && !this.stopped && !this.hasEnded();
	};
	
	C2AudioInstance.prototype.shouldSave = function ()
	{
		// save sounds which are playing, but include paused ones
		return !this.fresh && !this.stopped && !this.hasEnded();
	};
	
	C2AudioInstance.prototype.setVolume = function (v)
	{
		this.volume = v;
		this.updateVolume();
	};
	
	C2AudioInstance.prototype.updateVolume = function ()
	{
		var volToSet = this.volume * masterVolume;
		
		if (!isFinite(volToSet))
			volToSet = 0;		// HTMLMediaElement throws if setting non-finite volume
		
		switch (this.myapi) {
		case API_HTML5:
			// ff 3.6 doesn't seem to have this property
			if (typeof this.instanceObject.volume !== "undefined" && this.instanceObject.volume !== volToSet)
				this.instanceObject.volume = volToSet;
			break;
		case API_WEBAUDIO:
			// Work around an apparent bug in Chrome for Android: for some reason setting the gain node volume
			// here has no effect when connected to a MediaSourceNode. To work around, set the volume on the
			// audio element feeding audio in to the MediaSourceNode.
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				this.gainNode["gain"]["value"] = volToSet * this.mutevol;
			}
			else
			{
				if (typeof this.instanceObject.volume !== "undefined" && this.instanceObject.volume !== volToSet)
					this.instanceObject.volume = volToSet;
			}
			
			break;
		case API_CORDOVA:
			// not supported
			break;
		}
	};
	
	C2AudioInstance.prototype.getVolume = function ()
	{
		return this.volume;
	};
	
	C2AudioInstance.prototype.doSetMuted = function (m)
	{
		switch (this.myapi) {
		case API_HTML5:
			if (this.instanceObject.muted !== !!m)
				this.instanceObject.muted = !!m;
			break;
		case API_WEBAUDIO:
			// Due to the same workaround in updateVolume, mute the underlying audio source when using
			// MediaSourceNodes.
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				this.mutevol = (m ? 0 : 1);
				this.gainNode["gain"]["value"] = masterVolume * this.volume * this.mutevol;
			}
			else
			{
				if (this.instanceObject.muted !== !!m)
					this.instanceObject.muted = !!m;
			}
			break;
		case API_CORDOVA:
			// not supported
			break;
		}
	};
	
	C2AudioInstance.prototype.setMuted = function (m)
	{
		this.is_muted = !!m;
		
		this.doSetMuted(this.is_muted || this.is_silent);
	};
	
	C2AudioInstance.prototype.setSilent = function (m)
	{
		this.is_silent = !!m;
		
		this.doSetMuted(this.is_muted || this.is_silent);
	};
	
	C2AudioInstance.prototype.setLooping = function (l)
	{
		this.looping = l;
		
		switch (this.myapi) {
		case API_HTML5:
			if (this.instanceObject.loop !== !!l)
				this.instanceObject.loop = !!l;
			break;
		case API_WEBAUDIO:
			if (this.instanceObject.loop !== !!l)
				this.instanceObject.loop = !!l;
			break;
		case API_CORDOVA:
			// not supported
			break;
		}
	};
	
	C2AudioInstance.prototype.setPlaybackRate = function (r)
	{
		this.playbackRate = r;
		
		this.updatePlaybackRate();
	};
	
	C2AudioInstance.prototype.updatePlaybackRate = function ()
	{
		var r = this.playbackRate;
		
		if (this.isTimescaled)
			r *= audRuntime.timescale;
			
		switch (this.myapi) {
		case API_HTML5:
			if (this.instanceObject.playbackRate !== r)
				this.instanceObject.playbackRate = r;
			break;
		case API_WEBAUDIO:
			if (this.buffer.myapi === API_WEBAUDIO)
			{
				if (this.instanceObject["playbackRate"]["value"] !== r)
					this.instanceObject["playbackRate"]["value"] = r;
			}
			else
			{
				if (this.instanceObject.playbackRate !== r)
					this.instanceObject.playbackRate = r;
			}
			break;
		case API_CORDOVA:
			// not supported
			break;
		}
	};
	
	C2AudioInstance.prototype.setSuspended = function (s)
	{
		switch (this.myapi) {
		case API_HTML5:
			if (s)
			{
				// Pause if playing
				if (this.isPlaying())
				{
					this.resume_me = true;
					this.instanceObject["pause"]();
				}
				else
					this.resume_me = false;
			}
			else
			{
				if (this.resume_me)
				{
					this.instanceObject["play"]();
					this.resume_me = false;
				}
			}
			
			break;
		case API_WEBAUDIO:
		
			if (s)
			{
				if (this.isPlaying())
				{
					this.resume_me = true;
					
					if (this.buffer.myapi === API_WEBAUDIO)
					{
						// if buffer is looping, playback time may exceed the duration, and we
						// can't resume from somewhere after the duration. So wrap it back inside the duration.
						this.resume_position = this.getPlaybackTime(true);
						
						if (this.looping)
							this.resume_position = this.resume_position % this.getDuration();
						
						stopSource(this.instanceObject);
					}
					else
						this.instanceObject["pause"]();
				}
				else
					this.resume_me = false;
			}
			else
			{
				if (this.resume_me)
				{
					if (this.buffer.myapi === API_WEBAUDIO)
					{
						// resume by creating a new buffer source and playing from where it got paused
						this.instanceObject = context["createBufferSource"]();
						this.instanceObject["buffer"] = this.buffer.bufferObject;
						this.instanceObject["connect"](this.gainNode);
						this.instanceObject["onended"] = this.onended_handler;
						this.active_buffer = this.instanceObject;
						this.instanceObject.loop = this.looping;
						this.gainNode["gain"]["value"] = masterVolume * this.volume * this.mutevol;
						this.updatePlaybackRate();
						
						// set the start time back in time so it can still calculate the end time properly
						this.startTime = (this.isTimescaled ? audRuntime.kahanTime.sum : audRuntime.wallTime.sum) - (this.resume_position / (this.playbackRate || 0.001));
						startSourceAt(this.instanceObject, this.resume_position, this.getDuration());
					}
					else
					{
						this.instanceObject["play"]();
					}
					
					this.resume_me = false;
				}					
			}
			
			break;
		case API_CORDOVA:
		
			if (s)
			{
				// Pause if playing
				if (this.isPlaying())
				{
					this.instanceObject["pause"]();
					this.resume_me = true;
				}
				else
					this.resume_me = false;
			}
			else
			{
				if (this.resume_me)
				{
					this.resume_me = false;
					this.instanceObject["play"]();
				}
			}
			
			break;
		}
	};
	
	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
		audRuntime = this.runtime;
		audInst = this;
		this.listenerTracker = null;
		this.listenerZ = -600;
		
		// WKWebView can't load HTML audio in any sensible way, since it cannot load from a file: or a
		// blob: URL. Only a data URL works, but then we may as well just use the Web Audio API
		// and load music in to an entire buffer, since data URLs are probably even worse.
		// So for WKWebView the playMusicAsSoundWorkaround flag is specified.
		if (this.runtime.isWKWebView)
			playMusicAsSoundWorkaround = true;
		
		// Work around iOS & Android's requirement of having a user gesture start playback.
		// This is only used as a fall-back heuristic if play() does not return a Promise.
		// Note: skip this workaround if playing music as sound, since the Web Audio API is not affected by this limitation.
		if ((this.runtime.isiOS || (this.runtime.isAndroid && (this.runtime.isChrome || this.runtime.isAndroidStockBrowser))) && !this.runtime.isCrosswalk && !this.runtime.isAmazonWebApp && !playMusicAsSoundWorkaround)
		{
			useNextTouchWorkaround = true;
		}
		
		// Use APIs in order:
		// Always use Web Audio API if supported
		// Then Cordova Media if Web Audio API not supported and in Cordova
		// Then leave default (HTML5)
		
		context = null;

		if (typeof AudioContext !== "undefined")
		{
			api = API_WEBAUDIO;
			context = new AudioContext();
		}
		else if (typeof webkitAudioContext !== "undefined")
		{
			api = API_WEBAUDIO;
			context = new webkitAudioContext();
		}
		
		// Horrifying Safari iOS 9.2 bug: audio contexts can come back with the wrong sampling rate. Try to fix this
		// by hackily forcibly closing the audio context and recreating it.
		if (this.runtime.isiOS && context)
		{
			if (context.close)
				context.close();
			
			if (typeof AudioContext !== "undefined")
				context = new AudioContext();
			else if (typeof webkitAudioContext !== "undefined")
				context = new webkitAudioContext();
		}
			
		if (api !== API_WEBAUDIO)
		{
			// If using cordova check we really have Media objects (the permission may have been disabled)
			if (this.runtime.isCordova && typeof window["Media"] !== "undefined")
				api = API_CORDOVA;
			// else leave as HTML5
		}
		
		// Cordova needs to know where to find files
		if (api === API_CORDOVA)
		{
			appPath = location.href;
			
			var i = appPath.lastIndexOf("/");
			
			if (i > -1)
				appPath = appPath.substr(0, i + 1);
				
			appPath = appPath.replace("file://", "");
		}
		
		// Tick to fire 'on ended' for Web Audio API/Cordova, and keep timescale up to date
		this.runtime.tickMe(this);
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	instanceProto.onCreate = function ()
	{
		this.runtime.audioInstance = this;
		
		timescale_mode = this.properties[0];	// 0 = off, 1 = sounds only, 2 = all
		this.saveload = this.properties[1];		// 0 = all, 1 = sounds only, 2 = music only, 3 = none
		this.playinbackground = this.properties[2];
		this.nextPlayTime = 0;
		
		panningModel = this.properties[3];		// 0 = equalpower, 1 = hrtf, 3 = soundfield
		distanceModel = this.properties[4];		// 0 = linear, 1 = inverse, 2 = exponential
		this.listenerZ = -this.properties[5];
		refDistance = this.properties[6];
		maxDistance = this.properties[7];
		rolloffFactor = this.properties[8];
		
		this.listenerTracker = new ObjectTracker();
		
		var draw_width = (this.runtime.draw_width || this.runtime.width);
		var draw_height = (this.runtime.draw_height || this.runtime.height);
		
		if (api === API_WEBAUDIO)
		{
			context["listener"]["setPosition"](draw_width / 2, draw_height / 2, this.listenerZ);
			context["listener"]["setOrientation"](0, 0, 1, 0, -1, 0);
			
			// Add function for User Media object to call when mic input is received
			window["c2OnAudioMicStream"] = function (localMediaStream, tag)
			{
				if (micSource)
					micSource["disconnect"]();
				
				micTag = tag.toLowerCase();
				micSource = context["createMediaStreamSource"](localMediaStream);
				micSource["connect"](getDestinationForTag(micTag));
			};
		}
		
		this.runtime.addSuspendCallback(function(s)
		{
			audInst.onSuspend(s);
		});
		
		var self = this;
		this.runtime.addDestroyCallback(function (inst)
		{
			self.onInstanceDestroyed(inst);
		});
	};
	
	instanceProto.onInstanceDestroyed = function (inst)
	{
		// Remove instance from any object trackers
		var i, len, a;
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			a = audioInstances[i];
			
			if (a.objectTracker)
			{
				if (a.objectTracker.obj === inst)
				{
					a.objectTracker.obj = null;
				
					// Stop any looping sounds attached to objects
					if (a.pannerEnabled && a.isPlaying() && a.looping)
						a.stop();
				}
			}
		}
		
		if (this.listenerTracker.obj === inst)
			this.listenerTracker.obj = null;
	};
	
	instanceProto.saveToJSON = function ()
	{
		var o = {
			"silent": silent,
			"masterVolume": masterVolume,
			"listenerZ": this.listenerZ,
			"listenerUid": this.listenerTracker.hasObject() ? this.listenerTracker.obj.uid : -1,
			"playing": [],
			"effects": {}
		};
		
		var playingarr = o["playing"];
		
		var i, len, a, d, p, panobj, playbackTime;
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			a = audioInstances[i];
			
			if (!a.shouldSave())
				continue;				// no need to save stopped sounds
			
			if (this.saveload === 3)	// not saving/loading any sounds/music
				continue;
			if (a.is_music && this.saveload === 1)	// not saving/loading music
				continue;
			if (!a.is_music && this.saveload === 2)	// not saving/loading sound
				continue;
			
			playbackTime = a.getPlaybackTime();
			
			if (a.looping)
				playbackTime = playbackTime % a.getDuration();
			
			d = {
				"tag": a.tag,
				"buffersrc": a.buffer.src,
				"buffertype": a.buffer.type,
				"is_music": a.is_music,
				"playbackTime": playbackTime,
				"volume": a.volume,
				"looping": a.looping,
				"muted": a.is_muted,
				"playbackRate": a.playbackRate,
				"paused": a.is_paused,
				"resume_position": a.resume_position
			};
			
			if (a.pannerEnabled)
			{
				d["pan"] = {};
				panobj = d["pan"];
				
				if (a.objectTracker && a.objectTracker.hasObject())
				{
					panobj["objUid"] = a.objectTracker.obj.uid;
				}
				else
				{
					panobj["x"] = a.panX;
					panobj["y"] = a.panY;
					panobj["a"] = a.panAngle;
				}
				
				panobj["ia"] = a.panConeInner;
				panobj["oa"] = a.panConeOuter;
				panobj["og"] = a.panConeOuterGain;
			}
			
			playingarr.push(d);
		}
		
		var fxobj = o["effects"];
		var fxarr;
		
		for (p in effects)
		{
			if (effects.hasOwnProperty(p))
			{
				fxarr = [];
				
				for (i = 0, len = effects[p].length; i < len; i++)
				{
					fxarr.push({ "type": effects[p][i].type, "params": effects[p][i].params });
				}
				
				fxobj[p] = fxarr;
			}
		}
		
		return o;
	};
	
	var objectTrackerUidsToLoad = [];
	
	instanceProto.loadFromJSON = function (o)
	{
		var setSilent = o["silent"];
		masterVolume = o["masterVolume"];
		this.listenerZ = o["listenerZ"];
		
		this.listenerTracker.setObject(null);
		var listenerUid = o["listenerUid"];
		if (listenerUid !== -1)
		{
			this.listenerTracker.loadUid = listenerUid;
			objectTrackerUidsToLoad.push(this.listenerTracker);
		}
		
		var playingarr = o["playing"];
		
		var i, len, d, src, type, is_music, tag, playbackTime, looping, vol, b, a, p, pan, panObjUid;
		
		// Stop all current audio that is being saved/loaded
		if (this.saveload !== 3)
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
			{
				a = audioInstances[i];
				
				if (a.is_music && this.saveload === 1)
					continue;		// only saving/loading sound: leave music playing
				if (!a.is_music && this.saveload === 2)
					continue;		// only saving/loading music: leave sound playing
				
				a.stop();
			}
		}
		
		// Load effects
		var fxarr, fxtype, fxparams, fx;
		
		for (p in effects)
		{
			if (effects.hasOwnProperty(p))
			{
				for (i = 0, len = effects[p].length; i < len; i++)
					effects[p][i].remove();
			}
		}
		
		cr.wipe(effects);
		
		for (p in o["effects"])
		{
			if (o["effects"].hasOwnProperty(p))
			{
				fxarr = o["effects"][p];
				
				for (i = 0, len = fxarr.length; i < len; i++)
				{
					fxtype = fxarr[i]["type"];
					fxparams = fxarr[i]["params"];
					
					switch (fxtype) {
					case "filter":
						addEffectForTag(p, new FilterEffect(fxparams[0], fxparams[1], fxparams[2], fxparams[3], fxparams[4], fxparams[5]));
						break;
					case "delay":
						addEffectForTag(p, new DelayEffect(fxparams[0], fxparams[1], fxparams[2]));
						break;
					case "convolve":
						src = fxparams[2];
						
						// HACK: TODO: make this use info object
						b = this.getAudioBuffer(src, false);
						
						// buffer loaded
						if (b.bufferObject)
						{
							fx = new ConvolveEffect(b.bufferObject, fxparams[0], fxparams[1], src);
						}
						// else assign once loaded
						else
						{
							fx = new ConvolveEffect(null, fxparams[0], fxparams[1], src);
							b.normalizeWhenReady = fxparams[0];
							b.convolveWhenReady = fx;
						}
						
						addEffectForTag(p, fx);
						break;
					case "flanger":
						addEffectForTag(p, new FlangerEffect(fxparams[0], fxparams[1], fxparams[2], fxparams[3], fxparams[4]));
						break;
					case "phaser":
						addEffectForTag(p, new PhaserEffect(fxparams[0], fxparams[1], fxparams[2], fxparams[3], fxparams[4], fxparams[5]));
						break;
					case "gain":
						addEffectForTag(p, new GainEffect(fxparams[0]));
						break;
					case "tremolo":
						addEffectForTag(p, new TremoloEffect(fxparams[0], fxparams[1]));
						break;
					case "ringmod":
						addEffectForTag(p, new RingModulatorEffect(fxparams[0], fxparams[1]));
						break;
					case "distortion":
						addEffectForTag(p, new DistortionEffect(fxparams[0], fxparams[1], fxparams[2], fxparams[3], fxparams[4]));
						break;
					case "compressor":
						addEffectForTag(p, new CompressorEffect(fxparams[0], fxparams[1], fxparams[2], fxparams[3], fxparams[4]));
						break;
					case "analyser":
						addEffectForTag(p, new AnalyserEffect(fxparams[0], fxparams[1]));
						break;
					}
				}
			}
		}
		
		for (i = 0, len = playingarr.length; i < len; i++)
		{
			if (this.saveload === 3)	// not saving/loading any sounds/music
				continue;
			
			d = playingarr[i];
			src = d["buffersrc"];
			type = d["buffertype"] || "";
			is_music = d["is_music"];
			tag = d["tag"];
			playbackTime = d["playbackTime"];
			looping = d["looping"];
			vol = d["volume"];
			pan = d["pan"];
			panObjUid = (pan && pan.hasOwnProperty("objUid")) ? pan["objUid"] : -1;
			
			if (is_music && this.saveload === 1)	// not saving/loading music
				continue;
			if (!is_music && this.saveload === 2)	// not saving/loading sound
				continue;
			
			a = this.getAudioInstance({
				url: src,
				type: type
			}, tag, is_music, looping, vol);
		
			if (!a)
			{
				// tell buffer to do this panning when ready
				b = this.getAudioBuffer({ url: src, type: "" }, is_music);
				b.seekWhenReady = playbackTime;
				b.pauseWhenReady = d["paused"];
				
				if (pan)
				{
					if (panObjUid !== -1)
					{
						b.panWhenReady.push({ objUid: panObjUid, ia: pan["ia"], oa: pan["oa"], og: pan["og"], thistag: tag });
					}
					else
					{
						b.panWhenReady.push({ x: pan["x"], y: pan["y"], a: pan["a"], ia: pan["ia"], oa: pan["oa"], og: pan["og"], thistag: tag });
					}
				}
				
				continue;
			}
			
			a.resume_position = d["resume_position"];
			a.setPannerEnabled(!!pan);
			a.play(looping, vol, playbackTime);
			a.updatePlaybackRate();
			a.updateVolume();
			a.doSetMuted(a.is_muted || a.is_silent);
			
			if (d["paused"])
				a.pause();
				
			if (d["muted"])
				a.setMuted(true);
			
			a.doSetMuted(a.is_muted || a.is_silent);
			
			if (pan)
			{
				if (panObjUid !== -1)
				{
					a.objectTracker = a.objectTracker || new ObjectTracker();
					a.objectTracker.loadUid = panObjUid;
					objectTrackerUidsToLoad.push(a.objectTracker);
				}
				else
				{
					a.setPan(pan["x"], pan["y"], pan["a"], pan["ia"], pan["oa"], pan["og"]);
				}
			}
		}
		
		if (setSilent && !silent)			// setting silent
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
				audioInstances[i].setSilent(true);
			
			silent = true;
		}
		else if (!setSilent && silent)		// setting not silent
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
				audioInstances[i].setSilent(false);
			
			silent = false;
		}
	};
	
	instanceProto.afterLoad = function ()
	{
		// Update all object trackers that need to look up UIDs
		var i, len, ot, inst;
		for (i = 0, len = objectTrackerUidsToLoad.length; i < len; i++)
		{
			ot = objectTrackerUidsToLoad[i];
			inst = this.runtime.getObjectByUID(ot.loadUid);
			ot.setObject(inst);
			ot.loadUid = -1;
			
			if (inst)
			{
				listenerX = inst.x;
				listenerY = inst.y;
			}
		}
		
		cr.clearArray(objectTrackerUidsToLoad);
	};
	
	instanceProto.onSuspend = function (s)
	{
		// ignore suspend/resume events if set to play in background - normally
		// everything is paused in response to a suspend event
		if (this.playinbackground)
			return;
		
		// upon resume: first resume the whole context
		if (!s && context && context["resume"])
		{
			context["resume"]();
			isContextSuspended = false;
		}
		
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
			audioInstances[i].setSuspended(s);
		
		// after suspend: also suspend the whole context
		if (s && context && context["suspend"])
		{
			context["suspend"]();
			isContextSuspended = true;
		}
	};
	
	instanceProto.tick = function ()
	{
		var dt = this.runtime.dt;
		
		// Check for audio instances which have finished and trigger OnEnded as appropriate
		var i, len, a;
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			a = audioInstances[i];
			a.tick(dt);
			
			// Update time scales
			if (timescale_mode !== 0)
				a.updatePlaybackRate();
		}
		
		// keep flanger/phaser effects cycling
		var p, arr, f;
		for (p in effects)
		{
			if (effects.hasOwnProperty(p))
			{
				arr = effects[p];
				for (i = 0, len = arr.length; i < len; i++)
				{
					f = arr[i];
					if (f.tick)
						f.tick();
				}
			}
		}
		
		// keep listener up to date
		if (api === API_WEBAUDIO && this.listenerTracker.hasObject())
		{
			this.listenerTracker.tick(dt);
			listenerX = this.listenerTracker.obj.x;
			listenerY = this.listenerTracker.obj.y;
			context["listener"]["setPosition"](this.listenerTracker.obj.x, this.listenerTracker.obj.y, this.listenerZ);
		}
	};
	
	// For preloading audio at project start
	var preload_list = [];
	
	instanceProto.setPreloadList = function (arr)
	{
		var i, len, p, size;
		var total_size = 0;
		
		// Note runtime passes list of preferred formats so we simply preload everything given in the list
		for (i = 0, len = arr.length; i < len; ++i)
		{
			p = arr[i];
			
			// Double the reported size of the audio tracks. Audio has the additional step of needing
			// decoding after downloading, which images don't have to do, so audio should be more highly
			// represented in the loading bar. Downloaded but not yet decoded audio is reported as half
			// progress, so this actually reports downloading audio with the same weight as downloading
			// images, but with an additional decode job after it of the same size.
			size = p.size * 2;
			
			preload_list.push({
				filename: p.filename,
				size: size,
				type: p.type,
				obj: null
			});
			
			total_size += size;
		}
		
		return total_size;
	};
	
	instanceProto.startPreloads = function ()
	{
		var i, len, p, src;
		for (i = 0, len = preload_list.length; i < len; ++i)
		{
			p = preload_list[i];
			src = this.runtime.getProjectFileUrl(p.filename);
			p.obj = this.getAudioBuffer({
				url: src,
				type: p.type
			}, false);
		}
	};
	
	instanceProto.getPreloadedSize = function ()
	{
		var completed = 0;
		
		var i, len, p;
		for (i = 0, len = preload_list.length; i < len; ++i)
		{
			p = preload_list[i];
			
			// still count files that have failed to load, otherwise the loading progress hangs
			// don't trust Android stock browser to report this correctly (see https://www.scirra.com/forum/audio-causes-android-browser-to-hang-r178_t114743)
			if (p.obj.isLoadedAndDecoded() || p.obj.hasFailedToLoad() || this.runtime.isAndroidStockBrowser)
			{
				completed += p.size;
			}
			else if (p.obj.isLoaded())	// downloaded but not decoded: only happens in Web Audio API, count as half-way progress
			{
				completed += Math.floor(p.size / 2);
			}
		};
		
		return completed;
	};
	
	instanceProto.releaseAllMusicBuffers = function ()
	{
		var i, len, j, b;
		
		for (i = 0, j = 0, len = audioBuffers.length; i < len; ++i)
		{
			b = audioBuffers[i];
			audioBuffers[j] = b;
			
			if (b.is_music)
				b.release();
			else
				++j;		// keep
		}
		
		audioBuffers.length = j;
	};
	
	// find an existing audio buffer for the given source, else create a new one and return that
	instanceProto.getAudioBuffer = function (info, is_music, dont_create)
	{
		var i, len, a, ret = null, j, k, lenj, ai;
		var src = info.url;
		
		// Try to find existing buffer with same source
		for (i = 0, len = audioBuffers.length; i < len; i++)
		{
			a = audioBuffers[i];
			
			if (a.src === src)
			{
				ret = a;
				break;
			}
		}
		
		// Couldn't find it - add a new one and return it, unless dont_create is set.
		if (!ret && !dont_create)
		{
			// If playing music as sound, release all the old music buffers before adding a new music buffer.
			// This prevents decompressed music tracks accumulating in memory.
			if ((playMusicAsSoundWorkaround || hasAnySoftwareDecodedMusic) && is_music)
				this.releaseAllMusicBuffers();
			
			ret = new C2AudioBuffer(src, info.type, is_music);
			audioBuffers.push(ret);
		}
		
		return ret;
	};
	
	instanceProto.getAudioInstance = function (info, tag, is_music, looping, vol)
	{
		var i, len, a;
		var src = info.url;
		
		// Try to find existing recyclable instance from the same source
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			a = audioInstances[i];
			
			if (a.src === src && (a.canBeRecycled() || is_music))
			{
				a.tag = tag;
				return a;
			}
		}
		
		// Otherwise create a new instance
		var b = this.getAudioBuffer(info, is_music);
		
		// Not yet ready
		if (!b.bufferObject)
		{
			// Play once received
			if (tag !== "<preload>")
			{
				b.playTagWhenReady = tag;
				b.loopWhenReady = looping;
				b.volumeWhenReady = vol;
			}
				
			return null;
		}
			
		a = new C2AudioInstance(b, tag);
		audioInstances.push(a);
		return a;
	};
	
	var taggedAudio = [];
	
	function SortByIsPlaying(a, b)
	{
		var an = a.isPlaying() ? 1 : 0;
		var bn = b.isPlaying() ? 1 : 0;
		
		if (an === bn)
			return 0;
		else if (an < bn)
			return 1;
		else
			return -1;
	};
	
	function getAudioByTag(tag, sort_by_playing)
	{
		cr.clearArray(taggedAudio);
		
		// Empty tag: return last audio, if playing
		if (!tag.length)
		{
			if (!lastAudio || lastAudio.hasEnded())
				return;
			else
			{
				cr.clearArray(taggedAudio);
				taggedAudio[0] = lastAudio;
				return;
			}
		}
		
		var i, len, a;
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			a = audioInstances[i];
			
			if (cr.equals_nocase(tag, a.tag))
				taggedAudio.push(a);
		}
		
		if (sort_by_playing)
			taggedAudio.sort(SortByIsPlaying);
	};
	
	function reconnectEffects(tag)
	{
		var i, len, arr, n, toNode = context["destination"];
		
		if (effects.hasOwnProperty(tag))
		{
			arr = effects[tag];
			
			if (arr.length)
			{
				toNode = arr[0].getInputNode();
				
				for (i = 0, len = arr.length; i < len; i++)
				{
					n = arr[i];
					
					// last node connects to destination
					if (i + 1 === len)
						n.connectTo(context["destination"]);
					// otherwise connect to next effects
					else
						n.connectTo(arr[i + 1].getInputNode());
				}
			}
		}
		
		getAudioByTag(tag);
		
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].reconnect(toNode);
		
		// Reconnect microphone input if active
		if (micSource && micTag === tag)
		{
			micSource["disconnect"]();
			micSource["connect"](toNode);
		}
	};
	
	function addEffectForTag(tag, fx)
	{
		if (!effects.hasOwnProperty(tag))
			effects[tag] = [fx];
		else
			effects[tag].push(fx);
			
		reconnectEffects(tag);
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.OnEnded = function (t)
	{
		return cr.equals_nocase(audTag, t);
	};
	
	Cnds.prototype.PreloadsComplete = function ()
	{
		var i, len;
		for (i = 0, len = audioBuffers.length; i < len; i++)
		{
			if (!audioBuffers[i].isLoadedAndDecoded() && !audioBuffers[i].hasFailedToLoad())
				return false;
		}
		
		return true;
	};
	
	Cnds.prototype.AdvancedAudioSupported = function ()
	{
		return api === API_WEBAUDIO;
	};
	
	Cnds.prototype.IsSilent = function ()
	{
		return silent;
	};
	
	Cnds.prototype.IsAnyPlaying = function ()
	{
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			if (audioInstances[i].isPlaying())
				return true;
		}
		
		return false;
	};
	
	Cnds.prototype.IsTagPlaying = function (tag)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
		{
			if (taggedAudio[i].isPlaying())
				return true;
		}
		
		return false;
	};
	
	pluginProto.cnds = new Cnds();

	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.Play = function (file, looping, vol, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
			return;
		
		lastAudio.setPannerEnabled(false);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtPosition = function (file, looping, vol, x_, y_, angle_, innerangle_, outerangle_, outergain_, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
		
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ x: x_, y: y_, a: angle_, ia: innerangle_, oa: outerangle_, og: dbToLinear(outergain_), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		lastAudio.setPan(x_, y_, angle_, innerangle_, outerangle_, dbToLinear(outergain_));
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtObject = function (file, looping, vol, obj, innerangle, outerangle, outergain, tag)
	{
		if (silent || !obj)
			return;
			
		var inst = obj.getFirstPicked();
		if (!inst)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ obj: inst, ia: innerangle, oa: outerangle, og: dbToLinear(outergain), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		var px = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, true);
		var py = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, false);
		lastAudio.setPan(px, py, cr.to_degrees(inst.angle - inst.layer.getAngle()), innerangle, outerangle, dbToLinear(outergain));
		lastAudio.setObject(inst);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayByName = function (folder, filename, looping, vol, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
			return;
		
		lastAudio.setPannerEnabled(false);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtPositionByName = function (folder, filename, looping, vol, x_, y_, angle_, innerangle_, outerangle_, outergain_, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ x: x_, y: y_, a: angle_, ia: innerangle_, oa: outerangle_, og: dbToLinear(outergain_), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		lastAudio.setPan(x_, y_, angle_, innerangle_, outerangle_, dbToLinear(outergain_));
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtObjectByName = function (folder, filename, looping, vol, obj, innerangle, outerangle, outergain, tag)
	{
		if (silent || !obj)
			return;
			
		var inst = obj.getFirstPicked();
		if (!inst)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ obj: inst, ia: innerangle, oa: outerangle, og: dbToLinear(outergain), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		var px = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, true);
		var py = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, false);
		lastAudio.setPan(px, py, cr.to_degrees(inst.angle - inst.layer.getAngle()), innerangle, outerangle, dbToLinear(outergain));
		lastAudio.setObject(inst);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.SetLooping = function (tag, looping)
	{
		getAudioByTag(tag);
		
		// 0 = enable looping, 1 = disable looping
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setLooping(looping === 0);
	};
	
	Acts.prototype.SetMuted = function (tag, muted)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setMuted(muted === 0);
	};
	
	Acts.prototype.SetVolume = function (tag, vol)
	{
		getAudioByTag(tag);
		
		var v = dbToLinear(vol);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setVolume(v);
	};
	
	Acts.prototype.Preload = function (file)
	{
		if (silent)
			return;
			
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		if (api === API_CORDOVA)
		{
			// can't preload with Cordova's Media API
			return;
		}
		
		// Otherwise just request the object without doing anything to it - will be added to the caches
		this.getAudioInstance(info, "<preload>", is_music, false);
	};
	
	Acts.prototype.PreloadByName = function (folder, filename)
	{
		if (silent)
			return;
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		if (api === API_CORDOVA)
		{
			// can't preload with Cordova's Media API
			return;
		}
		
		// Otherwise just request the object without doing anything to it - will be added to the caches
		this.getAudioInstance(info, "<preload>", is_music, false);
	};
	
	Acts.prototype.SetPlaybackRate = function (tag, rate)
	{
		getAudioByTag(tag);
		
		// Only support forwards playback
		if (rate < 0.0)
			rate = 0;
			
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setPlaybackRate(rate);
	};
	
	Acts.prototype.Stop = function (tag)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].stop();
	};
	
	Acts.prototype.StopAll = function ()
	{
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
			audioInstances[i].stop();
	};
	
	Acts.prototype.SetPaused = function (tag, state)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
		{
			if (state === 0)
				taggedAudio[i].pause();
			else
				taggedAudio[i].resume();
		}
	};
	
	Acts.prototype.Seek = function (tag, pos)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
		{
			taggedAudio[i].seek(pos);
		}
	};
	
	Acts.prototype.SetSilent = function (s)
	{
		var i, len;
		
		if (s === 2)					// toggling
			s = (silent ? 1 : 0);		// choose opposite state
		
		if (s === 0 && !silent)			// setting silent
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
				audioInstances[i].setSilent(true);
			
			silent = true;
		}
		else if (s === 1 && silent)		// setting not silent
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
				audioInstances[i].setSilent(false);
			
			silent = false;
		}
	};
	
	Acts.prototype.SetMasterVolume = function (vol)
	{
		masterVolume = dbToLinear(vol);
		
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
			audioInstances[i].updateVolume();
	};

	Acts.prototype.AddFilterEffect = function (tag, type, freq, detune, q, gain, mix)
	{
		if (api !== API_WEBAUDIO || type < 0 || type >= filterTypes.length || !context["createBiquadFilter"])
			return;
		
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new FilterEffect(type, freq, detune, q, gain, mix));
	};
	
	Acts.prototype.AddDelayEffect = function (tag, delay, gain, mix)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new DelayEffect(delay, dbToLinear(gain), mix));
	};
	
	Acts.prototype.AddFlangerEffect = function (tag, delay, modulation, freq, feedback, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new FlangerEffect(delay / 1000, modulation / 1000, freq, feedback / 100, mix));
	};
	
	Acts.prototype.AddPhaserEffect = function (tag, freq, detune, q, mod, modfreq, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new PhaserEffect(freq, detune, q, mod, modfreq, mix));
	};
	
	Acts.prototype.AddConvolutionEffect = function (tag, file, norm, mix)
	{
		if (api !== API_WEBAUDIO || !context["createConvolver"])
			return;
		
		var doNormalize = (norm === 0);
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		var b = this.getAudioBuffer(info, false);
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		
		var fx;
		
		// buffer loaded
		if (b.bufferObject)
		{
			fx = new ConvolveEffect(b.bufferObject, doNormalize, mix, info.url);
		}
		// else assign once loaded
		else
		{
			fx = new ConvolveEffect(null, doNormalize, mix, info.url);
			b.normalizeWhenReady = doNormalize;
			b.convolveWhenReady = fx;
		}
		
		addEffectForTag(tag, fx);
	};
	
	Acts.prototype.AddGainEffect = function (tag, g)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new GainEffect(dbToLinear(g)));
	};
	
	Acts.prototype.AddMuteEffect = function (tag)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new GainEffect(0));	// re-use gain effect with 0 gain
	};
	
	Acts.prototype.AddTremoloEffect = function (tag, freq, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new TremoloEffect(freq, mix));
	};
	
	Acts.prototype.AddRingModEffect = function (tag, freq, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new RingModulatorEffect(freq, mix));
	};
	
	Acts.prototype.AddDistortionEffect = function (tag, threshold, headroom, drive, makeupgain, mix)
	{
		if (api !== API_WEBAUDIO || !context["createWaveShaper"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new DistortionEffect(threshold, headroom, drive, makeupgain, mix));
	};
	
	Acts.prototype.AddCompressorEffect = function (tag, threshold, knee, ratio, attack, release)
	{
		if (api !== API_WEBAUDIO || !context["createDynamicsCompressor"])
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new CompressorEffect(threshold, knee, ratio, attack / 1000, release / 1000));
	};
	
	Acts.prototype.AddAnalyserEffect = function (tag, fftSize, smoothing)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new AnalyserEffect(fftSize, smoothing));
	};
	
	Acts.prototype.RemoveEffects = function (tag)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		var i, len, arr;
		
		if (effects.hasOwnProperty(tag))
		{
			arr = effects[tag];
			
			if (arr.length)
			{
				for (i = 0, len = arr.length; i < len; i++)
					arr[i].remove();
				
				cr.clearArray(arr);
				reconnectEffects(tag);
			}
		}
	};
	
	Acts.prototype.SetEffectParameter = function (tag, index, param, value, ramp, time)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var arr;
		
		if (!effects.hasOwnProperty(tag))
			return;
		
		arr = effects[tag];
		
		if (index < 0 || index >= arr.length)
			return;
			
		arr[index].setParam(param, value, ramp, time);
	};
	
	Acts.prototype.SetListenerObject = function (obj_)
	{
		if (!obj_ || api !== API_WEBAUDIO)
			return;
			
		var inst = obj_.getFirstPicked();
		
		if (!inst)
			return;
			
		this.listenerTracker.setObject(inst);
		listenerX = inst.x;
		listenerY = inst.y;
	};
	
	Acts.prototype.SetListenerZ = function (z)
	{
		this.listenerZ = z;
	};
	
	Acts.prototype.ScheduleNextPlay = function (t)
	{
		if (!context)
			return;		// needs Web Audio API
		
		this.nextPlayTime = t;
	};
	
	Acts.prototype.UnloadAudio = function (file)
	{
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		var b = this.getAudioBuffer(info, is_music, true /* don't create if missing */);
		
		if (!b)
			return;		// not loaded
		
		b.release();
		cr.arrayFindRemove(audioBuffers, b);
	};
	
	Acts.prototype.UnloadAudioByName = function (folder, filename)
	{
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		var b = this.getAudioBuffer(info, is_music, true /* don't create if missing */);
		
		if (!b)
			return;		// not loaded
		
		b.release();
		cr.arrayFindRemove(audioBuffers, b);
	};
	
	Acts.prototype.UnloadAll = function ()
	{
		var i, len;
		
		for (i = 0, len = audioBuffers.length; i < len; ++i)
		{
			audioBuffers[i].release();
		};
		
		cr.clearArray(audioBuffers);
	};
	
	pluginProto.acts = new Acts();

	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.Duration = function (ret, tag)
	{
		getAudioByTag(tag, true);
		
		if (taggedAudio.length)
			ret.set_float(taggedAudio[0].getDuration());
		else
			ret.set_float(0);
	};
	
	Exps.prototype.PlaybackTime = function (ret, tag)
	{
		getAudioByTag(tag, true);
		
		if (taggedAudio.length)
			ret.set_float(taggedAudio[0].getPlaybackTime(true));
		else
			ret.set_float(0);
	};
	
	Exps.prototype.Volume = function (ret, tag)
	{
		getAudioByTag(tag, true);
		
		if (taggedAudio.length)
		{
			var v = taggedAudio[0].getVolume();
			ret.set_float(linearToDb(v));
		}
		else
			ret.set_float(0);
	};
	
	Exps.prototype.MasterVolume = function (ret)
	{
		ret.set_float(linearToDb(masterVolume));
	};
	
	Exps.prototype.EffectCount = function (ret, tag)
	{
		tag = tag.toLowerCase();
		var arr = null;
		
		if (effects.hasOwnProperty(tag))
			arr = effects[tag];
			
		ret.set_int(arr ? arr.length : 0);
	};
	
	function getAnalyser(tag, index)
	{
		var arr = null;
		
		if (effects.hasOwnProperty(tag))
			arr = effects[tag];
			
		if (arr && index >= 0 && index < arr.length && arr[index].freqBins)
			return arr[index];
		else
			return null;
	};
	
	Exps.prototype.AnalyserFreqBinCount = function (ret, tag, index)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var analyser = getAnalyser(tag, index);
			
		ret.set_int(analyser ? analyser.node["frequencyBinCount"] : 0);
	};
	
	Exps.prototype.AnalyserFreqBinAt = function (ret, tag, index, bin)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		bin = Math.floor(bin);
		var analyser = getAnalyser(tag, index);
			
		if (!analyser)
			ret.set_float(0);
		else if (bin < 0 || bin >= analyser.node["frequencyBinCount"])
			ret.set_float(0);
		else
			ret.set_float(analyser.freqBins[bin]);
	};
	
	Exps.prototype.AnalyserPeakLevel = function (ret, tag, index)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var analyser = getAnalyser(tag, index);
			
		if (analyser)
			ret.set_float(analyser.peak);
		else
			ret.set_float(0);
	};
	
	Exps.prototype.AnalyserRMSLevel = function (ret, tag, index)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var analyser = getAnalyser(tag, index);
			
		if (analyser)
			ret.set_float(analyser.rms);
		else
			ret.set_float(0);
	};
	
	Exps.prototype.SampleRate = function (ret)
	{
		ret.set_int(context ? context.sampleRate : 0);
	};
	
	Exps.prototype.CurrentTime = function (ret)
	{
		// fall back to now time if web audio API not available
		ret.set_float(context ? context.currentTime : cr.performance_now());
	};
	
	pluginProto.exps = new Exps();

}());

// Sprite font
// ECMAScript 5 strict mode
/* global cr,log,assert2 */
/* jshint globalstrict: true */
/* jshint strict: true */

;
;

/////////////////////////////////////
// Plugin class
cr.plugins_.Spritefont2 = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var pluginProto = cr.plugins_.Spritefont2.prototype;

	pluginProto.onCreate = function ()
	{
	};

	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
		if (this.is_family)
			return;

		this.texture_img = this.runtime.findWaitingTexture(this.texture_file);
		
		if (!this.texture_img)
		{
			this.texture_img = new Image();
			this.texture_img.cr_src = this.texture_file;
			this.texture_img.cr_filesize = this.texture_filesize;
			this.texture_img.c2webGL_texture = null;
			this.runtime.waitForImageLoad(this.texture_img, this.texture_file);
		}
		
		this.spriteX = this.texture_data[3];
		this.spriteY = this.texture_data[4];
		this.spriteWidth = this.texture_data[5];
		this.spriteHeight = this.texture_data[6];
		
		this.webGL_texture = null;
	};

	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;

		this.webGL_texture = null;
	};

	typeProto.onRestoreWebGLContext = function ()
	{
		// No need to create textures if no instances exist, will create on demand
		if (this.is_family || !this.instances.length)
			return;

		if (!this.webGL_texture)
		{
			this.webGL_texture = this.runtime.glwrap.loadTexture(this.texture_img, false, this.runtime.linearSampling, this.texture_pixelformat);
		}

		var i, len;
		for (i = 0, len = this.instances.length; i < len; i++)
			this.instances[i].webGL_texture = this.webGL_texture;
	};

	typeProto.unloadTextures = function ()
	{
		// Don't release textures if any instances still exist, they are probably using them
		if (this.is_family || this.instances.length || !this.webGL_texture)
			return;

		this.runtime.glwrap.deleteTexture(this.webGL_texture);
		this.webGL_texture = null;
	};

	typeProto.preloadCanvas2D = function (ctx)
	{
		// draw to preload, browser should lazy load the texture
		ctx.drawImage(this.texture_img, 0, 0);
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
	};

	var instanceProto = pluginProto.Instance.prototype;

	instanceProto.onDestroy = function()
	{
		// recycle the instance's objects
		freeAllLines (this.lines);
		freeAllClip  (this.clipList);
		freeAllClipUV(this.clipUV);
		cr.wipe(this.characterWidthList);
	};

	instanceProto.onCreate = function()
	{
		this.texture_img      = this.type.texture_img;
		this.text             = this.properties[0];
		this.characterWidth   = this.properties[1];
		this.characterHeight  = this.properties[2];
		this.characterSet     = this.properties[3];
		var spacingData = this.properties[4];
		this.characterScale   = this.properties[5];
		this.characterSpacing = this.properties[6];
		this.lineHeight       = this.properties[7];
		this.halign           = this.properties[8]/2.0;		// 0=left, 1=center, 2=right
		this.valign           = this.properties[9]/2.0;		// 0=top, 1=center, 2=bottom
		this.wrapbyword       = (this.properties[10] === 0);	// 0=word, 1=character
		this.visible          = this.properties[11];
		this.textWidth  = 0;
		this.textHeight = 0;

		// Use recycled properties to avoid garbage
		if (this.recycled)
		{
			cr.clearArray(this.lines);
			cr.wipe(this.clipList);
			cr.wipe(this.clipUV);
			cr.wipe(this.characterWidthList);
		}
		else
		{
			this.lines = [];
			this.clipList = {};
			this.clipUV = {};
			this.characterWidthList = {};
		}
		
		// only update text if it changes
		this.text_changed = true;
		
		// only update line calculations if this change
		this.lastwrapwidth = this.width;

		if (this.runtime.glwrap)
		{
			// Create WebGL texture if type doesn't have it yet
			if (!this.type.webGL_texture)
			{
				this.type.webGL_texture = this.runtime.glwrap.loadTexture(this.type.texture_img, false, this.runtime.linearSampling, this.type.texture_pixelformat);
			}

			this.webGL_texture = this.type.webGL_texture;
		}
		
		this.LoadCharacterSpacingData(spacingData);

		this.SplitSheet();
	};

	instanceProto.saveToJSON = function ()
	{
		var save = {
			"t": this.text,
			"csc": this.characterScale,
			"csp": this.characterSpacing,
			"lh": this.lineHeight,
			"tw": this.textWidth,
			"th": this.textHeight,
			"lrt": this.last_render_tick,
			"ha": this.halign,
			"va": this.valign,
			"cw": {}
		};

		for (var ch in this.characterWidthList)
			save["cw"][ch] = this.characterWidthList[ch];

		return save;
	};

	instanceProto.loadFromJSON = function (o)
	{
		this.text = o["t"];
		this.characterScale = o["csc"];
		this.characterSpacing = o["csp"];
		this.lineHeight = o["lh"];
		this.textWidth = o["tw"];
		this.textHeight = o["th"];
		this.last_render_tick = o["lrt"];
		
		// alignment properties added for r205
		if (o.hasOwnProperty("ha"))
			this.halign = o["ha"];
		
		if (o.hasOwnProperty("va"))
			this.valign = o["va"];

		for(var ch in o["cw"])
			this.characterWidthList[ch] = o["cw"][ch];

		this.text_changed = true;
		this.lastwrapwidth = this.width;
	};


	function trimRight(text)
	{
		return text.replace(/\s\s*$/, '');
	}

	// return what's in the cache
	// if the cache is empty, return a new object 
	// based on the given Constructor
	var MAX_CACHE_SIZE = 1000;
	function alloc(cache,Constructor)
	{
		if (cache.length)
			return cache.pop();
		else
			return new Constructor();
	}

	// store the data in the cache
	function free(cache,data)
	{
		if (cache.length < MAX_CACHE_SIZE)
		{
			cache.push(data);
		}
	}

	// store all the data from dataList in the cache
	// and wipe dataList
	function freeAll(cache,dataList,isArray)
	{
		if (isArray) {
			var i, len;
			for (i = 0, len = dataList.length; i < len; i++)
			{
				free(cache,dataList[i]);
			}
			cr.clearArray(dataList);
		} else {
			var prop;
			for(prop in dataList) {
				if(Object.prototype.hasOwnProperty.call(dataList,prop)) {
					free(cache,dataList[prop]);
					delete dataList[prop];
				}
			}
		}
	}

	function addLine(inst,lineIndex,cur_line) {
		var lines = inst.lines;
		var line;
		cur_line = trimRight(cur_line);
		// Recycle a line if possible
		if (lineIndex >= lines.length)
			lines.push(allocLine());

		line = lines[lineIndex];
		line.text = cur_line;
		line.width = inst.measureWidth(cur_line);
		inst.textWidth = cr.max(inst.textWidth,line.width);
	}

	var linesCache = [];
	function allocLine()       { return alloc(linesCache,Object); }
	function freeLine(l)       { free(linesCache,l); }
	function freeAllLines(arr) { freeAll(linesCache,arr,true); }


	function addClip(obj,property,x,y,w,h) {
		if (obj[property] === undefined) {
			obj[property] = alloc(clipCache,Object);
		}

		obj[property].x = x;
		obj[property].y = y;
		obj[property].w = w;
		obj[property].h = h;
	}
	var clipCache = [];
	function allocClip()      { return alloc(clipCache,Object); }
	function freeAllClip(obj) { freeAll(clipCache,obj,false);}

	function addClipUV(obj,property,left,top,right,bottom) {
		if (obj[property] === undefined) {
			obj[property] = alloc(clipUVCache,cr.rect);
		}

		obj[property].left   = left;
		obj[property].top    = top;
		obj[property].right  = right;
		obj[property].bottom = bottom;
	}
	var clipUVCache = [];
	function allocClipUV()      { return alloc(clipUVCache,cr.rect);}
	function freeAllClipUV(obj) { freeAll(clipUVCache,obj,false);}


	instanceProto.SplitSheet = function() {
		// Create Clipping regions for each letters of the spritefont sheet
		var texture      = this.texture_img;
		var texWidth	 = this.texture_img.width;
		var texHeight    = this.texture_img.height;
		var spriteWidth  = this.type.spriteWidth;
		var spriteHeight = this.type.spriteHeight;
		var offx		 = this.type.spriteX;
		var offy		 = this.type.spriteY;
		var charWidth    = this.characterWidth;
		var charHeight   = this.characterHeight;
		var offu         = offx / texWidth;
		var offv         = offy / texHeight;
		var charU        = charWidth / texWidth;
		var charV        = charHeight / texHeight;
		var charSet      = this.characterSet ;

		var cols = Math.floor(spriteWidth/charWidth);
		var rows = Math.floor(spriteHeight/charHeight);

		for ( var c = 0; c < charSet.length; c++) {
			// not enough texture space
			if  (c >= cols * rows) break;

			// create clipping coordinates for each characters
			var x = c%cols;
			var y = Math.floor(c/cols);
			var letter = charSet.charAt(c);
			if (this.runtime.glwrap) {
				addClipUV(
					this.clipUV, letter,
					x * charU + offu,
					y * charV + offv,
					(x+1) * charU + offu,
					(y+1) * charV + offv
				);
			} else {
				addClip(
					this.clipList, letter,
					x * charWidth + offx,
					y * charHeight + offy,
					charWidth,
					charHeight
				);
			}
		}
	};

	/*
     *	Word-Wrapping
     */

	var wordsCache = [];
	pluginProto.TokeniseWords = function (text)
	{
		cr.clearArray(wordsCache);
		var cur_word = "";
		var ch;

		// Loop every char
		var i = 0;

		while (i < text.length)
		{
			ch = text.charAt(i);

			if (ch === "\n")
			{
				// Dump current word if any
				if (cur_word.length)
				{
					wordsCache.push(cur_word);
					cur_word = "";
				}

				// Add newline word
				wordsCache.push("\n");

				++i;
			}
			// Whitespace or hyphen: swallow rest of whitespace and include in word
			else if (ch === " " || ch === "\t" || ch === "-")
			{
				do {
					cur_word += text.charAt(i);
					i++;
				}
				while (i < text.length && (text.charAt(i) === " " || text.charAt(i) === "\t"));

				wordsCache.push(cur_word);
				cur_word = "";
			}
			else if (i < text.length)
			{
				cur_word += ch;
				i++;
			}
		}

		// Append leftover word if any
		if (cur_word.length)
			wordsCache.push(cur_word);
	};


	pluginProto.WordWrap = function (inst)
	{
		var text = inst.text;
		var lines = inst.lines;

		if (!text || !text.length)
		{
			freeAllLines(lines);
			return;
		}

		var width = inst.width;
		if (width <= 2.0)
		{
			freeAllLines(lines);
			return;
		}


		// If under 100 characters (i.e. a fairly short string), try a short string optimisation: just measure the text
		// and see if it fits on one line, without going through the tokenise/wrap.
		// Text musn't contain a linebreak!
		var charWidth = inst.characterWidth;
		var charScale = inst.characterScale;
		var charSpacing = inst.characterSpacing;
		if ( (text.length * (charWidth * charScale + charSpacing) - charSpacing) <= width && text.indexOf("\n") === -1)
		{
			var all_width = inst.measureWidth(text);

			if (all_width <= width)
			{
				// fits on one line
				freeAllLines(lines);
				lines.push(allocLine());
				lines[0].text = text;
				lines[0].width = all_width;
				inst.textWidth  = all_width;
				inst.textHeight = inst.characterHeight * charScale + inst.lineHeight;
				return;
			}
		}

		var wrapbyword = inst.wrapbyword;

		this.WrapText(inst);
		inst.textHeight = lines.length * (inst.characterHeight * charScale + inst.lineHeight);
	};

	pluginProto.WrapText = function (inst)
	{
		var wrapbyword = inst.wrapbyword;
		var text       = inst.text;
		var lines      = inst.lines;
		var width      = inst.width;

		var wordArray;
		if (wrapbyword) {
			this.TokeniseWords(text);	// writes to wordsCache
			wordArray = wordsCache;
		} else {
			wordArray = text;
		}
		var cur_line = "";
		var prev_line;
		var line_width;
		var i;
		var lineIndex = 0;
		var line;
		var ignore_newline = false;

		for (i = 0; i < wordArray.length; i++)
		{
			// Look for newline
			if (wordArray[i] === "\n")
			{
				if (ignore_newline === true) {
					// if a newline as been added by the wrapping
					// we ignore any happening just after
					ignore_newline = false;
				} else {
					// Flush line.  
					addLine(inst,lineIndex,cur_line);
					lineIndex++;
				}
				cur_line = "";
				continue;
			}
			ignore_newline = false;

			// Otherwise add to line
			prev_line = cur_line;
			cur_line += wordArray[i];

			// Measure line
			line_width = inst.measureWidth(trimRight(cur_line));

			// Line too long: wrap the line before this word was added
			if (line_width > width)
			{

				if (prev_line === "") {
					// if it's the first word, we push it on the line
					// to avoid an unnecessary blank line
					// and since we are wrapping, we ignore the next newline if any
					addLine(inst,lineIndex,cur_line);
					cur_line = "";
					ignore_newline = true;
				} else {
					// else we push the previous line
					addLine(inst,lineIndex,prev_line);
					cur_line = wordArray[i];
				}

				lineIndex++;

				// Wrapping by character: avoid lines starting with spaces
				if (!wrapbyword && cur_line === " ")
					cur_line = "";
			}
		}

		// Add any leftover line
		if (trimRight(cur_line).length)
		{
			addLine(inst,lineIndex,cur_line);

			lineIndex++;
		}

		// truncate lines to the number that were used. recycle any spare line objects
		for (i = lineIndex; i < lines.length; i++)
			freeLine(lines[i]);

		lines.length = lineIndex;
	};

	instanceProto.measureWidth = function(text) {
		var spacing = this.characterSpacing;
		var len     = text.length;
		var width   = 0;
		for (var i = 0; i < len; i++) {
			width += this.getCharacterWidth(text.charAt(i)) * this.characterScale + spacing;
		}
		// we remove the trailing spacing
		width -= (width > 0) ? spacing : 0;
		return width;
	};

	/***/


	instanceProto.getCharacterWidth = function(character) {
		var widthList = this.characterWidthList;
		if (widthList[character] !== undefined) {
			// special width
			return widthList[character];
		} else {
			// common width
			return this.characterWidth;
		}
	};

	instanceProto.rebuildText = function() {
		// If text has changed, run the word wrap.
		if (this.text_changed || this.width !== this.lastwrapwidth) {
			this.textWidth = 0;
			this.textHeight = 0;
			this.type.plugin.WordWrap(this);
			this.text_changed = false;
			this.lastwrapwidth = this.width;
		}
	};
    
    // to handle floating point imprecision
	var EPSILON = 0.00001;
	instanceProto.draw = function(ctx, glmode)
	{
		var texture = this.texture_img;
		if (this.text !== "" && texture != null) {

			//console.log("draw");

			this.rebuildText();

			// textWidth and textHeight needs to be calculated here
			// since we can early exit if bounding box is too tiny to draw anything
			// be we would still like to know the dimension of the text according to current width
			if (this.height < this.characterHeight*this.characterScale + this.lineHeight) {
				return;
			}

			ctx.globalAlpha = this.opacity;


			var myx = this.x;
			var myy = this.y;

			if (this.runtime.pixel_rounding)
			{
				myx = Math.round(myx);
				myy = Math.round(myy);
			}
			
			// Viewport dimensions
			var viewLeft = this.layer.viewLeft;
			var viewTop = this.layer.viewTop;
			var viewRight = this.layer.viewRight;
			var viewBottom = this.layer.viewBottom;

			ctx.save();
			ctx.translate(myx, myy);
			ctx.rotate(this.angle);

			// convert alignement properties to some usable values
			// useful parameters
			var angle      = this.angle;
			var ha         = this.halign;
			var va         = this.valign;
			var scale      = this.characterScale;
			var charHeight = this.characterHeight * scale;
			var lineHeight = this.lineHeight;
			var charSpace  = this.characterSpacing;
			var lines = this.lines;
			var textHeight = this.textHeight;
			var letterWidth;

			// we compute the offsets for vertical alignement in object-space
			// but it can't be negative, else it would underflow the boundingbox
			// horizontal alignement is evaluated for each line
			var halign;
			var valign = va * cr.max(0,(this.height - textHeight));

			// we get the position of the top left corner of the bounding box
			var offx = -(this.hotspotX * this.width);
			var offy = -(this.hotspotY * this.height);
			// we add to that any extra offset 
			// for vertical alignement
			offy += valign;

			var drawX ;
			var drawY = offy;
			var roundX, roundY;

			for(var i = 0; i < lines.length; i++) {
				// for horizontal alignement, we need to work line by line
				var line = lines[i].text;
				var len  = lines[i].width;

				// compute horizontal empty space
				// offset drawX according to horizontal alignement
				// indentation could be negative if long word in wrapbyword mode
				halign = ha * cr.max(0,this.width - len);		
				drawX = offx + halign;

				// we round to avoid pixel blurring
				drawY += lineHeight;
				
				// above viewport: skip rendering this line
				if (angle === 0 && myy + drawY + charHeight < viewTop)
				{
					drawY += charHeight;
					continue;
				}
			
				for(var j = 0; j < line.length; j++) {

					var letter = line.charAt(j);
					letterWidth = this.getCharacterWidth(letter);
					// we skip unrecognized characters (creates a space)
					var clip = this.clipList[letter];
					
					// still off to the left of the viewport: skip drawing this character
					if (angle === 0 && myx + drawX + letterWidth * scale + charSpace < viewLeft)
					{
						drawX += letterWidth * scale + charSpace;
						continue;
					}

					// check if next letter fits in bounding box
					if ( drawX + letterWidth * scale > this.width + EPSILON ) {
						break;
					}

					if (clip !== undefined) {

						roundX = drawX;
						roundY = drawY;
						
						if (angle === 0 && scale === 1)
						{
							roundX = Math.round(roundX);
							roundY = Math.round(roundY);
						}
						
						ctx.drawImage( this.texture_img,
									 clip.x, clip.y, clip.w, clip.h,
									 roundX,roundY,clip.w*scale,clip.h*scale);
					}

					drawX += letterWidth * scale + charSpace;
					
					// Line extended off viewport to right: skip drawing rest of line
					if (angle === 0 && myx + drawX > viewRight)
						break;
				}
				drawY += charHeight;

				// check if next row fits in bounding box and viewport and quit drawing if so
				if (angle === 0 && (drawY + charHeight + lineHeight > this.height || myy + drawY > viewBottom))
				{
					break;
				}
			}
			ctx.restore();
		}

	};

	// drawingQuad
	var dQuad = new cr.quad();

	function rotateQuad(quad,cosa,sina) {
		var x_temp;

		x_temp   = (quad.tlx * cosa) - (quad.tly * sina);
		quad.tly = (quad.tly * cosa) + (quad.tlx * sina);
		quad.tlx = x_temp;

		x_temp    = (quad.trx * cosa) - (quad.try_ * sina);
		quad.try_ = (quad.try_ * cosa) + (quad.trx * sina);
		quad.trx  = x_temp;

		x_temp   = (quad.blx * cosa) - (quad.bly * sina);
		quad.bly = (quad.bly * cosa) + (quad.blx * sina);
		quad.blx = x_temp;

		x_temp    = (quad.brx * cosa) - (quad.bry * sina);
		quad.bry = (quad.bry * cosa) + (quad.brx * sina);
		quad.brx  = x_temp;

	}

	instanceProto.drawGL = function(glw)
	{
		glw.setTexture(this.webGL_texture);
		glw.setOpacity(this.opacity);

		if (!this.text)
			return;

		// If text has changed, run the word wrap.
		this.rebuildText();

		// textWidth and textHeight needs to be calculated here
		// since we can early exit if bounding box is too tiny to draw anything
		// be we would still like to know the dimension of the text according to current width
		if (this.height < this.characterHeight*this.characterScale + this.lineHeight) {
			return;
		}

		this.update_bbox();
		var q = this.bquad;
		var ox = 0;
		var oy = 0;
		if (this.runtime.pixel_rounding)
		{
			ox = Math.round(this.x) - this.x;
			oy = Math.round(this.y) - this.y;
		}
		
		// Viewport dimensions
		var viewLeft = this.layer.viewLeft;
		var viewTop = this.layer.viewTop;
		var viewRight = this.layer.viewRight;
		var viewBottom = this.layer.viewBottom;

		// convert alignement properties to some usable values
		// useful parameters
		var angle      = this.angle;
		var ha         = this.halign;
		var va         = this.valign;
		var scale      = this.characterScale;
		var charHeight = this.characterHeight * scale;   // to precalculate in onCreate or on change
		var lineHeight = this.lineHeight;
		var charSpace  = this.characterSpacing;
		var lines = this.lines;
		var textHeight = this.textHeight;
		var letterWidth;

		var cosa,sina;
		if (angle !== 0) 
		{
			cosa = Math.cos(angle);
			sina = Math.sin(angle);
		}

		// we compute the offsets for vertical alignement in object-space
		// but it can't be negative, else it would underflow the boundingbox
		var halign;
		var valign = va * cr.max(0,(this.height - textHeight));

		// we get the position of the top left corner of the bounding box
		var offx = q.tlx + ox;
		var offy = q.tly + oy;


		var drawX ;
		var drawY = valign;
		var roundX, roundY;
		
		// TODO: this drawing algorithm iterates characters in their position relative to the text box,
		// but possibly renders them at an angle. To optimise rendering it compares the text box relative position to the viewport
		// to skip offscreen lines/characters, but this is only valid to do when the object is not rotated, so currently we have
		// to turn off these optimisations for rotated text. A better algorithm would be to iterate the rendered positions so
		// we can tell if the rendered location is offscreen or not, allowing the optimisations to work correctly with rotated text.

		for(var i = 0; i < lines.length; i++) {
			// for horizontal alignement, we need to work line by line
			var line       = lines[i].text;
			var lineWidth  = lines[i].width;

			// compute horizontal empty space
			// offset drawX according to horizontal alignement
			// indentation could be negative if long word in wrapbyword mode
			halign = ha * cr.max(0,this.width - lineWidth);
			//halign = Math.floor(ha * cr.max(0,this.width - lineWidth));
			drawX = halign;

			// we round to avoid pixel blurring
			drawY += lineHeight;
			
			// above viewport: skip rendering this line
			if (angle === 0 && offy + drawY + charHeight < viewTop)
			{
				drawY += charHeight;
				continue;
			}
			
			for(var j = 0; j < line.length; j++) {

				var letter = line.charAt(j);
				letterWidth = this.getCharacterWidth(letter);
				var clipUV = this.clipUV[letter];
				
				// still off to the left of the viewport: skip drawing this character
				if (angle === 0 && offx + drawX + letterWidth * scale + charSpace < viewLeft)
				{
					drawX += letterWidth * scale + charSpace;
					continue;
				}

				// check if next letter fits in bounding box
				if (drawX + letterWidth * scale > this.width + EPSILON)
				{
					break;
				}

				// we skip unrecognized characters (creates a space)
				if (clipUV !== undefined) {
					var clipWidth  = this.characterWidth*scale;
					var clipHeight = this.characterHeight*scale;

					roundX = drawX;
					roundY = drawY;
					
					if (angle === 0 && scale === 1)
					{
						roundX = Math.round(roundX);
						roundY = Math.round(roundY);
					}
					
					// we build the quad
					dQuad.tlx  = roundX;
					dQuad.tly  = roundY;
					dQuad.trx  = roundX + clipWidth;
					dQuad.try_ = roundY ;
					dQuad.blx  = roundX;
					dQuad.bly  = roundY + clipHeight;
					dQuad.brx  = roundX + clipWidth;
					dQuad.bry  = roundY + clipHeight;

					// we then rotate the quad around 0,0
					// if necessary
					if(angle !== 0)
					{
						rotateQuad(dQuad,cosa,sina);
					}
					// we then apply the world space offset
					dQuad.offset(offx,offy);

					// and render
					glw.quadTex(
						dQuad.tlx, dQuad.tly,
						dQuad.trx, dQuad.try_,
						dQuad.brx, dQuad.bry,
						dQuad.blx, dQuad.bly,
						clipUV
					);
				}

				drawX += letterWidth * scale + charSpace;
				
				// Line extended off viewport to right: skip drawing rest of line
				if (angle === 0 && offx + drawX > viewRight)
					break;
			}
			
			drawY += charHeight;
			
			// check if next row fits in bounding box and viewport and quit drawing if so
			if (angle === 0 && (drawY + charHeight + lineHeight > this.height || offy + drawY > viewBottom))
			{
				break;
			}
		}

	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {}

	Cnds.prototype.CompareText = function(text_to_compare, case_sensitive)
	{
		if (case_sensitive)
			return this.text == text_to_compare;
		else
			return cr.equals_nocase(this.text, text_to_compare);
	};

	pluginProto.cnds = new Cnds();

	//////////////////////////////////////
	// Actions
	function Acts() {}

	Acts.prototype.SetText = function(param)
	{
		if (cr.is_number(param) && param < 1e9)
			param = Math.round(param * 1e10) / 1e10;	// round to nearest ten billionth - hides floating point errors

		var text_to_set = param.toString();

		if (this.text !== text_to_set)
		{
			this.text = text_to_set;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.AppendText = function(param)
	{
		if (cr.is_number(param))
			param = Math.round(param * 1e10) / 1e10;	// round to nearest ten billionth - hides floating point errors

		var text_to_append = param.toString();

		if (text_to_append)	// not empty
		{
			this.text += text_to_append;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.SetScale = function(param)
	{
		if (param !== this.characterScale) {
			this.characterScale = param;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.SetCharacterSpacing = function(param)
	{
		if (param !== this.CharacterSpacing) {
			this.characterSpacing = param;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.SetLineHeight = function(param)
	{
		if (param !== this.lineHeight) {
			this.lineHeight = param;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	// Utility method to set single character width
	instanceProto.SetCharWidth = function(character,width) {
		var w = parseInt(width,10);
		if (this.characterWidthList[character] !== w) {
			this.characterWidthList[character] = w;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};
	
	// Called on startup to load the "Spacing data" property (which is passed as a string)
	instanceProto.LoadCharacterSpacingData = function (dataStr)
	{
		var arr, i, len, entry, width, str, j, lenj;
		
		if (!dataStr)
			return;
		
		try {
			arr = JSON.parse(dataStr);
		}
		catch (e) {
			return;		// invalid JSON data
		}
		
		if (!Array.isArray(arr))
			return;
		
		for (i = 0, len = arr.length; i < len; ++i)
		{
			entry = arr[i];
			
			if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "number" || typeof entry[1] !== "string")
				continue;		// not a valid entry
			
			// Apply this entry's width for every character in the string
			width = entry[0];
			str = entry[1];
			
			// TODO: unicode support with spread
			for (j = 0, lenj = str.length; j < lenj; ++j)
				this.SetCharWidth(str.charAt(j), width);
		}
	};

	// Action to modify spacing data at runtime
	Acts.prototype.SetCharacterWidth = function(characterSet,width)
	{
		if (characterSet !== "") {
			for(var c = 0; c < characterSet.length; c++) {
				this.SetCharWidth(characterSet.charAt(c),width);
			}
		}
	};
	
	Acts.prototype.SetEffect = function (effect)
	{
		this.blend_mode = effect;
		this.compositeOp = cr.effectToCompositeOp(effect);
		cr.setGLBlend(this, effect, this.runtime.gl);
		this.runtime.redraw = true;
	};
	
	Acts.prototype.SetHAlign = function (a)
	{
		this.halign = a / 2.0;
		this.text_changed = true;
		this.runtime.redraw = true;
	};
	
	Acts.prototype.SetVAlign = function (a)
	{
		this.valign = a / 2.0;
		this.text_changed = true;
		this.runtime.redraw = true;
	};

	pluginProto.acts = new Acts();

	//////////////////////////////////////
	// Expressions
	function Exps() {}

	Exps.prototype.CharacterWidth = function(ret,character)
	{
		ret.set_int(this.getCharacterWidth(character));
	};

	Exps.prototype.CharacterHeight = function(ret)
	{
		ret.set_int(this.characterHeight);
	};

	Exps.prototype.CharacterScale = function(ret)
	{
		ret.set_float(this.characterScale);
	};

	Exps.prototype.CharacterSpacing = function(ret)
	{
		ret.set_int(this.characterSpacing);
	};

	Exps.prototype.LineHeight = function(ret)
	{
		ret.set_int(this.lineHeight);
	};

	Exps.prototype.Text = function(ret)
	{
		ret.set_string(this.text);
	};
	Exps.prototype.TextWidth = function (ret)
	{
		this.rebuildText();
		ret.set_float(this.textWidth);
	};

	Exps.prototype.TextHeight = function (ret)
	{
		this.rebuildText();
		ret.set_float(this.textHeight);
	};

	pluginProto.exps = new Exps();

}());

// Text
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Plugin class
cr.plugins_.Text = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var pluginProto = cr.plugins_.Text.prototype;

	pluginProto.onCreate = function ()
	{
		// Override the 'set width' action
		pluginProto.acts.SetWidth = function (w)
		{
			if (this.width !== w)
			{
				this.width = w;
				this.text_changed = true;	// also recalculate text wrapping
				this.set_bbox_changed();
			}
		};
	};

	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
	};
	
	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;
			
		var i, len, inst;
		for (i = 0, len = this.instances.length; i < len; i++)
		{
			inst = this.instances[i];
			inst.mycanvas = null;
			inst.myctx = null;
			inst.mytex = null;
		}
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
		
		if (this.recycled)
			cr.clearArray(this.lines);
		else
			this.lines = [];		// for word wrapping
		
		this.text_changed = true;
	};
	
	var instanceProto = pluginProto.Instance.prototype;

	var requestedWebFonts = {};		// already requested web fonts have an entry here
	
	instanceProto.onCreate = function()
	{
		this.text = this.properties[0];
		this.facename = this.properties[1];
		this.ptSize = this.properties[2];
		this.line_height_offset = this.properties[3];
		this.isBold = this.properties[4];
		this.isItalic = this.properties[5];
		var c = this.properties[6];
		this.color = "rgb(" + Math.floor(c[0] * 255) + "," + Math.floor(c[1] * 255) + "," + Math.floor(c[2] * 255) + ")";
		this.halign = this.properties[7];				// 0=left, 1=center, 2=right
		this.valign = this.properties[8];				// 0=top, 1=center, 2=bottom
		this.wrapbyword = (this.properties[9] === 0);	// 0=word, 1=character
		this.visible = this.properties[10];
		
		this.lastwidth = this.width;
		this.lastwrapwidth = this.width;
		this.lastheight = this.height;
		
		this.font = "";				// complete canvas2d font name, e.g. "bold 12pt Arial", derived from other properties
		this.fontstyle = "";
		this.pxHeight = 0;
		this.textWidth = 0;
		this.textHeight = 0;
		
		this.updateFont();
		
		// For WebGL rendering
		this.mycanvas = null;
		this.myctx = null;
		this.mytex = null;
		this.need_text_redraw = false;
		this.last_render_tick = this.runtime.tickcount;
		
		if (this.recycled)
			this.rcTex.set(0, 0, 1, 1);
		else
			this.rcTex = new cr.rect(0, 0, 1, 1);
			
		// In WebGL renderer tick this text object to release memory if not rendered any more
		if (this.runtime.glwrap)
			this.runtime.tickMe(this);
		
;
	};
	
	instanceProto.saveToJSON = function ()
	{
		return {
			"t": this.text,
			"f": this.font,
			"c": this.color,
			"ha": this.halign,
			"va": this.valign,
			"wr": this.wrapbyword,
			"lho": this.line_height_offset,
			"fn": this.facename,
			"fs": this.fontstyle,
			"ps": this.ptSize,
			"pxh": this.pxHeight,
			"tw": this.textWidth,
			"th": this.textHeight,
			"lrt": this.last_render_tick
		};
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		this.text = o["t"];
		this.font = o["f"];
		this.color = o["c"];
		this.halign = o["ha"];
		this.valign = o["va"];
		this.wrapbyword = o["wr"];
		this.line_height_offset = o["lho"];
		this.facename = o["fn"];
		this.fontstyle = o["fs"];
		this.ptSize = o["ps"];
		this.pxHeight = o["pxh"];
		this.textWidth = o["tw"];
		this.textHeight = o["th"];
		this.last_render_tick = o["lrt"];
		
		// Restore bold/italic flags based on fontstyle. Note for C2 compatibility, the fontstyle string is what is saved/loaded,
		// but in C3 we use separate bold and italic flags.
		this.isBold = this.fontstyle.indexOf("bold") !== 0;
		this.isItalic = this.fontstyle.indexOf("italic") !== 0;
		
		this.text_changed = true;
		this.lastwidth = this.width;
		this.lastwrapwidth = this.width;
		this.lastheight = this.height;
	};
	
	instanceProto.tick = function ()
	{
		// In WebGL renderer, if not rendered for 300 frames (about 5 seconds), assume
		// the object has gone off-screen and won't need its textures any more.
		// This allows us to free its canvas, context and WebGL texture to save memory.
		if (this.runtime.glwrap && this.mytex && (this.runtime.tickcount - this.last_render_tick >= 300))
		{
			// Only do this if on-screen, otherwise static scenes which aren't re-rendering will release
			// text objects that are on-screen.
			var layer = this.layer;
            this.update_bbox();
            var bbox = this.bbox;

            if (bbox.right < layer.viewLeft || bbox.bottom < layer.viewTop || bbox.left > layer.viewRight || bbox.top > layer.viewBottom)
			{
				this.runtime.glwrap.deleteTexture(this.mytex);
				this.mytex = null;
				this.myctx = null;
				this.mycanvas = null;
			}
		}
	};
	
	instanceProto.onDestroy = function ()
	{
		// Remove references to allow GC to collect and save memory
		this.myctx = null;
		this.mycanvas = null;
		
		if (this.runtime.glwrap && this.mytex)
			this.runtime.glwrap.deleteTexture(this.mytex);
		
		this.mytex = null;
	};
	
	instanceProto.updateFont = function ()
	{
		this.pxHeight = Math.ceil((this.ptSize / 72.0) * 96.0) + 4;		// assume 96dpi...
		
		var styles = [];
		if (this.isBold)
			styles.push("bold");
		if (this.isItalic)
			styles.push("italic");
		this.fontstyle = styles.join(" ");
		
		this.font = this.fontstyle + " " + this.ptSize.toString() + "pt '" + this.facename + "'";
		this.text_changed = true;
		this.runtime.redraw = true;
	};

	instanceProto.draw = function(ctx, glmode)
	{
		ctx.font = this.font;
		ctx.textBaseline = "top";
		ctx.fillStyle = this.color;
		
		ctx.globalAlpha = glmode ? 1 : this.opacity;

		var myscale = 1;
		
		if (glmode)
		{
			myscale = this.layer.getScale();
			ctx.save();
			ctx.scale(myscale, myscale);
		}
		
		// If text has changed, run the word wrap.
		if (this.text_changed || this.width !== this.lastwrapwidth)
		{
			this.type.plugin.WordWrap(this.text, this.lines, ctx, this.width, this.wrapbyword);
			this.text_changed = false;
			this.lastwrapwidth = this.width;
		}
		
		// Draw each line after word wrap
		this.update_bbox();
		var penX = glmode ? 0 : this.bquad.tlx;
		var penY = glmode ? 0 : this.bquad.tly;
		
		if (this.runtime.pixel_rounding)
		{
			penX = (penX + 0.5) | 0;
			penY = (penY + 0.5) | 0;
		}
		
		if (this.angle !== 0 && !glmode)
		{
			ctx.save();
			ctx.translate(penX, penY);
			ctx.rotate(this.angle);
			penX = 0;
			penY = 0;
		}
		
		var endY = penY + this.height;
		var line_height = this.pxHeight;
		line_height += this.line_height_offset;
		var drawX;
		var i;
		
		// Adjust penY for vertical alignment
		if (this.valign === 1)		// center
			penY += Math.max(this.height / 2 - (this.lines.length * line_height) / 2, 0);
		else if (this.valign === 2)	// bottom
			penY += Math.max(this.height - (this.lines.length * line_height) - 2, 0);
		
		for (i = 0; i < this.lines.length; i++)
		{
			// Adjust the line draw position depending on alignment
			drawX = penX;
			
			if (this.halign === 1)		// center
				drawX = penX + (this.width - this.lines[i].width) / 2;
			else if (this.halign === 2)	// right
				drawX = penX + (this.width - this.lines[i].width);
				
			ctx.fillText(this.lines[i].text, drawX, penY);
			penY += line_height;
			
			if (penY >= endY - line_height)
				break;
		}
		
		if (this.angle !== 0 || glmode)
			ctx.restore();
			
		this.last_render_tick = this.runtime.tickcount;
	};
	
	instanceProto.drawGL = function(glw)
	{
		if (this.width < 1 || this.height < 1)
			return;
		
		var need_redraw = this.text_changed || this.need_text_redraw;
		this.need_text_redraw = false;
		var layer_scale = this.layer.getScale();
		var layer_angle = this.layer.getAngle();
		var rcTex = this.rcTex;
		
		// Calculate size taking in to account scale
		var floatscaledwidth = layer_scale * this.width;
		var floatscaledheight = layer_scale * this.height;
		var scaledwidth = Math.ceil(floatscaledwidth);
		var scaledheight = Math.ceil(floatscaledheight);
		
		var halfw = this.runtime.draw_width / 2;
		var halfh = this.runtime.draw_height / 2;
		
		// Create 2D context for this instance if not already
		if (!this.myctx)
		{
			this.mycanvas = document.createElement("canvas");
			this.mycanvas.width = scaledwidth;
			this.mycanvas.height = scaledheight;
			this.lastwidth = scaledwidth;
			this.lastheight = scaledheight;
			need_redraw = true;
			this.myctx = this.mycanvas.getContext("2d");
		}
		
		// Update size if changed
		if (scaledwidth !== this.lastwidth || scaledheight !== this.lastheight)
		{
			this.mycanvas.width = scaledwidth;
			this.mycanvas.height = scaledheight;
			
			if (this.mytex)
			{
				glw.deleteTexture(this.mytex);
				this.mytex = null;
			}
			
			need_redraw = true;
		}
		
		// Need to update the GL texture
		if (need_redraw)
		{
			// Draw to my context
			this.myctx.clearRect(0, 0, scaledwidth, scaledheight);
			this.draw(this.myctx, true);
			
			// Create GL texture if none exists
			// Create 16-bit textures (RGBA4) on mobile to reduce memory usage - quality impact on desktop
			// was almost imperceptible
			if (!this.mytex)
				this.mytex = glw.createEmptyTexture(scaledwidth, scaledheight, this.runtime.linearSampling, this.runtime.isMobile);
				
			// Copy context to GL texture
			glw.videoToTexture(this.mycanvas, this.mytex, this.runtime.isMobile);
		}
		
		this.lastwidth = scaledwidth;
		this.lastheight = scaledheight;
		
		// Draw GL texture
		glw.setTexture(this.mytex);
		glw.setOpacity(this.opacity);
		
		glw.resetModelView();
		glw.translate(-halfw, -halfh);
		glw.updateModelView();
		
		var q = this.bquad;
		
		var tlx = this.layer.layerToCanvas(q.tlx, q.tly, true, true);
		var tly = this.layer.layerToCanvas(q.tlx, q.tly, false, true);
		var trx = this.layer.layerToCanvas(q.trx, q.try_, true, true);
		var try_ = this.layer.layerToCanvas(q.trx, q.try_, false, true);
		var brx = this.layer.layerToCanvas(q.brx, q.bry, true, true);
		var bry = this.layer.layerToCanvas(q.brx, q.bry, false, true);
		var blx = this.layer.layerToCanvas(q.blx, q.bly, true, true);
		var bly = this.layer.layerToCanvas(q.blx, q.bly, false, true);
		
		if (this.runtime.pixel_rounding || (this.angle === 0 && layer_angle === 0))
		{
			var ox = ((tlx + 0.5) | 0) - tlx;
			var oy = ((tly + 0.5) | 0) - tly
			
			tlx += ox;
			tly += oy;
			trx += ox;
			try_ += oy;
			brx += ox;
			bry += oy;
			blx += ox;
			bly += oy;
		}
		
		if (this.angle === 0 && layer_angle === 0)
		{
			trx = tlx + scaledwidth;
			try_ = tly;
			brx = trx;
			bry = tly + scaledheight;
			blx = tlx;
			bly = bry;
			rcTex.right = 1;
			rcTex.bottom = 1;
		}
		else
		{
			rcTex.right = floatscaledwidth / scaledwidth;
			rcTex.bottom = floatscaledheight / scaledheight;
		}
		
		glw.quadTex(tlx, tly, trx, try_, brx, bry, blx, bly, rcTex);
		
		glw.resetModelView();
		glw.scale(layer_scale, layer_scale);
		glw.rotateZ(-this.layer.getAngle());
		glw.translate((this.layer.viewLeft + this.layer.viewRight) / -2, (this.layer.viewTop + this.layer.viewBottom) / -2);
		glw.updateModelView();
		
		this.last_render_tick = this.runtime.tickcount;
	};
	
	var wordsCache = [];

	pluginProto.TokeniseWords = function (text)
	{
		cr.clearArray(wordsCache);
		var cur_word = "";
		var ch;
		
		// Loop every char
		var i = 0;
		
		while (i < text.length)
		{
			ch = text.charAt(i);
			
			if (ch === "\n")
			{
				// Dump current word if any
				if (cur_word.length)
				{
					wordsCache.push(cur_word);
					cur_word = "";
				}
				
				// Add newline word
				wordsCache.push("\n");
				
				++i;
			}
			// Whitespace or hyphen: swallow rest of whitespace and include in word
			else if (ch === " " || ch === "\t" || ch === "-")
			{
				do {
					cur_word += text.charAt(i);
					i++;
				}
				while (i < text.length && (text.charAt(i) === " " || text.charAt(i) === "\t"));
				
				wordsCache.push(cur_word);
				cur_word = "";
			}
			else if (i < text.length)
			{
				cur_word += ch;
				i++;
			}
		}
		
		// Append leftover word if any
		if (cur_word.length)
			wordsCache.push(cur_word);
	};
	
	var linesCache = [];
	
	function allocLine()
	{
		if (linesCache.length)
			return linesCache.pop();
		else
			return {};
	};
	
	function freeLine(l)
	{
		linesCache.push(l);
	};
	
	function freeAllLines(arr)
	{
		var i, len;
		for (i = 0, len = arr.length; i < len; i++)
		{
			freeLine(arr[i]);
		}
		
		cr.clearArray(arr);
	};

	pluginProto.WordWrap = function (text, lines, ctx, width, wrapbyword)
	{
		if (!text || !text.length)
		{
			freeAllLines(lines);
			return;
		}
			
		if (width <= 2.0)
		{
			freeAllLines(lines);
			return;
		}
		
		// If under 100 characters (i.e. a fairly short string), try a short string optimisation: just measure the text
		// and see if it fits on one line, without going through the tokenise/wrap.
		// Text musn't contain a linebreak!
		if (text.length <= 100 && text.indexOf("\n") === -1)
		{
			var all_width = ctx.measureText(text).width;
			
			if (all_width <= width)
			{
				// fits on one line
				freeAllLines(lines);
				lines.push(allocLine());
				lines[0].text = text;
				lines[0].width = all_width;
				return;
			}
		}
			
		this.WrapText(text, lines, ctx, width, wrapbyword);
	};
	
	function trimSingleSpaceRight(str)
	{
		if (!str.length || str.charAt(str.length - 1) !== " ")
			return str;
		
		return str.substring(0, str.length - 1);
	};

	pluginProto.WrapText = function (text, lines, ctx, width, wrapbyword)
	{
		var wordArray;
		
		if (wrapbyword)
		{
			this.TokeniseWords(text);	// writes to wordsCache
			wordArray = wordsCache;
		}
		else
			wordArray = text;
			
		var cur_line = "";
		var prev_line;
		var line_width;
		var i;
		var lineIndex = 0;
		var line;
		
		for (i = 0; i < wordArray.length; i++)
		{
			// Look for newline
			if (wordArray[i] === "\n")
			{
				// Flush line.  Recycle a line if possible
				if (lineIndex >= lines.length)
					lines.push(allocLine());
				
				cur_line = trimSingleSpaceRight(cur_line);		// for correct center/right alignment
				line = lines[lineIndex];
				line.text = cur_line;
				line.width = ctx.measureText(cur_line).width;
					
				lineIndex++;
				cur_line = "";
				continue;
			}
			
			// Otherwise add to line
			prev_line = cur_line;
			cur_line += wordArray[i];
			
			// Measure line
			line_width = ctx.measureText(cur_line).width;
			
			// Line too long: wrap the line before this word was added
			if (line_width >= width)
			{
				// Append the last line's width to the string object
				if (lineIndex >= lines.length)
					lines.push(allocLine());
				
				prev_line = trimSingleSpaceRight(prev_line);
				line = lines[lineIndex];
				line.text = prev_line;
				line.width = ctx.measureText(prev_line).width;
					
				lineIndex++;
				cur_line = wordArray[i];
				
				// Wrapping by character: avoid lines starting with spaces
				if (!wrapbyword && cur_line === " ")
					cur_line = "";
			}
		}
		
		// Add any leftover line
		if (cur_line.length)
		{
			if (lineIndex >= lines.length)
				lines.push(allocLine());
			
			cur_line = trimSingleSpaceRight(cur_line);
			line = lines[lineIndex];
			line.text = cur_line;
			line.width = ctx.measureText(cur_line).width;
				
			lineIndex++;
		}
		
		// truncate lines to the number that were used. recycle any spare line objects
		for (i = lineIndex; i < lines.length; i++)
			freeLine(lines[i]);
		
		lines.length = lineIndex;
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};

	Cnds.prototype.CompareText = function(text_to_compare, case_sensitive)
	{
		if (case_sensitive)
			return this.text == text_to_compare;
		else
			return cr.equals_nocase(this.text, text_to_compare);
	};
	
	pluginProto.cnds = new Cnds();

	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.SetText = function(param)
	{
		if (cr.is_number(param) && param < 1e9)
			param = Math.round(param * 1e10) / 1e10;	// round to nearest ten billionth - hides floating point errors
		
		var text_to_set = param.toString();
		
		if (this.text !== text_to_set)
		{
			this.text = text_to_set;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};
	
	Acts.prototype.AppendText = function(param)
	{
		if (cr.is_number(param))
			param = Math.round(param * 1e10) / 1e10;	// round to nearest ten billionth - hides floating point errors
			
		var text_to_append = param.toString();
		
		if (text_to_append)	// not empty
		{
			this.text += text_to_append;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};
	
	Acts.prototype.SetFontFace = function (face_, style_)
	{
		var newstyle = "";
		var newbold = false;
		var newitalic = false;
		
		switch (style_) {
		case 1: newbold = true; break;
		case 2: newitalic = true; break;
		case 3: newbold = true; newitalic = true; break;
		}
		
		if (face_ === this.facename && this.isBold === newbold && this.isItalic === newitalic)
			return;		// no change
			
		this.facename = face_;
		this.isBold = newbold;
		this.isItalic = newitalic;
		this.updateFont();
	};
	
	Acts.prototype.SetFontSize = function (size_)
	{
		if (this.ptSize === size_)
			return;

		this.ptSize = size_;
		this.updateFont();
	};
	
	Acts.prototype.SetFontColor = function (rgb)
	{
		var r = cr.clamp(Math.floor(cr.GetRValue(rgb) * 255), 0, 255);
		var g = cr.clamp(Math.floor(cr.GetGValue(rgb) * 255), 0, 255);
		var b = cr.clamp(Math.floor(cr.GetBValue(rgb) * 255), 0, 255);

		var newcolor = "rgb(" + r + "," + g + "," + b + ")";
		
		if (newcolor === this.color)
			return;

		this.color = newcolor;
		this.need_text_redraw = true;
		this.runtime.redraw = true;
	};
	
	Acts.prototype.SetWebFont = function (familyname_, cssurl_)
	{
		var self = this;
		var refreshFunc = (function () {
							self.runtime.redraw = true;
							self.text_changed = true;
						});

		// Already requested this web font?
		if (requestedWebFonts.hasOwnProperty(cssurl_))
		{
			// Use it immediately without requesting again.  Whichever object
			// made the original request will refresh the canvas when it finishes
			// loading.
			var newfacename = "'" + familyname_ + "'";
			
			if (this.facename === newfacename)
				return;	// no change
				
			this.facename = newfacename;
			this.updateFont();
			
			// There doesn't seem to be a good way to test if the font has loaded,
			// so just fire a refresh every 100ms for the first 1 second, then
			// every 1 second after that up to 10 sec - hopefully will have loaded by then!
			for (var i = 1; i < 10; i++)
			{
				setTimeout(refreshFunc, i * 100);
				setTimeout(refreshFunc, i * 1000);
			}
		
			return;
		}
		
		// Otherwise start loading the web font now
		var wf = document.createElement("link");
		wf.href = cssurl_;
		wf.rel = "stylesheet";
		wf.type = "text/css";
		wf.onload = refreshFunc;
					
		document.getElementsByTagName('head')[0].appendChild(wf);
		requestedWebFonts[cssurl_] = true;
		
		this.facename = "'" + familyname_ + "'";
		this.updateFont();
					
		// Another refresh hack
		for (var i = 1; i < 10; i++)
		{
			setTimeout(refreshFunc, i * 100);
			setTimeout(refreshFunc, i * 1000);
		}
		
;
	};
	
	Acts.prototype.SetEffect = function (effect)
	{
		this.blend_mode = effect;
		this.compositeOp = cr.effectToCompositeOp(effect);
		cr.setGLBlend(this, effect, this.runtime.gl);
		this.runtime.redraw = true;
	};

	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};

	Exps.prototype.Text = function(ret)
	{
		ret.set_string(this.text);
	};
	
	Exps.prototype.FaceName = function (ret)
	{
		ret.set_string(this.facename);
	};
	
	Exps.prototype.FaceSize = function (ret)
	{
		ret.set_int(this.ptSize);
	};
	
	Exps.prototype.TextWidth = function (ret)
	{
		var w = 0;
		var i, len, x;
		for (i = 0, len = this.lines.length; i < len; i++)
		{
			x = this.lines[i].width;
			
			if (w < x)
				w = x;
		}
		
		ret.set_int(w);
	};
	
	Exps.prototype.TextHeight = function (ret)
	{
		ret.set_int(this.lines.length * (this.pxHeight + this.line_height_offset) - this.line_height_offset);
	};
	
	pluginProto.exps = new Exps();
		
}());

cr.getObjectRefTable = function () {
	return [
		cr.plugins_.Sprite,
		cr.plugins_.Mouse,
		cr.plugins_.Audio,
		cr.plugins_.Spritefont2,
		cr.plugins_.Text,
		cr.plugins_.Mouse.prototype.cnds.OnObjectClicked,
		cr.plugins_.Sprite.prototype.acts.SetAnimFrame,
		cr.system_object.prototype.acts.Wait,
		cr.plugins_.Spritefont2.prototype.acts.AddInstanceVar,
		cr.system_object.prototype.exps.str,
		cr.plugins_.Spritefont2.prototype.acts.SetText,
		cr.system_object.prototype.cnds.OnLayoutStart,
		cr.plugins_.Audio.prototype.acts.PlayByName,
		cr.plugins_.Spritefont2.prototype.cnds.CompareInstanceVar,
		cr.plugins_.Sprite.prototype.acts.AddInstanceVar,
		cr.plugins_.Sprite.prototype.acts.SetInstanceVar,
		cr.plugins_.Text.prototype.acts.SetText,
		cr.system_object.prototype.cnds.Every,
		cr.system_object.prototype.cnds.Else
	];
};

