#include #include #include "../Daodan.h" #include "Input.h" #include "../Oni/Oni.h" #include "../Daodan_Config.h" #include "../Daodan_Patch.h" #include "Utility.h" typedef struct { 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 { 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; 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; }