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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return a;
	};

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

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

		return a;
	};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if (this.locSamplerFront)
			gl.uniform1i(this.locSamplerFront, 0);
			
		if (this.locSamplerBack)
			gl.uniform1i(this.locSamplerBack, 1);
		
		if (this.locSrcStart)
			gl.uniform2f(this.locSrcStart, 0.0, 0.0);
		
		if (this.locSrcEnd)
			gl.uniform2f(this.locSrcEnd, 1.0, 1.0);
		
		if (this.locSrcOriginStart)
			gl.uniform2f(this.locSrcOriginStart, 0.0, 0.0);
		
		if (this.locSrcOriginEnd)
			gl.uniform2f(this.locSrcOriginEnd, 0.0, 0.0);
	
		if (this.locLayoutStart)
			gl.uniform2f(this.locLayoutStart, 0.0, 0.0);
		
		if (this.locLayoutEnd)
			gl.uniform2f(this.locLayoutEnd, 0.0, 0.0);
		
		if (this.locDestStart)
			gl.uniform2f(this.locDestStart, 0.0, 0.0);
			
		if (this.locDestEnd)
			gl.uniform2f(this.locDestEnd, 1.0, 1.0);
			
		if (this.locLayerScale)
			gl.uniform1f(this.locLayerScale, 1.0);
		
		if (this.locLayerAngle)
			gl.uniform1f(this.locLayerAngle, 0.0);
		
		if (this.locSeconds)
			gl.uniform1f(this.locSeconds, 0.0);
		
		this.hasCurrentMatMV = false;		// matMV needs updating
	};
	
	function areMat4sEqual(a, b)
	{
		return a[0]===b[0]&&a[1]===b[1]&&a[2]===b[2]&&a[3]===b[3]&&
			   a[4]===b[4]&&a[5]===b[5]&&a[6]===b[6]&&a[7]===b[7]&&
			   a[8]===b[8]&&a[9]===b[9]&&a[10]===b[10]&&a[11]===b[11]&&
			   a[12]===b[12]&&a[13]===b[13]&&a[14]===b[14]&&a[15]===b[15];
	};
	
	GLShaderProgram.prototype.updateMatMV = function (mv)
	{
		if (areMat4sEqual(this.lpMatMV, mv))
			return;		// no change, save the expensive GL call
		
		mat4.set(mv, this.lpMatMV);
		this.gl.uniformMatrix4fv(this.locMatMV, false, mv);
	};
	
	GLWrap_.prototype.createShaderProgram = function(shaderEntry, vsSource, name)
	{
		var gl = this.gl;
		
		// Create fragment shader
		var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
		gl.shaderSource(fragmentShader, shaderEntry.src);
		gl.compileShader(fragmentShader);
		
		if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS))
		{
			// Failed to compile fragment shader
			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 < len; i++)
		{
			ret.parameters[i][1] = gl.getUniformLocation(shaderProgram, ret.parameters[i][0]);
			ret.lastCustomParams.push(0);
			
			// Note does not correctly handle color parameters which are vec3. Leave this and assume uniforms are zero-initialised.
			//gl.uniform1f(ret.parameters[i][1], 0);
		}
		
		cr.seal(ret);
		return ret;
	};
	
	GLWrap_.prototype.getShaderIndex = function(name_)
	{
		var i, len;
		for (i = 0, len = this.shaderPrograms.length; i < len; i++)
		{
			if (this.shaderPrograms[i].name === name_)
				return i;
		}
		
		return -1;
	};
	
	GLWrap_.prototype.project = function (x, y, out)
	{
		var mv = this.matMV;
		var proj = this.matP;
		
		// Based on http://www.opengl.org/wiki/GluProject_and_gluUnProject_code
		var fTempo = [0, 0, 0, 0, 0, 0, 0, 0];
		
		fTempo[0] = mv[0]*x+mv[4]*y+mv[12];
		fTempo[1] = mv[1]*x+mv[5]*y+mv[13];
		fTempo[2] = mv[2]*x+mv[6]*y+mv[14];
		fTempo[3] = mv[3]*x+mv[7]*y+mv[15];

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

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

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

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

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

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

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

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

		this.timerResults.push(o);

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

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

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

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

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

		this.isTiming = false;
	}

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

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

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

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

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

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

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

					o.query = null;
				}
			}
		}
	};

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

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

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

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

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

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

		// Otherwise return final sum
		return sum;
	};

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

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

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

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

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

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

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

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

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

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

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

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

}());

// 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" && window["c2isCrosswalk"]);
		
		this.isCordova = (this.exportType === "cordova");
		
		// for backwards compatibility with any plugins checking for the old isPhoneGap variable
		this.isPhoneGap = this.isCordova;
		
		// Detect the platform that we're running on by looking at the user agent string.
		// Note IE uses MSIE (<IE11), Trident (IE11), or IEMobile (Windows Phone).
		this.isMicrosoftEdge = /edge\//i.test(navigator.userAgent);
		this.isIE = (/msie/i.test(navigator.userAgent) || /trident/i.test(navigator.userAgent) || /iemobile/i.test(navigator.userAgent)) && !this.isMicrosoftEdge;
		this.isTizen = /tizen/i.test(navigator.userAgent);
		this.isAndroid = /android/i.test(navigator.userAgent) && !this.isTizen && !this.isIE && !this.isMicrosoftEdge;		// IE mobile and Tizen masquerade as Android
		this.isiPhone = (/iphone/i.test(navigator.userAgent) || /ipod/i.test(navigator.userAgent)) && !this.isIE && !this.isMicrosoftEdge;	// treat ipod as an iphone; IE mobile masquerades as iPhone
		this.isiPad = /ipad/i.test(navigator.userAgent);
		this.isiOS = this.isiPhone || this.isiPad;
		this.isChrome = (/chrome/i.test(navigator.userAgent) || /chromium/i.test(navigator.userAgent)) && !this.isIE && !this.isMicrosoftEdge;	// note true on Chromium-based webview on Android 4.4+; IE 'Edge' mode also pretends to be Chrome
		this.isAmazonWebApp = /amazonwebappplatform/i.test(navigator.userAgent);
		this.isFirefox = /firefox/i.test(navigator.userAgent);
		this.isSafari = /safari/i.test(navigator.userAgent) && !this.isChrome && !this.isIE && !this.isMicrosoftEdge;		// Chrome and IE Mobile masquerade as Safari
		this.isWindows = /windows/i.test(navigator.userAgent);
		this.isNWjs = (this.exportType === "nwjs" || /nodewebkit/i.test(navigator.userAgent) || /nwjs/i.test(navigator.userAgent));
		this.isNodeWebkit = this.isNWjs;		// old name for backwards compat
		this.isArcade = (this.exportType === "scirra-arcade");
		this.isWindows8App = !!(typeof window["c2isWindows8"] !== "undefined" && window["c2isWindows8"]);
		this.isWindowsPhone8 = !!(typeof window["c2isWindowsPhone8"] !== "undefined" && window["c2isWindowsPhone8"]);
		this.isWindowsPhone81 = !!(typeof window["c2isWindowsPhone81"] !== "undefined" && window["c2isWindowsPhone81"]);
		this.isWindows10 = (this.exportType === "windows-uwp");
		this.isWinJS = (this.isWindows8App || this.isWindowsPhone81 || this.isWindows10);	// note not WP8.0
		this.isBlackberry10 = !!(typeof window["c2isBlackberry10"] !== "undefined" && window["c2isBlackberry10"]);
		this.isAndroidStockBrowser = (this.isAndroid && !this.isChrome && !this.isCrosswalk && !this.isFirefox && !this.isAmazonWebApp);
		this.devicePixelRatio = 1;
		
		// Determine if running on a mobile: always true in mobile wrapper
		this.isMobile = (this.isCordova || this.isCrosswalk || this.isAndroid || this.isiOS || this.isWindowsPhone8 || this.isWindowsPhone81 || this.isBlackberry10 || this.isTizen);
		
		// Check for some other common mobile manufacturers
		if (!this.isMobile)
		{
			this.isMobile = /(blackberry|bb10|playbook|palm|symbian|nokia|windows\s+ce|phone|mobile|tablet|kindle|silk)/i.test(navigator.userAgent);
		}
		
		// Identify WKWebView. C3 no longer supports UIWebView so we can assume Cordova on iOS is always WKWebView.
		this.isWKWebView = !!(this.isiOS && this.isCordova);
		
		// NW.js detects as Chrome in preview mode. To work around this, allow a ?nw query string
		// to force it to detect as NW.js, or check for "nodewebkit" or "nwjs" in the user agent.
		if (this.isPreview && !this.isNWjs && (window.location.search === "?nw" || /nodewebkit/i.test(navigator.userAgent) || /nwjs/i.test(navigator.userAgent)))
		{
			this.isNWjs = true;
		}
		
		this.isDebug = (this.isPreview && window.location.search.indexOf("debug") > -1);
		
		// 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" && !this.isPreview && !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"] && window["nwgui"]["App"]["clearCache"])
				window["nwgui"]["App"]["clearCache"]();
		}

		if (this.isCordova && this.isiOS)
		{
			function IsInContentEditable(el)
			{
				do {
					// Don't check hasAttribute on the root document node (the method does not exist)
					if (el.parentNode && 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") > -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 > 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" && 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) && ("responseType" in xhr))
		{
			try {
				xhr["responseType"] = "json";
				supportsJsonResponse = (xhr["responseType"] === "json");
			}
			catch (e) {
				supportsJsonResponse = false;
			}
		}
		
		// If JSON not supported, try to force text mode instead
		if (!supportsJsonResponse && ("responseType" in xhr))
		{
			try {
				xhr["responseType"] = "text";
			}
			catch (e) {}
		}
		
		// Who knows what the server MIME type/charset configuration is, so try to force the browser to
		// correctly interpret the response as UTF-8 JSON data
		if ("overrideMimeType" in xhr)
		{
			try {
				xhr["overrideMimeType"]("application/json; charset=utf-8");
			}
			catch (e) {}
		}
		
		if (this.isWindowsPhone8)
		{
			// WP8 ActiveX doesn't support normal onload/onerror - use onreadystatechange instead.
			// Note WP8 doesn't support json response so skip that.
			xhr.onreadystatechange = function ()
			{
				if (xhr.readyState !== 4)
					return;

				self.loadProject(JSON.parse(xhr["responseText"]));
			};
		}
		else
		{
			xhr.onload = function ()
			{
				if (supportsJsonResponse)
				{
					self.loadProject(xhr["response"]);					// already parsed by browser
				}
				else
				{
					self.loadProject(JSON.parse(xhr["responseText"]));	// forced to sync parse JSON
				}
			};
			
			xhr.onerror = function (e)
			{
				cr.logerror("Error requesting " + datajs_filename + ":");
				cr.logerror(e);
			};
		}
		
		xhr.send();
	};
	
	// JSON data is loaded; init renderer and kick off asset loader
	Runtime.prototype.initRendererAndLoader = function ()
	{
		var self = this;
		var i, len, j, lenj, k, lenk, t, s, l, y;
		
		// Enable hi-dpi support (called Retina mode for legacy reasons). Note Android stock browser
		// reports devicePixelRatio incorrectly so disable on there.
		this.isRetina = (this.useHighDpi && !this.isAndroidStockBrowser);
		
		// iOS 8 bug: Safari apparently tries to auto-scale WebGL canvases, and does it wrong, so we can't size high-DPI non-fullscreen
		// canvases properly. To work around this, in non-fullscreen mode on iOS, disable retina mode (high-DPI display).
		if (this.fullscreen_mode === 0 && this.isiOS)
			this.isRetina = false;

		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 > 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 < len; i++)
			{
				t = this.types_by_index[i];
				
				for (j = 0, lenj = t.effect_types.length; j < lenj; j++)
				{
					s = t.effect_types[j];
					s.shaderindex = this.glwrap.getShaderIndex(s.id);
					s.preservesOpaqueness = this.glwrap.programPreservesOpaqueness(s.shaderindex);
					this.uses_background_blending = this.uses_background_blending || this.glwrap.programUsesDest(s.shaderindex);
				}
			}
			
			// Look up all layout shader indices
			for (i = 0, len = this.layouts_by_index.length; i < len; i++)
			{
				l = this.layouts_by_index[i];
				
				for (j = 0, lenj = l.effect_types.length; j < lenj; j++)
				{
					s = l.effect_types[j];
					s.shaderindex = this.glwrap.getShaderIndex(s.id);
					s.preservesOpaqueness = this.glwrap.programPreservesOpaqueness(s.shaderindex);
				}
				
				l.updateActiveEffects();		// update preserves opaqueness flag
				
				// Look up all layer shader indices
				for (j = 0, lenj = l.layers.length; j < lenj; j++)
				{
					y = l.layers[j];
					
					for (k = 0, lenk = y.effect_types.length; k < lenk; k++)
					{
						s = y.effect_types[k];
						s.shaderindex = this.glwrap.getShaderIndex(s.id);
						s.preservesOpaqueness = this.glwrap.programPreservesOpaqueness(s.shaderindex);
						this.uses_background_blending = this.uses_background_blending || this.glwrap.programUsesDest(s.shaderindex);
					}
					
					y.updateActiveEffects();		// update preserves opaqueness flag
				}
			}
		}
		else
		{
;
			
			attribs = {
				"alpha": true
			};
			this.ctx = this.canvas.getContext("2d", attribs);
			
			// Set image smoothing according to the project property
			this.ctx.imageSmoothingEnabled = this.linearSampling;
			
			this.overlay_canvas = null;
			this.overlay_ctx = null;
			
			// Mark GPU usage unavailable since measurements require WebGL
			this.gpuLastUtilisation = NaN;
			
			// 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 && !this.isWinJS && !this.isWindowsPhone8)
		{
			document.addEventListener("mousedown", function () {
				window.focus();
			}, true);
			document.addEventListener("touchstart", function () {
				window.focus();
			}, true);
		}
		
		// Preview-only code
		if (this.isPreview)
		{
			// Load continuous preview
			if (window.location.search.indexOf("continuous") > -1)
			{
				cr.logexport("Reloading for continuous preview");
				this.loadFromSlot = "__c2_continuouspreview";
				this.suspendDrawing = true;
			}
			
			// Pause on blur (unfocus)
			if (this.pauseOnBlur && !this.isMobile)
			{
				window.addEventListener("focus", function ()
				{
					self["setSuspended"](false);
				});
				
				window.addEventListener("blur", function ()
				{
					// In debug mode, if the debug frame gets focus then this frame gets a blur event.
					// Don't let that cause suspending - we only want to suspend if neither this frame
					// nor the debugger have focus.
					var parent = window.parent;
					
					if (!parent || !parent.document.hasFocus())
						self["setSuspended"](true);
				});
			}
		}
		
		// Inform all plugins and behaviors of blur events so they can reset any keyboard key flags
		window.addEventListener("blur", function () {
			self.onWindowBlur();
		});
		
		// Unfocus form controls when a canvas touch event is made or mouse clicks in space.
		var unfocusFormControlFunc = function (e) {
			// Block middle-clicks creating the pan cursor
			if (e.type === "mousedown" && e.button === 1)
				e.preventDefault();
			
			// IE9 quirk: document.activeElement can point to the body tag, and calling blur() on the body
			// tag sends the window in to the background, which is annoying. Make sure blur is not called
			// on the body element.
			if (cr.isCanvasInputEvent(e) && document["activeElement"] && document["activeElement"] !== document.getElementsByTagName("body")[0] && document["activeElement"].blur)
			{
				// IE11 sometimes throws here with "incorrect function", which is kind of mysterious,
				// so just ignore that.
				try {
					document["activeElement"].blur();
				}
				catch (e) {}
			}
		}
		
		if (typeof PointerEvent !== "undefined")
		{
			document.addEventListener("pointerdown", unfocusFormControlFunc);
		}
		else if (window.navigator["msPointerEnabled"])
		{
			document.addEventListener("MSPointerDown", unfocusFormControlFunc);
		}
		else
		{
			document.addEventListener("touchstart", unfocusFormControlFunc);
		}
		
		document.addEventListener("mousedown", unfocusFormControlFunc);
		
		// Non-fullscreen games on retina displays never call setSize to enable hi-dpi display.
		// Do this now if the device has hi-dpi support.
		if (this.fullscreen_mode === 0 && this.isRetina && this.devicePixelRatio > 1)
		{
			this["setSize"](this.original_width, this.original_height, true);
		}
		else
		{
			// Otherwise update the size for the overlay canvas.
			this["setSize"](window.innerWidth, window.innerHeight, true);
		}
		
		this.tryLockOrientation();
		
		this.getready();	// determine things to preload
		this.go();			// run loading screen
		
		this.extra = {};
		cr.seal(this);
	};
	
	Runtime.prototype["setSize"] = function (w, h, force)
	{
		var offx = 0, offy = 0;
		var neww = 0, newh = 0, intscale = 0;
		
		// Ignore redundant events
		if (this.lastWindowWidth === w && this.lastWindowHeight === h && !force)
			return;

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

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

			var isPortrait = window.innerWidth < window.innerHeight;

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

			if (height && 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"]) && !this.isCordova;
		
		if (!isfullscreen && this.fullscreen_mode === 0 && !force)
			return;			// ignore size events when not fullscreen and not using a fullscreen-in-browser mode
		
		if (isfullscreen)
			mode = this.fullscreen_scaling;
		
		var dpr = this.devicePixelRatio;
		
		// Letterbox or letterbox integer scale modes: adjust width and height and offset canvas accordingly
		if (mode >= 4)
		{
			// HACK: window inner size may have been rounded down by browser.
			if (mode === 5 && 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 > orig_aspect)
			{
				neww = h * orig_aspect;
				
				if (mode === 5)	// integer scaling
				{
					// integer scale by device pixels, not CSS pixels, since DPR may be non-integral
					intscale = (neww * dpr) / this.original_width;
					if (intscale > 1)
						intscale = Math.floor(intscale);
					else if (intscale < 1)
						intscale = 1 / Math.ceil(1 / intscale);
					neww = this.original_width * intscale / dpr;
					newh = this.original_height * intscale / dpr;
					offx = (w - neww) / 2;
					offy = (h - newh) / 2;
					w = neww;
					h = newh;
				}
				else
				{
					offx = (w - neww) / 2;
					w = neww;
				}
			}
			// otherwise scale to fit width
			else
			{
				newh = w / orig_aspect;
				
				if (mode === 5)	// integer scaling
				{
					intscale = (newh * dpr) / this.original_height;
					if (intscale > 1)
						intscale = Math.floor(intscale);
					else if (intscale < 1)
						intscale = 1 / Math.ceil(1 / intscale);
					neww = this.original_width * intscale / dpr;
					newh = this.original_height * intscale / dpr;
					offx = (w - neww) / 2;
					offy = (h - newh) / 2;
					w = neww;
					h = newh;
				}
				else
				{
					offy = (h - newh) / 2;
					h = newh;
				}
			}
		}
		// Centered mode in fullscreen: keep canvas size the same and just center it
		else if (isfullscreen && 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 < 2)
			this.aspect_scale = dpr;
		
		// hacks for iOS retina
		this.cssWidth = Math.round(w);
		this.cssHeight = Math.round(h);
		this.width = Math.round(w * dpr);
		this.height = Math.round(h * dpr);
		this.redraw = true;
		
		if (this.wantFullscreenScalingQuality)
		{
			this.draw_width = this.width;
			this.draw_height = this.height;
			this.fullscreenScalingQuality = true;
		}
		else
		{
			// Render directly even in low-res scale mode if the display area is smaller than the window size area,
			// or in crop mode (since no engine scaling happens)
			if ((this.width < this.original_width && this.height < this.original_height) || mode === 1)
			{
				this.draw_width = this.width;
				this.draw_height = this.height;
				this.fullscreenScalingQuality = true;
			}
			else
			{
				this.draw_width = this.original_width;
				this.draw_height = this.original_height;
				this.fullscreenScalingQuality = false;
				
				/*var orig_aspect = this.original_width / this.original_height;
				var cur_aspect = this.width / this.height;
				
				// note mode 2 (scale inner) inverts this logic and will use window width when width wider.
				if ((this.fullscreen_mode !== 2 && cur_aspect > orig_aspect) || (this.fullscreen_mode === 2 && cur_aspect < orig_aspect))
					this.aspect_scale = this.height / this.original_height;
				else
					this.aspect_scale = this.width / this.original_width;*/
				
				// Scale inner or scale outer mode: adjust the draw size to be proportional
				// to the window size, since the draw size is simply stretched-to-fit in the window
				if (mode === 2)		// scale inner
				{
					orig_aspect = this.original_width / this.original_height;
					cur_aspect = this.lastWindowWidth / this.lastWindowHeight;
					
					if (cur_aspect < orig_aspect)
						this.draw_width = this.draw_height * cur_aspect;
					else if (cur_aspect > orig_aspect)
						this.draw_height = this.draw_width / cur_aspect;
				}
				else if (mode === 3)
				{
					orig_aspect = this.original_width / this.original_height;
					cur_aspect = this.lastWindowWidth / this.lastWindowHeight;
					
					if (cur_aspect > orig_aspect)
						this.draw_width = this.draw_height * cur_aspect;
					else if (cur_aspect < orig_aspect)
						this.draw_height = this.draw_width / cur_aspect;
				}
			}
		}
		
		if (this.canvas)
		{
			this.canvas.width = Math.round(w * dpr);
			this.canvas.height = Math.round(h * dpr);
			
			if (this.isRetina)
			{
				this.canvas.style.width = Math.round(w) + "px";
				this.canvas.style.height = Math.round(h) + "px";
			}
			
			this.canvas.style.marginLeft = Math.floor(offx) + "px";
			this.canvas.style.marginTop = Math.floor(offy) + "px";
		}
		
		if (this.overlay_canvas)
		{
			this.overlay_canvas.width = Math.round(w * dpr);
			this.overlay_canvas.height = Math.round(h * dpr);
			
			this.overlay_canvas.style.width = this.cssWidth + "px";
			this.overlay_canvas.style.height = this.cssHeight + "px";
			
			this.overlay_canvas.style.left = Math.floor(offx) + "px";
			this.overlay_canvas.style.top = Math.floor(offy) + "px";
		}

		if (this.glwrap)
		{
			this.glwrap.setSize(Math.round(w * dpr), Math.round(h * dpr));
		}
		
		if (this.ctx)
		{
			// Re-apply the image smoothing property, since resizing the canvas resets its state
			this.ctx.imageSmoothingEnabled = this.linearSampling;
		}
		
		// Try to lock orientation to the project setting
		this.tryLockOrientation();
		
		// Work around iPhone landscape orientation weirdness. In some cases Safari on iPhone in landscape will
		// bring the address bar down over the page with a scroll offset. This means that despite reporting the
		// correct viewport size and getting the right canvas size, it appears offset under the address bar with
		// an empty space beneath. Forcing the scroll position back to 0 appears to correct this.
		if (this.isiPhone && !this.isCordova)
		{
			window.scrollTo(0, 0);
		}
	};
	
	Runtime.prototype.tryLockOrientation = function ()
	{
		if (!this.autoLockOrientation || this.orientations === 0)
			return;
		
		var orientation = "portrait";
		
		if (this.orientations === 2)
			orientation = "landscape";
		
		// Note IE/Edge can throw exceptions here if in an iframe (WrongDocumentError), which also affects the debugger.
		try {
			// latest API
			if (screen["orientation"] && screen["orientation"]["lock"])
				screen["orientation"]["lock"](orientation).catch(function(){});
			// legacy method names
			else if (screen["lockOrientation"])
				screen["lockOrientation"](orientation);
			else if (screen["webkitLockOrientation"])
				screen["webkitLockOrientation"](orientation);
			else if (screen["mozLockOrientation"])
				screen["mozLockOrientation"](orientation);
			else if (screen["msLockOrientation"])
				screen["msLockOrientation"](orientation);
		}
		catch (e)
		{
			if (console && console.warn)
				console.warn("Failed to lock orientation: ", e);
		}
	};
	
	Runtime.prototype.onContextLost = function ()
	{
		this.glwrap.contextLost();
		
		this.is_WebGL_context_lost = true;
		
		var i, len, t;
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onLostWebGLContext)
				t.onLostWebGLContext();
		}
	};
	
	Runtime.prototype.onContextRestored = function ()
	{
		this.is_WebGL_context_lost = false;
		
		// Recreate all object textures
		var i, len, t;
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onRestoreWebGLContext)
				t.onRestoreWebGLContext();
		}
	};
	
	var caf = window["cancelAnimationFrame"] ||
	  window["mozCancelAnimationFrame"]    ||
	  window["webkitCancelAnimationFrame"] ||
	  window["msCancelAnimationFrame"]     ||
	  window["oCancelAnimationFrame"];
	
	Runtime.prototype["setSuspended"] = function (s)
	{
		var i, len;
		var self = this;
		
		if (s && !this.isSuspended)
		{
			cr.logexport("[Construct 2] Suspending");
			this.isSuspended = true;			// next tick will be last
			
			if (this.raf_id !== -1)
				caf(this.raf_id);
			if (this.timeout_id !== -1)
				clearTimeout(this.timeout_id);
			
			for (i = 0, len = this.suspend_events.length; i < len; i++)
				this.suspend_events[i](true);
				
		}
		else if (!s && this.isSuspended)
		{
			cr.logexport("[Construct 2] Resuming");
			this.isSuspended = false;
			this.last_tick_time = cr.performance_now();	// ensure first tick is a zero-dt one
			this.last_fps_time = cr.performance_now();	// reset FPS counter
			this.framecount = 0;
			this.logictime = 0;
			
			for (i = 0, len = this.suspend_events.length; i < len; i++)
				this.suspend_events[i](false);
				
			
			this.tick(false);						// kick off runtime again
		}
	};
	
	Runtime.prototype.addSuspendCallback = function (f)
	{
		this.suspend_events.push(f);
	};
	
	Runtime.prototype.GetObjectReference = function (i)
	{
;
		return this.objectRefTable[i];
	};
	
	// Load the runtime scripts and data
	Runtime.prototype.loadProject = function (data_response)
	{
;
		if (!data_response || !data_response["project"])
			cr.logerror("Project model unavailable");
		
		var pm = data_response["project"];
		
		this.name = pm[0];
		this.first_layout = pm[1];
		
		// Determine fullscreen mode - needed before object creation
		this.fullscreen_mode = pm[12];	// 0 = off, 1 = crop, 2 = scale inner, 3 = scale outer, 4 = letterbox scale, 5 = integer letterbox scale
		this.fullscreen_mode_set = pm[12];
		
		this.original_width = pm[10];
		this.original_height = pm[11];
		
		this.parallax_x_origin = this.original_width / 2;
		this.parallax_y_origin = this.original_height / 2;
		
		// Determine loader layout - also needed before object creation
		this.uses_loader_layout = pm[18];
		
		// Determine loader style - need to start loading logo before anything else if it's to be shown
		this.loaderstyle = pm[19];
		this.iconsFolder = pm[28];

		// 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 < len; i++)
		{
			m = pm[2][i];
			p = this.GetObjectReference(m[0]);
;
			
			// Add common ACEs for the plugin
			cr.add_common_aces(m, p.prototype);

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

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

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

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

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

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

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

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

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

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

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

				type_inst.behaviors.push(behavior_type);
			}
			
			// Global setting
			type_inst.global = m[9];
			
			// Is on loader layout
			type_inst.isOnLoaderLayout = m[10];
			
			// Assign shaders
			type_inst.effect_types = [];
			
			for (j = 0, lenj = m[12].length; j < lenj; j++)
			{
				type_inst.effect_types.push({
					id: m[12][j][0],
					name: m[12][j][1],
					shaderindex: -1,
					preservesOpaqueness: false,
					active: true,
					index: j
				});
			}
			
			// Store tilemap collision polys (if any)
			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 < len; i++)
		{
			var familydata = pm[4][i];
			var familytype = this.types_by_index[familydata[0]];
			var familymember;
			
			for (j = 1, lenj = familydata.length; j < lenj; j++)
			{
				familymember = this.types_by_index[familydata[j]];
				familymember.families.push(familytype);
				familytype.members.push(familymember);
			}
		}
		
		// Assemble containers
		for (i = 0, len = pm[27].length; i < len; i++)
		{
			var containerdata = pm[27][i];
			var containertypes = [];
			
			for (j = 0, lenj = containerdata.length; j < lenj; j++)
				containertypes.push(this.types_by_index[containerdata[j]]);
			
			for (j = 0, lenj = containertypes.length; j < lenj; j++)
			{
				containertypes[j].is_contained = true;
				containertypes[j].container = containertypes;
			}
		}
		
		// Map instance variables, behaviors and effects for families
		if (this.family_count > 0)
		{
			for (i = 0, len = this.types_by_index.length; i < len; i++)
			{
				t = this.types_by_index[i];
				
				if (t.is_family || !t.families.length)
					continue;
					
				t.family_var_map = new Array(this.family_count);
				t.family_beh_map = new Array(this.family_count);
				t.family_fx_map = new Array(this.family_count);
				var all_fx = [];
					
				var varsum = 0;
				var behsum = 0;
				var fxsum = 0;
				
				for (j = 0, lenj = t.families.length; j < lenj; j++)
				{
					f = t.families[j];
					t.family_var_map[f.family_index] = varsum;
					varsum += f.vars_count;
					t.family_beh_map[f.family_index] = behsum;
					behsum += f.behs_count;
					t.family_fx_map[f.family_index] = fxsum;
					fxsum += f.fx_count;
					
					// Build list of all effect types including inherited for this object type.
					// Make shallow copies so we can set the correct index for each type
					for (k = 0, lenk = f.effect_types.length; k < lenk; k++)
						all_fx.push(cr.shallowCopy({}, f.effect_types[k]));
				}
				
				// Update effect types array to include inherited
				t.effect_types = all_fx.concat(t.effect_types);
				
				for (j = 0, lenj = t.effect_types.length; j < lenj; j++)
					t.effect_types[j].index = j;
			}
		}

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

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

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

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

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

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

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

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

			totalsize += filesize;

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

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

		return ret;
	};
	
	var isC3SplashDone = false;

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

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

		// Wait for any pending textures or audio to finish loading then forward to go_loading_finished
		if (this.areAllTexturesAndSoundsLoaded() && (this.loaderstyle !== 4 || isC3SplashDone))
		{
			// 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 & logo; 1 = progress bar only; 2 = percentage text; 3 = nothing
			var ms_elapsed = Date.now() - this.start_time;

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

			// Call again after 100ms
			setTimeout(function() { self.go(); }, 100);
		}
	};
	
	var splashStartTime = -1;
	var splashFadeInDuration = 300;
	var splashFadeOutDuration = 300;
	var splashIsFadeIn = true;
	var splashIsFadeOut = false;
	var splashFadeInFinish = 0;
	var splashFadeOutStart = 0;
	var renderViaCanvas = null;
	var renderViaCtx = null;
	var splashFrameNumber = 0;
	
	function maybeCreateRenderViaCanvas(w, h)
	{
		if (!renderViaCanvas || renderViaCanvas.width !== w || renderViaCanvas.height !== h)
		{
			renderViaCanvas = document.createElement("canvas");
			renderViaCanvas.width = w;
			renderViaCanvas.height = h;
			renderViaCtx = renderViaCanvas.getContext("2d");
		}
	};
	
	function mipImage(arr, size)
	{
		if (size <= 128)
			return arr[2];
		else if (size <= 256)
			return arr[1];
		else// if (size <= 512)
			return arr[0];
		//else
		//	return arr[0];
	};
	
	Runtime.prototype.draw_c3_splash_loader = function(ctx)
	{
		if (isC3SplashDone)
			return;
		
		// 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 < 3; ++i)
		{
			if (!poweredimages[i].complete || !websiteimages[i].complete)
				return;
		}
		
		// First draw
		if (splashFrameNumber === 0)
			splashStartTime = Date.now();
		
		var nowTime = Date.now();
		var isRenderingVia = false;
		var renderToCtx = ctx;
		var drawW, drawH;
		
		if (splashIsFadeIn || splashIsFadeOut)
		{
			ctx.clearRect(0, 0, w, h);
		
			maybeCreateRenderViaCanvas(w, h);
			renderToCtx = renderViaCtx;
			isRenderingVia = true;
			
			// Note we only really start the fade-in from the second frame. This is to avoid any jank on the first
			// frame (which has to create an extra canvas and possibly load GPU textures) and ensure the actual
			// fade-in transition itself can run smoothly.
			if (splashIsFadeIn && splashFrameNumber === 1)
				splashStartTime = Date.now();
		}
		else
		{
			ctx.globalAlpha = 1;
		}
		
		renderToCtx.fillStyle = "#3B4045";
		renderToCtx.fillRect(0, 0, w, h);
		
		// Ignore exceptions when drawing splash images. This is in case any splash image fails to load. The easiest
		// way to detect this is letting drawImage throw an exception and simply skipping. This allows the timing
		// code to continue to run and still end the splash when it's done.
		try {
			// Style 1: full height
			if (this.cssHeight > 256)
			{
				// 'Powered by' text
				drawW = cr.clamp(h * 0.22, 105, w * 0.6) * 1.5;
				drawH = drawW / 8;
				renderToCtx.drawImage(mipImage(poweredimages, drawW), w * 0.5 - drawW/2, h * 0.2 - drawH/2, drawW, drawH);
				
				// C3 logo (SVG)
				drawW = Math.min(h * 0.395, w * 0.95);
				drawH = drawW;
				renderToCtx.drawImage(logoimage, w * 0.5 - drawW/2, h * 0.485 - drawH/2, drawW, drawH);
				
				// Website text
				drawW = cr.clamp(h * 0.22, 105, w * 0.6) * 1.5;
				drawH = drawW / 8;
				renderToCtx.drawImage(mipImage(websiteimages, drawW), w * 0.5 - drawW/2, h * 0.868 - drawH/2, drawW, drawH);
				
				// Progress bar background
				renderToCtx.fillStyle = "#4D565D";
				drawW = w;
				drawH = Math.max(h * 0.005, 2);
				renderToCtx.fillRect(0, h * 0.8 - drawH/2, drawW, drawH);
				
				// Progress bar fill
				renderToCtx.fillStyle = anyImageHadError ? "red" : "#29F3D0";
				drawW = w * this.progress;
				renderToCtx.fillRect(w * 0.5 - drawW/2, h * 0.8 - drawH/2, drawW, drawH);
			}
			// Style 2: narrow height
			else
			{
				// C3 logo (SVG)
				drawW = h * 0.55;
				drawH = drawW;
				renderToCtx.drawImage(logoimage, w * 0.5 - drawW/2, h * 0.45 - drawH/2, drawW, drawH);
				
				// Progress bar background
				renderToCtx.fillStyle = "#4D565D";
				drawW = w;
				drawH = Math.max(h * 0.005, 2);
				renderToCtx.fillRect(0, h * 0.85 - drawH/2, drawW, drawH);
				
				// Progress bar fill
				renderToCtx.fillStyle = anyImageHadError ? "red" : "#29F3D0";
				drawW = w * this.progress;
				renderToCtx.fillRect(w * 0.5 - drawW/2, h * 0.85 - drawH/2, drawW, drawH);
			}
			
			
			if (isRenderingVia)
			{
				if (splashIsFadeIn)
				{
					if (splashFrameNumber === 0)
						ctx.globalAlpha = 0;
					else
						ctx.globalAlpha = Math.min((nowTime - splashStartTime) / splashFadeInDuration, 1);
				}
				else if (splashIsFadeOut)
				{
					ctx.globalAlpha = Math.max(1 - (nowTime - splashFadeOutStart) / splashFadeOutDuration, 0);
				}
					
				ctx.drawImage(renderViaCanvas, 0, 0, w, h);
			}
		}
		catch(e)
		{
			// Ignore the exception, presumably a splash image failed to load and couldn't be drawn
		}
		
		if (splashIsFadeIn && nowTime - splashStartTime >= splashFadeInDuration && splashFrameNumber >= 2)
		{
			splashIsFadeIn = false;
			splashFadeInFinish = nowTime;
		}
		
		if (!splashIsFadeIn && nowTime - splashFadeInFinish >= splashMinDisplayTime && !splashIsFadeOut && this.progress >= 1)
		{
			splashIsFadeOut = true;
			splashFadeOutStart = nowTime;
		}
		
		// Note there are two ways we end the C3 splash:
		// 1) normally, after the splash fade out and wait
		// 2) in preview/quick splash mode, if it loads quicker than 500ms, just skip straight to the game to avoid getting in the way
		if ((splashIsFadeOut && nowTime - splashFadeOutStart >= splashFadeOutDuration + splashAfterFadeOutWait) ||
			(allowQuickSplash && this.progress >= 1 && Date.now() - splashStartTime < 500))
		{
			isC3SplashDone = true;
			splashIsFadeIn = false;
			splashIsFadeOut = false;
			renderViaCanvas = null;
			renderViaCtx = null;
			this.loaderlogos = null;
		}
		
		++splashFrameNumber;
	};
	
	// Run once textures have all completed
	Runtime.prototype.go_loading_finished = function ()
	{
		// If there are any web fonts used in the project, and the browser supports loading them with document.fonts.load,
		// wait for them all to finish loading before starting. Otherwise just start right away.
		if (this.webFontNames.length && document.fonts && document.fonts.load)
		{
			var self = this;
			
			Promise.all(this.webFontNames.map(function (f)
			{
				return document.fonts.load("1em '" + f + "'");
			}))
			.then(function ()
			{
				self.start_first_layout();
			});
		}
		else
		{
			this.start_first_layout();
		}
	};
	
	Runtime.prototype.start_first_layout = function ()
	{
		// Remove overlay canvas if any
		if (this.overlay_canvas)
		{
			this.canvas.parentNode.removeChild(this.overlay_canvas);
			this.overlay_ctx = null;
			this.overlay_canvas = null;
		}
		
		// Reset the start time
		this.start_time = Date.now();
		this.last_fps_time = cr.performance_now();       // for counting framerate
		
		// Initialise debugger
		
		var i, len, t;
		
		// Create the rest of the types in the project if using loader layout
		if (this.uses_loader_layout)
		{
			for (i = 0, len = this.types_by_index.length; i < len; i++)
			{
				t = this.types_by_index[i];
				
				if (!t.is_family && !t.isOnLoaderLayout && t.plugin.is_world)
				{
					t.onCreate();
					cr.seal(t);
				}
			}
			
			// Now wait_for_textures has extra images waiting to be loaded...
		}
		else
			this.isloading = false;
			
		// Create all global non-world instances in all layouts
		for (i = 0, len = this.layouts_by_index.length; i < len; i++)
		{
			this.layouts_by_index[i].createGlobalNonWorlds();
		}
		
		// make sure aspect scale is correctly set in advance of first tick
		if (this.fullscreen_mode >= 2)
		{
			var orig_aspect = this.original_width / this.original_height;
			var cur_aspect = this.width / this.height;
			
			// note mode 2 (scale inner) inverts this logic and will use window width when width wider.
			if ((this.fullscreen_mode !== 2 && cur_aspect > orig_aspect) || (this.fullscreen_mode === 2 && cur_aspect < orig_aspect))
				this.aspect_scale = this.height / this.original_height;
			else
				this.aspect_scale = this.width / this.original_width;
		}
		
		// Trigger onBeforeAppBegin for any plugins with a handler
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.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"] && navigator["splashscreen"]["hide"])
			navigator["splashscreen"]["hide"]();
			
		// Trigger onAppBegin for any plugins with a handler
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			t = this.types_by_index[i];
			
			if (t.onAppBegin)
				t.onAppBegin();
		}
		
		// Add event listeners to suspend the engine when the page becomes invisible
		var onVisibilityChanged = function ()
		{
			if (document["hidden"] || document["webkitHidden"] || document["mozHidden"] || document["msHidden"])
				window["cr_setSuspended"](true);
			else
				window["cr_setSuspended"](false);
		};
		document.addEventListener("visibilitychange", onVisibilityChanged, false);
		document.addEventListener("mozvisibilitychange", onVisibilityChanged, false);
		document.addEventListener("webkitvisibilitychange", onVisibilityChanged, false);
		document.addEventListener("msvisibilitychange", onVisibilityChanged, false);
		
		// In Cordova apps, page visibility events don't seem to fire when pressing the power button, so listen
		// for Cordova's pause and resume events as well.
		if (this.isCordova)
		{
			document.addEventListener("pause", function ()
			{
				window["cr_setSuspended"](true);
			});
			document.addEventListener("resume", function ()
			{
				window["cr_setSuspended"](false);
			});
		}
		
		// Starting up in minimised/background window: suspend engine
		if (document["hidden"] || document["webkitHidden"] || document["mozHidden"] || document["msHidden"])
		{
			window["cr_setSuspended"](true);		// stop rendering
		}
		else
		{
			// Initial tick
			this.tick(false);
		}
	};
		  
	// Timer tick (process one frame)
	Runtime.prototype.tick = function (background_wake, timestamp, debug_step)
	{
		// Some platforms fire suspend/resume events before the runtime is ready, and resuming
		// calls tick() to kick off the game loop again. If the runtime is not ready just ignore this.
		if (!this.running_layout)
			return;
			
		var nowtime = cr.performance_now();
		var logic_start = nowtime;
		
		// App suspended: stop ticking so code stops executing. Also in background wakes
		// make sure we don't try to kick off any new timers/callbacks.
		// However if the debugger is stepping, allow one step while suspended.
		if (!debug_step && this.isSuspended && !background_wake)
			return;
		
		// Schedule the next frame ASAP, to minimise any latency in the browser frame scheduler,
		// except in background wake mode.
		if (!background_wake)
		{
			if (raf)
				this.raf_id = raf(this.tickFunc);
			else
			{
				// Some mobile browsers without RAF run at sub-optimal framerates with a timeout of
				// 16, presumably because they actually wait for a whole 16 ms until doing another tick.
				// Having a timeout of 1 improves performance by eliminating the wait until the next tick.
				// However, desktop browsers which have no RAF support are optimised for V-syncing with 16ms intervals,
				// otherwise framerates run up in to the hundreds.
				this.timeout_id = setTimeout(this.tickFunc, this.isMobile ? 1 : 16);
			}
		}
		
		// If provided, use the timestamp from requestAnimationFrame since it is probably aligned with
		// the browser v-sync events. Otherwise if not provided (as in a few edge cases like first tick
		// after resume, background wake, or old browsers) then just use the performance_now() time.
		var raf_time = timestamp || nowtime;
		
		
		// In some circumstances resize events are either not fired at all, or are buggy
		// and fire at the wrong times. To avoid any such issues, we check the window
		// size every tick and if it's changed since we last saw it, fire setSize.
		var fsmode = this.fullscreen_mode;
	
		var isfullscreen = (document["mozFullScreen"] || document["webkitIsFullScreen"] || document["fullScreen"] || !!document["msFullscreenElement"]) && !this.isCordova;
		
		if (isfullscreen && this.fullscreen_scaling > 0)
			fsmode = this.fullscreen_scaling;
		
		// Disable this workaround on iOS. Due to what looks like Safari bugs,
		// on iPhone it mis-aligns the canvas when changing orientation and this workaround
		// is in effect, and the iPad browser scrolling goes haywire when using textboxes.
		//if (fsmode > 0 && (!this.isiOS || window.self !== window.top))
		if (fsmode > 0)	// r222: experimentally enabling this workaround for all platforms
		{
			var curwidth = window.innerWidth;
			var curheight = window.innerHeight;
			if (this.lastWindowWidth !== curwidth || this.lastWindowHeight !== curheight)
			{
				this["setSize"](curwidth, curheight);
			}
		}
	
		// In fullscreen mode, make sure canvas appears aligned center. Note Chrome/nodewebkit does this anyway.
		if (isfullscreen)
		{
			if (!this.firstInFullscreen)
				this.firstInFullscreen = true;
		}
		// First tick coming out of fullscreen mode: restore previous margins
		else
		{
			if (this.firstInFullscreen)
			{
				this.firstInFullscreen = false;
				
				if (this.fullscreen_mode === 0)
				{
					this["setSize"](Math.round(this.oldWidth / this.devicePixelRatio), Math.round(this.oldHeight / this.devicePixelRatio), true);
				}
			}
			else
			{
				this.oldWidth = this.width;
				this.oldHeight = this.height;
			}
		}
		
		// If a loader layout loading, update the progress
		if (this.isloading)
		{
			var done = this.areAllTexturesAndSoundsLoaded();		// updates this.progress
			this.loadingprogress = this.progress;
			
			if (done)
			{
				this.isloading = false;
				this.progress = 1;
				this.trigger(cr.system_object.prototype.cnds.OnLoadFinished, null);
				
				// Register the SW now the loader layout has finished.
				if (window["C3_RegisterSW"])
					window["C3_RegisterSW"]();
			}
		}
		

		// Execute logic
		this.logic(raf_time);

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

		if (!this.hit_breakpoint)
		{
			this.tickcount++;
			this.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 >= 1000)  // every 1 second
		{
			this.last_fps_time += 1000;
			
			// still more than 1 second behind: must have had some kind of hang/pause, just bring up to date
			if (cur_time - this.last_fps_time >= 1000)
				this.last_fps_time = cur_time;
			
			this.fps = this.framecount;
			this.framecount = 0;
			this.cpuutilisation = Math.min(this.logictime, 1000);		// clamp to 100%
			this.logictime = 0;

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

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

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

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

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

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

		this.last_tick_time = cur_time;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return inst;
	};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		// Didn't get out a solid: oops, we're stuck.
		// Restore old position.
		inst.x = oldx;
		inst.y = oldy;
		inst.set_bbox_changed();
		return false;
	};
	
	// 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 < dist; ++i)
		{
			// Test both forwards and backwards directions at this distance
			for (which = 0; which < 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 < push_dist; i++)
		{
			inst.x = (oldx + (xdir * i));
			inst.y = (oldy + (ydir * i));
			inst.set_bbox_changed();
			
			// Test if we've cleared the last instance we were overlapping
			if (!this.testOverlap(inst, otherinst))
				return true;
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			var cur_parent = trig.parent;

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

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

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

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

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

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

	Runtime.prototype.popEventStack = function ()
	{
;

		this.event_stack_index--;
	};

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

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

	Runtime.prototype.popLoopStack = function ()
	{
;

		this.loop_stack_index--;
	};

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

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

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

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

			scope = scope.parent;
		}

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

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

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

		return null;
	};
	
	Runtime.prototype.getLayoutBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.layouts_by_index.length; i < len; i++)
		{
			if (this.layouts_by_index[i].sid === sid_)
				return this.layouts_by_index[i];
		}
		
		return null;
	};
	
	Runtime.prototype.getObjectTypeBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			if (this.types_by_index[i].sid === sid_)
				return this.types_by_index[i];
		}
		
		return null;
	};
	
	Runtime.prototype.getGroupBySid = function (sid_)
	{
		var i, len;
		for (i = 0, len = this.allGroups.length; i < len; i++)
		{
			if (this.allGroups[i].sid === sid_)
				return this.allGroups[i];
		}
		
		return null;
	};
	

	function GetPermissionAPI()
	{
		var api = window["cordova"] && window["cordova"]["plugins"] && 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") > -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] && typeof extra[p].c2userdata !== "undefined")
					continue;
				
				// Don't save a key called "spriteCreatedDestroyCallback", Sprite uses it to know
				// whether to create a destroy callback
				if (p === "spriteCreatedDestroyCallback")
					continue;
					
				ret[p] = extra[p];
			}
		}
		
		return ret;
	};
	
	Runtime.prototype.saveToJSONString = function()
	{
		var i, len, j, lenj, type, layout, typeobj, g, c, a, v, p;
		
		var o = {
			"c2save":				true,
			"version":				1,
			"rt": {
				"time":				this.kahanTime.sum,
				"walltime":			this.wallTime.sum,
				"timescale":		this.timescale,
				"tickcount":		this.tickcount,
				"execcount":		this.execcount,
				"next_uid":			this.next_uid,
				"running_layout":	this.running_layout.sid,
				"start_time_offset": (Date.now() - this.start_time)
			},
			"types": {},
			"layouts": {},
			"events": {
				"groups": {},
				"cnds": {},
				"acts": {},
				"vars": {}
			}
		};
		
		// Save types - skip families since they have duplicate references to the real instances
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family || this.typeHasNoSaveBehavior(type))
				continue;
			
			typeobj = {
				"instances": []
			};
			
			if (cr.hasAnyOwnProperty(type.extra))
				typeobj["ex"] = CopyExtraObject(type.extra);
			
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				typeobj["instances"].push(this.saveInstanceToJSON(type.instances[j]));
			}
			
			o["types"][type.sid.toString()] = typeobj;
		}
		
		// Save layouts
		for (i = 0, len = this.layouts_by_index.length; i < len; i++)
		{
			layout = this.layouts_by_index[i];
			o["layouts"][layout.sid.toString()] = layout.saveToJSON();
		}
		
		// Save event data: group activation states
		var ogroups = o["events"]["groups"];
		for (i = 0, len = this.allGroups.length; i < len; i++)
		{
			g = this.allGroups[i];
			ogroups[g.sid.toString()] = this.groups_by_name[g.group_name].group_active;
		}
		
		// condition extras
		var ocnds = o["events"]["cnds"];
		for (p in this.cndsBySid)
		{
			if (this.cndsBySid.hasOwnProperty(p))
			{
				c = this.cndsBySid[p];
				if (cr.hasAnyOwnProperty(c.extra))
					ocnds[p] = { "ex": CopyExtraObject(c.extra) };
			}
		}
		
		// action extras
		var oacts = o["events"]["acts"];
		for (p in this.actsBySid)
		{
			if (this.actsBySid.hasOwnProperty(p))
			{
				a = this.actsBySid[p];
				if (cr.hasAnyOwnProperty(a.extra))
					oacts[p] = { "ex": CopyExtraObject(a.extra) };
			}
		}
		
		// variable extras
		var ovars = o["events"]["vars"];
		for (p in this.varsBySid)
		{
			if (this.varsBySid.hasOwnProperty(p))
			{
				v = this.varsBySid[p];
				
				// Save global or static local vars
				if (!v.is_constant && (!v.parent || v.is_static))
					ovars[p] = v.data;
			}
		}
		
		o["system"] = this.system.saveToJSON();
		
		return JSON.stringify(o);
	};
	
	Runtime.prototype.refreshUidMap = function ()
	{
		var i, len, type, j, lenj, inst;
		this.objectsByUid = {};
		
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			if (type.is_family)
				continue;
			
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];
				this.objectsByUid[inst.uid.toString()] = inst;
			}
		}
	};
	
	Runtime.prototype.loadFromJSONString = function (str)
	{
		var o;
		
		try {
			o = JSON.parse(str);
		}
		catch (e) {
			return false;
		}
		
		if (!o["c2save"])
			return false;		// probably not a c2 save state
		
		if (o["version"] > 1)
			return false;		// from future version of c2; assume not compatible
		
		this.isLoadingState = true;
		
		var rt = o["rt"];
		this.kahanTime.reset();
		this.kahanTime.sum = rt["time"];
		this.wallTime.reset();
		this.wallTime.sum = rt["walltime"] || 0;
		this.timescale = rt["timescale"];
		this.tickcount = rt["tickcount"];
		this.execcount = rt["execcount"];
		this.start_time = Date.now() - rt["start_time_offset"];
		
		var layout_sid = rt["running_layout"];
		
		// Need to change to different layout
		if (layout_sid !== this.running_layout.sid)
		{
			var changeToLayout = this.getLayoutBySid(layout_sid);
			
			if (changeToLayout)
				this.doChangeLayout(changeToLayout);
			else
				return;		// layout that was saved on has gone missing (deleted?)
		}
		
		// Load all types
		var i, len, j, lenj, k, lenk, p, type, existing_insts, load_insts, inst, binst, layout, layer, g, iid, t;
		var otypes = o["types"];
		
		for (p in otypes)
		{
			if (otypes.hasOwnProperty(p))
			{
				type = this.getObjectTypeBySid(parseInt(p, 10));
				
				if (!type || type.is_family || this.typeHasNoSaveBehavior(type))
					continue;
				
				if (otypes[p]["ex"])
					type.extra = otypes[p]["ex"];
				else
					cr.wipe(type.extra);
				
				// Recycle any existing objects if possible
				existing_insts = type.instances;
				load_insts = otypes[p]["instances"];
				
				for (i = 0, len = cr.min(existing_insts.length, load_insts.length); i < len; i++)
				{
					// Can load directly in to existing instance
					this.loadInstanceFromJSON(existing_insts[i], load_insts[i]);
				}
				
				// Destroy the rest of the existing instances if there are too many
				for (i = load_insts.length, len = existing_insts.length; i < len; i++)
					this.DestroyInstance(existing_insts[i]);
					
				// Create additional instances if there are not enough existing instances
				for (i = existing_insts.length, len = load_insts.length; i < len; i++)
				{
					layer = null;
					
					if (type.plugin.is_world)
					{
						layer = this.running_layout.getLayerBySid(load_insts[i]["w"]["l"]);
						
						// layer's gone missing - just skip creating this instance
						if (!layer)
							continue;
					}
					
					// create an instance then load the state in to it
					// skip creating siblings; they will have been saved as well, we'll link them up later
					inst = this.createInstanceFromInit(type.default_instance, layer, false, 0, 0, true);
					this.loadInstanceFromJSON(inst, load_insts[i]);
				}
				
				type.stale_iids = true;
			}
		}
		
		this.ClearDeathRow();
		
		// Rebuild the objectsByUid map, since some objects will have loaded a different UID to the one
		// they were created with
		this.refreshUidMap();
		
		// Load all layouts
		var olayouts = o["layouts"];
		
		for (p in olayouts)
		{
			if (olayouts.hasOwnProperty(p))
			{
				layout = this.getLayoutBySid(parseInt(p, 10));
				
				if (!layout)
					continue;		// must've gone missing
					
				layout.loadFromJSON(olayouts[p]);
			}
		}
		
		// Load event states
		var ogroups = o["events"]["groups"];
		for (p in ogroups)
		{
			if (ogroups.hasOwnProperty(p))
			{
				g = this.getGroupBySid(parseInt(p, 10));
				
				if (g && this.groups_by_name[g.group_name])
					this.groups_by_name[g.group_name].setGroupActive(ogroups[p]);
			}
		}
		
		var ocnds = o["events"]["cnds"];
		for (p in this.cndsBySid)
		{
			if (this.cndsBySid.hasOwnProperty(p))
			{
				if (ocnds.hasOwnProperty(p))
				{
					this.cndsBySid[p].extra = ocnds[p]["ex"];
				}
				else
				{
					// Reset extra object to clear as it must have been upon saving if the save record is missing.
					this.cndsBySid[p].extra = {};
				}
			}
		}
		
		var oacts = o["events"]["acts"];
		for (p in this.actsBySid)
		{
			if (this.actsBySid.hasOwnProperty(p))
			{
				if (oacts.hasOwnProperty(p))
				{
					this.actsBySid[p].extra = oacts[p]["ex"];
				}
				else
				{
					this.actsBySid[p].extra = {};
				}
			}
		}
		
		var ovars = o["events"]["vars"];
		for (p in ovars)
		{
			if (ovars.hasOwnProperty(p) && this.varsBySid.hasOwnProperty(p))
			{
				this.varsBySid[p].data = ovars[p];
			}
		}
		
		this.next_uid = rt["next_uid"];
		this.isLoadingState = false;
		
		// Fire "On created" for any instances created during loading
		for (i = 0, len = this.fireOnCreateAfterLoad.length; i < len; ++i)
		{
			inst = this.fireOnCreateAfterLoad[i];
			this.trigger(Object.getPrototypeOf(inst.type.plugin).cnds.OnCreated, inst);
		}
		
		cr.clearArray(this.fireOnCreateAfterLoad);
		
		this.system.loadFromJSON(o["system"]);
		
		// Loop again and call afterLoad() on everything now that UIDs and all states are available
		// Also link together containers now that all objects are created
		for (i = 0, len = this.types_by_index.length; i < len; i++)
		{
			type = this.types_by_index[i];
			
			// note don't call afterLoad() on types with the No Save behavior - they completely ignore
			// the whole save/load cycle.
			if (type.is_family || this.typeHasNoSaveBehavior(type))
				continue;
			
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
			{
				inst = type.instances[j];
				
				// Link container
				if (type.is_contained)
				{
					iid = inst.get_iid();
					cr.clearArray(inst.siblings);
					
					for (k = 0, lenk = type.container.length; k < lenk; k++)
					{
						t = type.container[k];
						
						if (type === t)
							continue;
							
;
						inst.siblings.push(t.instances[iid]);
					}
				}
				
				if (inst.afterLoad)
					inst.afterLoad();
				
				if (inst.behavior_insts)
				{
					for (k = 0, lenk = inst.behavior_insts.length; k < lenk; k++)
					{
						binst = inst.behavior_insts[k];
						
						if (binst.afterLoad)
							binst.afterLoad();
					}
				}
			}
		}
		
		this.redraw = true;
		return true;
	};
	
	Runtime.prototype.saveInstanceToJSON = function(inst, state_only)
	{
		var i, len, world, behinst, et;
		var type = inst.type;
		var plugin = type.plugin;
		
		var o = {};
		
		if (state_only)
			o["c2"] = true;		// mark as known json data from Construct 2
		else
			o["uid"] = inst.uid;
		
		if (cr.hasAnyOwnProperty(inst.extra))
			o["ex"] = CopyExtraObject(inst.extra);
		
		// Save instance variables
		if (inst.instance_vars && inst.instance_vars.length)
		{
			o["ivs"] = {};
			
			for (i = 0, len = inst.instance_vars.length; i < len; i++)
			{
				o["ivs"][inst.type.instvar_sids[i].toString()] = inst.instance_vars[i];
			}
		}
		
		// Save world data
		if (plugin.is_world)
		{
			world = {
				"x": inst.x,
				"y": inst.y,
				"w": inst.width,
				"h": inst.height,
				"l": inst.layer.sid,
				"zi": inst.get_zindex()
			};
			
			if (inst.angle !== 0)
				world["a"] = inst.angle;
				
			if (inst.opacity !== 1)
				world["o"] = inst.opacity;
				
			if (inst.hotspotX !== 0.5)
				world["hX"] = inst.hotspotX;
				
			if (inst.hotspotY !== 0.5)
				world["hY"] = inst.hotspotY;
				
			if (inst.blend_mode !== 0)
				world["bm"] = inst.blend_mode;
				
			if (!inst.visible)
				world["v"] = inst.visible;
				
			if (!inst.collisionsEnabled)
				world["ce"] = inst.collisionsEnabled;
				
			if (inst.my_timescale !== -1)
				world["mts"] = inst.my_timescale;
			
			if (type.effect_types.length)
			{
				world["fx"] = [];
				
				for (i = 0, len = type.effect_types.length; i < len; i++)
				{
					et = type.effect_types[i];
					world["fx"].push({"name": et.name,
									  "active": inst.active_effect_flags[et.index],
									  "params": inst.effect_params[et.index] });
				}
			}
			
			o["w"] = world;
		}
		
		// Save behaviors
		if (inst.behavior_insts && inst.behavior_insts.length)
		{
			o["behs"] = {};
			
			for (i = 0, len = inst.behavior_insts.length; i < len; i++)
			{
				behinst = inst.behavior_insts[i];
				
				if (behinst.saveToJSON)
					o["behs"][behinst.type.sid.toString()] = behinst.saveToJSON();
			}
		}
		
		// Save plugin own data
		if (inst.saveToJSON)
			o["data"] = inst.saveToJSON();
		
		return o;
	};
	
	Runtime.prototype.getInstanceVarIndexBySid = function (type, sid_)
	{
		var i, len;
		for (i = 0, len = type.instvar_sids.length; i < len; i++)
		{
			if (type.instvar_sids[i] === sid_)
				return i;
		}
		
		return -1;
	};
	
	Runtime.prototype.getBehaviorIndexBySid = function (inst, sid_)
	{
		var i, len;
		for (i = 0, len = inst.behavior_insts.length; i < len; i++)
		{
			if (inst.behavior_insts[i].type.sid === sid_)
				return i;
		}
		
		return -1;
	};
	
	Runtime.prototype.loadInstanceFromJSON = function(inst, o, state_only)
	{
		var p, i, len, iv, oivs, world, fxindex, obehs, behindex, 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 < 0 || iv >= 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 < len; i++)
				{
					fxindex = type.getEffectIndexByName(world["fx"][i]["name"]);
					
					if (fxindex < 0)
						continue;		// must've gone missing
						
					inst.active_effect_flags[fxindex] = world["fx"][i]["active"];
					inst.effect_params[fxindex] = world["fx"][i]["params"];
				}
			}
			
			inst.updateActiveEffects();
		}
		
		// Load behaviors
		obehs = o["behs"];
		
		if (obehs)
		{
			for (p in obehs)
			{
				if (obehs.hasOwnProperty(p))
				{
					behindex = this.getBehaviorIndexBySid(inst, parseInt(p, 10));
					
					if (behindex < 0)
						continue;		// must've gone missing
					
					inst.behavior_insts[behindex].loadFromJSON(obehs[p]);
				}
			}
		}
		
		// Load plugin own data
		if (o["data"])
			inst.loadFromJSON(o["data"]);
	};
	
	Runtime.prototype.getProjectFileUrl = function (filename)
	{
		if (this.isPreview)
		{
			// Preview mode: need to look in the project file map to find what the blob URL is for this filename.
			// Keys are all lowercased to make it case insensitive.
			if (!window.cr_previewProjectFiles.hasOwnProperty(filename.toLowerCase()))
				return filename;
			
			return window.cr_previewProjectFiles[filename.toLowerCase()];
		}
		else
		{
			// Export mode: simply return the file appended to the files subfolder
			return this.files_subfolder + filename;
		}
	};
	
	// Hack for Safari blob video playback workaround in Remote Preview. This allows access to the original blob for a project file
	// which helps with the workaround in the Video plugin.
	Runtime.prototype.getProjectFileBlob = function (filename)
	{
		if (this.isPreview)
		{
			if (!window.cr_previewProjectFileBlobs || !window.cr_previewProjectFiles.hasOwnProperty(filename.toLowerCase()))
				return null;
			
			return window.cr_previewProjectFileBlobs[filename.toLowerCase()];
		}
		else
		{
			return null;
		}
	};
	
	Runtime.prototype.getLocalFileUrl = function (filename)
	{
		// As with getProjectFileUrl(), but relative to the root folder (not including files_subfolder)
		if (this.isPreview)
		{
			if (!window.cr_previewProjectFiles.hasOwnProperty(filename.toLowerCase()))
				return filename;
			
			return window.cr_previewProjectFiles[filename.toLowerCase()];
		}
		else
		{
			return filename;
		}
	};
	
	// Cordova loading utilities
	Runtime.prototype.fetchLocalFileViaCordova = function (filename, successCallback, errorCallback)
	{
		var path = window["cordova"]["file"]["applicationDirectory"] + "www/" + filename;
		
		window["resolveLocalFileSystemURL"](path, function (entry)
		{
			entry["file"](successCallback, errorCallback);
		}, errorCallback);
	};
	
	Runtime.prototype.fetchLocalFileViaCordovaAsText = function (filename, successCallback, errorCallback)
	{
		this.fetchLocalFileViaCordova(filename, function (file)
		{
			var reader = new FileReader();
			
			reader.onload = function (e)
			{
				successCallback(e.target.result);
			};
			
			reader.onerror = errorCallback;
			reader.readAsText(file);
			
		}, errorCallback);
	};
	
	var queuedArrayBufferReads = [];
	var activeArrayBufferReads = 0;
	var MAX_ARRAYBUFFER_READS = 8;
	
	Runtime.prototype.maybeStartNextArrayBufferRead = function()
	{
		if (!queuedArrayBufferReads.length)
			return;		// none left
		
		if (activeArrayBufferReads >= MAX_ARRAYBUFFER_READS)
			return;		// already got maximum number in-flight
		
		activeArrayBufferReads++;
		var job = queuedArrayBufferReads.shift();
		this.doFetchLocalFileViaCordovaAsArrayBuffer(job.filename, job.successCallback, job.errorCallback);
	};
	
	Runtime.prototype.fetchLocalFileViaCordovaAsArrayBuffer = function (filename, successCallback_, errorCallback_)
	{
		// This method creates an ArrayBuffer and is called in batches during startup. Since ArrayBuffer must be held
		// fully in RAM, this can spike memory use hard on startup. To avoid this, throttle the method to only have a maximum
		// number of requests in-flight at any given time.
		var self = this;
		
		queuedArrayBufferReads.push({
			filename: filename,
			successCallback: function (result)
			{
				activeArrayBufferReads--;
				self.maybeStartNextArrayBufferRead();
				successCallback_(result);
			},
			errorCallback: function (err)
			{
				activeArrayBufferReads--;
				self.maybeStartNextArrayBufferRead();
				errorCallback_(err);
			}
		});
		
		this.maybeStartNextArrayBufferRead();
	};
	
	Runtime.prototype.doFetchLocalFileViaCordovaAsArrayBuffer = function (filename, successCallback, errorCallback)
	{
		this.fetchLocalFileViaCordova(filename, function (file)
		{
			var reader = new FileReader();
			
			reader.onload = function (e)
			{
				successCallback(e.target.result);
			};
			
			reader.readAsArrayBuffer(file);
		}, errorCallback);
	};
	
	Runtime.prototype.fetchLocalFileViaCordovaAsURL = function (filename, successCallback, errorCallback)
	{
		// HACK: Safari is fussy about playing media elements from Blobs unless they have the correct type.
		// Assign a few common media types depending on the file extension.
		var blobType = "";
		var lowername = filename.toLowerCase();
		var ext3 = lowername.substr(lowername.length - 4);
		var ext4 = lowername.substr(lowername.length - 5);
		
		if (ext3 === ".mp4")
			blobType = "video/mp4";
		else if (ext4 === ".webm")
			blobType = "video/webm";		// use video type but hopefully works with audio too
		else if (ext3 === ".m4a")
			blobType = "audio/mp4";
		else if (ext3 === ".mp3")
			blobType = "audio/mpeg";
		
		// Convert fake Cordova file to a real Blob object, which we can create a URL to.
		this.fetchLocalFileViaCordovaAsArrayBuffer(filename, function (arrayBuffer)
		{
			var blob = new Blob([arrayBuffer], { type: blobType });
			var url = URL.createObjectURL(blob);
			successCallback(url);
		}, errorCallback);
	};
	
	Runtime.prototype.isAbsoluteUrl = function (url)
	{
		// e.g. true for "http://example.com", "data:foo", "blob:foo", false for "local.txt"
		return /^(?:[a-z]+:)?\/\//.test(url) || url.substr(0, 5) === "data:"  || url.substr(0, 5) === "blob:";
	};
	
	Runtime.prototype.setImageSrc = function (img, src)
	{
		// In WKWebView mode, load relative URLs as local files. Otherwise request normally.
		if (this.isWKWebView && !this.isAbsoluteUrl(src))
		{
			this.fetchLocalFileViaCordovaAsURL(src, function (url)
			{
				img.src = url;
			}, function (err)
			{
				alert("Failed to load image: " + err);
			});
		}
		else if (this.isPreview && !this.isAbsoluteUrl(src))
		{
			img.src = this.getLocalFileUrl(src);
		}
		else
		{
			// Export lowercases all local project files, so if it is a relative request also lowercase the name
			if (!this.isAbsoluteUrl(src))
				src = src.toLowerCase();
			
			img.src = src;
		}
	};
	
	Runtime.prototype.getPreferredAudioFile = function (namepart)
	{
		var lowername = namepart.toLowerCase();
		
		if (!this.audio_files_map.hasOwnProperty(lowername))
			return null;		// unknown audio file
		
		var info = this.audio_files_map[lowername];
		var webMOpusFile = null;
		
		// info is array of file data sorted by preference. Find the first entry that is supported
		var i, len, r, type;
		for (i = 0, len = info.length; i < len; ++i)
		{
			r = info[i];
			type = r[0];
			
			if (!webMOpusFile && type === "audio/webm; codecs=opus")
				webMOpusFile = r;
			
			if (supportedAudioFormats[type])
				return r;		// return whole record: [mimeType, extension, size]
		}
		
		// No supported format: return the WebM Opus file if there was one, which will be sent to the ffmpeg decoder.
		// If there wasn't one, it will just return null.
		return webMOpusFile;
	};
	
	Runtime.prototype.getProjectAudioFileUrl = function (namepart)
	{
		var lowername = namepart.toLowerCase();
		
		var info = this.getPreferredAudioFile(namepart);
		if (!info)
			return null;		// no supported audio file available
		
		// get full name by appending extension from preferred audio file
		var filename = lowername + info[1];
		
		// return correct URL to this filename, and include the type so we can figure out how to decode it
		return {
			url: this.getProjectFileUrl(filename),
			type: info[0]
		};
	};

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

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

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

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


// 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 < len; i++)
		{
			var inst = im[i];
			var type = this.runtime.types_by_index[inst[1]];
;
			
			// If type has no default instance properties, make it this one
			if (!type.default_instance)
			{
				type.default_instance = inst;
				type.default_layerindex = this.index;
			}
				
			this.initial_instances.push(inst);
			
			if (this.layout.initial_types.indexOf(type) === -1)
				this.layout.initial_types.push(type);
		}
		
		cr.shallowAssignArray(this.startup_initial_instances, this.initial_instances);
		
		// Assign shaders
		this.effect_types = [];
		this.active_effect_types = [];
		this.shaders_preserve_opaqueness = true;
		this.effect_params = [];
		
		for (i = 0, len = m[15].length; i < len; i++)
		{
			this.effect_types.push({
				id: m[15][i][0],
				name: m[15][i][1],
				shaderindex: -1,
				preservesOpaqueness: false,
				active: true,
				index: i
			});
			
			this.effect_params.push(m[15][i][2].slice(0));
		}
		
		this.updateActiveEffects();
		
		this.rcTexBounce = new cr.rect(0, 0, 1, 1);
		this.rcTexDest = new cr.rect(0, 0, 1, 1);
		this.rcTexOrigin = new cr.rect(0, 0, 1, 1);
	};
	
	Layer.prototype.updateActiveEffects = function ()
	{
		cr.clearArray(this.active_effect_types);
		
		this.shaders_preserve_opaqueness = true;
		
		var i, len, et;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			
			if (et.active)
			{
				this.active_effect_types.push(et);
				
				if (!et.preservesOpaqueness)
					this.shaders_preserve_opaqueness = false;
			}
		}
	};
	
	Layer.prototype.getEffectByName = function (name_)
	{
		var i, len, et;
		for (i = 0, len = this.effect_types.length; i < len; i++)
		{
			et = this.effect_types[i];
			
			if (et.name === name_)
				return et;
		}
		
		return null;
	};

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

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

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

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

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

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

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

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

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

		layer_ctx.save();

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

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

		layer_ctx.restore();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


// 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 < len; i++)
		{
			// Create real layer
			var layer = new cr.layer(this, lm[i]);
			layer.number = i;
			cr.seal(layer);
			this.layers.push(layer);
		}
		
		// Initialise nonworld instances from model
		var im = m[7];
		this.initial_nonworld = [];
		
		for (i = 0, len = im.length; i < len; i++)
		{
			var inst = im[i];

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

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

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

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

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

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

		this.runtime.changelayout = null;
		
		// Create queued objects
		this.runtime.ClearDeathRow();
		
		// Canvas 2D renderer: attempt to preload all images that are used by types on this layout.
		// Since some canvas 2D browsers load images on demand, games can jank during playback as textures
		// are upload before first draw.  By drawing everything once on startup we can try to avoid this.
		// This may increase the chance devices run out of memory, but that's a problem with canvas 2D anyway.
		if (this.runtime.ctx)
		{
			for (i = 0, len = this.runtime.types_by_index.length; i < len; i++)
			{
				t = this.runtime.types_by_index[i];
				
				// Don't preload images for family types or when no instances used
				if (t.is_family || !t.instances.length || !t.preloadCanvas2D)
					continue;
					
				t.preloadCanvas2D(this.runtime.ctx);
			}
		}
		
		/*
		// Print VRAM
		if (this.runtime.glwrap)
		{
			console.log("Estimated VRAM at layout start: " + this.runtime.glwrap.textureCount() + " textures, approx. " + Math.round(this.runtime.glwrap.estimateVRAM() / 1024) + " kb");
		}
		*/
		
		// Now every container object is created and linked, run through them all firing 'On created'.
		// Note if we are loading a savegame, this must be deferred until loading is complete.
		if (this.runtime.isLoadingState)
		{
			cr.shallowAssignArray(this.runtime.fireOnCreateAfterLoad, created_instances);
		}
		else
		{
			// Not loading: fire all "On Created" now
			for (i = 0, len = created_instances.length; i < len; i++)
			{
				inst = created_instances[i];
				this.runtime.trigger(Object.getPrototypeOf(inst.type.plugin).cnds.OnCreated, inst);
			}
		}
		
		// Clear array to drop references
		cr.clearArray(created_instances);
		
		// Trigger 'start of layout', unless we are changing layout because a savegame is loading
		if (!this.runtime.isLoadingState)
		{
			this.runtime.trigger(cr.system_object.prototype.cnds.OnLayoutStart, null);
		}
		
		// Mark persisted objects to be loaded instead of initial objects next time around
		this.first_visit = false;
	};
	
	Layout.prototype.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 < 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 < lenk; k++)
			{
				t = inst.type.container[k];
				
				if (inst.type === t)
					continue;
					
				if (t.instances.length > iid)
					inst.siblings.push(t.instances[iid]);
				else
				{
					// No initial paired instance in layout: create one
					if (!t.default_instance)
					{
					}
					else
					{
						s = this.runtime.createInstanceFromInit(t.default_instance, inst.layer, true, inst.x, inst.y, true);
						this.runtime.ClearDeathRow();
						t.updateIIDs();
						inst.siblings.push(s);
						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 < 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 < leni; i++)
			{
				// ensure Z indices up to date so next layout can try to preserve relative
				// order of globals
				this.layers[i].updateZIndices();
				
				layer_instances = this.layers[i].instances;
				
				for (j = 0, lenj = layer_instances.length; j < lenj; j++)
				{
					inst = layer_instances[j];
					
					if (!inst.type.global)
					{
						if (this.runtime.typeHasPersistBehavior(inst.type))
							this.saveObjectToPersist(inst);
					}
				}
			}
		}
		
		// Destroy all non-globals
		for (i = 0, leni = this.layers.length; i < leni; i++)
		{
			layer_instances = this.layers[i].instances;
			
			for (j = 0, lenj = layer_instances.length; j < lenj; j++)
			{
				inst = layer_instances[j];
				
				if (!inst.type.global)
				{
					this.runtime.DestroyInstance(inst);
				}
			}
			
			this.runtime.ClearDeathRow();
			
			// Clear layer instances.  startRunning() picks up global objects and moves them to the new layout's layers.
			cr.clearArray(layer_instances);
			this.layers[i].zindices_stale = true;
		}
		
		// Destroy all non-global, non-world object type instances
		for (i = 0, leni = this.runtime.types_by_index.length; i < leni; i++)
		{
			type = this.runtime.types_by_index[i];
			
			// note we don't do this for families, we iterate the non-family types anyway
			if (type.global || type.plugin.is_world || type.plugin.singleglobal || type.is_family)
				continue;
				
			for (j = 0, lenj = type.instances.length; j < lenj; j++)
				this.runtime.DestroyInstance(type.instances[j]);
				
			this.runtime.ClearDeathRow();
		}
		
		first_layout = false;
		this.runtime.isEndingLayout = false;
	};
	
	var temp_rect = new cr.rect(0, 0, 0, 0);
	
	Layout.prototype.recreateInitialObjects = function (type, x1, y1, x2, y2)
	{
		temp_rect.set(x1, y1, x2, y2);
		
		var i, len;
		for (i = 0, len = this.layers.length; i < len; i++)
		{
			this.layers[i].recreateInitialObjects(type, temp_rect);
		}
	};

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

	EventSheet.prototype.init_event = function (m, parent, nontriggers)
	{
		switch (m[0]) {
		case 0:	// event block
		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 < len; i++)
				{
					if (block.conditions[i].trigger)
						this.init_trigger(block, i);
				}
			}
			else
			{
				// Initialise and store triggers separately
				if (block.is_trigger())
					this.init_trigger(block, 0);
				else
					nontriggers.push(block);
			}
			break;
		}
		case 1: // variable
		{
			var v = new cr.eventvariable(this, parent, m);
			cr.seal(v);
			nontriggers.push(v);
			break;
		}
        case 2:	// include
        {
            var inc = new cr.eventinclude(this, parent, m);
			cr.seal(inc);
            nontriggers.push(inc);
			break;
        }
		default:
;
		}
	};

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

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

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

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

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

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

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

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

		// Get object type name
		var type_name;

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

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

		var obj_entry = triggers[type_name];

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

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

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

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

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

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

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

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

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

        // Is a group (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 < len; i++)
		{
			var cnd = new cr.condition(this, cm[i]);
			cnd.index = i;
			cr.seal(cnd);
			this.conditions.push(cnd);
			
			/*
			if (cnd.is_logical())
				this.is_logical = true;
				
			if (cnd.type && !cnd.type.plugin.singleglobal && this.cndReferences.indexOf(cnd.type) === -1)
				this.cndReferences.push(cnd.type);
			*/

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		this.run_actions_and_subevents();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			// Behavior action
			if (m[2])
			{
				this.behaviortype = this.type.getBehaviorByName(m[2]);
;
				
				this.beh_index = this.type.getBehaviorIndexByName(m[2]);
;
			}
			else
			{
				this.behaviortype = null;
				this.beh_index = -1;
			}
		}
		
		this.sid = m[3];
		
		// 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 < len; i++)
			{
				var param = new cr.parameter(this, em[i]);
				cr.seal(param);
				this.parameters.push(param);
			}

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

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

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

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

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

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

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

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

	// Parameter class
	function Parameter(owner, m)
	{
		// Runtime members
		this.owner = owner;
		this.block = owner.block;
		this.sheet = owner.sheet;
		this.runtime = owner.runtime;
		
		// Data members & initialisation
		this.type = m[0];
		
		this.expression = null;
		this.solindex = 0;
		this.get = null;
		this.combosel = 0;
		this.layout = null;
		this.key = 0;
		this.object = null;
		this.index = 0;
		this.varname = null;
		this.eventvar = null;
		this.fileinfo = null;
		this.subparams = null;
		this.variadicret = null;
		this.subparams = null;
		this.variadicret = null;
		
		// Indicates evaluated value could change between instances. If true, parameter must be
		// repeatedly evaluated for each instance, but if false, can be evaluated once and the value shared
		// for all instances, which is faster.
		this.variesPerInstance = false;
		
		var i, len, param;
		
		switch (this.type)
		{
			case 0:		// number
			case 7:		// any
				this.expression = new cr.expNode(this, m[1]);
				this.solindex = 0;
				this.get = this.get_exp;
				break;
			case 1:		// string
			case 14:	// objectname
				this.expression = new cr.expNode(this, m[1]);
				this.solindex = 0;
				this.get = this.get_exp_str;
				
				// objectname parameters could refer to anything, which could cause any SOL modification. Unfortunately
				// this means we have to deoptimise and set the entire project's object types as the SOL modifiers.
				if (this.type === 14)
				{
					this.block.setAllSolModifiers();
					
					// 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 && owner.type.is_family)
				{
					// Getting a family variable relies on the sol index and picked
					// objects, so always evaluate per-instance for this parameter type.
					this.get = this.get_familyvar;
					this.variesPerInstance = true;
				}
				else
					this.get = this.get_instvar;
				
				break;
			case 11:	// eventvar
				// Still in the middle of initialising the application event sheets
				// Wait until postInit() after all event variables are initialised to look up
				// the event variable we want.
				this.varname = m[1];
				this.eventvar = null;
				this.get = this.get_eventvar;
				break;
			case 2:		// audiofile	["name", ismusic]
			case 12:	// fileinfo		"name"
				this.fileinfo = m[1];
				this.get = this.get_audiofile;
				break;
			case 13:	// variadic
				this.get = this.get_variadic;
				this.subparams = [];
				this.variadicret = [];
				for (i = 1, len = m.length; i < len; i++)
				{
					param = new cr.parameter(this.owner, m[i]);
					cr.seal(param);
					this.subparams.push(param);
					this.variadicret.push(0);
				}
				break;
			default:
;
		}
	};

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

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

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

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

	Parameter.prototype.get_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 < len; i++)
		{
			this.variadicret[i] = this.subparams[i].get();
		}
		
		return this.variadicret;
	};
	
	cr.parameter = Parameter;

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

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

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

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

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

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

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

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

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

// 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 && this.func === cr.plugins_.Function.prototype.exps.Call)
			{
				this.owner.setVaries();
			}
			
			// has instance expression
			if (m[4])
				this.instance_expr = new cr.expNode(owner_, m[4]);
			else
				this.instance_expr = null;
				
			this.results = [];
			this.parameters = [];
				
			// has parameters
			if (m.length === 6)
			{
				paramsModel = m[5];
				this.results.length = paramsModel.length + 1;
			}
			else
				this.results.length = 1;	// to fit 'ret'
			
			break;
		case 21:		// instvar_exp
			// Locate the object type by name
			this.object_type = this.runtime.types_by_index[m[1]];
;
			this.return_string = m[2];

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

;
	};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

;
;
;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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 < len; i++)
	{
		w = this.waits[i];
		waitobj = {
			"t": w.time,
			"st": w.signaltag,
			"s": w.signalled,
			"ev": w.ev.sid,
			"sm": [],
			"sols": {}
		};
		
		if (w.ev.actions[w.actindex])
			waitobj["act"] = w.ev.actions[w.actindex].sid;
			
		for (j = 0, lenj = w.solModifiers.length; j < lenj; j++)
			waitobj["sm"].push(w.solModifiers[j].sid);
			
		for (p in w.sols)
		{
			if (w.sols.hasOwnProperty(p))
			{
				t = this.runtime.types_by_index[parseInt(p, 10)];
;
				
				sobj = {
					"sa": w.sols[p].sa,
					"insts": []
				};
				
				for (j = 0, lenj = w.sols[p].insts.length; j < lenj; j++)
					sobj["insts"].push(w.sols[p].insts[j].uid);
				
				waitobj["sols"][t.sid.toString()] = sobj;
			}
		}
		
		owaits.push(waitobj);
	}
	
	return o;
};

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


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

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

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

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

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

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

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

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

            return false;
        }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        cndextra["TriggerOnce_lastTick"] = cur_tick;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        if (ts < 0)
            ts = 0;

        this.runtime.timescale = ts;
    };

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

        if (ts < 0)
            ts = 0;

        if (!obj)
            return;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}());


// 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 < layer.viewLeft || bbox.bottom < layer.viewTop || bbox.left > layer.viewRight || bbox.top > layer.viewBottom);
			};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

				if (isNaN(newangle))
					return;

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

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

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

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

				if (isNaN(newangle))
					return;

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

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

				if (isNaN(newangle))
					return;

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

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

				if (isNaN(newangle))
					return;

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

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

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

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

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

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

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

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

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

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

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

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

			acts.SetVisible = function (v)
			{
				if (v === 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 < 0)
					new_opacity = 0;
				else if (new_opacity > 1)
					new_opacity = 1;

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

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

		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 < len; ++i)
		{
			callbacks[i](this);
		}
		
		// If layer is using render cells, update the bounding box right away to
		// update its render cells. This appears to be faster than trying to maintain
		// a queue of objects to update later.
		if (this.layer.useRenderCells)
			this.update_bbox();
	};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if (prevsol.select_all)
		{
			clonesol.select_all = true;
		}
		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 < len; i++)
			{
				f = this.families[i];
				
				for (j = 0, lenj = f.behaviors.length; j < lenj; j++)
				{
					if (behname === f.behaviors[j].name)
					{
						this.extra["lastBehIndex"] = index;
						return f.behaviors[j];
					}
						
					index++;
				}
			}
		}
		
		// Now check my behaviors
		for (i = 0, len = this.behaviors.length; i < len; i++) {
			if (behname === this.behaviors[i].name)
			{
				this.extra["lastBehIndex"] = index;
				return this.behaviors[i];
			}
				
			index++;
		}
		return null;
	};

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

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

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

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

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

})();

cr.shaders = {};


// Tilemap
// ECMAScript 5 strict mode

;
;

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

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

	var typeProto = pluginProto.Type.prototype;

	// called on startup for each object type
	typeProto.onCreate = function()
	{
		var i, len, p;
		
		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];
		
		// Array of tile id to either a pattern (canvas2d) or texture (webgl)
		this.cut_tiles = [];
		this.cut_tiles_valid = false;
		
		// Create tile collision polys from the poly data
		this.tile_polys = [];
		this.tile_polys_cached = false;		// first instance will cache polys with the tile width/height
		
		if (this.tile_poly_data && this.tile_poly_data.length)
		{
			for (i = 0, len = this.tile_poly_data.length; i < len; ++i)
			{
				p = this.tile_poly_data[i];
				
				if (p)
				{
					var poly = p[0];
					var use = !!p[1];
					
					// For rotated and flipped tiles, store a flipmap like the canvas2d tiles
					this.tile_polys.push({
						poly: poly,
						use: use,
						flipmap: [[[null, null], [null, null]], [[null, null], [null, null]]]
					});
				}
				else
					this.tile_polys.push(null);
			}
		}
	};
	
	typeProto.cacheTilePoly = function (tileid, tilewidth, tileheight, fliph, flipv, flipd)
	{
		if (tileid < 0 || tileid >= this.tile_polys.length)
			return;
		
		if (!this.tile_polys[tileid])
			return;		// no poly for this tile
		
		var poly = this.tile_polys[tileid].poly;
		var use = this.tile_polys[tileid].use;
		var flipmap = this.tile_polys[tileid].flipmap;
		
		var cached_poly = new cr.CollisionPoly(poly, use);
		cached_poly.cache_poly(tilewidth, tileheight, 0);
		
		if (flipd)
			cached_poly.diag();
		
		if (fliph)
			cached_poly.mirror(tilewidth / 2);
		
		if (flipv)
			cached_poly.flip(tileheight / 2);
		
		flipmap[fliph?1:0][flipv?1:0][flipd?1:0] = cached_poly;
	};
	
	typeProto.getTilePoly = function (id)
	{
		if (id === -1)
			return null;
		
		var tileid = (id & TILE_ID_MASK);
		
		if (tileid < 0 || tileid >= this.tile_polys.length)
			return null;		// out of range
		
		if (!this.tile_polys[tileid])
			return null;		// no poly for this tile
		
		var fliph = (id & TILE_FLIPPED_HORIZONTAL) ? 1 : 0;
		var flipv = (id & TILE_FLIPPED_VERTICAL) ? 1 : 0;
		var flipd = (id & TILE_FLIPPED_DIAGONAL) ? 1 : 0;
		return this.tile_polys[tileid].flipmap[fliph][flipv][flipd];
	};
	
	typeProto.isTilePolyEnabled = function(id)
	{
		var tilePoly = this.getTilePoly(id);
		
		if (!tilePoly)
			return true; // no poly assumes enabled whole tile poly
		
		return tilePoly.is_enabled();
	}
	
	typeProto.freeCutTiles = function ()
	{
		var i, len;
		
		// In WebGL mode, need to delete textures
		var glwrap = this.runtime.glwrap;
		
		if (glwrap)
		{
			for (i = 0, len = this.cut_tiles.length; i < len; ++i)
				glwrap.deleteTexture(this.cut_tiles[i]);
		}
		
		cr.clearArray(this.cut_tiles);
		this.cut_tiles_valid = false;
	}
	
	typeProto.maybeCutTiles = function (tw, th, offx, offy, sepx, sepy, seamless)
	{
		if (this.cut_tiles_valid)
			return;		// no changed
			
		if (tw <= 0 || th <= 0)
			return;
		
		this.freeCutTiles();
		
		var img_width = this.spriteWidth;
		var img_height = this.spriteHeight;
		
		var x, y;
		
		for (y = offy; y + th <= img_height; y += (th + sepy))
		{
			for (x = offx; x + tw <= img_width; x += (tw + sepx))
			{
				this.cut_tiles.push(this.CutTileImage(x, y, tw, th, seamless));
			}
		}
		
		this.cut_tiles_valid = true;
	};
	
	typeProto.CutTileImage = function(x, y, w, h, seamless)
	{
		// WebGL mode: just cut the texture without any flipping; at draw time we can rotate
		// and flip it accordingly using just texture co-ordinates
		if (this.runtime.glwrap)
		{
			return this.DoCutTileImage(x, y, w, h, false, false, false, seamless);
		}
		// Canvas2D mode: the 2D context doesn't give us convenient tools to do texture
		// co-ordinate flipping, especially given that we render quads of tiles at a time.
		// So we pre-process tiles with flipped images which can simply be drawn as if they
		// were a different type of tile. This can use up to 8x more memory, so we lazy create
		// all the flipped variants. This could jank, but it's probably better than wasting
		// memory in case large tilemaps are used.
		else
		{
			// Bit of an array nightmare but allows us to index a flipped tile via:
			// ret[horiz_flip ? 1 : 0][vert_flip ? 1 : 0][diag_flip ? 1 : 0]
			var flipmap = [[[null, null], [null, null]], [[null, null], [null, null]]];
			flipmap[0][0][0] = this.DoCutTileImage(x, y, w, h, false, false, false, seamless);
			
			return {
				flipmap: flipmap,
				x: x,
				y: y,
				w: w,
				h: h
			};
		}
	};
	
	typeProto.GetFlippedTileImage = function (tileid, fliph, flipv, flipd, seamless)
	{
		// Canvas2D only. Access the given pre-flipped tile, lazy creating it if missing.
		if (tileid < 0 || tileid >= this.cut_tiles.length)
			return null;
			
		var tile = this.cut_tiles[tileid];
		var flipmap = tile.flipmap;
		
		var hi = (fliph ? 1 : 0);
		var vi = (flipv ? 1 : 0);
		var di = (flipd ? 1 : 0);
		
		var ret = flipmap[hi][vi][di];
		
		if (ret)
		{
			return ret;
		}
		else
		{
			// Lazy create and return
			ret = this.DoCutTileImage(tile.x, tile.y, tile.w, tile.h, hi!==0, vi!==0, di!==0, seamless);
			flipmap[hi][vi][di] = ret;
			return ret;
		}
	};
	
	typeProto.DoCutTileImage = function(x, y, w, h, fliph, flipv, flipd, seamless)
	{
		var dw = w;
		var dh = h;
		
		// For WebGL: must size up to next power of two for tiling, unless "seamless" mode (which does not tile anything)
		if (this.runtime.glwrap && !seamless)
		{
			if (!cr.isPOT(dw))
				dw = cr.nextHighestPowerOfTwo(dw);
			
			if (!cr.isPOT(dh))
				dh = cr.nextHighestPowerOfTwo(dh);
		}
		
		var tmpcanvas = document.createElement("canvas");
		tmpcanvas.width = dw;
		tmpcanvas.height = dh;
		var tmpctx = tmpcanvas.getContext("2d");
			
		// Canvas2D mode: create a pattern from the source image
		if (this.runtime.ctx)
		{
			if (fliph)
			{
				if (flipv)
				{
					if (flipd)
					{
						// Flip diagonal, horizontal and vertical (aka rotate 90 clockwise, flip vertical)
						tmpctx.rotate(Math.PI / 2);
						tmpctx.scale(-1, 1);
						tmpctx.translate(-dw, -dh);
					}
					else
					{
						// Flip horizontal and vertical
						tmpctx.scale(-1, -1);
						tmpctx.translate(-dw, -dh);
					}
				}
				else
				{
					if (flipd)
					{
						// Flip horizontal and diagonal (aka rotate 90 clockwise)
						tmpctx.rotate(Math.PI / 2);
						tmpctx.translate(0, -dh);
					}
					else
					{
						// Flip horizontal only
						tmpctx.scale(-1, 1);
						tmpctx.translate(-dw, 0);
					}
				}
			}
			else
			{
				if (flipv)
				{
					if (flipd)
					{
						// Flip diagonal and vertical (aka rotate 90 anticlockwise)
						tmpctx.rotate(-Math.PI / 2);
						tmpctx.translate(-dw, 0);
					}
					else
					{
						// Flip vertical only
						tmpctx.scale(1, -1);
						tmpctx.translate(0, -dh);
					}
				}
				else
				{
					if (flipd)
					{
						// Only flip diagonal (aka rotate 90 anticlockwise, flip vertical)
						tmpctx.scale(-1, 1);
						tmpctx.rotate(Math.PI / 2);
					}
					//else no flipping, draw normally
				}
			}
			
			tmpctx.drawImage(this.texture_img, this.spriteX + x, this.spriteY + y, w, h, 0, 0, dw, dh);
			
			// Seamless mode directly returns the canvas to be drawn; non-seamless mode returns a repeating pattern.
			if (seamless)
				return tmpcanvas;
			else
				return this.runtime.ctx.createPattern(tmpcanvas, "repeat");
		}
		// WebGL mode: create a texture from the source image
		else
		{
;
			
			tmpctx.drawImage(this.texture_img, this.spriteX + x, this.spriteY + y, w, h, 0, 0, dw, dh);
			
			// Don't set tiling in seamless mode, each tile is drawn individually.
			var tex = this.runtime.glwrap.createEmptyTexture(dw, dh, this.runtime.linearSampling, false, !seamless);
			this.runtime.glwrap.videoToTexture(tmpcanvas, tex);
			return tex;
		}
	};
	
	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;
			
		this.freeCutTiles();
	};
	
	typeProto.onRestoreWebGLContext = function ()
	{
		// Do nothing, next draw will create on demand
	};
	
	typeProto.loadTextures = function ()
	{
		// Do nothing, next draw will create on demand
	};
	
	typeProto.unloadTextures = function ()
	{
		// Don't release textures if any instances still exist, they are probably using them
		if (this.is_family || this.instances.length)
			return;
		
		this.freeCutTiles();
	};
	
	typeProto.preloadCanvas2D = function (ctx)
	{
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
		
		// any other properties you need, e.g...
		// this.myValue = 0;
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	// Flip flags matching Tiled's format
	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
	var TILE_ID_MASK = 0x1FFFFFFF
	
	function TileQuad()
	{
		this.id = -1;
		this.tileid = -1;
		this.horiz_flip = false;
		this.vert_flip = false;
		this.diag_flip = false;
		this.any_flip = false;
		this.rc = new cr.rect(0, 0, 0, 0);
	};
	
	var tilequad_cache = [];
	
	function allocTileQuad()
	{
		if (tilequad_cache.length)
			return tilequad_cache.pop();
		else
			return new TileQuad();
	};
	
	function freeTileQuad(tq)
	{
		if (tilequad_cache.length < 10000)
			tilequad_cache.push(tq);
	};
	
	function TileCollisionRect()
	{
		this.id = -1;
		this.rc = new cr.rect(0, 0, 0, 0);
		this.poly = null;
	}
	
	var collrect_cache = [];
	
	function allocCollRect()
	{
		if (collrect_cache.length)
			return collrect_cache.pop();
		else
			return new TileCollisionRect();
	};
	
	function freeCollRect(r)
	{
		if (collrect_cache.length < 10000)
			collrect_cache.push(r);
	};
	
	var tile_cell_cache = [];
	
	function allocTileCell(inst_, x_, y_)
	{
		var ret;
		
		if (tile_cell_cache.length)
		{
			ret = tile_cell_cache.pop();
			ret.inst = inst_;
			ret.x = x_;
			ret.y = y_;
			ret.left = ret.x * ret.inst.cellwidth * ret.inst.tilewidth;
			ret.top = ret.y * ret.inst.cellheight * ret.inst.tileheight;
			ret.clear();
			ret.quadmap_valid = false;
			return ret;
		}
		else
			return new TileCell(inst_, x_, y_);
	};
	
	function freeTileCell(tc)
	{
		var i, len;
		
		// Recycle quads and collision rects
		for (i = 0, len = tc.quads.length; i < len; ++i)
			freeTileQuad(tc.quads[i]);
			
		cr.clearArray(tc.quads);
		
		for (i = 0, len = tc.collision_rects.length; i < len; ++i)
			freeCollRect(tc.collision_rects[i]);
		
		cr.clearArray(tc.collision_rects);
		
		if (tile_cell_cache.length < 1000)
			tile_cell_cache.push(tc);
	};
	
	function TileCell(inst_, x_, y_)
	{
		this.inst = inst_;
		this.x = x_;
		this.y = y_;
		this.left = this.x * this.inst.cellwidth * this.inst.tilewidth;
		this.top = this.y * this.inst.cellheight * this.inst.tileheight;
		
		this.tiles = [];
		this.quads = [];
		this.collision_rects = [];
		this.quadmap_valid = false;
		
		// Reserve enough tiles to fill the cell
		var i, len, j, lenj, arr;
		for (i = 0, len = this.inst.cellheight; i < len; ++i)
		{
			arr = [];
			
			for (j = 0, lenj = this.inst.cellwidth; j < lenj; ++j)
				arr.push(-1);
			
			this.tiles.push(arr);
		}
	};
	
	TileCell.prototype.clear = function ()
	{
		var i, len, j, lenj, arr;
		
		this.tiles.length = this.inst.cellheight;
		
		for (i = 0, len = this.tiles.length; i < len; ++i)
		{
			arr = this.tiles[i];
			
			if (!arr)
			{
				arr = [];
				this.tiles[i] = arr;
			}
			
			arr.length = this.inst.cellwidth;
			
			for (j = 0, lenj = arr.length; j < lenj; ++j)
				arr[j] = -1;
		}
	};
	
	TileCell.prototype.maybeBuildQuadMap = function ()
	{
		if (this.quadmap_valid)
			return;		// not changed
		
		var tilewidth = this.inst.tilewidth;
		var tileheight = this.inst.tileheight;
		
		if (tilewidth <= 0 || tileheight <= 0)
			return;
		
		var i, j, len, y, leny, x, lenx, arr, t, p, q;
		
		// Recycle all existing quads and collision rects
		for (i = 0, len = this.quads.length; i < len; ++i)
			freeTileQuad(this.quads[i]);
		
		for (i = 0, len = this.collision_rects.length; i < len; ++i)
			freeCollRect(this.collision_rects[i]);
		
		cr.clearArray(this.quads);
		cr.clearArray(this.collision_rects);
		
		// Only calculate to the edge of the visible area of the object, or to the edge of the map size, whichever comes first.
		var extentwidth = Math.min(this.inst.mapwidth, Math.floor(this.inst.width / tilewidth));
		var extentheight = Math.min(this.inst.mapheight, Math.floor(this.inst.height / tileheight));
		
		extentwidth -= this.left / tilewidth;
		extentheight -= this.top / tileheight;
		
		if (extentwidth > this.inst.cellwidth)
			extentwidth = this.inst.cellwidth;
		if (extentheight > this.inst.cellheight)
			extentheight = this.inst.cellheight;
		
		var seamless = this.inst.seamless;
		
		// Normalize tiles horizontally
		var cur_quad = null;
		for (y = 0, leny = extentheight; y < leny; ++y)
		{
			arr = this.tiles[y];
			
			for (x = 0, lenx = extentwidth; x < lenx; ++x)
			{
				t = arr[x];
				
				// Skip empty tiles
				if (t === -1)
				{
					if (cur_quad)
					{
						this.quads.push(cur_quad);
						cur_quad = null;
					}
					
					continue;
				}
				
				// First tile, or reached a tile of different id.
				// In seamless mode break up every tile individually.
				if (seamless || !cur_quad || t !== cur_quad.id)
				{
					if (cur_quad)
						this.quads.push(cur_quad);
					
					cur_quad = allocTileQuad();
					cur_quad.id = t;
					cur_quad.tileid = (t & TILE_ID_MASK);
					cur_quad.horiz_flip = (t & TILE_FLIPPED_HORIZONTAL) !== 0;
					cur_quad.vert_flip = (t & TILE_FLIPPED_VERTICAL) !== 0;
					cur_quad.diag_flip = (t & TILE_FLIPPED_DIAGONAL) !== 0;
					cur_quad.any_flip = (cur_quad.horiz_flip || cur_quad.vert_flip || cur_quad.diag_flip);
					cur_quad.rc.left = x * tilewidth + this.left;
					cur_quad.rc.top = y * tileheight + this.top;
					cur_quad.rc.right = cur_quad.rc.left + tilewidth;
					cur_quad.rc.bottom = cur_quad.rc.top + tileheight;
				}
				// Otherwise another tile of the same type. Extend existing quad
				else
				{
					cur_quad.rc.right += tilewidth;
				}
			}
			
			// End of row: flush current quad
			if (cur_quad)
			{
				this.quads.push(cur_quad);
				cur_quad = null;
			}
		}
		
		// Now do more or less the same thing but for collision rects, where any tile >= 0 makes
		// a collision rect. Use tile polys when set, and note every tile with a poly must be split
		// off in to its own rect
		var cur_rect = null;
		var tileid, tilepoly;
		var cur_has_poly = false;
		var rc;
		for (y = 0, leny = extentheight; y < leny; ++y)
		{
			arr = this.tiles[y];
			
			for (x = 0, lenx = extentwidth; x < lenx; ++x)
			{
				t = arr[x];
				
				var isPolyEnabled = this.inst.type.isTilePolyEnabled(t);
				
				// Skip empty tiles
				if (t === -1 || !isPolyEnabled)
				{
					if (cur_rect)
					{
						this.collision_rects.push(cur_rect);
						cur_rect = null;
						cur_has_poly = false;
					}
						
					continue;
				}
				
				tileid = (t & TILE_ID_MASK);
				tilepoly = this.inst.type.getTilePoly(t);
				
				// Got a new tile or new tile uses a collision poly: start making a new collision rect
				if (!cur_rect || tilepoly || cur_has_poly)
				{
					if (cur_rect)
					{
						this.collision_rects.push(cur_rect);
						cur_rect = null;
					}
					
;
					cur_rect = allocCollRect();
					cur_rect.id = t;
					cur_rect.poly = tilepoly ? tilepoly : null;
					rc = cur_rect.rc;
					
					rc.left = x * tilewidth + this.left;
					rc.top = y * tileheight + this.top;
					rc.right = rc.left + tilewidth;
					rc.bottom = rc.top + tileheight;
					
					cur_has_poly = !!tilepoly;
				}
				// Otherwise another tile. Extend existing collision rect
				else
				{
					cur_rect.rc.right += tilewidth;
				}
			}
			
			// End of row: flush current rect
			if (cur_rect)
			{
				this.collision_rects.push(cur_rect);
				cur_rect = null;
				cur_has_poly = false;
			}
		}
		
		// Now normalize quads vertically. Loop each quad and look for another quad on the row below it
		// which is the same width and id. If there is one, extend the current quad down instead.
		// In seamless mode skip this entirely.
		if (!seamless)
		{
			len = this.quads.length;
			
			for (i = 0; i < len; ++i)
			{
				q = this.quads[i];
				
				for (j = i + 1; j < len; ++j)
				{
					p = this.quads[j];
					
					// Not yet on to next row
					if (p.rc.top < q.rc.bottom)
						continue;
					// Gone too far (on next row down)
					if (p.rc.top > q.rc.bottom)
						break;
					// Is on the row below: check if gone too far to the right
					if (p.rc.right > q.rc.right || p.rc.left > q.rc.left)
						break;
					
					// Sits exactly beneath, is same width and same ID: extend q down and delete p
					if (p.id === q.id && p.rc.left === q.rc.left && p.rc.right === q.rc.right)
					{
						freeTileQuad(this.quads[j]);
						this.quads.splice(j, 1);
						--len;
						q.rc.bottom += tileheight;
						--j;		// look at same j index again
					}
				}
			}
		}
		
		// Again, do the same thing for collision rects.
		len = this.collision_rects.length;
		var prc, qrc;
		
		for (i = 0; i < len; ++i)
		{
			q = this.collision_rects[i];
			
			// Tiles with collision polys cannot be extended
			if (q.poly)
				continue;
			
			qrc = q.rc;
			
			for (j = i + 1; j < len; ++j)
			{
				p = this.collision_rects[j];
				prc = p.rc;
				
				// Not yet on to next row
				if (prc.top < qrc.bottom)
					continue;
				// Gone too far (on next row down)
				if (prc.top > qrc.bottom)
					break;
				// Is on the row below: check if gone too far to the right
				if (prc.right > qrc.right || prc.left > qrc.left)
					break;
				// Tiles with collision polys cannot be extended
				if (p.poly)
					continue;
				
				// Sits exactly beneath: extend q down and delete p
				if (prc.left === qrc.left && prc.right === qrc.right)
				{
					freeCollRect(this.collision_rects[j]);
					this.collision_rects.splice(j, 1);
					--len;
					qrc.bottom += tileheight;
					--j;		// look at same j index again
				}
			}
		}
		
		this.quadmap_valid = true;
	};
	
	TileCell.prototype.setTileAt = function (x_, y_, t_)
	{
		if (this.tiles[y_][x_] !== t_)
		{
			this.tiles[y_][x_] = t_;
			this.quadmap_valid = false;
			this.inst.any_quadmap_changed = true;
			this.inst.physics_changed = true;
			this.inst.runtime.redraw = true;
		}
	};
	
	// called whenever an instance is created
	instanceProto.onCreate = function()
	{
;
		var i, len, p;
		
		this.visible = this.properties[0];
		this.tilewidth = this.properties[1];
		this.tileheight = this.properties[2];
		this.tilexoffset = this.properties[3];
		this.tileyoffset = this.properties[4];
		this.tilexspacing = this.properties[5];
		this.tileyspacing = this.properties[6];
		this.seamless = true;		// always-on. TODO: remove redundant code
		
		this.mapwidth = this.tilemap_width;
		this.mapheight = this.tilemap_height;
		
		this.lastwidth = this.width;
		this.lastheight = this.height;
		
		var tw = this.tilewidth;
		var th = this.tileheight;
		
		if (tw === 0)
			tw = 1;
		if (th === 0)
			th = 1;
		
		this.cellwidth = Math.ceil(this.runtime.original_width / tw);
		this.cellheight = Math.ceil(this.runtime.original_height / th);
		
		// Cache tile collision polys if first instance
		if (!this.type.tile_polys_cached)
		{
			this.type.tile_polys_cached = true;
			
			for (i = 0, len = this.type.tile_polys.length; i < len; ++i)
			{
				p = this.type.tile_polys[i];
				
				if (!p)
					continue;
				
				this.type.cacheTilePoly(i, tw, th, false, false, false);
				this.type.cacheTilePoly(i, tw, th, false, false, true);
				this.type.cacheTilePoly(i, tw, th, false, true, false);
				this.type.cacheTilePoly(i, tw, th, false, true, true);
				this.type.cacheTilePoly(i, tw, th, true, false, false);
				this.type.cacheTilePoly(i, tw, th, true, false, true);
				this.type.cacheTilePoly(i, tw, th, true, true, false);
				this.type.cacheTilePoly(i, tw, th, true, true, true);
			}
		}
		
		if (!this.recycled)
			this.tilecells = [];
		
		this.maybeResizeTilemap(true);
		this.setTilesFromRLECSV(this.tilemap_data);
		
		this.type.maybeCutTiles(this.tilewidth, this.tileheight, this.tilexoffset, this.tileyoffset, this.tilexspacing, this.tileyspacing, this.seamless);
		
		this.physics_changed = false;		// to indicate to physics behavior to recreate body
		this.any_quadmap_changed = true;
		this.maybeBuildAllQuadMap();
	};
	
	instanceProto.maybeBuildAllQuadMap = function ()
	{
		if (!this.any_quadmap_changed)
			return;		// no change
			
		var i, len, j, lenj, arr;
		
		for (i = 0, len = this.tilecells.length; i < len; ++i)
		{
			arr = this.tilecells[i];
			
			for (j = 0, lenj = arr.length; j < lenj; ++j)
			{
				arr[j].maybeBuildQuadMap();
			}
		}
		
		this.any_quadmap_changed = false;
	};
	
	instanceProto.setAllQuadMapChanged = function ()
	{
		var i, len, j, lenj, arr;
		
		for (i = 0, len = this.tilecells.length; i < len; ++i)
		{
			arr = this.tilecells[i];
			
			for (j = 0, lenj = arr.length; j < lenj; ++j)
			{
				arr[j].quadmap_valid = false;
			}
		}
		
		this.any_quadmap_changed = true;
	};
	
	function RunLengthDecode(str)
	{
		var ret = [];
		
		var parts = str.split(",");
		var i, len, p, x, n, t, part;
		
		for (i = 0, len = parts.length; i < len; ++i)
		{
			p = parts[i];
			
			// Contains an 'x': treat as n x t format for repeating a number
			x = p.indexOf("x");
			
			if (x > -1)
			{
				n = parseInt(p.substring(0, x), 10);
				part = p.substring(x + 1);
				t = parseInt(part, 10);
				
				// Apply flip flags if found in the number string
				if (part.indexOf("h") > -1)
					t = t | TILE_FLIPPED_HORIZONTAL;
				if (part.indexOf("v") > -1)
					t = t | TILE_FLIPPED_VERTICAL;
				if (part.indexOf("d") > -1)
					t = t | TILE_FLIPPED_DIAGONAL;
				
				for ( ; n > 0; --n)
					ret.push(t);
			}
			else
			{
				// No 'x': treat as single number to add
				t = parseInt(p, 10);
				
				if (p.indexOf("h") > -1)
					t = t | TILE_FLIPPED_HORIZONTAL;
				if (p.indexOf("v") > -1)
					t = t | TILE_FLIPPED_VERTICAL;
				if (p.indexOf("d") > -1)
					t = t | TILE_FLIPPED_DIAGONAL;
				
				ret.push(t);
			}
		}
		
		return ret;
	};
	
	instanceProto.maybeResizeTilemap = function (force)
	{
		var curwidth = cr.floor(this.width / this.tilewidth);
		var curheight = cr.floor(this.height / this.tileheight);
		
		// Same size or smaller: no need to do anything
		if (curwidth <= this.mapwidth && curheight <= this.mapheight && !force)
			return;
		
		var vcells, hcells;
		
		if (force)
		{
			vcells = Math.ceil(this.mapheight / this.cellheight);
			hcells = Math.ceil(this.mapwidth / this.cellwidth);
		}
		else
		{
			vcells = this.tilecells.length;
			hcells = Math.ceil(this.mapwidth / this.cellwidth);
			
			// Otherwise map is now bigger: extend the tile cells if necessary
			if (curheight > this.mapheight)
			{
				this.mapheight = curheight;
				vcells = Math.ceil(this.mapheight / this.cellheight);
			}
			
			if (curwidth > this.mapwidth)
			{
				this.mapwidth = curwidth;
				hcells = Math.ceil(this.mapwidth / this.cellwidth);
			}
			
			this.setAllQuadMapChanged();
			this.physics_changed = true;
			this.runtime.redraw = true;
		}
		
		// Update tiles cells array to be the new size
		var y, x, arr;
		for (y = 0; y < vcells; ++y)
		{
			arr = this.tilecells[y];
			
			if (!arr)
			{
				// new row: insert a row of empty cells
				arr = [];
				for (x = 0; x < hcells; ++x)
					arr.push(allocTileCell(this, x, y));
				
				this.tilecells[y] = arr;
			}
			else
			{
				// row has gotten wider: insert empty tiles
				for (x = arr.length; x < hcells; ++x)
					arr.push(allocTileCell(this, x, y));
			}
		}
	};
	
	instanceProto.cellAt = function (tx, ty)
	{
		if (tx < 0 || ty < 0)
			return null;
		
		var cy = cr.floor(ty / this.cellheight);
		
		if (cy >= this.tilecells.length)
			return null;
		
		var row = this.tilecells[cy];
		
		var cx = cr.floor(tx / this.cellwidth);
		
		if (cx >= row.length)
			return null;
		
		return row[cx];
	};
	
	instanceProto.cellAtIndex = function (cx, cy)
	{
		if (cx < 0 || cy < 0 || cy >= this.tilecells.length)
			return null;
		
		var row = this.tilecells[cy];
		
		if (cx >= row.length)
			return null;
		
		return row[cx];
	};
	
	// RLECSV = run-length encoded comma separated values
	instanceProto.setTilesFromRLECSV = function (str)
	{
		// Run-length decode the tilemap in to a long number sequence
		var tilestream = RunLengthDecode(str);
		
		// Now fill tilemap arrays from the stream of tile data.
		// Note: this uses y by x format, editor stores in x by y
		var next = 0;
		var y, x, arr, tile, cell;
		
		for (y = 0; y < this.mapheight; ++y)
		{
			for (x = 0; x < this.mapwidth; ++x)
			{
				tile = tilestream[next++];
				
				cell = this.cellAt(x, y);
				
				if (cell)
					cell.setTileAt(x % this.cellwidth, y % this.cellheight, tile);
			}
		}
	};
	
	instanceProto.getTilesAsRLECSV = function ()
	{
		var ret = "";
		
		if (this.mapwidth <= 0 || this.mapheight <= 0)
			return ret;
			
		var run_count = 1;
		var run_number = this.getTileAt(0, 0);
		
		var y, leny, x, lenx, t;
		var tileid, horiz_flip, vert_flip, diag_flip;
		
		// Only extract tiles for the visible area
		lenx = cr.floor(this.width / this.tilewidth);
		leny = cr.floor(this.height / this.tileheight);
		
		for (y = 0; y < leny; ++y)
		{
			// First tile is pre-loaded in to run count, so start x from 1 on first iteration
			for (x = (y === 0 ? 1 : 0) ; x < lenx; ++x)
			{
				t = this.getTileAt(x, y);
				
				// Another of the same number: increase run count
				if (t === run_number)
					++run_count;
				else
				{
					if (run_number === -1)
					{
						tileid = -1;
						horiz_flip = false;
						vert_flip = false;
						diag_flip = false;
					}
					else
					{
						tileid = (run_number & TILE_ID_MASK);
						horiz_flip = (run_number & TILE_FLIPPED_HORIZONTAL) !== 0;
						vert_flip = (run_number & TILE_FLIPPED_VERTICAL) !== 0;
						diag_flip = (run_number & TILE_FLIPPED_DIAGONAL) !== 0;
					}
				
					// A different number: add current run then start new run
					if (run_count === 1)
						ret += "" + tileid;
					else
						ret += "" + run_count + "x" + tileid;
					
					if (horiz_flip)
						ret += "h";
					if (vert_flip)
						ret += "v";
					if (diag_flip)
						ret += "d";
					
					ret += ",";
					run_count = 1;
					run_number = t;
				}
			}
		}
		
		// Add last run
		if (run_number === -1)
		{
			tileid = -1;
			horiz_flip = false;
			vert_flip = false;
			diag_flip = false;
		}
		else
		{
			tileid = (run_number & TILE_ID_MASK);
			horiz_flip = (run_number & TILE_FLIPPED_HORIZONTAL) !== 0;
			vert_flip = (run_number & TILE_FLIPPED_VERTICAL) !== 0;
			diag_flip = (run_number & TILE_FLIPPED_DIAGONAL) !== 0;
		}
		
		// Add last run
		if (run_count === 1)
			ret += "" + tileid;
		else
			ret += "" + run_count + "x" + tileid;
		
		if (horiz_flip)
			ret += "h";
		if (vert_flip)
			ret += "v";
		if (diag_flip)
			ret += "d";
	
		return ret;
	};
	
	instanceProto.getTileAt = function (x_, y_)
	{
		x_ = Math.floor(x_);
		y_ = Math.floor(y_);
		
		if (x_ < 0 || y_ < 0 || x_ >= this.mapwidth || y_ >= this.mapheight)
			return -1;
		
		var cell = this.cellAt(x_, y_);
		
		if (!cell)
			return -1;
		
		return cell.tiles[y_ % this.cellheight][x_ % this.cellwidth];
	};
	
	instanceProto.setTileAt = function (x_, y_, t_)
	{
		x_ = Math.floor(x_);
		y_ = Math.floor(y_);
		
		if (x_ < 0 || y_ < 0 || x_ >= this.mapwidth || y_ >= this.mapheight)
			return -1;
			
		var cell = this.cellAt(x_, y_);
		
		if (!cell)
			return -1;
		
		cell.setTileAt(x_ % this.cellwidth, y_ % this.cellheight, t_);
	};
	
	instanceProto.worldToCellX = function (x)
	{
		return Math.floor((x - this.x) / (this.cellwidth * this.tilewidth));
	};
	
	instanceProto.worldToCellY = function (y)
	{
		return Math.floor((y - this.y) / (this.cellheight * this.tileheight));
	};
	
	instanceProto.worldToTileX = function (x)
	{
		return Math.floor((x - this.x) / this.tilewidth)
	};
	
	instanceProto.worldToTileY = function (y)
	{
		return Math.floor((y - this.y) / this.tileheight);
	};
	
	instanceProto.getCollisionRectCandidates = function (bbox, candidates)
	{
		var firstCellX = this.worldToCellX(bbox.left);
		var firstCellY = this.worldToCellY(bbox.top);
		var lastCellX = this.worldToCellX(bbox.right);
		var lastCellY = this.worldToCellY(bbox.bottom);
		
		var cx, cy, cell;
		
		for (cx = firstCellX; cx <= lastCellX; ++cx)
		{
			for (cy = firstCellY; cy <= lastCellY; ++cy)
			{
				cell = this.cellAtIndex(cx, cy);
				
				if (!cell)
					continue;
				
				cell.maybeBuildQuadMap();
				cr.appendArray(candidates, cell.collision_rects);
			}
		}
	};
	
	instanceProto.testPointOverlapTile = function (x, y)
	{
		var tx = this.worldToTileX(x);
		var ty = this.worldToTileY(y);
		var tile = this.getTileAt(tx, ty);
		
		if (tile === -1)
			return false;		// empty tile here
		
		// Get tile poly from ID
		var poly = this.type.getTilePoly(tile);
		
		if (!poly)
			return true;		// no poly; whole tile registers overlap
		
		if (!this.type.isTilePolyEnabled(poly)) // disabled poly, no overlap
			return false;
		
		// Offset so point is relative to tile and test against poly
		var tileStartX = (Math.floor((x - this.x) / this.tilewidth) * this.tilewidth) + this.x;
		var tileStartY = (Math.floor((y - this.y) / this.tileheight) * this.tileheight) + this.y;
		x -= tileStartX;
		y -= tileStartY;
		return poly.contains_pt(x, y);
	};
	
	instanceProto.getAllCollisionRects = function (candidates)
	{
		var i, len, j, lenj, row, cell;
		
		for (i = 0, len = this.tilecells.length; i < len; ++i)
		{
			row = this.tilecells[i];
			
			for (j = 0, lenj = row.length; j < lenj; ++j)
			{
				cell = row[j];
				cell.maybeBuildQuadMap();
				cr.appendArray(candidates, cell.collision_rects);
			}
		}
	};
	
	// called whenever an instance is destroyed
	// note the runtime may keep the object after this call for recycling; be sure
	// to release/recycle/reset any references to other objects in this function.
	instanceProto.onDestroy = function ()
	{
		// Recycle all existing tile cells
		var i, len, j, lenj, arr;
		
		for (i = 0, len = this.tilecells.length; i < len; ++i)
		{
			arr = this.tilecells[i];
			
			for (j = 0, lenj = arr.length; j < lenj; ++j)
			{
				freeTileCell(arr[j]);
			}
			
			cr.clearArray(arr);
		}
		
		cr.clearArray(this.tilecells);
	};
	
	// called when saving the full state of the game
	instanceProto.saveToJSON = function ()
	{
		this.maybeResizeTilemap();
		var curwidth = cr.floor(this.width / this.tilewidth);
		var curheight = cr.floor(this.height / this.tileheight);
		
		// return a Javascript object containing information about your object's state
		// note you MUST use double-quote syntax (e.g. "property": value) to prevent
		// Closure Compiler renaming and breaking the save format
		return {
			"w": curwidth,
			"h": curheight,
			"d": this.getTilesAsRLECSV()
		};
	};
	
	// called when loading the full state of the game
	instanceProto.loadFromJSON = function (o)
	{
		this.mapwidth = o["w"];
		this.mapheight = o["h"];
		this.maybeResizeTilemap(true);
		this.setTilesFromRLECSV(o["d"]);
		
		this.physics_changed = true;
		this.setAllQuadMapChanged();
	};
	
	// only called if a layout object - draw to a canvas 2D context
	instanceProto.draw = function(ctx)
	{
		if (this.tilewidth <= 0 || this.tileheight <= 0)
			return;
		
		this.type.maybeCutTiles(this.tilewidth, this.tileheight, this.tilexoffset, this.tileyoffset, this.tilexspacing, this.tileyspacing, this.seamless);
		
		// If size changed, rebuild the map so it only extends as far as the object bounds
		if (this.width !== this.lastwidth || this.height !== this.lastheight)
		{
			this.physics_changed = true;
			this.setAllQuadMapChanged();
			this.maybeBuildAllQuadMap();
			this.lastwidth = this.width;
			this.lastheight = this.height;
		}

		ctx.globalAlpha = this.opacity;
		//ctx.strokeStyle = "red";
		
		var layer = this.layer;
		var viewLeft = layer.viewLeft;
		var viewTop = layer.viewTop;
		var viewRight = layer.viewRight;
		var viewBottom = layer.viewBottom;
		
		var myx = this.x;
		var myy = this.y;
		var seamless = this.seamless;
		var qrc;
		
		if (this.runtime.pixel_rounding)
		{
			myx = Math.round(myx);
			myy = Math.round(myy);
		}
		
		var cellWidthPx = this.cellwidth * this.tilewidth;
		var cellHeightPx = this.cellheight * this.tileheight;
		
		var firstCellX = Math.floor((viewLeft - myx) / cellWidthPx);
		var lastCellX = Math.floor((viewRight - myx) / cellWidthPx);
		var firstCellY = Math.floor((viewTop - myy) / cellHeightPx);
		var lastCellY = Math.floor((viewBottom - myy) / cellHeightPx);
		
		// The canvas pattern API is complete nonsense. Fudge the transform so it draws in the
		// right darn place.
		var offx = myx % this.tilewidth;
		var offy = myy % this.tileheight;
		
		if (this.seamless)
		{
			offx = 0;
			offy = 0;
		}
		
		if (offx !== 0 || offy !== 0)
		{
			ctx.save();
			ctx.translate(offx, offy);
			myx -= offx;
			myy -= offy;
			viewLeft -= offx;
			viewTop -= offy;
			viewRight -= offx;
			viewBottom -= offy;
		}
		
		var cx, cy, cell, i, len, q, qleft, qtop, qright, qbottom, img;
		for (cx = firstCellX; cx <= lastCellX; ++cx)
		{
			for (cy = firstCellY; cy <= lastCellY; ++cy)
			{
				cell = this.cellAtIndex(cx, cy);
				
				if (!cell)
					continue;
				
				cell.maybeBuildQuadMap();
				
				for (i = 0, len = cell.quads.length; i < len; ++i)
				{
					q = cell.quads[i];
				
					if (q.id === -1)
						continue;
					
					qrc = q.rc;
					qleft = qrc.left + myx;
					qtop = qrc.top + myy;
					qright = qrc.right + myx;
					qbottom = qrc.bottom + myy;
					
					if (qleft > viewRight || qright < viewLeft || qtop > viewBottom || qbottom < viewTop)
						continue;
					
					img = this.type.GetFlippedTileImage(q.tileid, q.horiz_flip, q.vert_flip, q.diag_flip, this.seamless);
					
					// Seamless mode returns a canvas and drawImage's it; otherwise fill the repeating pattern
					if (seamless)
					{
						ctx.drawImage(img, qleft, qtop);
					}
					else
					{
						ctx.fillStyle = this.type.GetFlippedTileImage(q.tileid, q.horiz_flip, q.vert_flip, q.diag_flip, this.seamless);
						ctx.fillRect(qleft, qtop, qright - qleft, qbottom - qtop);
					}
					
					// Outline quads for debug
					//ctx.strokeRect(qleft, qtop, qright - qleft, qbottom - qtop);
				}
				
				/*
				// Highlight collision rects for debug
				for (i = 0, len = cell.collision_rects.length; i < len; ++i)
				{
					qrc = cell.collision_rects[i].rc;
					qleft = qrc.left + myx;
					qtop = qrc.top + myy;
					qright = qrc.right + myx;
					qbottom = qrc.bottom + myy;
					ctx.strokeRect(qleft, qtop, qright - qleft, qbottom - qtop);
				}
				*/
			}
		}
		
		if (offx !== 0 || offy !== 0)
			ctx.restore();
	};
	
	// only called if a layout object in WebGL mode - draw to the WebGL context
	// 'glw' is not a WebGL context, it's a wrapper - you can find its methods in GLWrap.js in the install
	// directory or just copy what other plugins do.
	var tmp_rect = new cr.rect(0, 0, 1, 1);
	
	instanceProto.drawGL_earlyZPass = function(glw)
	{
		this.drawGL(glw);
	};
	
	instanceProto.drawGL = function (glw)
	{
		if (this.tilewidth <= 0 || this.tileheight <= 0)
			return;
		
		this.type.maybeCutTiles(this.tilewidth, this.tileheight, this.tilexoffset, this.tileyoffset, this.tilexspacing, this.tileyspacing, this.seamless);
		
		// If size changed, rebuild the map so it only extends as far as the object bounds
		if (this.width !== this.lastwidth || this.height !== this.lastheight)
		{
			this.physics_changed = true;
			this.setAllQuadMapChanged();
			this.maybeBuildAllQuadMap();
			this.lastwidth = this.width;
			this.lastheight = this.height;
		}
		
		glw.setOpacity(this.opacity);
		
		var cut_tiles = this.type.cut_tiles;
		
		var layer = this.layer;
		var viewLeft = layer.viewLeft;
		var viewTop = layer.viewTop;
		var viewRight = layer.viewRight;
		var viewBottom = layer.viewBottom;
		
		var myx = this.x;
		var myy = this.y;
		var qrc;
		
		if (this.runtime.pixel_rounding)
		{
			myx = Math.round(myx);
			myy = Math.round(myy);
		}
		
		var cellWidthPx = this.cellwidth * this.tilewidth;
		var cellHeightPx = this.cellheight * this.tileheight;
		
		var firstCellX = Math.floor((viewLeft - myx) / cellWidthPx);
		var lastCellX = Math.floor((viewRight - myx) / cellWidthPx);
		var firstCellY = Math.floor((viewTop - myy) / cellHeightPx);
		var lastCellY = Math.floor((viewBottom - myy) / cellHeightPx);
		
		var i, len, q, qleft, qtop, qright, qbottom;
		var qtlx, qtly, qtrx, qtry, qbrx, qbry, qblx, qbly, temp;
		var cx, cy, cell;
		
		for (cx = firstCellX; cx <= lastCellX; ++cx)
		{
			for (cy = firstCellY; cy <= lastCellY; ++cy)
			{
				cell = this.cellAtIndex(cx, cy);
				
				if (!cell)
					continue;
				
				cell.maybeBuildQuadMap();
		
				for (i = 0, len = cell.quads.length; i < len; ++i)
				{
					q = cell.quads[i];
					
					if (q.id === -1)
						continue;
					
					qrc = q.rc;
					qleft = qrc.left + myx;
					qtop = qrc.top + myy;
					qright = qrc.right + myx;
					qbottom = qrc.bottom + myy;
					
					if (qleft > viewRight || qright < viewLeft || qtop > viewBottom || qbottom < viewTop)
						continue;
					
					glw.setTexture(cut_tiles[q.tileid]);
					tmp_rect.right = (qright - qleft) / this.tilewidth;
					tmp_rect.bottom = (qbottom - qtop) / this.tileheight;
					
					if (q.any_flip)
					{
						if (q.diag_flip)
						{
							temp = tmp_rect.right;
							tmp_rect.right = tmp_rect.bottom;
							tmp_rect.bottom = temp;
						}
						
						qtlx = 0;
						qtly = 0;
						qtrx = tmp_rect.right;
						qtry = 0;
						qbrx = tmp_rect.right;
						qbry = tmp_rect.bottom;
						qblx = 0;
						qbly = tmp_rect.bottom;
						
						if (q.diag_flip)
						{
							// Diagonal flip: swap topright and bottomleft points
							temp = qblx;		qblx = qtrx;		qtrx = temp;
							temp = qbly;		qbly = qtry;		qtry = temp;
						}
						
						if (q.horiz_flip)
						{
							// Horizontal flip: swap topleft and topright; bottomleft and bottomright points
							temp = qtlx;		qtlx = qtrx;		qtrx = temp;
							temp = qtly;		qtly = qtry;		qtry = temp;
							
							temp = qblx;		qblx = qbrx;		qbrx = temp;
							temp = qbly;		qbly = qbry;		qbry = temp;
						}
						
						if (q.vert_flip)
						{
							// Vertical flip: swap topleft and bottomleft; topright and bottomright points
							temp = qtlx;		qtlx = qblx;		qblx = temp;
							temp = qtly;		qtly = qbly;		qbly = temp;
							
							temp = qtrx;		qtrx = qbrx;		qbrx = temp;
							temp = qtry;		qtry = qbry;		qbry = temp;
						}
						
						glw.quadTexUV(qleft, qtop, qright, qtop, qright, qbottom, qleft, qbottom, qtlx, qtly, qtrx, qtry, qbrx, qbry, qblx, qbly);
					}
					else
					{
						// Normal render without any flip
						glw.quadTex(qleft, qtop, qright, qtop, qright, qbottom, qleft, qbottom, tmp_rect);
					}
				}
			}
		}
	};
	
	// The comments around these functions ensure they are removed when exporting, since the
	// debugger code is no longer relevant after publishing.

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

	Cnds.prototype.CompareTileAt = function (tx, ty, cmp, t)
	{
		var tile = this.getTileAt(tx, ty);
		
		if (tile !== -1)
			tile = (tile & TILE_ID_MASK);
		
		return cr.do_cmp(tile, cmp, t);
	};

	function StateComboToFlags(state)
	{
		switch (state) {
		case 0:		// normal
			return 0;
		case 1:		// flipped horizontal
			return TILE_FLIPPED_HORIZONTAL;
		case 2:		// flipped vertical
			return TILE_FLIPPED_VERTICAL;
		case 3:		// rotated 90
			return TILE_FLIPPED_HORIZONTAL | TILE_FLIPPED_DIAGONAL;
		case 4:		// rotated 180
			return TILE_FLIPPED_HORIZONTAL | TILE_FLIPPED_VERTICAL;
		case 5:		// rotated 270
			return TILE_FLIPPED_VERTICAL | TILE_FLIPPED_DIAGONAL;
		case 6:		// rotated 90, flipped vertical
			return TILE_FLIPPED_HORIZONTAL | TILE_FLIPPED_VERTICAL | TILE_FLIPPED_DIAGONAL;
		case 7:		// rotated 270, flipped vertical
			return TILE_FLIPPED_DIAGONAL;
		default:
			return 0;
		}
	};
	
	Cnds.prototype.CompareTileStateAt = function (tx, ty, state)
	{
		var tile = this.getTileAt(tx, ty);
		var flags = 0;
		
		if (tile !== -1)
			flags = (tile & TILE_FLAGS_MASK);
		
		return flags === StateComboToFlags(state);
	};
	
	Cnds.prototype.OnURLLoaded = function ()
	{
		return true;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.EraseTile = function (tx, ty)
	{
		this.maybeResizeTilemap();
		this.setTileAt(tx, ty, -1);
	};
	
	Acts.prototype.SetTile = function (tx, ty, t, state)
	{
		this.maybeResizeTilemap();
		this.setTileAt(tx, ty, (t & TILE_ID_MASK) | StateComboToFlags(state));
	};
	
	Acts.prototype.SetTileState = function (tx, ty, state)
	{
		var t = this.getTileAt(tx, ty);
		if (t !== -1)
		{
			this.maybeResizeTilemap();
			this.setTileAt(tx, ty, (t & TILE_ID_MASK) | StateComboToFlags(state));
		}
	};
	
	Acts.prototype.EraseTileRange = function (tx, ty, tw, th)
	{
		var fromx = Math.floor(cr.max(tx, 0));
		var fromy = Math.floor(cr.max(ty, 0));
		var tox = Math.floor(cr.min(tx + tw, this.mapwidth));
		var toy = Math.floor(cr.min(ty + th, this.mapheight));
		
		var x, y;
		for (y = fromy; y < toy; ++y)
		{
			for (x = fromx; x < tox; ++x)
			{
				this.setTileAt(x, y, -1);
			}
		}
	};
	
	Acts.prototype.SetTileRange = function (tx, ty, tw, th, t, state)
	{
		this.maybeResizeTilemap();
		
		var fromx = Math.floor(cr.max(tx, 0));
		var fromy = Math.floor(cr.max(ty, 0));
		var tox = Math.floor(cr.min(tx + tw, this.mapwidth));
		var toy = Math.floor(cr.min(ty + th, this.mapheight));
		
		var settile = (t & TILE_ID_MASK) | StateComboToFlags(state);
		
		var x, y;
		for (y = fromy; y < toy; ++y)
		{
			for (x = fromx; x < tox; ++x)
			{
				this.setTileAt(x, y, settile);
			}
		}
	};
	
	Acts.prototype.SetTileStateRange = function (tx, ty, tw, th, state)
	{
		this.maybeResizeTilemap();
		
		var fromx = Math.floor(cr.max(tx, 0));
		var fromy = Math.floor(cr.max(ty, 0));
		var tox = Math.floor(cr.min(tx + tw, this.mapwidth));
		var toy = Math.floor(cr.min(ty + th, this.mapheight));
		
		var setstate = StateComboToFlags(state);
		
		var x, y, t;
		for (y = fromy; y < toy; ++y)
		{
			for (x = fromx; x < tox; ++x)
			{
				t = this.getTileAt(x, y);
				
				if (t !== -1)
					this.setTileAt(x, y, (t & TILE_ID_MASK) | setstate);
			}
		}
	};
	
	Acts.prototype.LoadFromJSON = function (str)
	{
		var o;
		
		try {
			o = JSON.parse(str);
		}
		catch (e) {
			return;
		}
		
		if (!o["c2tilemap"])
			return;		// not a known tilemap data format
		
		this.mapwidth = o["width"];
		this.mapheight = o["height"];
		this.maybeResizeTilemap(true);
		this.setTilesFromRLECSV(o["data"]);
		this.setAllQuadMapChanged();
		this.physics_changed = true;
	};
	
	Acts.prototype.JSONDownload = function (filename)
	{
		var a = document.createElement("a");
		
		var o = {
			"c2tilemap": true,
			"width": this.mapwidth,
			"height": this.mapheight,
			"data": this.getTilesAsRLECSV()
		};
		
		if (typeof a.download === "undefined")
		{
			var str = 'data:text/html,' + encodeURIComponent("<p><a download='data.json' href=\"data:application/json,"
				+ encodeURIComponent(JSON.stringify(o))
				+ "\">Download link</a></p>");
			window.open(str);
		}
		else
		{
			// auto download
			var body = document.getElementsByTagName("body")[0];
			a.textContent = filename;
			a.href = "data:application/json," + encodeURIComponent(JSON.stringify(o));
			a.download = filename;
			body.appendChild(a);
			a.click();
			body.removeChild(a);
		}
	};
	
	Acts.prototype.LoadURL = function (url_, crossOrigin_)
	{
		var img = new Image();
		var self = this;
		
		img.onload = function ()
		{
			var type = self.type;
			type.freeCutTiles();
			type.texture_img = img;
			type.spriteX = 0;
			type.spriteY = 0;
			type.spriteWidth = img.width;
			type.spriteHeight = img.height;
			self.runtime.redraw = true;
			self.runtime.trigger(cr.plugins_.Tilemap.prototype.cnds.OnURLLoaded, self);
		};
		
		if (url_.substr(0, 5) !== "data:" && crossOrigin_ === 0)
			img.crossOrigin = "anonymous";
		
		// use runtime function to work around WKWebView permissions
		this.runtime.setImageSrc(img, url_);
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};

	Exps.prototype.TileAt = function (ret, tx, ty)
	{
		var tile = this.getTileAt(tx, ty);
		ret.set_int(tile === -1 ? -1 : (tile & TILE_ID_MASK));
	};
	
	Exps.prototype.PositionToTileX = function (ret, x_)
	{
		ret.set_float(this.worldToTileX(x_));
	};
	
	Exps.prototype.PositionToTileY = function (ret, y_)
	{
		ret.set_float(this.worldToTileY(y_));
	};
	
	Exps.prototype.TileToPositionX = function (ret, x_)
	{
		ret.set_float((x_ * this.tilewidth) + this.x + (this.tilewidth / 2));
	};
	
	Exps.prototype.TileToPositionY = function (ret, y_)
	{
		ret.set_float((y_ * this.tileheight) + this.y + (this.tileheight / 2));
	};
	
	Exps.prototype.SnapX = function (ret, x_)
	{
		ret.set_float((Math.floor((x_ - this.x) / this.tilewidth) * this.tilewidth) + this.x + (this.tilewidth / 2));
	};
	
	Exps.prototype.SnapY = function (ret, y_)
	{
		ret.set_float((Math.floor((y_ - this.y) / this.tileheight) * this.tileheight) + this.y + (this.tileheight / 2));
	};
	
	Exps.prototype.TilesJSON = function (ret)
	{
		this.maybeResizeTilemap();
		var curwidth = cr.floor(this.width / this.tilewidth);
		var curheight = cr.floor(this.height / this.tileheight);
		
		ret.set_string(JSON.stringify({
			"c2tilemap": true,
			"width": curwidth,
			"height": curheight,
			"data": this.getTilesAsRLECSV()
		}));
	};
	
	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 < leni; i++)
		{
			anim = this.animations[i];
			animobj = {};
			animobj.name = anim[0];
			animobj.speed = anim[1];
			animobj.loop = anim[2];
			animobj.repeatcount = anim[3];
			animobj.repeatto = anim[4];
			animobj.pingpong = anim[5];
			animobj.sid = anim[6];
			animobj.frames = [];
			
			for (j = 0, lenj = anim[7].length; j < lenj; j++)
			{
				frame = anim[7][j];
				frameobj = {};
				frameobj.texture_file = frame[0];
				frameobj.texture_filesize = frame[1];
				frameobj.offx = frame[2];
				frameobj.offy = frame[3];
				frameobj.width = frame[4];
				frameobj.height = frame[5];
				frameobj.duration = frame[6];
				frameobj.hotspotX = frame[7];
				frameobj.hotspotY = frame[8];
				frameobj.image_points = frame[9];
				frameobj.poly_pts = frame[10];
				frameobj.pixelformat = frame[11];
				frameobj.spritesheeted = (frameobj.width !== 0);
				frameobj.datauri = "";		// generated on demand and cached
				frameobj.getDataUri = frame_getDataUri;
				
				uv = {};
				uv.left = 0;
				uv.top = 0;
				uv.right = 1;
				uv.bottom = 1;
				frameobj.sheetTex = uv;
				
				frameobj.webGL_texture = null;
				
				// Sprite sheets may mean multiple frames reference one image
				// Ensure image is not created in duplicate
				wt = this.runtime.findWaitingTexture(frame[0]);
				
				if (wt)
				{
					frameobj.texture_img = wt;
				}
				else
				{
					frameobj.texture_img = new Image();
					frameobj.texture_img.cr_src = frame[0];
					frameobj.texture_img.cr_filesize = frame[1];
					frameobj.texture_img.c2webGL_texture = null;
					
					// Tell runtime to wait on this texture
					this.runtime.waitForImageLoad(frameobj.texture_img, frame[0]);
				}
				
				cr.seal(frameobj);
				animobj.frames.push(frameobj);
				this.all_frames.push(frameobj);
			}
			
			cr.seal(animobj);
			this.animations[i] = animobj;		// swap array data for object
		}
	};
	
	typeProto.updateAllCurrentTexture = function ()
	{
		var i, len, inst;
		for (i = 0, len = this.instances.length; i < len; i++)
		{
			inst = this.instances[i];
			inst.curWebGLTexture = inst.curFrame.webGL_texture;
		}
	};
	
	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;
			
		var i, len, frame;
		
		// Release all animation frames
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			frame.texture_img.c2webGL_texture = null;
			frame.webGL_texture = null;
		}
		
		this.has_loaded_textures = false;
		
		this.updateAllCurrentTexture();
	};
	
	typeProto.onRestoreWebGLContext = function ()
	{
		// No need to create textures if no instances exist, will create on demand
		if (this.is_family || !this.instances.length)
			return;
			
		var i, len, frame;
		
		// Re-load all animation frames
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			
			frame.webGL_texture = this.runtime.glwrap.loadTexture(frame.texture_img, false, this.runtime.linearSampling, frame.pixelformat);
		}
		
		this.updateAllCurrentTexture();
	};
	
	typeProto.loadTextures = function ()
	{
		if (this.is_family || this.has_loaded_textures || !this.runtime.glwrap)
			return;
			
		var i, len, frame;
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			
			frame.webGL_texture = this.runtime.glwrap.loadTexture(frame.texture_img, false, this.runtime.linearSampling, frame.pixelformat);
		}
		
		this.has_loaded_textures = true;
	};
	
	typeProto.unloadTextures = function ()
	{
		// Don't release textures if any instances still exist, they are probably using them
		if (this.is_family || this.instances.length || !this.has_loaded_textures)
			return;
			
		var i, len, frame;
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frame = this.all_frames[i];
			
			this.runtime.glwrap.deleteTexture(frame.webGL_texture);
			frame.webGL_texture = null;
		}
		
		this.has_loaded_textures = false;
	};
	
	var already_drawn_images = [];
	
	typeProto.preloadCanvas2D = function (ctx)
	{
		var i, len, frameimg;
		cr.clearArray(already_drawn_images);
		
		for (i = 0, len = this.all_frames.length; i < len; ++i)
		{
			frameimg = this.all_frames[i].texture_img;
			
			if (already_drawn_images.indexOf(frameimg) !== -1)
					continue;
				
			// draw to preload, browser should lazy load the texture
			ctx.drawImage(frameimg, 0, 0);
			already_drawn_images.push(frameimg);
		}
	};

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

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

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

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

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

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

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

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

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

}());

// Tiled Background
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.TiledBg.prototype;
		
	/////////////////////////////////////
	// 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.pattern = null;
		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, true, this.runtime.linearSampling, this.texture_pixelformat);
		}
		
		var i, len;
		for (i = 0, len = this.instances.length; i < len; i++)
			this.instances[i].webGL_texture = this.webGL_texture;
	};
	
	typeProto.loadTextures = function ()
	{
		if (this.is_family || this.webGL_texture || !this.runtime.glwrap)
			return;
			
		this.webGL_texture = this.runtime.glwrap.loadTexture(this.texture_img, true, this.runtime.linearSampling, this.texture_pixelformat);
	};
	
	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.onCreate = function()
	{
		this.visible = this.properties[0];									// 0=visible, 1=invisible
		this.rcTex = new cr.rect(0, 0, 0, 0);
		
		this.has_own_texture = false;										// true if a texture loaded in from URL
		this.texture_img = this.type.texture_img;
		
		if (this.runtime.glwrap)
		{
			// Create WebGL texture if type doesn't have it yet
			this.type.loadTextures();
			
			this.webGL_texture = this.type.webGL_texture;
		}
		else
		{
			// Create the pattern if the type doesn't have one yet
			if (!this.type.pattern)
				this.type.pattern = this.runtime.ctx.createPattern(this.type.texture_img, "repeat");
			
			this.pattern = this.type.pattern;
		}
	};
	
	instanceProto.afterLoad = function ()
	{
		// If has own texture, reset to normal texture since the texture isn't saved with the state
		this.has_own_texture = false;
		this.texture_img = this.type.texture_img;
	};
	
	instanceProto.onDestroy = function ()
	{
		// WebGL renderer: if we have our own texture (via load from URL), delete it now
		if (this.runtime.glwrap && this.has_own_texture && this.webGL_texture)
		{
			this.runtime.glwrap.deleteTexture(this.webGL_texture);
			this.webGL_texture = null;
		}
	};

	instanceProto.draw = function(ctx)
	{
		ctx.globalAlpha = this.opacity;
			
		ctx.save();
		
		ctx.fillStyle = this.pattern;
		
		var myx = this.x;
		var myy = this.y;
		
		if (this.runtime.pixel_rounding)
		{
			myx = Math.round(myx);
			myy = Math.round(myy);
		}
		
		// Patterns tile from the origin no matter where you draw from.
		// Translate the canvas to align with the draw position, then offset the draw position as well.
		var drawX = -(this.hotspotX * this.width);
		var drawY = -(this.hotspotY * this.height);
		
		var offX = drawX % this.texture_img.width;
		var offY = drawY % this.texture_img.height;
		if (offX < 0)
			offX += this.texture_img.width;
		if (offY < 0)
			offY += this.texture_img.height;
			
		ctx.translate(myx, myy);
		ctx.rotate(this.angle);
		ctx.translate(offX, offY);
		
		ctx.fillRect(drawX - offX,
					 drawY - offY,
					 this.width,
					 this.height);
		
		ctx.restore();
	};
	
	instanceProto.drawGL_earlyZPass = function(glw)
	{
		this.drawGL(glw);
	};
	
	instanceProto.drawGL = function(glw)
	{
		glw.setTexture(this.webGL_texture);
		glw.setOpacity(this.opacity);
		
		var rcTex = this.rcTex;
		rcTex.right = this.width / this.texture_img.width;
		rcTex.bottom = this.height / this.texture_img.height;
		
		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;
			
			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, rcTex);
		}
		else
			glw.quadTex(q.tlx, q.tly, q.trx, q.try_, q.brx, q.bry, q.blx, q.bly, rcTex);
	};

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.OnURLLoaded = function ()
	{
		return true;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	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.LoadURL = function (url_, crossOrigin_)
	{
		var img = new Image();
		var self = this;
		
		img.onload = function ()
		{
			self.texture_img = img;
			
			// WebGL renderer
			if (self.runtime.glwrap)
			{
				// Delete any previous own texture
				if (self.has_own_texture && self.webGL_texture)
					self.runtime.glwrap.deleteTexture(self.webGL_texture);
					
				self.webGL_texture = self.runtime.glwrap.loadTexture(img, true, self.runtime.linearSampling);
			}
			// Canvas2D renderer
			else
			{
				self.pattern = self.runtime.ctx.createPattern(img, "repeat");
			}
			
			self.has_own_texture = true;
			self.runtime.redraw = true;
			self.runtime.trigger(cr.plugins_.TiledBg.prototype.cnds.OnURLLoaded, self);
		};
		
		if (url_.substr(0, 5) !== "data:" && crossOrigin_ === 0)
			img.crossOrigin = "anonymous";
		
		// use runtime function to work around WKWebView permissions
		this.runtime.setImageSrc(img, url_);
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.ImageWidth = function (ret)
	{
		ret.set_float(this.texture_img.width);
	};
	
	Exps.prototype.ImageHeight = function (ret)
	{
		ret.set_float(this.texture_img.height);
	};
	
	pluginProto.exps = new Exps();

}());

// 9-patch
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.NinePatch.prototype;
		
	/////////////////////////////////////
	// 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;
		
		// Create the texture
		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.fillPattern = null;
		this.leftPattern = null;
		this.rightPattern = null;
		this.topPattern = null;
		this.bottomPattern = null;
		
		this.webGL_texture = null;
		this.webGL_fillTexture = null;
		this.webGL_leftTexture = null;
		this.webGL_rightTexture = null;
		this.webGL_topTexture = null;
		this.webGL_bottomTexture = null;
	};
	
	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;
			
		this.webGL_texture = null;
		this.webGL_fillTexture = null;
		this.webGL_leftTexture = null;
		this.webGL_rightTexture = null;
		this.webGL_topTexture = null;
		this.webGL_bottomTexture = 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, true, this.runtime.linearSampling, this.texture_pixelformat);
		}
	};
	
	typeProto.unloadTextures = function ()
	{
		// Don't release textures if any instances still exist, they are probably using them
		if (this.is_family || this.instances.length)
			return;
		
		// WebGL renderer
		if (this.runtime.glwrap)
		{
			this.runtime.glwrap.deleteTexture(this.webGL_texture);
			this.runtime.glwrap.deleteTexture(this.webGL_fillTexture);
			this.runtime.glwrap.deleteTexture(this.webGL_leftTexture);
			this.runtime.glwrap.deleteTexture(this.webGL_rightTexture);
			this.runtime.glwrap.deleteTexture(this.webGL_topTexture);
			this.runtime.glwrap.deleteTexture(this.webGL_bottomTexture);
			
			this.webGL_texture = null;
			this.webGL_fillTexture = null;
			this.webGL_leftTexture = null;
			this.webGL_rightTexture = null;
			this.webGL_topTexture = null;
			this.webGL_bottomTexture = null;
		}
	};
	
	typeProto.slicePatch = function (x1, y1, x2, y2)
	{
		var offx = this.texture_data[3];		// spritesheet offset X
		var offy = this.texture_data[4];		// spritesheet offset Y
		
		var tmpcanvas = document.createElement("canvas");
		var w = x2 - x1;
		var h = y2 - y1;
		tmpcanvas.width = w;
		tmpcanvas.height = h;
		var tmpctx = tmpcanvas.getContext("2d");
		tmpctx.drawImage(this.texture_img, x1 + offx, y1 + offy, w, h, 0, 0, w, h);
		return tmpcanvas;
	};
	
	typeProto.createPatch = function (lm, rm, tm, bm)
	{
		var iw = this.texture_data[5];			// spritesheeted width
		var ih = this.texture_data[6];			// spritesheeted height
		var re = iw - rm;
		var be = ih - bm;
		
		if (this.runtime.glwrap)
		{
			if (this.webGL_fillTexture)
				return;		// already created
				
			var glwrap = this.runtime.glwrap;
			var ls = this.runtime.linearSampling;
			var tf = this.texture_pixelformat;
			
			if (re > lm && be > tm)
				this.webGL_fillTexture = glwrap.loadTexture(this.slicePatch(lm, tm, re, be), true, ls, tf);
			
			if (lm > 0 && be > tm)
				this.webGL_leftTexture = glwrap.loadTexture(this.slicePatch(0, tm, lm, be), true, ls, tf, "repeat-y");
			if (rm > 0 && be > tm)
				this.webGL_rightTexture = glwrap.loadTexture(this.slicePatch(re, tm, iw, be), true, ls, tf, "repeat-y");
			if (tm > 0 && re > lm)
				this.webGL_topTexture = glwrap.loadTexture(this.slicePatch(lm, 0, re, tm), true, ls, tf, "repeat-x");
			if (bm > 0 && re > lm)
				this.webGL_bottomTexture = glwrap.loadTexture(this.slicePatch(lm, be, re, ih), true, ls, tf, "repeat-x");
		}
		else
		{
			if (this.fillPattern)
				return;		// already created
			
			var ctx = this.runtime.ctx;
			
			if (re > lm && be > tm)
				this.fillPattern = ctx.createPattern(this.slicePatch(lm, tm, re, be), "repeat");
			
			if (lm > 0 && be > tm)
				this.leftPattern = ctx.createPattern(this.slicePatch(0, tm, lm, be), "repeat");
			if (rm > 0 && be > tm)
				this.rightPattern = ctx.createPattern(this.slicePatch(re, tm, iw, be), "repeat");
			if (tm > 0 && re > lm)
				this.topPattern = ctx.createPattern(this.slicePatch(lm, 0, re, tm), "repeat");
			if (bm > 0 && re > lm)
				this.bottomPattern = ctx.createPattern(this.slicePatch(lm, be, re, ih), "repeat");
		}
	};

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

	instanceProto.onCreate = function()
	{
		this.leftMargin = this.properties[0];
		this.rightMargin = this.properties[1];
		this.topMargin = this.properties[2];
		this.bottomMargin = this.properties[3];
		this.edges = this.properties[4];					// 0=tile, 1=stretch
		this.fill = this.properties[5];						// 0=tile, 1=stretch, 2=transparent
		this.visible = this.properties[6];
		this.seamless = (this.properties[8] !== 0);			// 1px overdraw to hide seams
		
		if (this.recycled)
			this.rcTex.set(0, 0, 0, 0);
		else
			this.rcTex = new cr.rect(0, 0, 0, 0);
		
		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);
			}
		}
		
		// lazy create patches
		this.type.createPatch(this.leftMargin, this.rightMargin, this.topMargin, this.bottomMargin);
	};
	
	// patterns tile their image content relative to the origin, which is really
	// annoying for drawing tiled content.  Work around to tile from draw co-ordinates
	// using translate.
	function drawPatternProperly(ctx, pattern, pw, ph, drawX, drawY, w, h, ox, oy)
	{
		ctx.save();
		ctx.fillStyle = pattern;
		
		var offX = drawX % pw;
		var offY = drawY % ph;
		if (offX < 0)
			offX += pw;
		if (offY < 0)
			offY += ph;
			
		ctx.translate(offX + ox, offY + oy);
		
		ctx.fillRect(drawX - offX - ox, drawY - offY - oy, w, h);
		
		ctx.restore();
	};

	instanceProto.draw = function(ctx)
	{
		var img = this.type.texture_img;
		var lm = this.leftMargin;
		var rm = this.rightMargin;
		var tm = this.topMargin;
		var bm = this.bottomMargin;
		var iw = this.type.texture_data[5];			// spritesheeted width
		var ih = this.type.texture_data[6];			// spritesheeted height
		var ssoffx = this.type.texture_data[3];		// spritesheet offset X
		var ssoffy = this.type.texture_data[4];		// spritesheet offset Y
		var re = iw - rm;
		var be = ih - bm;
		
		ctx.globalAlpha = this.opacity;
			
		ctx.save();
		
		var myx = this.x;
		var myy = this.y;
		var myw = this.width;
		var myh = this.height;
		
		if (this.runtime.pixel_rounding)
		{
			myx = Math.round(myx);
			myy = Math.round(myy);
		}
		
		// Patterns tile from the origin no matter where you draw from.
		// Translate the canvas to align with the draw position, then offset the draw position as well.
		var drawX = -(this.hotspotX * this.width);
		var drawY = -(this.hotspotY * this.height);
		
		var offX = drawX % iw;
		var offY = drawY % ih;
		if (offX < 0)
			offX += iw;
		if (offY < 0)
			offY += ih;
			
		ctx.translate(myx + offX, myy + offY);
		
		var x = drawX - offX;
		var y = drawY - offY;
		
		var s = (this.seamless ? 1 : 0);
		
		// draw top-left, top-right, bottom-right and bottom-left corners
		// include an extra 1px from source image to avoid seams
		if (lm > 0 && tm > 0)
			ctx.drawImage(img, ssoffx, ssoffy, lm + s, tm + s, x, y, lm + s, tm + s);
			
		if (rm > 0 && tm > 0)
			ctx.drawImage(img, ssoffx + re - s, ssoffy, rm + s, tm + s, x + myw - rm - s, y, rm + s, tm + s);
			
		if (rm > 0 && bm > 0)
			ctx.drawImage(img, ssoffx + re - s, ssoffy + be - s, rm + s, bm + s, x + myw - rm - s, y + myh - bm - s, rm + s, bm + s);
			
		if (lm > 0 && bm > 0)
			ctx.drawImage(img, ssoffx, ssoffy + be - s, lm + s, bm + s, x, y + myh - bm - s, lm + s, bm + s);
		
		// draw edges, overlapping in to fill area by 1px to avoid seams unless fill is transparent
		if (this.edges === 0)		// tile edges
		{
			var off = (this.fill === 2 ? 0 : s);
			
			if (lm > 0 && be > tm)
				drawPatternProperly(ctx, this.type.leftPattern, lm, be - tm, x, y + tm, lm + off, myh - tm - bm, 0, 0);
				
			if (rm > 0 && be > tm)
				drawPatternProperly(ctx, this.type.rightPattern, rm, be - tm, x + myw - rm - off, y + tm, rm + off, myh - tm - bm, off, 0);
				
			if (tm > 0 && re > lm)
				drawPatternProperly(ctx, this.type.topPattern, re - lm, tm, x + lm, y, myw - lm - rm, tm + off, 0, 0);
				
			if (bm > 0 && re > lm)
				drawPatternProperly(ctx, this.type.bottomPattern, re - lm, bm, x + lm, y + myh - bm - off, myw - lm - rm, bm + off, 0, off);
		}
		else if (this.edges === 1)	// stretch edges
		{
			if (lm > 0 && be > tm && myh - tm - bm > 0)
				ctx.drawImage(img, ssoffx, ssoffy + tm, lm, be - tm, x, y + tm, lm, myh - tm - bm);
				
			if (rm > 0 && be > tm && myh - tm - bm > 0)
				ctx.drawImage(img, ssoffx + re, ssoffy + tm, rm, be - tm, x + myw - rm, y + tm, rm, myh - tm - bm);
				
			if (tm > 0 && re > lm && myw - lm - rm > 0)
				ctx.drawImage(img, ssoffx + lm, ssoffy, re - lm, tm, x + lm, y, myw - lm - rm, tm);
				
			if (bm > 0 && re > lm && myw - lm - rm > 0)
				ctx.drawImage(img, ssoffx + lm, ssoffy + be, re - lm, bm, x + lm, y + myh - bm, myw - lm - rm, bm);
		}
		
		// fill
		if (be > tm && re > lm)
		{
			if (this.fill === 0)		// tile fill
			{
				drawPatternProperly(ctx, this.type.fillPattern, re - lm, be - tm, x + lm, y + tm, myw - lm - rm, myh - tm - bm, 0, 0);
			}
			else if (this.fill === 1)	// stretch fill
			{
				if (myw - lm - rm > 0 && myh - tm - bm > 0)
				{
					ctx.drawImage(img, ssoffx + lm, ssoffy + tm, re - lm, be - tm, x + lm, y + tm, myw - lm - rm, myh - tm - bm);
				}
			}
			// else fill is 2, meaning transparent - don't draw
		}
		
		ctx.restore();
	};
	
	instanceProto.drawPatch = function(glw, tex, sx, sy, sw, sh, dx, dy, dw, dh)
	{
		glw.setTexture(tex);
		var rcTex = this.rcTex;
		rcTex.left = sx / tex.c2width;
		rcTex.top = sy / tex.c2height;
		rcTex.right = (sx + sw) / tex.c2width;
		rcTex.bottom = (sy + sh) / tex.c2height;
		glw.quadTex(dx, dy, dx + dw, dy, dx + dw, dy + dh, dx, dy + dh, rcTex);
	};
	
	instanceProto.tilePatch = function(glw, tex, dx, dy, dw, dh, ox, oy)
	{
		glw.setTexture(tex);
		var rcTex = this.rcTex;
		rcTex.left = -ox / tex.c2width;
		rcTex.top = -oy / tex.c2height;
		rcTex.right = (dw - ox) / tex.c2width;
		rcTex.bottom = (dh - oy) / tex.c2height;
		glw.quadTex(dx, dy, dx + dw, dy, dx + dw, dy + dh, dx, dy + dh, rcTex);
	};
	
	instanceProto.drawGL_earlyZPass = function(glw)
	{
		this.drawGL(glw);
	};
	
	instanceProto.drawGL = function(glw)
	{
		var lm = this.leftMargin;
		var rm = this.rightMargin;
		var tm = this.topMargin;
		var bm = this.bottomMargin;
		var iw = this.type.texture_data[5];			// spritesheeted width
		var ih = this.type.texture_data[6];			// spritesheeted height
		var offx = this.type.texture_data[3];		// spritesheet offset X
		var offy = this.type.texture_data[4];		// spritesheet offset Y
		var re = iw - rm;
		var be = ih - bm;
		
		glw.setOpacity(this.opacity);
		
		var rcTex = this.rcTex;
		var q = this.bquad;
		var myx = q.tlx;
		var myy = q.tly;
		var myw = this.width;
		var myh = this.height;
		
		if (this.runtime.pixel_rounding)
		{
			myx = Math.round(myx);
			myy = Math.round(myy);
		}
		
		var s = (this.seamless ? 1 : 0);
		
		// draw corners
		if (lm > 0 && tm > 0)
			this.drawPatch(glw, this.type.webGL_texture, offx, offy, lm + s, tm + s, myx, myy, lm + s, tm + s);
		
		if (rm > 0 && tm > 0)
			this.drawPatch(glw, this.type.webGL_texture, offx + re - s, offy, rm + s, tm + s, myx + myw - rm - s, myy, rm + s, tm + s);
			
		if (rm > 0 && bm > 0)
			this.drawPatch(glw, this.type.webGL_texture, offx + re - s, offy + be - s, rm + s, bm + s, myx + myw - rm - s, myy + myh - bm - s, rm + s, bm + s);
			
		if (lm > 0 && bm > 0)
			this.drawPatch(glw, this.type.webGL_texture, offx, offy + be - s, lm + s, bm + s, myx, myy + myh - bm - s, lm + s, bm + s);
		
		// draw edges
		if (this.edges === 0)		// tile edges
		{
			var off = (this.fill === 2 ? 0 : s);
			
			if (lm > 0 && be > tm)
				this.tilePatch(glw, this.type.webGL_leftTexture, myx, myy + tm, lm + off, myh - tm - bm, 0, 0);
			if (rm > 0 && be > tm)
				this.tilePatch(glw, this.type.webGL_rightTexture, myx + myw - rm - off, myy + tm, rm + off, myh - tm - bm, off, 0);
			if (tm > 0 && re > lm)
				this.tilePatch(glw, this.type.webGL_topTexture, myx + lm, myy, myw - lm - rm, tm + off, 0, 0);
			if (bm > 0 && re > lm)
				this.tilePatch(glw, this.type.webGL_bottomTexture, myx + lm, myy + myh - bm - off, myw - lm - rm, bm + off, 0, off);
		}
		else if (this.edges === 1)	// stretch edges
		{
			if (lm > 0 && be > tm)
				this.drawPatch(glw, this.type.webGL_texture, offx, offy + tm, lm, be - tm, myx, myy + tm, lm, myh - tm - bm);
			if (rm > 0 && be > tm)
				this.drawPatch(glw, this.type.webGL_texture, offx + re, offy + tm, rm, be - tm, myx + myw - rm, myy + tm, rm, myh - tm - bm);
			if (tm > 0 && re > lm)
				this.drawPatch(glw, this.type.webGL_texture, offx + lm, offy, re - lm, tm, myx + lm, myy, myw - lm - rm, tm);
			if (bm > 0 && re > lm)
				this.drawPatch(glw, this.type.webGL_texture, offx + lm, offy + be, re - lm, bm, myx + lm, myy + myh - bm, myw - lm - rm, bm);
		}
		
		if (be > tm && re > lm)
		{
			if (this.fill === 0)		// tile fill
			{
				this.tilePatch(glw, this.type.webGL_fillTexture, myx + lm, myy + tm, myw - lm - rm, myh - tm - bm, 0, 0);
			}
			else if (this.fill === 1)	// stretch fill
			{
				this.drawPatch(glw, this.type.webGL_texture, offx + lm, offy + tm, re - lm, be - tm, myx + lm, myy + tm, myw - lm - rm, myh - tm - bm);
			}
			// else fill is 2, meaning transparent - don't draw
		}
	};

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.OnURLLoaded = function ()
	{
		return true;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.SetEffect = function (effect)
	{
		this.blend_mode = effect;
		this.compositeOp = cr.effectToCompositeOp(effect);
		cr.setGLBlend(this, effect, this.runtime.gl);
		this.runtime.redraw = true;
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	pluginProto.exps = new Exps();

}());

// Particles
// ECMAScript 5 strict mode

;
;

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

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

	var typeProto = pluginProto.Type.prototype;

	// called on startup for each object type
	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];
	};
	
	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;
			
		this.webGL_texture = null;
	};
	
	typeProto.createParticleTexture = function ()
	{
		// Shortcut to correctly render spritesheeted particle in WebGL: cut the image out of its spritesheet
		// and create a texture from that.
		var tmpcanvas = document.createElement("canvas");
		var w = this.spriteWidth;
		var h = this.spriteHeight;
		tmpcanvas.width = w;
		tmpcanvas.height = h;
		var tmpctx = tmpcanvas.getContext("2d");
		tmpctx.drawImage(this.texture_img, this.spriteX, this.spriteY, w, h, 0, 0, w, h);
		
		this.webGL_texture = this.runtime.glwrap.loadTexture(tmpcanvas, true, this.runtime.linearSampling, this.texture_pixelformat);
	};
	
	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.createParticleTexture();
		}
	};
	
	typeProto.loadTextures = function ()
	{
		if (this.is_family || this.webGL_texture || !this.runtime.glwrap)
			return;
		
		this.createParticleTexture();
	};
	
	typeProto.unloadTextures = function ()
	{
		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);
	};
	
	/////////////////////////////////////
	// Particle class
	function Particle(owner)
	{
		this.owner = owner;
		this.active = false;
		this.x = 0;
		this.y = 0;
		this.speed = 0;
		this.angle = 0;
		this.opacity = 1;
		this.grow = 0;
		this.size = 0;
		this.gs = 0;			// gravity speed
		this.age = 0;
		cr.seal(this);
	};
	
	Particle.prototype.init = function ()
	{
		var owner = this.owner;
		this.x = owner.x - (owner.xrandom / 2) + (Math.random() * owner.xrandom);
		this.y = owner.y - (owner.yrandom / 2) + (Math.random() * owner.yrandom);
		
		this.speed = owner.initspeed - (owner.speedrandom / 2) + (Math.random() * owner.speedrandom);
		this.angle = owner.angle - (owner.spraycone / 2) + (Math.random() * owner.spraycone);
		this.opacity = owner.initopacity;
		this.size = owner.initsize - (owner.sizerandom / 2) + (Math.random() * owner.sizerandom);
		this.grow = owner.growrate - (owner.growrandom / 2) + (Math.random() * owner.growrandom);
		this.gs = 0;
		this.age = 0;
	};
	
	Particle.prototype.tick = function (dt)
	{
		var owner = this.owner;
		
		// Move
		this.x += Math.cos(this.angle) * this.speed * dt;
		this.y += Math.sin(this.angle) * this.speed * dt;
		
		// Apply gravity
		this.y += this.gs * dt;
		
		// Adjust lifetime parameters
		this.speed += owner.acc * dt;
		this.size += this.grow * dt;
		this.gs += owner.g * dt;
		this.age += dt;
		
		// Destroy particle if shrunk to less than a pixel in size
		if (this.size < 1)
		{
			this.active = false;
			return;
		}
		
		if (owner.lifeanglerandom !== 0)
			this.angle += (Math.random() * owner.lifeanglerandom * dt) - (owner.lifeanglerandom * dt / 2);
			
		if (owner.lifespeedrandom !== 0)
			this.speed += (Math.random() * owner.lifespeedrandom * dt) - (owner.lifespeedrandom * dt / 2);
			
		if (owner.lifeopacityrandom !== 0)
		{
			this.opacity += (Math.random() * owner.lifeopacityrandom * dt) - (owner.lifeopacityrandom * dt / 2);
			
			if (this.opacity < 0)
				this.opacity = 0;
			else if (this.opacity > 1)
				this.opacity = 1;
		}
		
		// Make inactive after timeout for both fade and timeout settings
		if (owner.destroymode <= 1 && this.age >= owner.timeout)
		{
			this.active = false;
		}
		// Or make inactive when stopped
		if (owner.destroymode === 2 && this.speed <= 0)
		{
			this.active = false;
		}
	};
	
	Particle.prototype.draw = function (ctx)
	{
		var curopacity = this.owner.opacity * this.opacity;
		
		if (curopacity === 0)
			return;
		
		// Modify opacity for fade-out
		if (this.owner.destroymode === 0)
			curopacity *= 1 - (this.age / this.owner.timeout);
			
		ctx.globalAlpha = curopacity;
			
		var drawx = this.x - this.size / 2;
		var drawy = this.y - this.size / 2;
		
		if (this.owner.runtime.pixel_rounding)
		{
			drawx = (drawx + 0.5) | 0;
			drawy = (drawy + 0.5) | 0;
		}
		
		var type = this.owner.type;
		ctx.drawImage(type.texture_img, type.spriteX, type.spriteY, type.spriteWidth, type.spriteHeight, drawx, drawy, this.size, this.size);
	};
	
	Particle.prototype.drawGL = function (glw)
	{
		var curopacity = this.owner.opacity * this.opacity;
		
		// Modify opacity for fade-out
		if (this.owner.destroymode === 0)
			curopacity *= 1 - (this.age / this.owner.timeout);
		
		var drawsize = this.size;
		var scaleddrawsize = drawsize * this.owner.particlescale;
		var drawx = this.x - drawsize / 2;
		var drawy = this.y - drawsize / 2;
		
		if (this.owner.runtime.pixel_rounding)
		{
			drawx = (drawx + 0.5) | 0;
			drawy = (drawy + 0.5) | 0;
		}
		
		// Don't bother issuing a quad for a particle smaller than 1px, it probably won't be visible anyway.
		if (scaleddrawsize < 1 || curopacity === 0)
			return;
			
		// Quad if outside the allowed point range, otherwise issue a point.  Hopefully there won't be too much
		// quad <-> point batch switching.  Note we have to manually scale particles which don't take in to account
		// the layout zoom etc. otherwise.
		if (scaleddrawsize < glw.minPointSize || scaleddrawsize > glw.maxPointSize)
		{
			glw.setOpacity(curopacity);
			glw.quad(drawx, drawy, drawx + drawsize, drawy, drawx + drawsize, drawy + drawsize, drawx, drawy + drawsize);
		}
		else
			glw.point(this.x, this.y, scaleddrawsize, curopacity);
	};
	
	Particle.prototype.left = function ()
	{
		return this.x - this.size / 2;
	};
	
	Particle.prototype.right = function ()
	{
		return this.x + this.size / 2;
	};
	
	Particle.prototype.top = function ()
	{
		return this.y - this.size / 2;
	};
	
	Particle.prototype.bottom = function ()
	{
		return this.y + this.size / 2;
	};

	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	// global array of particles to recycle
	var deadparticles = [];

	// called whenever an instance is created
	instanceProto.onCreate = function()
	{
		var props = this.properties;
		
		this.rate = props[0];
		this.spraycone = cr.to_radians(props[1]);
		this.spraytype = props[2];			// 0 = continuous, 1 = one-shot
		this.spraying = true;				// for continuous mode only
		
		// property index 3 is object class which is not supported in C2 runtime
		
		this.initspeed = props[4];
		this.initsize = props[5];
		this.initopacity = props[6] / 100.0;
		this.growrate = props[7];
		this.xrandom = props[8];
		this.yrandom = props[9];
		this.speedrandom = props[10];
		this.sizerandom = props[11];
		this.growrandom = props[12];
		this.acc = props[13];
		this.g = props[14];
		this.lifeanglerandom = props[15];
		this.lifespeedrandom = props[16];
		this.lifeopacityrandom = props[17];
		this.destroymode = props[18];		// 0 = fade, 1 = timeout, 2 = stopped
		this.timeout = props[19];
		
		this.particleCreateCounter = 0;
		this.particlescale = 1;
		
		// Dynamically set the bounding box to surround all created particles
		this.particleBoxLeft = this.x;
		this.particleBoxTop = this.y;
		this.particleBoxRight = this.x;
		this.particleBoxBottom = this.y;
		
		this.add_bbox_changed_callback(function (self) {
			self.bbox.set(self.particleBoxLeft, self.particleBoxTop, self.particleBoxRight, self.particleBoxBottom);
			self.bquad.set_from_rect(self.bbox);
			self.bbox_changed = false;
			self.update_collision_cell();
			self.update_render_cell();
		});
		
		// Check for recycling
		if (!this.recycled)
			this.particles = [];
		
		this.runtime.tickMe(this);
		
		this.type.loadTextures();
			
		// If in one-shot mode, create all particles now
		if (this.spraytype === 1)
		{
			for (var i = 0; i < this.rate; i++)
				this.allocateParticle().opacity = 0;
		}
		
		this.first_tick = true;		// for re-init'ing one-shot particles on first tick so they assume any new angle/position
	};
	
	instanceProto.saveToJSON = function ()
	{
		var o = {
			"r": this.rate,
			"sc": this.spraycone,
			"st": this.spraytype,
			"s": this.spraying,
			"isp": this.initspeed,
			"isz": this.initsize,
			"io": this.initopacity,
			"gr": this.growrate,
			"xr": this.xrandom,
			"yr": this.yrandom,
			"spr": this.speedrandom,
			"szr": this.sizerandom,
			"grnd": this.growrandom,
			"acc": this.acc,
			"g": this.g,
			"lar": this.lifeanglerandom,
			"lsr": this.lifespeedrandom,
			"lor": this.lifeopacityrandom,
			"dm": this.destroymode,
			"to": this.timeout,
			"pcc": this.particleCreateCounter,
			"ft": this.first_tick,
			"p": []
		};
		
		var i, len, p;
		var arr = o["p"];

		for (i = 0, len = this.particles.length; i < len; i++)
		{
			p = this.particles[i];
			arr.push([p.x, p.y, p.speed, p.angle, p.opacity, p.grow, p.size, p.gs, p.age]);
		}
		
		return o;
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		this.rate = o["r"];
		this.spraycone = o["sc"];
		this.spraytype = o["st"];
		this.spraying = o["s"];
		this.initspeed = o["isp"];
		this.initsize = o["isz"];
		this.initopacity = o["io"];
		this.growrate = o["gr"];
		this.xrandom = o["xr"];
		this.yrandom = o["yr"];
		this.speedrandom = o["spr"];
		this.sizerandom = o["szr"];
		this.growrandom = o["grnd"];
		this.acc = o["acc"];
		this.g = o["g"];
		this.lifeanglerandom = o["lar"];
		this.lifespeedrandom = o["lsr"];
		this.lifeopacityrandom = o["lor"];
		this.destroymode = o["dm"];
		this.timeout = o["to"];
		this.particleCreateCounter = o["pcc"];
		this.first_tick = o["ft"];
		
		// recycle all particles then load by reallocating them
		deadparticles.push.apply(deadparticles, this.particles);
		cr.clearArray(this.particles);
		
		var i, len, p, d;
		var arr = o["p"];
		
		for (i = 0, len = arr.length; i < len; i++)
		{
			p = this.allocateParticle();
			d = arr[i];
			p.x = d[0];
			p.y = d[1];
			p.speed = d[2];
			p.angle = d[3];
			p.opacity = d[4];
			p.grow = d[5];
			p.size = d[6];
			p.gs = d[7];
			p.age = d[8];
		}
	};
	
	instanceProto.onDestroy = function ()
	{
		// recycle all particles
		deadparticles.push.apply(deadparticles, this.particles);
		cr.clearArray(this.particles);
	};
	
	instanceProto.allocateParticle = function ()
	{
		var p;
		
		if (deadparticles.length)
		{
			p = deadparticles.pop();
			p.owner = this;
		}
		else
			p = new Particle(this);
		
		this.particles.push(p);
		p.active = true;
		return p;
	};
	
	instanceProto.tick = function()
	{
		var dt = this.runtime.getDt(this);
		
		var i, len, p, n, j;
		
		// Create spray particles for this tick
		if (this.spraytype === 0 && this.spraying)
		{
			this.particleCreateCounter += dt * this.rate;
			
			n = cr.floor(this.particleCreateCounter);
			this.particleCreateCounter -= n;
			
			for (i = 0; i < n; i++)
			{
				p = this.allocateParticle();
				p.init();
			}
		}
		
		this.particleBoxLeft = this.x;
		this.particleBoxTop = this.y;
		this.particleBoxRight = this.x;
		this.particleBoxBottom = this.y;
		
		for (i = 0, j = 0, len = this.particles.length; i < len; i++)
		{
			p = this.particles[i];
			this.particles[j] = p;
			
			this.runtime.redraw = true;
			
			// If the first tick for one-shot particles, call init() now so the particles
			// assume any changed position or angle of the Particles object.
			if (this.spraytype === 1 && this.first_tick)
				p.init();
			
			p.tick(dt);
			
			// Particle is dead: move to deadparticles for later recycling
			if (!p.active)
			{
				deadparticles.push(p);
				continue;
			}
			
			// measure bounding box
			if (p.left() < this.particleBoxLeft)
				this.particleBoxLeft = p.left();
			if (p.right() > this.particleBoxRight)
				this.particleBoxRight = p.right();
			if (p.top() < this.particleBoxTop)
				this.particleBoxTop = p.top();
			if (p.bottom() > this.particleBoxBottom)
				this.particleBoxBottom = p.bottom();
			
			j++;
		}
		
		cr.truncateArray(this.particles, j);
		
		// Update the bounding box based on active particles
		this.set_bbox_changed();
		
		this.first_tick = false;
		
		// If one-shot and all particles are dead, destroy the whole object
		if (this.spraytype === 1 && this.particles.length === 0)
			this.runtime.DestroyInstance(this);
	};
	
	// only called if a layout object - draw to a canvas 2D context
	instanceProto.draw = function (ctx)
	{
		var i, len, p, layer = this.layer;
		
		for (i = 0, len = this.particles.length; i < len; i++)
		{
			p = this.particles[i];
			
			// Only draw active and on-screen particles
			if (p.right() >= layer.viewLeft && p.bottom() >= layer.viewTop && p.left() <= layer.viewRight && p.top() <= layer.viewBottom)
			{
				p.draw(ctx);
			}
		}
	};
	
	// only called if a layout object in WebGL mode - draw to the WebGL context
	// 'glw' is not a WebGL context, it's a wrapper - you can find its methods in GLWrap.js in the install
	// directory or just copy what other plugins do.
	instanceProto.drawGL = function (glw)
	{
		this.particlescale = this.layer.getScale();
		glw.setTexture(this.type.webGL_texture);
		
		var i, len, p, layer = this.layer;
		
		for (i = 0, len = this.particles.length; i < len; i++)
		{
			p = this.particles[i];
			
			// Only draw active and on-screen particles
			if (p.right() >= layer.viewLeft && p.bottom() >= layer.viewTop && p.left() <= layer.viewRight && p.top() <= layer.viewBottom)
			{
				p.drawGL(glw);
			}
		}
	};
	

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

	// the example condition
	Cnds.prototype.IsSpraying = function ()
	{
		return this.spraying;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.SetSpraying = function (set_)
	{
		this.spraying = (set_ !== 0);
	};
	
	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.SetRate = function (x)
	{
		this.rate = x;
		var diff, i;
		
		// In one-shot mode, if still in the first tick, adjust the number of particles created 
		if (this.spraytype === 1 && this.first_tick)
		{
			// Reducing particle count
			if (x < this.particles.length)
			{
				diff = this.particles.length - x;
				
				for (i = 0; i < diff; i++)
					deadparticles.push(this.particles.pop());
			}
			// Increasing particle count
			else if (x > this.particles.length)
			{
				diff = x - this.particles.length;
				
				for (i = 0; i < diff; i++)
					this.allocateParticle().opacity = 0;
			}
		}
	};
	
	Acts.prototype.SetSprayCone = function (x)
	{
		this.spraycone = cr.to_radians(x);
	};
	
	Acts.prototype.SetInitSpeed = function (x)
	{
		this.initspeed = x;
	};
	
	Acts.prototype.SetInitSize = function (x)
	{
		this.initsize = x;
	};
	
	Acts.prototype.SetInitOpacity = function (x)
	{
		this.initopacity = x / 100;
	};
	
	Acts.prototype.SetGrowRate = function (x)
	{
		this.growrate = x;
	};
	
	Acts.prototype.SetXRandomiser = function (x)
	{
		this.xrandom = x;
	};
	
	Acts.prototype.SetYRandomiser = function (x)
	{
		this.yrandom = x;
	};
	
	Acts.prototype.SetSpeedRandomiser = function (x)
	{
		this.speedrandom = x;
	};
	
	Acts.prototype.SetSizeRandomiser = function (x)
	{
		this.sizerandom = x;
	};
	
	Acts.prototype.SetGrowRateRandomiser = function (x)
	{
		this.growrandom = x;
	};
	
	Acts.prototype.SetParticleAcc = function (x)
	{
		this.acc = x;
	};
	
	Acts.prototype.SetGravity = function (x)
	{
		this.g = x;
	};
	
	Acts.prototype.SetAngleRandomiser = function (x)
	{
		this.lifeanglerandom = x;
	};
	
	Acts.prototype.SetLifeSpeedRandomiser = function (x)
	{
		this.lifespeedrandom = x;
	};
	
	Acts.prototype.SetOpacityRandomiser = function (x)
	{
		this.lifeopacityrandom = x;
	};
	
	Acts.prototype.SetTimeout = function (x)
	{
		this.timeout = x;
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.ParticleCount = function (ret)
	{
		ret.set_int(this.particles.length);
	};
	
	Exps.prototype.Rate = function (ret)
	{
		ret.set_float(this.rate);
	};
	
	Exps.prototype.SprayCone = function (ret)
	{
		ret.set_float(cr.to_degrees(this.spraycone));
	};
	
	Exps.prototype.InitSpeed = function (ret)
	{
		ret.set_float(this.initspeed);
	};
	
	Exps.prototype.InitSize = function (ret)
	{
		ret.set_float(this.initsize);
	};
	
	Exps.prototype.InitOpacity = function (ret)
	{
		ret.set_float(this.initopacity * 100);
	};
	
	Exps.prototype.InitGrowRate = function (ret)
	{
		ret.set_float(this.growrate);
	};
	
	Exps.prototype.XRandom = function (ret)
	{
		ret.set_float(this.xrandom);
	};
	
	Exps.prototype.YRandom = function (ret)
	{
		ret.set_float(this.yrandom);
	};
	
	Exps.prototype.InitSpeedRandom = function (ret)
	{
		ret.set_float(this.speedrandom);
	};
	
	Exps.prototype.InitSizeRandom = function (ret)
	{
		ret.set_float(this.sizerandom);
	};
	
	Exps.prototype.InitGrowRandom = function (ret)
	{
		ret.set_float(this.growrandom);
	};
	
	Exps.prototype.ParticleAcceleration = function (ret)
	{
		ret.set_float(this.acc);
	};
	
	Exps.prototype.Gravity = function (ret)
	{
		ret.set_float(this.g);
	};
	
	Exps.prototype.ParticleAngleRandom = function (ret)
	{
		ret.set_float(this.lifeanglerandom);
	};
	
	Exps.prototype.ParticleSpeedRandom = function (ret)
	{
		ret.set_float(this.lifespeedrandom);
	};
	
	Exps.prototype.ParticleOpacityRandom = function (ret)
	{
		ret.set_float(this.lifeopacityrandom);
	};
	
	Exps.prototype.Timeout = function (ret)
	{
		ret.set_float(this.timeout);
	};
	
	pluginProto.exps = new Exps();

}());

// Keyboard
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.Keyboard.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.keyMap = new Array(256);		// stores key up/down state
		this.typedKeyMap = new Array(256);	// stores the "key" value of an event relative to its keyCode, for TypedKey expression
		this.usedKeys = new Array(256);
		this.triggerKey = 0;
	};
	
	var instanceProto = pluginProto.Instance.prototype;

	instanceProto.onCreate = function()
	{
		var self = this;
		
		// Bind keyboard events
		document.addEventListener("keydown", function (info)
		{
			self.onKeyDown(info);
		});
		
		document.addEventListener("keyup", function (info)
		{
			self.onKeyUp(info);
		});
	};
	
	// On iframe embedded games like the Scirra Arcade, these keys can end up
	// scrolling the parent page unless we specifically block them every time.
	var keysToBlockWhenFramed = [32, 33, 34, 35, 36, 37, 38, 39, 40, 44];

	instanceProto.onKeyDown = function (info)
	{
		var alreadyPreventedDefault = false;
		
		// Always block certain key presses in frames which can result in page scrolling.
		if (window != window.top && keysToBlockWhenFramed.indexOf(info.which) > -1)
		{
			info.preventDefault();
			alreadyPreventedDefault = true;
			info.stopPropagation();
		}
		
		// Key already down: ignore, must be a repeat
		if (this.keyMap[info.which])
		{
			if (this.usedKeys[info.which] && !alreadyPreventedDefault)
				info.preventDefault();
			
			return;
		}
		
		var which = info.which;
		// Set the key in the key map
		this.typedKeyMap[which] = info["key"];
		this.keyMap[which] = true;
		this.triggerKey = which;
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger 'On Any Key'
		this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnAnyKey, this);
		
		// Trigger 'On Key'
		var eventRan = this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnKey, this);
		
		// Trigger 'On Key Code'
		var eventRan2 = this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnKeyCode, this);
		
		this.runtime.isInUserInputEvent = false;
		
		// If any event ran, prevent the default behavior.  This does not include 'on any key' running though.
		if (eventRan || eventRan2)
		{
			this.usedKeys[which] = true;
			
			if (!alreadyPreventedDefault)
				info.preventDefault();
		}
	};

	instanceProto.onKeyUp = function (info)
	{
		var which = info.which;
		// Set the key in the key map
		this.typedKeyMap[which] = info["key"];
		this.keyMap[which] = false;
		this.triggerKey = which;
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger 'On Any Key Released'
		this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnAnyKeyReleased, this);
		
		// Trigger 'On Key Released'
		var eventRan = this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnKeyReleased, this);
		
		// Trigger 'On Key Code Released'
		var eventRan2 = this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnKeyCodeReleased, this);
		
		this.runtime.isInUserInputEvent = false;
		
		// If any event ran, prevent the default behavior
		if (eventRan || eventRan2 || this.usedKeys[info.which])
		{
			this.usedKeys[which] = true;
			info.preventDefault();
		}
	};
	
	instanceProto.onWindowBlur = function ()
	{
		// Fire "On key up" for any keys held down, to prevent stuck keys
		var i;
		for (i = 0; i < 256; ++i)
		{
			if (!this.keyMap[i])
				continue;		// key already up
			
			// Synthesise a "key up" event to prevent apps getting stuck with keys down.
			// Note this is not a real user input event, so we don't set isInUserInputEvent.
			this.keyMap[i] = false;
			this.triggerKey = i;
			
			// Trigger 'On Any Key Released'
			this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnAnyKeyReleased, this);
			
			// Trigger 'On Key Released'
			var eventRan = this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnKeyReleased, this);
			
			// Trigger 'On Key Code Released'
			var eventRan2 = this.runtime.trigger(cr.plugins_.Keyboard.prototype.cnds.OnKeyCodeReleased, this);
			
			// If any event ran, prevent the default behavior
			if (eventRan || eventRan2)
				this.usedKeys[i] = true;
		}
	};
	
	instanceProto.saveToJSON = function ()
	{
		return { "triggerKey": this.triggerKey };
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		this.triggerKey = o["triggerKey"];
	};
	

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

	Cnds.prototype.IsKeyDown = function(key)
	{
		return this.keyMap[key];
	};
	
	Cnds.prototype.OnKey = function(key)
	{
		return (key === this.triggerKey);
	};
	
	Cnds.prototype.OnAnyKey = function(key)
	{
		return true;
	};
	
	Cnds.prototype.OnAnyKeyReleased = function(key)
	{
		return true;
	};
	
	Cnds.prototype.OnKeyReleased = function(key)
	{
		return (key === this.triggerKey);
	};
	
	Cnds.prototype.IsKeyCodeDown = function(key)
	{
		key = Math.floor(key);
		
		if (key < 0 || key >= this.keyMap.length)
			return false;
		
		return this.keyMap[key];
	};
	
	Cnds.prototype.OnKeyCode = function(key)
	{
		return (key === this.triggerKey);
	};
	
	Cnds.prototype.OnKeyCodeReleased = function(key)
	{
		return (key === this.triggerKey);
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.TypedKey = function (ret)
	{
		ret.set_string(this.typedKeyMap[this.triggerKey] || "");
	};
	
	Exps.prototype.LastKeyCode = function (ret)
	{
		ret.set_int(this.triggerKey);
	};
	
	// Turns out Javascript's fromCharCode is nearly totally useless. Fix it with our own function.
	function fixedStringFromCharCode(kc)
	{
		kc = Math.floor(kc);
		
		// Alphanumerics work with fromCharCode, so just special case every other key
		switch (kc) {
		case 8:		return "backspace";
		case 9:		return "tab";
		case 13:	return "enter";
		case 16:	return "shift";
		case 17:	return "control";
		case 18:	return "alt";
		case 19:	return "pause";
		case 20:	return "capslock";
		case 27:	return "esc";
		case 33:	return "pageup";
		case 34:	return "pagedown";
		case 35:	return "end";
		case 36:	return "home";
		case 37:	return "←";
		case 38:	return "↑";
		case 39:	return "→";
		case 40:	return "↓";
		case 45:	return "insert";
		case 46:	return "del";
		case 91:	return "left window key";
		case 92:	return "right window key";
		case 93:	return "select";
		case 96:	return "numpad 0";
		case 97:	return "numpad 1";
		case 98:	return "numpad 2";
		case 99:	return "numpad 3";
		case 100:	return "numpad 4";
		case 101:	return "numpad 5";
		case 102:	return "numpad 6";
		case 103:	return "numpad 7";
		case 104:	return "numpad 8";
		case 105:	return "numpad 9";
		case 106:	return "numpad *";
		case 107:	return "numpad +";
		case 109:	return "numpad -";
		case 110:	return "numpad .";
		case 111:	return "numpad /";
		case 112:	return "F1";
		case 113:	return "F2";
		case 114:	return "F3";
		case 115:	return "F4";
		case 116:	return "F5";
		case 117:	return "F6";
		case 118:	return "F7";
		case 119:	return "F8";
		case 120:	return "F9";
		case 121:	return "F10";
		case 122:	return "F11";
		case 123:	return "F12";
		case 144:	return "numlock";
		case 145:	return "scroll lock";
		case 186:	return ";";
		case 187:	return "=";
		case 188:	return ",";
		case 189:	return "-";
		case 190:	return ".";
		case 191:	return "/";
		case 192:	return "'";
		case 219:	return "[";
		case 220:	return "\\";
		case 221:	return "]";
		case 222:	return "#";
		case 223:	return "`";
		default:	return String.fromCharCode(kc);
		}
	};
	
	Exps.prototype.StringFromKeyCode = function (ret, kc)
	{
		ret.set_string(fixedStringFromCharCode(kc));
	};
	
	pluginProto.exps = new Exps();

}());

// Sprite font
// ECMAScript 5 strict mode
/* global cr,log,assert2 */
/* jshint globalstrict: true */
/* jshint strict: true */

;
;

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

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

	pluginProto.onCreate = function ()
	{
	};

	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
		if (this.is_family)
			return;

		this.texture_img = this.runtime.findWaitingTexture(this.texture_file);
		
		if (!this.texture_img)
		{
			this.texture_img = new Image();
			this.texture_img.cr_src = this.texture_file;
			this.texture_img.cr_filesize = this.texture_filesize;
			this.texture_img.c2webGL_texture = null;
			this.runtime.waitForImageLoad(this.texture_img, this.texture_file);
		}
		
		this.spriteX = this.texture_data[3];
		this.spriteY = this.texture_data[4];
		this.spriteWidth = this.texture_data[5];
		this.spriteHeight = this.texture_data[6];
		
		this.webGL_texture = null;
	};

	typeProto.onLostWebGLContext = function ()
	{
		if (this.is_family)
			return;

		this.webGL_texture = null;
	};

	typeProto.onRestoreWebGLContext = function ()
	{
		// No need to create textures if no instances exist, will create on demand
		if (this.is_family || !this.instances.length)
			return;

		if (!this.webGL_texture)
		{
			this.webGL_texture = this.runtime.glwrap.loadTexture(this.texture_img, false, this.runtime.linearSampling, this.texture_pixelformat);
		}

		var i, len;
		for (i = 0, len = this.instances.length; i < len; i++)
			this.instances[i].webGL_texture = this.webGL_texture;
	};

	typeProto.unloadTextures = function ()
	{
		// Don't release textures if any instances still exist, they are probably using them
		if (this.is_family || this.instances.length || !this.webGL_texture)
			return;

		this.runtime.glwrap.deleteTexture(this.webGL_texture);
		this.webGL_texture = null;
	};

	typeProto.preloadCanvas2D = function (ctx)
	{
		// draw to preload, browser should lazy load the texture
		ctx.drawImage(this.texture_img, 0, 0);
	};

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

	var instanceProto = pluginProto.Instance.prototype;

	instanceProto.onDestroy = function()
	{
		// recycle the instance's objects
		freeAllLines (this.lines);
		freeAllClip  (this.clipList);
		freeAllClipUV(this.clipUV);
		cr.wipe(this.characterWidthList);
	};

	instanceProto.onCreate = function()
	{
		this.texture_img      = this.type.texture_img;
		this.text             = this.properties[0];
		// 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 < MAX_CACHE_SIZE)
		{
			cache.push(data);
		}
	}

	// store all the data from dataList in the cache
	// and wipe dataList
	function freeAll(cache,dataList,isArray)
	{
		if (isArray) {
			var i, len;
			for (i = 0, len = dataList.length; i < len; i++)
			{
				free(cache,dataList[i]);
			}
			cr.clearArray(dataList);
		} else {
			var prop;
			for(prop in dataList) {
				if(Object.prototype.hasOwnProperty.call(dataList,prop)) {
					free(cache,dataList[prop]);
					delete dataList[prop];
				}
			}
		}
	}

	function addLine(inst,lineIndex,cur_line) {
		var lines = inst.lines;
		var line;
		cur_line = trimRight(cur_line);
		// Recycle a line if possible
		if (lineIndex >= lines.length)
			lines.push(allocLine());

		line = lines[lineIndex];
		line.text = cur_line;
		line.width = inst.measureWidth(cur_line);
		inst.textWidth = cr.max(inst.textWidth,line.width);
	}

	var linesCache = [];
	function allocLine()       { return alloc(linesCache,Object); }
	function freeLine(l)       { free(linesCache,l); }
	function freeAllLines(arr) { freeAll(linesCache,arr,true); }


	function addClip(obj,property,x,y,w,h) {
		if (obj[property] === undefined) {
			obj[property] = alloc(clipCache,Object);
		}

		obj[property].x = x;
		obj[property].y = y;
		obj[property].w = w;
		obj[property].h = h;
	}
	var clipCache = [];
	function allocClip()      { return alloc(clipCache,Object); }
	function freeAllClip(obj) { freeAll(clipCache,obj,false);}

	function addClipUV(obj,property,left,top,right,bottom) {
		if (obj[property] === undefined) {
			obj[property] = alloc(clipUVCache,cr.rect);
		}

		obj[property].left   = left;
		obj[property].top    = top;
		obj[property].right  = right;
		obj[property].bottom = bottom;
	}
	var clipUVCache = [];
	function allocClipUV()      { return alloc(clipUVCache,cr.rect);}
	function freeAllClipUV(obj) { freeAll(clipUVCache,obj,false);}


	instanceProto.SplitSheet = function() {
		// Create Clipping regions for each letters of the spritefont sheet
		var texture      = this.texture_img;
		var texWidth	 = this.texture_img.width;
		var texHeight    = this.texture_img.height;
		var spriteWidth  = this.type.spriteWidth;
		var spriteHeight = this.type.spriteHeight;
		var offx		 = this.type.spriteX;
		var offy		 = this.type.spriteY;
		var charWidth    = this.characterWidth;
		var charHeight   = this.characterHeight;
		var offu         = offx / texWidth;
		var offv         = offy / texHeight;
		var charU        = charWidth / texWidth;
		var charV        = charHeight / texHeight;
		var charSet      = this.characterSet ;

		var cols = Math.floor(spriteWidth/charWidth);
		var rows = Math.floor(spriteHeight/charHeight);

		for ( var c = 0; c < charSet.length; c++) {
			// not enough texture space
			if  (c >= cols * rows) break;

			// create clipping coordinates for each characters
			var x = c%cols;
			var y = Math.floor(c/cols);
			var letter = charSet.charAt(c);
			if (this.runtime.glwrap) {
				addClipUV(
					this.clipUV, letter,
					x * charU + offu,
					y * charV + offv,
					(x+1) * charU + offu,
					(y+1) * charV + offv
				);
			} else {
				addClip(
					this.clipList, letter,
					x * charWidth + offx,
					y * charHeight + offy,
					charWidth,
					charHeight
				);
			}
		}
	};

	/*
     *	Word-Wrapping
     */

	var wordsCache = [];
	pluginProto.TokeniseWords = function (text)
	{
		cr.clearArray(wordsCache);
		var cur_word = "";
		var ch;

		// Loop every char
		var i = 0;

		while (i < text.length)
		{
			ch = text.charAt(i);

			if (ch === "\n")
			{
				// Dump current word if any
				if (cur_word.length)
				{
					wordsCache.push(cur_word);
					cur_word = "";
				}

				// Add newline word
				wordsCache.push("\n");

				++i;
			}
			// Whitespace or hyphen: swallow rest of whitespace and include in word
			else if (ch === " " || ch === "\t" || ch === "-")
			{
				do {
					cur_word += text.charAt(i);
					i++;
				}
				while (i < text.length && (text.charAt(i) === " " || text.charAt(i) === "\t"));

				wordsCache.push(cur_word);
				cur_word = "";
			}
			else if (i < text.length)
			{
				cur_word += ch;
				i++;
			}
		}

		// Append leftover word if any
		if (cur_word.length)
			wordsCache.push(cur_word);
	};


	pluginProto.WordWrap = function (inst)
	{
		var text = inst.text;
		var lines = inst.lines;

		if (!text || !text.length)
		{
			freeAllLines(lines);
			return;
		}

		var width = inst.width;
		if (width <= 2.0)
		{
			freeAllLines(lines);
			return;
		}


		// If under 100 characters (i.e. a fairly short string), try a short string optimisation: just measure the text
		// and see if it fits on one line, without going through the tokenise/wrap.
		// Text musn't contain a linebreak!
		var charWidth = inst.characterWidth;
		var charScale = inst.characterScale;
		var charSpacing = inst.characterSpacing;
		if ( (text.length * (charWidth * charScale + charSpacing) - charSpacing) <= width && text.indexOf("\n") === -1)
		{
			var all_width = inst.measureWidth(text);

			if (all_width <= width)
			{
				// fits on one line
				freeAllLines(lines);
				lines.push(allocLine());
				lines[0].text = text;
				lines[0].width = all_width;
				inst.textWidth  = all_width;
				inst.textHeight = inst.characterHeight * charScale + inst.lineHeight;
				return;
			}
		}

		var wrapbyword = inst.wrapbyword;

		this.WrapText(inst);
		inst.textHeight = lines.length * (inst.characterHeight * charScale + inst.lineHeight);
	};

	pluginProto.WrapText = function (inst)
	{
		var wrapbyword = inst.wrapbyword;
		var text       = inst.text;
		var lines      = inst.lines;
		var width      = inst.width;

		var wordArray;
		if (wrapbyword) {
			this.TokeniseWords(text);	// writes to wordsCache
			wordArray = wordsCache;
		} else {
			wordArray = text;
		}
		var cur_line = "";
		var prev_line;
		var line_width;
		var i;
		var lineIndex = 0;
		var line;
		var ignore_newline = false;

		for (i = 0; i < wordArray.length; i++)
		{
			// Look for newline
			if (wordArray[i] === "\n")
			{
				if (ignore_newline === true) {
					// if a newline as been added by the wrapping
					// we ignore any happening just after
					ignore_newline = false;
				} else {
					// Flush line.  
					addLine(inst,lineIndex,cur_line);
					lineIndex++;
				}
				cur_line = "";
				continue;
			}
			ignore_newline = false;

			// Otherwise add to line
			prev_line = cur_line;
			cur_line += wordArray[i];

			// Measure line
			line_width = inst.measureWidth(trimRight(cur_line));

			// Line too long: wrap the line before this word was added
			if (line_width > width)
			{

				if (prev_line === "") {
					// if it's the first word, we push it on the line
					// to avoid an unnecessary blank line
					// and since we are wrapping, we ignore the next newline if any
					addLine(inst,lineIndex,cur_line);
					cur_line = "";
					ignore_newline = true;
				} else {
					// else we push the previous line
					addLine(inst,lineIndex,prev_line);
					cur_line = wordArray[i];
				}

				lineIndex++;

				// Wrapping by character: avoid lines starting with spaces
				if (!wrapbyword && cur_line === " ")
					cur_line = "";
			}
		}

		// Add any leftover line
		if (trimRight(cur_line).length)
		{
			addLine(inst,lineIndex,cur_line);

			lineIndex++;
		}

		// truncate lines to the number that were used. recycle any spare line objects
		for (i = lineIndex; i < lines.length; i++)
			freeLine(lines[i]);

		lines.length = lineIndex;
	};

	instanceProto.measureWidth = function(text) {
		var spacing = this.characterSpacing;
		var len     = text.length;
		var width   = 0;
		for (var i = 0; i < len; i++) {
			width += this.getCharacterWidth(text.charAt(i)) * this.characterScale + spacing;
		}
		// we remove the trailing spacing
		width -= (width > 0) ? spacing : 0;
		return width;
	};

	/***/


	instanceProto.getCharacterWidth = function(character) {
		var widthList = this.characterWidthList;
		if (widthList[character] !== undefined) {
			// special width
			return widthList[character];
		} else {
			// common width
			return this.characterWidth;
		}
	};

	instanceProto.rebuildText = function() {
		// If text has changed, run the word wrap.
		if (this.text_changed || this.width !== this.lastwrapwidth) {
			this.textWidth = 0;
			this.textHeight = 0;
			this.type.plugin.WordWrap(this);
			this.text_changed = false;
			this.lastwrapwidth = this.width;
		}
	};
    
    // to handle floating point imprecision
	var EPSILON = 0.00001;
	instanceProto.draw = function(ctx, glmode)
	{
		var texture = this.texture_img;
		if (this.text !== "" && texture != null) {

			//console.log("draw");

			this.rebuildText();

			// textWidth and textHeight needs to be calculated here
			// since we can early exit if bounding box is too tiny to draw anything
			// be we would still like to know the dimension of the text according to current width
			if (this.height < this.characterHeight*this.characterScale + this.lineHeight) {
				return;
			}

			ctx.globalAlpha = this.opacity;


			var myx = this.x;
			var myy = this.y;

			if (this.runtime.pixel_rounding)
			{
				myx = Math.round(myx);
				myy = Math.round(myy);
			}
			
			// Viewport dimensions
			var viewLeft = this.layer.viewLeft;
			var viewTop = this.layer.viewTop;
			var viewRight = this.layer.viewRight;
			var viewBottom = this.layer.viewBottom;

			ctx.save();
			ctx.translate(myx, myy);
			ctx.rotate(this.angle);

			// convert alignement properties to some usable values
			// useful parameters
			var angle      = this.angle;
			var ha         = this.halign;
			var va         = this.valign;
			var scale      = this.characterScale;
			var charHeight = this.characterHeight * scale;
			var lineHeight = this.lineHeight;
			var charSpace  = this.characterSpacing;
			var lines = this.lines;
			var textHeight = this.textHeight;
			var letterWidth;

			// we compute the offsets for vertical alignement in object-space
			// but it can't be negative, else it would underflow the boundingbox
			// horizontal alignement is evaluated for each line
			var halign;
			var valign = va * cr.max(0,(this.height - textHeight));

			// we get the position of the top left corner of the bounding box
			var offx = -(this.hotspotX * this.width);
			var offy = -(this.hotspotY * this.height);
			// we add to that any extra offset 
			// for vertical alignement
			offy += valign;

			var drawX ;
			var drawY = offy;
			var roundX, roundY;

			for(var i = 0; i < lines.length; i++) {
				// for horizontal alignement, we need to work line by line
				var line = lines[i].text;
				var len  = lines[i].width;

				// compute horizontal empty space
				// offset drawX according to horizontal alignement
				// indentation could be negative if long word in wrapbyword mode
				halign = ha * cr.max(0,this.width - len);		
				drawX = offx + halign;

				// we round to avoid pixel blurring
				drawY += lineHeight;
				
				// above viewport: skip rendering this line
				if (angle === 0 && myy + drawY + charHeight < viewTop)
				{
					drawY += charHeight;
					continue;
				}
			
				for(var j = 0; j < line.length; j++) {

					var letter = line.charAt(j);
					letterWidth = this.getCharacterWidth(letter);
					// we skip unrecognized characters (creates a space)
					var clip = this.clipList[letter];
					
					// still off to the left of the viewport: skip drawing this character
					if (angle === 0 && myx + drawX + letterWidth * scale + charSpace < viewLeft)
					{
						drawX += letterWidth * scale + charSpace;
						continue;
					}

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

					if (clip !== undefined) {

						roundX = drawX;
						roundY = drawY;
						
						if (angle === 0 && scale === 1)
						{
							roundX = Math.round(roundX);
							roundY = Math.round(roundY);
						}
						
						ctx.drawImage( this.texture_img,
									 clip.x, clip.y, clip.w, clip.h,
									 roundX,roundY,clip.w*scale,clip.h*scale);
					}

					drawX += letterWidth * scale + charSpace;
					
					// Line extended off viewport to right: skip drawing rest of line
					if (angle === 0 && myx + drawX > viewRight)
						break;
				}
				drawY += charHeight;

				// check if next row fits in bounding box and viewport and quit drawing if so
				if (angle === 0 && (drawY + charHeight + lineHeight > this.height || myy + drawY > viewBottom))
				{
					break;
				}
			}
			ctx.restore();
		}

	};

	// drawingQuad
	var dQuad = new cr.quad();

	function rotateQuad(quad,cosa,sina) {
		var x_temp;

		x_temp   = (quad.tlx * cosa) - (quad.tly * sina);
		quad.tly = (quad.tly * cosa) + (quad.tlx * sina);
		quad.tlx = x_temp;

		x_temp    = (quad.trx * cosa) - (quad.try_ * sina);
		quad.try_ = (quad.try_ * cosa) + (quad.trx * sina);
		quad.trx  = x_temp;

		x_temp   = (quad.blx * cosa) - (quad.bly * sina);
		quad.bly = (quad.bly * cosa) + (quad.blx * sina);
		quad.blx = x_temp;

		x_temp    = (quad.brx * cosa) - (quad.bry * sina);
		quad.bry = (quad.bry * cosa) + (quad.brx * sina);
		quad.brx  = x_temp;

	}

	instanceProto.drawGL = function(glw)
	{
		glw.setTexture(this.webGL_texture);
		glw.setOpacity(this.opacity);

		if (!this.text)
			return;

		// If text has changed, run the word wrap.
		this.rebuildText();

		// textWidth and textHeight needs to be calculated here
		// since we can early exit if bounding box is too tiny to draw anything
		// be we would still like to know the dimension of the text according to current width
		if (this.height < this.characterHeight*this.characterScale + this.lineHeight) {
			return;
		}

		this.update_bbox();
		var q = this.bquad;
		var ox = 0;
		var oy = 0;
		if (this.runtime.pixel_rounding)
		{
			ox = Math.round(this.x) - this.x;
			oy = Math.round(this.y) - this.y;
		}
		
		// Viewport dimensions
		var viewLeft = this.layer.viewLeft;
		var viewTop = this.layer.viewTop;
		var viewRight = this.layer.viewRight;
		var viewBottom = this.layer.viewBottom;

		// convert alignement properties to some usable values
		// useful parameters
		var angle      = this.angle;
		var ha         = this.halign;
		var va         = this.valign;
		var scale      = this.characterScale;
		var charHeight = this.characterHeight * scale;   // to precalculate in onCreate or on change
		var lineHeight = this.lineHeight;
		var charSpace  = this.characterSpacing;
		var lines = this.lines;
		var textHeight = this.textHeight;
		var letterWidth;

		var cosa,sina;
		if (angle !== 0) 
		{
			cosa = Math.cos(angle);
			sina = Math.sin(angle);
		}

		// we compute the offsets for vertical alignement in object-space
		// but it can't be negative, else it would underflow the boundingbox
		var halign;
		var valign = va * cr.max(0,(this.height - textHeight));

		// we get the position of the top left corner of the bounding box
		var offx = q.tlx + ox;
		var offy = q.tly + oy;


		var drawX ;
		var drawY = valign;
		var roundX, roundY;
		
		// TODO: this drawing algorithm iterates characters in their position relative to the text box,
		// but possibly renders them at an angle. To optimise rendering it compares the text box relative position to the viewport
		// to skip offscreen lines/characters, but this is only valid to do when the object is not rotated, so currently we have
		// to turn off these optimisations for rotated text. A better algorithm would be to iterate the rendered positions so
		// we can tell if the rendered location is offscreen or not, allowing the optimisations to work correctly with rotated text.

		for(var i = 0; i < lines.length; i++) {
			// for horizontal alignement, we need to work line by line
			var line       = lines[i].text;
			var lineWidth  = lines[i].width;

			// compute horizontal empty space
			// offset drawX according to horizontal alignement
			// indentation could be negative if long word in wrapbyword mode
			halign = ha * cr.max(0,this.width - lineWidth);
			//halign = Math.floor(ha * cr.max(0,this.width - lineWidth));
			drawX = halign;

			// we round to avoid pixel blurring
			drawY += lineHeight;
			
			// above viewport: skip rendering this line
			if (angle === 0 && offy + drawY + charHeight < viewTop)
			{
				drawY += charHeight;
				continue;
			}
			
			for(var j = 0; j < line.length; j++) {

				var letter = line.charAt(j);
				letterWidth = this.getCharacterWidth(letter);
				var clipUV = this.clipUV[letter];
				
				// still off to the left of the viewport: skip drawing this character
				if (angle === 0 && offx + drawX + letterWidth * scale + charSpace < viewLeft)
				{
					drawX += letterWidth * scale + charSpace;
					continue;
				}

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

				// we skip unrecognized characters (creates a space)
				if (clipUV !== undefined) {
					var clipWidth  = this.characterWidth*scale;
					var clipHeight = this.characterHeight*scale;

					roundX = drawX;
					roundY = drawY;
					
					if (angle === 0 && scale === 1)
					{
						roundX = Math.round(roundX);
						roundY = Math.round(roundY);
					}
					
					// we build the quad
					dQuad.tlx  = roundX;
					dQuad.tly  = roundY;
					dQuad.trx  = roundX + clipWidth;
					dQuad.try_ = roundY ;
					dQuad.blx  = roundX;
					dQuad.bly  = roundY + clipHeight;
					dQuad.brx  = roundX + clipWidth;
					dQuad.bry  = roundY + clipHeight;

					// we then rotate the quad around 0,0
					// if necessary
					if(angle !== 0)
					{
						rotateQuad(dQuad,cosa,sina);
					}
					// we then apply the world space offset
					dQuad.offset(offx,offy);

					// and render
					glw.quadTex(
						dQuad.tlx, dQuad.tly,
						dQuad.trx, dQuad.try_,
						dQuad.brx, dQuad.bry,
						dQuad.blx, dQuad.bly,
						clipUV
					);
				}

				drawX += letterWidth * scale + charSpace;
				
				// Line extended off viewport to right: skip drawing rest of line
				if (angle === 0 && offx + drawX > viewRight)
					break;
			}
			
			drawY += charHeight;
			
			// check if next row fits in bounding box and viewport and quit drawing if so
			if (angle === 0 && (drawY + charHeight + lineHeight > this.height || offy + drawY > viewBottom))
			{
				break;
			}
		}

	};
	

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

	Cnds.prototype.CompareText = function(text_to_compare, case_sensitive)
	{
		if (case_sensitive)
			return this.text == text_to_compare;
		else
			return cr.equals_nocase(this.text, text_to_compare);
	};

	pluginProto.cnds = new Cnds();

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

	Acts.prototype.SetText = function(param)
	{
		if (cr.is_number(param) && param < 1e9)
			param = Math.round(param * 1e10) / 1e10;	// round to nearest ten billionth - hides floating point errors

		var text_to_set = param.toString();

		if (this.text !== text_to_set)
		{
			this.text = text_to_set;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.AppendText = function(param)
	{
		if (cr.is_number(param))
			param = Math.round(param * 1e10) / 1e10;	// round to nearest ten billionth - hides floating point errors

		var text_to_append = param.toString();

		if (text_to_append)	// not empty
		{
			this.text += text_to_append;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.SetScale = function(param)
	{
		if (param !== this.characterScale) {
			this.characterScale = param;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.SetCharacterSpacing = function(param)
	{
		if (param !== this.CharacterSpacing) {
			this.characterSpacing = param;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	Acts.prototype.SetLineHeight = function(param)
	{
		if (param !== this.lineHeight) {
			this.lineHeight = param;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};

	// Utility method to set single character width
	instanceProto.SetCharWidth = function(character,width) {
		var w = parseInt(width,10);
		if (this.characterWidthList[character] !== w) {
			this.characterWidthList[character] = w;
			this.text_changed = true;
			this.runtime.redraw = true;
		}
	};
	
	// Called on startup to load the "Spacing data" property (which is passed as a string)
	instanceProto.LoadCharacterSpacingData = function (dataStr)
	{
		var arr, i, len, entry, width, str, j, lenj;
		
		if (!dataStr)
			return;
		
		try {
			arr = JSON.parse(dataStr);
		}
		catch (e) {
			return;		// invalid JSON data
		}
		
		if (!Array.isArray(arr))
			return;
		
		for (i = 0, len = arr.length; i < len; ++i)
		{
			entry = arr[i];
			
			if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "number" || typeof entry[1] !== "string")
				continue;		// not a valid entry
			
			// Apply this entry's width for every character in the string
			width = entry[0];
			str = entry[1];
			
			for (j = 0, lenj = str.length; j < lenj; ++j)
				this.SetCharWidth(str.charAt(j), width);
		}
	};

	// Action to modify spacing data at runtime
	Acts.prototype.SetCharacterWidth = function(characterSet,width)
	{
		if (characterSet !== "") {
			for(var c = 0; c < characterSet.length; c++) {
				this.SetCharWidth(characterSet.charAt(c),width);
			}
		}
	};
	
	Acts.prototype.SetEffect = function (effect)
	{
		this.blend_mode = effect;
		this.compositeOp = cr.effectToCompositeOp(effect);
		cr.setGLBlend(this, effect, this.runtime.gl);
		this.runtime.redraw = true;
	};
	
	Acts.prototype.SetHAlign = function (a)
	{
		this.halign = a / 2.0;
		this.text_changed = true;
		this.runtime.redraw = true;
	};
	
	Acts.prototype.SetVAlign = function (a)
	{
		this.valign = a / 2.0;
		this.text_changed = true;
		this.runtime.redraw = true;
	};

	pluginProto.acts = new Acts();

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

	Exps.prototype.CharacterWidth = function(ret,character)
	{
		ret.set_int(this.getCharacterWidth(character));
	};

	Exps.prototype.CharacterHeight = function(ret)
	{
		ret.set_int(this.characterHeight);
	};

	Exps.prototype.CharacterScale = function(ret)
	{
		ret.set_float(this.characterScale);
	};

	Exps.prototype.CharacterSpacing = function(ret)
	{
		ret.set_int(this.characterSpacing);
	};

	Exps.prototype.LineHeight = function(ret)
	{
		ret.set_int(this.lineHeight);
	};

	Exps.prototype.Text = function(ret)
	{
		ret.set_string(this.text);
	};
	Exps.prototype.TextWidth = function (ret)
	{
		this.rebuildText();
		ret.set_float(this.textWidth);
	};

	Exps.prototype.TextHeight = function (ret)
	{
		this.rebuildText();
		ret.set_float(this.textHeight);
	};

	pluginProto.exps = new Exps();

}());

// 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 < 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 < 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 && cr.distanceTo(this.startx, this.starty, this.x, this.y) >= 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 >= GESTURE_HOLD_TIMEOUT && !this.tooFarForHold && cr.distanceTo(this.startx, this.starty, this.x, this.y) < 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 <= GESTURE_TAP_TIMEOUT && !this.tooFarForHold && cr.distanceTo(this.startx, this.starty, this.x, this.y) < 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 <= GESTURE_TAP_TIMEOUT * 2) && cr.distanceTo(lastTapX, lastTapY, this.x, this.y) < 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" && 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 > 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 && this.runtime.isCordova && navigator["accelerometer"] && 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 >= 0)
		{
			var offsetLeft = this.runtime.canvas.offsetLeft;
			var offsetTop = this.runtime.canvas.offsetTop;
			var t = this.touches[i];
			
			// Ignore events <2ms after the last event - seems events sometimes double-fire
			// very close which throws off speed measurements
			if (nowtime - t.time < 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 && 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 && cr.isCanvasInputEvent(info))
			info.preventDefault();
			
		var i = this.findTouch(info["pointerId"]);
		this.trigger_index = (i >= 0 ? this.touches[i].startindex : -1);
		this.trigger_id = (i >= 0 ? this.touches[i]["id"] : -1);
		
		this.runtime.isInUserInputEvent = true;
		
		// Trigger OnNthTouchEnd & 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 >= 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 < len; i++)
		{
			t = info.changedTouches[i];
			
			var j = this.findTouch(t["identifier"]);
			
			if (j >= 0)
			{
				var offsetLeft = this.runtime.canvas.offsetLeft;
				var offsetTop = this.runtime.canvas.offsetTop;
				u = this.touches[j];
				
				// Ignore events <2ms after the last event - seems events sometimes double-fire
				// very close which throws off speed measurements
				if (nowtime - u.time < 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 && 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 < 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 && cr.isCanvasInputEvent(info))
			info.preventDefault();
			
		this.runtime.isInUserInputEvent = true;
		
		var i, len, t, j;
		for (i = 0, len = info.changedTouches.length; i < len; i++)
		{
			t = info.changedTouches[i];
			j = this.findTouch(t["identifier"]);
			
			// Remove touch
			if (j >= 0)
			{
				// Trigger OnNthTouchEnd & 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 && this.orient_alpha === 0 && pg_accz !== 0)
			return pg_accz * 90;
		else
			return this.orient_alpha;
	};
	
	instanceProto.getBeta = function ()
	{
		if (this.runtime.isCordova && this.orient_beta === 0 && pg_accy !== 0)
			return pg_accy * 90;
		else
			return this.orient_beta;
	};
	
	instanceProto.getGamma = function ()
	{
		if (this.runtime.isCordova && this.orient_gamma === 0 && pg_accx !== 0)
			return pg_accx * 90;
		else
			return this.orient_gamma;
	};
	
	var noop_func = function(){};
	
	function isCompatibilityMouseEvent(e)
	{
		return (e["sourceCapabilities"] && e["sourceCapabilities"]["firesTouchEvents"]) ||
				(e.originalEvent && e.originalEvent["sourceCapabilities"] && 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 && this.runtime.had_a_click && !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 >= 0 && touchY >= 0 &&
		    	touchX < this.runtime.width && touchY < this.runtime.height;
	};
	
	instanceProto.tick2 = function()
	{
		var i, len, t;
		var nowtime = cr.performance_now();
		
		for (i = 0, len = this.touches.length; i < len; ++i)
		{
			// Update speed for touches which haven't moved for 50ms
			t = this.touches[i];
			
			if (t.time <= 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 < leni; i++)
		{
			var inst = instances[i];
			inst.update_bbox();
			
			for (j = 0, lenj = this.touches.length; j < 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 < 0 || index >= 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 > 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 >= 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"] && window["DeviceOrientationEvent"]["requestPermission"])
			{
				promise = window["DeviceOrientationEvent"]["requestPermission"]()
				.then(function (state)
				{
					return state === "granted";
				});
			}
		}
		else				// motion
		{
			if (window["DeviceMotionEvent"] && 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 < 0 || index >= 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 < 0 || index >= 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 < 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 < 0 || index >= 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 < 0 || index >= 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 < 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 < 0 || index >= 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 < 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 < 0 || index >= 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 < 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 < 0 || index >= 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 <= 0)
			ret.set_float(0);
		else
			ret.set_float(dist / timediff);
	};
	
	Exps.prototype.SpeedForID = function (ret, id)
	{
		var index = this.findTouch(id);
		
		if (index < 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 <= 0)
			ret.set_float(0);
		else
			ret.set_float(dist / timediff);
	};
	
	Exps.prototype.AngleAt = function (ret, index)
	{
		index = Math.floor(index);
		
		if (index < 0 || index >= 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 < 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 < 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 < 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 < 0)
		{
			ret.set_float(0);
			return;
		}
		
		var touch = this.touches[index];
		ret.set_float(touch.pressure);
	};
	
	pluginProto.exps = new Exps();
	
}());

// Array
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.Arr.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;
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	// For recycling arrays
	var arrCache = [];
	
	function allocArray()
	{
		if (arrCache.length)
			return arrCache.pop();
		else
			return [];
	};
	
	// Compatibility shim
	if (!Array.isArray)
	{
		Array.isArray = function (vArg) {
			return Object.prototype.toString.call(vArg) === "[object Array]";
		};
	}

	function freeArray(a)
	{
		// Try to recycle any other arrays stored in this array
		var i, len;
		for (i = 0, len = a.length; i < len; i++)
		{
			if (Array.isArray(a[i]))
				freeArray(a[i]);
		}
		
		cr.clearArray(a);
		arrCache.push(a);
	};

	instanceProto.onCreate = function()
	{
		this.cx = this.properties[0];
		this.cy = this.properties[1];
		this.cz = this.properties[2];
		
		// Recycle array if possible
		if (!this.recycled)
			this.arr = allocArray();
			
		var a = this.arr;
		
		a.length = this.cx;
		
		var x, y, z;
		for (x = 0; x < this.cx; x++)
		{
			if (!a[x])
				a[x] = allocArray();
			
			a[x].length = this.cy;
			
			for (y = 0; y < this.cy; y++)
			{
				if (!a[x][y])
					a[x][y] = allocArray();
				
				a[x][y].length = this.cz;
				
				for (z = 0; z < this.cz; z++)
					a[x][y][z] = 0;
			}
		}
		
		// Loop indices need to be a stack to support recursive loops
		this.forX = [];
		this.forY = [];
		this.forZ = [];
		this.forDepth = -1;
	};
	
	instanceProto.onDestroy = function ()
	{
		// Recycle as many arrays as possible
		var x;
		for (x = 0; x < this.cx; x++)
			freeArray(this.arr[x]);		// will recurse down and recycle other arrays
		
		cr.clearArray(this.arr);
	};
	
	instanceProto.at = function (x, y, z)
	{
		x = Math.floor(x);
		y = Math.floor(y);
		z = Math.floor(z);
		
		if (isNaN(x) || x < 0 || x > this.cx - 1)
			return 0;
			
		if (isNaN(y) || y < 0 || y > this.cy - 1)
			return 0;
			
		if (isNaN(z) || z < 0 || z > this.cz - 1)
			return 0;
			
		return this.arr[x][y][z];
	};
	
	instanceProto.set = function (x, y, z, val)
	{
		x = Math.floor(x);
		y = Math.floor(y);
		z = Math.floor(z);
		
		if (isNaN(x) || x < 0 || x > this.cx - 1)
			return;
			
		if (isNaN(y) || y < 0 || y > this.cy - 1)
			return;
			
		if (isNaN(z) || z < 0 || z > this.cz - 1)
			return;
			
		this.arr[x][y][z] = val;
	};

	instanceProto.getAsJSON = function ()
	{
		return JSON.stringify({
			"c2array": true,
			"size": [this.cx, this.cy, this.cz],
			"data": this.arr
		});
	};
	
	instanceProto.saveToJSON = function ()
	{
		return {
			"size": [this.cx, this.cy, this.cz],
			"data": this.arr
		};
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		var sz = o["size"];
		this.cx = sz[0];
		this.cy = sz[1];
		this.cz = sz[2];
		
		this.arr = o["data"];
	};
	
	instanceProto.setSize = function (w, h, d)
	{
		if (w < 0) w = 0;
		if (h < 0) h = 0;
		if (d < 0) d = 0;
		
		if (this.cx === w && this.cy === h && this.cz === d)
			return;		// no change
		
		this.cx = w;
		this.cy = h;
		this.cz = d;
		
		var x, y, z;
		var a = this.arr;
		a.length = w;
		
		for (x = 0; x < this.cx; x++)
		{
			if (cr.is_undefined(a[x]))
				a[x] = allocArray();
				
			a[x].length = h;
			
			for (y = 0; y < this.cy; y++)
			{
				if (cr.is_undefined(a[x][y]))
					a[x][y] = allocArray();
					
				a[x][y].length = d;
				
				for (z = 0; z < this.cz; z++)
				{
					if (cr.is_undefined(a[x][y][z]))
						a[x][y][z] = 0;
				}
			}
		}
	};
	
	
	instanceProto.getForX = function ()
	{
		if (this.forDepth >= 0 && this.forDepth < this.forX.length)
			return this.forX[this.forDepth];
		else
			return 0;
	};
	
	instanceProto.getForY = function ()
	{
		if (this.forDepth >= 0 && this.forDepth < this.forY.length)
			return this.forY[this.forDepth];
		else
			return 0;
	};
	
	instanceProto.getForZ = function ()
	{
		if (this.forDepth >= 0 && this.forDepth < this.forZ.length)
			return this.forZ[this.forDepth];
		else
			return 0;
	};

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

	Cnds.prototype.CompareX = function (x, cmp, val)
	{
		return cr.do_cmp(this.at(x, 0, 0), cmp, val);
	};
	
	Cnds.prototype.CompareXY = function (x, y, cmp, val)
	{
		return cr.do_cmp(this.at(x, y, 0), cmp, val);
	};
	
	Cnds.prototype.CompareXYZ = function (x, y, z, cmp, val)
	{
		return cr.do_cmp(this.at(x, y, z), cmp, val);
	};
	
	instanceProto.doForEachTrigger = function (current_event)
	{
		this.runtime.pushCopySol(current_event.solModifiers);
		current_event.retrigger();
		this.runtime.popSol(current_event.solModifiers);
	};
	
	Cnds.prototype.ArrForEach = function (dims)
	{
        var current_event = this.runtime.getCurrentEventStack().current_event;
		
		// Push the for indices stack
		this.forDepth++;
		var forDepth = this.forDepth;
		
		if (forDepth === this.forX.length)
		{
			this.forX.push(0);
			this.forY.push(0);
			this.forZ.push(0);
		}
		else
		{
			this.forX[forDepth] = 0;
			this.forY[forDepth] = 0;
			this.forZ[forDepth] = 0;
		}
        
		// dims: 0 = 3D, 1 = 2D, 2 = 1D
		switch (dims) {
		case 0:
			for (this.forX[forDepth] = 0; this.forX[forDepth] < this.cx; this.forX[forDepth]++)
			{
				for (this.forY[forDepth] = 0; this.forY[forDepth] < this.cy; this.forY[forDepth]++)
				{
					for (this.forZ[forDepth] = 0; this.forZ[forDepth] < this.cz; this.forZ[forDepth]++)
					{
						this.doForEachTrigger(current_event);
					}
				}
			}
			break;
		case 1:
			for (this.forX[forDepth] = 0; this.forX[forDepth] < this.cx; this.forX[forDepth]++)
			{
				for (this.forY[forDepth] = 0; this.forY[forDepth] < this.cy; this.forY[forDepth]++)
				{
					this.doForEachTrigger(current_event);
				}
			}
			break;
		case 2:
			for (this.forX[forDepth] = 0; this.forX[forDepth] < this.cx; this.forX[forDepth]++)
			{
				this.doForEachTrigger(current_event);
			}
			break;
		}

		this.forDepth--;
		return false;
	};
	
	Cnds.prototype.CompareCurrent = function (cmp, val)
	{
		return cr.do_cmp(this.at(this.getForX(), this.getForY(), this.getForZ()), cmp, val);
	};
	
	Cnds.prototype.Contains = function(val)
	{
		var x, y, z;
		
		for (x = 0; x < this.cx; x++)
		{
			for (y = 0; y < this.cy; y++)
			{
				for (z = 0; z < this.cz; z++)
				{
					if (this.arr[x][y][z] === val)
						return true;
				}
			}
		}
		
		return false;
	};
	
	Cnds.prototype.IsEmpty = function ()
	{
		return this.cx === 0 || this.cy === 0 || this.cz === 0;
	};
	
	Cnds.prototype.CompareSize = function (axis, cmp, value)
	{
		var s = 0;
		
		switch (axis) {
		case 0:
			s = this.cx;
			break;
		case 1:
			s = this.cy;
			break;
		case 2:
			s = this.cz;
			break;
		}
		
		return cr.do_cmp(s, cmp, value);
	};
	
	pluginProto.cnds = new Cnds();

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

	Acts.prototype.Clear = function (v)
	{
		var x, y, z;
		
		for (x = 0; x < this.cx; x++)
			for (y = 0; y < this.cy; y++)
				for (z = 0; z < this.cz; z++)
					this.arr[x][y][z] = v;
	};
	
	Acts.prototype.SetSize = function (w, h, d)
	{
		this.setSize(w, h, d);
	};
	
	Acts.prototype.SetX = function (x, val)
	{
		this.set(x, 0, 0, val);
	};
	
	Acts.prototype.SetXY = function (x, y, val)
	{
		this.set(x, y, 0, val);
	};
	
	Acts.prototype.SetXYZ = function (x, y, z, val)
	{
		this.set(x, y, z, val);
	};
	
	Acts.prototype.Push = function (where, value, axis)
	{
		var x = 0, y = 0, z = 0;
		var a = this.arr;
		
		switch (axis) {
		case 0:	// X axis
		
			if (where === 0)	// back
			{
				x = a.length;
				a.push(allocArray());
			}
			else				// front
			{
				x = 0;
				a.unshift(allocArray());
			}
			
			a[x].length = this.cy;
			
			for ( ; y < this.cy; y++)
			{
				a[x][y] = allocArray();
				a[x][y].length = this.cz;
				
				for (z = 0; z < this.cz; z++)
					a[x][y][z] = value;
			}
			
			this.cx++;
			
			break;
		case 1: // Y axis
			for ( ; x < this.cx; x++)
			{
				if (where === 0)	// back
				{
					y = a[x].length;
					a[x].push(allocArray());
				}
				else				// front
				{
					y = 0;
					a[x].unshift(allocArray());
				}
				
				a[x][y].length = this.cz;
				
				for (z = 0; z < this.cz; z++)
					a[x][y][z] = value;
			}
			
			this.cy++;
			
			break;
		case 2:	// Z axis
			for ( ; x < this.cx; x++)
			{
				for (y = 0; y < this.cy; y++)
				{
					if (where === 0)	// back
					{
						a[x][y].push(value);
					}
					else				// front
					{
						a[x][y].unshift(value);
					}
				}
			}
			
			this.cz++;
			
			break;
		}
	};
	
	Acts.prototype.Pop = function (where, axis)
	{
		var x = 0, y = 0, z = 0;
		var a = this.arr;
		
		switch (axis) {
		case 0:	// X axis
		
			if (this.cx === 0)
				break;
				
			if (where === 0)	// back
			{
				freeArray(a.pop());
			}
			else				// front
			{
				freeArray(a.shift());
			}
			
			this.cx--;
			
			break;
			
		case 1: // Y axis
			if (this.cy === 0)
				break;
				
			for ( ; x < this.cx; x++)
			{
				if (where === 0)	// back
				{
					freeArray(a[x].pop());
				}
				else				// front
				{
					freeArray(a[x].shift());
				}
			}
			
			this.cy--;
			
			break;
			
		case 2:	// Z axis
			if (this.cz === 0)
				break;
				
			for ( ; x < this.cx; x++)
			{
				for (y = 0; y < this.cy; y++)
				{
					if (where === 0)	// back
					{
						a[x][y].pop();
					}
					else				// front
					{
						a[x][y].shift();
					}
				}
			}
			
			this.cz--;
			
			break;
		}
	};
	
	Acts.prototype.Reverse = function (axis)
	{
		var x = 0, y = 0, z = 0;
		var a = this.arr;
		
		if (this.cx === 0 || this.cy === 0 || this.cz === 0)
			return;		// no point reversing empty array
		
		switch (axis) {
		case 0:	// X axis
		
			a.reverse();
			break;
			
		case 1: // Y axis

			for ( ; x < this.cx; x++)
				a[x].reverse();
			
			break;
			
		case 2:	// Z axis
			
			for ( ; x < this.cx; x++)
				for (y = 0; y < this.cy; y++)
					a[x][y].reverse();
			
			break;
		}
	};
	
	function compareValues(va, vb)
	{
		// Both numbers: compare as numbers
		if (cr.is_number(va) && cr.is_number(vb))
			return va - vb;
		// Either is a string: compare as strings
		else
		{
			var sa = "" + va;
			var sb = "" + vb;
			
			if (sa < sb)
				return -1;
			else if (sa > sb)
				return 1;
			else
				return 0;
		}
	}
	
	Acts.prototype.Sort = function (axis)
	{
		var x = 0, y = 0, z = 0;
		var a = this.arr;
		
		if (this.cx === 0 || this.cy === 0 || this.cz === 0)
			return;		// no point sorting empty array
		
		switch (axis) {
		case 0:	// X axis
			
			a.sort(function (a, b) {
				return compareValues(a[0][0], b[0][0]);
			});

			break;
			
		case 1: // Y axis

			for ( ; x < this.cx; x++)
			{
				a[x].sort(function (a, b) {
					return compareValues(a[0], b[0]);
				});
			}
			
			break;
			
		case 2:	// Z axis
			
			for ( ; x < this.cx; x++)
			{
				for (y = 0; y < this.cy; y++)
				{
					a[x][y].sort(compareValues);
				}
			}
			
			break;
		}
	};
	
	Acts.prototype.Delete = function (index, axis)
	{
		var x = 0, y = 0, z = 0;
		index = Math.floor(index);
		var a = this.arr;
		
		if (index < 0)
			return;
		
		switch (axis) {
		case 0:	// X axis
		
			if (index >= this.cx)
				break;
			
			freeArray(a[index]);
			a.splice(index, 1);
			
			this.cx--;
			
			break;
			
		case 1: // Y axis
		
			if (index >= this.cy)
				break;
				
			for ( ; x < this.cx; x++)
			{
				freeArray(a[x][index]);
				a[x].splice(index, 1);
			}
			
			this.cy--;
			
			break;
			
		case 2:	// Z axis
			if (index >= this.cz)
				break;
				
			for ( ; x < this.cx; x++)
			{
				for (y = 0; y < this.cy; y++)
				{
					a[x][y].splice(index, 1);
				}
			}
			
			this.cz--;
			
			break;
		}
	};
	
	Acts.prototype.Insert = function (value, index, axis)
	{
		var x = 0, y = 0, z = 0;
		index = Math.floor(index);
		var a = this.arr;
		
		if (index < 0)
			return;
		
		switch (axis) {
		case 0:	// X axis
		
			if (index > this.cx)
				return;
				
			x = index;
			a.splice(x, 0, allocArray());			
			a[x].length = this.cy;
			
			for ( ; y < this.cy; y++)
			{
				a[x][y] = allocArray();
				a[x][y].length = this.cz;
				
				for (z = 0; z < this.cz; z++)
					a[x][y][z] = value;
			}
			
			this.cx++;
			
			break;
		case 1: // Y axis
		
			if (index > this.cy)
				return;
				
			for ( ; x < this.cx; x++)
			{
				y = index;
				a[x].splice(y, 0, allocArray());
				a[x][y].length = this.cz;
				
				for (z = 0; z < this.cz; z++)
					a[x][y][z] = value;
			}
			
			this.cy++;
			
			break;
		case 2:	// Z axis
		
			if (index > this.cz)
				return;
				
			for ( ; x < this.cx; x++)
			{
				for (y = 0; y < this.cy; y++)
				{
					a[x][y].splice(index, 0, value);
				}
			}
			
			this.cz++;
			
			break;
		}
	};
	
	Acts.prototype.JSONLoad = function (json_)
	{
		var o;
		
		try {
			o = JSON.parse(json_);
		}
		catch(e) { return; }
		
		if (!o["c2array"])		// presumably not a c2array object
			return;
		
		var sz = o["size"];
		this.cx = sz[0];
		this.cy = sz[1];
		this.cz = sz[2];
		
		this.arr = o["data"];
	};
	
	Acts.prototype.JSONDownload = function (filename)
	{
		var a = document.createElement("a");
		
		if (typeof a.download === "undefined")
		{
			var str = 'data:text/html,' + encodeURIComponent("<p><a download='" + filename + "' href=\"data:application/json,"
				+ encodeURIComponent(this.getAsJSON())
				+ "\">Download link</a></p>");
			window.open(str);
		}
		else
		{
			// auto download
			var body = document.getElementsByTagName("body")[0];
			a.textContent = filename;
			a.href = "data:application/json," + encodeURIComponent(this.getAsJSON());
			a.download = filename;
			body.appendChild(a);
			a.click();
			body.removeChild(a);
		}
	};
	
	pluginProto.acts = new Acts();

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

	Exps.prototype.At = function (ret, x, y_, z_)
	{
		var y = y_ || 0;
		var z = z_ || 0;
		
		ret.set_any(this.at(x, y, z));
	};
	
	Exps.prototype.Width = function (ret)
	{
		ret.set_int(this.cx);
	};
	
	Exps.prototype.Height = function (ret)
	{
		ret.set_int(this.cy);
	};
	
	Exps.prototype.Depth = function (ret)
	{
		ret.set_int(this.cz);
	};
	
	Exps.prototype.CurX = function (ret)
	{
		ret.set_int(this.getForX());
	};
	
	Exps.prototype.CurY = function (ret)
	{
		ret.set_int(this.getForY());
	};
	
	Exps.prototype.CurZ = function (ret)
	{
		ret.set_int(this.getForZ());
	};
	
	Exps.prototype.CurValue = function (ret)
	{
		ret.set_any(this.at(this.getForX(), this.getForY(), this.getForZ()));
	};
	
	Exps.prototype.Front = function (ret)
	{
		ret.set_any(this.at(0, 0, 0));
	};
	
	Exps.prototype.Back = function (ret)
	{
		ret.set_any(this.at(this.cx - 1, 0, 0));
	};
	
	Exps.prototype.IndexOf = function (ret, v)
	{
		for (var i = 0; i < this.cx; i++)
		{
			if (this.arr[i][0][0] === v)
			{
				ret.set_int(i);
				return;
			}
		}
		
		ret.set_int(-1);
	};
	
	Exps.prototype.LastIndexOf = function (ret, v)
	{
		for (var i = this.cx - 1; i >= 0; i--)
		{
			if (this.arr[i][0][0] === v)
			{
				ret.set_int(i);
				return;
			}
		}
		
		ret.set_int(-1);
	};
	
	Exps.prototype.AsJSON = function (ret)
	{
		ret.set_string(this.getAsJSON());
	};
	
	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 && !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 < 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();
	
}());

// Audio
// ECMAScript 5 strict mode

;
;

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

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

	var typeProto = pluginProto.Type.prototype;

	typeProto.onCreate = function()
	{
	};

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

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

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

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

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.OnEnded = function (t)
	{
		return cr.equals_nocase(audTag, t);
	};
	
	Cnds.prototype.PreloadsComplete = function ()
	{
		var i, len;
		for (i = 0, len = audioBuffers.length; i < len; i++)
		{
			if (!audioBuffers[i].isLoadedAndDecoded() && !audioBuffers[i].hasFailedToLoad())
				return false;
		}
		
		return true;
	};
	
	Cnds.prototype.AdvancedAudioSupported = function ()
	{
		return api === API_WEBAUDIO;
	};
	
	Cnds.prototype.IsSilent = function ()
	{
		return silent;
	};
	
	Cnds.prototype.IsAnyPlaying = function ()
	{
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
		{
			if (audioInstances[i].isPlaying())
				return true;
		}
		
		return false;
	};
	
	Cnds.prototype.IsTagPlaying = function (tag)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
		{
			if (taggedAudio[i].isPlaying())
				return true;
		}
		
		return false;
	};
	
	pluginProto.cnds = new Cnds();

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

	Acts.prototype.Play = function (file, looping, vol, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
			return;
		
		lastAudio.setPannerEnabled(false);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtPosition = function (file, looping, vol, x_, y_, angle_, innerangle_, outerangle_, outergain_, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
		
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ x: x_, y: y_, a: angle_, ia: innerangle_, oa: outerangle_, og: dbToLinear(outergain_), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		lastAudio.setPan(x_, y_, angle_, innerangle_, outerangle_, dbToLinear(outergain_));
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtObject = function (file, looping, vol, obj, innerangle, outerangle, outergain, tag)
	{
		if (silent || !obj)
			return;
			
		var inst = obj.getFirstPicked();
		if (!inst)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ obj: inst, ia: innerangle, oa: outerangle, og: dbToLinear(outergain), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		var px = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, true);
		var py = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, false);
		lastAudio.setPan(px, py, cr.to_degrees(inst.angle - inst.layer.getAngle()), innerangle, outerangle, dbToLinear(outergain));
		lastAudio.setObject(inst);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayByName = function (folder, filename, looping, vol, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
			return;
		
		lastAudio.setPannerEnabled(false);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtPositionByName = function (folder, filename, looping, vol, x_, y_, angle_, innerangle_, outerangle_, outergain_, tag)
	{
		if (silent)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ x: x_, y: y_, a: angle_, ia: innerangle_, oa: outerangle_, og: dbToLinear(outergain_), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		lastAudio.setPan(x_, y_, angle_, innerangle_, outerangle_, dbToLinear(outergain_));
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.PlayAtObjectByName = function (folder, filename, looping, vol, obj, innerangle, outerangle, outergain, tag)
	{
		if (silent || !obj)
			return;
			
		var inst = obj.getFirstPicked();
		if (!inst)
			return;
			
		// Convert dB to linear scale
		var v = dbToLinear(vol);
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		lastAudio = this.getAudioInstance(info, tag, is_music, looping!==0, v);
		
		if (!lastAudio)
		{
			// tell buffer to do this panning when ready
			var b = this.getAudioBuffer(info, is_music);
			b.panWhenReady.push({ obj: inst, ia: innerangle, oa: outerangle, og: dbToLinear(outergain), thistag: tag });
			return;
		}
		
		lastAudio.setPannerEnabled(true);
		var px = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, true);
		var py = cr.rotatePtAround(inst.x, inst.y, -inst.layer.getAngle(), listenerX, listenerY, false);
		lastAudio.setPan(px, py, cr.to_degrees(inst.angle - inst.layer.getAngle()), innerangle, outerangle, dbToLinear(outergain));
		lastAudio.setObject(inst);
		lastAudio.play(looping!==0, v, 0, this.nextPlayTime);
		
		this.nextPlayTime = 0;
	};
	
	Acts.prototype.SetLooping = function (tag, looping)
	{
		getAudioByTag(tag);
		
		// 0 = enable looping, 1 = disable looping
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setLooping(looping === 0);
	};
	
	Acts.prototype.SetMuted = function (tag, muted)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setMuted(muted === 0);
	};
	
	Acts.prototype.SetVolume = function (tag, vol)
	{
		getAudioByTag(tag);
		
		var v = dbToLinear(vol);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setVolume(v);
	};
	
	Acts.prototype.Preload = function (file)
	{
		if (silent)
			return;
			
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		if (api === API_CORDOVA)
		{
			// can't preload with Cordova's Media API
			return;
		}
		
		// Otherwise just request the object without doing anything to it - will be added to the caches
		this.getAudioInstance(info, "<preload>", is_music, false);
	};
	
	Acts.prototype.PreloadByName = function (folder, filename)
	{
		if (silent)
			return;
			
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		if (api === API_CORDOVA)
		{
			// can't preload with Cordova's Media API
			return;
		}
		
		// Otherwise just request the object without doing anything to it - will be added to the caches
		this.getAudioInstance(info, "<preload>", is_music, false);
	};
	
	Acts.prototype.SetPlaybackRate = function (tag, rate)
	{
		getAudioByTag(tag);
		
		// Only support forwards playback
		if (rate < 0.0)
			rate = 0;
			
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].setPlaybackRate(rate);
	};
	
	Acts.prototype.Stop = function (tag)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
			taggedAudio[i].stop();
	};
	
	Acts.prototype.StopAll = function ()
	{
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
			audioInstances[i].stop();
	};
	
	Acts.prototype.SetPaused = function (tag, state)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
		{
			if (state === 0)
				taggedAudio[i].pause();
			else
				taggedAudio[i].resume();
		}
	};
	
	Acts.prototype.Seek = function (tag, pos)
	{
		getAudioByTag(tag);
		
		var i, len;
		for (i = 0, len = taggedAudio.length; i < len; i++)
		{
			taggedAudio[i].seek(pos);
		}
	};
	
	Acts.prototype.SetSilent = function (s)
	{
		var i, len;
		
		if (s === 2)					// toggling
			s = (silent ? 1 : 0);		// choose opposite state
		
		if (s === 0 && !silent)			// setting silent
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
				audioInstances[i].setSilent(true);
			
			silent = true;
		}
		else if (s === 1 && silent)		// setting not silent
		{
			for (i = 0, len = audioInstances.length; i < len; i++)
				audioInstances[i].setSilent(false);
			
			silent = false;
		}
	};
	
	Acts.prototype.SetMasterVolume = function (vol)
	{
		masterVolume = dbToLinear(vol);
		
		var i, len;
		for (i = 0, len = audioInstances.length; i < len; i++)
			audioInstances[i].updateVolume();
	};

	Acts.prototype.AddFilterEffect = function (tag, type, freq, detune, q, gain, mix)
	{
		if (api !== API_WEBAUDIO || type < 0 || type >= filterTypes.length || !context["createBiquadFilter"])
			return;
		
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new FilterEffect(type, freq, detune, q, gain, mix));
	};
	
	Acts.prototype.AddDelayEffect = function (tag, delay, gain, mix)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new DelayEffect(delay, dbToLinear(gain), mix));
	};
	
	Acts.prototype.AddFlangerEffect = function (tag, delay, modulation, freq, feedback, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new FlangerEffect(delay / 1000, modulation / 1000, freq, feedback / 100, mix));
	};
	
	Acts.prototype.AddPhaserEffect = function (tag, freq, detune, q, mod, modfreq, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new PhaserEffect(freq, detune, q, mod, modfreq, mix));
	};
	
	Acts.prototype.AddConvolutionEffect = function (tag, file, norm, mix)
	{
		if (api !== API_WEBAUDIO || !context["createConvolver"])
			return;
		
		var doNormalize = (norm === 0);
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		var b = this.getAudioBuffer(info, false);
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		
		var fx;
		
		// buffer loaded
		if (b.bufferObject)
		{
			fx = new ConvolveEffect(b.bufferObject, doNormalize, mix, info.url);
		}
		// else assign once loaded
		else
		{
			fx = new ConvolveEffect(null, doNormalize, mix, info.url);
			b.normalizeWhenReady = doNormalize;
			b.convolveWhenReady = fx;
		}
		
		addEffectForTag(tag, fx);
	};
	
	Acts.prototype.AddGainEffect = function (tag, g)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new GainEffect(dbToLinear(g)));
	};
	
	Acts.prototype.AddMuteEffect = function (tag)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new GainEffect(0));	// re-use gain effect with 0 gain
	};
	
	Acts.prototype.AddTremoloEffect = function (tag, freq, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new TremoloEffect(freq, mix));
	};
	
	Acts.prototype.AddRingModEffect = function (tag, freq, mix)
	{
		if (api !== API_WEBAUDIO || !context["createOscillator"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new RingModulatorEffect(freq, mix));
	};
	
	Acts.prototype.AddDistortionEffect = function (tag, threshold, headroom, drive, makeupgain, mix)
	{
		if (api !== API_WEBAUDIO || !context["createWaveShaper"])
			return;
			
		tag = tag.toLowerCase();
		mix = mix / 100;
		if (mix < 0) mix = 0;
		if (mix > 1) mix = 1;
		addEffectForTag(tag, new DistortionEffect(threshold, headroom, drive, makeupgain, mix));
	};
	
	Acts.prototype.AddCompressorEffect = function (tag, threshold, knee, ratio, attack, release)
	{
		if (api !== API_WEBAUDIO || !context["createDynamicsCompressor"])
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new CompressorEffect(threshold, knee, ratio, attack / 1000, release / 1000));
	};
	
	Acts.prototype.AddAnalyserEffect = function (tag, fftSize, smoothing)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		addEffectForTag(tag, new AnalyserEffect(fftSize, smoothing));
	};
	
	Acts.prototype.RemoveEffects = function (tag)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		var i, len, arr;
		
		if (effects.hasOwnProperty(tag))
		{
			arr = effects[tag];
			
			if (arr.length)
			{
				for (i = 0, len = arr.length; i < len; i++)
					arr[i].remove();
				
				cr.clearArray(arr);
				reconnectEffects(tag);
			}
		}
	};
	
	Acts.prototype.SetEffectParameter = function (tag, index, param, value, ramp, time)
	{
		if (api !== API_WEBAUDIO)
			return;
			
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var arr;
		
		if (!effects.hasOwnProperty(tag))
			return;
		
		arr = effects[tag];
		
		if (index < 0 || index >= arr.length)
			return;
			
		arr[index].setParam(param, value, ramp, time);
	};
	
	Acts.prototype.SetListenerObject = function (obj_)
	{
		if (!obj_ || api !== API_WEBAUDIO)
			return;
			
		var inst = obj_.getFirstPicked();
		
		if (!inst)
			return;
			
		this.listenerTracker.setObject(inst);
		listenerX = inst.x;
		listenerY = inst.y;
	};
	
	Acts.prototype.SetListenerZ = function (z)
	{
		this.listenerZ = z;
	};
	
	Acts.prototype.ScheduleNextPlay = function (t)
	{
		if (!context)
			return;		// needs Web Audio API
		
		this.nextPlayTime = t;
	};
	
	Acts.prototype.UnloadAudio = function (file)
	{
		var is_music = file[1];
		var info = this.runtime.getProjectAudioFileUrl(file[0]);
		if (!info)
			return;
		
		var b = this.getAudioBuffer(info, is_music, true /* don't create if missing */);
		
		if (!b)
			return;		// not loaded
		
		b.release();
		cr.arrayFindRemove(audioBuffers, b);
	};
	
	Acts.prototype.UnloadAudioByName = function (folder, filename)
	{
		var is_music = (folder === 1);
		var info = this.runtime.getProjectAudioFileUrl(filename);
		if (!info)
			return;
		
		var b = this.getAudioBuffer(info, is_music, true /* don't create if missing */);
		
		if (!b)
			return;		// not loaded
		
		b.release();
		cr.arrayFindRemove(audioBuffers, b);
	};
	
	Acts.prototype.UnloadAll = function ()
	{
		var i, len;
		
		for (i = 0, len = audioBuffers.length; i < len; ++i)
		{
			audioBuffers[i].release();
		};
		
		cr.clearArray(audioBuffers);
	};
	
	pluginProto.acts = new Acts();

	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.Duration = function (ret, tag)
	{
		getAudioByTag(tag, true);
		
		if (taggedAudio.length)
			ret.set_float(taggedAudio[0].getDuration());
		else
			ret.set_float(0);
	};
	
	Exps.prototype.PlaybackTime = function (ret, tag)
	{
		getAudioByTag(tag, true);
		
		if (taggedAudio.length)
			ret.set_float(taggedAudio[0].getPlaybackTime(true));
		else
			ret.set_float(0);
	};
	
	Exps.prototype.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 && index >= 0 && index < arr.length && arr[index].freqBins)
			return arr[index];
		else
			return null;
	};
	
	Exps.prototype.AnalyserFreqBinCount = function (ret, tag, index)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var analyser = getAnalyser(tag, index);
			
		ret.set_int(analyser ? analyser.node["frequencyBinCount"] : 0);
	};
	
	Exps.prototype.AnalyserFreqBinAt = function (ret, tag, index, bin)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		bin = Math.floor(bin);
		var analyser = getAnalyser(tag, index);
			
		if (!analyser)
			ret.set_float(0);
		else if (bin < 0 || bin >= analyser.node["frequencyBinCount"])
			ret.set_float(0);
		else
			ret.set_float(analyser.freqBins[bin]);
	};
	
	Exps.prototype.AnalyserPeakLevel = function (ret, tag, index)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var analyser = getAnalyser(tag, index);
			
		if (analyser)
			ret.set_float(analyser.peak);
		else
			ret.set_float(0);
	};
	
	Exps.prototype.AnalyserRMSLevel = function (ret, tag, index)
	{
		tag = tag.toLowerCase();
		index = Math.floor(index);
		var analyser = getAnalyser(tag, index);
			
		if (analyser)
			ret.set_float(analyser.rms);
		else
			ret.set_float(0);
	};
	
	Exps.prototype.SampleRate = function (ret)
	{
		ret.set_int(context ? context.sampleRate : 0);
	};
	
	Exps.prototype.CurrentTime = function (ret)
	{
		// fall back to now time if web audio API not available
		ret.set_float(context ? context.currentTime : cr.performance_now());
	};
	
	pluginProto.exps = new Exps();

}());

// Function
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.Function.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;
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	var funcStack = [];
	var funcStackPtr = -1;
	var isInPreview = false;	// set in onCreate
	
	function FuncStackEntry()
	{
		this.name = "";
		this.retVal = 0;
		this.params = [];
	};
	
	function pushFuncStack()
	{
		funcStackPtr++;
		
		if (funcStackPtr === funcStack.length)
			funcStack.push(new FuncStackEntry());
			
		return funcStack[funcStackPtr];
	};
	
	function getCurrentFuncStack()
	{
		if (funcStackPtr < 0)
			return null;
			
		return funcStack[funcStackPtr];
	};
	
	function getOneAboveFuncStack()
	{
		if (!funcStack.length)
			return null;
		
		var i = funcStackPtr + 1;
		
		if (i >= funcStack.length)
			i = funcStack.length - 1;
			
		return funcStack[i];
	};
	
	function popFuncStack()
	{
;
		
		funcStackPtr--;
	};

	instanceProto.onCreate = function()
	{
		isInPreview = this.runtime.isPreview;
		
		// Attach a global function to run functions so JS integrators don't have to
		// resort to appalling hacks
		var self = this;
		
		window["c2_callFunction"] = function (name_, params_)
		{
			var i, len, v;
			var fs = pushFuncStack();
			fs.name = name_.toLowerCase();
			fs.retVal = 0;
			
			if (params_)
			{
				// copy only number and string types; all others just set to 0
				fs.params.length = params_.length;
				
				for (i = 0, len = params_.length; i < len; ++i)
				{
					v = params_[i];
					
					if (typeof v === "number" || typeof v === "string")
						fs.params[i] = v;
					else if (typeof v === "boolean")
						fs.params[i] = (v ? 1 : 0);
					else
						fs.params[i] = 0;
				}
			}
			else
			{
				cr.clearArray(fs.params);
			}
			
			// Note: executing fast trigger path based on fs.name
			self.runtime.trigger(cr.plugins_.Function.prototype.cnds.OnFunction, self, fs.name);
			
			popFuncStack();
			
			return fs.retVal;
		};
	};
	
	//////////////////////////////////////
	// Conditions
	function Cnds() {};

	Cnds.prototype.OnFunction = function (name_)
	{
		var fs = getCurrentFuncStack();
		
		if (!fs)
			return false;
		
		return cr.equals_nocase(name_, fs.name);
	};
	
	Cnds.prototype.CompareParam = function (index_, cmp_, value_)
	{
		var fs = getCurrentFuncStack();
		
		if (!fs)
			return false;
		
		index_ = cr.floor(index_);
		var paramValue = 0;
		
		if (index_ >= 0 && index_ < fs.params.length)
			paramValue = fs.params[index_];
			
		return cr.do_cmp(paramValue, cmp_, value_);
	};
	
	pluginProto.cnds = new Cnds();

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

	Acts.prototype.CallFunction = function (name_, params_)
	{
		var fs = pushFuncStack();
		fs.name = name_.toLowerCase();
		fs.retVal = 0;
		cr.shallowAssignArray(fs.params, params_);
		
		// Note: executing fast trigger path based on fs.name
		var ran = this.runtime.trigger(cr.plugins_.Function.prototype.cnds.OnFunction, this, fs.name);
		
		// In preview mode, log to the console if nothing was triggered
		if (isInPreview && !ran)
		{
;
		}
		
		popFuncStack();
	};
	
	Acts.prototype.SetReturnValue = function (value_)
	{
		var fs = getCurrentFuncStack();
		
		if (fs)
			fs.retVal = value_;
		else
;
	};
	
	Acts.prototype.CallExpression = function (unused)
	{
		// no-op: the function will have been called during parameter evaluation.
	};
	
	pluginProto.acts = new Acts();

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

	Exps.prototype.ReturnValue = function (ret)
	{
		// The previous function has already popped - so check one level up the function stack
		var fs = getOneAboveFuncStack();
		
		if (fs)
			ret.set_any(fs.retVal);
		else
			ret.set_int(0);
	};
	
	Exps.prototype.ParamCount = function (ret)
	{
		var fs = getCurrentFuncStack();
		
		if (fs)
			ret.set_int(fs.params.length);
		else
		{
;
			ret.set_int(0);
		}
	};
	
	Exps.prototype.Param = function (ret, index_)
	{
		index_ = cr.floor(index_);
		var fs = getCurrentFuncStack();
		
		if (fs)
		{
			if (index_ >= 0 && index_ < fs.params.length)
			{
				ret.set_any(fs.params[index_]);
			}
			else
			{
;
				ret.set_int(0);
			}
		}
		else
		{
;
			ret.set_int(0);
		}
	};
	
	Exps.prototype.Call = function (ret, name_)
	{
		var fs = pushFuncStack();
		fs.name = name_.toLowerCase();
		fs.retVal = 0;
		
		// Copy rest of parameters from arguments
		cr.clearArray(fs.params);
		var i, len;
		for (i = 2, len = arguments.length; i < len; i++)
			fs.params.push(arguments[i]);
		
		// Note: executing fast trigger path based on fs.name
		var ran = this.runtime.trigger(cr.plugins_.Function.prototype.cnds.OnFunction, this, fs.name);
		
		// In preview mode, log to the console if nothing was triggered
		if (isInPreview && !ran)
		{
;
		}
		
		popFuncStack();

		ret.set_any(fs.retVal);
	};
	
	pluginProto.exps = new Exps();

}());

// AJAX
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var isNWjs = false;
	var path = null;
	var fs = null;
	var nw_appfolder = "";
	
	var pluginProto = cr.plugins_.AJAX.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.lastData = "";
		this.curTag = "";
		this.progress = 0;
		this.timeout = -1;
		
		isNWjs = this.runtime.isNWjs;
		
		if (isNWjs)
		{
			path = require("path");
			fs = require("fs");
			var process = window["process"] || nw["process"];
			nw_appfolder = path["dirname"](process["execPath"]) + "\\";
		}
	};
	
	var instanceProto = pluginProto.Instance.prototype;
	
	instanceProto.onCreate = function()
	{
	};
	
	instanceProto.saveToJSON = function ()
	{
		return { "lastData": this.lastData };
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		this.lastData = o["lastData"];
		this.curTag = "";
		this.progress = 0;
	};
	
	var next_request_headers = {};
	var next_override_mime = "";
	
	instanceProto.doRequest = function (tag_, url_, method_, data_)
	{
		// Create a context object with the tag name and a reference back to this
		var self = this;
		var request = null;
		
		var doErrorFunc = function ()
		{
			self.curTag = tag_;
			self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyError, self);
			self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnError, self);
		};
		
		var errorFunc = function ()
		{
			// In node-webkit, try looking up the file on disk instead since it wasn't found in the project.
			if (isNWjs)
			{
				var filepath = nw_appfolder + url_;
				
				if (fs["existsSync"](filepath))
				{
					fs["readFile"](filepath, {"encoding": "utf8"}, function (err, data) {
						if (err)
						{
							doErrorFunc();
							return;
						}
						
						self.curTag = tag_;
						self.lastData = data.replace(/\r\n/g, "\n")
						self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyComplete, self);
						self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnComplete, self);
					});
				}
				else
					doErrorFunc();
			}
			else
				doErrorFunc();
		};
			
		var progressFunc = function (e)
		{
			if (!e["lengthComputable"])
				return;
				
			self.progress = e.loaded / e.total;
			self.curTag = tag_;
			self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnProgress, self);
		};
			
		try
		{
			// 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.runtime.isWindowsPhone8)
				request = new ActiveXObject("Microsoft.XMLHTTP");
			else
				request = new XMLHttpRequest();
			
			request.onreadystatechange = function()
			{
				if (request.readyState === 4)
				{
					self.curTag = tag_;
					
					if (request.responseText)
						self.lastData = request.responseText.replace(/\r\n/g, "\n");		// fix windows style line endings
					else
						self.lastData = "";
					
					if (request.status >= 400)
					{
						self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyError, self);
						self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnError, self);
					}
					else
					{
						// In node-webkit, don't trigger 'on success' with empty string if file not found.
						// In a browser also don't trigger 'on success' if the returned string is empty and the status is 0,
						// which is what happens when onerror is about to fire.
						if ((!isNWjs || self.lastData.length) && !(!isNWjs && request.status === 0 && !self.lastData.length))
						{
							self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyComplete, self);
							self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnComplete, self);
						}
					}
				}
			};
			
			if (!this.runtime.isWindowsPhone8)
			{
				request.onerror = errorFunc;
				request.ontimeout = errorFunc;
				request.onabort = errorFunc;
				request["onprogress"] = progressFunc;
			}
			
			request.open(method_, url_);
			
			if (!this.runtime.isWindowsPhone8)
			{
				// IE requires timeout be set after open()
				if (this.timeout >= 0 && typeof request["timeout"] !== "undefined")
					request["timeout"] = this.timeout;
			}
			
			request.responseType = "text";
			
			if (data_)
			{
				if (request["setRequestHeader"] && !next_request_headers.hasOwnProperty("Content-Type"))
				{
					request["setRequestHeader"]("Content-Type", "application/x-www-form-urlencoded");
				}
			}
			
			// Apply custom headers
			if (request["setRequestHeader"])
			{
				var p;
				for (p in next_request_headers)
				{
					if (next_request_headers.hasOwnProperty(p))
					{
						try {
							request["setRequestHeader"](p, next_request_headers[p]);
						}
						catch (e) {}
					}
				}
				
				// Reset for next request
				next_request_headers = {};
			}
			
			// Apply MIME type override if one set
			if (next_override_mime && request["overrideMimeType"])
			{
				try {
					request["overrideMimeType"](next_override_mime);
				}
				catch (e) {}
				
				// Reset for next request
				next_override_mime = "";
			}

			if (data_)
				request.send(data_);
			else
				request.send();
			
		}
		catch (e)
		{
			errorFunc();
		}
	};
	

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

	Cnds.prototype.OnComplete = function (tag)
	{
		return cr.equals_nocase(tag, this.curTag);
	};
	
	Cnds.prototype.OnAnyComplete = function (tag)
	{
		return true;
	};
	
	Cnds.prototype.OnError = function (tag)
	{
		return cr.equals_nocase(tag, this.curTag);
	};
	
	Cnds.prototype.OnAnyError = function (tag)
	{
		return true;
	};
	
	Cnds.prototype.OnProgress = function (tag)
	{
		return cr.equals_nocase(tag, this.curTag);
	};
	
	pluginProto.cnds = new Cnds();

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

	Acts.prototype.Request = function (tag_, url_)
	{
		var self = this;
		
		// In WKWebView mode, if the url is relative (e.g. foo.txt) it should be read as a local file
		// as RequestFile does. Otherwise it still issues an AJAX request that fails.
		if (this.runtime.isWKWebView && !this.runtime.isAbsoluteUrl(url_))
		{
			this.runtime.fetchLocalFileViaCordovaAsText(url_,
			function (str)
			{
				self.curTag = tag_;
				self.lastData = str.replace(/\r\n/g, "\n");		// fix windows style line endings
				
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyComplete, self);
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnComplete, self);
			},
			function (err)
			{
				self.curTag = tag_;
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyError, self);
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnError, self);
			});
		}
		// In preview mode, also convert relative URLs to local file requests.
		else if (this.runtime.isPreview && !this.runtime.isAbsoluteUrl(url_))
		{
			this.doRequest(tag_, this.runtime.getLocalFileUrl(url_), "GET");
		}
		else
		{
			this.doRequest(tag_, url_, "GET");
		}
	};
	
	Acts.prototype.RequestFile = function (tag_, file_)
	{
		var self = this;
		
		// Use cordova to load local files in WKWebView mode, since AJAX requests will fail.
		if (this.runtime.isWKWebView)
		{
			this.runtime.fetchLocalFileViaCordovaAsText(file_,
			function (str)
			{
				self.curTag = tag_;
				self.lastData = str.replace(/\r\n/g, "\n");		// fix windows style line endings
				
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyComplete, self);
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnComplete, self);
			},
			function (err)
			{
				self.curTag = tag_;
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnAnyError, self);
				self.runtime.trigger(cr.plugins_.AJAX.prototype.cnds.OnError, self);
			});
		}
		else
		{
			this.doRequest(tag_, this.runtime.getLocalFileUrl(file_), "GET");
		}
	};
	
	Acts.prototype.Post = function (tag_, url_, data_, method_)
	{
		this.doRequest(tag_, url_, method_, data_);
	};
	
	Acts.prototype.SetTimeout = function (t)
	{
		this.timeout = t * 1000;
	};
	
	Acts.prototype.SetHeader = function (n, v)
	{
		next_request_headers[n] = v;
	};
	
	Acts.prototype.OverrideMIMEType = function (m)
	{
		next_override_mime = m;
	};
	
	pluginProto.acts = new Acts();

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

	Exps.prototype.LastData = function (ret)
	{
		ret.set_string(this.lastData);
	};
	
	Exps.prototype.Progress = function (ret)
	{
		ret.set_float(this.progress);
	};
	
	Exps.prototype.Tag = function (ret)
	{
		ret.set_string(this.curTag);
	};
	
	pluginProto.exps = new Exps();

}());

// Dictionary
// ECMAScript 5 strict mode

;
;

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

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

	var typeProto = pluginProto.Type.prototype;

	// called on startup for each object type
	typeProto.onCreate = function()
	{
	};

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

	// called whenever an instance is created
	instanceProto.onCreate = function()
	{
		this.dictionary = {};
		this.cur_key = "";		// current key in for-each loop
		this.key_count = 0;
	};
	
	instanceProto.saveToJSON = function ()
	{
		return this.dictionary;
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		this.dictionary = o;
		
		// Update the key count
		this.key_count = 0;
		
		for (var p in this.dictionary)
		{
			if (this.dictionary.hasOwnProperty(p))
				this.key_count++;
		}
	};
	

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

	Cnds.prototype.CompareValue = function (key_, cmp_, value_)
	{
		return cr.do_cmp(this.dictionary[key_], cmp_, value_);
	};
	
	Cnds.prototype.ForEachKey = function ()
	{
		var current_event = this.runtime.getCurrentEventStack().current_event;
	
		for (var p in this.dictionary)
		{
			if (this.dictionary.hasOwnProperty(p))
			{
				this.cur_key = p;
				this.runtime.pushCopySol(current_event.solModifiers);
				current_event.retrigger();
				this.runtime.popSol(current_event.solModifiers);
			}
		}

		this.cur_key = "";
		return false;
	};
	
	Cnds.prototype.CompareCurrentValue = function (cmp_, value_)
	{
		return cr.do_cmp(this.dictionary[this.cur_key], cmp_, value_);
	};
	
	Cnds.prototype.HasKey = function (key_)
	{
		return this.dictionary.hasOwnProperty(key_);
	};
	
	Cnds.prototype.IsEmpty = function ()
	{
		return this.key_count === 0;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.AddKey = function (key_, value_)
	{
		if (!this.dictionary.hasOwnProperty(key_))
			this.key_count++;
		
		this.dictionary[key_] = value_;
	};
	
	Acts.prototype.SetKey = function (key_, value_)
	{
		if (this.dictionary.hasOwnProperty(key_))
			this.dictionary[key_] = value_;
	};
	
	Acts.prototype.DeleteKey = function (key_)
	{
		if (this.dictionary.hasOwnProperty(key_))
		{
			delete this.dictionary[key_];
			this.key_count--;
		}
	};
	
	Acts.prototype.Clear = function ()
	{
		cr.wipe(this.dictionary);		// avoid garbaging
		this.key_count = 0;
	};
	
	Acts.prototype.JSONLoad = function (json_)
	{
		var o;
		
		try {
			o = JSON.parse(json_);
		}
		catch(e) { return; }
		
		if (!o["c2dictionary"])		// presumably not a c2dictionary object
			return;
		
		this.dictionary = o["data"];
		
		// Update the key count
		this.key_count = 0;
		
		for (var p in this.dictionary)
		{
			if (this.dictionary.hasOwnProperty(p))
				this.key_count++;
		}
	};
	
	Acts.prototype.JSONDownload = function (filename)
	{
		var a = document.createElement("a");
		
		if (typeof a.download === "undefined")
		{
			var str = 'data:text/html,' + encodeURIComponent("<p><a download='data.json' href=\"data:application/json,"
				+ encodeURIComponent(JSON.stringify({
						"c2dictionary": true,
						"data": this.dictionary
					}))
				+ "\">Download link</a></p>");
			window.open(str);
		}
		else
		{
			// auto download
			var body = document.getElementsByTagName("body")[0];
			a.textContent = filename;
			a.href = "data:application/json," + encodeURIComponent(JSON.stringify({
						"c2dictionary": true,
						"data": this.dictionary
					}));
			a.download = filename;
			body.appendChild(a);
			a.click();
			body.removeChild(a);
		}
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	// ret.set_float, ret.set_string, ret.set_any
	function Exps() {};
	
	Exps.prototype.Get = function (ret, key_)
	{
		if (this.dictionary.hasOwnProperty(key_))
			ret.set_any(this.dictionary[key_]);
		else
			ret.set_int(0);
	};

	Exps.prototype.GetDefault = function (ret, key_, default_)
	{
		if (this.dictionary.hasOwnProperty(key_))
			ret.set_any(this.dictionary[key_]);
		else
			ret.set_any(default_);
	};
	
	Exps.prototype.KeyCount = function (ret)
	{
		ret.set_int(this.key_count);
	};
	
	Exps.prototype.CurrentKey = function (ret)
	{
		ret.set_string(this.cur_key);
	};
	
	Exps.prototype.CurrentValue = function (ret)
	{
		// Could be requested outside for-each loop
		if (this.dictionary.hasOwnProperty(this.cur_key))
			ret.set_any(this.dictionary[this.cur_key]);
		else
			ret.set_int(0);
	};
	
	Exps.prototype.AsJSON = function (ret)
	{
		ret.set_string(JSON.stringify({
			"c2dictionary": true,
			"data": this.dictionary
		}));
	};
	
	pluginProto.exps = new Exps();

}());

// Local storage
// ECMAScript 5 strict mode

;
;

var localForageInitFailed = false;

try {

/*!
    localForage -- Offline Storage, Improved
    Version 1.5.0
    https://localforage.github.io/localForage
    (c) 2013-2017 Mozilla, Apache License 2.0
*/
!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.localforage=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){"use strict";function d(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}function e(){try{if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof webkitIndexedDB)return webkitIndexedDB;if("undefined"!=typeof mozIndexedDB)return mozIndexedDB;if("undefined"!=typeof OIndexedDB)return OIndexedDB;if("undefined"!=typeof msIndexedDB)return msIndexedDB}catch(a){}}function f(){try{if(!ga)return!1;var a="undefined"!=typeof openDatabase&&/(Safari|iPhone|iPad|iPod)/.test(navigator.userAgent)&&!/Chrome/.test(navigator.userAgent)&&!/BlackBerry/.test(navigator.platform),b="function"==typeof fetch&&-1!==fetch.toString().indexOf("[native code");return(!a||b)&&"undefined"!=typeof indexedDB&&"undefined"!=typeof IDBKeyRange}catch(c){return!1}}function g(){return"function"==typeof openDatabase}function h(){try{return"undefined"!=typeof localStorage&&"setItem"in localStorage&&localStorage.setItem}catch(a){return!1}}function i(a,b){a=a||[],b=b||{};try{return new Blob(a,b)}catch(c){if("TypeError"!==c.name)throw c;for(var d="undefined"!=typeof BlobBuilder?BlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder?MozBlobBuilder:WebKitBlobBuilder,e=new d,f=0;f<a.length;f+=1)e.append(a[f]);return e.getBlob(b.type)}}function j(a,b){b&&a.then(function(a){b(null,a)},function(a){b(a)})}function k(a,b,c){"function"==typeof b&&a.then(b),"function"==typeof c&&a["catch"](c)}function l(a){for(var b=a.length,c=new ArrayBuffer(b),d=new Uint8Array(c),e=0;b>e;e++)d[e]=a.charCodeAt(e);return c}function m(a){return new ja(function(b){var c=a.transaction(ka,"readwrite"),d=i([""]);c.objectStore(ka).put(d,"key"),c.onabort=function(a){a.preventDefault(),a.stopPropagation(),b(!1)},c.oncomplete=function(){var a=navigator.userAgent.match(/Chrome\/(\d+)/),c=navigator.userAgent.match(/Edge\//);b(c||!a||parseInt(a[1],10)>=43)}})["catch"](function(){return!1})}function n(a){return"boolean"==typeof ha?ja.resolve(ha):m(a).then(function(a){return ha=a})}function o(a){var b=ia[a.name],c={};c.promise=new ja(function(a){c.resolve=a}),b.deferredOperations.push(c),b.dbReady?b.dbReady=b.dbReady.then(function(){return c.promise}):b.dbReady=c.promise}function p(a){var b=ia[a.name],c=b.deferredOperations.pop();c&&c.resolve()}function q(a,b){return new ja(function(c,d){if(a.db){if(!b)return c(a.db);o(a),a.db.close()}var e=[a.name];b&&e.push(a.version);var f=ga.open.apply(ga,e);b&&(f.onupgradeneeded=function(b){var c=f.result;try{c.createObjectStore(a.storeName),b.oldVersion<=1&&c.createObjectStore(ka)}catch(d){if("ConstraintError"!==d.name)throw d;console.warn('The database "'+a.name+'" has been upgraded from version '+b.oldVersion+" to version "+b.newVersion+', but the storage "'+a.storeName+'" already exists.')}}),f.onerror=function(a){a.preventDefault(),d(f.error)},f.onsuccess=function(){c(f.result),p(a)}})}function r(a){return q(a,!1)}function s(a){return q(a,!0)}function t(a,b){if(!a.db)return!0;var c=!a.db.objectStoreNames.contains(a.storeName),d=a.version<a.db.version,e=a.version>a.db.version;if(d&&(a.version!==b&&console.warn('The database "'+a.name+"\" can't be downgraded from version "+a.db.version+" to version "+a.version+"."),a.version=a.db.version),e||c){if(c){var f=a.db.version+1;f>a.version&&(a.version=f)}return!0}return!1}function u(a){return new ja(function(b,c){var d=new FileReader;d.onerror=c,d.onloadend=function(c){var d=btoa(c.target.result||"");b({__local_forage_encoded_blob:!0,data:d,type:a.type})},d.readAsBinaryString(a)})}function v(a){var b=l(atob(a.data));return i([b],{type:a.type})}function w(a){return a&&a.__local_forage_encoded_blob}function x(a){var b=this,c=b._initReady().then(function(){var a=ia[b._dbInfo.name];return a&&a.dbReady?a.dbReady:void 0});return k(c,a,a),c}function y(a){function b(){return ja.resolve()}var c=this,d={db:null};if(a)for(var e in a)d[e]=a[e];ia||(ia={});var f=ia[d.name];f||(f={forages:[],db:null,dbReady:null,deferredOperations:[]},ia[d.name]=f),f.forages.push(c),c._initReady||(c._initReady=c.ready,c.ready=x);for(var g=[],h=0;h<f.forages.length;h++){var i=f.forages[h];i!==c&&g.push(i._initReady()["catch"](b))}var j=f.forages.slice(0);return ja.all(g).then(function(){return d.db=f.db,r(d)}).then(function(a){return d.db=a,t(d,c._defaultConfig.version)?s(d):a}).then(function(a){d.db=f.db=a,c._dbInfo=d;for(var b=0;b<j.length;b++){var e=j[b];e!==c&&(e._dbInfo.db=d.db,e._dbInfo.version=d.version)}})}function z(a,b){var c=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=f.get(a);g.onsuccess=function(){var a=g.result;void 0===a&&(a=null),w(a)&&(a=v(a)),b(a)},g.onerror=function(){d(g.error)}})["catch"](d)});return j(d,b),d}function A(a,b){var c=this,d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=f.openCursor(),h=1;g.onsuccess=function(){var c=g.result;if(c){var d=c.value;w(d)&&(d=v(d));var e=a(d,c.key,h++);void 0!==e?b(e):c["continue"]()}else b()},g.onerror=function(){d(g.error)}})["catch"](d)});return j(d,b),d}function B(a,b,c){var d=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=new ja(function(c,e){var f;d.ready().then(function(){return f=d._dbInfo,"[object Blob]"===la.call(b)?n(f.db).then(function(a){return a?b:u(b)}):b}).then(function(b){var d=f.db.transaction(f.storeName,"readwrite"),g=d.objectStore(f.storeName),h=g.put(b,a);null===b&&(b=void 0),d.oncomplete=function(){void 0===b&&(b=null),c(b)},d.onabort=d.onerror=function(){var a=h.error?h.error:h.transaction.error;e(a)}})["catch"](e)});return j(e,c),e}function C(a,b){var c=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readwrite"),g=f.objectStore(e.storeName),h=g["delete"](a);f.oncomplete=function(){b()},f.onerror=function(){d(h.error)},f.onabort=function(){var a=h.error?h.error:h.transaction.error;d(a)}})["catch"](d)});return j(d,b),d}function D(a){var b=this,c=new ja(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readwrite"),f=e.objectStore(d.storeName),g=f.clear();e.oncomplete=function(){a()},e.onabort=e.onerror=function(){var a=g.error?g.error:g.transaction.error;c(a)}})["catch"](c)});return j(c,a),c}function E(a){var b=this,c=new ja(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readonly").objectStore(d.storeName),f=e.count();f.onsuccess=function(){a(f.result)},f.onerror=function(){c(f.error)}})["catch"](c)});return j(c,a),c}function F(a,b){var c=this,d=new ja(function(b,d){return 0>a?void b(null):void c.ready().then(function(){var e=c._dbInfo,f=e.db.transaction(e.storeName,"readonly").objectStore(e.storeName),g=!1,h=f.openCursor();h.onsuccess=function(){var c=h.result;return c?void(0===a?b(c.key):g?b(c.key):(g=!0,c.advance(a))):void b(null)},h.onerror=function(){d(h.error)}})["catch"](d)});return j(d,b),d}function G(a){var b=this,c=new ja(function(a,c){b.ready().then(function(){var d=b._dbInfo,e=d.db.transaction(d.storeName,"readonly").objectStore(d.storeName),f=e.openCursor(),g=[];f.onsuccess=function(){var b=f.result;return b?(g.push(b.key),void b["continue"]()):void a(g)},f.onerror=function(){c(f.error)}})["catch"](c)});return j(c,a),c}function H(a){var b,c,d,e,f,g=.75*a.length,h=a.length,i=0;"="===a[a.length-1]&&(g--,"="===a[a.length-2]&&g--);var j=new ArrayBuffer(g),k=new Uint8Array(j);for(b=0;h>b;b+=4)c=na.indexOf(a[b]),d=na.indexOf(a[b+1]),e=na.indexOf(a[b+2]),f=na.indexOf(a[b+3]),k[i++]=c<<2|d>>4,k[i++]=(15&d)<<4|e>>2,k[i++]=(3&e)<<6|63&f;return j}function I(a){var b,c=new Uint8Array(a),d="";for(b=0;b<c.length;b+=3)d+=na[c[b]>>2],d+=na[(3&c[b])<<4|c[b+1]>>4],d+=na[(15&c[b+1])<<2|c[b+2]>>6],d+=na[63&c[b+2]];return c.length%3===2?d=d.substring(0,d.length-1)+"=":c.length%3===1&&(d=d.substring(0,d.length-2)+"=="),d}function J(a,b){var c="";if(a&&(c=Ea.call(a)),a&&("[object ArrayBuffer]"===c||a.buffer&&"[object ArrayBuffer]"===Ea.call(a.buffer))){var d,e=qa;a instanceof ArrayBuffer?(d=a,e+=sa):(d=a.buffer,"[object Int8Array]"===c?e+=ua:"[object Uint8Array]"===c?e+=va:"[object Uint8ClampedArray]"===c?e+=wa:"[object Int16Array]"===c?e+=xa:"[object Uint16Array]"===c?e+=za:"[object Int32Array]"===c?e+=ya:"[object Uint32Array]"===c?e+=Aa:"[object Float32Array]"===c?e+=Ba:"[object Float64Array]"===c?e+=Ca:b(new Error("Failed to get type for BinaryArray"))),b(e+I(d))}else if("[object Blob]"===c){var f=new FileReader;f.onload=function(){var c=oa+a.type+"~"+I(this.result);b(qa+ta+c)},f.readAsArrayBuffer(a)}else try{b(JSON.stringify(a))}catch(g){console.error("Couldn't convert value into a JSON string: ",a),b(null,g)}}function K(a){if(a.substring(0,ra)!==qa)return JSON.parse(a);var b,c=a.substring(Da),d=a.substring(ra,Da);if(d===ta&&pa.test(c)){var e=c.match(pa);b=e[1],c=c.substring(e[0].length)}var f=H(c);switch(d){case sa:return f;case ta:return i([f],{type:b});case ua:return new Int8Array(f);case va:return new Uint8Array(f);case wa:return new Uint8ClampedArray(f);case xa:return new Int16Array(f);case za:return new Uint16Array(f);case ya:return new Int32Array(f);case Aa:return new Uint32Array(f);case Ba:return new Float32Array(f);case Ca:return new Float64Array(f);default:throw new Error("Unkown type: "+d)}}function L(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"!=typeof a[d]?a[d].toString():a[d];var e=new ja(function(a,d){try{c.db=openDatabase(c.name,String(c.version),c.description,c.size)}catch(e){return d(e)}c.db.transaction(function(e){e.executeSql("CREATE TABLE IF NOT EXISTS "+c.storeName+" (id INTEGER PRIMARY KEY, key unique, value)",[],function(){b._dbInfo=c,a()},function(a,b){d(b)})})});return c.serializer=Fa,e}function M(a,b){var c=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName+" WHERE key = ? LIMIT 1",[a],function(a,c){var d=c.rows.length?c.rows.item(0).value:null;d&&(d=e.serializer.deserialize(d)),b(d)},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function N(a,b){var c=this,d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT * FROM "+e.storeName,[],function(c,d){for(var f=d.rows,g=f.length,h=0;g>h;h++){var i=f.item(h),j=i.value;if(j&&(j=e.serializer.deserialize(j)),j=a(j,i.key,h+1),void 0!==j)return void b(j)}b()},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function O(a,b,c,d){var e=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var f=new ja(function(f,g){e.ready().then(function(){void 0===b&&(b=null);var h=b,i=e._dbInfo;i.serializer.serialize(b,function(b,j){j?g(j):i.db.transaction(function(c){c.executeSql("INSERT OR REPLACE INTO "+i.storeName+" (key, value) VALUES (?, ?)",[a,b],function(){f(h)},function(a,b){g(b)})},function(b){if(b.code===b.QUOTA_ERR){if(d>0)return void f(O.apply(e,[a,h,c,d-1]));g(b)}})})})["catch"](g)});return j(f,c),f}function P(a,b,c){return O.apply(this,[a,b,c,1])}function Q(a,b){var c=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("DELETE FROM "+e.storeName+" WHERE key = ?",[a],function(){b()},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function R(a){var b=this,c=new ja(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("DELETE FROM "+d.storeName,[],function(){a()},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function S(a){var b=this,c=new ja(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT COUNT(key) as c FROM "+d.storeName,[],function(b,c){var d=c.rows.item(0).c;a(d)},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function T(a,b){var c=this,d=new ja(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){c.executeSql("SELECT key FROM "+e.storeName+" WHERE id = ? LIMIT 1",[a+1],function(a,c){var d=c.rows.length?c.rows.item(0).key:null;b(d)},function(a,b){d(b)})})})["catch"](d)});return j(d,b),d}function U(a){var b=this,c=new ja(function(a,c){b.ready().then(function(){var d=b._dbInfo;d.db.transaction(function(b){b.executeSql("SELECT key FROM "+d.storeName,[],function(b,c){for(var d=[],e=0;e<c.rows.length;e++)d.push(c.rows.item(e).key);a(d)},function(a,b){c(b)})})})["catch"](c)});return j(c,a),c}function V(a){var b=this,c={};if(a)for(var d in a)c[d]=a[d];return c.keyPrefix=c.name+"/",c.storeName!==b._defaultConfig.storeName&&(c.keyPrefix+=c.storeName+"/"),b._dbInfo=c,c.serializer=Fa,ja.resolve()}function W(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo.keyPrefix,c=localStorage.length-1;c>=0;c--){var d=localStorage.key(c);0===d.indexOf(a)&&localStorage.removeItem(d)}});return j(c,a),c}function X(a,b){var c=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=c.ready().then(function(){var b=c._dbInfo,d=localStorage.getItem(b.keyPrefix+a);return d&&(d=b.serializer.deserialize(d)),d});return j(d,b),d}function Y(a,b){var c=this,d=c.ready().then(function(){for(var b=c._dbInfo,d=b.keyPrefix,e=d.length,f=localStorage.length,g=1,h=0;f>h;h++){var i=localStorage.key(h);if(0===i.indexOf(d)){var j=localStorage.getItem(i);if(j&&(j=b.serializer.deserialize(j)),j=a(j,i.substring(e),g++),void 0!==j)return j}}});return j(d,b),d}function Z(a,b){var c=this,d=c.ready().then(function(){var b,d=c._dbInfo;try{b=localStorage.key(a)}catch(e){b=null}return b&&(b=b.substring(d.keyPrefix.length)),b});return j(d,b),d}function $(a){var b=this,c=b.ready().then(function(){for(var a=b._dbInfo,c=localStorage.length,d=[],e=0;c>e;e++)0===localStorage.key(e).indexOf(a.keyPrefix)&&d.push(localStorage.key(e).substring(a.keyPrefix.length));return d});return j(c,a),c}function _(a){var b=this,c=b.keys().then(function(a){return a.length});return j(c,a),c}function aa(a,b){var c=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var d=c.ready().then(function(){var b=c._dbInfo;localStorage.removeItem(b.keyPrefix+a)});return j(d,b),d}function ba(a,b,c){var d=this;"string"!=typeof a&&(console.warn(a+" used as a key, but it is not a string."),a=String(a));var e=d.ready().then(function(){void 0===b&&(b=null);var c=b;return new ja(function(e,f){var g=d._dbInfo;g.serializer.serialize(b,function(b,d){if(d)f(d);else try{localStorage.setItem(g.keyPrefix+a,b),e(c)}catch(h){"QuotaExceededError"!==h.name&&"NS_ERROR_DOM_QUOTA_REACHED"!==h.name||f(h),f(h)}})})});return j(e,c),e}function ca(a,b){a[b]=function(){var c=arguments;return a.ready().then(function(){return a[b].apply(a,c)})}}function da(){for(var a=1;a<arguments.length;a++){var b=arguments[a];if(b)for(var c in b)b.hasOwnProperty(c)&&(Oa(b[c])?arguments[0][c]=b[c].slice():arguments[0][c]=b[c])}return arguments[0]}function ea(a){for(var b in Ja)if(Ja.hasOwnProperty(b)&&Ja[b]===a)return!0;return!1}var fa="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol?"symbol":typeof a},ga=e();"undefined"==typeof Promise&&a("lie/polyfill");var ha,ia,ja=Promise,ka="local-forage-detect-blob-support",la=Object.prototype.toString,ma={_driver:"asyncStorage",_initStorage:y,iterate:A,getItem:z,setItem:B,removeItem:C,clear:D,length:E,key:F,keys:G},na="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",oa="~~local_forage_type~",pa=/^~~local_forage_type~([^~]+)~/,qa="__lfsc__:",ra=qa.length,sa="arbf",ta="blob",ua="si08",va="ui08",wa="uic8",xa="si16",ya="si32",za="ur16",Aa="ui32",Ba="fl32",Ca="fl64",Da=ra+sa.length,Ea=Object.prototype.toString,Fa={serialize:J,deserialize:K,stringToBuffer:H,bufferToString:I},Ga={_driver:"webSQLStorage",_initStorage:L,iterate:N,getItem:M,setItem:P,removeItem:Q,clear:R,length:S,key:T,keys:U},Ha={_driver:"localStorageWrapper",_initStorage:V,iterate:Y,getItem:X,setItem:ba,removeItem:aa,clear:W,length:_,key:Z,keys:$},Ia={},Ja={INDEXEDDB:"asyncStorage",LOCALSTORAGE:"localStorageWrapper",WEBSQL:"webSQLStorage"},Ka=[Ja.INDEXEDDB,Ja.WEBSQL,Ja.LOCALSTORAGE],La=["clear","getItem","iterate","key","keys","length","removeItem","setItem"],Ma={description:"",driver:Ka.slice(),name:"localforage",size:4980736,storeName:"keyvaluepairs",version:1},Na={};Na[Ja.INDEXEDDB]=f(),Na[Ja.WEBSQL]=g(),Na[Ja.LOCALSTORAGE]=h();var Oa=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)},Pa=function(){function a(b){d(this,a),this.INDEXEDDB=Ja.INDEXEDDB,this.LOCALSTORAGE=Ja.LOCALSTORAGE,this.WEBSQL=Ja.WEBSQL,this._defaultConfig=da({},Ma),this._config=da({},this._defaultConfig,b),this._driverSet=null,this._initDriver=null,this._ready=!1,this._dbInfo=null,this._wrapLibraryMethodsWithReady(),this.setDriver(this._config.driver)["catch"](function(){})}return a.prototype.config=function(a){if("object"===("undefined"==typeof a?"undefined":fa(a))){if(this._ready)return new Error("Can't call config() after localforage has been used.");for(var b in a){if("storeName"===b&&(a[b]=a[b].replace(/\W/g,"_")),"version"===b&&"number"!=typeof a[b])return new Error("Database version must be a number.");this._config[b]=a[b]}return"driver"in a&&a.driver?this.setDriver(this._config.driver):!0}return"string"==typeof a?this._config[a]:this._config},a.prototype.defineDriver=function(a,b,c){var d=new ja(function(b,c){try{var d=a._driver,e=new Error("Custom driver not compliant; see https://mozilla.github.io/localForage/#definedriver"),f=new Error("Custom driver name already in use: "+a._driver);if(!a._driver)return void c(e);if(ea(a._driver))return void c(f);for(var g=La.concat("_initStorage"),h=0;h<g.length;h++){var i=g[h];if(!i||!a[i]||"function"!=typeof a[i])return void c(e)}var j=ja.resolve(!0);"_support"in a&&(j=a._support&&"function"==typeof a._support?a._support():ja.resolve(!!a._support)),j.then(function(c){Na[d]=c,Ia[d]=a,b()},c)}catch(k){c(k)}});return k(d,b,c),d},a.prototype.driver=function(){return this._driver||null},a.prototype.getDriver=function(a,b,c){var d=this,e=ja.resolve().then(function(){if(!ea(a)){if(Ia[a])return Ia[a];throw new Error("Driver not found.")}switch(a){case d.INDEXEDDB:return ma;case d.LOCALSTORAGE:return Ha;case d.WEBSQL:return Ga}});return k(e,b,c),e},a.prototype.getSerializer=function(a){var b=ja.resolve(Fa);return k(b,a),b},a.prototype.ready=function(a){var b=this,c=b._driverSet.then(function(){return null===b._ready&&(b._ready=b._initDriver()),b._ready});return k(c,a,a),c},a.prototype.setDriver=function(a,b,c){function d(){g._config.driver=g.driver()}function e(a){return g._extend(a),d(),g._ready=g._initStorage(g._config),g._ready}function f(a){return function(){function b(){for(;c<a.length;){var f=a[c];return c++,g._dbInfo=null,g._ready=null,g.getDriver(f).then(e)["catch"](b)}d();var h=new Error("No available storage method found.");return g._driverSet=ja.reject(h),g._driverSet}var c=0;return b()}}var g=this;Oa(a)||(a=[a]);var h=this._getSupportedDrivers(a),i=null!==this._driverSet?this._driverSet["catch"](function(){return ja.resolve()}):ja.resolve();return this._driverSet=i.then(function(){var a=h[0];return g._dbInfo=null,g._ready=null,g.getDriver(a).then(function(a){g._driver=a._driver,d(),g._wrapLibraryMethodsWithReady(),g._initDriver=f(h)})})["catch"](function(){d();var a=new Error("No available storage method found.");return g._driverSet=ja.reject(a),g._driverSet}),k(this._driverSet,b,c),this._driverSet},a.prototype.supports=function(a){return!!Na[a]},a.prototype._extend=function(a){da(this,a)},a.prototype._getSupportedDrivers=function(a){for(var b=[],c=0,d=a.length;d>c;c++){var e=a[c];this.supports(e)&&b.push(e)}return b},a.prototype._wrapLibraryMethodsWithReady=function(){for(var a=0;a<La.length;a++)ca(this,La[a])},a.prototype.createInstance=function(b){return new a(b)},a}(),Qa=new Pa;b.exports=Qa},{undefined:void 0}]},{},[1])(1)});
// End localforage code

}
catch (e)
{
	// Firefox with cookies disabled throws exception starting up localforage.
	localForageInitFailed = true;
}

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

(function ()
{
	var currentKey = "";
	var lastValue = "";
	var keyNamesList = [];
	var errorMessage = "";
	
	function getErrorString(err)
	{
		if (!err)
			return "unknown error";
		else if (typeof err === "string")
			return err;
		else if (typeof err.message === "string")
			return err.message;
		else if (typeof err.name === "string")
			return err.name;
		else if (typeof err.data === "string")
			return err.data;
		else
			return "unknown error";
	};
	
	function TriggerStorageError(self, msg)
	{
		errorMessage = msg;
		self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
	};
	
	// Scirra Arcade support
	var prefix = "";
	var is_arcade = (typeof window["is_scirra_arcade"] !== "undefined");
	
	if (is_arcade)
		prefix = "sa" + window["scirra_arcade_id"] + "_";
	
	function hasRequiredPrefix(key)
	{
		if (!prefix)
			return true;
		
		return key.substr(0, prefix.length) === prefix;
	};
	
	function removePrefix(key)
	{
		if (!prefix)
			return key;
		
		if (hasRequiredPrefix(key))
			return key.substr(prefix.length);
	};
	
	/////////////////////////////////////
	var pluginProto = cr.plugins_.LocalStorage.prototype;
		
	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
	};

	var typeProto = pluginProto.Type.prototype;

	// called on startup for each object type
	typeProto.onCreate = function()
	{
	};

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

	// called whenever an instance is created
	instanceProto.onCreate = function()
	{
		this.pendingSets = 0;		// number of pending 'Set item' actions
		this.pendingGets = 0;		// number of pending 'Get item' actions
	};
	
	// called whenever an instance is destroyed
	// note the runtime may keep the object after this call for recycling; be sure
	// to release/recycle/reset any references to other objects in this function.
	instanceProto.onDestroy = function ()
	{
	};
	
	// called when saving the full state of the game
	instanceProto.saveToJSON = function ()
	{
		// return a Javascript object containing information about your object's state
		// note you MUST use double-quote syntax (e.g. "property": value) to prevent
		// Closure Compiler renaming and breaking the save format
		return {
			// e.g.
			//"myValue": this.myValue
		};
	};
	
	// called when loading the full state of the game
	instanceProto.loadFromJSON = function (o)
	{
		// load from the state previously saved by saveToJSON
		// 'o' provides the same object that you saved, e.g.
		// this.myValue = o["myValue"];
		// note you MUST use double-quote syntax (e.g. o["property"]) to prevent
		// Closure Compiler renaming and breaking the save format
	};
	
	// Prevent thrashing storage in the debugger view by setting a flag whenever storage is changed. Note this must be included
	// in exported code since the actions reference it.
	var debugDataChanged = true;
	

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

	Cnds.prototype.OnItemSet = function (key)
	{
		return currentKey === key;
	};
	
	Cnds.prototype.OnAnyItemSet = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnItemGet = function (key)
	{
		return currentKey === key;
	};
	
	Cnds.prototype.OnAnyItemGet = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnItemRemoved = function (key)
	{
		return currentKey === key;
	};
	
	Cnds.prototype.OnAnyItemRemoved = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnCleared = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnAllKeyNamesLoaded = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnError = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnItemExists = function (key)
	{
		return currentKey === key;
	};
	
	Cnds.prototype.OnItemMissing = function (key)
	{
		return currentKey === key;
	};
	
	Cnds.prototype.CompareKey = function (cmp, key)
	{
		return cr.do_cmp(currentKey, cmp, key);
	};
	
	Cnds.prototype.CompareValue = function (cmp, v)
	{
		return cr.do_cmp(lastValue, cmp, v);
	};
	
	Cnds.prototype.IsProcessingSets = function ()
	{
		return this.pendingSets > 0;
	};
	
	Cnds.prototype.IsProcessingGets = function ()
	{
		return this.pendingGets > 0;
	};
	
	Cnds.prototype.OnAllSetsComplete = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnAllGetsComplete = function ()
	{
		return true;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.SetItem = function (keyNoPrefix, value)
	{
		if (localForageInitFailed)
		{
			TriggerStorageError(this, "storage failed to initialise - may be disabled in browser settings");
			return;
		}
		
		var keyPrefix = prefix + keyNoPrefix;
		
		this.pendingSets++;
		
		var self = this;
		localforage["setItem"](keyPrefix, value, function (err, valueSet)
		{
			debugDataChanged = true;
			self.pendingSets--;
			
			if (err)
			{
				errorMessage = getErrorString(err);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
			}
			else
			{
				currentKey = keyNoPrefix;
				lastValue = valueSet;
				
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnAnyItemSet, self);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnItemSet, self);
				
				currentKey = "";
				lastValue = "";
			}
			
			// Last outstanding set completed
			if (self.pendingSets === 0)
			{
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnAllSetsComplete, self);
			}
		});
	};
	
	Acts.prototype.GetItem = function (keyNoPrefix)
	{
		if (localForageInitFailed)
		{
			TriggerStorageError(this, "storage failed to initialise - may be disabled in browser settings");
			return;
		}
		
		var keyPrefix = prefix + keyNoPrefix;
		
		this.pendingGets++;
		
		var self = this;
		localforage["getItem"](keyPrefix, function (err, value)
		{
			self.pendingGets--;
			
			if (err)
			{
				errorMessage = getErrorString(err);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
			}
			else
			{
				currentKey = keyNoPrefix;
				lastValue = value;
				
				// default undefined or null values to empty string (also converts to number as NaN)
				if (typeof lastValue === "undefined" || lastValue === null)
					lastValue = "";
				
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnAnyItemGet, self);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnItemGet, self);
				
				currentKey = "";
				lastValue = "";
			}
			
			// Last outstanding get completed
			if (self.pendingGets === 0)
			{
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnAllGetsComplete, self);
			}
		});
	};
	
	Acts.prototype.CheckItemExists = function (keyNoPrefix)
	{
		if (localForageInitFailed)
		{
			TriggerStorageError(this, "storage failed to initialise - may be disabled in browser settings");
			return;
		}
		
		var keyPrefix = prefix + keyNoPrefix;
		
		var self = this;
		localforage["getItem"](keyPrefix, function (err, value)
		{
			if (err)
			{
				errorMessage = getErrorString(err);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
			}
			else
			{
				currentKey = keyNoPrefix;
				
				if (value === null)		// null value indicates key missing
				{
					lastValue = "";		// prevent ItemValue meaning anything
					self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnItemMissing, self);
				}
				else
				{
					lastValue = value;	// make available to ItemValue expression
					self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnItemExists, self);
				}
				
				currentKey = "";
				lastValue = "";
			}
		});
	};
	
	Acts.prototype.RemoveItem = function (keyNoPrefix)
	{
		if (localForageInitFailed)
		{
			TriggerStorageError(this, "storage failed to initialise - may be disabled in browser settings");
			return;
		}
		
		var keyPrefix = prefix + keyNoPrefix;
		
		var self = this;
		localforage["removeItem"](keyPrefix, function (err)
		{
			debugDataChanged = true;
			
			if (err)
			{
				errorMessage = getErrorString(err);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
			}
			else
			{
				currentKey = keyNoPrefix;
				lastValue = "";
				
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnAnyItemRemoved, self);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnItemRemoved, self);
				
				currentKey = "";
			}
		});
	};
	
	Acts.prototype.ClearStorage = function ()
	{
		if (localForageInitFailed)
		{
			TriggerStorageError(this, "storage failed to initialise - may be disabled in browser settings");
			return;
		}
		
		// Can't clear storage in the Scirra Arcade, would remove other game's keys
		if (is_arcade)
			return;
		
		var self = this;
		localforage["clear"](function (err)
		{
			debugDataChanged = true;
			
			if (err)
			{
				errorMessage = getErrorString(err);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
			}
			else
			{
				currentKey = "";
				lastValue = "";
				cr.clearArray(keyNamesList);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnCleared, self);
			}
		});
	};
	
	Acts.prototype.GetAllKeyNames = function ()
	{
		if (localForageInitFailed)
		{
			TriggerStorageError(this, "storage failed to initialise - may be disabled in browser settings");
			return;
		}
		
		var self = this;
		localforage["keys"](function (err, keyList)
		{
			var i, len, k;
			
			if (err)
			{
				errorMessage = getErrorString(err);
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnError, self);
			}
			else
			{
				cr.clearArray(keyNamesList);
				
				// Only use key names which pass the valid prefix test (for the Scirra Arcade)
				for (i = 0, len = keyList.length; i < len; ++i)
				{
					k = keyList[i];
					
					if (!hasRequiredPrefix(k))
						continue;
					
					keyNamesList.push(removePrefix(k));
				}
				
				self.runtime.trigger(cr.plugins_.LocalStorage.prototype.cnds.OnAllKeyNamesLoaded, self);
			}
		});
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};
	
	Exps.prototype.ItemValue = function (ret)
	{
		ret.set_any(lastValue);
	};
	
	Exps.prototype.Key = function (ret)
	{
		ret.set_string(currentKey);
	};
	
	Exps.prototype.KeyCount = function (ret)
	{
		ret.set_int(keyNamesList.length);
	};
	
	Exps.prototype.KeyAt = function (ret, i)
	{
		i = Math.floor(i);
		
		if (i < 0 || i >= keyNamesList.length)
		{
			ret.set_string("");
			return;
		}
		
		ret.set_string(keyNamesList[i]);
	};
	
	Exps.prototype.ErrorMessage = function (ret)
	{
		ret.set_string(errorMessage);
	};
	
	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"] && 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 && browserPluginReady && 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 && 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 && 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 <= 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"] && navigator["app"]["exitApp"])
			navigator["app"]["exitApp"]();
		else if (navigator["device"] && 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"] && navigator["app"]["backHistory"])
			navigator["app"]["backHistory"]();
		else if (!this.is_arcade && window.back)
			window.back();
	};
	
	Acts.prototype.GoForward = function ()
	{
		if (!this.is_arcade && window.forward)
			window.forward();
	};
	
	Acts.prototype.GoHome = function ()
	{
		if (!this.is_arcade && 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"] && 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 && !this.is_arcade)		// top
				window.top.location = url;
			else if (target === 1 && !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"] && 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 && 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 >= 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 >= 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" && 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 < 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 && console.log)
			console.log(msg_.toString());
		if (type_ === 1 && console.warn)
			console.warn(msg_.toString());
		if (type_ === 2 && console.error)
			console.error(msg_.toString());
	};
	
	Acts.prototype.ConsoleGroup = function (name_)
	{
		if (console && console.group)
			console.group(name_);
	};

	Acts.prototype.ConsoleGroupEnd = function ()
	{
		if (console && 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 && 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 < 0 || o >= orientations.length)
			return;
		
		this.runtime.autoLockOrientation = false;
		
		var orientation = orientations[o];
		
		if (screen["orientation"] && 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"] && 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 && 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 && navigator.product)
			ret.set_string(navigator.product);
		else
			ret.set_string("");
	};
	
	Exps.prototype.Vendor = function (ret)
	{
		// Not in IE or DC
		if (navigator && 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('[?&]' + paramname + '=([^&]*)').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 && 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();
	
}());

// Gamepad
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var pluginProto = cr.plugins_.gamepad.prototype;
	
	var isSupported = false;
		
	/////////////////////////////////////
	// Object type class
	pluginProto.Type = function(plugin)
	{
		this.plugin = plugin;
		this.runtime = plugin.runtime;
		
		isSupported = !!(navigator["getGamepads"] || navigator["webkitGetGamepads"] || navigator["mozGetGamepads"] || navigator["gamepads"] || navigator["webkitGamepads"] || navigator["MozGamepads"]);
	};

	var typeProto = pluginProto.Type.prototype;

	// called on startup for each object type
	typeProto.onCreate = function()
	{
	};
	
	var gamepadRuntime = null;
	var gamepadInstance = null;
	var controllers = new Array(16);
	var padStates = new Array(16);
	var padOldStates = new Array(16);
	var osToken = "";
	var browserToken = "";
	
	function getPadState(i)
	{
		var j;
		
		if (!padStates[i])
		{
			padStates[i] = new Array(20);
			
			for (j = 0; j < 20; ++j)
				padStates[i][j] = 0;
		}
		
		return padStates[i];
	};
	
	function getPadOldState(i)
	{
		var j;
		
		if (!padOldStates[i])
		{
			padOldStates[i] = new Array(20);
			
			for (j = 0; j < 20; ++j)
				padOldStates[i][j] = 0;
		}
		
		return padOldStates[i];
	};
	
	function updatePadOldState(i)
	{
		var cur = getPadState(i);
		var old = getPadOldState(i);
		var j;
		
		for (j = 0; j < 20; ++j)
			old[j] = cur[j];
	};
	
	function clearPadState(i)
	{
		padStates[i] = null;
		padOldStates[i] = null;
	};
	
	/////////////////////////////////////
	// Control mappings
	// ctrlmap[osToken][browserToken][controllertoken] = function (index, isAxis, value)
	// Reference control system (which everything else maps to) is Windows/Chrome/XBox 360
	// See http://www.scirra.com/forum/topic49061.html
	var axisOffset = 16;
	var curCtrlMap = null;
	var ctrlmap = {};
	ctrlmap["windows"] = {};
	ctrlmap["windows"]["firefox"] = {};
	
	function doControllerMapping(index, isAxis, buttonmap, axismap)
	{
		if (isAxis)
		{
			if (index >= axismap.length)
				return -1;			// unknown axis
			
			if (cr.is_number(axismap[index]))
				return axismap[index] + axisOffset;
			else
			{
				return axismap[index];	// returning array
			}
		}
		else
		{
			if (index >= buttonmap.length)
				return -1;			// unknown button
				
			return buttonmap[index];
		}
	};
	
	// How to write a mapping:
	// Construct 2 normalises all controller states to the format used by the XBox 360 controller
	// on Chrome/Windows.  Everything is mapped to a single 20-element array in the format:
	// 0: A								10: Left analog button
	// 1: B								11: Right analog button
	// 2: X								12: D-pad up
	// 3: Y								13: D-pad down
	// 4: Left shoulder button			14: D-pad left
	// 5: Right shoulder button			15: D-pad right
	// 6: Left shoulder trigger			16: Left analog X axis
	// 7: Right shoulder trigger		17: Left analog Y axis
	// 8: Back							18: Right analog X axis
	// 9: Start							19: Right analog Y axis
	// Since different controllers return buttons and axes in a different order on different
	// browsers and OSs, all combinations need to be mapped to the above 20-element array,
	// called the 'c2state'.
	// The buttons mapping array translates a raw button input to the c2state, and the axis
	// mapping array translates a raw axis input to the range 0, 1, 2 or 3 (automatically offset
	// by 16 to fit in to the c2state).  However, sometimes an axis is mapped to a pair of buttons,
	// e.g. for the D-pad coming up as an axis.  In this case the mapping is another array
	// of the buttons to map the axis to; the first element for the button when axis negative,
	// and the second element for the button when the axis positive.  e.g. XBox 360 on Firefox/Windows
	// has axis 5 mapped to D-pad left and right; the entry is [14, 15] to map negative (left) to
	// c2state 14 (D-pad left), and positive (right) to c2state 15 (D-pad right).
	// If no mapping exists for an OS/browser/controller configuration, it defaults to assuming it's
	// the same as Windows/Chrome/XBox 360, done by the defaultMap function.
	
	var win_ff_xbox360_buttons = [0, 1, 2, 3, 4, 5, 8, 9, 10, 11];
	var win_ff_xbox360_axes    = [0, 1, [7, 6], 2, 3, [14, 15], [12, 13]];
	ctrlmap["windows"]["firefox"]["xbox360"] = function (index, isAxis)
	{		
		return doControllerMapping(index, isAxis, win_ff_xbox360_buttons, win_ff_xbox360_axes);
	};
	
	var win_ff_lda_buttons = [2, 0, 1, 3, 4, 6, 5, 7, 8, 9];
	var win_ff_lda_axes    = [0, 1, 2, 3, [14, 15], [12, 13]];
	ctrlmap["windows"]["firefox"]["logitechdualaction"] = function (index, isAxis)
	{		
		return doControllerMapping(index, isAxis, win_ff_lda_buttons, win_ff_lda_axes);
	};
	
	function defaultMap(index, isAxis)
	{
		if (isAxis)
		{
			if (index >= 4)
				return -1;		// unknown axis
				
			return index + axisOffset;
		}
		else
		{
			if (index >= 16)
				return -1;		// unknown button
				
			return index;
		}
	};
	
	function getMapper(id_)
	{
		if (!curCtrlMap)
			return defaultMap;
		
		var controllertoken = "";
		var id = id_.toLowerCase();
		
		if (id.indexOf("xbox 360") > -1)
			controllertoken = "xbox360";
		else if (id.indexOf("logitech dual action") > -1)
			controllertoken = "logitechdualaction";
			
		var curmap = curCtrlMap[controllertoken];
		
		return curmap || defaultMap;
	};
	
	/////////////////////////////////////
	
	function onConnected(e)
	{
		controllers[e["gamepad"]["index"]] = e["gamepad"];
		gamepadRuntime.trigger(cr.plugins_.gamepad.prototype.cnds.OnGamepadConnected, gamepadInstance);
	};
	
	function onDisconnected(e)
	{
		gamepadRuntime.trigger(cr.plugins_.gamepad.prototype.cnds.OnGamepadDisconnected, gamepadInstance);
		controllers[e["gamepad"]["index"]] = null;
	};
	
	/////////////////////////////////////
	// Instance class
	pluginProto.Instance = function(type)
	{
		this.type = type;
		this.runtime = type.runtime;
		gamepadRuntime = this.runtime;
		gamepadInstance = this;
	};
	
	var instanceProto = pluginProto.Instance.prototype;

	// called whenever an instance is created
	instanceProto.onCreate = function()
	{
		this.deadzone = this.properties[0];
		this.lastButton = 0;
		
		var userAgent = navigator.userAgent;
		
		// Check PC vs Mac
		osToken = "windows";
		
		if (/mac/i.test(userAgent))
			osToken = "mac";
			
		curCtrlMap = ctrlmap[osToken];
		
		// Check Chrome vs Firefox
		browserToken = "chrome";
		
		if (/firefox/i.test(userAgent))
			browserToken = "firefox";
			
		if (curCtrlMap)
			curCtrlMap = curCtrlMap[browserToken];
		
		window.addEventListener("webkitgamepadconnected", onConnected, false);
		window.addEventListener("webkitgamepaddisconnected", onDisconnected, false);
		window.addEventListener("MozGamepadConnected", onConnected, false);
		window.addEventListener("MozGamepadDisconnected", onDisconnected, false);
		window.addEventListener("gamepadconnected", onConnected, false);
		window.addEventListener("gamepaddisconnected", onDisconnected, false);
		
		this.runtime.tickMe(this);
		
		this.activeControllers = [];
	};
	
	instanceProto.tick = function ()
	{
		this.activeControllers.length = 0;
		
		var gamepads = null;
		
		if (navigator["getGamepads"])
			gamepads = navigator["getGamepads"]();
		else if (navigator["webkitGetGamepads"])
			gamepads = navigator["webkitGetGamepads"]();
		else if (navigator["mozGetGamepads"])
			gamepads = navigator["mozGetGamepads"]();
		else if (navigator["msGetGamepads"])
			gamepads = navigator["msGetGamepads"]();
		else
			gamepads = navigator["gamepads"] || navigator["webkitGamepads"] || navigator["MozGamepads"] || controllers;
		
		if (!gamepads)
			return;
		
		var i, len, j, lenj, mapfunc, index, value;
		for (i = 0, len = gamepads.length; i < len; i++)
		{
			var pad = gamepads[i];
			
			if (!pad)
			{
				clearPadState(i);
				continue;
			}
			
			var state = getPadState(i);
			var oldstate = getPadOldState(i);
			updatePadOldState(i);
				
			mapfunc = getMapper(pad.id);
			
			for (j = 0, lenj = pad["buttons"].length; j < lenj; j++)
			{
				if (typeof pad["buttons"][j]["value"] !== "undefined")
					value = pad["buttons"][j]["value"];
				else
					value = pad["buttons"][j];
				
				index = mapfunc(j, false, value);
				
				if (index >= 0 && index < 20)
				{
					state[index] = value * 100;
					
					// Ensure lastButton is always set to a useful value even if no conditions used
					if (state[index] >= 50 && oldstate[index] < 50)
						this.lastButton = index;
				}
			}
			
			for (j = 0, lenj = pad["axes"].length; j < lenj; j++)
			{
				value = pad["axes"][j];
				index = mapfunc(j, true, value);
				
				if (cr.is_number(index))
				{
					if (index >= 0 && index < 20)
						state[index] = value * 100;
				}
				else
				{
					// returned array: map to buttons
					state[index[0]] = 0;
					state[index[1]] = 0;
					
					if (value <= 0)
						state[index[0]] = Math.abs(value * 100);
					else
						state[index[1]] = Math.abs(value * 100);
				}
			}
				
			this.activeControllers.push(pad);
		}
		
		// clear remaining unused pads
		for ( ; i < 20; ++i)
			clearPadState(i);
	};
	
	instanceProto.saveToJSON = function ()
	{
		return { "lastButton": this.lastButton };
	};
	
	instanceProto.loadFromJSON = function (o)
	{
		this.lastButton = o["lastButton"];
	};
	

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.SupportsGamepad = function ()
	{
		return isSupported;
	};
	
	Cnds.prototype.OnGamepadConnected = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnGamepadDisconnected = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsButtonDown = function (gamepad, button)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		
		if (!state)
			return false;
		
		var ret = state[button] >= 50;
		
		if (ret)
			this.lastButton = button;
		
		return ret;
	};
	
	Cnds.prototype.OnButtonDown = function (gamepad, button)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		var oldstate = getPadOldState(gamepad);
		
		if (!state || !oldstate)
			return false;
			
		// Is down this tick and was not down last tick
		var ret = state[button] >= 50 && oldstate[button] < 50;
		
		if (ret)
			this.lastButton = button;
		
		return ret;
	};
	
	Cnds.prototype.OnButtonUp = function (gamepad, button)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		var oldstate = getPadOldState(gamepad);
		
		if (!state || !oldstate)
			return false;
			
		// Is not down this tick and was down last tick
		var ret = state[button] < 50 && oldstate[button] >= 50;
		
		if (ret)
			this.lastButton = button;
		
		return ret;
	};
	
	Cnds.prototype.HasGamepads = function ()
	{
		return this.activeControllers.length > 0;
	};
	
	Cnds.prototype.CompareAxis = function (gamepad, axis, comparison, value)
	{
		gamepad = Math.floor(gamepad);
		axis = Math.floor(axis);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		
		if (!state)
			return;
			
		var axisvalue = state[axis + axisOffset];
		
		// To implement a radial deadzone, we need to take in to account the
		// other axis value. Even values are X axis and odd values are Y axis,
		// so use this to get the other axis.
		var othervalue = 0;
		
		if (axis % 2 === 0)										// is X axis
			othervalue = state[axis + axisOffset + 1];	// get next axis (Y)
		else
			othervalue = state[axis + axisOffset - 1];	// get previous axis (X)
		
		// Is within the deadzone radius: return 0
		if (Math.sqrt(axisvalue * axisvalue + othervalue * othervalue) <= this.deadzone)
			axisvalue = 0;
		
		return cr.do_cmp(axisvalue, comparison, value);
	};
	
	Cnds.prototype.OnAnyButtonDown = function (gamepad)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		var oldstate = getPadOldState(gamepad);
		
		if (!state || !oldstate)
			return false;
			
		// Is any button down this tick and was not down last tick
		var i, len;
		for (i = 0, len = state.length; i < len; i++)
		{
			if (state[i] >= 50 && oldstate[i] < 50)
			{
				this.lastButton = i;
				return true;
			}
		}
		
		return false;
	};
	
	Cnds.prototype.OnAnyButtonUp = function (gamepad)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		var oldstate = getPadOldState(gamepad);
		
		if (!state || !oldstate)
			return false;
			
		// Is any button up this tick and was down last tick
		var i, len;
		for (i = 0, len = state.length; i < len; i++)
		{
			if (state[i] < 50 && oldstate[i] >= 50)
			{
				this.lastButton = i;
				return true;
			}
		}
		
		return false;
	};
	
	Cnds.prototype.IsButtonIndexDown = function (gamepad, button)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		
		if (!state)
			return false;
			
		button = Math.floor(button);
		
		if (button < 0 || button >= state.length)
			return false;
		
		var ret = state[button] >= 50;
		
		if (ret)
			this.lastButton = button;
		
		return ret;
	};
	
	Cnds.prototype.OnButtonIndexDown = function (gamepad, button)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		var oldstate = getPadOldState(gamepad);
		
		if (!state || !oldstate)
			return false;
			
		button = Math.floor(button);
		
		if (button < 0 || button >= state.length)
			return false;
			
		// Is down this tick and was not down last tick
		var ret = state[button] >= 50 && oldstate[button] < 50;
		
		if (ret)
			this.lastButton = button;
		
		return ret;
	};
	
	Cnds.prototype.OnButtonIndexUp = function (gamepad, button)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
			return false;
			
		var state = getPadState(gamepad);
		var oldstate = getPadOldState(gamepad);
		
		if (!state || !oldstate)
			return false;
		
		button = Math.floor(button);
		
		if (button < 0 || button >= state.length)
			return false;
			
		// Is not down this tick and was down last tick
		var ret = state[button] < 50 && oldstate[button] >= 50;
		
		if (ret)
			this.lastButton = button;
		
		return ret;
	};
	
	pluginProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	Acts.prototype.Vibrate = function (gamepadIndex, duration, weakMag, strongMag)
	{
		gamepadIndex = Math.floor(gamepadIndex);
		
		if (gamepadIndex < 0 || gamepadIndex >= this.activeControllers.length)
			return;
		
		var gamepad = this.activeControllers[gamepadIndex];
		var vibrationActuator = gamepad["vibrationActuator"];
		if (!vibrationActuator || !vibrationActuator["playEffect"])
			return;			// not supported
		
		vibrationActuator["playEffect"]("dual-rumble", {
			"duration": duration,
			"startDelay": 0,
			"weakMagnitude": cr.clamp(weakMag / 100, 0, 1),
			"strongMagnitude": cr.clamp(strongMag / 100, 0, 1)
		})
		.catch(function (err)
		{
			console.warn("[Gamepad] Failed to vibrate gamepad: ", err);
		});
	};
	
	Acts.prototype.ResetVibrate = function (gamepadIndex)
	{
		gamepadIndex = Math.floor(gamepadIndex);
		
		if (gamepadIndex < 0 || gamepadIndex >= this.activeControllers.length)
			return;
		
		var gamepad = this.activeControllers[gamepadIndex];
		var vibrationActuator = gamepad["vibrationActuator"];
		if (vibrationActuator && vibrationActuator["reset"])
			vibrationActuator["reset"]();
	};
	
	pluginProto.acts = new Acts();
	
	//////////////////////////////////////
	// Expressions
	function Exps() {};

	Exps.prototype.GamepadCount = function (ret)
	{
		ret.set_int(this.activeControllers.length);
	};
	
	Exps.prototype.GamepadID = function (ret, index)
	{
		if (index < 0 || index >= this.activeControllers.length)
		{
			ret.set_string("");
			return;
		}
		
		ret.set_string(this.activeControllers[index].id);
	};
	
	Exps.prototype.GamepadAxes = function (ret, index)
	{
		if (index < 0 || index >= this.activeControllers.length)
		{
			ret.set_string("");
			return;
		}
		
		var axes = this.activeControllers[index]["axes"];
		var str = "";
		
		var i, len;
		for (i = 0, len = axes.length; i < len; i++)
		{
			str += "Axis " + i + ": " + Math.round(axes[i] * 100) + "\n";
		}
		
		ret.set_string(str);
	};
	
	Exps.prototype.GamepadButtons = function (ret, index)
	{
		if (index < 0 || index >= this.activeControllers.length)
		{
			ret.set_string("");
			return;
		}
		
		var buttons = this.activeControllers[index]["buttons"];
		var str = "";
		
		var i, len, value;
		for (i = 0, len = buttons.length; i < len; i++)
		{
			if (typeof buttons[i]["value"] !== "undefined")
				value = buttons[i]["value"];
			else
				value = buttons[i];
				
			str += "Button " + i + ": " + Math.round(value * 100) + "\n";
		}
		
		ret.set_string(str);
	};
	
	Exps.prototype.RawButton = function (ret, gamepad, index)
	{
		gamepad = Math.floor(gamepad);
		index = Math.floor(index);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
		{
			ret.set_float(0);
			return;
		}
		
		var state = this.activeControllers[gamepad]["buttons"];
		
		if (!state || index < 0 || index >= state.length)
		{
			ret.set_float(0);
			return;
		}
		
		if (typeof state[index]["value"] !== "undefined")
			ret.set_float(state[index]["value"]);
		else
			ret.set_float(state[index]);
	};
	
	Exps.prototype.RawAxis = function (ret, gamepad, index)
	{
		gamepad = Math.floor(gamepad);
		index = Math.floor(index);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
		{
			ret.set_float(0);
			return;
		}
		
		var state = this.activeControllers[gamepad]["axes"];
		
		if (!state || index < 0 || index >= state.length)
		{
			ret.set_float(0);
			return;
		}
		
		ret.set_float(state[index]);
	};
	
	Exps.prototype.RawButtonCount = function (ret, gamepad)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
		{
			ret.set_int(0);
			return;
		}
		
		ret.set_int(this.activeControllers[gamepad]["buttons"].length);
	};
	
	Exps.prototype.RawAxisCount = function (ret, gamepad)
	{
		gamepad = Math.floor(gamepad);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
		{
			ret.set_int(0);
			return;
		}
		
		ret.set_int(this.activeControllers[gamepad]["axes"].length);
	};
	
	Exps.prototype.Button = function (ret, gamepad, index)
	{
		gamepad = Math.floor(gamepad);
		index = Math.floor(index);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
		{
			ret.set_float(0);
			return;
		}
		
		var state = getPadState(gamepad);
		
		if (!state || index < 0 || index >= axisOffset)
		{
			ret.set_float(0);
			return;
		}
		
		ret.set_float(state[index]);
	};
	
	Exps.prototype.Axis = function (ret, gamepad, index)
	{
		gamepad = Math.floor(gamepad);
		index = Math.floor(index);
		
		if (gamepad < 0 || gamepad >= this.activeControllers.length)
		{
			ret.set_float(0);
			return;
		}
		
		var state = getPadState(gamepad);
		
		if (!state || index < 0 || index >= 4)
		{
			ret.set_float(0);
			return;
		}
		
		var value = state[index + axisOffset];
		
		// To implement a radial deadzone, we need to take in to account the
		// other axis value. Even values are X axis and odd values are Y axis,
		// so use this to get the other axis.
		var othervalue = 0;
		
		if (index % 2 === 0)								// is X axis
			othervalue = state[index + axisOffset + 1];		// get next axis (Y)
		else
			othervalue = state[index + axisOffset - 1];		// get previous axis (X)
		
		// Is within the deadzone radius: return 0
		if (Math.sqrt(value * value + othervalue * othervalue) <= this.deadzone)
			value = 0;
			
		ret.set_float(value);
	};
	
	Exps.prototype.LastButton = function (ret)
	{
		ret.set_int(this.lastButton);
	};
	
	pluginProto.exps = new Exps();

}());

// Solid
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var behaviorProto = cr.behaviors.solid.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.inst.extra["solidEnabled"] = this.properties[0];
	};

	behinstProto.tick = function ()
	{
	};
	
	
	function Cnds() {};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.inst.extra["solidEnabled"];
	};
	
	behaviorProto.cnds = new Cnds();
	
	function Acts() {};
	
	Acts.prototype.SetEnabled = function (e)
	{
		this.inst.extra["solidEnabled"] = !!e;
	};
	
	behaviorProto.acts = new Acts();
	
}());

// Anchor
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var behaviorProto = cr.behaviors.Anchor.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.anch_left = this.properties[0];		// 0 = left, 1 = right, 2 = none
		this.anch_top = this.properties[1];			// 0 = top, 1 = bottom, 2 = none
		this.anch_right = this.properties[2];		// 0 = none, 1 = right
		this.anch_bottom = this.properties[3];		// 0 = none, 1 = bottom
		
		this.inst.update_bbox();
		this.xleft = this.inst.bbox.left;
		this.ytop = this.inst.bbox.top;
		this.xright = this.runtime.original_width - this.inst.bbox.left;
		this.ybottom = this.runtime.original_height - this.inst.bbox.top;
		this.rdiff = this.runtime.original_width - this.inst.bbox.right;
		this.bdiff = this.runtime.original_height - this.inst.bbox.bottom;
		
		this.enabled = this.properties[4];
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"xleft": this.xleft,
			"ytop": this.ytop,
			"xright": this.xright,
			"ybottom": this.ybottom,
			"rdiff": this.rdiff,
			"bdiff": this.bdiff,
			"enabled": this.enabled
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.xleft = o["xleft"];
		this.ytop = o["ytop"];
		this.xright = o["xright"];
		this.ybottom = o["ybottom"];
		this.rdiff = o["rdiff"];
		this.bdiff = o["bdiff"];
		this.enabled = o["enabled"];
	};

	behinstProto.tick = function ()
	{
		if (!this.enabled)
			return;
		
		var n;
		var layer = this.inst.layer;
		var inst = this.inst;
		var bbox = this.inst.bbox;
		
		// Anchor left to window left
		if (this.anch_left === 0)
		{
			inst.update_bbox();
			n = (layer.viewLeft + this.xleft) - bbox.left;
			
			if (n !== 0)
			{
				inst.x += n;
				inst.set_bbox_changed();
			}
		}
		
		// Anchor left to window right
		else if (this.anch_left === 1)
		{
			inst.update_bbox();
			n = (layer.viewRight - this.xright) - bbox.left;

			if (n !== 0)
			{
				inst.x += n;
				inst.set_bbox_changed();
			}
		}
		
		// Anchor top to window top
		if (this.anch_top === 0)
		{
			inst.update_bbox();
			n = (layer.viewTop + this.ytop) - bbox.top;

			if (n !== 0)
			{
				inst.y += n;
				inst.set_bbox_changed();
			}
		}
		
		// Anchor top to window bottom
		else if (this.anch_top === 1)
		{
			inst.update_bbox();
			n = (layer.viewBottom - this.ybottom) - bbox.top;

			if (n !== 0)
			{
				inst.y += n;
				inst.set_bbox_changed();
			}
		}
		
		// Anchor right to window right
		if (this.anch_right === 1)
		{
			inst.update_bbox();
			n = (layer.viewRight - this.rdiff) - bbox.right;
			
			if (n !== 0)
			{
				inst.width += n;
				
				if (inst.width < 0)
					inst.width = 0;
				
				inst.set_bbox_changed();
			}
		}
		
		// Anchor bottom to window bottom
		if (this.anch_bottom === 1)
		{
			inst.update_bbox();
			n = (layer.viewBottom - this.bdiff) - bbox.bottom;
			
			if (n !== 0)
			{
				inst.height += n;
				
				if (inst.height < 0)
					inst.height = 0;
				
				inst.set_bbox_changed();
			}
		}
	};

	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.enabled;
	};
	
	behaviorProto.cnds = new Cnds();

	//////////////////////////////////////
	// Actions
	function Acts() {};
	
	Acts.prototype.SetEnabled = function (e)
	{
		// Is enabled and disabling
		if (this.enabled && e === 0)
			this.enabled = false;
		// Is disabled and enabling
		else if (!this.enabled && e !== 0)
		{
			this.inst.update_bbox();
			this.xleft = this.inst.bbox.left;
			this.ytop = this.inst.bbox.top;
			this.xright = this.runtime.original_width - this.inst.bbox.left;
			this.ybottom = this.runtime.original_height - this.inst.bbox.top;
			this.rdiff = this.runtime.original_width - this.inst.bbox.right;
			this.bdiff = this.runtime.original_height - this.inst.bbox.bottom;
			this.enabled = true;
		}
	};
	
	behaviorProto.acts = new Acts();

	//////////////////////////////////////
	// Expressions
	function Exps() {};
	behaviorProto.exps = new Exps();
	
}());

// Pin
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var behaviorProto = cr.behaviors.Pin.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.pinObject = null;
		this.pinObjectUid = -1;		// for loading
		this.pinAngle = 0;
		this.pinDist = 0;
		this.myStartAngle = 0;
		this.theirStartAngle = 0;
		this.lastKnownAngle = 0;
		this.mode = 0;				// 0 = position & angle; 1 = position; 2 = angle; 3 = rope; 4 = bar
		
		var self = this;
		
		// Need to know if pinned object gets destroyed
		if (!this.recycled)
		{
			this.myDestroyCallback = (function(inst) {
													self.onInstanceDestroyed(inst);
												});
		}
										
		this.runtime.addDestroyCallback(this.myDestroyCallback);
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"uid": this.pinObject ? this.pinObject.uid : -1,
			"pa": this.pinAngle,
			"pd": this.pinDist,
			"msa": this.myStartAngle,
			"tsa": this.theirStartAngle,
			"lka": this.lastKnownAngle,
			"m": this.mode
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.pinObjectUid = o["uid"];		// wait until afterLoad to look up		
		this.pinAngle = o["pa"];
		this.pinDist = o["pd"];
		this.myStartAngle = o["msa"];
		this.theirStartAngle = o["tsa"];
		this.lastKnownAngle = o["lka"];
		this.mode = o["m"];
	};
	
	behinstProto.afterLoad = function ()
	{
		// Look up the pinned object UID now getObjectByUID is available
		if (this.pinObjectUid === -1)
			this.pinObject = null;
		else
		{
			this.pinObject = this.runtime.getObjectByUID(this.pinObjectUid);
;
		}
		
		this.pinObjectUid = -1;
	};
	
	behinstProto.onInstanceDestroyed = function (inst)
	{
		// Pinned object being destroyed
		if (this.pinObject == inst)
			this.pinObject = null;
	};
	
	behinstProto.onDestroy = function()
	{
		this.pinObject = null;
		this.runtime.removeDestroyCallback(this.myDestroyCallback);
	};
	
	behinstProto.tick = function ()
	{
		// do work in tick2 instead, after events to get latest object position
	};

	behinstProto.tick2 = function ()
	{
		if (!this.pinObject)
			return;
			
		// Instance angle has changed by events/something else
		if (this.lastKnownAngle !== this.inst.angle)
			this.myStartAngle = cr.clamp_angle(this.myStartAngle + (this.inst.angle - this.lastKnownAngle));
			
		var newx = this.inst.x;
		var newy = this.inst.y;
		
		if (this.mode === 3 || this.mode === 4)		// rope mode or bar mode
		{
			var dist = cr.distanceTo(this.inst.x, this.inst.y, this.pinObject.x, this.pinObject.y);
			
			if ((dist > this.pinDist) || (this.mode === 4 && dist < this.pinDist))
			{
				var a = cr.angleTo(this.pinObject.x, this.pinObject.y, this.inst.x, this.inst.y);
				newx = this.pinObject.x + Math.cos(a) * this.pinDist;
				newy = this.pinObject.y + Math.sin(a) * this.pinDist;
			}
		}
		else
		{
			newx = this.pinObject.x + Math.cos(this.pinObject.angle + this.pinAngle) * this.pinDist;
			newy = this.pinObject.y + Math.sin(this.pinObject.angle + this.pinAngle) * this.pinDist;
		}
		
		var newangle = cr.clamp_angle(this.myStartAngle + (this.pinObject.angle - this.theirStartAngle));
		this.lastKnownAngle = newangle;
		
		if ((this.mode === 0 || this.mode === 1 || this.mode === 3 || this.mode === 4)
			&& (this.inst.x !== newx || this.inst.y !== newy))
		{
			this.inst.x = newx;
			this.inst.y = newy;
			this.inst.set_bbox_changed();
		}
		
		if ((this.mode === 0 || this.mode === 2) && (this.inst.angle !== newangle))
		{
			this.inst.angle = newangle;
			this.inst.set_bbox_changed();
		}
	};
	

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

	Cnds.prototype.IsPinned = function ()
	{
		return !!this.pinObject;
	};
	
	behaviorProto.cnds = new Cnds();

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

	Acts.prototype.Pin = function (obj, mode_)
	{
		if (!obj)
			return;
			
		var otherinst = obj.getFirstPicked(this.inst);
		
		if (!otherinst)
			return;
			
		this.pinObject = otherinst;
		this.pinAngle = cr.angleTo(otherinst.x, otherinst.y, this.inst.x, this.inst.y) - otherinst.angle;
		this.pinDist = cr.distanceTo(otherinst.x, otherinst.y, this.inst.x, this.inst.y);
		this.myStartAngle = this.inst.angle;
		this.lastKnownAngle = this.inst.angle;
		this.theirStartAngle = otherinst.angle;
		this.mode = mode_;
	};
	
	Acts.prototype.Unpin = function ()
	{
		this.pinObject = null;
	};
	
	behaviorProto.acts = new Acts();

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

	Exps.prototype.PinnedUID = function (ret)
	{
		ret.set_int(this.pinObject ? this.pinObject.uid : -1);
	};
	
	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 >= 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 >= 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 < 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 && !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 < 0)
			t = 0;
		
		this.fadeInTime = t;
	};
	
	Acts.prototype.SetWaitTime = function (t)
	{
		if (t < 0)
			t = 0;
		
		this.waitTime = t;
	};
	
	Acts.prototype.SetFadeOutTime = function (t)
	{
		if (t < 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();

}());

// Lite Tween
// common.js start

var easeOutBounceArray = [];
var easeInElasticArray = [];
var easeOutElasticArray = [];
var easeInOutElasticArray = [];
var easeInCircle = [];
var easeOutCircle = [];
var easeInOutCircle = [];
var easeInBack = [];
var easeOutBack = [];
var easeInOutBack = [];
var litetween_precision = 10000;
var updateLimit = 0; //0.0165;

function easeOutBouncefunc(t) {
  var b=0.0;
  var c=1.0;
  var d=1.0;
	if ((t/=d) < (1/2.75)) {
		result = c*(7.5625*t*t) + b;
	} else if (t < (2/2.75)) {
		result = c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
	} else if (t < (2.5/2.75)) {
		result = c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
	} else {
		result = c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
	}		
  return result;
}

function integerize(t, d)
{
  return Math.round(t/d*litetween_precision);
}

function easeFunc(easing, t, b, c, d, flip, param)
{
  var ret_ease = 0;

  switch (easing) {
	case 0:		// linear
		ret_ease = c*t/d + b;
    break;
	case 1:		// easeInQuad
		ret_ease = c*(t/=d)*t + b;
    break;
	case 2:		// easeOutQuad
		ret_ease = -c *(t/=d)*(t-2) + b;
    break;
	case 3:		// easeInOutQuad
		if ((t/=d/2) < 1) 
      ret_ease = c/2*t*t + b
    else
		  ret_ease = -c/2 * ((--t)*(t-2) - 1) + b;
    break;
	case 4:		// easeInCubic
		ret_ease = c*(t/=d)*t*t + b;
    break;
	case 5:		// easeOutCubic
		ret_ease = c*((t=t/d-1)*t*t + 1) + b;
    break;
	case 6:		// easeInOutCubic
		if ((t/=d/2) < 1) 
			ret_ease = c/2*t*t*t + b
    else
		  ret_ease = c/2*((t-=2)*t*t + 2) + b;
    break;
	case 7:		// easeInQuart
		ret_ease = c*(t/=d)*t*t*t + b;
    break;
	case 8:		// easeOutQuart
		ret_ease = -c * ((t=t/d-1)*t*t*t - 1) + b;
    break;
	case 9:		// easeInOutQuart
		if ((t/=d/2) < 1) 
      ret_ease = c/2*t*t*t*t + b
    else
		  ret_ease = -c/2 * ((t-=2)*t*t*t - 2) + b;
    break;
	case 10:		// easeInQuint
		ret_ease = c*(t/=d)*t*t*t*t + b;
    break;
	case 11:		// easeOutQuint
		ret_ease = c*((t=t/d-1)*t*t*t*t + 1) + b;
    break;
	case 12:		// easeInOutQuint
		if ((t/=d/2) < 1) 
      ret_ease = c/2*t*t*t*t*t + b
    else
		  ret_ease = c/2*((t-=2)*t*t*t*t + 2) + b;
    break;
	case 13:		// easeInCircle
    if (param.optimized) {
		  ret_ease = easeInCircle[integerize(t,d)];
    } else {
      ret_ease = -(Math.sqrt(1-t*t) - 1);
    }
    break;
	case 14:		// easeOutCircle
    if (param.optimized) {
  		ret_ease = easeOutCircle[integerize(t,d)];
    } else {
      ret_ease = Math.sqrt(1 - ((t-1)*(t-1)));
    }
    break;
	case 15:		// easeInOutCircle
    if (param.optimized) {
  		ret_ease = easeInOutCircle[integerize(t,d)];
    } else {
  		if ((t/=d/2) < 1) ret_ease = -c/2 * (Math.sqrt(1 - t*t) - 1) + b
      else ret_ease = c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
    } 
    break;
	case 16:		// easeInBack
    if (param.optimized) {
		  ret_ease = easeInBack[integerize(t,d)];
    } else {
  		var s = param.s;
	  	ret_ease = c*(t/=d)*t*((s+1)*t - s) + b;
    }
    break;
	case 17:		// easeOutBack
    if (param.optimized) {
		  ret_ease = easeOutBack[integerize(t,d)];
    } else {
   		var s = param.s;
	  	ret_ease = c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
    }
    break;
	case 18:		// easeInOutBack
    if (param.optimized) {
		  ret_ease = easeInOutBack[integerize(t,d)];
    } else {
      var s = param.s
  		if ((t/=d/2) < 1) 
        ret_ease = c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b
      else
  		  ret_ease = c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
    }
    break;
	case 19:	//easeInElastic
    if (param.optimized) {
  		ret_ease = easeInElasticArray[integerize(t, d)];
    } else {
      var a = param.a;
      var p = param.p;
      var s = 0;
      if (t==0) ret_ease = b; if ((t/=d)==1) ret_ease = b+c; 
      if (p==0) p=d*.3;	if (a==0 || a < Math.abs(c)) { a=c; s=p/4; }
      else var s = p/(2*Math.PI) * Math.asin (c/a);
  		ret_ease = -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
    }
    break;
	case 20:	//easeOutElastic
    if (param.optimized) {
      ret_ease = easeOutElasticArray[integerize(t,d)];
    } else {
      var a = param.a;
      var p = param.p;
      var s = 0;
  		if (t==0) ret_ease= b;  if ((t/=d)==1) ret_ease= b+c;  if (p == 0) p=d*.3;
  		if (a==0 || a < Math.abs(c)) { a=c; var s=p/4; }
  		else var s = p/(2*Math.PI) * Math.asin (c/a);
  		ret_ease= (a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b);
    }
    break;
	case 21:	//easeInOutElastic
    if (param.optimized) {
      ret_ease = easeInOutElasticArray[integerize(t,d)];
    } else {
      var a = param.a;
      var p = param.p;
      var s = 0;
  		if (t==0) ret_ease = b; 
  		if ((t/=d/2)==2) ret_ease = b+c;  
  		if (p==0) p=d*(.3*1.5);
  		if (a==0 || a < Math.abs(c)) { a=c; var s=p/4; }
  		else var s = p/(2*Math.PI) * Math.asin (c/a);
  		if (t < 1) 
        ret_ease = -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b
      else
  		  ret_ease = a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
    }
    break;
	case 22:	//easeInBounce
    if (param.optimized) {
  		ret_ease = c - easeOutBounceArray[integerize(d-t, d)] + b;
    } else {
  		ret_ease = c - easeOutBouncefunc(d-t/d) + b;
    }
    break;
	case 23:	//easeOutBounce
    if (param.optimized) {
  		ret_ease = easeOutBounceArray[integerize(t, d)];
    } else {
  		ret_ease = easeOutBouncefunc(t/d);
    }
    break;
	case 24:	//easeInOutBounce
    if (param.optimized) {
  		if (t < d/2) 
        ret_ease = (c - easeOutBounceArray[integerize(d-(t*2), d)] + b) * 0.5 +b;
  		else 
        ret_ease = easeOutBounceArray[integerize(t*2-d, d)] * .5 + c*.5 + b;
    } else {
  		if (t < d/2) 
        ret_ease = (c - easeOutBouncefunc(d-(t*2)) + b) * 0.5 +b;
  		else 
        ret_ease = easeOutBouncefunc((t*2-d)/d) * .5 + c *.5 + b;
    }
    break;
	case 25:	//easeInSmoothstep
		var mt = (t/d) / 2;
		ret_ease = (2*(mt * mt * (3 - 2*mt)));
    break;
	case 26:	//easeOutSmoothstep
		var mt = ((t/d) + 1) / 2;
		ret_ease = ((2*(mt * mt * (3 - 2*mt))) - 1);
    break;
	case 27:	//easeInOutSmoothstep
		var mt = (t / d);
		ret_ease = (mt * mt * (3 - 2*mt));
    break;
	};
	
  if (flip)
    return (c - b) - ret_ease
  else
    return ret_ease;
};

(function preCalculateArray() {
  var d = 1.0;
  var b = 0.0;
  var c = 1.0;
  var result = 0.0;
  var a = 0.0;
  var p = 0.0;
  var t = 0.0;
  var s = 0.0;

  for (var ti = 0; ti <= litetween_precision; ti++) {
    t = ti/litetween_precision; 
  	if ((t/=d) < (1/2.75)) {
  		result = c*(7.5625*t*t) + b;
  	} else if (t < (2/2.75)) {
  		result = c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
  	} else if (t < (2.5/2.75)) {
  		result = c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
  	} else {
  		result = c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
  	}		
    easeOutBounceArray[ti] = result;

    t = ti/litetween_precision; a = 0; p = 0;
    if (t==0) result = b; if ((t/=d)==1) result = b+c; 
    if (p==0) p=d*.3;	if (a==0 || a < Math.abs(c)) { a=c; var s=p/4; }
    else var s = p/(2*Math.PI) * Math.asin (c/a);
		result = -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
    easeInElasticArray[ti] = result;

    t = ti/litetween_precision; a = 0; p = 0;
		if (t==0) result= b;  if ((t/=d)==1) result= b+c;  if (p == 0) p=d*.3;
		if (a==0 || a < Math.abs(c)) { a=c; var s=p/4; }
		else var s = p/(2*Math.PI) * Math.asin (c/a);
		result= (a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b);
    easeOutElasticArray[ti] = result;

    t = ti/litetween_precision; a = 0; p = 0;
		if (t==0) result = b; 
		if ((t/=d/2)==2) result = b+c;  
		if (p==0) p=d*(.3*1.5);
		if (a==0 || a < Math.abs(c)) { a=c; var s=p/4; }
		else var s = p/(2*Math.PI) * Math.asin (c/a);
		if (t < 1) 
      result = -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b
    else
		  result = a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
    easeInOutElasticArray[ti] = result;
    
    t = ti/litetween_precision; easeInCircle[ti] = -(Math.sqrt(1-t*t) - 1);
    t = ti/litetween_precision; easeOutCircle[ti] = Math.sqrt(1 - ((t-1)*(t-1)));

    t = ti/litetween_precision; 
		if ((t/=d/2) < 1) result = -c/2 * (Math.sqrt(1 - t*t) - 1) + b
    else result = c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
    easeInOutCircle[ti] = result;

    t = ti/litetween_precision; s = 0;
		if (s==0) s = 1.70158;
		result = c*(t/=d)*t*((s+1)*t - s) + b;
    easeInBack[ti] = result;

    t = ti/litetween_precision; s = 0;
		if (s==0) s = 1.70158;
		result = c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
    easeOutBack[ti] = result;

    t = ti/litetween_precision; s = 0; if (s==0) s = 1.70158;
		if ((t/=d/2) < 1) 
      result = c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b
    else
		  result = c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
    easeInOutBack[ti] = result;
	}	
}());

var TweenObject = function() 
{
	var constructor = function (tname, tweened, easefunc, initial, target, duration, enforce) 
	{
		//0=Position|1=Size|2=Width|3=Height|4=Angle|5=Opacity|6=Value only
    //state -> 0=paused | 1=playing | 2=reversing | 3=seek only
    this.name = tname;
    this.value = 0; 
    this.setInitial(initial);
    this.setTarget(target);
    this.easefunc = easefunc;
    this.tweened = tweened;
    this.duration = duration;
    this.progress = 0;
    this.state = 0;
    this.onStart = false;
    this.onEnd = false;
    this.onReverseStart = false;
    this.onReverseEnd = false;
    this.lastKnownValue = 0; 
    this.lastKnownValue2 = 0;
    this.enforce = enforce; 
    this.pingpong = 1.0;
    this.flipEase = false;
    this.easingparam = [];
    this.lastState = 1;
    for (var i=0; i<28; i++) {
      this.easingparam[i] = {}; 
      this.easingparam[i].a = 0.0;
      this.easingparam[i].p = 0.0;
      this.easingparam[i].t = 0.0;
      this.easingparam[i].s = 0.0;
      this.easingparam[i].optimized = true;
    }
	}
	
	return constructor;
}();

(function () {
	TweenObject.prototype = {
	};

  TweenObject.prototype.flipTarget = function ()
  {
    var x1 = this.initialparam1;
    var x2 = this.initialparam2;
    this.initialparam1 = this.targetparam1; 
    this.initialparam2 = this.targetparam2;
    this.targetparam1 = x1;
    this.targetparam2 = x2;
    this.lastKnownValue = 0;
    this.lastKnownValue2 = 0;
  }

  TweenObject.prototype.setInitial = function (initial)
  {
    this.initialparam1 = parseFloat(initial.split(",")[0]); 
    this.initialparam2 = parseFloat(initial.split(",")[1]);
		this.lastKnownValue = 0;
		this.lastKnownValue2 = 0;
  } 
  
  TweenObject.prototype.setTarget = function (target)
  {
    this.targetparam1 = parseFloat(target.split(",")[0]);
    this.targetparam2 = parseFloat(target.split(",")[1]);
    if (isNaN(this.targetparam2)) this.targetparam2 = this.targetparam1;
  }

  TweenObject.prototype.OnTick = function(dt) 
  {
    //stopped
    if (this.state === 0) return -1.0;
     
    //if starting
    if (this.state === 1) 
      this.progress += dt;
    
    //if reversing
    if (this.state === 2) 
      this.progress -= dt;

    //seek
    if (this.state === 3) {
      this.state = 0;
    } 

    //ping pong
    if ((this.state === 4) || (this.state === 6)) {
      this.progress += dt * this.pingpong;
    } 

    //loop
    if (this.state === 5) {
      this.progress += dt * this.pingpong;
    } 

    if (this.progress < 0) {
      //it has reached negative timeline due to reversing, pausing
      this.progress = 0;
      //if it's ping pong, don't stop
      if (this.state === 4) {
        this.pingpong = 1;
      } else if (this.state === 6) {
        this.pingpong = 1;
        this.flipEase = false;
      } else {
        this.state = 0;
      }
      this.onReverseEnd = true;
      return 0.0;
    } else if (this.progress > this.duration) {
      //it has reached more than duration, pausing
      this.progress = this.duration;
      if (this.state === 4) {
        this.pingpong = -1;
      } else if (this.state === 6) {
        this.pingpong = -1;
        this.flipEase = true;
      } else if (this.state === 5) {
        this.progress = 0.0;
      } else {
        this.state = 0;
      }
      this.onEnd = true;
      return 1.0;
    } else {
      if (this.flipEase) {
        var factor = easeFunc(this.easefunc, this.duration - this.progress, 0, 1, this.duration, this.flipEase, this.easingparam[this.easefunc]);
      } else {
        var factor = easeFunc(this.easefunc, this.progress, 0, 1, this.duration, this.flipEase, this.easingparam[this.easefunc]);
      }
      return factor;
    }
  };  
}());

// common.js stop
// runtime.js start

// ECMAScript 5 strict mode
;
;

function trim (str) {
    return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}

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

(function ()
{
	var behaviorProto = cr.behaviors.lunarray_LiteTween.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;		// progress
	};
	
	var behinstProto = behaviorProto.Instance.prototype;
	
	behinstProto.onCreate = function()
	{
		// Load properties
    this.playmode = this.properties[0];
    this.active = (this.playmode == 1) || (this.playmode == 2) || (this.playmode == 3) || (this.playmode == 4);
		this.tweened = this.properties[1]; // 0=Position|1=Size|2=Width|3=Height|4=Angle|5=Opacity|6=Value only|7=Horizontal|8=Vertical|9=Scale
		this.easing = this.properties[2];
		this.target = this.properties[3];
		this.targetmode = this.properties[4];
    this.useCurrent = false;
    if (this.targetmode === 1) this.target = "relative("+this.target+")";
		this.duration = this.properties[5];
		this.enforce = (this.properties[6] === 1);
    this.value = 0;
    
    this.tween_list = {};
    this.addToTweenList("default", this.tweened, this.easing, "current", this.target, this.duration, this.enforce);
    
    if (this.properties[0] === 1) this.startTween(0)
    if (this.properties[0] === 2) this.startTween(2)
    if (this.properties[0] === 3) this.startTween(3)
    if (this.properties[0] === 4) this.startTween(4)
	};

	behinstProto.parseCurrent = function(tweened, parseText)
  {
    if (parseText === undefined) parseText = "current";
    var parsed = trim(parseText);
    parseText = trim(parseText);
    var value = this.value;
    
    if (parseText === "current") {
      switch (tweened) {
        case 0: parsed = this.inst.x + "," + this.inst.y; break;
        case 1: parsed = this.inst.width + "," + this.inst.height; break;
        case 2: parsed = this.inst.width + "," + this.inst.height; break;
        case 3: parsed = this.inst.width + "," + this.inst.height; break;
        case 4: parsed = cr.to_degrees(this.inst.angle) + "," + cr.to_degrees(this.inst.angle); break;
        case 5: parsed = (this.inst.opacity*100) + "," + (this.inst.opacity*100); break;
        case 6: parsed = value + "," + value; break;
        case 7: parsed = this.inst.x + "," + this.inst.y; break;
        case 8: parsed = this.inst.x + "," + this.inst.y; break;
        case 9: 
          if (this.inst.curFrame !== undefined)
            parsed = (this.inst.width/this.inst.curFrame.width) + "," +(this.inst.height/this.inst.curFrame.height)
          else 
            parsed = "1,1"; 
          break;
        default:  break;
      }
    }
    if (parseText.substring(0,8) === "relative") {
      var param1 = parseText.match(/\((.*?)\)/);
      if (param1) {
        var relativex = parseFloat(param1[1].split(",")[0]);
        var relativey = parseFloat(param1[1].split(",")[1]);
      }
      if (isNaN(relativex)) relativex = 0; 
      if (isNaN(relativey)) relativey = 0; 
      switch (tweened) {
        case 0: parsed = (this.inst.x+relativex) + "," + (this.inst.y+relativey); break;
        case 1: parsed = (this.inst.width+relativex) + "," + (this.inst.height+relativey); break;
        case 2: parsed = (this.inst.width+relativex) + "," + (this.inst.height+relativey); break;
        case 3: parsed = (this.inst.width+relativex) + "," + (this.inst.height+relativey); break;
        case 4: parsed = (cr.to_degrees(this.inst.angle)+relativex) + "," + (cr.to_degrees(this.inst.angle)+relativey); break;
        case 5: parsed = (this.inst.opacity*100+relativex) + "," + (this.inst.opacity*100+relativey); break;
        case 6: parsed = value+relativex + "," + value+relativex; break;
        case 7: parsed = (this.inst.x+relativex) + "," + (this.inst.y); break;
        case 8: parsed = (this.inst.x) + "," + (this.inst.y+relativex); break;
        case 9: parsed = (relativex) + "," + (relativey); break;
        default:  break;
      }
    }
    return parsed;
  };
  
	behinstProto.addToTweenList = function(tname, tweened, easing, init, targ, duration, enforce)
  {
    init = this.parseCurrent(tweened, init);
    targ = this.parseCurrent(tweened, targ);

    if (this.tween_list[tname] !== undefined) {
      delete this.tween_list[tname]
    } 
    this.tween_list[tname] = new TweenObject(tname, tweened, easing, init, targ, duration, enforce);
    this.tween_list[tname].dt = 0;
  };

  //initialize all needed parameter for tweening
	behinstProto.saveToJSON = function ()
	{
    var v = JSON.stringify(this.tween_list["default"]);
		return {
			"playmode": this.playmode,
			"active": this.active,
			"tweened": this.tweened,
			"easing": this.easing,
			"target": this.target,
			"targetmode": this.targetmode,
			"useCurrent": this.useCurrent,
			"duration": this.duration,
			"enforce": this.enforce,
			"value": this.value,
			"tweenlist": JSON.stringify(this.tween_list["default"])
		};
	};

  TweenObject.Load = function(rawObj, tname, tweened, easing, init, targ, duration, enforce)
  {
    var obj = new TweenObject(tname, tweened, easing, init, targ, duration, enforce);
    for(var i in rawObj)
        obj[i] = rawObj[i];
    return obj;
  };
	
	// called when loading the full state of the game
	behinstProto.loadFromJSON = function (o)
	{
    var x = JSON.parse(o["tweenlist"]);
    var tempObj = TweenObject.Load(x, x.name, x.tweened, x.easefunc, x.initialparam1+","+x.initialparam2, x.targetparam1+","+x.targetparam2, x.duration, x.enforce);
		this.tween_list["default"] = tempObj;
	  this.playmode = o["playmode"];
		this.active = o["active"];
		this.movement = o["tweened"];
		this.easing = o["easing"];
		this.target = o["target"];
		this.targetmode = o["targetmode"];
		this.useCurrent = o["useCurrent"];
		this.duration = o["duration"];
		this.enforce = o["enforce"];
		this.value = o["value"];
	};

	behinstProto.setProgressTo = function (mark)
	{
    if (mark > 1.0) mark = 1.0;
    if (mark < 0.0) mark = 0.0;

    for (var i in this.tween_list) { 
      var inst = this.tween_list[i];
      inst.lastKnownValue = 0;
      inst.lastKnownValue2 = 0;
      inst.state = 3;
      inst.progress = mark * inst.duration;
      var factor = inst.OnTick(0);
      this.updateTween(inst, factor);
    }
  }

	behinstProto.startTween = function (startMode)
	{
    for (var i in this.tween_list) {
      var inst = this.tween_list[i];
      if (this.useCurrent) {
        var init = this.parseCurrent(inst.tweened, "current");
        var target = this.parseCurrent(inst.tweened, this.target);
        inst.setInitial(init);
        inst.setTarget(target);
      }
      if (startMode === 0) {
        inst.progress = 0.000001;
        inst.lastKnownValue = 0;
        inst.lastKnownValue2 = 0;
        inst.onStart = true;
        inst.state = 1;
      }
      if (startMode === 1) {
        inst.state = inst.lastState;
      }
      if ((startMode === 2) || (startMode === 4)) {
        inst.progress = 0.000001;
        inst.lastKnownValue = 0;
        inst.lastKnownValue2 = 0;
        inst.onStart = true;
        if (startMode == 2) inst.state = 4; //state ping pong
        if (startMode == 4) inst.state = 6; //state flip flop
      }
      if (startMode === 3) {
        inst.progress = 0.000001;
        inst.lastKnownValue = 0;
        inst.lastKnownValue2 = 0;
        inst.onStart = true;
        inst.state = 5;
      }
    }
  }

	behinstProto.stopTween = function (stopMode)
	{
    for (var i in this.tween_list) {
      var inst = this.tween_list[i];
      if ((inst.state != 3) && (inst.state != 0)) //don't save paused/seek state
        inst.lastState = inst.state;
      if (stopMode === 1) inst.progress = 0.0; 
      if (stopMode === 2) inst.progress = inst.duration;
      inst.state = 3;
      var factor = inst.OnTick(0);
      this.updateTween(inst, factor);
    }
  }

	behinstProto.reverseTween = function(reverseMode)
	{
    for (var i in this.tween_list) {
      var inst = this.tween_list[i];
      if (reverseMode === 1) {
        inst.progress = inst.duration;
        inst.lastKnownValue = 0;
        inst.lastKnownValue2 = 0;
        inst.onReverseStart = true;
      } 
      inst.state = 2;
    }
  }
	
	behinstProto.updateTween = function (inst, factor)
	{
    //var isMirrored = 1;
    //var isFlipped = 1;
    //if (this.inst.width < 0) isMirrored = -1; 
    //if (this.inst.height < 0) isFlipped = -1;
    if (inst.tweened === 0) {
      //if tweening position
      if (inst.enforce) {
        //enforce new coordinate
	      this.inst.x = inst.initialparam1 + (inst.targetparam1 - inst.initialparam1) * factor;
        this.inst.y = inst.initialparam2 + (inst.targetparam2 - inst.initialparam2) * factor;
      } else {
        //compromise coordinate change
        this.inst.x += ((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue;
        this.inst.y += ((inst.targetparam2 - inst.initialparam2) * factor) - inst.lastKnownValue2;
        inst.lastKnownValue = ((inst.targetparam1 - inst.initialparam1) * factor); 
        inst.lastKnownValue2 = ((inst.targetparam2 - inst.initialparam2) * factor);
      }
    } else if (inst.tweened === 1) {
      //if tweening size
      if (inst.enforce) {
  			this.inst.width = (inst.initialparam1 + ((inst.targetparam1 - inst.initialparam1) * (factor)));
	   		this.inst.height = (inst.initialparam2 + ((inst.targetparam2 - inst.initialparam2) * (factor)));
      } else {
       	this.inst.width +=  ((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue;
      	this.inst.height += ((inst.targetparam2 - inst.initialparam2) * factor) - inst.lastKnownValue2;
        inst.lastKnownValue = ((inst.targetparam1 - inst.initialparam1) * factor); 
        inst.lastKnownValue2 = ((inst.targetparam2 - inst.initialparam2) * factor); 
      }
    } else if (inst.tweened === 2) {
      //if tweening size width only
      if (inst.enforce) {
  			this.inst.width = (inst.initialparam1 + ((inst.targetparam1 - inst.initialparam1) * (factor)));
      } else {
      	this.inst.width += ((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue;
        inst.lastKnownValue = ((inst.targetparam1 - inst.initialparam1) * factor); 
      }
    } else if (inst.tweened === 3) {
      //if tweening size height only
      if (inst.enforce) {  
	   		this.inst.height = (inst.initialparam2 + ((inst.targetparam2 - inst.initialparam2) * (factor)));
      } else {
      	this.inst.height += ((inst.targetparam2 - inst.initialparam2) * factor) - inst.lastKnownValue2;
        inst.lastKnownValue2 = ((inst.targetparam2 - inst.initialparam2) * factor); 
      }
    } else if (inst.tweened === 4) {
      //if tweening angle
      if (inst.enforce) {
  		  var tangle = inst.initialparam1 + (inst.targetparam1 - inst.initialparam1) * factor;
  		  this.inst.angle = cr.clamp_angle(cr.to_radians(tangle));
      } else {
  		  var tangle = ((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue;
  		  this.inst.angle = cr.clamp_angle(this.inst.angle + cr.to_radians(tangle));
        inst.lastKnownValue = (inst.targetparam1 - inst.initialparam1) * factor; 
      }
    } else if (inst.tweened === 5) {
      //if tweening opacity
      if (inst.enforce) {
  		  this.inst.opacity = (inst.initialparam1 + (inst.targetparam1 - inst.initialparam1) * factor) / 100;
      } else {
  		  this.inst.opacity += (((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue) / 100;
        inst.lastKnownValue = ((inst.targetparam1 - inst.initialparam1) * factor);
      }
    } else if (inst.tweened === 6) {
      //if tweening value
      if (inst.enforce) {
  		  this.value = (inst.initialparam1 + (inst.targetparam1 - inst.initialparam1) * factor);
      } else {
  		  this.value += (((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue);
        inst.lastKnownValue = ((inst.targetparam1 - inst.initialparam1) * factor);
      }
    } else if (inst.tweened === 7) {
      //if tweening position X only
      if (inst.enforce) {
        //enforce new coordinate
	      this.inst.x = inst.initialparam1 + (inst.targetparam1 - inst.initialparam1) * factor;
      } else {
        //compromise coordinate change
        this.inst.x += ((inst.targetparam1 - inst.initialparam1) * factor) - inst.lastKnownValue;
        inst.lastKnownValue = ((inst.targetparam1 - inst.initialparam1) * factor); 
      }
    } else if (inst.tweened === 8) {
      //if tweening position Y only
      if (inst.enforce) {
        //enforce new coordinate
        this.inst.y = inst.initialparam2 + (inst.targetparam2 - inst.initialparam2) * factor;
      } else {
        //compromise coordinate change
        this.inst.y += ((inst.targetparam2 - inst.initialparam2) * factor) - inst.lastKnownValue2;
        inst.lastKnownValue2 = ((inst.targetparam2 - inst.initialparam2) * factor);
      }
    } else if (inst.tweened === 9) {
      //if tweening scale
      var scalex = inst.initialparam1 + (inst.targetparam1 - inst.initialparam1) * factor;
      var scaley = inst.initialparam2 + (inst.targetparam2 - inst.initialparam2) * factor;
      if (this.inst.width < 0) scalex = inst.initialparam1 + (inst.targetparam1 + inst.initialparam1) * -factor;
      if (this.inst.height < 0)  scaley = inst.initialparam2 + (inst.targetparam2 + inst.initialparam2) * -factor;
      if (inst.enforce) {
        this.inst.width = this.inst.curFrame.width * scalex;
        this.inst.height = this.inst.curFrame.height * scaley;
      } else {
        if (this.inst.width < 0) { 
      	  this.inst.width = scalex * (this.inst.width / (-1+inst.lastKnownValue)); 
          inst.lastKnownValue = scalex + 1 
        } else { 
      	  this.inst.width = scalex * (this.inst.width / (1+inst.lastKnownValue)); 
          inst.lastKnownValue = scalex - 1;
        }
        if (this.inst.height < 0) {
          this.inst.height = scaley * (this.inst.height / (-1+inst.lastKnownValue2));
          inst.lastKnownValue2 = scaley + 1 
        } else {
          this.inst.height = scaley * (this.inst.height / (1+inst.lastKnownValue2));
          inst.lastKnownValue2 = scaley - 1;
        } 
      }
    }

    this.inst.set_bbox_changed();
  }
  
	behinstProto.tick = function ()
	{
		var dt = this.runtime.getDt(this.inst);

    var inst = this.tween_list["default"];

    if (inst.state !== 0) {
      if (inst.onStart) {
  			this.runtime.trigger(cr.behaviors.lunarray_LiteTween.prototype.cnds.OnStart, this.inst);
        inst.onStart = false;
      }
  
      if (inst.onReverseStart) {
  		  this.runtime.trigger(cr.behaviors.lunarray_LiteTween.prototype.cnds.OnReverseStart, this.inst);
        inst.onReverseStart = false;
      }

      this.active = (inst.state == 1) || (inst.state == 2) || (inst.state == 4) || (inst.state == 5) || (inst.state == 6);
      var factor = inst.OnTick(dt);
      this.updateTween(inst, factor);

      if (inst.onEnd) {
  		  this.runtime.trigger(cr.behaviors.lunarray_LiteTween.prototype.cnds.OnEnd, this.inst);
        inst.onEnd = false;
      }
  
      if (inst.onReverseEnd) {
  		  this.runtime.trigger(cr.behaviors.lunarray_LiteTween.prototype.cnds.OnReverseEnd, this.inst);
        inst.onReverseEnd = false;
      }
    }
	};


	// Conditions
	behaviorProto.cnds = {};
	var cnds = behaviorProto.cnds;
	
	cnds.IsActive = function ()
	{
		return (this.tween_list["default"].state !== 0);
	};

	cnds.IsReversing = function ()
	{
		return (this.tween_list["default"].state == 2);
	};

	cnds.CompareProgress = function (cmp, v)
	{
    var inst = this.tween_list["default"];
		return cr.do_cmp((inst.progress / inst.duration), cmp, v);
	};

	cnds.OnThreshold = function (cmp, v)
	{
    var inst = this.tween_list["default"];
    this.threshold = (cr.do_cmp((inst.progress / inst.duration), cmp, v)); 
    var ret = (this.oldthreshold != this.threshold) && (this.threshold); 
    if (ret) { 
      this.oldthreshold = this.threshold;
    }
		return ret;
	};

	cnds.OnStart = function ()
	{
    if (this.tween_list["default"] === undefined) 
      return false;

    return this.tween_list["default"].onStart;    
	};

	cnds.OnReverseStart = function ()
	{
    if (this.tween_list["default"] === undefined) 
      return false;

    return this.tween_list["default"].onReverseStart;    
	};

  cnds.OnEnd = function ()
	{
    if (this.tween_list["default"] === undefined) 
      return false;
      
    return this.tween_list["default"].onEnd;    
	};

  cnds.OnReverseEnd = function ()
	{
    if (this.tween_list["default"] === undefined) 
      return false;

    return this.tween_list["default"].onReverseEnd;    
	};

	// Actions
	behaviorProto.acts = {};
	var acts = behaviorProto.acts;
	
	acts.Start = function (startmode, current)
	{
    this.threshold = false;
    this.oldthreshold = false;

    this.useCurrent = (current == 1);
    this.startTween(startmode);
	};

	acts.Stop = function (stopmode)
	{
    this.stopTween(stopmode);
	};

	acts.Reverse = function (revMode)
	{
    this.threshold = false;
    this.oldthreshold = false;

    this.reverseTween(revMode);
	};

 	acts.ProgressTo = function (progress)
	{
    this.setProgressTo(progress);
	};
	
	acts.SetDuration = function (x)
	{
    if (isNaN(x)) return;
    if (x < 0) return;
    if (this.tween_list["default"] === undefined) return;
		this.tween_list["default"].duration = x;
	};

	acts.SetEnforce = function (x)
	{
    if (this.tween_list["default"] === undefined) return;
		this.tween_list["default"].enforce = (x===1);
	};

	acts.SetInitial = function (x)
	{
    if (this.tween_list["default"] === undefined) return;
    var init = this.parseCurrent(this.tween_list["default"].tweened, x);
		this.tween_list["default"].setInitial(init);
	};

	acts.SetTarget = function (targettype, absrel, x)
	{
    if (this.tween_list["default"] === undefined) return;
    if (isNaN(x)) return;
    var inst = this.tween_list["default"];
    var parsed = x + "";
    this.targetmode = absrel;
    var x1 = "";
    var x2 = "";
    if (absrel === 1) {
      //relative
      this.target = "relative(" + parsed + ")";
      switch (targettype) {
        case 0: x1 = (this.inst.x + x); x2 = inst.targetparam2; break;
        case 1: x1 = inst.targetparam1; x2 = (this.inst.y + x); break;
        case 2: x1 = "" + cr.to_degrees(this.inst.angle + cr.to_radians(x)); x2 = x1; break; //angle
        case 3: x1 = "" + (this.inst.opacity*100) + x; x2 = x1; break; //opacity
        case 4: x1 = (this.inst.width + x); x2 = inst.targetparam2; break; //width
        case 5: x1 = inst.targetparam1; x2 = (this.inst.height + x); break; //height
        case 6: x1 = x; x2 = x; break; //value
        default:  break;
      }
      parsed = x1 + "," + x2;
    } else {
      switch (targettype) {
        case 0: x1 = x; x2 = inst.targetparam2; break;
        case 1: x1 = inst.targetparam1; x2 = x; break;
        case 2: x1 = x; x2 = x; break; //angle
        case 3: x1 = x; x2 = x; break; //opacity
        case 4: x1 = x; x2 = inst.targetparam2; break; //width
        case 5: x1 = inst.targetparam1; x2 = x; break; //height
        case 6: x1 = x; x2 = x; break; //value
        default:  break;
      }
      parsed = x1 + "," + x2;
      this.target = parsed;
    }
    var init = this.parseCurrent(this.tween_list["default"].tweened, "current");
    var targ = this.parseCurrent(this.tween_list["default"].tweened, parsed);
 		inst.setInitial(init);
 		inst.setTarget(targ);
	};              

	acts.SetTweenedProperty = function (x)
	{
    if (this.tween_list["default"] === undefined) return;
		this.tween_list["default"].tweened = x;
	};

	acts.SetEasing = function (x)
	{
    if (this.tween_list["default"] === undefined) return;
		this.tween_list["default"].easefunc = x;
	};

 	acts.SetEasingParam = function (x, a, p, t, s)
	{
    if (this.tween_list["default"] === undefined) return;
    this.tween_list["default"].easingparam[x].optimized = false;
		this.tween_list["default"].easingparam[x].a = a;
		this.tween_list["default"].easingparam[x].p = p;
		this.tween_list["default"].easingparam[x].t = t;
		this.tween_list["default"].easingparam[x].s = s;
	};

 	acts.ResetEasingParam = function ()
	{
    if (this.tween_list["default"] === undefined) return;
    this.tween_list["default"].optimized = true;
	};

 	acts.SetValue = function (x)
	{
    var inst = this.tween_list["default"];
		this.value = x;
    if (inst.tweened === 6)
      inst.setInitial( this.parseCurrent(inst.tweened, "current") );
	};

	acts.SetParameter = function (tweened, easefunction, target, duration, enforce)
	{
    if (this.tween_list["default"] === undefined) {
      this.addToTweenList("default", tweened, easefunction, initial, target, duration, enforce, 0);
    } else {
      var inst = this.tween_list["default"];
      inst.tweened = tweened; 
  		inst.easefunc = easefunction;
      inst.setInitial( this.parseCurrent(tweened, "current") );
      inst.setTarget( this.parseCurrent(tweened, target) );
      inst.duration = duration; 
      inst.enforce = (enforce === 1); 
    }
	};
  
	// Expressions
	behaviorProto.exps = {};
	var exps = behaviorProto.exps;
	exps.State = function (ret)
	{
    var parsed = "N/A"; 
    switch (this.tween_list["default"].state) {
      case 0: parsed = "paused"; break;
      case 1: parsed = "playing"; break;
      case 2: parsed = "reversing"; break;
      case 3: parsed = "seeking"; break;
      default:  break;
    }
    ret.set_string(parsed);
	};

	exps.Progress = function (ret)
	{
    var progress = this.tween_list["default"].progress/this.tween_list["default"].duration; 
    ret.set_float(progress);
	};

	exps.Duration = function (ret)
	{
    ret.set_float(this.tween_list["default"].duration);
	};

	exps.Target = function (ret)
	{
    var inst = this.tween_list["default"];
    var parsed = "N/A"; 
    switch (inst.tweened) {
      case 0: parsed = inst.targetparam1; break;
      case 1: parsed = inst.targetparam2; break;
      case 2: parsed = inst.targetparam1; break;
      case 3: parsed = inst.targetparam1; break;
      case 4: parsed = inst.targetparam1; break;
      case 5: parsed = inst.targetparam2; break;
      case 6: parsed = inst.targetparam1; break;
      default:  break;
    }
    ret.set_float(parsed);
	};

	exps.Value = function (ret)
	{
    var tval = this.value; 
    ret.set_float(tval);
	};

	exps.Tween = function (ret, a_, b_, x_, easefunc_)
	{
    var currX = (x_>1.0?1.0:x_);
    var factor = easeFunc(easefunc_, currX<0.0?0.0:currX, 0.0, 1.0, 1.0, false, false);
    ret.set_float(a_ + factor * (b_-a_));
	};
  
}());


// Scroll To
// ECMAScript 5 strict mode

;
;

/////////////////////////////////////
// Behavior class
cr.behaviors.scrollto = function(runtime)
{
	this.runtime = runtime;
	
	this.shakeMag = 0;
	this.shakeStart = 0;
	this.shakeEnd = 0;
	this.shakeMode = 0;
};

(function ()
{
	var behaviorProto = cr.behaviors.scrollto.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.enabled = this.properties[0];
	};

	behinstProto.saveToJSON = function ()
	{
		return {
			"smg": this.behavior.shakeMag,
			"ss": this.behavior.shakeStart,
			"se": this.behavior.shakeEnd,
			"smd": this.behavior.shakeMode
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.behavior.shakeMag = o["smg"];
		this.behavior.shakeStart = o["ss"];
		this.behavior.shakeEnd = o["se"];
		this.behavior.shakeMode = o["smd"];
	};
	
	behinstProto.tick = function ()
	{
		// Do work in tick2 instead, to eliminate one-frame lag if object position changes in events
	};
	
	function getScrollToBehavior(inst)
	{
		var i, len, binst;
		for (i = 0, len = inst.behavior_insts.length; i < len; ++i)
		{
			binst = inst.behavior_insts[i];
			
			if (binst.behavior instanceof cr.behaviors.scrollto)
				return binst;
		}
		
		return null;
	};
	
	behinstProto.tick2 = function ()
	{
		if (!this.enabled)
			return;
		
		// Get all instances with this behavior
		var all = this.behavior.my_instances.valuesRef();
		var sumx = 0, sumy = 0;
		var i, len, binst, count = 0;
		
		for (i = 0, len = all.length; i < len; i++)
		{
			binst = getScrollToBehavior(all[i]);
			
			if (!binst || !binst.enabled)
				continue;
			
			sumx += all[i].x;
			sumy += all[i].y;
			++count;
		}
		
		var layout = this.inst.layer.layout;
		
		// Is in a shake?
		var now = this.runtime.kahanTime.sum;
		var offx = 0, offy = 0;
		
		if (now >= this.behavior.shakeStart && now < this.behavior.shakeEnd)
		{
			var mag = this.behavior.shakeMag * Math.min(this.runtime.timescale, 1);
			
			// Mode 0 - reducing magnitude - lerp to zero
			if (this.behavior.shakeMode === 0)
				mag *= 1 - (now - this.behavior.shakeStart) / (this.behavior.shakeEnd - this.behavior.shakeStart);
				
			var a = Math.random() * Math.PI * 2;
			var d = Math.random() * mag;
			offx = Math.cos(a) * d;
			offy = Math.sin(a) * d;
		}
		
		layout.scrollToX(sumx / count + offx);
		layout.scrollToY(sumy / count + offy);
	};
	
	//////////////////////////////////////
	// Conditions
	function Cnds() {};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.enabled;
	};
	
	behaviorProto.cnds = new Cnds();
	
	//////////////////////////////////////
	// Actions
	function Acts() {};

	Acts.prototype.Shake = function (mag, dur, mode)
	{
		this.behavior.shakeMag = mag;
		this.behavior.shakeStart = this.runtime.kahanTime.sum;
		this.behavior.shakeEnd = this.behavior.shakeStart + dur;
		this.behavior.shakeMode = mode;
	};
	
	Acts.prototype.SetEnabled = function (e)
	{
		this.enabled = (e !== 0);
	};
	
	behaviorProto.acts = new Acts();
	
}());

// Platform
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var behaviorProto = cr.behaviors.Platform.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
	
	// animation modes
	var ANIMMODE_STOPPED = 0;
	var ANIMMODE_MOVING = 1;
	var ANIMMODE_JUMPING = 2;
	var ANIMMODE_FALLING = 3;
	
	behaviorProto.Instance = function(type, inst)
	{
		this.type = type;
		this.behavior = type.behavior;
		this.inst = inst;				// associated object instance to modify
		this.runtime = type.runtime;
		
		// Key states
		this.leftkey = false;
		this.rightkey = false;
		this.jumpkey = false;
		this.jumped = false;			// prevent bunnyhopping
		this.doubleJumped = false;
		this.canDoubleJump = false;
		this.ignoreInput = false;
		
		// Simulated controls
		this.simleft = false;
		this.simright = false;
		this.simjump = false;
		
		// Last floor object for moving platform
		this.lastFloorObject = null;
		this.loadFloorObject = -1;
		this.lastFloorX = 0;
		this.lastFloorY = 0;
		this.floorIsJumpthru = false;
		
		this.ceilingCollisionMode = 0;	// 0 = stop, 1 = preserve momentum
		
		this.animMode = ANIMMODE_STOPPED;
		
		this.fallthrough = 0;			// fall through jump-thru.  >0 to disable, lasts a few ticks
		this.firstTick = true;
		
		// Movement
		this.dx = 0;
		this.dy = 0;
	};

	var behinstProto = behaviorProto.Instance.prototype;
	
	behinstProto.updateGravity = function()
	{
		// down vector
		this.downx = Math.cos(this.ga);
		this.downy = Math.sin(this.ga);
		
		// right vector
		this.rightx = Math.cos(this.ga - Math.PI / 2);
		this.righty = Math.sin(this.ga - Math.PI / 2);
		
		// get rid of any sin/cos small errors
		this.downx = cr.round6dp(this.downx);
		this.downy = cr.round6dp(this.downy);
		this.rightx = cr.round6dp(this.rightx);
		this.righty = cr.round6dp(this.righty);
		
		this.g1 = this.g;
		
		// gravity is negative (up): flip the down vector and make gravity positive
		// (i.e. change the angle of gravity instead)
		if (this.g < 0)
		{
			this.downx *= -1;
			this.downy *= -1;
			this.g = Math.abs(this.g);
		}
	};

	behinstProto.onCreate = function()
	{
		// Load properties
		this.maxspeed = this.properties[0];
		this.acc = this.properties[1];
		this.dec = this.properties[2];
		this.jumpStrength = this.properties[3];
		this.g = this.properties[4];
		this.g1 = this.g;
		this.maxFall = this.properties[5];
		this.enableDoubleJump = this.properties[6];
		this.jumpSustain = (this.properties[7] / 1000);		// convert ms to s
		this.defaultControls = this.properties[8];
		this.enabled = this.properties[9];
		this.wasOnFloor = false;
		this.wasOverJumpthru = this.runtime.testOverlapJumpThru(this.inst);
		this.loadOverJumpthru = -1;
		
		this.sustainTime = 0;				// time of jump sustain remaining

		// Angle of gravity
		this.ga = cr.to_radians(90);
		this.updateGravity();
		
		var self = this;
		
		// Only bind keyboard events if default controls are in use
		if (this.defaultControls)
		{
			document.addEventListener("keydown", function (info)
			{
				self.onKeyDown(info);
			});
			
			document.addEventListener("keyup", function (info)
			{
				self.onKeyUp(info);
			});
		}
		
		// Need to know if floor object gets destroyed
		if (!this.recycled)
		{
			this.myDestroyCallback = function(inst) {
										self.onInstanceDestroyed(inst);
									};
		}
										
		this.runtime.addDestroyCallback(this.myDestroyCallback);
		
		this.inst.extra["isPlatformBehavior"] = true;
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"ii": this.ignoreInput,
			"lfx": this.lastFloorX,
			"lfy": this.lastFloorY,
			"lfo": (this.lastFloorObject ? this.lastFloorObject.uid : -1),
			"am": this.animMode,
			"en": this.enabled,
			"fall": this.fallthrough,
			"ft": this.firstTick,
			"dx": this.dx,
			"dy": this.dy,
			"ms": this.maxspeed,
			"acc": this.acc,
			"dec": this.dec,
			"js": this.jumpStrength,
			"g": this.g,
			"g1": this.g1,
			"mf": this.maxFall,
			"wof": this.wasOnFloor,
			"woj": (this.wasOverJumpthru ? this.wasOverJumpthru.uid : -1),
			"ga": this.ga,
			"edj": this.enableDoubleJump,
			"cdj": this.canDoubleJump,
			"dj": this.doubleJumped,
			"sus": this.jumpSustain,
			"cc": this.ceilingCollisionMode
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.ignoreInput = o["ii"];
		this.lastFloorX = o["lfx"];
		this.lastFloorY = o["lfy"];
		this.loadFloorObject = o["lfo"];
		this.animMode = o["am"];
		this.enabled = o["en"];
		this.fallthrough = o["fall"];
		this.firstTick = o["ft"];
		this.dx = o["dx"];
		this.dy = o["dy"];
		this.maxspeed = o["ms"];
		this.acc = o["acc"];
		this.dec = o["dec"];
		this.jumpStrength = o["js"];
		this.g = o["g"];
		this.g1 = o["g1"];
		this.maxFall = o["mf"];
		this.wasOnFloor = o["wof"];
		this.loadOverJumpthru = o["woj"];
		this.ga = o["ga"];
		this.enableDoubleJump = o["edj"];
		this.canDoubleJump = o["cdj"];
		this.doubleJumped = o["dj"];
		this.jumpSustain = o["sus"];
		this.ceilingCollisionMode = o["cc"] || 0;
		
		this.leftkey = false;
		this.rightkey = false;
		this.jumpkey = false;
		this.jumped = false;
		this.simleft = false;
		this.simright = false;
		this.simjump = false;
		this.sustainTime = 0;
		this.updateGravity();
	};
	
	behinstProto.afterLoad = function ()
	{
		if (this.loadFloorObject === -1)
			this.lastFloorObject = null;
		else
			this.lastFloorObject = this.runtime.getObjectByUID(this.loadFloorObject);
			
		if (this.loadOverJumpthru === -1)
			this.wasOverJumpthru = null;
		else
			this.wasOverJumpthru = this.runtime.getObjectByUID(this.loadOverJumpthru);
	};
	
	behinstProto.onInstanceDestroyed = function (inst)
	{
		// Floor object being destroyed
		if (this.lastFloorObject == inst)
			this.lastFloorObject = null;
	};
	
	behinstProto.onDestroy = function ()
	{
		this.lastFloorObject = null;
		this.runtime.removeDestroyCallback(this.myDestroyCallback);
	};

	behinstProto.onKeyDown = function (info)
	{	
		switch (info.which) {
		case 38:	// up
			info.preventDefault();
			this.jumpkey = true;
			break;
		case 37:	// left
			info.preventDefault();
			this.leftkey = true;
			break;
		case 39:	// right
			info.preventDefault();
			this.rightkey = true;
			break;
		}
	};

	behinstProto.onKeyUp = function (info)
	{
		switch (info.which) {
		case 38:	// up
			info.preventDefault();
			this.jumpkey = false;
			this.jumped = false;
			break;
		case 37:	// left
			info.preventDefault();
			this.leftkey = false;
			break;
		case 39:	// right
			info.preventDefault();
			this.rightkey = false;
			break;
		}
	};
	
	behinstProto.onWindowBlur = function ()
	{
		this.leftkey = false;
		this.rightkey = false;
		this.jumpkey = false;
	};
	
	behinstProto.getGDir = function ()
	{
		if (this.g < 0)
			return -1;
		else
			return 1;
	};

	behinstProto.isOnFloor = function ()
	{
		var ret = null;
		var ret2 = null;
		var i, len, j;
		
		// Move object one pixel down
		var oldx = this.inst.x;
		var oldy = this.inst.y;
		this.inst.x += this.downx;
		this.inst.y += this.downy;
		this.inst.set_bbox_changed();
		
		// See if still overlapping last floor object (if any).
		// If it is, also check that if it has the solid behavior, that the solid is still enabled. This is to ensure if the floor
		// we are standing on has its solid behavior disabled, we then fall through it.
		if (this.lastFloorObject && this.runtime.testOverlap(this.inst, this.lastFloorObject) &&
			(!this.runtime.typeHasBehavior(this.lastFloorObject.type, cr.behaviors.solid) || this.lastFloorObject.extra["solidEnabled"]))
		{
			// Put the object back
			this.inst.x = oldx;
			this.inst.y = oldy;
			this.inst.set_bbox_changed();
			return this.lastFloorObject;
		}
		else
		{
			ret = this.runtime.testOverlapSolid(this.inst);
			
			if (!ret && this.fallthrough === 0)
				ret2 = this.runtime.testOverlapJumpThru(this.inst, true);
			
			// Put the object back
			this.inst.x = oldx;
			this.inst.y = oldy;
			this.inst.set_bbox_changed();
			
			if (ret)		// was overlapping solid
			{
				// If the object is still overlapping the solid one pixel up, it
				// must be stuck inside something.  So don't count it as floor.
				if (this.runtime.testOverlap(this.inst, ret))
					return null;
				else
				{
					this.floorIsJumpthru = false;
					return ret;
				}
			}
			
			// Is overlapping one or more jumpthrus
			if (ret2 && ret2.length)
			{
				// Filter out jumpthrus it is still overlapping one pixel up
				for (i = 0, j = 0, len = ret2.length; i < len; i++)
				{
					ret2[j] = ret2[i];
					
					if (!this.runtime.testOverlap(this.inst, ret2[i]))
						j++;
				}
				
				// All jumpthrus it is only overlapping one pixel down are floor pieces/tiles.
				// Return first in list.
				if (j >= 1)
				{
					this.floorIsJumpthru = true;
					return ret2[0];
				}
			}
			
			return null;
		}
	};
	
	behinstProto.tick = function ()
	{
	};

	behinstProto.posttick = function ()
	{
		var dt = this.runtime.getDt(this.inst);
		var mx, my, obstacle, mag, allover, i, len, j, oldx, oldy;
		
		// The "jumped" flag needs resetting whenever the jump key is not simulated for custom controls
		// This musn't conflict with default controls so make sure neither the jump key nor simulate jump is on
		if (!this.jumpkey && !this.simjump)
			this.jumped = false;
			
		var left = this.leftkey || this.simleft;
		var right = this.rightkey || this.simright;
		var jumpkey = (this.jumpkey || this.simjump);
		var jump = jumpkey && !this.jumped;
		this.simleft = false;
		this.simright = false;
		this.simjump = false;
		
		if (!this.enabled)
			return;
		
		// Ignoring input: ignore all keys
		if (this.ignoreInput)
		{
			left = false;
			right = false;
			jumpkey = false;
			jump = false;
		}
		
		if (!jumpkey)
			this.sustainTime = 0;
		
		var lastFloor = this.lastFloorObject;
		var floor_moved = false;
		
		// On first tick, push up out the floor with sub-pixel precision.  This resolves 1px float issues
		// with objects placed starting exactly on the floor.
		if (this.firstTick)
		{
			if (this.runtime.testOverlapSolid(this.inst) || this.runtime.testOverlapJumpThru(this.inst))
			{
				this.runtime.pushOutSolid(this.inst, -this.downx, -this.downy, 4, true);
			}
			
			this.firstTick = false;
		}
		
		// Track moving platforms
		if (lastFloor && this.dy === 0 && (lastFloor.y !== this.lastFloorY || lastFloor.x !== this.lastFloorX))
		{
			mx = (lastFloor.x - this.lastFloorX);
			my = (lastFloor.y - this.lastFloorY);
			this.inst.x += mx;
			this.inst.y += my;
			this.inst.set_bbox_changed();
			this.lastFloorX = lastFloor.x;
			this.lastFloorY = lastFloor.y;
			floor_moved = true;
			
			// Platform moved player in to a solid: push out of the solid again
			if (this.runtime.testOverlapSolid(this.inst))
			{
				this.runtime.pushOutSolid(this.inst, -mx, -my, Math.sqrt(mx * mx + my * my) * 2.5);
			}
		}
		
		// Test if on floor
		var floor_ = this.isOnFloor();
		
		// Push out nearest here to prevent moving objects crushing/trapping the player.
		// Skip this when input predicted by the multiplayer object, since it just conflicts horribly and
		// makes the player wobble all over the place.
		var collobj = this.runtime.testOverlapSolid(this.inst);
		if (collobj)
		{
			// Use absolute size to ensure negative size from mirror/flip does not affect the push distance
			var instWidth = Math.abs(this.inst.width);
			var instHeight = Math.abs(this.inst.height);
			
			if (this.inst.extra["inputPredicted"])
			{
				this.runtime.pushOutSolid(this.inst, -this.downx, -this.downy, 10, false);
			}
			// First try to push out on the up axis by up to 1/8th player height (for rotating floors)
			else if (this.runtime.pushOutSolidAxis(this.inst, -this.downx, -this.downy, instHeight / 8))
			{
				this.runtime.registerCollision(this.inst, collobj);
			}
			// Next try to push out on horizontal axis (for moving walls)
			else if (this.runtime.pushOutSolidAxis(this.inst, this.rightx, this.righty, instWidth / 2))
			{
				this.runtime.registerCollision(this.inst, collobj);
			}
			// Next try to push out on the down axis (for moving ceilings)
			else if (this.runtime.pushOutSolidAxis(this.inst, this.downx, this.downy, instHeight / 2))
			{
				this.runtime.registerCollision(this.inst, collobj);
			}
			// Otherwise push out in any direction
			else if (this.runtime.pushOutSolidNearest(this.inst, Math.max(instWidth, instHeight) / 2))
			{
				this.runtime.registerCollision(this.inst, collobj);
			}
			// If can't push out, must be stuck, give up
			else
				return;
		}
		
		if (floor_)
		{
			this.doubleJumped = false;		// reset double jump flags for next jump
			this.canDoubleJump = false;
			
			if (this.dy > 0)
			{
				// By chance we may have fallen perfectly to 1 pixel above the floor, which might make
				// isOnFloor return true before we've had a pushOutSolid from the floor to make us sit
				// tightly on it.  So we might actually be hovering 1 pixel in the air.  To resolve this,
				// if this is the first landing issue another pushInFractional.
				if (!this.wasOnFloor)
				{
					this.runtime.pushInFractional(this.inst, -this.downx, -this.downy, floor_, 16);
					this.wasOnFloor = true;
				}
					
				this.dy = 0;
			}

			// First landing on the floor or floor changed
			if (lastFloor != floor_)
			{
				this.lastFloorObject = floor_;
				this.lastFloorX = floor_.x;
				this.lastFloorY = floor_.y;
				this.runtime.registerCollision(this.inst, floor_);
			}
			// If the floor has moved, check for moving in to a solid
			else if (floor_moved)
			{
				collobj = this.runtime.testOverlapSolid(this.inst);
				if (collobj)
				{
					this.runtime.registerCollision(this.inst, collobj);
					
					// Push out horizontally then up
					if (mx !== 0)
					{
						if (mx > 0)
							this.runtime.pushOutSolid(this.inst, -this.rightx, -this.righty);
						else
							this.runtime.pushOutSolid(this.inst, this.rightx, this.righty);
					}

					this.runtime.pushOutSolid(this.inst, -this.downx, -this.downy);
				}
			}
		}
		// not on floor
		else
		{
			// If in mid-air and not holding jump key, set flag ready for double jump
			if (!jumpkey)
				this.canDoubleJump = true;
		}
		
		// If jumping from floor or double-jumping in mid-air
		if ((floor_ && jump) || (!floor_ && this.enableDoubleJump && jumpkey && this.canDoubleJump && !this.doubleJumped))
		{			
			// Check we can move up 1px else assume jump is blocked.
			oldx = this.inst.x;
			oldy = this.inst.y;
			this.inst.x -= this.downx;
			this.inst.y -= this.downy;
			this.inst.set_bbox_changed();
			
			if (!this.runtime.testOverlapSolid(this.inst))
			{
				// Reset sustain time
				this.sustainTime = this.jumpSustain;
				
				// Trigger On Jump
				this.runtime.trigger(cr.behaviors.Platform.prototype.cnds.OnJump, this.inst);
				this.animMode = ANIMMODE_JUMPING;
				this.dy = -this.jumpStrength;
				jump = true;		// set in case is double jump
				
				// Prevent bunnyhopping: dont allow another jump until key up
				if (floor_)
					this.jumped = true;
				else
					this.doubleJumped = true;
			}
			else
				jump = false;
				
			this.inst.x = oldx;
			this.inst.y = oldy;
			this.inst.set_bbox_changed();
		}

		// Not on floor: apply gravity
		if (!floor_)
		{
			// Holding jump sustain
			if (jumpkey && this.sustainTime > 0)
			{
				this.dy = -this.jumpStrength;
				this.sustainTime -= dt;
			}
			// Otherwise apply gravity
			else
			{
				this.lastFloorObject = null;
				
				this.dy += this.g * dt;
				
				// Cap to max fall speed
				if (this.dy > this.maxFall)
					this.dy = this.maxFall;
			}
			
			// Still set the jumped flag to prevent double tap bunnyhop
			if (jump)
				this.jumped = true;
		}
		
		this.wasOnFloor = !!floor_;
		
		// Apply horizontal deceleration when no arrow key pressed
		if (left == right)	// both up or both down
		{
			if (this.dx < 0)
			{
				this.dx += this.dec * dt;
				
				if (this.dx > 0)
					this.dx = 0;
			}
			else if (this.dx > 0)
			{
				this.dx -= this.dec * dt;
				
				if (this.dx < 0)
					this.dx = 0;
			}
		}
		
		// Apply horizontal acceleration
		var hacc = 0;
		
		if (left && !right)
		{
			// Moving in opposite direction to current motion: add deceleration
			if (this.dx > 0)
				hacc = -(this.acc + this.dec);
			else
				hacc = -this.acc;
		}
		
		if (right && !left)
		{
			if (this.dx < 0)
				hacc = this.acc + this.dec;
			else
				hacc = this.acc;
		}
		
		this.dx += hacc * dt;
		
		// Cap to max speed
		if (this.dx > this.maxspeed)
			this.dx = this.maxspeed;
		else if (this.dx < -this.maxspeed)
			this.dx = -this.maxspeed;
		
		var landed = false;
		
		if (this.dx !== 0)
		{		
			// Attempt X movement
			oldx = this.inst.x;
			oldy = this.inst.y;
			mx = this.runtime.accelerate(this.dx, -this.maxspeed, this.maxspeed, hacc, dt) * this.rightx;
			my = this.runtime.accelerate(this.dx, -this.maxspeed, this.maxspeed, hacc, dt) * this.righty;
			
			// Check that 1 px across and 1 px up is free.  Otherwise the slope is too steep to
			// try climbing.
			this.inst.x += this.rightx * (this.dx > 1 ? 1 : -1) - this.downx;
			this.inst.y += this.righty * (this.dx > 1 ? 1 : -1) - this.downy;
			this.inst.set_bbox_changed();
			
			var is_jumpthru = false;
			
			var slope_too_steep = this.runtime.testOverlapSolid(this.inst);
			
			/*
			if (!slope_too_steep && floor_)
			{
				slope_too_steep = this.runtime.testOverlapJumpThru(this.inst);
				is_jumpthru = true;
				
				// Check not also overlapping jumpthru from original position, in which
				// case ignore it as a bit of background.
				if (slope_too_steep)
				{
					this.inst.x = oldx;
					this.inst.y = oldy;
					this.inst.set_bbox_changed();
					
					if (this.runtime.testOverlap(this.inst, slope_too_steep))
					{
						slope_too_steep = null;
						is_jumpthru = false;
					}
				}
			}
			*/

			// Move back and move the real amount
			this.inst.x = oldx + mx;
			this.inst.y = oldy + my;
			this.inst.set_bbox_changed();
			
			// Test for overlap to side.
			obstacle = this.runtime.testOverlapSolid(this.inst);

			if (!obstacle && floor_)
			{
				obstacle = this.runtime.testOverlapJumpThru(this.inst);
				
				// Check not also overlapping jumpthru from original position, in which
				// case ignore it as a bit of background.
				if (obstacle)
				{
					this.inst.x = oldx;
					this.inst.y = oldy;
					this.inst.set_bbox_changed();
					
					if (this.runtime.testOverlap(this.inst, obstacle))
					{
						obstacle = null;
						is_jumpthru = false;
					}
					else
						is_jumpthru = true;
						
					this.inst.x = oldx + mx;
					this.inst.y = oldy + my;
					this.inst.set_bbox_changed();
				}
			}
			
			if (obstacle)
			{
				// First try pushing out up the same distance that was moved horizontally.
				// If this works it's an acceptable slope.
				var push_dist = Math.abs(this.dx * dt) + 2;
				
				if (slope_too_steep || !this.runtime.pushOutSolid(this.inst, -this.downx, -this.downy, push_dist, is_jumpthru, obstacle))
				{
					// Failed to push up out of slope.  Must be a wall - push back horizontally.
					// Push either 2.5x the horizontal distance moved this tick, or at least 30px.
					this.runtime.registerCollision(this.inst, obstacle);
					push_dist = Math.max(Math.abs(this.dx * dt * 2.5), 30);
					
					// Push out of solid: push left if moving right, or push right if moving left
					if (!this.runtime.pushOutSolid(this.inst, this.rightx * (this.dx < 0 ? 1 : -1), this.righty * (this.dx < 0 ? 1 : -1), push_dist, false))
					{
						// Failed to push out of solid.  Restore old position.
						this.inst.x = oldx;
						this.inst.y = oldy;
						this.inst.set_bbox_changed();
					}
					else if (floor_ && !is_jumpthru && !this.floorIsJumpthru)
					{
						// Push out wall horizontally succeeded. The player might be on a slope, in which case they might
						// now be hovering in the air slightly. So push 1px in to the floor and push out again.
						oldx = this.inst.x;
						oldy = this.inst.y;
						this.inst.x += this.downx;
						this.inst.y += this.downy;
						
						if (this.runtime.testOverlapSolid(this.inst))
						{
							if (!this.runtime.pushOutSolid(this.inst, -this.downx, -this.downy, 3, false))
							{
								// Failed to push out of solid.  Restore old position.
								this.inst.x = oldx;
								this.inst.y = oldy;
								this.inst.set_bbox_changed();
							}
						}
						else
						{
							// Not over a solid. Put it back.
							this.inst.x = oldx;
							this.inst.y = oldy;
							this.inst.set_bbox_changed();
						}
					}
					
					if (!is_jumpthru)
						this.dx = 0;	// stop
				}
				else if (!slope_too_steep && !jump && (Math.abs(this.dy) < Math.abs(this.jumpStrength / 4)))
				{
					// Must have pushed up out of slope.  Set dy to 0 to handle rare edge case when
					// jumping on to a platform from the side triggers slope detection upon landing.
					this.dy = 0;
					
					// On this rare occasion, if the player was not on the floor, they may have landed without
					// ever having been falling.  This will mean 'On landed' doesn't trigger, so trigger it now.
					if (!floor_)
						landed = true;
				}
			}
			else
			{
				// Was on floor but now isn't
				var newfloor = this.isOnFloor();
				if (floor_ && !newfloor)
				{
					// Moved horizontally but not overlapping anything.  Push down
					// to keep feet on downwards slopes (to an extent).
					mag = Math.ceil(Math.abs(this.dx * dt)) + 2;
					oldx = this.inst.x;
					oldy = this.inst.y;
					this.inst.x += this.downx * mag;
					this.inst.y += this.downy * mag;
					this.inst.set_bbox_changed();
					
					if (this.runtime.testOverlapSolid(this.inst) || this.runtime.testOverlapJumpThru(this.inst))
						this.runtime.pushOutSolid(this.inst, -this.downx, -this.downy, mag + 2, true);
					else
					{
						this.inst.x = oldx;
						this.inst.y = oldy;
						this.inst.set_bbox_changed();
					}
				}
				else if (newfloor)
				{
					// Handle edge case where the player moves horizontally on to a platform at the peak of their jump.
					// This means they land on a floor through horizontal motion only. This is important for landing on
					// jump-thrus, since otherwise a residual nonzero can be left behind, causing a slow sink in to the platform (public #2321).
					// Note this should only apply to jump-thru floors and not solid floors, else it breaks slope jumps (public #2532).
					if (!floor_ && this.floorIsJumpthru)
					{
						this.lastFloorObject = newfloor;
						this.lastFloorX = newfloor.x;
						this.lastFloorY = newfloor.y;
						this.dy = 0;
						landed = true;
					}

					if (this.dy === 0)
					{
						// Push in to the floor fractionally to ensure player stays tightly on ground
						this.runtime.pushInFractional(this.inst, -this.downx, -this.downy, newfloor, 16);
					}
				}
			}
		}
		
		if (this.dy !== 0)
		{
			// Attempt Y movement
			oldx = this.inst.x;
			oldy = this.inst.y;
			// Note max speed is only clamped in the downwards (falling) direction - there's no clamp on the upward speed.
			this.inst.x += this.runtime.accelerate(this.dy, -Infinity, this.maxFall, this.g, dt) * this.downx;
			this.inst.y += this.runtime.accelerate(this.dy, -Infinity, this.maxFall, this.g, dt) * this.downy;
			var newx = this.inst.x;
			var newy = this.inst.y;
			this.inst.set_bbox_changed();
			
			collobj = this.runtime.testOverlapSolid(this.inst);
			
			var fell_on_jumpthru = false;
			
			if (!collobj && (this.dy > 0) && !floor_)
			{
				// Get all jump-thrus currently overlapping
				allover = this.fallthrough > 0 ? null : this.runtime.testOverlapJumpThru(this.inst, true);
				
				// Filter out all objects it is not overlapping in its old position
				if (allover && allover.length)
				{
					// Special case to support vertically moving jumpthrus.
					if (this.wasOverJumpthru)
					{
						this.inst.x = oldx;
						this.inst.y = oldy;
						this.inst.set_bbox_changed();
						
						for (i = 0, j = 0, len = allover.length; i < len; i++)
						{
							allover[j] = allover[i];
							
							if (!this.runtime.testOverlap(this.inst, allover[i]))
								j++;
						}
						
						allover.length = j;
							
						this.inst.x = newx;
						this.inst.y = newy;
						this.inst.set_bbox_changed();
					}
					
					if (allover.length >= 1)
						collobj = allover[0];
				}
				
				fell_on_jumpthru = !!collobj;
			}
			
			if (collobj)
			{
				this.runtime.registerCollision(this.inst, collobj);
				this.sustainTime = 0;
				
				// Push either 2.5x the vertical distance (+10px) moved this tick, or at least 30px. Don't clamp to 30px when falling on a jumpthru.
				var push_dist = (fell_on_jumpthru ? Math.abs(this.dy * dt * 2.5 + 10) : Math.max(Math.abs(this.dy * dt * 2.5 + 10), 30));
				
				// Push out of solid: push down if moving up, or push up if moving down
				if (!this.runtime.pushOutSolid(this.inst, this.downx * (this.dy < 0 ? 1 : -1), this.downy * (this.dy < 0 ? 1 : -1), push_dist, fell_on_jumpthru, collobj))
				{
					// Failed to push out of solid.  Restore old position.
					this.inst.x = oldx;
					this.inst.y = oldy;
					this.inst.set_bbox_changed();
					this.wasOnFloor = true;		// prevent adjustment for unexpected floor landings
					
					// If shearing through a jump-thru while falling, we fail the push out, but
					// want to let the player keep falling, so don't stop them.
					if (!fell_on_jumpthru)
						this.dy = 0;	// stop
				}
				else
				{
					this.lastFloorObject = collobj;
					this.lastFloorX = collobj.x;
					this.lastFloorY = collobj.y;
					this.floorIsJumpthru = fell_on_jumpthru;
					
					// Make sure 'On landed' triggers for landing on a jumpthru
					if (fell_on_jumpthru)
						landed = true;
						
					// By default always stop, unless the player is jumping and the ceiling collision mode is "preserve momentum"
					if (this.dy > 0 || this.ceilingCollisionMode === 0)
						this.dy = 0;
					
					// If hitting the ceiling with "preserve momentum" mode, push in to the ceiling fractionally for a tighter fit
					if (this.dy < 0 && this.ceilingCollisionMode === 1)
						this.runtime.pushInFractional(this.inst, this.downx, this.downy, collobj, 32);
				}
			}
		}
		
		// Run animation triggers
		
		// Has started falling?
		if (this.animMode !== ANIMMODE_FALLING && this.dy > 0 && !floor_)
		{
			this.runtime.trigger(cr.behaviors.Platform.prototype.cnds.OnFall, this.inst);
			this.animMode = ANIMMODE_FALLING;
		}
		
		// Is on floor? Note: don't handle these if 'Set vector Y' set a negative dy as a manual way to make a jump
		if ((floor_ || landed) && this.dy >= 0)
		{
			// Was falling? (i.e. has just landed) or has jumped, but jump was blocked
			if (this.animMode === ANIMMODE_FALLING || landed || (jump && this.dy === 0))
			{
				this.runtime.trigger(cr.behaviors.Platform.prototype.cnds.OnLand, this.inst);
				
				if (this.dx === 0 && this.dy === 0)
					this.animMode = ANIMMODE_STOPPED;
				else
					this.animMode = ANIMMODE_MOVING;
			}
			// Has not just landed: handle normal moving/stopped triggers
			else
			{
				if (this.animMode !== ANIMMODE_STOPPED && this.dx === 0 && this.dy === 0)
				{
					this.runtime.trigger(cr.behaviors.Platform.prototype.cnds.OnStop, this.inst);
					this.animMode = ANIMMODE_STOPPED;
				}
				
				// Has started moving and is on floor?
				if (this.animMode !== ANIMMODE_MOVING && (this.dx !== 0 || this.dy !== 0) && !jump)
				{
					this.runtime.trigger(cr.behaviors.Platform.prototype.cnds.OnMove, this.inst);
					this.animMode = ANIMMODE_MOVING;
				}
			}
		}
		
		if (this.fallthrough > 0)
			this.fallthrough--;
			
		this.wasOverJumpthru = this.runtime.testOverlapJumpThru(this.inst);
	};
	

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

	Cnds.prototype.IsMoving = function ()
	{
		return this.dx !== 0 || this.dy !== 0;
	};
	
	Cnds.prototype.CompareSpeed = function (cmp, s)
	{
		var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
		
		return cr.do_cmp(speed, cmp, s);
	};
	
	Cnds.prototype.IsOnFloor = function ()
	{
		if (this.dy !== 0)
			return false;
			
		var ret = null;
		var ret2 = null;
		var i, len, j;
		
		// Move object one pixel down
		var oldx = this.inst.x;
		var oldy = this.inst.y;
		this.inst.x += this.downx;
		this.inst.y += this.downy;
		this.inst.set_bbox_changed();
		
		ret = this.runtime.testOverlapSolid(this.inst);
		
		if (!ret && this.fallthrough === 0)
			ret2 = this.runtime.testOverlapJumpThru(this.inst, true);
		
		// Put the object back
		this.inst.x = oldx;
		this.inst.y = oldy;
		this.inst.set_bbox_changed();
		
		if (ret)		// was overlapping solid
		{
			// If the object is still overlapping the solid one pixel up, it
			// must be stuck inside something.  So don't count it as floor.
			return !this.runtime.testOverlap(this.inst, ret);
		}
		
		// Is overlapping one or more jumpthrus
		if (ret2 && ret2.length)
		{
			// Filter out jumpthrus it is still overlapping one pixel up
			for (i = 0, j = 0, len = ret2.length; i < len; i++)
			{
				ret2[j] = ret2[i];
				
				if (!this.runtime.testOverlap(this.inst, ret2[i]))
					j++;
			}
			
			// All jumpthrus it is only overlapping one pixel down are floor pieces/tiles.
			// Return first in list.
			if (j >= 1)
				return true;
		}
		
		return false;
	};
	
	Cnds.prototype.IsByWall = function (side)
	{
		// First move 2px to the side
		var ret = false;
		var oldx = this.inst.x;
		var oldy = this.inst.y;
		
		if (side === 0)		// left
		{
			this.inst.x -= this.rightx * 2;
			this.inst.y -= this.righty * 2;
		}
		else
		{
			this.inst.x += this.rightx * 2;
			this.inst.y += this.righty * 2;
		}
		
		this.inst.set_bbox_changed();
		
		// If we are not overlapping a solid to the side, we aren't by a wall.
		if (!this.runtime.testOverlapSolid(this.inst))
		{
			this.inst.x = oldx;
			this.inst.y = oldy;
			this.inst.set_bbox_changed();
			return false;
		}
		
		// There is a solid to the side: now move 3px up and test again, expecting to find a solid.
		// This is to ensure that slopes don't count as walls, since if we are clear slightly above
		// the wall, then it's a slope and the player can likely walk up it.
		this.inst.x -= this.downx * 3;
		this.inst.y -= this.downy * 3;
		
		this.inst.set_bbox_changed();
		
		ret = this.runtime.testOverlapSolid(this.inst);
		
		this.inst.x = oldx;
		this.inst.y = oldy;
		this.inst.set_bbox_changed();
		
		return ret;
	};
	
	Cnds.prototype.IsJumping = function ()
	{
		return this.dy < 0;
	};
	
	Cnds.prototype.IsFalling = function ()
	{
		return this.dy > 0;
	};
	
	Cnds.prototype.OnJump = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnFall = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnStop = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnMove = function ()
	{
		return true;
	};
	
	Cnds.prototype.OnLand = function ()
	{
		return true;
	};
	
	Cnds.prototype.IsDoubleJumpEnabled = function ()
	{
		return this.enableDoubleJump;
	};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.enabled;
	};
	
	behaviorProto.cnds = new Cnds();

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

	Acts.prototype.SetIgnoreInput = function (ignoring)
	{
		this.ignoreInput = ignoring;
	};
	
	Acts.prototype.SetMaxSpeed = function (maxspeed)
	{
		this.maxspeed = maxspeed;
		
		if (this.maxspeed < 0)
			this.maxspeed = 0;
	};
	
	Acts.prototype.SetAcceleration = function (acc)
	{
		this.acc = acc;
		
		if (this.acc < 0)
			this.acc = 0;
	};
	
	Acts.prototype.SetDeceleration = function (dec)
	{
		this.dec = dec;
		
		if (this.dec < 0)
			this.dec = 0;
	};
	
	Acts.prototype.SetJumpStrength = function (js)
	{
		this.jumpStrength = js;
		
		if (this.jumpStrength < 0)
			this.jumpStrength = 0;
	};
	
	Acts.prototype.SetGravity = function (grav)
	{
		if (this.g1 === grav)
			return;		// no change
		
		this.g = grav;
		this.updateGravity();
		
		// Push up to 10px out any current solid to prevent glitches
		if (this.runtime.testOverlapSolid(this.inst))
		{
			this.runtime.pushOutSolid(this.inst, this.downx, this.downy, 10);
			
			// Bodge to workaround 1px float causing pushOutSolidNearest
			this.inst.x += this.downx * 2;
			this.inst.y += this.downy * 2;
			this.inst.set_bbox_changed();
		}
		
		// Allow to fall off current floor in case direction of gravity changed
		this.lastFloorObject = null;
	};
	
	Acts.prototype.SetMaxFallSpeed = function (mfs)
	{
		this.maxFall = mfs;
		
		if (this.maxFall < 0)
			this.maxFall = 0;
	};
	
	Acts.prototype.SimulateControl = function (ctrl)
	{
		// 0=left, 1=right, 2=jump
		switch (ctrl) {
		case 0:		this.simleft = true;	break;
		case 1:		this.simright = true;	break;
		case 2:		this.simjump = true;	break;
		}
	};
	
	Acts.prototype.SetVectorX = function (vx)
	{
		this.dx = vx;
	};
	
	Acts.prototype.SetVectorY = function (vy)
	{
		this.dy = vy;
	};
	
	Acts.prototype.SetGravityAngle = function (a)
	{
		a = cr.to_radians(a);
		a = cr.clamp_angle(a);
		
		if (this.ga === a)
			return;		// no change
			
		this.ga = a;
		this.updateGravity();
		
		// Allow to fall off current floor in case direction of gravity changed
		this.lastFloorObject = null;
	};
	
	Acts.prototype.SetEnabled = function (en)
	{
		if (this.enabled !== (en === 1))
		{
			this.enabled = (en === 1);
			
			// when disabling, drop the last floor object, otherwise resets to the moving platform when enabled again
			if (!this.enabled)
				this.lastFloorObject = null;
		}
	};
	
	Acts.prototype.FallThrough = function ()
	{
		// Test is standing on jumpthru 1px down
		var oldx = this.inst.x;
		var oldy = this.inst.y;
		this.inst.x += this.downx;
		this.inst.y += this.downy;
		this.inst.set_bbox_changed();
		
		var overlaps = this.runtime.testOverlapJumpThru(this.inst, false);
		
		this.inst.x = oldx;
		this.inst.y = oldy;
		this.inst.set_bbox_changed();
		
		if (!overlaps)
			return;
			
		this.fallthrough = 3;			// disable jumpthrus for 3 ticks (1 doesn't do it, 2 does, 3 to be on safe side)
		this.lastFloorObject = null;
	};
	
	Acts.prototype.SetDoubleJumpEnabled = function (e)
	{
		this.enableDoubleJump = (e !== 0);
	};
	
	Acts.prototype.SetJumpSustain = function (s)
	{
		this.jumpSustain = s / 1000;		// convert to ms
	};
	
	Acts.prototype.SetCeilingCollision = function (m)
	{
		this.ceilingCollisionMode = m;
	};
	
	behaviorProto.acts = new Acts();

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

	Exps.prototype.Speed = function (ret)
	{
		ret.set_float(Math.sqrt(this.dx * this.dx + this.dy * this.dy));
	};
	
	Exps.prototype.MaxSpeed = function (ret)
	{
		ret.set_float(this.maxspeed);
	};
	
	Exps.prototype.Acceleration = function (ret)
	{
		ret.set_float(this.acc);
	};
	
	Exps.prototype.Deceleration = function (ret)
	{
		ret.set_float(this.dec);
	};
	
	Exps.prototype.JumpStrength = function (ret)
	{
		ret.set_float(this.jumpStrength);
	};
	
	Exps.prototype.Gravity = function (ret)
	{
		ret.set_float(this.g);
	};
	
	Exps.prototype.GravityAngle = function (ret)
	{
		ret.set_float(cr.to_degrees(this.ga));
	};
	
	Exps.prototype.MaxFallSpeed = function (ret)
	{
		ret.set_float(this.maxFall);
	};
	
	Exps.prototype.MovingAngle = function (ret)
	{
		ret.set_float(cr.to_degrees(Math.atan2(this.dy, this.dx)));
	};
	
	Exps.prototype.VectorX = function (ret)
	{
		ret.set_float(this.dx);
	};
	
	Exps.prototype.VectorY = function (ret)
	{
		ret.set_float(this.dy);
	};
	
	Exps.prototype.JumpSustain = function (ret)
	{
		ret.set_float(this.jumpSustain * 1000);		// convert back to ms
	};
	
	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 <= _pi_2)
				return x / _pi_2;
			else if (x <= _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 < 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 < 0)
				this.inst.opacity = 0;
			else if (this.inst.opacity > 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 && 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();
	
}());

// 8 Direction
// ECMAScript 5 strict mode

;
;

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

(function ()
{
	var behaviorProto = cr.behaviors.EightDir.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;
		
		// Key states
		this.upkey = false;
		this.downkey = false;
		this.leftkey = false;
		this.rightkey = false;
		this.ignoreInput = false;
		
		// Simulated key states
		this.simup = false;
		this.simdown = false;
		this.simleft = false;
		this.simright = false;
		
		// attempted workaround for sticky keys bug
		this.lastuptick = -1;
		this.lastdowntick = -1;
		this.lastlefttick = -1;
		this.lastrighttick = -1;
		
		// Movement
		this.dx = 0;
		this.dy = 0;
	};
	
	var behinstProto = behaviorProto.Instance.prototype;

	behinstProto.onCreate = function()
	{
		// Load properties
		this.maxspeed = this.properties[0];
		this.acc = this.properties[1];
		this.dec = this.properties[2];
		this.directions = this.properties[3];	// 0=Up & down, 1=Left & right, 2=4 directions, 3=8 directions"
		this.angleMode = this.properties[4];	// 0=No,1=90-degree intervals, 2=45-degree intervals, 3=360 degree (smooth)
		this.defaultControls = this.properties[5];
		
		this.enabled = this.properties[6];
		
		var self = this;
		
		// Only bind keyboard events if default controls are in use
		if (this.defaultControls)
		{
			document.addEventListener("keydown", function (info)
			{
				self.onKeyDown(info);
			});
			
			document.addEventListener("keyup", function (info)
			{
				self.onKeyUp(info);
			});
		}
	};
	
	behinstProto.saveToJSON = function ()
	{
		return {
			"dx": this.dx,
			"dy": this.dy,
			"enabled": this.enabled,
			"maxspeed": this.maxspeed,
			"acc": this.acc,
			"dec": this.dec,
			"ignoreInput": this.ignoreInput
		};
	};
	
	behinstProto.loadFromJSON = function (o)
	{
		this.dx = o["dx"];
		this.dy = o["dy"];
		this.enabled = o["enabled"];
		this.maxspeed = o["maxspeed"];
		this.acc = o["acc"];
		this.dec = o["dec"];
		this.ignoreInput = o["ignoreInput"];
		
		this.upkey = false;
		this.downkey = false;
		this.leftkey = false;
		this.rightkey = false;
		
		this.simup = false;
		this.simdown = false;
		this.simleft = false;
		this.simright = false;
		
		this.lastuptick = -1;
		this.lastdowntick = -1;
		this.lastlefttick = -1;
		this.lastrighttick = -1;
	};

	behinstProto.onKeyDown = function (info)
	{	
		var tickcount = this.runtime.tickcount;
		
		switch (info.which) {
		case 37:	// left
			info.preventDefault();
			
			// Workaround for sticky keys bug: reject if arriving on same tick as onKeyUp event
			if (this.lastlefttick < tickcount)
				this.leftkey = true;
			
			break;
		case 38:	// up
			info.preventDefault();
			
			if (this.lastuptick < tickcount)
				this.upkey = true;
				
			break;
		case 39:	// right
			info.preventDefault();
			
			if (this.lastrighttick < tickcount)
				this.rightkey = true;
				
			break;
		case 40:	// down
			info.preventDefault();
			
			if (this.lastdowntick < tickcount)
				this.downkey = true;
			
			break;
		}
	};

	behinstProto.onKeyUp = function (info)
	{
		var tickcount = this.runtime.tickcount;
		
		switch (info.which) {
		case 37:	// left
			info.preventDefault();
			this.leftkey = false;
			this.lastlefttick = tickcount;
			break;
		case 38:	// up
			info.preventDefault();
			this.upkey = false;
			this.lastuptick = tickcount;
			break;
		case 39:	// right
			info.preventDefault();
			this.rightkey = false;
			this.lastrighttick = tickcount;
			break;
		case 40:	// down
			info.preventDefault();
			this.downkey = false;
			this.lastdowntick = tickcount;
			break;
		}
	};
	
	behinstProto.onWindowBlur = function ()
	{
		this.upkey = false;
		this.downkey = false;
		this.leftkey = false;
		this.rightkey = false;
	};

	behinstProto.tick = function ()
	{
		var dt = this.runtime.getDt(this.inst);
		
		var left = this.leftkey || this.simleft;
		var right = this.rightkey || this.simright;
		var up = this.upkey || this.simup;
		var down = this.downkey || this.simdown;
		this.simleft = false;
		this.simright = false;
		this.simup = false;
		this.simdown = false;
		
		if (!this.enabled)
			return;
		
		// Is already overlapping solid: must have moved itself in (e.g. by rotating or being crushed),
		// so push out
		var collobj = this.runtime.testOverlapSolid(this.inst);
		if (collobj)
		{
			this.runtime.registerCollision(this.inst, collobj);
			if (!this.runtime.pushOutSolidNearest(this.inst))
				return;		// must be stuck in solid
		}
		
		// Ignoring input: ignore all keys
		if (this.ignoreInput)
		{
			left = false;
			right = false;
			up = false;
			down = false;
		}
		
		// Up & down mode: ignore left & right keys
		if (this.directions === 0)
		{
			left = false;
			right = false;
		}
		// Left & right mode: ignore up & down keys
		else if (this.directions === 1)
		{
			up = false;
			down = false;
		}
		
		// 4 directions mode: up/down take priority over left/right
		if (this.directions === 2 && (up || down))
		{
			left = false;
			right = false;
		}
		
		// Apply deceleration when no arrow key pressed, for each axis
		if (left == right)	// both up or both down
		{
			if (this.dx < 0)
			{
				this.dx += this.dec * dt;
				
				if (this.dx > 0)
					this.dx = 0;
			}
			else if (this.dx > 0)
			{
				this.dx -= this.dec * dt;
				
				if (this.dx < 0)
					this.dx = 0;
			}
		}
		
		if (up == down)
		{
			if (this.dy < 0)
			{
				this.dy += this.dec * dt;
				
				if (this.dy > 0)
					this.dy = 0;
			}
			else if (this.dy > 0)
			{
				this.dy -= this.dec * dt;
				
				if (this.dy < 0)
					this.dy = 0;
			}
		}
		
		// Apply acceleration
		var xacc = 0;
		var yacc = 0;
		
		if (left && !right)
		{
			// Moving in opposite direction to current motion: add deceleration
			if (this.dx > 0)
				xacc = -(this.acc + this.dec);
			else
				xacc = -this.acc;
		}
		
		if (right && !left)
		{
			if (this.dx < 0)
				xacc = this.acc + this.dec;
			else
				xacc = this.acc;
		}
		
		if (up && !down)
		{
			if (this.dy > 0)
				yacc = -(this.acc + this.dec);
			else
				yacc = -this.acc;
		}
		
		if (down && !up)
		{
			if (this.dy < 0)
				yacc = this.acc + this.dec;
			else
				yacc = this.acc;
		}
		
		this.dx += xacc * dt;
		this.dy += yacc * dt;
		
		var ax, ay;
		
		if (this.dx !== 0 || this.dy !== 0)
		{
			// Limit to max speed
			var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
			
			// Limit both the movement vector and the X and Y speeds according to the maximum speed at the current angle
			var a = Math.atan2(this.dy, this.dx);
			var maxSpeedX = this.maxspeed * Math.cos(a);
			var maxSpeedY = this.maxspeed * Math.sin(a);
			
			if (speed > this.maxspeed)
			{
				this.dx = maxSpeedX;
				this.dy = maxSpeedY;
			}
			
			// For clamping when calling accelerate(), take the absolute value of the max speed on each axis
			maxSpeedX = Math.abs(maxSpeedX);
			maxSpeedY = Math.abs(maxSpeedY);
			
			// Save old position and angle
			var oldx = this.inst.x;
			var oldy = this.inst.y;
			var oldangle = this.inst.angle;
			
			// Attempt X movement
			this.inst.x += this.runtime.accelerate(this.dx, -maxSpeedX, maxSpeedX, xacc, dt);
			this.inst.set_bbox_changed();
			
			collobj = this.runtime.testOverlapSolid(this.inst);
			if (collobj)
			{
				// Try to push back out horizontally for a closer fit to the obstacle.
				if (!this.runtime.pushOutSolid(this.inst, (this.dx < 0 ? 1 : -1), 0, Math.abs(Math.floor(this.dx * dt))))
				{
					// Failed to push out: restore previous (known safe) position.
					this.inst.x = oldx;
					this.inst.set_bbox_changed();
				}
				
				this.dx = 0;
				this.runtime.registerCollision(this.inst, collobj);
			}
			
			this.inst.y += this.runtime.accelerate(this.dy, -maxSpeedY, maxSpeedY, yacc, dt);
			this.inst.set_bbox_changed();
			
			collobj = this.runtime.testOverlapSolid(this.inst);
			if (collobj)
			{
				// Try to push back out vertically.
				if (!this.runtime.pushOutSolid(this.inst, 0, (this.dy < 0 ? 1 : -1), Math.abs(Math.floor(this.dy * dt))))
				{
					// Failed to push out
					this.inst.y = oldy;
					this.inst.set_bbox_changed();
				}
				
				this.dy = 0;
				this.runtime.registerCollision(this.inst, collobj);
			}
			
			ax = cr.round6dp(this.dx);
			ay = cr.round6dp(this.dy);
			
			// Apply angle so long as object is still moving and isn't entirely blocked by a solid
			if ((ax !== 0 || ay !== 0) && this.inst.type.plugin.is_rotatable)
			{
				if (this.angleMode === 1)	// 90 degree intervals
					this.inst.angle = cr.to_clamped_radians(Math.round(cr.to_degrees(Math.atan2(ay, ax)) / 90.0) * 90.0);
				else if (this.angleMode === 2)	// 45 degree intervals
					this.inst.angle = cr.to_clamped_radians(Math.round(cr.to_degrees(Math.atan2(ay, ax)) / 45.0) * 45.0);
				else if (this.angleMode === 3)	// 360 degree
					this.inst.angle = Math.atan2(ay, ax);
			}
				
			this.inst.set_bbox_changed();
			
			if (this.inst.angle != oldangle)
			{
				collobj = this.runtime.testOverlapSolid(this.inst);
				if (collobj)
				{
					this.inst.angle = oldangle;
					this.inst.set_bbox_changed();
					this.runtime.registerCollision(this.inst, collobj);
				}
			}
		}
	};
	

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

	Cnds.prototype.IsMoving = function ()
	{
		// In some cases floating point precision leaves the behavior with a tiny speed like 2.2e-16.
		// To avoid this rounding error making "Is moving" true, use a small minimum speed instead.
		var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
		return speed > 1e-10;
	};
	
	Cnds.prototype.CompareSpeed = function (cmp, s)
	{
		var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
		
		return cr.do_cmp(speed, cmp, s);
	};
	
	Cnds.prototype.IsEnabled = function ()
	{
		return this.enabled;
	};
	
	behaviorProto.cnds = new Cnds();

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

	Acts.prototype.Stop = function ()
	{
		this.dx = 0;
		this.dy = 0;
	};
	
	Acts.prototype.Reverse = function ()
	{
		this.dx *= -1;
		this.dy *= -1;
	};
	
	Acts.prototype.SetIgnoreInput = function (ignoring)
	{
		this.ignoreInput = ignoring;
	};
	
	Acts.prototype.SetSpeed = function (speed)
	{
		if (speed < 0)
			speed = 0;
		if (speed > this.maxspeed)
			speed = this.maxspeed;
			
		// Speed is new magnitude of vector of motion
		var a = Math.atan2(this.dy, this.dx);
		this.dx = speed * Math.cos(a);
		this.dy = speed * Math.sin(a);
	};
	
	Acts.prototype.SetMaxSpeed = function (maxspeed)
	{
		this.maxspeed = maxspeed;
		
		if (this.maxspeed < 0)
			this.maxspeed = 0;
	};
	
	Acts.prototype.SetAcceleration = function (acc)
	{
		this.acc = acc;
		
		if (this.acc < 0)
			this.acc = 0;
	};
	
	Acts.prototype.SetDeceleration = function (dec)
	{
		this.dec = dec;
		
		if (this.dec < 0)
			this.dec = 0;
	};
	
	Acts.prototype.SimulateControl = function (ctrl)
	{
		// 0=left, 1=right, 2=up, 3=down
		switch (ctrl) {
		case 0:		this.simleft = true;	break;
		case 1:		this.simright = true;	break;
		case 2:		this.simup = true;		break;
		case 3:		this.simdown = true;	break;
		}
	};
	
	Acts.prototype.SetEnabled = function (en)
	{
		this.enabled = (en === 1);
	};
	
	Acts.prototype.SetVectorX = function (x_)
	{
		this.dx = x_;
	};
	
	Acts.prototype.SetVectorY = function (y_)
	{
		this.dy = y_;
	};
	
	behaviorProto.acts = new Acts();

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

	Exps.prototype.Speed = function (ret)
	{
		ret.set_float(Math.sqrt(this.dx * this.dx + this.dy * this.dy));
	};
	
	Exps.prototype.MaxSpeed = function (ret)
	{
		ret.set_float(this.maxspeed);
	};
	
	Exps.prototype.Acceleration = function (ret)
	{
		ret.set_float(this.acc);
	};
	
	Exps.prototype.Deceleration = function (ret)
	{
		ret.set_float(this.dec);
	};
	
	Exps.prototype.MovingAngle = function (ret)
	{
		ret.set_float(cr.to_degrees(Math.atan2(this.dy, this.dx)));
	};
	
	Exps.prototype.VectorX = function (ret)
	{
		ret.set_float(this.dx);
	};
	
	Exps.prototype.VectorY = function (ret)
	{
		ret.set_float(this.dy);
	};
	
	behaviorProto.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 && 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 < 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 && (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 <= 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 <= 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();
	
}());

cr.getObjectRefTable = function () {
	return [
		cr.plugins_.Tilemap,
		cr.plugins_.Sprite,
		cr.plugins_.TiledBg,
		cr.behaviors.solid,
		cr.behaviors.Anchor,
		cr.plugins_.NinePatch,
		cr.behaviors.Pin,
		cr.behaviors.Fade,
		cr.behaviors.lunarray_LiteTween,
		cr.behaviors.scrollto,
		cr.behaviors.Platform,
		cr.behaviors.Sin,
		cr.behaviors.EightDir,
		cr.plugins_.Particles,
		cr.behaviors.Bullet,
		cr.plugins_.Keyboard,
		cr.plugins_.Spritefont2,
		cr.plugins_.Touch,
		cr.plugins_.Arr,
		cr.plugins_.WebStorage,
		cr.plugins_.Audio,
		cr.plugins_.Function,
		cr.plugins_.AJAX,
		cr.plugins_.Dictionary,
		cr.plugins_.LocalStorage,
		cr.plugins_.Browser,
		cr.plugins_.gamepad,
		cr.plugins_.Keyboard.prototype.cnds.OnAnyKey,
		cr.plugins_.Touch.prototype.cnds.OnTouchStart,
		cr.plugins_.gamepad.prototype.cnds.OnAnyButtonDown,
		cr.system_object.prototype.cnds.IsGroupActive,
		cr.system_object.prototype.cnds.CompareVar,
		cr.system_object.prototype.acts.SetGroupActive,
		cr.behaviors.Fade.prototype.acts.RestartFade,
		cr.system_object.prototype.cnds.LayerCmpOpacity,
		cr.system_object.prototype.cnds.EveryTick,
		cr.system_object.prototype.acts.SetLayerOpacity,
		cr.system_object.prototype.exps.layeropacity,
		cr.system_object.prototype.acts.SetLayerVisible,
		cr.plugins_.Sprite.prototype.cnds.OnCreated,
		cr.plugins_.WebStorage.prototype.cnds.LocalStorageExists,
		cr.plugins_.Sprite.prototype.acts.SetAnimFrame,
		cr.system_object.prototype.exps.int,
		cr.system_object.prototype.exps.left,
		cr.plugins_.WebStorage.prototype.exps.LocalValue,
		cr.plugins_.LocalStorage.prototype.cnds.OnItemMissing,
		cr.plugins_.LocalStorage.prototype.acts.SetItem,
		cr.system_object.prototype.acts.Wait,
		cr.plugins_.LocalStorage.prototype.cnds.OnItemExists,
		cr.system_object.prototype.cnds.OnLayoutStart,
		cr.plugins_.Audio.prototype.cnds.IsTagPlaying,
		cr.plugins_.Audio.prototype.acts.PlayByName,
		cr.system_object.prototype.cnds.CompareBetween,
		cr.plugins_.LocalStorage.prototype.acts.CheckItemExists,
		cr.system_object.prototype.acts.SetVar,
		cr.plugins_.Sprite.prototype.exps.Count,
		cr.behaviors.Platform.prototype.acts.SetGravity,
		cr.system_object.prototype.acts.CreateObject,
		cr.plugins_.Sprite.prototype.acts.SetVisible,
		cr.plugins_.Spritefont2.prototype.cnds.CompareInstanceVar,
		cr.plugins_.Spritefont2.prototype.acts.SetText,
		cr.plugins_.Dictionary.prototype.exps.Get,
		cr.behaviors.Platform.prototype.acts.SetEnabled,
		cr.plugins_.Sprite.prototype.acts.SetSize,
		cr.system_object.prototype.exps.viewportright,
		cr.system_object.prototype.exps.viewportbottom,
		cr.plugins_.Sprite.prototype.acts.SetPos,
		cr.plugins_.Sprite.prototype.exps.X,
		cr.plugins_.Sprite.prototype.exps.Y,
		cr.behaviors.Platform.prototype.acts.SimulateControl,
		cr.system_object.prototype.cnds.TriggerOnce,
		cr.plugins_.Sprite.prototype.acts.SetMirrored,
		cr.plugins_.Touch.prototype.cnds.OnTouchObject,
		cr.plugins_.Keyboard.prototype.cnds.OnKey,
		cr.plugins_.gamepad.prototype.cnds.OnButtonDown,
		cr.plugins_.Sprite.prototype.cnds.IsMirrored,
		cr.system_object.prototype.cnds.Else,
		cr.plugins_.Sprite.prototype.cnds.IsOverlapping,
		cr.behaviors.Platform.prototype.acts.SetVectorY,
		cr.plugins_.Sprite.prototype.cnds.CompareInstanceVar,
		cr.plugins_.Sprite.prototype.acts.SetAnim,
		cr.plugins_.Sprite.prototype.acts.SetCollisions,
		cr.behaviors.Platform.prototype.acts.SetJumpStrength,
		cr.behaviors.Platform.prototype.cnds.IsOnFloor,
		cr.plugins_.Touch.prototype.cnds.OnTouchEnd,
		cr.plugins_.Sprite.prototype.acts.SetOpacity,
		cr.plugins_.Sprite.prototype.cnds.OnCollision,
		cr.plugins_.Touch.prototype.cnds.IsTouchingObject,
		cr.plugins_.gamepad.prototype.cnds.IsButtonDown,
		cr.plugins_.Keyboard.prototype.cnds.OnKeyReleased,
		cr.plugins_.gamepad.prototype.cnds.OnButtonUp,
		cr.plugins_.Keyboard.prototype.cnds.IsKeyDown,
		cr.behaviors.Platform.prototype.cnds.OnJump,
		cr.plugins_.Particles.prototype.acts.SetSpraying,
		cr.plugins_.Audio.prototype.acts.Stop,
		cr.behaviors.Platform.prototype.cnds.OnFall,
		cr.behaviors.Platform.prototype.cnds.IsMoving,
		cr.plugins_.Particles.prototype.acts.SetPos,
		cr.behaviors.Platform.prototype.cnds.OnLand,
		cr.plugins_.Sprite.prototype.acts.SetWidth,
		cr.plugins_.Sprite.prototype.acts.SetHeight,
		cr.system_object.prototype.acts.Scroll,
		cr.system_object.prototype.exps.lerp,
		cr.system_object.prototype.exps.scrollx,
		cr.system_object.prototype.exps.clamp,
		cr.system_object.prototype.exps.scrolly,
		cr.behaviors.Pin.prototype.acts.Pin,
		cr.behaviors.Platform.prototype.acts.SetVectorX,
		cr.system_object.prototype.cnds.Every,
		cr.plugins_.Sprite.prototype.cnds.IsOverlappingOffset,
		cr.behaviors.Platform.prototype.cnds.IsByWall,
		cr.behaviors.Platform.prototype.cnds.IsFalling,
		cr.plugins_.Sprite.prototype.cnds.OnAnimFinished,
		cr.plugins_.Sprite.prototype.exps.AnimationFrame,
		cr.plugins_.Sprite.prototype.acts.Spawn,
		cr.plugins_.Sprite.prototype.acts.Destroy,
		cr.system_object.prototype.acts.AddVar,
		cr.system_object.prototype.cnds.Compare,
		cr.plugins_.Sprite.prototype.acts.StartAnim,
		cr.plugins_.Sprite.prototype.acts.SetScale,
		cr.behaviors.Bullet.prototype.acts.SetEnabled,
		cr.behaviors.Bullet.prototype.acts.SetSpeed,
		cr.behaviors.Bullet.prototype.acts.SetAngleOfMotion,
		cr.system_object.prototype.exps.random,
		cr.behaviors.Bullet.prototype.acts.SetGravity,
		cr.plugins_.Sprite.prototype.cnds.OnAnyAnimFinished,
		cr.system_object.prototype.exps.viewportleft,
		cr.system_object.prototype.exps.viewporttop,
		cr.plugins_.Browser.prototype.acts.ExecJs,
		cr.system_object.prototype.exps.float,
		cr.plugins_.Spritefont2.prototype.exps.Text,
		cr.plugins_.Keyboard.prototype.cnds.OnKeyCode,
		cr.plugins_.Sprite.prototype.cnds.CompareFrame,
		cr.plugins_.LocalStorage.prototype.acts.RemoveItem,
		cr.system_object.prototype.acts.GoToLayoutByName,
		cr.system_object.prototype.acts.RestartLayout,
		cr.plugins_.Browser.prototype.acts.GoToURLWindow,
		cr.plugins_.Sprite.prototype.exps.Width,
		cr.plugins_.Sprite.prototype.exps.Height,
		cr.plugins_.Sprite.prototype.cnds.CompareWidth,
		cr.plugins_.LocalStorage.prototype.cnds.OnItemRemoved,
		cr.plugins_.Spritefont2.prototype.cnds.OnCreated,
		cr.plugins_.Arr.prototype.acts.JSONLoad,
		cr.plugins_.Spritefont2.prototype.acts.SetCharacterWidth,
		cr.system_object.prototype.cnds.For,
		cr.plugins_.Arr.prototype.exps.Height,
		cr.plugins_.Arr.prototype.exps.At,
		cr.system_object.prototype.exps.loopindex,
		cr.system_object.prototype.cnds.OnLayoutEnd,
		cr.system_object.prototype.acts.SetTimescale,
		cr.system_object.prototype.exps.timescale,
		cr.system_object.prototype.cnds.LayerVisible,
		cr.plugins_.Sprite.prototype.cnds.IsAnimPlaying,
		cr.system_object.prototype.exps.floor,
		cr.system_object.prototype.cnds.IsMobile,
		cr.plugins_.Spritefont2.prototype.acts.SetPos,
		cr.plugins_.Sprite.prototype.cnds.IsOnLayer,
		cr.plugins_.Sprite.prototype.acts.StopAnim,
		cr.plugins_.Sprite.prototype.acts.SetInstanceVar,
		cr.plugins_.Sprite.prototype.acts.SetPosToObject,
		cr.plugins_.Sprite.prototype.acts.SetY,
		cr.system_object.prototype.exps.dt,
		cr.plugins_.Sprite.prototype.acts.SetBoolInstanceVar,
		cr.plugins_.Sprite.prototype.cnds.IsBoolInstanceVarSet,
		cr.system_object.prototype.cnds.ForEach,
		cr.behaviors.EightDir.prototype.acts.SimulateControl,
		cr.plugins_.Sprite.prototype.acts.ToggleBoolInstanceVar,
		cr.system_object.prototype.acts.GoToLayout,
		cr.plugins_.Touch.prototype.cnds.OnTapGestureObject,
		cr.plugins_.Browser.prototype.cnds.IsFullscreen,
		cr.plugins_.Browser.prototype.acts.CancelFullScreen,
		cr.plugins_.Browser.prototype.acts.RequestFullScreen,
		cr.plugins_.Audio.prototype.acts.SetSilent,
		cr.plugins_.WebStorage.prototype.acts.StoreLocal,
		cr.plugins_.WebStorage.prototype.acts.RemoveLocal,
		cr.plugins_.Audio.prototype.acts.SetMuted,
		cr.plugins_.Function.prototype.acts.CallFunction,
		cr.plugins_.Function.prototype.cnds.OnFunction,
		cr.plugins_.WebStorage.prototype.cnds.CompareKeyText,
		cr.plugins_.AJAX.prototype.acts.RequestFile,
		cr.plugins_.AJAX.prototype.cnds.OnComplete,
		cr.plugins_.Dictionary.prototype.acts.JSONLoad,
		cr.system_object.prototype.exps.str,
		cr.plugins_.AJAX.prototype.exps.LastData,
		cr.behaviors.scrollto.prototype.acts.SetEnabled,
		cr.plugins_.LocalStorage.prototype.acts.GetItem,
		cr.plugins_.LocalStorage.prototype.cnds.OnItemGet,
		cr.plugins_.LocalStorage.prototype.exps.ItemValue,
		cr.plugins_.Sprite.prototype.acts.SetX,
		cr.plugins_.Sprite.prototype.cnds.PickTopBottom,
		cr.plugins_.Sprite.prototype.cnds.CompareX,
		cr.system_object.prototype.exps.cosp,
		cr.plugins_.Sprite.prototype.exps.Opacity,
		cr.plugins_.Sprite.prototype.acts.MoveToTop,
		cr.plugins_.Sprite.prototype.exps.LayerName,
		cr.plugins_.Spritefont2.prototype.acts.SetOpacity,
		cr.plugins_.Spritefont2.prototype.exps.X,
		cr.plugins_.Spritefont2.prototype.exps.Y,
		cr.plugins_.Spritefont2.prototype.acts.SetScale,
		cr.system_object.prototype.exps.round,
		cr.system_object.prototype.exps.loadingprogress,
		cr.plugins_.NinePatch.prototype.acts.SetWidth,
		cr.plugins_.NinePatch.prototype.acts.SetVisible,
		cr.behaviors.lunarray_LiteTween.prototype.acts.SetParameter,
		cr.behaviors.lunarray_LiteTween.prototype.acts.SetValue,
		cr.behaviors.lunarray_LiteTween.prototype.acts.SetTarget,
		cr.behaviors.lunarray_LiteTween.prototype.acts.Start,
		cr.plugins_.Sprite.prototype.cnds.IsVisible,
		cr.behaviors.lunarray_LiteTween.prototype.exps.Value
	];
};
	self.C3_JsPropNameTable = [
		{BackTilemap1: 0},
		{cloud1: 0},
		{Bush1: 0},
		{Tree1_1: 0},
		{Tree2_1: 0},
		{sky_tiled1: 0},
		{Solid: 0},
		{Tilemap2_1: 0},
		{Tilemap3_1: 0},
		{AlertBackg: 0},
		{BlackLine: 0},
		{DecoBottonforRight: 0},
		{ExitToMenu2_BT: 0},
		{ExitToMenu_BT: 0},
		{Facebook_BT: 0},
		{GPlus_BT: 0},
		{Anchor: 0},
		{GUI_Plash: 0},
		{GUI_Stars: 0},
		{LoadLine: 0},
		{PauseBT: 0},
		{PlayNext_BT: 0},
		{GameOverBackg: 0},
		{Replay_BT: 0},
		{GameOverPicture: 0},
		{playBT: 0},
		{Replay2_BT: 0},
		{Star: 0},
		{Twitter_BT: 0},
		{WinLevelStar: 0},
		{YouWin: 0},
		{DecoBottonforLeft: 0},
		{DecoBottonforStop: 0},
		{DecoBottonforFlip: 0},
		{DecoHighJumpBT: 0},
		{DecoLeftRunBT: 0},
		{DecoRightRunBT: 0},
		{DecoSimpleJumpBT: 0},
		{Pin: 0},
		{HighJumpBT: 0},
		{LeftRunBT: 0},
		{RightRunBT: 0},
		{SimpleJumpBT: 0},
		{"9patch": 0},
		{BackArrowBT: 0},
		{CommerceBT: 0},
		{CreditsBT: 0},
		{CloseBT: 0},
		{GPlusBT: 0},
		{LanguageBotton: 0},
		{leaderboardBT: 0},
		{LeftArrowBT: 0},
		{LinkToSite: 0},
		{MusicBotton: 0},
		{LevelBT_1: 0},
		{LogoStartScreen: 0},
		{SoundBotton: 0},
		{Settings_BT: 0},
		{RightArrowBT: 0},
		{LevelsPack: 0},
		{Star_For_Pack: 0},
		{StartScreenBack: 0},
		{StartScreenPlay_Button: 0},
		{UnlockPackBT: 0},
		{LevelBT_2: 0},
		{LevelBT_3: 0},
		{LevelBT_4: 0},
		{UpArrowBT: 0},
		{GPlusBT_Frame: 0},
		{GPlusBT_Mask: 0},
		{achievmentsBT: 0},
		{MenuBack: 0},
		{FullScreenBT: 0},
		{lineLoadBack: 0},
		{Fade: 0},
		{LoaderPick: 0},
		{LogoCompany: 0},
		{LoaderBackground: 0},
		{LiteTween: 0},
		{LoaderBT: 0},
		{border_bottom: 0},
		{border_left: 0},
		{border_right: 0},
		{border_top: 0},
		{ScrollTo: 0},
		{camera: 0},
		{DestroSprite_Floor: 0},
		{Platform: 0},
		{Rabbit: 0},
		{LeftJumpBT: 0},
		{StarterBack: 0},
		{RightJumpBT: 0},
		{Sprite10: 0},
		{RoundMask: 0},
		{RoundMask2: 0},
		{Thorns: 0},
		{PortalExit: 0},
		{Tilemap4: 0},
		{DestroSprite1: 0},
		{Booble: 0},
		{Lock: 0},
		{Keys: 0},
		{Sine: 0},
		{MovingCloud: 0},
		{Trap: 0},
		{"8Direction": 0},
		{Hedgehog: 0},
		{HedgehogMarker: 0},
		{FallStoune: 0},
		{FallStoneFloorBig: 0},
		{FallStoneMarkerBig: 0},
		{FallStouneSmall: 0},
		{FallStoneMarkerSmall: 0},
		{FallStoneFloorSmall: 0},
		{StopBT: 0},
		{FlipBT: 0},
		{carrot: 0},
		{GetEffect: 0},
		{RabbitLight: 0},
		{Shadow: 0},
		{ParticlesDust: 0},
		{GetVegetableEffect: 0},
		{Bullet: 0},
		{VegetableTail: 0},
		{BackTilemap2: 0},
		{cloud2: 0},
		{cloud3: 0},
		{sky_tiled2: 0},
		{Tilemap3_2: 0},
		{Tree1_2: 0},
		{Tree1_3: 0},
		{Tree1_4: 0},
		{Tilemap2_2: 0},
		{Sprite3: 0},
		{BackTilemap3: 0},
		{sky_tiled3: 0},
		{Sprite4: 0},
		{Sprite5: 0},
		{Tilemap3_3: 0},
		{Tilemap2_3: 0},
		{Keyboard: 0},
		{Font: 0},
		{Touch: 0},
		{Array: 0},
		{WebStorage: 0},
		{Audio: 0},
		{Function: 0},
		{AJAX: 0},
		{TextBase: 0},
		{LocalStorage: 0},
		{Browser: 0},
		{Helper: 0},
		{Gamepad: 0},
		{LevelBT: 0},
		{Tilemaps: 0},
		{Obstacles: 0},
		{CONTROLTYPE: 0},
		{GRAVITY: 0},
		{JUMP_LOW: 0},
		{JUMP_HIGH: 0},
		{Language: 0},
		{LevelNum: 0},
		{Timer: 0},
		{Stars: 0},
		{ItemsCount: 0},
		{Score: 0},
		{Running: 0},
		{BouncePlace: 0},
		{Movepower: 0},
		{Seconds: 0},
		{Miliseconds: 0},
		{ScaleLock: 0},
		{GP_LogIn: 0},
		{JUMPSOUND: 0},
		{ClickSound: 0},
		{Coordinates: 0},
		{NowPack: 0},
		{StarBonus: 0},
		{TimeBonus: 0},
		{FinalScore: 0},
		{ShowNumber: 0},
		{NewBest: 0},
		{NewLevelWin: 0}
	];

