summary refs log tree commit diff
path: root/src/Input.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/Input.c')
-rw-r--r--src/Input.c1467
1 files changed, 1467 insertions, 0 deletions
diff --git a/src/Input.c b/src/Input.c
new file mode 100644
index 0000000..0f6bc80
--- /dev/null
+++ b/src/Input.c
@@ -0,0 +1,1467 @@
+#include "Input.h"
+#include "String.h"
+#include "Event.h"
+#include "Funcs.h"
+#include "Options.h"
+#include "Logger.h"
+#include "Platform.h"
+#include "Chat.h"
+#include "Utils.h"
+#include "Server.h"
+#include "HeldBlockRenderer.h"
+#include "Game.h"
+#include "ExtMath.h"
+#include "Camera.h"
+#include "Inventory.h"
+#include "World.h"
+#include "Event.h"
+#include "Window.h"
+#include "Entity.h"
+#include "Screens.h"
+#include "Block.h"
+#include "Menus.h"
+#include "Gui.h"
+#include "Protocol.h"
+#include "AxisLinesRenderer.h"
+#include "Picking.h"
+
+struct _InputState Input;
+static cc_bool input_buttonsDown[3];
+static int input_pickingId = -1;
+static double input_lastClick;
+static float input_fovIndex = -1.0f;
+#ifdef CC_BUILD_WEB
+static cc_bool suppressEscape;
+#endif
+enum MouseButton_ { MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE };
+/* Raises PointerEvents.Up or PointerEvents.Down */
+static void Pointer_SetPressed(int idx, cc_bool pressed);
+
+
+/*########################################################################################################################*
+*------------------------------------------------------Touch support------------------------------------------------------*
+*#########################################################################################################################*/
+#ifdef CC_BUILD_TOUCH
+static struct TouchPointer {
+	long id;
+	cc_uint8 type;
+	int begX, begY;
+	double start;
+} touches[INPUT_MAX_POINTERS];
+
+int Pointers_Count;
+int Input_TapMode  = INPUT_MODE_PLACE;
+int Input_HoldMode = INPUT_MODE_DELETE;
+cc_bool Input_TouchMode;
+
+static void MouseStatePress(int button);
+static void MouseStateRelease(int button);
+
+static cc_bool AnyBlockTouches(void) {
+	int i;
+	for (i = 0; i < Pointers_Count; i++) {
+		if (!(touches[i].type & TOUCH_TYPE_BLOCKS)) continue;
+
+		/* Touch might be an 'all' type - remove 'gui' type */
+		touches[i].type &= TOUCH_TYPE_BLOCKS | TOUCH_TYPE_CAMERA;
+		return true;
+	}
+	return false;
+}
+
+static void ClearTouches(void) {
+	int i;
+	for (i = 0; i < INPUT_MAX_POINTERS; i++) touches[i].type = 0;
+	Pointers_Count = Input_TouchMode ? 0 : 1;
+}
+
+void Input_SetTouchMode(cc_bool enabled) {
+	Input_TouchMode = enabled;
+	ClearTouches();
+}
+
+static cc_bool MovedFromBeg(int i, int x, int y) {
+	return Math_AbsI(x - touches[i].begX) > Display_ScaleX(5) ||
+		   Math_AbsI(y - touches[i].begY) > Display_ScaleY(5);
+}
+
+static cc_bool TryUpdateTouch(long id, int x, int y) {
+	int i;
+	for (i = 0; i < Pointers_Count; i++) {
+		if (touches[i].id != id || !touches[i].type) continue;
+
+		if (Input.RawMode && (touches[i].type & TOUCH_TYPE_CAMERA)) {
+			/* If the pointer hasn't been locked to gui or block yet, moving a bit */
+			/* should cause the pointer to get locked to camera movement. */
+			if (touches[i].type == TOUCH_TYPE_ALL && MovedFromBeg(i, x, y)) {
+				/* Allow a little bit of leeway because though, because devices */
+				/* might still report a few pixels of movement depending on how */
+				/* user is holding the finger down on the touch surface */
+				if (touches[i].type == TOUCH_TYPE_ALL) touches[i].type = TOUCH_TYPE_CAMERA;
+			}
+			Event_RaiseRawMove(&PointerEvents.RawMoved, x - Pointers[i].x, y - Pointers[i].y);
+		}
+		Pointer_SetPosition(i, x, y);
+		return true;
+	}
+	return false;
+}
+
+void Input_AddTouch(long id, int x, int y) {
+	int i;
+	/* Check if already existing pointer with same ID */
+	if (TryUpdateTouch(id, x, y)) return;
+
+	for (i = 0; i < INPUT_MAX_POINTERS; i++) {
+		if (touches[i].type) continue;
+
+		touches[i].id   = id;
+		touches[i].type = TOUCH_TYPE_ALL;
+		touches[i].begX = x;
+		touches[i].begY = y;
+
+		touches[i].start = Game.Time;
+		/* Also set last click time, otherwise quickly tapping */
+		/* sometimes triggers a 'delete' in InputHandler_Tick, */
+		/* and then another 'delete' in CheckBlockTap. */
+		input_lastClick  = Game.Time;
+
+		if (i == Pointers_Count) Pointers_Count++;
+		Pointer_SetPosition(i, x, y);
+		Pointer_SetPressed(i, true);
+		return;
+	}
+}
+void Input_UpdateTouch(long id, int x, int y) { TryUpdateTouch(id, x, y); }
+
+/* Quickly tapping should trigger a block place/delete */
+static void CheckBlockTap(int i) {
+	int btn, pressed;
+	if (Game.Time > touches[i].start + 0.25) return;
+	if (touches[i].type != TOUCH_TYPE_ALL)   return;
+
+	if (Input_TapMode == INPUT_MODE_PLACE) {
+		btn = MOUSE_RIGHT;
+	} else if (Input_TapMode == INPUT_MODE_DELETE) {
+		btn = MOUSE_LEFT;
+	} else { return; }
+
+	pressed = input_buttonsDown[btn];
+	MouseStatePress(btn);
+
+	if (btn == MOUSE_LEFT) { 
+		InputHandler_DeleteBlock();
+	} else { 
+		InputHandler_PlaceBlock();
+	}
+	if (!pressed) MouseStateRelease(btn);
+}
+
+void Input_RemoveTouch(long id, int x, int y) {
+	int i;
+	for (i = 0; i < Pointers_Count; i++) {
+		if (touches[i].id != id || !touches[i].type) continue;
+
+		Pointer_SetPosition(i, x, y);
+		Pointer_SetPressed(i, false);
+
+		/* found the touch, remove it */
+		Pointer_SetPosition(i, -100000, -100000);
+		touches[i].type = 0;
+
+		if ((i + 1) == Pointers_Count) Pointers_Count--;
+		return;
+	}
+}
+#else
+static void ClearTouches(void) { }
+#endif
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------------Key-----------------------------------------------------------*
+*#########################################################################################################################*/
+#define Key_Function_Names \
+"F1",  "F2",  "F3",  "F4",  "F5",  "F6",  "F7",  "F8",  "F9",  "F10",\
+"F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20",\
+"F21", "F22", "F23", "F24"
+#define Key_Ascii_Names \
+"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",\
+"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",\
+"U", "V", "W", "X", "Y", "Z"
+#define Pad_Names \
+"PAD_A", "PAD_B", "PAD_X", "PAD_Y", "PAD_L", "PAD_R", \
+"PAD_Z", "PAD_C", "PAD_D", \
+"PAD_LEFT", "PAD_RIGHT", "PAD_UP", "PAD_DOWN", \
+"PAD_START", "PAD_SELECT", "PAD_ZL", "PAD_ZR", \
+"PAD_LSTICK", "PAD_RSTICK", \
+"PAD_CLEFT", "PAD_CRIGHT", "PAD_CUP", "PAD_CDOWN"
+
+/* Names for each input button when stored to disc */
+static const char* const storageNames[INPUT_COUNT] = {
+	"None",
+	Key_Function_Names,
+	"Tilde", "Minus", "Plus", "BracketLeft", "BracketRight", "Slash",
+	"Semicolon", "Quote", "Comma", "Period", "BackSlash",
+	"ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight",
+	"AltLeft", "AltRight", "WinLeft", "WinRight",
+	"Up", "Down", "Left", "Right",
+	"Number0", "Number1", "Number2", "Number3", "Number4",
+	"Number5", "Number6", "Number7", "Number8", "Number9",
+	"Insert", "Delete", "Home", "End", "PageUp", "PageDown",
+	"Menu",
+	Key_Ascii_Names,
+	"Enter", "Escape", "Space", "BackSpace", "Tab", "CapsLock",
+	"ScrollLock", "PrintScreen", "Pause", "NumLock",
+	"Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4",
+	"Keypad5", "Keypad6", "Keypad7", "Keypad8", "Keypad9",
+	"KeypadDivide", "KeypadMultiply", "KeypadSubtract",
+	"KeypadAdd", "KeypadDecimal", "KeypadEnter",
+
+	"XButton1", "XButton2", "LeftMouse", "RightMouse", "MiddleMouse",
+	"WheelUp", "WheelDown", "WheelLeft", "WheelRight",
+	"XButton3", "XButton4", "XButton5", "XButton6",
+	
+	"VolumeMute", "VolumeUp", "VolumeDown", "Sleep",
+	"MediaNext", "MediaPrev", "MediaPlay", "MediaStop",
+	"BrowserPrev", "BrowserNext", "BrowserRefresh", "BrowserStop", "BrowserSsearch", "BrowserFavorites", "BrowserHome",
+	"LaunchMail", "LaunchMedia", "LaunchApp1", "LaunchCalc", 
+
+	Pad_Names
+};
+
+const char* const Input_DisplayNames[INPUT_COUNT] = {
+	"NONE",
+	Key_Function_Names,
+	"GRAVE", "MINUS", "PLUS", "LBRACKET", "RBRACKET", "SLASH",
+	"SEMICOLON", "APOSTROPHE", "COMMA", "PERIOD", "BACKSLASH",
+	"LSHIFT", "RSHIFT", "LCONTROL", "RCONTROL",
+	"LALT", "RALT", "LWIN", "RWIN",
+	"UP", "DOWN", "LEFT", "RIGHT",
+	"0", "1", "2", "3", "4",
+	"5", "6", "7", "8", "9",
+	"INSERT", "DELETE", "HOME", "END", "PRIOR", "DOWN",
+	"MENU",
+	Key_Ascii_Names,
+	"RETURN", "ESCAPE", "SPACE", "BACK", "TAB", "CAPITAL",
+	"SCROLL", "PRINT", "PAUSE", "NUMLOCK",
+	"NUMPAD0", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD4",
+	"NUMPAD5", "NUMPAD6", "NUMPAD7", "NUMPAD8", "NUMPAD9",
+	"DIVIDE", "MULTIPLY", "SUBTRACT",
+	"ADD", "DECIMAL", "NUMPADENTER",
+
+	"XBUTTON1", "XBUTTON2", "LMOUSE", "RMOUSE", "MMOUSE",
+	"WHEELUP", "WHEELDOWN", "WHEELLEFT", "WHEELRIGHT",
+	"XBUTTON3", "XBUTTON4", "XBUTTON5", "XBUTTON6",
+	
+	"VOLUMEMUTE", "VOLUMEUP", "VOLUMEDOWN", "SLEEP",
+	"MEDIANEXT", "MEDIAPREV", "MEDIAPLAY", "MEDIASTOP",
+	"BROWSERPREV", "BROWSERNEXT", "BROWSERREFRESH", "BROWSERSTOP", "BROWSERSEARCH", "BROWSERFAVORITES", "BROWSERHOME",
+	"LAUNCHMAIL", "LAUNCHMEDIA", "LAUNCHAPP1", "LAUNCHCALC", 
+
+	Pad_Names
+};
+
+void Input_SetPressed(int key) {
+	cc_bool wasPressed = Input.Pressed[key];
+	Input.Pressed[key] = true;
+	Event_RaiseInput(&InputEvents.Down, key, wasPressed);
+
+	if (key == 'C' && Input_IsActionPressed()) Event_RaiseInput(&InputEvents.Down, INPUT_CLIPBOARD_COPY,  0);
+	if (key == 'V' && Input_IsActionPressed()) Event_RaiseInput(&InputEvents.Down, INPUT_CLIPBOARD_PASTE, 0);
+
+	/* don't allow multiple left mouse down events */
+	if (key != CCMOUSE_L || wasPressed) return;
+	Pointer_SetPressed(0, true);
+}
+
+void Input_SetReleased(int key) {
+	if (!Input.Pressed[key]) return;
+	Input.Pressed[key] = false;
+
+	Event_RaiseInt(&InputEvents.Up, key);
+	if (key == CCMOUSE_L) Pointer_SetPressed(0, false);
+}
+
+void Input_Set(int key, int pressed) {
+	if (pressed) {
+		Input_SetPressed(key);
+	} else {
+		Input_SetReleased(key);
+	}
+}
+
+void Input_SetNonRepeatable(int key, int pressed) {
+	if (pressed) {
+		if (Input.Pressed[key]) return;
+		Input_SetPressed(key);
+	} else {
+		Input_SetReleased(key);
+	}
+}
+
+void Input_Clear(void) {
+	int i;
+	for (i = 0; i < INPUT_COUNT; i++) 
+	{
+		if (Input.Pressed[i]) Input_SetReleased(i);
+	}
+	/* TODO: Properly release instead of just clearing */
+	ClearTouches();
+}
+
+int Input_CalcDelta(int key, int horDelta, int verDelta) {
+	if (Input_IsLeftButton(key)  || key == CCKEY_KP4) return -horDelta;
+	if (Input_IsRightButton(key) || key == CCKEY_KP6) return +horDelta;
+	if (Input_IsUpButton(key)    || key == CCKEY_KP8) return -verDelta;
+	if (Input_IsDownButton(key)  || key == CCKEY_KP2) return +verDelta;
+	
+	return 0;
+}
+
+
+/*########################################################################################################################*
+*----------------------------------------------------------Mouse----------------------------------------------------------*
+*#########################################################################################################################*/
+struct Pointer Pointers[INPUT_MAX_POINTERS];
+
+void Pointer_SetPressed(int idx, cc_bool pressed) {
+	if (pressed) {
+		Event_RaiseInt(&PointerEvents.Down, idx);
+	} else {
+		Event_RaiseInt(&PointerEvents.Up,   idx);
+	}
+}
+
+static float scrollingVAcc;
+void Mouse_ScrollVWheel(float delta) {
+	int steps = Utils_AccumulateWheelDelta(&scrollingVAcc, delta);
+	Event_RaiseFloat(&InputEvents.Wheel, delta);
+	
+	if (steps > 0) {
+		for (; steps != 0; steps--) 
+			Input_SetPressed(CCWHEEL_UP);
+		Input_SetReleased(CCWHEEL_UP);
+	} else if (steps < 0) {
+		for (; steps != 0; steps++) 
+			Input_SetPressed(CCWHEEL_DOWN);
+		Input_SetReleased(CCWHEEL_DOWN);
+	}
+}
+
+static float scrollingHAcc;
+void Mouse_ScrollHWheel(float delta) {
+	int steps = Utils_AccumulateWheelDelta(&scrollingHAcc, delta);
+	
+	if (steps > 0) {
+		for (; steps != 0; steps--) 
+			Input_SetPressed(CCWHEEL_RIGHT);
+		Input_SetReleased(CCWHEEL_RIGHT);
+	} else if (steps < 0) {
+		for (; steps != 0; steps++) 
+			Input_SetPressed(CCWHEEL_LEFT);
+		Input_SetReleased(CCWHEEL_LEFT);
+	}
+}
+
+void Pointer_SetPosition(int idx, int x, int y) {
+	if (x == Pointers[idx].x && y == Pointers[idx].y) return;
+	/* TODO: reset to -1, -1 when pointer is removed */
+	Pointers[idx].x = x; Pointers[idx].y = y;
+	
+#ifdef CC_BUILD_TOUCH
+	if (Input_TouchMode && !(touches[idx].type & TOUCH_TYPE_GUI)) return;
+#endif
+	Event_RaiseInt(&PointerEvents.Moved, idx);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------Keybinds--------------------------------------------------------*
+*#########################################################################################################################*/
+BindMapping PadBind_Mappings[BIND_COUNT];
+BindMapping KeyBind_Mappings[BIND_COUNT];
+BindTriggered Bind_OnTriggered[BIND_COUNT];
+BindReleased  Bind_OnReleased[BIND_COUNT];
+
+const BindMapping PadBind_Defaults[BIND_COUNT] = {
+	{ CCPAD_UP,   0 },  { CCPAD_DOWN,  0 }, /* BIND_FORWARD, BIND_BACK */
+	{ CCPAD_LEFT, 0 },  { CCPAD_RIGHT, 0 }, /* BIND_LEFT, BIND_RIGHT */
+	{ CCPAD_A, 0 },     { 0, 0 },           /* BIND_JUMP, BIND_RESPAWN */
+	{ CCPAD_START, 0 }, { CCPAD_Y,     0 }, /* BIND_SET_SPAWN, BIND_CHAT */
+	{ CCPAD_X, 0     }, { 0, 0 },           /* BIND_INVENTORY, BIND_FOG */
+	{ CCPAD_START, 0 }, { 0, 0 },           /* BIND_SEND_CHAT, BIND_TABLIST */
+	{ CCPAD_B, CCPAD_L},{ CCPAD_B, CCPAD_X},/* BIND_SPEED, BIND_NOCLIP */ 
+	{ CCPAD_B, CCPAD_R },                   /* BIND_FLY */ 
+	{CCPAD_B,CCPAD_UP},{CCPAD_B,CCPAD_DOWN},/* BIND_FLY_UP, BIND_FLY_DOWN */
+	{ 0, 0 }, { 0, 0 },                     /* BIND_EXT_INPUT, BIND_HIDE_FPS */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_SCREENSHOT, BIND_FULLSCREEN, BIND_THIRD_PERSON, BIND_HIDE_GUI */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 },           /* BIND_AXIS_LINES, BIND_ZOOM_SCROLL, BIND_HALF_SPEED */
+	{ CCPAD_L, 0 }, { 0, 0 },{ CCPAD_R, 0 },/* BIND_DELETE_BLOCK, BIND_PICK_BLOCK, BIND_PLACE_BLOCK */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 },           /* BIND_AUTOROTATE, BIND_HOTBAR_SWITCH, BIND_SMOOTH_CAMERA */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 },           /* BIND_DROP_BLOCK, BIND_IDOVERLAY, BIND_BREAK_LIQUIDS */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_LOOK_UP, BIND_LOOK_DOWN, BIND_LOOK_RIGHT, BIND_LOOK_LEFT */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 },           /* BIND_HOTBAR_1, BIND_HOTBAR_2, BIND_HOTBAR_3 */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 },           /* BIND_HOTBAR_4, BIND_HOTBAR_5, BIND_HOTBAR_6 */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 },           /* BIND_HOTBAR_7, BIND_HOTBAR_8, BIND_HOTBAR_9 */
+	{ CCPAD_ZL, 0 }, { CCPAD_ZR, 0 }        /* BIND_HOTBAR_LEFT, BIND_HOTBAR_RIGHT */
+};
+
+const BindMapping KeyBind_Defaults[BIND_COUNT] = {
+	{ 'W', 0 }, { 'S', 0 }, { 'A', 0 }, { 'D', 0 }, /* BIND_FORWARD - BIND_RIGHT */
+	{ CCKEY_SPACE, 0 },  { 'R', 0 },                /* BIND_JUMP, BIND_RESPAWN */
+	{ CCKEY_ENTER, 0 },  { 'T', 0 },                /* BIND_SET_SPAWN, BIND_CHAT */
+	{ 'B', 0 },          { 'F', 0 },                /* BIND_INVENTORY, BIND_FOG */
+	{ CCKEY_ENTER, 0 },  { CCKEY_TAB, 0 },          /* BIND_SEND_CHAT, BIND_TABLIST */
+	{ CCKEY_LSHIFT, 0 }, { 'X', 0}, { 'Z', 0 },     /* BIND_SPEED, BIND_NOCLIP, BIND_FLY */ 
+	{ 'Q', 0 },          { 'E', 0 },                /* BIND_FLY_UP, BIND_FLY_DOWN */
+	{ CCKEY_LALT, 0 },   { CCKEY_F3, 0 },           /* BIND_EXT_INPUT, BIND_HIDE_FPS */
+	{ CCKEY_F12, 0 },    { CCKEY_F11, 0 },          /* BIND_SCREENSHOT, BIND_FULLSCREEN */
+	{ CCKEY_F5, 0 },     { CCKEY_F1, 0 },           /* BIND_THIRD_PERSON, BIND_HIDE_GUI */ 
+	{ CCKEY_F7, 0 }, { 'C', 0 }, { CCKEY_LCTRL, 0 },/* BIND_AXIS_LINES, BIND_ZOOM_SCROLL, BIND_HALF_SPEED */
+	{ CCMOUSE_L, 0},{ CCMOUSE_M, 0},{ CCMOUSE_R, 0},/* BIND_DELETE_BLOCK, BIND_PICK_BLOCK, BIND_PLACE_BLOCK */
+	{ CCKEY_F6, 0 },     { CCKEY_LALT, 0 },         /* BIND_AUTOROTATE, BIND_HOTBAR_SWITCH */
+	{ CCKEY_F8, 0 },     { 'G', 0 },                /* BIND_SMOOTH_CAMERA, BIND_DROP_BLOCK */
+	{ CCKEY_F10, 0 },    { 0, 0 },                  /* BIND_IDOVERLAY, BIND_BREAK_LIQUIDS */
+	{ 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 },         /* BIND_LOOK_UP, BIND_LOOK_DOWN, BIND_LOOK_RIGHT, BIND_LOOK_LEFT */
+	{ '1', 0 }, { '2', 0 }, { '3', 0 },             /* BIND_HOTBAR_1, BIND_HOTBAR_2, BIND_HOTBAR_3 */
+	{ '4', 0 }, { '5', 0 }, { '6', 0 },             /* BIND_HOTBAR_4, BIND_HOTBAR_5, BIND_HOTBAR_6 */
+	{ '7', 0 }, { '8', 0 }, { '9', 0 },             /* BIND_HOTBAR_7, BIND_HOTBAR_8, BIND_HOTBAR_9 */
+	{ 0, 0 }, { 0, 0 }                              /* BIND_HOTBAR_LEFT, BIND_HOTBAR_RIGHT */
+};
+
+static const char* const bindNames[BIND_COUNT] = {
+	"Forward", "Back", "Left", "Right",
+	"Jump", "Respawn", "SetSpawn", "Chat", "Inventory", 
+	"ToggleFog", "SendChat", "PlayerList", 
+	"Speed", "NoClip", "Fly", "FlyUp", "FlyDown", 
+	"ExtInput", "HideFPS", "Screenshot", "Fullscreen", 
+	"ThirdPerson", "HideGUI", "AxisLines", "ZoomScrolling", 
+	"HalfSpeed", "DeleteBlock", "PickBlock", "PlaceBlock", 
+	"AutoRotate", "HotbarSwitching", "SmoothCamera", 
+	"DropBlock", "IDOverlay", "BreakableLiquids",
+	"LookUp", "LookDown", "LookRight", "LookLeft",
+	"Hotbar1", "Hotbar2", "Hotbar3",
+	"Hotbar4", "Hotbar5", "Horbar6",
+	"Hotbar7", "Hotbar8", "Hotbar9",
+	"HotbarLeft", "HotbarRight"
+};
+
+
+#define BindMapping2_Claims(mapping, btn) (Input.Pressed[(mapping)->button1] && (mapping)->button2 == btn)
+static cc_bool Mappings_DoesClaim(InputBind binding, int btn, BindMapping* mappings) {
+	BindMapping* bind = &mappings[binding];
+	int i;
+	if (bind->button2) return BindMapping2_Claims(bind, btn);
+	
+	/* Two button mapping takes priority over one button mapping */
+	for (i = 0; i < BIND_COUNT; i++)
+	{
+		if (mappings[i].button2 && BindMapping2_Claims(&mappings[i], btn)) return false;
+	}
+	return bind->button1 == btn;
+}
+
+static cc_bool Mappings_IsPressed(InputBind binding, BindMapping* mappings) {
+	BindMapping* bind = &mappings[binding];
+	int btn = bind->button1;
+	int i;
+	
+	if (!Input.Pressed[btn]) return false;
+	if (bind->button2) return Input.Pressed[bind->button2];
+	
+	/* Two button mappings to the button takes priority one button mapping */
+	for (i = 0; i < BIND_COUNT; i++)
+	{	
+		bind = &mappings[i];
+		if (!bind->button2) continue;
+		if (!(bind->button1 == btn || bind->button2 == btn)) continue;
+		
+		if (Input.Pressed[bind->button1] && Input.Pressed[bind->button2]) return false;
+	}
+	return true;
+}
+
+
+cc_bool InputBind_Claims(InputBind binding, int btn) { 
+	return Mappings_DoesClaim(binding, btn, KeyBind_Mappings) || 
+		   Mappings_DoesClaim(binding, btn, PadBind_Mappings);
+}
+
+cc_bool InputBind_IsPressed(InputBind binding) {
+	return Mappings_IsPressed(binding, KeyBind_Mappings) || 
+		   Mappings_IsPressed(binding, PadBind_Mappings);
+}
+
+static void KeyBind_Load(const char* prefix, BindMapping* keybinds, const BindMapping* defaults) {
+	cc_string name; char nameBuffer[STRING_SIZE + 1];
+	BindMapping mapping;
+	cc_string str, part1, part2;
+	int i;
+
+	String_InitArray_NT(name, nameBuffer);
+	for (i = 0; i < BIND_COUNT; i++) 
+	{
+		name.length = 0;
+		String_Format1(&name, prefix, bindNames[i]);
+		name.buffer[name.length] = '\0';
+		
+		if (!Options_UNSAFE_Get(name.buffer, &str)) {
+			keybinds[i] = defaults[i];
+			continue;
+		}
+
+		String_UNSAFE_Separate(&str, ',', &part1, &part2); 
+		mapping.button1 = Utils_ParseEnum(&part1, defaults[i].button1, storageNames, INPUT_COUNT);
+		mapping.button2 = Utils_ParseEnum(&part2, defaults[i].button2, storageNames, INPUT_COUNT);
+		
+		if (mapping.button1 == CCKEY_ESCAPE) mapping = defaults[i];
+		keybinds[i] = mapping;
+	}
+}
+
+static void InputBind_Set(InputBind binding, int btn, BindMapping* binds, const char* fmt) {
+	cc_string name; char nameBuffer[STRING_SIZE];
+	cc_string value;
+	String_InitArray(name, nameBuffer);
+
+	String_Format1(&name, fmt, bindNames[binding]);
+	value = String_FromReadonly(storageNames[btn]);
+	Options_SetString(&name, &value);
+	
+	BindMapping_Set(&binds[binding], btn, 0);
+}
+
+void KeyBind_Set(InputBind binding, int btn) {
+	InputBind_Set(binding, btn, KeyBind_Mappings, "key-%c");
+}
+
+void PadBind_Set(InputBind binding, int btn) {
+	InputBind_Set(binding, btn, PadBind_Mappings, "pad-%c");
+}
+
+static void InputBind_ResetOption(InputBind binding, const char* fmt) {
+	cc_string name; char nameBuffer[STRING_SIZE];
+	String_InitArray(name, nameBuffer);
+	
+	String_Format1(&name, fmt, bindNames[binding]);
+	Options_SetString(&name, &String_Empty);
+}
+
+void KeyBind_Reset(InputBind binding) {
+	InputBind_ResetOption(binding, "key-%c");
+	KeyBind_Mappings[binding] = KeyBind_Defaults[binding];
+}
+
+void PadBind_Reset(InputBind binding) {
+	InputBind_ResetOption(binding, "pad-%c");
+	PadBind_Mappings[binding] = PadBind_Defaults[binding];
+}
+
+/* Initialises and loads input bindings from options */
+static void KeyBind_Init(void) {
+	KeyBind_Load("key-%c", KeyBind_Mappings, KeyBind_Defaults);
+	KeyBind_Load("pad-%c", PadBind_Mappings, PadBind_Defaults);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------Gamepad---------------------------------------------------------*
+*#########################################################################################################################*/
+#define GAMEPAD_BEG_BTN CCPAD_A
+#define GAMEPAD_BTN_COUNT (INPUT_COUNT - GAMEPAD_BEG_BTN)
+
+int Gamepad_AxisBehaviour[2]   = { AXIS_BEHAVIOUR_MOVEMENT, AXIS_BEHAVIOUR_CAMERA };
+int Gamepad_AxisSensitivity[2] = { AXIS_SENSI_NORMAL, AXIS_SENSI_NORMAL };
+static const float axis_sensiFactor[] = { 0.25f, 0.5f, 1.0f, 2.0f, 4.0f };
+
+struct GamepadState {
+	float axisX[2], axisY[2];
+	cc_bool pressed[GAMEPAD_BTN_COUNT];
+	float holdtime[GAMEPAD_BTN_COUNT];
+};
+static struct GamepadState gamepads[INPUT_MAX_GAMEPADS];
+
+static void Gamepad_Update(struct GamepadState* pad, float delta) {
+	int btn;
+	for (btn = 0; btn < GAMEPAD_BTN_COUNT; btn++)
+	{
+		if (!pad->pressed[btn]) continue;
+		pad->holdtime[btn] += delta;
+		if (pad->holdtime[btn] < 1.0f) continue;
+
+		/* Held for over a second, trigger a fake press */
+		pad->holdtime[btn] = 0;
+		Input_SetPressed(btn + GAMEPAD_BEG_BTN);
+	}
+}
+
+
+void Gamepad_SetButton(int port, int btn, int pressed) {
+	struct GamepadState* pad = &gamepads[port];
+	int i;
+	btn -= GAMEPAD_BEG_BTN;
+
+	/* Reset hold tracking time */
+	if (pressed && !pad->pressed[btn]) pad->holdtime[btn] = 0;
+	pad->pressed[btn] = pressed != 0;;
+
+	/* Set pressed if button pressed on any gamepad, to avoid constant flip flopping */
+	/*  between pressed and non-pressed when multiple controllers are plugged in */
+	for (i = 0; i < INPUT_MAX_GAMEPADS; i++) 
+		pressed |= gamepads[i].pressed[btn];
+
+	Input_SetNonRepeatable(btn + GAMEPAD_BEG_BTN, pressed);
+}
+
+void Gamepad_SetAxis(int port, int axis, float x, float y, float delta) {
+	gamepads[port].axisX[axis] = x;
+	gamepads[port].axisY[axis] = y;
+	if (x == 0 && y == 0) return;
+
+	int sensi   = Gamepad_AxisSensitivity[axis];
+	float scale = delta * 60.0f * axis_sensiFactor[sensi];
+	Event_RaisePadAxis(&ControllerEvents.AxisUpdate, port, axis, x * scale, y * scale);
+}
+
+void Gamepad_Tick(float delta) {
+	int port;
+	Window_ProcessGamepads(delta);
+	
+	for (port = 0; port < INPUT_MAX_GAMEPADS; port++)
+	{
+		Gamepad_Update(&gamepads[port], delta);
+	}
+}
+
+static void PlayerInputPad(int port, int axis, struct LocalPlayer* p, float* xMoving, float* zMoving) {
+	float x, y, angle;
+	if (Gamepad_AxisBehaviour[axis] != AXIS_BEHAVIOUR_MOVEMENT) return;
+	
+	x = gamepads[port].axisX[axis];
+	y = gamepads[port].axisY[axis];
+	
+	if (x != 0 || y != 0) {
+		angle    = Math_Atan2f(x, y);
+		*xMoving = Math_CosF(angle);
+		*zMoving = Math_SinF(angle);
+	}
+}
+
+static void PlayerInputGamepad(struct LocalPlayer* p, float* xMoving, float* zMoving) {
+	int port;
+	for (port = 0; port < INPUT_MAX_GAMEPADS; port++)
+	{
+		/* In splitscreen mode, tie a controller to a specific player*/
+		if (Game_NumLocalPlayers > 1 && p->index != port) continue;
+		
+		PlayerInputPad(port, PAD_AXIS_LEFT,  p, xMoving, zMoving);
+		PlayerInputPad(port, PAD_AXIS_RIGHT, p, xMoving, zMoving);
+	}
+}
+static struct LocalPlayerInput gamepadInput = { PlayerInputGamepad };
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------Hotkeys---------------------------------------------------------*
+*#########################################################################################################################*/
+const cc_uint8 Hotkeys_LWJGL[256] = {
+	0, CCKEY_ESCAPE, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', CCKEY_MINUS, CCKEY_EQUALS, CCKEY_BACKSPACE, CCKEY_TAB,
+	'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', CCKEY_LBRACKET, CCKEY_RBRACKET, CCKEY_ENTER, CCKEY_LCTRL, 'A', 'S',
+	'D', 'F', 'G', 'H', 'J', 'K', 'L', CCKEY_SEMICOLON, CCKEY_QUOTE, CCKEY_TILDE, CCKEY_LSHIFT, CCKEY_BACKSLASH, 'Z', 'X', 'C', 'V',
+	'B', 'N', 'M', CCKEY_COMMA, CCKEY_PERIOD, CCKEY_SLASH, CCKEY_RSHIFT, 0, CCKEY_LALT, CCKEY_SPACE, CCKEY_CAPSLOCK, CCKEY_F1, CCKEY_F2, CCKEY_F3, CCKEY_F4, CCKEY_F5,
+	CCKEY_F6, CCKEY_F7, CCKEY_F8, CCKEY_F9, CCKEY_F10, CCKEY_NUMLOCK, CCKEY_SCROLLLOCK, CCKEY_KP7, CCKEY_KP8, CCKEY_KP9, CCKEY_KP_MINUS, CCKEY_KP4, CCKEY_KP5, CCKEY_KP6, CCKEY_KP_PLUS, CCKEY_KP1,
+	CCKEY_KP2, CCKEY_KP3, CCKEY_KP0, CCKEY_KP_DECIMAL, 0, 0, 0, CCKEY_F11, CCKEY_F12, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, CCKEY_F13, CCKEY_F14, CCKEY_F15, CCKEY_F16, CCKEY_F17, CCKEY_F18, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CCKEY_KP_PLUS, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CCKEY_KP_ENTER, CCKEY_RCTRL, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, CCKEY_KP_DIVIDE, 0, 0, CCKEY_RALT, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, CCKEY_PAUSE, 0, CCKEY_HOME, CCKEY_UP, CCKEY_PAGEUP, 0, CCKEY_LEFT, 0, CCKEY_RIGHT, 0, CCKEY_END,
+	CCKEY_DOWN, CCKEY_PAGEDOWN, CCKEY_INSERT, CCKEY_DELETE, 0, 0, 0, 0, 0, 0, 0, CCKEY_LWIN, CCKEY_RWIN, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+struct HotkeyData HotkeysList[HOTKEYS_MAX_COUNT];
+struct StringsBuffer HotkeysText;
+
+static void Hotkeys_QuickSort(int left, int right) {
+	struct HotkeyData* keys = HotkeysList; struct HotkeyData key;
+
+	while (left < right) {
+		int i = left, j = right;
+		cc_uint8 pivot = keys[(i + j) >> 1].mods;
+
+		/* partition the list */
+		while (i <= j) {
+			while (pivot < keys[i].mods) i++;
+			while (pivot > keys[j].mods) j--;
+			QuickSort_Swap_Maybe();
+		}
+		/* recurse into the smaller subset */
+		QuickSort_Recurse(Hotkeys_QuickSort)
+	}
+}
+
+static void Hotkeys_AddNewHotkey(int trigger, cc_uint8 modifiers, const cc_string* text, cc_uint8 flags) {
+	struct HotkeyData hKey;
+	hKey.trigger = trigger;
+	hKey.mods    = modifiers;
+	hKey.textIndex = HotkeysText.count;
+	hKey.flags   = flags;
+
+	if (HotkeysText.count == HOTKEYS_MAX_COUNT) {
+		Chat_AddRaw("&cCannot define more than 256 hotkeys");
+		return;
+	}
+
+	HotkeysList[HotkeysText.count] = hKey;
+	StringsBuffer_Add(&HotkeysText, text);
+	/* sort so that hotkeys with largest modifiers are first */
+	Hotkeys_QuickSort(0, HotkeysText.count - 1);
+}
+
+static void Hotkeys_RemoveText(int index) {
+	 struct HotkeyData* hKey = HotkeysList;
+	 int i;
+
+	for (i = 0; i < HotkeysText.count; i++, hKey++) {
+		if (hKey->textIndex >= index) hKey->textIndex--;
+	}
+	StringsBuffer_Remove(&HotkeysText, index);
+}
+
+
+void Hotkeys_Add(int trigger, cc_uint8 modifiers, const cc_string* text, cc_uint8 flags) {
+	struct HotkeyData* hk = HotkeysList;
+	int i;
+
+	for (i = 0; i < HotkeysText.count; i++, hk++) {		
+		if (hk->trigger != trigger || hk->mods != modifiers) continue;
+		Hotkeys_RemoveText(hk->textIndex);
+
+		hk->flags     = flags;
+		hk->textIndex = HotkeysText.count;
+		StringsBuffer_Add(&HotkeysText, text);
+		return;
+	}
+	Hotkeys_AddNewHotkey(trigger, modifiers, text, flags);
+}
+
+cc_bool Hotkeys_Remove(int trigger, cc_uint8 modifiers) {
+	struct HotkeyData* hk = HotkeysList;
+	int i, j;
+
+	for (i = 0; i < HotkeysText.count; i++, hk++) {
+		if (hk->trigger != trigger || hk->mods != modifiers) continue;
+		Hotkeys_RemoveText(hk->textIndex);
+
+		for (j = i; j < HotkeysText.count; j++) {
+			HotkeysList[j] = HotkeysList[j + 1];
+		}
+		return true;
+	}
+	return false;
+}
+
+int Hotkeys_FindPartial(int key) {
+	struct HotkeyData hk;
+	int i, modifiers = 0;
+
+	if (Input_IsCtrlPressed())  modifiers |= HOTKEY_MOD_CTRL;
+	if (Input_IsShiftPressed()) modifiers |= HOTKEY_MOD_SHIFT;
+	if (Input_IsAltPressed())   modifiers |= HOTKEY_MOD_ALT;
+
+	for (i = 0; i < HotkeysText.count; i++) {
+		hk = HotkeysList[i];
+		/* e.g. if holding Ctrl and Shift, a hotkey with only Ctrl modifiers matches */
+		if ((hk.mods & modifiers) == hk.mods && hk.trigger == key) return i;
+	}
+	return -1;
+}
+
+static const cc_string prefix = String_FromConst("hotkey-");
+static void StoredHotkey_Parse(cc_string* key, cc_string* value) {
+	cc_string strKey, strMods, strMore, strText;
+	int trigger;
+	cc_uint8 modifiers;
+	cc_bool more;
+
+	/* Format is: key&modifiers = more-input&text */
+	key->length -= prefix.length; key->buffer += prefix.length;
+	
+	if (!String_UNSAFE_Separate(key,   '&', &strKey,  &strMods)) return;
+	if (!String_UNSAFE_Separate(value, '&', &strMore, &strText)) return;
+	
+	trigger = Utils_ParseEnum(&strKey, INPUT_NONE, storageNames, INPUT_COUNT);
+	if (trigger == INPUT_NONE) return; 
+	if (!Convert_ParseUInt8(&strMods, &modifiers)) return;
+	if (!Convert_ParseBool(&strMore,  &more))      return;
+	
+	Hotkeys_Add(trigger, modifiers, &strText, more);
+}
+
+static void StoredHotkeys_LoadAll(void) {
+	cc_string entry, key, value;
+	int i;
+
+	for (i = 0; i < Options.count; i++) {
+		StringsBuffer_UNSAFE_GetRaw(&Options, i, &entry);
+		String_UNSAFE_Separate(&entry, '=', &key, &value);
+
+		if (!String_CaselessStarts(&key, &prefix)) continue;
+		StoredHotkey_Parse(&key, &value);
+	}
+}
+
+void StoredHotkeys_Load(int trigger, cc_uint8 modifiers) {
+	cc_string key, value; char keyBuffer[STRING_SIZE];
+	String_InitArray(key, keyBuffer);
+
+	String_Format2(&key, "hotkey-%c&%b", storageNames[trigger], &modifiers);
+	key.buffer[key.length] = '\0'; /* TODO: Avoid this null terminator */
+
+	Options_UNSAFE_Get(key.buffer, &value);
+	StoredHotkey_Parse(&key, &value);
+}
+
+void StoredHotkeys_Remove(int trigger, cc_uint8 modifiers) {
+	cc_string key; char keyBuffer[STRING_SIZE];
+	String_InitArray(key, keyBuffer);
+
+	String_Format2(&key, "hotkey-%c&%b", storageNames[trigger], &modifiers);
+	Options_SetString(&key, NULL);
+}
+
+void StoredHotkeys_Add(int trigger, cc_uint8 modifiers, cc_bool moreInput, const cc_string* text) {
+	cc_string key;   char keyBuffer[STRING_SIZE];
+	cc_string value; char valueBuffer[STRING_SIZE * 2];
+	String_InitArray(key, keyBuffer);
+	String_InitArray(value, valueBuffer);
+
+	String_Format2(&key, "hotkey-%c&%b", storageNames[trigger], &modifiers);
+	String_Format2(&value, "%t&%s", &moreInput, text);
+	Options_SetString(&key, &value);
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------Mouse helpers-------------------------------------------------------*
+*#########################################################################################################################*/
+static void MouseStateUpdate(int button, cc_bool pressed) {
+	struct Entity* p;
+	input_buttonsDown[button] = pressed;
+	if (!Server.SupportsPlayerClick) return;
+
+	/* defer getting the targeted entity, as it's a costly operation */
+	if (input_pickingId == -1) {
+		p = &Entities.CurPlayer->Base;
+		input_pickingId = Entities_GetClosest(p);
+		
+		if (input_pickingId == -1) 
+			input_pickingId = ENTITIES_SELF_ID;
+	}
+
+	
+	CPE_SendPlayerClick(button, pressed, (EntityID)input_pickingId, &Game_SelectedPos);	
+}
+
+static void MouseStatePress(int button) {
+	input_lastClick = Game.Time;
+	input_pickingId = -1;
+	MouseStateUpdate(button, true);
+}
+
+static void MouseStateRelease(int button) {
+	input_pickingId = -1;
+	if (!input_buttonsDown[button]) return;
+	MouseStateUpdate(button, false);
+}
+
+void InputHandler_OnScreensChanged(void) {
+	input_lastClick = Game.Time;
+	input_pickingId = -1;
+	if (!Gui.InputGrab) return;
+
+	/* If input is grabbed, then the mouse isn't used for picking blocks in world anymore. */
+	/* So release all mouse buttons, since game stops sending PlayerClick during grabbed input */
+	MouseStateRelease(MOUSE_LEFT);
+	MouseStateRelease(MOUSE_RIGHT);
+	MouseStateRelease(MOUSE_MIDDLE);
+}
+
+static cc_bool TouchesSolid(BlockID b) { return Blocks.Collide[b] == COLLIDE_SOLID; }
+static cc_bool PushbackPlace(struct AABB* blockBB) {
+	struct Entity* p        = &Entities.CurPlayer->Base;
+	struct HacksComp* hacks = &Entities.CurPlayer->Hacks;
+	Face closestFace;
+	cc_bool insideMap;
+
+	Vec3 pos = p->Position;
+	struct AABB playerBB;
+	struct LocationUpdate update;
+
+	/* Offset position by the closest face */
+	closestFace = Game_SelectedPos.closest;
+	if (closestFace == FACE_XMAX) {
+		pos.x = blockBB->Max.x + 0.5f;
+	} else if (closestFace == FACE_ZMAX) {
+		pos.z = blockBB->Max.z + 0.5f;
+	} else if (closestFace == FACE_XMIN) {
+		pos.x = blockBB->Min.x - 0.5f;
+	} else if (closestFace == FACE_ZMIN) {
+		pos.z = blockBB->Min.z - 0.5f;
+	} else if (closestFace == FACE_YMAX) {
+		pos.y = blockBB->Min.y + 1 + ENTITY_ADJUSTMENT;
+	} else if (closestFace == FACE_YMIN) {
+		pos.y = blockBB->Min.y - p->Size.y - ENTITY_ADJUSTMENT;
+	}
+
+	/* Exclude exact map boundaries, otherwise player can get stuck outside map */
+	/* Being vertically above the map is acceptable though */
+	insideMap =
+		pos.x > 0.0f && pos.y >= 0.0f && pos.z > 0.0f &&
+		pos.x < World.Width && pos.z < World.Length;
+	if (!insideMap) return false;
+
+	AABB_Make(&playerBB, &pos, &p->Size);
+	if (!hacks->Noclip && Entity_TouchesAny(&playerBB, TouchesSolid)) {
+		/* Don't put player inside another block */
+		return false;
+	}
+
+	update.flags = LU_HAS_POS | LU_POS_ABSOLUTE_INSTANT;
+	update.pos   = pos;
+	p->VTABLE->SetLocation(p, &update);
+	return true;
+}
+
+static cc_bool IntersectsOthers(Vec3 pos, BlockID block) {
+	struct AABB blockBB, entityBB;
+	struct Entity* e;
+	int id;
+
+	Vec3_Add(&blockBB.Min, &pos, &Blocks.MinBB[block]);
+	Vec3_Add(&blockBB.Max, &pos, &Blocks.MaxBB[block]);
+	
+	for (id = 0; id < ENTITIES_MAX_COUNT; id++)	
+	{
+		e = Entities.List[id];
+		if (!e || e == &Entities.CurPlayer->Base) continue;
+
+		Entity_GetBounds(e, &entityBB);
+		entityBB.Min.y += 1.0f / 32.0f; /* when player is exactly standing on top of ground */
+		if (AABB_Intersects(&entityBB, &blockBB)) return true;
+	}
+	return false;
+}
+
+static cc_bool CheckIsFree(BlockID block) {
+	struct Entity* p        = &Entities.CurPlayer->Base;
+	struct HacksComp* hacks = &Entities.CurPlayer->Hacks;
+
+	Vec3 pos, nextPos;
+	struct AABB blockBB, playerBB;
+	struct LocationUpdate update;
+
+	/* Non solid blocks (e.g. water/flowers) can always be placed on players */
+	if (Blocks.Collide[block] != COLLIDE_SOLID) return true;
+
+	IVec3_ToVec3(&pos, &Game_SelectedPos.translatedPos);
+	if (IntersectsOthers(pos, block)) return false;
+	
+	nextPos = p->next.pos;
+	Vec3_Add(&blockBB.Min, &pos, &Blocks.MinBB[block]);
+	Vec3_Add(&blockBB.Max, &pos, &Blocks.MaxBB[block]);
+
+	/* NOTE: Need to also test against next position here, otherwise player can */
+	/* fall through the block at feet as collision is performed against nextPos */
+	Entity_GetBounds(p, &playerBB);
+	playerBB.Min.y = min(nextPos.y, playerBB.Min.y);
+
+	if (hacks->Noclip || !AABB_Intersects(&playerBB, &blockBB)) return true;
+	if (hacks->CanPushbackBlocks && hacks->PushbackPlacing && hacks->Enabled) {
+		return PushbackPlace(&blockBB);
+	}
+
+	playerBB.Min.y += 0.25f + ENTITY_ADJUSTMENT;
+	if (AABB_Intersects(&playerBB, &blockBB)) return false;
+
+	/* Push player upwards when they are jumping and trying to place a block underneath them */
+	nextPos.y = pos.y + Blocks.MaxBB[block].y + ENTITY_ADJUSTMENT;
+
+	update.flags = LU_HAS_POS | LU_POS_ABSOLUTE_INSTANT;
+	update.pos   = nextPos;
+	p->VTABLE->SetLocation(p, &update);
+	return true;
+}
+
+void InputHandler_DeleteBlock(void) {
+	IVec3 pos;
+	BlockID old;
+	/* always play delete animations, even if we aren't deleting a block */
+	HeldBlockRenderer_ClickAnim(true);
+
+	pos = Game_SelectedPos.pos;
+	if (!Game_SelectedPos.valid || !World_Contains(pos.x, pos.y, pos.z)) return;
+
+	old = World_GetBlock(pos.x, pos.y, pos.z);
+	if (Blocks.Draw[old] == DRAW_GAS || !Blocks.CanDelete[old]) return;
+
+	Game_ChangeBlock(pos.x, pos.y, pos.z, BLOCK_AIR);
+	Event_RaiseBlock(&UserEvents.BlockChanged, pos, old, BLOCK_AIR);
+}
+
+void InputHandler_PlaceBlock(void) {
+	IVec3 pos;
+	BlockID old, block;
+	pos = Game_SelectedPos.translatedPos;
+	if (!Game_SelectedPos.valid || !World_Contains(pos.x, pos.y, pos.z)) return;
+
+	old   = World_GetBlock(pos.x, pos.y, pos.z);
+	block = Inventory_SelectedBlock;
+	if (AutoRotate_Enabled) block = AutoRotate_RotateBlock(block);
+
+	if (Game_CanPick(old) || !Blocks.CanPlace[block]) return;
+	/* air-ish blocks can only replace over other air-ish blocks */
+	if (Blocks.Draw[block] == DRAW_GAS && Blocks.Draw[old] != DRAW_GAS) return;
+
+	/* undeletable gas blocks can't be replaced with other blocks */
+	if (Blocks.Collide[old] == COLLIDE_NONE && !Blocks.CanDelete[old]) return;
+
+	if (!CheckIsFree(block)) return;
+
+	Game_ChangeBlock(pos.x, pos.y, pos.z, block);
+	Event_RaiseBlock(&UserEvents.BlockChanged, pos, old, block);
+}
+
+void InputHandler_PickBlock(void) {
+	IVec3 pos;
+	BlockID cur;
+	pos = Game_SelectedPos.pos;
+	if (!World_Contains(pos.x, pos.y, pos.z)) return;
+
+	cur = World_GetBlock(pos.x, pos.y, pos.z);
+	if (Blocks.Draw[cur] == DRAW_GAS) return;
+	if (!(Blocks.CanPlace[cur] || Blocks.CanDelete[cur])) return;
+	Inventory_PickBlock(cur);
+}
+
+void InputHandler_Tick(void) {
+	cc_bool left, middle, right;
+	double now, delta;
+	
+	if (Gui.InputGrab) return;
+	now   = Game.Time;
+	delta = now - input_lastClick;
+
+	if (delta < 0.2495) return; /* 4 times per second */
+	/* NOTE: 0.2495 is used instead of 0.25 to produce delta time */
+	/*  values slightly closer to the old code which measured */
+	/*  elapsed time using DateTime_CurrentUTC_MS() instead */
+	input_lastClick = now;
+
+	left   = input_buttonsDown[MOUSE_LEFT];
+	middle = input_buttonsDown[MOUSE_MIDDLE];
+	right  = input_buttonsDown[MOUSE_RIGHT];
+	
+#ifdef CC_BUILD_TOUCH
+	if (Input_TouchMode) {
+		left   = (Input_HoldMode == INPUT_MODE_DELETE) && AnyBlockTouches();
+		right  = (Input_HoldMode == INPUT_MODE_PLACE)  && AnyBlockTouches();
+		middle = false;
+	}
+#endif
+
+	if (Server.SupportsPlayerClick) {
+		input_pickingId = -1;
+		if (left)   MouseStateUpdate(MOUSE_LEFT,   true);
+		if (right)  MouseStateUpdate(MOUSE_RIGHT,  true);
+		if (middle) MouseStateUpdate(MOUSE_MIDDLE, true);
+	}
+
+	if (left) {
+		InputHandler_DeleteBlock();
+	} else if (right) {
+		InputHandler_PlaceBlock();
+	} else if (middle) {
+		InputHandler_PickBlock();
+	}
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------Input helpers-------------------------------------------------------*
+*#########################################################################################################################*/
+static cc_bool InputHandler_IsShutdown(int key) {
+	if (key == CCKEY_F4 && Input_IsAltPressed()) return true;
+
+	/* On macOS, Cmd+Q should also end the process */
+#ifdef CC_BUILD_DARWIN
+	return key == 'Q' && Input_IsWinPressed();
+#else
+	return false;
+#endif
+}
+
+static void InputHandler_Toggle(int key, cc_bool* target, const char* enableMsg, const char* disableMsg) {
+	*target = !(*target);
+	if (*target) {
+		Chat_Add2("%c. &ePress &a%c &eto disable.",   enableMsg,  Input_DisplayNames[key]);
+	} else {
+		Chat_Add2("%c. &ePress &a%c &eto re-enable.", disableMsg, Input_DisplayNames[key]);
+	}
+}
+
+cc_bool InputHandler_SetFOV(int fov) {
+	struct HacksComp* h = &Entities.CurPlayer->Hacks;
+	if (!h->Enabled || !h->CanUseThirdPerson) return false;
+
+	Camera.ZoomFov = fov;
+	Camera_SetFov(fov);
+	return true;
+}
+
+cc_bool Input_HandleMouseWheel(float delta) {
+	struct HacksComp* h;
+	cc_bool hotbar;
+
+	hotbar = Input_IsAltPressed() || Input_IsCtrlPressed() || Input_IsShiftPressed();
+	if (!hotbar && Camera.Active->Zoom(delta))   return true;
+	if (!InputBind_IsPressed(BIND_ZOOM_SCROLL)) return false;
+
+	h = &Entities.CurPlayer->Hacks;
+	if (!h->Enabled || !h->CanUseThirdPerson) return false;
+
+	if (input_fovIndex == -1.0f) input_fovIndex = (float)Camera.ZoomFov;
+	input_fovIndex -= delta * 5.0f;
+
+	Math_Clamp(input_fovIndex, 1.0f, Camera.DefaultFov);
+	return InputHandler_SetFOV((int)input_fovIndex);
+}
+
+static void InputHandler_CheckZoomFov(void* obj) {
+	struct HacksComp* h = &Entities.CurPlayer->Hacks;
+	if (!h->Enabled || !h->CanUseThirdPerson) Camera_SetFov(Camera.DefaultFov);
+}
+
+
+static cc_bool BindTriggered_DeleteBlock(int key) {
+	MouseStatePress(MOUSE_LEFT);
+	InputHandler_DeleteBlock();
+	return true;
+}
+
+static cc_bool BindTriggered_PlaceBlock(int key) {
+	MouseStatePress(MOUSE_RIGHT);
+	InputHandler_PlaceBlock();
+	return true;
+}
+
+static cc_bool BindTriggered_PickBlock(int key) {
+	MouseStatePress(MOUSE_MIDDLE);
+	InputHandler_PickBlock();
+	return true;
+}
+
+static void BindReleased_DeleteBlock(int key) {
+	MouseStateRelease(MOUSE_LEFT);
+}
+
+static void BindReleased_PlaceBlock(int key) {
+	MouseStateRelease(MOUSE_RIGHT);
+}
+
+static void BindReleased_PickBlock(int key) {
+	MouseStateRelease(MOUSE_MIDDLE);
+}
+
+
+static cc_bool BindTriggered_HideFPS(int key) {
+	Gui.ShowFPS = !Gui.ShowFPS;
+	return true;
+}
+
+static cc_bool BindTriggered_Fullscreen(int key) {
+	Game_ToggleFullscreen();
+	return true;
+}
+
+static cc_bool BindTriggered_Fog(int key) {
+	Game_CycleViewDistance();
+	return true;
+}
+
+
+static cc_bool BindTriggered_HideGUI(int key) {
+	Game_HideGui = !Game_HideGui;
+	return true;
+}
+
+static cc_bool BindTriggered_SmoothCamera(int key) {
+	InputHandler_Toggle(key, &Camera.Smooth,
+		"  &eSmooth camera is &aenabled",
+		"  &eSmooth camera is &cdisabled");
+	return true;
+}
+
+static cc_bool BindTriggered_AxisLines(int key) {
+	InputHandler_Toggle(key, &AxisLinesRenderer_Enabled,
+		"  &eAxis lines (&4X&e, &2Y&e, &1Z&e) now show",
+		"  &eAxis lines no longer show");
+	return true;
+} 
+
+static cc_bool BindTriggered_AutoRotate(int key) {
+	InputHandler_Toggle(key, &AutoRotate_Enabled,
+		"  &eAuto rotate is &aenabled",
+		"  &eAuto rotate is &cdisabled");
+	return true;
+}
+
+static cc_bool BindTriggered_ThirdPerson(int key) {
+	Camera_CycleActive();
+	return true;
+}
+
+static cc_bool BindTriggered_DropBlock(int key) {
+	if (Inventory_CheckChangeSelected() && Inventory_SelectedBlock != BLOCK_AIR) {
+		/* Don't assign SelectedIndex directly, because we don't want held block
+		switching positions if they already have air in their inventory hotbar. */
+		Inventory_Set(Inventory.SelectedIndex, BLOCK_AIR);
+		Event_RaiseVoid(&UserEvents.HeldBlockChanged);
+	}
+	return true;
+}
+
+static cc_bool BindTriggered_IDOverlay(int key) {
+	TexIdsOverlay_Show();
+	return true;
+}
+
+static cc_bool BindTriggered_BreakLiquids(int key) {
+	InputHandler_Toggle(key, &Game_BreakableLiquids,
+		"  &eBreakable liquids is &aenabled",
+		"  &eBreakable liquids is &cdisabled");
+	return true;
+}
+
+static void HandleHotkeyDown(int key) {
+	struct HotkeyData* hkey;
+	cc_string text;
+	int i = Hotkeys_FindPartial(key);
+
+	if (i == -1) return;
+	hkey = &HotkeysList[i];
+	text = StringsBuffer_UNSAFE_Get(&HotkeysText, hkey->textIndex);
+
+	if (!(hkey->flags & HOTKEY_FLAG_STAYS_OPEN)) {
+		Chat_Send(&text, false);
+	} else if (!Gui.InputGrab) {
+		ChatScreen_OpenInput(&text);
+	}
+}
+
+static void HookInputBinds(void) {
+	Bind_OnTriggered[BIND_HIDE_FPS]   = BindTriggered_HideFPS;
+	Bind_OnTriggered[BIND_FULLSCREEN] = BindTriggered_Fullscreen;
+	Bind_OnTriggered[BIND_FOG]        = BindTriggered_Fog;
+
+	Bind_OnTriggered[BIND_DELETE_BLOCK] = BindTriggered_DeleteBlock;
+	Bind_OnTriggered[BIND_PLACE_BLOCK]  = BindTriggered_PlaceBlock;
+	Bind_OnTriggered[BIND_PICK_BLOCK]   = BindTriggered_PickBlock;
+
+	Bind_OnReleased[BIND_DELETE_BLOCK] = BindReleased_DeleteBlock;
+	Bind_OnReleased[BIND_PLACE_BLOCK]  = BindReleased_PlaceBlock;
+	Bind_OnReleased[BIND_PICK_BLOCK]   = BindReleased_PickBlock;
+
+	if (Game_ClassicMode) return;
+	Bind_OnTriggered[BIND_HIDE_GUI]      = BindTriggered_HideGUI;
+	Bind_OnTriggered[BIND_SMOOTH_CAMERA] = BindTriggered_SmoothCamera;
+	Bind_OnTriggered[BIND_AXIS_LINES]    = BindTriggered_AxisLines;
+	Bind_OnTriggered[BIND_AUTOROTATE]    = BindTriggered_AutoRotate;
+	Bind_OnTriggered[BIND_THIRD_PERSON]  = BindTriggered_ThirdPerson;
+	Bind_OnTriggered[BIND_DROP_BLOCK]    = BindTriggered_DropBlock;
+	Bind_OnTriggered[BIND_IDOVERLAY]     = BindTriggered_IDOverlay;
+	Bind_OnTriggered[BIND_BREAK_LIQUIDS] = BindTriggered_BreakLiquids;
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------Base handlers-------------------------------------------------------*
+*#########################################################################################################################*/
+static void OnPointerDown(void* obj, int idx) {
+	struct Screen* s;
+	int i, x, y, mask;
+#ifdef CC_BUILD_TOUCH
+	if (Input_TouchMode && !(touches[idx].type & TOUCH_TYPE_GUI)) return;
+#endif
+	x = Pointers[idx].x; y = Pointers[idx].y;
+
+	for (i = 0; i < Gui.ScreensCount; i++) {
+		s = Gui_Screens[i];
+		s->dirty = true;
+		mask = s->VTABLE->HandlesPointerDown(s, 1 << idx, x, y);
+
+#ifdef CC_BUILD_TOUCH
+		if (mask) {
+			/* Using &= mask instead of = mask is to handle one specific case */
+			/*  - when clicking 'Quit game' in android version, it will call  */
+			/*  Game_Free, which will in turn call InputComponent.Free.       */
+			/* That resets the type of all touches to 0 - however, since it is */
+			/*  called DURING HandlesPointerDown, using = mask here would undo */
+			/*  the resetting of type to 0 for one of the touches states,      */
+			/*  causing problems later with Input_AddTouch as it will assume that */
+			/*  the aforementioned touches state is wrongly still in use */
+			touches[idx].type &= mask; return;
+		}
+#else
+		if (mask) return;
+#endif
+	}
+}
+
+static void OnPointerUp(void* obj, int idx) {
+	struct Screen* s;
+	int i, x, y;
+#ifdef CC_BUILD_TOUCH
+	CheckBlockTap(idx);
+	if (Input_TouchMode && !(touches[idx].type & TOUCH_TYPE_GUI)) return;
+#endif
+	x = Pointers[idx].x; y = Pointers[idx].y;
+
+	for (i = 0; i < Gui.ScreensCount; i++) {
+		s = Gui_Screens[i];
+		s->dirty = true;
+		s->VTABLE->OnPointerUp(s, 1 << idx, x, y);
+	}
+}
+
+static void OnInputDown(void* obj, int key, cc_bool was) {
+	struct Screen* s;
+	cc_bool triggered;
+	int i;
+	if (Window_Main.SoftKeyboardFocus) return;
+
+#ifndef CC_BUILD_WEB
+	if (Input_IsEscapeButton(key) && (s = Gui_GetClosable())) {
+		/* Don't want holding down escape to go in and out of pause menu */
+		if (!was) Gui_Remove(s);
+		return;
+	}
+#endif
+
+	if (InputHandler_IsShutdown(key)) {
+		/* TODO: Do we need a separate exit function in Game class? */
+		Window_RequestClose(); return;
+	} else if (InputBind_Claims(BIND_SCREENSHOT, key) && !was) {
+		Game_ScreenshotRequested = true; return;
+	}
+	
+	for (i = 0; i < Gui.ScreensCount; i++) {
+		s = Gui_Screens[i];
+		s->dirty = true;
+		if (s->VTABLE->HandlesInputDown(s, key)) return;
+	}
+	if (Gui.InputGrab) return;
+
+	if (Input_IsPauseButton(key)) {
+#ifdef CC_BUILD_WEB
+		/* Can't do this in KeyUp, because pressing escape without having */
+		/* explicitly disabled mouse lock means a KeyUp event isn't sent. */
+		/* But switching to pause screen disables mouse lock, causing a KeyUp */
+		/* event to be sent, triggering the active->closable case which immediately */
+		/* closes the pause screen. Hence why the next KeyUp must be supressed. */
+		suppressEscape = true;
+#endif
+		Gui_ShowPauseMenu(); return;
+	}
+
+	/* These should not be triggered multiple times when holding down */
+	if (was) return;
+	triggered = false;
+
+	for (i = 0; i < BIND_COUNT; i++)
+	{
+		if (!Bind_OnTriggered[i])      continue;
+		if (!InputBind_Claims(i, key)) continue;
+
+		triggered |= Bind_OnTriggered[i](key);
+	}
+
+	if (triggered) {
+	} else if (key == CCKEY_F5 && Game_ClassicMode) {
+		int weather = Env.Weather == WEATHER_SUNNY ? WEATHER_RAINY : WEATHER_SUNNY;
+		Env_SetWeather(weather);
+	} else { HandleHotkeyDown(key); }
+}
+
+static void OnInputUp(void* obj, int key) {
+	struct Screen* s;
+	int i;
+
+	if (InputBind_Claims(BIND_ZOOM_SCROLL, key)) Camera_SetFov(Camera.DefaultFov);
+#ifdef CC_BUILD_WEB
+	/* When closing menus (which reacquires mouse focus) in key down, */
+	/* this still leaves the cursor visible. But if this is instead */
+	/* done in key up, the cursor disappears as expected. */
+	if (key == CCKEY_ESCAPE && (s = Gui_GetClosable())) {
+		if (suppressEscape) { suppressEscape = false; return; }
+		Gui_Remove(s); return;
+	}
+#endif
+
+	for (i = 0; i < Gui.ScreensCount; i++) {
+		s = Gui_Screens[i];
+		s->dirty = true;
+		s->VTABLE->OnInputUp(s, key);
+	}
+
+	for (i = 0; i < BIND_COUNT; i++)
+	{
+		if (!Bind_OnReleased[i])      continue;
+		if (!InputBind_Claims(i, key)) continue;
+
+		Bind_OnReleased[i](key);
+	}
+}
+
+static void OnFocusChanged(void* obj) { if (!Window_Main.Focused) Input_Clear(); }
+
+static void PlayerInputNormal(struct LocalPlayer* p, float* xMoving, float* zMoving) {
+	if (InputBind_IsPressed(BIND_FORWARD)) *zMoving -= 1;
+	if (InputBind_IsPressed(BIND_BACK))    *zMoving += 1;
+	if (InputBind_IsPressed(BIND_LEFT))    *xMoving -= 1;
+	if (InputBind_IsPressed(BIND_RIGHT))   *xMoving += 1;
+}
+static struct LocalPlayerInput normalInput = { PlayerInputNormal };
+
+static void OnInit(void) {
+	LocalPlayerInput_Add(&normalInput);
+	LocalPlayerInput_Add(&gamepadInput);
+	HookInputBinds();
+	
+	Event_Register_(&PointerEvents.Down,  NULL, OnPointerDown);
+	Event_Register_(&PointerEvents.Up,    NULL, OnPointerUp);
+	Event_Register_(&InputEvents.Down,    NULL, OnInputDown);
+	Event_Register_(&InputEvents.Up,      NULL, OnInputUp);
+
+	Event_Register_(&WindowEvents.FocusChanged,   NULL, OnFocusChanged);
+	Event_Register_(&UserEvents.HackPermsChanged, NULL, InputHandler_CheckZoomFov);
+	KeyBind_Init();
+	StoredHotkeys_LoadAll();
+	/* Fix issue with Android where if you double click in server list to join, a touch */
+	/*  pointer is stuck down when the game loads (so you instantly start deleting blocks) */
+	ClearTouches();
+}
+
+static void OnFree(void) {
+	ClearTouches();
+	HotkeysText.count = 0;
+}
+
+struct IGameComponent Input_Component = {
+	OnInit, /* Init  */
+	OnFree, /* Free  */
+};