--- Daodan/src/Patches/Input.c 2015/03/23 23:29:19 1017 +++ Daodan/src/Patches/Input.c 2021/10/24 02:50:48 1163 @@ -4,84 +4,869 @@ #include "../Daodan.h" #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; - } else { - STARTUPMESSAGE("Registering action %s failed, maximum actions reached", actionname); + 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]; +} -_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; +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); } +} - return Oni_LIrBinding_Add(key, name); +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; } -_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; - } +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); } } - 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); +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; +}