<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">// Generated by Construct 3, the game and app creator :: https://www.construct.net
"use strict";
// c2/common_prelude.js
// ECMAScript 5 strict mode

// Create the cr namespace for all runtime names
var cr = 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 &amp;&amp; window.console.log)
			window.console.log(msg);
	};
	
	cr.logerror = function (msg)
	{
		if (window.console &amp;&amp; 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 &gt; 0 &amp;&amp; ((x - 1) &amp; x) === 0;
	};
	
	cr.nextHighestPowerOfTwo = function(x) {
		--x;
		for (var i = 1; i &lt; 32; i &lt;&lt;= 1) {
			x = x | x &gt;&gt; i;
		}
		return x + 1;
	}
	
	cr.abs = function (x)
	{
		// Faster than Math.abs
		return (x &lt; 0 ? -x : x);
	};
	
	cr.max = function (a, b)
	{
		// Faster than Math.max
		return (a &gt; b ? a : b);
	};
	
	cr.min = function (a, b)
	{
		// Faster than Math.min
		return (a &lt; 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 &gt;= 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 &lt; a2x)
		{
			min_ax = a1x;
			max_ax = a2x;
		}
		else
		{
			min_ax = a2x;
			max_ax = a1x;
		}
		
		if (b1x &lt; b2x)
		{
			min_bx = b1x;
			max_bx = b2x;
		}
		else
		{
			min_bx = b2x;
			max_bx = b1x;
		}
		
		if (max_ax &lt; min_bx || min_ax &gt; max_bx)
			return false;
		
		if (a1y &lt; a2y)
		{
			min_ay = a1y;
			max_ay = a2y;
		}
		else
		{
			min_ay = a2y;
			max_ay = a1y;
		}
		
		if (b1y &lt; b2y)
		{
			min_by = b1y;
			max_by = b2y;
		}
		else
		{
			min_by = b2y;
			max_by = b1y;
		}
		
		if (max_ay &lt; min_by || min_ay &gt; 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) &gt; d)
			return false;
		
		var lb = qax * dpy - qay * dpx;
		
		return cr.abs(lb) &lt;= 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 &gt; this.right)
		{
			temp = this.left;
			this.left = this.right;
			this.right = temp;
		}
		
		if (this.top &gt; this.bottom)
		{
			temp = this.top;
			this.top = this.bottom;
			this.bottom = temp;
		}
	};

	Rect.prototype.intersects_rect = function (rc)
	{
		return !(rc.right &lt; this.left || rc.bottom &lt; this.top || rc.left &gt; this.right || rc.top &gt; this.bottom);
	};
	
	Rect.prototype.intersects_rect_off = function (rc, ox, oy)
	{
		return !(rc.right + ox &lt; this.left || rc.bottom + oy &lt; this.top || rc.left + ox &gt; this.right || rc.top + oy &gt; this.bottom);
	};
	
	Rect.prototype.contains_pt = function (x, y)
	{
		return (x &gt;= this.left &amp;&amp; x &lt;= this.right) &amp;&amp; (y &gt;= this.top &amp;&amp; y &lt;= this.bottom);
	};
	
	Rect.prototype.equals = function (r)
	{
		return this.left === r.left &amp;&amp; this.top === r.top &amp;&amp; this.right === r.right &amp;&amp; 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 &lt; b)
		{
			if (c &lt; d)
			{
				// sort order: (a, c) (b, d)
				if (a &lt; c)
					minresult = a;
				else
					minresult = c;
				
				if (b &gt; d)
					maxresult = b;
				else
					maxresult = d;
			}
			else
			{
				// sort order: (a, d) (b, c)
				if (a &lt; d)
					minresult = a;
				else
					minresult = d;
				
				if (b &gt; c)
					maxresult = b;
				else
					maxresult = c;
			}
		}
		else
		{
			if (c &lt; d)
			{
				// sort order: (b, c) (a, d)
				if (b &lt; c)
					minresult = b;
				else
					minresult = c;
				
				if (a &gt; d)
					maxresult = a;
				else
					maxresult = d;
			}
			else
			{
				// sort order: (b, d) (a, c)
				if (b &lt; d)
					minresult = b;
				else
					minresult = d;
				
				if (a &gt; 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 &gt;= 0.0) &amp;&amp; (v &gt; 0.0) &amp;&amp; (u + v &lt; 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 &gt;= 0.0) &amp;&amp; (v &gt; 0.0) &amp;&amp; (u + v &lt; 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 &lt; 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 &lt; 4; i++)
		{
			for (j = 0; j &lt; 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) &lt;&lt; 8)
			 | (Math.max(Math.min(blue, 255), 0) &lt;&lt; 16);
	};

	// Extended range RGBA value, backported from C3 runtime
	var ALPHAEX_SHIFT = 1024;		// 2^10
	var RGBEX_SHIFT = 16384;		// 2^14
	var RGBEX_MAX = 8191;			// 2^13 - 1
	var RGBEX_MIN = -8192;			// -(2^13)
	cr.RGBAEx = function RGBAEx(red, green, blue, alpha)
	{
		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);
		alpha = cr.clamp(Math.floor(alpha * 1024), 0, 1024);		// no overdrive for alpha

		// Shift negative color components in to upper positive range
		if (red &lt; 0)
			red += RGBEX_SHIFT;
		if (green &lt; 0)
			green += RGBEX_SHIFT;
		if (blue &lt; 0)
			blue += RGBEX_SHIFT;
		
		// Combine arithmetically since bitwise operators will truncate to 32-bit
		return -(
			red * RGBEX_SHIFT * RGBEX_SHIFT * ALPHAEX_SHIFT +
			green * RGBEX_SHIFT * ALPHAEX_SHIFT +
			blue * ALPHAEX_SHIFT +
			alpha
		);
	};
	
	cr.RGBEx = function RGBEx(red, green, blue)
	{
		return cr.RGBAEx(red, green, blue, 1);
	};
	
	function isNegativeZero(x)
	{
		return x === 0 &amp;&amp; (1 / x &lt; 0);		// dividing by -0 gives -Infinity
	}
	
	// Extract color components as a float [0, 1]
	cr.GetRValue = function GetRValue(rgb)
	{
		if (rgb &gt;= 0)
		{
			return (rgb &amp; 0xFF) / 255;
		}
		else
		{
			var v = Math.floor(-rgb / (RGBEX_SHIFT * RGBEX_SHIFT * ALPHAEX_SHIFT));
			if (v &gt; RGBEX_MAX)
				v -= RGBEX_SHIFT;
			return v / 1024;
		}
	};
	
	cr.GetGValue = function GetGValue(rgb)
	{
		if (rgb &gt;= 0)
		{
			return ((rgb &amp; 0xFF00) &gt;&gt; 8) / 255;
		}
		else
		{
			var v = Math.floor((-rgb % (RGBEX_SHIFT * RGBEX_SHIFT * ALPHAEX_SHIFT)) / (RGBEX_SHIFT * ALPHAEX_SHIFT));
			if (v &gt; RGBEX_MAX)
				v -= RGBEX_SHIFT;
			return v / 1024;
		}
	};
	
	cr.GetBValue = function GetBValue(rgb)
	{
		if (rgb &gt;= 0)
		{
			return ((rgb &amp; 0xFF0000) &gt;&gt; 16) / 255;
		}
		else
		{
			var v = Math.floor((-rgb % (RGBEX_SHIFT * ALPHAEX_SHIFT)) / ALPHAEX_SHIFT);
			if (v &gt; RGBEX_MAX)
				v -= RGBEX_SHIFT;
			return v / 1024;
		}
	};
	
	cr.GetAValue = function GetAValue(rgb)
	{
		if (isNegativeZero(rgb))
		{
			return 0;
		}
		else if (rgb &gt;= 0)
		{
			return 1;
		}
		else
		{
			var v = Math.floor(-rgb % ALPHAEX_SHIFT);
			// note alpha components cannot be negative
			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 &lt; 0 || index &gt;= arr.length)
			return;							// index out of bounds

		for (i = index, len = arr.length - 1; i &lt; 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 &lt; 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 &lt; 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 &lt; a)
			return a;
		else if (x &gt; 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 &lt; 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 &lt; 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 &gt;= 1)
			return 0;
		if (n &lt;= -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) &gt; step)
		{
			if (cs * se - ss * ce &gt; 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 &lt;= 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 &amp;&amp; /safari/i.test(navigator.userAgent);
		isiOS = /(iphone|ipod|ipad)/i.test(navigator.userAgent);
	}
	
	var supports_set = (typeof Set !== "undefined" &amp;&amp; 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 &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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, "\\$&amp;");
	};
	
	// Collision polys
	function CollisionPoly_(pts_array_, enabled)
	{
		if (!pts_array_)
			pts_array_ = [0, 0, 1, 0, 1, 1, 0, 1];
		
		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.enabled = typeof enabled === "undefined" ? true : enabled;
		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 &lt; len; ++i)
		{
			i2 = i*2;
			x = myptscache[i2];
			y = myptscache[i2+1];
			
			if (x &lt; bboxLeft_)
				bboxLeft_ = x;
			if (x &gt; bboxRight_)
				bboxRight_ = x;
			if (y &lt; bboxTop_)
				bboxTop_ = y;
			if (y &gt; 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 &amp;&amp; this.cache_height === h &amp;&amp; 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 &lt; 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] &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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;
		}
	};
	
	CollisionPoly_.prototype.is_enabled = function()
	{
		return this.enabled;
	}
	
	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 &lt;= lenx; ++x)
			{
				for (y = oldrange.top, leny = oldrange.bottom; y &lt;= leny; ++y)
				{
					if (newrange &amp;&amp; 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 &lt;= lenx; ++x)
			{
				for (y = newrange.top, leny = newrange.bottom; y &lt;= leny; ++y)
				{
					if (oldrange &amp;&amp; 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 &lt;= lenx; ++x)
		{
			for (y = ystart; y &lt;= 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 &lt;= lenx; ++x)
			{
				for (y = oldrange.top, leny = oldrange.bottom; y &lt;= leny; ++y)
				{
					if (newrange &amp;&amp; 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 &lt;= lenx; ++x)
			{
				for (y = newrange.top, leny = newrange.bottom; y &lt;= leny; ++y)
				{
					if (oldrange &amp;&amp; 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 &lt;= lenx; ++x)
		{
			for (y = ystart; y &lt;= 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 &lt;= lenx; ++x)
		{
			for (y = ystart; y &lt;= 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 &lt; 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 &lt; 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 &gt; 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) &gt;= 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() &gt; 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) &gt;= 0, "expected instance to be in objects list");
;
	};
	
	RenderCell_.prototype.remove = function (inst)
	{
		//assert2(this.objects.indexOf(inst) &gt;= 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() &gt;= 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 &lt; 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 &lt;= 0 || effect &gt;= 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 &amp;&amp; "a".localeCompare("A", undefined, localeCompare_options) === 0);
	var localeCompare_works2 = (has_localeCompare &amp;&amp; "a".localeCompare("Ã¡", undefined, localeCompare_options) !== 0);
	var supports_localeCompare = (has_localeCompare &amp;&amp; localeCompare_works1 &amp;&amp; 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" -&gt; "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 &lt; 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 &amp;&amp; document.body &amp;&amp; target === document.body)
			return true;
		
		if (cr.equals_nocase(target.tagName, "canvas"))
			return true;
		
		return false;
	};
	
}());

// c2/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&amp;&amp;(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&amp;&amp;(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&amp;&amp;(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&amp;&amp;(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&amp;&amp;(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&amp;&amp;(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&amp;&amp;(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&amp;&amp;(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]&amp;&amp;o===c&amp;&amp;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&amp;&amp;(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)&gt;=1)return d!==a&amp;&amp;(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)&lt;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 &lt; 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 &lt; 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, "&lt;default&gt;");
;
		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, "&lt;point&gt;");
;
		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 &lt; 1.0)",
			"		discard;",						// discarding non-opaque fragments
			"}"
		].join("\n");

		var shaderProg = this.createShaderProgram({src: fsSource}, vsSource, "&lt;earlyz&gt;");
;
		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, "&lt;fill&gt;");
;
		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 &gt; 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]&amp;&amp;a[1]===b[1]&amp;&amp;a[2]===b[2]&amp;&amp;a[3]===b[3]&amp;&amp;
			   a[4]===b[4]&amp;&amp;a[5]===b[5]&amp;&amp;a[6]===b[6]&amp;&amp;a[7]===b[7]&amp;&amp;
			   a[8]===b[8]&amp;&amp;a[9]===b[9]&amp;&amp;a[10]===b[10]&amp;&amp;a[11]===b[11]&amp;&amp;
			   a[12]===b[12]&amp;&amp;a[13]===b[13]&amp;&amp;a[14]===b[14]&amp;&amp;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
			var compilationlog = gl.getShaderInfoLog(fragmentShader);
			gl.deleteShader(fragmentShader);
			throw new Error("error compiling fragment shader: " + compilationlog);
		}
		
		// 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
			var compilationlog = gl.getShaderInfoLog(vertexShader);
			gl.deleteShader(fragmentShader);
			gl.deleteShader(vertexShader);
			throw new Error("error compiling vertex shader: " + compilationlog);
		}
		
		// 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))
		{
			var compilationlog = gl.getProgramInfoLog(shaderProgram);
			gl.deleteShader(fragmentShader);
			gl.deleteShader(vertexShader);
			gl.deleteProgram(shaderProgram);
			throw new Error("error linking shader program: " + compilationlog);
		}
		
		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 &lt; 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 &lt; 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 &amp;&amp; this.height === h &amp;&amp; !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 &lt; 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 &amp;&amp; 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 &amp;&amp; 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 &gt; 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 &amp;&amp; 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 &lt; len; i++)
		{
			s = shaderPrograms[i];
			
			// This is the current program: send the updated model view now
			if (i === currentProgram &amp;&amp; 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 &amp;&amp; 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 &gt;= 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 &gt;= 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 &amp;&amp; s.locMatMV)
		{
			s.updateMatMV(glwrap.currentMV);
			s.hasCurrentMatMV = true;
		}
		
		// Write current opacity
		if (s.locOpacity &amp;&amp; s.lpOpacity !== glwrap.currentOpacity)
		{
			s.lpOpacity = glwrap.currentOpacity;
			gl.uniform1f(s.locOpacity, glwrap.currentOpacity);
		}
		
		// Update vertex attribute array bindings
		if (s.locAPos &gt;= 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 &gt;= 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 &amp;&amp; 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 &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; (v !== s.lpDestEndX || v2 !== s.lpDestEndY))
		{
			s.lpDestEndX = v;
			s.lpDestEndY = v2;
			gl.uniform2f(s.locDestEnd, v, v2);
		}
		
		v = mat4param[6];
		
		if (s.locLayerScale &amp;&amp; v !== s.lpLayerScale)
		{
			s.lpLayerScale = v;
			gl.uniform1f(s.locLayerScale, v);
		}
		
		v = mat4param[7];
		
		if (s.locLayerAngle &amp;&amp; v !== s.lpLayerAngle)
		{
			s.lpLayerAngle = v;
			gl.uniform1f(s.locLayerAngle, v);
		}
		
		v = mat4param[12];
		v2 = mat4param[13];
		
		if (s.locSrcStart &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; (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 &amp;&amp; v !== s.lpSeconds)
		{
			s.lpSeconds = v;
			gl.uniform1f(s.locSeconds, v);
		}
			
		if (s.parameters.length)
		{
			for (i = 0, len = s.parameters.length; i &lt; 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 &gt; 0)
		{
			gl.bindBuffer(gl.ARRAY_BUFFER, this.pointBuffer);
			gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.pointData.subarray(0, this.pointPtr));
			
			if (s &amp;&amp; s.locAPos &gt;= 0 &amp;&amp; s.name === "&lt;point&gt;")
				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 &gt; 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 &amp;&amp; s.locAPos &gt;= 0 &amp;&amp; s.name !== "&lt;point&gt;")
				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 &amp;&amp; s.locATex &gt;= 0 &amp;&amp; s.name !== "&lt;point&gt;")
				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 &lt; 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 &gt;= 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 &gt; 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 &lt; len; ++i)
		{
			o = this.timerResults[i];

			// Gone beyond frame number range to check: stop checking and return
			if (o.frameNumber &gt;= 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 &amp;&amp; !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 &lt;= 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 &lt; len; ++i)
		{
			o = this.timerResults[i];

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

			// Result is within range
			if (o.frameNumber &gt;= 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 &lt; 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 &lt; len; ++i)
		{
			o = this.timerResults[i];

			if (o.frameNumber &gt;= frameNum)
			{
				if (i &gt; 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 &amp;&amp; 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 &amp;&amp; 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 &gt;= 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 &gt;= 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 &gt;= 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 &lt; 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 &gt;= 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 &lt; 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 &lt; 0 || h &lt; 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 &lt; 32; i &lt;&lt;= 1) {
			x = x | x &gt;&gt; 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" &amp;&amp; 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) &amp;&amp; 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 &amp;&amp; !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 &amp;&amp; !isPOT &amp;&amp; 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 &gt;= 2) &amp;&amp; this.enable_mipmaps &amp;&amp; !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 &amp;&amp; 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" &amp;&amp; tex.c2refcount &gt; 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 &lt; 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_;

}());

// c2/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" &amp;&amp; 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 (&lt;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)) &amp;&amp; !this.isMicrosoftEdge;
		this.isTizen = /tizen/i.test(navigator.userAgent);
		this.isAndroid = /android/i.test(navigator.userAgent) &amp;&amp; !this.isTizen &amp;&amp; !this.isIE &amp;&amp; !this.isMicrosoftEdge;		// IE mobile and Tizen masquerade as Android
		this.isiPhone = (/iphone/i.test(navigator.userAgent) || /ipod/i.test(navigator.userAgent)) &amp;&amp; !this.isIE &amp;&amp; !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)) &amp;&amp; !this.isIE &amp;&amp; !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) &amp;&amp; !this.isChrome &amp;&amp; !this.isIE &amp;&amp; !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" &amp;&amp; window["c2isWindows8"]);
		this.isWindowsPhone8 = !!(typeof window["c2isWindowsPhone8"] !== "undefined" &amp;&amp; window["c2isWindowsPhone8"]);
		this.isWindowsPhone81 = !!(typeof window["c2isWindowsPhone81"] !== "undefined" &amp;&amp; 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" &amp;&amp; window["c2isBlackberry10"]);
		this.isAndroidStockBrowser = (this.isAndroid &amp;&amp; !this.isChrome &amp;&amp; !this.isCrosswalk &amp;&amp; !this.isFirefox &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; !this.isNWjs &amp;&amp; (window.location.search === "?nw" || /nodewebkit/i.test(navigator.userAgent) || /nwjs/i.test(navigator.userAgent)))
		{
			this.isNWjs = true;
		}
		
		this.isDebug = (this.isPreview &amp;&amp; window.location.search.indexOf("debug") &gt; -1);
		
		// Disable FB instant in preview mode because there seems to be a bug where the initialize promise never resolves/rejects.
		// To avoid the timeout delaying load for Cordova games, also disable Instant Games in Cordova apps.
		this.useFbInstant = (typeof FBInstant !== "undefined" &amp;&amp; !this.isPreview &amp;&amp; !this.isCordova);
		this.hasInitialized = false;

		// 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; };
		this.canvas.ontouchstart = 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"] &amp;&amp; window["nwgui"]["App"]["clearCache"])
				window["nwgui"]["App"]["clearCache"]();
		}

		if (this.isCordova &amp;&amp; this.isiOS)
		{
			function IsInContentEditable(el)
			{
				do {
					// Don't check hasAttribute on the root document node (the method does not exist)
					if (el.parentNode &amp;&amp; el.hasAttribute("contenteditable"))
						return true;

					el = el.parentNode;
				}
				while (el);

				return false;
			}
			
			function KeyboardIsVisible()
			{
				var elem = document.activeElement;

				if (!elem)
					return false;

				var tagName = elem.tagName.toLowerCase();
				var inputTypes = ["email", "number", "password", "search", "tel", "text", "url"];

				if (tagName === "textarea")
					return true;
				
				if (tagName === "input")
					return inputTypes.indexOf(elem.type.toLowerCase() || "text") &gt; -1;

				return IsInContentEditable(elem);
			}
			
			/*
				HACK work around specific issue on notched iOS devices
				where opening the keyboard causing the view to scroll
				above the top of the screen, but the scroll position
				doesn't revert once the keyboard closes
			*/
			window.addEventListener("focusout", function ()
			{
				if (!KeyboardIsVisible())
					document.scrollingElement.scrollTop = 0;
			});
		}
		
		// 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 &gt; 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.tickcount_nosave = 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.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" &amp;&amp; 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.)");
		}
		
		// HACK: if Facebook Instant Games in use, wait for it to initialize before loading runtime.
		// We have to pass loading progress to the API, so we need to load it up front.
		if (this.useFbInstant)
		{
			FBInstant["initializeAsync"]()
			.then(function ()
			{
				self.requestProjectData();
			})
			.catch(function (err)
			{
				console.warn("[Instant Games] Unable to load: ", err);
				self.requestProjectData();
			});
			
			// initializeAsync() appears to never resolve if used outside of Instant Games. To avoid this causing the game
			// to fail to load, race it with a 4 second timeout to continue without Instant Games.
			window.setTimeout(function ()
			{
				if (self.hasInitialized)
					return;
				
				console.warn("[Instant Games] Initialization timed out after 4 seconds. Continuing with Instant Games disabled.");
				self.useFbInstant = false;
				self.requestProjectData();
			}, 4000);
		}
		else
		{
			// normal loading: kick off AJAX request for data.js
			this.requestProjectData();
		}
	};
	
	Runtime.prototype.requestProjectData = function ()
	{
		if (this.hasInitialized)
			return;		// ignore double initialization calls - can happen with FBInstant
		
		this.hasInitialized = true;
		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) &amp;&amp; ("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 &amp;&amp; ("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 &amp;&amp; !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 &amp;&amp; this.isiOS)
			this.isRetina = false;

		var dpr = window["devicePixelRatio"] || window["webkitDevicePixelRatio"] || window["mozDevicePixelRatio"] || window["msDevicePixelRatio"] || 1;

		this.devicePixelRatio = (this.isRetina ? dpr : 1);
		
		// ios - hide statusbar
		if (typeof window["StatusBar"] === "object")
			window["StatusBar"]["hide"]();

		// android - hide status and nav bar
		if (typeof window["AndroidFullScreen"] === "object")
			window["AndroidFullScreen"]["immersiveMode"]();
			
		// 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 &gt; 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
				};
				
				// HACK: WebGL 2 disabled on Android for the time being due to prevalence of seemingly GPU driver related issues
				if (!this.isAndroid)
					this.gl = this.canvas.getContext("webgl2", attribs);
				
				if (!this.gl)
				{
					this.gl = (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 &lt; len; i++)
			{
				t = this.types_by_index[i];
				
				for (j = 0, lenj = t.effect_types.length; j &lt; 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 &lt; len; i++)
			{
				l = this.layouts_by_index[i];
				
				for (j = 0, lenj = l.effect_types.length; j &lt; 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 &lt; lenj; j++)
				{
					y = l.layers[j];
					
					for (k = 0, lenk = y.effect_types.length; k &lt; 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;
			
			// Low quality fullscreen mode only supported with WebGL
			this.wantFullscreenScalingQuality = true;
			this.fullscreenScalingQuality = true;
		}
		
		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 &amp;&amp; !this.isWinJS &amp;&amp; !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") &gt; -1)
			{
				cr.logexport("Reloading for continuous preview");
				this.loadFromSlot = "__c2_continuouspreview";
				this.suspendDrawing = true;
			}
			
			// Pause on blur (unfocus)
			if (this.pauseOnBlur &amp;&amp; !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" &amp;&amp; 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) &amp;&amp; document["activeElement"] &amp;&amp; document["activeElement"] !== document.getElementsByTagName("body")[0] &amp;&amp; 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 &amp;&amp; this.isRetina &amp;&amp; this.devicePixelRatio &gt; 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);
	};
	
	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 &amp;&amp; this.lastWindowHeight === h &amp;&amp; !force)
			return;

		// ---------- do iphoneX hack ------------------------------------------

		if (this.isiPhone &amp;&amp; this.isCordova &amp;&amp; window["screen"])
		{
			var docStyle = document["documentElement"].style;
			var bodyStyle = document["body"].style;

			var isPortrait = window.innerWidth &lt; window.innerHeight;

			var height = isPortrait ? window["screen"]["height"] : window["screen"]["width"];
			var width = isPortrait ? window["screen"]["width"] : window["screen"]["height"];

			if (height &amp;&amp; width)
			{
				bodyStyle["height"] = docStyle["height"] = height + "px";
				bodyStyle["width"] = docStyle["width"] = width + "px";
			}
		}

		// ---------------------------------------------------------------------
		
		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"]) &amp;&amp; !this.isCordova;
		
		if (!isfullscreen &amp;&amp; this.fullscreen_mode === 0 &amp;&amp; !force)
			return;			// ignore size events when not fullscreen and not using a fullscreen-in-browser mode
		
		if (isfullscreen)
			mode = this.fullscreen_scaling;
		
		var dpr = this.devicePixelRatio;
		
		// Letterbox or letterbox integer scale modes: adjust width and height and offset canvas accordingly
		if (mode &gt;= 4)
		{
			// HACK: window inner size may have been rounded down by browser.
			if (mode === 5 &amp;&amp; dpr !== 1)	// integer scaling
			{
				w += 1;
				h += 1;
			}

			orig_aspect = this.original_width / this.original_height;
			cur_aspect = w / h;
			
			// too wide: scale to fit height
			if (cur_aspect &gt; 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 &gt; 1)
						intscale = Math.floor(intscale);
					else if (intscale &lt; 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 &gt; 1)
						intscale = Math.floor(intscale);
					else if (intscale &lt; 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 fullscreen: keep canvas size the same and just center it
		else if (isfullscreen &amp;&amp; mode === 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 &lt; 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 &lt; this.original_width &amp;&amp; this.height &lt; 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 &amp;&amp; cur_aspect &gt; orig_aspect) || (this.fullscreen_mode === 2 &amp;&amp; cur_aspect &lt; 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 &lt; orig_aspect)
						this.draw_width = this.draw_height * cur_aspect;
					else if (cur_aspect &gt; 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 &gt; orig_aspect)
						this.draw_width = this.draw_height * cur_aspect;
					else if (cur_aspect &lt; 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 &amp;&amp; !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"] &amp;&amp; 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 &amp;&amp; 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 &lt; 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 &lt; 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 &amp;&amp; !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 &lt; len; i++)
				this.suspend_events[i](true);
				
		}
		else if (!s &amp;&amp; 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 &lt; 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];

		// Note C3 exports pairs of [name, url] - only the names are used in C2 runtime
		this.webFontNames = pm[29].map(function (a) { return a[0]; });
		
		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 the loading logo image from blob project files list
			var loadingLogoFilename = pm[39];
			var loadingLogoUrl;
			if (this.isPreview)
				loadingLogoUrl = this.getProjectFileUrl(loadingLogoFilename);
			else
				loadingLogoUrl = this.iconsFolder + loadingLogoFilename;
			this.setImageSrc(loaderImage, loadingLogoUrl);
		}
		else if (this.loaderstyle === 4)	// c3 splash
		{
			var loaderC3logo = new Image();
			loaderC3logo.src = /*{splash}*/"splash-images/splash-logo.svg";
			
			//var loaderPowered_1024 = new Image();
			//loaderPowered_1024.src = /*{splash}*/"splash-images/splash-poweredby-1024.png";
			var loaderPowered_512 = new Image();
			loaderPowered_512.src = /*{splash}*/"splash-images/splash-poweredby-512.png";
			var loaderPowered_256 = new Image();
			loaderPowered_256.src = /*{splash}*/"splash-images/splash-poweredby-256.png";
			var loaderPowered_128 = new Image();
			loaderPowered_128.src = /*{splash}*/"splash-images/splash-poweredby-128.png";
			
			//var loaderWebsite_1024 = new Image();
			//loaderWebsite_1024.src = /*{splash}*/"splash-images/splash-website-1024.png";
			var loaderWebsite_512 = new Image();
			loaderWebsite_512.src = /*{splash}*/"splash-images/splash-website-512.png";
			var loaderWebsite_256 = new Image();
			loaderWebsite_256.src = /*{splash}*/"splash-images/splash-website-256.png";
			var loaderWebsite_128 = new Image();
			loaderWebsite_128.src = /*{splash}*/"splash-images/splash-website-128.png";
			
			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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; behavior_plugin instanceof cr.behaviors.solid)
						this.solidBehavior = behavior_plugin;
					
					if (cr.behaviors.jumpthru &amp;&amp; behavior_plugin instanceof cr.behaviors.jumpthru)
						this.jumpthruBehavior = behavior_plugin;
					
					if (cr.behaviors.shadowcaster &amp;&amp; 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 &lt; 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)
			var tilePolyData = m[13];
			type_inst.tile_poly_data = (tilePolyData ? tilePolyData[0] : null);
			
			// 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[16];

				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 &lt; len; i++)
		{
			var familydata = pm[4][i];
			var familytype = this.types_by_index[familydata[0]];
			var familymember;
			
			for (j = 1, lenj = familydata.length; j &lt; 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 &lt; len; i++)
		{
			var containerdata = pm[27][i];
			var containertypes = [];
			
			for (j = 0, lenj = containerdata.length; j &lt; lenj; j++)
				containertypes.push(this.types_by_index[containerdata[j]]);
			
			for (j = 0, lenj = containertypes.length; j &lt; lenj; j++)
			{
				containertypes[j].is_contained = true;
				containertypes[j].container = containertypes;
			}
		}
		
		// Map instance variables, behaviors and effects for families
		if (this.family_count &gt; 0)
		{
			for (i = 0, len = this.types_by_index.length; i &lt; 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 &lt; 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 &lt; 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 &lt; lenj; j++)
					t.effect_types[j].index = j;
			}
		}

		// Create layouts
		for (i = 0, len = pm[5].length; i &lt; 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 &lt; 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 &lt; len; i++)
			this.eventsheets_by_index[i].postInit();
		
		for (i = 0, len = this.eventsheets_by_index.length; i &lt; len; i++)
			this.eventsheets_by_index[i].updateDeepIncludes();

		for (i = 0, len = this.triggers_to_postinit.length; i &lt; 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 &lt; 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 &gt; 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] &amp;&amp; !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 &amp;&amp; !supportedAudioFormats["audio/webm; codecs=opus"] &amp;&amp; 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 &amp;&amp; src_ &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &lt; 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 &lt; len; ++i)
		{
			info = this.audio_files_info[i];
			namepart = info[0];
			isMusic = info[2];
			r = this.getPreferredAudioFile(namepart);
			
			if (r &amp;&amp; !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 &lt; 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 &lt;= 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 &amp;&amp; img.complete &amp;&amp; !img.c2error)
				completedsize += filesize;
			else
				ret = false;    // not all textures loaded
		}
		
		// All images finished loading: preload sounds if enabled
		if (ret &amp;&amp; this.preloadSounds &amp;&amp; this.audioInstance)
		{
			if (!audio_preload_started)
			{
				this.audioInstance.startPreloads();
				audio_preload_started = true;
			}
			
			audiocompletedsize = this.audioInstance.getPreloadedSize();
			
			completedsize += audiocompletedsize;
			
			if (audiocompletedsize &lt; 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 &amp;&amp; !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 &gt; 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() &amp;&amp; (this.loaderstyle !== 4 || isC3SplashDone))
		{
			// HACK: if Instant Games present, wait for startGameAsync() to resolve before kicking off game
			if (this.useFbInstant)
			{
				FBInstant["startGameAsync"]()
				.then(function ()
				{
					self.go_loading_finished();
				});
			}
			else
			{
				this.go_loading_finished();
			}
		}
		else
		{
			// Post progress to debugger if present
			
			// HACK: post progress to Instant Games if present
			if (this.useFbInstant)
				FBInstant["setLoadingProgress"](this.progress * 100);
			
			// 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 &amp; 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 &lt; 3 &amp;&amp; (ms_elapsed &gt;= 500 &amp;&amp; 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 &amp;&amp; this.loaderlogos.logo &amp;&amp; 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 &lt;= 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 &lt;= 128)
			return arr[2];
		else if (size &lt;= 256)
			return arr[1];
		else// if (size &lt;= 512)
			return arr[0];
		//else
		//	return arr[0];
	};
	
	Runtime.prototype.draw_c3_splash_loader = function(ctx)
	{
		if (isC3SplashDone)
			return;
		
		// Use a minimum splash duration to make sure the C3 logo is shown. However in preview mode,
		// or when using Facebook Instant Games, ignore this limit. For preview mode this helps speed
		// up testing, and in Instant Games there is an overlay showing which will obscure the logo,
		// and the splash then needlessly delays completion of loading.
		var allowQuickSplash = (this.isPreview || this.useFbInstant);
		
		var splashAfterFadeOutWait = (allowQuickSplash ? 0 : 200);
		var splashMinDisplayTime = (allowQuickSplash ? 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 &lt; 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 &amp;&amp; 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 &gt; 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 &amp;&amp; nowTime - splashStartTime &gt;= splashFadeInDuration &amp;&amp; splashFrameNumber &gt;= 2)
		{
			splashIsFadeIn = false;
			splashFadeInFinish = nowTime;
		}
		
		if (!splashIsFadeIn &amp;&amp; nowTime - splashFadeInFinish &gt;= splashMinDisplayTime &amp;&amp; !splashIsFadeOut &amp;&amp; this.progress &gt;= 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/quick splash mode, if it loads quicker than 500ms, just skip straight to the game to avoid getting in the way
		if ((splashIsFadeOut &amp;&amp; nowTime - splashFadeOutStart &gt;= splashFadeOutDuration + splashAfterFadeOutWait) ||
			(allowQuickSplash &amp;&amp; this.progress &gt;= 1 &amp;&amp; Date.now() - splashStartTime &lt; 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 &amp;&amp; document.fonts &amp;&amp; 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 &lt; len; i++)
			{
				t = this.types_by_index[i];
				
				if (!t.is_family &amp;&amp; !t.isOnLoaderLayout &amp;&amp; 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 &lt; len; i++)
		{
			this.layouts_by_index[i].createGlobalNonWorlds();
		}
		
		// make sure aspect scale is correctly set in advance of first tick
		if (this.fullscreen_mode &gt;= 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 &amp;&amp; cur_aspect &gt; orig_aspect) || (this.fullscreen_mode === 2 &amp;&amp; cur_aspect &lt; orig_aspect))
				this.aspect_scale = this.height / this.original_height;
			else
				this.aspect_scale = this.width / this.original_width;
		}
		
		// Trigger onBeforeAppBegin for any plugins with a handler
		for (i = 0, len = this.types_by_index.length; i &lt; len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onBeforeAppBegin)
				t.onBeforeAppBegin();
		}
		
		// 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"] &amp;&amp; navigator["splashscreen"]["hide"])
			navigator["splashscreen"]["hide"]();
			
		// Trigger onAppBegin for any plugins with a handler
		for (i = 0, len = this.types_by_index.length; i &lt; 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 &amp;&amp; this.isSuspended &amp;&amp; !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"]) &amp;&amp; !this.isCordova;
		
		if (isfullscreen &amp;&amp; this.fullscreen_scaling &gt; 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 &gt; 0 &amp;&amp; (!this.isiOS || window.self !== window.top))
		if (fsmode &gt; 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 &amp;&amp; !this.is_WebGL_context_lost &amp;&amp; !this.suspendDrawing &amp;&amp; !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 &amp;&amp; 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.tickcount_nosave++;
			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 &gt;= 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 &gt;= 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 &amp;&amp; this.gpuCurUtilisation === -1 &amp;&amp; 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 &lt; 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 &gt; 0.5)
				this.dt1 = 0;
			// Cap at a max dt of 0.1 (min framerate 10fps).
			else if (this.dt1 &gt; 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"]) &amp;&amp; !this.isCordova;
		
		// Calculate the project-wide zoom for fullscreen-scale mode
		if (this.fullscreen_mode &gt;= 2 /* scale */ || (isfullscreen &amp;&amp; this.fullscreen_scaling &gt; 0))
		{
			var orig_aspect = this.original_width / this.original_height;
			var cur_aspect = this.width / this.height;
			
			var mode = this.fullscreen_mode;
					
			if (isfullscreen &amp;&amp; this.fullscreen_scaling &gt; 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 &amp;&amp; cur_aspect &gt; orig_aspect) || (mode === 2 &amp;&amp; cur_aspect &lt; 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 &lt; leni; i++)
            tickarr[i].pretick();

		// Tick behaviors
		for (i = 0, leni = this.types_by_index.length; i &lt; 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 &amp;&amp; !type.families.length))
				continue;

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

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

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

				// For each behavior-instance in instance
				for (k = 0, lenk = inst.behavior_insts.length; k &lt; 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 &lt; 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 &amp;&amp; i++ &lt; 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 &lt; 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 &lt; leni; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family || (!type.behaviors.length &amp;&amp; !type.families.length))
				continue;	// type doesn't have any behaviors

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

				// For each behavior-instance in instance
				for (k = 0, lenk = inst.behavior_insts.length; k &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; len; i++)
			{
				type = this.types_by_index[i];
				
				if (type.is_family)
					continue;
				
				// This type not used on next layout
				if (type.unloadTextures &amp;&amp; (!type.global || type.instances.length === 0) &amp;&amp; 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 &lt; 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 &lt; len; i++)
		{
			type = this.types_by_index[i];
			
			if (!type.global &amp;&amp; !type.plugin.singleglobal)
				continue;

			// For each instance in type
			for (j = 0, lenj = type.instances.length; j &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; is_world &amp;&amp; !type.isOnLoaderLayout)
			return null;
			
		// Fail to create if no WebGL renderer available and effect fallback is 'destroy'
		if (is_world &amp;&amp; !this.glwrap &amp;&amp; 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 &amp;&amp; !skip_siblings &amp;&amp; !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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; 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 &lt; len; i++)
				{
					for (j = 0, lenj = wm[12][i].length; j &lt; 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 &lt; 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 &lt; 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[13])
			{
				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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; !skip_siblings)	// layout links initial instances
			{
				for (i = 0, len = type.container.length; i &lt; 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 &lt; len; i++)
				{
					inst.siblings[i].siblings.push(inst);
					
					for (j = 0; j &lt; 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 &lt; 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 &lt; 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 &lt; 0)
			index = 0;
		if (index &gt;= 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; len; ++i)
		{
			instances[i].update_collision_cell();
		}
		
		// include anything on createrow
		var createRow = this.createRow;
		
		for (i = 0, len = createRow.length; i &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; (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 &amp;&amp; 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 &amp;&amp; !a.collision_poly.is_empty());
			haspolyb = (b.collision_poly &amp;&amp; !b.collision_poly.is_empty());
			
			// Neither have collision polys: in bounding quad overlap
			if (!haspolya &amp;&amp; !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 &amp;&amp; !a.collision_poly.is_empty());
			haspolyb = (b.collision_poly &amp;&amp; !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 &lt; 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 &lt; 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 &amp;&amp; !a.collision_poly.is_empty());
		
		for (i = 0, len = collrects.length; i &lt; 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 &lt; 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 &amp;&amp; !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 &lt; 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 &amp;&amp; !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 &lt; 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 &lt; len; i++)
			{
				f = t.families[i];
				
				for (j = 0, lenj = f.behaviors.length; j &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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;
	};
	
	// As with pushOutSolid, but pushes out in either direction on a given axis.
	Runtime.prototype.pushOutSolidAxis = function(inst, xdir, ydir, dist)
	{
		dist = dist || 50;
		var oldX = inst.x;
		var oldY = inst.y;
		
		var lastOverlapped = null;
		var secondLastOverlapped = null;
		var i, which, sign;
		
		for (i = 0; i &lt; dist; ++i)
		{
			// Test both forwards and backwards directions at this distance
			for (which = 0; which &lt; 2; ++which)
			{
				sign = which * 2 - 1;		// -1 or 1
				
				inst.x = oldX + (xdir * i * sign);
				inst.y = oldY + (ydir * i * sign);
				inst.set_bbox_changed();
				
				// Test if we've cleared the last instance we were overlapping
				if (!this.testOverlap(inst, lastOverlapped))
				{
					// See if we're still overlapping a different solid
					lastOverlapped = this.testOverlapSolid(inst);
					
					if (lastOverlapped)
					{
						secondLastOverlapped = lastOverlapped;
					}
					else
					{
						// Clear of all solids - completed push out.
						// Push in fractionally against the last overlapped instance if any.
						if (secondLastOverlapped)
							this.pushInFractional(inst, xdir * sign, ydir * sign, secondLastOverlapped, 16);
						
						return true;
					}
				}
			}
		}
		
		// Didn't get out of a solid: oops, we're stuck. Just restore the 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 &lt; 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 &lt;= 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 &lt;= 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]);
	};
	
	// Look through all registered collisions to see what 'inst' has registered a collision with.
	// If any of those instances belong to 'otherType' (either directly or via family), add them
	// to 'arr' if not already there. This helps 'On collision' correctly include registered
	// collisions when using collision cells.
	Runtime.prototype.addRegisteredCollisionCandidates = function (inst, otherType, arr)
	{
		var i, len, r, otherInst;
		for (i = 0, len = this.registered_collisions.length; i &lt; len; ++i)
		{
			r = this.registered_collisions[i];
			
			otherInst = null;
			if (r[0] === inst)
				otherInst = r[1];
			else if (r[1] === inst)
				otherInst = r[0];
			else
				continue;
			
			// Check otherInst belongs to otherType. If it's a family check it's a family member,
			// otherwise just check it has the same type.
			if (otherType.is_family)
			{
				if (otherType.members.indexOf(otherType) === -1)
					continue;
			}
			else
			{
				if (otherInst.type !== otherType)
					continue;
			}
			
			// otherInst is a registered collision candidate. Add it to the array if not already in.
			if (arr.indexOf(otherInst) === -1)
				arr.push(otherInst);
		}
	};
	
	Runtime.prototype.checkRegisteredCollision = function (a, b)
	{
		var i, len, x;
		for (i = 0, len = this.registered_collisions.length; i &lt; len; i++)
		{
			x = this.registered_collisions[i];
			
			if ((x[0] === a &amp;&amp; x[1] === b) || (x[0] === b &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &gt; 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 &lt; 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 &amp;&amp; this.isInOnDestroy === 0 &amp;&amp; triggerSheetIndex === 0 &amp;&amp; !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 &gt;= 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 &gt;= 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 &gt;= 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 &lt; leni; i++)
			{
				e = scope.subevents[i];

				if (e instanceof cr.eventvariable &amp;&amp; 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 &lt; leni; i++)
		{
			sheet = this.eventsheets_by_index[i];

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

				if (e instanceof cr.eventvariable &amp;&amp; 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 &lt; 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 &lt; 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 &lt; len; i++)
		{
			if (this.allGroups[i].sid === sid_)
				return this.allGroups[i];
		}
		
		return null;
	};
	

	function GetPermissionAPI()
	{
		var api = window["cordova"] &amp;&amp; window["cordova"]["plugins"] &amp;&amp; window["cordova"]["plugins"]["permissions"];

		if (typeof api !== "object")
		{
			Promise.reject("Permission API is not loaded");
		}

		return Promise.resolve(api);
	}

	function RequestPermission(id)
	{
		return GetPermissionAPI()
		.then(function (api)
		{
			return new Promise(function (resolve, reject)
			{
				api["requestPermission"](api[id], function (status) {
					resolve(status["hasPermission"]);
				}, reject);
			});
		});
	}

	function HasPermission(id)
	{
		return GetPermissionAPI()
		.then(function (api)
		{
			return new Promise(function (resolve, reject)
			{
				api["checkPermission"](api[id], function (status) {
					resolve(status["hasPermission"]);
				}, reject);
			});
		});
	}

	Runtime.prototype.requirePermissions = function RequirePermissions(permissions)
	{
		return this.getPermissions(permissions)
		.then(function (result)
		{
			if (result === false)
				return Promise.reject("Permission not granted");
		})
	}

	Runtime.prototype.getPermissions = function GetPermissions(permissions)
	{
		// only required with android ( cordova )
		if ( !this.isCordova || !this.isAndroid)
			return Promise.resolve(true);

		return permissions.reduce(function (previous, id)
		{	
			return previous.then(function (previousResult) {
				if (previousResult === false)
					return false;

				return HasPermission(id)
				.then(function (status)
				{
					if (status)
						return true;
					else
						return RequestPermission(id);
				});
			})
		}, Promise.resolve(true))
	}
	

	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") &gt; -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 = "";
					savingJson = "";
					
					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 = "";
						savingJson = "";
						
						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 = "";
					savingJson = "";
					
					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] &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; (!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 &lt; len; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family)
				continue;
			
			for (j = 0, lenj = type.instances.length; j &lt; 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"] &gt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; 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) &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; inst.instance_vars.length)
		{
			o["ivs"] = {};
			
			for (i = 0, len = inst.instance_vars.length; i &lt; 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 &lt; 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 &amp;&amp; inst.behavior_insts.length)
		{
			o["behs"] = {};
			
			for (i = 0, len = inst.behavior_insts.length; i &lt; 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 &lt; 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 &lt; 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, value;
		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 &lt; 0 || iv &gt;= inst.instance_vars.length)
						continue;		// must've gone missing
					
					// NaN, Infinity and -Infinity save to JSON as null. This isn't a number type so breaks things on load.
					// If we load null, just convert it to NaN. This changes Infinity but it's a limitation in JSON.
					value = oivs[p];
					if (value === null)
						value = NaN;
					
					inst.instance_vars[iv] = value;
				}
			}
		}
		
		// 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 &lt; len; i++)
				{
					fxindex = type.getEffectIndexByName(world["fx"][i]["name"]);
					
					if (fxindex &lt; 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 &lt; 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 &gt;= 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 &amp;&amp; !this.isAbsoluteUrl(src))
		{
			this.fetchLocalFileViaCordovaAsURL(src, function (url)
			{
				img.src = url;
			}, function (err)
			{
				alert("Failed to load image: " + err);
			});
		}
		else if (this.isPreview &amp;&amp; !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 &lt; len; ++i)
		{
			r = info[i];
			type = r[0];
			
			if (!webMOpusFile &amp;&amp; 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);
}


// c2/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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; 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 &lt; 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) &lt; 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 &lt; 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 &lt; 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 &lt; left, since that means it's not in the grid.
		if (remove_from_grid &amp;&amp; this.useRenderCells &amp;&amp; inst.rendercells &amp;&amp; inst.rendercells.right &gt;= 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 &amp;&amp; this.useRenderCells &amp;&amp; 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 &amp;&amp; this.useRenderCells &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; lena &amp;&amp; j &lt; lenb; ++k)
		{
			ai = a[i];
			bj = b[j];
			
			if (ai.zindex &lt; bj.zindex)
			{
				out[k] = ai;
				++i;
			}
			else
			{
				out[k] = bj;
				++j;
			}
		}
		
		// Finish last run of either array if not done yet
		for ( ; i &lt; lena; ++i, ++k)
			out[k] = a[i];
		
		for ( ; j &lt; 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 &lt; 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 &gt; 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 &gt;= 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 &lt; 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 &lt; this.viewLeft || bbox.bottom &lt; this.viewTop || bbox.left &gt; this.viewRight || bbox.top &gt; 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 &gt;= 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 shaderindex = 0;
		var etindex = 0;
		
		// Needs own texture
		this.render_offscreen = (this.forceOwnTexture || this.opacity !== 1.0 || this.active_effect_types.length &gt; 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;
		
		// Bypass drawing instances if the layer scale is set to 0 (or close to 0). Some instances e.g. Tilemap render
		// based on cells in the visible viewport area, and as the scale approaches 0 the viewport size approaches infinity,
		// resulting in an infinite loop trying to render across the viewport area.
		// NOTE: Number.EPSILON is not defined in IE11, so use its actual value instead for compatibility.
		var epsilon = 2.220446049250313e-16;		// Number.EPSILON
		if (this.getNormalScale() &gt; epsilon)
		{
			for (i = 0, len = instances_to_draw.length; i &lt; 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 &amp;&amp; !glw.programUsesCrossSampling(shaderindex) &amp;&amp;
			//		!glw.programExtendsBox(shaderindex) &amp;&amp; (!inst.angle || !glw.programUsesDest(shaderindex)) &amp;&amp;
			//		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 &amp;&amp;
				!glw.programUsesCrossSampling(shaderindex) &amp;&amp; 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 &lt; this.viewLeft || bbox.bottom &lt; this.viewTop || bbox.left &gt; this.viewRight || bbox.top &gt; 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 &lt; this.viewLeft || bbox.bottom &lt; this.viewTop || bbox.left &gt; this.viewRight || bbox.top &gt; 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();
		var windowWidth = this.runtime.draw_width;
		var windowHeight = this.runtime.draw_height;
		
		if (inst.active_effect_types.length === 1 &amp;&amp; !glw.programUsesCrossSampling(shaderindex) &amp;&amp;
			!glw.programExtendsBox(shaderindex) &amp;&amp; ((!inst.angle &amp;&amp; !inst.layer.getAngle()) || !glw.programUsesDest(shaderindex)) &amp;&amp;
			inst.opacity === 1 &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; !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 &lt; 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 &lt; len; ++i)
			temp_set.add(this.created_globals[i]);
		
		for (i = 0, j = 0, len = this.initial_instances.length; i &lt; 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 &lt; 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;
}());


// c2/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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; layer.opacity === 1.0 &amp;&amp; !layer.forceOwnTexture &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; lenk; k++)
			{
				inst = type_instances[k];
				
				if (inst.layer)
				{
					var num = inst.layer.number;
					if (num &gt;= 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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();
		}
		
		this.createAndLinkContainerInstances(created_instances);
		
		// Create all initial non-world instances
		for (i = 0, len = this.initial_nonworld.length; i &lt; 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 &lt; 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 &lt; 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.createAndLinkContainerInstances = function (arr)
	{
		// 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.
		var i, inst, iid, k, lenk, t, s;
		
		for (i = 0; i &lt; arr.length; i++)
		{
			inst = arr[i];
			
			if (!inst.type.is_contained)
				continue;
				
			iid = inst.get_iid();
				
			for (k = 0, lenk = inst.type.container.length; k &lt; lenk; k++)
			{
				t = inst.type.container[k];
				
				if (inst.type === t)
					continue;
					
				if (t.instances.length &gt; 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);
						arr.push(s);		// come back around and link up its own instances too
					}
				}
			}
		}
	};
	
	function hasAnyWorldType(container)
	{
		return container.some(function (t)
		{
			return t.plugin.is_world;
		});
	}
	
	Layout.prototype.createGlobalNonWorlds = function ()
	{
		var i, k, len, initial_inst, type;
		var created = [];
		
		// Create all initial global non-world instances
		for (i = 0, k = 0, len = this.initial_nonworld.length; i &lt; 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 with any world types, don't create it - it should only be created along
				// with its container instances. However if there are no world types (e.g. a container of Dictionaries) go ahead
				// and create the instance.
				if (!type.is_contained || !hasAnyWorldType(type.container))
				{
					created.push(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);
		
		// Link up any containers of non-world instances
		this.runtime.ClearDeathRow();
		this.createAndLinkContainerInstances(created);
	};

	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 &lt; 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 &lt; 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 &lt; leni; i++)
		{
			layer_instances = this.layers[i].instances;
			
			for (j = 0, lenj = layer_instances.length; j &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; !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 &lt; 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 &amp;&amp; l.opacity &gt; 0 &amp;&amp; l.blend_mode !== 11 &amp;&amp; (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 &gt;= 0; --i)
		{
			l = this.layers[i];
			
			// Only early-pass render layers which preserve opaqueness
			if (l.visible &amp;&amp; l.opacity === 1 &amp;&amp; l.shaders_preserve_opaqueness &amp;&amp;
				l.blend_mode === 0 &amp;&amp; (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 &gt; 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 &amp;&amp; !this.hasOpaqueBottomLayer())
			glw.clear(0, 0, 0, 0);

		// Draw each layer
		var i, len, l;
		for (i = 0, len = this.layers.length; i &lt; len; i++)
		{
			l = this.layers[i];
			
			if (l.visible &amp;&amp; l.opacity &gt; 0 &amp;&amp; (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 &amp;&amp; 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 &gt; 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 &lt; len; i++)
		{
			l = this.layers[i];
			
			if (l.parallaxX === 0 &amp;&amp; l.parallaxY === 0)
				continue;
			
			if (l.getScale() &lt; 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 &gt; 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 &lt; 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 &gt; 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 &lt; 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 &lt; 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 &lt; 0)					screenleft = 0;
			if (screentop &lt; 0)					screentop = 0;
			if (screenright &gt; windowWidth)		screenright = windowWidth;
			if (screenbottom &gt; windowHeight)	screenbottom = windowHeight;
			if (clearleft &lt; 0)					clearleft = 0;
			if (cleartop &lt; 0)					cleartop = 0;
			if (clearright &gt; windowWidth)		clearright = windowWidth;
			if (clearbottom &gt; 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 &amp;&amp; (glw.programUsesDest(active_effect_types[0].shaderindex) || boxExtendHorizontal !== 0 || boxExtendVertical !== 0 || inst.opacity !== 1 || inst.type.plugin.must_predraw)) || (layer &amp;&amp; !inst &amp;&amp; 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 &amp;&amp; !inst &amp;&amp; !this.runtime.fullscreenScalingQuality);
		
		var etindex = 0;
		
		// For each effect to render
		for (i = 0, len = active_effect_types.length; i &lt; 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 &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; !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 &amp;&amp; !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 &amp;&amp; !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 &amp;&amp; active_effect_types.length === 1 &amp;&amp; !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 &lt; 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 &lt; 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 &lt; 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 &lt; 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;
	
}());


// c2/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] &amp;&amp; arr1[1] === arr2[1];
		default:
			for (i = 0; i &lt; 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 &gt; arr[1].index)
			{
				temp = arr[0];
				arr[0] = arr[1];
				arr[1] = temp;
			}
		}
		else if (arr.length &gt; 2)
			arr.sort(solArraySorter);		// so testSolsMatch compares in same order
			
		if (arr.length &gt;= 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 &lt; 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 &lt; 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
		case 3:	// event group
		{
			// 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 &lt; 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 &lt; len; i++)
		{
			this.events[i].postInit(i &lt; len - 1 &amp;&amp; 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 &lt; len; ++i)
		{
			inc = arr[i];
			sheet = inc.include_sheet;
			
			if (!inc.isActive() || root_sheet === sheet || already_included_sheets.indexOf(sheet) &gt; -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 &lt; 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 &amp;&amp; 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 &lt; 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 &lt; 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 (uses type 3)
        if (m[0] === 3)
        {
			// [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[6];
		
		for (i = 0, len = cm.length; i &lt; 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 &amp;&amp; !cnd.type.plugin.singleglobal &amp;&amp; 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[7];
		
		for (i = 0, len = am.length; i &lt; 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 === 9)
		{
			var em = m[8];
			
			for (i = 0, len = em.length; i &lt; 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 &amp;&amp; this.conditions[0].func == cr.system_object.prototype.cnds.Else);
		}
	};
	
	/*c2h=aacfd919-6396-41a2-9cfe-30143ffa5c74*/
	
	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() &amp;&amp; (!this.parent || (this.parent.group &amp;&amp; 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 &lt; 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 &lt; len; i++)
			this.conditions[i].postInit();

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

		for (i = 0, len = this.subevents.length; i &lt; len; i++)
		{
			this.subevents[i].postInit(i &lt; len - 1 &amp;&amp; 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 &amp;&amp; this.prev_block)
		{
			for (i = 0, len = this.prev_block.solModifiers.length; i &lt; 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 &lt; len; ++i)
		{
			this.contained_includes[i].updateActive();
		}
		
		// Pre-computed deep includes list may have changed if any includes
		// contained in this group
		if (len &gt; 0 &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; 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 &amp;&amp; this.has_else_block)
			evinfo.else_branch_ran = true;
			
		// Clear death row between top-level events
		if (this.toplevelevent &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; i &lt; 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 &lt; 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 &lt; 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 &lt; 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 &gt;= 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 &amp; initialisation
		this.func = this.runtime.GetObjectReference(m[1]);
;

		this.trigger = (m[3] &gt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; !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 &gt; -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 &lt; 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 &lt; 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 &lt; leni; ++i)
			{
				inst = arr[i];
;

				// Evaluate parameters
				if (params_vary)
				{
					for (j = 0, lenj = parameters.length; j &lt; 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 &amp;&amp; !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 &lt; leni; ++i)
			{
				inst = arr[i];
;

				// Evaluate parameters
				if (params_vary)
				{
					for (j = 0, lenj = parameters.length; j &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; !any_true)
			{
				for (i = 0, leni = sol.instances.length; i &lt; leni; i++)
				{
					inst = sol.instances[i];

					// Evaluate parameters
					if (params_vary)
					{
						for (j = 0, lenj = parameters.length; j &lt; 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 &amp; 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];
		
		// note m[4] (action return type) not used in C2 runtime
		
		this.runtime.actsBySid[this.sid.toString()] = this;

		// Initialise parameters
		if (m.length === 7)
		{
			var i, len;
			var em = m[6];
			
			for (i = 0, len = em.length; i &lt; 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 &lt; 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 &lt; 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 &gt; -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 &lt; 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 &lt; lenj; ++j)
				results[j] = parameters[j].get(0);
		}

		for (i = 0, leni = instances.length; i &lt; 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 &lt; 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 &amp; 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();
					
					// For actions, also set the owner block as a SOL writer after conditions, as done with normal object parameters
					if (this.owner instanceof cr.action)
						this.block.setSolWriterAfterCnds();
				}
				
				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 16:	// boolean
				this.combosel = m[1] ? 1 : 0;		// keep as number
				this.get = this.get_boolean;		// but return as boolean
				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 &amp;&amp; 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 &lt; 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 &lt; 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_boolean = function ()
	{
		return !!this.combosel;		// 0 or 1, convert to boolean
	};

	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 &lt; 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 &gt;= 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 &gt;= 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 &amp;&amp; !this.is_static &amp;&amp; !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 &amp;&amp; !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 &lt; 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;
	
}());

// c2/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 &amp;&amp; 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 &gt;= 4 &amp;&amp; this.type &lt;= 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 &lt; 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 &lt; 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 &lt; 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 &gt;= len || index &lt;= -len)
			index %= len;      // wraparound

		// If negative, offset such that -1 is the last selected
		if (index &lt; 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 &gt;= len || index &lt;= -len)
			index %= len;      // wraparound

		// If negative, offset such that -1 is the last selected
		if (index &lt; 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 &lt; 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 &gt;= len || index &lt;= -len)
			index %= len;		// wraparound
		
		// If negative, offset such that -1 is the last selected
		if (index &lt; 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() &amp;&amp; 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() &amp;&amp; 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() &amp;&amp; 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() &amp;&amp; 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() &amp;&amp; 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() &amp;&amp; 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() &amp;&amp; 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 &amp;&amp; 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() &amp;&amp; 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 &lt; 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 &lt;= 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 &gt; 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 &gt;= 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
	};
}());

// c2/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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &gt;= 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 &lt; count &amp;&amp; !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 &lt; count &amp;&amp; !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 &lt; start)
		{
			if (solModifierAfterCnds)
			{
				for (i = start; i &gt;= end &amp;&amp; !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 &gt;= end &amp;&amp; !current_loop.stopped; --i)  // inclusive to end
				{
					current_loop.index = i;
					current_event.retrigger();
				}
			}
		}
		else
		{
			if (solModifierAfterCnds)
			{
				for (i = start; i &lt;= end &amp;&amp; !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 &lt;= end &amp;&amp; !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 &lt; len &amp;&amp; !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 &lt; 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 &lt; len &amp;&amp; !current_loop.stopped; i++)
			{
				inst = instances[i];
				sol.instances[0] = inst;
				
				if (is_contained)
				{
					for (j = 0, lenj = inst.siblings.length; j &lt; 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) &amp;&amp; cr.is_number(vb))
			return va - vb;
		else
		{
			va = "" + va;
			vb = "" + vb;
			
			if (va &lt; vb)
				return -1;
			else if (va &gt; 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 &lt; 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 &lt; len &amp;&amp; !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 &lt; 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 &lt; len &amp;&amp; !current_loop.stopped; i++)
			{
				inst = instances[i];
				sol.instances[0] = inst;
				
				if (is_contained)
				{
					for (j = 0, lenj = inst.siblings.length; j &lt; 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 &lt; 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 &lt; 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 &gt;= 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 &gt;= 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 &lt; 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 &lt; 0 || index &gt;= 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 &gt;= 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 &amp;&amp; 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 &gt;= a &amp;&amp; x &lt;= 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 &lt; 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 &amp;&amp; sol.else_instances.length === 0 &amp;&amp; s.instances.length &gt;= 2)
				{
					inst = sol.instances[0];
					sol.instances.length = 0;
					
					for (j = 0, lenj = s.instances.length; j &lt; 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 &amp;&amp; !rt.isCordova &amp;&amp; !rt.isWinJS &amp;&amp; !rt.isWindowsPhone8 &amp;&amp; !rt.isBlackberry10 &amp;&amp; !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 &lt; 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 &gt;= 0; --i)
		{
			inst = createRow[i];
			
			if (isFamily)
			{
				if (inst.type.families.indexOf(obj_) &gt;= 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 &amp;&amp; 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)) &lt;= 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) &amp;&amp; cr.angleClockwise(angle, upper));
		else
			return cr.angleClockwise(angle, lower) &amp;&amp; !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 &amp;&amp; index === 0)	
			return;		// cannot go to previous layout from first layout
		
		if (!prev &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 0)
            ts = 0;

        this.runtime.timescale = ts;
    };

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

        if (ts &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; len; i++)
		{
			t = this.runtime.types_by_index[i];
			s = t.getCurrentSol();
			
			if (s.select_all &amp;&amp; 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 &lt; len; i++)
		{
			t = this.runtime.types_by_index[i];
			s = t.getCurrentSol();
			
			if (s.select_all &amp;&amp; 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 &lt; 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 &lt; 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 &amp;&amp; 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 &lt; 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 &amp;&amp; layer.background_color[1] === g &amp;&amp; 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 &lt; 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) &amp;&amp; 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 &lt; 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 &lt;= 0 || h &lt;= 0)
			return;
		
		var mode = this.runtime.fullscreen_mode;
		
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || !!document["msFullscreenElement"] || document["fullScreen"]);
		
		if (isfullscreen &amp;&amp; this.runtime.fullscreen_scaling &gt; 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_ &lt; 0 || index_ &gt;= 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_ &lt; 0 || index_ &gt;= 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"]);
		
		if (!isfullscreen &amp;&amp; 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 &lt; 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 &lt; 1)
			f = 1;
		if (f &gt; 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;
		var anyChanged = false;
		
		// 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 &lt; 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 &lt; 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
				inst.layer = layer;						// update instance's layer reference (could have changed)
				layer.setZIndicesStaleFrom(toZ);		// mark Z indices stale from this point since they have changed
				anyChanged = true;
			}
		}
		
		if (anyChanged)
			this.runtime.redraw = true;
	};
	
	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 &lt; len; i++)
		{
			a = arguments[i];
			
			if (typeof a !== "number")
				continue;		// ignore non-numeric types
			
			if (max_ &lt; 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 &lt; len; i++)
		{
			a = arguments[i];
			
			if (typeof a !== "number")
				continue;		// ignore non-numeric types
			
			if (min_ &gt; 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 &gt;= 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 &lt; l)
			ret.set_float(l);
		else if (x &gt; 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) &amp;&amp; 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) &amp;&amp; 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) &amp;&amp; cr.is_string(sep))
		{
			var arr = text.split(sep);
			var i = cr.floor(index_);
			
			if (i &lt; 0 || i &gt;= 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) &amp;&amp; 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) &amp;&amp; cr.is_string(find_) &amp;&amp; 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 &lt; 0 ? "-" : "");
		if (n &lt; 0) n = -n;
		var zeroes = d - n.toString().length;
		for (var i = 0; i &lt; 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 &amp;&amp; regex_ === lastMatchesRegex &amp;&amp; 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_.toString(), 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_.toString(), regex_, flags_);
		
		if (!regexMatches || index_ &lt; 0 || index_ &gt;= 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 &amp; ~(1 &lt;&lt; b)) | (v &lt;&lt; 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 &lt;&lt; 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 &amp; (1 &lt;&lt; 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 &lt; 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 &gt; 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 &lt; len; i++)
		{
			w = this.waits[i];
			this.waits[j] = w;
			
			if (w.deleteme)
				freeWaitObject(w);
			else
				j++;
		}
		
		cr.truncateArray(this.waits, j);
	};

}());


// c2/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];
		var element_aces = m[10];
		var element_focus_aces = m[11];
		var element_enabled_aces = m[12];
				
		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 &lt; layer.viewLeft || bbox.bottom &lt; layer.viewTop || bbox.left &gt; layer.viewRight || bbox.top &gt; layer.viewBottom);
			};

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

				return (bbox.right &lt; 0 || bbox.bottom &lt; 0 || bbox.left &gt; layout.width || bbox.top &gt; 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 &lt; len; i++)
				{
					inst = instances[i];
					d = cr.distanceTo(inst.x, inst.y, x, y);
					
					if ((which === 0 &amp;&amp; d &lt; dist) || (which === 1 &amp;&amp; d &gt; 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)) &lt;= 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) &amp;&amp; cr.angleClockwise(angle, upper));
				else
					return cr.angleClockwise(angle, lower) &amp;&amp; !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 &amp;&amp; !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 &amp;&amp; !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 &lt; len; i++)
				{
					inst = instances[i];
					v = inst.instance_vars[iv];
					
					if ((which === 0 &amp;&amp; v &lt; val) || (which === 1 &amp;&amp; v &gt; 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 &lt; 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 &lt; 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 &amp;&amp; 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 &lt; 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 &lt; 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 &lt; len; i++)
				{
					inst = this.runtime.createRow[i];
					
					if (ret.object_class.is_family)
					{
						if (inst.type.families.indexOf(ret.object_class) &gt;= 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 === 2)		// toggle
					v = !this.visible;
				else
					v = (v !== 0);
				
				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 &lt; 0)
					new_opacity = 0;
				else if (new_opacity &gt; 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 &lt; len; i++)
				{
					inst = instances[i];
					
					// Testing if above
					if (which_ === 0)
					{
						if (inst.layer.index &gt; pickme.layer.index || (inst.layer.index === pickme.layer.index &amp;&amp; inst.get_zindex() &gt; pickme.get_zindex()))
						{
							pickme = inst;
						}
					}
					// Testing if below
					else
					{
						if (inst.layer.index &lt; pickme.layer.index || (inst.layer.index === pickme.layer.index &amp;&amp; inst.get_zindex() &lt; 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 &amp;&amp; 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 &amp;&amp; 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 &lt; 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 &lt; 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_ &lt; 0 || index_ &gt;= 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 &amp;&amp; arr[1] === g &amp;&amp; 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;
				}
			};
		}

		if (element_aces)
		{
			acts.SetVisible = function (vis)
			{
				this.visible = (vis !== 0);
			};

			acts.SetCSSStyle = function (p, v)
			{
				this.elem.style[cr.cssToCamelCase(p)] = v;
			};
		}

		if (element_focus_aces)
		{
			acts.SetFocus = function ()
			{
				// hack for checkbox buttons
				var elem = (this.inputElem || this.elem);
				elem.focus();
			};

			acts.SetBlur = function ()
			{
				var elem = (this.inputElem || this.elem);
				elem.blur();
			};
		}

		if (element_enabled_aces)
		{
			acts.SetEnabled = function (en)
			{
				var elem = (this.inputElem || this.elem);
				elem.disabled = (en === 0);
			};
		}
	};

	// 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; !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 &lt; 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 &amp;&amp; frominst.is_contained &amp;&amp; frominst.type != this)
		{
			var i, len, s;
			for (i = 0, len = frominst.siblings.length; i &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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;
		}
		else
		{
			clonesol.select_all = false;
			cr.shallowAssignArray(clonesol.instances, prevsol.instances);
		}
		
		// Make sure any leftover else_instances are cleared; not always reset by next event
		cr.clearArray(clonesol.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 &lt; len; i++)
			{
				f = this.families[i];
				
				for (j = 0, lenj = f.behaviors.length; j &lt; 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 &lt; 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 &lt; 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 &amp;&amp; es.current_event &amp;&amp; es.current_event.orblock;
		
		for (i = 0, len = this.container.length; i &lt; 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 &lt; 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 &lt; 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 &lt; y;
			case 3:     // less/equal
				return x &lt;= y;
			case 4:     // greater
				return x &gt; y;
			case 5:     // greater/equal
				return x &gt;= y;
			default:
;
				return false;
		}
	};

})();

cr.shaders = {};


// Touch
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.Touch.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.touches = [];
		this.mouseDown = false;
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	var dummyoffset = {left: 0, top: 0};

	instanceProto.findTouch = function (id)
	{
		var i, len;
		for (i = 0, len = this.touches.length; i &lt; len; i++)
		{
			if (this.touches[i]["id"] === id)
				return i;
		}
		
		return -1;
	};
	
	var pg_accx = 0;
	var pg_accy = 0;
	var pg_accz = 0;
	
	function PhoneGapGetAcceleration(evt)
	{
		pg_accx = evt.x;
		pg_accy = evt.y;
		pg_accz = evt.z;
	};
	
	var theInstance = null;
	
	var touchinfo_cache = [];
	
	function AllocTouchInfo(x, y, id, index)
	{
		var ret;
		
		if (touchinfo_cache.length)
			ret = touchinfo_cache.pop();
		else
			ret = new TouchInfo();
		
		ret.init(x, y, id, index);
		return ret;
	};
	
	function ReleaseTouchInfo(ti)
	{
		if (touchinfo_cache.length &lt; 100)
			touchinfo_cache.push(ti);
	};
	
	var GESTURE_HOLD_THRESHOLD = 15;		// max px motion for hold gesture to register
	var GESTURE_HOLD_TIMEOUT = 500;			// time for hold gesture to register
	var GESTURE_TAP_TIMEOUT = 333;			// time for tap gesture to register
	var GESTURE_DOUBLETAP_THRESHOLD = 25;	// max distance apart for taps to be
	
	function TouchInfo()
	{
		this.starttime = 0;
		this.time = 0;
		this.lasttime = 0;
		
		this.startx = 0;
		this.starty = 0;
		this.x = 0;
		this.y = 0;
		this.lastx = 0;
		this.lasty = 0;
		
		this["id"] = 0;
		this.startindex = 0;
		
		this.triggeredHold = false;
		this.tooFarForHold = false;
	};
	
	TouchInfo.prototype.init = function (x, y, id, index)
	{
		var nowtime = cr.performance_now();
		this.time = nowtime;
		this.lasttime = nowtime;
		this.starttime = nowtime;
		
		this.startx = x;
		this.starty = y;
		this.x = x;
		this.y = y;
		this.lastx = x;
		this.lasty = y;
		this.width = 0;
		this.height = 0;
		this.pressure = 0;
		
		this["id"] = id;
		this.startindex = index;
		
		this.triggeredHold = false;
		this.tooFarForHold = false;
	};
	
	TouchInfo.prototype.update = function (nowtime, x, y, width, height, pressure)
	{
		this.lasttime = this.time;
		this.time = nowtime;
		
		this.lastx = this.x;
		this.lasty = this.y;
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
		this.pressure = pressure;
		
		if (!this.tooFarForHold &amp;&amp; cr.distanceTo(this.startx, this.starty, this.x, this.y) &gt;= GESTURE_HOLD_THRESHOLD)
		{
			this.tooFarForHold = true;
		}
	};
	
	TouchInfo.prototype.maybeTriggerHold = function (inst, index)
	{
		if (this.triggeredHold)
			return;		// already triggered this gesture
		
		var nowtime = cr.performance_now();
		
		// Is within 10px after 500ms
		if (nowtime - this.starttime &gt;= GESTURE_HOLD_TIMEOUT &amp;&amp; !this.tooFarForHold &amp;&amp; cr.distanceTo(this.startx, this.starty, this.x, this.y) &lt; GESTURE_HOLD_THRESHOLD)
		{
			this.triggeredHold = true;
			
			inst.trigger_index = this.startindex;
			inst.trigger_id = this["id"];
			inst.getTouchIndex = index;
			inst.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnHoldGesture, inst);
			
			inst.curTouchX = this.x;
			inst.curTouchY = this.y;
			inst.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnHoldGestureObject, inst);
			
			inst.getTouchIndex = 0;
		}
	};
	
	var lastTapX = -1000;
	var lastTapY = -1000;
	var lastTapTime = -10000;
	
	TouchInfo.prototype.maybeTriggerTap = function (inst, index)
	{
		if (this.triggeredHold)
			return;
		
		var nowtime = cr.performance_now();
		
		// Must also come within the hold threshold
		if (nowtime - this.starttime &lt;= GESTURE_TAP_TIMEOUT &amp;&amp; !this.tooFarForHold &amp;&amp; cr.distanceTo(this.startx, this.starty, this.x, this.y) &lt; GESTURE_HOLD_THRESHOLD)
		{
			inst.trigger_index = this.startindex;
			inst.trigger_id = this["id"];
			inst.getTouchIndex = index;
			
			// Is within the distance and time of last tap: trigger a double tap
			if ((nowtime - lastTapTime &lt;= GESTURE_TAP_TIMEOUT * 2) &amp;&amp; cr.distanceTo(lastTapX, lastTapY, this.x, this.y) &lt; GESTURE_DOUBLETAP_THRESHOLD)
			{
				inst.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnDoubleTapGesture, inst);
				
				inst.curTouchX = this.x;
				inst.curTouchY = this.y;
				inst.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnDoubleTapGestureObject, inst);
				
				lastTapX = -1000;
				lastTapY = -1000;
				lastTapTime = -10000;
			}
			// Otherwise trigger single tap
			else
			{
				inst.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTapGesture, inst);
				
				inst.curTouchX = this.x;
				inst.curTouchY = this.y;
				inst.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTapGestureObject, inst);
				
				lastTapX = this.x;
				lastTapY = this.y;
				lastTapTime = nowtime;
			}
			
			inst.getTouchIndex = 0;
		}
	};

	instanceProto.onCreate = function()
	{
		theInstance = this;
		this.isWindows8 = !!(typeof window["c2isWindows8"] !== "undefined" &amp;&amp; window["c2isWindows8"]);
		
		this.orient_alpha = 0;
		this.orient_beta = 0;
		this.orient_gamma = 0;
		
		this.acc_g_x = 0;
		this.acc_g_y = 0;
		this.acc_g_z = 0;
		this.acc_x = 0;
		this.acc_y = 0;
		this.acc_z = 0;
		
		this.curTouchX = 0;
		this.curTouchY = 0;
		
		this.trigger_index = 0;
		this.trigger_id = 0;
		this.trigger_permission = 0;
		
		// For returning correct position for TouchX and TouchY expressions in a trigger
		this.getTouchIndex = 0;
		
		this.useMouseInput = this.properties[0];
		
		// Use document touch input for PhoneGap or fullscreen mode
		var elem = (this.runtime.fullscreen_mode &gt; 0) ? document : this.runtime.canvas;
		
		// Use elem2 to attach the up and cancel events to document, since we want to know about
		// these even if they happen off the main canvas.
		var elem2 = document;
		
		var self = this;
		
		if (typeof PointerEvent !== "undefined")
		{
			elem.addEventListener("pointerdown",
				function(info) {
					self.onPointerStart(info);
				},
				false
			);
			
			elem.addEventListener("pointermove",
				function(info) {
					self.onPointerMove(info);
				},
				false
			);
			
			// Always attach up/cancel events to document (note elem2),
			// otherwise touches dragged off the canvas could get lost
			elem2.addEventListener("pointerup",
				function(info) {
					self.onPointerEnd(info, false);
				},
				false
			);
			
			// Treat pointer cancellation the same as a touch end
			elem2.addEventListener("pointercancel",
				function(info) {
					self.onPointerEnd(info, true);
				},
				false
			);
			
			if (this.runtime.canvas)
			{
				this.runtime.canvas.addEventListener("MSGestureHold", function(e) {
					e.preventDefault();
				}, false);
				document.addEventListener("MSGestureHold", function(e) {
					e.preventDefault();
				}, false);
				this.runtime.canvas.addEventListener("gesturehold", function(e) {
					e.preventDefault();
				}, false);
				document.addEventListener("gesturehold", function(e) {
					e.preventDefault();
				}, false);
			}
		}
		// IE10-style MS prefixed pointer events
		else if (window.navigator["msPointerEnabled"])
		{
			elem.addEventListener("MSPointerDown",
				function(info) {
					self.onPointerStart(info);
				},
				false
			);
			
			elem.addEventListener("MSPointerMove",
				function(info) {
					self.onPointerMove(info);
				},
				false
			);
			
			// Always attach up/cancel events to document (note elem2),
			// otherwise touches dragged off the canvas could get lost
			elem2.addEventListener("MSPointerUp",
				function(info) {
					self.onPointerEnd(info, false);
				},
				false
			);
			
			// Treat pointer cancellation the same as a touch end
			elem2.addEventListener("MSPointerCancel",
				function(info) {
					self.onPointerEnd(info, true);
				},
				false
			);
			
			if (this.runtime.canvas)
			{
				this.runtime.canvas.addEventListener("MSGestureHold", function(e) {
					e.preventDefault();
				}, false);
				document.addEventListener("MSGestureHold", function(e) {
					e.preventDefault();
				}, false);
			}
		}
		// otherwise old style touch events
		else
		{
			elem.addEventListener("touchstart",
				function(info) {
					self.onTouchStart(info);
				},
				false
			);
			
			elem.addEventListener("touchmove",
				function(info) {
					self.onTouchMove(info);
				},
				false
			);
			
			// Always attach up/cancel events to document (note elem2),
			// otherwise touches dragged off the canvas could get lost
			elem2.addEventListener("touchend",
				function(info) {
					self.onTouchEnd(info, false);
				},
				false
			);
			
			// Treat touch cancellation the same as a touch end
			elem2.addEventListener("touchcancel",
				function(info) {
					self.onTouchEnd(info, true);
				},
				false
			);
		}
		
		if (this.isWindows8)
		{
			var win8accelerometerFn = function(e) {
					var reading = e["reading"];
					self.acc_x = reading["accelerationX"];
					self.acc_y = reading["accelerationY"];
					self.acc_z = reading["accelerationZ"];
				};
				
			var win8inclinometerFn = function(e) {
					var reading = e["reading"];
					self.orient_alpha = reading["yawDegrees"];
					self.orient_beta = reading["pitchDegrees"];
					self.orient_gamma = reading["rollDegrees"];
				};
				
			var accelerometer = Windows["Devices"]["Sensors"]["Accelerometer"]["getDefault"]();
			
            if (accelerometer)
			{
                accelerometer["reportInterval"] = Math.max(accelerometer["minimumReportInterval"], 16);
				accelerometer.addEventListener("readingchanged", win8accelerometerFn);
            }
			
			var inclinometer = Windows["Devices"]["Sensors"]["Inclinometer"]["getDefault"]();
			
			if (inclinometer)
			{
				inclinometer["reportInterval"] = Math.max(inclinometer["minimumReportInterval"], 16);
				inclinometer.addEventListener("readingchanged", win8inclinometerFn);
			}
			
			document.addEventListener("visibilitychange", function(e) {
				if (document["hidden"] || document["msHidden"])
				{
					if (accelerometer)
						accelerometer.removeEventListener("readingchanged", win8accelerometerFn);
					if (inclinometer)
						inclinometer.removeEventListener("readingchanged", win8inclinometerFn);
				}
				else
				{
					if (accelerometer)
						accelerometer.addEventListener("readingchanged", win8accelerometerFn);
					if (inclinometer)
						inclinometer.addEventListener("readingchanged", win8inclinometerFn);
				}
			}, false);
		}
		else
		{
			
			window.addEventListener("deviceorientation", function (eventData) {
			
				self.orient_alpha = eventData["alpha"] || 0;
				self.orient_beta = eventData["beta"] || 0;
				self.orient_gamma = eventData["gamma"] || 0;
			
			}, false);
			
			window.addEventListener("devicemotion", function (eventData) {
			
				if (eventData["accelerationIncludingGravity"])
				{
					self.acc_g_x = eventData["accelerationIncludingGravity"]["x"] || 0;
					self.acc_g_y = eventData["accelerationIncludingGravity"]["y"] || 0;
					self.acc_g_z = eventData["accelerationIncludingGravity"]["z"] || 0;
				}
				
				if (eventData["acceleration"])
				{
					self.acc_x = eventData["acceleration"]["x"] || 0;
					self.acc_y = eventData["acceleration"]["y"] || 0;
					self.acc_z = eventData["acceleration"]["z"] || 0;
				}
				
			}, false);
		}
		
		if (this.useMouseInput)
		{
			document.addEventListener("mousemove", function(info) {
				self.onMouseMove(info);
			});
			
			document.addEventListener("mousedown", function(info) {
				self.onMouseDown(info);
			});
			
			document.addEventListener("mouseup", function(info) {
				self.onMouseUp(info);
			});
		}
		
		// Use PhoneGap in case browser does not support accelerometer but device does
		if (!this.runtime.isiOS &amp;&amp; this.runtime.isCordova &amp;&amp; navigator["accelerometer"] &amp;&amp; navigator["accelerometer"]["watchAcceleration"])
		{
			navigator["accelerometer"]["watchAcceleration"](PhoneGapGetAcceleration, null, { "frequency": 40 });
		}
		
		this.runtime.tick2Me(this);
	};
	
	instanceProto.onPointerMove = function (info)
	{
		// Ignore mouse events (note check for both IE10 and IE11 style pointerType values)
		if (info["pointerType"] === info["MSPOINTER_TYPE_MOUSE"] || info["pointerType"] === "mouse")
			return;
		
		if (info.preventDefault)
			info.preventDefault();
		
		var i = this.findTouch(info["pointerId"]);
		var nowtime = cr.performance_now();
		
		if (i &gt;= 0)
		{
			var offsetLeft = this.runtime.canvas.offsetLeft;
			var offsetTop = this.runtime.canvas.offsetTop;
			var t = this.touches[i];
			
			// Ignore events &lt;2ms after the last event - seems events sometimes double-fire
			// very close which throws off speed measurements
			if (nowtime - t.time &lt; 2)
				return;
			
			t.update(nowtime, info.pageX - offsetLeft, info.pageY - offsetTop, info.width || 0, info.height || 0, info.pressure || 0);
		}
	};

	instanceProto.onPointerStart = function (info)
	{
		// Ignore mouse events (note check for both IE10 and IE11 style pointerType values)
		if (info["pointerType"] === info["MSPOINTER_TYPE_MOUSE"] || info["pointerType"] === "mouse")
			return;
		
		// Ignore if already got a pointer with this ID. (This is mainly to deal with buggy implementations
		// that fire the same pointer IDs down twice, e.g. Firefox desktop.)
		var i = this.findTouch(info["pointerId"]);
		if (i !== -1)
			return;
			
		if (info.preventDefault &amp;&amp; cr.isCanvasInputEvent(info))
			info.preventDefault();
		
		var offsetLeft = this.runtime.canvas.offsetLeft;
		var offsetTop = this.runtime.canvas.offsetTop;
		var touchx = info.pageX - offsetLeft;
		var touchy = info.pageY - offsetTop;
		var nowtime = cr.performance_now();
		
		this.trigger_index = this.touches.length;
		this.trigger_id = info["pointerId"];
		
		this.touches.push(AllocTouchInfo(touchx, touchy, info["pointerId"], this.trigger_index));
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger OnNthTouchStart then OnTouchStart
		this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnNthTouchStart, this);
		this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTouchStart, this);
		
		// Trigger OnTouchObject for each touch started event		
		this.curTouchX = touchx;
		this.curTouchY = touchy;
		this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTouchObject, this);
		
		this.runtime.isInUserInputEvent = false;
	};

	instanceProto.onPointerEnd = function (info, isCancel)
	{
		// Ignore mouse events (note check for both IE10 and IE11 style pointerType values)
		if (info["pointerType"] === info["MSPOINTER_TYPE_MOUSE"] || info["pointerType"] === "mouse")
			return;
			
		if (info.preventDefault &amp;&amp; cr.isCanvasInputEvent(info))
			info.preventDefault();
			
		var i = this.findTouch(info["pointerId"]);
		this.trigger_index = (i &gt;= 0 ? this.touches[i].startindex : -1);
		this.trigger_id = (i &gt;= 0 ? this.touches[i]["id"] : -1);
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger OnNthTouchEnd &amp; OnTouchEnd
		this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnNthTouchEnd, this);
		this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTouchEnd, this);
		
		// Remove touch
		if (i &gt;= 0)
		{
			if (!isCancel)
				this.touches[i].maybeTriggerTap(this, i);
			
			ReleaseTouchInfo(this.touches[i]);
			this.touches.splice(i, 1);
		}
		
		this.runtime.isInUserInputEvent = false;
	};

	instanceProto.onTouchMove = function (info)
	{
		if (info.preventDefault)
			info.preventDefault();
		
		var nowtime = cr.performance_now();
		
		var i, len, t, u;
		for (i = 0, len = info.changedTouches.length; i &lt; len; i++)
		{
			t = info.changedTouches[i];
			
			var j = this.findTouch(t["identifier"]);
			
			if (j &gt;= 0)
			{
				var offsetLeft = this.runtime.canvas.offsetLeft;
				var offsetTop = this.runtime.canvas.offsetTop;
				u = this.touches[j];
				
				// Ignore events &lt;2ms after the last event - seems events sometimes double-fire
				// very close which throws off speed measurements
				if (nowtime - u.time &lt; 2)
					continue;
				
				var touchWidth = (t.radiusX || t.webkitRadiusX || t.mozRadiusX || t.msRadiusX || 0) * 2;
				var touchHeight = (t.radiusY || t.webkitRadiusY || t.mozRadiusY || t.msRadiusY || 0) * 2;
				var touchForce = t.force || t.webkitForce || t.mozForce || t.msForce || 0;
				u.update(nowtime, t.pageX - offsetLeft, t.pageY - offsetTop, touchWidth, touchHeight, touchForce);
			}
		}
	};

	instanceProto.onTouchStart = function (info)
	{
		if (info.preventDefault &amp;&amp; cr.isCanvasInputEvent(info))
			info.preventDefault();
			
		var offsetLeft = this.runtime.canvas.offsetLeft;
		var offsetTop = this.runtime.canvas.offsetTop;
		var nowtime = cr.performance_now();
		
		this.runtime.isInUserInputEvent = true;
		
		var i, len, t, j;
		for (i = 0, len = info.changedTouches.length; i &lt; len; i++)
		{
			t = info.changedTouches[i];
			
			// WORKAROUND Chrome for Android bug: touchstart sometimes fires twice with same id.
			// If there is already a touch with this id, ignore this event.
			j = this.findTouch(t["identifier"]);
			
			if (j !== -1)
				continue;
			// END WORKAROUND
			
			var touchx = t.pageX - offsetLeft;
			var touchy = t.pageY - offsetTop;
			
			this.trigger_index = this.touches.length;
			this.trigger_id = t["identifier"];
			
			this.touches.push(AllocTouchInfo(touchx, touchy, t["identifier"], this.trigger_index));
			
			// Trigger OnNthTouchStart then OnTouchStart
			this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnNthTouchStart, this);
			this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTouchStart, this);
			
			// Trigger OnTouchObject for each touch started event		
			this.curTouchX = touchx;
			this.curTouchY = touchy;
			this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTouchObject, this);
		}
		
		this.runtime.isInUserInputEvent = false;
	};

	instanceProto.onTouchEnd = function (info, isCancel)
	{
		if (info.preventDefault &amp;&amp; cr.isCanvasInputEvent(info))
			info.preventDefault();
			
		this.runtime.isInUserInputEvent = true;
		
		var i, len, t, j;
		for (i = 0, len = info.changedTouches.length; i &lt; len; i++)
		{
			t = info.changedTouches[i];
			j = this.findTouch(t["identifier"]);
			
			// Remove touch
			if (j &gt;= 0)
			{
				// Trigger OnNthTouchEnd &amp; OnTouchEnd
				// NOTE: Android stock browser is total garbage and fires touchend twice
				// when a single touch ends. So we only fire these events when we found the
				// touch identifier exists.
				this.trigger_index = this.touches[j].startindex;
				this.trigger_id = this.touches[j]["id"];
			
				this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnNthTouchEnd, this);
				this.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnTouchEnd, this);
			
				if (!isCancel)
					this.touches[j].maybeTriggerTap(this, j);
				
				ReleaseTouchInfo(this.touches[j]);
				this.touches.splice(j, 1);
			}
		}
		
		this.runtime.isInUserInputEvent = false;
	};
	
	instanceProto.getAlpha = function ()
	{
		if (this.runtime.isCordova &amp;&amp; this.orient_alpha === 0 &amp;&amp; pg_accz !== 0)
			return pg_accz * 90;
		else
			return this.orient_alpha;
	};
	
	instanceProto.getBeta = function ()
	{
		if (this.runtime.isCordova &amp;&amp; this.orient_beta === 0 &amp;&amp; pg_accy !== 0)
			return pg_accy * 90;
		else
			return this.orient_beta;
	};
	
	instanceProto.getGamma = function ()
	{
		if (this.runtime.isCordova &amp;&amp; this.orient_gamma === 0 &amp;&amp; pg_accx !== 0)
			return pg_accx * 90;
		else
			return this.orient_gamma;
	};
	
	var noop_func = function(){};
	
	function isCompatibilityMouseEvent(e)
	{
		return (e["sourceCapabilities"] &amp;&amp; e["sourceCapabilities"]["firesTouchEvents"]) ||
				(e.originalEvent &amp;&amp; e.originalEvent["sourceCapabilities"] &amp;&amp; e.originalEvent["sourceCapabilities"]["firesTouchEvents"]);
	};

	instanceProto.onMouseDown = function(info)
	{
		// Ignore compatibility mouse events fired after touches, since this can cause double-firing triggers in iframes.
		if (isCompatibilityMouseEvent(info))
			return;
		
		// Send a fake touch start event
		var t = { pageX: info.pageX, pageY: info.pageY, "identifier": 0 };
		var fakeinfo = { changedTouches: [t] };
		this.onTouchStart(fakeinfo);
		this.mouseDown = true;
	};
	
	instanceProto.onMouseMove = function(info)
	{
		if (!this.mouseDown)
			return;
		
		if (isCompatibilityMouseEvent(info))
			return;
			
		// Send a fake touch move event
		var t = { pageX: info.pageX, pageY: info.pageY, "identifier": 0 };
		var fakeinfo = { changedTouches: [t] };
		this.onTouchMove(fakeinfo);
	};

	instanceProto.onMouseUp = function(info)
	{
		if (info.preventDefault &amp;&amp; this.runtime.had_a_click &amp;&amp; !this.runtime.isMobile)
			info.preventDefault();
			
		this.runtime.had_a_click = true;
		
		if (isCompatibilityMouseEvent(info))
			return;
		
		// Send a fake touch end event
		var t = { pageX: info.pageX, pageY: info.pageY, "identifier": 0 };
		var fakeinfo = { changedTouches: [t] };
		this.onTouchEnd(fakeinfo);
		this.mouseDown = false;
	};
	
	instanceProto.isClientPosOverCanvas = function (touchX, touchY)
	{
		return touchX &gt;= 0 &amp;&amp; touchY &gt;= 0 &amp;&amp;
		    	touchX &lt; this.runtime.width &amp;&amp; touchY &lt; this.runtime.height;
	};
	
	instanceProto.tick2 = function()
	{
		var i, len, t;
		var nowtime = cr.performance_now();
		
		for (i = 0, len = this.touches.length; i &lt; len; ++i)
		{
			// Update speed for touches which haven't moved for 50ms
			t = this.touches[i];
			
			if (t.time &lt;= nowtime - 50)
				t.lasttime = nowtime;
			
			// Gesture detection
			t.maybeTriggerHold(this, i);
		}
	};
	

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

	Cnds.prototype.OnTouchStart = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnTouchEnd = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsInTouch = function ()
	{
		return this.touches.length;
	};
	
	Cnds.prototype.OnTouchObject = function (type)
	{
		if (!type)
			return false;
		
		if (!this.isClientPosOverCanvas(this.curTouchX, this.curTouchY))
			return false;
			
		return this.runtime.testAndSelectCanvasPointOverlap(type, this.curTouchX, this.curTouchY, false);
	};
	
	var touching = [];
	
	Cnds.prototype.IsTouchingObject = function (type)
	{
		if (!type)
			return false;
			
		var sol = type.getCurrentSol();
		var instances = sol.getObjects();
		var px, py;
			
		// Check all touches for overlap with any instance
		var i, leni, j, lenj;
		for (i = 0, leni = instances.length; i &lt; leni; i++)
		{
			var inst = instances[i];
			inst.update_bbox();
			
			for (j = 0, lenj = this.touches.length; j &lt; lenj; j++)
			{
				var touch = this.touches[j];
				
				if (!this.isClientPosOverCanvas(touch.x, touch.y))
					continue;
				
				px = inst.layer.canvasToLayer(touch.x, touch.y, true);
				py = inst.layer.canvasToLayer(touch.x, touch.y, false);
				
				if (inst.contains_pt(px, py))
				{
					touching.push(inst);
					break;
				}
			}
		}
		
		if (touching.length)
		{
			sol.select_all = false;
			cr.shallowAssignArray(sol.instances, touching);
			type.applySolToContainer();
			cr.clearArray(touching);
			return true;
		}
		else
			return false;
	};
	
	Cnds.prototype.CompareTouchSpeed = function (index, cmp, s)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
			return false;
		
		var t = this.touches[index];
		var dist = cr.distanceTo(t.x, t.y, t.lastx, t.lasty);
		var timediff = (t.time - t.lasttime) / 1000;
		var speed = 0;
		
		if (timediff &gt; 0)
			speed = dist / timediff;
			
		return cr.do_cmp(speed, cmp, s);
	};
	
	Cnds.prototype.OrientationSupported = function ()
	{
		return typeof window["DeviceOrientationEvent"] !== "undefined";
	};
	
	Cnds.prototype.MotionSupported = function ()
	{
		return typeof window["DeviceMotionEvent"] !== "undefined";
	};
	
	Cnds.prototype.CompareOrientation = function (orientation_, cmp_, angle_)
	{
		var v = 0;
		
		if (orientation_ === 0)
			v = this.getAlpha();
		else if (orientation_ === 1)
			v = this.getBeta();
		else
			v = this.getGamma();
			
		return cr.do_cmp(v, cmp_, angle_);
	};
	
	Cnds.prototype.CompareAcceleration = function (acceleration_, cmp_, angle_)
	{
		var v = 0;
		
		if (acceleration_ === 0)
			v = this.acc_g_x;
		else if (acceleration_ === 1)
			v = this.acc_g_y;
		else if (acceleration_ === 2)
			v = this.acc_g_z;
		else if (acceleration_ === 3)
			v = this.acc_x;
		else if (acceleration_ === 4)
			v = this.acc_y;
		else if (acceleration_ === 5)
			v = this.acc_z;
		
		return cr.do_cmp(v, cmp_, angle_);
	};
	
	Cnds.prototype.OnNthTouchStart = function (touch_)
	{
		touch_ = Math.floor(touch_);
		return touch_ === this.trigger_index;
	};
	
	Cnds.prototype.OnNthTouchEnd = function (touch_)
	{
		touch_ = Math.floor(touch_);
		return touch_ === this.trigger_index;
	};
	
	Cnds.prototype.HasNthTouch = function (touch_)
	{
		touch_ = Math.floor(touch_);
		return this.touches.length &gt;= touch_ + 1;
	};
	
	Cnds.prototype.OnHoldGesture = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnTapGesture = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnDoubleTapGesture = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnHoldGestureObject = function (type)
	{
		if (!type)
			return false;
		
		if (!this.isClientPosOverCanvas(this.curTouchX, this.curTouchY))
			return false;
		
		return this.runtime.testAndSelectCanvasPointOverlap(type, this.curTouchX, this.curTouchY, false);
	};
	
	Cnds.prototype.OnTapGestureObject = function (type)
	{
		if (!type)
			return false;
		
		if (!this.isClientPosOverCanvas(this.curTouchX, this.curTouchY))
			return false;
		
		return this.runtime.testAndSelectCanvasPointOverlap(type, this.curTouchX, this.curTouchY, false);
	};
	
	Cnds.prototype.OnDoubleTapGestureObject = function (type)
	{
		if (!type)
			return false;
		
		if (!this.isClientPosOverCanvas(this.curTouchX, this.curTouchY))
			return false;
		
		return this.runtime.testAndSelectCanvasPointOverlap(type, this.curTouchX, this.curTouchY, false);
	};
	
	Cnds.prototype.OnPermissionGranted = function (type)
	{
		return this.trigger_permission === type;
	};
	
	Cnds.prototype.OnPermissionDenied = function (type)
	{
		return this.trigger_permission === type;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	Acts.prototype.RequestPermission = function (type)
	{
		var self = this;
		var promise = Promise.resolve(true);
		
		if (type === 0)		// orientation
		{
			if (window["DeviceOrientationEvent"] &amp;&amp; window["DeviceOrientationEvent"]["requestPermission"])
			{
				promise = window["DeviceOrientationEvent"]["requestPermission"]()
				.then(function (state)
				{
					return state === "granted";
				});
			}
		}
		else				// motion
		{
			if (window["DeviceMotionEvent"] &amp;&amp; window["DeviceMotionEvent"]["requestPermission"])
			{
				promise = window["DeviceMotionEvent"]["requestPermission"]()
				.then(function (state)
				{
					return state === "granted";
				});
			}
		}
		
		promise.then(function (result)
		{
			self.trigger_permission = type;
			
			if (result)
				self.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnPermissionGranted, self);
			else
				self.runtime.trigger(cr.plugins_.Touch.prototype.cnds.OnPermissionDenied, self);
		});
	};
	
	pluginProto.acts = new Acts();

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

	Exps.prototype.TouchCount = function (ret)
	{
		ret.set_int(this.touches.length);
	};
	
	Exps.prototype.X = function (ret, layerparam)
	{
		var index = this.getTouchIndex;
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}
		
		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.touches[index].x, this.touches[index].y, 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.touches[index].x, this.touches[index].y, true));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.XAt = function (ret, index, layerparam)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}
		
		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.touches[index].x, this.touches[index].y, 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.touches[index].x, this.touches[index].y, true));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.XForID = function (ret, id, layerparam)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		
		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(touch.x, touch.y, 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(touch.x, touch.y, true));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.Y = function (ret, layerparam)
	{
		var index = this.getTouchIndex;
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}
		
		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.touches[index].x, this.touches[index].y, 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.touches[index].x, this.touches[index].y, false));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.YAt = function (ret, index, layerparam)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}
		
		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.touches[index].x, this.touches[index].y, 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.touches[index].x, this.touches[index].y, false));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.YForID = function (ret, id, layerparam)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		
		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(touch.x, touch.y, 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(touch.x, touch.y, false));
			else
				ret.set_float(0);
		}
	};
	
	Exps.prototype.AbsoluteX = function (ret)
	{
		if (this.touches.length)
			ret.set_float(this.touches[0].x);
		else
			ret.set_float(0);
	};
	
	Exps.prototype.AbsoluteXAt = function (ret, index)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}

		ret.set_float(this.touches[index].x);
	};
	
	Exps.prototype.AbsoluteXForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];

		ret.set_float(touch.x);
	};
	
	Exps.prototype.AbsoluteY = function (ret)
	{
		if (this.touches.length)
			ret.set_float(this.touches[0].y);
		else
			ret.set_float(0);
	};
	
	Exps.prototype.AbsoluteYAt = function (ret, index)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}

		ret.set_float(this.touches[index].y);
	};
	
	Exps.prototype.AbsoluteYForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];

		ret.set_float(touch.y);
	};
	
	Exps.prototype.SpeedAt = function (ret, index)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}
		
		var t = this.touches[index];
		var dist = cr.distanceTo(t.x, t.y, t.lastx, t.lasty);
		var timediff = (t.time - t.lasttime) / 1000;
		
		if (timediff &lt;= 0)
			ret.set_float(0);
		else
			ret.set_float(dist / timediff);
	};
	
	Exps.prototype.SpeedForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		
		var dist = cr.distanceTo(touch.x, touch.y, touch.lastx, touch.lasty);
		var timediff = (touch.time - touch.lasttime) / 1000;
		
		if (timediff &lt;= 0)
			ret.set_float(0);
		else
			ret.set_float(dist / timediff);
	};
	
	Exps.prototype.AngleAt = function (ret, index)
	{
		index = Math.floor(index);
		
		if (index &lt; 0 || index &gt;= this.touches.length)
		{
			ret.set_float(0);
			return;
		}
		
		var t = this.touches[index];
		ret.set_float(cr.to_degrees(cr.angleTo(t.lastx, t.lasty, t.x, t.y)));
	};
	
	Exps.prototype.AngleForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		
		ret.set_float(cr.to_degrees(cr.angleTo(touch.lastx, touch.lasty, touch.x, touch.y)));
	};
	
	Exps.prototype.Alpha = function (ret)
	{
		ret.set_float(this.getAlpha());
	};
	
	Exps.prototype.Beta = function (ret)
	{
		ret.set_float(this.getBeta());
	};
	
	Exps.prototype.Gamma = function (ret)
	{
		ret.set_float(this.getGamma());
	};
	
	Exps.prototype.AccelerationXWithG = function (ret)
	{
		ret.set_float(this.acc_g_x);
	};
	
	Exps.prototype.AccelerationYWithG = function (ret)
	{
		ret.set_float(this.acc_g_y);
	};
	
	Exps.prototype.AccelerationZWithG = function (ret)
	{
		ret.set_float(this.acc_g_z);
	};
	
	Exps.prototype.AccelerationX = function (ret)
	{
		ret.set_float(this.acc_x);
	};
	
	Exps.prototype.AccelerationY = function (ret)
	{
		ret.set_float(this.acc_y);
	};
	
	Exps.prototype.AccelerationZ = function (ret)
	{
		ret.set_float(this.acc_z);
	};
	
	Exps.prototype.TouchIndex = function (ret)
	{
		ret.set_int(this.trigger_index);
	};
	
	Exps.prototype.TouchID = function (ret)
	{
		ret.set_float(this.trigger_id);
	};
	
	Exps.prototype.WidthForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		ret.set_float(touch.width);
	};
	
	Exps.prototype.HeightForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		ret.set_float(touch.height);
	};
	
	Exps.prototype.PressureForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index &lt; 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		ret.set_float(touch.pressure);
	};
	
	pluginProto.exps = new Exps();
	
}());

// Browser
// ECMAScript 5 strict mode

;
;

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

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

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
	};
	
	// As soon as possible, start loading the offlineclient.js file, to best have a shot at having it running within
	// the 3 second delay that the SW puts on messages it sends out. Set the message callback only when both the script
	// and the Browser plugin are ready. Only do this if a C3_RegisterSW() method exists, since that indicates use of SW.
	var offlineScriptReady = false;
	var browserPluginReady = false;
	
	// note wait for DOMContentLoaded since C3_RegisterSW is only assigned after a later script executes.
	document.addEventListener("DOMContentLoaded", function ()
	{
		if (window["C3_RegisterSW"] &amp;&amp; navigator["serviceWorker"])
		{
			var offlineClientScript = document.createElement("script");
			offlineClientScript.onload = function ()
			{
				offlineScriptReady = true;
				checkReady()
			};
			offlineClientScript.src = "offlineclient.js";
			document.head.appendChild(offlineClientScript);
		}
	});
	
	var browserInstance = null;
	
	// wait for onAppBegin call from runtime (made just after Start of Layout) to ensure layout is running so triggers will work
	typeProto.onAppBegin = function ()
	{
		browserPluginReady = true;
		checkReady();
	};
	
	function checkReady()
	{
		// need both script and browser plugin to be ready, then we start listening for messages
		if (offlineScriptReady &amp;&amp; browserPluginReady &amp;&amp; window["OfflineClientInfo"])
		{
			window["OfflineClientInfo"]["SetMessageCallback"](function (e)
			{
				browserInstance.onSWMessage(e);
			});
		}
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
	};

	var instanceProto = pluginProto.Instance.prototype;

	instanceProto.onCreate = function()
	{
		var self = this;
		
		window.addEventListener("resize", function () {
			self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnResize, self);
		});
		
		browserInstance = this;
		
		// register for online/offline events
		if (typeof navigator.onLine !== "undefined")
		{
			window.addEventListener("online", function() {
				self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnOnline, self);
			});
			
			window.addEventListener("offline", function() {
				self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnOffline, self);
			});
		}
		
		// Listen for Cordova's button events
		document.addEventListener("backbutton", function() {
			self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnBackButton, self);
		});
		
		document.addEventListener("menubutton", function() {
			self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnMenuButton, self);
		});
		
		document.addEventListener("searchbutton", function() {
			self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnSearchButton, self);
		});
		
		// Listen for Tizen's hardware key events
		document.addEventListener("tizenhwkey", function (e) {
			var ret;
			
			switch (e["keyName"]) {
			case "back":
				ret = self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnBackButton, self);
				
				// If nothing was triggered, end the application with the Back button
				if (!ret)
				{
					if (window["tizen"])
						window["tizen"]["application"]["getCurrentApplication"]()["exit"]();
				}
					
				break;
			case "menu":
				ret = self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnMenuButton, self);
				
				// Only prevent default if something was triggered
				if (!ret)
					e.preventDefault();
					
				break;
			}
		});
		
		// In Windows Phone 8.1 or Windows 10, listen for back click events
		if (this.runtime.isWindows10 &amp;&amp; typeof Windows !== "undefined")
		{
			Windows["UI"]["Core"]["SystemNavigationManager"]["getForCurrentView"]().addEventListener("backrequested", function (e)
			{
				var ret = self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnBackButton, self);
				
				if (ret)
					e["handled"] = true;
		    });
		}
		else if (this.runtime.isWinJS &amp;&amp; WinJS["Application"])
		{
			WinJS["Application"]["onbackclick"] = function (e)
			{
				// If anything triggers, return true to cancel default behavior.
				return !!self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnBackButton, self);
			};
		}
		
		// browser visibility change events as well as platform-specific events like cordova's
		// pause and resume will suspend the runtime.  handle this event as the 'page visible' trigger
		this.runtime.addSuspendCallback(function(s) {
			if (s)
			{
				self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnPageHidden, self);
			}
			else
			{
				self.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnPageVisible, self);
			}
		});
		
		this.is_arcade = (typeof window["is_scirra_arcade"] !== "undefined");
	};
	
	instanceProto.onSWMessage = function (e)
	{
		var messageType = e["data"]["type"];
		
		if (messageType === "downloading-update")
			this.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnUpdateFound, this);
		else if (messageType === "update-ready" || messageType === "update-pending")
			this.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnUpdateReady, this);
		else if (messageType === "offline-ready")
			this.runtime.trigger(cr.plugins_.Browser.prototype.cnds.OnOfflineReady, this);
	};
	
	
	var batteryManager = null;
	var loadedBatteryManager = false;
	
	function maybeLoadBatteryManager()
	{
		if (loadedBatteryManager)
			return;
		
		if (!navigator["getBattery"])
			return;
		
		var promise = navigator["getBattery"]();
		loadedBatteryManager = true;
		
		if (promise)
		{
			promise.then(function (manager) {
				batteryManager = manager;
			});
		}
	};
	
	//////////////////////////////////////
	// Conditions
	function Cnds() {};

	Cnds.prototype.CookiesEnabled = function()
	{
		return navigator ? navigator.cookieEnabled : false;
	};
	
	Cnds.prototype.IsOnline = function()
	{
		return navigator ? navigator.onLine : false;
	};
	
	Cnds.prototype.HasJava = function()
	{
		return navigator ? navigator.javaEnabled() : false;
	};
	
	Cnds.prototype.OnOnline = function()
	{
		return true;
	};
	
	Cnds.prototype.OnOffline = function()
	{
		return true;
	};
	
	Cnds.prototype.IsDownloadingUpdate = function ()
	{
		return false;		// deprecated
	};
	
	Cnds.prototype.PageVisible = function ()
	{
		return !this.runtime.isSuspended;
	};
	
	Cnds.prototype.OnPageVisible = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnPageHidden = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnResize = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsFullscreen = function ()
	{
		return !!(document["mozFullScreen"] || document["webkitIsFullScreen"] || document["fullScreen"]);
	};
	
	Cnds.prototype.OnBackButton = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnMenuButton = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnSearchButton = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsMetered = function ()
	{
		var connection = navigator["connection"] || navigator["mozConnection"] || navigator["webkitConnection"];
		
		if (!connection)
			return false;
			
		return !!connection["metered"];
	};
	
	Cnds.prototype.IsCharging = function ()
	{
		// check old API first
		var battery = navigator["battery"] || navigator["mozBattery"] || navigator["webkitBattery"];
		
		if (battery)
		{
			return !!battery["charging"]
		}
		else
		{
			// try to use new API
			maybeLoadBatteryManager();
			
			if (batteryManager)
			{
				return !!batteryManager["charging"];
			}
			else
			{
				return true;		// if unknown, default to charging (powered)
			}
		}
	};
	
	Cnds.prototype.IsPortraitLandscape = function (p)
	{
		var current = (window.innerWidth &lt;= window.innerHeight ? 0 : 1);
		
		return current === p;
	};
	
	Cnds.prototype.SupportsFullscreen = function ()
	{
		if (this.runtime.isNWjs)
			return true;
		
		var elem = this.runtime.canvas;
		return !!(elem["requestFullscreen"] || elem["mozRequestFullScreen"] || elem["msRequestFullscreen"] || elem["webkitRequestFullScreen"]);
	};
	
	Cnds.prototype.OnUpdateFound = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnUpdateReady = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnOfflineReady = function ()
	{
		return true;
	};
	
	
	pluginProto.cnds = new Cnds();

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

	Acts.prototype.Alert = function (msg)
	{
		alert(msg.toString());
	};
	
	Acts.prototype.Close = function ()
	{
		if (window["tizen"])
			window["tizen"]["application"]["getCurrentApplication"]()["exit"]();
		else if (navigator["app"] &amp;&amp; navigator["app"]["exitApp"])
			navigator["app"]["exitApp"]();
		else if (navigator["device"] &amp;&amp; navigator["device"]["exitApp"])
			navigator["device"]["exitApp"]();
		else if (!this.is_arcade)
			window.close();
	};
	
	Acts.prototype.Focus = function ()
	{
		if (this.runtime.isNWjs)
		{
			var win = nw["Window"]["get"]();
			win["focus"]();
		}
		else if (!this.is_arcade)
			window.focus();
	};
	
	Acts.prototype.Blur = function ()
	{
		if (this.runtime.isNWjs)
		{
			var win = nw["Window"]["get"]();
			win["blur"]();
		}
		else if (!this.is_arcade)
			window.blur();
	};
	
	Acts.prototype.GoBack = function ()
	{
		if (navigator["app"] &amp;&amp; navigator["app"]["backHistory"])
			navigator["app"]["backHistory"]();
		else if (!this.is_arcade &amp;&amp; window.back)
			window.back();
	};
	
	Acts.prototype.GoForward = function ()
	{
		if (!this.is_arcade &amp;&amp; window.forward)
			window.forward();
	};
	
	Acts.prototype.GoHome = function ()
	{
		if (!this.is_arcade &amp;&amp; window.home)
			window.home();
	};
	
	Acts.prototype.GoToURL = function (url, target)
	{
		if (this.runtime.isWinJS)
			Windows["System"]["Launcher"]["launchUriAsync"](new Windows["Foundation"]["Uri"](url));
		else if (navigator["app"] &amp;&amp; navigator["app"]["loadUrl"])
			navigator["app"]["loadUrl"](url, { "openExternal": true });
		else if (this.runtime.isCordova)
			window.open(url, "_system");
		else if (this.runtime.isPreview)
			window.open(url, "_blank");					// preview mode in C3 can't change URL since it causes security exceptions - just open in new window instead
		else if (!this.is_arcade)
		{
			if (target === 2 &amp;&amp; !this.is_arcade)		// top
				window.top.location = url;
			else if (target === 1 &amp;&amp; !this.is_arcade)	// parent
				window.parent.location = url;
			else					// self
				window.location = url;
		}
	};
	
	Acts.prototype.GoToURLWindow = function (url, tag)
	{
		if (this.runtime.isWinJS)
			Windows["System"]["Launcher"]["launchUriAsync"](new Windows["Foundation"]["Uri"](url));
		else if (navigator["app"] &amp;&amp; navigator["app"]["loadUrl"])
			navigator["app"]["loadUrl"](url, { "openExternal": true });
		else if (this.runtime.isCordova)
			window.open(url, "_system");
		else if (!this.is_arcade)
			window.open(url, tag);
	};
	
	Acts.prototype.Reload = function ()
	{
		if (!this.is_arcade)
			window.location.reload();
	};
	
	var firstRequestFullscreen = true;
	var crruntime = null;
	
	function onFullscreenError(e)
	{
		if (console &amp;&amp; console.warn)
			console.warn("Fullscreen request failed: ", e);
		
		// need to call setSize again for display to update correctly given the request failed
		crruntime["setSize"](window.innerWidth, window.innerHeight);
	};
	
	Acts.prototype.RequestFullScreen = function (stretchmode, navUI)
	{
		// Scale inner comes at end of list for backwards compatibility; rearrange parameters to be correct
		if (stretchmode &gt;= 2)
			stretchmode += 1;
			
		if (stretchmode === 6)
			stretchmode = 2;
		
		if (document["mozFullScreen"] || document["webkitIsFullScreen"] || !!document["msFullscreenElement"] || document["fullScreen"] || document["fullScreenElement"])
		{
			return;
		}
		
		this.runtime.fullscreen_scaling = (stretchmode &gt;= 2 ? stretchmode : 0);
		
		var opts = { "navigationUI": "auto" };
		if (navUI === 1)		// hide
			opts["navigationUI"] = "hide";
		else if (navUI === 2)	// show
			opts["navigationUI"] = "show";
		
		var elem = document.documentElement;
		
		// If the first request, add a fullscreen error handler. We need to call
		// setSize() again for the display to end up correct if the request failed.
		if (firstRequestFullscreen)
		{
			firstRequestFullscreen = false;
			crruntime = this.runtime;
			elem.addEventListener("mozfullscreenerror", onFullscreenError);
			elem.addEventListener("webkitfullscreenerror", onFullscreenError);
			elem.addEventListener("MSFullscreenError", onFullscreenError);
			elem.addEventListener("fullscreenerror", onFullscreenError);
		}
		
		// note case sensitivity
		if (elem["requestFullscreen"])
			elem["requestFullscreen"](opts);
		else if (elem["mozRequestFullScreen"])
			elem["mozRequestFullScreen"](opts);
		else if (elem["msRequestFullscreen"])
			elem["msRequestFullscreen"](opts);
		else if (elem["webkitRequestFullScreen"])		// note 'opts' not passed to webkit prefixed variant
		{
			if (typeof Element !== "undefined" &amp;&amp; typeof Element["ALLOW_KEYBOARD_INPUT"] !== "undefined")
				elem["webkitRequestFullScreen"](Element["ALLOW_KEYBOARD_INPUT"]);
			else
				elem["webkitRequestFullScreen"]();
		}
	};
	
	Acts.prototype.CancelFullScreen = function ()
	{
		// note case difference, see RequestFullScreen
		if (document["exitFullscreen"])
			document["exitFullscreen"]();
		else if (document["mozCancelFullScreen"])
			document["mozCancelFullScreen"]();
		else if (document["msExitFullscreen"])
			document["msExitFullscreen"]();
		else if (document["webkitCancelFullScreen"])
			document["webkitCancelFullScreen"]();
	};
	
	Acts.prototype.Vibrate = function (pattern_)
	{
		try {
			var arr = pattern_.split(",");
			
			var i, len;
			for (i = 0, len = arr.length; i &lt; len; i++)
			{
				arr[i] = parseInt(arr[i], 10);
			}
			
			if (navigator["vibrate"])
				navigator["vibrate"](arr);
			else if (navigator["mozVibrate"])
				navigator["mozVibrate"](arr);
			else if (navigator["webkitVibrate"])
				navigator["webkitVibrate"](arr);
			else if (navigator["msVibrate"])
				navigator["msVibrate"](arr);
		}
		catch (e) {}
	};
	
	Acts.prototype.InvokeDownload = function (url_, filename_)
	{
		if (!filename_)
			return;
		
		var a = document.createElement("a");
		
		if (typeof a["download"] === "undefined")
		{
			// can't do much better than this without download tag support
			window.open(url_);
		}
		else
		{
			// auto download
			var body = document.getElementsByTagName("body")[0];
			a.textContent = filename_;
			a.href = url_;
			a["download"] = filename_;
			body.appendChild(a);
			var clickEvent = new MouseEvent("click");
			a.dispatchEvent(clickEvent);
			body.removeChild(a);
		}
	};
	
	Acts.prototype.InvokeDownloadString = function (str_, mimetype_, filename_)
	{
		if (!filename_)
			return;
		
		var datauri = "data:" + mimetype_ + "," + encodeURIComponent(str_);
		var a = document.createElement("a");
		
		if (typeof a["download"] === "undefined")
		{
			// can't do much better than this without download tag support
			window.open(datauri);
		}
		else
		{
			// auto download
			var body = document.getElementsByTagName("body")[0];
			a.textContent = filename_;
			a.href = datauri;
			a["download"] = filename_;
			body.appendChild(a);
			var clickEvent = new MouseEvent("click");
			a.dispatchEvent(clickEvent);
			body.removeChild(a);
		}
	};
	
	Acts.prototype.ConsoleLog = function (type_, msg_)
	{
		if (typeof console === "undefined")
			return;
		
		if (type_ === 0 &amp;&amp; console.log)
			console.log(msg_.toString());
		if (type_ === 1 &amp;&amp; console.warn)
			console.warn(msg_.toString());
		if (type_ === 2 &amp;&amp; console.error)
			console.error(msg_.toString());
	};
	
	Acts.prototype.ConsoleGroup = function (name_)
	{
		if (console &amp;&amp; console.group)
			console.group(name_);
	};

	Acts.prototype.ConsoleGroupEnd = function ()
	{
		if (console &amp;&amp; console.groupEnd)
			console.groupEnd();
	};
	
	Acts.prototype.ExecJs = function (js_)
	{
		// let's hope this is used responsibly
		try {
			if (eval)
				eval(js_);
		}
		catch (e)
		{
			if (console &amp;&amp; console.error)
				console.error("Error executing Javascript: ", e);
		}
	};
	
	var orientations = [
		"portrait",
		"landscape",
		"portrait-primary",
		"portrait-secondary",
		"landscape-primary",
		"landscape-secondary"
	];
	
	Acts.prototype.LockOrientation = function (o)
	{
		o = Math.floor(o);
		
		if (o &lt; 0 || o &gt;= orientations.length)
			return;
		
		this.runtime.autoLockOrientation = false;
		
		var orientation = orientations[o];
		
		if (screen["orientation"] &amp;&amp; screen["orientation"]["lock"])
			screen["orientation"]["lock"](orientation);
		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);
	};
	
	Acts.prototype.UnlockOrientation = function ()
	{
		// Stop the runtime trying to lock orientation on every size event
		// since the user probably wants to take control of it themselves
		this.runtime.autoLockOrientation = false;
		
		if (screen["orientation"] &amp;&amp; screen["orientation"]["unlock"])
			screen["orientation"]["unlock"]();
		else if (screen["unlockOrientation"])
			screen["unlockOrientation"]();
		else if (screen["webkitUnlockOrientation"])
			screen["webkitUnlockOrientation"]();
		else if (screen["mozUnlockOrientation"])
			screen["mozUnlockOrientation"]();
		else if (screen["msUnlockOrientation"])
			screen["msUnlockOrientation"]();
	};

	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};

	Exps.prototype.URL = function (ret)
	{
		ret.set_string(window.location.toString());
	};
	
	Exps.prototype.Protocol = function (ret)
	{
		ret.set_string(window.location.protocol);
	};
	
	Exps.prototype.Domain = function (ret)
	{
		ret.set_string(window.location.hostname);
	};
	
	Exps.prototype.PathName = function (ret)
	{
		ret.set_string(window.location.pathname);
	};
	
	Exps.prototype.Hash = function (ret)
	{
		ret.set_string(window.location.hash);
	};
	
	Exps.prototype.Referrer = function (ret)
	{
		ret.set_string(document.referrer);
	};
	
	Exps.prototype.Title = function (ret)
	{
		ret.set_string(document.title);
	};
	
	Exps.prototype.Name = function (ret)
	{
		ret.set_string(navigator.appName);
	};
	
	Exps.prototype.Version = function (ret)
	{
		ret.set_string(navigator.appVersion);
	};
	
	Exps.prototype.Language = function (ret)
	{
		// Not in IE or DC
		if (navigator &amp;&amp; navigator.language)
			ret.set_string(navigator.language);
		else
			ret.set_string("");
	};
	
	Exps.prototype.Platform = function (ret)
	{
		ret.set_string(navigator.platform);
	};
	
	Exps.prototype.Product = function (ret)
	{
		// Not in IE or DC
		if (navigator &amp;&amp; navigator.product)
			ret.set_string(navigator.product);
		else
			ret.set_string("");
	};
	
	Exps.prototype.Vendor = function (ret)
	{
		// Not in IE or DC
		if (navigator &amp;&amp; navigator.vendor)
			ret.set_string(navigator.vendor);
		else
			ret.set_string("");
	};
	
	Exps.prototype.UserAgent = function (ret)
	{
		ret.set_string(navigator.userAgent);
	};
	
	Exps.prototype.QueryString = function (ret)
	{
		ret.set_string(window.location.search);
	};
	
	Exps.prototype.QueryParam = function (ret, paramname)
	{
		var match = RegExp('[?&amp;]' + paramname + '=([^&amp;]*)').exec(window.location.search);
 
		if (match)
			ret.set_string(decodeURIComponent(match[1].replace(/\+/g, ' ')));
		else
			ret.set_string("");
	};
	
	Exps.prototype.Bandwidth = function (ret)
	{
		var connection = navigator["connection"] || navigator["mozConnection"] || navigator["webkitConnection"];
		
		if (!connection)
			ret.set_float(Number.POSITIVE_INFINITY);
		else
		{
			// "bandwidth" is old API name, "downlinkMax" is latest spec
			if (typeof connection["bandwidth"] !== "undefined")
				ret.set_float(connection["bandwidth"]);
			else if (typeof connection["downlinkMax"] !== "undefined")
				ret.set_float(connection["downlinkMax"]);
			else
				ret.set_float(Number.POSITIVE_INFINITY);
		}
	};
	
	Exps.prototype.ConnectionType = function (ret)
	{
		var connection = navigator["connection"] || navigator["mozConnection"] || navigator["webkitConnection"];
		
		if (!connection)
			ret.set_string("unknown");
		else
		{
			ret.set_string(connection["type"] || "unknown");
		}
	};
	
	Exps.prototype.BatteryLevel = function (ret)
	{
		// check old API style first
		var battery = navigator["battery"] || navigator["mozBattery"] || navigator["webkitBattery"];
		
		if (battery)
		{
			ret.set_float(battery["level"]);
		}
		else
		{
			// otherwise try using new API
			maybeLoadBatteryManager();
			
			if (batteryManager)
			{
				ret.set_float(batteryManager["level"]);
			}
			else
			{
				ret.set_float(1);		// not supported/unknown: assume charged
			}
		}
	};
	
	Exps.prototype.BatteryTimeLeft = function (ret)
	{
		// check old API style first
		var battery = navigator["battery"] || navigator["mozBattery"] || navigator["webkitBattery"];
		
		if (battery)
		{
			ret.set_float(battery["dischargingTime"]);
		}
		else
		{
			// otherwise try using new API
			maybeLoadBatteryManager();
			
			if (batteryManager)
			{
				ret.set_float(batteryManager["dischargingTime"]);
			}
			else
			{
				ret.set_float(Number.POSITIVE_INFINITY);		// not supported/unknown: assume infinite time left
			}
		}
	};
	
	Exps.prototype.ExecJS = function (ret, js_)
	{
		// let's hope this is used responsibly
		if (!eval)
		{
			ret.set_any(0);
			return;
		}
		
		var result = 0;
		
		try {
			result = eval(js_);
		}
		catch (e)
		{
			if (console &amp;&amp; console.error)
				console.error("Error executing Javascript: ", e);
		}
		
		if (typeof result === "number")
			ret.set_any(result);
		else if (typeof result === "string")
			ret.set_any(result);
		else if (typeof result === "boolean")
			ret.set_any(result ? 1 : 0);
		else
			ret.set_any(0);
	};
	
	Exps.prototype.ScreenWidth = function (ret)
	{
		ret.set_int(screen.width);
	};
	
	Exps.prototype.ScreenHeight = function (ret)
	{
		ret.set_int(screen.height);
	};
	
	Exps.prototype.DevicePixelRatio = function (ret)
	{
		ret.set_float(this.runtime.devicePixelRatio);
	};
	
	Exps.prototype.WindowInnerWidth = function (ret)
	{
		ret.set_int(window.innerWidth);
	};
	
	Exps.prototype.WindowInnerHeight = function (ret)
	{
		ret.set_int(window.innerHeight);
	};
	
	Exps.prototype.WindowOuterWidth = function (ret)
	{
		ret.set_int(window.outerWidth);
	};
	
	Exps.prototype.WindowOuterHeight = function (ret)
	{
		ret.set_int(window.outerHeight);
	};
	
	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 &amp;&amp; !audRuntime.isInUserInputEvent)
		{
			addAudioToPlayOnNextInput(a);
		}
	};
	
	function playQueuedAudio()
	{
		var i, len, m, playRet;
		
		// On first call, play a dummy buffer to unblock the Web Audio API
		if (!hasPlayedDummyBuffer &amp;&amp; !isContextSuspended &amp;&amp; context)
		{
			playDummyBuffer();
			
			// Only unflag this once the audio context state indicates it is running. This means we keep trying
			// to unblock the audio context until it's successfully unblocked.
			if (context["state"] === "running")
				hasPlayedDummyBuffer = true;
		}
		
		// play() calls can still fail in a user input event due to browser heuristics. Make sure any play calls
		// that fail are added back in to the play queue.
		var tryPlay = playOnNextInput.slice(0);
		cr.clearArray(playOnNextInput);
		
		// If not in silent mode, make play() calls for any queued audio
		if (!silent)
		{
			for (i = 0, len = tryPlay.length; i &lt; len; ++i)
			{
				m = tryPlay[i];
				
				if (!m.stopped &amp;&amp; !m.is_paused)
				{
					playRet = m.instanceObject.play();
					
					if (playRet)
					{
						playRet.catch(function (err)
						{
							addAudioToPlayOnNextInput(m);
						});
					}
				}
			}
		}
	};
	
	function playDummyBuffer()
	{
		// First try to call resume() if AudioContext is in suspended state. This unblocks Chrome for Android.
		// Otherwise fall back to code intended for Safari, which plays an empty buffer to enable audio output.
		if (context["state"] === "suspended" &amp;&amp; context["resume"])
			context["resume"]();
		
		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("pointerup", playQueuedAudio, true);
	document.addEventListener("touchend", playQueuedAudio, true);
	document.addEventListener("click", playQueuedAudio, true);
	document.addEventListener("keydown", playQueuedAudio, true);
	document.addEventListener("gamepadconnected", 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 &lt; 0)
			v = 0;
		if (v &gt; 1)
			v = 1;
		return v;
	};
	
	function linearToDb(x)
	{
		if (x &lt; 0)
			x = 0;
		if (x &gt; 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 &lt; 0) value = 0;
			if (value &gt; 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 &lt; 0) value = 0;
			if (value &gt; 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 &lt; 0) value = 0;
			if (value &gt; 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-&gt;dry goes 100 -&gt; 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 -&gt; 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 &lt; 0) value = 0;
			if (value &gt; 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-&gt;dry goes 100 -&gt; 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 -&gt; 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 &lt; 0) value = 0;
			if (value &gt; 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 &lt; 0) value = 0;
			if (value &gt; 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 &lt; 0) value = 0;
			if (value &gt; 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 &lt; 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 &lt; 0 ? -1 : +1;
		var absx = x &lt; 0 ? -x : x;
		
		var shapedInput = absx &lt; 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 &lt; 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 &lt; 0) value = 0;
			if (value &gt; 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 &lt; fftSize; i++)
		{
			// get signal as absolute value from 0 to 1 then convert to dB
			s = (this.signal[i] - 128) / 128;
			if (s &lt; 0)
				s = -s;
			
			if (this.peak &lt; 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 &lt;=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" &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; is_music &amp;&amp; !playMusicAsSoundWorkaround &amp;&amp; !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 &amp;&amp; context["createMediaElementSource"] &amp;&amp; !/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 &amp;&amp; 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 &lt; 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, decodedBytes)
			{
				if (err)
				{
					self.onDecodeError(err);
					return;
				}
				var rawAudio = new Float32Array(decodedBytes);
				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) &amp;&amp; !silent)
		{								
			if (this.panWhenReady.length)
			{
				for (i = 0, len = this.panWhenReady.length; i &lt; 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;
		
		console.error("Failed to decode audio '" + this.src + "': " + err);
	};
	
	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"] &gt;= 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 &amp;&amp; !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 &amp;&amp; this.buffer.myapi === API_HTML5 &amp;&amp; !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 &amp;&amp; !this.stopped &amp;&amp; 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 -&gt; gain -&gt; dest
		// when enabled, we insert a panner node: source -&gt; gain -&gt; panner -&gt; dest
		
		// disabled and switching to enabled: reconnect the gain node via the panner node
		if (!this.pannerEnabled &amp;&amp; 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 &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; ret &gt; duration)
			ret = duration;
		
		return ret;
	};
	
	C2AudioInstance.prototype.isPlaying = function ()
	{
		return !this.is_paused &amp;&amp; !this.fresh &amp;&amp; !this.stopped &amp;&amp; !this.hasEnded();
	};
	
	C2AudioInstance.prototype.shouldSave = function ()
	{
		// save sounds which are playing, but include paused ones
		return !this.fresh &amp;&amp; !this.stopped &amp;&amp; !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" &amp;&amp; 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" &amp;&amp; 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.getPlaybackRate = function (r)
	{
		return this.playbackRate;
	};
	
	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 &amp; 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 &amp;&amp; (this.runtime.isChrome || this.runtime.isAndroidStockBrowser))) &amp;&amp; !this.runtime.isCrosswalk &amp;&amp; !this.runtime.isAmazonWebApp &amp;&amp; !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 &amp;&amp; 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 &amp;&amp; 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 &gt; -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;
		
		// property 3 is latency hint, which is only supported in C3 runtime
		
		panningModel = this.properties[4];		// 0 = equalpower, 1 = hrtf, 3 = soundfield
		distanceModel = this.properties[5];		// 0 = linear, 1 = inverse, 2 = exponential
		this.listenerZ = -this.properties[6];
		refDistance = this.properties[7];
		maxDistance = this.properties[8];
		rolloffFactor = this.properties[9];
		
		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 &lt; 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 &amp;&amp; a.isPlaying() &amp;&amp; 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 &lt; 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 &amp;&amp; this.saveload === 1)	// not saving/loading music
				continue;
			if (!a.is_music &amp;&amp; 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 &amp;&amp; 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 &lt; 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 &lt; len; i++)
			{
				a = audioInstances[i];
				
				if (a.is_music &amp;&amp; this.saveload === 1)
					continue;		// only saving/loading sound: leave music playing
				if (!a.is_music &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; pan.hasOwnProperty("objUid")) ? pan["objUid"] : -1;
			
			if (is_music &amp;&amp; this.saveload === 1)	// not saving/loading music
				continue;
			if (!is_music &amp;&amp; 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 &amp;&amp; !silent)			// setting silent
		{
			for (i = 0, len = audioInstances.length; i &lt; len; i++)
				audioInstances[i].setSilent(true);
			
			silent = true;
		}
		else if (!setSilent &amp;&amp; silent)		// setting not silent
		{
			for (i = 0, len = audioInstances.length; i &lt; 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 &lt; 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 &amp;&amp; context &amp;&amp; context["resume"])
		{
			context["resume"]();
			isContextSuspended = false;
		}
		
		var i, len;
		for (i = 0, len = audioInstances.length; i &lt; len; i++)
			audioInstances[i].setSuspended(s);
		
		// after suspend: also suspend the whole context
		if (s &amp;&amp; context &amp;&amp; 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 &lt; 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 &lt; len; i++)
				{
					f = arr[i];
					if (f.tick)
						f.tick();
				}
			}
		}
		
		// keep listener up to date
		if (api === API_WEBAUDIO &amp;&amp; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &amp;&amp; !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) &amp;&amp; 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 &lt; len; i++)
		{
			a = audioInstances[i];
			
			if (a.src === src &amp;&amp; (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 !== "&lt;preload&gt;")
			{
				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 &lt; 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 &lt; 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 &lt; 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 &lt; len; i++)
			taggedAudio[i].reconnect(toNode);
		
		// Reconnect microphone input if active
		if (micSource &amp;&amp; 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 &lt; len; i++)
		{
			if (!audioBuffers[i].isLoadedAndDecoded() &amp;&amp; !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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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, "&lt;preload&gt;", 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, "&lt;preload&gt;", is_music, false);
	};
	
	Acts.prototype.SetPlaybackRate = function (tag, rate)
	{
		getAudioByTag(tag);
		
		// Only support forwards playback
		if (rate &lt; 0.0)
			rate = 0;
			
		var i, len;
		for (i = 0, len = taggedAudio.length; i &lt; len; i++)
			taggedAudio[i].setPlaybackRate(rate);
	};
	
	Acts.prototype.Stop = function (tag)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i &lt; len; i++)
			taggedAudio[i].stop();
	};
	
	Acts.prototype.StopAll = function ()
	{
		var i, len;
		for (i = 0, len = audioInstances.length; i &lt; len; i++)
			audioInstances[i].stop();
	};
	
	Acts.prototype.SetPaused = function (tag, state)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i &lt; 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 &lt; 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 &amp;&amp; !silent)			// setting silent
		{
			for (i = 0, len = audioInstances.length; i &lt; len; i++)
				audioInstances[i].setSilent(true);
			
			silent = true;
		}
		else if (s === 1 &amp;&amp; silent)		// setting not silent
		{
			for (i = 0, len = audioInstances.length; i &lt; 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 &lt; len; i++)
			audioInstances[i].updateVolume();
	};

	Acts.prototype.AddFilterEffect = function (tag, type, freq, detune, q, gain, mix)
	{
		if (api !== API_WEBAUDIO || type &lt; 0 || type &gt;= filterTypes.length || !context["createBiquadFilter"])
			return;
		
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 0) mix = 0;
		if (mix &gt; 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 &lt; 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 &lt; 0 || index &gt;= 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 &lt; 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.PlaybackRate = function (ret, tag)
	{
		getAudioByTag(tag, true);
		
		if (taggedAudio.length)
			ret.set_float(taggedAudio[0].getPlaybackRate());
		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 &amp;&amp; index &gt;= 0 &amp;&amp; index &lt; arr.length &amp;&amp; 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 &lt; 0 || bin &gt;= 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();

}());

// 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 &gt; 0)
			return true;
			
		return this.mouseXcanvas &gt;= 0 &amp;&amp; this.mouseYcanvas &gt;= 0
		    &amp;&amp; this.mouseXcanvas &lt; this.runtime.width &amp;&amp; this.mouseYcanvas &lt; 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 &amp; 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 &amp;&amp; !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 &amp; 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 &lt; 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 &amp;&amp; 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 &lt; 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);
		}
	};
	
	instanceProto.isMouseOverCanvas = function ()
	{
		return this.mouseXcanvas &gt;= 0 &amp;&amp; this.mouseYcanvas &gt;= 0
		    &amp;&amp; this.mouseXcanvas &lt; this.runtime.width &amp;&amp; this.mouseYcanvas &lt; this.runtime.height;
	};
	

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

	Cnds.prototype.OnClick = function (button, type)
	{
		return button === this.triggerButton &amp;&amp; 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)
	{
		if (!this.isMouseOverCanvas())
			return;
		
		// 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
		
		if (!this.isMouseOverCanvas())
			return;
		
		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();
	
}());


// WebStorage
// ECMAScript 5 strict mode

;
;

cr.plugins_.WebStorage = function(runtime)
{
	this.runtime = runtime;
};

(function()
{
	var pluginProto = cr.plugins_.WebStorage.prototype;

	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};
	
	var typeProto = pluginProto.Type.prototype;
	typeProto.onCreate = function()
	{
	};

	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
	};
	
	var instanceProto = pluginProto.Instance.prototype;

	var prefix = "";
	var is_arcade = (typeof window["is_scirra_arcade"] !== "undefined");
	
	if (is_arcade)
		prefix = "arcade" + window["scirra_arcade_id"];
	
	// Check localStorage is supported and works
	var isSupported = false;
	
	try {
		localStorage.getItem("test");
		isSupported = true;
	}
	catch (e)
	{
		isSupported = false;
	}
	
		
	instanceProto.onCreate = function()
	{
		if (!isSupported)
		{
			cr.logexport("[Construct 2] Webstorage plugin: local storage is not supported on this platform.");
		}
	};
	

	function Cnds() {};

	Cnds.prototype.LocalStorageEnabled = function()
	{
		return isSupported;
	};
	
	Cnds.prototype.SessionStorageEnabled = function()
	{
		return isSupported;
	};
	
	Cnds.prototype.LocalStorageExists = function(key)
	{
		if (!isSupported)
			return false;
		
		return localStorage.getItem(prefix + key) != null;
	};
	
	Cnds.prototype.SessionStorageExists = function(key)
	{
		if (!isSupported)
			return false;
		
		return sessionStorage.getItem(prefix + key) != null;
	};
	
	Cnds.prototype.OnQuotaExceeded = function ()
	{
		return true;
	};
	
	Cnds.prototype.CompareKeyText = function (key, text_to_compare, case_sensitive)
	{
		if (!isSupported)
			return false;
		
		var value = localStorage.getItem(prefix + key) || "";
		
		if (case_sensitive)
			return value == text_to_compare;
		else
			return cr.equals_nocase(value, text_to_compare);
	};
	
	Cnds.prototype.CompareKeyNumber = function (key, cmp, x)
	{
		if (!isSupported)
			return false;
		
		var value = localStorage.getItem(prefix + key) || "";
		
		return cr.do_cmp(parseFloat(value), cmp, x);
	};
	
	pluginProto.cnds = new Cnds();

	function Acts() {};
	
	Acts.prototype.StoreLocal = function(key, data)
	{
		if (!isSupported)
			return;
		
		try {
			localStorage.setItem(prefix + key, data);
		}
		catch (e)
		{
			this.runtime.trigger(cr.plugins_.WebStorage.prototype.cnds.OnQuotaExceeded, this);
		}
	};
	
	Acts.prototype.StoreSession = function(key,data)
	{
		if (!isSupported)
			return;
		
		try {
			sessionStorage.setItem(prefix + key, data);
		}
		catch (e)
		{
			this.runtime.trigger(cr.plugins_.WebStorage.prototype.cnds.OnQuotaExceeded, this);
		}
	};
	
	Acts.prototype.RemoveLocal = function(key)
	{
		if (!isSupported)
			return;
		
		localStorage.removeItem(prefix + key);
	};
	
	Acts.prototype.RemoveSession = function(key)
	{
		if (!isSupported)
			return;
		
		sessionStorage.removeItem(prefix + key);
	};
	
	Acts.prototype.ClearLocal = function()
	{
		if (!isSupported)
			return;
		
		if (!is_arcade)
			localStorage.clear();
	};
	
	Acts.prototype.ClearSession = function()
	{
		if (!isSupported)
			return;
		
		if (!is_arcade)
			sessionStorage.clear();
	};
	
	Acts.prototype.JSONLoad = function (json_, mode_)
	{
		if (!isSupported)
			return;
		
		var d;
		
		try {
			d = JSON.parse(json_);
		}
		catch(e) { return; }
		
		if (!d["c2dictionary"])			// presumably not a c2dictionary object
			return;
		
		var o = d["data"];
		
		if (mode_ === 0 &amp;&amp; !is_arcade)	// 'set' mode: must clear webstorage first
			localStorage.clear();
			
		var p;
		for (p in o)
		{
			if (o.hasOwnProperty(p))
			{
				try {
					localStorage.setItem(prefix + p, o[p]);
				}
				catch (e)
				{
					this.runtime.trigger(cr.plugins_.WebStorage.prototype.cnds.OnQuotaExceeded, this);
					return;
				}
			}
		}
	};
	
	pluginProto.acts = new Acts();
	
	function Exps() {};
	
	Exps.prototype.LocalValue = function(ret,key)
	{
		if (!isSupported)
		{
			ret.set_string("");
			return;
		}
		
		ret.set_string(localStorage.getItem(prefix + key) || "");
	};
	
	Exps.prototype.SessionValue = function(ret,key)
	{
		if (!isSupported)
		{
			ret.set_string("");
			return;
		}
		
		ret.set_string(sessionStorage.getItem(prefix + key) || "");
	};
	
	Exps.prototype.LocalCount = function(ret)
	{
		if (!isSupported)
		{
			ret.set_int(0);
			return;
		}
		
		ret.set_int(is_arcade ? 0 : localStorage.length);
	};
	
	Exps.prototype.SessionCount = function(ret)
	{
		if (!isSupported)
		{
			ret.set_int(0);
			return;
		}
		
		ret.set_int(is_arcade ? 0 : sessionStorage.length);
	};
	
	Exps.prototype.LocalAt = function(ret,n)
	{
		if (is_arcade || !isSupported)
			ret.set_string("");
		else
			ret.set_string(localStorage.getItem(localStorage.key(n)) || "");
	};
	
	Exps.prototype.SessionAt = function(ret,n)
	{
		if (is_arcade || !isSupported)
			ret.set_string("");
		else
			ret.set_string(sessionStorage.getItem(sessionStorage.key(n)) || "");
	};
	
	Exps.prototype.LocalKeyAt = function(ret,n)
	{
		if (is_arcade || !isSupported)
			ret.set_string("");
		else
			ret.set_string(localStorage.key(n) || "");
	};
	
	Exps.prototype.SessionKeyAt = function(ret,n)
	{
		if (is_arcade || !isSupported)
			ret.set_string("");
		else
			ret.set_string(sessionStorage.key(n) || "");
	};
	
	Exps.prototype.AsJSON = function (ret)
	{
		if (!isSupported)
		{
			ret.set_string("");
			return;
		}
		
		var o = {}, i, len, k;
		
		for (i = 0, len = localStorage.length; i &lt; len; i++)
		{
			k = localStorage.key(i);
			
			if (is_arcade)
			{
				// Only include if has proper prefix, but remove prefix from result
				if (k.substr(0, prefix.length) === prefix)
				{
					o[k.substr(prefix.length)] = localStorage.getItem(k);
				}
			}
			else
				o[k] = localStorage.getItem(k);
		}
		
		ret.set_string(JSON.stringify({
			"c2dictionary": true,
			"data": o
		}));
	};
	
	pluginProto.exps = new Exps();
	
}());

// 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 0)
			this.cur_frame = 0;
		if (this.cur_frame &gt;= 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 &gt;= 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 &amp;&amp; this.type.animations[0].frames.length === 1) &amp;&amp; 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 &lt; leni; i++)
		{
			anim = this.type.animations[i];
			
			for (j = 0, lenj = anim.frames.length; j &lt; 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 &amp;&amp; frame.offy === 0 &amp;&amp; frame.width === maintex.width &amp;&amp; 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 &lt; 0)
			this.cur_frame = 0;
		if (this.cur_frame &gt;= 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 &gt;= 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 &amp;&amp; now &gt;= 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 &gt;= 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 &gt;= 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 &lt; 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 &gt;= 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 &gt;= 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 &lt; 0)
				this.cur_frame = 0;
			else if (this.cur_frame &gt;= 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 &gt; 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 &lt; 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 &lt; 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) &amp;&amp; this.animPlaying)
			return;
			
		this.cur_animation = anim;
		this.animForwards = (anim.speed &gt;= 0);
		this.cur_anim_speed = Math.abs(anim.speed);
		this.cur_anim_repeatto = anim.repeatto;
		
		if (this.cur_frame &lt; 0)
			this.cur_frame = 0;
		if (this.cur_frame &gt;= 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 &lt; 0)
			this.cur_frame = 0;
		if (this.cur_frame &gt;= 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 &lt; 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 &amp;&amp; w &gt;= 0 &amp;&amp; h &gt;= 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 &gt; 0 ? 1 : -1;
			var heightfactor = h &gt; 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 &lt; 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 &gt;= 1 &amp;&amp; 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 &lt; 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 &lt; 0 || index &gt;= 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 &lt; 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;
		var registeredInstances;
		
		// 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 &lt; linstances.length; l++)
		{
			linst = linstances[l];
			
			if (rsol.select_all)
			{
				linst.update_bbox();
				this.runtime.getCollisionCandidates(linst.layer, rtype, linst.bbox, candidates1);
				rinstances = candidates1;
				
				// NOTE: some behaviors like Platform can register a collision, then push the instance back in to a different
				// collision cell. This will cause the instance that registered a collision to not be in the collision
				// candidates, and therefore fail to detect a collision. To avoid this, specifically search for a list of all
				// instances that have registered a collision with linst, and ensure they are in the candidates.
				this.runtime.addRegisteredCollisionCandidates(linst, rtype, rinstances);
			}
			else
			{
				rinstances = rsol.getObjects();
			}
			
			for (r = 0; r &lt; 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 &lt; 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() &amp;&amp; !rsol.else_instances.length &amp;&amp; rsol.instances.length)
				rinstances = rsol.instances;
			else
				rinstances = rsol.else_instances;
		}
		else
		{
			rinstances = rsol.instances;
		}
		
		rpicktype = rtype;
		needscollisionfinish = (ltype !== rtype &amp;&amp; !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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 0;
	};
	
	Cnds.prototype.IsFlipped = function ()
	{
		return this.height &lt; 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 &lt; 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"] &lt; 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 &lt; 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 &amp;&amp; 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 &gt;= 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 &lt; 0)
			s = 0;
		if (s &gt;= 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 &lt; 0 ? -1 : 1);
		var flip_factor = (this.height &lt; 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 &amp;&amp; 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:" &amp;&amp; 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 &gt;= 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();

}());

// 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 &lt; 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];
		// note properties[1] corresponds to enable-bbcode, which is not supported in C2 runtime
		this.characterWidth   = this.properties[2];
		this.characterHeight  = this.properties[3];
		this.characterSet     = this.properties[4];
		var spacingData = this.properties[5];
		this.characterScale   = this.properties[6];
		this.characterSpacing = this.properties[7];
		this.lineHeight       = this.properties[8];
		this.halign           = this.properties[9]/2.0;			// 0=left, 1=center, 2=right
		this.valign           = this.properties[10]/2.0;		// 0=top, 1=center, 2=bottom
		this.wrapbyword       = (this.properties[11] === 0);	// 0=word, 1=character
		this.visible          = this.properties[12];
		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 &lt; 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 &lt; 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 &gt;= 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 &lt; charSet.length; c++) {
			// not enough texture space
			if  (c &gt;= 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 &lt; 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 &lt; text.length &amp;&amp; (text.charAt(i) === " " || text.charAt(i) === "\t"));

				wordsCache.push(cur_word);
				cur_word = "";
			}
			else if (i &lt; 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 &lt;= 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) &lt;= width &amp;&amp; text.indexOf("\n") === -1)
		{
			var all_width = inst.measureWidth(text);

			if (all_width &lt;= 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 &lt; 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 &gt; 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 &amp;&amp; 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 &lt; 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 &lt; len; i++) {
			width += this.getCharacterWidth(text.charAt(i)) * this.characterScale + spacing;
		}
		// we remove the trailing spacing
		width -= (width &gt; 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 !== "" &amp;&amp; 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 &lt; 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 &lt; 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 &amp;&amp; myy + drawY + charHeight &lt; viewTop)
				{
					drawY += charHeight;
					continue;
				}
			
				for(var j = 0; j &lt; 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 &amp;&amp; myx + drawX + letterWidth * scale + charSpace &lt; viewLeft)
					{
						drawX += letterWidth * scale + charSpace;
						continue;
					}

					// check if next letter fits in bounding box
					if ( drawX + letterWidth * scale &gt; this.width + EPSILON ) {
						break;
					}

					if (clip !== undefined) {

						roundX = drawX;
						roundY = drawY;
						
						if (angle === 0 &amp;&amp; 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 &amp;&amp; myx + drawX &gt; viewRight)
						break;
				}
				drawY += charHeight;

				// check if next row fits in bounding box and viewport and quit drawing if so
				if (angle === 0 &amp;&amp; (drawY + charHeight + lineHeight &gt; this.height || myy + drawY &gt; 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 &lt; 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 &lt; 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 &amp;&amp; offy + drawY + charHeight &lt; viewTop)
			{
				drawY += charHeight;
				continue;
			}
			
			for(var j = 0; j &lt; 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 &amp;&amp; offx + drawX + letterWidth * scale + charSpace &lt; viewLeft)
				{
					drawX += letterWidth * scale + charSpace;
					continue;
				}

				// check if next letter fits in bounding box
				if (drawX + letterWidth * scale &gt; 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 &amp;&amp; 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 &amp;&amp; offx + drawX &gt; viewRight)
					break;
			}
			
			drawY += charHeight;
			
			// check if next row fits in bounding box and viewport and quit drawing if so
			if (angle === 0 &amp;&amp; (drawY + charHeight + lineHeight &gt; this.height || offy + drawY &gt; 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) &amp;&amp; param &lt; 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 &lt; 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];
			
			for (j = 0, lenj = str.length; j &lt; 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 &lt; 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();

}());

// GameMonetize SDK

;
;

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

(function() {
  var pluginProto = cr.plugins_.GM_SDK.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.available_adtypes = ["interstitial"];

    // Initialise object properties
    this._gameID = 0;
    this._adPlaying = false;
    this._adViewed = false;
    this._adPreloaded = false;
  };

  var instanceProto = pluginProto.Instance.prototype;

  instanceProto.onCreate = function() {
    this._gameID = this.properties[0];

    try {
      try {
        if (this._runtime.IsPreview()) {
          localStorage.setItem("gm_debug", true);
          localStorage.setItem(
            "gm_tag",
            "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&amp;iu=/124319096/external/single_ad_samples&amp;ciu_szs=300x250&amp;impl=s&amp;gdfp_req=1&amp;env=vp&amp;output=vast&amp;unviewed_position_start=1&amp;cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&amp;correlator="
          );
        } else {
          localStorage.removeItem("gm_debug");
          localStorage.removeItem("gm_tag");
        }
      } catch (e) {}
    } catch (e) {}

    window.SDK_OPTIONS = {
      gameId: this._gameID,
      onEvent: event =&gt; {
        switch (event.name) {
          case "SDK_GAME_START":
            // advertisement done, resume game logic and unmute audio
            this._adPlaying = false;
            break;
          case "SDK_GAME_PAUSE":
            // pause game logic / mute audio
            this._adPlaying = true;
            break;
          case "COMPLETE":
            // this event is triggered when the user watched an entire ad
            this._adViewed = true;
            setTimeout(() =&gt; {
              this._adViewed = false;
            }, 5000);
            break;
          case "SDK_READY":
            let debugBar = document.querySelector("#sdk__implementation");
            if (debugBar) debugBar.remove();
            this._sdkReady = true;
            break;
        }
      }
    };

    //Load the SDK from the CDN
    (function(d, s, id) {
      var js,
        fjs = d.getElementsByTagName(s)[0];
      if (d.getElementById(id)) return;
      js = d.createElement(s);
      js.id = id;
      js.src = "//api.gamemonetize.com/sdk.js";
      fjs.parentNode.insertBefore(js, fjs);
    })(document, "script", "gamemonetize-sdk");
  };

  instanceProto.saveToJSON = function() {
    return {};
  };

  instanceProto.loadFromJSON = function(o) {};


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

  (Cnds.prototype.ResumeGame = function() {
    return !this._adPlaying;
  }),
    (Cnds.prototype.PauseGame = function() {
      return this._adPlaying;
    }),
    (Cnds.prototype.AdViewed = function() {
      return this._adViewed;
    }),
    (Cnds.prototype.PreloadedAd = function() {
      return this._preloadedAd;
    });

  pluginProto.cnds = new Cnds();

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

  Acts.prototype.ShowAd = function() {
    if (!this._sdkReady) return;

    if (!this._sdkReady) return;
    var sdk = window["sdk"];
    if (sdk !== "undefined" &amp;&amp; sdk.showBanner !== "undefined") {
      sdk.showBanner();
    }
  };

  pluginProto.acts = new Acts();

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

  pluginProto.exps = new Exps();
})();


// Bullet
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Behavior class
cr.behaviors.Bullet = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var behaviorProto = cr.behaviors.Bullet.prototype;
		
	/////////////////////////////////////
	// Behavior type class
	behaviorProto.Type = function(behavior, objtype)
	{
		this.behavior = behavior;
		this.objtype = objtype;
		this.runtime = behavior.runtime;
	};

	var behtypeProto = behaviorProto.Type.prototype;

	behtypeProto.onCreate = function()
	{
	};

	/////////////////////////////////////
	// Behavior instance class
	behaviorProto.Instance = function(type, inst)
	{
		this.type = type;
		this.behavior = type.behavior;
		this.inst = inst;				// associated object instance to modify
		this.runtime = type.runtime;
	};

	var behinstProto = behaviorProto.Instance.prototype;

	behinstProto.onCreate = function()
	{
		var speed = this.properties[0];
		this.acc = this.properties[1];
		this.g = this.properties[2];
		this.bounceOffSolid = this.properties[3];
		this.setAngle = this.properties[4];
		this.step = this.properties[5];
		this.stepSize = Math.abs(Math.min(this.inst.width, this.inst.height) / 2);
		this.stopStepping = false;
		
		this.dx = Math.cos(this.inst.angle) * speed;
		this.dy = Math.sin(this.inst.angle) * speed;
		this.lastx = this.inst.x;
		this.lasty = this.inst.y;		
		this.lastKnownAngle = this.inst.angle;
		this.travelled = 0;
		
		this.enabled = this.properties[6];
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"acc": this.acc,
			"g": this.g,
			"dx": this.dx,
			"dy": this.dy,
			"lx": this.lastx,
			"ly": this.lasty,
			"lka": this.lastKnownAngle,
			"t": this.travelled,
			"st": this.step,
			"e": this.enabled
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.acc = o["acc"];
		this.g = o["g"];
		this.dx = o["dx"];
		this.dy = o["dy"];
		this.lastx = o["lx"];
		this.lasty = o["ly"];
		this.lastKnownAngle = o["lka"];
		this.travelled = o["t"];
		this.step = !!o["st"];
		this.enabled = o["e"];
	};

	behinstProto.tick = function ()
	{
		if (!this.enabled)
			return;
			
		var dt = this.runtime.getDt(this.inst);
		var s, a;
		var bounceSolid, bounceAngle;
		
		// Object had its angle changed: change angle of motion, providing 'Set angle' is enabled.
		if (this.inst.angle !== this.lastKnownAngle)
		{
			if (this.setAngle)
			{
				s = cr.distanceTo(0, 0, this.dx, this.dy);
				this.dx = Math.cos(this.inst.angle) * s;
				this.dy = Math.sin(this.inst.angle) * s;
			}
			
			this.lastKnownAngle = this.inst.angle;
		}
		
		// Apply acceleration
		var xacc = 0;
		var yacc = 0;
		
		if (this.acc !== 0)
		{
			s = cr.distanceTo(0, 0, this.dx, this.dy);
			
			if (this.dx === 0 &amp;&amp; this.dy === 0)
				a = this.inst.angle;
			else
				a = cr.angleTo(0, 0, this.dx, this.dy);
			
			// Note acceleration is applied in polar co-ordinates, but we must separately track the
			// X and Y components of acceleration for the position calculation below.
			s += this.acc * dt;
			xacc = Math.cos(a) * this.acc;
			yacc = Math.sin(a) * this.acc;
			
			// Don't decelerate to negative speeds
			if (s &lt; 0)
			{
				s = 0;
				xacc = 0;
				yacc = 0;
			}
			
			this.dx = Math.cos(a) * s;
			this.dy = Math.sin(a) * s;
		}
		
		// Apply gravity
		if (this.g !== 0)
		{
			this.dy += this.g * dt;
			yacc += this.g;
		}
			
		this.lastx = this.inst.x;
		this.lasty = this.inst.y;
		
		// Apply movement to the object
		if (this.dx !== 0 || this.dy !== 0)
		{
			var mx = this.runtime.accelerate(this.dx, -Infinity, Infinity, xacc, dt);
			var my = this.runtime.accelerate(this.dy, -Infinity, Infinity, yacc, dt);
			
			// offsets the X and Y, or does stepping if enabled
			this.moveBy(mx, my);
			
			this.travelled += cr.distanceTo(this.lastx, this.lasty, this.inst.x, this.inst.y);
			
			if (this.setAngle &amp;&amp; (mx !== 0 || my !== 0))			// skip if no movement (e.g. dt is 0) otherwise resets angle to right
			{
				this.inst.angle = cr.angleTo(0, 0, mx, my);
				this.inst.set_bbox_changed();
				this.lastKnownAngle = this.inst.angle;
			}
			
			// Is bouncing off solid and has moved in to a solid
			if (this.bounceOffSolid)
			{
				bounceSolid = this.runtime.testOverlapSolid(this.inst);
				
				// Has hit a solid
				if (bounceSolid)
				{
					this.runtime.registerCollision(this.inst, bounceSolid);
					
					s = cr.distanceTo(0, 0, this.dx, this.dy);
					bounceAngle = this.runtime.calculateSolidBounceAngle(this.inst, this.lastx, this.lasty);
					this.dx = Math.cos(bounceAngle) * s;
					this.dy = Math.sin(bounceAngle) * s;
					this.inst.x += this.dx * dt;			// move out for one tick since the object can't have spent a tick in the solid
					this.inst.y += this.dy * dt;
					this.inst.set_bbox_changed();
					
					if (this.setAngle)
					{
						// Setting the object angle after a bounce may cause it to overlap a solid again.
						// Make sure it's pushed out.
						this.inst.angle = bounceAngle;
						this.lastKnownAngle = bounceAngle;
						this.inst.set_bbox_changed();
					}
					
					// Advance the object until it is outside the solid
					if (!this.runtime.pushOutSolid(this.inst, this.dx / s, this.dy / s, Math.max(s * 2.5 * dt, 30)))
						this.runtime.pushOutSolidNearest(this.inst, 100);
				}
			}
		}
	};
	
	behinstProto.moveBy = function (mx, my)
	{
		var stepDist = cr.distanceTo(0, 0, mx, my);
		
		// Stepping disabled or moving less than the step size: just move to destination
		if (!this.step || stepDist &lt;= this.stepSize)
		{
			this.inst.x += mx;
			this.inst.y += my;
			this.inst.set_bbox_changed();
			
			// If stepping is disabled (and we're just skipping because the step distance is small), trigger 'On step' anyway.
			// This is so if we only have a collision check in an 'On step' trigger, it continues to work as expected.
			if (this.step)
			{
				this.runtime.trigger(cr.behaviors.Bullet.prototype.cnds.OnStep, this.inst);
			}
			
			return;
		}
		
		this.stopStepping = false;
		
		// Move in steps of stepSize.
		var startX = this.inst.x;
		var startY = this.inst.y;
		var endX = startX + mx;
		var endY = startY + my;
		var a = cr.angleTo(0, 0, mx, my);
		var stepX = Math.cos(a) * this.stepSize;
		var stepY = Math.sin(a) * this.stepSize;
		
		var stepCount = Math.floor(stepDist / this.stepSize);
		var i = 1;						// skip 0th step (is same as starting position)
		for ( ; i &lt;= stepCount; ++i)	// include last step
		{
			this.inst.x = startX + stepX * i;
			this.inst.y = startY + stepY * i;
			this.inst.set_bbox_changed();
			
			this.runtime.trigger(cr.behaviors.Bullet.prototype.cnds.OnStep, this.inst);
			
			if (this.inst.isDestroyed || this.stopStepping)
				return;
		}
		
		// Do one last step at the finishing position, so we don't need an extra collision event
		this.inst.x = endX;
		this.inst.y = endY;
		this.inst.set_bbox_changed();
		this.runtime.trigger(cr.behaviors.Bullet.prototype.cnds.OnStep, this.inst);
	};
	

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

	Cnds.prototype.CompareSpeed = function (cmp, s)
	{
		return cr.do_cmp(cr.distanceTo(0, 0, this.dx, this.dy), cmp, s);
	};
	
	Cnds.prototype.CompareTravelled = function (cmp, d)
	{
		return cr.do_cmp(this.travelled, cmp, d);
	};
	
	Cnds.prototype.OnStep = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.enabled;
	};
	
	behaviorProto.cnds = new Cnds();

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

	Acts.prototype.SetSpeed = function (s)
	{
		var a = cr.angleTo(0, 0, this.dx, this.dy);
		this.dx = Math.cos(a) * s;
		this.dy = Math.sin(a) * s;
	};
	
	Acts.prototype.SetAcceleration = function (a)
	{
		this.acc = a;
	};
	
	Acts.prototype.SetGravity = function (g)
	{
		this.g = g;
	};
	
	Acts.prototype.SetAngleOfMotion = function (a)
	{
		a = cr.to_radians(a);
		var s = cr.distanceTo(0, 0, this.dx, this.dy)
		this.dx = Math.cos(a) * s;
		this.dy = Math.sin(a) * s;
	};
	
	Acts.prototype.Bounce = function (objtype)
	{
		if (!objtype)
			return;
		
		var otherinst = objtype.getFirstPicked(this.inst);
		
		if (!otherinst)
			return;
			
		var dt = this.runtime.getDt(this.inst);
		var s = cr.distanceTo(0, 0, this.dx, this.dy);
		var bounceAngle = this.runtime.calculateSolidBounceAngle(this.inst, this.lastx, this.lasty, otherinst);
		this.dx = Math.cos(bounceAngle) * s;
		this.dy = Math.sin(bounceAngle) * s;
		this.inst.x += this.dx * dt;			// move out for one tick since the object can't have spent a tick in the solid
		this.inst.y += this.dy * dt;
		this.inst.set_bbox_changed();
		
		if (this.setAngle)
		{
			// Setting the object angle after a bounce may cause it to overlap a solid again.
			// Make sure it's pushed out.
			this.inst.angle = bounceAngle;
			this.lastKnownAngle = bounceAngle;
			this.inst.set_bbox_changed();
		}
		
		// Advance the object until it is outside the solid
		if (s !== 0)		// prevent divide-by-zero
		{
			if (this.bounceOffSolid)
			{
				if (!this.runtime.pushOutSolid(this.inst, this.dx / s, this.dy / s, Math.max(s * 2.5 * dt, 30)))
					this.runtime.pushOutSolidNearest(this.inst, 100);
			}
			else 
			{
				this.runtime.pushOut(this.inst, this.dx / s, this.dy / s, Math.max(s * 2.5 * dt, 30), otherinst)
			}
		}
	};

	Acts.prototype.SetBounceOffSolids = function (b)
	{
		this.bounceOffSolid = b;
	};
	
	Acts.prototype.SetDistanceTravelled = function (d)
	{
		this.travelled = d;
	};
	
	Acts.prototype.SetEnabled = function (en)
	{
		this.enabled = (en === 1);
	};
	
	Acts.prototype.StopStepping = function ()
	{
		this.stopStepping = true;
	}
	
	behaviorProto.acts = new Acts();

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

	Exps.prototype.Speed = function (ret)
	{
		var s = cr.distanceTo(0, 0, this.dx, this.dy);
		
		// Due to floating point inaccuracy is likely to return 99.9999999 when speed is set to 100.
		// So round to nearest millionth of a pixel per second.
		s = cr.round6dp(s);
		
		ret.set_float(s);
	};
	
	Exps.prototype.Acceleration = function (ret)
	{
		ret.set_float(this.acc);
	};
	
	Exps.prototype.AngleOfMotion = function (ret)
	{
		ret.set_float(cr.to_degrees(cr.angleTo(0, 0, this.dx, this.dy)));
	};
	
	Exps.prototype.DistanceTravelled = function (ret)
	{
		ret.set_float(this.travelled);
	};
	
	Exps.prototype.Gravity = function (ret)
	{
		ret.set_float(this.g);
	};
	
	behaviorProto.exps = new Exps();
	
}());

// Fade
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Behavior class
cr.behaviors.Fade = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var behaviorProto = cr.behaviors.Fade.prototype;
		
	/////////////////////////////////////
	// Behavior type class
	behaviorProto.Type = function(behavior, objtype)
	{
		this.behavior = behavior;
		this.objtype = objtype;
		this.runtime = behavior.runtime;
	};

	var behtypeProto = behaviorProto.Type.prototype;

	behtypeProto.onCreate = function()
	{
	};

	/////////////////////////////////////
	// Behavior instance class
	behaviorProto.Instance = function(type, inst)
	{
		this.type = type;
		this.behavior = type.behavior;
		this.inst = inst;				// associated object instance to modify
		this.runtime = type.runtime;
	};

	var behinstProto = behaviorProto.Instance.prototype;

	behinstProto.onCreate = function()
	{
		this.fadeInTime = this.properties[0];
		this.waitTime = this.properties[1];
		this.fadeOutTime = this.properties[2];
		this.destroy = this.properties[3];
		this.activeAtStart = this.properties[4];
		
		this.setMaxOpacity = false;					// used to retrieve maxOpacity once in first 'Start fade' action if initially inactive
		
		this.stage = this.activeAtStart ? 0 : 3;		// 0 = fade in, 1 = wait, 2 = fade out, 3 = done
		
		if (this.recycled)
			this.stageTime.reset();
		else
			this.stageTime = new cr.KahanAdder();
		
		this.maxOpacity = (this.inst.opacity ? this.inst.opacity : 1.0);
		
		if (this.activeAtStart)
		{
			// Skip fade-in
			if (this.fadeInTime === 0)
			{
				this.stage = 1;
				
				// Skip wait
				if (this.waitTime === 0)
					this.stage = 2;
			}
			// Otherwise we don't want it at default opacity for first tick.  Set to 0 opacity.
			else
			{
				this.inst.opacity = 0;
				this.runtime.redraw = true;
			}
		}
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"fit": this.fadeInTime,
			"wt": this.waitTime,
			"fot": this.fadeOutTime,
			"s": this.stage,
			"st": this.stageTime.sum,
			"mo": this.maxOpacity,
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.fadeInTime = o["fit"];
		this.waitTime = o["wt"];
		this.fadeOutTime = o["fot"];
		this.stage = o["s"];
		this.stageTime.reset();
		this.stageTime.sum = o["st"];
		this.maxOpacity = o["mo"];
	};

	behinstProto.tick = function ()
	{
		this.stageTime.add(this.runtime.getDt(this.inst));
		
		// Stage 0: fade-in
		if (this.stage === 0)
		{
			this.inst.opacity = (this.stageTime.sum / this.fadeInTime) * this.maxOpacity;
			this.runtime.redraw = true;
			
			// Fade-in completed
			if (this.inst.opacity &gt;= this.maxOpacity)
			{
				this.inst.opacity = this.maxOpacity;
				this.stage = 1;	// wait stage
				this.stageTime.reset();
				
				// On fade-in end
				this.runtime.trigger(cr.behaviors.Fade.prototype.cnds.OnFadeInEnd, this.inst);
			}
		}
		
		// Stage 1: wait
		if (this.stage === 1)
		{
			// Wait time has elapsed
			if (this.stageTime.sum &gt;= this.waitTime)
			{
				this.stage = 2;	// fade out stage
				this.stageTime.reset();
				
				// On wait end
				this.runtime.trigger(cr.behaviors.Fade.prototype.cnds.OnWaitEnd, this.inst);
			}
		}
		
		// Stage 2: fade out
		if (this.stage === 2)
		{
			if (this.fadeOutTime !== 0)
			{
				this.inst.opacity = this.maxOpacity - ((this.stageTime.sum / this.fadeOutTime) * this.maxOpacity);
				this.runtime.redraw = true;
				
				// Fade-out completed
				if (this.inst.opacity &lt; 0)
				{
					this.inst.opacity = 0;
					this.stage = 3;	// done
					this.stageTime.reset();
					
					// On fade-out end
					this.runtime.trigger(cr.behaviors.Fade.prototype.cnds.OnFadeOutEnd, this.inst);
					
					// Destroy after fade out
					if (this.destroy)
						this.runtime.DestroyInstance(this.inst);
				}
			}
		}
	};
	
	behinstProto.doStart = function ()
	{
		this.stage = 0;
		this.stageTime.reset();
		
		// Skip fade-in
		if (this.fadeInTime === 0)
		{
			this.stage = 1;
			
			// Skip wait
			if (this.waitTime === 0)
				this.stage = 2;
		}
		// Otherwise we don't want it at default opacity for first tick.  Set to 0 opacity.
		else
		{
			this.inst.opacity = 0;
			this.runtime.redraw = true;
		}
	};
	

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

	Cnds.prototype.OnFadeOutEnd = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnFadeInEnd = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnWaitEnd = function ()
	{
		return true;
	};
	
	behaviorProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	Acts.prototype.StartFade = function ()
	{
		// If the fade was not active at the start, grab the max opacity the first time the 'Start fade' action
		// is used. This allows changes in opacity made before the fade behavior starts to have an effect.
		if (!this.activeAtStart &amp;&amp; !this.setMaxOpacity)
		{
			this.maxOpacity = (this.inst.opacity ? this.inst.opacity : 1.0);
			this.setMaxOpacity = true;
		}
		
		if (this.stage === 3)
			this.doStart();
	};
	
	Acts.prototype.RestartFade = function ()
	{
		this.doStart();
	};
	
	Acts.prototype.SetFadeInTime = function (t)
	{
		if (t &lt; 0)
			t = 0;
		
		this.fadeInTime = t;
	};
	
	Acts.prototype.SetWaitTime = function (t)
	{
		if (t &lt; 0)
			t = 0;
		
		this.waitTime = t;
	};
	
	Acts.prototype.SetFadeOutTime = function (t)
	{
		if (t &lt; 0)
			t = 0;
		
		this.fadeOutTime = t;
	};
	
	behaviorProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.FadeInTime = function (ret)
	{
		ret.set_float(this.fadeInTime);
	};
	
	Exps.prototype.WaitTime = function (ret)
	{
		ret.set_float(this.waitTime);
	};
	
	Exps.prototype.FadeOutTime = function (ret)
	{
		ret.set_float(this.fadeOutTime);
	};
	
	behaviorProto.exps = new Exps();

}());

// Sine
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Behavior class
cr.behaviors.Sin = function(runtime)
{
	this.runtime = runtime;
};

(function ()
{
	var behaviorProto = cr.behaviors.Sin.prototype;
		
	/////////////////////////////////////
	// Behavior type class
	behaviorProto.Type = function(behavior, objtype)
	{
		this.behavior = behavior;
		this.objtype = objtype;
		this.runtime = behavior.runtime;
	};
	
	var behtypeProto = behaviorProto.Type.prototype;

	behtypeProto.onCreate = function()
	{
	};

	/////////////////////////////////////
	// Behavior instance class
	behaviorProto.Instance = function(type, inst)
	{
		this.type = type;
		this.behavior = type.behavior;
		this.inst = inst;				// associated object instance to modify
		this.runtime = type.runtime;
		
		this.i = 0;		// period offset (radians)
	};
	
	var behinstProto = behaviorProto.Instance.prototype;
	
	var _2pi = 2 * Math.PI;
	var _pi_2 = Math.PI / 2;
	var _3pi_2 = (3 * Math.PI) / 2;
	
	// C2 compatibility: C2 used a different list of movements. To preserve compatibility with the savegame format, convert
	// the initial movement value on startup. The table below converts the C3 index to the C2 index.
	// C2 list: 0=Horizontal|1=Vertical|2=Size|3=Width|4=Height|5=Angle|6=Opacity|7=Value only|8=Forwards/backwards
	// C3 list: 0=Horizontal|1=Vertical|2=Forwards/backwards|3=Width|4=Height|5=Size|6=Angle|7=Opacity|8=Value only
	var movementLookup = [0, 1, 8, 3, 4, 2, 5, 6, 7];

	behinstProto.onCreate = function()
	{
		// Load properties
		this.movement = movementLookup[this.properties[0]]; // note converted for C2 compatibility, see above
		this.wave = this.properties[1];		// 0=Sine|1=Triangle|2=Sawtooth|3=Reverse sawtooth|4=Square
		this.period = this.properties[2];
		this.period += Math.random() * this.properties[3];								// period random
		
		if (this.period === 0)
			this.i = 0;
		else
		{
			this.i = (this.properties[4] / this.period) * _2pi;							// period offset
			this.i += ((Math.random() * this.properties[5]) / this.period) * _2pi;		// period offset random
		}
		
		this.mag = this.properties[6];													// magnitude
		this.mag += Math.random() * this.properties[7];									// magnitude random
		
		this.isEnabled = this.properties[8];
		
		this.initialValue = 0;
		this.initialValue2 = 0;
		this.ratio = 0;
		
		// Convert magnitude from degrees to radians. Don't do this in init() otherwise it is repeated
		// by the 'Update initial state' action.
		if (this.movement === 5)			// angle
			this.mag = cr.to_radians(this.mag);
		
		this.init();
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"i": this.i,
			"a": this.isEnabled,
			"mv": this.movement,
			"w": this.wave,
			"p": this.period,
			"mag": this.mag,
			"iv": this.initialValue,
			"iv2": this.initialValue2,
			"r": this.ratio,
			"lkv": this.lastKnownValue,
			"lkv2": this.lastKnownValue2
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.i = o["i"];
		this.isEnabled = o["a"];
		this.movement = o["mv"];
		this.wave = o["w"];
		this.period = o["p"];
		this.mag = o["mag"];
		this.initialValue = o["iv"];
		this.initialValue2 = o["iv2"] || 0;
		this.ratio = o["r"];
		this.lastKnownValue = o["lkv"];
		this.lastKnownValue2 = o["lkv2"] || 0;
	};
	
	behinstProto.init = function ()
	{
		switch (this.movement) {
		case 0:		// horizontal
			this.initialValue = this.inst.x;
			break;
		case 1:		// vertical
			this.initialValue = this.inst.y;
			break;
		case 2:		// size
			this.initialValue = this.inst.width;
			this.ratio = this.inst.height / this.inst.width;
			break;
		case 3:		// width
			this.initialValue = this.inst.width;
			break;
		case 4:		// height
			this.initialValue = this.inst.height;
			break;
		case 5:		// angle
			this.initialValue = this.inst.angle;
			break;
		case 6:		// opacity
			this.initialValue = this.inst.opacity;
			break;
		case 7:
			//value only, leave at 0
			this.initialValue = 0;
			break;
		case 8:		// forwards/backwards
			this.initialValue = this.inst.x;
			this.initialValue2 = this.inst.y;
			break;
		default:
;
		}
		
		this.lastKnownValue = this.initialValue;
		this.lastKnownValue2 = this.initialValue2;
	};
	
	behinstProto.waveFunc = function (x)
	{
		x = x % _2pi;
		
		switch (this.wave) {
		case 0:		// sine
			return Math.sin(x);
		case 1:		// triangle
			if (x &lt;= _pi_2)
				return x / _pi_2;
			else if (x &lt;= _3pi_2)
				return 1 - (2 * (x - _pi_2) / Math.PI);
			else
				return (x - _3pi_2) / _pi_2 - 1;
		case 2:		// sawtooth
			return 2 * x / _2pi - 1;
		case 3:		// reverse sawtooth
			return -2 * x / _2pi + 1;
		case 4:		// square
			return x &lt; Math.PI ? -1 : 1;
		};
		
		// should not reach here
		return 0;
	};

	behinstProto.tick = function ()
	{
		var dt = this.runtime.getDt(this.inst);
		
		if (!this.isEnabled || dt === 0)
			return;
		
		if (this.period === 0)
			this.i = 0;
		else
		{
			this.i += (dt / this.period) * _2pi;
			this.i = this.i % _2pi;
		}
		this.updateFromPhase();
	};
	
	behinstProto.updateFromPhase = function ()
	{
		switch (this.movement) {
		case 0:		// horizontal
			if (this.inst.x !== this.lastKnownValue)
				this.initialValue += this.inst.x - this.lastKnownValue;
				
			this.inst.x = this.initialValue + this.waveFunc(this.i) * this.mag;
			this.lastKnownValue = this.inst.x;
			break;
		case 1:		// vertical
			if (this.inst.y !== this.lastKnownValue)
				this.initialValue += this.inst.y - this.lastKnownValue;
				
			this.inst.y = this.initialValue + this.waveFunc(this.i) * this.mag;
			this.lastKnownValue = this.inst.y;
			break;
		case 2:		// size
			this.inst.width = this.initialValue + this.waveFunc(this.i) * this.mag;
			this.inst.height = this.inst.width * this.ratio;
			break;
		case 3:		// width
			this.inst.width = this.initialValue + this.waveFunc(this.i) * this.mag;
			break;
		case 4:		// height
			this.inst.height = this.initialValue + this.waveFunc(this.i) * this.mag;
			break;
		case 5:		// angle
			if (this.inst.angle !== this.lastKnownValue)
				this.initialValue = cr.clamp_angle(this.initialValue + (this.inst.angle - this.lastKnownValue));
				
			this.inst.angle = cr.clamp_angle(this.initialValue + this.waveFunc(this.i) * this.mag);
			this.lastKnownValue = this.inst.angle;
			break;
		case 6:		// opacity
			this.inst.opacity = this.initialValue + (this.waveFunc(this.i) * this.mag) / 100;
			
			if (this.inst.opacity &lt; 0)
				this.inst.opacity = 0;
			else if (this.inst.opacity &gt; 1)
				this.inst.opacity = 1;
				
			break;
		case 8:		// forwards/backwards
			if (this.inst.x !== this.lastKnownValue)
				this.initialValue += this.inst.x - this.lastKnownValue;
			if (this.inst.y !== this.lastKnownValue2)
				this.initialValue2 += this.inst.y - this.lastKnownValue2;
				
			this.inst.x = this.initialValue + Math.cos(this.inst.angle) * this.waveFunc(this.i) * this.mag;
			this.inst.y = this.initialValue2 + Math.sin(this.inst.angle) * this.waveFunc(this.i) * this.mag;
			this.lastKnownValue = this.inst.x;
			this.lastKnownValue2 = this.inst.y;
			break;
		}

		this.inst.set_bbox_changed();
	};
	
	behinstProto.onSpriteFrameChanged = function (prev_frame, next_frame)
	{
		// Handle size change when in width, height or size mode
		switch (this.movement) {
		case 2:	// size
			this.initialValue *= (next_frame.width / prev_frame.width);
			this.ratio = next_frame.height / next_frame.width;
			break;
		case 3:	// width
			this.initialValue *= (next_frame.width / prev_frame.width);
			break;
		case 4:	// height
			this.initialValue *= (next_frame.height / prev_frame.height);
			break;
		}
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.isEnabled;
	};
	
	Cnds.prototype.CompareMovement = function (m)
	{
		return this.movement === m;
	};
	
	Cnds.prototype.ComparePeriod = function (cmp, v)
	{
		return cr.do_cmp(this.period, cmp, v);
	};
	
	Cnds.prototype.CompareMagnitude = function (cmp, v)
	{
		if (this.movement === 5)
			return cr.do_cmp(this.mag, cmp, cr.to_radians(v));
		else
			return cr.do_cmp(this.mag, cmp, v);
	};
	
	Cnds.prototype.CompareWave = function (w)
	{
		return this.wave === w;
	};
	
	behaviorProto.cnds = new Cnds();

	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	Acts.prototype.SetEnabled = function (a)
	{
		this.isEnabled = (a === 1);
	};
	
	Acts.prototype.SetPeriod = function (x)
	{
		this.period = x;
	};
	
	Acts.prototype.SetMagnitude = function (x)
	{
		this.mag = x;
		
		if (this.movement === 5)	// angle
			this.mag = cr.to_radians(this.mag);
	};
	
	Acts.prototype.SetMovement = function (m)
	{
		// Undo radians conversion if switching away from angle mode
		if (this.movement === 5 &amp;&amp; m !== 5)
			this.mag = cr.to_degrees(this.mag);
			
		this.movement = m;
		this.init();
	};
	
	Acts.prototype.SetWave = function (w)
	{
		this.wave = w;
	};
	
	Acts.prototype.SetPhase = function (x)
	{
		this.i = (x * _2pi) % _2pi;
		this.updateFromPhase();
	};
	
	Acts.prototype.UpdateInitialState = function ()
	{
		this.init();
	};
	
	behaviorProto.acts = new Acts();

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

	Exps.prototype.CyclePosition = function (ret)
	{
		ret.set_float(this.i / _2pi);
	};
	
	Exps.prototype.Period = function (ret)
	{
		ret.set_float(this.period);
	};
	
	Exps.prototype.Magnitude = function (ret)
	{
		if (this.movement === 5)	// angle
			ret.set_float(cr.to_degrees(this.mag));
		else
			ret.set_float(this.mag);
	};
	
	Exps.prototype.Value = function (ret)
	{
		ret.set_float(this.waveFunc(this.i) * this.mag);
	};
	
	behaviorProto.exps = new Exps();
	
}());

// Physics
// ECMAScript 5 strict mode

var Box2D = window["Box2DFn"]();
var b2Vec2 = Box2D["b2Vec2"];

// from helper script for creating collision polygons
function createPolygonShape(vertices)
{
	var shape = new Box2D["b2PolygonShape"]();
	var buffer = Box2D["_malloc"](vertices.length * 8);
	var offset = 0;
	for (var i = 0; i &lt; vertices.length; i++)
	{
		Box2D["HEAPF32"][buffer + offset &gt;&gt; 2] = vertices[i]["get_x"]();
		Box2D["HEAPF32"][buffer + (offset + 4) &gt;&gt; 2] = vertices[i]["get_y"]();
		offset += 8;
	}
	var ptr_wrapped = Box2D["wrapPointer"](buffer, Box2D["b2Vec2"]);
	shape["Set"](ptr_wrapped, vertices.length);
	Box2D["_free"](buffer);
	return shape;
}

// Recycler for b2Vec2s
b2Vec2._freeCache = [];

b2Vec2.Get = function (x, y)
{
	var ret;
	
	if (b2Vec2._freeCache.length)
	{
		ret = b2Vec2._freeCache.pop();
		ret["set_x"](x);
		ret["set_y"](y);
		return ret;
	}
	else
	{
		return new b2Vec2(x, y);
	}
};

b2Vec2.Free = function (v)
{
	b2Vec2._freeCache.push(v);
};

b2Vec2.Clone = function (v)
{
	return b2Vec2.Get(v["get_x"](), v["get_y"]());
};

// For where vec2s are only needed temporarily and we don't want to make garbage
var tmpvec2a = b2Vec2.Get(0, 0);
var tmpvec2b = b2Vec2.Get(0, 0);

function getTempVec2a(x, y)
{
	tmpvec2a["set_x"](x);
	tmpvec2a["set_y"](y);
	return tmpvec2a;
};

function getTempVec2b(x, y)
{
	tmpvec2b["set_x"](x);
	tmpvec2b["set_y"](y);
	return tmpvec2b;
};

///////////////////////////////////////////////////////////////////////////////////////////////
// b2Separator
// Translated from code written by Antoan Angelov
// http://www.emanueleferonato.com/2011/09/12/create-non-convex-complex-shapes-with-box2d/
// Original license:
/*
* Convex Separator for Box2D Flash
*
* This class has been written by Antoan Angelov. 
* It is designed to work with Erin Catto's Box2D physics library.
*
* Everybody can use this software for any purpose, under two restrictions:
* 1. You cannot claim that you wrote this software.
* 2. You can not remove or alter this notice.
*
*/
// Translation from ActionScript to Javascript by Ashley Gullen
cr.b2Separator = function() {};

cr.b2Separator.det = function(x1, y1, x2, y2, x3, y3)
{
	return x1*y2 + x2*y3 + x3*y1 - y1*x2 - y2*x3 - y3*x1;
};

cr.b2Separator.hitRay = function(x1, y1, x2, y2, x3, y3, x4, y4)
{
	var t1 = x3-x1, t2 = y3-y1, t3 = x2-x1, t4 = y2-y1, t5 = x4-x3, t6 = y4-y3, t7 = t4*t5 - t3*t6;
	var a = (t5*t2 - t6*t1) / t7;
	var px = x1 + a*t3, py = y1 + a*t4;
	var b1 = cr.b2Separator.isOnSegment(x2, y2, x1, y1, px, py);
	var b2 = cr.b2Separator.isOnSegment(px, py, x3, y3, x4, y4);

	if (b1 &amp;&amp; b2)
		return b2Vec2.Get(px, py);
	else
		return null;
};

cr.b2Separator.isOnSegment = function(px, py, x1, y1, x2, y2)
{
	var b1 = (x1+0.1 &gt;= px &amp;&amp; px &gt;= x2-0.1) || (x1-0.1 &lt;= px &amp;&amp; px &lt;= x2+0.1);
	var b2 = (y1+0.1 &gt;= py &amp;&amp; py &gt;= y2-0.1) || (y1-0.1 &lt;= py &amp;&amp; py &lt;= y2+0.1);
	return (b1 &amp;&amp; b2) &amp;&amp; cr.b2Separator.isOnLine(px, py, x1, y1, x2, y2);
};

cr.b2Separator.isOnLine = function(px, py, x1, y1, x2, y2)
{
	if (Math.abs(x2-x1) &gt; 0.1)
	{
		var a = (y2-y1) / (x2-x1);
		var possibleY = a * (px-x1)+y1;
		var diff = Math.abs(possibleY-py);
		return diff &lt; 0.1;
	}

	return Math.abs(px-x1) &lt; 0.1;
};

cr.b2Separator.pointsMatch = function(x1, y1, x2, y2)
{
	return Math.abs(x2-x1) &lt; 0.1 &amp;&amp; Math.abs(y2-y1) &lt; 0.1;
};

cr.b2Separator.Separate = function(verticesVec /*array of b2Vec2*/, objarea)
{
	var calced = cr.b2Separator.calcShapes(verticesVec);
	
	// deep copy output
	var ret = [];
	var poly, a, b, c;
	var i, len, j, lenj;
	var areasum;
	
	for (i = 0, len = calced.length; i &lt; len; i++)
	{
		a = calced[i];
		poly = [];
		poly.length = a.length;
		areasum = 0;
		
		for (j = 0, lenj = a.length; j &lt; lenj; j++)
		{
			b = a[j];
			c = a[(j + 1) % lenj];
			
			// calculating area
			areasum += (b["get_x"]() * c["get_y"]() - b["get_y"]() * c["get_x"]());
			
			// copy vertex
			poly[j] = b2Vec2.Get(b["get_x"](), b["get_y"]());
		}
		
		areasum = Math.abs(areasum / 2);
		
		// The separation algorithm seems to generate tiny polygons as artefacts.
		// I've no idea why (TODO: find out why).  As a hack, any polygons
		// with an area less than 0.1% the total object area are discarded!
		// (This used to be 1.5%, but complex polys on large objects would incorrectly
		// get filtered.)
		if (areasum &gt;= objarea * 0.001)
			ret.push(poly);
		else
		{
			for (j = 0, lenj = poly.length; j &lt; lenj; j++)
				b2Vec2.Free(poly[j]);
		}
	}
	
	// Since box2d has a limit on 8 points for a convex poly, split any polys with more points in to multiple polys.
	ret = SplitConvexPolysOver8Points(ret);
	
;
	return ret;
};

cr.b2Separator.calcShapes = function(verticesVec /*array of b2Vec2*/)
{
	var vec = [];										// array of b2Vec2
	var i = 0, n = 0, j = 0;							// ints
	var d = 0, t = 0, dx = 0, dy = 0, minLen = 0;		// numbers
	var i1 = 0, i2 = 0, i3 = 0;							// ints
	var p1, p2, p3, v1, v2, v, hitV;					// b2Vec2s
	var j1 = 0, j2 = 0, k = 0, h = 0;					// ints
	var vec1 = [], vec2 = [];							// array of b2Vec2
	var isConvex = false;								// boolean
	var figsVec = [], queue = [];						// Arrays
	var pushed = false;

	queue.push(verticesVec);

	while (queue.length)
	{
		vec = queue[0];
		n = vec.length;
		isConvex = true;

		for (i = 0; i &lt; n; i++)
		{
			i1 = i;
			i2 = (i &lt; n-1) ? i+1 : i+1-n;
			i3 = (i &lt; n-2) ? i+2 : i+2-n;

			p1 = vec[i1];
			p2 = vec[i2];
			p3 = vec[i3];

			d = cr.b2Separator.det(p1["get_x"](), p1["get_y"](), p2["get_x"](), p2["get_y"](), p3["get_x"](), p3["get_y"]());
			
			if (d &lt; 0)
			{
				isConvex = false;
				minLen = 1e9;

				for (j = 0; j &lt; n; j++)
				{
					if ((j !== i1) &amp;&amp; (j !== i2))
					{
						j1 = j;
						j2 = (j&lt;n - 1) ? j+1 : 0;

						v1 = vec[j1];
						v2 = vec[j2];

						v = cr.b2Separator.hitRay(p1["get_x"](), p1["get_y"](), p2["get_x"](), p2["get_y"](), v1["get_x"](), v1["get_y"](), v2["get_x"](), v2["get_y"]());

						if (v)
						{
							dx = p2["get_x"]() - v["get_x"]();
							dy = p2["get_y"]() - v["get_y"]();
							t = dx*dx + dy*dy;

							if (t &lt; minLen)
							{
								h = j1;
								k = j2;
								hitV = v;
								minLen = t;
							}
							else
								b2Vec2.Free(v);
						}
					}
				}

				// invalid poly
				if (minLen === 1e9)
					return [];

				vec1 = [];
				vec2 = [];

				j1 = h;
				j2 = k;
				v1 = vec[j1];
				v2 = vec[j2];

				pushed = false;
				
				if (!cr.b2Separator.pointsMatch(hitV["get_x"](), hitV["get_y"](), v2["get_x"](), v2["get_y"]()))
				{
					vec1.push(hitV);
					pushed = true;
				}
					
				if (!cr.b2Separator.pointsMatch(hitV["get_x"](), hitV["get_y"](), v1["get_x"](), v1["get_y"]()))
				{
					vec2.push(hitV);
					pushed = true;
				}
				
				// Last use of hitV - release if no longer used
				if (!pushed)
					b2Vec2.Free(hitV);

				h = -1;
				k = i1;
				
				while (true)
				{
					if (k !== j2)
						vec1.push(vec[k]);
					else
					{
						// invalid poly
						if (h &lt; 0 || h &gt;= n)
							return [];
							
						if (!cr.b2Separator.isOnSegment(v2["get_x"](), v2["get_y"](), vec[h]["get_x"](), vec[h]["get_y"](), p1["get_x"](), p1["get_y"]()))
							vec1.push(vec[k]);
							
						break;
					}

					h = k;
					
					if (k-1 &lt; 0)
						k = n-1;
					else
						k--;
				}

				vec1.reverse();

				h = -1;
				k = i2;
				
				while (true)
				{
					if (k !== j1)
						vec2.push(vec[k]);
					else
					{
						// invalid poly
						if (h &lt; 0 || h &gt;= n)
							return [];

						if (k === j1 &amp;&amp; !cr.b2Separator.isOnSegment(v1["get_x"](), v1["get_y"](), vec[h]["get_x"](), vec[h]["get_y"](), p2["get_x"](), p2["get_y"]()))
							vec2.push(vec[k]);
							
						break;
					}

					h = k;
					
					if (k+1 &gt; n-1)
						k = 0;
					else
						k++;
				}

				queue.push(vec1, vec2);
				queue.shift();

				break;
			}
		}

		if (isConvex)
			figsVec.push(queue.shift());
	}

	return figsVec;
};

function SplitConvexPolysOver8Points(convexPolys)
{
	var ret = [];
	var i, len, arr;
	
	for (i = 0, len = convexPolys.length; i &lt; len; ++i)
	{
		arr = convexPolys[i];
		
		// &lt;=8 points: can directly use this poly
		if (arr.length &lt;= 8)
		{
			ret.push(arr);
		}
		// &gt;8 points: split up
		else
		{
			ret.push.apply(ret, SplitConvexPoly(arr));
		}
	}
	
	return ret;
}

function SplitConvexPoly(arr)
{
	var poly, nextLast;
	var ret = [];
	
	// At first, take 8 points to make a new poly. For every poly after that, take only 6 more points, and join it to
	// the first and last points to make a new 8-point poly.
	ret.push(arr.splice(0, 8));
	var first = ret[0][0];
	var last = ret[0][7];
	
	while (arr.length)
	{
		poly = arr.splice(0, Math.min(arr.length, 6));
		nextLast = poly[poly.length - 1];
		poly.push(b2Vec2.Clone(first));
		poly.push(b2Vec2.Clone(last));
		ret.push(poly);
		
		last = nextLast;
	}
	
	return ret;
}

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

;
;

/////////////////////////////////////
// Behavior class
cr.behaviors.Physics = function(runtime)
{
	this.runtime = runtime;
	this.world = new Box2D["b2World"](getTempVec2a(0, 10),	// gravity
								 	  true);				// allow sleep
	
	this.worldG = 10;
	this.lastUpdateTick = -1;
	
	// For registering collisions
	var listener = new Box2D["JSContactListener"]();

	listener["BeginContact"] = function (contactPtr) {
		var contact = Box2D["wrapPointer"](contactPtr, Box2D["b2Contact"]);
		var behA = contact["GetFixtureA"]()["GetBody"]().c2userdata;
		var behB = contact["GetFixtureB"]()["GetBody"]().c2userdata;
		runtime.registerCollision(behA.inst, behB.inst);
	};
	listener["EndContact"] = function () {};		// unused
	listener["PreSolve"] = function () {};			// unused
	listener["PostSolve"] = function () {};			// unused

	this.world["SetContactListener"](listener);
	
	// For disabling collisions
	var filter = new Box2D["JSContactFilter"]();
	var self = this;
	
	filter["ShouldCollide"] = function (fixAPtr, fixBPtr) {
		if (self.allCollisionsEnabled)
			return true;
			
		var fixtureA = Box2D["wrapPointer"](fixAPtr, Box2D["b2Fixture"]);
		var fixtureB = Box2D["wrapPointer"](fixBPtr, Box2D["b2Fixture"]);
			
		var typeA = fixtureA["GetBody"]().c2userdata.inst.type;
		var typeB = fixtureB["GetBody"]().c2userdata.inst.type;
		
		var s = typeA.extra["Physics_DisabledCollisions"];
		if (s &amp;&amp; s.contains(typeB))
			return false;
			
		s = typeB.extra["Physics_DisabledCollisions"];
		if (s &amp;&amp; s.contains(typeA))
			return false;
			
		return true;
	};
	
	this.world["SetContactFilter"](filter);
	
	this.steppingMode = 0;		// fixed
	this.velocityIterations = 8;
	this.positionIterations = 3;
	this.allCollisionsEnabled = true;
	
};

(function ()
{
	// Import Box2D names
	var b2BodyDef = Box2D["b2BodyDef"],
		b2Body = Box2D["b2Body"],
		b2FixtureDef = Box2D["b2FixtureDef"],
		b2Fixture = Box2D["b2Fixture"],
		b2World = Box2D["b2World"],
		b2PolygonShape = Box2D["b2PolygonShape"],
		b2CircleShape = Box2D["b2CircleShape"],
		b2DistanceJointDef = Box2D["b2DistanceJointDef"],
		b2RevoluteJointDef = Box2D["b2RevoluteJointDef"];
	
	// From Tilemap
	var TILE_FLIPPED_HORIZONTAL = -0x80000000		// note: pretend is a signed int, so negate
	var TILE_FLIPPED_VERTICAL = 0x40000000
	var TILE_FLIPPED_DIAGONAL = 0x20000000
	var TILE_FLAGS_MASK = 0xE0000000
		
	// box2D apparently works well with sizes ranging 0.1 to 10 - this means our objects
	// should work well over a 5 to 500 pixel range.
	var worldScale = 0.02;
		  
	var behaviorProto = cr.behaviors.Physics.prototype;
		
	/////////////////////////////////////
	// Behavior type class
	behaviorProto.Type = function(behavior, objtype)
	{
		this.behavior = behavior;
		this.objtype = objtype;
		this.runtime = behavior.runtime;
	};
	
	var behtypeProto = behaviorProto.Type.prototype;

	behtypeProto.onCreate = function()
	{
	};

	/////////////////////////////////////
	// Behavior instance class
	behaviorProto.Instance = function(type, inst)
	{
		this.type = type;
		this.behavior = type.behavior;
		this.inst = inst;				// associated object instance to modify
		this.runtime = type.runtime;
		this.world = this.behavior.world;
	};
	
	var behinstProto = behaviorProto.Instance.prototype;

	behinstProto.onCreate = function()
	{
		// Load properties
		this.immovable = this.properties[0];
		this.collisionmask = this.properties[1];
		this.preventRotation = this.properties[2];
		this.density = this.properties[3];
		this.friction = this.properties[4];
		this.restitution = this.properties[5];
		this.linearDamping = this.properties[6];
		this.angularDamping = this.properties[7];
		this.bullet = this.properties[8];
		this.enabled = this.properties[9];
		
		this.body = null;
		this.fixtures = [];
		
		this.inst.update_bbox();
		this.lastKnownX = this.inst.x;
		this.lastKnownY = this.inst.y;
		this.lastKnownAngle = this.inst.angle;
		this.lastWidth = 0;
		this.lastHeight = 0;
		this.lastTickOverride = false;
		this.recreateBody = false;
		
		this.lastAnimation = null;			// for sprites only - will be undefined for other objects
		this.lastAnimationFrame = -1;		// for sprites only - will be undefined for other objects
		
		if (this.myJoints)
		{
			cr.clearArray(this.myJoints);
			cr.clearArray(this.myCreatedJoints);
			this.joiningMe.clear();
		}
		else
		{
			this.myJoints = [];						// Created Box2D joints
			this.myCreatedJoints = [];				// List of actions called to create joints
			this.joiningMe = new cr.ObjectSet();	// Instances with joints to me
		}
		
		//this.myconvexpolys = null;
		
		// Need to know if joint-connected objects get destroyed
		var self = this;
		
		if (!this.recycled)
		{
			this.myDestroyCallback = (function(inst) {
													self.onInstanceDestroyed(inst);
												});
		}
										
		this.runtime.addDestroyCallback(this.myDestroyCallback);
	};
	
	behinstProto.postCreate = function ()
	{
		// Sprite animation frame is now available
		this.inst.update_bbox();
		this.createBody();
		
		this.lastAnimation = this.inst.cur_animation;
		this.lastAnimationFrame = this.inst.cur_frame;
	};
	
	behinstProto.onDestroy = function()
	{
		this.destroyBody();
		
		cr.clearArray(this.myCreatedJoints);	
		this.joiningMe.clear();
		
		this.runtime.removeDestroyCallback(this.myDestroyCallback);
	};
	
	behinstProto.saveToJSON = function ()
	{
		var o = {
			"e": this.enabled,
			"im": this.immovable,
			"pr": this.preventRotation,
			"d": this.density,
			"fr": this.friction,
			"re": this.restitution,
			"ld": this.linearDamping,
			"ad": this.angularDamping,
			"b": this.bullet,
			"mcj": this.myCreatedJoints
		};
		
		if (this.enabled)
		{
			var temp = this.body["GetLinearVelocity"]();
			o["vx"] = temp["get_x"]();
			o["vy"] = temp["get_y"]();
			o["om"] = this.body["GetAngularVelocity"]();
		}
		
		return o;
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.destroyBody();
		
		cr.clearArray(this.myCreatedJoints);
		this.joiningMe.clear();
		
		this.enabled = o["e"];
		this.immovable = o["im"];
		this.preventRotation = o["pr"];
		this.density = o["d"];
		this.friction = o["fr"];
		this.restitution = o["re"];
		this.linearDamping = o["ld"];
		this.angularDamping = o["ad"];
		this.bullet = o["b"];
		
		this.lastKnownX = this.inst.x;
		this.lastKnownY = this.inst.y;
		this.lastKnownAngle = this.inst.angle;
		this.lastWidth = this.inst.width;
		this.lastHeight = this.inst.height;
		
		if (this.enabled)
		{
			this.createBody();
			
			this.body["SetLinearVelocity"](getTempVec2a(o["vx"], o["vy"]));
			this.body["SetAngularVelocity"](o["om"]);
			
			if (o["vx"] !== 0 || o["vy"] !== 0 || o["om"] !== 0)
				this.body["SetAwake"](true);
			
			this.myCreatedJoints = o["mcj"];
		}
	};
	
	behinstProto.afterLoad = function ()
	{
		if (this.enabled)
			this.recreateMyJoints();
	};
	
	behinstProto.onInstanceDestroyed = function (inst)
	{
		// Remove any joints referencing the destroyed instance
		var i, len, j, instuid = inst.uid;
		for (i = 0, j = 0, len = this.myCreatedJoints.length; i &lt; len; i++)
		{
			this.myCreatedJoints[j] = this.myCreatedJoints[i];
			
			// Sometimes myJoints is empty if this is happening during load. Don't fill it
			// with undefined values.
			if (j &lt; this.myJoints.length)
				this.myJoints[j] = this.myJoints[i];
			
			if (this.myCreatedJoints[i].params[1] == instuid)		// attached instance is always 2nd param
			{
				if (i &lt; this.myJoints.length)						// myJoints can already be empty in some cases
					this.world["DestroyJoint"](this.myJoints[i]);
			}
			else
				j++;
		}
		
		// Forget about any instance joining on to me if the joining instance was destroyed
		this.myCreatedJoints.length = j;
		
		if (j &lt; this.myJoints.length)
			this.myJoints.length = j;
		
		this.joiningMe.remove(inst);
	};
	
	behinstProto.destroyMyJoints = function()
	{
		var i, len;
		for (i = 0, len = this.myJoints.length; i &lt; len; i++)
			this.world["DestroyJoint"](this.myJoints[i]);
			
		cr.clearArray(this.myJoints);
	};
	
	behinstProto.recreateMyJoints = function()
	{
		var i, len, j;
		for (i = 0, len = this.myCreatedJoints.length; i &lt; len; i++)
		{
			j = this.myCreatedJoints[i];
			
			switch (j.type) {
			case 0:			// distance joint
				this.doCreateDistanceJoint(j.params[0], j.params[1], j.params[2], j.params[3], j.params[4]);
				break;
			case 1:			// revolute joint
				this.doCreateRevoluteJoint(j.params[0], j.params[1]);
				break;
			case 2:			// limited revolute joint
				this.doCreateLimitedRevoluteJoint(j.params[0], j.params[1], j.params[2], j.params[3]);
				break;
			default:
;
			}
		}
	};
	
	behinstProto.createFixture = function (fixDef)
	{
		if (!this.body)
			return;
		
		var fixture = this.body["CreateFixture"](fixDef);
		this.fixtures.push(fixture);
		return fixture;
	};
	
	behinstProto.destroyFixtures = function ()
	{
		if (!this.body)
			return;
		
		var i, len;
		for (i = 0, len = this.fixtures.length; i &lt; len; ++i)
			this.body["DestroyFixture"](this.fixtures[i]);
		
		cr.clearArray(this.fixtures);
	};
	
	behinstProto.destroyBody = function()
	{
		if (!this.body)
			return;
		
		// Destroy the body and all joints, which will be recreated later
		this.destroyMyJoints();
		
		this.destroyFixtures();
		this.world["DestroyBody"](this.body);
		this.body = null;
		this.inst.extra.box2dbody = null;
	};
	
	var collrects = [];
	
	behinstProto.createBody = function()
	{
		if (!this.enabled)
			return;
		
		var inst = this.inst;
		inst.update_bbox();
		var i, len, j, lenj, k, lenk, vec, arr, b, tv, c, rc, pts_cache, pts_count, convexpolys, cp, offx, offy, oldAngle;
		
		// Create the physics body if we don't have one yet.
		if (!this.body)
		{
			var bodyDef = new b2BodyDef();
			bodyDef["set_type"](this.immovable ? 0 : 2);		// 0 = b2_staticBody, 2 = b2_dynamicBody
			
			// Body expects position to be in center, but the object hotspot may not be centred.
			// Determine the actual object center.  The easiest way to do this is take its bounding
			// quad and get its mid point.
			bodyDef["set_position"](getTempVec2b(inst.bquad.midX() * worldScale, inst.bquad.midY() * worldScale));
			bodyDef["set_angle"](inst.angle);
			
			bodyDef["set_fixedRotation"](this.preventRotation);
			bodyDef["set_linearDamping"](this.linearDamping);
			bodyDef["set_angularDamping"](this.angularDamping);
			bodyDef["set_bullet"](this.bullet);
			
			// Create the body - no fixtures attached yet
			this.body = this.world["CreateBody"](bodyDef);
			this.body.c2userdata = this;
			inst.extra.box2dbody = this.body;
			
			Box2D["destroy"](bodyDef);
		}
		
		// If we already have fixtures, destroy them. This method is also called to update the fixture on the body, e.g. when an object changes size.
		this.destroyFixtures();
		
		var fixDef = new b2FixtureDef();
		fixDef["set_density"](this.density);
		fixDef["set_friction"](this.friction);
		fixDef["set_restitution"](this.restitution);
		
		// If 'use collision poly' set, but no collision poly present, switch to bounding box
		var hasPoly = this.inst.collision_poly &amp;&amp; !this.inst.collision_poly.is_empty();
		var usecollisionmask = this.collisionmask;
		
		if (!hasPoly &amp;&amp; !this.inst.tilemap_exists &amp;&amp; this.collisionmask === 0)
			usecollisionmask = 1;
			
		// Zero size objects crash Box2D
		var instw = Math.max(Math.abs(inst.width), 1);
		var insth = Math.max(Math.abs(inst.height), 1);
		var ismirrored = inst.width &lt; 0;
		var isflipped = inst.height &lt; 0;
		var shape;
		
		// use collision poly
		if (usecollisionmask === 0)
		{
			// Is tilemap object: handle separately
			if (inst.tilemap_exists)
			{
				offx = inst.bquad.midX() - inst.x;
				offy = inst.bquad.midY() - inst.y;
				inst.getAllCollisionRects(collrects);
				
				arr = [];
				
				for (i = 0, len = collrects.length; i &lt; len; ++i)
				{
					c = collrects[i];
					rc = c.rc;
					
					// Tile has collision polygon
					if (c.poly)
					{
						// Lazily cache in to convexpolys so we don't keep doing the expensive separate work
						// for every tile in the tilemap
						if (!c.poly.convexpolys)
						{
							pts_cache = c.poly.pts_cache;
							pts_count = c.poly.pts_count;
							
							// convert to array of b2Vec for the separator
							for (j = 0; j &lt; pts_count; ++j)
							{
								arr.push(b2Vec2.Get(pts_cache[j*2], pts_cache[j*2+1]));
							}
							
							// mirrored or flipped: must reverse to keep clockwise points order
							var flags = (c.id &amp; TILE_FLAGS_MASK);
							
							if (flags === TILE_FLIPPED_HORIZONTAL || flags === TILE_FLIPPED_VERTICAL || flags === TILE_FLIPPED_DIAGONAL ||
								((flags &amp; TILE_FLIPPED_HORIZONTAL) &amp;&amp; (flags &amp; TILE_FLIPPED_VERTICAL) &amp;&amp; (flags &amp; TILE_FLIPPED_DIAGONAL)))
							{
								arr.reverse();
							}
							
							// Run the separator to split to convex polys
							c.poly.convexpolys = cr.b2Separator.Separate(arr, (rc.right - rc.left) * (rc.bottom - rc.top));
							
							for (j = 0, lenj = arr.length; j &lt; lenj; ++j)
								b2Vec2.Free(arr[j]);
							
							cr.clearArray(arr);
						}
						
						// Now we have the tile-relative convex polys in c.poly.convexpolys.
						// We still need to offset then scale the result.
						for (j = 0, lenj = c.poly.convexpolys.length; j &lt; lenj; ++j)
						{
							cp = c.poly.convexpolys[j];
;
							
							for (k = 0, lenk = cp.length; k &lt; lenk; ++k)
							{
								arr.push(b2Vec2.Get((rc.left + cp[k]["get_x"]() - offx) * worldScale, (rc.top + cp[k]["get_y"]() - offy) * worldScale));
							}
							
							shape = createPolygonShape(arr);
							fixDef["set_shape"](shape);
							this.createFixture(fixDef);
							
							Box2D["destroy"](shape);
							
							for (k = 0, lenk = arr.length; k &lt; lenk; ++k)
								b2Vec2.Free(arr[k]);
							
							cr.clearArray(arr);
						}
					}
					else
					{
						// Bounding box collision for this tile
						arr.push(b2Vec2.Get((rc.left - offx) * worldScale, (rc.top - offy) * worldScale));
						arr.push(b2Vec2.Get((rc.right - offx) * worldScale, (rc.top - offy) * worldScale));
						arr.push(b2Vec2.Get((rc.right - offx) * worldScale, (rc.bottom - offy) * worldScale));
						arr.push(b2Vec2.Get((rc.left - offx) * worldScale, (rc.bottom - offy) * worldScale));
					
						shape = createPolygonShape(arr);
						fixDef["set_shape"](shape);
						this.createFixture(fixDef);
						
						Box2D["destroy"](shape);
					}
					
					for (j = 0, lenj = arr.length; j &lt; lenj; ++j)
						b2Vec2.Free(arr[j]);
					
					cr.clearArray(arr);
				}
			}
			else
			{
				// offset of poly from hotspot. poly has to be generated unrotated
				oldAngle = inst.angle;
				inst.angle = 0;
				inst.set_bbox_changed();
				inst.update_bbox();
				offx = inst.bquad.midX() - inst.x;
				offy = inst.bquad.midY() - inst.y;
				inst.angle = oldAngle;
				inst.set_bbox_changed();
				
				// cache the poly to get vertices at pixel scale relative to object's origin
				// don't rotate the poly for box2D, it rotates it itself
				inst.collision_poly.cache_poly(ismirrored ? -instw : instw, isflipped ? -insth : insth, 0);
				
				// convert to array of b2Vec for the separator
				pts_cache = inst.collision_poly.pts_cache;
				pts_count = inst.collision_poly.pts_count;
				arr = [];
				arr.length = pts_count;
				
				for (i = 0; i &lt; pts_count; i++)
				{
					// offset so the poly is relative to body origin.  Don't scale yet - the separator
					// works with fractional pixel values, scaling down will throw that off
					arr[i] = b2Vec2.Get(pts_cache[i*2] - offx, pts_cache[i*2+1] - offy);
				}
				
				if (ismirrored !== isflipped)
					arr.reverse();		// wrong clockwise order when flipped
				
				// Run the separator to split to convex polys
				convexpolys = cr.b2Separator.Separate(arr, instw * insth);
				//this.myconvexpolys = convexpolys;
				
				for (i = 0; i &lt; pts_count; i++)
					b2Vec2.Free(arr[i]);
				
				if (convexpolys.length)
				{
					// Add each convex poly as a fixture
					for (i = 0, len = convexpolys.length; i &lt; len; i++)
					{
						arr = convexpolys[i];
;
						
						// Scale down each poly point
						for (j = 0, lenj = arr.length; j &lt; lenj; j++)
						{
							vec = arr[j];
							vec["set_x"](vec["get_x"]() * worldScale);
							vec["set_y"](vec["get_y"]() * worldScale);
						}
						
						shape = createPolygonShape(arr);
						fixDef["set_shape"](shape);
						this.createFixture(fixDef);
						
						Box2D["destroy"](shape);
						
						// recycle vec2s
						for (j = 0, lenj = arr.length; j &lt; lenj; j++)
							b2Vec2.Free(arr[j]);
					}
				}
				// invalid poly: use bounding box
				else
				{
					shape = new b2PolygonShape();
					shape["SetAsBox"](instw * worldScale * 0.5, insth * worldScale * 0.5);
					fixDef["set_shape"](shape);
					this.createFixture(fixDef);
					Box2D["destroy"](shape);
				}
			}
		}
		// bounding box
		else if (usecollisionmask === 1)
		{
			shape = new b2PolygonShape();
			shape["SetAsBox"](instw * worldScale * 0.5, insth * worldScale * 0.5);
			fixDef["set_shape"](shape);
			this.createFixture(fixDef);
			Box2D["destroy"](shape);
		}
		// circle (2)
		else
		{
			shape = new b2CircleShape();
			shape["set_m_radius"](Math.min(instw, insth) * worldScale * 0.5);
			fixDef["set_shape"](shape);
			this.createFixture(fixDef);
			Box2D["destroy"](shape);
		}
		
		this.lastWidth = inst.width;
		this.lastHeight = inst.height;
		
		Box2D["destroy"](fixDef);
		
		cr.clearArray(collrects);
	};

	// Debug: draw polys
	/*
	behinstProto.draw = function (ctx)
	{
		if (!this.myconvexpolys)
			return;
			
		this.inst.update_bbox();
		var midx = this.inst.bquad.midX();
		var midy = this.inst.bquad.midY();
		var i, len, j, lenj;
		
		var sina = 0;
		var cosa = 1;
		
		if (this.inst.angle !== 0)
		{
			sina = Math.sin(this.inst.angle);
			cosa = Math.cos(this.inst.angle);
		}
		
		var strokeStyles = ["#f00", "#0f0", "#00f", "#ff0", "#0ff", "#f0f"];
		ctx.lineWidth = 2;
		
		var i, len, j, lenj, ax, ay, bx, by, poly, va, vb;
		for (i = 0, len = this.myconvexpolys.length; i &lt; len; i++)
		{
			poly = this.myconvexpolys[i];
			ctx.strokeStyle = strokeStyles[i];
			
			for (j = 0, lenj = poly.length; j &lt; lenj; j++)
			{
				va = poly[j];
				vb = poly[(j + 1) % lenj];
				
				ax = va.x / worldScale;
				ay = va.y / worldScale;
				bx = vb.x / worldScale;
				by = vb.y / worldScale;
				
				ctx.beginPath();
				ctx.moveTo(((ax * cosa) - (ay * sina)) + midx, ((ay * cosa) + (ax * sina)) + midy);
				ctx.lineTo(((bx * cosa) - (by * sina)) + midx, ((by * cosa) + (bx * sina)) + midy);
				ctx.stroke();
				ctx.closePath();
			}
		}
	};
	*/
	
	behinstProto.tick = function ()
	{
		if (!this.enabled)
			return;
		
		var inst = this.inst;
		var dt;
		if (this.behavior.steppingMode === 0)		// fixed
		{
			dt = this.runtime.timescale / 60;
		}
		else
		{
			dt = this.runtime.getDt(this.inst);
			
			// Cap step at 30 FPS, otherwise instability can result
			if (dt &gt; 1 / 30)
				dt = 1 / 30;
		}
		
		// A new step is necessary.
		// Don't step at all if the game is paused (timescale is zero).
		if (this.runtime.tickcount_nosave &gt; this.behavior.lastUpdateTick &amp;&amp; this.runtime.timescale &gt; 0)
		{
			
			if (dt !== 0)
			{
				this.world["Step"](dt, this.behavior.velocityIterations, this.behavior.positionIterations);		// still apply timescale
			}
			
			this.world["ClearForces"]();
			
			
			this.behavior.lastUpdateTick = this.runtime.tickcount_nosave;
		}
		
		// Size, body, animation frame or tilemap has has changed: recreate body
		if (this.recreateBody || inst.width !== this.lastWidth || inst.height !== this.lastHeight
			|| inst.cur_animation !== this.lastAnimation || inst.cur_frame !== this.lastAnimationFrame
			|| (inst.tilemap_exists &amp;&amp; inst.physics_changed))
		{
			this.createBody();
			this.recreateBody = false;
			this.lastAnimation = inst.cur_animation;
			this.lastAnimationFrame = inst.cur_frame;
			
			if (inst.tilemap_exists &amp;&amp; inst.physics_changed)
				inst.physics_changed = false;
		}
		
		// Something has changed the object (an event or other behavior): update the body
		var pos_changed = (inst.x !== this.lastKnownX || inst.y !== this.lastKnownY);
		var angle_changed = (inst.angle !== this.lastKnownAngle);
		
		if (pos_changed)
		{
			inst.update_bbox();
			var newmidx = inst.bquad.midX();
			var newmidy = inst.bquad.midY();
			var diffx = newmidx - this.lastKnownX;
			var diffy = newmidy - this.lastKnownY;

			if (angle_changed)
				this.body["SetTransform"](getTempVec2a(newmidx * worldScale, newmidy * worldScale), inst.angle);
			else
				this.body["SetTransform"](getTempVec2a(newmidx * worldScale, newmidy * worldScale), this.body["GetAngle"]());
			
			this.body["SetLinearVelocity"](getTempVec2a(diffx, diffy));
			this.lastTickOverride = true;
			this.body["SetAwake"](true);
		}
		// clean up residual velocity if something else is controlling the object and it has now stopped
		else if (this.lastTickOverride)
		{
			this.lastTickOverride = false;
			this.body["SetLinearVelocity"](getTempVec2a(0, 0));
			this.body["SetTransform"](getTempVec2a(inst.bquad.midX() * worldScale, inst.bquad.midY() * worldScale), this.body["GetAngle"]());
		}
		
		if (!pos_changed &amp;&amp; angle_changed)
		{
			this.body["SetTransform"](this.body["GetPosition"](), inst.angle);
			this.body["SetAwake"](true);
		}
		
		// Update position and angle of the object from the body
		var pos = this.body["GetPosition"]();
		var newx = pos["get_x"]() / worldScale;
		var newy = pos["get_y"]() / worldScale;
		var newangle = this.body["GetAngle"]();
		
		if (newx !== inst.x || newy !== inst.y || newangle !== inst.angle)
		{
			inst.x = newx;
			inst.y = newy;
			inst.angle = newangle;
			inst.set_bbox_changed();
			
			// The body position is the midpoint of the object, but the hotspot might not be
			// at the mid point.  Calculate the new mid-point of the object, and offset the
			// instance's x and y position by that much.
			inst.update_bbox();
			var dx = inst.bquad.midX() - inst.x;
			var dy = inst.bquad.midY() - inst.y;
			
			if (dx !== 0 || dy !== 0)
			{
				inst.x -= dx;
				inst.y -= dy;
				inst.set_bbox_changed();
			}
		}
		
		this.lastKnownX = inst.x;
		this.lastKnownY = inst.y;
		this.lastKnownAngle = inst.angle;
	};
	
	behinstProto.getInstImgPointX = function(imgpt)
	{
		if (imgpt === -1 || !this.inst.getImagePoint)
			return this.inst.x;
			
		// use center of mass instead of origin
		if (imgpt === 0 &amp;&amp; this.body)
			return (this.body["GetPosition"]()["get_x"]() + this.body["GetLocalCenter"]()["get_x"]()) / worldScale;
		
		return this.inst.getImagePoint(imgpt, true);
	};
	
	behinstProto.getInstImgPointY = function(imgpt)
	{
		if (imgpt === -1 || !this.inst.getImagePoint)
			return this.inst.y;
			
		// use center of mass instead of origin
		if (imgpt === 0 &amp;&amp; this.body)
			return (this.body["GetPosition"]()["get_y"]() + this.body["GetLocalCenter"]()["get_y"]()) / worldScale;
		
		return this.inst.getImagePoint(imgpt, false);
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.IsSleeping = function ()
	{
		if (!this.enabled)
			return false;
		
		return !this.body["IsAwake"]();
	};
	
	Cnds.prototype.CompareVelocity = function (which_, cmp_, x_)
	{
		if (!this.enabled)
			return false;
		
		var velocity_vec = this.body["GetLinearVelocity"]();
		var v, vx, vy;
		
		if (which_ === 0)		// X velocity
			v = velocity_vec["get_x"]() / worldScale;
		else if (which_ === 1)	// Y velocity
			v = velocity_vec["get_y"]() / worldScale;
		else					// Overall velocity
		{
			vx = velocity_vec["get_x"]() / worldScale;
			vy = velocity_vec["get_y"]() / worldScale;
			v = cr.distanceTo(0, 0, vx, vy);
		}
		
		return cr.do_cmp(v, cmp_, x_);
	};
	
	Cnds.prototype.CompareAngularVelocity = function (cmp_, x_)
	{
		if (!this.enabled)
			return false;
		
		var av = cr.to_degrees(this.body["GetAngularVelocity"]());
		return cr.do_cmp(av, cmp_, x_);
	};
	
	Cnds.prototype.CompareMass = function (cmp_, x_)
	{
		if (!this.enabled)
			return false;
		
		var mass = this.body["GetMass"]() / worldScale;
		return cr.do_cmp(mass, cmp_, x_);
	};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.enabled;
	};
	
	behaviorProto.cnds = new Cnds();

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

	Acts.prototype.ApplyForce = function (fx, fy, imgpt)
	{
		if (!this.enabled)
			return;
		
		var x = this.getInstImgPointX(imgpt);
		var y = this.getInstImgPointY(imgpt);
		this.body["ApplyForce"](getTempVec2a(fx, fy), getTempVec2b(x * worldScale, y * worldScale), true);
	};
	
	Acts.prototype.ApplyForceToward = function (f, px, py, imgpt)
	{
		if (!this.enabled)
			return;
		
		var x = this.getInstImgPointX(imgpt);
		var y = this.getInstImgPointY(imgpt);
		var a = cr.angleTo(x, y, px, py);
		this.body["ApplyForce"](getTempVec2a(Math.cos(a) * f, Math.sin(a) * f), getTempVec2b(x * worldScale, y * worldScale), true);
	};
	
	Acts.prototype.ApplyForceAtAngle = function (f, a, imgpt)
	{
		if (!this.enabled)
			return;
		
		a = cr.to_radians(a);
		var x = this.getInstImgPointX(imgpt);
		var y = this.getInstImgPointY(imgpt);		
		this.body["ApplyForce"](getTempVec2a(Math.cos(a) * f, Math.sin(a) * f), getTempVec2b(x * worldScale, y * worldScale), true);
	};
	
	Acts.prototype.ApplyImpulse = function (fx, fy, imgpt)
	{
		if (!this.enabled)
			return;
		
		var x = this.getInstImgPointX(imgpt);
		var y = this.getInstImgPointY(imgpt);
		this.body["ApplyLinearImpulse"](getTempVec2a(fx, fy), getTempVec2b(x * worldScale, y * worldScale), true);
		
		// Disable velocity overrides from using 'set position'
		this.lastTickOverride = false;
		this.lastKnownX = this.inst.x;
		this.lastKnownY = this.inst.y;
	};
	
	Acts.prototype.ApplyImpulseToward = function (f, px, py, imgpt)
	{
		if (!this.enabled)
			return;
		
		var x = this.getInstImgPointX(imgpt);
		var y = this.getInstImgPointY(imgpt);
		var a = cr.angleTo(x, y, px, py);
		this.body["ApplyLinearImpulse"](getTempVec2a(Math.cos(a) * f, Math.sin(a) * f), getTempVec2b(x * worldScale, y * worldScale), true);
		
		// Disable velocity overrides from using 'set position'
		this.lastTickOverride = false;
		this.lastKnownX = this.inst.x;
		this.lastKnownY = this.inst.y;
	};
	
	Acts.prototype.ApplyImpulseAtAngle = function (f, a, imgpt)
	{
		if (!this.enabled)
			return;
		
		a = cr.to_radians(a);
		var x = this.getInstImgPointX(imgpt);
		var y = this.getInstImgPointY(imgpt);		
		this.body["ApplyLinearImpulse"](getTempVec2a(Math.cos(a) * f, Math.sin(a) * f), getTempVec2b(x * worldScale, y * worldScale), true);
		
		// Disable velocity overrides from using 'set position'
		this.lastTickOverride = false;
		this.lastKnownX = this.inst.x;
		this.lastKnownY = this.inst.y;
	};
	
	Acts.prototype.ApplyTorque = function (m)
	{
		if (!this.enabled)
			return;
		
		this.body["ApplyTorque"](cr.to_radians(m), true);
	};
	
	Acts.prototype.ApplyTorqueToAngle = function (m, a)
	{
		if (!this.enabled)
			return;
		
		m = cr.to_radians(m);
		a = cr.to_radians(a);
		
		// instance is clockwise of angle to apply torque toward: apply reverse torque
		if (cr.angleClockwise(this.inst.angle, a))
			this.body["ApplyTorque"](-m, true);
		else
			this.body["ApplyTorque"](m, true);
	};
	
	Acts.prototype.ApplyTorqueToPosition = function (m, x, y)
	{
		if (!this.enabled)
			return;
		
		m = cr.to_radians(m);
		var a = cr.angleTo(this.inst.x, this.inst.y, x, y);
		
		// instance is clockwise of angle to apply torque toward: apply reverse torque
		if (cr.angleClockwise(this.inst.angle, a))
			this.body["ApplyTorque"](-m, true);
		else
			this.body["ApplyTorque"](m, true);
	};
	
	Acts.prototype.SetAngularVelocity = function (v)
	{
		if (!this.enabled)
			return;
		
		this.body["SetAngularVelocity"](cr.to_radians(v));
		this.body["SetAwake"](true);
	};
	
	Acts.prototype.CreateDistanceJoint = function (imgpt, obj, objimgpt, damping, freq)
	{
		if (!obj || !this.enabled)
			return;
			
		var otherinst = obj.getFirstPicked(this.inst);
		
		if (!otherinst || otherinst == this.inst)
			return;
		if (!otherinst.extra.box2dbody)
			return;		// no physics behavior on other object
		
		this.myCreatedJoints.push({type: 0, params: [imgpt, otherinst.uid, objimgpt, damping, freq]});
		this.doCreateDistanceJoint(imgpt, otherinst.uid, objimgpt, damping, freq);
	};
	
	behinstProto.doCreateDistanceJoint = function (imgpt, otherinstuid, objimgpt, damping, freq)
	{
		if (!this.enabled)
			return;
		
		var otherinst = this.runtime.getObjectByUID(otherinstuid);
		
		if (!otherinst || otherinst == this.inst || !otherinst.extra.box2dbody)
			return;
			
		otherinst.extra.box2dbody.c2userdata.joiningMe.add(this.inst);
		
		var myx = this.getInstImgPointX(imgpt);
		var myy = this.getInstImgPointY(imgpt);
		var theirx, theiry;
		
		if (otherinst.getImagePoint)
		{
			theirx = otherinst.getImagePoint(objimgpt, true);
			theiry = otherinst.getImagePoint(objimgpt, false);
		}
		else
		{
			theirx = otherinst.x;
			theiry = otherinst.y;
		}
		
		var dx = myx - theirx;
		var dy = myy - theiry;
		
		var jointDef = new b2DistanceJointDef();
		jointDef["Initialize"](this.body, otherinst.extra.box2dbody, getTempVec2a(myx * worldScale, myy * worldScale), getTempVec2b(theirx * worldScale, theiry * worldScale));
		jointDef["set_length"](Math.sqrt(dx*dx + dy*dy) * worldScale);
		jointDef["set_dampingRatio"](damping);
		jointDef["set_frequencyHz"](freq);
		this.myJoints.push(this.world["CreateJoint"](jointDef));
		Box2D["destroy"](jointDef);
	};
	
	Acts.prototype.CreateRevoluteJoint = function (imgpt, obj)
	{
		if (!obj || !this.enabled)
			return;
			
		var otherinst = obj.getFirstPicked(this.inst);
		
		if (!otherinst || otherinst == this.inst)
			return;
		if (!otherinst.extra.box2dbody)
			return;		// no physics behavior on other object
		
		this.myCreatedJoints.push({type: 1, params: [imgpt, otherinst.uid]});
		this.doCreateRevoluteJoint(imgpt, otherinst.uid);
	};
	
	behinstProto.doCreateRevoluteJoint = function (imgpt, otherinstuid)
	{
		if (!this.enabled)
			return;
		
		var otherinst = this.runtime.getObjectByUID(otherinstuid);
		
		if (!otherinst || otherinst == this.inst || !otherinst.extra.box2dbody)
			return;
			
		otherinst.extra.box2dbody.c2userdata.joiningMe.add(this.inst);
		
		var myx = this.getInstImgPointX(imgpt);
		var myy = this.getInstImgPointY(imgpt);
		
		var jointDef = new b2RevoluteJointDef();
		jointDef["Initialize"](this.body, otherinst.extra.box2dbody, getTempVec2a(myx * worldScale, myy * worldScale));
		this.myJoints.push(this.world["CreateJoint"](jointDef));
		Box2D["destroy"](jointDef);
	};
	
	Acts.prototype.CreateLimitedRevoluteJoint = function (imgpt, obj, lower, upper)
	{
		if (!obj || !this.enabled)
			return;
			
		var otherinst = obj.getFirstPicked(this.inst);
		
		if (!otherinst || otherinst == this.inst)
			return;
		if (!otherinst.extra.box2dbody)
			return;		// no physics behavior on other object
		
		this.myCreatedJoints.push({type: 2, params: [imgpt, otherinst.uid, lower, upper]});
		this.doCreateLimitedRevoluteJoint(imgpt, otherinst.uid, lower, upper);
	};
	
	behinstProto.doCreateLimitedRevoluteJoint = function (imgpt, otherinstuid, lower, upper)
	{
		if (!this.enabled)
			return;
		
		var otherinst = this.runtime.getObjectByUID(otherinstuid);
		
		if (!otherinst || otherinst == this.inst || !otherinst.extra.box2dbody)
			return;
			
		otherinst.extra.box2dbody.c2userdata.joiningMe.add(this.inst);
		
		var myx = this.getInstImgPointX(imgpt);
		var myy = this.getInstImgPointY(imgpt);
		
		var jointDef = new b2RevoluteJointDef();
		jointDef["Initialize"](this.body, otherinst.extra.box2dbody, getTempVec2a(myx * worldScale, myy * worldScale));
		jointDef["set_enableLimit"](true);
		jointDef["set_lowerAngle"](cr.to_radians(lower));
		jointDef["set_upperAngle"](cr.to_radians(upper));
		this.myJoints.push(this.world["CreateJoint"](jointDef));
		Box2D["destroy"](jointDef);
	};
	
	Acts.prototype.SetWorldGravity = function (g)
	{
		if (g === this.behavior.worldG)
			return;
		
		this.world["SetGravity"](getTempVec2a(0, g));
		this.behavior.worldG = g;
		
		// Wake up every physics instance
		var i, len, arr = this.behavior.my_instances.valuesRef();
		
		for (i = 0, len = arr.length; i &lt; len; i++)
		{
			if (arr[i].extra.box2dbody)
				arr[i].extra.box2dbody["SetAwake"](true);
		}
	};
	
	Acts.prototype.SetSteppingMode = function (mode)
	{
		this.behavior.steppingMode = mode;
	};
	
	Acts.prototype.SetIterations = function (vel, pos)
	{
		if (vel &lt; 1) vel = 1;
		if (pos &lt; 1) pos = 1;
		
		this.behavior.velocityIterations = vel;
		this.behavior.positionIterations = pos;
	};
	
	Acts.prototype.SetVelocity = function (vx, vy)
	{
		if (!this.enabled)
			return;
		
		this.body["SetLinearVelocity"](getTempVec2a(vx * worldScale, vy * worldScale));
		this.body["SetAwake"](true);
		
		// Disable velocity overrides from using 'set position'
		this.lastTickOverride = false;
		this.lastKnownX = this.inst.x;
		this.lastKnownY = this.inst.y;
	};
	
	Acts.prototype.SetDensity = function (d)
	{
		if (!this.enabled)
			return;
		
		if (this.density === d)
			return;
			
		this.density = d;
		
		var i, len;
		for (i = 0, len = this.fixtures.length; i &lt; len; ++i)
			this.fixtures[i]["SetDensity"](d);
		
		this.body["ResetMassData"]();
	};
	
	Acts.prototype.SetFriction = function (f)
	{
		if (!this.enabled)
			return;
		
		if (this.friction === f)
			return;
			
		this.friction = f;
		
		var i, len;
		for (i = 0, len = this.fixtures.length; i &lt; len; ++i)
			this.fixtures[i]["SetFriction"](f);
		
		// Update friction for all existing contacts
		// NOTE: looks like a bug in Box2D binding here - GetContactList returns an actual b2ContactEdge, not a list, and
		// it has to be traversed via get_next().
		var contactEdge, contact;

		for (contactEdge = this.body["GetContactList"](); Box2D["getPointer"](contactEdge); contactEdge = contactEdge["get_next"]())
		{
			var contact = contactEdge["get_contact"]();
			
			if (contact)
				contact["ResetFriction"]();
		}
	};
	
	Acts.prototype.SetElasticity = function (e)
	{
		if (!this.enabled)
			return;
		
		if (this.restitution === e)
			return;
			
		this.restitution = e;
		
		var i, len;
		for (i = 0, len = this.fixtures.length; i &lt; len; ++i)
			this.fixtures[i]["SetRestitution"](e);
	};
	
	Acts.prototype.SetLinearDamping = function (ld)
	{
		if (!this.enabled)
			return;
		
		if (this.linearDamping === ld)
			return;
			
		this.linearDamping = ld;
		this.body["SetLinearDamping"](ld);
	};
	
	Acts.prototype.SetAngularDamping = function (ad)
	{
		if (!this.enabled)
			return;
		
		if (this.angularDamping === ad)
			return;
			
		this.angularDamping = ad;
		this.body["SetAngularDamping"](ad);
	};
	
	Acts.prototype.SetImmovable = function (i)
	{
		if (!this.enabled)
			return;
		
		if (this.immovable === (i !== 0))
			return;
			
		this.immovable = (i !== 0);
		this.body["SetType"](this.immovable ? 0 /*b2BodyDef.b2_staticBody*/ : 2 /*b2BodyDef.b2_dynamicBody*/);
		this.body["SetAwake"](true);
	};
	
	function SetCollisionsEnabled(typeA, typeB, state)
	{
		var s;
		
		// Enable collisions between A and B
		if (state)
		{
			s = typeA.extra["Physics_DisabledCollisions"];
			
			if (s)
				s.remove(typeB);
				
			s = typeB.extra["Physics_DisabledCollisions"];
			
			if (s)
				s.remove(typeA);
		}
		// Disable collisions between A and B
		else
		{
			if (!typeA.extra["Physics_DisabledCollisions"])
				typeA.extra["Physics_DisabledCollisions"] = new cr.ObjectSet();
				
			typeA.extra["Physics_DisabledCollisions"].add(typeB);
			
			if (!typeB.extra["Physics_DisabledCollisions"])
				typeB.extra["Physics_DisabledCollisions"] = new cr.ObjectSet();
				
			typeB.extra["Physics_DisabledCollisions"].add(typeA);
		}
	};
	
	Acts.prototype.EnableCollisions = function (obj, state)
	{
		if (!obj || !this.enabled)
			return;
			
		var i, len;
			
		// If passed a family, set the collisions enabled for the members directly instead of on the family		
		if (obj.is_family)
		{
			for (i = 0, len = obj.members.length; i &lt; len; i++)
			{
				SetCollisionsEnabled(this.inst.type, obj.members[i], state !== 0);
			}
		}
		else
		{
			SetCollisionsEnabled(this.inst.type, obj, state !== 0);
		}
		
		// Turn off the fast response to ShouldCollide optimisation
		this.behavior.allCollisionsEnabled = false;
	};
	
	Acts.prototype.SetPreventRotate = function (i)
	{
		if (!this.enabled)
			return;
		
		if (this.preventRotation === (i !== 0))
			return;
			
		this.preventRotation = (i !== 0);
		this.body["SetFixedRotation"](this.preventRotation);
		this.body["SetAngularVelocity"](0);
		this.body["SetAwake"](true);
	};
	
	Acts.prototype.SetBullet = function (i)
	{
		if (!this.enabled)
			return;
		
		if (this.bullet === (i !== 0))
			return;
			
		this.bullet = (i !== 0);
		this.body["SetBullet"](this.bullet);
		this.body["SetAwake"](true);
	};
	
	Acts.prototype.RemoveJoints = function ()
	{
		if (!this.enabled)
			return;
		
		this.destroyMyJoints();	
		cr.clearArray(this.myCreatedJoints);	
		this.joiningMe.clear();
	};
	
	Acts.prototype.SetEnabled = function (e)
	{
		// Is enabled, and setting disabled
		if (this.enabled &amp;&amp; e === 0)
		{
			this.destroyBody();
			this.enabled = false;
		}
		// Is disabled, and setting enabled
		else if (!this.enabled &amp;&amp; e === 1)
		{
			this.enabled = true;
			this.createBody();
		}
	};
	
	behaviorProto.acts = new Acts();

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

	Exps.prototype.VelocityX = function (ret)
	{
		ret.set_float(this.enabled ? this.body["GetLinearVelocity"]()["get_x"]() / worldScale : 0);
	};
	
	Exps.prototype.VelocityY = function (ret)
	{
		ret.set_float(this.enabled ? this.body["GetLinearVelocity"]()["get_y"]() / worldScale : 0);
	};
	
	Exps.prototype.AngularVelocity = function (ret)
	{
		ret.set_float(this.enabled ? cr.to_degrees(this.body["GetAngularVelocity"]()) : 0);
	};
	
	Exps.prototype.Mass = function (ret)
	{
		ret.set_float(this.enabled ? this.body["GetMass"]() / worldScale : 0);
	};
	
	Exps.prototype.CenterOfMassX = function (ret)
	{
		ret.set_float(this.enabled ? (this.body["GetPosition"]()["get_x"]() + this.body["GetLocalCenter"]()["get_x"]()) / worldScale : 0);
	};
	
	Exps.prototype.CenterOfMassY = function (ret)
	{
		ret.set_float(this.enabled ? (this.body["GetPosition"]()["get_y"]() + this.body["GetLocalCenter"]()["get_y"]()) / worldScale : 0);
	};
	
	Exps.prototype.Density = function (ret)
	{
		ret.set_float(this.enabled ? this.density : 0);
	};
	
	Exps.prototype.Friction = function (ret)
	{
		ret.set_float(this.enabled ? this.friction : 0);
	};
	
	Exps.prototype.Elasticity = function (ret)
	{
		ret.set_float(this.enabled ? this.restitution : 0);
	};
	
	Exps.prototype.LinearDamping = function (ret)
	{
		ret.set_float(this.enabled ? this.linearDamping : 0);
	};
	
	Exps.prototype.AngularDamping = function (ret)
	{
		ret.set_float(this.enabled ? this.angularDamping : 0);
	};
	
	behaviorProto.exps = new Exps();
	
}());

cr.getObjectRefTable = function () {
	return [
		cr.plugins_.Touch,
		cr.plugins_.Browser,
		cr.plugins_.Audio,
		cr.plugins_.Mouse,
		cr.plugins_.WebStorage,
		cr.plugins_.Sprite,
		cr.behaviors.Bullet,
		cr.plugins_.Spritefont2,
		cr.behaviors.Fade,
		cr.behaviors.Sin,
		cr.behaviors.Physics,
		cr.plugins_.GM_SDK,
		cr.system_object.prototype.cnds.OnLayoutStart,
		cr.system_object.prototype.acts.SetVar,
		cr.plugins_.Sprite.prototype.acts.SetPos,
		cr.plugins_.Spritefont2.prototype.acts.SetPos,
		cr.plugins_.Sprite.prototype.acts.SetVisible,
		cr.plugins_.WebStorage.prototype.cnds.LocalStorageExists,
		cr.plugins_.WebStorage.prototype.exps.LocalValue,
		cr.plugins_.Spritefont2.prototype.acts.SetText,
		cr.plugins_.GM_SDK.prototype.cnds.PauseGame,
		cr.plugins_.GM_SDK.prototype.acts.ShowAd,
		cr.system_object.prototype.cnds.IsGroupActive,
		cr.system_object.prototype.cnds.Every,
		cr.system_object.prototype.acts.CreateObject,
		cr.system_object.prototype.exps.random,
		cr.plugins_.Touch.prototype.cnds.OnTouchObject,
		cr.plugins_.Sprite.prototype.acts.Spawn,
		cr.plugins_.Sprite.prototype.acts.SetAngle,
		cr.plugins_.Sprite.prototype.acts.Destroy,
		cr.system_object.prototype.acts.AddVar,
		cr.behaviors.Fade.prototype.acts.StartFade,
		cr.system_object.prototype.acts.SubVar,
		cr.plugins_.Sprite.prototype.acts.SetHeight,
		cr.plugins_.Sprite.prototype.exps.Height,
		cr.system_object.prototype.cnds.EveryTick,
		cr.plugins_.Sprite.prototype.acts.SetWidth,
		cr.system_object.prototype.cnds.CompareVar,
		cr.system_object.prototype.acts.SetGroupActive,
		cr.plugins_.WebStorage.prototype.acts.StoreLocal,
		cr.behaviors.Fade.prototype.cnds.OnFadeOutEnd,
		cr.system_object.prototype.acts.GoToLayout,
		cr.plugins_.Browser.prototype.acts.GoToURL,
		cr.system_object.prototype.acts.SetTimescale,
		cr.system_object.prototype.acts.RestartLayout,
		cr.plugins_.Audio.prototype.acts.Play,
		cr.plugins_.Sprite.prototype.cnds.OnCollision,
		cr.plugins_.Sprite.prototype.acts.SetAnim,
		cr.system_object.prototype.cnds.Else,
		cr.plugins_.Audio.prototype.acts.Stop,
		cr.plugins_.Mouse.prototype.cnds.IsButtonDown,
		cr.plugins_.Mouse.prototype.acts.SetCursorSprite,
		cr.system_object.prototype.exps.round,
		cr.system_object.prototype.exps.loadingprogress,
		cr.system_object.prototype.cnds.OnLoadFinished,
		cr.plugins_.Audio.prototype.acts.Preload
	];
};
	self.C3_JsPropNameTable = [
		{Touch: 0},
		{Browser: 0},
		{Audio: 0},
		{Mouse: 0},
		{WebStorage: 0},
		{Bullet: 0},
		{Balloon1: 0},
		{Balloon2: 0},
		{Balloon3: 0},
		{BackGround: 0},
		{Balloon4: 0},
		{Balloon5: 0},
		{Balloon6: 0},
		{MiniBalloon1: 0},
		{MiniBalloon2_1: 0},
		{MiniBalloon3: 0},
		{MiniBalloon4: 0},
		{MiniBalloon2_2: 0},
		{MiniBalloon2_3: 0},
		{MiniBalloon5: 0},
		{MiniBalloon6: 0},
		{Balloon1Font: 0},
		{Balloon5Font: 0},
		{Balloon3Font: 0},
		{Balloon4Font: 0},
		{Balloon6Font: 0},
		{ScoreFont: 0},
		{HighScoreFont: 0},
		{LastScoreFont: 0},
		{Fade: 0},
		{Balloon1Crash: 0},
		{Balloon2Crash: 0},
		{Balloon3Crash: 0},
		{Balloon4Crash: 0},
		{Balloon5Crash: 0},
		{Balloon6Crash: 0},
		{HighScoreGui: 0},
		{Paused: 0},
		{Pause: 0},
		{MoreGames: 0},
		{Sine: 0},
		{Play: 0},
		{BestScore: 0},
		{Sun: 0},
		{BestScoreFont: 0},
		{WellDone: 0},
		{Restart: 0},
		{Menu: 0},
		{Physics: 0},
		{Collision: 0},
		{Sounds: 0},
		{Loading: 0},
		{LoadingBar: 0},
		{LoadingBarBackground: 0},
		{LoadingFont: 0},
		{Cursor: 0},
		{CursorHover: 0},
		{Balloons: 0},
		{ScoreGui: 0},
		{LastScoreGui: 0},
		{MiniBalloon2_0: 0},
		{GameMonetizeSDK: 0},
		{Score: 0},
		{Sound: 0},
		{Balloon: 0},
		{LastScore: 0},
		{HighScore: 0}
	];

</pre></body></html>