<!DOCTYPE html>
<!-- @MineKhan version Alpha 0.8.2
 
	NOTE:
The "Save" button saves your world to your local browser. Nobody else can see it. However, if you're on a public or shared computer, that save may be deleted unexpectedly. So it's a good idea to keep a safe copy of your save string somewhere where it won't be deleted.
 
To share your save with other people, copy your world string into the "loadString" variable on line 293.
 
	Controls:
 
 * Mouse Right-click (or ctrl + left-click): place block
 * Mouse Left-click: Remove block
 * Mouse Middle-click: Pick block
 * Q: Sprint
 * W-A-S-D: Walk around
 * Shift: Sneak
 * E: Open/Close inventory
 * B: Toggle super (B)reaker
 * H: Toggle (H)yper builder
 * Z: Zoom
 * L: Toggle Spectator mode
 * Arrow Keys: Look around
 * P or Esc: Pause/unpause
 * 1-9: Navigate hotbar
 * Spacebar: Jump
 * Double jump: Toggle flight
 * Shift (flying): Fly down
 * Space (flying): Fly up
 * T: Open chat
 * ; (semicolon): Release the pointer without pausing (for screenshots)
 
	Notes and accreditation:
 * This program (MineKhan) was made by Willard (me). The original is https://www.khanacademy.org/cs/mc/5647155001376768 (just adding this so that spin-offs have it)
 * Phi helped impliment VAOs and entities and split the project into multiple files via GitHub. https://www.khanacademy.org/profile/
 * Zushah helped me with some of the menus in the 0.6 update, and added the flower models via GitHub. https://www.khanacademy.org/profile/zushah77
 * Element118 helped speed up the process of adding new textures significantly. https://www.khanacademy.org/profile/element118
 * Danielkshin made an old main menu background. https://www.khanacademy.org/profile/danielkshin
 
 * This program originated as a spin-off of ScusCraft by ScusX.
 * It's 99% different code at this point, but I still never would have made it this far without his code for reference.
 * ScusCraft can be found here: https://www.khanacademy.org/computer-programming/scuscraft-3d/5145400332058624
 * My original spin-off can be found here: https://www.khanacademy.org/computer-programming/high-performance-minecraft/5330738124357632
 * While porting the code from PJS to a webpage, I copied the PJS source code for a few of their helper functions from here: https://raw.githubusercontent.com/Khan/processing-js/master/processing.js
 * The textures are 100% copied from real Minecraft, and are the property of Mojang and Microsoft with whom I have no association.
 * The real Minecraft game can be bought and downloaded at https://www.minecraft.net/en-us/
 
	To-Do:
 * A lot. Check out the GitHub repo if you'd like to collaborate on this.
 * https://github.com/Willard21/MineKhan
 
 
	How it works:
 
The graphics in this project are done using WebGL, which is a web implementation of OpenGL ES 2 (Open Graphics Library Embedded Systems 2).
It allows us to write shader programs that run right on the GPU, which means it can run as fast as any C++ game.
The infamous P3D mode in Processing.js uses WebGL behind the scenes, but it does it so inefficiently that it might as well not even being using the GPU.
 
To properly utilize WebGL like this program does, you need to make efficient use of buffers, which are arrays containing data on all the triangles that need to be rendered.
Ideally you should be rendering less than 100 buffers per frame for the slowest computers to get a good framerate.
To achieve this, you can store all the textures in 1 very big texture called a "texture atlas". Then, when you're constructing the buffer, you specify which part of the texture atlas to use for each triangle you're drawing.
My buffers have 8 values for each vertex: X, Y, Z, textureX, textureY, ambient occlusion, sky light, and block light.
I use 1 buffer per 16x16 chunk. I use "frustum culling" to only render chunks that are visible on the screen.
I learned WebGL from https://webglfundamentals.org and just by playing around with it.
-->
 
<html lang="en-us">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<meta name="description" content="Play creative mode Minecraft right from your browser!">
		<meta name="keywords" content="Khan Academy, Programs, Search">
		<meta property="og:title" content="MineKhan"/>
		<meta property="og:description" content="Play creative mode Minecraft right from your browser!"/>
		<meta property="og:image" content="https://www.khanacademy.org/computer-programming/minekhan/5647155001376768/latest.png"/>
		<title>MineKhan</title>
		<link rel="shortcut icon" type="image/ico" href="https://willard.fun/assets/minekhan.ico">
		<style>
.hidden {
	display: none !important;
}
.corner {
	position: absolute;
	top: 0;
	left: 0;
}
#overlay {
	background-size: cover;
	background-position: center;
	/* background-image: url('./background.webp'); -- Added and removed in the JS */
}
body {
	overflow: hidden; /* Hide scrollbars */
	background-color: black;
}
canvas:focus {
	outline: none;
}
 
.world-select {
	width: 99vw;
	min-width: 300px;
	height: calc(100vh - 220px);
	position: absolute;
	bottom: 120px;
	overflow-y: auto;
	background-color: RGBA(0, 0, 0, 0.6);
	justify-content: center;
	margin: 0 auto;
	font-family: monospace;
}
.world {
	width: 250px;
	height: auto;
	border: 1px solid black;
	font-size: 18px;
	font-family: 'Courier New', Courier, monospace;
	color: rgb(180, 180, 180);
	margin: 0 auto;
	margin-top: 15px;
	padding: 5px;
	cursor: pointer;
}
strong {
	color: white;
}
.selected {
	border: 3px solid white;
	padding: 3px;
}
input[type=text] {
	background-color: black;
	caret-color: white;
	border: 2px solid gray;
	color: white;
	font-size: 24px;
	padding-left: 12px;
	font-family: monospace;
}
input[type=text]:focus {
	border: 2px solid lightgray;
}
#boxcentertop {
	z-index: 1;
	width: 80vw;
	max-width: 400px;
	height: 50px;
	position: relative;
	top: 30px;
	display: block;
	margin: 0 auto;
}
#onhover {
	background-color: rgba(0, 0, 0, 0.9);
	color: rgb(200, 200, 200);
	font-family: 'Courier New', Courier, monospace;
	word-wrap: normal;
	width: auto;
	max-width: 400px;
	position: absolute;
	z-index: 11;
	padding: 10px;
	cursor: default;
	user-select: none;
}
#quota {
	display: block;
	position: absolute;
	width: 99vw;
	margin: 0 auto;
	bottom: 110px;
	z-index: 1;
	background-color: RGBA(0, 0, 0, 0.6);
	justify-content: center;
	text-align: center;
	color: white;
	font-family: monospace;
}
#chat {
	position: absolute;
	left: 0px;
	top: 100px;
	height: calc(100vh - 200px);
	overflow-y: auto;
	overflow-x: hidden;
	padding-right: 20px;
	width: 40vw;
	min-width: 600px;
	background-color: RGBA(0, 0, 0, 0.8);
	color: white;
	font-family: monospace;
}
#chat > div > span {
	white-space: pre-wrap;
	margin: 0;
	padding: 0;
}
#chatbar {
	position: absolute;
	left: 30px;
	bottom: 0px;
	height: 20;
	width: calc(100vw - 60px);
	background-color: RGBA(0, 0, 0, 0.8);
	color: white;
	font-family: monospace;
}
.message {
	width: 100%;
	background-color: transparent;
	padding: 10px;
	word-wrap: break-word;
}
#background-text {
	position: absolute;
	left: 0;
	top: 0;
	width: 100vw;
	height: 100vh;
	z-index: -10;
}
#loading-text {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	text-align: center;
	color: #fff;
	font-size: 30px;
	font-family: monospace;
}
#inv-container {
	position: absolute;
	left: 50%;
	transform: translate(-50%, 0);
	z-index: 1;
}
#inv-scroll {
	height: calc(100vh - (min(100vh, 100vw) / 3.25));
	overflow-y: auto;
	border-bottom: solid 2px lightgray;
}
#hotbar {
	position: absolute;
	left: 50%;
	transform: translate(-50%, 0);
	bottom: 10px;
	z-index: 0;
}
#webgl-canvas {
	z-index: -1;
}
 
		</style>
	</head>
	<body>
		<!-- Text so that Lighthouse believes the page is loaded and to provide search terms. -->
		<div id="background-text">
			<h1>MineKhan</h1>
			<p>Play MineKhan online! This game is a free Minecraft clone developed on <a href="https://www.khanacademy.org/computer-programming/minekhan/5647155001376768">Khan Academy</a>, and later moved to my own website so that I could take advantage of multiplayer servers. The front-end code is completely open source and maintained on <a href="https://github.com/Willard21/MineKhan">GitHub</a>, where contributions are welcome.</p>
		</div>
		<canvas id="overlay" tabindex="0" width="600" height="600" class="corner background"></canvas>
		<canvas id="webgl-canvas" class="corner"></canvas>
		<input type="text" id="savebox" class="hidden" spellcheck="false" style="position: absolute; top: 10px; left: 10px; z-index: 1;">
		<input type="text" id="boxcentertop" class="hidden" spellcheck="false">
		<div id="quota" class="hidden"></div>
		<div id="onhover" class="hidden"></div>
		<div id="chat" class="hidden"></div>
		<input id="chatbar" class="hidden" list="commands">
		<datalist id="commands">
			<option>/ban</option>
			<option>/online</option>
		</datalist>
		<p id="savedirections" class="hidden" style="position: absolute; top: 40px; left: 10px; z-index: 1; background-color: rgba(255, 255, 255, 0.3);">
			To share your world on KA, copy/paste the saveString<br>
			from this box into the code on line 293.<br>
			var loadString = "Your Code Here";<br>
			Then save the program as a Spin-off.
		</p>
		<div class="world-select hidden" id="worlds"></div>
		<p id="message" class="hidden" style="position: absolute; top: 10px; right: 10px; z-index: 1; text-align: right; background-color: rgba(255, 255, 255, 0.3);"></p>
		<span class="hidden" id="loading-text"></span>
		<div id="inv-container" class="hidden">
			<div id="inv-scroll">
				<!-- This is very very important: canvases need that tabindex thing to capture keyboard events. Idk why. But I spent hours trying to figure it out. -->
				<canvas id="container" tabindex="0"></canvas>
			</div>
			<canvas id="inventory" tabindex="0"></canvas>
		</div>
		<canvas id="hotbar" class="hidden"></canvas>
		<script type="application/javascript">
			// Khan Academy instructions:
			// Place save code here to load your world. Make extra sure you got it copied so you don't paste in the wrong thing and delete your world on accident lol
			window.parent.loadString = ""
			window.parent.exports = {}
 
			/*
				/cs/pro/5733417664643072
				This comment ^ is to hide spin-offs from the hotlist.
				The hostlist is intended to show off impressive coding ability, not gaming ability.
				If you've actually coded something on this and want people to see it, you'll need to remove this comment before saving it for the first time.
				I would encourage you to re-add the comment after saving so spin-offs of your spin-off are also hidden by default.
				(Personally I don't mind just seeing builds on the hostlist, but they're off-topic and I know it annoys other people.)
				<script>
			*/
		</script>
		<script id="src/shaders/blockVert.glsl" type="x-shader/x-vertex">
attribute vec3  aVertex;
attribute vec2  aTexture;
attribute float aShadow;
attribute float aSkylight;
attribute float aBlocklight;
varying vec2  vTexture;
varying float vShadow;
varying float vFog;
varying vec3 vPosition;
uniform mat4 uView;
uniform float uDist;
uniform vec3 uPos;
uniform float uTime;
uniform float uLantern;
 
mat4 no_translate (mat4 mat) {
	mat4 nmat = mat;
	nmat[3].xyz = vec3(0.0);
 
	return nmat;
}
 
void main() {
	vPosition = uPos - aVertex;
	vTexture = aTexture;
 
	gl_Position = uView * vec4(aVertex, 1.0);
	float worldLight = max(aSkylight * uTime, aBlocklight);
	float dynamicLight = max(worldLight, uLantern - length(uPos - aVertex) / 10.0);
 
	vShadow = aShadow * min(dynamicLight * 0.9 + 0.1, 1.0);
 
	float range = 8.0;//clamp(uDist / 5.0, 8.0, 24.0);
	vFog = clamp((length(uPos.xz - aVertex.xz) - uDist + range) / range, 0.0, 1.0);
}
		</script>
		<script id="src/shaders/blockFrag.glsl" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
	precision highp float;
#else
	precision mediump float;
#endif
 
uniform sampler2D uSampler;
uniform float uTime;
uniform bool uTrans;
uniform vec3 uSky; // The horizon color
uniform vec3 uSun; // The sun position
varying float vShadow;
varying vec2 vTexture;
varying float vFog;
varying vec3 vPosition;
 
const vec3 skyColor = vec3(0.25, 0.45, 0.7);
void main(){
	vec3 dir = normalize(vPosition);
	float horizonal = 1.0 - abs(dir.y);
	float sunDot = dot(dir, uSun);
	vec4 sky = vec4(mix(skyColor, uSky, horizonal * horizonal * (sunDot * 0.5 + 1.2)) * uTime, 1.0);
		// * max(smoothstep(-0.5, 0.2, uTime), 0.1);
 
	vec4 color = texture2D(uSampler, vTexture);
	gl_FragColor = mix(vec4(color.rgb * vShadow, color.a), sky, vFog);
	if (!uTrans && color.a != 1.0 || uTrans && (color.a == 1.0 || color.a == 0.0)) discard;
}
		</script>
		<script id="src/shaders/blockVertFogless.glsl" type="x-shader/x-vertex">
attribute vec3  aVertex;
attribute vec2  aTexture;
attribute float aShadow;
attribute float aSkylight;
attribute float aBlocklight;
varying vec2  vTexture;
varying float vShadow;
uniform mat4 uView;
uniform vec3 uPos;
uniform float uTime;
uniform float uLantern;
uniform float uZoffset;
 
mat4 no_translate (mat4 mat) {
	mat4 nmat = mat;
	nmat[3].xyz = vec3(0.0);
 
	return nmat;
}
 
void main() {
	vTexture = aTexture;
	gl_Position = uView * vec4(aVertex, 1.0);
	gl_Position.z += uZoffset;
 
	float dist = length(uPos - aVertex);
	float worldLight = max(aSkylight * uTime, aBlocklight);
	float dynamicLight = max(worldLight, uLantern - dist / 10.0);
	vShadow = aShadow * min(dynamicLight * 0.9 + 0.1, 1.0);
}
		</script>
		<script id="src/shaders/blockFragFogless.glsl" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
	precision highp float;
#else
	precision mediump float;
#endif
 
uniform sampler2D uSampler;
uniform bool uTrans;
varying float vShadow;
varying vec2 vTexture;
 
void main(){
	vec4 color = texture2D(uSampler, vTexture);
	gl_FragColor = vec4(color.rgb * vShadow, color.a);
 
	if (!uTrans && gl_FragColor.a != 1.0 || uTrans && gl_FragColor.a == 1.0) discard;
}
		</script>
		<script id="src/shaders/2dVert.glsl" type="x-shader/x-vertex">
attribute vec3 aVertex;
attribute vec2 aTexture;
attribute float aShadow;
varying vec2 vTexture;
varying float vShadow;
uniform vec2 uOffset;
uniform mat4 uView;
 
void main() {
	vTexture = aTexture;
	vShadow = aShadow;
	vec4 pos;
	if (uOffset.x != 0.0) {
		pos = uView * vec4(aVertex, 1.0);
	}
	else {
		pos.xy = aVertex.xy;
		pos.z = 0.5;
		pos.w = 1.0;
	}
 
	gl_Position = vec4(pos.x + uOffset.x, pos.y + uOffset.y, pos.z, 1.0);
}
		</script>
		<script id="src/shaders/2dFrag.glsl" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
	precision highp float;
#else
	precision mediump float;
#endif
 
uniform sampler2D uSampler;
varying vec2 vTexture;
varying float vShadow;
 
void main() {
	vec4 color = texture2D(uSampler, vTexture);
	if (color.a > 0.0 && color.a < 0.7) color.a = 0.7;
	gl_FragColor = vec4(color.rgb * vShadow, color.a);
}
		</script>
		<script id="src/shaders/entityVert.glsl" type="x-shader/x-vertex">
attribute vec3  aVertex;
attribute vec2  aTexture;
varying vec2  vTexture;
uniform mat4 uView;
 
void main() {
	vTexture = aTexture;
	gl_Position = uView * vec4(aVertex, 1.0);
}
		</script>
		<script id="src/shaders/entityFrag.glsl" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
	precision highp float;
#else
	precision mediump float;
#endif
 
uniform sampler2D uSampler;
uniform float uLightLevel;
varying vec2 vTexture;
 
void main(){
	vec4 color = texture2D(uSampler, vTexture);
	gl_FragColor = vec4(color.rgb * uLightLevel, color.a);
}
		</script>
		<script id="src/workers/Caves.js" type="text/js">
const worker = async () => {
	// Originally this stuff was generated in code
	const GRADIENTS_3D = new Int8Array([-11,4,4,-4,11,4,-4,4,11,11,4,4,4,11,4,4,4,11,-11,-4,4,-4,-11,4,-4,-4,11,11,-4,4,4,-11,4,4,-4,11,-11,4,-4,-4,11,-4,-4,4,-11,11,4,-4,4,11,-4,4,4,-11,-11,-4,-4,-4,-11,-4,-4,-4,-11,11,-4,-4,4,-11,-4,4,-4,-11])
	const POSITIONS = [-1,180,216,528,624,-1,912,288,144,360,252,816,-1,-1,720,216,-1,-1,72,960,-1,-1,912,36,144,360,0,816,-1,480,576,72,324,-1,144,-1,432,-1,624,36,-1,288,108,576,-1,864,-1,180,252,36,144,672,-1,-1,-1,108,-1,-1,396,-1,-1,-1,432,360,252,36,324,-1,768,-1,528,396,0,-1,252,480,-1,672,-1,360]
	const DATA = [0,0,0,0,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-1,-255,0,1,255,0,-1,0,-255,1,0,255,0,0,0,0,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-255,-1,0,255,1,0,0,-1,-255,0,1,255,0,0,0,0,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-255,0,-1,255,0,1,0,-255,-1,0,255,1,0,0,0,0,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-4/3,-4/3,-255.33333333333334,1,1,255,0,0,0,0,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-2/3,-5/3,1,0,1,-4/3,-255.33333333333334,-4/3,1,255,1,0,0,0,0,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-2/3,-5/3,-5/3,0,1,1,-255.33333333333334,-4/3,-4/3,255,1,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-3,-2,-1,2,1,0,-2,-3,-1,1,2,0,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-3,-1,-2,2,0,1,-2,-1,-3,1,0,2,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-1,-3,-2,0,2,1,-1,-2,-3,0,1,2,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-4/3,-1/3,-1/3,1,0,0,-8/3,-2/3,-2/3,2,0,0,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-1/3,-4/3,-1/3,0,1,0,-2/3,-8/3,-2/3,0,2,0,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-1/3,-1/3,-4/3,0,0,1,-2/3,-2/3,-8/3,0,0,2,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,0,0,0,0,0,0,-4/3,-255.33333333333334,-4/3,1,255,1,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,0,0,0,0,0,0,-255.33333333333334,-4/3,-4/3,255,1,1,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,0,0,0,0,0,0,-4/3,-4/3,-255.33333333333334,1,1,255,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-2/3,-2/3,-8/3,0,0,2,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-8/3,-2/3,-2/3,2,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-2,-2,-2,1,1,1,-2/3,-8/3,-2/3,0,2,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-4/3,-255.33333333333334,-4/3,1,255,1,-2/3,-2/3,-8/3,0,0,2,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-4/3,-255.33333333333334,-4/3,1,255,1,-8/3,-2/3,-2/3,2,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-255.33333333333334,-4/3,-4/3,255,1,1,-2/3,-2/3,-8/3,0,0,2,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-255.33333333333334,-4/3,-4/3,255,1,1,-2/3,-8/3,-2/3,0,2,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-4/3,-4/3,-255.33333333333334,1,1,255,-8/3,-2/3,-2/3,2,0,0,-4/3,-1/3,-1/3,1,0,0,-1/3,-4/3,-1/3,0,1,0,-1/3,-1/3,-4/3,0,0,1,-5/3,-5/3,-2/3,1,1,0,-5/3,-2/3,-5/3,1,0,1,-2/3,-5/3,-5/3,0,1,1,-4/3,-4/3,-255.33333333333334,1,1,255,-2/3,-8/3,-2/3,0,2,0]
	let SPHERE = new Int16Array([-529, -528, -527, -513, -512, -511, -497, -496, -495, -289, -288, -287, -274, -273, -272, -271, -270, -258, -257, -256, -255, -254, -242, -241, -240, -239, -238, -225, -224, -223, -33, -32, -31, -18, -17, -16, -15, -14, -2, -1, 0, 1, 2, 14, 15, 16, 17, 18, 31, 32, 33, 223, 224, 225, 238, 239, 240, 241, 242, 254, 255, 256, 257, 258, 270, 271, 272, 273, 274, 287, 288, 289, 495, 496, 497, 511, 512, 513, 527, 528, 529])
 
	let data, positions, perm, perm3D, caves, gradients3D, sphere
	const seedNoise = (seed, buffer) => {
		positions = new Int32Array(buffer, 0, 80)
		data = new Float64Array(buffer, positions.byteLength, DATA.length)
		const source = new Uint8Array(buffer, data.byteOffset + data.byteLength, 256)
		perm = new Uint8Array(buffer, source.byteOffset + source.byteLength, 256)
		perm3D = new Uint8Array(buffer, perm.byteOffset + perm.byteLength, 256)
		gradients3D = new Int8Array(buffer, perm3D.byteOffset + perm3D.byteLength, GRADIENTS_3D.length)
		caves = new Uint8Array(buffer, gradients3D.byteOffset + gradients3D.byteLength, 16 * 16 * 82)
		sphere = new Int16Array(buffer, caves.byteOffset + caves.byteLength, SPHERE.length)
 
		sphere.set(SPHERE)
		positions.set(POSITIONS)
		data.set(DATA)
		gradients3D.set(GRADIENTS_3D)
 
		for (let i = 0; i < 256; i++) source[i] = i
		for (let i = 0; i < 3; i++) {
			seed = seed * 1664525 + 1013904223 | 0
		}
		for (let i = 255; i >= 0; i--) {
			seed = seed * 1664525 + 1013904223 | 0
			let r = (seed + 31) % (i + 1)
			if (r < 0) r += i + 1
			perm[i] = source[r]
			perm3D[i] = perm[i] % 24 * 3
			source[r] = source[i]
		}
	}
 
	// This is my compiled cave generation code. I wrote it in C. It includes my OpenSimplexNoise function, plus the logic to carve caves within the borders of the chunk it's operating on.
	const program = new Uint8Array(atob("AGFzbQEAAAABEQNgAABgA3x8fAF8YAJ/fwF/AwQDAAECBAUBcAEBAQUEAQEBAQcdBAZtZW1vcnkCAAFiAAAIZ2V0Q2F2ZXMAAgFkAQAMAQAKzwYDAwABC4YEAgR/CHxEAAAAAAAA8D8gASAAoCACoERVVVVVVVXFv6IiByABoCILIAucIguhIgqhIAcgAKAiDCAMnCIMoSIIoKohA0GACCgCAEQAAAAAAADwPyAHIAKgIgcgB5wiB6EiDaEiCSAKoKogA0EBdHIgCSAIoKpBAnRyIAggCqAgDaAiCapBA3RyIAkgDaCqQQV0ciAJIAqgqkEHdHIgCSAIoKpBCXRyQaOXvWlsQd/mu+MDakEBdkHQAHBBAnRqKAIAIgRBf0YEQEQAAAAAAAAAAA8LIAIgB6EgDCALoCAHoERVVVVVVVXVv6IiAqAhCSABIAuhIAKgIQ0gACAMoSACoCEOQQZBCCAEQbADSBshBkQAAAAAAAAAACEBA0BEAAAAAAAAAEAgDSAEQQN0IgMrA8gCoCIAIACiIA4gAysDwAKgIgIgAqKgIAkgAysD0AKgIgogCqKgoSIIRAAAAAAAAAAAZUUEQCAIIAiiIgggCKIgACADKwPoAiAHoKogAysD2AIgDKCqQf8BcUHAwwBqLQAAIAMrA+ACIAugqmpqQf8BcUHAxQBqLAAAIgNBwccAaiwAALeiIAIgA0HAxwBqLAAAt6KgIAogA0HCxwBqLAAAt6KgoiABoCEBCyAEQQZqIQQgBUEBaiIFIAZHDQALIAFEAqnkvCzicz+iRAAAAAAAAOA/oAvAAgIDfwN8QYjIAEEAQYCkAfwLAEGACCECA0ACQEQAAAAAAADgPyACQQR2QQ9xIgQgAGq3RHsUrkfhepQ/oiIFIAJBCHa3RHsUrkfhepQ/oiIGIAJBD3EiAyABardEexSuR+F6lD+iIgcQAaGZRLpJDAIrh3Y/Zg0ARAAAAAAAAOA/IAYgByAFEAGhmUS6SQwCK4d2P2YNAAJAIARBAmtBC0sNACADQQJJDQBBACEEIANBDUsNAANAIAIgBEEBdEGI7AFqLgEAaiIDQYjIAGotAABBAUcEQCADQQI6AIhICyAEQQFyIgNB0QBGDQIgAiADQQF0QYjsAWouAQBqIgNBiMgAai0AAEEBRwRAIANBAjoAiEgLIARBAmohBAwACwALIAJBiMgAakEBOgAACyACQQFqIgJBgKABRw0AC0GIyAAL").split("").map(c => c.charCodeAt(0))).buffer
 
	const wasm = await WebAssembly.instantiate(program)
 
	const exports = wasm.instance.exports
	const wasmCaves = exports.getCaves || exports.get_caves || exports.c
	const wasmMemory = exports.memory || exports.a
 
	self.onmessage = function(e) {
		if (e.data && e.data.seed) {
			if (exports.seed_noise) exports.seed_noise(e.data.seed)
			else seedNoise(e.data.seed, wasmMemory.buffer)
			self.postMessage(e.data)
		}
		if (e.data && e.data.caves) {
			const { x, z } = e.data
			const ptr = wasmCaves(x, z)
			// const buffer = wasmMemory.buffer.slice(ptr, ptr + 20992)
			const arr = new Int8Array(wasmMemory.buffer, ptr, 20992)
 
			let air = []
			let carve = []
			for (let i = 512; i < arr.length; i++) {
				if (arr[i] === 1) carve.push(i)
				else if (arr[i] === 2) air.push(i)
			}
			let airArr = new Uint16Array(air)
			let carveArr = new Uint16Array(carve)
 
			self.postMessage({
				air: airArr,
				carve: carveArr
			}, [airArr.buffer, carveArr.buffer])
		}
	}
}
worker()
		</script>
		<script id="src/js/random.js" type="application/javascript">
{const { imul, floor } = Math
 
// implementation of xxHash
const {
	seedHash,
	hash
} = (() => {
	// closure around mutable `seed`; updated via calls to `seedHash`
 
	let seed = Math.random() * 2100000000 | 0
 
	const PRIME32_2 = 1883677709
	const PRIME32_3 = 2034071983
	const PRIME32_4 = 668265263
	const PRIME32_5 = 374761393
 
	const seedHash = s => {
		seed = s | 0
	}
 
	const hash = (x, y) => {
		let h32 = 0
 
		h32 = seed + PRIME32_5 | 0
		h32 += 8
 
		h32 += imul(x, PRIME32_3)
		h32 = imul(h32 << 17 | h32 >> 32 - 17, PRIME32_4)
		h32 += imul(y, PRIME32_3)
		h32 = imul(h32 << 17 | h32 >> 32 - 17, PRIME32_4)
 
		h32 ^= h32 >> 15
		h32 *= PRIME32_2
		h32 ^= h32 >> 13
		h32 *= PRIME32_3
		h32 ^= h32 >> 16
 
		return h32 / 2147483647
	}
 
	return {
		seedHash,
		hash
	}
})()
 
class Marsaglia {
	// from http://www.math.uni-bielefeld.de/~sillke/ALGORITHMS/random/marsaglia-c
 
	nextInt() {
		const { z, w } = this
 
		this.z = 36969 * (z & 65535) + (z >>> 16) & 0xFFFFFFFF
		this.w = 18000 * (w & 65535) + (w >>> 16) & 0xFFFFFFFF
 
		return ((this.z & 0xFFFF) << 16 | this.w & 0xFFFF) & 0xFFFFFFFF
	}
 
	nextDouble() {
		const i = this.nextInt() / 4294967296
 
		const isNegative = i < 0 | 0 // cast to 1 or 0
 
		return isNegative + i
	}
 
	constructor(i1, i2) { // better param names
		this.z = i1 | 0 || 362436069
		this.w = i2 || hash(521288629, this.z) * 2147483647 | 0
	}
}
 
// The noise and random functions are copied from the processing.js source code
 
const {
	randomSeed,
	random
} = (() => {
	// closure around mut `currentRandom`
 
	let currentRandom = null
 
	const randomSeed = seed => {
		currentRandom = new Marsaglia(seed)
	}
 
	const random = (min, max) => {
		if (!max) {
			if (min) {
				max = min
				min = 0
			}
			else {
				min = 0
				max = 1
			}
		}
 
		return currentRandom.nextDouble() * (max - min) + min
	}
 
	return {
		randomSeed,
		random
	}
})()
 
class PerlinNoise {
	// http://www.noisemachine.com/talk1/17b.html
	// http://mrl.nyu.edu/~perlin/noise/
 
	static grad3d(i, x, y, z) {
		const h = i & 15 // convert into 12 gradient directions
 
		const u = h < 8
			? x
			: y
 
		const v = h < 4
			? y
			: h === 12 || h === 14
				? x
				: z
 
		return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v)
	}
 
	static grad2d(i, x, y) {
		const v = (i & 1) === 0
			? x
			: y
 
		return (i & 2) === 0
			? -v
			: v
	}
 
	static grad1d(i, x) {
		return (i & 1) === 0
			? -x
			: x
	}
 
	static lerp(t, a, b) {
		return a + t * (b - a)
	}
 
	// end of statics
 
	// prototype functions:
	noise3d(x, y, z) {
		const X = floor(x) & 0xff
		const Y = floor(y) & 0xff
		const Z = floor(z) & 0xff
 
		x -= floor(x)
		y -= floor(y)
		z -= floor(z)
 
		const fx = (3 - 2 * x) * x * x
		const fy = (3 - 2 * y) * y * y
		const fz = (3 - 2 * z) * z * z
 
		const { perm } = this
 
		const p0 = perm[X] + Y
		const p00 = perm[p0] + Z
		const p01 = perm[p0 + 1] + Z
		const p1 = perm[X + 1] + Y
		const p10 = perm[p1] + Z
		const p11 = perm[p1 + 1] + Z
 
		const { lerp, grad3d } = PerlinNoise
 
		return lerp(
			fz,
			lerp(
				fy,
				lerp(
					fx,
					grad3d(perm[p00], x, y, z),
					grad3d(perm[p10], x - 1, y, z)
				),
				lerp(
					fx,
					grad3d(perm[p01], x, y - 1, z),
					grad3d(perm[p11],x - 1, y - 1, z)
				)
			),
			lerp(
				fy,
				lerp(
					fx,
					grad3d(perm[p00 + 1], x, y, z - 1),
					grad3d(perm[p10 + 1], x - 1, y, z - 1)
				),
				lerp(
					fx,
					grad3d(perm[p01 + 1], x, y - 1, z - 1),
					grad3d(perm[p11 + 1], x - 1, y - 1, z - 1)
				)
			)
		)
	}
 
	noise2d(x, y) {
		const X = floor(x) & 0xff
		const Y = floor(y) & 0xff
 
		x -= floor(x)
		y -= floor(y)
 
		const { perm } = this
		const fx = (3 - 2 * x) * x * x
		const fy = (3 - 2 * y) * y * y
		const p0 = perm[X] + Y
		const p1 = perm[X + 1] + Y
 
		const { lerp, grad2d } = PerlinNoise
 
		return lerp(
			fy,
			lerp(
				fx,
				grad2d(
					perm[p0],
					x,
					y
				),
				grad2d(
					perm[p1],
					x - 1,
					y
				)
			),
			lerp(
				fx,
				grad2d(
					perm[p0 + 1],
					x,
					y - 1
				),
				grad2d(
					perm[p1 + 1],
					x - 1,
					y - 1
				)
			)
		)
	}
 
	constructor(seed) {
		if (seed === undefined) {
			throw new TypeError("A value for `seed` parameter was not provided to `PerlinNoise`")
		}
		// console.log("New noise generator with seed", seed)
 
		const rnd = new Marsaglia(seed)
 
		// generate permutation
		const perm = new Uint8Array(0x200)
		this.perm = perm
 
		// fill 0x0..0x100
		for (let i = 0; i < 0x100; ++i) {
			perm[i] = i
		}
 
		for (let i = 0; i < 0x100; ++i) {
			const j = rnd.nextInt() & 0xFF
			const t = perm[j]
			perm[j] = perm[i]
			perm[i] = t
		}
 
		// copy to avoid taking mod in perm[0]
		// copies from first half of array, into the second half
		perm.copyWithin(0x100, 0x0, 0x100)
	}
}
 
const noiseProfile = {
	generator: undefined,
	octaves: 4,
	fallout: 0.5,
	seed: undefined,
	noiseSeed(seed) {
		this.seed = seed
		this.generator = new PerlinNoise(noiseProfile.seed)
	},
	noise(x, y, z) {
		const { generator, octaves, fallout } = this
 
		let effect = 1,
			sum = 0
 
		for (let i = 0; i < octaves; ++i) {
			effect *= fallout
 
			const k = 1 << i
 
			let temp
			switch (arguments.length) {
				case 1: {
					temp = generator.noise1d(k * x)
					break
				} case 2: {
					temp = generator.noise2d(k * x, k * y)
					break
				} case 3: {
					temp = generator.noise3d(k * x, k * y, k * z)
					break
				}
			}
 
			sum += effect * (1 + temp) / 2
		}
 
		return sum
	}
}
 
// Copied and modified from https://github.com/blindman67/SimplexNoiseJS
const openSimplexNoise = (clientSeed) => {
	const toNums = function(s) {
		return s.split(",").map(function(s) {
			return new Uint8Array(s.split("").map(function(v) {
				return Number(v)
			}))
		})
	}
	const decode = function(m, r, s) {
		return new Int8Array(s.split("").map(function(v) {
			return parseInt(v, r) + m
		}))
	}
	const toNumsB32 = function(s) {
		return s.split(",").map(function(s) {
			return parseInt(s, 32)
		})
	}
	const NORM_3D = 1.0 / 206.0
	const SQUISH_3D = 1 / 3
	const STRETCH_3D = -1 / 6
	var base3D = toNums("0000110010101001,2110210120113111,110010101001211021012011")
	const gradients3D = decode(-11, 23, "0ff7mf7fmmfffmfffm07f70f77mm7ff0ff7m0f77m77f0mf7fm7ff0077707770m77f07f70")
	var lookupPairs3D = function() {
		return new Uint16Array(toNumsB32("0,2,1,1,2,2,5,1,6,0,7,0,10,2,12,2,41,1,45,1,50,5,51,5,g6,0,g7,0,h2,4,h6,4,k5,3,k7,3,l0,5,l1,5,l2,4,l5,3,l6,4,l7,3,l8,d,l9,d,la,c,ld,e,le,c,lf,e,m8,k,ma,i,p9,l,pd,n,q8,k,q9,l,15e,j,15f,m,16a,i,16e,j,19d,n,19f,m,1a8,f,1a9,h,1aa,f,1ad,h,1ae,g,1af,g,1ag,b,1ah,a,1ai,b,1al,a,1am,9,1an,9,1bg,b,1bi,b,1eh,a,1el,a,1fg,8,1fh,8,1qm,9,1qn,9,1ri,7,1rm,7,1ul,6,1un,6,1vg,8,1vh,8,1vi,7,1vl,6,1vm,7,1vn,6"))
	}
	var p3D = decode(-1, 5, "112011210110211120110121102132212220132122202131222022243214231243124213241324123222113311221213131221123113311112202311112022311112220342223113342223311342223131322023113322023311320223113320223131322203311322203131")
	const setOf = function(count) {
		var a = [], i = 0
		while (i < count) {
			a.push(i++)
		}
		return a
	}
	const doFor = function(count, cb) {
		var i = 0
		while (i < count && cb(i++) !== true);
	}
 
	const shuffleSeed = (seed,count) => {
		seed = seed * 1664525 + 1013904223 | 0
		count -= 1
		return count > 0 ? shuffleSeed(seed, count) : seed
	}
	const types = {
		_3D : {
			base : base3D,
			squish : SQUISH_3D,
			dimensions : 3,
			pD : p3D,
			lookup : lookupPairs3D,
		}
	}
 
	const createContribution = (type, baseSet, index) => {
		var i = 0
		const multiplier = baseSet[index ++]
		const c = { next : undefined }
		while(i < type.dimensions) {
			const axis = "xyzw"[i]
			c[axis + "sb"] = baseSet[index + i]
			c["d" + axis] = - baseSet[index + i++] - multiplier * type.squish
		}
		return c
	}
 
	const createLookupPairs = (lookupArray, contributions) => {
		var i
		const a = lookupArray()
		const res = new Map()
		for (i = 0; i < a.length; i += 2) {
			res.set(a[i], contributions[a[i + 1]])
		}
		return res
	}
 
	const createContributionArray = (type) => {
		const conts = []
		const d = type.dimensions
		const baseStep = d * d
		var k, i = 0
		while (i < type.pD.length) {
			const baseSet = type.base[type.pD[i]]
			let previous, current
			k = 0
			do {
				current = createContribution(type, baseSet, k)
				if (!previous) {
					conts[i / baseStep] = current
				}
				else {
					previous.next = current
				}
				previous = current
				k += d + 1
			} while(k < baseSet.length)
 
			current.next = createContribution(type, type.pD, i + 1)
			if (d >= 3) {
				current.next.next = createContribution(type, type.pD, i + d + 2)
			}
			if (d === 4) {
				current.next.next.next = createContribution(type, type.pD, i + 11)
			}
			i += baseStep
		}
		const result = [conts, createLookupPairs(type.lookup, conts)]
		type.base = undefined
		type.lookup = undefined
		return result
	}
 
	let temp = createContributionArray(types._3D)
	const lookup3D = temp[1]
	const perm = new Uint8Array(256)
	const perm3D = new Uint8Array(256)
	const source = new Uint8Array(setOf(256))
	var seed = shuffleSeed(clientSeed, 3)
	doFor(256, function(i) {
		i = 255 - i
		seed = shuffleSeed(seed, 1)
		var r = (seed + 31) % (i + 1)
		r += r < 0 ? i + 1 : 0
		perm[i] = source[r]
		perm3D[i] = perm[i] % 24 * 3
		source[r] = source[i]
	})
	base3D = undefined
	lookupPairs3D = undefined
	p3D = undefined
 
	return function(x, y, z) {
		const pD = perm3D
		const p = perm
		const g = gradients3D
		const stretchOffset = (x + y + z) * STRETCH_3D
		const xs = x + stretchOffset, ys = y + stretchOffset, zs = z + stretchOffset
		const xsb = floor(xs), ysb = floor(ys), zsb = floor(zs)
		const squishOffset	= (xsb + ysb + zsb) * SQUISH_3D
		const dx0 = x - (xsb + squishOffset), dy0 = y - (ysb + squishOffset), dz0 = z - (zsb + squishOffset)
		const xins = xs - xsb, yins = ys - ysb, zins = zs - zsb
		const inSum = xins + yins + zins
		var c = lookup3D.get(
			yins - zins + 1
				| xins - yins + 1 << 1
				| xins - zins + 1 << 2
				| inSum << 3
				| inSum + zins << 5
				| inSum + yins << 7
				| inSum + xins << 9
		)
		var i, value = 0
		while (c !== undefined) {
			const dx = dx0 + c.dx, dy = dy0 + c.dy, dz = dz0 + c.dz
			let attn = 2 - dx * dx - dy * dy - dz * dz
			if (attn > 0) {
				i = pD[(p[xsb + c.xsb & 0xFF] + (ysb + c.ysb) & 0xFF) + (zsb + c.zsb) & 0xFF]
				attn *= attn
				value += attn * attn * (g[i++] * dx + g[i++] * dy + g[i] * dz)
			}
			c = c.next
		}
		return value * NORM_3D + 0.5
	}
}
 
window.parent.exports["src/js/random.js"] = { seedHash, hash, random, randomSeed, openSimplexNoise, noiseProfile }}
		</script>
		<script id="src/js/3Dutils.js" type="application/javascript">
{class PVector {
	constructor(x, y, z) {
		this.x = x
		this.y = y
		this.z = z
	}
	set(x, y, z) {
		if (y === undefined) {
			this.x = x.x
			this.y = x.y
			this.z = x.z
		}
		else {
			this.x = x
			this.y = y
			this.z = z
		}
	}
	normalize() {
		let mag = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
		this.x /= mag
		this.y /= mag
		this.z /= mag
	}
	add(v) {
		this.x += v.x
		this.y += v.y
		this.z += v.z
	}
	mult(m) {
		this.x *= m
		this.y *= m
		this.z *= m
	}
	mag() {
		return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
	}
	magSquared() {
		return this.x * this.x + this.y * this.y + this.z * this.z
	}
}
 
const { cos, sin } = Math
 
class Matrix {
	constructor(arr) {
		this.elements = new Float32Array(arr || 16)
		// this.elements[0] = 1
		// this.elements[5] = 1
		// this.elements[10] = 1
		// this.elements[15] = 1
	}
	translate(x, y, z) {
		let a = this.elements
		a[3] += a[0] * x + a[1] * y + a[2] * z
		a[7] += a[4] * x + a[5] * y + a[6] * z
		a[11] += a[8] * x + a[9] * y + a[10] * z
		a[15] += a[12] * x + a[13] * y + a[14] * z
	}
	rotX(angle) {
		let elems = this.elements
		let c = cos(angle)
		let s = sin(angle)
		let t = elems[1]
		elems[1] = t * c + elems[2] * s
		elems[2] = t * -s + elems[2] * c
		t = elems[5]
		elems[5] = t * c + elems[6] * s
		elems[6] = t * -s + elems[6] * c
		t = elems[9]
		elems[9] = t * c + elems[10] * s
		elems[10] = t * -s + elems[10] * c
		t = elems[13]
		elems[13] = t * c + elems[14] * s
		elems[14] = t * -s + elems[14] * c
	}
	rotY(angle) {
		let c = cos(angle)
		let s = sin(angle)
		let elems = this.elements
		let t = elems[0]
		elems[0] = t * c + elems[2] * -s
		elems[2] = t * s + elems[2] * c
		t = elems[4]
		elems[4] = t * c + elems[6] * -s
		elems[6] = t * s + elems[6] * c
		t = elems[8]
		elems[8] = t * c + elems[10] * -s
		elems[10] = t * s + elems[10] * c
		t = elems[12]
		elems[12] = t * c + elems[14] * -s
		elems[14] = t * s + elems[14] * c
	}
	scale(x, y = x, z = x) {
		let a = this.elements
		a[0] *= x
		a[1] *= y
		a[2] *= z
		a[4] *= x
		a[5] *= y
		a[6] *= z
		a[8] *= x
		a[9] *= y
		a[10] *= z
		a[12] *= x
		a[13] *= y
		a[14] *= z
	}
	identity() {
		let a = this.elements
		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
	}
	// somebody optimize this
	// you just have to expand it
	mult(b) {
		const a = this.elements.slice()
		const out = this.elements
		let e = 0
		for (let row = 0; row < 4; row++) {
			for (let col = 0; col < 4; col++) {
				out[e++] = a[row * 4 + 0] * b[col + 0] + a[row * 4 + 1] * b[col + 4] + a[row * 4 + 2] * b[col + 8] + a[row * 4 + 3] * b[col + 12]
			}
		}
	}
	// same here
	postMult(a) {
		const b = this.elements.slice()
		const out = this.elements
		let e = 0
		for (let row = 0; row < 4; row++) {
			for (let col = 0; col < 4; col++) {
				out[e++] = a[row * 4 + 0] * b[col + 0] + a[row * 4 + 1] * b[col + 4] + a[row * 4 + 2] * b[col + 8] + a[row * 4 + 3] * b[col + 12]
			}
		}
	}
	transpose() {
		let matrix = this.elements
		let temp = matrix[4]
		matrix[4] = matrix[1]
		matrix[1] = temp
 
		temp = matrix[8]
		matrix[8] = matrix[2]
		matrix[2] = temp
 
		temp = matrix[6]
		matrix[6] = matrix[9]
		matrix[9] = temp
 
		temp = matrix[3]
		matrix[3] = matrix[12]
		matrix[12] = temp
 
		temp = matrix[7]
		matrix[7] = matrix[13]
		matrix[13] = temp
 
		temp = matrix[11]
		matrix[11] = matrix[14]
		matrix[14] = temp
	}
	copyArray(from) {
		let to = this.elements
		for (let i = 0; i < from.length; i++) {
			to[i] = from[i]
		}
	}
	copyMatrix(from) {
		let to = this.elements
		from = from.elements
		for (let i = 0; i < from.length; i++) {
			to[i] = from[i]
		}
	}
}
 
class Plane {
	constructor(nx, ny, nz) {
		this.set(nx, ny, nz)
	}
	set(nx, ny, nz) {
		// Pre-computed chunk offsets to reduce branching during culling
		this.dx = nx > 0 ? 16 : 0
		this.dy = ny > 0
		this.dz = nz > 0 ? 16 : 0
 
		// Normal vector for the plane
		this.nx = nx
		this.ny = ny
		this.nz = nz
	}
}
 
const cross = (v1, v2, result) => {
	let x = v1.x,
		y = v1.y,
		z = v1.z,
		x2 = v2.x,
		y2 = v2.y,
		z2 = v2.z
	result.x = y * z2 - y2 * z
	result.y = z * x2 - z2 * x
	result.z = x * y2 - x2 * y
}
 
window.parent.exports["src/js/3Dutils.js"] = { PVector, Matrix, Plane, cross }}
		</script>
		<script id="src/js/utils.js" type="application/javascript">
{const { floor } = Math
 
const timeString = (millis) => {
	if (millis > 300000000000 || !millis) {
		return "never"
	}
	const SECOND = 1000
	const MINUTE = SECOND * 60
	const HOUR = MINUTE * 60
	const DAY = HOUR * 24
	const YEAR = DAY * 365
 
	let years = floor(millis / YEAR)
	millis -= years * YEAR
 
	let days = floor(millis / DAY)
	millis -= days * DAY
 
	let hours = floor(millis / HOUR)
	millis -= hours * HOUR
 
	let minutes = floor(millis / MINUTE)
	millis -= minutes * MINUTE
 
	let seconds = floor(millis / SECOND)
 
	if (years) {
		return `${years} year${years > 1 ? "s" : ""} and ${days} day${days !== 1 ? "s" : ""} ago`
	}
	if (days) {
		return `${days} day${days > 1 ? "s" : ""} and ${hours} hour${hours !== 1 ? "s" : ""} ago`
	}
	if (hours) {
		return `${hours} hour${hours > 1 ? "s" : ""} and ${minutes} minute${minutes !== 1 ? "s" : ""} ago`
	}
	if (minutes) {
		return `${minutes} minute${minutes > 1 ? "s" : ""} and ${seconds} second${seconds !== 1 ? "s" : ""} ago`
	}
	return `${seconds} second${seconds !== 1 ? "s" : ""} ago`
}
 
const roundBits = (number) => {
	return Math.round(number * 100000) / 100000
}
 
const compareArr = (arr, out) => {
	let minX = 1000
	let maxX = -1000
	let minY = 1000
	let maxY = -1000
	let minZ = 1000
	let maxZ = -1000
	let num = 0
	for (let i = 0; i < arr.length; i += 3) {
		num = arr[i]
		minX = minX > num ? num : minX
		maxX = maxX < num ? num : maxX
		num = arr[i + 1]
		minY = minY > num ? num : minY
		maxY = maxY < num ? num : maxY
		num = arr[i + 2]
		minZ = minZ > num ? num : minZ
		maxZ = maxZ < num ? num : maxZ
	}
	out[0] = minX
	out[1] = minY
	out[2] = minZ
	out[3] = maxX
	out[4] = maxY
	out[5] = maxZ
	return out
}
 
class BitArrayBuilder {
	constructor() {
		this.bitLength = 0
		this.data = [] // Byte array
	}
	add(num, bits) {
		if (isNaN(num) || isNaN(bits) || bits < 0) throw "Broken"
		num &= -1 >>> 32 - bits
		let index = this.bitLength >>> 3
		let openBits = 8 - (this.bitLength & 7)
		this.bitLength += bits
		while (bits > 0) {
			this.data[index] |= (openBits >= bits ? num << openBits - bits : num >>> bits - openBits) & 255
			bits -= openBits
			index++
			openBits = 8
		}
		return this // allow chaining like arr.add(x, 16).add(y, 8).add(z, 16)
	}
	/**
	 * Takes all the bits from another BAB and adds them to this one.
	 * @param {BitArrayBuilder} bab The BAB to append
	 */
	append(bab) {
		// If our bits are already aligned, just add them directly
		if ((this.bitLength & 7) === 0) {
			this.data.push(...bab.data)
			this.bitLength += bab.bitLength
			return
		}
 
		// Add them 1 at a time, except for the last one
		let bits = bab.bitLength
		let i = 0
		while (bits > 7) {
			this.add(bab.data[i++], 8)
			bits -= 8
		}
		if (bits) {
			this.add(bab.data[i] >>> 8 - bits, bits)
		}
	}
	get array() {
		return new Uint8Array(this.data)
	}
	/**
	 * @param {Number} num
	 * @returns The number of bits required to hold num
	 */
	static bits(num) {
		return Math.ceil(Math.log2(num))
	}
}
class BitArrayReader {
	/**
	 * @param {Uint8Array} array An array of values from 0 to 255
	 */
	constructor(array) {
		this.data = array // Byte array; values are assumed to be under 256
 
		/**
		 * A pointer to the bit position of the reader; can be adjusted to read a different part of the array.
		 */
		this.bit = 0
	}
	read(bits, negative = false) {
		let openBits = 32 - bits
		let { data, bit } = this
		this.bit += bits // Move pointer
		if (bit > data.length * 8 || bit < 0) {
			throw "You done messed up A-A-Ron"
		}
 
		let unread = 8 - (bit & 7)
		let index = bit >>> 3
		let ret = 0
		while (bits > 0) {
			let n = data[index] & -1 >>> 32 - unread
			ret |= bits >= unread ? n << bits - unread : n >> unread - bits
			bits -= unread
			unread = 8
			index++
		}
		if (negative) {
			// console.log("Negative", ret, ret << openBits >> openBits)
			return ret << openBits >> openBits
		}
		return ret
	}
}
 
window.parent.exports["src/js/utils.js"] = { timeString, roundBits, compareArr, BitArrayBuilder, BitArrayReader }}
		</script>
		<script id="src/js/shapes.js" type="application/javascript">
{const { compareArr } = window.parent.exports["src/js/utils.js"]
 
const textureAtlasWidth = 16 // That's 16 textures wide
 
const CUBE     = 0
const SLAB     = 0x100 // 9th bit
const STAIR    = 0x200 // 10th bit
const FLIP     = 0x400 // 11th bit
const SOUTH    = 0x800
const EAST     = 0x1000
const WEST     = 0x1800
 
/**
 * Return the parameters as an array. Honestly just did this for an excuse to write out the parameters.
 * @param {Number} x The X coordinate of the face with the top left corner of the texture
 * @param {Number} y The Y coordinate of the face with the top left corner of the texture
 * @param {Number} z The Z coordinate of the face with the top left corner of the texture
 * @param {Number} width The width of the face/texture. Refers to deltaX if X changes, or deltaZ otherwise
 * @param {Number} height The height of the face/texture. Refers to deltaY if Y changes, or deltaZ otherwise
 * @param {Number} textureX The X coordinate of the texture at (x, y, z) on the face
 * @param {Number} textureY The Y coordinate of the texture at (x, y, z) on the face
 */
const arrayify = (x, y, z, width, height, textureX, textureY) => {
	return [x, y, z, width, height, textureX, textureY]
}
let shapes = {
	cube: {
		verts: [
			// x, y, z, width, height, textureX, textureY
			// 0, 0, 0 is the corner on the top left of the texture
			[arrayify( 0,  0,  0, 16, 16, 0, 0)], //bottom
			[arrayify( 0, 16, 16, 16, 16, 0, 0)], //top
			[arrayify(16, 16, 16, 16, 16, 0, 0)], //north
			[arrayify( 0, 16,  0, 16, 16, 0, 0)], //south
			[arrayify(16, 16,  0, 16, 16, 0, 0)], //east
			[arrayify( 0, 16, 16, 16, 16, 0, 0)]  //west
		],
		cull: {
			top: 15,
			bottom: 15,
			north: 15,
			south: 15,
			east: 15,
			west: 15
		},
		texVerts: [],
		buffer: null,
		size: 6,
		variants: [],
		flip: false,
		rotate: true,
	},
	slab: {
		verts: [
			[arrayify( 0, 0,  0, 16, 16, 0, 0)], //bottom
			[arrayify( 0, 8, 16, 16, 16, 0, 0)], //top
			[arrayify(16, 8, 16, 16, 8, 0, 0)], //north
			[arrayify( 0, 8,  0, 16, 8, 0, 0)], //south
			[arrayify(16, 8,  0, 16, 8, 0, 0)], //east
			[arrayify( 0, 8, 16, 16, 8, 0, 0)]  //west
		],
		cull: {
			top: 0,
			bottom: 15,
			north: 3,
			south: 3,
			east: 3,
			west: 3
		},
		texVerts: [],
		buffer: null,
		size: 6,
		variants: [],
		flip: true,
		rotate: false
	},
	stair: {
		verts: [
			[[0,0,0,16,16,0,0]], // -y
			[[0,8,16,8,16,8,0],[8,16,16,8,16,0,0]], // +y
			[[8,8,16,8,8,0,0],[16,16,16,8,16,0,0]], // +z
			[[0,8,0,8,8,0,0],[8,16,0,8,16,0,0]], // -z
			[[16,16,0,16,16,0,0]], // +x
			[[0,8,16,16,8,0,0],[8,16,16,16,8,0,0]] // -x
		],
		cull: {
			top: 0b1100,
			bottom: 15,
			north: 15,
			south: 3,
			east: 0b0111,
			west: 0b1011
		},
		texVerts: [],
		buffer: null,
		size: 10,
		variants: [],
		flip: true,
		rotate: true
	},
	flower: {
		verts: [
			[],
			[],
			// [arrayify( 8,  0,  8,  2,  2,  0,  0)],
			// [arrayify( 8, 16,  8,  2,  2,  0,  0)],
			[arrayify(16, 16,  8, 16, 16,  0,  0)],
			[arrayify( 0, 16,  8, 16, 16,  0,  0)],
			[arrayify( 8, 16,  0, 16, 16,  0,  0)],
			[arrayify( 8, 16, 16, 16, 16,  0,  0)]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 0,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 4,
		variants: [],
		flip: false,
		rotate: false
	},
	lantern: {
		verts: [
			[[5,1,5,6,6,0,9]],
			[[5,8,11,6,6,0,9],[6,10,10,4,4,1,10]],
			[[11,8,11,6,7,0,2],[10,10,10,4,2,1,0],[9.5,15,8,3,4,11,1]],
			[[5,8,5,6,7,0,2],[6,10,6,4,2,1,0],[6.5,15,8,3,4,11,1]],
			[[11,8,5,6,7,0,2],[10,10,6,4,2,1,0],[8,16,6.5,3,6,11,6]],
			[[5,8,11,6,7,0,2],[6,10,10,4,2,1,0],[8,16,9.5,3,6,11,6]]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 0,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 15,
		variants: [],
		flip: false,
		rotate: false
	},
	door: {
		verts: [
			[[0,0,0,3,16,13,0]],
			[[0,16,16,3,16,0,0]],
			[[3,16,16,3,16,0,0]],
			[[0,16,0,3,16,0,0]],
			[[3,16,0,16,16,0,0]],
			[[0,16,16,16,16,0,0]]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 15,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 6,
		variants: [],
		flip: false,
		rotate: true
	},
	fence: {
		verts: [
			[[6,0,6,4,4,6,6]],
			[[6,16,10,4,4,6,6]],
			[[10,16,10,4,16,6,0]],
			[[6,16,6,4,16,6,0]],
			[[10,16,6,4,16,6,0]],
			[[6,16,10,4,16,6,0]]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 0,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 6,
		variants: [],
		flip: false,
		rotate: false
	},
	fenceSide: {
		verts: [
			[[7,12,0,2,6,7,0],[7,6,0,2,6,7,0]],
			[[7,15,6,2,6,7,0],[7,9,6,2,6,7,0]],
			[],//[9,15,6,2,3,7,1],[9,9,6,2,3,7,7]],
			[[7,15,0,2,3,7,1],[7,9,0,2,3,7,7]], //
			[[9,15,0,6,3,0,1],[9,9,0,6,3,0,7]],
			[[7,15,6,6,3,0,1],[7,9,6,6,3,0,7]]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 0,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 10,
		variants: [],
		flip: false,
		rotate: true
	},
	innerStairs: {
		verts: [
			[[0,0,0,16,16,0,0]],
			[[0,8,8,8,8,0,0],[8,16,16,8,16,8,0],[0,16,16,8,8,0,8]],
			[[16,16,16,16,16,0,0]],
			[[0,8,0,16,8,0,8],[8,16,0,8,8,8,0],[0,16,8,8,8,0,0]],
			[[16,16,0,16,16,0,0]],
			[[0,8,16,16,8,0,8],[8,16,8,8,8,0,0],[0,16,16,8,8,8,0]]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 0,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 12,
		variants: [],
		flip: true,
		rotate: true
	},
	outerStairs: {
		verts: [
			[[0,0,0,16,16,0,0]],
			[[0,8,16,16,16,0,0],[8,16,16,8,8,8,8]],
			[[16,8,16,16,8,0,0],[16,16,16,8,8,0,0]],
			[[0,8,0,16,8,0,0],[8,16,8,8,8,8,0]],
			[[16,8,0,16,8,0,0],[16,16,8,8,8,0,0]],
			[[0,8,16,16,8,0,0],[8,16,16,8,8,8,0]]
		],
		cull: {
			top: 0,
			bottom: 0,
			north: 0,
			south: 0,
			east: 0,
			west: 0
		},
		texVerts: [],
		buffer: null,
		size: 11,
		variants: [],
		flip: true,
		rotate: true
	},
}
 
const mapCoords = (rect, face) => {
	const [x, y, z, w, h, tx, ty] = rect
	const tex = [tx+w,ty, tx,ty, tx,ty+h, tx+w,ty+h].map(c => c / 16 / textureAtlasWidth)
	const pos = [x, y, z]
	switch(face) {
		case 0: // Bottom
			pos.push(x+w,y,z, x+w,y,z+h, x,y,z+h)
			break
		case 1: // Top
			pos.push(x+w,y,z, x+w,y,z-h, x,y,z-h)
			break
		case 2: // North
			pos.push(x-w,y,z, x-w,y-h,z, x,y-h,z)
			break
		case 3: // South
			pos.push(x+w,y,z, x+w,y-h,z, x,y-h,z)
			break
		case 4: // East
			pos.push(x,y,z+w, x,y-h,z+w, x,y-h,z)
			break
		case 5: // West
			pos.push(x,y,z-w, x,y-h,z-w, x,y-h,z)
			break
	}
	for(let i = 0; i < 12; i++) pos[i] = (pos[i] - 8) / 16
	let minmax = compareArr(pos, [])
	pos.max = minmax.splice(3, 3)
	pos.min = minmax
 
	return {
		pos: pos,
		tex: tex
	}
}
 
// 90 degree clockwise rotation; returns a new shape object
const rotate = (shape) => {
	let verts = shape.verts
	let texVerts = shape.texVerts
	let cull = shape.cull
	let pos = []
	let tex = []
	for (let i = 0; i < verts.length; i++) {
		let side = verts[i]
		pos[i] = []
		tex[i] = []
		for (let j = 0; j < side.length; j++) {
			let face = side[j]
			let c = []
			pos[i][j] = c
			for (let k = 0; k < face.length; k += 3) {
				c[k] = face[k + 2]
				c[k + 1] = face[k + 1]
				c[k + 2] = -face[k]
			}
 
			tex[i][j] = texVerts[i][j].slice() // Copy texture verts exactly
			if (i === 0) {
				// Bottom
				c.push(...c.splice(0, 3))
				tex[i][j].push(...tex[i][j].splice(0, 2))
			}
			if (i === 1) {
				// Top
				c.unshift(...c.splice(-3, 3))
				tex[i][j].unshift(...tex[i][j].splice(-2, 2))
			}
 
			let minmax = compareArr(c, [])
			c.max = minmax.splice(3, 3)
			c.min = minmax
		}
	}
 
	// Make sure each direction has the correct number of faces and whatnot.
	let temp = tex[2] // North
	tex[2] = tex[5] // North = West
	tex[5] = tex[3] // West = South
	tex[3] = tex[4] // South = East
	tex[4] = temp // East = North
 
	temp = pos[2] // North
	pos[2] = pos[5] // North = West
	pos[5] = pos[3] // West = South
	pos[3] = pos[4] // South = East
	pos[4] = temp // East = North
 
	let cull2 = {
		top: cull.top,
		bottom: cull.bottom,
		north: cull.west,
		west: cull.south,
		south: cull.east,
		east: cull.north
	}
 
	// let buffer = gl.createBuffer()
	// gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
	// gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos.flat(2)), gl.STATIC_DRAW)
 
	return {
		verts: pos,
		texVerts: tex,
		cull: cull2,
		rotate: true,
		flip: shape.flip,
		buffer: null,
		size: shape.size,
		variants: shape.variants
	}
}
 
// Reflect over the y plane; returns a new shape object
const flip = (shape) => {
	let verts = shape.verts
	let texVerts = shape.texVerts
	let cull = shape.cull
	let pos = []
	let tex = []
	for (let i = 0; i < verts.length; i++) {
		let side = verts[i]
		pos[i] = []
		tex[i] = []
		for (let j = 0; j < side.length; j++) {
			let face = side[j].slice().reverse()
			let c = []
			pos[i][j] = c
			for (let k = 0; k < face.length; k += 3) {
				c[k] = face[k + 2]
				c[k + 1] = -face[k + 1]
				c[k + 2] = face[k]
			}
			let minmax = compareArr(c, [])
			c.max = minmax.splice(3, 3)
			c.min = minmax
 
			tex[i][j] = texVerts[i][j].slice() // Copy texture verts exactly
		}
	}
	let temp = pos[0] // Bottom
	pos[0] = pos[1] // Bottom = Top
	pos[1] = temp // Top = Bottom
 
	temp = tex[0] // Bottom
	tex[0] = tex[1] // Bottom = Top
	tex[1] = temp // Top = Bottom
 
	let cull2 = {
		top: cull.bottom,
		bottom: cull.top,
		north: (cull.north & 1) << 1 | (cull.north & 2) >> 1,
		west: (cull.west & 1) << 1 | (cull.west & 2) >> 1,
		south: (cull.south & 1) << 1 | (cull.south & 2) >> 1,
		east: (cull.east & 1) << 1 | (cull.east & 2) >> 1
	}
 
	// let buffer = gl.createBuffer()
	// gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
	// gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos.flat(2)), gl.STATIC_DRAW)
 
	return {
		verts: pos,
		texVerts: tex,
		cull: cull2,
		rotate: shape.rotate,
		flip: shape.flip,
		buffer: null,
		size: shape.size,
		variants: shape.variants
	}
}
 
for (let shape in shapes) {
	let obj = shapes[shape]
	let verts = obj.verts
	obj.size = obj.verts.flat().length
 
	// Populate the vertex coordinates
	for (let i = 0; i < verts.length; i++) { // 6 directions
		let side = verts[i] // Array of faces in this direction
		let texArr = []
		obj.texVerts.push(texArr)
		for (let j = 0; j < side.length; j++) { // Each face in this direction
			let face = side[j] // Array of arrayified data
			let mapped = mapCoords(face, i)
			side[j] = mapped.pos
			texArr.push(mapped.tex)
		}
	}
 
	const v = obj.variants
	if (obj.rotate) {
		v[4] = rotate(obj)
		v[2] = rotate(v[4])
		v[6] = rotate(v[2])
		v[0] = obj
	}
	if (obj.flip) {
		v[1] = flip(obj)
		if (obj.rotate) {
			v[3] = flip(v[2])
			v[5] = flip(v[4])
			v[7] = flip(v[6])
		}
	}
}
 
// Now that fenceSide is rotated, let's generate a bunch of fence variants
{
	const clone = obj => {
		return {
			verts: obj.verts.map(a => a.slice()),
			texVerts: obj.texVerts.map(a => a.slice()),
			cull: obj.cull,
			rotate: false,
			flip: false,
			buffer: null,
			size: obj.size,
			variants: obj.variants
		}
	}
	const v = shapes.fence.variants
	for (let i = 0; i < 16; i++) {
		let obj = clone(shapes.fence)
		for (let j = 0; j < 4; j++) {
			if (i & 1 << j) for (let k = 0; k < 6; k++) {
				obj.size += shapes.fenceSide.variants[j * 2].verts[k].length
				obj.verts[k].push(...shapes.fenceSide.variants[j * 2].verts[k])
				obj.texVerts[k].push(...shapes.fenceSide.variants[j * 2].texVerts[k])
			}
		}
		v.push(obj)
	}
 
	// shapes.fence.texVerts = shapes.cube.texVerts
	// shapes.fence.verts = shapes.cube.verts // Make the hitbox a cube
	shapes.fence.getShape = (x, y, z, world, blockData) => {
		let mask = 0
		if (blockData[world.getBlock(x + 1, y, z)].solid) mask |= 8
		if (blockData[world.getBlock(x - 1, y, z)].solid) mask |= 4
		if (blockData[world.getBlock(x, y, z + 1)].solid) mask |= 2
		if (blockData[world.getBlock(x, y, z - 1)].solid) mask |= 1
		return shapes.fence.variants[mask]
	}
}
 
 
// Generate functions for each stair direction to transform it into corners
const stairs = shapes.stair.variants
const getShapeFunction = (shape, dx, dz, indices, negShape, posShape) => {
	const stair1 = stairs[indices[0]]
	const stair2 = stairs[indices[1]]
	const ret1 = negShape[indices[0]]
	const ret2 = negShape[indices[2]]
	const ret3 = posShape[indices[0]]
	const ret4 = posShape[indices[2]]
	return (x, y, z, world, blockData) => {
		const n = blockData[world.getBlock(x - dx, y, z - dz)].shape
		if (n === stair1) return ret1
		if (n === stair2) return ret2
 
		const p = blockData[world.getBlock(x + dx, y, z + dz)].shape
		if (p === stair1) return ret3
		if (p === stair2) return ret4
 
		return shape
	}
}
 
const inner = shapes.innerStairs.variants
const outer = shapes.outerStairs.variants
const stairFunctions = [
	[1, 0, [4, 6, 0], inner, outer],
	[1, 0, [5, 7, 1], inner, outer],
	[1, 0, [6, 4, 2], outer, inner],
	[1, 0, [7, 5, 3], outer, inner],
	[0, 1, [2, 0, 4], outer, inner],
	[0, 1, [3, 1, 5], outer, inner],
	[0, 1, [0, 2, 6], inner, outer],
	[0, 1, [1, 3, 7], inner, outer]
]
 
for (let i = 0; i < 8; i++) {
	stairs[i].getShape = getShapeFunction(stairs[i], ...stairFunctions[i])
}
 
 
window.parent.exports["src/js/shapes.js"] = { shapes, CUBE, SLAB, STAIR, FLIP, SOUTH, EAST, WEST }}
		</script>
		<script id="src/js/blockData.js" type="application/javascript">
{const { shapes } = window.parent.exports["src/js/shapes.js"]
const blockIds = {}
 
const texturesFunc = function (setPixel, getPixels) {
	return {
		grassTop: n => {
			for (let x = 0; x < 16; ++x) {
				for (let y = 0; y < 16; ++y) {
					const d = Math.random() * 0.25 + 0.65
 
					const r = 0x4B * d
					const g = 0x7D * d
					const b = 0x40 * d
 
					setPixel(n, x, y, r, g, b)
				}
			}
		},
		grassSide: function(n) {
			const pix = getPixels(this.dirt)
 
			// Fill in the dirt texture first
			for (let i = 0; i < pix.length; i += 4) {
				setPixel(n, i >> 2 & 15, i >> 6, pix[i], pix[i + 1], pix[i + 2], pix[i + 3])
			}
 
			const { random } = Math
 
			for (let x = 0; x < 16; ++x) {
				const m = random() * 4 + 1
				for (let y = 0; y < m; ++y) {
					const d = random() * 0.25 + 0.65
					const r = 0x4B * d
					const g = 0x7D * d
					const b = 0x40 * d
					setPixel(n, x, y, r, g, b)
				}
			}
		},
		leaves: n => {
			const { floor, random } = Math
 
			for (let x = 0; x < 16; ++x) {
				for (let y = 0; y < 16; ++y) {
					const r = 0
					const g = floor(random() * 30 + 75)
					const b = floor(random() * 30)
					const a = random() < 0.35 ? 0x0 : 0xff
 
					setPixel(n, x, y, r, g, b, a)
				}
			}
		},
		hitbox: "0g0g100W",  // Black
		air: "0g0g1000", // Transparent black
		"acaciaLog": "0g0g6ÖïYÇQYåĭYÁAWÇUZ÷nH50ķcyX6ħœcy4eSœ4i4{SQgNkQSīĘSÀSXTęgÀëïwT0ÀìXy1Tg5ķyh?g0ķwhko0x3gko4x3Ĉ/8Č5jĘ(wĈX1Àg0SĈj4iëSĊh42X",
		"acaciaLogTop": "0g0gbÖïYVQYÁ)HPjZġîHĕàWĨěZöNYāRYĉÃW?3Y1xizNj1g4Q??ÒUQTAGIĀāāIÏkãÑQ?Q]>čXVVVVPÂ)üÆòĀï]Á*ïÅVVïÆTBüÆÇýýPÀ5üÆÇïï]À5üÆVVýÆÁlXÆñòýPÂBüVVVV]Â)ü?QQ@]Ã)ĀĀIIII>ČQV?ÄVQTgNxg0iz(",
		"acaciaPlanks": "0g0g7ġîHĕàWĨěZāRYĉÃWöNYòiY4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂTAX40cùĪzSāAAā4ŁğãļłĞSA4PkiA9cë0PNgÐ0İAĽŔÉGËĞ",
		"andesite": "0g0g6éŞZâľHĆåZóEZÖĎYĒđY4ĎĨò61aĘĹĕĩ2BĜõwAħ]ĩŁV+peČ0ÚĘ^Úðj6Rĺc!ìóĎłTěd|ðŁÙüÃÖëēÒ+ë4ĻłÙ({*CħïĚ!S+rÖ)ĀÙgŀđĞłVĚSÚĩiÎëő$m3c)Ā",
		"bedrock": "0g0g5ÎðW(ĪWVVHþÇHwíW4JĀ|iów(Ī%IÁ(ĀPÒAķ{5j]J^ïJ^A+1FyúMyÎÙwĿTĘijPkú(üRdĊÂdðłQĩóxiÆ1ùŀïĞ9òyÀÚ0ŃQùòcJ^c*hCkr1iòTĜ^(ĀĿERÀ",
		"birchLog": "0g0g8ŚĦYņ|HłÙZZZZĦšZÎâH)ĹYŖĵY0Č0Q4ëQ0rÎiÀÚJî04rÚ_ĝTĞĺSFÛđĘĔĝwòTBãĘ4]ÚìĻ?+łÖvĩÎwņŔğłÙjZÚëù]+Ŀ1iĿTĞĪÚŝĩB0ùfŜ&ķ6ĿQēĞŁČăÏļ%9Àł5wù",
		"birchLogTop": "0g0gaZZZŒĖHĂÆWÁ)Hľ8HıľYľEZĎÄHĖGHĢďH1xizNj1g4Q??ÒUQTAGIĀāāIÏkãÑQ?Q]>BXVVVVPÂ)üÆòĀï]Á*ïÅVVïÆTBüÆÇýýPÀ5üÆÇïï]À5üÆVVýÆÁlXÆñòýPÂBüVVVV]Â)ü?QQ@]Ã)ĀĀIIII>AQV?ÄVQTgNxg0iz(",
		"birchPlanks": "0g0g7ľ8HıľYľEZĖGHĢďHĎÄHĆCW4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"blackConcrete": "0g0g48wZ8MW4wZ8xWmeşŞÑØĪij9ŠóşĶøŞ;îŃÓyÀĈĶÃĞX^æĆşŒĔ<~Ŕ<ĢL4IJţŘŇěøÌĩĶõ;ŞkŇěĚċŇŖĝĜř,ʼnĩ",
		"blackWool": "0g0gfcMWcTH8MWgÁYoÎZcMHkÎZ8wZkÁY8xWAJYgTH4wZsßWwíH1w)5ÑßIFIĒQģ9Éĩr|ģŔô|Qő]ì|ę<bńìĕô8QGÆÂQDDd717rýbĒQOĠ^ØĒ]hĒ@ń5|ÕńQBĽ1ÉÇ:yÉĜGĠ8ĒĜĒńQĕ@ĻĠîEwIhQ0ÆGĠhGŅD)XĒò@őĻôç|ģŁŔôĿXČVXgùĿ1ŅęÚ0|",
		"blueConcrete": "0g0g3$ġW$ĠZ$İWlÀ?ÑT?QA]0@VUUh?Čkkw55kUÒoTlV0UhhgÑ0VUR0gÁ?QghX54UVS4Á0UýÁlÀUükÄ",
		"blueWool": "0g0gj$İH(ŀH$İW)ŀY-šW(İH)őW$ġW)ŐZ)ŀZ<:W-šH(ŀY$ĠZ.aH<aY-őW<qY.aY0Q1ùčMeAwR^kčúõ0óĭI$ÉĀ,ġczĉ8ĐI]]ō4Û#sŋï}>aJgŎ!ayg[hıʼnĄ1FĩĨkcÂþTöIÈõĬEI8UĊpĵ1]ŊxĵA4Åoĉ#axĘR#oěąI!kĉĨ^àI?-ðßĿXcPhÀëXù1^îąI8}G;[ph5F2ìĊďhc-ŇōdıèJĮx4ÂıUSwhÊë5ĸK{ŇlÛ",
		"bookshelf": "0g0gtĚ*WĆkZāĻWéîZ$ìW)ĉHAÞWMÕZF,YSĨYùTWĒKYòŗYMóWáħHĤÁHãMWĞłHóEYi+YĵþZKĈHËħZdčWĦÓWVĈYPAY;ŚZÖÃW0RxcRgRgIw18RüXx^ÐĈë1ÂýFF^ĿþĈĈiĄŎđFXÑĎĊÏċŃŎđČíĿĎĊá3ńÅġĘĚŇōĆK2Ēý.m#wŋuňgMóÂĀ0Mõ020XgõygRh8K1^ÐmŀFcÂĀë3ĻÄĊmÂÃÙČĈkÒãĊmÃİb}ŊċļãċÉàļb@U3ńãćÉ!ćb}ŇaąīuĽŊýÙń06M^Ã06Mõ00",
		"bricks": "0g0g9ęXZāUWòĞHĬčWĊnZö>ZéjHÝŚWĒÆZ0iO(0k(0hUÒhhUÎhÔäGVÔáâÓy]RyA]RyO0gk(0giÎhlÑÒVVÑÒmÓäGÒÄáRyA]Qyy]0kO(gkO0hUÁhhÑÁhÓáÓVÓäÒVA]Ryy]Ryg0gi(0gkÎhhUÁhhÑãÓÓäãÓÓäRyy]QyAI",
		"brownConcrete": "0g0g3ÊňZKřWKňZqÖlÄÄÄĂþÏĉVÄÈTÈÏĂĎĒÄÒÒýÑāāÈÒVÒlÄVJāúÒþïĂýÓóÇāIĀĒāýÖþVāÖĎþIÄđĂÁVč",
		"brownWool": "0g0giÒ2WÖ2HÑřWÞiHåNZÖ2WâyYKřWÞyYÞiYìÃWéNZÚiHKňZéRZì>WåyYÚ2H0QxùĉMeAw[PüčúõhĩĭI$ÉĀĮġczĉ7Đð]]ō4Û#sŋï}OaJgŎa2yg[hıʼnĄ1FĩĪXÁÂþTöIÈõĬEI8UĊpĵg]ŊpĵA4noĉb2ÞĘRbyěąðFüĎT^àI?-ðßĿîcPh{KXùhRÐĆC8}G;[ph5EĮìĆďhc:wōMıçĹĮwīÂĩkSy.Êì5ĶK{ňlÛ",
		"chiseledQuartzBlock": "0g0g6ņÙYŖĖHŖąZŒöYŊéWŚĦZ4Ja]+]5BrÙi]9,A0iÀdĞķ4üüd9wJ0Ń9_PFĘĿi2Ñ1Ę0ĜJÎxA|AČĨÀJPi@ëùā4kíPB.{4ìwJ0]Xû]mJ]òDw0iÀd*%AAù4û9CCĿ",
		"chiseledQuartzBlockTop": "0g0g5ņÙYŖĖHŚĦZŊéWŖąZ4ĊĻMJĈhĘ4ë6ŇgB1ìEŇgD2ì-zg+Tí0biiR÷RK002öĘ0(ĊÂJiJJPAJPA00põ039kÈöpz8,2ö+wÑ@RëĚwÝRìík8hĞ1ìĘgiRĻúJĈ",
		"chiseledStoneBricks": "0g0g7ĆÖZóEYÖĞYéŞZÇÒYÎðWåľY00]0ëRdĜłÖ+Ń&ŀAĞļü!090óáe2ŅÚĒÊe7JŀőÊ!nČïĒ)&cČìŏk!nČñE)aÿIBĐ)eðIJLEáeõĚİ!ÊdÉÑJÈáA2S0Jś:;ěç$üÂPAJPA",
		"coalBlock": "0g0g5sÞZkÁHc(Z4gHEĊY0ü_ÑĎĸAĊăÒ)SFĞòÚĚP|ċ1AĚÃÚg9FĚă$J^ÚĞòÕiûÖiPÑüĸy2ÃÒCI4üłÚkTF(ÉEĊ^ÚJú5NúFĜ]ÚüX5g9ÚĊxÙíÃÚgÃÕJăQJòQüł",
		"coalOre": "0g0ga÷-ZéŞZâľHÖĎYËâZAJH-ŚH)ĺH$ĚZPzZ00ixzyhhhxxQTj)iiOAÄáxñhkyoÔĀÁiyBßy]VRyhg1x02A>wwiRyiÕÑhAAÄXh@ú02lÖäiiyigAñRw1kTM2Qiiy?ÐhQAhxQyykÇâÀ4þÂO2@UyAâRxiyhähTh0hyx2g1ih",
		"cobblestone": "0g0g6ÚĞZÎðWĎĂHÁ?WĞłHóoY5C^óăl!ÈŋÄě?!ĈVĐmÕCíÕĈļ_KĿöCAđì_TãĬ?UļÕA!cĜbTęh|6wdþĹÆMÁSĜîÁĊó_wmüĈi$QģBmwÏĐr?MÈVmíÕ^ó8ĜlP)úT4ĿĐ",
		"cyanConcrete": "0g0g3lĿYlŏYlĿH1IÀpE?SmkÀw6PÁB?S4k299úkÄRPÒÎwĊ?A2ÑIĀ8püUiSÒgý95Eòak?ý?1RÀFÀKSÒù",
		"cyanWool": "0g0gelşYm8ZmFWmPHlşZm.WlŏYmÖZmÇHmoZlĿYmÇYmÖYm.H10zTÃKy6BGîā6pĎpôāIJF:ńĮ*x:ú#PģxçÇROÓimO6Ó_@1@pÎ1GîyJñÇGNhG*ģ1:Vģ<ÎĝTpmF6poÓý?GąGģOç*ĠJÂ24Vh<4lÓlhÓĤÓz%GÄļĮěFÖłāğijFĚÃäh%gÑĚ1ĤúÉ4ô",
		"darkOakLog": "0g0g6;ĨZ(úW]ňZEìW(úHÇiY50ķcyX6ħœcy4eSœ4i4{SQgNkQSīĘSÀSXTęgÀëïwT0ÀìXy1Tg5ķyh?g0ķwhko0x3gko4x3Ĉ/8Č5jĘ(wĈX1Àg0SĈj4iëSĊh42X",
		"darkOakLogTop": "0g0gb;ĨZ)ĉYAÞWsKZ{ĨY]ęHÀňY(ÝZ-úW;ĉW(úH1xizNj1g4Q??ÒUQTAGIĀāāIÏkãÑQ?Q]>čXVVVVPÂ)üÆòĀï]Á*ïÅVVïÆTBüÆÇýýPÀ5üÆÇïï]À5üÆVVýÆÁlXÆñòýPÂBüVVVV]Â)ü?QQ@]Ã)ĀĀIIII>ČQV?ÄVQTgNxg0iz(",
		"darkOakPlanks": "0g0g7{ĨY]ęHÀňY-úW;ĉW(ÝZEKZ4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"diamondBlock": "0g0g9_ĦHW÷HncHľZHZZZćťYÔŅWàŖWeŒZ00h01hg23QVO*ÄÐN4ÓàVKh7N4Ô*ÅK0GÂlâVã0ÔãÂlVÅK6VKo3VGãÒÄ1o3ÅGÓGK18lGãÔãK0ÏlGÓGÓÓ7ÏmãÒÄÓÓÅ8gÓVK65Ä8gÒÄ1gGKÏhGKh6Ó0ßgg1gÓ0ÔÆyíEIyIyI",
		"diamondOre": "0g0ga÷-ZéŞZâľHÒþHv|HGąWyÇY÷ģWľZHĊóW1gixzyg0hx(01iOxiOSjNzÄh01Ý*Ô1Ýyxhz(Þx2hgi-Ãiïzwy8U@Þ@hh3(åGO7gā*[1zïxh2nÞMB@Ô9úMxR(G3Nyhhg02*K1i3M2ÆU@20âÐxþ[ÝhiD01gÞyxhhhwhhh0",
		"diorite": "0g0g6óEYĎóWĦţWŒĖYĶ;ZåŎY4üİPĀă)yR×,gÎ+E?ĠóÒĜbÕX_{oî|iŀö+Ň]ČıÖoŀhĠFM2óöĞTQĻÉúpbĉĚjKĐŇNxRò+lÚóAA(Ã&njÝü^wĐìÞīĸX6įöĊcÒ+ŇNgÉ",
		"dirt": "0g0g7ĢlZýĜYåÃYÆřYðoHÚĞZâÑH4Č9PČg?ČÐSĈÉ9(J9Cĩ)yķBkaEðÂ%UÈ{üÉÖ)ù9Eù84Á]2Â$üòFkÃQČĂ?ČŁPwh?0ìKNÏFihČĎÃ{ĊRPAë?$ò{)9FXĺ1kòEiĊByÃ",
		"emeraldBlock": "0g0g6nkHqěHîŁZ>įHnãWuÏY0000019AĂÖ]ń800w0Ëc)ûJ@Ë8w00mV8wJÚoÒcwăúĀ?c(ĂúĀ?8(ŋ4gÒc(ŋCgÒcħĄim?gĩPAþ?cB01ĠÒgJPAJËg]4ù8ļ+łÚĞłÖ",
		"emeraldOre": "0g0gc÷-ZéŞZâľHÖĎYÁčWłťYnãW1ňY*ÐZ>įHuÏYnkH00ixzyhhhQg01QNikÄyhAÄhhhÔhT0Ô2yxh4ìg02hgg9ĂRzzwx-ýæìxkTA]ÿēXy?Î?ÎĢ0yhmßmÝ0h1Q(1(xgjpĀNyhhh1åÅĉgiyQiçæĚN0kÄiEĈ2wixÔ1(1g0hh0yg0ih",
		"glass": "0g0g5ĺĖYē|Y000æģHôcZ0000019AJPAú9wJPAû94JPAû8ČJPAû9AJPAü9AJPAû9AJPAü9AJPAü9AJPAü9AJPAü9AJPAüFAJPAk9AJPwüFAJPAúCpAJP9",
		"glowstone": "0g0g8ŢÔHĵlHïRYÚiWZZZZĴYòċYÞNH5+T%^ÄĈYĸäŁb?ŋćŢĘÌĶgÃŗãŝèIJ_mćĐÕÈ2wĕKŔùb~ŋ>rĜÍä$āĉÓĦÂñīČĒe+ÿĘFùÂÑDŚÜDïijĦğşnœ5őjĩÈŗ#ò_ĭíćÜyćŃlŏÍĞť",
		"goldBlock": "0g0g9ŞNHšřWĹÃWZŠWZŢZZĜZZÐZZXYĵNH00h01hg23QVO*ÄÐN4ÓàVKh7N4Ô*ÅK0GÂlâVã0ÔãÂlVÅK6VKo3VGãÒÄ1o3ÅGÓGK18lGãÔãK0ÏlGÓGÓÓ7ÏmãÒÄÓÓÅ8gÓVK65Ä8gÒÄ1gGKÏhGKh6Ó0ßgg1gÓ0ÔÆyíEIyIyI",
		"goldOre": "0g0ga÷-ZâľHéŞZÖĎYąĩWËâZőÝZZĜYĊóWZŢH00hijhyyyiRzyz*xxO3UMlURw2*@TB@TizUÔ>0ÓíwzÔåÎjIgiwðã]hyyhOoÕíhmR3@8]0yNxxÔíë2VÃ2(I1OlQÐMyO(2)Óÿ>xQÎAoÔäì4Ôÿ8i@ìgxðÝyyI0Kywíhw2xy",
		"granite": "0g0gaĞÖWąčYúïWéUH{ĹZĞDHđĽHË3HıÆWŊaZ1xMihTÁmiãoMjMjNnhiCMûnlnihÅmÏNhNjzGwÎyjh+ÏjÞygMMmÐhjÓÏOjh1A,ÓMylxjÓÐNhMÓCM+ÐljmÓ2ÞMEh,+ÓnÎj>h+RRNMhMzhFiÓMDNÓxhoÓzãiÓgMÓh2yMMh+",
		"gravel": "0g0g8ÒÒYó7Zþ-ZÞĎYþÇHìŞZĚóWĚĢZ5,$âł#þģ_ĔÂ{ĝíİþĀijĜĺÊĞ/ÚÓŋńĝôdlĈİÿİØ$#èßgŔùĿÒčģÎðÅÖ$ÇńčY#üŁĴáįÆĚěKĞj<Ùł#ĔłÙ..$BôFĒŁŌ(ĹÉĐþcGDÚ)ľË",
		"grayConcrete": "0g0g2)ŊZ-ŊZ00000000090000000000000000S100gg",
		"grayWool": "0g0gd-śW<4HTAZTkYTAY)ŊZ<kY-ŊZ?*W?AZ?)Z.4W-ŚZ00i0)ÀÓ7jIüh71Å1ĂhóÎ^QI>g!rq1ĒgòM1yV6cy5âa5051À1IyhmĀMI@0I>đ0!OđQÝûb15hc1pVj3IkIĂQó>đmM10ObA03VjĘVĀĬiÑI*>IüÎÁ^hĐIhĎNò0ÑĘÝþbĐg.0Ă",
		"greenConcrete": "0g0g2PÏHPßHh;ĒÉÙŐ߯ðM!ľĈÇylĖ|aŗÈëÀļĸŚđøcKæÙ",
		"greenWool": "0g0gh|íWÁúZÁĉYÇĸH|úZVĨYÇĨYPßWVęYÓhYÁĉZPÏHËňHËŘHÇňHPßH|ßW0S1c4F÷4w7TÂúā^1įčĄ!rkĎMaOcÓČĬS@Ĭ4{2Āī5&!ëìõŖy91õ7.ħħ;13ĩċù1^@M9]q^ČČį8Âûl÷16ĩý÷M÷[k1yIÞEgyí,ą]1ÂýpFàcÑĘĬKĻíI80gëXÓ08ÃĄB8ÌGNfgķÑČďMÈÿpaæõĬFòÐgĎwċ]įQow-së5öÀ^Ň0Ù",
		"ironBlock": "0g0gbĺ;ZĪcWŚĶWŖĦZŒĖYŎĆHĢŒYĚIJWņéZŊ÷WľËH1g0001hgiyyO)VVÁlVVVVVVÄäIIIāĂĒďiyyzOVVÄlVVVVVVÄäIIāāĒĒďizOOVVVÄlVVVVVVÄäIIIòāĒďiyyzO*VÄlVVVVVVÄäIIòāĂĒďiyzO*VVÄlVVVVVVÄhhhmÎÓÓÓ",
		"ironOre": "0g0g9÷-ZéŞZâľHÖĎYâüZòļHĖ,HŁġWŊaY1hixzyy0hzMg1?UiiÄÂjylÎhlÔ1zOxiyw0z*ÄÒOxghÄGñÎBKxg0ÔÝig1yjN01zMh0*ÐNkÄRig@ÔÝgÔ01M0ä3x0iyhh0ghkÀgiNÄ2?ÔãÂ5Ïgi6äK1gUxhg01hh0hyh0ih",
		"jungleLog": "0g0g9ÇhYÖNWéßHÀŘHSĸW;ĨWVMYâJHÎÏW1y3OOhg004S404VQQ@ä?US4xh0hy33O(Sh04Q6ñK03OÕÑ??Vh10OO02x??V1g>O(0iwQÁy00QOñðQO)Väë0hhgÕñ4?U>(>UhQ0xh6KQQjÕÓ3)Q010Q?ÀhTg01g>O00OO",
		"jungleLogTop": "0g0g9éßHÎÏWSĸW;ĨWĢmHđŌHĦ+YýČHąīY1xiOyi1g4Q??ÒUQTAGGñIIGÏkãÑQ?Q[>BáVVVV]Â)XÅäñâ[Á*âÅVVâÅTBXÅÆïï]À5XÅÆââ[À5XÅVVïÅÁláÅGäï]Â*XVVVV[Ã)X?QQ@[ÃAññGGGGRAQV?ÄVQTgzxg0izw",
		"junglePlanks": "0g0g7ĢmHđŌHĦ+YýČHąīYåÃHÖiZ4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"lapisBlock": "0g0gdB×YxPZ*āWMIYtFHxEYsŐWt8HoĿHoĿYt7ZFÆZkĮW100gzkkkljÁÄâÓâÆ5MnVâåÒæ7ÁVÒÁãÔæ5ÞÅGÒrãÈbVVGÔĝÄä5ĝââåÔÅ×7ÅÔGğþÔĂDÔããÅãýÈDÓÞÄãÓþÆ7ÖÒlÔÅÖÆnâāÔÓlãä5ãÇþÖâÅÙlÅÓÓþÔGÆ@ÅGÓãÓâÊoIIñõāijij",
		"lapisOre": "0g0ge÷-ZéŞZâľHÖĎYËâZpÉZxÕYloZgłZĊóWhqZ?ĥZ×ÍHgŀZ1gixzyg0h)>w1jQiiÄÅ)Rwñh19aDĘhùyxiAR1Q2hgj?ĉjÕzwx]Ĥğ1Ĥ0h!ġā4Opw1bİ1/?GkRpúM/Éğ/OMOĊzôħ#Îhéĸgā1cęiādOçěĀĘ0iĤÁĤòFúixÉåpúyxhh9úhhh0",
		"lightBlueConcrete": "0g0g3C$Yy$HysHiVUS1kklk?ÀgVKk4ÂlVTVlÁhS5UhhlxTTÁkVÁ9ÁhVgFSÀ1ì5Ò5VÎkh??TlV4VlSl",
		"lightBlueWool": "0g0gq&ÙZNąW!ÊZ/ĴY@ťW&éW&öW=ŔZRŤZC;Y!ÙZC{Y~uH[eW+ĕHy$H!{Y+ĥH_eH/ńYRŔZRťWNąHC$YNöW=ńZ0Q1ùĎ/iĉö_pĿĭĉĵ2ijŎħMÙąm^ÛQđ@M7oØDÆß#čxïJ;^NV,ħijyhPí_yŃ1%ĩŌĀmÐ9Á÷zØeōMj8ÆīuÂ1ēzĆÂ?xòsĞĪõęĠRĪĔĻčŀFĿįĸõüùðP7ûÊOö>ÁàùĿü1ÝáĎĮĘÂā(Åp7@(ŒĉĿİwÛ{ÖEĭ{ÑðŐcŜÐ|òĹħÇÒ(È{I~wlĚ",
		"lightGrayConcrete": "0g0g2éŞHéŞWàĝGşąŁÊļņZÍŕYŜGHņ<Ŏʼn:|ĞćŠĞľŢŤÔŤŝ",
		"lightGrayWool": "0g0giðnYðnZí7Y÷-HþÇWJ]YþÆZéŞHí7HĆåHĂÖWóEWéŞWó-H÷-YJÆZóDZĂÖH0QxùxF÷xõ]pÂýpØi2Ğw#ÂþĻđIJNĆśČĬo{čħ×aĻĉï!!ĩNoŎ8ĩyg[TĩĈĄ19ĩĜ4_^]ÎöÐÂ^ĜČĵ8Âûl^g{Ċþ!Oķlk(aĩìĔRaĽċąÒ9ÂþĥFU{ÑĐĬóþNõ>g^ÀħøgRÃąÒë}âA[p6śČĐåÂýoØ:ÌčĝP{@ĝcĎ^ín=2SUňTPÆ^ĉ5^",
		"limeConcrete": "0g0g3ÌĉYÏĉYÌúY402ë00ë88ùEwg1204000ëëëAwëw2A0ó2Ĉì4A14gh00020wEë01g00oĈìS081Ĉ820",
		"limeWool": "0g0ggÓęY×ĨYÏęYÛĸYç1YÓĨYãŘYÏĉYßňYÛňYßŘYñNH×ĸYÌĉYH1YHhZ1w)VÓßIyĂģ@ij2ÊĹj}ĪťõÛÓŢ×úÛĨ=ÊŕúĜċÇ@éÆÂQDDÌË1ËsB1ģ@O-_ÙģÕhģÓŋ5ÛלÓB@ÁÊÂ;yÊkľįÈģ+ģ~ÓĦÓśįċFBIh@5Èéqhľ~%)ðģĊÓŢ@õĺÛOŒZĄŏĎĜVðgB]1ŖMÑ5}",
		"magentaConcrete": "0g0g4ĐİZĐıWĐġZČġZ5ÒSüVÁPTÀUÆVÇ@Ŀì2Î^áÇĮSKÊ@ņ3Äĸ45Ä@9ÎVoRtÞä4VVx}ĕãÁxQâ11Àhïxl50Îĸ",
		"magentaWool": "0g0gsĘőHĜšYĔŁHĘŁHġbWĬ:YĘőYĨ#HĨ#YĐıWĥrWĔŁWĥbWĨrHļöWĬ:ZĠšZĜšZĐİZġaZİ{WĸÙYĥrHĬ#Yİ_ZĜőYĔıWĴÊH0QNkĭ/iBE_ÐÊŎĊhqĴp$Oéz/Ĺĸ?góġdKÜ]Xü)F@5ĿÙijĩńFıõyüPÈ|>A1N>toÞàćßi×çø7.m8ÊŋuĀpė>ĞĀRnĄsĨij|ĊSRijyśĎdOÊŐĉĵĬgñÁ7ċʼnĎù{hĂķXĐpĚĻĎ%8ÂĄUPxUñ.ĢTőœxÁ{ĸPĵ÷ĖŀaFğàĴăwzÇęÎðýĀ~ìāś",
		"mossyCobblestone": "0g0gbÎŋWÁàYĎĂHÇĜWæ*HßlWÎðWóoYĞłHÚĞZÁ?W1yMj?6äBiñÞ)ÀÞÔÿÏßúÀ3úhåÓåMàågÀhđÎnÐÓâV3ì?ßþďwÝ,DgDåMnåiGhnúV1ÿÔúkÀgÝÀ+đjÃ0āG(j1å0MhpûgÂÞjj4ÁÎßDj?ú?5þGÿ47ÞÀÿåhåâååpGþn1nÓûhmÔíÝ",
		"mossyStoneBricks": "0g0gcóEYĆÖZÎŋWßlWæ*HÁàYéŞZÇĜWÇÒYåľYÖĞYÎðW1z)>xQ3?m7>R6ÓÏDgÔNNÓ(ãEmþÝ.KÖÖäpFþCÏ+ĂÕ9ßÖāzDãýĂGđďyGāÔģïģIïVÉģQMh81hjQw06ÕgK036KÓĀmùùCK6ÏĐpÖÓÝÓÓJĀ3@6āāđyĀ4ÖþÖĒGFÕÿåđĂĝVÉIïVģV",
		"netherBricks": "0g0g7oMW;ßHQJYwTH(ÎZEÁY-ÎZ000000BmÂQþòþÎČJKÑĂÅBĚÅA0+ħ0+ħÒAãÑĄ}7PAbPAæP%æ_čÙ03Ù03ŀčĂŀĕĆùÖ2ùÖ6ęŐuýİudĘłdĘłIJÔJ_ÓþJTČăTAJĸAJTA",
		"netherQuartzOre": "0g0gcÀÁHUíWÀÎYÑĊYÝĪWSÁHĕĭYľ#YĞĀZï4WŒąZÚÑW12NQOÃ)MjMBzQ5Ow>>l@äwN)ü)^GÑT3zPQďÑûQAM4@ôPÔØO>3QĜĢäĘ)ÑT5CðĜ>!òN)óKOQæÑzPďR))ĐĚk,ôÂ@QĜÂN@Ě5×SRi3>A-ä4-(k)P@ímòSzQûxÒ))Oy)R)lzQO",
		"netherrack": "0g0g7ÀÁHUíWÀÎYÑĊYÝĪWSÁHï4W4CČÛğp%ýÃIÄķ÷ORÒ6ĄĸĭõßgĺĂ)ōĵ?phú+úĎŊe#sö7)XUŊ2)ŝÖĭùÛ@s}ÕőÞį2MįőĂ?×Ö@ïÁóóe*o][oMİĈ]ġĈ}{ĸVĮrPįĄ*.r",
		"netherWartBlock": "0g0g5ä0WÕgWÆ0WüÎYĔíW02I0w10ÿ24Č1A2084oQ0ó{wwÀ0ùwJ1{8Ĺ04RE0h0Sù52ìwNë9A104ë809KkgQ4^Xy1SyIAùU1Ċ21gë4yg50g0wg17g1SÃ10ķ",
		"oakLog": "0g0g6âÐH{řHúīW-ĉYËyYāŋY50ķcyX6ħœcy4eSœ4i4{SQgNkQSīĘSÀSXTęgÀëïwT0ÀìXy1Tg5ķyh?g0ķwhko0x3gko4x3Ĉ/8Č5jĘ(wĈX1Àg0SĈj4iëSĊh42X",
		"oakLogTop": "0g0g9ËyY{řHâÐHĢVZĖ*HĩãWéîHýĻWĆkZ1210x0g0jO))U>OM3ÓGñIIGÀ3ÒÃO)O,(4àQQQQ-Sjî[äñá,T4á@QQá[(4î[]XX-S4î[]áá,Skî[QQX[T4à[ãäX-S4îQQQQ,Sjî)OO*,T3ññGGGG(3OQ)?QO(1010x0i0",
		"oakPlanks": "0g0g7ĢVZĖ*HĩãWýĻWĆkZéîHÒRZ4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"orangeConcrete": "0g0g2ňëWńëWRgguhKoCįù124Sw0x8QùĜ2Áę1ÄSSSo40",
		"orangeWool": "0g0gmŐĈHŔĘYŌùHŌĈHŘĨWŠŘYŐĈYŜňHŜňYňëWŘĸWŌùWšRYš1ZŔħZŔĘZšiWšNHŘĸHŠŘZŜĸHšyH0QNkĭ/iBE_xĿĭĉĶqijśI&Ùu]ĩ~QgðM7w_7ĤùOĉ25ö:õRýFNõyüPi|3A1NRŜĀÜÐCÂhıØeōMi8Æīudpô3ĆdR4ÓĄĶOijĚ-ROĆĻčĻMĿĩxõÙgðT7îÌċùQhđSĤĐpõáčŎäÂþīPxUð(ŒTĿĭĉ~Ph7icö]ŏEŌÐ{ÖxŊÇăxĦcÝ}ĸāĘ",
		"pinkConcrete": "0g0g3ļĀZļāWĸĀZ5QSÀ14gkgk01gQ1À0gQ4000ghÀS0?0]9kgk41Q42T4g01hÁ105k4S4hS00gggQÁS",
		"pinkWool": "0g0gvŌŀYŔšWňİHř!YŝçYŌŐZŐŐZŝÉWŝØHŀđWňİYŝ/ZńġHř/YŝijYŝôZřqHŕaHļāWŝĄWŝĤHŝ^ZŝijHŝçHŕaWŝăZŀāWŐŠZŝôYŝĔWŝ_W0Q1ùĎ/ičĕ{ÖŃŎĊh3dp((èʼn/}ęQķô.7ÕÜ-Ňà$D+ïŀ;aĹË/ŁijĩhPÏ_Nŏ1&îmooàġÞ÷bçø7.l8Êŋuå1ėOćġRpĤĄĠŃ{Ĩħ{Ń(ŤCuFŃňĉĵĶÀñ|7Œʼněö|ÁéŇŇI1ãáď7ħáĄĸĂpmñ-œTœœĉÞķĸ-eÛčr9ĔŞàĴĢċ0ÎàÀÏãë~ùlŚ",
		"polishedAndesite": "0g0g9ĎāZĞıZó.ZðEHéşWÞľWÖğWÇâYóPW11hhh1gijQ>OÃ)Ñ,jOO)SIO[3Õ8Q)Oî,jO*NîQQ,k>)Q*OQ@jOX-Iy)Åk>QQÄUO+jEÃO-ë8ñj>)>)>N,j-ëXQIO,mOOOIOIÅjIí)QÑ(ñjOQQOíQ,kQ]îO)>,ðGGGGÓGG",
		"polishedDiorite": "0g0g8Ŏ÷HłÚYĢœWIJ$YĒĢYåşWÎþWó-Y0i00J25+_5@VAkòKEĆ$ġz%)ýxįĀ!)Ĭ5ČįiC}cùTÛyĆ92Ī$ďmMĚÉQ-ą$þû%ČdB]ôÕĚ}cIł1üądíĻdjm9þĀ!(þtCİÖþąŞÉãĽŔÛ",
		"polishedGranite": "0g0g9ıÆWĞÖWĞDHđĽHąčYúïWéUHË3H{ĹZ00gwy2zz4VQU)QV?kk>)QÑR[4QÑVUOV@4QQ>VQQÄCUVQQÁU@5>)ÂQQQÅ4QQ@U>)@B@ÒQOQVBAQ?U?UQ@lQQQQAV@4TVQÑQ@Å4OQQ>)V[BU?ÏQU>+AQUQVQT[ÔGIGäGGI",
		"purpleConcrete": "0g0g4ÑòZÑåZKåYÑåYlm100ĸþTNVQgp5820Áĕ0S2RV1Àlhgìg4pĽjŏk0ÆT)S?lüUìlĻRS1ý0TTp0T]Q4T1",
		"purpleWool": "0g0gmÙóWÝĂHÕóWáĂYëģWÙĂWèĒZÑòZäĒYÕòZýcZXģWáĂHÑåZXIJHIIJYäĒZèĢZõłZXģHÙĂHIIJH0Q1ù5MeAwPTüčúì2^mČzÉí,Ģ1zċpEðS]ŇXÛ3sŋ5Ã(ħígŎĊaJg[PħŇs11ĩjü1ÂþTõÕÈõĬEù8UĊpĺ1]ŊpĺAÃPĀ1Ĉ^ÞĘPċ2ěąð1üĉĬ^ăI?-ðßĩîc]gcKXú0EÐĄC8}åĥDphlEĮòČđú1-ʼnňöıéÀĮwīÂIJ]Qw]D05Ĺo|(1Û",
		"quartzBlockBottom": "0g0g3ŖĖHŖąZŒöY05Èë?ÈĐ1ÄĒù5Ēč0ÄčÈþĒÄĒÈđV0ĒV01VU1×À0þù05006Ē05ĒĈ0ÇÒ0Vč05ĎÀ1ÄS01Ē",
		"quartzBlockSide": "0g0g6ŚĦZŖĖHŖąZŒöYŊéWņÙY0000005AăÚJ{9+łÕJV%ĞĹAČý%AăÖ+Ń9+łPĞŃ9yPÚĜý4J^PAý4Čł]J|9+òAJV4JPB+Ń4JÂÚĞĴ4üĂ|y|9CĹAJV%)òAČüJłAJ_Ú",
		"quartzBlockTop": "0g0g6ŚĦZŖĖHŖąZŒöYŊéWņÙY0000005AăÚJ{9+łÕJV%ĞĹAČý%AăÖ+Ń9+łPĞŃ9yPÚĜý4J^PAý4Čł]J|9+òAJV4JPB+Ń4JÂÚĞĴ4üĂ|y|9CĹAJV%)òAČüJłAJ_Ú",
		"quartzPillar": "0g0g4ŊéWŖĖHŚĦZŒöYh&Ņtiu&%uŕĹň&ňʼnŕŕ%xŅň%%&ňŕňyĹʼnʼnʼnŅyŕ%uŕuńŕŅhĹňŅĹiňĹʼnʼnňuyŕ%&%Ŕtŕtĸ",
		"quartzPillarTop": "0g0g5ŊéWņÙYŒöYŖĖHŚĦZ54Ăó6ÁP4ù90úFDs÷)JÎ.rJ@ë1ħJP8ĩóQŋúđi^*i^FjòĝkíF2^?k×D4×?i2CĹ^QłúđkTħJP8ħ1.AJ@îPDAJ)úF0J90J8ęk|yò",
		"redConcrete": "0g0g1õíW",
		"redNetherBricks": "0g0g7$0WÕTHÝÁY)0WQgH-gHUMW000000BmÂQþòþÎČJKÑĂÅBĚÅA0+ħ0+ħÒAãÑĄ}7PAbPAæP%æ_čÙ03Ù03ŀčĂŀĕĆùÖ2ùÖ6ęŐuýİudĘłdĘłIJÔJ_ÓþJTČăTAJĸAJTA",
		"redstoneBlock": "0g0g5ŋëYĤëYČKYÝ(WüÀW0000004íÂQí]4XĂPyI4ċzßCI0đs}Q05@łÚĘI5ĐłÚİ8a@łÚĠù9ołÚİ859q÷]I5]łdīë4đsÛ]į5BAJAù0īköyI4ù9]J]000000",
		"redstoneOre": "0g0geJ-ZéŞZâľHÖĎYÖâZügHįgHţ0WŤâZīgHü0WťEYĦPWśÞZ00ixzyhhhxxhhjNiiOzQyxhhhi*VOyiyxCÄäĂiRhgwÿğħhÚwxhcij2ighA>zyhAQh%ï1g5ÒÕĹlĸh01įĥĨMxyOx$ijyhj)Qhh0giBĽļTmķN0gôħyg2wixc1hh00hhhyg1ih",
		"redWool": "0g0gdüíWĀJWùíWĄJWĐĊWČĊWĈJWĠĹZĐĊHõíWĔĚHĔĊHĀíW1w)0VwÓ2*GïM21JjójĒÎÈVďVM^h/3ĒMçÃ3QFĭ2Q2ya919hw1GQO+ñÃGÄhGVē0^VēV$ĝ1Ĩ2OyĪoJ+5G*GĢVæVĐmÃ30Óh?0ĬylhJĢF)ÒGÂVďĝÎûÈOďĒOĎUäcÒgwĞ1ĒMÉ0ó",
		"sand": "0g0g6ŎăYł/WŊØWľpHĹŏYŖĔY4Ċĸ?ĊĂÑĚŁõr8@+9AĚŀFNĺPĊİÓþóEþ^$üúÒNÇKğÇÛiIJ$þ_%ĚbÒiĄÖüÇ5JÉ(ĚÃ(ĊıBoıÙüÇPĞÇÒĎôlmı?laEĊÇEĒú?oò?kó$üÁ",
		"smoothStone": "0g0g7éŞZâľH÷-ZĒĒYĊóWĚIJWĆåZ42ÂByg&,ÚĕŐqOÖsJ+ŀmĿłĞğy|İsãłp*ĞłÛÖĈ|łÒĕĭŀÃÖČĺPÕmĢŊÚĭő%ĞŊĞģÕ&.+úįʼndğÚĝģ×Â[ÙßÚqlįłĕĠĿN:Øãġx5wiSJg",
		"soulSand": "0g0g6ÇjYSĩH)ĊW]ĹZÖQHåïW4A3{č4ëhÕBCyÁĪFcĊňMItöþĩTįĴõĞ]dIUdħpÖ(KÙq3ÚC3ÏÈRc+İKPRì(qyĬoÖIħ}No{RĈÑwĺRĬwÒðĂëİÐAĞĀĐ^T$4Ĭö-pTÿd",
		"spruceLog": "0g0g6-úW(ÝY{ĨH$ÀY$ÝYUňZ50ķcyX6ħœcy4eSœ4i4{SQgNkQSīĘSÀSXTęgÀëïwT0ÀìXy1Tg5ķyh?g0ķwhko0x3gko4x3Ĉ/8Č5jĘ(wĈX1Àg0SĈj4iëSĊh42X",
		"spruceLogTop": "0g0g8UňZQĩWÇiHìîYåÐHòûYÎyZÞRZ4wSQ20%ğsĚ+ŀd%ĦZŤĐdÈłÞğĀj.AJ[ŇLġğŢ[ĉj]ČûPĀjġĎĺĮŇjġĎĺ[ĈOġČûİĉj.ĚņĮŇjġAJ[ĈLğrÚľĉfŖĞłÚĀdġsþ@Ŀ40SQ2ë",
		"sprucePlanks": "0g0g7ìîYåÐHòûYÎyZÞRZÇiHUňZ4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"cherryLog": "0g0g5(ßY-íZQěYAÂWUĻW5yS4ë]w0JA+ħÙiUúwĿAðĿAČIw0]ÚĞħPi0díÂQ0łFy20ioA+ħÚFi0irÙ2ùÚðĿ1ĞħKgĨ?í]FFywly]ë9dë1Pi00JĿ0JÂPy>×QP",
		"cherryLogTop": "0g0g9AÂW-íZ(ßYōŢHōŁZŎrYĵoWŅåHʼnĒW0ix0xh0h3O))U>O(jÓGñIIÓÁjÒÃO)O+MkàQQQQ-T3à[äñá+Ská@QQá@Mkî[]XX-Rkî[]áá+T4î[QQX@Skà[ãäX-TAîQQQQ,T3î)OO*+SjññGãÓÓÀjOQ)?OVÁgh0hwh2h",
		"cherryPlanks": "0g0g7ŎbYōŒHŎ$HʼnĒWōıZŅåHĵoW4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"stone": "0g0g4÷-ZéŞZâľHÖĎY0ÖĢVÇýÅōÜēđVÀ?5×þĎSB?VØĠü8!VėĢÈý1k5ÄÁÀk1ŀėā×VTV4×@ÿŕ6þčĐÖVV0VÈTÒ",
		"stoneBricks": "0g0g7óEYĆÖZÇÒYéŞZåľYÖĞYÎðW4JPAù2$(0dĞĩxðłÙ8Ł&(sÎĮyNįĪß.ÈiğAõ^ʼnĞłÚĞł×ŀ%JÉÚĞAJR4JPë0Łxë3dðʼn&8rK,!MĭĿÚĠʼni(ŋJIJÏdįŃĞł×ĞłÚłÚJ_ÚĞ",
		"tntBottom": "0g0g4ĘÂHĿęY÷-ZùęWkkkkØØØØØØØØZZZZkkkkØØØØØØØØZZZZkkkkØØØØØØØØZZZZkkkkØØØØØØØØZZZZ",
		"tntSide": "0g0gaŐ1YĿęYĘÂHùęWZZZĶ;ZņÚY)ļHoÐZĦIJW0i0i0i0ihzhzhzhzhzhzhzhzhzhzhzhzhzhzhzhz?ÓVÓÒÄÓÑ@GðâÆÔäUÖÆVGÅÄãÓ?ÔÓãGVïþ@ÆÄâÆVðUāāāāāāāāyOyOyOyOhzhzhzhzhzhzhzhzhzhzhzhzyzyzyzyz",
		"tntTop": "0g0g7ĿęYŐ1Y÷-ZĘÂHVVHùęWgTZ4ë]4ë]FNûFNû!ÂĎĸËýÛĮĕÜÓŔ4œ]4Ň]FNěó;û!]ĞŀÇýÛľ+Ĺ[ĕ4òĞłc]!{ĞĹ;û!ÃþĸÖlÛŀĔøÄŔ4ħÝĿį]!NċMOjFÂýFÂýÛĽŔÛĽŔ",
		"waterStill": "0g0g8Īc%ĖĢŔĎĂÚĺ|%Ķ;ŔľËÚZZŔŢŖĕ4üúAùŌPyPBAJBAJA0ÂFy1A2P]JJ?AP]ĊTAiJ4JPAþŗÞJTAJSŀñĀwë_PAJPAÂFAÂQíPPAJPAúPAJPkJFAPA2óPAúzÕawÿşAiP",
		"whiteConcrete": "0g0g3ĶËHĺËHĶ|H4?541S4k40ggh50g?À0Àk1wA0l4g04U0kQ?À4l00U01hÁ0044Àl0hÁ1QÀkTg4Á5h",
		"whiteWool": "0g0gnłêWŊĆHľéZłéZŒĖYŞņHł÷Wņ÷WŚĶWŚņHĺÚYŒĦZľÚYŖĶWZZZŢŖYŎĆYĺËHľÚZŎĖYŖĦZĺËYŞŖY0QNkĮRU*ĔÙÉÊŎĎ1reoįOçĪ,=ì_Ĺÿ.%Æ^ŗXø)uŜïĶÙijĚýV/Ķyü×J÷ŚU1*RmsÎàĔÂiØçú7.)8ÊŋĉŖpıś*Ŗ^ÑGĈĨ-}ĩSÂ;2šĒdOÊŐčĵçĹÿ<EçĩĞùØpĂķXđp÷ļE%8×ďĢVxÄÿ.Ĕ^sŎčÎĔĹŠĴŕàÿ7ĕčàĵæAzÎćTïŕĀÀŘāŖ",
		"yellowConcrete": "0g0g4řĨHřęHřĨYŕęHlV01zs@S1àÁá?ħń4S9551ÿOÄúKV14ÁVÁN[lÃÆśÁllħħĬĨRļŗh(0oUVUV{Á{0SľQh",
		"yellowWool": "0g0gjŝňZŝŘZŝĸYŝňYŢiHZ>WŢyYŢNZřĨHřĸYZÐZŞ2WŢ2WZÃHZÃYŢyHZNZZ>HŝřW0QNk0MķygPxüĊąØqSĜA$#ÄĮ*Œ:}GEōwKļXö2ŁĹ5ÁO0RĨ890yü]^0ķw122ĜëßÂô2hX!õĝEŖ8Uċāęowĺpę.ĹoĀ1aħJ$RaŋUxÓ1üĎt^ô}â)ōÖÄ>gQgcKXďo2Ñy@8ÀIĠ]x]âEİ.ĄĐĄô>eĽMĒàIĞùķÁSnĄz]yŘïĖK|hëĕ",
		"light": "0g0g1Zŗ7",
		"lightIcon": "0g0g1ZŗA",
		"lavaStill": "0g0g*ńĨZļÀZńęYőîZōÐHńřWįŇHĴgYĸSZŀúHńʼnWļìWļÞWʼn2HŀęYĸÁWʼnyYʼn2YōÃHʼniYňřHįŗHĸwYļÎWĸ(ZńřHʼnNZŢďWŕĻYŀĉYĴwYĴ0YńĸZńęZŕīYŕīHĸÀZōOWŀìHʼniZőČWőüWŕŋZŕĻZĸ(YʼnyZįķHőĜHīķHŚ*YōÐYőîYōàY0gRcTToß1Ay^EJij)ʼnI;Č1MkûÀĹTxVÿK6]5ĎŃÁķÆåŚwïÔĿCDz0ëÆÏ>ÒÑāĀAOÈáî8ù9Ã(İıxĹ^BGP-^0èőÃÆňË%âĨÓĒĴuĪIoí11ď]Îm÷tŞįxVõĝAòäíĽqŌĄĥð5ÖēħęÎĘVŚMĥïĈwĩij~#&tñąMVĔ(mnĴħÍKĩIoÏ9ĥ*ńCÒ[(üįKàEĒĉÈMÎýÆî9ĜùÈEÆ)|ķ[éKUPlİ",
		"obsidian": "0g0g540Y00WgMZ-ýHAàZ4Jg&1s4yìÕ8ķBĠQòl8&B28ùìMAPAë8Pië1ħ9]EŃ6g]5)óAJňBīüëĀJIüASĈëSg20ücE4RdīJdĊJö4ķú0a]0K(4w9g]SĊkQ00",
		"cryingObsidian": "0g0g840Y00WE7YgMZ-ýHAàZKqYë&H4üoNìÒ4NúõØoC^?ĕŞ]Âğàfúú*)Ğţë(ÜʼnħqÀhŃ:y6Ą^5ĭťQŊFCVńēqł~nŜÐ(řÖÛ20ÿö$DîiÄĹiNăě6ĀĝõbÜAÕ)ÓEpü]KʼntÕ0ë",
		"chiseledNetherBricks": "0g0g7QJY(ÎZ;ßH-ÎZEÁYoMWwTH0229238EpAĐýgĠòAĐd{łÚĞŁd,UĂ?ĕĕQķjPcąQĹóBsdcĻÂScĴ:VĚÉ#Úcķh]õdQĹŝŀĂdQķòSaĴ:ØĞĞłĝTĤħ0ctQĂİ%ĂËçØĞłØĝ",
		"crackedNetherBricks": "0g0g8oMW;ßHQJYwTHEÁY(ÎZ-ÎZ8gH000000BmÂXþđĚňĐĽŇ?Ġ[ÕĞĮÕ0+ħ0+ħÒzśÔs}7ÁÚałAæņÑæİAÙ00c03ŀŜîŘĖuĜb[ĿÎ@ýŖÌĝŢÌdĘłdĘłĴÔJĽÄťĖĸĕþĨļĝĨÙJÁĕ",
		"crackedPolishedBlackstoneBricks": "0g0g6(ěH|BHkMW;ŋHAìZsTY4ĊÇ(þĩcwÉeëqcīŇöĸÈ×6îÎēR{0o192ìę0óPÏúXAóôÏĝ#Â^łÚ$ĉyĘyÇ1ĉyìmÉìĂyÀþĿK/2A+îÖëJÒXy8]2Ù_$þIJ!2_Ú^ŁJ^ēÂ",
		"crackedStoneBricks": "0g0g7ÎðWéŞZĆÖZåľYóEYÇÒYÖĞY5CJFFBīùÑÝĪdQ6{-ù|Ï!PA0ń?ĀpÒíŜöĞĵïĨËçÙŝĶ<?2ķ%Ĉ00P8ĬNīJCÏâõOcùħËÎįTAĂE@i9AŌBQĀpÜRËõġÉŀōĴç<Û00%Ĝ00",
		"endStoneBricks": "0g0g8ĩŏYŒłWŖŃYŖőZĒñHņĂHĶ.HľÇH4ČJÚCQčJÉČJÛÓJŀAě},ł|ĕĞÛĜĞÚ)ěÕĦûÜŝłÕvZZğZīî0AùcA|ČIëĜJAĚ|kJPAĂŀcJŀ)ĞÚĭĽĴĔņŖĽĆÚŝš~mŢHğZşîZZëÙ4J1A",
		"polishedBlackstoneBricks": "0g0g6(ěH|BH;ŋHkMWAìZsTY4ĊÁEüî8wÂi0j8ČĈúEûT42T9Ð]0g0_Ðì#0þ_3ú8AþI3ĝģÉÛłÚEüûęiÁ14j8AÂìkûëüùSwċ]A2T8z?8wgPØ]_$þIJ#2_ÚÛŁłÛģÉ",
		"prismarine": "0g0g5ÌāH@DHă#ZæŁYR6Y5({ÕIăgiú|Āoí)Ĩc?8^m0wĎAÙRSïĘ^KmAwkāëębÓ)ijiiÈ}6R5Cj0új9íĿ5F0$8SFíň(Qį$2ûÓCijÑīħ2(zPþ00þ4{NcúĐ1X1i",
		"prismarineBricks": "0g0g8ÛőZÿ;HîrZ@DHÈĢHR6YÂÕWÏāY4J]QJî(0BwSz(86wå5g~ōST<2x<S9:y1Bĩ8jÂ1őĊPĪuTùÿĶŢÊ{SiZ3;ÏA2Ñ3ÌÐ4jʼn<ÍŋDûÐŌjŊWťÃŊiÒZJÒBăŀWłņŚÛĽŔâģÉ",
		"darkPrismarine": "0g0g6<ĝZ/6YMQW*UYAŚH.ïW4Ĉa4Ĉ^xĩsxĩsd^ŃdwĻÂAJÂByKĊĩ0Ċ×CħÙčıcdĨÊdSőÂBiÂBy4Ĉ^ĈĈ^yĩsCĩsdħĻĈxÊÁByÁByĈĈ^4ĈRxĩ$xĩsdSŃeħĻÂByÂBy",
		"seaLantern": "0g0g_ÂDYÂnYÈ,ZÂnHÂDZÈDZÄDZÌEWÈ-WÄDY}nHßĢW÷šZðőZěÊYē$WĮéYĪÚHė;HěÊZûbHãġZ÷ŢWĮéHĺöZľĆWľąZĶöYJŢW}7HĶöZņĖWņĖHŊĖHłĆWIJöYPŞWė$WņĕZņąYĺöYğÊZė{YŎĖHŎĦHŊĖWŊĕZē;H^7HĮöYŊĦWÄEWŊĦHYËWņĕY^7WłąZłĖWłĖHJŢHĪËWĮÚHßġZ÷őZľöZĺąZYÚWPŞHãđZíıHðŁYė{HãĢWÂnZ}nY048wÎ01ag8KK>IiEÆĨċñùy]úNïSó6Uęà6cĂNÕĸěÔaî/ÝġŊ[ķxRXšzÕĵAcüĨČ9ûC{ā.Ś!kągĐĸśİýĔÈęNÑ6òt6]ĸśIJmxRęÏŋ6ÿĘcīĸśIJgĕÈïJÑCôXÓõĹkIJÀĎÈĸJŋCĂX2ČĸśIJmĉRęÏá6P0eüŘśİþ$ÈęĚÑ67,ÓPrñFûCÊāMŞ@ò(2ŔňàDķĉ=ëš3f_A/ŠŠmCöS*29Đ}ıĪ/6zĠęñnwĎ/JĹ01JúzÞ|Ŀ>ÛâwgöĪ^",
		"quartzBricks": "0g0g6ŚĦZŒöYŖĖHŖąZŊéWņÙY0000019ĞİAČüdJP%AąQJÉP+ń?Ğİ%J{dJPÙJ{dĜJAþńĚ_ÙþłÑ0S0TÀë%SJPķŀPÀāAħJPķPBÀJÙĻÉPÀJBķJPķłÚUûÚīÇĚÇAĞŀ%",
		"oakDoorBottom": "0g0g9ÖğYýĻWÒRZéîHĞ*WÀřHĎ4ZĢVZíFH1xj)xlUjÎ+Q@+Q@iÎÓhOhjMiÞÂyxÂyClÞxhmxhmjÞxh)xh)iÞxj)xj)iÞ+Q@+Q@*ÎÓÎOmÎOOÞÂyxNyCiìxhmxhmi1xh)xh)lÎxj)xj)jÎ+Q@+Q@NàMÓjMÓjNBVVVVVVV",
		"oakDoorTop": "0g0gbĢVZĎ4ZýĻWÒRZÀřH000íFHĞ*WéîHÚĞZÖğY1g0100g2iyyyyyyz2>ON>ONz2*VÁ*VÁAÏ*VÅ*VÅEJ*VÁ*VÁziìGhìGhz2>ON>ONA2*VÁ*VÁEi*VÅ*VÅz2*VÁ*VÁz2ìGhìGhA2hiIhiIE2>ON>ONziNyxNC×zÕ-Iì-ó]X",
		"warpedDoorBottom": "0g0gb]ŜW/8WyŀZCĂZtVWFĭHCÖH%âWgŊH/-ZËÓY1z?ÔV]IáJVÓGhU?kBV[nāUÇüBhTnāUÇÿxā?ÅāUÁnMāTnhUylMlPÿhßVMýOPÿVAá[NlBnÅ)VllGBVGÑÁlďVRBãQß,5VQ>ÑáBàúh[áâU,V.hTllÐâk.āTlmUÁkU??Q?ááQ",
		"warpedDoorTop": "0g0gcyŀZCĂZ/8WtVWFĭHCÖH/-Z%âWËÓY]ŜWt4WgŊH0gg11102iyNy4>ÂAg4)TQ>?gÔÞVnRzà,í[Å[R>yzJAk>y>ÓÐÏwNzÓ>ÓÑw4+ÔÓzÏwÑ[+ÑÏzÞ7nQ+Ñy>ná4y+ÏS3á?2yNAlSQU2Ó+K[à1>ÏÓ+KGO,àßÓNxáĈÓċíÏN?QĞAğ",
		"endStone": "0g0g6ņĂHľÖHŖłHĭşYĶoYŞŒZ4XI}iĉAEÂùíg9n8?wRÝĊňa^üXĻTÁûÉ(!IÀħĉxoV]XIPİaĉnhČĠTÏNR]Rjïo]&wÁA2RÑüĐ9ħhB4Ï}gSS#zwíýòÂŀwAb(yT5ħĄ",
		"ironTrapdoor": "0g0g8ĚIJWĢŒYĪcWĺ;ZľËH000ņéZŎĆH4üJPi]%ĞŃöĞŀ$ČċÒBp%ÉÐ׳p|ÉÐ׳q|Éà׳Ă|İĤ÷ÜŁÁĞŋúĞʼnÁĞŋúĞʼn{ČČïBq|ÉÐ׳q%ÉÐ׳q%Éà׳ā%ĴŢÛ|ŀ%ĞŃöĞŀ4JJPi]",
		"amethystBlock": "0g0g7ÒpZåØHöĔZı~WZ&HčŖWÊŐY5ibJĈÄ!ĈÁÅÏù8ëđna]5dI4ÄòFĄ0EkMÄy9ĕjëßÈ]Eg2!ëÖQĊg6ïÈwgĐAĒÀĿċÈBÄ÷5UÀ!ľÀ!ĢIlİÖ0ŃF5DëīùÈwĒ8$íŌħkPÞsÌ",
		"ancientDebrisSide": "0g0g7ÊĹZÒkW]ĚWSÞH(ÎWéïYþnZ4ĎüQíĨArēSùI0×|w0ëSù0ctđS4î÷Ĺ0Ï#ĞÝ0g}Â|ļwí}iPCAJ|ùTzÚĕęë9kJPÝŃ0SJ]Þrę]ùPÚëdÕiSŁĈ0I2&,U2ò09AXXÅÙò",
		"ancientDebrisTop": "0g0g7SÞHéïYþnZÒkW]ĚW(ÎWÊĹZ0ČôíõęčbÕÒAàEĚČ2ķûıĸi&.2ěS5ĈĢ×ĢnÈ(qĒġöÀ6Ăìgļ|aĐXìVsCĒKx2%0×ĈUħÂÔ]čVŁ40ÈÒÅÙı?<÷EìÚĈ]ļoüúÙÀðdÓÉïkT",
		"netheriteBlock": "0g0ga;ĪW(ĊYAÞZPkYÇVY|AZ-ŊYT4W|4W$ĊY1000010ijQQQQ?QÂ4Ð*VàIOÂ4àIV*ÆãÂ4,ïïVÃÝN4àä*?VÝM4GàVUO+N4,ÒVV,+Â4*VÃ*àãM4V?GàG+MlVïGGÓ0Â4ÆÆ+ÔÓ9ÂlÐOãÔKāÂlKÓKāāùÁlO*V*VVNiyxiyhyy",
		"copperOre": "0g0ge÷-ZéŞZâľHÖĎY.ĽWËâZňīZĨüHĊóWí5Yř6Y.čYÈÆW}ŐY00g1zyhxhxyhhjNhiQ*ÃOCÝyh2U?(gìxyBPÔùhyhg.ãĐ2BÃwxh8ëzÇýxzBÃxrĴĮM0)ýM0Ĥþßsńİgi4I1w{ëlMgìih8j,Ãh1yiy)þďzzM0PëP]1)ĹgI18ëh0ghiighhg1",
		"copperBlock": "0g0g8ʼn6ZļŌYİļHĨČZčÑWęXHā>YúzW0g]02Ą9+ú4þŕ8UIAęÛeŋT?ģėdy^|İÍ9iÂÛÕŖ8íûà[Ü4kăĂ,Å0ČœĸĢÿ5DÙöĜ÷9:ÒÚČ@9łØâyecÇÉđhÅmőŔ]ăćjÅ×QüŅ/ÚŞJÚŝ",
		"cutCopper": "0g0g6ʼn6ZļŌYčÑWęXHİļHā>Y42a4ëR&1tFIńg8ýc@ĂgQči)Ōe*2Mč5%Ď2dĐ2%-B%SyÄļÚVÉV0íR4ë^Mğ2(8Ĺd-yw@J9Ĩly)ą&0ąMĜĩ(6ŌdĎ5w,y%-Bđ#VP#Â",
		"exposedCopper": "0g0g8ĵ-WĢ7HýĽWđĽWĂ+YéĬHåýHæÓY0g]02^dįňÛpÛg=^ÑħŕhĿÂOĮÌeãāJAōdĭ_ÚUÒdNÉ×ċd9OrViÌ9ĀĺĒäŅcĭĂÂRĴeQłÑğljįł%ĠBk/ŊÙħōlľxÑĮĖaŐcMBÚ+łĖĞłĕ",
		"exposedCutCopper": "0g0g8ĵ-WĢ7HéĬHĂ+YýĽWđĽWÞïWæÓY42^42ıMăm!OCsĿĊgĀĊeļ&mĬ&m?È6ÇÌ&]æ+ıæLıťMÂő_BĞÇÚþ0íR0g^mÆ^hămM^ÏLŀÄaÃmuIJCNĀĊe?ÏáăÓM!Ûœł!FÅıİ%þŀ%J",
		"weatheredCopper": "0g0g8æŀYÓďHÓñHÛVZ÷nWã*HÖīHPĭH0g]02Ą9+PÀþŕ8Â^ùęÛeŋRïłėmJa|łÍ9iÂÚ#L8JJÖ.L4üăPġÍ0Čŋ|Aņ5DzQíĆ9ĎłÑAÌ9Ļû?AĵcÈĉÂxÅmŋ^úrćmĻÂúüŅ/ŔŞZŔŝ",
		"weatheredCutCopper": "0g0g8æŀYÓďHÖīHã*H÷nWÛVZÓñHPĭH02a40R%İGi.De_ğhŀ!qĴğfÅ,rÈő:Ôğ:Ôqr|Ě,ÖD+įřÍŜZËHľ0íR40^O.y+ÚĚdĥ!NÖßlŔćNĿŞmœņ+Ğő:ËvnÅv/ġyOÖDŐLľPLĹ",
		"oxidizedCopper": "0g0g8ÜpZÈİW}đWÂñY=6Y}ÆH.ĝY.ýH0g802Ą4üŔAĎŕ4ÂĒEęÛ6ĩÇFģv9ĜÁ|ÄņdĎPBĎć5ĊPB)Ŗ5kò|čÍ4üú%iņ5ióAċÌ9ĊúB/ĆdĜİ{Łĵ$ÄĸVMÅ+ĮÇâăn9Å×Ğüþ:ÚŞZŔŝ",
		"oxidizedCutCopper": "0g0g8ÜpZÈİW<ōHÂĀY}ÆH*ýWÂñYÌĐW42a0ùRdġBe]ŌdŖBdĴšfHtf,ńrèŁ:,ĝ/ÓŁhĥy&@ŌN.yÄļÚVÉV0íR02aq{ʼni|qeèʼnqÚŁdŝń%Ŗń.œń%ĤřpŐąhŔBMİNNÖBđ#VP#Â",
		"deepslate": "0g0g5åŎYÒþHÁ?W;śW$ěH5AăAČĹ}@ł?CĺAiüJ,rÕČłQ2^öCŁAíÃ1kĊBkú4črPġqFĞįB-Ĺ|+ķEĎŁÛQúPďzEĎûß)ëBN0|+ìFĜ9FĞòÚįò}.r0üŃKČ]0ĊüSüò",
		"deepslateTop": "0g0g5ÒþHÇÒYPBWâľH;śW5yS1čgiAôdħňQñi^6ŋúhxw0ňÝEóúgëÝ0P]Ċ3ÂRú!QòdIJ0,1xĠaA78düÂQĐú1Č>Igr@S3ú2ĪIýA]JüÀðsxñhQ0ŃA,0AđyBiÂ",
		"cobbledDeepslate": "0g0g6PAZ)ĺYÖĎY;śHåŎYÇÒY0ĎqùÀĕcķijTðĐAĞ^cěÉÏ#?5JIJúÀbÑăwV0įę9#U+^íĜ3cþÈĈq1úM?cmŀÄĘ%ÎJÚSmoeī5ĉJ_Û0SĔ+U÷ë>ÒóŌSiŀÑ]ļknSčı]Ùły",
		"polishedDeepslate": "0g0g7ÚĞZÇÒY$ĚZP)ZT4W-ĺHAJH00S42a4ÿ#AþŁ4ġjÑJÈhğAĝUĖcþİAĚÏlFzCBmNÆŃöĞʼnBĭPCiÏe@ŀ&JŁl#AđÇmhJPýJŅii_úJŁaıĒĞ]JAJŋÑJÈcJĹÙJ}FÑĞIJÒĞ",
		"deepslateBricks": "0g0g7ÇÒYÚĞZT4WAJHP)Z-ĺH$ĚZ4JPAùjw00ë0ēw1w08û!8ùi5j)!ĊúFÃiļÂóčàĞłÚğÉēŀĥăçÓŅA2>4JP20ēw10i1jw40iS#y8ëýE#h10ĞČēaIĊĞłØĞłÚłÚăÜÚĞ",
		"crackedDeepslateBricks": "0g0g8ÇÒYÚĞZP)ZT4W-ĺH$ĚZAJHoÎY4ĊPAñ&wS0S0ŕw8ùJ8Ņ%5A9@Ć(;ĺPĥuaÔuĶÔĖJÖ$ĂİĎğçYťńŜA>nÀJì1(ōX0ă×IĆw40lxCRĜħVĜ&ýóëĂ+ōe5ÃĂİCJPAģÉãŁłÚ",
		"deepslateTiles": "0g0g6$ĚZAJHT4W-ĺHÇÒYP)Z0ù90J1Pā$JVÀÙpÚPAĿKp×đAĀ0qJÚĞŀAù1wgPJIJ×%ðĿĝUăc0oĒČădĘ1wg0w2PdîÑĚ^û$3ÂPAĿw2J|ĞħAù10ù9ÖAùÚíûP+ĿÙ2ă",
		"crackedDeepslateTiles": "0g0g6$ĚZAJHT4W-ĺHÇÒYP)Z4ù9wJ19ā$únÀKpÚ]AĿÑr×ĉAĀ0ùĹÒĞŀAJ1AgPJıĀ%íĿĝUŀc2oÄĎĨdĘTwg0w2PdîÑę^û$jÂ]AĿw2J|þħAù10ù9ÕAĀÑNûT+ħxĘă",
		"deepslateCoalOre": "0g0g9åŎYÒþHÁ?W;śW$ěH8wYEĊYsÞZPzZ1yyOhizyzQOQTyOzh1iÄÑ>ãONiDÓñÀhi?ÏMnV0hz0hyT0ARx1iQ>yÔÑNiOÄáhCëyzBÕÔig2NOTã0yN)>ii0z)>?Kh)QhzQM1iÆÒÁiðÂhOmÀ1zÒ4O0g3ÔM0xg0iiAwhyh",
		"deepslateCopperOre": "0g0gdåŎYÒþHÁ?W;śW$ěH.ĽWňīZĨüHí5Yř6Y.čYÈÆW}ŐY1yyOhizyzQOOxA>zkV3QO@ÝONgBÃx01i>yÆÔìghz0Eãùh)>x1i03z]XNi)>gqĤğyz?îwiēðÝ:ijĠ1y*03iÉ0z)Sygh0g7zzN1iOBðÿONhOÆRÆÀ>?Ī0hz(1iw00iiAwhyh",
		"deepslateDiamondOre": "0g0gcåŎYÒþHÁ?W;śW$ěHM4Wv|HGąWm.WÓEZľZHÎŎH1yyOhizyzQÃOxyOzh1ÏBU>äûNiû,ò0āi>yO.úghz0x!ßhďĚx1ĂãÕĚÕüNiÇăăVă>yDØOVďiON:ĜyDÕô)>iiÏzġýÂ0hzg0BÅí1i*ÀhæãÕęOğòhEÖăO0pû):Jxg0iiAwhyh",
		"deepslateEmeraldOre": "0g0gcåŎYÒþHÁ?W;śW$ěHłťYnãW1ňY*ÐZ>įHuÏYnkH1yyOhizyzQOOxQOzkÄiAQÄ)ONÔ3OxÔ1i>0kíh0hz0zPĂhxyx1oýæíOQNj]ÿēìz?Ï*ÐĢ0ii+Ý+Ý0xA>(1g2NzPĀM0hzg0åÅĊ1iOQhçæĚhO)ÄhEĈ4O0hÔ4(2xg0i0Awhyh",
		"deepslateGoldOre": "0g0g9åŎYÒþHÁ?W;śW$ěHąJWőÝZZĜYZŢH1yyNhizyzQÀzxy)>h12?Q>?ÃNiAÄÁ0ÄÂ>y*ÔÁgÓ30xÔäÎx0x1i6ãÂOQNiO(K1zCÀzÄzÀii(1OÔ21yQM>i0hz)VÏ0hQ(3BÓñÁkVKlgÔÝ1*ÔñgzÄ1O0mÝ)(0yK0g2Awhy1",
		"deepslateIronOre": "0g0gaåŎYÒþHÁ?W;śW$ěHâüZòļHĖ,HŁġWŊaY1yyOhizyzQOOxÄÒzhÔÐQ>6ÝO+ä2zxg1i>0O+Ôãhz0xÔIĀÝCÞ1i0äë3QNiOO01z>yzCàwlÔÃNOÅäìyä4>igò3)0y0hz00zBÏ1iOÔ1ÄäñÎ+á0hDòÝ30Òz)M01gh0iAxhyh",
		"deepslateLapisOre": "0g0gdåŎYÒþHÁ?W;śW$ěHpÉZxÕYloZgłZhqZ?ĥZ×ÍHgŀZ1yyOhizyz>QOxy>zhÄÅA)>ñ3N1p7Ĉg0i>y((1hhz0z*úhÕyx1-ēďyē1Npĉ00h0jy!ĚOFlGkN(0ypÈďp>giJwóĘ!Khèħ1w1rĉi(chæĊoĈO)ēÁēëw30hÈÝw2xg0ig2(hyh",
		"deepslateRedstoneOre": "0g0ggåŎYÒþHÁ?W;śW$ěH{ĺH-ĚZügHįgHţ0WŤâZīgHü0WťEYïÒYśÞZ1yyOhizyzQOOxyOzh1lÄQ>)ONlÅGÎ0hi>EäĂĤghz0xġŀňxøň1i&ŕyOŕNlÄOghV>yÍďÐwnñóř,ťyxy/ņŊuʼnNz)>ŕ0hCVKzzN1iÔşŞi-řhO=ĕhz>QO0hŕ)(ixg0iiAwhyh",
		"netherGoldOre": "0g0gcÀÁHUíWÀÎYÑĊYÝĪWSÁHę1HšĚYï4WZĜYZŢHĬúZ12NQOÃ)MjMBzR5Ow>>Ó2MÝN)X.ďk)ă3z][û]îQAM4MĜIQ:O>3RU)>!üXT5z>ç_àIN)NCOQ()zPċÂ))S2k)àÂ]QCÂ+ANbA(Ôi3>A/J4)(k)]Pçj]SzQîxC))Oy)R)lzQO",
		"soulSoil": "0g0g5ÇjY]ĹZ;ĚH)ĊWÖQH4Č^ÖgABNj|24FĘjÕû8PwÉQù1|ùİA2aÖ0]0ë^]20ùiÃwù8ëüúF1wAČP]91Fwag]9]íJJ2óSmķgiJ0Ďõëüŀ1+Qìmĸ5)KXĎòFĊ4",
		"blackstone": "0g0g6(ěHAìZwTZkMW;ŋH|BH4ČĩÑSÀwgi0]qÕJú2]ÃQ0ÀJÂôwPamüôJ^9^CÉiķıEĞh0Ďį4Iú0ČQ9ČÁByú|gaS4ŁNP3ySÃyĿ92]aE2úJÀŀF4ùþXŁÒkõACŀ{J]",
		"blackstoneTop": "0g0g5AìZ(ěH;ŋHsTYkMW5yÆEëÑòirKñaIĈa%ĚìKI^TČÀ2]iik_0xpJR>ÎyõÎįrÒk_]8İ0ĉ2AĀa2+2QòrÂ2I]-ĨQiJiNíÀč1e4ùÎyoÛ2IÑĈTQSSxĚTP]õ",
		"polishedBlackstone": "0g0g6|BH;ŋH(ěHkMWAìZsTY0ë840^5ih]ĊûEiJIíV1iúFkÒ0ĐĊEĐĊ82^IíÂBiúFkÏx2U4ĬB0XÑB?bBk9ÂAö4iÏüĀ{8ĊúÀđU5AóM^ý8ăiFq|EĐČþČUÂP$JP$",
		"gildedBlackstone": "0g0ga(ěHAìZwTZkMW;ŋHQKWŁSZégZZĜY|BH1iz2M45ÀgÔÀy04SÂ+hix6XTzÞ01wQäijg4Qi9úßlQQùhyRÁ*4ā3lijÀx0i*Ý1gRx0ix42ÔÁxhyixBx0lxgãN4QQ7gQ1î0āSm0QSÏi0iÞQP3,iwBÀPùz*Mxykh2OMzhÁg",
		"basaltSide": "0g0g5|AZoûW-ŋYËâZ(ĪZ50ĪÙ(wðSĪÙ(iN0łÙ(ëA6łc-ëQ(ĩ1waÂ+icxaJ)ķÙxgùQëÎĨ2ë(ĀÚĩëë6rÚĠăù6rdírù+rTĚs0+2ìXkëQgëwĹì6ăgAħ46rÎXo",
		"basaltTop": "0g0g7ËâZâľH|AZ)ĻWóEY;śZoûW0üÉA636mĹKÈŁč+ëeĪÀ{qì{ĀqĭhSÙJ&ħìØ5ìSĿdĀ0Ģý%ŋĐ4Ĥđ1Č5DÙįĞĈ?ġJħSKocëÈ0ĄĨwëO4ăTPëŝī(×~Ó6ĴëeÑĘ]Kwe(wS",
		"polishedBasaltSide": "0g0g6(ĪZ-ŋY|AZâľHËâZoûW5.júF8č-ĺ÷B9č-Ŋ÷*99+łöġ95@ŊöĠúB]ŊöĠú5QłòįI1AłÖįIĉ)ŊÞĎIĉ)ŊÚĭöĊ-ŊÚįï6]ŊÚĠïa-ĺÛ)I!-ĺÛ-ù!-ĺ÷-ù!.j÷Ph",
		"polishedBasaltTop": "0g0g7(ĪZ;śZâľH|AZ)ĻWËâZóEY5Cŋ^ĠįańØĽďÇ#$ýİÒÚâ/ŔÚĞû+ĥĞPCŀ.ËËĖĎĕ*ĤřİēĝÉ;ŚĒĎĕĞĜŜÖŎû#*ÂPŒĚâ*ËÛōĕĝ)JÉÓúÖÄńâğû,ÑŜÈĻúFÒÂÈĒđ6ēÈ^ĢI",
		"shroomlight": "0g0g8ļSYĜĘYŌħHťjYťĞZZåZZnZZłH4wŁ4IJÆFAAÕØOdİĜìġĜ9IJÛó[zRÍOjÕʼnaÇoăŢqcdĈăŠq×ķûqįċßŎÛØÓAØ[èİìz|<Čĵ]JÚÑĞòÉK×Őj2Íir]Ğí@ķ})ĜÎAì]AgAw9",
		"crimsonPlanks": "0g0g7èŌHÕĻYXŜY]ûHÊīWQîW;ßZ4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"crimsonStem": "0g0g8QîWÀÎWĘJH]ûHä0WüÁHI(ZUěZ0ĎňČMI0ķňčě8/iĵĪíÞTĩ1ĮĠÚLĀÇïŚđvĀç(ĘÞOŚvōjôĶě3ţįÉħgÎdĀW)ÓijX]ç!úş(ĩĿčíZis1Ĵĩv(óT$Ŋ3īN÷7h8)ĞÖ2þw!úI",
		"crimsonStemTop": "0g0gbQîWÀÎWä0W]ûHüÎYĔíWXŜYèŌHú6WÊīWÕĻY0iOTÁ34gmÓÔÔñãÓÎ@OāđĒĒāX6-ðÓÔÓÖK,þGGGG×à6ĎåĂđÿÖÝnÿàGGÿåÎÅĎåæďď×ânĎåæÿÿÖÞ[ĎåGGďåá,þåûĂď×à,ĎGGGGÖàCĎÔÓÓÕÖßmđđāāāāÎ6ÓGÔäGÓK0iOTÁ34g",
		"warpedPlanks": "0g0g7/8WFĭH/-ZtVW%âWt4WgŊH4AJ9Aî0ÿ80ùAw2cJi3ãğãğËĖaAüP2KwoÐXë1ùí_0jAŁľãŁŁŕ92ÂPAX40cùĪzSāAAā4ŁğãļłĞÀA4PQiA9cëgPNgÐ0İAĽŔÉGËĞ",
		"warpedStem": "0g0g8QîWQĝZmÄZ]ûHlïYi6HhĞYUěZ0ĎňČMI0ķňčě8/iĵĪíÞTĩ1ĮĠÚLĀÇïŚđvĀç(ĘÞOŚvōjôĶě3ţįÉħgÎdĀW)ÓijX]ç!úş(ĩĿčíZis1Ĵĩv(óT$Ŋ3īN÷7h8)ĞÖ2þw!úI",
		"warpedStemTop": "0g0gcQîWQĝZlïY]ûHlşHiÕH/-Z/8W+åWtVW%âWFĭH0iOTÁ34gmÓÔÔñãÓÎ@āĒĢģģĒX6ĀðÓÔÓ×K,ĎGGGGØà6ĞæēĢď×ÝnďåGGďæÎÅĞæçğğØânĞæçďď×Þ[ĞæGGğæá,ĎæđēğØà,ĞGGGG×àCĞÔÓÓÕ×ßmĢĢĒĒĒĒÎ6ÓGÔäGÓK0iOTÁ34g",
		"poppy": "0g0g9000œĩZĀìYĤJYáëWxiHBÏH^NYFĩY00000000000000000000000000000000000000000000i000000jx(00001)Mw00001hi000000*K0000000Ý0000000Ý0000000Ý000005KëÓ00000ÀV0000005Ò000",
		"cornflower": "0g0gb000ßWW?ĖYÏeYF;HSġY%ĊHöĤWÄ)ZÈâW<ŊH000000000000000000000000000000000011z00000xiNw00003QÂS000006Ý0000008ë0000000K00000ë0ë0000080ù00000aëù000000Đù0000006ù0000000K000",
		"dandelion": "0g0g8000ZĜZZÃYřßHĥĊWlŗH^NYÄĊZ000000000000000000000000000000000000000000001w0000aÑ0000ÇIë000cQ00005Ĉ0001Għ0000LŁë0007Ĝ00006Ĉ00",
		"jackOLantern": "0g0gaĬĸHčìWĉKYʼnTZʼnüYZ6HZïHZšWĽċHèŗZ0102g0i2O(3(j(jN4SV43Ã3RzSÄU5V(Sz*ÓÐ5ÓÀ(z*Ôà5GKOz*ÔÝlGÕOz)G(kÔã>zO>43Q)Oz**3À5*OzVVâÅVÒOzÄÔGGGÓÐw*ÐÔ7Ó6ëw)Oîz-ûNJ90ywyJ9JFwyJFJF",
		"lantern": "0g0gc000<lWP@Hò>Wì2WAěZĨíWřQYŢCHZŤHZşYTBZ1yg000003Q(002h0iyx00150@GÑ00150,Ià005l0-Ăî00000-đî00150[Iá005l0iyx00000hģh00000ryę002h0Ěy#00150Ěy#00000ryę00000hģh0000000000000",
		"carvedPumpkin": "0g0g8ĬĸHĉKYčìWʼnTZʼnüY$0WQSWèŗZ0g1S4TÚùĿ%íŀirQyĚx&#Ĕ+ķK%ńě,ׯ%ńě,ÙÉ%ńĘ,Úă%ĴĀOÚċ%Ġīe[r%ĺ>ĈÃÉ&łâĢńÉ&ŔĞłÚěxń+rÎĘxĽŚ%ŕŀŋÝ9wĆ[ŋř9ŋť~",
		"pumpkinSide": "0g0g6ĬęHĉÀYđìWʼnxZʼnüYèŗZ0g1S4TÚùĿ%íŀßlzNRx&zA&SK%íł%ĞÆ%íł%ĞÉ%Ěł%ĞÉ%Ěł%ĘÉ%Ěł%ĘÉ%ĚłĕĚr%íĿĕĚł%óĿĕĚłxóħĕóħxķ0Ĉa1ČÂ9Ĉq?ČĹPČŁ|",
		"pumpkinTop": "0g0g8ĬęHđìWʼnxZʼnüYĉÀYʼnĈWĞgWKķH0gëw211yJFyI9AjdĈg0)ÂEmò9l0ëXg9đGùyĀPĉĖʼngĀ8İŜĘ4ìxx=Ņ]Àdo7ŗ4J{ČwíyÀ8ASxQ85ĊJF2IxwĿPyú9mĸ9Ěhwgh10T",
		"cobweb": "0g0g4Į<W000ZZZŎĖYlVýUÒÃęÇÊïþÒÅÄlļUĀGÒÁãËËËrÔÁ=őļü?ÿė-Á}?ÿÇãÇąÅÔ^ÁUĺlĬÇÚÃĝÒÃĬÇlÇVU",
	}
}
 
// Compute texture coordinates so I can store them in blockData.textures.
const textures = Object.keys(texturesFunc())
const textureMap = {}
for (let i = 0; i < textures.length; i++) {
	const s = 1/16 // numberOfTexturesPerRowOfTheAtlas
	let texX = i & 15
	let texY = i >> 4
	let offsetX = texX * s
	let offsetY = texY * s
 
	textureMap[textures[i]] = new Float32Array([offsetX, offsetY, offsetX + s, offsetY, offsetX + s, offsetY + s, offsetX, offsetY + s])
}
 
class BlockData {
	id = 0
	name = ""
	textures = [new Float32Array()]
	transparent = false
	shadow = true
	lightLevel = 0
	solid = true
	icon = ""
	semiTrans = false
	hideInterior = false
	rotate = false
	flip = false
	iconImg = document.createElement("canvas")
	shape = shapes.cube
	uniqueShape = false
 
	constructor(data, id, hasIcon = true) {
		if (data instanceof BlockData) {
			const canvas = this.iconImg
			Object.assign(this, data)
 
			this.id = id
			if (!hasIcon) {
				this.iconImg = null
				this.icon = ""
			}
			else {
				canvas.width = 64
				canvas.height = 64
				this.iconImg = canvas
			}
			return
		}
		if (data.shape) this.uniqueShape = true
		this.iconImg.width = 64
		this.iconImg.height = 64
		this.id = id
		blockIds[data.name] = id
 
		if ( !("textures" in data) ) {
			data.textures = new Array(6).fill(data.name)
		}
		else if (typeof data.textures === "string") {
			data.textures = new Array(6).fill(data.textures)
		}
		else {
			const { textures } = data
 
			if (textures.length === 3) {
				textures[3] = textures[2]
				textures[4] = textures[2]
				textures[5] = textures[2]
			}
			else if (textures.length === 2) {
				// Top and bottom are the first texture, sides are the second.
				textures[2] = textures[1]
				textures[3] = textures[2]
				textures[4] = textures[2]
				textures[5] = textures[2]
				textures[1] = textures[0]
			}
		}
		for (let i = 0; i < 6; i++) {
			this.textures[i] = textureMap[data.textures[i]]
		}
		delete data.textures
 
		data.name = data.name.replace(/[A-Z]/g, " $&").replace(/./, c => c.toUpperCase())
		Object.assign(this, data)
	}
}
 
const blockData = [
	{ name: "air", textures: "tntSide", transparent: true, shadow: false, solid: false },
	{ name: "grass", textures: ["dirt", "grassTop", "grassSide"] },
	{ name: "dirt" },
	{ name: "stone" },
	{ name: "bedrock" },
	{ name: "sand" },
	{ name: "gravel" },
	{ name: "leaves", transparent: true },
	{ name: "glass", transparent: true, shadow: false, hideInterior: true },
	{ name: "cobblestone" },
	{ name: "mossyCobblestone" },
	{ name: "stoneBricks" },
	{ name: "mossyStoneBricks" },
	{ name: "bricks" },
	{ name: "coalOre" },
	{ name: "ironOre" },
	{ name: "goldOre" },
	{ name: "diamondOre" },
	{ name: "redstoneOre" },
	{ name: "lapisOre" },
	{ name: "emeraldOre" },
	{ name: "coalBlock" },
	{ name: "ironBlock" },
	{ name: "goldBlock" },
	{ name: "diamondBlock" },
	{ name: "redstoneBlock" },
	{ name: "lapisBlock" },
	{ name: "emeraldBlock" },
	{ name: "oakPlanks" },
	{ name: "oakLog", textures: ["oakLogTop", "oakLog"] },
	{ name: "acaciaPlanks" },
	{ name: "acaciaLog", textures: ["acaciaLogTop", "acaciaLog"] },
	{ name: "birchPlanks" },
	{ name: "birchLog", textures: ["birchLogTop", "birchLog"] },
	{ name: "darkOakPlanks" },
	{ name: "darkOakLog", textures: ["darkOakLogTop", "darkOakLog"] },
	{ name: "junglePlanks" },
	{ name: "jungleLog", textures: ["jungleLogTop", "jungleLog"] },
	{ name: "sprucePlanks" },
	{ name: "spruceLog", textures: ["spruceLogTop", "spruceLog"] },
	{ name: "whiteWool" },
	{ name: "orangeWool" },
	{ name: "magentaWool" },
	{ name: "lightBlueWool" },
	{ name: "yellowWool" },
	{ name: "limeWool" },
	{ name: "pinkWool" },
	{ name: "grayWool" },
	{ name: "lightGrayWool" },
	{ name: "cyanWool" },
	{ name: "purpleWool" },
	{ name: "blueWool" },
	{ name: "brownWool" },
	{ name: "greenWool" },
	{ name: "redWool" },
	{ name: "blackWool" },
	{ name: "whiteConcrete" },
	{ name: "orangeConcrete" },
	{ name: "magentaConcrete" },
	{ name: "lightBlueConcrete" },
	{ name: "yellowConcrete" },
	{ name: "limeConcrete" },
	{ name: "pinkConcrete" },
	{ name: "grayConcrete" },
	{ name: "lightGrayConcrete" },
	{ name: "cyanConcrete" },
	{ name: "purpleConcrete" },
	{ name: "blueConcrete" },
	{ name: "brownConcrete" },
	{ name: "greenConcrete" },
	{ name: "redConcrete" },
	{ name: "blackConcrete" },
	{ name: "bookshelf", textures: ["oakPlanks", "bookshelf"] },
	{ name: "netherrack" },
	{ name: "soulSand" },
	{ name: "glowstone", lightLevel: 15 },
	{ name: "netherWartBlock" },
	{ name: "netherBricks" },
	{ name: "redNetherBricks" },
	{ name: "netherQuartzOre" },
	{ name: "quartzBlock", textures: ["quartzBlockBottom", "quartzBlockTop", "quartzBlockSide"] },
	{ name: "quartzPillar", textures: ["quartzPillarTop", "quartzPillar"] },
	{ name: "chiseledQuartzBlock", textures: ["chiseledQuartzBlock", "chiseledQuartzBlockTop"] },
	{ name: "chiseledStoneBricks" },
	{ name: "smoothStone" },
	{ name: "andesite" },
	{ name: "polishedAndesite" },
	{ name: "diorite" },
	{ name: "polishedDiorite" },
	{ name: "granite" },
	{ name: "polishedGranite" },
	{ name: "light", lightLevel: 15, solid: false, transparent: true, shadow: false, semiTrans: true, icon: "lightIcon", hideInterior: true },
	{ name: "water", textures: "waterStill", semiTrans: true, transparent: true, solid: false, shadow: true, hideInterior: true },
	{ name: "lava", textures: "lavaStill", solid: false, lightLevel: 15 },
	{ name: "obsidian" },
	{ name: "cryingObsidian", lightLevel: 10 },
	{ name: "endStone" },
	{ name: "endStoneBricks" },
	{ name: "chiseledNetherBricks" },
	{ name: "crackedNetherBricks" },
	{ name: "crackedPolishedBlackstoneBricks" },
	{ name: "crackedStoneBricks" },
	{ name: "polishedBlackstoneBricks" },
	{ name: "prismarineBricks" },
	{ name: "quartzBricks" },
	{ name: "oakDoorTop", solid: false, transparent: true, icon: "oakDoorTop", shape: shapes.door },
	{ name: "oakDoorBottom", solid: false, transparent: true, icon: "oakDoorBottom", shape: shapes.door },
	{ name: "warpedDoorTop", solid: false, transparent: true, icon: "warpedDoorTop", shape: shapes.door },
	{ name: "warpedDoorBottom", solid: false, transparent: true, icon: "warpedDoorBottom", shape: shapes.door },
	{ name: "ironTrapdoor", solid: false, transparent: true, shape: shapes.cube },
	{ name: "cherryPlanks" },
	{ name: "cherryLog", textures: ["cherryLogTop", "cherryLog"] },
	{ name: "copperOre" },
	{ name: "copperBlock" },
	{ name: "cutCopper" },
	{ name: "exposedCopper" },
	{ name: "exposedCutCopper" },
	{ name: "weatheredCopper" },
	{ name: "weatheredCutCopper" },
	{ name: "oxidizedCopper" },
	{ name: "oxidizedCutCopper" },
	{ name: "prismarine" },
	{ name: "darkPrismarine" },
	{ name: "seaLantern", lightLevel: 15, shadow: false },
	{ name: "netherGoldOre" },
	{ name: "ancientDebris", textures: ["ancientDebrisTop", "ancientDebrisSide"] },
	{ name: "netheriteBlock" },
	{ name: "soulSoil" },
	{ name: "blackstone", textures: ["blackstoneTop", "blackstone"] },
	{ name: "polishedBlackstone" },
	{ name: "gildedBlackstone" },
	{ name: "basalt", textures: ["basaltTop", "basaltSide"] },
	{ name: "polishedBasalt", textures: ["polishedBasaltTop", "polishedBasaltSide"] },
	{ name: "shroomlight", lightLevel: 15 },
	{ name: "crimsonPlanks" },
	{ name: "crimsonStem", textures: ["crimsonStemTop", "crimsonStem"] },
	{ name: "warpedPlanks" },
	{ name: "warpedStem", textures: ["warpedStemTop", "warpedStem"] },
	{ name: "amethystBlock" },
	{ name: "deepslate", textures: ["deepslateTop", "deepslate"] },
	{ name: "cobbledDeepslate" },
	{ name: "polishedDeepslate" },
	{ name: "deepslateBricks" },
	{ name: "crackedDeepslateBricks" },
	{ name: "deepslateTiles" },
	{ name: "crackedDeepslateTiles" },
	{ name: "deepslateCoalOre" },
	{ name: "deepslateCopperOre" },
	{ name: "deepslateDiamondOre" },
	{ name: "deepslateEmeraldOre" },
	{ name: "deepslateGoldOre" },
	{ name: "deepslateIronOre" },
	{ name: "deepslateLapisOre" },
	{ name: "deepslateRedstoneOre" },
	{
		name: "poppy",
		textures: ["air", "poppy"],
		solid: false,
		transparent: true,
		shadow: false,
		icon: "poppy",
		shape: shapes.flower
	},
	{
		name: "cornflower",
		textures: ["air", "cornflower"],
		solid: false,
		transparent: true,
		shadow: false,
		icon: "cornflower",
		shape: shapes.flower
	},
	{
		name: "dandelion",
		textures: ["air", "dandelion"],
		solid: false,
		transparent: true,
		shadow: false,
		icon: "dandelion",
		shape: shapes.flower
	},
	{
		name: "cobweb",
		textures: ["air", "cobweb"],
		solid: false,
		transparent: true,
		icon: "cobweb",
		shape: shapes.flower
	},
	{ name: "pumpkin", textures: ["pumpkinTop", "pumpkinSide"] },
	{ name: "carvedPumpkin", textures: ["pumpkinTop", "pumpkinTop", "pumpkinSide", "pumpkinSide", "pumpkinSide", "carvedPumpkin"], rotate: true, shape: shapes.cube },
	{
		name: "jackOLantern",
		textures: ["pumpkinTop", "pumpkinTop", "pumpkinSide", "pumpkinSide", "pumpkinSide", "jackOLantern"],
		shadow: "false",
		lightLevel: 15,
		rotate: true,
		shape: shapes.cube
	},
	{
		name: "lantern",
		solid: false,
		transparent: true,
		shadow: false,
		lightLevel: 15,
		shape: shapes.lantern
	},
	{
		name: "oakFence",
		textures: "oakPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "acaciaFence",
		textures: "acaciaPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "birchFence",
		textures: "birchPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "darkOakFence",
		textures: "darkOakPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "jungleFence",
		textures: "junglePlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "spruceFence",
		textures: "sprucePlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "cherryFence",
		textures: "cherryPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "netherBrickFence",
		textures: "netherBricks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "crimsonFence",
		textures: "crimsonPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	{
		name: "warpedFence",
		textures: "warpedPlanks",
		transparent: true,
		shadow: false,
		shape: shapes.fence
	},
	// Removed because everyone wants them to explode, but they don't explode.
	/* {
 	name: "tnt",
 	textures: ["tntBottom", "tntTop", "tntSide"]
	},*/
].map((data, i) => new BlockData(data, i))
blockData[0].iconImg = null // Air doesn't need an icon.
 
const BLOCK_COUNT = blockData.length
 
const Block = {
	top: 0x4,
	bottom: 0x8,
	north: 0x20,
	south: 0x10,
	east: 0x2,
	west: 0x1,
}
 
window.parent.exports["src/js/blockData.js"] = { texturesFunc, blockData, BLOCK_COUNT, blockIds, Block, BlockData }}
		</script>
		<script id="src/js/indexDB.js" type="application/javascript">
{const createDatabase = async () => {
	return await new Promise((resolve, reject) => {
		let request = window.indexedDB.open("MineKhan", 1)
 
		request.onupgradeneeded = function(event) {
			let DB = event.target.result
			// Worlds will contain and ID containing the timestamp at which the world was created, a "saved" timestamp,
			// and a "data" string that's identical to the copy/paste save string
			let store = DB.createObjectStore("worlds", { keyPath: "id" })
			store.createIndex("id", "id", { unique: true })
			store.createIndex("data", "data", { unique: false })
		}
 
		request.onsuccess = function() {
			resolve(request.result)
		}
 
		request.onerror = function(e) {
			console.error(e)
			reject(e)
		}
	})
}
const loadFromDB = async (id) => {
	let db = await createDatabase()
	let trans = db.transaction("worlds", "readwrite")
	let store = trans.objectStore("worlds")
	let req = id ? store.get(id) : store.getAll()
	return await new Promise(resolve => {
		req.onsuccess = function() {
			resolve(req.result)
			db.close()
		}
		req.onerror = function() {
			resolve(null)
			db.close()
		}
	})
}
const saveToDB = async (id, data) => {
	let db = await createDatabase()
	let trans = db.transaction("worlds", "readwrite")
	let store = trans.objectStore("worlds")
	let req = store.put({ id: id, data: data })
	return new Promise((resolve, reject) => {
		req.onsuccess = function() {
			resolve(req.result)
		}
		req.onerror = function(e) {
			reject(e)
		}
	})
}
const deleteFromDB = async (id) => {
	let db = await createDatabase()
	let trans = db.transaction("worlds", "readwrite")
	let store = trans.objectStore("worlds")
	let req = store.delete(id)
	return new Promise((resolve, reject) => {
		req.onsuccess = function() {
			resolve(req.result)
		}
		req.onerror = function(e) {
			reject(e)
		}
	})
}
 
window.parent.exports["src/js/indexDB.js"] = { createDatabase, loadFromDB, saveToDB, deleteFromDB }}
		</script>
		<script id="src/js/inventory.js" type="application/javascript">
{const { SLAB, STAIR, shapes } = window.parent.exports["src/js/shapes.js"]
const { blockData, blockIds } = window.parent.exports["src/js/blockData.js"]
 
/**
* @type {HTMLCanvasElement}
*/
const invCanvas = document.getElementById("inventory")
const invCtx = invCanvas.getContext("2d")
 
/**
* @type {HTMLCanvasElement}
*/
const containerCanvas = document.getElementById("container")
const contCtx = containerCanvas.getContext("2d")
 
const heldItemCanvas = document.createElement("canvas")
heldItemCanvas.style.zIndex = 2
heldItemCanvas.style.pointerEvents = "none"
heldItemCanvas.width = 64
heldItemCanvas.height = 64
heldItemCanvas.className = "hidden corner"
heldItemCanvas.id = "heldItem"
document.body.append(heldItemCanvas)
 
invCanvas.oncontextmenu = heldItemCanvas.oncontextmenu = containerCanvas.oncontextmenu = function(e) {
	e.preventDefault()
}
 
const heldCtx = heldItemCanvas.getContext("2d")
 
/**
 * @type {HTMLDivElement}
 */
const hoverBox = document.getElementById("onhover")
 
const displayHoverText = (text, mouseX, mouseY) => {
	hoverBox.textContent = text
	hoverBox.classList.remove("hidden")
	if (mouseY < window.parent.innerHeight / 2) {
		hoverBox.style.bottom = ""
		hoverBox.style.top = mouseY + 10 + "px"
	}
	else {
		hoverBox.style.top = ""
		hoverBox.style.bottom = window.parent.innerHeight - mouseY + 10 + "px"
	}
	if (mouseX < window.parent.innerWidth / 2) {
		hoverBox.style.right = ""
		hoverBox.style.left = mouseX + 10 + "px"
	}
	else {
		hoverBox.style.left = ""
		hoverBox.style.right = window.parent.innerWidth - mouseX + 10 + "px"
	}
}
 
class InventoryItem {
	/**
	 * @param {Number} id
	 * @param {String} name
	 * @param {Number} stackSize
	 * @param {HTMLCanvasElement} icon
	 */
	constructor(id, name, stackSize, icon) {
		this.id = id
		this.name = name
		this.stackSize = stackSize
		this.icon = icon
	}
 
	/**
	 * @param {CanvasRenderingContext2D} ctx
	 * @param {Number} x
	 * @param {Number} y
	 * @param {Number} width
	 */
	render(ctx, x, y, width) {
		if (!this.icon) return
		ctx.drawImage(this.icon, x, y, width, width)
 
		if (this.stackSize > 1) {
			ctx.font = "12px Monospace"
			ctx.textAlign = "right"
			ctx.fillStyle = "white"
			ctx.fillText(this.stackSize.toString(), x + width - 4, y + width - 4)
		}
	}
	copy() {
		return new InventoryItem(this.id, this.name, this.stackSize, this.icon)
	}
}
 
const air = new InventoryItem(0, "Air", 1, null)
 
class InventoryPage {
	creative = true
	left = 0
	top = 0
	slotSize = 64
	size = 27
	width = 9 * this.slotSize
	height = Math.ceil(this.size / 9) * this.slotSize
	hoverIndex = -1
 
	/**
	 * @type {Array<InventoryItem>}
	 */
	items = []
 
	/**
	 * @param {CanvasRenderingContext2D} context The context to render to.
	 * @param {HTMLCanvasElement} icon The icon for the inventory page. Like a stair block for the stair inventory or whatever.
	 */
	constructor(context, icon) {
		this.icon = icon
		this.ctx = context
	}
 
	/**
	 * @param {InventoryItem} item
	 */
	addItem(item) {
		if (!item || item === air) return
		for (let i = 0; i < this.size; i++) {
			if (!this.items[i]) {
				this.items[i] = item
				return
			}
			if (this.items[i].id === item.id) {
				this.items[i].stackSize += item.stackSize
				return
			}
		}
	}
	sortByName() {
		this.items.sort((a, b) => a.name.localeCompare(b.name))
	}
	sortById() {
		this.items.sort((a, b) => a.id - b.id)
	}
 
	indexAt(x, y) {
		if (x < this.left || y < this.top || x > this.left + this.width || y > this.top + this.height) return -1
		x = (x - this.left) / this.slotSize | 0
		y = (y - this.top) / this.slotSize | 0
		if (x < 0 || x > 9 || y < 0 || y * 9 + x >= this.size) return -1
		return y * 9 + x
	}
 
	renderRow(left, top, slotSize, index) {
		for (let px = 0; px < 9 && index < this.size; px++) {
			if (this.items[index]?.icon) {
				this.items[index].render(this.ctx, left + px * slotSize, top, slotSize)
			}
			index++
		}
	}
 
	/**
	 * @param {Number} left
	 * @param {Number} top
	 * @param {Number} slotSize
	 */
	render(left = this.left, top = this.top, slotSize = this.slotSize) {
		// Save render data so we'll have it for click detection
		this.left = left
		this.top = top
		this.slotSize = slotSize
		this.width = 9 * slotSize
		this.height = Math.ceil(this.size / 9) * slotSize
		this.ctx.canvas.height = top + this.height + 10 // Clears the canvas like ctx.clearRect
		this.ctx.canvas.width = this.width + left * 2
 
		// Draw the blocks
		let drawn = 0
		for (let py = 0; drawn < this.size; py++) {
			this.renderRow(left, top + py * slotSize, slotSize, drawn)
			drawn += 9
		}
 
		// Draw the grid
		this.ctx.lineWidth = 4
		this.ctx.strokeStyle = "black"
		this.ctx.beginPath()
		for (let y = 0; y <= this.height; y += slotSize) {
			this.ctx.moveTo(left,              top + y)
			this.ctx.lineTo(left + this.width, top + y)
		}
		for (let x = 0; x <= this.width; x += slotSize) {
			this.ctx.moveTo(left + x, top)
			this.ctx.lineTo(left + x, top + this.height)
		}
		this.ctx.stroke()
	}
	/**
	 * @param {MouseEvent} event
	 */
	mouseMove(event) {
		const mouseX = event.offsetX
		const mouseY = event.offsetY
		const overIndex = this.indexAt(mouseX, mouseY)
		if (this.items[overIndex]) displayHoverText(this.items[overIndex].name, event.x, event.y)
		if (this.hoverIndex === overIndex) return
		this.ctx.lineWidth = 4
 
		// Clear the previous highlight
		if (this.hoverIndex >= 0) {
			this.ctx.strokeStyle = "black"
			const x = this.hoverIndex % 9 * this.slotSize + this.left
			const y = (this.hoverIndex / 9 | 0) * this.slotSize + this.top
			this.ctx.strokeRect(x, y, this.slotSize, this.slotSize)
		}
		this.hoverIndex = overIndex
 
		// Draw new highlight and hover text
		if (overIndex >= 0 && this.items[overIndex]?.icon) {
			this.ctx.strokeStyle = "white"
			const x = overIndex % 9 * this.slotSize + this.left
			const y = (overIndex / 9 | 0) * this.slotSize + this.top
			this.ctx.strokeRect(x, y, this.slotSize, this.slotSize)
		}
		else hoverBox.classList.add("hidden")
	}
 
	/**
	 * What happens when the inventory is clicked
	 * @param {InventoryItem} heldItem The item being dragged around by the mouse
	 * @returns InvenetoryItem
	 */
	mouseClick(heldItem) {
		if (this.hoverIndex === -1) return null
		if (this.creative) {
			if (heldItem?.id === this.items[this.hoverIndex].id) {
				if (heldItem.stackSize < 64) heldItem.stackSize++
				return heldItem
			}
			return this.items[this.hoverIndex].copy() // Discard the previously held item
		}
		let old = this.items[this.hoverIndex]
		if (!heldItem && !old) return null
		if (old?.id === heldItem?.id) {
			old.stackSize += heldItem.stackSize
			if (old.stackSize > 64) {
				heldItem.stackSize = old.stackSize - 64
				old.stackSize = 64
				old = heldItem
			}
			else old = null
		}
		else this.items[this.hoverIndex] = heldItem || null
 
		// Redraw the tile
		const x = this.hoverIndex % 9 * this.slotSize + this.left
		const y = (this.hoverIndex / 9 | 0) * this.slotSize + this.top
		this.ctx.clearRect(x, y, this.slotSize, this.slotSize)
		if (this.items[this.hoverIndex]) {
			this.items[this.hoverIndex].render(this.ctx, x, y, this.slotSize)
			this.ctx.strokeStyle = "white"
		}
		else invCtx.strokeStyle = "black"
		invCtx.strokeRect(x, y, this.slotSize, this.slotSize)
 
		return old
	}
 
	/**
	 * @param {InventoryItem | Number} item
	 * @param {Number} index
	 */
	setItem(item, index) {
		if (!item) {
			this.items[index] = null
		}
		else if (item instanceof InventoryItem) {
			this.items[index] = item
		}
		else {
			this.items[index] = new InventoryItem(item, blockData[item].name, 1, blockData[item].iconImg)
		}
	}
}
 
class Hotbar {
	/**
	 * @param {InventoryPage} inventory
	 * @param {Number} start The first index in the inv to use as the hotbar
	 */
	constructor(inventory, start) {
		this.inventory = inventory
		this.start = this.index = start
 
		/**
		 * @type {HTMLCanvasElement}
		 */
		this.canvas = document.getElementById("hotbar")
		this.ctx = this.canvas.getContext("2d")
	}
 
	// Make for..of loops loop over the correct elements
	*[Symbol.iterator]() {
		for (let i = this.start; i < this.inventory.size; i++) yield this.inventory.items[i]?.id || 0
	}
 
	pickBlock(blockID) {
		let empty = -1
		for (let i = this.start; i < this.inventory.size; i++) {
			if (this.inventory.items[i]?.id === blockID) {
				this.select("black")
				this.index = i
				this.select("white")
				return
			}
			else if (!this.inventory.items[i] && empty === -1) empty = i
		}
 
		if (empty >= 0 && this.hand !== air) {
			this.select("black")
			this.index = empty
		}
		else this.inventory.addItem(this.inventory.items[this.index])
		let itemData = blockData[blockID]
		this.inventory.items[this.index] = new InventoryItem(blockID, itemData.name, 1, itemData.iconImg)
		this.render()
	}
 
	setPosition(index) {
		this.select("black")
		this.index = this.start + index
		this.select("white")
	}
	shiftPosition(amount) {
		this.select("black")
		this.index += Math.sign(amount)
		if (this.index >= this.inventory.size) this.index -= 9
		if (this.index < this.start) this.index += 9
		this.select("white")
	}
 
	get hand() {
		return this.inventory.items[this.index] || air
	}
 
	select(color) {
		this.ctx.lineWidth = 4
		this.ctx.strokeStyle = color
 
		const width = this.inventory.slotSize
		this.ctx.strokeRect(2 + width * (this.index - this.start), 2, width, width)
	}
 
	render() {
		const width = this.inventory.slotSize
		this.canvas.width = width * 9 + 4
		this.canvas.height = width + 4
		this.ctx.lineWidth = 4
		this.ctx.strokeStyle = "black"
 
		for (let i = 0; i < 9; i++) {
			const x = 2 + width * i
			this.inventory.items[this.start + i]?.render(this.ctx, x, 2, width)
			this.ctx.strokeRect(x, 2, width, width)
		}
		this.select("white")
	}
}
 
class InventoryManager {
	/**
	 * @type {Array<InventoryPage>}
	 */
	containers = []
	currentPage = 0
	canvas = invCanvas
	iconSize = 64
 
	/**
	 * @type {InventoryItem}
	 */
	heldItem = null
 
	// Don't initialize the inventory before the icons have been generated!
	init(creative) {
		// Creative Inventories
		if (creative) {
			let cubes = new InventoryPage(contCtx, blockData[blockIds.grass].iconImg)
			let slabs = new InventoryPage(contCtx, blockData[blockIds.smoothStone | SLAB].iconImg)
			let stairs = new InventoryPage(contCtx, blockData[blockIds.oakPlanks | STAIR].iconImg)
			let decor = new InventoryPage(contCtx, blockData[blockIds.poppy].iconImg)
			for (let id in blockData) {
				const block = blockData[id]
				// eslint-disable-next-line no-prototype-builtins
				if (!block.iconImg) continue
 
				let item = new InventoryItem(+id, block.name, 1, block.iconImg)
 
				if (block.shape === shapes.cube && block.solid) {
					cubes.items.push(item)
				}
				else if (block.shape === shapes.slab && block.solid) {
					slabs.items.push(item)
				}
				else if (block.shape === shapes.stair && block.solid) {
					stairs.items.push(item)
				}
				else {
					decor.items.push(item)
				}
			}
			cubes.size = cubes.items.length
			slabs.size = slabs.items.length
			stairs.size = stairs.items.length
			decor.size = decor.items.length
			this.containers.push(cubes, slabs, stairs, decor)
		}
		containerCanvas.onmousemove = e => this.mouseMove(e)
		containerCanvas.onmousedown = e => this.mouseClick(e)
		this.render()
 
		// Survival/hotbar inventory
		let storage = new InventoryPage(invCtx, blockData[blockIds.bookshelf].iconImg)
		storage.creative = false
		this.playerStorage = storage
		this.hotbar = new Hotbar(storage, 27)
		storage.size = 36
		storage.render(10, 10, this.iconSize)
 
		containerCanvas.onkeydown = invCanvas.onkeydown = window.parent.canvas.onkeydown
		containerCanvas.onkeyup = invCanvas.onkeyup = window.parent.canvas.onkeyup
 
		invCanvas.onmousemove = e => {
			storage.mouseMove(e)
		}
		invCanvas.onmousedown = () => {
			this.heldItem = storage.mouseClick(this.heldItem)
 
			if (this.heldItem) {
				heldItemCanvas.classList.remove("hidden")
				heldCtx.clearRect(0, 0, this.iconSize, this.iconSize)
				this.heldItem.render(heldCtx, 0, 0, this.iconSize)
			}
			else heldItemCanvas.classList.add("hidden")
 
			for (let i = 0; i < this.hotbar.length; i++) {
				this.hotbar[i] = storage.items[i + 27]?.id || 0
			}
		}
	}
 
	render() {
		const left = 10
		const top = 10
		const tileSize = this.iconSize
 
		this.containers[this.currentPage].render(left, top + tileSize + 5, tileSize)
 
		for (let i = 0; i < this.containers.length; i++) {
			const inv = this.containers[i]
			contCtx.drawImage(inv.icon, left + i * tileSize, top, tileSize, tileSize)
			contCtx.strokeStyle = "red"
			contCtx.strokeRect(left + tileSize * i, top, tileSize, tileSize)
		}
		contCtx.strokeStyle = "green"
		contCtx.strokeRect(left + tileSize * this.currentPage, top, tileSize, tileSize)
	}
 
	/**
	 * @param {MouseEvent} event
	 */
	mouseMove(event) {
		this.containers[this.currentPage].mouseMove(event)
	}
 
	mouseClick(event) {
		const mouseX = event.offsetX
		const mouseY = event.offsetY
		if (mouseY < 10 + this.iconSize && mouseY > 10 && mouseX > 10 && mouseX < 10 + this.iconSize * this.containers.length) {
			let newPage = (mouseX - 10) / this.iconSize | 0
			if (newPage !== this.currentPage) {
				this.currentPage = newPage
				this.render()
			}
		}
		else {
			this.heldItem = this.containers[this.currentPage].mouseClick(this.heldItem)
			if (this.heldItem) {
				heldItemCanvas.classList.remove("hidden")
				heldCtx.clearRect(0, 0, this.iconSize, this.iconSize)
				this.heldItem.render(heldCtx, 0, 0, this.iconSize)
			}
			else heldItemCanvas.classList.add("hidden")
		}
	}
 
	/**
	 * @param {Number} newSize
	 */
	set size(newSize) {
		heldItemCanvas.width = heldItemCanvas.height = this.iconSize = newSize
		if (this.playerStorage) {
			this.playerStorage.render(10, 10, newSize)
			this.render()
		}
	}
}
 
const inventory = new InventoryManager()
window.parent.exports["src/js/inventory.js"] = { InventoryItem, InventoryPage, InventoryManager, inventory }}
		</script>
		<script id="src/js/glUtils.js" type="application/javascript">
{const createProgramObject = (curContext, vetexShaderSource, fragmentShaderSource) => {
	let vertexShaderObject = curContext.createShader(curContext.VERTEX_SHADER)
	curContext.shaderSource(vertexShaderObject, vetexShaderSource)
	curContext.compileShader(vertexShaderObject)
	if (!curContext.getShaderParameter(vertexShaderObject, curContext.COMPILE_STATUS)) {
		throw curContext.getShaderInfoLog(vertexShaderObject)
	}
 
	let fragmentShaderObject = curContext.createShader(curContext.FRAGMENT_SHADER)
	curContext.shaderSource(fragmentShaderObject, fragmentShaderSource)
	curContext.compileShader(fragmentShaderObject)
	if (!curContext.getShaderParameter(fragmentShaderObject, curContext.COMPILE_STATUS)) {
		throw curContext.getShaderInfoLog(fragmentShaderObject)
	}
 
	let programObject = curContext.createProgram()
	curContext.attachShader(programObject, vertexShaderObject)
	curContext.attachShader(programObject, fragmentShaderObject)
	curContext.linkProgram(programObject)
	if (!curContext.getProgramParameter(programObject, curContext.LINK_STATUS)) {
		throw "Error linking shaders."
	}
 
	return programObject
}
 
const uniformMatrix = (gl, glCache, cacheId, programObj, vrName, transpose, matrix) => {
	let vrLocation = glCache[cacheId]
	if(vrLocation === undefined) {
		vrLocation = gl.getUniformLocation(programObj, vrName)
		glCache[cacheId] = vrLocation
	}
	gl.uniformMatrix4fv(vrLocation, transpose, matrix)
}
 
/**
 *
 * @param {WebGLRenderingContext} gl
 * @param {{}} glCache
 * @param {String} cacheId
 * @param {WebGLProgram} programObj
 * @param {String} vrName
 * @param {Number} size
 * @param {WebGLBuffer} VBO
 */
const vertexAttribPointer = (gl, glCache, cacheId, programObj, vrName, size, VBO) => {
	let vrLocation = glCache[cacheId]
	if(vrLocation === undefined) {
		vrLocation = gl.getAttribLocation(programObj, vrName)
		glCache[cacheId] = vrLocation
	}
	if (vrLocation !== -1) {
		gl.enableVertexAttribArray(vrLocation)
		gl.bindBuffer(gl.ARRAY_BUFFER, VBO)
		gl.vertexAttribPointer(vrLocation, size, gl.FLOAT, false, 0, 0)
 
	}
}
 
window.parent.exports["src/js/glUtils.js"] = { createProgramObject, uniformMatrix, vertexAttribPointer }}
		</script>
		<script id="src/js/texture.js" type="application/javascript">
{const { texturesFunc } = window.parent.exports["src/js/blockData.js"]
 
/**
 * Texture name to index map.
 */
const hitboxTextureCoords = new Float32Array(192)
const textureSize = 256
const scale = 1 / 16
const texturePixels = new Uint8Array(textureSize * textureSize * 4)
 
/**
 * @param {WebGLRenderingContext} gl
 */
const initTextures = (gl, glCache) => {
	const setPixel = function(textureNum, x, y, r, g, b, a) {
		let texX = textureNum & 15
		let texY = textureNum >> 4
		let offset = (texY * 16 + y) * 1024 + texX * 64 + x * 4
		texturePixels[offset] = r
		texturePixels[offset + 1] = g
		texturePixels[offset + 2] = b
		texturePixels[offset + 3] = a !== undefined ? a : 255
	}
 
	const base256CharSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEF!#$%&L(MNO)*+,-./:;<=WSTR>Q?@[]P^_{|}~ÀÁÂÃUVÄÅÆÇÈÉÊËÌÍKÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãGäåæçèéêHëìíîXïðñIòóôõö÷øùúJûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦYħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťZ'
	const base256DecodeMap = new Map()
	for (let i = 0; i < 256; i++) base256DecodeMap.set(base256CharSet[i], i)
	const decodeByte = (str) => {
		let num = 0
		for (let char of str) {
			num <<= 8
			num += base256DecodeMap.get(char)
		}
		return num
	}
 
	const getPixels = function(str, r = 255, g = 255, b = 255) {
		if (Array.isArray(r)) {
			[r, g, b] = r
		}
		const width = decodeByte(str.substr(0, 2))
		const height = decodeByte(str.substr(2, 2))
		const colorCount = decodeByte(str.substr(4, 1))
		const colors = []
		const pixels = new Uint8ClampedArray(width * height * 4)
		for (let i = 3; i < pixels.length; i += 4) pixels[i] = 255
		let pixi = 0
 
		for (let i = 0; i < colorCount; i++) {
			const num = decodeByte(str.substr(5 + i * 3, 3))
 
			let alpha = (num & 63) << 2
			let blue  = (num >>> 6 & 63) << 2
			let green = (num >>> 12 & 63) << 2
			let red   = (num >>> 18 & 63) << 2
			if (alpha >= 240) alpha = 255 // Make sure we didn't accidentally make the texture transparent
 
			if (red === blue && red === green) {
				red = red / 252 * r | 0
				green = green / 252 * g | 0
				blue = blue / 252 * b | 0
			}
			colors.push([red, green, blue, alpha])
		}
 
		// Special case for a texture filled with 1 pixel color
		if (colorCount === 1) {
			while (pixi < pixels.length) {
				pixels[pixi + 0] = colors[0][0]
				pixels[pixi + 1] = colors[0][1]
				pixels[pixi + 2] = colors[0][2]
				pixels[pixi + 3] = colors[0][3]
				pixi += 4
			}
			return pixels
		}
 
		let bytes = []
		for (let i = 5 + colorCount * 3; i < str.length; i++) { // Load the bit-packed index array
			const byte = decodeByte(str[i])
			bytes.push(byte)
		}
 
		const bits = Math.ceil(Math.log2(colorCount))
		const bitMask = (1 << bits) - 1
		let filledBits = 8
		let byte = bytes.shift()
		while (bytes.length || filledBits) {
			let num = 0
			if (filledBits >= bits) { // The entire number is inside the byte
				num = byte >> filledBits - bits & bitMask
				if (filledBits === bits && bytes.length) {
					byte = bytes.shift()
					filledBits = 8
				}
				else filledBits -= bits
			}
			else {
				num = byte << bits - filledBits & bitMask // Only part of the number is in the byte
				byte = bytes.shift() // Load in the next byte
				num |= byte >> 8 - bits + filledBits // Apply the rest of the number from this byte
				filledBits += 8 - bits
			}
 
			pixels[pixi + 0] = colors[num][0]
			pixels[pixi + 1] = colors[num][1]
			pixels[pixi + 2] = colors[num][2]
			pixels[pixi + 3] = colors[num][3]
			pixi += 4
		}
		return pixels
	}
 
	const textures = texturesFunc(setPixel, getPixels)
 
	{
		// Set all of the textures into 1 big tiled texture
		let n = 0
		for (let name in textures) {
			if (typeof textures[name] === "function") {
				textures[name](n)
			}
			else if (typeof textures[name] === "string") {
				let pix = name.includes("water")
					? getPixels(textures[name], 40, 100, 220)
					: getPixels(textures[name])
				for (let j = 0; j < pix.length; j += 4) {
					setPixel(n, j >> 2 & 15, j >> 6, pix[j], pix[j+1], pix[j+2], pix[j+3])
				}
			}
 
			if (name === "hitbox") {
				//Set the hitbox texture to 1 point
				let texX = n & 15
				let texY = n >> 4
				let offsetX = texX * scale + 0.01
				let offsetY = texY * scale + 0.01
				for (let i = 0; i < 192; i += 2) {
					hitboxTextureCoords[i] = offsetX
					hitboxTextureCoords[i + 1] = offsetY
				}
			}
			n++
		}
	}
 
	const makeTexture = (index, width, height, pixels, edge) => {
		const texture = gl.createTexture()
		gl.activeTexture(index)
		gl.bindTexture(gl.TEXTURE_2D, texture)
		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
		gl.generateMipmap(gl.TEXTURE_2D)
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, edge)
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, edge)
		return texture
	}
 
	// Big texture with everything in it
	makeTexture(gl.TEXTURE0, textureSize, textureSize, texturePixels, gl.CLAMP_TO_EDGE)
	gl.uniform1i(glCache.uSampler, 0)
 
	// Dirt texture for the background
	let dirtPixels = new Uint8Array(getPixels(textures.dirt))
	makeTexture(gl.TEXTURE1, 16, 16, dirtPixels, gl.REPEAT)
}
 
const animateTextures = (gl) => {
	// const x = Math.random() * 16 | 0
	// const y = Math.random() * 16 | 0
 
	// const d = Math.random() * 0.25 + 0.65
	// texturePixels[y * textureSize * 4 + x * 4 + 0] = 75 * d
	// texturePixels[y * textureSize * 4 + x * 4 + 1] = 125 * d
	// texturePixels[y * textureSize * 4 + x * 4 + 2] = 64 * d
 
	// gl.activeTexture(gl.TEXTURE0) // Moved to play()
	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, textureSize, textureSize, 0, gl.RGBA, gl.UNSIGNED_BYTE, texturePixels)
}
 
window.parent.exports["src/js/texture.js"] = { initTextures, hitboxTextureCoords, animateTextures }}
		</script>
		<script id="src/shaders/skyFrag.glsl" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform float uTime;
uniform vec3 uSun;
uniform vec3 uHorizon;
varying vec3 position;
 
const vec3 skyColor = vec3(0.25, 0.45, 0.7);
const vec3 sunColor = vec3(1.0, 1.0, 0.7);
const vec3 moonColor = vec3(0.7);
void main (void) {
	vec3 dir = normalize(position);
	float horizonal = 1.0 - abs(dir.y);
 
	float sunDot = dot(dir, uSun);
	vec3 col = mix(skyColor, uHorizon, horizonal * horizonal * (sunDot * 0.5 + 1.2)); // Mix the sky and the horizon
 
	// The sky starts getting darker when it's 30% above the horizon, then reachest max darkness at 50% below the horizon
	col *= max(smoothstep(-0.5, 0.3, -uSun.y), 0.1);
 
	// Draw the sun
	float sun = 1.0 - max(sunDot * 50.0 - 49.0, 0.0);
	col = mix(col, sunColor, 1.0 - sun * sun);
 
	if (dot(dir, -uSun) > 0.994) col = moonColor; // Draw the moon
	gl_FragColor = vec4(col, 1.0);
}
		</script>
		<script id="src/shaders/skyVert.glsl" type="x-shader/x-vertex">
attribute vec3 aVertex;
uniform float uTime;
uniform mat4 uView;
varying vec3 position;
mat4 no_translate (mat4 mat) {
	mat4 nmat = mat;
	nmat[3].xyz = vec3(0.0);
 
	return nmat;
}
void main(void) {
   position = aVertex;
   gl_Position = no_translate(uView) * vec4(aVertex * -100.0, 0.0);
}
		</script>
		<script id="src/js/sky.js" type="application/javascript">
{const skyFragmentShaderSrc = document.getElementById("src/shaders/skyFrag.glsl").textContent
const skyVertexShaderSrc = document.getElementById("src/shaders/skyVert.glsl").textContent
const { createProgramObject } = window.parent.exports["src/js/glUtils.js"]
/**
 * Initialize the skybox shaders and return the render function
 * @param {WebGLRenderingContext} gl gl
 * @returns Rendering function
 */
const getSkybox = (gl, glCache, program3D, program3DFogless) => {
	// I can't explain why, but the directions are all backwards from the world coordinates
	const vertexData = new Float32Array([
		// top
		-1, -1, -1, // 1
		 1, -1, -1, // 2
		 1, -1,  1, // 3
		 1, -1,  1, // 3
		-1, -1,  1, // 4
		-1, -1, -1, // 1
 
		// bottom
		-1,  1, -1, // 1
		-1,  1,  1, // 2
		 1,  1,  1, // 3
		 1,  1,  1, // 3
		 1,  1, -1, // 4
		-1,  1, -1, // 1
 
		// south
		1, -1, -1, // 1
		1,  1, -1, // 2
		1,  1,  1, // 3
		1,  1,  1, // 3
		1, -1,  1, // 4
		1, -1, -1, // 1
 
		// north
		-1, -1, -1, // 1
		-1, -1,  1, // 2
		-1,  1,  1, // 3
		-1,  1,  1, // 3
		-1,  1, -1, // 4
		-1, -1, -1, // 1
 
		// west
		-1, -1, 1, // 1
		 1, -1, 1, // 2
		 1,  1, 1, // 3
		 1,  1, 1, // 3
		-1,  1, 1, // 4
		-1, -1, 1, // 1
 
		// east
		-1, -1, -1, // 1
		-1,  1, -1, // 2
		 1,  1, -1, // 3
		 1,  1, -1, // 3
		 1, -1, -1, // 4
		-1, -1, -1, // 1
	])
 
	const buffer = gl.createBuffer()
	gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
	gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW)
 
	const skyboxProgram = createProgramObject(gl, skyVertexShaderSrc, skyFragmentShaderSrc)
 
	const aVertex = gl.getAttribLocation(skyboxProgram, "aVertex")
	const uTime = gl.getUniformLocation(skyboxProgram, "uTime")
	const uView = gl.getUniformLocation(skyboxProgram, "uView")
	const uSun = gl.getUniformLocation(skyboxProgram, "uSun")
	const uHorizon = gl.getUniformLocation(skyboxProgram, "uHorizon")
 
	const dayLength = 600 // seconds
	const horizonDay = [0.65, 0.7, 0.8]
	const horizonDawn = [0.95, 0.35, 0.2]
 
	const smoothstep = (edge0, edge1, x) => {
		let t = Math.min(Math.max((x - edge0) / (edge1 - edge0), 0.0), 1.0)
		return t * t * (3.0 - 2.0 * t)
	}
	return function renderSkybox(time, view) {
		time %= dayLength
		const cos = Math.cos(time * Math.PI * 2 / dayLength)
		const sin = Math.sin(time * Math.PI * 2 / dayLength)
		const mag = Math.sqrt(cos*cos + sin*sin*2)
		let sunset = 1 - Math.abs(cos / mag)
		sunset *= sunset
 
		const horizonColor = [
			(horizonDawn[0] - horizonDay[0]) * sunset + horizonDay[0],
			(horizonDawn[1] - horizonDay[1]) * sunset + horizonDay[1],
			(horizonDawn[2] - horizonDay[2]) * sunset + horizonDay[2],
		]
 
		const sun = [sin/mag, cos/mag, sin/mag]
		const skyBrightness = Math.max(smoothstep(-0.5, 0.3, -cos/mag), 0.1)
 
		// Set uniform for program3DFogless
		gl.useProgram(program3DFogless)
		gl.uniform1f(glCache.uTimeFogless, skyBrightness)
 
		// Set uniform for program3D
		gl.useProgram(program3D)
		gl.uniform3f(glCache.uSky, horizonColor[0], horizonColor[1], horizonColor[2])
		gl.uniform3f(glCache.uSun, sun[0], sun[1], sun[2])
		gl.uniform1f(glCache.uTime, skyBrightness)
 
		gl.useProgram(skyboxProgram)
		gl.disableVertexAttribArray(glCache.aSkylight)
		gl.disableVertexAttribArray(glCache.aBlocklight)
		gl.disableVertexAttribArray(glCache.aShadow)
		gl.disableVertexAttribArray(glCache.aTexture)
 
		gl.uniform1f(uTime, time)
		gl.uniform3f(uSun, sun[0], sun[1], sun[2])
		gl.uniform3f(uHorizon, horizonColor[0], horizonColor[1], horizonColor[2])
		gl.uniformMatrix4fv(uView, false, view)
 
		gl.depthFunc(gl.LEQUAL)
 
		gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
		gl.vertexAttribPointer(aVertex, 3, gl.FLOAT, false, 0, 0)
		gl.drawArrays(gl.TRIANGLES, 0, 6 * 6)//, gl.UNSIGNED_INT, 0)
 
		gl.depthFunc(gl.LESS)
	}
}
 
window.parent.exports["src/js/sky.js"] = { getSkybox }}
		</script>
		<script id="src/js/chunk.js" type="application/javascript">
{const { random, randomSeed, hash, noiseProfile } = window.parent.exports["src/js/random.js"]
const { blockData, blockIds, Block } = window.parent.exports["src/js/blockData.js"]
const { BitArrayBuilder, BitArrayReader } = window.parent.exports["src/js/utils.js"]
 
const { floor, max, abs } = Math
const semiTrans = new Uint8Array(blockData.filter((data, i) => data && i < 256).map(data => data.semiTrans ? 1 : 0))
const transparent = new Uint8Array(1 << 13) // 5 bits of block state
for (let i = 0; i < blockData.length; i++) transparent[i] = blockData[i].transparent ? 1 : 0
const hideInterior = new Uint8Array(255)
hideInterior.set(blockData.slice(0, 255).map(data => data.hideInterior))
 
transparent.fill(1, 256) // Anything other than default cubes should be considered transparent for lighting and face culling
 
const shadow = new Uint8Array(blockData.map(data => data.shadow ? 1 : 0))
const lightLevels = new Uint8Array(blockData.map(data => data.lightLevel || 0))
 
// Save the coords for a small sphere used to carve out caves
let sphere = new Int8Array([-2,-1,-1,-2,-1,0,-2,-1,1,-2,0,-1,-2,0,0,-2,0,1,-2,1,-1,-2,1,0,-2,1,1,-1,-2,-1,-1,-2,0,-1,-2,1,-1,-1,-2,-1,-1,-1,-1,-1,0,-1,-1,1,-1,-1,2,-1,0,-2,-1,0,-1,-1,0,0,-1,0,1,-1,0,2,-1,1,-2,-1,1,-1,-1,1,0,-1,1,1,-1,1,2,-1,2,-1,-1,2,0,-1,2,1,0,-2,-1,0,-2,0,0,-2,1,0,-1,-2,0,-1,-1,0,-1,0,0,-1,1,0,-1,2,0,0,-2,0,0,-1,0,0,0,0,0,1,0,0,2,0,1,-2,0,1,-1,0,1,0,0,1,1,0,1,2,0,2,-1,0,2,0,0,2,1,1,-2,-1,1,-2,0,1,-2,1,1,-1,-2,1,-1,-1,1,-1,0,1,-1,1,1,-1,2,1,0,-2,1,0,-1,1,0,0,1,0,1,1,0,2,1,1,-2,1,1,-1,1,1,0,1,1,1,1,1,2,1,2,-1,1,2,0,1,2,1,2,-1,-1,2,-1,0,2,-1,1,2,0,-1,2,0,0,2,0,1,2,1,-1,2,1,0,2,1,1])
 
// {
// 	let blocks = []
// 	let radius = 3.5
// 	let radsq = radius * radius
// 	for (let i = -radius; i <= radius; i++) {
// 		for (let j = -radius; j <= radius; j++) {
// 			for (let k = -radius; k <= radius; k++) {
// 				if (i*i + j*j + k*k < radsq) {
// 					blocks.push(i|0, j|0, k|0)
// 				}
// 			}
// 		}
// 	}
// 	sphere = new Int8Array(blocks)
// }
// console.log(sphere)
 
const carveSphere = (x, y, z, world) => {
	if (y > 3) {
		for (let i = 0; i < sphere.length; i += 3) {
			world.setWorldBlock(x + sphere[i], y + sphere[i + 1], z + sphere[i + 2], blockIds.air, true)
		}
	}
}
 
let getShadows
{
	const shade = [1, 0.85, 0.7, 0.6, 0.3]
	const ret = []
	getShadows = [
		b => { // Bottom
			ret[0] = shade[b[0] + b[3] + b[1] + b[4]]*0.75
			ret[1] = shade[b[3] + b[6] + b[4] + b[7]]*0.75
			ret[2] = shade[b[7] + b[4] + b[8] + b[5]]*0.75
			ret[3] = shade[b[4] + b[1] + b[5] + b[2]]*0.75
			return ret
		},
		b => { // Top
			ret[0] = shade[b[20] + b[19] + b[23] + b[22]]
			ret[1] = shade[b[26] + b[25] + b[23] + b[22]]
			ret[2] = shade[b[24] + b[21] + b[25] + b[22]]
			ret[3] = shade[b[18] + b[21] + b[19] + b[22]]
			return ret
		},
		b => { // North
			ret[0] = shade[b[17] + b[26] + b[23] + b[14]]*0.95
			ret[1] = shade[b[11] + b[20] + b[23] + b[14]]*0.95
			ret[2] = shade[b[11] + b[2]  + b[5]  + b[14]]*0.95
			ret[3] = shade[b[17] + b[8]  + b[5]  + b[14]]*0.95
			return ret
		},
		b => { // South
			ret[0] = shade[b[9]  + b[18] + b[12] + b[21]]*0.95
			ret[1] = shade[b[21] + b[12] + b[24] + b[15]]*0.95
			ret[2] = shade[b[12] + b[3]  + b[15] + b[6]]*0.95
			ret[3] = shade[b[0]  + b[9]  + b[3]  + b[12]]*0.95
			return ret
		},
		b => { // East
			ret[0] = shade[b[15] + b[24] + b[16] + b[25]]*0.8
			ret[1] = shade[b[25] + b[16] + b[26] + b[17]]*0.8
			ret[2] = shade[b[16] + b[7] + b[17] + b[8]]*0.8
			ret[3] = shade[b[6] + b[15] + b[7] + b[16]]*0.8
			return ret
		},
		b => { // West
			ret[0] = shade[b[11] + b[20] + b[10] + b[19]]*0.8
			ret[1] = shade[b[19] + b[10] + b[18] + b[9]]*0.8
			ret[2] = shade[b[10] + b[1]  + b[9]  + b[0]]*0.8
			ret[3] = shade[b[2]  + b[11] + b[1]  + b[10]]*0.8
			return ret
		},
	]
}
 
const average = (l, a, b, c, d) => {
	a = l[a]
	b = l[b]
	c = l[c]
	d = l[d]
	let count = 1
	let zero = 0
	let total = a
	if (b && abs(a-b) <= 2) {
		total += b
		count++
	}
	else zero++
	if (c && abs(a-c) <= 2) {
		total += c
		count++
	}
	else zero++
	if (d && abs(a-d) <= 2) {
		total += d
		count++
	}
	else zero++
 
	let mx = max(a, b, c, d)
	if (mx > 2) {
		return total / (count * 15)
	}
	if (mx > 1) {
		return zero ? total / (count * 15 + 15) : total / (count * 15)
	}
	return total / 60
}
 
let getLight
{
	const blocks = []
	getLight = [
		(lights27, ret, blockMask, blockShift) => { // Bottom
			const face = (lights27[4] & blockMask) >> blockShift
			if (face === 0 || face === 15) {
				const n = face / 15
				ret[0] = n
				ret[1] = n
				ret[2] = n
				ret[3] = n
				return ret
			}
			blocks[0] = (lights27[0] & blockMask) >> blockShift
			blocks[1] = (lights27[3] & blockMask) >> blockShift
			blocks[2] = (lights27[6] & blockMask) >> blockShift
			blocks[3] = (lights27[1] & blockMask) >> blockShift
			blocks[4] = face
			blocks[5] = (lights27[7] & blockMask) >> blockShift
			blocks[6] = (lights27[2] & blockMask) >> blockShift
			blocks[7] = (lights27[5] & blockMask) >> blockShift
			blocks[8] = (lights27[8] & blockMask) >> blockShift
 
			ret[0] = average(blocks, 4, 0, 1, 3)
			ret[1] = average(blocks, 4, 1, 2, 5)
			ret[2] = average(blocks, 4, 5, 7, 8)
			ret[3] = average(blocks, 4, 3, 6, 7)
			return ret
		},
		(lights27, ret, blockMask, blockShift) => { // Top
			const face = (lights27[22] & blockMask) >> blockShift
			if (face === 0 || face === 15) {
				const n = face / 15
				ret[0] = n
				ret[1] = n
				ret[2] = n
				ret[3] = n
				return ret
			}
			blocks[0] = (lights27[18] & blockMask) >> blockShift
			blocks[1] = (lights27[21] & blockMask) >> blockShift
			blocks[2] = (lights27[24] & blockMask) >> blockShift
			blocks[3] = (lights27[19] & blockMask) >> blockShift
			blocks[4] = face
			blocks[5] = (lights27[25] & blockMask) >> blockShift
			blocks[6] = (lights27[20] & blockMask) >> blockShift
			blocks[7] = (lights27[23] & blockMask) >> blockShift
			blocks[8] = (lights27[26] & blockMask) >> blockShift
 
			ret[0] = average(blocks, 4, 3, 6, 7)
			ret[1] = average(blocks, 4, 5, 7, 8)
			ret[2] = average(blocks, 4, 1, 2, 5)
			ret[3] = average(blocks, 4, 0, 1, 3)
			return ret
		},
		(lights27, ret, blockMask, blockShift) => { // North
			const face = (lights27[14] & blockMask) >> blockShift
			if (face === 0 || face === 15) {
				const n = face / 15
				ret[0] = n
				ret[1] = n
				ret[2] = n
				ret[3] = n
				return ret
			}
			blocks[0] = (lights27[2] & blockMask) >> blockShift
			blocks[1] = (lights27[5] & blockMask) >> blockShift
			blocks[2] = (lights27[8] & blockMask) >> blockShift
			blocks[3] = (lights27[11] & blockMask) >> blockShift
			blocks[4] = face
			blocks[5] = (lights27[17] & blockMask) >> blockShift
			blocks[6] = (lights27[20] & blockMask) >> blockShift
			blocks[7] = (lights27[23] & blockMask) >> blockShift
			blocks[8] = (lights27[26] & blockMask) >> blockShift
 
			ret[0] = average(blocks, 4, 5, 7, 8)
			ret[1] = average(blocks, 4, 3, 6, 7)
			ret[2] = average(blocks, 4, 0, 1, 3)
			ret[3] = average(blocks, 4, 1, 2, 5)
			return ret
		},
		(lights27, ret, blockMask, blockShift) => { // South
			const face = (lights27[12] & blockMask) >> blockShift
			if (face === 0 || face === 15) {
				const n = face / 15
				ret[0] = n
				ret[1] = n
				ret[2] = n
				ret[3] = n
				return ret
			}
			blocks[0] = (lights27[0] & blockMask) >> blockShift
			blocks[1] = (lights27[9] & blockMask) >> blockShift
			blocks[2] = (lights27[18] & blockMask) >> blockShift
			blocks[3] = (lights27[3] & blockMask) >> blockShift
			blocks[4] = face
			blocks[5] = (lights27[21] & blockMask) >> blockShift
			blocks[6] = (lights27[6] & blockMask) >> blockShift
			blocks[7] = (lights27[15] & blockMask) >> blockShift
			blocks[8] = (lights27[24] & blockMask) >> blockShift
 
			ret[0] = average(blocks, 4, 1, 2, 5)
			ret[1] = average(blocks, 4, 5, 7, 8)
			ret[2] = average(blocks, 4, 3, 6, 7)
			ret[3] = average(blocks, 4, 0, 1, 3)
			return ret
		},
		(lights27, ret, blockMask, blockShift) => { // East
			const face = (lights27[16] & blockMask) >> blockShift
			if (face === 0 || face === 15) {
				const n = face / 15
				ret[0] = n
				ret[1] = n
				ret[2] = n
				ret[3] = n
				return ret
			}
			blocks[0] = (lights27[6] & blockMask) >> blockShift
			blocks[1] = (lights27[15] & blockMask) >> blockShift
			blocks[2] = (lights27[24] & blockMask) >> blockShift
			blocks[3] = (lights27[7] & blockMask) >> blockShift
			blocks[4] = face
			blocks[5] = (lights27[25] & blockMask) >> blockShift
			blocks[6] = (lights27[8] & blockMask) >> blockShift
			blocks[7] = (lights27[17] & blockMask) >> blockShift
			blocks[8] = (lights27[27] & blockMask) >> blockShift
 
			ret[0] = average(blocks, 4, 1, 2, 5)
			ret[1] = average(blocks, 4, 5, 7, 8)
			ret[2] = average(blocks, 4, 3, 6, 7)
			ret[3] = average(blocks, 4, 0, 1, 3)
			return ret
		},
		(lights27, ret, blockMask, blockShift) => { // West
			const face = (lights27[10] & blockMask) >> blockShift
			if (face === 0 || face === 15) {
				const n = face / 15
				ret[0] = n
				ret[1] = n
				ret[2] = n
				ret[3] = n
				return ret
			}
			blocks[0] = (lights27[0] & blockMask) >> blockShift
			blocks[1] = (lights27[9] & blockMask) >> blockShift
			blocks[2] = (lights27[18] & blockMask) >> blockShift
			blocks[3] = (lights27[1] & blockMask) >> blockShift
			blocks[4] = face
			blocks[5] = (lights27[19] & blockMask) >> blockShift
			blocks[6] = (lights27[2] & blockMask) >> blockShift
			blocks[7] = (lights27[11] & blockMask) >> blockShift
			blocks[8] = (lights27[20] & blockMask) >> blockShift
 
			ret[0] = average(blocks, 4, 5, 7, 8)
			ret[1] = average(blocks, 4, 1, 2, 5)
			ret[2] = average(blocks, 4, 0, 1, 3)
			ret[3] = average(blocks, 4, 3, 6, 7)
			return ret
		}
	]
}
 
class Chunk {
	/**
	 * @param {Number} x
	 * @param {Number} z
	 * @param {World} world
	 * @param undefined glExtensions
	 * @param {WebGLRenderingContext} gl
	 * @param {Object} glCache
	 * @param {Boolean} superflat
	 * @param {Boolean} caves
	 */
	constructor(x, z, world, glExtensions, gl, glCache, superflat, caves) {
		this.x = x
		this.z = z
		this.maxY = 0
		this.minY = 255
		this.tops = new Uint8Array(16 * 16) // Store the heighest block at every (x,z) coordinate
		this.optimized = false
		this.generated = false // Terrain
		this.populated = superflat // Details and ores
		this.lit = false
		this.lightDropped = false
		this.edited = false
		this.loaded = false
		// vao for this chunk
		this.vao = glExtensions.vertex_array_object.createVertexArrayOES()
		this.caves = !caves
		this.world = world
		this.gl = gl
		this.glCache = glCache
		this.glExtensions = glExtensions
		this.doubleRender = false
		this.blocks = new Int16Array(16*16*256)
		this.originalBlocks = new Int16Array(0)
		this.light = new Uint8Array(16*16*256)
		this.renderData = []
		this.renderLength = 0
		this.hasBlockLight = false
 
		// These are temporary and will be removed after the chunk is generated.
		this.blockSpread = []
		this.visFlags = new Int8Array(0)
		this.shadowFlags = new Int8Array(0)
	}
	getBlock(x, y, z) {
		// if (y < 0 || y > 255) debugger
		return this.blocks[y * 256 + x * 16 + z]
	}
	setBlock(x, y, z, blockID, user) {
		if (user && !this.edited) {
			this.edited = true
			this.saveData = null
			if (!this.originalBlocks.length) this.originalBlocks = this.blocks.slice() // save originally generated chunk
		}
 
		if (semiTrans[blockID & 255]) {
			this.doubleRender = true
			if (!this.world.doubleRenderChunks.includes(this)) {
				this.world.doubleRenderChunks.push(this)
			}
		}
		this.blocks[y * 256 + x * 16 + z] = blockID
	}
	deleteBlock(x, y, z, user) {
		if (user && !this.edited) {
			this.edited = true
			this.saveData = null
			if (!this.originalBlocks.length) this.originalBlocks = this.blocks.slice() // save originally generated chunk
		}
		this.blocks[y * 256 + x * 16 + z] = 0
		this.minY = y < this.minY ? y : this.minY
	}
 
	processBlocks() {
		// Do some pre-processing for dropLight, optimize, and genMesh. It's more efficient to do it all at once.
		const { blocks, maxY, blockSpread, world } = this
		const trans = transparent
 
		const chunkN = world.getChunk(this.x, this.z + 16).blocks
		const chunkS = world.getChunk(this.x, this.z - 16).blocks
		const chunkE = world.getChunk(this.x + 16, this.z).blocks
		const chunkW = world.getChunk(this.x - 16, this.z).blocks
 
		const flags = new Int8Array((maxY + 1) * 256)
		this.visFlags = flags
		// this.shadowFlags = new Int32Array(flags.length >> 5)
		for (let i = flags.length - 256; i < flags.length; i++) {
			flags[i] = 8 // Up is Air
		}
		for (let i = 0; i < 255; i++) this.shadowFlags[i] = 1
		for (let i = 256; i < flags.length; i++) {
			const x = i >> 4 & 15
			const z = i & 15
			const b = blocks[i]
 
			// Set bit flags on adjacent blocks
			if (trans[b] === 1) {
				if (b === 0) flags[i] ^= 128 // This is an air block, so it can't be rendered
				else if (hideInterior[b] === 1) {
					flags[i] ^= (b === (i >> 4 & 15 ? blocks[i - 16] : chunkW[i + 240])) << 0
					| (b === (~i >> 4 & 15 ? blocks[i + 16] : chunkE[i - 240])) << 1
					| (b === blocks[i - 256]) << 2
					| (b === blocks[i + 256]) << 3
					| (b === (i & 15 ? blocks[i - 1] : chunkS[i + 15])) << 4
					| (b === (~i & 15 ? blocks[i + 1] : chunkN[i - 15])) << 5
				}
 
				flags[i - 256] ^= 8 // Top face of block below is visible
				if (i < flags.length - 256) flags[i + 256] ^= 4 // Bottom face of block above is visible
				if (z)      flags[i - 1] ^= 32 // South face of North block is visible
				if (z < 15) flags[i + 1] ^= 16 // North face of South block is visible
				if (x)      flags[i - 16] ^= 2 // West face of East block is visible
				if (x < 15) flags[i + 16] ^= 1 // East face of West block is visible
			}
 
			// Some lighting stuff
			const light = lightLevels[255 & b]
			if (light) {
				if (!blockSpread[light]) blockSpread[light] = []
				blockSpread[light].push(this.x + x, i >> 8, this.z + z)
				this.light[i] |= light * 16
			}
		}
	}
 
	dropLight() {
		// Drop light from the sky without spreading it.
		if (this.lightDropped) return
		const { blocks, hasBlockLight } = this
 
		if (!hasBlockLight) this.light.fill(15, (this.maxY + 1) * 256)
		else {
			// May introduce a subtle lighting glitch, but it's worth it for the time savings.
			let end = Math.min((this.maxY + 14) * 256, blocks.length)
			this.light.fill(15, end)
			for (let i = this.maxY * 256; i < end; i++) this.light[i] |= 15
		}
 
		// Set vertical columns of light to level 15
		this.tops.fill(0)
		for (let x = 0; x < 16; x++) {
			for (let z = 0; z < 16; z++) {
				for (let y = this.maxY; y > 0; y--) {
					const block = blocks[y * 256 + x * 16 + z]
 
					if (block && !transparent[block]) {
					// if ((visFlags[y*256 + x*16 + z - 256] & 8) === 0) {
						this.tops[x * 16 + z] = y
						break
					}
					this.light[y * 256 + x * 16 + z] |= 15
				}
			}
		}
 
		this.lightDropped = true
	}
	fillLight() {
		const { world } = this
		this.processBlocks()
		this.dropLight()
		let blockSpread = this.blockSpread
		this.blockSpread = null
 
		// Drop light in neighboring chunk borders so we won't need to spread into them as much.
		world.getChunk(this.x - 1, this.z).dropLight()
		world.getChunk(this.x + 17, this.z).dropLight()
		world.getChunk(this.x, this.z - 1).dropLight()
		world.getChunk(this.x, this.z + 17).dropLight()
 
		// Spread the light to places where the vertical columns stopped earlier, plus chunk borders
		let spread = []
		for (let x = 0; x < 16; x++) {
			for (let z = 0; z < 16; z++) {
				for (let y = this.tops[x * 16 + z] + 1; y <= this.maxY; y++) {
					if (   x === 15 || this.tops[x * 16 + z + 1 ] > y
						|| x === 0  || this.tops[x * 16 + z - 1 ] > y
						|| z === 15 || this.tops[x * 16 + z + 16] > y
						|| z === 0  || this.tops[x * 16 + z - 16] > y
					) {
						spread.push(x + this.x, y, z + this.z)
					}
					else break
				}
			}
		}
		this.spreadLight(spread, 14)
 
		for (let i = blockSpread.length - 1; i > 0; i--) {
			let blocks = blockSpread[i]
			if (blocks && blocks.length) {
				this.spreadLight(blocks, i - 1, false, 1)
			}
		}
 
		this.lit = true
	}
	setLight(x, y, z, level) {
		const i = y * 256 + x * 16 + z
		this.light[i] = level
		// debugger
	}
	setBlockLight(x, y, z, level) {
		this.hasBlockLight = true
		const i = y * 256 + x * 16 + z
		this.light[i] = level << 4 | this.light[i] & 15
	}
	setSkyLight(x, y, z, level) {
		const i = y * 256 + x * 16 + z
		this.light[i] = level | this.light[i] & 240
	}
	getLight(x, y, z) {
		return this.light[y * 256 + x * 16 + z]
	}
	getBlockLight(x, y, z) {
		return this.light[y * 256 + x * 16 + z] >>> 4
	}
	getSkyLight(x, y, z) {
		return this.light[y * 256 + x * 16 + z] & 15
	}
	trySpread(x, y, z, level, spread, blockLight, update = false) {
		if (y > 255) return
		const { world } = this
		if (world.getLight(x, y, z, blockLight) < level) {
			if (transparent[world.getBlock(x, y, z)]) {
				world.setLight(x, y, z, level, blockLight)
				spread.push(x, y, z)
			}
		}
		if (update && (x < this.x || x > this.x + 15 || z < this.z || z > this.z + 15)) {
			let chunk = world.getChunk(x, z)
			if (chunk.buffer && !world.meshQueue.includes(chunk)) {
				world.meshQueue.push(chunk)
			}
		}
	}
	spreadLight(blocks, level, update = false, blockLight = 0) {
		let spread = []
		for (let i = 0; i < blocks.length; i += 3) {
			let x = blocks[i]
			let y = blocks[i+1]
			let z = blocks[i+2]
			this.trySpread(x - 1, y, z, level, spread, blockLight, update)
			this.trySpread(x + 1, y, z, level, spread, blockLight, update)
			this.trySpread(x, y - 1, z, level, spread, blockLight, update)
			this.trySpread(x, y + 1, z, level, spread, blockLight, update)
			this.trySpread(x, y, z - 1, level, spread, blockLight, update)
			this.trySpread(x, y, z + 1, level, spread, blockLight, update)
		}
		if (level > 1 && spread.length) {
			this.spreadLight(spread, level - 1, update, blockLight)
		}
	}
	tryUnSpread(x, y, z, level, spread, respread, blockLight) {
		if (y > 255) return
		const { world } = this
		let light = world.getLight(x, y, z, blockLight)
		let trans = transparent[world.getBlock(x, y, z)]
		if (light === level) {
			if (trans) {
				world.setLight(x, y, z, 0, blockLight)
				spread.push(x, y, z)
			}
		}
		else if (light > level) {
			respread[light].push(x, y, z)
		}
		if (x < this.x || x > this.x + 15 || z < this.z || z > this.z + 15) {
			let chunk = world.getChunk(x, z)
			if (chunk && chunk.buffer && !world.meshQueue.includes(chunk)) {
				world.meshQueue.push(chunk)
			}
		}
	}
	unSpreadLight(blocks, level, respread, blockLight) {
		let spread = []
		let x = 0, y = 0, z = 0
 
		for (let i = 0; i < blocks.length; i += 3) {
			x = blocks[i]
			y = blocks[i+1]
			z = blocks[i+2]
			this.tryUnSpread(x - 1, y, z, level, spread, respread, blockLight)
			this.tryUnSpread(x + 1, y, z, level, spread, respread, blockLight)
			this.tryUnSpread(x, y - 1, z, level, spread, respread, blockLight)
			this.tryUnSpread(x, y + 1, z, level, spread, respread, blockLight)
			this.tryUnSpread(x, y, z - 1, level, spread, respread, blockLight)
			this.tryUnSpread(x, y, z + 1, level, spread, respread, blockLight)
		}
		if (level > 1 && spread.length) {
			this.unSpreadLight(spread, level - 1, respread, blockLight)
		}
	}
	reSpreadLight(respread, blockLight) {
		for (let i = respread.length - 1; i > 1; i--) {
			let blocks = respread[i]
			let level = i - 1
			let spread = respread[level]
			for (let j = 0; j < blocks.length; j += 3) {
				let x = blocks[j]
				let y = blocks[j+1]
				let z = blocks[j+2]
				this.trySpread(x - 1, y, z, level, spread, blockLight)
				this.trySpread(x + 1, y, z, level, spread, blockLight)
				this.trySpread(x, y - 1, z, level, spread, blockLight)
				this.trySpread(x, y + 1, z, level, spread, blockLight)
				this.trySpread(x, y, z - 1, level, spread, blockLight)
				this.trySpread(x, y, z + 1, level, spread, blockLight)
			}
		}
	}
	generate() {
		if (this.generated) return
		const cx = this.x
		const cz = this.z
 
		const { grass, dirt, stone, bedrock, sand, water } = blockIds
 
		const waterHeight = this.maxY = 55
		const smoothness = 0.01 // How close hills and valleys are together
		const hilliness = 80 // Height of the hills
		const extra = 30 // Extra blocks stacked onto the terrain
		const superflat = this.populated
		let gen = 0
		for (let i = 0; i < 16; i++) {
			for (let k = 0; k < 16; k++) {
				if (superflat) gen = 4
				else {
					let n = noiseProfile.noise((cx + i) * smoothness, (cz + k) * smoothness)
					gen = Math.round(n * hilliness + extra)
					if (this.world.rivers) {
						const m = (1 - abs(noiseProfile.noise((cz + k - 5432.123) * 0.003, (cx + i + 9182.543) * 0.003) - 0.5))**50
						gen = Math.round((waterHeight - 5 - gen) * m + gen)
					}
				}
 
				this.tops[i * 16 + k] = gen
				if (gen > this.maxY) this.maxY = gen
 
				let index = i * 16 + k
				this.blocks[index] = bedrock
				index += 256
				for (let max = (gen - 3) * 256; index < max; index += 256) {
					this.blocks[index] = stone
				}
				if (gen > waterHeight || !this.world.rivers || superflat) {
					this.blocks[index] = dirt
					this.blocks[index + 256] = dirt
					this.blocks[index + 512] = dirt
					this.blocks[index + 768] = grass
				}
				else {
					this.blocks[index] = sand
					this.blocks[index + 256] = sand
					this.blocks[index + 512] = sand
					this.blocks[index + 768] = sand
					for (index += 1024; index < (waterHeight + 1) * 256; index += 256) this.blocks[index] = water
					if (gen <= waterHeight) this.doubleRender = true
				}
			}
		}
		if (this.doubleRender) this.world.doubleRenderChunks.push(this)
		this.generated = true
		this.getCaveData() // Queue up the multithreaded cave gen
	}
	optimize() {
		const { blocks, renderData, world, x, z, maxY } = this
		const trans = transparent
		const flags = this.visFlags // Computed in this.processBlocks()
		this.visFlags = null
 
		// Load adjacent chunks blocks
		const chunkN = world.getChunk(x, z + 17).blocks
		const chunkS = world.getChunk(x, z - 1).blocks
		const chunkE = world.getChunk(x + 17, z).blocks
		const chunkW = world.getChunk(x - 1, z).blocks
		// let max = (maxY - 1) * 256
		// for (let i = 256; i <= max; i += 16) if (trans[chunkN[i]]) flags[i + 15] |= 32
 
		// Culling faces on chunk borders as needed
		for (let y = 1; y <= maxY; y++) {
			let indexN = y * 256
			let indexS = indexN + 15
			let indexE = indexN
			let indexW = indexN + 240
			for (let i = 0; i < 16; i++) {
				if (trans[chunkN[indexN]]) flags[indexN + 15] ^= 32
				if (trans[chunkS[indexS]]) flags[indexS - 15] ^= 16
				if (trans[chunkE[indexE]]) flags[indexE + 240] ^= 2
				if (trans[chunkW[indexW]]) flags[indexW - 240] ^= 1
				indexN += 16
				indexS += 16
				indexE++
				indexW++
			}
		}
 
		//Check all the blocks in the chunk to see if they're visible.
		for (let index = 256; index < flags.length; index++) {
			if (flags[index] > 0) {
				renderData[this.renderLength++] = index << 16 | flags[index] << 10
			}
		}
		this.minY = renderData[0] >>> 24
 
		// The bottom layer of bedrock is only ever visible on top
		for (let i = 0; i < 16; i++) {
			for (let k = 0; k < 16; k++) {
				if (transparent[blocks[256 + i*16 + k]]) {
					this.minY = 0
					renderData.push(i*16 + k << 16 | 1 << 13)
				}
			}
		}
		this.renderLength = renderData.length
 
		if (!world.meshQueue.includes(this)) {
			world.meshQueue.push(this)
		}
		this.optimized = true
	}
	render(p, global) {
		const { glExtensions, gl } = this
		if (this.buffer === undefined) {
			return
		}
		if (p.canSee(this.x, this.minY, this.z, this.maxY)) {
			global.renderedChunks++
			glExtensions.vertex_array_object.bindVertexArrayOES(this.vao)
			gl.drawElements(gl.TRIANGLES, 6 * this.faces, gl.UNSIGNED_INT, 0)
			glExtensions.vertex_array_object.bindVertexArrayOES(null)
		}
	}
	updateBlock(x, y, z, world) {
		if (!this.buffer) return
		if (!world.meshQueue.includes(this)) {
			world.meshQueue.push(this)
		}
		let i = x
		let j = y
		let k = z
		x += this.x
		z += this.z
		let index = j * 256 + i * 16 + k
		let blockState = this.blocks[index]
 
		let w = i      ? this.blocks[index - 16] : world.getBlock(x - 1, j, z)
		let e = i < 15 ? this.blocks[index + 16] : world.getBlock(x + 1, j, z)
		let d = y      ? this.blocks[index - 256]: 4
		let u =          this.blocks[index + 256]
		let s = k      ? this.blocks[index - 1] : world.getBlock(x, j, z - 1)
		let n = k < 15 ? this.blocks[index + 1] : world.getBlock(x, j, z + 1)
 
		let visible = blockState && transparent[w]
		+ transparent[e] * 2
		+ transparent[d] * 4
		+ transparent[u] * 8
		+ transparent[s] * 16
		+ transparent[n] * 32
 
		if (blockState < 256 && hideInterior[blockState]) {
			visible ^= w === blockState
			| (e === blockState) << 1
			| (d === blockState) << 2
			| (u === blockState) << 3
			| (s === blockState) << 4
			| (n === blockState) << 5
		}
 
		let pos = index << 16
		index = -1
 
		// Find index of current block in this.renderData
		for (let i = 0; i < this.renderLength; i++) {
			if ((this.renderData[i] & 0xffff0000) === pos) {
				index = i
				break
			}
		}
 
		// Update palette
		// if (!this.paletteMap[blockState]) {
		// 	this.paletteMap[blockState] = this.palette.length
		// 	this.palette.push(blockState)
		// }
 
		if (index < 0 && !visible) {
			// Wasn't visible before, isn't visible after.
			return
		}
		if (!visible) {
			// Was visible before, isn't visible after.
			this.renderData.splice(index, 1)
			this.renderLength--
			return
		}
		if (visible && index < 0) {
			if (y <= this.minY) this.minY = y - 1
			if (y > this.maxY) this.maxY = y
			// Wasn't visible before, is visible after.
			index = this.renderLength++
		}
		this.renderData[index] = pos | visible << 10// | this.paletteMap[blockState]
	}
	getCaveData() {
		if (this.caves || this.caveData) return
		this.caveData = new Promise(resolve => {
			window.parent.doWork({
				caves: true,
				x: this.x,
				y: 0,
				z: this.z
			}, resolve)
		})
	}
	async carveCaves() {
		const { world } = this
		this.caves = true
 
		const { carve, air } = await this.caveData
 
		// Find the lowest point we need to check.
		let lowest = 255
		for (let i = 0; i < 256; i++) {
			let n = this.tops[i]
			lowest = lowest < n ? lowest : n
		}
		lowest = (lowest - 1) * 256
 
		// Set air blocks
		for (let i = 0; i < air.length; i++) {
			let index = air[i]
			// if (index === 20030) debugger
			if (index < lowest || index >> 8 <= this.tops[index & 255]) {
				this.blocks[index] = 0
			}
		}
 
		// // Carve spheres
		for (let i = 0; i < carve.length; i++) {
			let index = carve[i]
			// if (index === 20030) debugger
			if (index < lowest || index >> 8 <= this.tops[index & 255]) {
				let x = index >> 4 & 15
				let y = index >> 8
				let z = index & 15
				carveSphere(x + this.x, y, z + this.z, world)
			}
		}
 
		// let sx = this.x, sy = 0, sz = this.z
		// let cy = 0
		// for (let xz = 0; xz < 256; xz++) {
		// 	cy = this.tops[xz]
		// 	cy = cy > 82 ? 82 : cy
		// 	for (let y = 2; y <= cy; y++) {
		// 		let i = y * 256 + xz
		// 		let c = caves[i]
		// 		if (c === 1) {
		// 			carveSphere(sx + (xz >> 4), sy + y, sz + (xz & 15), world)
		// 		}
		// 		else if (c === 2) {
		// 			this.blocks[i] = 0
		// 		}
		// 	}
		// }
		this.caveData = null
	}
	populate(details) {
		if (this.populated) return
		const { world } = this
		randomSeed(hash(this.x, this.z) * 210000000)
		let wx = 0, wz = 0, ground = 0, top = 0, rand = 0, place = false
 
		// Spawn trees and ores
		for (let i = 0; i < 16; i++) {
			for (let k = 0; k < 16; k++) {
				wx = this.x + i
				wz = this.z + k
 
				ground = this.tops[i * 16 + k]
				let topBlock = this.getBlock(i, ground, k)
				if (details && random() < 0.005 && topBlock === blockIds.grass) {
 
					top = ground + floor(4.5 + random(2.5))
					rand = floor(random(4096))
					let tree = random() < 0.6 ? blockIds.oakLog : ++top && blockIds.birchLog
 
					//Center
					for (let j = ground + 1; j <= top; j++) {
						this.setBlock(i, j, k, tree)
					}
					this.setBlock(i, top + 1, k, blockIds.leaves)
					this.setBlock(i, ground, k, blockIds.dirt)
 
					//Bottom leaves
					for (let x = -2; x <= 2; x++) {
						for (let z = -2; z <= 2; z++) {
							if (x || z) { // Not center
								if ((x * z & 7) === 4) {
									place = rand & 1
									rand >>>= 1
									if (place) {
										world.spawnBlock(wx + x, top - 2, wz + z, blockIds.leaves)
									}
								}
								else {
									world.spawnBlock(wx + x, top - 2, wz + z, blockIds.leaves)
								}
							}
						}
					}
 
					//2nd layer leaves
					for (let x = -2; x <= 2; x++) {
						for (let z = -2; z <= 2; z++) {
							if (x || z) {
								if ((x * z & 7) === 4) {
									place = rand & 1
									rand >>>= 1
									if (place) {
										world.spawnBlock(wx + x, top - 1, wz + z, blockIds.leaves)
									}
								}
								else {
									world.spawnBlock(wx + x, top - 1, wz + z, blockIds.leaves)
								}
							}
						}
					}
 
					//3rd layer leaves
					for (let x = -1; x <= 1; x++) {
						for (let z = -1; z <= 1; z++) {
							if (x || z) {
								if (x & z) {
									place = rand & 1
									rand >>>= 1
									if (place) {
										world.spawnBlock(wx + x, top, wz + z, blockIds.leaves)
									}
								}
								else {
									world.spawnBlock(wx + x, top, wz + z, blockIds.leaves)
								}
							}
						}
					}
 
					//Top leaves
					world.spawnBlock(wx + 1, top + 1, wz, blockIds.leaves)
					world.spawnBlock(wx, top + 1, wz - 1, blockIds.leaves)
					world.spawnBlock(wx, top + 1, wz + 1, blockIds.leaves)
					world.spawnBlock(wx - 1, top + 1, wz, blockIds.leaves)
				}
 
				// Blocks of each per chunk in Minecraft
				// Coal: 185.5
				// Iron: 111.5
				// Gold: 10.4
				// Redstone: 29.1
				// Diamond: 3.7
				// Lapis: 4.1
				ground -= 4
				while (this.getBlock(i, ground, k) !== blockIds.stone) ground--
 
				if (random() < 3.7 / 256) {
					let y = random() * 16 | 0 + 1
					y = y < ground ? y : ground
					if (this.getBlock(i, y, k) === blockIds.stone) {
						this.setBlock(i, y < ground ? y : ground, k, blockIds.diamondOre)
					}
				}
 
				if (random() < 111.5 / 256) {
					let y = random() * 64 | 0 + 1
					y = y < ground ? y : ground
					if (this.getBlock(i, y, k) === blockIds.stone) {
						this.setBlock(i, y < ground ? y : ground, k, blockIds.ironOre)
					}
				}
 
				if (random() < 185.5 / 256) {
					let y = random() * ground | 0 + 1
					y = y < ground ? y : ground
					if (this.getBlock(i, y, k) === blockIds.stone) {
						this.setBlock(i, y < ground ? y : ground, k, blockIds.coalOre)
					}
				}
 
				if (random() < 10.4 / 256) {
					let y = random() * 32 | 0 + 1
					y = y < ground ? y : ground
					if (this.getBlock(i, y, k) === blockIds.stone) {
						this.setBlock(i, y < ground ? y : ground, k, blockIds.goldOre)
					}
				}
 
				if (random() < 29.1 / 256) {
					let y = random() * 16 | 0 + 1
					y = y < ground ? y : ground
					if (this.getBlock(i, y, k) === blockIds.stone) {
						this.setBlock(i, y < ground ? y : ground, k, blockIds.redstoneOre)
					}
				}
 
				if (random() < 4.1 / 256) {
					let y = random() * 32 | 0 + 1
					y = y < ground ? y : ground
					if (this.getBlock(i, y, k) === blockIds.stone) {
						this.setBlock(i, y < ground ? y : ground, k, blockIds.lapisOre)
					}
				}
			}
		}
 
		// Spawn water pools; 1 in 5000 blocks
		let queue = []
		for (let i = 0; i < 16; i++) {
			for (let k = 0; k < 16; k++) {
				wx = this.x + i
				wz = this.z + k
				ground = this.tops[i * 16 + k]
 
				if (details && random() < 0.0002 && world.getBlock(wx, ground, wz) === blockIds.grass) {
					let size = 0
					let maxSize = random(7, 30) | 0
					queue.push(wx, wz)
					while(queue.length) {
						let x = queue.shift()
						let z = queue.shift()
						if (Math.abs(x - wx) > 15 || Math.abs(z - wz) > 15) continue
						if ((size < 3 || random() < 0.5) && world.getBlock(x, ground, z) === blockIds.grass) {
							world.setWorldBlock(x, ground, z, blockIds.water)
							world.setWorldBlock(x, ground + 1, z, blockIds.air) // Remove any flowers above the water
							size++
 
							// Waterfall
							if (!world.getBlock(x - 1, ground, z)
								|| !world.getBlock(x + 1, ground, z)
								|| !world.getBlock(x, ground, z - 1)
								|| !world.getBlock(x, ground, z + 1)
							) {
								maxSize -= Math.min(size, 3)
								if (maxSize < 7) maxSize = 7
								size = 0
								world.setWorldBlock(x, --ground, z, blockIds.water)
								queue.length = 0
							}
							if (size < maxSize) queue.push(x - 1, z, x + 1, z, x, z - 1, x, z + 1)
						}
					}
				}
			}
		}
 
		// Spawn flower patches; 1 in 500 blocks
		for (let i = 0; i < 16; i++) {
			for (let k = 0; k < 16; k++) {
				wx = this.x + i
				wz = this.z + k
 
				if (details && random() < 0.002) {
					const types = []
					if (random() < 0.5) types.push(blockIds.poppy)
					if (random() < 0.5) types.push(blockIds.cornflower)
					if (random() < 0.5) types.push(blockIds.dandelion)
 
					for (let i = 0; i < types.length * 4; i++) {
						let x = wx + random(-2, 3) | 0
						let z = wz + random(-2, 3) | 0
						let y = world.getSurfaceHeight(x, z)
						if (world.getBlock(x, y, z) === blockIds.grass && !world.getBlock(x, y + 1, z)) {
							world.spawnBlock(x, y + 1, z, types[random(types.length) | 0])
						}
					}
				}
			}
		}
 
		this.populated = true
	}
	getSurroundingBlocks(loc, sides, blocks27, lights27) {
		// Used in this.genMesh()
		// Note that, while this *does* do more work than simply loading the blocks/lights as they're needed,
		// the improved cache efficiency nearly doubled the overall speed of genMesh (on Chrome).
		const chunkX = loc >> 4 & 15
		const chunkY = loc >> 8
		const chunkZ = loc & 15
 
		const worldX = chunkX + this.x
		const worldY = chunkY
		const worldZ = chunkZ + this.z
 
		// Preload any blocks that will be used for the shadows/lighting.
		if (chunkX && chunkZ && chunkX < 15 && chunkZ < 15) {
			const index = loc
			let topLight = this.light[index + 256]
			lights27[22] = topLight
			if (topLight !== 15 || sides !== 8) {
				// Bottom layer
				lights27[0] = this.light[index - 273]
				lights27[1] = this.light[index - 272]
				lights27[2] = this.light[index - 271]
				lights27[3] = this.light[index - 257]
				lights27[4] = this.light[index - 256]
				lights27[5] = this.light[index - 255]
				lights27[6] = this.light[index - 241]
				lights27[7] = this.light[index - 240]
				lights27[8] = this.light[index - 239]
 
				// Middle layer
				lights27[9] = this.light[index - 17]
				lights27[10] = this.light[index - 16]
				lights27[11] = this.light[index - 15]
				lights27[12] = this.light[index - 1]
				lights27[14] = this.light[index + 1]
				lights27[15] = this.light[index + 15]
				lights27[16] = this.light[index + 16]
				lights27[17] = this.light[index + 17]
 
				// Top layer
				lights27[18] = this.light[index + 239]
				lights27[19] = this.light[index + 240]
				lights27[20] = this.light[index + 241]
				lights27[21] = this.light[index + 255]
				lights27[23] = this.light[index + 257]
				lights27[24] = this.light[index + 271]
				lights27[25] = this.light[index + 272]
				lights27[26] = this.light[index + 273]
			}
			if (sides !== 8) {
				// Bottom layer
				blocks27[0] = shadow[255 & this.blocks[index - 273]]
				blocks27[1] = shadow[255 & this.blocks[index - 272]]
				blocks27[2] = shadow[255 & this.blocks[index - 271]]
				blocks27[3] = shadow[255 & this.blocks[index - 257]]
				blocks27[4] = shadow[255 & this.blocks[index - 256]]
				blocks27[5] = shadow[255 & this.blocks[index - 255]]
				blocks27[6] = shadow[255 & this.blocks[index - 241]]
				blocks27[7] = shadow[255 & this.blocks[index - 240]]
				blocks27[8] = shadow[255 & this.blocks[index - 239]]
 
				// Middle layer
				blocks27[9] = shadow[255 & this.blocks[index - 17]]
				blocks27[10] = shadow[255 & this.blocks[index - 16]]
				blocks27[11] = shadow[255 & this.blocks[index - 15]]
				blocks27[12] = shadow[255 & this.blocks[index - 1]]
				blocks27[14] = shadow[255 & this.blocks[index + 1]]
				blocks27[15] = shadow[255 & this.blocks[index + 15]]
				blocks27[16] = shadow[255 & this.blocks[index + 16]]
				blocks27[17] = shadow[255 & this.blocks[index + 17]]
			}
 
			// Top layer
			blocks27[18] = shadow[255 & this.blocks[index + 239]]
			blocks27[19] = shadow[255 & this.blocks[index + 240]]
			blocks27[20] = shadow[255 & this.blocks[index + 241]]
			blocks27[21] = shadow[255 & this.blocks[index + 255]]
			blocks27[22] = shadow[255 & this.blocks[index + 256]]
			blocks27[23] = shadow[255 & this.blocks[index + 257]]
			blocks27[24] = shadow[255 & this.blocks[index + 271]]
			blocks27[25] = shadow[255 & this.blocks[index + 272]]
			blocks27[26] = shadow[255 & this.blocks[index + 273]]
		}
		else {
			// Lights
			let topLight = this.light[loc + 256]
			lights27[22] = topLight
			if (topLight !== 15 || sides !== 8) {
				// Lights in this chunk
				for (let x = -1, i = 0; x <= 1; x++) {
					for (let z = -1; z <= 1; z++, i++) {
						if (chunkX + x >= 0 && chunkZ + z >= 0 && chunkX + x < 16 && chunkZ + z < 16) {
							const index = loc + x * 16 + z
							lights27[i] = this.light[index - 256]
							lights27[i+9] = this.light[index]
							lights27[i+18] = this.light[index + 256]
						}
					}
				}
 
				// Lights in the other chunks
				for (let x = -1, i = 0; x <= 1; x++) {
					for (let z = -1; z <= 1; z++, i++) {
						if (chunkX + x < 0 || chunkZ + z < 0 || chunkX + x > 15 || chunkZ + z > 15) {
							let chunk = this.world.getChunk(worldX + x, worldZ + z)
							let index = worldY * 256 + (worldX + x & 15) * 16 + (worldZ + z & 15)
							lights27[i] = chunk.light[index - 256]
							lights27[i+9] = chunk.light[index]
							lights27[i+18] = chunk.light[index + 256]
						}
					}
				}
			}
 
			// Blocks in this chunk
			for (let x = -1, i = 0; x <= 1; x++) {
				for (let z = -1; z <= 1; z++, i++) {
					if (chunkX + x >= 0 && chunkZ + z >= 0 && chunkX + x < 16 && chunkZ + z < 16) {
						const index = loc + x * 16 + z
						blocks27[i] = shadow[255 & this.blocks[index - 256]]
						blocks27[i+9] = shadow[255 & this.blocks[index]]
						blocks27[i+18] = shadow[255 & this.blocks[index + 256]]
					}
				}
			}
 
			// Blocks in the other chunks
			for (let x = -1, i = 0; x <= 1; x++) {
				for (let z = -1; z <= 1; z++, i++) {
					if (chunkX + x < 0 || chunkZ + z < 0 || chunkX + x > 15 || chunkZ + z > 15) {
						const chunk = this.world.getChunk(worldX + x, worldZ + z)
						const index = worldY * 256 + (worldX + x & 15) * 16 + (worldZ + z & 15)
						blocks27[i] = shadow[255 & chunk.blocks[index - 256]]
						blocks27[i+9] = shadow[255 & chunk.blocks[index]]
						blocks27[i+18] = shadow[255 & chunk.blocks[index + 256]]
					}
				}
			}
		}
	}
	genMesh(indexBuffer, bigArray) {
		const { glExtensions, gl, glCache, renderLength, renderData } = this
		let index = 0
		if (!this.renderLength) {
			return index
		}
		let verts = null, texVerts = null, texShapeVerts = null,
			tx = 0, ty = 0
 
		let shadows = [1, 1, 1, 1], slights = [1, 1, 1, 1], blights = [1, 1, 1, 1]
		let blockMasks = Object.values(Block)
		const blocks27 = new Uint8Array(27)
		const lights27 = new Uint8Array(27)
 
		for (let i = 0; i < renderLength; i++) {
			const data = renderData[i]
			const sides = data >> 10 & 0x3f // 6 bit flags indicating which faces should be rendered
			const loc = data >>> 16 // #yyyyxxzz
			const block = blockData[this.blocks[loc]]
			const tex = block.textures
			const chunkX = loc >> 4 & 15
			const chunkY = loc >> 8
			const chunkZ = loc & 15
 
			const worldX = chunkX + this.x
			const worldY = chunkY
			const worldZ = chunkZ + this.z
 
			// Preload any blocks that will be used for the shadows/lighting.
			this.getSurroundingBlocks(loc, sides, blocks27, lights27)
 
			let shapeVerts = block.shape.verts
			let shapeTexVerts = block.shape.texVerts
			if (block.shape.getShape) {
				let newShape = block.shape.getShape(worldX, chunkY, worldZ, this.world, blockData)
				shapeVerts = newShape.verts
				shapeTexVerts = newShape.texVerts
			}
 
			for (let n = 0; n < 6; n++) {
				if (sides & blockMasks[n]) {
					shadows = getShadows[n](blocks27)
					slights = getLight[n](lights27, slights, 0x0f, 0)
					blights = getLight[n](lights27, blights, 0xf0, 4)
 
					let directionalFaces = shapeVerts[n]
 
					// Add vertices for a single rectangle.
					for (let facei = 0; facei < directionalFaces.length; facei++) {
						verts = directionalFaces[facei]
						texVerts = tex[n]
						tx = texVerts[0]
						ty = texVerts[1]
						texShapeVerts = shapeTexVerts[n][facei]
 
						bigArray[index] = verts[0] + worldX
						bigArray[index+1] = verts[1] + worldY
						bigArray[index+2] = verts[2] + worldZ
						bigArray[index+3] = tx + texShapeVerts[0]
						bigArray[index+4] = ty + texShapeVerts[1]
						bigArray[index+5] = shadows[0]
						bigArray[index+6] = slights[0]
						bigArray[index+7] = blights[0]
 
						bigArray[index+8] = verts[3] + worldX
						bigArray[index+9] = verts[4] + worldY
						bigArray[index+10] = verts[5] + worldZ
						bigArray[index+11] = tx + texShapeVerts[2]
						bigArray[index+12] = ty + texShapeVerts[3]
						bigArray[index+13] = shadows[1]
						bigArray[index+14] = slights[1]
						bigArray[index+15] = blights[1]
 
						bigArray[index+16] = verts[6] + worldX
						bigArray[index+17] = verts[7] + worldY
						bigArray[index+18] = verts[8] + worldZ
						bigArray[index+19] = tx + texShapeVerts[4]
						bigArray[index+20] = ty + texShapeVerts[5]
						bigArray[index+21] = shadows[2]
						bigArray[index+22] = slights[2]
						bigArray[index+23] = blights[2]
 
						bigArray[index+24] = verts[9] + worldX
						bigArray[index+25] = verts[10] + worldY
						bigArray[index+26] = verts[11] + worldZ
						bigArray[index+27] = tx + texShapeVerts[6]
						bigArray[index+28] = ty + texShapeVerts[7]
						bigArray[index+29] = shadows[3]
						bigArray[index+30] = slights[3]
						bigArray[index+31] = blights[3]
						index += 32
					}
				}
			}
		}
 
		if (!this.buffer) {
			this.buffer = gl.createBuffer()
		}
		let data = new Float32Array(bigArray.buffer, 0, index)
 
		// let maxY = 0
		// let minY = 255
		// for (let i = 1; i < data.length; i += 6) {
		// 	const y = data[i]
		// 	maxY = y > maxY ? y : maxY
		// 	minY = y < minY ? y : minY
		// }
		// this.maxY = maxY
		// this.minY = minY
 
		this.faces = data.length / 32
		glExtensions.vertex_array_object.bindVertexArrayOES(this.vao)
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
		gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer)
		gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW)
		gl.enableVertexAttribArray(glCache.aVertex)
		gl.enableVertexAttribArray(glCache.aTexture)
		gl.enableVertexAttribArray(glCache.aShadow)
		gl.enableVertexAttribArray(glCache.aSkylight)
		gl.enableVertexAttribArray(glCache.aBlocklight)
		gl.vertexAttribPointer(glCache.aVertex, 3, gl.FLOAT, false, 32, 0)
		gl.vertexAttribPointer(glCache.aTexture, 2, gl.FLOAT, false, 32, 12)
		gl.vertexAttribPointer(glCache.aShadow, 1, gl.FLOAT, false, 32, 20)
		gl.vertexAttribPointer(glCache.aSkylight, 1, gl.FLOAT, false, 32, 24)
		gl.vertexAttribPointer(glCache.aBlocklight, 1, gl.FLOAT, false, 32, 28)
		glExtensions.vertex_array_object.bindVertexArrayOES(null)
	}
	tick() {
		if (this.edited) {
			// tick()
		}
	}
	compressSection(blocks) {
		let section = []
		let blockSet = new Set()
		for (let i = 0; i < 6; i++) section.push(new Int16Array(512).fill(-1))
		for (let i in blocks) {
			blockSet.add(blocks[i])
			let y = i >> 6
			let x = i >> 3 & 7
			let z = i & 7
 
			// 3 copies of the section, all oriented in different directions so we can see which one compresses the most
			section[0][(y & 7) << 6 | (x & 7) << 3 | z & 7] = blocks[i]
			section[1][(y & 7) << 6 | (z & 7) << 3 | x & 7] = blocks[i]
			section[2][(x & 7) << 6 | (y & 7) << 3 | z & 7] = blocks[i]
			section[3][(x & 7) << 6 | (z & 7) << 3 | y & 7] = blocks[i]
			section[4][(z & 7) << 6 | (x & 7) << 3 | y & 7] = blocks[i]
			section[5][(z & 7) << 6 | (y & 7) << 3 | x & 7] = blocks[i]
		}
 
		let palette = {}
		let paletteBlocks = Array.from(blockSet)
		paletteBlocks.forEach((block, index) => palette[block] = index)
		let paletteBits = BitArrayBuilder.bits(paletteBlocks.length)
 
		let bestBAB = null
		for (let i = 0; i < 6; i++) {
			let bab = new BitArrayBuilder()
			bab.add(paletteBlocks.length, 9)
			for (let block of paletteBlocks) bab.add(block, 16)
 
			// Store the orientation so it can be loaded properly
			let blocks = section[i]
			bab.add(i, 3)
 
			let run = null
			let runs = []
			let singles = []
			for (let i = 0; i < blocks.length; i++) {
				const block = blocks[i]
				if (block >= 0) {
					if (!run && i < blocks.length - 2 && blocks[i + 1] >= 0 && blocks[i + 2] >= 0) {
						run = [i, []]
						runs.push(run)
					}
					if (run) {
						if (run[1].length && block === run[1].at(-1)[1]) run[1].at(-1)[0]++
						else run[1].push([1, block])
					}
					else singles.push([i, blocks[i]])
				}
				else run = null
			}
 
			bab.add(runs.length, 8)
			bab.add(singles.length, 9)
			for (let [start, blocks] of runs) {
				// Determine the number of bits needed to store the lengths of each block type
				let maxBlocks = 0
				for (let block of blocks) maxBlocks = Math.max(maxBlocks, block[0])
				let lenBits = BitArrayBuilder.bits(maxBlocks)
 
				bab.add(start, 9).add(blocks.length, 9).add(lenBits, 4)
				for (let [count, block] of blocks) bab.add(count - 1, lenBits).add(palette[block], paletteBits)
			}
			for (let [index, block] of singles) {
				bab.add(index, 9).add(palette[block], paletteBits)
			}
			if (!bestBAB || bab.bitLength < bestBAB.bitLength) {
				bestBAB = bab
			}
		}
		return bestBAB
	}
	getSave() {
		if (!this.originalBlocks.length) return
 
		const bab = new BitArrayBuilder()
		if (this.edited || !this.saveData?.reader) {
 
			// Find all the edited blocks and sort them into 8x8x8 sections
			const sectionMap = {}
			const { blocks, originalBlocks } = this
			for (let i = 0; i < blocks.length; i++) {
				if (blocks[i] !== originalBlocks[i]) {
					let y = i >> 8
					let x = (i >> 4 & 15) + this.x
					let z = (i & 15) + this.z
					let str = `${x>>3},${y>>3},${z>>3}` // 8x8x8 sections
					if (!sectionMap[str]) {
						sectionMap[str] = []
					}
					sectionMap[str][(y & 7) << 6 | (x & 7) << 3 | z & 7] = blocks[i]
				}
			}
 
			// Compress edited blocks into binary
			for (let [coords, section] of Object.entries(sectionMap)) {
				let [sx, sy, sz] = coords.split(",").map(Number)
				bab.add(sx, 16).add(sy, 5).add(sz, 16)
 
				bab.append(this.compressSection(section))
			}
		}
		else {
			const { reader, startPos, endPos } = this.saveData
			reader.bit = startPos
			while (reader.bit < endPos) {
				let len = Math.min(endPos - reader.bit, 8)
				bab.add(reader.read(len), len)
			}
		}
		return bab
	}
	loadFromBlocks(blocks) {
		let last = 0
		for (let j in blocks) {
			last = +j
			let block = blocks[last]
			if (!blockData[block]) {
				if (blockData[block & 255]) block &= 255
				else block = blockIds.pumpkin
			}
			this.blocks[last] = block
			if (!this.doubleRender && blockData[block].semiTrans) {
				this.doubleRender = true
				if (!this.world.doubleRenderChunks.includes(this)) {
					this.world.doubleRenderChunks.push(this)
				}
			}
		}
		if (last >> 8 > this.maxY) this.maxY = last >> 8
	}
	load() {
		if (this.loaded) return
		const chunkX = this.x >> 4
		const chunkZ = this.z >> 4
		const str = `${chunkX},${chunkZ}`
		const load = this.world.loadFrom[str]
 
		if (load) {
			delete this.world.loadFrom[str]
			if (load.reader && !load.edits) this.saveData = {
				reader: load.reader,
				startPos: load.startPos,
				endPos: load.endPos
			}
			this.originalBlocks = this.blocks.slice()
 
			if (load.blocks) {
				// The initial world load had to parse the blocks, so they were stored.
				this.loadFromBlocks(load.blocks)
			}
			else if (load.reader) {
				// The chunk was loaded, then later unloaded.
				const getIndex = [
					(index, x, y, z) => (y + (index >> 6 & 7))*256 + (x + (index >> 3 & 7))*16 + z + (index >> 0 & 7),
					(index, x, y, z) => (y + (index >> 6 & 7))*256 + (x + (index >> 0 & 7))*16 + z + (index >> 3 & 7),
					(index, x, y, z) => (y + (index >> 3 & 7))*256 + (x + (index >> 6 & 7))*16 + z + (index >> 0 & 7),
					(index, x, y, z) => (y + (index >> 0 & 7))*256 + (x + (index >> 6 & 7))*16 + z + (index >> 3 & 7),
					(index, x, y, z) => (y + (index >> 0 & 7))*256 + (x + (index >> 3 & 7))*16 + z + (index >> 6 & 7),
					(index, x, y, z) => (y + (index >> 3 & 7))*256 + (x + (index >> 0 & 7))*16 + z + (index >> 6 & 7)
				]
				const { reader, startPos, endPos } = load
 
				reader.bit = startPos
				while (reader.bit < endPos) {
					let x = reader.read(16, true) * 8
					let y = reader.read(5, false) * 8
					let z = reader.read(16, true) * 8
 
					const paletteLen = reader.read(9)
					const paletteBits = BitArrayBuilder.bits(paletteLen)
					const palette = []
					for (let i = 0; i < paletteLen; i++) {
						let block = reader.read(16)
						if (!blockData[block]) {
							if (blockData[block & 255]) block &= 255
							else block = blockIds.pumpkin
						}
						palette.push(block)
						if (blockData[block].semiTrans) {
							this.doubleRender = true
							if (!this.world.doubleRenderChunks.includes(this)) {
								this.world.doubleRenderChunks.push(this)
							}
						}
					}
 
					const orientation = reader.read(3)
 
					const cx = x >> 4
					const cz = z >> 4
 
					// Make them into local chunk coords
					x = x !== cx * 16 ? 8 : 0
					z = z !== cz * 16 ? 8 : 0
 
					const runs = reader.read(8)
					const singles = reader.read(9)
					for (let j = 0; j < runs; j++) {
						let index = reader.read(9)
						const types = reader.read(9)
						const lenSize = reader.read(4)
						for (let k = 0; k < types; k++) {
							const chain = reader.read(lenSize) + 1
							const block = reader.read(paletteBits)
							for (let l = 0; l < chain; l++) {
								const i = getIndex[orientation](index, x, y, z)
								this.blocks[i] = palette[block]
								this.maxY = max(this.maxY, i >> 8)
								index++
							}
						}
					}
					for (let j = 0; j < singles; j++) {
						const index = reader.read(9)
						const block = reader.read(paletteBits)
						const i = getIndex[orientation](index, x, y, z)
						this.blocks[i] = palette[block]
						this.maxY = max(this.maxY, i >> 8)
					}
				}
			}
 
			// Edits happened while the chunk was unloaded.
			if (load.edits) {
				this.loadFromBlocks(load.edits)
			}
		}
		this.loaded = true
	}
	unload() {
		if (this.originalBlocks) {
			const save = this.getSave()
			if (save) {
				const chunkX = this.x >> 4
				const chunkZ = this.z >> 4
				const str = `${chunkX},${chunkZ}`
				this.world.loadFrom[str] = {
					startPos: 0,
					endPos: save.bitLength,
					reader: new BitArrayReader(save.array)
				}
			}
		}
		if (this.buffer) this.gl.deleteBuffer(this.buffer)
	}
}
 
window.parent.exports["src/js/chunk.js"] = { Chunk }}
		</script>
		<script id="src/js/entity.js" type="application/javascript">
{const { blockData } = window.parent.exports["src/js/blockData.js"]
const { roundBits } = window.parent.exports["src/js/utils.js"]
 
const { round, floor, ceil } = Math
 
class Contacts {
	constructor() {
		this.array = []
		this.size = 0
	}
	add(x, y, z, block) {
		if (this.size === this.array.length) {
			this.array.push([x, y, z, block])
		}
		else {
			this.array[this.size][0] = x
			this.array[this.size][1] = y
			this.array[this.size][2] = z
			this.array[this.size][3] = block
		}
		this.size++
	}
	clear() {
		this.size = 0
	}
}
 
class Entity {
	constructor(x, y, z, pitch, yaw, velx, vely, velz, width, height, depth, vertices, texture, faces, despawns, glExtensions, gl, glCache, indexBuffer, world, p) {
		this.x = x
		this.y = y
		this.z = z
		this.previousX = x
		this.previousY = y
		this.previousZ = z
		this.canStepX = true
		this.canStepY = true
		this.pitch = pitch
		this.yaw = yaw
		this.velx = velx
		this.vely = vely
		this.velz = velz
		this.width = width
		this.height = height
		this.depth = depth
		this.contacts = new Contacts()
		this.lastUpdate = performance.now()
		this.onGround = false
		this.despawns = despawns
		this.spawn = this.lastUpdate
		this.canDespawn = false
		this.faces = faces
		this.vao = glExtensions.vertex_array_object.createVertexArrayOES()
		const verticesBuffer = gl.createBuffer()
		const textureBuffer = gl.createBuffer()
		glExtensions.vertex_array_object.bindVertexArrayOES(this.vao)
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
 
		gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer)
		gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
		gl.vertexAttribPointer(glCache.aVertexEntity, 3, gl.FLOAT, false, 0, 0)
 
		gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer)
		gl.bufferData(gl.ARRAY_BUFFER, texture, gl.STATIC_DRAW)
		gl.vertexAttribPointer(glCache.aTextureEntity, 2, gl.FLOAT, false, 0, 0)
 
		gl.enableVertexAttribArray(glCache.aVertexEntity)
		gl.enableVertexAttribArray(glCache.aTextureEntity)
		glExtensions.vertex_array_object.bindVertexArrayOES(null)
		this.glExtensions = glExtensions
		this.gl = gl
		this.glCache = glCache
		this.indexBuffer = indexBuffer
		this.world = world
		this.p = p
	}
	updateVelocity(now) {
		let dt = (now - this.lastUpdate) / 33
		dt = dt > 2 ? 2 : dt
		// this.vely += -0.02 * dt
		if (this.vely < -1.5) {
			this.vely = -1.5
		}
 
		this.velz += (this.velz * 0.9 - this.velz) * dt
		this.velx += (this.velx * 0.9 - this.velx) * dt
		// this.vely += (this.vely * 0.9 - this.vely) * dt
	}
	collided(x, y, z, vx, vy, vz, block) {
		let verts = blockData[block].shape.verts
		let px = roundBits(this.x - this.width / 2 - x)
		let py = roundBits(this.y - this.height / 2 - y)
		let pz = roundBits(this.z - this.depth / 2 - z)
		let pxx = roundBits(this.x + this.width / 2 - x)
		let pyy = roundBits(this.y + this.height / 2 - y)
		let pzz = roundBits(this.z + this.depth / 2 - z)
		let minX, minY, minZ, maxX, maxY, maxZ, min, max
 
		//Top and bottom faces
		let faces = verts[0]
		if (vy <= 0) {
			faces = verts[1]
		}
		if (!vx && !vz) {
			for (let face of faces) {
				min = face.min
				minX = min[0]
				minZ = min[2]
				max = face.max
				maxX = max[0]
				maxZ = max[2]
				if (face[1] > py && face[1] < pyy && minX < pxx && maxX > px && minZ < pzz && maxZ > pz) {
					if (vy <= 0) {
						this.onGround = true
						this.y = round((face[1] + y + this.height / 2) * 10000) / 10000
						this.vely = 0
						return false
					}
					else {
						return true
					}
				}
			}
			return false
		}
 
		//West and East faces
		if (vx < 0) {
			faces = verts[4]
		}
		else if (vx > 0) {
			faces = verts[5]
		}
		if (vx) {
			let col = false
			for (let face of faces) {
				min = face.min
				minZ = min[2]
				minY = min[1]
				max = face.max
				maxZ = max[2]
				maxY = max[1]
				if (face[0] > px && face[0] < pxx && minY < pyy && maxY > py && minZ < pzz && maxZ > pz) {
					if (maxY - py > 0.5) {
						this.canStepX = false
					}
					col = true
				}
			}
			return col
		}
 
		//South and North faces
		if (vz < 0) {
			faces = verts[2]
		}
		else if (vz > 0) {
			faces = verts[3]
		}
		if (vz) {
			let col = false
			for (let face of faces) {
				min = face.min
				minX = min[0]
				minY = min[1]
				max = face.max
				maxX = max[0]
				maxY = max[1]
				if (face[2] > pz && face[2] < pzz && minY < pyy && maxY > py && minX < pxx && maxX > px) {
					if (maxY - py > 0.5) {
						this.canStepZ = false
					}
					col = true
				}
			}
			return col
		}
	}
	move(now) {
		let dx = this.p.x - this.x
		let dy = this.p.y - this.y
		let dz = this.p.z - this.z
		const dist = this.world.settings.renderDistance * 16 - 32
		if (dx * dx + dy * dy + dz * dz >= dist * dist) return
 
		const { world } = this
		let pminX = floor(this.x - this.width / 2)
		let pmaxX = ceil(this.x + this.width / 2)
		let pminY = floor(this.y - this.height / 2)
		let pmaxY = ceil(this.y + this.height / 2)
		let pminZ = floor(this.z - this.depth / 2)
		let pmaxZ = ceil(this.z + this.depth / 2)
		let block = null
 
		for (let x = pminX; x <= pmaxX; x++) {
			for (let y = pminY; y <= pmaxY; y++) {
				for (let z = pminZ; z <= pmaxZ; z++) {
					let block = world.getBlock(x, y, z)
					if (block) {
						this.contacts.add(x, y, z, block)
					}
				}
			}
		}
		let dt = (now - this.lastUpdate) / 33
		dt = dt > 2 ? 2 : dt
 
		this.previousX = this.x
		this.previousY = this.y
		this.previousZ = this.z
 
		this.canStepX = false
		this.canStepY = false
		this.onGround = false
		//Check collisions in the Y direction
		this.y += this.vely * dt
		for (let i = 0; i < this.contacts.size; i++) {
			block = this.contacts.array[i]
			if (this.collided(block[0], block[1], block[2], 0, this.vely, 0, block[3])) {
				this.y = this.previousY
				this.vely = 0
				break
			}
		}
 
		if (this.y === this.previousY) {
			this.canStepX = true
			this.canStepZ = true
		}
 
		//Check collisions in the X direction
		this.x += this.velx * dt
		for (let i = 0; i < this.contacts.size; i++) {
			block = this.contacts.array[i]
			if (this.collided(block[0], block[1], block[2], this.velx, 0, 0, block[3])) {
				if (this.canStepX && !world.getBlock(block[0], block[1] + 1, block[2]) && !world.getBlock(block[0], block[1] + 2, block[2])) {
					continue
				}
				this.x = this.previousX
				this.velx = 0
				break
			}
		}
 
		//Check collisions in the Z direction
		this.z += this.velz * dt
		for (let i = 0; i < this.contacts.size; i++) {
			block = this.contacts.array[i]
			if (this.collided(block[0], block[1], block[2], 0, 0, this.velz, block[3])) {
				if (this.canStepZ && !world.getBlock(block[0], block[1] + 1, block[2]) && !world.getBlock(block[0], block[1] + 2, block[2])) {
					continue
				}
				this.z = this.previousZ
				this.velz = 0
				break
			}
		}
 
		this.lastUpdate = now
		this.contacts.clear()
	}
	update() {
		let now = performance.now()
		this.updateVelocity(now)
		this.move(now)
		if (now - this.spawn > this.despawns) {
			this.canDespawn = true
		}
	}
}
 
window.parent.exports["src/js/entity.js"] = { Entity }}
		</script>
		<script id="src/js/player.js" type="application/javascript">
{const { blockData } = window.parent.exports["src/js/blockData.js"]
const { shapes } = window.parent.exports["src/js/shapes.js"]
const { Entity } = window.parent.exports["src/js/entity.js"]
const { Matrix } = window.parent.exports["src/js/3Dutils.js"]
 
class Player extends Entity {
	constructor(x, y, z, vx, vy, vz, blockID, glExtensions, gl, glCache, indexBuffer, world, p) {
		const block = blockData[blockID & 255]
		const tex = block.textures
		const shape = shapes.cube
		const shapeVerts = shape.verts
		const shapeTexVerts = shape.texVerts
		const size = shape.size
		let texNum = 0
		let texture = []
		let index = 0
		for (let n = 0; n < 6; n++) {
			let directionalFaces = shapeVerts[n]
			for (let facei = 0; facei < directionalFaces.length; facei++) {
				let texVerts = tex[texNum]
				let tx = texVerts[0]
				let ty = texVerts[1]
				let texShapeVerts = shapeTexVerts[n][facei]
				texture[index    ] = tx + texShapeVerts[0]
				texture[index + 1] = ty + texShapeVerts[1]
				texture[index + 2] = tx + texShapeVerts[2]
				texture[index + 3] = ty + texShapeVerts[3]
				texture[index + 4] = tx + texShapeVerts[4]
				texture[index + 5] = ty + texShapeVerts[5]
				texture[index + 6] = tx + texShapeVerts[6]
				texture[index + 7] = ty + texShapeVerts[7]
				index += 8
			}
			texNum++
		}
		super(x, y, z, 0, 0, vx || 0, vy || 0, vz || 0, 0.6, 1.7, 0.6, new Float32Array(shapeVerts.flat(Infinity)), new Float32Array(texture), size, Infinity, glExtensions, gl, glCache, indexBuffer, world, p)
		if (p) this.camera = null
	}
	render() {
		const { gl, glCache, glExtensions, p } = this
		const modelMatrix = new Matrix()
		modelMatrix.identity()
		modelMatrix.translate(this.x, this.y, this.z)
		modelMatrix.rotX(this.pitch)
		modelMatrix.rotY(this.yaw)
		modelMatrix.scale(this.width, this.height, this.depth)
		const viewMatrix = p.transformation.elements
		const proj = p.projection
		const projectionMatrix = [proj[0], 0, 0, 0, 0, proj[1], 0, 0, 0, 0, proj[2], proj[3], 0, 0, proj[4], 0]
		const modelViewProjectionMatrix = new Matrix()
		modelViewProjectionMatrix.identity()
		modelViewProjectionMatrix.mult(projectionMatrix)
		modelViewProjectionMatrix.mult(viewMatrix)
		modelViewProjectionMatrix.mult(modelMatrix.elements)
		// row major to column major
		modelViewProjectionMatrix.transpose()
 
		const lightLevel = 1 // min(max(skyLight, blockLight) * 0.9 + 0.1, 1.0)
		gl.uniform1i(glCache.uSamplerEntity, 0)
		// gl.bindTexture(gl.TEXTURE_2D, textureAtlas)
		gl.uniform1f(glCache.uLightLevelEntity, lightLevel)
		gl.uniformMatrix4fv(glCache.uViewEntity, false, modelViewProjectionMatrix.elements)
		glExtensions.vertex_array_object.bindVertexArrayOES(this.vao)
		gl.drawElements(gl.TRIANGLES, 6 * this.faces, gl.UNSIGNED_INT, 0)
		glExtensions.vertex_array_object.bindVertexArrayOES(null)
	}
}
 
window.parent.exports["src/js/player.js"] = { Player }}
		</script>
		<script id="src/main.js" type="application/javascript">
{// import './index.css'
 
// GLSL Shader code
const vertexShaderSrc3D = document.getElementById("src/shaders/blockVert.glsl").textContent
const fragmentShaderSrc3D = document.getElementById("src/shaders/blockFrag.glsl").textContent
const foglessVertexShaderSrc3D = document.getElementById("src/shaders/blockVertFogless.glsl").textContent
const foglessFragmentShaderSrc3D = document.getElementById("src/shaders/blockFragFogless.glsl").textContent
const vertexShaderSrc2D = document.getElementById("src/shaders/2dVert.glsl").textContent
const fragmentShaderSrc2D = document.getElementById("src/shaders/2dFrag.glsl").textContent
const vertexShaderSrcEntity = document.getElementById("src/shaders/entityVert.glsl").textContent
const fragmentShaderSrcEntity = document.getElementById("src/shaders/entityFrag.glsl").textContent
 
// Import WebWorker code
const workerCode = document.getElementById("src/workers/Caves.js").textContent
 
// imports
const { seedHash, randomSeed, noiseProfile } = window.parent.exports["src/js/random.js"]
const { PVector, Matrix, Plane, cross } = window.parent.exports["src/js/3Dutils.js"]
const { timeString, roundBits, BitArrayBuilder, BitArrayReader } = window.parent.exports["src/js/utils.js"]
const { blockData, BLOCK_COUNT, blockIds, BlockData } = window.parent.exports["src/js/blockData.js"]
const { loadFromDB, saveToDB, deleteFromDB } = window.parent.exports["src/js/indexDB.js"]
const { shapes, CUBE, SLAB, STAIR, FLIP, SOUTH, EAST, WEST } = window.parent.exports["src/js/shapes.js"]
const { InventoryItem, inventory } = window.parent.exports["src/js/inventory.js"]
const { createProgramObject } = window.parent.exports["src/js/glUtils.js"]
const { initTextures, animateTextures, hitboxTextureCoords } = window.parent.exports["src/js/texture.js"]
const { getSkybox } = window.parent.exports["src/js/sky.js"]
const { Chunk } = window.parent.exports["src/js/chunk.js"]
// import { Item } from './js/item.js'
const { Player } = window.parent.exports["src/js/player.js"]
 
 
const win = window.parent
const MineKhan = async () => {
	// cache global objects locally.
	const { Math, performance, Date, document, console } = window
	const { cos, sin, round, floor, ceil, min, max, abs, sqrt } = Math
	const chatOutput = document.getElementById("chat")
	const chatInput = document.getElementById("chatbar")
	let now = Date.now()
 
	win.blockData = blockData
	win.savebox = document.getElementById("savebox")
	win.boxCenterTop = document.getElementById("boxcentertop")
	win.saveDirections = document.getElementById("savedirections")
	win.message = document.getElementById("message")
	win.worlds = document.getElementById("worlds") // I have too many "worlds" variables. This one uses "win" as its namespace.
	win.quota = document.getElementById("quota")
	win.hoverbox = document.getElementById("onhover")
	win.controlMap = {}
 
	// Cache user-defined globals
	const { savebox, boxCenterTop, saveDirections, message, quota, hoverbox, loadString, controlMap } = win
	const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
 
	/**
	 * @type {HTMLCanvasElement}
	 */
	const canvas = document.getElementById("overlay")
	canvas.width  = win.innerWidth
	canvas.height = win.innerHeight
	const ctx = canvas.getContext("2d")
	win.canvas = canvas
 
	// Shh don't tell anyone I'm overriding native objects
	String.prototype.hashCode = function() {
		var hash = 0, i, chr
		if (this.length === 0) return hash
		for (i = 0; i < this.length; i++) {
			chr   = this.charCodeAt(i)
			hash  = (hash << 5) - hash + chr
			hash |= 0 // Convert to 32bit integer
		}
		return hash
	}
	Uint8Array.prototype.toString = function() {
		let str = ""
		for (let i = 0; i < this.length; i++) {
			str += String.fromCharCode(this[i])
		}
		return btoa(str)
	}
 
	let yieldThread
	{
		// await yieldThread() will pause the current task until the event loop is cleared
		const channel = new MessageChannel()
		let res
		channel.port1.onmessage = () => res()
		yieldThread = () => {
			return new Promise(resolve => {
				res = resolve
				channel.port2.postMessage("")
			})
		}
	}
 
	// Create Web Workers for generating caves. Stored in the window scope so Khan Academy won't generate a zillion workers.
	if (!win.workers) {
		const workerURL = URL.createObjectURL(new Blob([workerCode], { type: "text/javascript" }))
		win.workers = []
		const jobQueue = []
		const workerCount = (navigator.hardwareConcurrency || 4) - 1 || 1
		for (let i = 0; i < workerCount; i++) { // Generate between 1 and (processors - 1) workers.
			let worker = new Worker(workerURL, { name: `Cave Worker ${i + 1}` })
			worker.onmessage = e => {
				if (worker.resolve) worker.resolve(e.data)
				worker.resolve = null
				if (jobQueue.length) {
					let [data, resolve] = jobQueue.shift()
					worker.resolve = resolve
					worker.postMessage(data)
				}
				else win.workers.push(worker)
			}
			win.workers.push(worker)
		}
 
		win.doWork = (data, resolve) => {
			if (win.workers.length) {
				let worker = win.workers.pop()
				worker.resolve = resolve
				worker.postMessage(data)
			}
			else jobQueue.push([data, resolve])
		}
	}
 
	/**
	 * @type {World}
	 */
	let world
 
	let fill = function(r, g, b) {
		if (g === undefined) {
			g = r
			b = r
		}
		ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
	}
	let stroke = function(r, g, b) {
		if (g === undefined) {
			g = r
			b = r
		}
		ctx.strokeStyle = `rgb(${r}, ${g}, ${b})`
	}
	// const line = (x1, y1, x2, y2) => {
	// 	ctx.moveTo(x1, y1)
	// 	ctx.lineTo(x2, y2)
	// }
	const text = (txt, x, y, h) => {
		h = h || 0
 
		let lines = txt.split("\n")
		for (let i = 0; i < lines.length; i++) {
			ctx.fillText(lines[i], x, y + h * i)
		}
	}
	const textSize = (size) => {
		ctx.font = size + 'px Monospace' // VT323
	}
	let strokeWeight = function(num) {
		ctx.lineWidth = num
	}
	// const ARROW = "arrow"
	const HAND = "pointer"
	// const CROSS = "crosshair"
	let cursor = function(type) {
		canvas.style.cursor = type
	}
	randomSeed(Math.random() * 10000000 | 0)
 
	const save = async () => {
		let saveObj = {
			id: world.id,
			edited: now,
			name: world.name,
			version: version,
			code: world.getSaveString()
		}
		await saveToDB(world.id, saveObj).catch(e => console.error(e))
		world.edited = now
		if (location.href.startsWith("https://willard.fun/")) {
			console.log('Saving to server')
			await fetch(`https://willard.fun/minekhan/saves?id=${world.id}&edited=${saveObj.edited}&name=${encodeURIComponent(world.name)}&version=${encodeURIComponent(version)}`, {
				method: "POST",
				headers: {
					"Content-Type": "application/octet-stream"
				},
				body: saveObj.code.buffer
			})
		}
	}
 
	// Globals
	let version = "Alpha 0.8.2"
	let superflat = false
	let details = true
	let caves = true
 
	/**
	 * @type {WebGLRenderingContext}*/
	let gl
	/**
	 * @type undefined*/
	let glExtensions
 
	/**
	 * @type {(time: Number, view: Matrix) => {}}
	 */
	let skybox
 
	win.blockData = blockData
	win.blockIds = blockIds
 
	// Configurable and savable settings
	let settings = {
		renderDistance: 4,
		fov: 70, // Field of view in degrees
		mouseSense: 100, // Mouse sensitivity as a percentage of the default
		reach: 5,
		showDebug: 3,
		// inventorySort: "blockid"
	}
	let generatedChunks
	let mouseX, mouseY, mouseDown
	let width = win.innerWidth
	let height = win.innerHeight
 
	if (height === 400) alert("Canvas is too small. Click the \"Settings\" button to the left of the \"Vote Up\" button under the editor and change the height to 600.")
 
	let maxHeight = 255
 
	// const ROTATION = 0x1800 // Mask for the direction bits
	let background = "url(./background.webp)"
	if (location.origin === "https://www.kasandbox.org") background = "url(https://www.khanacademy.org/computer-programming/minekhan/5647155001376768/latest.png)"
	else if (!location.origin.includes("localhost") && location.origin !== "file://") background = "url(https://willard.fun/minekhan/background.webp)"
	canvas.style.backgroundImage = background
 
	let dirtBuffer
	let bigArray = win.bigArray || new Float32Array(1000000)
	win.bigArray = bigArray
 
	const use2d = () => {
		gl.disableVertexAttribArray(glCache.aSkylight)
		gl.disableVertexAttribArray(glCache.aBlocklight)
		gl.enableVertexAttribArray(glCache.aVertex2)
		gl.enableVertexAttribArray(glCache.aTexture2)
		gl.enableVertexAttribArray(glCache.aShadow2)
		gl.useProgram(program2D)
		gl.uniform2f(glCache.uOffset, 0, 0) // Remove offset from rendering icons
		gl.depthFunc(gl.ALWAYS)
	}
	const use3d = () => {
		gl.useProgram(program3D)
		gl.enableVertexAttribArray(glCache.aVertex)
		gl.enableVertexAttribArray(glCache.aTexture)
		gl.enableVertexAttribArray(glCache.aShadow)
		gl.enableVertexAttribArray(glCache.aSkylight)
		gl.enableVertexAttribArray(glCache.aBlocklight)
		gl.activeTexture(gl.TEXTURE0)
		// gl.depthFunc(gl.LESS)
	}
	const dirt = () => {
		ctx.clearRect(0, 0, width, height) // Don't draw over the beautful dirt
 
		use2d()
		gl.bindBuffer(gl.ARRAY_BUFFER, dirtBuffer)
		gl.uniform1i(glCache.uSampler2, 1) // Dirt texture
		gl.vertexAttribPointer(glCache.aVertex2, 2, gl.FLOAT, false, 20, 0)
		gl.vertexAttribPointer(glCache.aTexture2, 2, gl.FLOAT, false, 20, 8)
		gl.vertexAttribPointer(glCache.aShadow2, 1, gl.FLOAT, false, 20, 16)
		gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
	}
	const initDirt = () => {
		// Dirt background
		let aspect = width / height
		let stack = height / 96
		let bright = 0.4
		if (!dirtBuffer) dirtBuffer = gl.createBuffer()
		gl.bindBuffer(gl.ARRAY_BUFFER, dirtBuffer)
		let bgCoords = new Float32Array([
			-1, -1, 0, stack, bright,
			1, -1, stack * aspect, stack, bright,
			1, 1, stack * aspect, 0, bright,
			-1, 1, 0, 0, bright
		])
		gl.bufferData(gl.ARRAY_BUFFER, bgCoords, gl.STATIC_DRAW)
		dirt()
	}
	const startLoad = () => {
		dirt()
		world.loadChunks()
	}
 
	// Callback functions for all the screens; will define them further down the page
	let drawScreens = {
		"main menu": () => {},
		"options": () => {},
		"play": () => {},
		"pause": () => {},
		"creation menu": () => {},
		"inventory": () => {},
		"multiplayer menu": () => {},
		"comingsoon menu": () => {},
		"loadsave menu": () => {},
		"chat": () => {}
	}
	let html = {
		play: {
			enter: [document.getElementById("hotbar")],
			exit: [document.getElementById("hotbar")]
		},
		pause: {
			enter: [win.message],
			exit: [win.savebox, win.saveDirections, win.message]
		},
		"main menu": {
			onenter: () => {
				canvas.style.backgroundImage = background
			},
			onexit: () => {
				canvas.style.backgroundImage = ""
				dirt()
			}
		},
		"loadsave menu": {
			enter: [win.worlds, win.boxCenterTop, quota],
			exit: [win.worlds, win.boxCenterTop, quota],
			onenter: () => {
				win.boxCenterTop.placeholder = "Enter Save String (Optional)"
				if (navigator && navigator.storage && navigator.storage.estimate) {
					navigator.storage.estimate().then(data => {
						quota.innerText = `${data.usage.toLocaleString()} / ${data.quota.toLocaleString()} bytes (${(100 * data.usage / data.quota).toLocaleString(undefined, { maximumSignificantDigits: 2 })}%) of your quota used`
					}).catch(console.error)
				}
				win.boxCenterTop.onmousedown = () => {
					let elem = document.getElementsByClassName("selected")
					if (elem && elem[0]) {
						elem[0].classList.remove("selected")
					}
					selectedWorld = 0
					Button.draw()
				}
			},
			onexit: () => {
				win.boxCenterTop.onmousedown = null
			}
		},
		"creation menu": {
			enter: [win.boxCenterTop],
			exit: [win.boxCenterTop],
			onenter: () => {
				win.boxCenterTop.placeholder = "Enter World Name"
				win.boxCenterTop.value = ""
			}
		},
		loading: {
			enter: [document.getElementById("loading-text")],
			exit: [document.getElementById("loading-text")],
			onenter: startLoad
		},
		editworld: {
			enter: [win.boxCenterTop],
			exit: [win.boxCenterTop],
			onenter: () => {
				win.boxCenterTop.placeholder = "Enter World Name"
				win.boxCenterTop.value = ""
			}
		},
		"multiplayer menu": {
			enter: [win.worlds],
			exit: [win.worlds]
		},
		chat: {
			enter: [chatInput, chatOutput],
			exit: [chatInput, chatOutput],
			onenter: () => {
				chatInput.focus()
				releasePointer()
				chatOutput.scroll(0, 10000000)
			}
		},
		inventory: {
			enter: [document.getElementById('inv-container')],
			exit: [document.getElementById('inv-container'), hoverbox, document.getElementById('heldItem')],
			onenter: () => {
				ctx.clearRect(0, 0, width, height) // Hide the GUI and text and stuff
				inventory.playerStorage.render()
			}
		}
	}
 
	let screen = "main menu"
	let previousScreen = screen
	const changeScene = (newScene) => {
		document.getElementById('background-text').classList.add('hidden')
		if (screen === "options") {
			saveToDB("settings", settings).catch(e => console.error(e))
		}
 
		if (html[screen] && html[screen].exit) {
			for (let element of html[screen].exit) {
				element.classList.add("hidden")
			}
		}
 
		if (html[newScene] && html[newScene].enter) {
			for (let element of html[newScene].enter) {
				element.classList.remove("hidden")
			}
		}
 
		if (html[newScene] && html[newScene].onenter) {
			html[newScene].onenter()
		}
		if (html[screen] && html[screen].onexit) {
			html[screen].onexit()
		}
 
		previousScreen = screen
		screen = newScene
		mouseDown = false
		drawScreens[screen]()
		Button.draw()
		Slider.draw()
	}
	let hitBox = {}
	let holding = 0
	let Key = {}
	let glCache
	let worlds, selectedWorld = 0
	let freezeFrame = 0
	let p
	let vec1 = new PVector(), vec2 = new PVector(), vec3 = new PVector()
	let move = {
		x: 0,
		y: 0,
		z: 0,
		ang: Math.sqrt(0.5),
	}
	let p2 = {
		x: 0,
		y: 0,
		z: 0,
	}
 
	const setControl = (name, key, shift = false, ctrl = false, alt = false) => {
		controlMap[name] = {
			key,
			shift,
			ctrl,
			alt,
			get pressed() {
				return Boolean(Key[this.key]
					&& (!this.shift || Key.ShiftLeft || Key.ShiftRight)
					&& (!this.ctrl || Key.ControlLeft || Key.ControlRight)
					&& (!this.alt || Key.AltLeft || Key.AltRight))
			},
			// Check to see if all of an event's data matches this key map
			event(e) {
				return Boolean(e.code === this.key
					&& (!this.shift || e.shiftKey)
					&& (!this.ctrl || e.ctrlKey)
					&& (!this.alt || e.altKey))
			}
		}
	}
	setControl("jump", "Space")
	setControl("walkForwards", "KeyW")
	setControl("strafeLeft", "KeyA")
	setControl("walkBackwards", "KeyS")
	setControl("strafeRight", "KeyD")
	setControl("sprint", "KeyQ")
	setControl("openInventory", "KeyE")
	setControl("openChat", "KeyT")
	setControl("pause", "KeyP")
	setControl("hyperBuilder", "KeyH")
	setControl("superBreaker", "KeyB")
	setControl("toggleSpectator", "KeyL")
	setControl("zoom", "KeyZ")
	setControl("sneak", "ShiftLeft")
	setControl("dropItem", "Backspace")
	setControl("breakBlock", "leftMouse")
	setControl("placeBlock", "rightMouse")
	setControl("pickBlock", "middleMouse")
	//}
 
	const play = () => {
		canvas.onblur()
		p.lastBreak = now
		holding = inventory.hotbar.hand.id
		use3d()
		getPointer()
		fill(255, 255, 255)
		textSize(20)
		canvas.focus()
		changeScene("play")
		ctx.clearRect(0, 0, width, height)
		crosshair()
		hud(true)
		inventory.hotbar.render()
		hotbar()
	}
 
	const getPointer = () => {
		if (canvas.requestPointerLock) {
			canvas.requestPointerLock()
		}
	}
	const releasePointer = () => {
		if (document.exitPointerLock) {
			document.exitPointerLock()
		}
	}
 
	let program3D, program2D, programEntity, program3DFogless
 
	win.shapes = shapes
 
	const initShapes = () => {
 
		// Create buffers for the hitbox outline shapes and icon geometry
		for (let shape in shapes) {
			let obj = shapes[shape]
			obj.buffer = gl.createBuffer()
			gl.bindBuffer(gl.ARRAY_BUFFER, obj.buffer)
			gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(obj.verts.flat(2)), gl.STATIC_DRAW)
			for (let i in obj.variants) {
				let v = obj.variants[i]
				v.buffer = gl.createBuffer()
				gl.bindBuffer(gl.ARRAY_BUFFER, v.buffer)
				gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(v.verts.flat(2)), gl.STATIC_DRAW)
			}
		}
 
		// Create blockData for each of the block variants
		for (let id = 1; id < BLOCK_COUNT; id++) {
			let baseBlock = blockData[id]
			if (baseBlock.shape === shapes.door) baseBlock.rotate = true
 
			if (!baseBlock.uniqueShape) {
				const slabBlock = new BlockData(baseBlock, id | SLAB, true)
				slabBlock.transparent = true
				slabBlock.name += " Slab"
				slabBlock.shape = shapes.slab
				slabBlock.flip = true
				blockData[id | SLAB] = slabBlock
				let v = slabBlock.shape.variants
				for (let j = 1; j < v.length; j++) {
					if (v[j]) {
						let block = new BlockData(slabBlock, id | SLAB | j << 10, false)
						block.shape = v[j]
						blockData[id | SLAB | j << 10] = block
					}
				}
 
				const stairBlock = new BlockData(baseBlock, id | STAIR, true)
				stairBlock.transparent = true
				stairBlock.name += " Stairs"
				stairBlock.shape = shapes.stair
				stairBlock.rotate = true
				stairBlock.flip = true
				blockData[id | STAIR] = stairBlock
				v = stairBlock.shape.variants
				for (let j = 1; j < v.length; j++) {
					if (v[j]) {
						let block = new BlockData(stairBlock, id | STAIR | j << 10, false)
						block.shape = v[j]
						blockData[id | STAIR | j << 10] = block
					}
				}
			}
 
			// Cubes; blocks like pumpkins and furnaces.
			const v = baseBlock.shape.variants
			if (baseBlock.rotate) {
				const [ny, py, pz, nz, px, nx] = baseBlock.textures
				const orders = [
					[ny, py, nz, pz, nx, px], // Opposite
					[ny, py, nx, px, pz, nz],
					[ny, py, px, nx, nz, pz],
				]
				for (let j = 2; j < v.length; j += 2) {
					if (v[j]) {
						let block = new BlockData(baseBlock, id | j << 10, false)
						block.shape = v[j]
						block.textures = orders[j / 2 - 1]
						blockData[id | j << 10] = block
					}
				}
			}
			if (baseBlock.flip) {
				for (let j = 1; j < v.length; j += 2) {
					if (v[j]) {
						let block = new BlockData(baseBlock, id | j << 10, false)
						block.shape = v[j]
						block.textures = v[j-1].textures || block.textures
						blockData[id | j << 10] = block
					}
				}
			}
		}
	}
	let indexOrder = new Uint32Array(bigArray.length / 6 | 0)
	for (let i = 0, j = 0; i < indexOrder.length; i += 6, j += 4) {
		indexOrder[i + 0] = 0 + j
		indexOrder[i + 1] = 1 + j
		indexOrder[i + 2] = 2 + j
		indexOrder[i + 3] = 0 + j
		indexOrder[i + 4] = 2 + j
		indexOrder[i + 5] = 3 + j
	}
 
	const genIcons = () => {
		let start = Date.now()
 
		let shadows = [1, 1, 0.4, 0.4, 0.7, 0.7]
 
		use2d()
		gl.depthFunc(gl.LESS)
		gl.uniform1i(glCache.uSampler2, 0)
 
		const limitX = gl.canvas.width >> 6
		const limitY = gl.canvas.height >> 6
		const limit = limitX * limitY
		const total = (BLOCK_COUNT - 1) * 3
		const pages = Math.ceil(total / limit)
 
		let masks = [CUBE, SLAB, STAIR]
		let drawn = 1 // 0 = air
 
		const mat = new Matrix()
		mat.identity()
		mat.scale(70 / width, 70 / height, -1)
		mat.rotX(Math.PI / 4)
		mat.rotY(Math.PI / 4)
 
		mat.transpose()
		gl.uniformMatrix4fv(gl.getUniformLocation(program2D, "uView"), false, mat.elements)
 
		// Draw as many icons as possible on a canvas the size of the screen, then clear it and continue
		for (let i = 0; i < pages; i++) {
 
			// Draw icons to the WebGL canvas
			let blocksPerPage = (limit / 3 | 0) * 3
			gl.clearColor(0, 0, 0, 0)
			gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
			let pageStart = drawn
			for (let j = 0; j < blocksPerPage; j += 3) {
				for (let k = 0; k < 3; k++) {
					const id = drawn | masks[k]
					if (blockData[id]?.iconImg) {
						const x = ((j + k) % limitX * 128 + 64) / width - 1
						const y = 1 - (((j + k) / limitX | 0) * 128 + 64) / height
 
						gl.uniform2f(glCache.uOffset, x, y)
 
						const shape = blockData[id].shape === shapes.fence ? shapes.fence.variants[3] : blockData[id].shape
						// if (blockData[id].shape === shapes.fence) {
						// Coordinates
						gl.bindBuffer(gl.ARRAY_BUFFER, shape.buffer)
						gl.vertexAttribPointer(glCache.aVertex2, 3, gl.FLOAT, false, 12, 0)
 
						// Textures
						const texture = []
						const shade = []
						new Float32Array(shape.texVerts.flat(3))
						for (let i = 0; i < shape.texVerts.length; i++) { // Direction index
							for (let j = 0; j < shape.texVerts[i].length; j++) { // Face index for the direction
								for (let k = 0; k < 8; k++) { // x,y pairs of the 4 corners
									texture.push(shape.texVerts[i][j][k] + blockData[id].textures[i][k & 1])
								}
								shade.push(shadows[i], shadows[i], shadows[i], shadows[i])
							}
						}
						const texBuffer = gl.createBuffer()
						gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer)
						gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texture), gl.STATIC_DRAW)
						gl.vertexAttribPointer(glCache.aTexture2, 2, gl.FLOAT, false, 8, 0)
 
						// Shading
						const shadeBuffer = gl.createBuffer()
						gl.bindBuffer(gl.ARRAY_BUFFER, shadeBuffer)
						gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(shade), gl.STATIC_DRAW)
						gl.vertexAttribPointer(glCache.aShadow2, 1, gl.FLOAT, false, 4, 0)
 
						if (shape === shapes.lantern || shape === shapes.flower) {
							mat.transpose()
							mat.scale(2)
							mat.transpose()
							gl.uniformMatrix4fv(gl.getUniformLocation(program2D, "uView"), false, mat.elements)
						}
 
						// Index buffer and draw
						gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
						gl.drawElements(gl.TRIANGLES, 6 * shade.length / 4, gl.UNSIGNED_INT, 0)
 
						// Free GPU memory
						gl.deleteBuffer(shadeBuffer)
						gl.deleteBuffer(texBuffer)
 
						if (shape === shapes.lantern || shape === shapes.flower) {
							mat.transpose()
							mat.scale(1/2)
							mat.transpose()
							gl.uniformMatrix4fv(gl.getUniformLocation(program2D, "uView"), false, mat.elements)
						}
					}
				}
				drawn++
				if (drawn === BLOCK_COUNT) {
					blocksPerPage = j + 3
					break
				}
			}
 
			// WebGL canvas is full, now copy it onto the 2D canvas
			ctx.clearRect(0, 0, width, height)
			ctx.drawImage(gl.canvas, 0, 0)
 
			// Now copy all the icons from the big 2D canvas to the little icon canvases
			for (let j = 0; j < blocksPerPage; j += 3) {
				for (let k = 0; k < 3; k++) {
					let id = pageStart + j/3 | masks[k]
 
					if (blockData[id]?.iconImg) {
						const x = (j + k) % limitX
						const y = (j + k) / limitX | 0
						const c = blockData[id].iconImg.getContext("2d")
						c.drawImage(ctx.canvas, x * 64, y * 64, 64, 64, 0, 0, 64, 64)
					}
				}
			}
		}
		console.log("Block icons drawn and extracted in:", Date.now() - start, "ms")
	}
 
	let indexBuffer
 
	let matrix = new Float32Array(16) // A temperary matrix that may store random data.
 
	let defaultTransformation = new Matrix([-10,0,0,0,0,10,0,0,0,0,-10,0,0,0,0,1])
	class Camera {
		constructor() {
			this.x = 0
			this.y = 0
			this.z = 0
			this.px = 0
			this.py = 0
			this.pz = 0
 
			this.rx = 0 // Pitch
			this.ry = 0 // Yaw
			this.prx = 0 // Pitch
			this.pry = 0 // Yaw
 
			this.currentFov = 0
			this.defaultFov = settings.fov
			this.targetFov = settings.fov
			this.step = 0
			this.lastStep = 0
			this.projection = new Float32Array(5)
			this.transformation = new Matrix()
			this.direction = { x: 1, y: 0, z: 0 } // Normalized direction vector
			this.frustum = [] // The 5 planes of the viewing frustum (there's no far plane)
			for (let i = 0; i < 5; i++) {
				this.frustum.push(new Plane(1, 0, 0))
			}
		}
		FOV(fov, time) {
			if (fov === this.currentFov) return
 
			if (!fov) {
				fov = this.currentFov + this.step * (now - this.lastStep)
				this.lastStep = now
				if (Math.sign(this.targetFov - this.currentFov) !== Math.sign(this.targetFov - fov)) {
					fov = this.targetFov
				}
			}
			else if (time) {
				this.targetFov = fov
				this.step = (fov - this.currentFov) / time
				this.lastStep = now
				return
			}
			else {
				this.targetFov = fov
			}
 
			const tang = Math.tan(fov * Math.PI / 360)
			const scale = 1 / tang
			const near = 1
			const far = 1000000
			this.currentFov = fov // Store the state of the projection matrix
			this.nearH = near * tang // This is needed for frustum culling
 
			this.projection[0] = scale / width * height
			this.projection[1] = scale
			this.projection[2] = -far / (far - near)
			this.projection[3] = -1
			this.projection[4] = -far * near / (far - near)
		}
		transform() {
			let diff = (performance.now() - this.lastUpdate) / 50
			if (diff > 1 || isNaN(diff)) diff = 1
			let x = (this.x - this.px) * diff + this.px
			let y = (this.y - this.py) * diff + this.py
			let z = (this.z - this.pz) * diff + this.pz
			this.transformation.copyMatrix(defaultTransformation)
			if (this.rx) this.transformation.rotX(this.rx)
			if (this.ry) this.transformation.rotY(this.ry)
			this.transformation.translate(-x, -y, -z)
		}
		getMatrix() {
			let proj = this.projection
			let view = this.transformation.elements
			matrix[0]  = proj[0] * view[0]
			matrix[1]  = proj[1] * view[4]
			matrix[2]  = proj[2] * view[8] + proj[3] * view[12]
			matrix[3]  = proj[4] * view[8]
			matrix[4]  = proj[0] * view[1]
			matrix[5]  = proj[1] * view[5]
			matrix[6]  = proj[2] * view[9] + proj[3] * view[13]
			matrix[7]  = proj[4] * view[9]
			matrix[8]  = proj[0] * view[2]
			matrix[9]  = proj[1] * view[6]
			matrix[10] = proj[2] * view[10] + proj[3] * view[14]
			matrix[11] = proj[4] * view[10]
			matrix[12] = proj[0] * view[3]
			matrix[13] = proj[1] * view[7]
			matrix[14] = proj[2] * view[11] + proj[3] * view[15]
			matrix[15] = proj[4] * view[11]
			return matrix
		}
		setDirection() {
			if (this.targetFov !== this.currentFov) {
				this.FOV()
			}
			this.direction.x = -sin(this.ry) * cos(this.rx)
			this.direction.y = sin(this.rx)
			this.direction.z = cos(this.ry) * cos(this.rx)
			this.computeFrustum()
		}
		computeFrustum() {
			let X = vec1
			let dir = this.direction
			X.x = dir.z
			X.y = 0
			X.z = -dir.x
			X.normalize()
 
			let Y = vec2
			Y.set(dir)
			Y.mult(-1)
			cross(Y, X, Y)
 
			// Near plane
			this.frustum[0].set(dir.x, dir.y, dir.z)
 
			let aux = vec3
			aux.set(Y)
			aux.mult(this.nearH)
			aux.add(dir)
			aux.normalize()
			cross(aux, X, aux)
			this.frustum[1].set(aux.x, aux.y, aux.z)
 
			aux.set(Y)
			aux.mult(-this.nearH)
			aux.add(dir)
			aux.normalize()
			cross(X, aux, aux)
			this.frustum[2].set(aux.x, aux.y, aux.z)
 
			aux.set(X)
			aux.mult(-this.nearH * width / height)
			aux.add(dir)
			aux.normalize()
			cross(aux, Y, aux)
			this.frustum[3].set(aux.x, aux.y, aux.z)
 
			aux.set(X)
			aux.mult(this.nearH * width / height)
			aux.add(dir)
			aux.normalize()
			cross(Y, aux, aux)
			this.frustum[4].set(aux.x, aux.y, aux.z)
		}
		canSee(x, y, z, maxY) {
			// If it's inside the viewing frustum
			x -= 0.5
			y -= 0.5
			z -= 0.5
			maxY += 0.5
 
			// Player's position is only updated once per tick (20 TPS), but the camera is interpolated between ticks.
			// Add the velocity here to make sure the chunks don't become invisible in those interpolated areas.
			let cx = p.x - p.velocity.x, cy = p.y - p.velocity.y, cz = p.z - p.velocity.z
			for (let i = 0; i < 5; i++) {
				let plane = this.frustum[i]
				let px = x + plane.dx
				let py = plane.dy ? maxY : y
				let pz = z + plane.dz
				if ((px - cx) * plane.nx + (py - cy) * plane.ny + (pz - cz) * plane.nz < 0) {
					return false
				}
			}
 
			return true
 
			// If it's within the range of the fog
			// const dx = x - this.x
			// const dy = this.y < maxY ? 0 : maxY - this.y
			// const dz = z - this.z
			// const d = settings.renderDistance * 16 + 24
			// return dx * dx + dy * dy + dz * dz < d * d
		}
	}
 
	const initModelView = (camera) => {
		if (camera) {
			// Inside the game
			camera.transform()
			camera.getMatrix()
 
			gl.useProgram(program3DFogless)
			gl.uniformMatrix4fv(glCache.uViewFogless, false, matrix)
 
			gl.useProgram(program3D)
			gl.uniformMatrix4fv(glCache.uView, false, matrix)
		}
	}
 
	const rayTrace = (x, y, z, shape) => {
		let cf, cd = 1e9 // Closest face and distance
		let m // Absolute distance to intersection point
		let ix, iy, iz // Intersection coords
		let minX, minY, minZ, maxX, maxY, maxZ, min, max //Bounds of face coordinates
		let dx = p.direction.x < 0
		let dy = p.direction.y < 0
		let dz = p.direction.z < 0
		let verts = shape.verts
		let faces = verts[0]
 
		// Top and bottom faces
		if (dy) {
			faces = verts[1]
		}
		if (p.direction.y) {
			for (let face of faces) {
				min = face.min
				minX = min[0]
				minZ = min[2]
				max = face.max
				maxX = max[0]
				maxZ = max[2]
				m = (y + face[1] - p.y) / p.direction.y
				ix = m * p.direction.x + p.x
				iz = m * p.direction.z + p.z
				if (m > 0 && m < cd && ix >= x + minX && ix <= x + maxX && iz >= z + minZ && iz <= z + maxZ) {
					cd = m // Ray crosses bottom face
					cf = dy ? "top" : "bottom"
				}
			}
		}
 
		// West and East faces
		if (dx) {
			faces = verts[4]
		}
		else {
			faces = verts[5]
		}
		if (p.direction.x) {
			for (let face of faces) {
				min = face.min
				minY = min[1]
				minZ = min[2]
				max = face.max
				maxY = max[1]
				maxZ = max[2]
				m = (x + face[0] - p.x) / p.direction.x
				iy = m * p.direction.y + p.y
				iz = m * p.direction.z + p.z
				if (m > 0 && m < cd && iy >= y + minY && iy <= y + maxY && iz >= z + minZ && iz <= z + maxZ) {
					cd = m
					cf = dx ? "east" : "west"
				}
			}
		}
 
		// South and North faces
		if (dz) {
			faces = verts[2]
		}
		else {
			faces = verts[3]
		}
		if (p.direction.z) {
			for (let face of faces) {
				min = face.min
				minX = min[0]
				minY = min[1]
				max = face.max
				maxX = max[0]
				maxY = max[1]
				m = (z + face[2] - p.z) / p.direction.z
				ix = m * p.direction.x + p.x
				iy = m * p.direction.y + p.y
				if (m > 0 && m < cd && ix >= x + minX && ix <= x + maxX && iy >= y + minY && iy <= y + maxY) {
					cd = m
					cf = dz ? "north" : "south"
				}
			}
		}
		return [cd, cf]
	}
	const runRayTrace = (x, y, z) => {
		let block = world.getBlock(x, y, z)
		if (block) {
			let shape = blockData[block].shape
			if (shape.getShape) {
				shape = shape.getShape(x, y, z, world, blockData)
			}
			let rt = rayTrace(x, y, z, shape)
 
			if (rt[1] && rt[0] < hitBox.closest) {
				hitBox.closest = rt[0]
				hitBox.face = rt[1]
				hitBox.pos = [x, y, z]
				hitBox.shape = shape
			}
		}
	}
	const lookingAt = () => {
		// Checks blocks in front of the player to see which one they're looking at
		hitBox.pos = null
		hitBox.closest = 1e9
 
		if (p.spectator) {
			return
		}
		let blockState = world.getBlock(p2.x, p2.y, p2.z)
		if (blockState) {
			hitBox.pos = [p2.x, p2.y, p2.z]
			hitBox.closest = 0
			hitBox.shape = blockData[blockState].shape
			return
		}
 
		let pd = p.direction
 
		let minX = p2.x
		let maxX = 0
		let minY = p2.y
		let maxY = 0
		let minZ = p2.z
		let maxZ = 0
 
		for (let i = 0; i < settings.reach + 1; i++) {
			if (i > settings.reach) {
				i = settings.reach
			}
			maxX = round(p.x + pd.x * i)
			maxY = round(p.y + pd.y * i)
			maxZ = round(p.z + pd.z * i)
			if (maxX === minX && maxY === minY && maxZ === minZ) {
				continue
			}
			if (minX !== maxX) {
				if (minY !== maxY) {
					if (minZ !== maxZ) {
						runRayTrace(maxX, maxY, maxZ)
					}
					runRayTrace(maxX, maxY, minZ)
				}
				if (minZ !== maxZ) {
					runRayTrace(maxX, minY, maxZ)
				}
				runRayTrace(maxX, minY, minZ)
			}
			if (minY !== maxY) {
				if (minZ !== maxZ) {
					runRayTrace(minX, maxY, maxZ)
				}
				runRayTrace(minX, maxY, minZ)
			}
			if (minZ !== maxZ) {
				runRayTrace(minX, minY, maxZ)
			}
			if (hitBox.pos) {
				return // The ray has collided; it can't possibly find a closer collision now
			}
			minZ = maxZ
			minY = maxY
			minX = maxX
		}
	}
 
	const collide = (faces, compare = max, index = 0, offset = 0) => {
		if (p.spectator) {
			if (p2.y === 2 && index === 1 && faces.length && p.velocity.y <= 0) return 0.5 + p.bottomH // Stop them at bottom bedrock
			return false
		}
		let col = false
		let pos = 0
		for (let face of faces) {
			const [minX, minY, minZ, maxX, maxY, maxZ] = face
			const cy = index === 1 ? p.minY() <= maxY : p.minY() < maxY // Collide with the floor when perfectly level with it
			let colliding = p.minX() < maxX && p.maxX() > minX && cy && p.maxY() > minY && p.minZ() < maxZ && p.maxZ() > minZ
			if (colliding) {
				pos = col ? compare(pos, face[index] + offset) : face[index] + offset
				col = true
			}
		}
 
		return col && pos
	}
 
	let contacts = {
		faces: [[], [], [], [], [], []],
		add: function(x, y, z, verts) {
			for (let dir = 0; dir < 6; dir++) {
				for (let i = 0; i < verts[dir].length; i++) {
					const face = verts[dir][i]
					const minX = roundBits(min(face[0], face[6]) + x - p2.x)
					const maxX = roundBits(max(face[0], face[6]) + x - p2.x)
					const minY = roundBits(min(face[1], face[7]) + y - p2.y)
					const maxY = roundBits(max(face[1], face[7]) + y - p2.y)
					const minZ = roundBits(min(face[2], face[8]) + z - p2.z)
					const maxZ = roundBits(max(face[2], face[8]) + z - p2.z)
					this.faces[dir].push([minX, minY, minZ, maxX, maxY, maxZ])
				}
			}
		},
		clear: function() {
			for (let i = 0; i < 6; i++) this.faces[i].length = 0
		},
	}
	let resolveContactsAndUpdatePosition = function() {
		if (p.y < 0) p.y = 70 // Keep players out of the void; there's no way out.
 
		let mag = p.velocity.mag()
		let steps = Math.ceil(mag / p.w)
		let VX = p.velocity.x / steps
		let VY = p.velocity.y / steps
		let VZ = p.velocity.z / steps
 
		let pminX = floor(0.5 + p.x - p.w + (p.velocity.x < 0 ? p.velocity.x : 0))
		let pmaxX = ceil(-0.5 + p.x + p.w + (p.velocity.x > 0 ? p.velocity.x : 0))
		let pminY = max(floor(0.5 + p.y - p.bottomH + (p.velocity.y < 0 ? p.velocity.y : 0)), 0)
		let pmaxY = min(ceil(p.y + p.topH + (p.velocity.y > 0 ? p.velocity.y : 0)), maxHeight) // Half a block higher than necessary to ensure half-steps up work
		let pminZ = floor(0.5 + p.z - p.w + (p.velocity.z < 0 ? p.velocity.z : 0))
		let pmaxZ = ceil(-0.5 + p.z + p.w + (p.velocity.z > 0 ? p.velocity.z : 0))
 
		for (let y = pmaxY; y >= pminY; y--) {
			for (let x = pminX; x <= pmaxX; x++) {
				for (let z = pminZ; z <= pmaxZ; z++) {
					let block = world.getBlock(x, y, z)
					const data = blockData[block]
					if (data.solid) {
						let shape = data.shape
						if (shape.getShape) {
							shape = shape.getShape(x, y, z, world, blockData)
						}
						contacts.add(x, y, z, shape.verts)
					}
				}
			}
		}
 
		// let hasCollided = false
		p.px = p.x
		p.py = p.y
		p.pz = p.z
		p.onGround = false
		for (let j = 1; j <= steps; j++) {
			let px = p.x
			let pz = p.z
 
			// Check collisions in the Y direction
			p.y += VY
			if (VY > 0) {
				let npy = collide(contacts.faces[0], min, 1, p2.y - p.topH - 0.01)
				if (npy !== false) {
					p.y = npy
					p.velocity.y = 0
				}
			}
			else {
				let npy = collide(contacts.faces[1], max, 1, p2.y + p.bottomH)
				if (npy !== false) {
					p.onGround = true
					p.y = npy
					p.velocity.y = 0
				}
			}
 
			// Check collisions in the X direction
			if (VX) {
				p.x += VX
				let npx
				if (VX > 0) npx = collide(contacts.faces[5], min, 0, p2.x - p.w)
				else        npx = collide(contacts.faces[4], max, 0, p2.x + p.w)
				if (npx !== false) {
					if (p.onGround) {
						// See if we can step up
						p.y += 0.5
						if (collide(contacts.faces[0]) === false) {
							if (VX > 0) npx = collide(contacts.faces[5], min, 0, p2.x - p.w)
							else        npx = collide(contacts.faces[4], max, 0, p2.x + p.w)
						}
						p.y -= 0.5
					}
					if (npx !== false) {
						p.x = npx
						p.velocity.x = 0
					}
				}
 
				if (p.sneaking && p.onGround) {
					if (collide(contacts.faces[1], max, 1, p2.y + p.bottomH) === false) {
						p.x = px
						p.velocity.x = 0
					}
				}
			}
 
			// Check collisions in the Z direction
			if (VZ) {
				p.z += VZ
				let npz
				if (VZ > 0) npz = collide(contacts.faces[3], min, 2, p2.z - p.w)
				else        npz = collide(contacts.faces[2], max, 2, p2.z + p.w)
				if (npz !== false) {
					if (p.onGround) {
						// See if we can step up
						p.y += 0.5
						if (collide(contacts.faces[0]) === false) {
							if (VZ > 0) npz = collide(contacts.faces[3], min, 2, p2.z - p.w)
							else        npz = collide(contacts.faces[2], max, 2, p2.z + p.w)
						}
						p.y -= 0.5
					}
					if (npz !== false) {
						p.z = npz
						p.velocity.z = 0
					}
				}
 
				if (p.sneaking && p.onGround) {
					if (collide(contacts.faces[1], max, 1, p2.y + p.bottomH) === false) {
						p.z = pz
						p.velocity.z = 0
					}
				}
			}
 
			if (p.velocity.x === 0) VX = 0
			if (p.velocity.y === 0) VY = 0
			if (p.velocity.z === 0) VZ = 0
		}
 
 
		if (!p.flying) {
			let drag = p.onGround ? 0.5 : 0.85
			p.velocity.z += p.velocity.z * drag - p.velocity.z
			p.velocity.x += p.velocity.x * drag - p.velocity.x
		}
		else {
			let drag = 0.9
			if (!controlMap.walkForwards.pressed && !controlMap.walkBackwards.pressed && !controlMap.strafeLeft.pressed && !controlMap.strafeRight.pressed) drag = 0.7
			p.velocity.z += p.velocity.z * drag - p.velocity.z
			p.velocity.x += p.velocity.x * drag - p.velocity.x
			p.velocity.y += p.velocity.y * 0.7 - p.velocity.y
			if (p.onGround && !p.spectator) {
				p.flying = false
			}
		}
 
		p.lastUpdate = performance.now()
		contacts.clear()
		lookingAt()
	}
	let runGravity = function() {
		if (p.flying) {
			return
		}
 
		p.velocity.y += p.gravityStrength
		if(p.velocity.y < -p.maxYVelocity) {
			p.velocity.y = -p.maxYVelocity
		}
		if(p.onGround) {
			if(controlMap.jump.pressed) {
				p.velocity.y = p.jumpSpeed
				p.onGround = false
			}
		}
	}
 
	/**
	 * @param {Camera} camera
	 */
	const drawHitbox = (camera) => {
		// Get the transformation matrix in position
		const [x, y, z] = hitBox.pos
		camera.transformation.translate(x, y, z)
		// camera.transformation.elements[15] -= 1
		gl.useProgram(program3DFogless)
		gl.uniformMatrix4fv(glCache.uViewFogless, false, camera.getMatrix())
		// console.log(...[...matrix].map(n => +n.toFixed(3)))
		camera.transformation.translate(-x, -y, -z)
 
		// Bind texture buffer with black texture
		gl.bindBuffer(gl.ARRAY_BUFFER, hitBox.buffer)
		gl.vertexAttribPointer(glCache.aTexture, 2, gl.FLOAT, false, 0, 0)
 
		// Bind vertex buffer with the shape of the targeted block
		gl.bindBuffer(gl.ARRAY_BUFFER, hitBox.shape.buffer)
		gl.vertexAttribPointer(glCache.aVertex, 3, gl.FLOAT, false, 0, 0)
		gl.disableVertexAttribArray(glCache.aSkylight)
		gl.disableVertexAttribArray(glCache.aBlocklight)
		gl.disableVertexAttribArray(glCache.aShadow)
 
		// Avoid Z-fighting on the outline since it overlaps the block
		gl.uniform1f(glCache.uZoffset, -0.0005)
 
		// Draw lines around 1 face at a time to avoid drawing triangles
		for (let i = 0; i < hitBox.shape.size; i++) {
			gl.drawArrays(gl.LINE_LOOP, i * 4, 4)
		}
		gl.uniform1f(glCache.uZoffset, 0)
	}
 
	const changeWorldBlock = (t, x, y, z) => {
		if (!hitBox.pos) return
		x ??= hitBox.pos[0]
		y ??= hitBox.pos[1]
		z ??= hitBox.pos[2]
		if (y <= 0 || y >= maxHeight) return
 
		const data = blockData[t]
		if (t && data.rotate) {
			let pi = Math.PI / 4
			if (p.ry <= pi || p.ry >= 7 * pi) {
				t |= WEST
			}
			else if (p.ry < 3 * pi) {
				t |= SOUTH
			}
			else if (p.ry < 5 * pi) {
				t |= EAST
			}
			else if (p.ry < 7 * pi) {
				// t |= NORTH
			}
		}
 
		if (t && data.flip && hitBox.face !== "top" && (hitBox.face === "bottom" || (p.direction.y * hitBox.closest + p.y) % 1 < 0.5)) {
			t |= FLIP
		}
 
		// Check for block collision with player
		const newBlock = blockData[t]
		if (newBlock.solid) {
			const verts = newBlock.shape.verts
			let [ny, py, pz, nz, px, nx] = [0.5, -0.5, -0.5, 0.5, -0.5, 0.5]
			for (let face of verts[0]) ny = min(ny, face[1])
			for (let face of verts[1]) py = max(py, face[1])
			for (let face of verts[2]) pz = max(pz, face[2])
			for (let face of verts[3]) nz = min(nz, face[2])
			for (let face of verts[4]) px = max(px, face[0])
			for (let face of verts[5]) nx = min(nx, face[0])
 
			let pny = roundBits(p.y - p.bottomH - y)
			let ppy = roundBits(p.y + p.topH - y)
			let pnx = p.x - p.w - x
			let ppx = p.x + p.w - x
			let pnz = p.z - p.w - z
			let ppz = p.z + p.w - z
			// console.table([[ny, py, pz, nz, px, nx], [pny, ppy, ppz, pnz, ppx, pnx]])
			if (ppx > nx && ppy > ny && ppz > nz && pnx < px && pny < py && pnz < pz) return
		}
 
		world.setBlock(x, y, z, t, 0)
		if (t) {
			p.lastPlace = now
		}
		else {
			p.lastBreak = now
		}
	}
	const newWorldBlock = () => {
		if(!hitBox.pos || !holding) return
		let pos = hitBox.pos, [x, y, z] = pos
		switch(hitBox.face) {
			case "top":
				y += 1
				break
			case "bottom":
				y -= 1
				break
			case "south":
				z -= 1
				break
			case "north":
				z += 1
				break
			case "west":
				x -= 1
				break
			case "east":
				x += 1
				break
		}
		let oldBlock = world.getBlock(x, y, z)
 
		if (y < maxHeight && y >= 0 && (!oldBlock || blockData[oldBlock].shape === shapes.flower) && oldBlock !== holding) {
			changeWorldBlock(holding, x, y, z)
		}
	}
 
	let renderedChunks = 0
 
	let analytics = {
		totalTickTime: 0,
		worstFrameTime: 0,
		totalRenderTime: 0,
		totalFrameTime: 0,
		lastUpdate: 0,
		frames: 1,
		displayedTickTime: "0",
		displayedRenderTime: "0",
		displayedFrameTime: "0",
		displayedwFrameTime: "0",
		displayedwFps: 0,
		fps: 0,
		worstFps: 60,
	}
	const chunkDist = (c) => {
		let dx = p.x - c.x
		let dz = p.z - c.z
		if (dx > 16) {
			dx -= 16
		}
		else if (dx > 0) {
			dx = 0
		}
		if (dz > 16) {
			dz -= 16
		}
		else if (dz > 0) {
			dz = 0
		}
		return Math.sqrt(dx * dx + dz * dz)
	}
	const sortChunks = (c1, c2) => { // Sort the list of chunks based on distance from the player
		let dx1 = p.x - c1.x - 8
		let dy1 = p.z - c1.z - 8
		let dx2 = p.x - c2.x - 8
		let dy2 = p.z - c2.z - 8
		return dx1 * dx1 + dy1 * dy1 - (dx2 * dx2 + dy2 * dy2)
	}
	const fillReqs = (x, z) => {
		// Chunks must all be loaded first.
		let done = true
		for (let i = x - 4; i <= x + 4; i++) {
			for (let j = z - 4; j <= z + 4; j++) {
				let chunk = world.loaded[(i + world.offsetX) * world.lwidth + j + world.offsetZ]
				if (!chunk.generated) {
					world.generateQueue.push(chunk)
					done = false
				}
 
				if (!chunk.populated && i >= x - 3 && i <= x + 3 && j >= z - 3 && j <= z + 3) {
					world.populateQueue.push(chunk)
					done = false
				}
 
				if (!chunk.loaded && i >= x - 2 && i <= x + 2 && j >= z - 2 && j <= z + 2) {
					if (world.loadFrom[`${chunk.x >> 4},${chunk.z >> 4}`]) {
						world.loadQueue.push(chunk)
						done = false
					}
					else chunk.load()
				}
 
				if (!chunk.lit && i >= x - 1 && i <= x + 1 && j >= z - 1 && j <= z + 1) {
					world.lightingQueue.push(chunk)
					done = false
				}
			}
		}
 
		return done
	}
	const renderFilter = (chunk) => {
		const d = settings.renderDistance + Math.SQRT1_2
		return chunk.distSq <= d * d
	}
 
	const debug = (message) => {
		let ellapsed = performance.now() - debug.start
		if (ellapsed > 30) {
			console.log(message, ellapsed.toFixed(2), "milliseconds")
		}
	}
 
	let alerts = []
	const chatAlert = (msg) => {
		if (screen !== "play") return
		alerts.push({
			msg: msg.substr(0, 50),
			created: now,
			rendered: false
		})
		if (alerts.length > 5) alerts.shift()
		renderChatAlerts()
	}
	let charWidth = 6
	{
		// Determine the width of the user's system monospace font.
		let span = document.createElement('span')
		span.style.fontFamily = "monospace"
		span.style.fontSize = "20px"
		span.textContent = "a"
		document.body.append(span)
		charWidth = span.offsetWidth
		span.remove()
	}
	const renderChatAlerts = () => {
		if (!alerts.length || screen !== "play") return
		let y = height - 150
		if (now - alerts[0].created > 10000 || !alerts.at(-1).rendered) {
			// Clear old alerts
			let x = 50
			let y2 = y - 50 * (alerts.length - 1) - 20
			let w = charWidth * alerts.reduce((mx, al) => max(mx, al.msg.length), 0)
			let h = 50 * (alerts.length - 1) + 24
			ctx.clearRect(x, y2, w + 5, h + 5)
		}
		else return
		while(alerts.length && now - alerts[0].created > 10000) {
			alerts.shift()
		}
		textSize(20)
		for (let i = alerts.length - 1; i >= 0; i--) {
			let alert = alerts[i]
			text(alert.msg, 50, y)
			y -= 50
		}
	}
 
	const chat = (msg, color, author) => {
		let lockScroll = false
		if (chatOutput.scrollTop + chatOutput.clientHeight + 50 > chatOutput.scrollHeight) {
			lockScroll = true
		}
		let div = document.createElement("div")
		div.className = "message"
 
		let content = document.createElement('span')
		if (color) content.style.color = color
		content.textContent = msg
 
		if (author) {
			let name = document.createElement('span')
			name.textContent = author + ": "
			if (author === "Willard") {
				name.style.color = "cyan"
			}
			div.append(name)
			msg = `${author}: ${msg}` // For the chatAlert
		}
 
		div.append(content)
 
		chatOutput.append(div)
		chatAlert(msg)
		if (lockScroll) {
			chatOutput.scroll(0, 10000000)
		}
	}
 
	const sendChat = (msg) => {
		if (multiplayer) {
			multiplayer.send(JSON.stringify({
				type: "chat",
				data: msg
			}))
		}
		chat(`${currentUser.username}: ${msg}`, "lightgray")
	}
 
	let currentUser = { username: "Player" }
	let blockLog = { Player: [] }
	let commands = new Map()
	let autocompleteList = []
	let commandList = []
	var multiplayer = null
	let playerPositions = {}
	let playerEntities = {}
	let playerDistances = []
	const setAutocomplete = (list) => {
		if (list === autocompleteList) return
		if (list.length === autocompleteList.length) {
			let i = 0
			for (; i < list.length; i++) {
				if (list[i] !== autocompleteList[i]) break
			}
			if (i === list.length) return
		}
 
		let element = document.getElementById("commands")
		while (element.childElementCount) element.removeChild(element.lastChild)
 
		for (let string of list) {
			let option = document.createElement("option")
			option.value = string
			element.append(option)
		}
		autocompleteList = list
	}
	const addCommand = (name, callback, usage, description, autocomplete) => {
		if (!autocomplete) autocomplete = () => {}
		commands.set(name, {
			name,
			callback,
			usage,
			description,
			autocomplete
		})
		commandList.push("/" + name)
	}
	addCommand("help", args => {
		let commandName = args[0]
		if (commands.has(commandName)) {
			const command = commands.get(commandName)
			chat(`Usage: ${command.usage}\nDescription: ${command.description}`, "lime")
		}
		else chat(`/help shows command usage with /help <command name>. Syntax is like "/commandName <required> [optional=default]". So for example "/undo [username=yourself] <count>" means you can do "/undo 12" to undo your own last 12 block edits, or "/undo 1337 griefer 5000" to undo 1337 griefer's last 5000 block edits.\n\nCommands: ${commandList.map(command => `${command.slice(1)}`).join(", ")}`)
	}, "/help <command name>", "Shows how to use a command", () => {
		setAutocomplete(commandList.map(command => `/help ${command.slice(1)}`))
	})
	addCommand("ban", args => {
		let username = args.join(" ")
		if (!username) {
			chat("Please provide a username. Like /ban Willard", "tomato")
			return
		}
		if (!win.ban) {
			chat("This is a singleplayer world. There's nobody to ban.", "tomato")
			return
		}
		win.ban(username)
	}, "/ban <username>", "IP ban a player from your world until you close it.", () => {
		setAutocomplete(Object.keys(playerPositions).map(player => `/ban ${player}`))
	})
	addCommand("online", () => {
		if (win.online && multiplayer) {
			win.online()
		}
		else {
			chat("You're all alone. Sorry.", "tomato")
		}
	}, "/online", "Lists online players")
	addCommand("history", args => {
		let dist = +args[0] || 20
		dist *= dist
		let lines = []
		for (let name in blockLog) {
			let list = blockLog[name]
			let oldest = 0
			let newest = 0
			let broken = 0
			let placed = 0
			for (let i = 0; i < list.length; i++) {
				let block = list[i]
				let dx = block[0] - p.x
				let dy = block[1] - p.y
				let dz = block[2] - p.z
				if (dx * dx + dy * dy + dz * dz <= dist) {
					if (block[3]) placed++
					else broken++
					newest = block[5]
					if (!oldest) oldest = block[5]
				}
			}
			if (oldest) {
				lines.push(`${name}: ${broken} blocks broken and ${placed} blocks placed between ${timeString(now-oldest)} and ${timeString(now-newest)}.`)
			}
		}
		if (lines.length) {
			let ul = document.createElement("ul")
			for (let line of lines) {
				let li = document.createElement("li")
				li.textContent = line
				ul.append(li)
			}
			chatOutput.append(ul)
			// chat(`Within ${Math.sqrt(dist)} blocks of your position:\n` + lines.join("\n"), "lime")
		}
		else chat(`No blocks edited within ${Math.sqrt(dist)} blocks within this world session.`, "tomato")
	}, "/history [dist=20]", "Shows a list of block edits within a specified range from your current world session.")
	addCommand("undo", async args => {
		if (multiplayer && !multiplayer.host) {
			chat("Only the world's host may use this command.", "tomato")
			return
		}
		let count = +args.pop()
		if (isNaN(count)) {
			chat("Please provide a count of the number of blocks to undo. Like /undo Willard 4000", "tomato")
			return
		}
		let name = currentUser.username
		if (args.length) name = args.join(" ")
		let list = blockLog[name]
		if (!list) {
			chat("You provided a name that didn't match any users with a block history. Names are case-sensitive.", "tomato")
			return
		}
 
		if (count > list.length) count = list.length;
 
		// Run after closing chat
		(async () => {
			await sleep(1)
			chat(`Undoing the last ${count} block edits from ${name}`, "lime")
			for (let i = 0; i < count; i++) {
				let [x, y, z, , oldBlock] = list.pop()
				if (multiplayer) await sleep(50)
				world.setBlock(x, y, z, oldBlock, false, false, true)
			}
			chat(`${count} block edits undone.`, "lime")
		})()
		play()
	}, "/undo [username=Player] <blockCount>", "Undoes the last <blockCount> block edits made by [username]", () => {
		setAutocomplete(Object.keys(blockLog).map(name => `/undo ${name} ${blockLog[name].length}`))
	})
	addCommand("fill", args => {
		if (multiplayer) {
			chat("This command only works on offline worlds.", "tomato")
			return
		}
		if (blockLog[currentUser.username].length < 2) {
			chat("You must place (or break) 2 blocks to indicate the fill zone before using this command.", "tomato")
			return
		}
 
		let solid = true
		if (args[1]?.toLowerCase()[0] === "h") solid = false
		let shape = "cube"
		if (args[0]?.toLowerCase() === "sphere") shape = "sphere"
		if (args[0]?.toLowerCase().startsWith("cyl")) shape = "cylinder"
 
		const block = inventory.hotbar.hand.id
		const name = inventory.hotbar.hand.name
 
		let [start, end] = blockLog[currentUser.username].slice(-2)
		let [x1, y1, z1] = start
		let [x2, y2, z2] = end
 
		const range = settings.renderDistance * 16 - 16
		const sort = (a, b) => a - b
 
		if (shape === "cube") {
			// Make x1 y1 z1 be the lowest corner so I can loop up
 
			[x1, x2] = [x1, x2].sort(sort);
			[y1, y2] = [y1, y2].sort(sort);
			[z1, z2] = [z1, z2].sort(sort)
 
			if (x2 - p.x > range || p.x - x1 > range || z2 - p.z > range || p.z - z1 > range) {
				chat("You're trying to use this command outside your loaded chunks. That'll crash the game, so move closer or re-select your corners.", "tomato")
				return
			}
 
			let count = (x2 - x1 + 1) * (y2 - y1 + 1) * (z2 - z1 + 1)
			if (!solid) count -= (x2 - x1 - 1) * (y2 - y1 - 1) * (z2 - z1 - 1)
 
			if (count > 1000000) {
				chat(`You're trying to edit ${count.toLocaleString()} blocks at once. Stop and reevaluate your life choices.`, "tomato")
				return
			}
 
			if (!confirm(`Width: ${x2 - x1 + 1}\nHeight: ${y2 - y1 + 1}\nDepth: ${z2 - z1 + 1}\nYou're about to set ${count.toLocaleString()} blocks of ${name}  with a ${solid ? "solid" : "hollow"} cuboid. Are you sure you want to proceed?`)) return
 
			new Promise(async resolve => {
				await sleep(0) // Let the player watch the blocks change
				let edited = 0
				for (let x = x1; x <= x2; x++) {
					for (let y = y1; y <= y2; y++) {
						for (let z = z1; z <= z2; z++) {
							if ((solid || x === x1 || x === x2 || y === y1 || y === y2 || z === z1 || z === z2) && world.getBlock(x, y, z) !== block) {
								world.setBlock(x, y, z, block)
								edited++
								if ((edited & 1023) === 0) await sleep(4)
							}
						}
					}
				}
				chat(`${edited.toLocaleString()} ${name} blocks successfully filled!`, "lime")
				resolve()
			})
		}
		else if (shape === "sphere") {
			const radius = Math.hypot(x1 - x2, y1 - y2, z1 - z2) + 0.5
 
			if (Math.hypot(x1 - p.x, z1 - p.z) + radius > range) {
				chat("You're trying to use this command outside your loaded chunks. That'll crash the game, so move closer or re-select your center and edge.", "tomato")
				return
			}
 
			let count = 4 / 3 * Math.PI * radius**3 | 0
			if (!solid) count -= 4 / 3 * Math.PI * (radius - 1)**3 | 0
 
			if (count > 1000000) {
				chat(`You're trying to edit ${count.toLocaleString()} blocks at once. Stop and reevaluate your life choices.`, "tomato")
				return
			}
 
			if (!confirm(`Center: (${x1}, ${y1}, ${z1})\nRadius: ${radius | 0}\nYou're about to set approximately ${count.toLocaleString()} blocks of ${name} with a ${solid ? "solid" : "hollow"} sphere. Are you sure you want to proceed?`)) return
 
			const offset = Math.ceil(radius)
			new Promise(async resolve => {
				await sleep(0) // Let the player watch the blocks change
				let edited = 0
				for (let x = x1 - offset; x <= x1 + offset; x++) {
					for (let y = Math.max(y1 - offset, 1); y <= Math.min(y1 + offset, maxHeight); y++) {
						for (let z = z1 - offset; z <= z1 + offset; z++) {
							let d = Math.hypot(x1 - x, y1 - y, z1 - z)
							if (d <= radius && (solid || radius - d < 1.0) && world.getBlock(x, y, z) !== block) {
								world.setBlock(x, y, z, block)
								edited++
								if ((edited & 1023) === 0) await sleep(4)
							}
						}
					}
				}
				chat(`${edited.toLocaleString()} ${name} blocks successfully filled!`, "lime")
				resolve()
			})
		}
		else if (shape === "cylinder") {
			const radius = Math.hypot(x1 - x2, z1 - z2) + 0.5;
			[y1, y2] = [y1, y2].sort(sort)
 
			if (Math.hypot(x1 - p.x, z1 - p.z) + radius > range) {
				chat("You're trying to use this command outside your loaded chunks. That'll crash the game, so move closer or re-select your center and edge.", "tomato")
				return
			}
 
			let count = (y2 - y1 + 1) * Math.PI * radius**2 | 0
			if (!solid) count -= (y2 - y1 - 1) * Math.PI * (radius - 1)**2 | 0
 
			if (count > 1000000) {
				chat(`You're trying to edit ${count.toLocaleString()} blocks at once. Stop and reevaluate your life choices.`, "tomato")
				return
			}
 
			if (!confirm(`Center: (${x1}, ${y1}, ${z1})\nRadius: ${radius | 0}\nHeight: ${y2 - y1 + 1}\nYou're about to set approximately ${count.toLocaleString()} blocks of ${name} with a ${solid ? "solid" : "hollow"} cylinder. Are you sure you want to proceed?`)) return
 
			const offset = Math.ceil(radius)
			new Promise(async resolve => {
				await sleep(0) // Let the player watch the blocks change
				let edited = 0
				for (let x = x1 - offset; x <= x1 + offset; x++) {
					for (let y = y1; y <= y2; y++) {
						for (let z = z1 - offset; z <= z1 + offset; z++) {
							let d = Math.hypot(x1 - x, z1 - z)
							if (d <= radius && (solid || radius - d < 1.0 || y === y1 || y === y2) && world.getBlock(x, y, z) !== block) {
								world.setBlock(x, y, z, block)
								edited++
								if ((edited & 1023) === 0) await sleep(4)
							}
						}
					}
				}
				chat(`${edited.toLocaleString()} ${name} blocks successfully filled!`, "lime")
				resolve()
			})
		}
 
		play()
	},
	"/fill [cuboid|sphere|cylinder] [solid|hollow]",
	"Uses the player's last 2 edited blocks to designate an area to fill, then fills it with the block in the player's hand.\nWith a cuboid, the 2 blocks are the corners.\nWith a sphere, the first block you edit is the center, and the 2nd block determines the radius.\nWith a cylinder, the first block determines the center, and the 2nd block determines the radius and height.",
	() => {
		setAutocomplete(["/fill cuboid hollow", "/fill sphere hollow", "/fill cylinder hollow", "/fill cuboid solid", "/fill sphere solid", "/fill cylinder solid"])
	})
	addCommand("time",
		args => {
			let time = world.tickCount % 12000
			if (!args.length) return chat(`Current time: ${time}`, "lime")
			let arg = args[0].toLowerCase()
			let target = 0
			if (/^\d+$/.test(arg)) target = +arg
			else if (arg === "dawn") target = 0
			else if (arg === "dusk") target = 6000
			else if (arg === "noon") target = 3000
			else if (arg === "night") target = 9000
 
			chat(`Setting time to ${target}`, "lime")
			if (target < time) target += 12000
			world.addedTime = (target - time + 49) / 50 | 0
			play()
		},
		"/time [dawn|dusk|noon|night|<Number>]",
		"Either displays the current time, or sets the time to the desired value, measured in 1/20ths of a second.",
		() => setAutocomplete(["/time dawn", "/time noon", "/time dusk", "/time night"])
	)
 
	const sendCommand = (msg) => {
		msg = msg.substr(1)
		let parts = msg.split(" ")
		let cmd = parts.shift()
		if (commands.has(cmd)) {
			commands.get(cmd).callback(parts)
		}
		setAutocomplete(commandList)
	}
 
	const loggedIn = async () => {
		let exists = await fetch("https://willard.fun/profile").then(res => res.text()).catch(() => "401")
		if (!exists || exists === "401") {
			if (location.href.startsWith("https://willard.fun")) {
				alert("You're not logged in. Head over to https://willard.fun/login to login or register before connecting to the server.")
			}
			else {
				alert("Multiplayer is currently only available on https://willard.fun/login => https://willard.fun/minekhan")
			}
			return false
		}
		currentUser = JSON.parse(exists)
		if (blockLog.Player) {
			blockLog[currentUser.username] = blockLog.Player
			delete blockLog.Player
		}
		return true
	}
 
	const initMultiplayer = async (target) => {
		if (multiplayer) return
		let logged = await loggedIn()
		if (!logged) return
 
		let host = false
		if (!target) {
			target = world.id
			host = true
		}
		multiplayer = new WebSocket("wss://willard.fun/ws?target=" + target)
		multiplayer.host = host
		multiplayer.binaryType = "arraybuffer"
		multiplayer.onopen = () => {
			let password = ""
			if (!host && !worlds[target].public) password = prompt(`What's the password for ${worlds[target].name}?`) || ""
			multiplayer.send(JSON.stringify({
				type: "connect",
				password
			}))
			if (host) {
				let password = prompt("Enter a password to make this a private world, or leave it blank for a public world.") || ""
				multiplayer.send(JSON.stringify({
					type: "init",
					name: world.name,
					version,
					password
				}))
			}
			multiplayer.pos = setInterval(() => multiplayer.send(JSON.stringify({
				type: "pos",
				data: { x: p.x, y: p.y, z: p.z, vx: p.velocity.x, vy: p.velocity.y, vz: p.velocity.z }
			})), 500)
		}
		let multiplayerError = ""
		multiplayer.onmessage = msg => {
			if (msg.data === "ping") {
				multiplayer.send("pong")
				return
			}
			if (typeof msg.data !== "string" && screen === "multiplayer menu") {
				world = new World(true) // Non-hosts can't use console commands since this isn't global.
				world.loadSave(new Uint8Array(msg.data))
				changeScene("loading")
				return
			}
			let packet = JSON.parse(msg.data)
			if (packet.type === "setBlock") {
				let a = packet.data
 
				if (!a[4]) {
					// If it's not an "Undo" packet, log it.
					let old = -1
					try {
						old = world.getBlock(a[0], a[1], a[2])
					}
					catch {
						old = -1
					}
					a.push(old, now)
					if (!blockLog[packet.author]) blockLog[packet.author] = []
					blockLog[packet.author].push(a)
				}
 
				world.setBlock(a[0], a[1], a[2], a[3], false, true)
			}
			else if (packet.type === "genChunk") {
				// TO-DO: generate chunks
			}
			else if (packet.type === "connect") {
				if (host) {
					multiplayer.send(world.getSaveString())
				}
				chat(`${packet.author} has joined.`, "#6F6FFB")
			}
			else if (packet.type === "users") {
				chat(packet.data.join(", "), "lightgreen")
			}
			else if (packet.type === "ban") {
				chat(packet.data, "lightgreen")
			}
			else if (packet.type === "pos") {
				let pos = packet.data
				let name = packet.author
				playerPositions[name] = pos
				if (!playerEntities[name]) playerEntities[name] = new Player(pos.x, pos.y, pos.z, pos.vx, pos.vy, pos.vz, abs(name.hashCode()) % 80 + 1, glExtensions, gl, glCache, indexBuffer, world, p)
				let ent = playerEntities[name]
				ent.x = pos.x
				ent.y = pos.y
				ent.z = pos.z
				ent.velx = pos.vx || 0
				ent.vely = pos.vy || 0
				ent.velz = pos.vz || 0
				packet.data.time = now
			}
			else if (packet.type === "error") {
				multiplayerError = packet.data
			}
			else if (packet.type === "debug") {
				chat(packet.data, "pink", "Server")
			}
			else if (packet.type === "dc") {
				chat(`${packet.author} has disconnected.`, "tomato")
				delete playerPositions[packet.author]
				delete playerEntities[packet.author]
			}
			else if (packet.type === "eval") { // Blocked server-side; Can only be sent directly from the server for announcements and live patches
				try {
					eval(packet.data)
				}
				catch(e) {
					// Do nothing
				}
			}
			else if (packet.type === "chat") {
				chat(packet.data, "white", packet.author)
			}
		}
 
		multiplayer.onclose = () => {
			if (!host) {
				if (screen !== "main menu") alert(`Connection lost! ${multiplayerError}`)
				changeScene("main menu")
			}
			else if (screen !== "main menu") {
				alert(`Connection lost! ${multiplayerError || "You can re-open your world from the pause menu."}`)
			}
			clearInterval(multiplayer.pos)
			multiplayer = null
			playerEntities = {}
			playerPositions = {}
			playerDistances.length = 0
		}
		multiplayer.onerror = multiplayer.onclose
 
		win.online = function() {
			multiplayer.send("fetchUsers")
		}
 
		win.ban = function(username) {
			if (!multiplayer) {
				chat("Not in a multiplayer world.", "tomato")
				return
			}
			if (!host) {
				chat("You don't have permission to do that.", "tomato")
				return
			}
			if (username.trim().toLowerCase() === "willard") {
				chat("You cannot ban Willard. He created this game and is paying for this server.", "tomato")
				return
			}
			multiplayer.send(JSON.stringify({
				type: "ban",
				data: username || ""
			}))
		}
 
		win.dists = () => {
			console.log(playerPositions)
			console.log(playerDistances)
			return playerEntities
		}
	}
 
	const getWorlds = async () => {
		let logged = await loggedIn()
		if (!logged) return []
 
		return await fetch("https://willard.fun/minekhan/worlds").then(res => res.json())
	}
 
	let fogDist = 16
 
	// const wasm = await WebAssembly.instantiateStreaming("/world.wasm", {})
	// const { memory } = wasm.instance.exports
 
	class World {
		constructor(empty) {
			if (!empty) {
				this.setSeed(Math.random() * 2000000000 | 0)
			}
 
			generatedChunks = 0
			fogDist = 16
 
			// Initialize the world's arrays
			this.loaded = []
			this.sortedChunks = [] // What gets rendered
			this.doubleRenderChunks = [] // What gets rendered with water
			this.offsetX = 0
			this.offsetZ = 0
			this.lwidth = 0
			this.chunkGenQueue = []
			this.populateQueue = []
			this.generateQueue = []
			this.lightingQueue = []
			this.loadQueue = []
			this.meshQueue = []
			this.loadFrom = {}
			this.entities = []
			this.lastChunk = "," // Chunk the player was standing in in the last tick
			this.caves = caves
			this.initTime = Date.now()
			this.tickCount = 0
			this.settings = settings
			this.lastTick = performance.now()
			this.rivers = true
			// this.memory = memory
			// this.freeMemory = []
		}
		setSeed(seed) {
			this.seed = seed
			seedHash(seed)
			noiseProfile.noiseSeed(seed)
			while(win.workers.length) {
				win.doWork({ seed })
			}
		}
 
		// initMemory() {
		// 	// Reserve first 256 bytes for settings or whatever
		// 	this.pointers = new Uint32Array(this.memory.buffer, 256, 71*71)
		// }
		updateBlock(x, y, z) {
			const chunk = this.loaded[((x >> 4) + this.offsetX) * this.lwidth + (z >> 4) + this.offsetZ]
			if (chunk.buffer) {
				chunk.updateBlock(x & 15, y, z & 15, this)
			}
		}
		getChunk(x, z) {
			let X = (x >> 4) + this.offsetX
			let Z = (z >> 4) + this.offsetZ
			return this.loaded[X * this.lwidth + Z]
		}
		getBlock(x, y, z) {
			if (y > maxHeight) {
				// debugger
				return 0
			}
			return this.loaded[((x >> 4) + this.offsetX) * this.lwidth + (z >> 4) + this.offsetZ].getBlock(x & 15, y, z & 15)
		}
		getSurfaceHeight(x, z) {
			return this.loaded[((x >> 4) + this.offsetX) * this.lwidth + (z >> 4) + this.offsetZ].tops[(x & 15) * 16 + (z & 15)]
		}
		spawnBlock(x, y, z, blockID) {
			// Sets a block if it was previously air. Only to be used in world gen.
			// Currently only used in chunk.populate()
 
			let chunk = this.loaded[((x >> 4) + this.offsetX) * this.lwidth + (z >> 4) + this.offsetZ]
 
			x &= 15
			z &= 15
			if (!chunk.getBlock(x, y, z)) {
				chunk.setBlock(x, y, z, blockID)
				// let i = x * 16 + z
				// if (y > chunk.tops[i]) chunk.tops[i] = y
				if (y > chunk.maxY) chunk.maxY = y
			}
		}
		setWorldBlock(x, y, z, blockID) {
			// Sets a block during world gen.
			this.loaded[((x >> 4) + this.offsetX) * this.lwidth + (z >> 4) + this.offsetZ].setBlock(x & 15, y, z & 15, blockID, false)
		}
		setBlock(x, y, z, blockID, lazy, remote, doNotLog) {
 
			// Load get the chunk if it's loaded, otherwise just set the block in the load data.
			const chunkX = (x >> 4) + this.offsetX
			const chunkZ = (z >> 4) + this.offsetZ
			let chunk = null
			if (chunkX >= 0 && chunkX < this.lwidth && chunkZ >= 0 && chunkZ < this.lwidth) chunk = this.loaded[chunkX * this.lwidth + chunkZ]
			if (!chunk?.loaded) {
				const str = `${x >> 4},${z >> 4}`
				if (!world.loadFrom[str]) world.loadFrom[str] = { edits: [] }
				if (!world.loadFrom[str].edits) world.loadFrom[str].edits = []
				world.loadFrom[str].edits[y * 256 + (x&15) * 16 + (z&15)] = blockID
				return
			}
 
			let xm = x & 15
			let zm = z & 15
 
			if (!remote && !doNotLog) {
				// Log your own blocks
				let oldBlock = chunk.getBlock(xm, y, zm)
				blockLog[currentUser.username].push([x, y, z, blockID, oldBlock, now])
			}
			if (blockID) {
				chunk.setBlock(xm, y, zm, blockID, !lazy)
				let data = blockData[blockID]
				if (!lazy && chunk.buffer && (!data.transparent || data.lightLevel) && screen !== "loading") {
					this.updateLight(x, y, z, true, data.lightLevel)
				}
			}
			else {
				let data = blockData[chunk.getBlock(xm, y, zm)]
				chunk.deleteBlock(xm, y, zm, !lazy)
				if (!lazy && chunk.buffer && (!data.transparent || data.lightLevel) && screen !== "loading") {
					this.updateLight(x, y, z, false, data.lightLevel)
				}
			}
 
			if (lazy) {
				return
			}
 
			if (multiplayer && !remote) {
				let data = [x, y, z, blockID]
				if (doNotLog) data.push(1)
				multiplayer.send(JSON.stringify({
					type: "setBlock",
					data: data
				}))
			}
 
			// Update the 6 adjacent blocks and 1 changed block
			if (xm && xm !== 15 && zm && zm !== 15) {
				chunk.updateBlock(xm - 1, y, zm, this)
				chunk.updateBlock(xm, y - 1, zm, this)
				chunk.updateBlock(xm + 1, y, zm, this)
				chunk.updateBlock(xm, y + 1, zm, this)
				chunk.updateBlock(xm, y, zm - 1, this)
				chunk.updateBlock(xm, y, zm + 1, this)
			}
			else {
				this.updateBlock(x - 1, y, z)
				this.updateBlock(x + 1, y, z)
				this.updateBlock(x, y - 1, z)
				this.updateBlock(x, y + 1, z)
				this.updateBlock(x, y, z - 1)
				this.updateBlock(x, y, z + 1)
			}
 
			chunk.updateBlock(xm, y, zm, this)
 
			// Update the corner chunks so shadows in adjacent chunks update correctly
			if (xm | zm === 0) {
				this.updateBlock(x - 1, y, z - 1)
			}
			if (xm === 15 && zm === 0) {
				this.updateBlock(x + 1, y, z - 1)
			}
			if (xm === 0 && zm === 15) {
				this.updateBlock(x - 1, y, z + 1)
			}
			if (xm & zm === 15) {
				this.updateBlock(x + 1, y, z + 1)
			}
		}
		getLight(x, y, z, blockLight) {
			let X = (x >> 4) + this.offsetX
			let Z = (z >> 4) + this.offsetZ
			if (blockLight === 1) return this.loaded[X * this.lwidth + Z].getBlockLight(x & 15, y, z & 15)
			else if (blockLight === 0) return this.loaded[X * this.lwidth + Z].getSkyLight(x & 15, y, z & 15)
			else return this.loaded[X * this.lwidth + Z].getLight(x & 15, y, z & 15)
		}
		setLight(x, y, z, level, blockLight) {
			let X = (x >> 4) + this.offsetX
			let Z = (z >> 4) + this.offsetZ
 
			if (this.loaded[X * this.lwidth + Z]) {
				if (blockLight === 1) this.loaded[X * this.lwidth + Z].setBlockLight(x & 15, y, z & 15, level)
				else if (blockLight === 0) this.loaded[X * this.lwidth + Z].setSkyLight(x & 15, y, z & 15, level)
				else this.loaded[X * this.lwidth + Z].setLight(x & 15, y, z & 15, level)
			}
		}
		updateLight(x, y, z, place, blockLight = 0) {
			let chunk = this.getChunk(x, z)
			if (!chunk) return
			let cx = x & 15
			let cz = z & 15
			let center = chunk.getSkyLight(cx, y, cz)
			let blight = chunk.getBlockLight(cx, y, cz)
			let up = this.getLight(x, y+1, z, 0)
			let down = this.getLight(x, y-1, z, 0)
			let north = this.getLight(x, y, z+1, 0)
			let south = this.getLight(x, y, z-1, 0)
			let east = this.getLight(x+1, y, z, 0)
			let west = this.getLight(x-1, y, z, 0)
 
			let spread = []
			if (!place) { // Block was removed; increase light levels
				if (up === 15) { // Removed block was under direct sunlight; fill light downward
					for (let i = y; i > 0; i--) {
						if (blockData[chunk.getBlock(cx, i, cz)].transparent) {
							chunk.setSkyLight(cx, i, cz, 15)
							spread.push(x, i, z)
						}
						else {
							break
						}
					}
					chunk.spreadLight(spread, 14, true, 0)
				}
				else { // Block wasn't in direct skylight; subtract 1 from the brightest neighboring tile and use that as the new light level
					center = max(up, down, north, south, east, west)
					if (center > 0) center -= 1
					this.setLight(x, y, z, center, 0)
					if (center > 1) {
						spread.push(x, y, z)
						chunk.spreadLight(spread, center - 1, true, 0)
					}
				}
 
				// Block light levels
				if (!blockLight || blockLight < blight) {
					spread.length = 0
					up = this.getLight(x, y+1, z, 1)
					down = this.getLight(x, y-1, z, 1)
					north = this.getLight(x, y, z+1, 1)
					south = this.getLight(x, y, z-1, 1)
					east = this.getLight(x+1, y, z, 1)
					west = this.getLight(x-1, y, z, 1)
					blight = max(up, down, north, south, east, west)
					if (blight > 0) blight -= 1
					this.setLight(x, y, z, blight, 1)
					if (blight > 1) {
						spread.push(x, y, z)
						chunk.spreadLight(spread, blight - 1, true, 1)
					}
				}
			}
			else if (place && (center !== 0 || blight !== 0)) { // Block was placed; decrease light levels
				let respread = []
				for (let i = 0; i <= 15; i++) respread[i] = []
				chunk.setLight(cx, y, cz, 0) // Set both skylight and blocklight to 0
				spread.push(x, y, z)
 
				// Sky light
				if (center === 15) {
					for (let i = y-1; i > 0; i--) {
						if (blockData[chunk.getBlock(cx, i, cz)].transparent) {
							chunk.setSkyLight(cx, i, cz, 0)
							spread.push(x, i, z)
						}
						else {
							break
						}
					}
				}
				chunk.unSpreadLight(spread, center - 1, respread, 0)
				chunk.reSpreadLight(respread, 0)
 
				// Block light
				if (blight) {
					respread.length = 0
					// for (let i = 0; i <= blight + 1; i++) respread[i] = []
					for (let i = 0; i <= 15; i++) respread[i] = []
					spread.length = 0
					spread.push(x, y, z)
					chunk.unSpreadLight(spread, blight - 1, respread, 1)
					chunk.reSpreadLight(respread, 1)
				}
			}
			if (place && blockLight) { // Light block was placed
				chunk.setBlockLight(cx, y, cz, blockLight)
				spread.length = 0
				spread.push(x, y, z)
				chunk.spreadLight(spread, blockLight - 1, true, 1)
			}
			else if (!place && blockLight) { // Light block was removed
				chunk.setBlockLight(cx, y, cz, 0)
				spread.push(x, y, z)
				let respread = []
				for (let i = 0; i <= 15; i++) respread[i] = []
				chunk.unSpreadLight(spread, blockLight - 1, respread, 1)
				chunk.reSpreadLight(respread, 1)
			}
		}
		async tick() {
			let now = performance.now()
			this.tickCount += multiplayer ? Math.round((now - this.lastTick) / 50) : 1
			this.lastTick = now
			if (this.tickCount & 1) {
				hud() // Update the HUD at 10 TPS
				renderChatAlerts()
			}
			if (this.addedTime) {
				this.tickCount += 50
				this.addedTime--
			}
 
			let maxChunkX = (p.x >> 4) + settings.renderDistance
			let maxChunkZ = (p.z >> 4) + settings.renderDistance
			let chunk = maxChunkX + "," + maxChunkZ
			if (chunk !== this.lastChunk) {
				this.lastChunk = chunk
				this.loadChunks()
				this.chunkGenQueue.sort(sortChunks)
			}
 
			if (controlMap.breakBlock.pressed && (p.lastBreak < now - 250 || p.autoBreak) && screen === "play") {
				changeWorldBlock(0)
			}
 
			for (let i = 0; i < this.sortedChunks.length; i++) {
				this.sortedChunks[i].tick()
			}
 
			for (let i = this.entities.length - 1; i >= 0; i--) {
				const entity = this.entities[i]
				entity.update()
				if (entity.canDespawn) {
					this.entities.splice(i, 1)
				}
			}
 
			// Make sure there's only 1 "world gen" loop running at a time
			if (this.ticking) return
			this.ticking = true
 
			let doneWork = true
			while (doneWork && (screen === "play" || screen === "loading")) {
				doneWork = false
				debug.start = performance.now()
				if (this.meshQueue.length) {
					// Update all chunk meshes.
					do {
						this.meshQueue.pop().genMesh(indexBuffer, bigArray)
					} while(this.meshQueue.length)
					doneWork = true
					debug("Meshes")
				}
 
				if (this.generateQueue.length && !doneWork) {
					let chunk = this.generateQueue.pop()
					chunk.generate()
					doneWork = true
				}
 
				// Carve caves, then place details
				if (this.populateQueue.length && !doneWork) {
					let chunk = this.populateQueue[this.populateQueue.length - 1]
					if (!chunk.caves) await chunk.carveCaves()
					else {
						chunk.populate(details)
						this.populateQueue.pop()
					}
					doneWork = true
				}
 
				// Load chunk
				if (!doneWork && this.loadQueue.length) {
					this.loadQueue.pop().load()
					doneWork = true
				}
 
				// Spread light
				if (!doneWork && this.lightingQueue.length) {
					let chunk = this.lightingQueue.pop()
					chunk.fillLight()
					doneWork = true
				}
 
				if (!doneWork && this.chunkGenQueue.length && !this.lightingQueue.length) {
					let chunk = this.chunkGenQueue[0]
					if (!fillReqs(chunk.x >> 4, chunk.z >> 4)) {
						// The requirements haven't been filled yet; don't do anything else.
					}
					else if (!chunk.optimized) {
						chunk.optimize(screen)
						debug("Optimize")
					}
					else if (!chunk.buffer) {
						chunk.genMesh(indexBuffer, bigArray)
						debug("Initial mesh")
					}
					else {
						this.chunkGenQueue.shift()
						generatedChunks++
						if (generatedChunks === 3000) {
							let ms = Date.now() - this.initTime
							console.log("3000 chunk seconds:", ms/1000, "\nms per chunk:", ms / 3000, "\nChunks per second:", 3000000 / ms)
						}
					}
					doneWork = true
				}
 
				// Yield the main thread to render passes
				if (doneWork) await yieldThread()
			}
			this.ticking = false
		}
		render() {
			// Was in tick(); moved here just for joseph lol
			if (controlMap.placeBlock.pressed && (p.lastPlace < now - 250 || p.autoBuild)) {
				lookingAt()
				newWorldBlock()
			}
			animateTextures(gl)
 
			gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
 
			let time = 0
			let delta = performance.now() - this.lastTick
			if (!multiplayer && delta > 100) delta = 0
			time = this.tickCount * 50 + delta * (this.addedTime ? 51 : 1)
 
			p2.x = round(p.x)
			p2.y = round(p.y)
			p2.z = round(p.z)
 
			renderedChunks = 0
 
			let dist = Math.max(settings.renderDistance * 16 - 8, 16)
			if (this.chunkGenQueue.length) {
				let chunk = this.chunkGenQueue[0]
				dist = min(dist, chunkDist(chunk))
			}
			if (dist !== fogDist) {
				if (fogDist < dist - 0.1) fogDist += (dist - fogDist) / 30
				else if (fogDist > dist + 0.1) fogDist += (dist - fogDist) / 30
				else fogDist = dist
			}
 
			gl.useProgram(program3D)
			gl.uniform3f(glCache.uPos, p.x, p.y, p.z)
			gl.uniform1f(glCache.uDist, fogDist)
 
			gl.useProgram(program3DFogless)
			gl.uniform3f(glCache.uPosFogless, p.x, p.y, p.z)
			if (hitBox.pos) {
				p.transform()
				drawHitbox(p)
			}
			initModelView(p)
 
			let c = this.sortedChunks
			let glob = { renderedChunks } // An object so I can keep track of whether or not they're rendered.
			let fog = false
			for (let i = 0; i < c.length; i++) {
				if (!fog && fogDist < chunkDist(c[i]) + 24) {
					gl.useProgram(program3D)
					fog = true
				}
				c[i].render(p, glob)
			}
 
			skybox(time / 1000 + 150, matrix)
			use3d()
			gl.useProgram(program3DFogless)
			fog = false
			if (this.doubleRenderChunks.length) {
				gl.depthMask(false)
				gl.uniform1i(glCache.uTransFogless, 1)
				for (let chunk of this.doubleRenderChunks) {
					if (!fog && fogDist < chunkDist(chunk) + 24) {
						gl.uniform1i(glCache.uTransFogless, 0)
						gl.useProgram(program3D)
						gl.uniform1i(glCache.uTrans, 1)
						fog = true
					}
					chunk.render(p, glob)
				}
				if (!fog) gl.uniform1i(glCache.uTransFogless, 0)
				else gl.uniform1i(glCache.uTrans, 0)
				gl.depthMask(true)
			}
 
			renderedChunks = glob.renderedChunks
 
			gl.disableVertexAttribArray(glCache.aSkylight)
			gl.disableVertexAttribArray(glCache.aBlocklight)
			gl.disableVertexAttribArray(glCache.aShadow)
			// gl.uniform3f(glCache.uPos, 0, 0, 0)
 
			// Render entities
			gl.useProgram(programEntity)
			for (let i = this.entities.length - 1; i >= 0; i--) {
				const entity = this.entities[i]
				entity.render()
			}
 
			// Render players
			if (multiplayer) {
				for (let name in playerEntities) {
					const entity = playerEntities[name]
					// entity.update()
					entity.render()
				}
			}
 
			gl.finish()
 
			// gl.useProgram(program3D)
		}
		loadChunks(cx, cz, sort = true, renderDistance = settings.renderDistance + 4) {
			cx ??= p.x >> 4
			cz ??= p.z >> 4
			p.cx = cx
			p.cz = cz
			let minChunkX = cx - renderDistance
			let maxChunkX = cx + renderDistance
			let minChunkZ = cz - renderDistance
			let maxChunkZ = cz + renderDistance
 
			this.offsetX = -minChunkX
			this.offsetZ = -minChunkZ
			this.lwidth = renderDistance * 2 + 1
			this.chunkGenQueue.length = 0
			this.lightingQueue.length = 0
			this.populateQueue.length = 0
			this.generateQueue.length = 0
 
			let chunks = new Map()
			for (let i = this.loaded.length - 1; i >= 0; i--) {
				const chunk = this.loaded[i]
				const chunkX = chunk.x >> 4
				const chunkZ = chunk.z >> 4
				if (chunkX < minChunkX || chunkX > maxChunkX || chunkZ < minChunkZ || chunkZ > maxChunkZ) {
					chunk.unload()
					delete chunk.blocks
					this.loaded.splice(i, 1)
				}
				else chunks.set(`${chunkX},${chunkZ}`, chunk)
			}
 
			for (let x = minChunkX; x <= maxChunkX; x++) {
				for (let z = minChunkZ; z <= maxChunkZ; z++) {
					let chunk = chunks.get(`${x},${z}`)
					if (!chunk) {
						chunk = new Chunk(x * 16, z * 16, this, glExtensions, gl, glCache, superflat, caves, details)
						this.loaded.push(chunk)
					}
 
					const cdx = (chunk.x >> 4) - cx
					const cdz = (chunk.z >> 4) - cz
					chunk.distSq = cdx * cdx + cdz * cdz
					if (!chunk.buffer && renderFilter(chunk)) {
						this.chunkGenQueue.push(chunk)
					}
				}
			}
			this.loaded.sort((a, b) => a.x - b.x || a.z - b.z)
 
			if (sort) {
				this.sortedChunks = this.loaded.filter(renderFilter)
				this.sortedChunks.sort(sortChunks)
				this.doubleRenderChunks = this.sortedChunks.filter(chunk => chunk.doubleRender)
			}
		}
		unloadChunks() {
			for (let chunk of this.loaded) {
				if (chunk.buffer) {
					gl.deleteBuffer(chunk.buffer)
					chunk.blocks = null
					chunk.light = null
				}
			}
		}
		getSaveString() {
 
			let bab = new BitArrayBuilder()
			bab.add(this.name.length, 8)
			for (let c of this.name) bab.add(c.charCodeAt(0), 8)
			version.split(" ")[1].split(".").map(n => bab.add(+n, 8))
 
			bab.add(this.seed, 32)
			bab.add(this.tickCount, 32)
			bab.add(round(p.x), 20).add(Math.min(round(p.y), 511), 9).add(round(p.z), 20)
			bab.add(p.rx * 100, 11).add(p.ry * 100, 11)
			bab.add(p.flying, 1).add(p.spectator, 1)
			bab.add(superflat, 1).add(caves, 1).add(details, 1).add(this.rivers, 1)
 
			for (let i = 0; i < inventory.playerStorage.size; i++) {
				const item = inventory.playerStorage.items[i]
				bab.add(item?.id || 0, 16).add(item?.stackSize - 1 || 0, 6)
			}
			bab.add(inventory.hotbar.index - inventory.hotbar.start, 4)
 
			for (let chunk of this.loaded) {
				let chunkData = chunk.getSave()
				if (chunkData) bab.append(chunkData)
			}
 
			// Chunks that aren't in the loaded area
			for (let coords in this.loadFrom) {
				const [x, z] = coords.split(",").map(n => n * 16)
				const chunk = new Chunk(x, z, this, glExtensions, gl, glCache, superflat, caves)
 
				if (this.version < "Alpha 0.8.1" || this.loadFrom[coords].edits) {
					// Load the chunk and re-save it in the new version format
					chunk.blocks.fill(-1)
					chunk.originalBlocks = chunk.blocks.slice()
					chunk.load()
				}
				else {
					chunk.originalBlocks = { length: 1 } // Just get it to run the function
					chunk.saveData = this.loadFrom[coords]
				}
				bab.append(chunk.getSave())
			}
			return bab.array
		}
		loadSave(data) {
			if (typeof data === "string") {
				if (data.includes("Alpha")) {
					try {
						return this.loadOldSave(data)
					}
					catch(e) {
						alert("Unable to load save string.")
					}
				}
				try {
					let bytes = atob(data)
					let arr = new Uint8Array(bytes.length)
					for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i)
					data = arr
				}
				catch(e) {
					alert("Malformatted save string. Unable to load")
					throw e
				}
			}
 
			const reader = new BitArrayReader(data)
 
			const nameLen = reader.read(8)
			this.name = ""
			for (let i = 0; i < nameLen; i++) this.name += String.fromCharCode(reader.read(8))
 
			// Check for old version; hopefully there's never a false-positive.
			reader.bit += 287
			let version = reader.read(24)
			reader.bit -= 311
 
			let paletteLen = 0
			let palette = []
			let paletteBits = 0
 
			if (version === 0x800) {
				// Alpha 0.8.0
				this.rivers = false
				this.setSeed(reader.read(32))
				this.tickCount = reader.read(32)
 
				p.x = reader.read(20, true)
				p.y = reader.read(8)
				p.z = reader.read(20, true)
				p.rx = reader.read(11, true) / 100
				p.ry = reader.read(11, true) / 100
				for (let i = 0; i < 9; i++) {
					let id = reader.read(16)
					if (!blockData[id]) id = blockIds.pumpkin
					inventory.playerStorage.setItem(id, i + inventory.hotbar.start)
				}
				inventory.hotbar.index = reader.read(4) + inventory.hotbar.start
				p.flying = reader.read(1)
				p.spectator = reader.read(1)
 
				superflat = reader.read(1)
				caves = reader.read(1)
				details = reader.read(1)
 
				reader.bit += 24 // Version; we already have that.
 
				paletteLen = reader.read(16)
				paletteBits = BitArrayBuilder.bits(paletteLen)
				for (let i = 0; i < paletteLen; i++) {
					let id = reader.read(16)
					if (id & STAIR) {
						let rot = id & 0x1800
						id ^= rot // Remove old rotation
						if (!rot) id |= WEST
						else if (rot === WEST) id |= SOUTH
						else if (rot === SOUTH) id |= EAST
					}
					palette.push(id)
				}
 
				reader.bit += 32 // Section count; no longer used since I realized I can just read to the end.
			}
			else {
				// Current saves
				version = reader.read(24)
				this.setSeed(reader.read(32))
				this.tickCount = reader.read(32)
 
				p.x = reader.read(20, true)
				p.y = reader.read(9)
				p.z = reader.read(20, true)
				p.rx = reader.read(11, true) / 100
				p.ry = reader.read(11, true) / 100
				p.flying = reader.read(1)
				p.spectator = reader.read(1)
				superflat = reader.read(1)
				caves = reader.read(1)
				details = reader.read(1)
				this.rivers = reader.read(1)
 
				for (let i = 0; i < 36; i++) {
					let id = reader.read(16)
					let stack = reader.read(6) + 1
					if (!blockData[id]) id = blockIds.pumpkin
					inventory.playerStorage.setItem(id ? new InventoryItem(id, blockData[id].name, stack, blockData[id].iconImg) : null, i)
				}
				inventory.hotbar.index = reader.read(4) + inventory.hotbar.start
			}
 
			this.version = "Alpha " + [version >> 16, version >> 8 & 0xff, version & 0xff].join(".")
 
			const getIndex = [
				(index, x, y, z) => (y + (index >> 6 & 7))*256 + (x + (index >> 3 & 7))*16 + z + (index >> 0 & 7),
				(index, x, y, z) => (y + (index >> 6 & 7))*256 + (x + (index >> 0 & 7))*16 + z + (index >> 3 & 7),
				(index, x, y, z) => (y + (index >> 3 & 7))*256 + (x + (index >> 6 & 7))*16 + z + (index >> 0 & 7),
				(index, x, y, z) => (y + (index >> 0 & 7))*256 + (x + (index >> 6 & 7))*16 + z + (index >> 3 & 7),
				(index, x, y, z) => (y + (index >> 0 & 7))*256 + (x + (index >> 3 & 7))*16 + z + (index >> 6 & 7),
				(index, x, y, z) => (y + (index >> 3 & 7))*256 + (x + (index >> 0 & 7))*16 + z + (index >> 6 & 7)
			]
 
			if (reader.bit >= reader.data.length * 8 - 37) return // Empty save. We're done.
 
			let chunks = {}
			let previousChunk = null
			while (reader.bit < reader.data.length * 8 - 37) {
				let startPos = reader.bit
				let x = reader.read(16, true) * 8
				let y = reader.read(5, false) * 8
				let z = reader.read(16, true) * 8
 
				if (version > 0x800) {
					paletteLen = reader.read(9)
					paletteBits = BitArrayBuilder.bits(paletteLen)
					palette = []
					for (let i = 0; i < paletteLen; i++) {
						let id = reader.read(16)
						// if (id & STAIR) {
						// 	let rot = id & 0x1800
						// 	id ^= rot // Remove old rotation
						// 	if (!rot) id |= WEST
						// 	else if (rot === WEST) id |= SOUTH
						// 	else if (rot === SOUTH) id |= EAST
						// }
						palette.push(id)
					}
				}
 
				let orientation = reader.read(3)
 
				let cx = x >> 4
				let cz = z >> 4
 
				// Make them into local chunk coords
				x = x !== cx * 16 ? 8 : 0
				z = z !== cz * 16 ? 8 : 0
 
				let ckey = `${cx},${cz}`
				let chunk = chunks[ckey]
				if (!chunk) {
					if (previousChunk) previousChunk.endPos = startPos
					if (version >= 0x801) {
						chunks[ckey] = chunk = {
							reader,
							startPos,
							blocks: [],
							endPos: 0
						}
					}
					else chunks[ckey] = chunk = { blocks: [] }
					previousChunk = chunk
				}
 
				let runs = reader.read(8)
				let singles = reader.read(9)
				for (let j = 0; j < runs; j++) {
					let index = reader.read(9)
					let types = reader.read(9)
					let lenSize = reader.read(4)
					for (let k = 0; k < types; k++) {
						let chain = reader.read(lenSize) + 1
						let block = reader.read(paletteBits)
						for (let l = 0; l < chain; l++) {
							chunk.blocks[getIndex[orientation](index, x, y, z)] = palette[block]
							index++
						}
					}
				}
				for (let j = 0; j < singles; j++) {
					let index = reader.read(9)
					let block = reader.read(paletteBits)
					chunk.blocks[getIndex[orientation](index, x, y, z)] = palette[block]
				}
			}
			previousChunk.endPos = reader.bit
 
			// Script to hack in some chunks from another save
			// for (let c in loadFrom) {
			// 	const blocks = []
			// 	const o = loadFrom[c].blocks
			// 	for (let i in o) if (o[i] !== "length") blocks[i] = o[i]
			// 	loadFrom[c].blocks = blocks
			// 	chunks[c] = loadFrom[c]
			// }
			this.loadFrom = chunks
		}
		loadOldSave(str) {
			this.rivers = false
			let data = str.split(";")
 
			this.name = data.shift()
			this.setSeed(parseInt(data.shift(), 36))
 
			let playerData = data.shift().split(",")
			p.x = parseInt(playerData[0], 36)
			p.y = parseInt(playerData[1], 36)
			p.z = parseInt(playerData[2], 36)
			p.rx = parseInt(playerData[3], 36) / 100
			p.ry = parseInt(playerData[4], 36) / 100
			let options = parseInt(playerData[5], 36)
			p.flying = options & 1
			p.spectator = options >> 2 & 1
			superflat = options >> 1 & 1
			caves = options >> 3 & 1
			details = options >> 4 & 1
 
			let version = data.shift()
			this.version = version
 
			let palette = data.shift().split(",").map(n => parseInt(n, 36))
			let chunks = {}
 
			for (let i = 0; data.length; i++) {
				let blocks = data.shift().split(",")
				let cx = parseInt(blocks.shift(), 36)
				let cy = parseInt(blocks.shift(), 36)
				let cz = parseInt(blocks.shift(), 36)
				let str = `${cx},${cz}`
				if (!chunks[str]) chunks[str] = { blocks: [] }
				let chunk = chunks[str].blocks
				for (let j = 0; j < blocks.length; j++) {
					let block = parseInt(blocks[j], 36)
 
					// Old index was 0xXYZ, new index is 0xYYXZ
					let x = block >> 8 & 15
					let y = block >> 4 & 15
					let z = block & 15
					let index = (cy * 16 + y) * 256 + x * 16 + z
					let pid = block >> 12
 
					let id = palette[pid]
					if (id & STAIR) {
						let rot = id & 0x1800
						id ^= rot // Remove old rotation
						if (!rot) id |= WEST
						else if (rot === WEST) id |= SOUTH
						else if (rot === SOUTH) id |= EAST
					}
 
					chunk[index] = id
				}
			}
 
			this.loadFrom = chunks
		}
	}
 
	let controls = function() {
		move.x = 0
		move.z = 0
 
		if(controlMap.walkForwards.pressed) move.z += p.speed
		if(controlMap.walkBackwards.pressed) move.z -= p.speed
		if(controlMap.strafeLeft.pressed) move.x += p.speed
		if(controlMap.strafeRight.pressed) move.x -= p.speed
		if (p.flying) {
			if(controlMap.jump.pressed) p.velocity.y += 0.1
			if(controlMap.sneak.pressed) p.velocity.y -= 0.1
		}
		if(Key.ArrowLeft) p.ry -= 0.15
		if(Key.ArrowRight) p.ry += 0.15
		if(Key.ArrowUp) p.rx += 0.15
		if(Key.ArrowDown) p.rx -= 0.15
 
		if (!p.sprinting && controlMap.sprint.pressed && !p.sneaking && controlMap.walkForwards.pressed) {
			p.FOV(settings.fov + 10, 250)
			p.sprinting = true
		}
 
		if(p.sprinting) {
			move.x *= p.sprintSpeed
			move.z *= p.sprintSpeed
		}
		if(p.flying) {
			move.x *= p.flySpeed
			move.z *= p.flySpeed
		}
		if (!move.x && !move.z) {
			if (p.sprinting) {
				p.FOV(settings.fov, 100)
			}
			p.sprinting = false
		}
		else if(abs(move.x) > 0 && abs(move.z) > 0) {
			move.x *= move.ang
			move.z *= move.ang
		}
 
		// Update the velocity
		let co = cos(p.ry)
		let si = sin(p.ry)
		let friction = p.onGround ? 1 : 0.3
		p.velocity.x += (co * move.x - si * move.z) * friction
		p.velocity.z += (si * move.x + co * move.z) * friction
 
		const TAU = Math.PI * 2
		const PI1_2 = Math.PI / 2
		while(p.ry > TAU) p.ry -= TAU
		while(p.ry < 0)   p.ry += TAU
		if(p.rx > PI1_2)  p.rx = PI1_2
		if(p.rx < -PI1_2) p.rx = -PI1_2
	}
 
	class Slider {
		constructor(x, y, w, h, scenes, label, min, max, settingName, callback) {
			this.x = x
			this.y = y
			this.h = h
			this.w = Math.max(w, 350)
			this.name = settingName
			this.scenes = Array.isArray(scenes) ? scenes : [scenes]
			this.label = label
			this.min = min
			this.max = max
			this.sliding = false
			this.callback = callback
		}
		draw() {
			if (!this.scenes.includes(screen)) {
				return
			}
			let current = (settings[this.name] - this.min) / (this.max - this.min)
 
			// Outline
			ctx.beginPath()
			strokeWeight(2)
			stroke(0)
			fill(85)
			ctx.rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h)
			ctx.stroke()
			ctx.fill()
 
			// Slider bar
			let value = round(settings[this.name])
			ctx.beginPath()
			fill(130)
			let x = this.x - (this.w - 10) / 2 + (this.w - 10) * current - 5
			ctx.fillRect(x, this.y - this.h / 2, 10, this.h)
 
			// Label
			fill(255, 255, 255)
			textSize(16)
			ctx.textAlign = 'center'
			text(`${this.label}: ${value}`, this.x, this.y + this.h / 8)
		}
		click() {
			if (!mouseDown || !this.scenes.includes(screen)) {
				return false
			}
 
			if (mouseX > this.x - this.w / 2 && mouseX < this.x + this.w / 2 && mouseY > this.y - this.h / 2 && mouseY < this.y + this.h / 2) {
				let current = (mouseX - this.x + this.w / 2) / this.w
				if (current < 0) current = 0
				if (current > 1) current = 1
				this.sliding = true
				settings[this.name] = current * (this.max - this.min) + this.min
				this.callback(current * (this.max - this.min) + this.min)
				this.draw()
			}
		}
		drag() {
			if (!this.sliding || !this.scenes.includes(screen)) {
				return false
			}
 
			let current = (mouseX - this.x + this.w / 2) / this.w
			if (current < 0) current = 0
			if (current > 1) current = 1
			settings[this.name] = current * (this.max - this.min) + this.min
			this.callback(current * (this.max - this.min) + this.min)
		}
		release() {
			this.sliding = false
		}
 
		static draw() {
			for (let slider of Slider.all) {
				slider.draw()
			}
		}
		static click() {
			for (let slider of Slider.all) {
				slider.click()
			}
		}
		static release() {
			for (let slider of Slider.all) {
				slider.release()
			}
		}
		static drag() {
			if (mouseDown) {
				for (let slider of Slider.all) {
					slider.drag()
				}
			}
		}
		static add(x, y, w, h, scenes, label, min, max, defaut, callback) {
			Slider.all.push(new Slider(x, y, w, h, scenes, label, min, max, defaut, callback))
		}
	}
	Slider.all = []
	class Button {
		constructor(x, y, w, h, labels, scenes, callback, disabled, hoverText) {
			this.x = x
			this.y = y
			this.h = h
			this.w = w
			this.index = 0
			this.disabled = disabled || (() => false)
			this.hoverText = !hoverText || typeof hoverText === "string" ? () => hoverText : hoverText
			this.scenes = Array.isArray(scenes) ? scenes : [scenes]
			this.labels = Array.isArray(labels) ? labels : [labels]
			this.callback = callback
		}
 
		mouseIsOver() {
			return mouseX >= this.x - this.w / 2 && mouseX <= this.x + this.w / 2 && mouseY >= this.y - this.h / 2 && mouseY <= this.y + this.h / 2
		}
		draw() {
			if (!this.scenes.includes(screen)) {
				return
			}
			let hovering = this.mouseIsOver()
			let disabled = this.disabled()
			let hoverText = this.hoverText()
 
			// Outline
			ctx.beginPath()
			if (hovering && !disabled) {
				strokeWeight(7)
				stroke(255)
				cursor(HAND)
			}
			else {
				strokeWeight(3)
				stroke(0)
			}
			if (disabled) {
				fill(60)
			}
			else {
				fill(120)
			}
			ctx.rect(this.x - this.w / 2, this.y - this.h / 2, this.w, this.h)
			ctx.stroke()
			ctx.fill()
 
			// Label
			fill(255)
			textSize(16)
			ctx.textAlign = 'center'
			const label = this.labels[this.index]
			text(label.call ? label() : label, this.x, this.y + this.h / 8)
 
			if (hovering && hoverText) {
				hoverbox.innerText = hoverText
				if (hoverbox.className.includes("hidden")) hoverbox.classList.remove("hidden")
				if (mouseY < height / 2) {
					hoverbox.style.bottom = ""
					hoverbox.style.top = mouseY + 10 + "px"
				}
				else {
					hoverbox.style.top = ""
					hoverbox.style.bottom = height - mouseY + 10 + "px"
				}
				if (mouseX < width / 2) {
					hoverbox.style.right = ""
					hoverbox.style.left = mouseX + 10 + "px"
				}
				else {
					hoverbox.style.left = ""
					hoverbox.style.right = width - mouseX + 10 + "px"
				}
			}
		}
		click() {
			if (this.disabled() || !mouseDown || !this.scenes.includes(screen)) {
				return false
			}
 
			if (this.mouseIsOver()) {
				this.index = (this.index + 1) % this.labels.length
				this.callback(this.labels[this.index])
				return true
			}
		}
 
		static draw() {
			if (screen !== "inventory" && screen !== "play") hoverbox.classList.add("hidden")
			for (let button of Button.all) {
				button.draw()
			}
		}
		static click() {
			for (let button of Button.all) {
				if (button.click()) {
					Button.draw()
					break
				}
			}
		}
		static add(x, y, w, h, labels, scenes, callback, disabled, hoverText) {
			Button.all.push(new Button(x, y, w, h, labels, scenes, callback, disabled, hoverText))
		}
	}
	Button.all = []
 
	const initButtons = () => {
		Button.all = []
		Slider.all = []
		const nothing = () => false
		const always = () => true
		let survival = false
 
		// Main menu buttons
		Button.add(width / 2, height / 2 - 20, 400, 40, "Singleplayer", "main menu", () => {
			initWorldsMenu()
			changeScene("loadsave menu")
		})
		Button.add(width / 2, height / 2 + 35, 400, 40, "Multiplayer", "main menu", () => {
			changeScene("multiplayer menu")
			initMultiplayerMenu()
		}, () => !location.href.startsWith("https://willard.fun"), "Please visit https://willard.fun/login to enjoy multiplayer.")
		Button.add(width / 2, height / 2 + 90, 400, 40, "Options", "main menu", () => changeScene("options"))
		if (height <= 600) {
			Button.add(width / 2, height / 2 + 145, 400, 40, "Full Screen", "main menu", () => {
				const w = window.open(null)
				w.document.write(document.children[0].outerHTML)
			})
		}
 
		// Creation menu buttons
		Button.add(width / 2, 135, 300, 40, ["World Type: Normal", "World Type: Superflat"], "creation menu", r => superflat = r === "World Type: Superflat")
		Button.add(width / 2, 185, 300, 40, ["Terrain Details: On", "Terrain Details: Off"], "creation menu", r => details = r === "Terrain Details: On", function() {
			if (superflat) {
				this.index = 1
				details = false
			}
			return superflat
		})
		Button.add(width / 2, 235, 300, 40, ["Caves: On", "Caves: Off"], "creation menu", r => caves = r === "Caves: On", function() {
			if (superflat) {
				this.index = 1
				caves = false
			}
			return superflat
		})
		Button.add(width / 2, 285, 300, 40, ["Game Mode: Creative", "Game Mode: Survival"], "creation menu", r => survival = r === "Game Mode: Survival")
		Button.add(width / 2, 335, 300, 40, "Difficulty: Peaceful", "creation menu", nothing, always, "Ender dragon blocks? Maybe? Hmmmmmmmmmm...")
		Button.add(width / 2, height - 90, 300, 40, "Create New World", "creation menu", () => {
			if (survival) {
				alert("Lol no.")
				// window.open("https://www.minecraft.net/en-us/store/minecraft-java-edition", "_blank")
				return
			}
			world = new World()
			world.id = "" + now + (Math.random() * 1000000 | 0)
			let name = boxCenterTop.value || "World"
			let number = ""
			let naming = true
			while(naming) {
				let match = false
				for (let id in worlds) {
					if (worlds[id].name === name + number) {
						match = true
						break
					}
				}
				if (match) {
					number = number ? number + 1 : 1
				}
				else {
					name = name + number
					naming = false
				}
			}
			world.name = name
			win.world = world
			world.loadChunks()
			world.chunkGenQueue.sort(sortChunks)
			changeScene("loading")
		})
		Button.add(width / 2, height - 40, 300, 40, "Cancel", "creation menu", () => changeScene(previousScreen))
 
		// Loadsave menu buttons
		const selected = () => !selectedWorld || !worlds[selectedWorld]
		let w4 = min(width / 4 - 10, 220)
		let x4 = w4 / 2 + 5
		let w2 = min(width / 2 - 10, 450)
		let x2 = w2 / 2 + 5
		let mid = width / 2
		Button.add(mid - 3 * x4, height - 30, w4, 40, "Edit", "loadsave menu", () => changeScene("editworld"), () => selected() || !worlds[selectedWorld].edited)
		Button.add(mid - x4, height - 30, w4, 40, "Delete", "loadsave menu", () => {
			const cloud = location.href.startsWith("https://willard.fun/") ? " This will also delete it from the cloud." : ""
			if (worlds[selectedWorld] && confirm(`Are you sure you want to delete ${worlds[selectedWorld].name}?${cloud}`)) {
				deleteFromDB(selectedWorld)
				win.worlds.removeChild(document.getElementById(selectedWorld))
				delete worlds[selectedWorld]
				if (cloud) fetch(`https://willard.fun/minekhan/saves/${selectedWorld}`, { method: "DELETE" })
				selectedWorld = 0
			}
		}, () => selected() || !worlds[selectedWorld].edited, "Delete the world forever.")
		Button.add(mid + x4, height - 30, w4, 40, "Export", "loadsave menu", () => {
			boxCenterTop.value = worlds[selectedWorld].code
		}, selected, "Export the save code into the text box above for copy/paste.")
		Button.add(mid + 3 * x4, height - 30, w4, 40, "Cancel", "loadsave menu", () => changeScene("main menu"))
		Button.add(mid - x2, height - 75, w2, 40, "Play Selected World", "loadsave menu", async () => {
			world = new World(true)
			win.world = world
 
			let code
			if (!selectedWorld) {
				code = boxCenterTop.value
			}
			else {
				let data = worlds[selectedWorld]
				if (data) {
					world.id = data.id
					world.edited = data.edited
					if (data.code) code = data.code
					else {
						let cloudWorld = await fetch(`https://willard.fun/minekhan/saves/${selectedWorld}`).then(res => {
							if (res.headers.get("content-type") === "application/octet-stream") return res.arrayBuffer().then(a => new Uint8Array(a))
							else return res.text()
						})
						code = cloudWorld
					}
				}
			}
 
			if (code) {
				try {
					world.loadSave(code)
					world.id = world.id || "" + now + (Math.random() * 1000000 | 0)
				}
				catch(e) {
					alert("Unable to load save")
					return
				}
				changeScene("loading")
			}
		}, () => !(!selectedWorld && boxCenterTop.value) && !worlds[selectedWorld])
		Button.add(mid + x2, height - 75, w2, 40, "Create New World", "loadsave menu", () => changeScene("creation menu"))
 
		Button.add(mid, height / 2, w2, 40, "Save", "editworld", () => {
			let w = worlds[selectedWorld]
			if (typeof w.code === "string") {
				// Legacy world saves
				w.name = boxCenterTop.value.replace(/;/g, "\u037e")
				let split = w.code.split(";")
				split[0] = w.name
				w.code = split.join(";")
			}
			else {
				let oldLength = w.name.length
				w.name = boxCenterTop.value.slice(0, 256) || w.name
				let newLength = w.name.length
				let newCode = new Uint8Array(w.code.length + newLength - oldLength)
				newCode[0] = newLength
				for (let i = 0; i < newLength; i++) newCode[i + 1] = w.name.charCodeAt(i) & 255
				let newIndex = newLength + 1
				let oldIndex = oldLength + 1
				while (newIndex < newCode.length) {
					newCode[newIndex++] = w.code[oldIndex++]
				}
				w.code = newCode
			}
 
			saveToDB(w.id, w).then(() => {
				initWorldsMenu()
				changeScene("loadsave menu")
			}).catch(e => console.error(e))
		})
		Button.add(mid, height / 2 + 50, w2, 40, "Back", "editworld", () => changeScene(previousScreen))
 
		// Pause buttons
		Button.add(width / 2, 225, 300, 40, "Resume", "pause", play)
		Button.add(width / 2, 275, 300, 40, "Options", "pause", () => changeScene("options"))
		Button.add(width / 2, 325, 300, 40, "Save", "pause", save, () => !!multiplayer && !multiplayer.host, () => {
			const account = location.href.startsWith("https://willard.fun") ? " + account" : ""
			return `Save the world to your browser${account}. Doesn't work in incognito.\n\nLast saved ${timeString(now - world.edited)}.`
		})
		Button.add(width / 2, 375, 300, 40, "Get Save Code", "pause", () => {
			savebox.classList.remove("hidden")
			saveDirections.classList.remove("hidden")
			savebox.value = world.getSaveString()
		})
		Button.add(width / 2, 425, 300, 40, "Open World To Public", "pause", () => {
			initMultiplayer()
		}, () => !!multiplayer || !location.href.startsWith("https://willard.fun"))
		Button.add(width / 2, 475, 300, 40, "Exit Without Saving", "pause", () => {
			// savebox.value = world.getSaveString()
			if (multiplayer) {
				multiplayer.close()
			}
			initWorldsMenu()
			changeScene("main menu")
			world.unloadChunks()
			world = null
		})
 
		// Comingsoon menu buttons
		Button.add(width / 2, 395, width / 3, 40, "Back", "comingsoon menu", () => changeScene(previousScreen))
 
		// Multiplayer buttons
		Button.add(mid + 3 * x4, height - 30, w4, 40, "Cancel", "multiplayer menu", () => changeScene("main menu"))
		Button.add(mid - x2, height - 75, w2, 40, "Play Selected World", "multiplayer menu", () => {
			// world = new World()
			win.world = null
 
			if (selectedWorld) {
				initMultiplayer(selectedWorld)
			}
		}, () => !selectedWorld)
 
		// Options buttons
		const optionsBottom = height >= 550 ? 500 : height - 50
		// Button.add(width/2, 185, width / 3, 40, () => `Sort Inventory By: ${settings.inventorySort === "name" ? "Name" : "Block ID"}`, "options", () => {
		// 	if (settings.inventorySort === "name") {
		// 		settings.inventorySort = "blockid"
		// 	}
		// 	else if (settings.inventorySort === "blockid") {
		// 		settings.inventorySort = "name"
		// 	}
		// })
 
		// Settings Sliders
		Slider.add(width/2, optionsBottom - 60 * 4, width / 3, 40, "options", "Render Distance", 1, 32, "renderDistance", val => settings.renderDistance = round(val))
		Slider.add(width/2, optionsBottom - 60 * 3, width / 3, 40, "options", "FOV", 30, 110, "fov", val => {
			p.FOV(val)
			if (world) {
				p.setDirection()
				world.render()
			}
		})
		Slider.add(width/2, optionsBottom - 60 * 2, width / 3, 40, "options", "Mouse Sensitivity", 30, 400, "mouseSense", val => settings.mouseSense = val)
		Slider.add(width/2, optionsBottom - 60, width / 3, 40, "options", "Reach", 5, 100, "reach", val => settings.reach = val)
		Button.add(width / 2, optionsBottom, width / 3, 40, "Back", "options", () => changeScene(previousScreen))
	}
 
	const hotbar = () => {
		if (screen !== "play") return
		// The selected block has changed. Update lantern brightness
		{
			let heldLight = blockData[holding].lightLevel / 15 || 0
			gl.useProgram(program3D)
			gl.uniform1f(glCache.uLantern, heldLight)
			gl.useProgram(program3DFogless)
			gl.uniform1f(glCache.uLanternFogless, heldLight)
		}
	}
 
	const crosshair = () => {
		if (p.spectator) return
		let x = width / 2 + 0.5
		let y = height / 2 + 0.5
		ctx.lineWidth = 1
		ctx.strokeStyle = "white"
		ctx.beginPath()
		ctx.moveTo(x - 10, y)
		ctx.lineTo(x + 10, y)
		ctx.moveTo(x, y - 10)
		ctx.lineTo(x, y + 10)
		ctx.stroke()
	}
 
	let debugLines = []
	let newDebugLines = []
	const hud = (clear) => {
		if (p.spectator || screen !== "play") return
		if (clear) debugLines.length = 0
 
		let x = 5
		let lineHeight = 24
		let y = lineHeight + 3
		let heightOffset = floor(lineHeight / 5)
 
		let lines = 0
		if (settings.showDebug === 3) {
			newDebugLines[0] = "Press F3 to cycle debug info."
			lines = 1
		}
		else {
			if (settings.showDebug >= 1) {
				newDebugLines[lines++] = analytics.fps + "/" + analytics.displayedwFps + "fps, C: " + renderedChunks.toLocaleString()
				newDebugLines[lines++] = "XYZ: " + p2.x + ", " + p2.y + ", " + p2.z
			}
			if (settings.showDebug >= 2) {
				newDebugLines[lines++] = "Average Frame Time: " + analytics.displayedFrameTime + "ms"
				newDebugLines[lines++] = "Worst Frame Time: " + analytics.displayedwFrameTime + "ms"
				newDebugLines[lines++] = "Render Time: " + analytics.displayedRenderTime + "ms"
				newDebugLines[lines++] = "Tick Time: " + analytics.displayedTickTime + "ms"
				newDebugLines[lines++] = "Generated Chunks: " + generatedChunks.toLocaleString()
			}
		}
		if (p.autoBreak) newDebugLines[lines++] = "Super breaker enabled"
		if (p.autoBuild) newDebugLines[lines++] = "Hyper builder enabled"
		if (multiplayer) {
			playerDistances.length = 0
			let closest = Infinity
			let cname = "Yourself"
			for (let name in playerPositions) {
				let pos = playerPositions[name]
				let distance = sqrt((pos.x - p2.x)*(pos.x - p2.x) + (pos.y - p2.y)*(pos.y - p2.y) + (pos.z - p2.z)*(pos.z - p2.z))
				playerDistances.push({
					name,
					distance
				})
				if (distance < closest) {
					closest = distance
					cname = name
				}
			}
			newDebugLines[lines++] = `Closest player: ${cname} (${round(closest)} blocks away)`
		}
 
		// Draw updated text
		ctx.textAlign = 'left'
		for (let i = 0; i < lines; i++) {
			if (debugLines[i] !== newDebugLines[i]) {
				let start = 0
				if (debugLines[i]) {
					for (let j = 0; j < debugLines[i].length; j++) {
						if (debugLines[i][j] !== newDebugLines[i][j]) {
							start = j
							break
						}
					}
					ctx.clearRect(x + start * charWidth, y + lineHeight * (i - 1) + heightOffset, (debugLines[i].length - start) * charWidth, lineHeight)
				}
				ctx.fillStyle = "rgba(50, 50, 50, 0.4)"
				ctx.fillRect(x + start * charWidth, y + lineHeight * (i-1) + heightOffset, (newDebugLines[i].length - start) * charWidth, lineHeight)
				ctx.fillStyle = "#fff"
				ctx.fillText(newDebugLines[i].slice(start), x + start*charWidth, y + lineHeight * i)
				debugLines[i] = newDebugLines[i]
			}
		}
 
		// Remove extra lines
		if (lines < debugLines.length) {
			let maxWidth = 0
			for (let i = lines; i < debugLines.length; i++) {
				maxWidth = Math.max(maxWidth, debugLines[i].length)
			}
			ctx.clearRect(x, y + (lines - 1) * lineHeight + heightOffset, maxWidth * charWidth, lineHeight * (debugLines.length - lines))
			debugLines.length = lines
		}
 
		// "Block light (head): " + world.getLight(p2.x, p2.y, p2.z, 1) + "\n"
		// + "Sky light (head): " + world.getLight(p2.x, p2.y, p2.z, 0) + "\n"
 
		// let str = "Average Frame Time: " + analytics.displayedFrameTime + "ms\n"
		// + "Worst Frame Time: " + analytics.displayedwFrameTime + "ms\n"
		// + "Render Time: " + analytics.displayedRenderTime + "ms\n"
		// + "Tick Time: " + analytics.displayedTickTime + "ms\n"
		// + "Rendered Chunks: " + renderedChunks.toLocaleString() + " / " + world.sortedChunks.length + "\n"
		// + "Generated Chunks: " + generatedChunks.toLocaleString() + "\n"
		// + "FPS: " + analytics.fps
 
		// if (p.autoBreak) {
		// 	text("Super breaker enabled", 5, height - 89, 12)
		// }
		// if (p.autoBuild) {
		// 	text("Hyper builder enabled", 5, height - 101, 12)
		// }
 
		// ctx.textAlign = 'right'
		// text(p2.x + ", " + p2.y + ", " + p2.z, width - 10, 15, 0)
		// ctx.textAlign = 'left'
		// text(str, 5, height - 77, 12)
	}
	let sortedBlocks = Object.entries(blockIds)
	sortedBlocks.shift() // Get rid of the air block in the array of sorted blocks
	sortedBlocks.sort()
 
	const clickInv = () => {
		inventory.heldItem = null
		document.getElementById("heldItem")?.classList.add("hidden")
	}
 
	let unpauseDelay = 0
	const mmoved = (e) => {
		let mouseS = settings.mouseSense / 30000
		p.rx -= e.movementY * mouseS
		p.ry += e.movementX * mouseS
 
		while(p.ry > Math.PI*2) {
			p.ry -= Math.PI*2
		}
		while(p.ry < 0) {
			p.ry += Math.PI*2
		}
		if(p.rx > Math.PI / 2) {
			p.rx = Math.PI / 2
		}
		if(p.rx < -Math.PI / 2) {
			p.rx = -Math.PI / 2
		}
	}
	const trackMouse = (e) => {
		if (screen !== "play") {
			cursor("")
			mouseX = e.x
			mouseY = e.y
			drawScreens[screen]()
			Button.draw()
			Slider.draw()
			Slider.drag()
		}
		if (screen === "inventory") {
			const heldItemCanvas = document.getElementById("heldItem")
			heldItemCanvas.style.left = (event.x - inventory.iconSize / 2 | 0) + "px"
			heldItemCanvas.style.top = (event.y - inventory.iconSize / 2 | 0) + "px"
		}
	}
 
	// For user controls that react immediately in the event handlers.
	const controlEvent = (name, event) => {
		if(screen === "play") {
			if (document.pointerLockElement !== canvas) {
				getPointer()
				p.lastBreak = now
			}
			else {
				if (name === controlMap.breakBlock.key) {
					changeWorldBlock(0)
				}
 
				if(name === controlMap.placeBlock.key && holding) {
					newWorldBlock()
				}
 
				if (name === controlMap.pickBlock.key && hitBox.pos) {
					let block = world.getBlock(hitBox.pos[0], hitBox.pos[1], hitBox.pos[2]) & 0x3ff
					inventory.hotbar.pickBlock(block)
					holding = inventory.hotbar.hand.id
					hotbar()
				}
 
				if(name === controlMap.pause.key) {
					releasePointer()
					changeScene("pause")
				}
 
				if (name === controlMap.openChat.key) {
					event.preventDefault()
					changeScene("chat")
				}
				if (name === "Slash") {
					changeScene("chat")
					chatInput.value = "/"
				}
 
				if(name === controlMap.superBreaker.key) {
					p.autoBreak = !p.autoBreak
					hud()
				}
 
				if(name === controlMap.hyperBuilder.key) {
					p.autoBuild = !p.autoBuild
					hud()
				}
 
				if (name === controlMap.jump.key && !p.spectator) {
					if (now < p.lastJump + 400) {
						p.flying = !p.flying
					}
					else {
						p.lastJump = now
					}
				}
 
				if (name === controlMap.zoom.key) {
					p.FOV(10, 300)
				}
 
				if (name === controlMap.sneak.key && !p.flying) {
					p.sneaking = true
					if (p.sprinting) {
						p.FOV(settings.fov, 100)
					}
					p.sprinting = false
					p.speed = 0.05
					p.bottomH = 1.32
				}
 
				if (name === controlMap.toggleSpectator.key) {
					p.spectator = !p.spectator
					p.flying = true
					p.onGround = false
					if (!p.spectator) {
						hotbar()
						crosshair()
						hud(true)
					}
					else {
						ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
					}
				}
 
				if (name === controlMap.openInventory.key) {
					changeScene("inventory")
					releasePointer()
				}
 
				if (name === "Semicolon") {
					releasePointer()
					freezeFrame = now + 500
				}
 
				if (name === "F3") {
					settings.showDebug = (settings.showDebug + 1) % 3
					hud()
				}
 
				// Drop held item; this just crashes since I broke Item entities.
				// if (name === controlMap.dropItem.key) {
				// 	let d = p.direction
				// 	world.entities.push(new Item(p.x, p.y, p.z, d.x/4, d.y/4, d.z/4, holding || inventory.hotbar[inventory.hotbarSlot], glExtensions, gl, glCache, indexBuffer, world, p))
				// }
			}
		}
		else if (screen === "pause" && name === controlMap.pause.key) {
			play()
		}
		else if (screen === "inventory") {
			if (name === "leftMouse") {
				clickInv()
			}
			if (name === controlMap.openInventory.key) {
				play()
			}
		}
	}
	document.onmousemove = trackMouse
	document.onpointerlockchange = function() {
		if (document.pointerLockElement === canvas) {
			document.onmousemove = mmoved
		}
		else {
			document.onmousemove = trackMouse
			if (screen === "play" && now > freezeFrame) {
				changeScene("pause")
				unpauseDelay = now + 500
			}
		}
		for (let key in Key) {
			Key[key] = false
		}
	}
	canvas.onmousedown = function(e) {
		mouseX = e.x
		mouseY = e.y
		mouseDown = true
		let name
		switch(e.button) {
			case 0:
				if (Key.ControlRight || Key.ControlLeft) name = "rightMouse"
				else name = "leftMouse"
				break
			case 1:
				name = "middleMouse"
				break
			case 2:
				name = "rightMouse"
				break
		}
		Key[name] = true
		controlEvent(name)
 
		Button.click()
		Slider.click()
	}
	canvas.onmouseup = function(e) {
		let name
		switch(e.button) {
			case 0:
				if (Key.ControlRight || Key.ControlLeft) name = "rightMouse"
				else name = "leftMouse"
				break
			case 1:
				name = "middleMouse"
				break
			case 2:
				name = "rightMouse"
				break
		}
		Key[name] = false
		mouseDown = false
		Slider.release()
	}
	canvas.onkeydown = function(e) {
		let code = e.code
		// code === "Space" || code === "ArrowDown" || code === "ArrowUp" || code === "F3") {
		if (!Key.ControlLeft && !Key.ControlRight && code !== "F12" && code !== "F11") {
			e.preventDefault()
		}
		if (e.repeat || Key[code]) {
			return
		}
		Key[code] = true
 
		controlEvent(code, e)
 
		if (screen === "play" && Number(e.key)) {
			inventory.hotbar.setPosition(e.key - 1)
			holding = inventory.hotbar.hand.id
			hotbar()
		}
	}
	canvas.onkeyup = function(e) {
		Key[e.code] = false
		if(e.code === "Escape" && (screen === "chat" || screen === "pause" || screen === "inventory" || screen === "options" && previousScreen === "pause") && now > unpauseDelay) {
			play()
		}
		if (screen === "play") {
			if (e.code === controlMap.zoom.key) {
				p.FOV(settings.fov, 300)
			}
 
			if (e.code === controlMap.sneak.key && p.sneaking) {
				p.sneaking = false
				p.speed = 0.11
				p.bottomH = 1.62
			}
		}
	}
	canvas.onblur = function() {
		for (let key in Key) {
			Key[key] = false
		}
		mouseDown = false
		Slider.release()
	}
	canvas.oncontextmenu = function(e) {
		e.preventDefault()
	}
	win.onbeforeunload = e => {
		if (screen === "play" && Key.control) {
			releasePointer()
			e.preventDefault()
			e.returnValue = "Q is the sprint button; Ctrl + W closes the page."
			return true
		}
	}
	canvas.onwheel = e => {
		e.preventDefault()
		e.stopPropagation()
		if (screen === "play") {
			inventory.hotbar.shiftPosition(e.deltaY)
			holding = inventory.hotbar.hand.id
			hotbar()
		}
	}
	document.onwheel = () => {} // Shouldn't do anything, but it helps with a Khan Academy bug somewhat
	win.onresize = () => {
		width = win.innerWidth
		height = win.innerHeight
		canvas.height = height
		canvas.width = width
		if (!gl) return
		gl.canvas.height = height
		gl.canvas.width = width
		gl.viewport(0, 0, width, height)
		initButtons()
		initDirt()
		inventory.size = min(width, height) / 15 | 0
		use3d()
		p.FOV(p.currentFov + 0.0001)
 
		if (screen === "play") {
			play()
		}
		else {
			drawScreens[screen]()
			Button.draw()
			Slider.draw()
		}
	}
	chatInput.oninput = () => {
		if (chatInput.value.length > 512) chatInput.value = chatInput.value.slice(0, 512)
	}
	chatInput.onkeydown = e => e.key === "Tab" ? e.preventDefault() : 0
	chatInput.onkeyup = e => {
		if (e.key === "Enter") {
			let msg = chatInput.value.trim()
			if (msg) {
				e.preventDefault()
				e.stopPropagation()
				if (msg.startsWith("/")) {
					sendCommand(msg)
				}
				else {
					sendChat(msg)
				}
				chatInput.value = ""
			}
			else {
				play()
			}
		}
		else {
			let msg = chatInput.value
			if (msg.startsWith("/")) {
				let words = msg.split(" ")
				if (words.length > 1) {
					let cmd = words[0].slice(1)
					if (commands.has(cmd)) commands.get(cmd).autocomplete(msg)
				}
				else {
					let possible = commandList.filter(name => name.startsWith(msg))
					if (possible.length === 1) commands.get(possible[0].slice(1)).autocomplete(msg)
					else setAutocomplete(commandList)
				}
			}
		}
	}
 
	document.onkeyup = e => {
		if (e.key === "Escape" && screen === "chat") {
			e.preventDefault()
			e.stopPropagation()
			chatInput.value = ""
			play()
		}
		else if (screen === "chat" && !chatInput.hasFocus) chatInput.focus()
	}
 
	let pTouch = { x: 0, y: 0 }
	canvas.addEventListener("touchstart", function(e) {
		pTouch.x = e.changedTouches[0].pageX
		pTouch.y = e.changedTouches[0].pageY
	}, false)
	canvas.addEventListener("touchmove", function(e) {
		e.movementY = e.changedTouches[0].pageY - pTouch.y
		e.movementX = e.changedTouches[0].pageX - pTouch.x
		pTouch.x = e.changedTouches[0].pageX
		pTouch.y = e.changedTouches[0].pageY
		mmoved(e)
		e.preventDefault()
	}, false)
 
	const initWebgl = () => {
		if (!win.gl) {
			let canv = document.getElementById("webgl-canvas")
			canv.width = ctx.canvas.width
			canv.height = ctx.canvas.height
			win.gl = gl = canv.getContext("webgl", { preserveDrawingBuffer: true, antialias: false, premultipliedAlpha: false })
			if (!gl) {
				alert("Error: WebGL not detected. Please enable WebGL and/or \"hardware acceleration\" in your browser settings.")
				throw "Error: Cannot play a WebGL game without WebGL."
			}
			win.glExtensions = glExtensions = {
				"vertex_array_object": gl.getExtension("OES_vertex_array_object"),
				"element_index_uint": gl.getExtension("OES_element_index_uint")
			}
			if (!glExtensions.element_index_uint || !glExtensions.vertex_array_object) {
				alert("Unable to load WebGL extension. Please use a supported browser, or update your current browser.")
			}
			gl.viewport(0, 0, canv.width, canv.height)
			gl.enable(gl.DEPTH_TEST)
			gl.enable(gl.BLEND)
			gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
 
			win.glCache = glCache = {}
			program3DFogless = createProgramObject(gl, foglessVertexShaderSrc3D, foglessFragmentShaderSrc3D)
			program3D = createProgramObject(gl, vertexShaderSrc3D, fragmentShaderSrc3D)
			program2D = createProgramObject(gl, vertexShaderSrc2D, fragmentShaderSrc2D)
			programEntity = createProgramObject(gl, vertexShaderSrcEntity, fragmentShaderSrcEntity)
			skybox = getSkybox(gl, glCache, program3D, program3DFogless)
 
			gl.useProgram(program2D)
			glCache.uOffset = gl.getUniformLocation(program2D, "uOffset")
			glCache.uSampler2 = gl.getUniformLocation(program2D, "uSampler")
			glCache.aTexture2 = gl.getAttribLocation(program2D, "aTexture")
			glCache.aVertex2 = gl.getAttribLocation(program2D, "aVertex")
			glCache.aShadow2 = gl.getAttribLocation(program2D, "aShadow")
 
			gl.useProgram(programEntity)
			glCache.uSamplerEntity = gl.getUniformLocation(programEntity, "uSampler")
			glCache.uLightLevelEntity = gl.getUniformLocation(programEntity, "uLightLevel")
			glCache.uViewEntity = gl.getUniformLocation(programEntity, "uView")
			glCache.aTextureEntity = gl.getAttribLocation(programEntity, "aTexture")
			glCache.aVertexEntity = gl.getAttribLocation(programEntity, "aVertex")
 
			gl.useProgram(program3DFogless)
			glCache.uViewFogless = gl.getUniformLocation(program3DFogless, "uView")
			glCache.uSamplerFogless = gl.getUniformLocation(program3DFogless, "uSampler")
			glCache.uPosFogless = gl.getUniformLocation(program3DFogless, "uPos")
			glCache.uTimeFogless = gl.getUniformLocation(program3DFogless, "uTime")
			glCache.uTransFogless = gl.getUniformLocation(program3DFogless, "uTrans")
			glCache.uLanternFogless = gl.getUniformLocation(program3DFogless, "uLantern")
			glCache.uZoffset = gl.getUniformLocation(program3DFogless, "uZoffset")
 
			gl.useProgram(program3D)
			glCache.uView = gl.getUniformLocation(program3D, "uView")
			glCache.uSampler = gl.getUniformLocation(program3D, "uSampler")
			glCache.uPos = gl.getUniformLocation(program3D, "uPos")
			glCache.uDist = gl.getUniformLocation(program3D, "uDist")
			glCache.uTime = gl.getUniformLocation(program3D, "uTime")
			glCache.uSky = gl.getUniformLocation(program3D, "uSky")
			glCache.uSun = gl.getUniformLocation(program3D, "uSun")
			glCache.uTrans = gl.getUniformLocation(program3D, "uTrans")
			glCache.uLantern = gl.getUniformLocation(program3D, "uLantern")
			glCache.aShadow = gl.getAttribLocation(program3D, "aShadow")
			glCache.aSkylight = gl.getAttribLocation(program3D, "aSkylight")
			glCache.aBlocklight = gl.getAttribLocation(program3D, "aBlocklight")
			glCache.aTexture = gl.getAttribLocation(program3D, "aTexture")
			glCache.aVertex = gl.getAttribLocation(program3D, "aVertex")
 
			win.glPrograms = { program2D, program3D, program3DFogless, programEntity, skybox }
		}
		else {
			({ gl, glCache, glExtensions } = win);
			({ program2D, program3D, program3DFogless, programEntity, skybox } = win.glPrograms)
			document.getElementById("webgl-canvas").remove() // There can be only 1
			document.body.prepend(gl.canvas)
 
			gl.useProgram(program3D)
		}
 
		gl.uniform1f(glCache.uDist, 1000)
		gl.uniform1i(glCache.uTrans, 0)
 
		// Send the block textures to the GPU
		initTextures(gl, glCache)
		initShapes()
 
		// Make a buffer for the hitbox outline texture (just black repeatedly)
		hitBox.buffer = gl.createBuffer()
		gl.bindBuffer(gl.ARRAY_BUFFER, hitBox.buffer)
		gl.bufferData(gl.ARRAY_BUFFER, hitboxTextureCoords, gl.STATIC_DRAW)
 
		// Bind the index buffer that will be used to draw squares instead of 2 triangles for everything
		indexBuffer = gl.createBuffer()
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
		gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexOrder, gl.STATIC_DRAW)
 
		// Tell it not to render the insides of blocks
		gl.enable(gl.CULL_FACE)
		gl.cullFace(gl.BACK)
 
		gl.depthRange(0, 2)
		gl.lineWidth(2)
		gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
	}
 
	const initPlayer = () => {
		p = new Camera()
		p.speed = 0.11
		p.velocity = new PVector(0, 0, 0)
		// p.pos = new Float32Array(3)
		p.sprintSpeed = 1.5
		p.flySpeed = 3.75
		p.x = 8
		p.y = 0
		p.z = 8
		p.w = 6 / 16
		p.bottomH = 1.62
		p.topH = 0.18
		p.onGround = false
		p.jumpSpeed = 0.45
		p.sprinting = false
		p.maxYVelocity = 4.5
		p.gravityStrength = -0.091
		p.lastUpdate = performance.now()
		p.lastBreak = now
		p.lastPlace = now
		p.lastJump = now
		p.autoBreak = false
		p.autoBuild = false
		p.flying = false
		p.sneaking = false
		p.spectator = false
 
		p.minX = () => roundBits(p.x - p.w - p2.x)
		p.minY = () => roundBits(p.y - p.bottomH - p2.y)
		p.minZ = () => roundBits(p.z - p.w - p2.z)
		p.maxX = () => roundBits(p.x + p.w - p2.x)
		p.maxY = () => roundBits(p.y + p.topH - p2.y)
		p.maxZ = () => roundBits(p.z + p.w - p2.z)
 
		win.player = p
		win.p2 = p2
	}
 
	const sanitize = (text) => {
		const el = document.createElement('div')
		el.textContent = text
		return el.innerHTML
	}
 
	const initWorldsMenu = () => {
		while (win.worlds.firstChild) {
			win.worlds.removeChild(win.worlds.firstChild)
		}
		selectedWorld = 0
		win.boxCenterTop.value = ""
 
		const deselect = () => {
			let elem = document.getElementsByClassName("selected")
			if (elem && elem[0]) {
				elem[0].classList.remove("selected")
			}
		}
 
		const addWorld = (name, version, size, id, edited, cloud) => {
			let div = document.createElement("div")
			div.className = "world"
			div.onclick = () => {
				deselect()
				div.classList.add("selected")
				selectedWorld = id
			}
			let br = "<br>"
			div.id = id
			div.innerHTML = "<strong>" + sanitize(name) + "</strong>" + br
 
			if (edited){
				let str = new Date(edited).toLocaleDateString(undefined, {
					year: "numeric",
					month: "short",
					day: "numeric",
					hour: "numeric",
					minute: "2-digit"
				})
				div.innerHTML += str + br
			}
			div.innerHTML += version + br
			if (cloud) div.innerHTML += `Cloud Save (${size.toLocaleString()} bytes)`
			else div.innerHTML += `${size.toLocaleString()} bytes used`
 
			win.worlds.appendChild(div)
		}
 
		worlds = {}
		if (loadString) {
			try {
				let tempWorld = new World(true)
				tempWorld.loadSave(loadString)
				addWorld(`${tempWorld.name} (Pre-loaded)`, tempWorld.version, loadString.length, now)
				worlds[now] = {
					code: loadString,
					id: now
				}
			}
			catch(e) {
				console.log("Unable to load hardcoded save.")
				console.error(e)
			}
		}
		loadFromDB().then(async res => {
			if(res && res.length) {
				let index = res.findIndex(obj => obj.id === "settings")
				if (index >= 0) {
					Object.assign(settings, res[index].data) // Stored data overrides any hardcoded settings
					p.FOV(settings.fov)
					res.splice(index, 1)
				}
			}
 
			if (res && res.length) {
				res = res.map(d => d.data).filter(d => d && d.code).sort((a, b) => b.edited - a.edited)
				for (let data of res) {
					addWorld(data.name, data.version, data.code.length + 60, data.id, data.edited, false)
					data.cloud = false
					worlds[data.id] = data
				}
			}
 
			if (location.href.startsWith("https://willard.fun/")) {
				let cloudSaves = await fetch('https://willard.fun/minekhan/saves').then(res => res.json())
				if (Array.isArray(cloudSaves) && cloudSaves.length) {
					for (let data of cloudSaves) {
						if (worlds[data.id] && worlds[data.id].edited >= data.edited) continue
 
						addWorld(data.name, data.version, data.size + 60, data.id, data.edited, true)
						data.cloud = true
						worlds[data.id] = data
					}
				}
			}
 
			win.worlds.onclick = Button.draw
			win.boxCenterTop.onkeyup = Button.draw
		}).catch(e => console.error(e))
 
		superflat = false
		details = true
		caves = true
	}
 
	const initMultiplayerMenu = async () => {
		while (win.worlds.firstChild) {
			win.worlds.removeChild(win.worlds.firstChild)
		}
		selectedWorld = 0
		win.boxCenterTop.value = ""
 
		const deselect = () => {
			let elem = document.getElementsByClassName("selected")
			if (elem && elem[0]) {
				elem[0].classList.remove("selected")
			}
		}
 
		let servers = await getWorlds()
 
		const addWorld = (name, host, online, id, version, password) => {
			let div = document.createElement("div")
			div.className = "world"
			div.onclick = () => {
				deselect()
				div.classList.add("selected")
				selectedWorld = id
			}
			let br = "<br>"
			div.id = id
			div.innerHTML = "<strong>" + sanitize(name) + "</strong>" + br
 
			div.innerHTML += "Hosted by " + sanitize(host) + br
			const span = document.createElement("span")
			span.className = "online"
			span.textContent = online.toString()
			div.appendChild(span)
			div.innerHTML += " players online" + br
			div.innerHTML += version + br
			if (password) div.innerHTML += "Password-protected" + br
 
			win.worlds.appendChild(div)
		}
 
		worlds = {}
 
		for (let data of servers) {
			addWorld(data.name, data.host, data.online, data.target, data.version, !data.public)
			worlds[data.target] = data
		}
		win.worlds.onclick = Button.draw
		win.boxCenterTop.onkeyup = Button.draw
 
		let refresh = setInterval(async () => {
			if (screen !== "multiplayer menu") return clearInterval(refresh)
			let servers = await getWorlds()
			clear: for (let target in worlds) {
				for (let data of servers) if (data.target === target) continue clear
				document.getElementById(target).remove()
				delete worlds[target]
			}
			for (let data of servers) {
				if (!document.getElementById(data.target)) {
					addWorld(data.name, data.host, data.online, data.target, data.version, !data.public)
				}
				worlds[data.target] = data
 
				const element = document.getElementById(data.target)
				element.getElementsByClassName("online")[0].textContent = data.online.toString()
			}
		}, 5000)
	}
 
	const initEverything = () => {
		generatedChunks = 0
 
		initPlayer()
		initWebgl()
 
		let l = 0
		for (let d of document.getElementsByTagName("script")) {
			l += d.innerHTML.length
		}
		if (win.location.origin === "https://www.kasandbox.org" && l !== 311283) {
			// This is only for KA, since forks that make it onto the hotlist get a lot of hate for "not giving credit".
			// If you're making significant changes and want to remove this, then you can.
			// If publishing this on another website, I'd encourage giving credit somewhere to avoid being accused of plagiarism.
			message.innerHTML = '.oot lanigiro eht tuo kcehc ot>rb<erus eb ,siht ekil uoy fI>rb<.dralliW yb >a/<nahKeniM>"wen_"=tegrat "8676731005517465/cm/sc/gro.ymedacanahk.www//:sptth"=ferh a< fo>rb<ffo-nips a si margorp sihT'.split("").reverse().join("")
		}
 
		genIcons() // Generate all the block icons
		initDirt()
 
		drawScreens[screen]()
		Button.draw()
		Slider.draw()
 
		p.FOV(settings.fov)
		initWorldsMenu()
		initButtons()
 
		inventory.size = min(width, height) / 15 | 0
		inventory.init(true)
 
		// See if a user followed a link here.
		var urlParams = new URLSearchParams(win.location.search)
		if (urlParams.has("target")) {
			changeScene("multiplayer menu")
			initMultiplayer(urlParams.get("target"))
		}
 
		if (win.tickid) win.clearInterval(win.tickid)
		win.tickid = setInterval(tickLoop, 50)
	}
 
	// Define all the scene draw functions
	(function() {
		const title = () => {
			let title = "MINEKHAN"
			let subtext = "JAVASCRIPT EDITION"
			let font = "monospace"
			strokeWeight(1)
			ctx.textAlign = 'center'
 
			const scale = Math.min(width / 600, 1.5)
			for (let i = 0; i < 15; i++) {
				if (i < 12) {
					fill(i * 10)
				}
				else if (i > 11) {
					fill(125)
				}
 
				ctx.font = `bold ${80 * scale + i}px ${font}`
				text(title, width / 2, 158 - i)
 
				ctx.font = `bold ${32 * scale + i/4}px ${font}`
				text(subtext, width / 2, 140 + 60 * scale - i / 2)
			}
		}
		const clear = () => ctx.clearRect(0, 0, canvas.width, canvas.height)
 
		drawScreens["main menu"] = () => {
			ctx.clearRect(0, 0, width, height)
			title()
			fill(220)
			ctx.font = "20px monospace"
			ctx.textAlign = 'left'
			text("MineKhan " + version, width - (width - 2), height - 2)
		}
 
		drawScreens.play = () => {
			let renderStart = performance.now()
			p.setDirection()
			world.render()
			analytics.totalRenderTime += performance.now() - renderStart
		}
 
		drawScreens.loading = () => {
			// This is really stupid, but it basically works by teleporting the player around to each chunk I'd like to load.
			// If chunks loaded from a save aren't generated, they're deleted from the save, so this loads them all.
 
			let standing = true
 
			let cx = p.x >> 4
			let cz = p.z >> 4
 
			for (let x = cx - 1; x <= cx + 1; x++) {
				for (let z = cz - 1; z <= cz + 1; z++) {
					if (!world.getChunk(x * 16, z * 16).buffer) {
						standing = false
					}
				}
			}
			if (!standing) {
				world.tick()
			}
			else {
				play()
				if (p.y === 0 && !p.flying && !p.spectator) {
					p.y = world.getChunk(p.x|0, p.z|0).tops[(p.x & 15) * 16 + (p.z & 15)] + 2
				}
				return
			}
 
			let progress = round(100 * generatedChunks / 9)
			document.getElementById("loading-text").textContent = `Loading... ${progress}% complete (${generatedChunks} / 9)`
		}
 
		drawScreens.pause = () => {
			strokeWeight(1)
			clear()
		}
 
		drawScreens.options = () => {
			clear()
		}
		drawScreens["creation menu"] = () => {
			clear()
			ctx.textAlign = 'center'
			textSize(20)
			fill(255)
			text("Create New World", width / 2, 20)
		}
		drawScreens["loadsave menu"] = () => {
			clear()
			ctx.textAlign = 'center'
			textSize(20)
			fill(255)
			text("Select World", width / 2, 20)
		}
		drawScreens.editworld = dirt
		drawScreens["multiplayer menu"] = () => {
			clear()
			ctx.textAlign = 'center'
			textSize(20)
			fill(255)
			text("Select Server", width / 2, 20)
		}
	})()
 
	// Give the font time to load and redraw the homescreen
	setTimeout(() => {
		drawScreens[screen]()
		Button.draw()
		Slider.draw()
	}, 100)
 
	const tickLoop = () => {
		if (world && screen === "play") {
			let tickStart = performance.now()
			world.tick()
			analytics.ticks++
			analytics.totalTickTime += performance.now() - tickStart
 
			controls()
			runGravity()
			resolveContactsAndUpdatePosition()
		}
	}
 
	let prevTime = 0
	const renderLoop = (time) => {
		let frameFPS = Math.round(10000/(time - prevTime)) / 10
		prevTime = time
 
		if (!gl && window.innerWidth && window.innerHeight) initEverything()
 
		now = Date.now()
		let frameStart = performance.now()
 
		if (screen === "play" || screen === "loading") {
			try {
				drawScreens[screen]()
			}
			catch(e) {
				console.error(e)
			}
		}
 
		if (screen === "play" && now - analytics.lastUpdate > 500 && analytics.frames) {
			analytics.displayedTickTime = (analytics.totalTickTime / analytics.ticks).toFixed(1)
			analytics.displayedRenderTime = (analytics.totalRenderTime / analytics.frames).toFixed(1)
			analytics.displayedFrameTime = (analytics.totalFrameTime / analytics.frames).toFixed(1)
			analytics.fps = round(analytics.frames * 1000 / (now - analytics.lastUpdate))
			analytics.displayedwFrameTime = analytics.worstFrameTime.toFixed(1)
			analytics.displayedwFps = analytics.worstFps
			analytics.worstFps = 1000000
			analytics.frames = 0
			analytics.totalRenderTime = 0
			analytics.totalTickTime = 0
			analytics.ticks = 0
			analytics.totalFrameTime = 0
			analytics.worstFrameTime = 0
			analytics.lastUpdate = now
			hud()
		}
 
		analytics.frames++
		analytics.totalFrameTime += performance.now() - frameStart
		analytics.worstFrameTime = max(performance.now() - frameStart, analytics.worstFrameTime)
		analytics.worstFps = min(frameFPS, analytics.worstFps)
		win.raf = requestAnimationFrame(renderLoop)
	}
	return renderLoop
}
 
(async function() {
	if (win.raf) {
		win.cancelAnimationFrame(win.raf)
		console.log("Canceled", win.raf)
	}
	var init = await MineKhan()
	init()
})()
}
		</script>
	  <script crossorigin type="text/javascript" src="https://codesandbox.io/static/js/watermark-button.eeb14a97b.js"></script>
  </body>
</html>
 
<!--
// Just a fun function to use in the web console.
const asphereoid = async (w, h, d, id) => {
	const sleep = ms => new Promise(resolve => setTimeout(() => resolve(), ms))
	let px = p2.x
	let py = p2.y
	let pz = p2.z
	let w2 = w * w
	let h2 = h * h
	let d2 = d * d
	let w3 = (w - 1.5) * (w - 1.5)
	let h3 = (h - 1.5) * (h - 1.5)
	let d3 = (d - 1.5) * (d - 1.5)
 
	for (let y = -h; y < h; y++) {
		for (let x = -w; x <= w; x++) {
			for (let z = -d; z <= d; z++) {
				let n = x * x / w2 + y * y / h2 + z * z / d2
				let n2 = x * x / w3 + y * y / h3 + z * z / d3
				if (n < 1 && n2 >= 1) {
					world.setBlock(px + x, py + y, pz + z, id)
					await sleep(10)
				}
			}
		}
	}
}
-->