Downloads containing main.js

Downloads
Name Author Game Mode Rating
WebJCS 1.3.3Featured Download djazz Utility 10 Download file

File preview

"use strict";

window.requestAnimFrame = (function(){
	return window.requestAnimationFrame       ||
	       window.webkitRequestAnimationFrame ||
	       window.mozRequestAnimationFrame    ||
	       window.oRequestAnimationFrame      ||
	       window.msRequestAnimationFrame     ||
	       function(/* function */ callback, /* DOMElement */ element) {
	          window.setTimeout(callback, 1000 / 10);
	       };
})();

// From http://stackoverflow.com/questions/1359761/sorting-a-json-object-in-javascript
/*function sortObject(o) {
	var sorted = {};
	var key;
	var a = [];
	for (key in o) {
		if (o.hasOwnProperty(key)) {
			a.push(key);
		}
	}
	a.sort(function (a, b) {
		a = a.toLowerCase();
		b = b.toLowerCase();
		if (a < b)
			return -1;
		if (a > b)
			return 1;
		return 0;
	});
	for (key = 0; key < a.length; key+=1) {
		sorted[a[key]] = o[a[key]];
	}
	return sorted;
};*/

var alphaSort = function (a) {
	a.sort(function (a, b) {
		a = a.title;
		b = b.title;
		if (a < b)
			return -1;
		if (a > b)
			return 1;
		return 0;
	});
};

var trimNull = function (str) {
	str = str.replace(/^\0\0*/, '');
	var ws = /\0/;
	var i = str.length;
	while (ws.test(str.charAt(--i)));
	return str.slice(0, i + 1);
};

function cloneObject(obj) {
	var clone = {};
	for(var i in obj) {
		if(obj.hasOwnProperty(i)) {
			if(typeof(obj[i]) === "object")
				clone[i] = cloneObject(obj[i]);
			else
				clone[i] = obj[i];
		}
	}
	return clone;
};

var WebJCS = (function (global, undefined) {
	var fs;
	
	var createCookie = function (name, value, days) {
		if (days) {
			var date = new Date();
			date.setTime(date.getTime()+(days*24*60*60*1000));
			var expires = "; expires="+date.toGMTString();
		}
		else var expires = "";
		global.document.cookie = name+"="+value+expires+"; path=/";
	};
	var readCookie = function (name) {
		var nameEQ = name + "=";
		var ca = global.document.cookie.split(';');
		for(var i=0;i < ca.length;i++) {
			var c = ca[i];
			while (c.charAt(0)==' ') c = c.substring(1,c.length);
			if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
		}
		return '';
	};
	
	
	var contentSection = global.document.getElementById('content');
	var tilesetdiv = global.document.getElementById('tilesetdiv');
	var layerdiv = global.document.querySelector("#layerdiv");
	var layercanvas = global.document.getElementById("layercanvas");
	var animsdrag = global.document.querySelector("#animsdrag");
	animsdrag.isDragging = false;
	var animsdiv = global.document.querySelector("#animsdiv");
	var animscanvas = global.document.querySelector("#animscanvas");
	var removeAnimFrame = global.document.querySelector("#removeAnimFrame");
	var moveAnimUp = global.document.querySelector("#moveAnimUp");
	var moveAnimDown = global.document.querySelector("#moveAnimDown");
	var parallaxdrag = global.document.querySelector("#parallaxdrag");
	parallaxdrag.isDragging = false;
	var parallaxdiv = global.document.querySelector("#parallaxdiv");
	var parallaxcanvas = global.document.querySelector("#parallaxcanvas");
	var parallaxLight = global.document.getElementById('parallaxLight');
	var pxLightOutput = global.document.getElementById('pxLightOutput');
	var pxLightLevel = 100;
	var lightcanvas = global.document.createElement('canvas');
	var lightc = lightcanvas.getContext('2d');
	var chatPanel = global.document.getElementById('chatPanel');
	var chatResizer = global.document.getElementById('chatResizer');
	var chatUserlist = global.document.getElementById('chatUserlist');
	var chatContent = global.document.getElementById('chatContent');
	var chatInput = global.document.getElementById('chatInput');
	var chatSend = global.document.getElementById('chatSend');
	var pingDiv = document.getElementById('ping');
	var pingVal = document.getElementById('pingval');
	var layerbuttons = global.document.querySelector("#layerbuttons");
	var clearLayerButton = global.document.getElementById('clearLayerButton');
	var layerpropertiesbutton = global.document.getElementById('layerpropertiesbutton');
	var layerpropertiesform = global.document.forms['layerpropertiesform'];
	var levelpropertiesform = global.document.forms['levelpropertiesform'];
	var selecteventform = global.document.forms['selecteventform'];
	var animpropertiesform = global.document.forms['animpropertiesform'];
	var tmpHelpStrings;
	var toolbarLayerName = global.document.getElementById('toolbarLayerName');
	var lc = layercanvas.getContext("2d");
	var pxc = parallaxcanvas.getContext("2d");
	var anc = animscanvas.getContext("2d");
	var lightBgColor = localStorage['WebJCS_lightBgColor'] || 'rgb(72, 48, 168)';
	var darkBgColor = localStorage['WebJCS_darkBgColor'] || 'rgb(32, 24, 80)';
	global.document.getElementById('lightBgColorPicker').textContent = lightBgColor;
	global.document.getElementById('darkBgColorPicker').textContent = darkBgColor;
	var fpsSpan = global.document.getElementById('fpsSpan');
	var fps = 0;
	var stats = new Stats();
	stats.domElement = stats.getDomElement();
	stats.domElement.style.position = 'absolute';
	stats.domElement.style.right = '0px';
	stats.domElement.style.bottom = '0px';
	stats.domElement.style.zIndex = '0';
	parallaxdiv.appendChild(stats.domElement);
	var eventPointers = {
		230: 240,
		/*95: 234,
		246: 234,
		21: 130*/
	};
	var eventPointerCache = [];
	var zoomlevelSpan = global.document.querySelector('#zoomlevel');
	var frameOffset = 0;
	var dynamicTileCache = [];
	var layernames = ['Foreground layer #2', 'Foreground layer #1', 'Sprite foreground layer', 'Sprite layer', 'Background layer #1', 'Background layer #2', 'Background layer #3', 'Background layer'];
	var fixJ2L = function () {
		selectedTiles = [[{'id': 0, 'animated': false, 'flipped': false, 'event': 0}]];
		selectedSource = 'tileset';
		animSelection =  false;
		J2L = {
			'fileName': 'Untitled.j2l',
			'LEVEL_INFO':
				{
					'JcsHorizontal': 0,
					'SecurityEnvelope1': 0,
					'JcsVertical': 0,
					'SecurityEnvelope2': 0,
					'SecEnvAndLayer': 0,
					'MinimumAmbient': 64,
					'StartingAmbient': 64,
					'AnimsUsed': 0,
					'SplitScreenDivider': 0,
					'IsItMultiplayer': 0,
					'StreamSize': 0,
					'LevelName': 'Untitled',
					'Tileset': '',
					'BonusLevel': '',
					'NextLevel': '',
					'SecretLevel': '',
					'MusicFile': '',
					'HelpString': ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
					'LayerProperties': [0, 0, 0, 0, 0, 0, 0, 3],
					'LayerUnknown1': [0, 0, 0, 0, 0, 0, 0, 0],
					'IsLayerUsed': [0, 0, 0, 1, 0, 0, 0, 0],
					'LayerWidth': [864, 576, 256, 256, 171, 114, 76, 8],
					'JJ2LayerWidth': [864, 576, 256, 256, 171, 114, 76, 8],
					'LayerHeight': [216, 144, 64, 64, 43, 29, 19, 8],
					'LayerUnknown2': [-300, -200, -100, 0, 100, 200, 300, 400],
					'LayerUnknown3': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
					'LayerXSpeed': [3.375, 2.25, 1, 1, 0.6666717529296875, 0.4444580078125, 0.2963104248046875, 0],
					'LayerYSpeed': [3.375, 2.25, 1, 1, 0.6666717529296875, 0.4444580078125, 0.2963104248046875, 0],
					'LayerAutoXSpeed': [0, 0, 0, 0, 0, 0, 0, 0],
					'LayerAutoYSpeed': [0, 0, 0, 0, 0, 0, 0, 0],
					'LayerUnknown4': [0, 0, 0, 0, 0, 0, 0, 0],
					'LayerRGB1': [0, 0, 0],
					'LayerRGB2': [0, 0, 0],
					'LayerRGB3': [0, 0, 0],
					'LayerRGB4': [0, 0, 0],
					'LayerRGB5': [0, 0, 0],
					'LayerRGB6': [0, 0, 0],
					'LayerRGB7': [0, 0, 0],
					'LayerRGB8': [0, 0, 0],
					'StaticTiles': 1024
				},
			'HEADER_INFO':
				{
					'Copyright': 'Jazz Jackrabbit 2 Data File...',
					'Identifier': 'LEVL',
					'PasswordHash': 0,
					'HideLevel': 0,
					'LevelName': 'Untitled',
					'Version': 513,
					'FileSize': 0,
					'CRC32': 0,
					'CSize1': 0,
					'USize1': 0,
					'CSize2': 0,
					'USize2': 0,
					'CSize3': 0,
					'USize3': 0,
					'CSize4': 0,
					'USize4': 0
				},
			'ANIMS': [],
			'MAX_TILES': 1024,
			'isTSF': false,
			'LEVEL': [, , , , , , , ],
			'TilesetProperties':
				{
					'TileEvent': [],
					'TileUnknown1': [],
					'TileType': [],
					'TileUnknown2': []
				}
		};
		J2L.EVENTS = new Uint32Array(J2L.LEVEL_INFO.LayerWidth[3]*J2L.LEVEL_INFO.LayerHeight[3]);
		setTimeout(function () {
			updateEventPointers();
		}, 0);
		var l = 8;
		var x, y, w, h;
		for(l=0; l < 8; l+=1) {
			J2L.LEVEL[l] = [];
			w = J2L.LEVEL_INFO.LayerWidth[l];
			h = J2L.LEVEL_INFO.LayerHeight[l];
			for(x=0; x < w; x+=1) {
				J2L.LEVEL[l][x] = [];
				for(y=0; y < h; y+=1) {
					J2L.LEVEL[l][x][y] = {'flipped': false, 'animated': false, 'id': 0};
				}
			}
		}
		var i;
		for(i=0; i < J2L.MAX_TILES; i+=1) {
			J2L.TilesetProperties.TileEvent[i] = 0;
			J2L.TilesetProperties.TileUnknown1[i] = 0;
			J2L.TilesetProperties.TileType[i] = 0;
			J2L.TilesetProperties.TileUnknown2[i] = 0;
		}
		global.document.title = "Jazz Creation Station - "+J2L.LEVEL_INFO.LevelName+" - "+J2L.fileName;
	};
	var J2L;
	var J2T;
	
	
	
	var currentLayer = 3;
	var changeLayer = function (l) {
		layerbuttons.childNodes[currentLayer].className = '';
		layerbuttons.childNodes[l].className = 'selected';
		currentLayer = l;
		toolbarLayerName.textContent = "Layer "+(l+1)+": "+layernames[l];
		layermenu[0].node.className = layermenu[1].node.className = layermenu[2].node.className = currentLayer === 3 ? '' : 'disabled';
	};
	var scrollbars = 	{
	'layers':
		{
			gripPosition: [0, 0],
			contentSize: [0, 0],
			scrollSize: [0, 0],
			gripSize: [0, 0],
			scroll: [0, 0],
			offset: [0, 0],
			bar: ""
		},
	'anims':
		{
			gripPosition: [0, 0],
			contentSize: [0, 0],
			scrollSize: [0, 0],
			gripSize: [0, 0],
			scroll: [0, 0],
			offset: [0, 0],
			bar: ""
		},
	'tileset':
		{
			gripPosition: [0, 0],
			contentSize: [0, 0],
			scrollSize: [0, 0],
			gripSize: [0, 0],
			scroll: [0, 0],
			offset: [0, 0],
			bar: ""
		}
	};
	var scrollingWhat = "";
	var scrollarrow = new Image(); scrollarrow.src = "media/icons/scroll-arrow.png";
	var unknownTile = new Image(); unknownTile.src = "media/images/unknown-tile.png";
	var mousetarget = null;
	var mousepos = false;
	var mousedownpos = false;
	var mousedownscroll = false;
	var whichmouse = 0;
	var tileSelection = false;
	var animSelection = false;
	var holdingShiftKey = false;
	var holdingCtrlKey = false;
	var holdingFlipKey = false;
	var holdingTileTypeKey = false;
	var isBSelecting = false;
	var BSelectStart = [0, 0];
	var BSelection = [0, 0, 0, 0];
	var holdingBKey = false;
	var windowFocus = true;
	var selectedTiles = [[{'id': 0, 'animated': false, 'flipped': false, 'event': 0}]];
	var selectedSource = 'tileset';
	var selectedEvent = 0;
	var selectEventPos = [0, 0, null];
	var oldEvent = {'id': 0, 'params': []};
	var starttime = 0;
	var selectTileset = global.document.querySelector("#selectTileset");
	var tilesetTypePicker = global.document.querySelector("#tilesetType");
	var tilePositionDiv = global.document.querySelector("#tilePositionDiv");
	var eventInfoSpan = global.document.querySelector("#eventInfoSpan");
	var tilesetTypeSelected = 2;
	var tilesetcanvas = global.document.querySelector("#tilesetcanvas");
	var tilec = tilesetcanvas.getContext('2d');
	var currentTileset = new Image();
	var currentTilesetMask = new Image();
	var zoomin = global.document.querySelector("#zoomin");
	var zoomout = global.document.querySelector("#zoomout");
	var zoomlevel = 1;
	var tilesets = [];
	var tile_url = "", mask_url = "";
	var toggleMask = global.document.querySelector("#toggleMask");
	var toggleEvents = global.document.querySelector("#toggleEvents");
	var saveParallaxBtn = global.document.querySelector("#saveparallax");
	var toggleParallaxEvents = global.document.querySelector("#toggleParallaxEvents");
	var toggleTexturedBg = global.document.getElementById('toggleTexturedBg');
	var toggleAnimMask = global.document.querySelector("#toggleAnimMask");
	var showLayerMask = false;
	var showLayerEvents = true;
	var showParallaxEvents = true;
	var showTexturedBg = true;
	var showAnimMask = false;
	var diffColors = ["white", "yellow", "red", "#F0F"];
	var JCSini;
	var ANIMS = false;
	var undoStack = [];
	var redoStack = [];
	
	var toggleTileCacheVar = localStorage['WebJCS_toggleTileCache'] === '1';
	var toggleEventLinksVar = localStorage['WebJCS_toggleEventLinks'] === '1';
	
	var handleUndoRedo = function (newPart, isRedo) {
		if(socket && socket.readyState === 1) {
			sendUpdate(newPart);
		}
		switch(newPart.what) {
			case 'layer':
				newPart.selection = updateTiles(newPart.layer, newPart.startX, newPart.startY, newPart.width, newPart.height, newPart.selection, newPart.includeEmpty);
				updateEventPointers();
				break;
			case 'event':
				var oldEvt = 0;
				oldEvt = J2L.EVENTS[newPart.x+J2L.LEVEL_INFO.LayerWidth[3]*newPart.y]
				J2L.EVENTS[newPart.x+J2L.LEVEL_INFO.LayerWidth[3]*newPart.y] = newPart.event;
				newPart.event = oldEvt;
				updateEventPointers();
				break;
			default:
				console.log('UNDO/REDO DEBUG:', newPart, undoStack, redoStack);
				break;
		}
	};
	var doUndo = function () {
		if(undoStack.length === 0) return;
		var newPart = undoStack.pop();
		handleUndoRedo(newPart, false);
		redoStack.push(newPart);
		if(undoStack.length === 0) menuUndo.className = 'disabled';
		menuRedo.className = '';
	};
	var doRedo = function () {
		if(redoStack.length === 0) return;
		var newPart = redoStack.pop();
		handleUndoRedo(newPart, true);
		undoStack.push(newPart);
		if(redoStack.length === 0) menuRedo.className = 'disabled';
		menuUndo.className = '';
	};
	var clearUndoHistory = function () {
		menuUndo.className = 'disabled';
		menuRedo.className = 'disabled';
		undoStack = [];
		redoStack = [];
	};
	
	var Popup = popup(global);
	global.hidePopup = Popup.hide; // Can be accessed through buttons etc..
	var menuNewLevel = function () {
		Popup.hide();
		Colorpicker.hide();
		if(confirm("Are you sure you want to create a new level?")) {
			//document.location.reload();
			if(socket && socket.readyState === 1) {
				sendUpdate({what: 'new'});
			}
			else {
				fixJ2L();
				changeTileset(0);
				clearUndoHistory();
			}
		}
	};
	var menuOpenLevel = function () {
		Popup.hide();
		Colorpicker.hide();
		getFileList(function (err, results) {
			if(err) {
				alert('Could not read list of files');
				return;
			}
			global.document.forms['openlevelform'].reset();
			Popup.open('openlevel');
			var levellist = global.document.querySelector("#openlevel #levellist");
			levellist.options.length = 0;
			var i, l = results.length;
			var levels = [];
			for(i=0; i < l; i+=1) {
				if(results[i].substr(-4, 4).toLowerCase() !== '.j2l') {
					continue;
				}
				levels.push(results[i]);
			}
			levels.sort(function (a, b) {
				a = a.toLowerCase();
				b = b.toLowerCase();
				if (a < b)
					return -1;
				if (a > b)
					return 1;
				return 0;
			});
			l = levels.length;
			for(i=0; i < l; i+=1) {
				levellist.add(new Option(levels[i], levels[i]), null);
			}
		});
	};
	makeMainMenu(global.document.getElementById('menubar'), [
		{
			name: 'File',
			items: [
				{name: 'New', action: menuNewLevel, key: 'Alt+N', icon: 'new'},
				{name: 'Open...', action: menuOpenLevel, key: 'Ctrl+O', icon: 'open'},
				,
				{name: 'Save', action: function () {
					writeLevel({save: true});
				}, icon: 'save', disabled: true},
				{name: 'Save As...', action: function () {
					writeLevel({save: true, newfile: true});
				}, key: 'Ctrl+Shift+S', icon: 'saveas'},
				{name: 'Save & Run', action: function () {
					writeLevel({run: true, save: true});
				}, key: 'Ctrl+Shift+R', disabled: true},
				{name: 'Run', action: function () {
					writeLevel({run: true, save: false});
				}, key: 'Ctrl+R', disabled: serverInfo.isCollab}
			]
		},
		{
			name: 'Tools',
			items: [
				{name: 'Undo', action: doUndo, disabled: true, id: 'menuUndo', key: 'Ctrl+Z'},
				{name: 'Redo', action: doRedo, disabled: true, id: 'menuRedo', key: 'Ctrl+Shift+Z'},
				,
				{name: 'Level properties...', action: function () {
					global.document.forms['levelpropertiesform'].reset();
					Popup.open('levelproperties');
					tmpHelpStrings = [];
					for(var i=0; i < 16; i+=1) {
						tmpHelpStrings[i] = J2L.LEVEL_INFO.HelpString[i];
					}
					var lpform = global.document.forms['levelpropertiesform'];
					lpform.leveltitle.value = J2L.LEVEL_INFO.LevelName;
					lpform.nextlevel.value = J2L.LEVEL_INFO.NextLevel;
					lpform.secretlevel.value = J2L.LEVEL_INFO.SecretLevel;
					lpform.bonuslevel.value = J2L.LEVEL_INFO.BonusLevel;
					lpform.musicfile.value = J2L.LEVEL_INFO.MusicFile;
					lpform.minlightslider.value = J2L.LEVEL_INFO.MinimumAmbient/0.64;
					lpform.minlightnumber.value = Math.round(J2L.LEVEL_INFO.MinimumAmbient/0.64);
					lpform.startlightslider.value = J2L.LEVEL_INFO.StartingAmbient/0.64;
					lpform.startlightnumber.value = Math.round(J2L.LEVEL_INFO.StartingAmbient/0.64);
					if(J2L.LEVEL_INFO.SplitScreenDivider > 0)
						lpform.splitscreenradio[1].checked = true;
					if(J2L.LEVEL_INFO.IsItMultiplayer > 0)
						lpform.isMultiplayer.setAttribute('on');
					else
						lpform.isMultiplayer.setAttribute('off');
					if(J2L.HEADER_INFO.HideLevel > 0)
						lpform.hideLevel.setAttribute('on');
					else
						lpform.hideLevel.setAttribute('off');
					helpStringEditor.value = tmpHelpStrings[0].replace(/\@/g, "\n");
					updateHelpStringPreview();
				}},
				{name: 'Level password...', disabled: true},
				,
				{name: 'Settings', action: function () {
					global.document.forms['settingsform'].reset();
					Popup.open('settings');
					var lightBgColorPicker = global.document.querySelector('#lightBgColorPicker');
					var darkBgColorPicker = global.document.querySelector('#darkBgColorPicker');
					lightBgColorPicker.onchange = function (color, cstr) {
						lightBgColor = cstr;
						localStorage['WebJCS_lightBgColor'] = cstr;
						//redraw(0, true);
						requestAnimFrame(function () {redraw(0, true);}, layercanvas);
					};
					lightBgColorPicker.onupdate = function (color, cstr) {
						lightBgColor = cstr;
					};
					lightBgColorPicker.oncancel = function (oldcolor, cstr) {
						lightBgColor = cstr;
					};
					darkBgColorPicker.onchange = function (color, cstr) {
						darkBgColor = cstr;
						localStorage['WebJCS_darkBgColor'] = cstr;
						//redraw(0, true);
						requestAnimFrame(function () {redraw(0, true);}, layercanvas);
					};
					darkBgColorPicker.onupdate = function (color, cstr) {
						darkBgColor = cstr;
					};
					darkBgColorPicker.oncancel = function (oldcolor, cstr) {
						darkBgColor = cstr;
					};
					
					var toggleTileCache = global.document.getElementById('toggleTileCache');
					toggleTileCache.set(localStorage['WebJCS_toggleTileCache'] === '1');
					toggleTileCache.addEventListener('click', function () {
						localStorage['WebJCS_toggleTileCache'] = toggleTileCache.get()? '1' : '0';
						toggleTileCacheVar = toggleTileCache.get();
					}, false);
					var toggleEventLinks = global.document.getElementById('toggleEventLinks');
					toggleEventLinks.set(localStorage['WebJCS_toggleEventLinks'] !== '0');
					toggleEventLinks.addEventListener('click', function () {
						localStorage['WebJCS_toggleEventLinks'] = toggleEventLinks.get()? '1' : '0';
						toggleEventLinksVar = toggleEventLinks.get();
					}, false);
				}/*, icon: 'HTML5_Performance_16'*/}
			]
		},
		{
			name: 'Help',
			items: [
				{name: 'Howto JCS', href: 'http://ninjadodo.net/htjcs/'},
				{name: 'JcsRef', href: 'http://www.jazz2online.com/jcsref/index.php?&menu=topics'},
				,
				{name: 'About', action: function () {
					Popup.open('about');
				}}
			]
		}
	]);
	var menuUndo = document.getElementById('menuUndo');
	var menuRedo = document.getElementById('menuRedo');
	
	Popup.add('settings', 'settingspopup', function (res) {
		
	});
	Popup.add('openlevel', 'openlevel', function (res) {
		Popup.open('loadinglevel');
		if(res === 1) {
			var openfieldlevel = global.document.querySelector('#openlevel #openfieldlevel');
			if(openfieldlevel.files[0]===undefined) return;
			var handle = openfieldlevel.files[0];
			var fd = new FormData();
			fd.append('level', handle);
			uploadAndParse(fd, function (err, data) {
				var datastr = '';
				Array.prototype.slice.apply(data).forEach(function (v, i, a) {
					datastr += String.fromCharCode(v);
				});
				switch(datastr) {
					case "checksum error":
						alert("Could not load level: Checksum error\nFile is broken");
						Popup.close();
						break;
					
					default:
						if(socket && socket.readyState === 1) {
							/*socket.emit('level', {
								data: datastr,
								filename: handle.name
							});*/
							sendLevel(handle.name, data);
						}
						else {
							loadlevel(data, handle.name);
							clearUndoHistory();
						}
						break;
				}
			});
		}
		else if(res === 2) {
			var levellist = global.document.querySelector('#openlevel #levellist');
			if(levellist.selectedIndex > -1 && levellist.options[levellist.selectedIndex].value !== undefined) {
				var filename = levellist.options[levellist.selectedIndex].value;
				parseFile(filename, function (err, data) {
					var datastr = '';
					Array.prototype.slice.apply(data).forEach(function (v, i, a) {
						datastr += String.fromCharCode(v);
					});
					if(socket && socket.readyState === 1) {
						/*socket.emit('level', {
							data: datastr,
							filename: filename
						});*/
						sendLevel(filename, data);
					}
					else {
						loadlevel(data, filename);
						clearUndoHistory();
					}
				});
			}
		}
	});
	Popup.add('opentileset', 'opentileset', function (res) {
		if(res.files[0]===undefined) return;
		var handle = res.files[0];
		var file;
		var reader;
		for(var i=0; i < res.files.length; i+=1) {
			file = res.files[i];
			if(file.fileName.toLowerCase().substr(-4, 4) === '.j2t') { // Check if it's a .j2t file
				reader = new FileReader();
				reader.onloadend = (function (file) {
					return function (e) { // File content arrived
						if(e.target.readyState !== FileReader.DONE) return;
						var res = this.result;
						if(res.substr(180, 4) === "TILE") { // Is it a tileset?
							var tilesetTitle = trimNull(res.substr(180+4+4, 32)); // Get the title of the tileset
							var v = res.substr(180+4+4+32, 2);
							var buf = new ArrayBuffer(2);
							var uint8 = new Uint8Array(buf);
							var i;
							for(i=0; i < 2; i+=1) {
								uint8[i] = v.charCodeAt(i);
							}
							var ver = new Uint16Array(buf)[0];
							
							fs.root.getFile(file.fileName, {create: true}, function (fileEntry) {
								fileEntry.createWriter(function (fileWriter) {
									fileWriter.write(file);
									var tileset = {'name': file.fileName, 'title': tilesetTitle, 'version': ver};
									for(var i=0; i < tilesets.length; i+=1) {
										if(tilesets[i].name === file.fileName) {
											tilesets[i] = tileset;
											break;
										}
									}
									if(i === tilesets.length) {
										tilesets.push(tileset);
									}
									var si = 0;
									var oldTileset = "" || selectTileset.options[selectTileset.selectedIndex].value;
									alphaSort(tilesets);
									selectTileset.length = 1;
									var opt;
									for(i=0; i < tilesets.length; i+=1) {
										opt = new Option(tilesets[i].title, tilesets[i].name);
										if(tilesets[i].version === 513) {
											opt.className = "TSF";
										}
										selectTileset.add(opt, null);
										if(oldTileset === tilesets[i].name) {
											si = i+1;
										}
									}
									selectTileset.selectedIndex = si;
									selectTileset.disabled = false;
									
									
								}, fileSystemError);
							}, fileSystemError);
						}
					};
				})(file);
				reader.readAsBinaryString(file);
			}
		}
	});
	Popup.add('about', 'about');
	Popup.add('levelproperties', 'levelproperties', function (res) {
		var lpform = levelpropertiesform;
		J2L.LEVEL_INFO.LevelName = J2L.HEADER_INFO.LevelName = lpform.leveltitle.value.substring(0, 32);
		J2L.LEVEL_INFO.NextLevel = lpform.nextlevel.value.substring(0, 32);
		J2L.LEVEL_INFO.SecretLevel = lpform.secretlevel.value.substring(0, 32);
		J2L.LEVEL_INFO.BonusLevel = lpform.bonuslevel.value.substring(0, 32);
		J2L.LEVEL_INFO.MusicFile = lpform.musicfile.value.substring(0, 32);
		J2L.LEVEL_INFO.MinimumAmbient = ~~Math.max((+lpform.minlightslider.value || 100)*0.64, 0);
		J2L.LEVEL_INFO.StartingAmbient = ~~Math.max((+lpform.startlightslider.value || 100)*0.64, 0);
		J2L.LEVEL_INFO.SplitScreenDivider = lpform.splitscreenradio[1].checked? 1 : 0;
		J2L.LEVEL_INFO.IsItMultiplayer = lpform.isMultiplayer.getAttribute('on') !== null;
		J2L.HEADER_INFO.HideLevel = lpform.hideLevel.getAttribute('on') !== null;
		for(var i = 0; i < 16; i+=1) {
			J2L.LEVEL_INFO.HelpString[i] = tmpHelpStrings[i].substring(0, 512);
		}
		global.document.title = "Jazz Creation Station - "+J2L.HEADER_INFO.LevelName+" - "+J2L.fileName;
		
		if(socket && socket.readyState === 1) {
			sendUpdate({
				what: 'levelprop',
				levelName: J2L.LEVEL_INFO.LevelName,
				nextLevel: J2L.LEVEL_INFO.NextLevel,
				secretLevel: J2L.LEVEL_INFO.SecretLevel,
				bonusLevel: J2L.LEVEL_INFO.BonusLevel,
				musicFile: J2L.LEVEL_INFO.MusicFile,
				minLight: J2L.LEVEL_INFO.MinimumAmbient,
				startLight: J2L.LEVEL_INFO.StartingAmbient,
				splitScreen: J2L.LEVEL_INFO.SplitScreenDivider,
				multiPlayer: J2L.LEVEL_INFO.IsItMultiplayer,
				hideLevel: J2L.HEADER_INFO.HideLevel ? 1 : 0,
				helpString: J2L.LEVEL_INFO.HelpString
			});
		}
	});
	Popup.add('loadinglevel', 'loadinglevel', undefined, {closable: false});
	Popup.add('loadingtileset', 'loadingtileset', undefined, {closable: false});
	Popup.add('saving', 'saving', undefined, {closable: false});
	Popup.add('layerproperties', 'layerproperties', function (res) {
		
	});
	Popup.add('animproperties', 'animproperties', function () {
		var animId = Math.round(animpropertiesform.animID.value);
		
		J2L.ANIMS[animId].FPS = Math.round(Math.min(255, animpropertiesform.animSpeedSlider.value));
		J2L.ANIMS[animId].FramesBetweenCycles = Math.round(animpropertiesform.animFrameWait.value);
		J2L.ANIMS[animId].RandomAdder = Math.round(animpropertiesform.animRandomAdder.value);
		J2L.ANIMS[animId].PingPongWait = Math.round(animpropertiesform.animPingPongWait.value);
		J2L.ANIMS[animId].IsItPingPong = animpropertiesform.animPingPong.get()? 1 : 0;
		
		if(socket && socket.readyState === 1) {
			sendUpdate({
				what: 'anims',
				type: 'props',
				anim: animId,
				fps: J2L.ANIMS[animId].FPS,
				frameWait: J2L.ANIMS[animId].FramesBetweenCycles,
				randomAdder: J2L.ANIMS[animId].RandomAdder,
				pingPongWait: J2L.ANIMS[animId].PingPongWait,
				isItPingPong: J2L.ANIMS[animId].IsItPingPong
			});
		}
	});
	
	Popup.add('selectevent', 'selectevent', function () {
		// selectEventPos[x, y, target];
		var x = selectEventPos[0];
		var y = selectEventPos[1];
		updateEvent();
		var evt = createEvent(oldEvent.id, selecteventform.eventType.selectedIndex, selecteventform.illuminate.getAttribute('on') !== null, 0, oldEvent.params);
		var oldEvt = 0;
		if(selectEventPos[2] === layercanvas) {
			oldEvt = J2L.EVENTS[x+J2L.LEVEL_INFO.LayerWidth[3]*y];
			J2L.EVENTS[x+J2L.LEVEL_INFO.LayerWidth[3]*y] = evt;
			updateEventPointers();
		}
		else {
			oldEvt = J2L.TilesetProperties.TileEvent[x+10*y];
			J2L.TilesetProperties.TileEvent[x+10*y] = evt;
		}
		
		if(evt !== oldEvt && selectEventPos[2] === layercanvas) {
			var oldPart = {
				what: 'event',
				event: oldEvt,
				where: 'layer',
				x: x,
				y: y
			};
		
			undoStack.push(oldPart);
			redoStack = [];
		
			menuUndo.className = '';
			menuRedo.className = 'disabled';
		}
		
		if(socket && socket.readyState === 1) {
			sendUpdate({
				what: 'event',
				event: evt,
				where: selectEventPos[2] === layercanvas ? 'layer' : 'tileset',
				x: x,
				y: y
			});
		}
		selectedEvent = evt;
		oldEvent.id = 0;
		oldEvent.params = [];
	});
	
	var selectEventList = global.document.getElementById('eventlist');
	var selectEventFilter = selecteventform.eventfilter;
	var selectEventListType = selecteventform.listtype;
	var selectEventParameters = global.document.getElementById('selecteventparameters');
	
	var createEvent = function (id, difficulty, illumi, unknownbit, parameters) {
		if(selecteventform.generator.getAttribute('on') !== null) {
			id = 216;
		}
		else if(id === 0) {
			return 0;
		}
		
		var paramsamount = Math.min(JCSini.Events[id].length - 5, parameters.length);
		var offset = 0;
		var params = 0;
		var paramArray = [];
		var isSigned = false;
		var bits = 0;
		var pmax = 0, pmin = 0;
		
		for(var i = 0; i < paramsamount; i+=1) {
			bits = JCSini.Events[id][5+i][1];
			isSigned = bits < 0;
			bits = Math.abs(bits);
			if(isSigned) {
				pmax = Math.pow(2, bits-1)-1;
				pmin = -Math.pow(2, bits-1);
			}
			else {
				pmax = Math.pow(2, bits)-1;
				pmin = 0;
			}
			parameters[i] = Math.max(Math.min(parameters[i], pmax), pmin);
			if(isSigned && parameters[i] < 0)
				parameters[i] = + Math.pow(2, bits) + parameters[i];
			params |= parameters[i] << offset;
			offset += bits;
		}
		
		return (id & 255) | ((difficulty & 3) << 8) | ((illumi & 1) << 10) | ((unknownbit & 1) << 11) | ((params) << 12);
	};
	
	var updateEventList = function (oldID) {
		selectEventList.innerHTML = "";
		var container = global.document.createElement('div');
		if(selectEventListType.selectedIndex === 0) {
			var listener = function (id) {
				return function (e) {
					oldEvent.id = id;
					updateEvent();
				};
			};
			var topnode = global.document.createElement('ul');
			var oldEventNode = null;
			topnode.className = "tree";
			var counter = 0;
			var recursive = function (subtree, parent) {
				var listItem;
				subtree.sort(function (a, b) {
					if(typeof a === 'object') {
						var c = a[0];
					}
					else {
						var c = JCSini.Events[a][0];
					}
					if(typeof b === 'object') {
						var d = b[0];
					}
					else {
						var d = JCSini.Events[b][0];
					}
					if (c < d)
						return -1;
					if (c > d)
						return 1;
					return 0;
				});
				for(var i=0, l = subtree.length; i < l; i+=1) {
					listItem = global.document.createElement('li');
					if(typeof subtree[i] === 'object' && subtree[i].length > 0) {
						var label = global.document.createElement('label')
						label.textContent = subtree[i][0];
						var id = 'selectEventTree_'+(counter++);
						label.setAttribute('for', id);
						listItem.appendChild(label);
						var checkbox = global.document.createElement('input');
						checkbox.type = 'checkbox';
						checkbox.id = id;
						listItem.appendChild(checkbox);
						var subnode = global.document.createElement('ul');
						listItem.appendChild(subnode);
						recursive(subtree[i].slice(1, subtree[i].length), subnode);
					}
					else {
						listItem.className = 'item';
						var label = global.document.createElement('label');
						var radio = global.document.createElement('input');
						var id = 'selectEventTree_radio_'+subtree[i];
						radio.type = 'radio';
						radio.name = "selectEventTree_radio";
						radio.id = id;
						radio.addEventListener('change', listener(subtree[i]), false);
						if(subtree[i] === oldID) {
							oldEventNode = radio;
						}
						label.setAttribute('for', id);
						if(subtree[i] === 'MCE') {
						
						}
						else {
							var eventname = JCSini.Events[subtree[i]][0];
							if(eventname.toLowerCase().indexOf(selectEventFilter.value.toLowerCase().trim()) === -1) continue;
							label.innerHTML = eventname + " <span style=\"color: rgba(0, 0, 0, 0.15);\">("+subtree[i]+")</span>";
							listItem.appendChild(radio);
							listItem.appendChild(label);
						}
					}
					parent.appendChild(listItem);
				}
			};
			recursive(eventTree, topnode);
			var spaceAbove = 0;
			if(oldEventNode) {
				oldEventNode.checked = true;
				var stepCategory = oldEventNode;
				while(stepCategory && stepCategory !== topnode) {
					spaceAbove += stepCategory.offsetTop || 0;
					stepCategory = stepCategory.parentNode;
					if(stepCategory && stepCategory.childNodes[1] && stepCategory.childNodes[1].nodeName.toLowerCase() === 'input') {
						stepCategory.childNodes[1].checked = true;
					}
				}
			}
			container.className = 'eventtree';
			container.appendChild(topnode);
		}
		else {
			var isAlpha = selectEventListType.selectedIndex === 1;
			var topnode = global.document.createElement('select');
			topnode.size = 2;
			var eventlist = JCSini.Events.slice(0); // Make a copy
			if(isAlpha) {
				eventlist.sort(function (a, b) {
					if(a[0] === b[0]) return 0;
					else if(a[0] < b[0]) return -1;
					else return 1;
				});
			}
			var si = -1;
			var len = 0;
			var option;
			for(var i=0; i < 256; i+=1) {
				if(eventlist[i][0].toLowerCase().indexOf(selectEventFilter.value.toLowerCase().trim()) === -1) continue;
				option = new Option(eventlist[i][0]+" ("+eventlist[i].id+")", eventlist[i].id);
				topnode.add(option, null);
				if(eventlist[i].id === oldID) {
					si = len;
				}
				len+=1;
			}
			topnode.selectedIndex = si;
			var selectScroll = si > -1
			
			var listchange = function () {
				if(this.selectedIndex > -1) {
					oldEvent.id = this.options[this.selectedIndex].value;
					updateEvent();
				}
			};
			topnode.addEventListener('change', listchange, false);
			topnode.addEventListener('keyup', listchange, false);
			container.className = 'eventselect';
			container.appendChild(topnode);
		}
		selectEventList.appendChild(container);
		if(oldEventNode && oldEventNode.parentNode) {
			oldEventNode.parentNode.scrollIntoView(false);
		}
		if(selectScroll) {
			topnode.scrollTop = (si+1)*(topnode.scrollHeight / topnode.options.length) - topnode.offsetHeight/2;
		}
	};
	selectEventFilter.addEventListener('input', function (e) {
		updateEventList(oldEvent.id);
	}, false);
	(function () {
		var listTypeChange = function (e) {
			localStorage.selectEventListType = this.selectedIndex;
			updateEventList(oldEvent.id);
		};
		selectEventListType.addEventListener('change', listTypeChange, false);
		selectEventListType.addEventListener('keyup', listTypeChange, false);
	})();
	
	var updateEvent = function (firstOpen) {
		
		var isGenerator = false;
		var id = oldEvent.id;
		if(selecteventform.generator.getAttribute('on') !== null) {
			oldEvent.params[0] = id;
			isGenerator = true;
			id = 216;
		}
		
		var paramsamount = JCSini.Events[id].length - 5;
		oldEvent.params.length = paramsamount;
		var params = oldEvent.params;
		var isSigned = false;
		var bitcount = 0;
		var pmax = 0;
		var pmin = 0;
		var paramHTML = [];
		var oldParams = selectEventParameters.querySelectorAll('input');
		
		for(var i=0; i < paramsamount; i+=1) {
			bitcount = Math.abs(JCSini.Events[id][5+i][1]);
			isSigned = JCSini.Events[id][5+i][1] < 0;
			
			if(isSigned) {
				pmax = Math.pow(2, bitcount-1)-1;
				pmin = -Math.pow(2, bitcount-1);
			}
			else {
				pmax = Math.pow(2, bitcount)-1;
				pmin = 0;
			}
			var oldParam = params[i];
			if(!firstOpen) {
				params[i] = Math.max(Math.min(id === 216 && i === 0 ? params[i] : ((oldParams[oldParams.length-1-i] && +oldParams[oldParams.length-1-i].value)), pmax), pmin);
				if(isNaN(params[i])) params[i] = oldParam;
			}
			if(isNaN(params[i])) params[i] = 0;
			paramHTML.push("<div>"+JCSini.Events[id][5+i][0]+"</div><input type=number value='"+params[i]+"' min='"+pmin+"' max='"+pmax+"' step=1>");
		}
		selectEventParameters.innerHTML = paramHTML.reverse().join("");
		var newParams = selectEventParameters.querySelectorAll('input');
		var paramInput = function (i) {
			return function (e) {
				if(i === 0 && selecteventform.generator.getAttribute('on') !== null) {
					oldEvent.id = +this.value;
				}
			};
		};
		var paramBlur = function (i) {
			return function (e) {
				if(i === 0 && selecteventform.generator.getAttribute('on') !== null) {
					updateEventList(+this.value);
				}
			};
		};
		for(var i=0; i < newParams.length; i+=1) {
			newParams[newParams.length-1-i].addEventListener('input', paramInput(i), false);
			newParams[newParams.length-1-i].addEventListener('blur', paramBlur(i), false);
		}
		
	};
	
	var tileReachable = function (offx, offy, target) {
		if(target === layercanvas) {
			var tilex = Math.floor((offx + scrollbars.layers.scroll[0])/(32*zoomlevel));
			var tiley = Math.floor((offy + scrollbars.layers.scroll[1])/(32*zoomlevel));
			var maxw = J2L.LEVEL_INFO.LayerWidth[currentLayer];
			var maxh = J2L.LEVEL_INFO.LayerHeight[currentLayer];
			var outside = mouseOnScrollbars('layers', layercanvas.offsetWidth, layercanvas.offsetHeight, offx, offy, {which: 3});
		}
		else {
			var tilex = Math.floor(offx/32);
			var tiley = Math.floor((offy + scrollbars.tileset.scroll[1])/32);
			var maxw = 10;
			var maxh = currentTileset.height/32;
			var outside = mouseOnScrollbars('tileset', tilesetcanvas.offsetWidth, tilesetcanvas.offsetHeight, offx, offy, {which: 3});
		}
		if(tilex < 0 || tiley < 0 || tilex >= maxw || tiley >= maxh || outside) {
			return false;
		}
		return [tilex, tiley];
	};
	
	var selectEvent = function (offx, offy, target) {
		var tile = tileReachable(offx, offy, target);
		if(!tile) return false;
		
		
		if((target === layercanvas && currentLayer === 3) || target === tilesetcanvas) {
			selectEventPos = [tile[0], tile[1], target];
			var evt = target === layercanvas ? J2L.EVENTS[tile[0] + J2L.LEVEL_INFO.LayerWidth[3]*tile[1]] : J2L.TilesetProperties.TileEvent[tile[0] + 10*tile[1]];
			var id = evt & 255;
			var isGenerator = false;
			var difficulty = (evt & (Math.pow(2, 10)-1)) >> 8; // bits 10-8 (2)
			var illumi = (evt & (Math.pow(2, 11)-1)) >> 10 // bits 10-11 (1)
			var unknownbit = (evt & (Math.pow(2, 12)-1)) >> 11 // bits 10-11 (1)
			var params = (evt & (Math.pow(2, 32)-1)) >> 12; // bits 12-32 (20)
			var paramsamount = JCSini.Events[id].length - 5;
			var paramoffset = 0;
			var param = 0;
			var paramArray = [];
			for(var i=0; i < paramsamount; i+=1) {
				param = (params & (Math.pow(2, paramoffset+Math.abs(JCSini.Events[id][5+i][1]))-1)) >> paramoffset;
				if(JCSini.Events[id][5+i][1] < 0 && param > Math.pow(2, Math.abs(JCSini.Events[id][5+i][1])-1))
					param -= Math.pow(2, Math.abs(JCSini.Events[id][5+i][1]));
				paramArray.push(param);
				paramoffset += Math.abs(JCSini.Events[id][5+i][1]);
			}
			if(id === 216) {
				id = paramArray[0];
				isGenerator = true;
			}
			Popup.open('selectevent');
			selecteventform.reset();
			
			selectEventListType.selectedIndex = +localStorage.selectEventListType || 0;
			
			if(isGenerator)
				selecteventform.generator.setAttribute('on');
			else
				selecteventform.generator.removeAttribute('on');
			if(illumi)
				selecteventform.illuminate.setAttribute('on');
			else
				selecteventform.illuminate.removeAttribute('on');
			
			selecteventform.eventType.selectedIndex = difficulty;
			
			oldEvent.id = id;
			oldEvent.params = paramArray;
			updateEventList(id);
			updateEvent(true);
			
		}
		else return false;
		
		//alert("Select event is not implemented yet.");
	};
	var grabEvent = function (offx, offy, target) {
		var tile = tileReachable(offx, offy, target);
		if(!tile) return false;
		
		if(target === layercanvas && currentLayer === 3) {
			selectedEvent = J2L.EVENTS[tile[0] + J2L.LEVEL_INFO.LayerWidth[3]*tile[1]];
		}
		else if(target === tilesetcanvas) {
			selectedEvent = J2L.TilesetProperties.TileEvent[tile[0] + 10*tile[1]];
		}
		else return false;
	};
	var pasteEvent = function (offx, offy, target) {
		var tile = tileReachable(offx, offy, target);
		if(!tile) return false;
		
		var oldEvt = 0;
		
		if(target === layercanvas && currentLayer === 3) {
			oldEvt = J2L.EVENTS[tile[0] + J2L.LEVEL_INFO.LayerWidth[3]*tile[1]];
			J2L.EVENTS[tile[0] + J2L.LEVEL_INFO.LayerWidth[3]*tile[1]] = selectedEvent;
			updateEventPointers();
		}
		else if(target === tilesetcanvas) {
			oldEvt = J2L.TilesetProperties.TileEvent[tile[0] + 10*tile[1]];
			J2L.TilesetProperties.TileEvent[tile[0] + 10*tile[1]] = selectedEvent;
		}
		else return false;
		
		if(selectedEvent !== oldEvt && target === layercanvas && currentLayer === 3) {
			var oldPart = {
				what: 'event',
				event: oldEvt,
				where: 'layer',
				x: tile[0],
				y: tile[1]
			};
			
			undoStack.push(oldPart);
			redoStack = [];
			
			menuUndo.className = '';
			menuRedo.className = 'disabled';
		}
		
		if(socket && socket.readyState === 1) {
			sendUpdate({
				what: 'event',
				event: selectedEvent,
				where: target === layercanvas ? 'layer' : 'tileset',
				x: tile[0],
				y: tile[1]
			});
		}
	};
	var changeTileType = function (type) {
		return function (offx, offy, target) {
			var tile = tileReachable(offx, offy, target);
			if(!tile) return false;
			var pos = tile[0] + 10*tile[1];
			if(pos === 0) return;
			J2L.TilesetProperties.TileType[pos] = type;
			if(socket && socket.readyState === 1) {
				sendUpdate({
					what: 'tiletype',
					type: type,
					pos: pos
				});
			}
		};
	};
	
	var tilesetmenu = contextmenu.create('tileset', [
		{title: 'Select event', onclick: selectEvent},
		{title: 'Grab event', onclick: grabEvent},
		{title: 'Paste event', onclick: pasteEvent},
		,
		{title: 'Tile type', sub: [
			{title: 'Normal', onclick: changeTileType(0)},
			{title: 'Translucent', onclick: changeTileType(1)},
			{title: 'Caption', onclick: changeTileType(4)}
		]}
	]);
	var layermenu = contextmenu.create('layer', [
		{title: 'Select event', onclick: selectEvent},
		{title: 'Grab event', onclick: grabEvent},
		{title: 'Paste event', onclick: pasteEvent}
	]);
	
	contextmenu.bind(tilesetcanvas, 'tileset', function (offx, offy) {
		var tile = tileReachable(offx, offy, tilesetcanvas);
		if(!tile) {
			return false;
		}
		else {
			layermenu[0].node.className = layermenu[1].node.className = layermenu[2].node.className = currentLayer === 3 ? '' : 'disabled';
		}
	});
	
	contextmenu.bind(layercanvas, 'layer', function (offx, offy) {
		var tile = tileReachable(offx, offy, layercanvas);
		if(!tile) {
			return false;
		}
		else {
			layermenu[0].node.className = layermenu[1].node.className = layermenu[2].node.className = currentLayer === 3 ? '' : 'disabled';
		}
	});
	
	var helpStringSelect = levelpropertiesform.helpStringSelect;
	var helpStringEditor = levelpropertiesform.helpStringEditor;
	var helpStringPreviewCanvas = global.document.getElementById('helpStringPreviewCanvas');
	var hsc = helpStringPreviewCanvas.getContext('2d');
	var helpStringCharCount = global.document.getElementById('helpStringCharCount');
	
	var updateHelpStringPreview = function (e) {
			if(helpStringEditor.value.length > 512)
				helpStringEditor.value = helpStringEditor.value.substring(0, 512);
			helpStringCharCount.textContent = helpStringEditor.value.length;
			var lines = helpStringEditor.value.replace(/\@/g, "\n").trim().split("\n");
			var w = Math.max(jjFont.width(lines.join("@"), 1), 1);
			
			var h = lines.length*16;
			if(helpStringPreviewCanvas.width !== w)
				helpStringPreviewCanvas.width = w;
			if(helpStringPreviewCanvas.height !== h)
				helpStringPreviewCanvas.height = h;
			hsc.clearRect(0, 0, w, h);
			jjFont.draw(hsc, lines.join("@"), 0, 0, 1, 'left', 0);
		};
	
	(function () { // Prepare
		var sliders = global.document.querySelectorAll('#minlightslider, #startlightslider');
		var finetuning = global.document.querySelectorAll('#minlightnumber, #startlightnumber');
		var sliderChange = function (i) {
			return function (e) {
				finetuning[i].value = Math.round(this.value);
			};
		};
		var tuneBlur = function (i) {
			return function (e) {
				var value = Math.min(Math.max(Math.round(finetuning[i].value*0.64)/0.64, 0), 127);
				finetuning[i].value = Math.round(value);
				sliders[i].value = value;
			};
		};
		for(var i=0; i < sliders.length; i+=1) {
			sliders[i].addEventListener('change', sliderChange(i), false);
			finetuning[i].addEventListener('blur', tuneBlur(i), false);
		}
		animpropertiesform.animSpeedSlider.addEventListener('change', function (e) {
			animpropertiesform.animSpeedInput.value = parseInt(this.value, 10);
		}, false);
		animpropertiesform.animSpeedInput.addEventListener('blur', function (e) {
			animpropertiesform.animSpeedSlider.value = this.value = Math.max(0, Math.round(this.value));
		}, false);
		
		for(i=0; i < 8; i+=1) {
			layerpropertiesform.editlayer.add(new Option((i+1)+": "+layernames[i], i), null);
		}
		
		var oldId = 0;
		var changeHelpStringID = function () {
			var id = helpStringSelect.selectedIndex;
			helpStringEditor.value = tmpHelpStrings[id].replace(/\@/g, "\n");
			updateHelpStringPreview();
			oldId = id;
		};
		
		helpStringSelect.addEventListener('change', changeHelpStringID, false);
		helpStringSelect.addEventListener('keyup', changeHelpStringID, false);
		helpStringEditor.addEventListener('input', updateHelpStringPreview, false);
		helpStringEditor.addEventListener('blur', function (e) {
			tmpHelpStrings[helpStringSelect.selectedIndex] = helpStringEditor.value.replace(/\n/g, "@").substring(0, 512);
		}, false);
		
		var keys = ['#', '§', '|'];
		var hsKeys = global.document.getElementById('helpStringKeys');
		var node;
		var hsKeyClick = function (i) {
			var key = keys[i];
			return function (e) {
				var fullText = helpStringEditor.value;
				var insertPos = helpStringEditor.selectionStart;
				helpStringEditor.value = fullText.substring(0, insertPos) + key + fullText.substring(insertPos, fullText.length);
				helpStringEditor.focus();
				helpStringEditor.selectionStart += 1;
				updateHelpStringPreview();
			};
		};
		for(var i=0; i < keys.length; i+=1) {
			node = global.document.createElement('input');
			node.type = 'button';
			node.value = keys[i];
			node.addEventListener('click', hsKeyClick(i), false);
			hsKeys.appendChild(node);
		}
		
	})();
	
	var trimNull = function (str) {
		var str = str.replace(/^\0\0*/, ''),
			 ws = /\0/,
			 i = str.length;
		while (ws.test(str.charAt(--i)));
		return str.slice(0, i + 1);
	};
	
	var unpackStruct = function (formatCodes, arr) {
		var bufStr = '';
		var byteArray = new Uint8Array(arr);
		for(var i=0, l = byteArray.length; i < l; i+=1) {
			bufStr += String.fromCharCode(byteArray[i]);
		}
		var buffer = new DataView(byteArray.buffer);
		var offset = 0;
		var output = {};
		var part;
		for(var i=0; i < formatCodes.length; i+=1) {
			var len = formatCodes[i][2] === undefined ? 1 : Math.max(1, parseInt(formatCodes[i][2], 10));
			var name = formatCodes[i][1];
			var type = [
				formatCodes[i][0].substring(0, 1),
				parseInt(formatCodes[i][0].substring(1), 10)
			];
			var doArray = len > 1;
			var bufFunc = 'get' + (type[0].toLowerCase()==='u'? 'Ui':'I') + 'nt' + type[1];
			if(doArray) {
				output[name] = [];
			}
			for(var j=0; j < len; j+=1) {
				if(type[0].toLowerCase() === 'c') {
					part = bufStr.substring(offset, offset+type[1]);
					if(type[0] === 'C') {
						part = trimNull(part);
					}
					offset += type[1];
				}
				else if(type[0].toLowerCase() === 'u' || type[0].toLowerCase() === 's') {
					part = buffer[bufFunc](offset, true);
					offset += type[1]/8; // number of bytes, not bits
				}
			
				if(doArray) {
					output[name][j] = part;
				}
				else {
					output[name] = part;
				}
			}
		
		}
		return output;
	};
	var packStruct = function (struct, data) {
		'use strict';
		var binaryData = [];
		var dataCursor = 0;
		struct.forEach(function (type, i) {
			var repeats = 1;
			if(typeof type === 'object') {
				repeats = type[1];
				type = type[0];
			}
			type = [
				type.substring(0, 1),
				parseInt(type.substring(1), 10)
			];
			var bufFunc = (type[0].toLowerCase()==='u'? 'Ui':'I') + 'nt' + type[1] + 'Array';
			for(var j=0; j < repeats; j++) {
				if(type[0].toLowerCase() === 'c') {
					var part = data[dataCursor].substring(0, type[1]);
					var pad = type[0] === 'C' ? 0x00 : 0x20;
					for(var k=0; k < type[1]; k++) {
						binaryData.push(part.charCodeAt(k) || pad);
					}
				}
				else if(type[0].toLowerCase() === 'u' || type[0].toLowerCase() === 's') {
					Array.prototype.slice.call(new Uint8Array(new global[bufFunc]([data[dataCursor]]).buffer), 0).forEach(function (x) {
						binaryData.push(x);
					});
				}
				dataCursor++;
			}
		});
		return new Uint8Array(binaryData).buffer;
	};
	
	var getBinary = function (code, buf) {
		var type = [
			code.substring(0, 1),
			parseInt(code.substring(1), 10)
		];
		buf = new Uint8Array(buf);
		var bufFunc = (type[0].toLowerCase()==='u'? 'Ui':'I') + 'nt' + type[1] + 'Array';
		return new global[bufFunc](buf.buffer)[0];
	};
	
	var hasLoadedApp = false;
	var incDecClient = function (doInc) {
		var X = new XMLHttpRequest();
		X.open("POST", '/node/?'+(doInc?'inc':'dec')+'client', false);
		X.send();
	};
	global.addEventListener('load', function () {
		incDecClient(true);
		hasLoadedApp = true;
	}, false);
	global.addEventListener('unload', function () {
		if(!hasLoadedApp) {
			incDecClient(true);
		}
		incDecClient(false);
	}, false);
	global.addEventListener('beforeunload', function () {
		if(!socket) {
			return "Are you sure you want to exit?";
		}
	}, false);
	
	var XHR2 = function (uri, data, callback, progress) {
		var X = new XMLHttpRequest();
		X.open("POST", uri, true);
		var fd = new FormData();
		for (var i in data) {
			if(data.hasOwnProperty(i)) {
				fd.append(i, data[i]);
			}
		}
		if(typeof callback === 'function') {
			X.addEventListener('load', function (e) {
				callback(X);
			}, false);
		}
		if(typeof progress === 'function') {
			X.upload.addEventListener('progress', function (e) {
				progress(e);
			});
		}
		X.send(fd);
		return X;
	};
	
	var readFileHeaderInfo = function (callback) {
		var X = new XMLHttpRequest();
		X.open("GET", '/node/?getheader', true);
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				callback(true, []);
			}
			else {
				callback(false, JSON.parse(X.response));
			}
		});
		X.send();
	};
	
	var readFile = function (filename, callback, returnBuffer) {
		var X = new XMLHttpRequest();
		X.open("GET", '/node/?file='+encodeURIComponent(filename), true);
		X.responseType = 'arraybuffer';
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				callback(true, returnBuffer? new Uint8Array(0) : '');
			}
			else {
				var data = new Uint8Array(X.response);
				if(returnBuffer) {
					callback(false, data);
				}
				else {
					var file = '';
					for(var i=0, l = data.length; i < l; i+=1) {
						file += String.fromCharCode(data[i]);
					}
					callback(false, file);
				}
			}
		});
		X.send();
	};
	var getFileList = function (callback) {
		var X = new XMLHttpRequest();
		X.open("GET", '/node/?files', true);
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				callback(true, []);
			}
			else {
				callback(false, JSON.parse(X.response));
			}
		});
		X.send();
	};
	var parseFile = function (filename, callback, onprogr) {
		var X = new XMLHttpRequest();
		var st = Date.now();
		X.open("GET", '/node/?parse='+encodeURIComponent(filename), true);
		//X.overrideMimeType("text/plain; charset=x-user-defined");
		X.responseType = 'arraybuffer';
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				callback(true, new Uint8Array([]));
			}
			else {
				var data = new Uint8Array(X.response);
				/*for(var i=0; i < X.response.length; i+=1) {
					data[i] = X.response.charCodeAt(i) & 0xFF;
				}*/
				console.log('Parsed the file in '+(Date.now() - st)/1000+'s');
				callback(false, data);
			}
		});
		var maxProgr = 0;
		if(typeof onprogr === 'function') {
			X.addEventListener('progress', function (e) {
				maxProgr = Math.max(maxProgr, e.loaded/e.total);
				onprogr(maxProgr);
			});
		}
		X.send();
	};
	var uploadAndParse = function (form, callback) {
		var X = new XMLHttpRequest();
		var st = Date.now();
		X.open("POST", '/node/?parsedata', true);
		X.overrideMimeType("text/plain; charset=x-user-defined");
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				callback(true, '');
			}
			else {
				var data = new Uint8Array(X.response.length);
				for(var i=0; i < X.response.length; i+=1) {
					data[i] = X.response.charCodeAt(i) & 0xFF;
				}
				console.log('Uploaded and parsed the file in '+(Date.now() - st)/1000+'s');
				callback(false, data);
			}
		});
		X.send(form);
	};
	var uploadAndSave = function (blob, filename, options, callback) {
		var X = new XMLHttpRequest();
		var st = Date.now();
		X.open("POST", '/node/?savelevel', true);
		X.overrideMimeType("text/plain; charset=x-user-defined");
		var fd = new FormData();
		fd.append('filedata', blob);
		fd.append('filename', filename);
		fd.append('doRun', !!options.run);
		fd.append('doSave', !!options.save);
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				callback(true, '');
			}
			else {
				var data = new Uint8Array(X.response.length);
				for(var i=0; i < X.response.length; i+=1) {
					data[i] = X.response.charCodeAt(i) & 0xFF;
				}
				console.log('Uploaded and saved the file in '+(Date.now() - st)/1000+'s');
				callback(false, data);
			}
		});
		X.send(fd);
	};
	
	var defaultJCSini = "";
	var loadJCSini = function (data) {
		data = (data || defaultJCSini).split("\n");
		var i, l = data.length;
		var matches;
		var lastTitle = "";
		var obj = {};
		var tmp;
		for(i=0; i < l; i+=1) {
			data[i] = data[i].trim();
			if(data[i] === "" || data[i][0] === ";") continue;
			matches = data[i].match(/^\[(.*)\]$/);
			if(matches !== null) {
				lastTitle = matches[1];
				obj[lastTitle] = [];
				continue;
			}
			matches = data[i].match(/^(.*)=(.*)$/);
			tmp = matches[2].split("|");
			if(tmp.length === 4 && lastTitle === 'Events') {
				tmp.push("");
			}
			tmp.forEach(function (v, k, a) {
				v = v.trim();
				if(k >= 5 && lastTitle === 'Events') {
					v = v.split(":");
					v[1] = +v[1];
				}
				a[k] = v;
			});
			tmp.id = +matches[1];
			obj[lastTitle][+matches[1]] = tmp;
		}
		JCSini = obj;
	};

	(function () {
		var X = new XMLHttpRequest();
		X.open("GET", 'JCS.ini', true);
		X.addEventListener('load', function (e) {
			if(X.status !== 200) {
				defaultJCSini = oldJCSini;
			}
			else {
				defaultJCSini = X.responseText;
			}
			loadJCSini();
		});
		X.send();
	}());
	
	
	
	var XYid = function (x, y, w) {
		return x+(y*w);
	};
	var idXY = function (tileID, w) {
		var divBy=1;
		if(tileID>=w) {divBy=w;}
		return [(tileID%w), (tileID-(tileID%w))/divBy];
	};
	
	var loadlevel = function (file, filename, auto) {
		Popup.open('loadinglevel');
		var levloadprogr = global.document.querySelector("#levelloadingprogress");
		global.document.querySelector("#loadlevelname").innerText = filename;
		levloadprogr.style.width = "0%";
		level(file, function (data) { // Successful read
			for(var i in data) {
				if(data.hasOwnProperty(i))
					J2L[i] = data[i];
			}
			/*for(i=211; i < J2L.Streams[0].length; i+=512) {
				console.log((i-211)/512, J2L.Streams[0].substr(i, 512).replace(/\|/g, ""));
			}*/
			//console.log(J2L, J2L.Streams[0].substr(J2L.LEVEL_INFO.normalStreamLength+512, 512).split("\0"), J2L.Streams[0].substr(J2L.LEVEL_INFO.normalStreamLength+512, 512).split("\0").length);
			J2L.fileName = filename;
			var pos, i, l = J2L.EVENTS.length, lw = J2L.LEVEL_INFO.LayerWidth[3];
			/*for(i=0; i < l; i+=1) {
				pos = idXY(i, lw);
				J2L.LEVEL[3][pos[0]][pos[1]].event = J2L.EVENTS[i];
			}*/
			//delete J2L.EVENTS;
			changeLayer(J2L.LEVEL_INFO.SecEnvAndLayer & 15);
			scrollbars.layers.scroll = [J2L.LEVEL_INFO.JcsHorizontal, J2L.LEVEL_INFO.JcsVertical];
			global.document.title = "Jazz Creation Station - "+J2L.LEVEL_INFO.LevelName+" - "+J2L.fileName;
			Popup.hide();
			levloadprogr.style.width = "0%";
			var si = 0;
			alphaSort(tilesets);
			var l = tilesets.length;
			for(var i=0; i < l; i+=1) {
				si+=1;
				if(tilesets[i].name.toLowerCase() === data.LEVEL_INFO.Tileset.toLowerCase()) {
					break;
				}
			}
			if(i === l) {
				si = 0;
			}
			if(si === 0 && data.LEVEL_INFO.Tileset !== "") {
				alert("Couldn't find tileset \""+data.LEVEL_INFO.Tileset+"\"");
			}
			else {
				changeTileset(0, auto);
				changeTileset(si, auto);
			}
			animSelection =  false;
			updateEventPointers();
		}, function (x) { // Progress update
			var p = Math.round(x*100)+"%";
			levloadprogr.style.width = p;
		}, function (err) { // Error occured
			Popup.hide();
			levloadprogr.style.width = "0%";
			alert("Error: Couldn't read level!\n"+err);
		});
	};
	
	var loadtileset = function (filename) {
		Popup.open('loadingtileset');
		var si = selectTileset.selectedIndex;
		var tileloadprogr = global.document.querySelector("#tilesetloadingprogress");
		global.document.querySelector("#loadtilesetname").innerText = selectTileset.options[si].textContent;
		tileloadprogr.style.width = "0%";
		parseFile(filename, function (err, file) { // Successful read
			var firstFive = unpackStruct([['c5']], file.subarray(0, 5));
			if(firstFive === 'error') {
				alert('Error parsing J2T file, it\'s corrupted');
				Popup.hide();
				tileloadprogr.style.width = "0%";
			}
			if(err) {
				alert('Error reading '+filename);
				Popup.hide();
				tileloadprogr.style.width = "0%";
			}
			var headerSize = 262;
			var headerStruct = [
				['c180', 'Copyright'],
				['c4',   'Identifier'],
				['u32',  'Signature'],
				['C32',  'TilesetName'],
				['u16',  'Version'],
				['u32',  'FileSize'],
				['s32',  'Checksum'],
				['u32',  'StreamSizes', 8]
			];
			
			var header = unpackStruct(headerStruct, file.subarray(0, headerSize));
			if(header.Identifier !== 'TILE') alert('Not a tileset');
			/*if(header.Checksum !== crc32(file.subarray(headerSize))) {
				console.error('Error: J2T file corrupt; Checksum error');
			}*/
			var isTSF = header.Version === 0x201;
			var maxTiles = isTSF ? 4096 : 1024;
			
			var filestr = '';
			for(var i=0, l = file.length; i < l; i+=1) {
				filestr += String.fromCharCode(file[i]);
			}
			
			var offset = headerSize;
			var streams = [];
			for(var i=0; i < 4; i+=1) {
				streams[i] = filestr.substring(offset, offset+header.StreamSizes[i*2+1]);
				streams[i] = streams[i].split('');
				for(var j=0, l = streams[i].length; j < l; j+=1) {
					streams[i][j] = streams[i][j].charCodeAt(0) & 0xFF;
				}
				streams[i] = new Uint8Array(streams[i]);
				offset += header.StreamSizes[i*2+1];
			}
			var tilesetInfoStruct = [
				['u32', 'Palette', 256],
				['u32', 'TileCount'],
				['u8',  'EmptyTile', maxTiles],
				['u8',  'Unknown1', maxTiles],
				['u32', 'ImageAddress', maxTiles],
				['u32', 'Unknown2', maxTiles],
				['u32', 'TMaskAddress', maxTiles],
				['u32', 'Unknown3', maxTiles],
				['u32', 'MaskAddress', maxTiles],
				['u32', 'FMaskAddress', maxTiles]
			];
			var tilesetInfo = unpackStruct(tilesetInfoStruct, streams[0]);
			J2T = {};
			J2T.Palette = tilesetInfo.Palette;
			var tilesetImage = streams[1];
			var tilesetMask = streams[3];
			J2T.tilesetMask = tilesetMask;
			J2T.maskAddress = tilesetInfo.MaskAddress;
			tileloadprogr.style.width = "66%";
			setTimeout(function () {
				var tilesetname = header.TilesetName;
				
				var tilesetcanvas = global.document.createElement("canvas");
				var tilec = tilesetcanvas.getContext("2d");
				tilesetcanvas.width = 320;
				var tilecount = tilesetInfo.TileCount;
				var tileh = tilesetcanvas.height = 32*Math.ceil(tilecount/10);
				
				var imgdata = tilec.createImageData(32, 32);
				var imgd = imgdata.data;
				var maxTiles = isTSF ? 4096 : 1024;
				var tilecache = [];
				
				var i, j, x, y, tile, index, color, pos, cachepos, masked, mbyte, pixpos;
				
				for (i=0; i < tilecount; i+=1) {
					tile = tilesetInfo.ImageAddress[i];
					if(tile === 0) { continue; }
					pos = idXY(i, 10);
					if(tilecache[tile] !== undefined) {
						cachepos = idXY(tilecache[tile], 10);
						tilec.drawImage(tilesetcanvas, cachepos[0]*32, cachepos[1]*32, 32, 32, pos[0]*32, pos[1]*32, 32, 32);
					}
					else {
						tilecache[tile] = i;
						for(j=0; j < 4096 /* 32x32x4 */; j+=4) {
							index = tilesetImage[tile+j/4];
							if(index > 1) {
								color = tilesetInfo.Palette[index];
								imgd[j + 0] = color & 0xFF;
								imgd[j + 1] = (color >> 8) & 0xFF;
								imgd[j + 2] = (color >> 16) & 0xFF;
								imgd[j + 3] = 255;
							}
							else {
								imgd[j + 3] = 0;
							}
						}
						tilec.putImageData(imgdata, pos[0]*32, pos[1]*32);
					}
				}
				tile_url = tilesetcanvas.toDataURL("image/png");
				tileloadprogr.style.width = "83%";
				setTimeout(function () {
					tilec.clearRect(0, 0, 320, tileh);
					tilecache = [];
					for (i=0; i < tilecount; i+=1) {
						tile = tilesetInfo.MaskAddress[i];
						if(tile === 0) { continue; }
						pos = idXY(i, 10);
						if(tilecache[tile] !== undefined) {
							cachepos = idXY(tilecache[tile], 10);
							tilec.drawImage(tilesetcanvas, cachepos[0]*32, cachepos[1]*32, 32, 32, pos[0]*32, pos[1]*32, 32, 32);
						}
						else {
							tilecache[tile] = i;
							for (x=0; x < 128; x+=1) {
								mbyte = tilesetMask[tile+x];
								for (y=0; y < 8; y+=1) {
									color = 0;
									if(tile > 0) {
										masked = (mbyte & Math.pow(2, y)); //bit value
										if(masked > 0) {
											color = 255;
										}
									}
									pixpos = ((x * 8) + y)*4; //bit index, for position
									imgd[pixpos + 0] = 0;
									imgd[pixpos + 1] = 0;
									imgd[pixpos + 2] = 0;
									imgd[pixpos + 3] = color;
								}
							}
							tilec.putImageData(imgdata, pos[0]*32, pos[1]*32);
						}
					}
					mask_url = tilesetcanvas.toDataURL("image/png");
					tilesetcanvas.width = 1;
					tilesetcanvas.height = 1;
					var totalSize = tile_url.length + mask_url.length;
			
					currentTileset.src = tile_url;
					currentTilesetMask.src = mask_url;
					tileloadprogr.style.width = "100%";
					setTimeout(function () {
						Popup.hide();
						tileloadprogr.style.width = "0%";
					}, 300);
				}, 300);
			}, 300);
			
		}, function (x) { // Progress update
			tileloadprogr.style.width = Math.round(x*50)+"%";
		});
	};
	
	var checkIfLayerIsUsed = function (l) {
		if(l === 3) return 1;
		var i, j;
		var w = J2L.LEVEL_INFO.LayerWidth[l];
		var h = J2L.LEVEL_INFO.LayerHeight[l];
		for(i=0; i < w; i+=1) {
			for(j=0; j < h; j+=1) {
				if(J2L.LEVEL[l][i][j].id > 0 || J2L.LEVEL[l][i][j].animated) {
					return 1;
				}
			}
		}
		return 0;
	};
	
	var updateDynamicTileCache = function () {
		//var st = Date.now();
		setTimeout(function () {
			updateDynamicTileCache();
		}, 3*1000);
		if(!toggleTileCacheVar) return;
		var isTSF = false;
		var version = isTSF?0x203:0x202;
		var staticTiles = (isTSF?4096:1024) - J2L.ANIMS.length;
		var MAX_TILES = (isTSF?4096:1024);
		
		var map = [], tmp, hasAnimAndEvent, tile;
		var jcslw, lw, lh, realWidth;
		var l, i, j, k, animEventComboCount = 0;
		
		for(l = 0; l < 8; l+=1) {
			map[l] = [];
			//J2L.LEVEL_INFO.IsLayerUsed[l] = checkIfLayerIsUsed(l);
			//if(J2L.LEVEL_INFO.IsLayerUsed[l] === 0) continue;
			jcslw = J2L.LEVEL_INFO.LayerWidth[l];
			lw = jcslw*(J2L.LEVEL_INFO.LayerProperties[l] & 1 === 1? 4:1);
			lh = J2L.LEVEL_INFO.LayerHeight[l];
			realWidth = Math.ceil(jcslw/4)*4;
			for(j = 0; j < lh; j+=1) {
				for(i = 0; i < realWidth; i+=4) {
					tmp = [0, 0, 0, 0];
					hasAnimAndEvent = false;
					for(k = 0; k < 4; k+=1) {
						if(i+k < jcslw) {
							tile = J2L.LEVEL[l][i+k][j];
							tmp[k] = tile.id + (tile.flipped?MAX_TILES:0) + (tile.animated?staticTiles:0);
							if(l === 3 && tile.animated && J2L.EVENTS[i+k+j*lw] > 0) {
								hasAnimAndEvent = true;
							}
						}
					}
					map[l].push({
						str: tmp.join(','), // Make it to a string
						newWord: hasAnimAndEvent
					});
				}
			}
		}
		
		var dict = {'0,0,0,0': 0}; // Initialize dictionary with the empty word
		var tileCache = [];
		var lastDictId = 1;
		for(i=0; i < 8; i+=1) {
			tileCache[i] = [];
			for(j=0; j < map[i].length; j+=1) {
				if(dict[map[i][j].str] === undefined || map[i][j].newWord) {
					dict[map[i][j].str] = lastDictId++;
				}
				
				tileCache[i].push(dict[map[i][j].str]);
			}
		}
		dynamicTileCache = tileCache;
		//console.log("Updated dynamic tilecache in", (Date.now() - st)/1000, "s");
	};
	
	var writeLevel = function (options) {
		Popup.open("saving");
		var saveprogress = global.document.querySelector("#savingprogress");
		var saving = global.document.querySelector("#saving");
		saveprogress.style.width = "0%";
		global.document.querySelector("#savinglevelname").textContent = trimNull(J2L.LEVEL_INFO.LevelName);
		
		var copyright = "                      Jazz Jackrabbit 2 Data File\r\n\r\n"+
	                    "         Retail distribution of this data is prohibited without\r\n"+
	                    "             written permission from Epic MegaGames, Inc.\r\n\r\n\x1A";
		var isTSF = false;
		var version = isTSF?0x203:0x202;
		var i, j, k, w, h;
		var staticTiles = (isTSF?4096:1024) - J2L.ANIMS.length;
		var MAX_TILES = (isTSF?4096:1024);
		var streams = [0, 0, 0, 0];
		var streamSizes = [0, 0, 0, 0];
		
		var map = [];
		var tmp;
		var l;
		/*var viewBytes = function (b) {
			var i, s = "";
			for(i=0; i < b.length; i+=1) {
				s += b[i].toString(16)+" ";
			}
			console.log(s);
		};*/
		var realWidth;
		var hasAnimAndEvent;
		var animEventComboCount = 0;
		var tilesetNeedsFlipped = {};
		var animNeedsFlip = {};
		var tile;
		
		for(l = 0; l < 8; l+=1) {
			J2L.LEVEL_INFO.IsLayerUsed[l] = checkIfLayerIsUsed(l);
			map[l] = [];
			J2L.LEVEL_INFO.JJ2LayerWidth[l] = J2L.LEVEL_INFO.LayerWidth[l]*(J2L.LEVEL_INFO.LayerProperties[l] & 1 === 1? 4:1);
			if(J2L.LEVEL_INFO.IsLayerUsed[l] === 0) continue;
			w = J2L.LEVEL_INFO.JJ2LayerWidth[l];
			h = J2L.LEVEL_INFO.LayerHeight[l];
			var tileW = 1;
			realWidth = Math.ceil(w/4)*4;
			for(j = 0; j < h; j+=1) {
				for(i = 0; i < realWidth; i+=4) {
					tmp = [0, 0, 0, 0];
					hasAnimAndEvent = false;
					for(k = 0; k < 4; k+=1) {
						if(i+k < w) {
							tile = J2L.LEVEL[l][(i+k) % (J2L.LEVEL_INFO.LayerWidth[l])][j];
							
							tmp[k] = tile.id + (tile.flipped?MAX_TILES:0) + (tile.animated?staticTiles:0);
							
							if(l === 3 && tile.animated && J2L.EVENTS[i+k+j*w] > 0) {
								hasAnimAndEvent = true;
							}
							if(tile.flipped && !tile.animated) {
								tilesetNeedsFlipped[tile.id] = true;
							}
							else if(tile.flipped) {
								animNeedsFlip[tile.id] = true;
							}
						}
					}
					
					map[l].push({
						str: tmp.join(','), // Make it to a string
						newWord: hasAnimAndEvent
					});
				}
			}
		}
		
		var dict = {'0,0,0,0': 0};
		var tileCache = [];
		var lastDictId = 1;
		var dictionary = ['0,0,0,0'];
		for(i=0; i < 8; i+=1) {
			for(j=0; j < map[i].length; j+=1) {
				if(dict[map[i][j].str] === undefined || map[i][j].newWord) {
					dict[map[i][j].str] = lastDictId++;
				}
				tileCache.push(dict[map[i][j].str]);
				dictionary[dict[map[i][j].str]] = map[i][j].str.split(",");
			}
		}
		
		
		var words = new Uint16Array(dictionary.length*4);
		var wordOffset = 0;
		for(i=0; i < dictionary.length; i++) {
			tmp = dictionary[i];//.split(",");
			for(k = 0; k < 4; k+=1) {
				words[wordOffset+k] = +tmp[k];
			}
			wordOffset+=4;
		}
		
		
		streamSizes[2] = words.byteLength;
		
		streams[2] = words.buffer;
		streamSizes[2] = streams[2].byteLength;
		
		var dictstream = new Uint16Array(tileCache.length);
		for(i = 0; i < tileCache.length; i+=1) {
			dictstream[i] = tileCache[i];
		}
		
		
		streams[3] = dictstream.buffer;
		streamSizes[3] = streams[3].byteLength;
		
		var animOffset = 8813+MAX_TILES*7;
		var streamLength = animOffset + 128*137;
		var arbuf = new ArrayBuffer(streamLength);
		var buf = new DataView(arbuf);
		buf.setUint16(0, ~~(scrollbars.layers.scroll[0]), true); // JcsHorizontal
		buf.setUint16(4, ~~(scrollbars.layers.scroll[1]), true); // JcsVertical
		buf.setUint8(8, currentLayer, true); // SecEnvAndLayer
		buf.setUint8(9, J2L.LEVEL_INFO.MinimumAmbient);
		buf.setUint8(10, J2L.LEVEL_INFO.StartingAmbient);
		buf.setUint16(11, J2L.ANIMS.length, true);
		buf.setUint8(13, J2L.LEVEL_INFO.SplitScreenDivider);
		buf.setUint8(14, J2L.LEVEL_INFO.IsItMultiplayer);
		buf.setUint32(15, streamLength, true);
		J2L.LEVEL_INFO.Tileset = selectTileset.options[selectTileset.selectedIndex] !== undefined ? selectTileset.options[selectTileset.selectedIndex].value:"";
		if(J2L.LEVEL_INFO.LevelName === "") {
			J2L.LEVEL_INFO.LevelName = "Untitled";
		}
		for(i=0; i < 32; i+=1) {
			buf.setUint8(19+i, J2L.LEVEL_INFO.LevelName.charCodeAt(i) || 0);
			buf.setUint8(19+i+32*1, J2L.LEVEL_INFO.Tileset.charCodeAt(i) || 0);
			buf.setUint8(19+i+32*2, J2L.LEVEL_INFO.BonusLevel.charCodeAt(i) || 0);
			buf.setUint8(19+i+32*3, J2L.LEVEL_INFO.NextLevel.charCodeAt(i) || 0);
			buf.setUint8(19+i+32*4, J2L.LEVEL_INFO.SecretLevel.charCodeAt(i) || 0);
			buf.setUint8(19+i+32*5, J2L.LEVEL_INFO.MusicFile.charCodeAt(i) || 0);
		}
		for(i=0; i < J2L.LEVEL_INFO.HelpString.length; i+=1) {
			for(j=0; j < J2L.LEVEL_INFO.HelpString[i].length; j+=1) {
				buf.setUint8(211+512*i+j, J2L.LEVEL_INFO.HelpString[i].charCodeAt(j) || " ");
			}
		}
		for(i=0; i < 8; i+=1) {
			buf.setUint32(8403+i*4, J2L.LEVEL_INFO.LayerProperties[i], true);
			/* 8 unknowns */
			buf.setUint8 (8403+8*4+8+i, J2L.LEVEL_INFO.IsLayerUsed[i]);
			buf.setUint32(8403+8*4+8+8+i*4, J2L.LEVEL_INFO.LayerWidth[i], true);
			buf.setUint32(8403+8*4+8+8+8*4+i*4, J2L.LEVEL_INFO.JJ2LayerWidth[i], true);
			buf.setUint32(8403+8*4+8+8+8*4+8*4+i*4, J2L.LEVEL_INFO.LayerHeight[i], true);
			buf.setInt32(8403+8*4+8+8+8*4+8*4+8*4+i*4, i*100-300, true);
			/* 18*4 unknowns */
			buf.setInt32(8403+8*4+8+8+8*4+8*4+8*4+8*4+18*4+i*4, J2L.LEVEL_INFO.LayerXSpeed[i]*65536, true);
			buf.setInt32(8403+8*4+8+8+8*4+8*4+8*4+8*4+18*4+8*4+i*4, J2L.LEVEL_INFO.LayerYSpeed[i]*65536, true);
			buf.setInt32(8403+8*4+8+8+8*4+8*4+8*4+8*4+18*4+8*4+8*4+i*4, J2L.LEVEL_INFO.LayerAutoXSpeed[i]*65536, true);
			buf.setInt32(8403+8*4+8+8+8*4+8*4+8*4+8*4+18*4+8*4+8*4+8*4+i*4, J2L.LEVEL_INFO.LayerAutoYSpeed[i]*65536, true);
			/* 8 unknowns */
		}
		for(i=0; i < 3; i+=1) {
			// Ugly...
			buf.setUint8(8787+3*0+i, J2L.LEVEL_INFO.LayerRGB1[i]);
			buf.setUint8(8787+3*1+i, J2L.LEVEL_INFO.LayerRGB2[i]);
			buf.setUint8(8787+3*2+i, J2L.LEVEL_INFO.LayerRGB3[i]);
			buf.setUint8(8787+3*3+i, J2L.LEVEL_INFO.LayerRGB4[i]);
			buf.setUint8(8787+3*4+i, J2L.LEVEL_INFO.LayerRGB5[i]);
			buf.setUint8(8787+3*5+i, J2L.LEVEL_INFO.LayerRGB6[i]);
			buf.setUint8(8787+3*6+i, J2L.LEVEL_INFO.LayerRGB7[i]);
			buf.setUint8(8787+3*7+i, J2L.LEVEL_INFO.LayerRGB8[i]);
		}
		buf.setUint16(8811, staticTiles, true);
		
		var frames = 0;
		for(i=0; i < J2L.ANIMS.length; i+=1) {
			buf.setUint16(animOffset + i*137, J2L.ANIMS[i].FramesBetweenCycles, true);
			buf.setUint16(animOffset + i*137 + 2, J2L.ANIMS[i].RandomAdder, true);
			buf.setUint16(animOffset + i*137 + 4, J2L.ANIMS[i].PingPongWait, true);
			buf.setUint8(animOffset + i*137 + 6, J2L.ANIMS[i].IsItPingPong);
			buf.setUint8(animOffset + i*137 + 7, J2L.ANIMS[i].FPS);
			frames = Math.min(J2L.ANIMS[i].Tiles.length, 64);
			buf.setUint8(animOffset + i*137 + 8, frames);
			for(j=0; j < frames; j+=1) {
				buf.setUint16(animOffset + i*137 + 9 + j*2, J2L.ANIMS[i].Tiles[j].id + (J2L.ANIMS[i].Tiles[j].flipped?MAX_TILES:0) + (J2L.ANIMS[i].Tiles[j].animated?staticTiles:0), true);
				if(animNeedsFlip[i] === true && !J2L.ANIMS[i].Tiles[j].animated) {
					tilesetNeedsFlipped[J2L.ANIMS[i].Tiles[j].id] = true;
				}
				else if(animNeedsFlip[i] === true) {
					animNeedsFlip[J2L.ANIMS[i].Tiles[j].id] = true;
				}
				if(J2L.ANIMS[i].Tiles[j].flipped && !J2L.ANIMS[i].Tiles[j].animated) {
					tilesetNeedsFlipped[J2L.ANIMS[i].Tiles[j].id] = true;
				}
				else if(J2L.ANIMS[i].Tiles[j].flipped) {
					animNeedsFlip[J2L.ANIMS[i].Tiles[j].id] = true;
				}
				//tilesetNeedsFlipped[J2L.ANIMS[i].Tiles[j].id] = true;
			}
		}
		
		
		for(i=1; i < staticTiles; i+=1) {
			buf.setUint32(8813+i*4, J2L.TilesetProperties.TileEvent[i], true);
			buf.setUint8(8813+4*MAX_TILES+i, tilesetNeedsFlipped[i] === true ? 1 : 0); // Flipped tiles in tileset, so JJ2 know what flipped tiles to load from flipped tileset image ;D
			buf.setUint8(8813+MAX_TILES*4+MAX_TILES+i, J2L.TilesetProperties.TileType[i], true);
			/* skip MAX_TILES bytes */
		}
		
		/*var levelinfo = "";
		for(i=0; i < arbuf.byteLength; i+=1) {
			levelinfo += String.fromCharCode(buf.getUint8(i));
		}*/
		streams[0] = arbuf;
		streamSizes[0] = streams[0].byteLength;
		
		w = J2L.LEVEL_INFO.LayerWidth[3];
		h = J2L.LEVEL_INFO.LayerHeight[3];
		
		arbuf = new ArrayBuffer(w*h*4);
		buf = new DataView(arbuf);
		for(i=0; i < w; i+=1) {
			for(j=0; j < h; j+=1) {
				buf.setUint32((i+(j*w))*4, J2L.EVENTS[i+w*j] || 0, true);
			}
		}
		/*var events = "";
		for(i=0; i < arbuf.byteLength; i+=1) {
			events += String.fromCharCode(buf.getUint8(i));
		}*/
		streams[1] = arbuf;
		streamSizes[1] = streams[1].byteLength;
		
		arbuf = new ArrayBuffer(262);
		buf = new DataView(arbuf);
		for(i=0; i < 180; i+=1) {
			buf.setUint8(i, copyright.charCodeAt(i));
		}
		buf.setInt32(180+3, 0xBABE, false); // Password
		var identifier = "LEVL";
		for(i=0; i < 4; i+=1) {
			buf.setUint8(180+i, identifier.charCodeAt(i));
		}
		buf.setUint8(180+4+3, J2L.HEADER_INFO.HideLevel);
		for(i=0; i < J2L.HEADER_INFO.LevelName.length; i+=1) {
			buf.setUint8(180+4+3+1+i, J2L.HEADER_INFO.LevelName.charCodeAt(i) || 0);
		}
		var allBuffers = new Uint8Array(streamSizes[0]+streamSizes[1]+streamSizes[2]+streamSizes[3]);
		var offset = 0;
		for(i=0; i < 4; i+=1) {
			allBuffers.set(new Uint8Array(streams[i]), offset);
			offset+= streamSizes[i];
		}
		buf.setUint16(180+4+3+1+32, version, true);
		//buf.setUint32(180+4+3+1+32+2, 262+allBuffers.length, true);
		//buf.setInt32(180+4+3+1+32+2+4, crc32(allBuffers), true); // CRC32 of level
		for(i=0; i < 4; i+=1) {
			//buf.setUint32(230+i*8, 0, true);
			buf.setUint32(230+i*8+4, streamSizes[i], true);
		}
		
		/*var bb = new BlobBuilder();
		bb.append(arbuf); // Header
		bb.append(allBuffers.buffer); // Streams
		var blob = bb.getBlob("application/octet-stream");*/

		var blob = new Blob([new Uint8Array(arbuf), allBuffers]);

		uploadAndSave(blob, J2L.fileName, {run: !!options.run, save: !!options.save, newfile: !!options.newfile}, function (err, data) {
			if(!err && data.length > 0) {
				/*var bb = new BlobBuilder();
				bb.append(data.buffer);
				var blob = bb.getBlob("application/octet-stream");*/
				var blob = new Blob([data]);
				//webkitURL.createObjectURL(blob)
				saveAs(blob, J2L.fileName);
			}
			Popup.hide();
		});
		/*fs.root.getFile(J2L.fileName, {create: true}, function (fileEntry) {
			fileEntry.createWriter(function (fileWriter) {
				fileWriter.onwriteend = function(e) {
					Popup.hide();
					if(confirm("Download saved level?")) {
						global.document.location = fileEntry.toURL();
					}
				};
				fileWriter.write(blob);
			}, fileSystemError);
		}, fileSystemError);*/
		
	};
	
	var updateTiles = function (layer, startX, startY, maxw, maxh, selection, includeEmpty) {
		var oldSelection = [];
		var lw = J2L.LEVEL_INFO.LayerWidth[layer];
		var lh = J2L.LEVEL_INFO.LayerHeight[layer];
		var isIdenticalCount = 0;
		var totalCount = 0;
		for(var x = 0; x < maxw; x+=1) {
			oldSelection[x] = [];
			for(var y = 0; y < maxh; y+=1) {
				oldSelection[x][y] = {};
				if(x+startX < lw && y+startY < lh) {
					oldSelection[x][y] = {
						'id': 0,
						'animated': false,
						'flipped': false,
						'event': 0
					};
					oldSelection[x][y] = {
						'id': J2L.LEVEL[layer][x+startX][y+startY].id,
						'animated': J2L.LEVEL[layer][x+startX][y+startY].animated,
						'flipped': J2L.LEVEL[layer][x+startX][y+startY].flipped,
						'event': layer === 3 ? J2L.EVENTS[x+startX + lw * (y+startY)] || 0 : 0
					};
					if(selection[x] && selection[x][y] && (includeEmpty || selection[x][y].id > 0 || selection[x][y].animated || (selection.length===1 && selection[x].length === 1))) {
						J2L.LEVEL[layer][x+startX][y+startY] = {'id': selection[x][y].id, 'animated': selection[x][y].animated, 'flipped': selection[x][y].flipped};
						if(layer === 3) {
							J2L.EVENTS[x+startX + lw * (y+startY)] = selection[x][y].event || 0;
						}
						if(oldSelection[x][y].id === selection[x][y].id &&
						   oldSelection[x][y].animated === selection[x][y].animated &&
						   oldSelection[x][y].flipped === selection[x][y].flipped &&
						   oldSelection[x][y].event === J2L.EVENTS[x+startX + lw * (y+startY)]) {
							isIdenticalCount++;
						}
						totalCount++;
					}
				}
			}
		}
		return isIdenticalCount === totalCount ? false : oldSelection;
	};
	
	/*var freeRef = function (obj) {
		var chi, cou, len, nam;
		chi = obj.childNodes;
		if(chi) {
			len = chi.length;
			for (cou = 0; cou < len; cou++) {
				freeRef(obj.childNodes[cou]);
			}
		}
		chi = obj.attributes;
		if(chi) {
			len = chi.length;
			for(cou = 0; cou < len; cou++) {
				nam = chi[cou].name;
				if(typeof(obj[nam]) === 'function') {
					obj[nam] = null;
				}
			}
		}
	};*/
	
	var mousedowndrag = function (e) {
		if(e.which === 1) {
			var node = this.parentNode.parentNode.parentNode;
			var nodebounding = node.getBoundingClientRect();
			this.offsetY = (e.pageY - nodebounding.top);
			this.isDragging = true;
		}
		chatInput.blur();
		e.preventDefault();
		return false;
	};
	var mousedownlayers = function (e) {
		var layerbounding = layercanvas.getBoundingClientRect();
		var offsetX = e.pageX - layerbounding.left;
		var offsetY = e.pageY - layerbounding.top;
		mousedownscroll = [scrollbars.layers.scroll[0], scrollbars.layers.scroll[1]];
		if(e.which === 1) {
			layercanvas.isDragging = true;
		}
		else if(e.which === 2) {
			layercanvas.isPanning = true;
		}
		
		var w = layerdiv.offsetWidth;
		var h = layerdiv.offsetHeight;
		
		if(mouseOnScrollbars('layers', w, h, offsetX, offsetY, e)) {
			
		}
		else {
			winmove(e);
		}
		chatInput.blur();
		e.preventDefault();
	};
	var mousedownanims = function (e) {
		var animsbounding = animscanvas.getBoundingClientRect();
		var offsetX = e.pageX /*- animsbounding.left*/;
		var offsetY = e.pageY - animsbounding.top;
		animscanvas.isDragging = true;
		var btn = e.which;
		
		var w = animsdiv.offsetWidth;
		var h = animsdiv.offsetHeight;
		if(mouseOnScrollbars('anims', w, h, offsetX, offsetY, e)) {
			
		}
		else if(btn === 1) {
			var offx = e.pageX /*- animscanvas.offsetLeft*/ + scrollbars.anims.scroll[0];
			var offy = e.pageY - animsbounding.top + scrollbars.anims.scroll[1];
			var anx = 0;
			var any = Math.floor(offy/32);
			if(e.pageX > 32+4) anx = Math.floor((offx-4)/32);
			if(!holdingCtrlKey) {
				animSelection =  false;
				if((any <= J2L.ANIMS.length && anx === 0) || (J2L.ANIMS[any] && anx-2 < J2L.ANIMS[any].Frames)) {
					animSelection = [anx, any];
				}
				if(anx === 0 && any < J2L.ANIMS.length) {
					selectedTiles = [[{'id': any, 'animated': true, 'flipped': false, 'event': 0}]];
					selectedSource = 'anims';
				}
			}
			else if(holdingCtrlKey && anx === 0 && any < J2L.ANIMS.length) {
				var id = any;
				var addWhere;
				if(animSelection !== false) {
					if(animSelection[0] === 0 && animSelection[1] === J2L.ANIMS.length) {
						J2L.ANIMS.push({
							'FramesBetweenCycles': 0,
							'RandomAdder': 0,
							'PingPongWait': 0,
							'IsItPingPong': 0,
							'FPS': 10,
							'Frames': 0,
							'Tiles': []
						});
						if(socket && socket.readyState === 1) {
							sendUpdate({
								what: 'anims',
								type: 'new'
							});
						}
					}
					addWhere = J2L.ANIMS[animSelection[1]].Frames;
					if(animSelection[0] > 0) {
						addWhere = animSelection[0]-1;
					}
					J2L.ANIMS[animSelection[1]].Tiles.splice(addWhere, 0, {'id': id, 'animated': true, 'flipped': holdingFlipKey});
					J2L.ANIMS[animSelection[1]].Frames+=1;
					if(socket && socket.readyState === 1) {
						sendUpdate({
							what: 'anims',
							type: 'frame',
							anim: animSelection[1],
							pos: addWhere,
							tile: {'id': id, 'animated': true, 'flipped': holdingFlipKey}
						});
					}
				}
			}
		}
		else if(btn === 3) {
			var offy = e.pageY - animsbounding.top + scrollbars.anims.scroll[1];
			var any = Math.floor(offy/32);
			
			if(e.pageX < 32 && any < J2L.ANIMS.length) {
				animpropertiesform.animID.value = any;
				animpropertiesform.animSpeedSlider.value = animpropertiesform.animSpeedInput.value = J2L.ANIMS[any].FPS;
				animpropertiesform.animFrameWait.value = J2L.ANIMS[any].FramesBetweenCycles;
				animpropertiesform.animRandomAdder.value = J2L.ANIMS[any].RandomAdder;
				animpropertiesform.animPingPongWait.value = J2L.ANIMS[any].PingPongWait;
				animpropertiesform.animPingPong.set(!!J2L.ANIMS[any].IsItPingPong);
				
				
				Popup.open('animproperties');
			}
		}
		chatInput.blur();
		e.preventDefault();
		return false;
	};
	var moveAnim = function (updown) {
		return function (e) {
			if(animSelection !== false && animSelection[0] === 0 && animSelection[1] < J2L.ANIMS.length && animSelection[1]+updown < J2L.ANIMS.length && animSelection[1]+updown > -1) {
				var tmpHolder = J2L.ANIMS[animSelection[1]];
				J2L.ANIMS[animSelection[1]] = J2L.ANIMS[animSelection[1]+updown];
				J2L.ANIMS[animSelection[1]+updown] = tmpHolder;
				if(socket && socket.readyState === 1) {
					sendUpdate({
						what: 'anims',
						type: 'move',
						anim: animSelection[1],
						by: updown
					});
				}
				animSelection[1]+=updown;
			}
		};
	};
	var delAnimFrame = function () {
		if(animSelection !== false && animSelection[1] < J2L.ANIMS.length && animSelection[0]-1 < J2L.ANIMS[animSelection[1]].Frames) {
			if(animSelection[0] > 0) {
				J2L.ANIMS[animSelection[1]].Frames-=1;
				J2L.ANIMS[animSelection[1]].Tiles.splice(animSelection[0]-1, 1);
				if(socket && socket.readyState === 1) {
					sendUpdate({
						what: 'anims',
						type: 'delFrame',
						anim: animSelection[1],
						frame: animSelection[0]-1
					});
				}
			}
			if(animSelection[0] === 0 || J2L.ANIMS[animSelection[1]].Frames === 0) {
				J2L.ANIMS.splice(animSelection[1], 1);

[preview ends here]