Index: /Daodan/src/Daodan.h
===================================================================
--- /Daodan/src/Daodan.h	(revision 1162)
+++ /Daodan/src/Daodan.h	(revision 1163)
@@ -3,4 +3,5 @@
 
 #include <windows.h>
+#include <windowsx.h>
 #include <assert.h>
 #include "stdint.h"
Index: /Daodan/src/Daodan_Config.c
===================================================================
--- /Daodan/src/Daodan_Config.c	(revision 1162)
+++ /Daodan/src/Daodan_Config.c	(revision 1163)
@@ -252,4 +252,9 @@
 			{.intBoolVal = true},
 			{.intBoolVal = true} },
+		{ "daodaninput",
+			"New input system that reports input on every frame, supports Raw Input for mice.",
+			C_BOOL,
+			{.intBoolVal = true},
+			{.intBoolVal = true} },
 		{ "directinput",
 			"Enforces the usage of DirectInput on every system. Should be off for Linux/Wine.",
@@ -257,4 +262,9 @@
 			{.intBoolVal = true},
 			{.intBoolVal = true} },
+		{ "mousesensitivity",
+			"Multiplier for Oni's mouse sensitivity. 1.0 is Oni's default.",
+			C_FLOAT,
+			{.floatVal = 1.0f},
+			{.floatVal = 1.0f} },
 		{ "disablecmdline",
 			"Disables Oni's existing command line parser as Daodan has its own.",
Index: /Daodan/src/Oni/GameState.h
===================================================================
--- /Daodan/src/Oni/GameState.h	(revision 1162)
+++ /Daodan/src/Oni/GameState.h	(revision 1163)
@@ -1353,7 +1353,7 @@
   int field_FC;
   int field_100;
-  char field_104;
+  char SlowMotion;
   char gap_105[3];
-  int SlowMotion;
+  int SlowMotionTimer;
   char gap_10c[4];
   int field_110;
@@ -1362,7 +1362,7 @@
   char field_138;
   char gap_139[7];
-  int field_140;
+  int LastSixtieths;
   int GameTime;
-  int field_148;
+  int TargetGameTime;
   int field_14C;
   OniLevel* Level;
Index: /Daodan/src/Oni/Input.h
===================================================================
--- /Daodan/src/Oni/Input.h	(revision 1163)
+++ /Daodan/src/Oni/Input.h	(revision 1163)
@@ -0,0 +1,130 @@
+#ifndef ONI_H
+#error Do not include this file directly, include Oni/Oni.h instead!
+#endif
+
+#ifndef ONI_INPUT_H
+#define ONI_INPUT_H
+
+enum {
+	LIcKey_MouseButton1 = 0x01,
+	LIcKey_MouseButton2 = 0x02,
+	LIcKey_MouseButton3 = 0x03,
+	LIcKey_MouseButton4 = 0x04,
+	LIcKey_MouseXAxis = 0x05,
+	LIcKey_MouseYAxis = 0x06,
+	LIcKey_MouseZAxis = 0x07,
+	LIcKey_Backspace = 0x08,
+	LIcKey_Tab = 0x09,
+	LIcKey_Enter = 0x0d,
+	LIcKey_Escape = 0x1b, /* Not in LIgInputNames */
+	/* Printable ASCII characters are in here */
+	LIcKey_FKey1 = 0x81,
+	LIcKey_FKey2 = 0x82,
+	LIcKey_FKey3 = 0x83,
+	LIcKey_FKey4 = 0x84,
+	LIcKey_FKey5 = 0x85,
+	LIcKey_FKey6 = 0x86,
+	LIcKey_FKey7 = 0x87,
+	LIcKey_FKey8 = 0x88,
+	LIcKey_FKey9 = 0x89,
+	LIcKey_FKey10 = 0x8a,
+	LIcKey_FKey11 = 0x8b,
+	LIcKey_FKey12 = 0x8c,
+	LIcKey_PrintScreen = 0x8d,
+	LIcKey_FKey13 = 0x8d, /* Alias of LIcKey_PrintScreen */
+	LIcKey_ScrollLock = 0x8e,
+	LIcKey_FKey14 = 0x8e, /* Alias of LIcKey_ScrollLock */
+	LIcKey_Pause = 0x8f,
+	LIcKey_FKey15 = 0x8f, /* Alias of LIcKey_Pause */
+	LIcKey_CapsLock = 0x90,
+	LIcKey_LeftShift = 0x91,
+	LIcKey_RightShift = 0x92,
+	LIcKey_LeftControl = 0x93,
+	/* LIgInputNames has both "leftwindows" and "leftoption" for key 0x94, but
+	   nothing is mapped to 0x94 in the DirectInput and Win32 keymaps. In both
+	   of them, the left Windows key is mapped to 0xc7 instead. */
+	LIcKey_LeftAlt = 0x95,
+	LIcKey_RightAlt = 0x96,
+	/* LIgInputNames has both "rightoption" and "rightwindows" for key 0x97, but
+	   nothing is mapped to 0x97 in the DirectInput and Win32 keymaps. In both
+	   of them, the right Windows key is mapped to 0xc9 instead. */
+	LIcKey_Apps = 0x98,
+	LIcKey_RightControl = 0x99,
+	LIcKey_Insert = 0xaa,
+	LIcKey_Home = 0xab,
+	LIcKey_PageUp = 0xac,
+	LIcKey_Delete = 0xad,
+	LIcKey_End = 0xae,
+	LIcKey_PageDown = 0xaf,
+	LIcKey_UpArrow = 0xb0,
+	LIcKey_LeftArrow = 0xb1,
+	LIcKey_DownArrow = 0xb2,
+	LIcKey_RightArrow = 0xb3,
+	LIcKey_NumLock = 0xb4,
+	LIcKey_Divide = 0xb5,
+	LIcKey_Multiply = 0xb6,
+	LIcKey_Subtract = 0xb7,
+	LIcKey_Add = 0xb8,
+	LIcKey_Decimal = 0xb9,
+	LIcKey_NumpadEnter = 0xba,
+	LIcKey_NumpadComma = 0xbb,
+	LIcKey_NumpadEquals = 0xbc,
+	LIcKey_Numpad0 = 0xbd,
+	LIcKey_Numpad1 = 0xbe,
+	LIcKey_Numpad2 = 0xbf,
+	LIcKey_Numpad3 = 0xc0,
+	LIcKey_Numpad4 = 0xc1,
+	LIcKey_Numpad5 = 0xc2,
+	LIcKey_Numpad6 = 0xc3,
+	LIcKey_Numpad7 = 0xc4,
+	LIcKey_Numpad8 = 0xc5,
+	LIcKey_Numpad9 = 0xc6,
+	LIcKey_LeftWindows = 0xc7, /* Mapped to 0x94 in LIgInputNames */
+	LIcKey_RightWindows = 0xc9, /* Mapped to 0x97 in LIgInputNames */
+	LIcKey_Max,
+};
+
+enum {
+	LIcActionType_None = 0,
+	LIcActionType_Digital = 1,
+	LIcActionType_Unknown = 2, // Unused in LIgActionDescriptions
+	LIcActionType_Accumulate = 3,
+	LIcActionType_Increment = 4,
+	LIcActionType_Decrement = 5,
+};
+
+typedef struct {
+	uint32_t input;
+	float analog;
+} LItDeviceInput;
+
+typedef struct {
+	uint64_t button_flags;
+	float analogs[10];
+} LItActionBuffer;
+
+typedef struct {
+	short type;
+	short index;
+	char name[32];
+} LItActionDescription;
+
+typedef struct {
+	int key;
+	LItActionDescription *descr;
+} LItBinding;
+
+typedef struct {
+	int event;
+	IMtPoint mouse_pos;
+	short ch;
+	/* short padding; */
+	int modifiers;
+} LItInputEvent;
+
+typedef struct {
+	char name[32];
+	int key;
+} LItInputName;
+
+#endif
Index: /Daodan/src/Oni/Oni.h
===================================================================
--- /Daodan/src/Oni/Oni.h	(revision 1162)
+++ /Daodan/src/Oni/Oni.h	(revision 1163)
@@ -9,5 +9,12 @@
 #define ONICALL __fastcall
 
+typedef struct {
+	short x;
+	short y;
+} IMtPoint;
 
+// The following headers were written when Daodan used -fpack-struct, and
+// should be rewritten
+#pragma pack(push, 1)
 #include "stdtypes.h"
 #include "BFW_Motoko_Draw.h"
@@ -17,4 +24,7 @@
 #include "GL.h"
 #include "OBJt.h"
+#pragma pack(pop)
+
+#include "Input.h"
 
 #define DDmAssert(expr) assert(expr);
@@ -34,9 +44,4 @@
 } UUtRect;
 
-typedef struct {
-	uint32_t input;
-	float analog;
-} LItDeviceInput;
-
 #include "Symbols_Var.h"
 #include "Symbols_Func.h"
Index: /Daodan/src/Oni/Symbols_Func.h
===================================================================
--- /Daodan/src/Oni/Symbols_Func.h	(revision 1162)
+++ /Daodan/src/Oni/Symbols_Func.h	(revision 1163)
@@ -57,5 +57,5 @@
 
 DefFunc(void, ONrGameState_Timer_Start, ONICALL, (char* function, int time), 0x004FD370);
-DefFunc(void, ONrGameState_HandleUtilityInput, ONICALL, (const void* inInput), 0x004f6200);
+DefFunc(void, ONrGameState_HandleUtilityInput, ONICALL, (GameInput *input), 0x004f6200);
 DefFunc(uint8_t, ONrDebugKey_WentDown, ONICALL, (uint32_t inKey), 0x005050d0);
 
@@ -177,4 +177,28 @@
 DefFunc(void, LIrActionBuffer_Add, ONICALL, (void* unknown, LItDeviceInput* input), 0x00403b30);
 
+// Called during the game loop to run the Windows message loop
+DefFunc(UUtBool, LIiPlatform_InputEvent_GetEvent, ONICALL, (void), 0x004036d0);
+
+// Called by the game loop to get the latest input frames
+DefFunc(void, LIrActionBuffer_Get, ONICALL, (short* count, LItActionBuffer **buffers), 0x00403be0);
+
+// Gets the mouse position
+DefFunc(void, LIrPlatform_InputEvent_GetMouse, ONICALL, (int active, LItInputEvent *info), 0x00402ca0);
+
+// Checks if a keyboard key is pressed
+DefFunc(UUtBool, LIrPlatform_TestKey, ONICALL, (int key, int active), 0x00403930);
+
+// Translates a key name (from key_config.txt) to a key code
+DefFunc(int, LIrTranslate_InputName, ONICALL, (char *name), 0x00403a90);
+
+// Updates TargetGameTime in the game state
+DefFunc(void, ONrGameState_UpdateServerTime, ONICALL, (GameState *game_state), 0x004fbeb0);
+
+// Basically stricmp, but it only ever does ASCII case folding
+DefFunc(int, UUrString_Compare_NoCase, ONICALL, (const char *str1, const char *str2), 0x004266d0);
+
+// A safe strcpy which lets you specify the size of the target buffer and always
+// NUL-terminates. Kind of like strlcpy. Just don't call it with dest_size = 0.
+DefFunc(void, UUrString_Copy, ONICALL, (char *dest, const char *src, size_t dest_size), 0x004265f0);
 
 #undef DefFunc
Index: /Daodan/src/Oni/Symbols_Var.h
===================================================================
--- /Daodan/src/Oni/Symbols_Var.h	(revision 1162)
+++ /Daodan/src/Oni/Symbols_Var.h	(revision 1163)
@@ -84,3 +84,30 @@
 //#define OBJgFlag_DrawNameDistance	(*((void*)0x005ec634))
 
+// Oni's platform input system stores the window HWND in this global
+#define LIgPlatform_HWND (*((HWND *)0x0055cae8))
+
+// Is Oni's input system active? True in gameplay and false in menus.
+// LIrPlatform_Mode_Set gets called when this changes.
+#define LIgMode_Internal (*((UUtBool *)0x0055fd40))
+
+// List of bindings from Oni's internal key codes to LItActionDescriptions
+#define LIgBindingArray ((LItBinding *)0x0055fa20)
+
+// Is "Invert Mouse" checked in the Options menu? If so, platform specific input
+// code should invert the cursor's Y offset when building LItActionBuffers.
+#define LIgMode_InvertMouse (*((UUtBool *)0x0052ecf4))
+
+// This is a map from DirectInput scan codes to Oni's internal key codes, the
+// latter of which mostly match ASCII for a US keyboard layout. DirectInput scan
+// codes are mostly the same as the PS2 make codes that Windows uses in other
+// APIs except that extended keys are represented by bit 0x80 being set.
+#define LIgPlatform_ScanCodeToChar ((uint8_t *)0x005292b8)
+
+// Script variable to enable/disable centering the cursor in the window when
+// processing input. On by default.
+#define LIgCenterCursor (*((int *)0x0052d88d))
+
+// List of bindable input actions
+#define LIgActionDescriptions ((LItActionDescription*)0x0052db18)
+
 #endif
Index: /Daodan/src/Patches/Cheater.c
===================================================================
--- /Daodan/src/Patches/Cheater.c	(revision 1162)
+++ /Daodan/src/Patches/Cheater.c	(revision 1163)
@@ -297,5 +297,5 @@
 
 
-static void BindableCheatCallback (CustomActionCallbackArgument cheatnum) {
+static void BindableCheatCallback (intptr_t cheatnum) {
 	uint8_t res = DDrCheater (cheatnum);
 	if (res)
@@ -308,7 +308,6 @@
 	oniCheatCode* cur;
 	for (cur = DDr_CheatTable; cur->name != 0; cur++) {
-//		char* val = malloc(20);
-//		sprintf(val, "cheat_%s", cur->name);
-		Input_RegisterCustomAction (cur->name, EVENT_KEYPRESS, 0, BindableCheatCallback, cur->func);
+		DDrInput_RegisterCustomAction(cur->name, DDcEventType_KeyPress,
+		                              BindableCheatCallback, cur->func);
 	}
 }
Index: /Daodan/src/Patches/Input.c
===================================================================
--- /Daodan/src/Patches/Input.c	(revision 1162)
+++ /Daodan/src/Patches/Input.c	(revision 1163)
@@ -5,83 +5,868 @@
 #include "Input.h"
 #include "../Oni/Oni.h"
+#include "../Daodan_Config.h"
 #include "../Daodan_Patch.h"
 #include "Utility.h"
 
-#define EVENT_KEYPRESS_SECURETIME 3
-
 typedef struct {
-	uint32_t key;
-	const char* actionname;
-	ActionEventType_t eventType;
-	uint32_t keydownTimeoutTicks;
-	int64_t lastevent;
-	
-	CustomActionCallback_t callback;	
-	CustomActionCallbackArgument callbackArgument;
-} DD_CustomAction_t;
-
-static DD_CustomAction_t customActions[100];
-
-
-void Input_RegisterCustomAction (const char* actionname, ActionEventType_t eventType, uint32_t keydownTimeoutTicks, CustomActionCallback_t callback, CustomActionCallbackArgument callbackArgument) {
-	uint16_t i = 0;
-	DD_CustomAction_t* cur = customActions;
-	
-	while ( (i < ARRAY_SIZE(customActions)) && (cur->callback != 0)) {
-		cur++;
-		i++;
-	}
-	
-	if (i < ARRAY_SIZE(customActions)) {
-		cur->actionname = actionname;
-		cur->eventType = eventType;
-		cur->keydownTimeoutTicks = keydownTimeoutTicks;
-		cur->callback = callback;
-		cur->callbackArgument = callbackArgument;
+	LItActionDescription descr;
+	DDtActionEventType eventType;
+
+	DDtCustomActionCallback callback;
+	intptr_t ctx;
+} DDtCustomAction;
+
+static DDtCustomAction DDgCustomActions[100] = { 0 };
+
+// Extra keys (make sure these don't collide with Oni's LIc_* keys)
+enum {
+	DDcKey_MouseButton5 = LIcKey_Max,
+	DDcKey_ScrollUp,
+	DDcKey_ScrollDown,
+	DDcKey_ScrollLeft,
+	DDcKey_ScrollRight,
+};
+
+// Enhanced version of LIgInputNames from Oni with some extra keys
+static const LItInputName DDgInputNames[] = {
+	// The following key names are mapped in Oni
+	{"fkey1", LIcKey_FKey1}, {"fkey2", LIcKey_FKey2}, {"fkey3", LIcKey_FKey3},
+	{"fkey4", LIcKey_FKey4}, {"fkey5", LIcKey_FKey5}, {"fkey6", LIcKey_FKey6},
+	{"fkey7", LIcKey_FKey7}, {"fkey8", LIcKey_FKey8}, {"fkey9", LIcKey_FKey9},
+	{"fkey10", LIcKey_FKey10}, {"fkey11", LIcKey_FKey11},
+	{"fkey12", LIcKey_FKey12}, {"fkey13", LIcKey_FKey13},
+	{"fkey14", LIcKey_FKey14}, {"fkey15", LIcKey_FKey15},
+	{"backspace", LIcKey_Backspace}, {"tab", LIcKey_Tab},
+	{"capslock", LIcKey_CapsLock}, {"enter", LIcKey_Enter},
+	{"leftshift", LIcKey_LeftShift}, {"rightshift", LIcKey_RightShift},
+	{"leftcontrol", LIcKey_LeftControl},
+	{"leftwindows", 0x94}, {"leftoption", 0x94}, // Does nothing in Oni
+	{"leftalt", LIcKey_LeftAlt}, {"space", ' '}, {"rightalt", LIcKey_RightAlt},
+	{"rightoption", 0x97}, {"rightwindows", 0x97}, // Does nothing in Oni
+	{"rightcontrol", LIcKey_RightControl}, {"printscreen", LIcKey_PrintScreen},
+	{"scrolllock", LIcKey_ScrollLock}, {"pause", LIcKey_Pause},
+	{"insert", LIcKey_Insert}, {"home", LIcKey_Home}, {"pageup", LIcKey_PageUp},
+	{"delete", LIcKey_Delete}, {"end", LIcKey_End},
+	{"pagedown", LIcKey_PageDown}, {"uparrow", LIcKey_UpArrow},
+	{"leftarrow", LIcKey_LeftArrow}, {"downarrow", LIcKey_DownArrow},
+	{"rightarrow", LIcKey_RightArrow}, {"numlock", LIcKey_NumLock},
+	{"divide", LIcKey_Divide}, {"multiply", LIcKey_Multiply},
+	{"subtract", LIcKey_Subtract}, {"add", LIcKey_Add},
+	{"numpadequals", LIcKey_NumpadEquals}, {"numpadenter", LIcKey_NumpadEnter},
+	{"decimal", LIcKey_Decimal}, {"numpad0", LIcKey_Numpad0},
+	{"numpad1", LIcKey_Numpad1}, {"numpad2", LIcKey_Numpad2},
+	{"numpad3", LIcKey_Numpad3}, {"numpad4", LIcKey_Numpad4},
+	{"numpad5", LIcKey_Numpad5}, {"numpad6", LIcKey_Numpad6},
+	{"numpad7", LIcKey_Numpad7}, {"numpad8", LIcKey_Numpad8},
+	{"numpad9", LIcKey_Numpad9}, {"backslash", '\\'}, {"semicolon", ';'},
+	{"period", '.'}, {"apostrophe", '\''}, {"slash", '/'}, {"leftbracket", '['},
+	{"rightbracket", ']'}, {"comma", ','},
+	{"mousebutton1", LIcKey_MouseButton1},
+	{"mousebutton2", LIcKey_MouseButton2},
+	{"mousebutton3", LIcKey_MouseButton3},
+	{"mousebutton4", LIcKey_MouseButton4},
+	{"mousexaxis", LIcKey_MouseXAxis}, {"mouseyaxis", LIcKey_MouseYAxis},
+	{"mousezaxis", LIcKey_MouseZAxis},
+
+	// Extra keys for Daodan Input
+	{"mousebutton5", DDcKey_MouseButton5},
+	{"scrollup", DDcKey_ScrollUp},
+	{"scrolldown", DDcKey_ScrollDown},
+	{"scrollleft", DDcKey_ScrollLeft},
+	{"scrollright", DDcKey_ScrollRight},
+
+	{"", 0}
+};
+
+// Enhanced version of LIgPlatform_ScanCodeToChar from Oni
+static const uint8_t DDgPlatform_ScanCodeToChar[256] = {
+	// The following scan codes are mapped in Oni
+	[0x01] = LIcKey_Escape, [0x02] = '1', [0x03] = '2', [0x04] = '3',
+	[0x05] = '4', [0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8',
+	[0x0a] = '9', [0x0b] = '0', [0x0c] = '-', [0x0d] = '=',
+	[0x0e] = LIcKey_Backspace, [0x0f] = LIcKey_Tab, [0x10] = 'q', [0x11] = 'w',
+	[0x12] = 'e', [0x13] = 'r', [0x14] = 't', [0x15] = 'y', [0x16] = 'u',
+	[0x17] = 'i', [0x18] = 'o', [0x19] = 'p', [0x1a] = '[', [0x1b] = ']',
+	[0x1c] = LIcKey_Enter, [0x1d] = LIcKey_LeftControl, [0x1e] = 'a',
+	[0x1f] = 's', [0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
+	[0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x27] = ';', [0x28] = '\'',
+	[0x29] = '`', [0x2a] = LIcKey_LeftShift, [0x2b] = '\\', [0x2c] = 'z',
+	[0x2d] = 'x', [0x2e] = 'c', [0x2f] = 'v', [0x30] = 'b', [0x31] = 'n',
+	[0x32] = 'm', [0x33] = ',', [0x34] = '.', [0x35] = '/',
+	[0x36] = LIcKey_RightShift, [0x37] = LIcKey_Multiply,
+	[0x38] = LIcKey_LeftAlt, [0x39] = ' ', [0x3a] = LIcKey_CapsLock,
+	[0x3b] = LIcKey_FKey1, [0x3c] = LIcKey_FKey2, [0x3d] = LIcKey_FKey3,
+	[0x3e] = LIcKey_FKey4, [0x3f] = LIcKey_FKey5, [0x40] = LIcKey_FKey6,
+	[0x41] = LIcKey_FKey7, [0x42] = LIcKey_FKey8, [0x43] = LIcKey_FKey9,
+	[0x44] = LIcKey_FKey10, [0x45] = LIcKey_NumLock, [0x46] = LIcKey_ScrollLock,
+	[0x47] = LIcKey_Numpad7, [0x48] = LIcKey_Numpad8, [0x49] = LIcKey_Numpad9,
+	[0x4a] = LIcKey_Subtract, [0x4b] = LIcKey_Numpad4, [0x4c] = LIcKey_Numpad5,
+	[0x4d] = LIcKey_Numpad6, [0x4e] = LIcKey_Add, [0x4f] = LIcKey_Numpad1,
+	[0x50] = LIcKey_Numpad2, [0x51] = LIcKey_Numpad3, [0x52] = LIcKey_Numpad0,
+	[0x53] = LIcKey_Decimal, [0x57] = LIcKey_FKey11, [0x58] = LIcKey_FKey12,
+	[0x64] = LIcKey_FKey13, [0x65] = LIcKey_FKey14, [0x66] = LIcKey_FKey15,
+	[0x8d] = LIcKey_NumpadEquals, [0x9c] = LIcKey_NumpadEnter,
+	[0x9d] = LIcKey_RightControl, [0xb3] = LIcKey_NumpadComma,
+	[0xb5] = LIcKey_Divide, [0xb8] = LIcKey_RightAlt, [0xc7] = LIcKey_Home,
+	[0xc8] = LIcKey_UpArrow, [0xc9] = LIcKey_PageUp, [0xcb] = LIcKey_LeftArrow,
+	[0xcd] = LIcKey_RightArrow, [0xcf] = LIcKey_End, [0xd0] = LIcKey_DownArrow,
+	[0xd2] = LIcKey_Insert, [0xd3] = LIcKey_Delete, [0xdb] = LIcKey_LeftWindows,
+	[0xdc] = LIcKey_RightWindows, [0xdd] = LIcKey_Apps,
+
+	// Missing in Oni
+	[0xd1] = LIcKey_PageDown,
+};
+
+// Set in Patches.c if the Daodan input patches are applied. This just enables
+// the windows message handling for now
+bool DDgUseDaodanInput = false;
+
+// The Oni key codes that correspond to the togglable keys
+static uint8_t DDgCapsOniKey = 0;
+static uint8_t DDgScrollOniKey = 0;
+static uint8_t DDgNumLockOniKey = 0;
+
+// Multiplier for mouse values
+static float DDgMouseSensitivity = 1.0;
+
+// Accumulators for mouse scrolling. These are needed because some mice have
+// continuous scroll wheels (not to mention touchpads.) We should only add an
+// action to Oni's input if one of these accumulators exceeds +/-WHEEL_DELTA.
+static int DDgWheelDelta_V = 0;
+static int DDgWheelDelta_H = 0;
+
+// UUrMachineTime_High for the last update of the accumulators. Used so they can
+// be reset after a period of no scroll events.
+static int64_t DDgWheelDelta_Updated = 0;
+
+// Temporary action buffer that we build over the duration of a frame with the
+// input we're going to send to the engine. This includes the accumulated
+// movement of the mouse cursor and all actions (keyboard and mouse buttons)
+// that were pressed this frame (but not held down from previous frames - that
+// gets added later from DDgInputState.)
+static LItActionBuffer DDgActionBuffer = { 0 };
+
+// Temporary buffer containing the current state of the keyboard and mouse
+// buttons, that is, if they're being held now
+static char DDgInputState[256] = { 0 };
+
+static short ONICALL DDrBinding_Add(int key, const char *name)
+{
+	// First try to replace an existing binding for the same key
+	LItBinding *binding = NULL;
+	for (int i = 0; i < 100; i++) {
+		if (LIgBindingArray[i].key == key) {
+			binding = &LIgBindingArray[i];
+			break;
+		}
+	}
+
+	// If there are no existing bindings for this key, find a free entry
+	if (!binding) {
+		for (int i = 0; i < 100; i++) {
+			if (!LIgBindingArray[i].key) {
+				binding = &LIgBindingArray[i];
+				break;
+			}
+		}
+	}
+	// No free entries, so give up
+	if (!binding)
+		return 2;
+
+	// Now try to find the action to bind to. First check Oni's built-in list
+	// of actions.
+	LItActionDescription *descr = NULL;
+	for (int i = 0; LIgActionDescriptions[i].name[0]; i++) {
+		if (!UUrString_Compare_NoCase(name, LIgActionDescriptions[i].name)) {
+			descr = &LIgActionDescriptions[i];
+			break;
+		}
+	}
+
+	// Next, try Daodan's list of custom actions
+	if (!descr) {
+		for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
+			if (!DDgCustomActions[i].descr.name[0])
+				break;
+
+			if (!UUrString_Compare_NoCase(name, DDgCustomActions[i].descr.name)) {
+				descr = &DDgCustomActions[i].descr;
+				break;
+			}
+		}
+	}
+	if (!descr)
+		return 0;
+
+	binding->key = key;
+	binding->descr = descr;
+	return 0;
+}
+
+static void ONICALL DDrGameState_HandleUtilityInput(GameInput *input)
+{
+	// Mac Oni 1.2.1 checks the cheat binds here, so we should too. Note that
+	// unlike Mac Oni, which hardcodes each cheat here, we use our flexible
+	// custom action system.
+	for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
+		if (!DDgCustomActions[i].descr.name[0])
+			break;
+
+		uint64_t action = 1ull << DDgCustomActions[i].descr.index;
+		bool active = false;
+
+		switch (DDgCustomActions[i].eventType) {
+		case DDcEventType_KeyPress:
+			if (input->ActionsPressed & action)
+				active = true;
+			break;
+		case DDcEventType_KeyDown:
+			if (input->ActionsDown & action)
+				active = true;
+			break;
+		}
+
+		if (active)
+			DDgCustomActions[i].callback(DDgCustomActions[i].ctx);
+	}
+
+	// Now do everything Oni does in this function
+	ONrGameState_HandleUtilityInput(input);
+
+	// This is for show_triggervolumes. Mac Oni does this at the end of
+	// HandleUtilityInput too.
+	if (ONrDebugKey_WentDown(7))
+		OBJgTriggerVolume_Visible = !OBJgTriggerVolume_Visible;
+}
+
+static int GetLowestFreeDigitalAction(void)
+{
+	// Get the digital action indexes that Oni is using right now, plus any in
+	// use by our custom actions
+	uint64_t used = 0;
+	for (int i = 0; LIgActionDescriptions[i].name[0]; i++) {
+		if (LIgActionDescriptions[i].type != LIcActionType_Digital)
+			continue;
+		used |= 1ull << LIgActionDescriptions[i].index;
+	}
+	for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
+		if (!DDgCustomActions[i].descr.name[0])
+			break;
+
+		if (DDgCustomActions[i].descr.type != LIcActionType_Digital)
+			continue;
+		used |= 1ull << DDgCustomActions[i].descr.index;
+	}
+
+	// Get the lowest unused action index and use it. This isn't totally safe
+	// since Oni _might_ have "orphaned" actions that are checked in the code
+	// but not bindable, but Mac Oni 1.2.1 seems to have allocated its new
+	// bindings this way, including filling the gaps between eg. f12 and
+	// lookmode, so we're probably fine to do the same thing.
+	unsigned long lowest;
+	if (BitScanForward(&lowest, ~(unsigned long)used))
+		return lowest;
+	if (BitScanForward(&lowest, ~(unsigned long)(used >> 32)))
+		return lowest + 32;
+	return -1;
+}
+
+void DDrInput_RegisterCustomAction(const char *name, DDtActionEventType type,
+                                   DDtCustomActionCallback callback,
+                                   intptr_t ctx)
+{
+	int index = GetLowestFreeDigitalAction();
+	if (index < 0) {
+		STARTUPMESSAGE("Registering action %s failed, maximum actions reached",
+		               name);
+		return;
+	}
+
+	DDtCustomAction *action;
+	for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
+		if (!DDgCustomActions[i].descr.name[0]) {
+			action = &DDgCustomActions[i];
+			break;
+		}
+	}
+	if (!action) {
+		STARTUPMESSAGE("Registering action %s failed, maximum actions reached",
+		               name);
+		return;
+	}
+
+	*action = (DDtCustomAction) {
+		.descr = {
+			.type = 1,
+			.index = index,
+		},
+		.callback = callback,
+		.ctx = ctx,
+	};
+	UUrString_Copy(action->descr.name, name, sizeof(action->descr.name));
+}
+
+static uint8_t VKeyToChar(UINT vkey)
+{
+	int sc = MapVirtualKeyA(vkey, MAPVK_VK_TO_VSC_EX);
+	if ((sc & 0xff00) == 0xe000)
+		sc |= 0x80;
+	sc &= 0xff;
+	return DDgPlatform_ScanCodeToChar[sc];
+}
+
+static int ONICALL DDrTranslate_InputName(char *name)
+{
+	// Mutate the source argument to convert to lowercase. It's ugly but Oni
+	// does this too. Unlike Oni, we don't use tolower, since passing
+	// potentially out-of-range values to tolower is undefined.
+	for (char *c = name; *c; c++) {
+		if (*c >= 'A' && *c <= 'Z')
+			*c = *c - 0x20;
+	}
+
+	// Single character names just use that character as the key code. Unlike
+	// Oni, we restrict this to printable ASCII.
+	if (strlen(name) == 1 && name[0] >= ' ' && name[0] <= '~')
+		return name[0];
+
+	// Otherwise, look up the name in DDgInputNames
+	for (int i = 0; DDgInputNames[i].name[0]; i++) {
+		if (!strcmp(name, DDgInputNames[i].name))
+			return DDgInputNames[i].key;
+	}
+	return 0;
+}
+
+static void CenterCursor(void)
+{
+	// This can be set to false by script. Not sure why you'd turn it off, but
+	// let's respect it.
+	if (!LIgCenterCursor)
+		return;
+
+	RECT rc;
+	if (!GetClientRect(LIgPlatform_HWND, &rc))
+		return;
+	POINT mid = { rc.right / 2, rc.bottom / 2 };
+	if (!ClientToScreen(LIgPlatform_HWND, &mid))
+		return;
+	SetCursorPos(mid.x, mid.y);
+}
+
+static void ONICALL DDrPlatform_Mode_Set(int active)
+{
+	// Oni's input system uses LIgPlatform_HWND instead of
+	// ONgPlatformData.Window, but they should both have the same value
+	DDmAssert(LIgPlatform_HWND);
+
+	// Clear the input state when switching input modes
+	for (int i = 0; i < ARRAY_SIZE(DDgInputState); i++)
+		DDgInputState[i] = 0;
+	DDgActionBuffer = (LItActionBuffer) { 0 };
+
+	// Center the cursor before switching modes. Raw Input doesn't need the
+	// cursor to be centered, but when switching modes, centering the cursor
+	// means it will be in a predictable position for using the pause or F1
+	// menu, which are centered on the screen. Also, the cursor must be inside
+	// the clip region when we call ClipCursor, otherwise it doesn't work.
+	CenterCursor();
+
+	// If leaving input mode (switching from gameplay to menus,) unregister the
+	// input device. Otherwise, register it.
+	RegisterRawInputDevices(&(RAWINPUTDEVICE) {
+		.usUsagePage = 0x01, // HID_USAGE_PAGE_GENERIC
+		.usUsage = 0x02, // HID_USAGE_GENERIC_MOUSE
+		.hwndTarget = LIgPlatform_HWND,
+		.dwFlags = active ? 0 : RIDEV_REMOVE,
+	}, 1, sizeof(RAWINPUTDEVICE));
+
+	if (active) {
+		DDgMouseSensitivity =
+			DDrConfig_GetOptOfType("windows.mousesensitivity", C_FLOAT)->value.floatVal;
+
+		// Get the Oni key codes corresponding to the togglable keys
+		DDgCapsOniKey = VKeyToChar(VK_CAPITAL);
+		DDgScrollOniKey = VKeyToChar(VK_SCROLL);
+		DDgNumLockOniKey = VKeyToChar(VK_NUMLOCK);
+
+		// Clip the cursor to the window bounds when entering input mode to
+		// prevent other programs being clicked in windowed mode
+		RECT rc;
+		if (GetClientRect(LIgPlatform_HWND, &rc)) {
+			if (MapWindowRect(LIgPlatform_HWND, NULL, &rc))
+				ClipCursor(&rc);
+		}
 	} else {
-		STARTUPMESSAGE("Registering action %s failed, maximum actions reached", actionname);
-	}
-}
-
-
-_LIrBinding_Add Oni_LIrBinding_Add = (_LIrBinding_Add)0;
-uint16_t ONICALL DD_LIrBinding_Add(uint32_t key, const char* name) {
-	DD_CustomAction_t* cur;
-	for (cur = customActions; cur->callback != 0; cur++) {
-		if (!strcmp(name, cur->actionname)) {
-			cur->key = key;
-			return 0;
-		}
-	}
-
-	return Oni_LIrBinding_Add(key, name);
-}
-
-_LIrActionBuffer_Add Oni_LIrActionBuffer_Add = (_LIrActionBuffer_Add)0;
-void ONICALL DD_LIrActionBuffer_Add(void* unknown, LItDeviceInput* input) {
-	DD_CustomAction_t* cur;
-	for (cur = customActions; cur->callback != 0; cur++) {
-		if (cur->key == input->input) {
-			int64_t curTime = UUrMachineTime_Sixtieths();
-			if (cur->eventType == EVENT_KEYPRESS) {
-				if (cur->lastevent + EVENT_KEYPRESS_SECURETIME < curTime) {
-					cur->callback(cur->callbackArgument);
-				}
-				cur->lastevent = curTime;
-			} else if (cur->eventType == EVENT_KEYDOWN) {
-				if (cur->lastevent + cur->keydownTimeoutTicks < curTime) {
-					cur->callback(cur->callbackArgument);
-					cur->lastevent = curTime;
-				}
+		ClipCursor(NULL);
+	}
+}
+
+static void ONICALL DDrPlatform_InputEvent_GetMouse(int active,
+                                                    LItInputEvent *event)
+{
+	POINT pt;
+	if (!GetCursorPos(&pt))
+		goto error;
+
+	// Unlike Oni's version of this function, we support windowed mode by
+	// mapping the cursor coordinates to the window's client area
+	if (!ScreenToClient(LIgPlatform_HWND, &pt))
+		goto error;
+
+	*event = (LItInputEvent) { .mouse_pos = { pt.x, pt.y } };
+	return;
+
+error:
+	*event = (LItInputEvent) { 0 };
+	return;
+}
+
+static UUtBool ONICALL DDrPlatform_TestKey(int ch, int active)
+{
+	// DDrPlatform_TestKey is always called with active = LIgMode_Internal
+
+	if (active) {
+		// The input system is running, which means DDgInputState will be up to
+		// date, so just use that
+		return DDgInputState[ch];
+	} else {
+		// Use Oni's map from key codes to DirectInput scan codes to get the
+		// scan code we want to test for
+		int sc = 0;
+		for (int i = 0; i < 256; i++) {
+			if (DDgPlatform_ScanCodeToChar[i] == ch) {
+				sc = i;
+				break;
 			}
+		}
+		if (!sc)
+			return UUcFalse;
+
+		// DirectInput scan codes have 0x80 set for extended keys. Replace this
+		// with an 0xe0 prefix for MapVirtualKey.
+		if (sc & 0x80) {
+			sc &= 0x7f;
+			sc |= 0xe000;
+		}
+		int vkey = MapVirtualKeyA(sc, MAPVK_VSC_TO_VK_EX);
+
+		// Now check if the key is down. We must use GetAsyncKeyState here
+		// because DDrPlatform_TestKey can be called from anywhere, even before
+		// we have a message loop or game loop. For example, it's called from
+		// ONiMain to test the state of the shift key on startup.
+		return (GetAsyncKeyState(vkey) & 0x8000) ? UUcTrue : UUcFalse;
+	}
+}
+
+// Update DDgInputState and DDgActionBuffer with a new key state
+static void SetKeyState(int key, bool down)
+{
+	// Keep track of held keys. Held keys are added to every buffer and they're
+	// also checked in DDrPlatform_TestKey.
+	DDgInputState[key] = down;
+
+	if (down) {
+		// Add the key to the next buffer. This is so key presses are never
+		// dropped, even if the key is released before Oni checks the buffer.
+		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+			.input = key,
+			.analog = 1.0,
+		});
+	}
+}
+
+static void ProcessRawInputPacket(RAWINPUT *ri)
+{
+	if (ri->header.dwType != RIM_TYPEMOUSE)
+		return;
+
+	// We don't handle MOUSE_MOVE_ABSOLUTE at all yet
+	if (!(ri->data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
+		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+			.input = LIcKey_MouseXAxis,
+			.analog = (float)ri->data.mouse.lLastX * 0.25 * DDgMouseSensitivity,
+		});
+		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+			.input = LIcKey_MouseYAxis,
+			.analog = (float)ri->data.mouse.lLastY *
+			          (LIgMode_InvertMouse ? -0.25 : 0.25) * DDgMouseSensitivity,
+		});
+	}
+
+	// Oni supports using the mouse wheel to look up and down or left and right
+	// by binding mousezaxis to aim_lr or aim_ud. We don't support this
+	// incredibly useful feature, but if you need it, let me know. Instead, we
+	// allow scrolling to be bound to digital actions.
+	if (ri->data.mouse.usButtonFlags & (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL)) {
+		int64_t now = UUrMachineTime_High();
+		int64_t last_updated = now - DDgWheelDelta_Updated;
+		DDgWheelDelta_Updated = now;
+
+		// Reset the accumulators if too much time has passed since the last
+		// scroll event. The player is assumed to have finished scrolling.
+		if (last_updated / UUrMachineTime_High_Frequency() > 0.3) {
+			DDgWheelDelta_V = 0;
+			DDgWheelDelta_H = 0;
+		}
+
+		int neg_key, pos_key;
+		int *delta;
+		if (ri->data.mouse.usButtonFlags & RI_MOUSE_WHEEL) {
+			neg_key = DDcKey_ScrollUp;
+			pos_key = DDcKey_ScrollDown;
+			delta = &DDgWheelDelta_V;
+		} else {
+			neg_key = DDcKey_ScrollLeft;
+			pos_key = DDcKey_ScrollRight;
+			delta = &DDgWheelDelta_H;
+		}
+
+		// To support touchpad scrolling and mice with continuous scroll wheels,
+		// accumulate the wheel delta and only generate an input event once it
+		// crosses the WHEEL_DELTA threshold
+		*delta += (short)ri->data.mouse.usButtonData;
+		if (*delta >= WHEEL_DELTA) {
+			LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+				.input = neg_key,
+				.analog = 1.0,
+			});
+
+			*delta -= (*delta / WHEEL_DELTA) * WHEEL_DELTA;
+		} else if (*delta <= -WHEEL_DELTA) {
+			LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+				.input = pos_key,
+				.analog = 1.0,
+			});
+
+			*delta -= (*delta / -WHEEL_DELTA) * -WHEEL_DELTA;
+		}
+	}
+
+	// This probably doesn't obey SM_SWAPBUTTON... should it?
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN)
+		SetKeyState(LIcKey_MouseButton1, true);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP)
+		SetKeyState(LIcKey_MouseButton1, false);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_DOWN)
+		SetKeyState(LIcKey_MouseButton2, true);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_UP)
+		SetKeyState(LIcKey_MouseButton2, false);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_DOWN)
+		SetKeyState(LIcKey_MouseButton3, true);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_UP)
+		SetKeyState(LIcKey_MouseButton3, false);
+
+	// Oni supports binding this button too. It's the back button on most mice.
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN)
+		SetKeyState(LIcKey_MouseButton4, true);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP)
+		SetKeyState(LIcKey_MouseButton4, false);
+
+	// Daodan supports binding the forward button too
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN)
+		SetKeyState(DDcKey_MouseButton5, true);
+	if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP)
+		SetKeyState(DDcKey_MouseButton5, false);
+}
+
+static void DrainRawInput(void)
+{
+	if (!LIgMode_Internal)
+		return;
+
+	UINT ri_size = 10240;
+	static RAWINPUT *ri_buf = NULL;
+	if (!ri_buf)
+		ri_buf = calloc(1, ri_size);
+
+	BOOL wow_hack;
+	IsWow64Process(GetCurrentProcess(), &wow_hack);
+
+	for (;;) {
+		UINT count = GetRawInputBuffer(ri_buf, &ri_size, sizeof ri_buf->header);
+		if (count == 0 || count == (UINT)-1)
 			return;
-		}
-	}
-	Oni_LIrActionBuffer_Add(unknown, input);
-}
-
-void Input_PatchCode () {
-	Oni_LIrBinding_Add = DDrPatch_MakeDetour((void*)LIrBinding_Add, (void*)DD_LIrBinding_Add);
-	Oni_LIrActionBuffer_Add = DDrPatch_MakeDetour((void*)LIrActionBuffer_Add, (void*)DD_LIrActionBuffer_Add);
-}
-
+
+		RAWINPUT *ri = ri_buf;
+		for (UINT i = 0; i < count; i++) {
+			// In WOW64, these structures are aligned like in Win64 and they
+			// have to be fixed to use from 32-bit code. Yes, really.
+			if (wow_hack) {
+				memmove(&ri->data, ((char *)&ri->data) + 8,
+					ri->header.dwSize - offsetof(RAWINPUT, data) - 8);
+			}
+
+			ProcessRawInputPacket(ri);
+			ri = NEXTRAWINPUTBLOCK(ri);
+		}
+	}
+}
+
+static UUtBool ONICALL DDiPlatform_InputEvent_GetEvent(void)
+{
+	// Center the cursor just in case. Raw Input doesn't need it, but sometimes
+	// ClipCursor doesn't work for some reason and in that case we should still
+	// prevent the user from accidentally clicking on other windows.
+	if (LIgMode_Internal)
+		CenterCursor();
+
+	// Do a buffered read of raw input. Apparently this is faster for high-res
+	// mice. Note that we still have to handle WM_INPUT in our wndproc in case
+	// a WM_INPUT message arrives during the message loop.
+	DrainRawInput();
+
+	// Oni only processes a maximum of three messages here (for performance
+	// reasons?) We're actually using Windows messages for input so we need to
+	// process all of them.
+	MSG msg;
+	while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
+		TranslateMessage(&msg);
+		DispatchMessageA(&msg);
+	}
+
+	// Oni returns true here if there are still messages to process, so that
+	// LIrMode_Set and LIrMode_Set_Internal can call this repeatedly to drain
+	// all messages. We always drain all messages so return false.
+	return UUcFalse;
+}
+
+static bool HandleWmInput(HRAWINPUT hri, WPARAM wparam)
+{
+	if (!LIgMode_Internal)
+		return false;
+
+	static RAWINPUT* ri = NULL;
+	static UINT ri_size = 0;
+	UINT minsize = 0;
+
+	GetRawInputData(hri, RID_INPUT, NULL, &minsize, sizeof ri->header);
+	if (ri_size < minsize) {
+		if (ri)
+			free(ri);
+		ri_size = minsize;
+		ri = calloc(1, ri_size);
+	}
+	if (GetRawInputData(hri, RID_INPUT, ri, &ri_size, sizeof ri->header) == (UINT)-1)
+		return false;
+
+	ProcessRawInputPacket(ri);
+	return true;
+}
+
+static void HandleWmWindowPosChanged(WINDOWPOS *pos)
+{
+	if (!LIgMode_Internal)
+		return;
+
+	CenterCursor();
+
+	RECT rc = { pos->x, pos->y, pos->x + pos->cx, pos->y + pos->cy };
+	ClipCursor(&rc);
+}
+
+static void HandleWmKeyboard(int vkey, WORD repeat_count, WORD flags)
+{
+	if (!LIgMode_Internal)
+		return;
+
+	bool is_extended = flags & KF_EXTENDED;
+	bool is_repeat = flags & KF_REPEAT;
+	bool is_up = flags & KF_UP;
+	BYTE sc = LOBYTE(flags);
+
+	// Ignore togglable keys since we handle them specially
+	if (vkey == VK_CAPITAL || vkey == VK_SCROLL || vkey == VK_NUMLOCK)
+		return;
+
+	// Ignore key down messages sent because of key-repeat
+	if (!is_up && is_repeat)
+		return;
+
+	// Apparently some synthetic keyboard messages can be missing the scancode,
+	// so get it from the vkey
+	if (!sc)
+		sc = MapVirtualKeyA(vkey, MAPVK_VK_TO_VSC);
+
+	// DirectInput scan codes have 0x80 set for extended keys, and we're using
+	// a map based on Oni's DirectInput map to convert to key codes
+	if (is_extended)
+		sc |= 0x80;
+	uint8_t ch = DDgPlatform_ScanCodeToChar[sc];
+	if (!ch)
+		return;
+
+	SetKeyState(ch, !is_up);
+}
+
+bool DDrInput_WindowProc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam,
+                         LRESULT* res)
+{
+	// This is called from our own window proc for now, so we only want to use
+	// it when Daodan input is enabled
+	if (!DDgUseDaodanInput)
+		return false;
+
+	switch (msg) {
+		case WM_INPUT:
+			if (HandleWmInput((HRAWINPUT)lparam, wparam)) {
+				*res = 0;
+				return true;
+			}
+			break;
+		case WM_WINDOWPOSCHANGED:
+			HandleWmWindowPosChanged((WINDOWPOS *)lparam);
+			break;
+		case WM_KEYDOWN:
+		case WM_SYSKEYDOWN:
+		case WM_KEYUP:
+		case WM_SYSKEYUP:
+			HandleWmKeyboard(LOWORD(wparam), LOWORD(lparam), HIWORD(lparam));
+			break;
+	}
+
+	return false;
+}
+
+static void ONICALL DDrActionBuffer_Get(short* count, LItActionBuffer** buffers)
+{
+	// So, Oni's version of this function was totally different. In unpatched
+	// Oni, action buffers were produced at 60Hz by a separate high-priority
+	// input thread, LIiInterruptHandleProc, and consumed by
+	// LIrActionBuffer_Get, which was called from the game loop and could
+	// provide multiple outstanding action buffers to the engine at once.
+	//
+	// That was a problem for a couple of reasons. Firstly, the resolution of
+	// Windows timers is limited by the timer frequency, which can be as low as
+	// 15.6ms and in the worst case would cause a delay of 31.2ms between action
+	// buffers. That meant that even if Oni was running at a steady 60 fps, the
+	// input thread would provide no action buffers on a lot of frames.
+	//
+	// Secondly, even though Oni drained the list of pending action buffers by
+	// calling DDrActionBuffer_Get on every frame, the engine only uses them
+	// when the internal game time advances, and that happens on a separate 60Hz
+	// timer which was totally unsynchronized with the 60Hz timer on the input
+	// thread. That wasn't too much of a problem when the game loop was running
+	// at less than 60 fps, but when it ran faster, the only action buffers that
+	// got processed were the ones produced when the game timer and the input
+	// thread timer happened to tick at the same time, meaning potentially a lot
+	// of dropped input.
+	//
+	// Oni's input system was probably designed that way so that input would
+	// still run at 60Hz even on PCs that weren't powerful enough to render at
+	// 60 fps. It was a well-meaning design, but due to the aforementioned
+	// flaws, we do something much different and simpler here. On the frames
+	// that Oni will consume input, we generate a single action buffer inside
+	// DDrActionBuffer_Get based on most up-to-date input.
+
+	// Update ONgGameState->TargetGameTime. We use TargetGameTime to determine
+	// if Oni is going to consume input on this frame. Unfortunately, in
+	// unpatched Oni, the call to ONrGameState_UpdateServerTime happened after
+	// LIrActionBuffer_Get. In Daodan, we NOOP out the original call and call it
+	// here instead, so it runs before our code.
+	ONrGameState_UpdateServerTime(ONgGameState);
+	bool time_updated = ONgGameState->GameTime != ONgGameState->TargetGameTime;
+
+	// Only produce input buffers when input is enabled. LIrActionBuffer_Get
+	// does the same thing. Also only produce them when Oni will consume them.
+	if (!LIgMode_Internal || !time_updated) {
+		*count = 0;
+		*buffers = NULL;
+		return;
+	}
+
+	// Add held keys to the action buffer
+	for (int i = 0; i < ARRAY_SIZE(DDgInputState); i++) {
+		if (DDgInputState[i]) {
+			LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+				.input = i,
+				.analog = 1.0,
+			});
+		}
+	}
+
+	// Add togglable keys to the action buffer
+	if (DDgCapsOniKey && (GetKeyState(VK_CAPITAL) & 0x01)) {
+		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+			.input = DDgCapsOniKey,
+			.analog = 1.0,
+		});
+	}
+	if (DDgScrollOniKey && (GetKeyState(VK_SCROLL) & 0x01)) {
+		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+			.input = DDgScrollOniKey,
+			.analog = 1.0,
+		});
+	}
+	if (DDgNumLockOniKey && (GetKeyState(VK_NUMLOCK) & 0x01)) {
+		LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
+			.input = DDgNumLockOniKey,
+			.analog = 1.0,
+		});
+	}
+
+	// Make a copy of our temporary action buffer with all the input we've
+	// gathered this frame. This is the copy that Oni's engine will see.
+	static LItActionBuffer buf = { 0 };
+	buf = DDgActionBuffer;
+	DDgActionBuffer	= (LItActionBuffer) { 0 };
+
+	*count = 1;
+	*buffers = &buf;
+}
+
+void DDrInput_PatchUtilityInput(void)
+{
+	// Patch the call to ONrGameState_HandleUtilityInput in
+	// ONrGameState_ProcessHeartbeat. This is where Oni checks a bunch of
+	// miscellaneous inputs, and where Mac Oni 1.2.1 checks the cheat bindings.
+	// It's also where Mac Oni toggles the show_triggervolumes flag.
+	DDrPatch_MakeCall((void *)0x004fa91c, (void *)DDrGameState_HandleUtilityInput);
+}
+
+void DDrInput_PatchCustomActions(void)
+{
+	DDrInput_PatchUtilityInput();
+
+	// Replace the function which adds bindings with ours, which checks the list
+	// of custom bindings as well
+	DDrPatch_MakeJump((void *)LIrBinding_Add, (void *)DDrBinding_Add);
+}
+
+void DDrInput_PatchDaodanInput(void)
+{
+	// In LIrInitialize, NOOP the call to UUrInterruptProc_Install and
+	// associated error checking
+	DDrPatch_NOOP((char *)(OniExe + 0x421f), 106);
+
+	// In LIrPlatform_Initialize, NOOP the Windows version checks so we never
+	// use DirectInput
+	DDrPatch_NOOP((char *)(OniExe + 0x2e64), 11);
+
+	// Replace Oni's Windows message loop with one that does buffered raw input
+	// reads and processes all messages
+	DDrPatch_MakeJump((void *)LIiPlatform_InputEvent_GetEvent, (void *)DDiPlatform_InputEvent_GetEvent);
+
+	// Replace the function that gets the latest input frames
+	DDrPatch_MakeJump((void *)LIrActionBuffer_Get, (void *)DDrActionBuffer_Get);
+
+	// Replace the function that gets the mouse cursor position
+	DDrPatch_MakeJump((void *)LIrPlatform_InputEvent_GetMouse, (void *)DDrPlatform_InputEvent_GetMouse);
+
+	// Replace the function that performs platform-specific actions when the
+	// input mode changes
+	DDrPatch_MakeJump((void *)LIrPlatform_Mode_Set, (void *)DDrPlatform_Mode_Set);
+
+	// Replaces the function that tests the state of keyboard keys
+	DDrPatch_MakeJump((void *)LIrPlatform_TestKey, (void *)DDrPlatform_TestKey);
+
+	// Enable extra key names in key_config.txt
+	DDrPatch_MakeJump((void *)LIrTranslate_InputName, (void *)DDrTranslate_InputName);
+
+	// Patch out the call to ONrGameState_UpdateServerTime in ONiRunGame because
+	// we want to do it earlier, in DDrActionBuffer_Get
+	DDrPatch_NOOP((char *)(OniExe + 0xd4708), 11);
+
+	DDgUseDaodanInput = true;
+}
Index: /Daodan/src/Patches/Input.h
===================================================================
--- /Daodan/src/Patches/Input.h	(revision 1162)
+++ /Daodan/src/Patches/Input.h	(revision 1163)
@@ -4,16 +4,20 @@
 #include "../Daodan.h"
 
-typedef uint32_t CustomActionCallbackArgument;
-
-typedef void (*CustomActionCallback_t) (CustomActionCallbackArgument argument);
+typedef void (*DDtCustomActionCallback)(intptr_t ctx);
 
 typedef enum {
-	EVENT_KEYPRESS,
-	EVENT_KEYDOWN,
-} ActionEventType_t;
+	DDcEventType_KeyPress,
+	DDcEventType_KeyDown,
+} DDtActionEventType;
 
-void Input_PatchCode ();
+void DDrInput_RegisterCustomAction(const char *name, DDtActionEventType type,
+                                   DDtCustomActionCallback callback,
+                                   intptr_t ctx);
+bool DDrInput_WindowProc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam,
+                         LRESULT* res);
 
-void Input_RegisterCustomAction (const char* actionname, ActionEventType_t eventType, uint32_t keydownTimeoutTicks, CustomActionCallback_t callback, CustomActionCallbackArgument callbackArgument);
+void DDrInput_PatchUtilityInput(void);
+void DDrInput_PatchCustomActions(void);
+void DDrInput_PatchDaodanInput(void);
 
 #endif
Index: /Daodan/src/Patches/Patches.c
===================================================================
--- /Daodan/src/Patches/Patches.c	(revision 1162)
+++ /Daodan/src/Patches/Patches.c	(revision 1163)
@@ -183,15 +183,4 @@
 	return Oni_ONrMechanics_Register(inObjectType, inObjectTypeIndex, inGroupName, inSizeInMemory, inObjectMethods, inFlags, inMechanicsMethods);
 }
-
-_ONrGameState_HandleUtilityInput Oni_ONrGameState_HandleUtilityInput = (_ONrGameState_HandleUtilityInput)0;
-void ONICALL DD_ONrGameState_HandleUtilityInput(const void* inInput)
-{
-	Oni_ONrGameState_HandleUtilityInput(inInput);
-	
-	if (ONrDebugKey_WentDown(7)) {
-		OBJgTriggerVolume_Visible = !OBJgTriggerVolume_Visible;
-	}
-}
-
 
 // Enables d_regen script command. Instead of one global flag to only regenerate player each char has a flag to enable local regeneration
@@ -385,17 +374,4 @@
 		DD_Patch_Chinese();
 
-	// Limit cursor to Oni's window
-	if (DDrConfig_GetOptOfType("windows.clipcursor", C_BOOL)->value.intBoolVal)
-	{
-		// LIrMode_Set: replace LIrPlatform_Mode_Set call with our hook.
-		DDrPatch_MakeCall((void*)(OniExe + 0x00003f9f), (void*) DD_LIrPlatform_Mode_Set);
-
-		// LIrMode_Set_Internal: replace LIrPlatform_Mode_Set call with our hook.
-		DDrPatch_MakeCall((void*)(OniExe + 0x00003fff), (void*) DD_LIrPlatform_Mode_Set);
-	
-		// LIrTerminate: replace LIrPlatform_Terminate call with our hook.
-		DDrPatch_MakeCall((void*)(OniExe + 0x000004cb8), (void*) DD_LIrPlatform_Terminate);
-	}
-
 	// Disables weapon cooldown exploit
 	if (DDrConfig_GetOptOfType("gameplay.cooldowntimer", C_BOOL)->value.intBoolVal)
@@ -410,10 +386,33 @@
 		DDrPatch_MakeJump((void*)gl_enumerate_valid_display_modes, (void*)DD_GLrEnumerateDisplayModes);
 	}
-	
-	// Forced DirectInput (for Windows NT)
-	if (DDrConfig_GetOptOfType("windows.directinput", C_BOOL)->value.intBoolVal)
-	{
-		// LIrPlatform_Initialize: replace conditional jump by unconditional
-		DDrPatch_Byte((char*)(OniExe + 0x00002e6d), 0xeb);
+
+	if (DDrConfig_GetOptOfType("windows.daodaninput", C_BOOL)->value.intBoolVal)
+	{
+		DDrInput_PatchDaodanInput();
+	}
+	else
+	{
+		// The following patches aren't compatible with, or are superseded by
+		// Daodan input
+
+		// Limit cursor to Oni's window
+		if (DDrConfig_GetOptOfType("windows.clipcursor", C_BOOL)->value.intBoolVal)
+		{
+			// LIrMode_Set: replace LIrPlatform_Mode_Set call with our hook.
+			DDrPatch_MakeCall((void*)(OniExe + 0x00003f9f), (void*) DD_LIrPlatform_Mode_Set);
+
+			// LIrMode_Set_Internal: replace LIrPlatform_Mode_Set call with our hook.
+			DDrPatch_MakeCall((void*)(OniExe + 0x00003fff), (void*) DD_LIrPlatform_Mode_Set);
+
+			// LIrTerminate: replace LIrPlatform_Terminate call with our hook.
+			DDrPatch_MakeCall((void*)(OniExe + 0x000004cb8), (void*) DD_LIrPlatform_Terminate);
+		}
+
+		// Forced DirectInput (for Windows NT)
+		if (DDrConfig_GetOptOfType("windows.directinput", C_BOOL)->value.intBoolVal)
+		{
+			// LIrPlatform_Initialize: replace conditional jump by unconditional
+			DDrPatch_Byte((char*)(OniExe + 0x00002e6d), 0xeb);
+		}
 	}
 
@@ -449,8 +448,6 @@
 	// Allow custom actions to be bound through Daodan
 	if (DDrConfig_GetOptOfType("gameplay.customactions", C_BOOL)->value.intBoolVal)
-	{
-		Input_PatchCode ();
-	}
-	
+		DDrInput_PatchCustomActions();
+
 	// Hackish fix for Konoko not kicking guns
 	// Don't use this, it breaks stairs.
@@ -571,8 +568,8 @@
 	if (DDrConfig_GetOptOfType("devmode.showtriggervolumes", C_BOOL)->value.intBoolVal)
 	{
+		DDrInput_PatchUtilityInput();
 		Oni_ONrMechanics_Register = DDrPatch_MakeDetour((void*)ONrMechanics_Register, (void*)DD_ONrMechanics_Register);
-		Oni_ONrGameState_HandleUtilityInput = DDrPatch_MakeDetour((void*)ONrGameState_HandleUtilityInput, (void*)DD_ONrGameState_HandleUtilityInput);
-	}
-	
+	}
+
 	// Experiment with allowing enemies to be thrown over railings
 	if (DDrConfig_GetOptOfType("gameplay.throwtest", C_BOOL)->value.intBoolVal)
@@ -591,4 +588,7 @@
 		DDrPatch_NOOP((char*) OniExe + 0x0002651c, 6);
 		DDrPatch_MakeCall((char*) OniExe + 0x0002651c, (void*) GetClientRect);
+
+		// Note: Daodan input makes the following GetCursorPos and SetCursorPos
+		// patches unnecessary
 
 		// LIrPlatform_PollInputForAction: fix GetCursorPos call to return client coordinates.
Index: /Daodan/src/Patches/Utility.c
===================================================================
--- /Daodan/src/Patches/Utility.c	(revision 1162)
+++ /Daodan/src/Patches/Utility.c	(revision 1163)
@@ -102,14 +102,8 @@
 		memcpy(output_ptr, default_msg, sizeof(default_msg));
 	}
-	
+
 }
 
-typedef struct
-{
-	uint16_t x;
-	uint16_t y;
-
-} IMtPoint2D;
-IMtPoint2D Point = {256, 250};
+IMtPoint Point = {256, 250};
 extern void* TSrTest;
 extern void* TSrScores;
Index: /Daodan/src/Patches/Win32.c
===================================================================
--- /Daodan/src/Patches/Win32.c	(revision 1162)
+++ /Daodan/src/Patches/Win32.c	(revision 1163)
@@ -3,4 +3,5 @@
 #include "../Daodan.h"
 #include "../Daodan_Config.h"
+#include "Input.h"
 #include "Win32.h"
 
@@ -105,8 +106,12 @@
 				return TRUE;
 			}
-			
+
 			break;
 	}
-	
+
+	LRESULT res;
+	if (DDrInput_WindowProc(hWnd, uMsg, wParam, lParam, &res))
+		return res;
+
 	return ONrPlatform_WindowProc(hWnd, uMsg, wParam, lParam);
 }
Index: /Daodan/src/makefile
===================================================================
--- /Daodan/src/makefile	(revision 1162)
+++ /Daodan/src/makefile	(revision 1163)
@@ -6,6 +6,6 @@
 DEF = BEA_ENGINE_STATIC
 INCLUDEPATHS = .
-GCCFLAGS = -std=gnu99 -O0 -Wall -fomit-frame-pointer -fpack-struct -Wextra -Wno-pragmas -Wno-unused-variable $(addprefix -I,$(INCLUDEPATHS)) -Wno-unused-parameter $(addprefix -D,$(DEF))
-LINKFLAGS = -O0 -Wall -fomit-frame-pointer -fpack-struct -s -mdll
+GCCFLAGS = -std=gnu99 -ggdb -O0 -Wall -fomit-frame-pointer -Wextra -Wno-pragmas -Wno-unused-variable $(addprefix -I,$(INCLUDEPATHS)) -Wno-unused-parameter $(addprefix -D,$(DEF))
+LINKFLAGS = -O0 -Wall -fomit-frame-pointer -mdll
 LOCALE = LC_MESSAGES=C
 
