From eb8f733e67672550ca2ab19112772f7af9d97291 Mon Sep 17 00:00:00 2001 From: anticroom Date: Tue, 1 Sep 2026 23:47:45 -0400 Subject: [PATCH] Added CBF --- .../Click-Between-Frames-master/.editorconfig | 9 + .../.github/workflows/build.yml | 52 ++ .../Click-Between-Frames-master/.gitignore | 2 + .../Click-Between-Frames-master/.gitmodules | 9 + .../CMakeLists.txt | 27 + mods-v2/Click-Between-Frames-master/LICENSE | 21 + mods-v2/Click-Between-Frames-master/README.md | 45 ++ .../src/dllmain.cpp | 29 + .../Click-Between-Frames-master/src/gd.hpp | 147 +++++ .../src/includes.hpp | 61 ++ .../src/linux/build.sh | 4 + .../src/linux/linux-input.cpp | 388 ++++++++++++ .../src/linuxeventcodes.hpp | 33 + .../Click-Between-Frames-master/src/log.cpp | 59 ++ .../Click-Between-Frames-master/src/log.hpp | 11 + .../Click-Between-Frames-master/src/main.cpp | 468 ++++++++++++++ .../src/options.cpp | 110 ++++ .../src/options.hpp | 8 + .../src/settings.cpp | 90 +++ .../src/settings.hpp | 16 + .../src/thunks.cpp | 17 + .../src/windows.cpp | 578 ++++++++++++++++++ .../src/windows.hpp | 46 ++ 23 files changed, 2230 insertions(+) create mode 100644 mods-v2/Click-Between-Frames-master/.editorconfig create mode 100644 mods-v2/Click-Between-Frames-master/.github/workflows/build.yml create mode 100644 mods-v2/Click-Between-Frames-master/.gitignore create mode 100644 mods-v2/Click-Between-Frames-master/.gitmodules create mode 100644 mods-v2/Click-Between-Frames-master/CMakeLists.txt create mode 100644 mods-v2/Click-Between-Frames-master/LICENSE create mode 100644 mods-v2/Click-Between-Frames-master/README.md create mode 100644 mods-v2/Click-Between-Frames-master/src/dllmain.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/gd.hpp create mode 100644 mods-v2/Click-Between-Frames-master/src/includes.hpp create mode 100644 mods-v2/Click-Between-Frames-master/src/linux/build.sh create mode 100644 mods-v2/Click-Between-Frames-master/src/linux/linux-input.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/linuxeventcodes.hpp create mode 100644 mods-v2/Click-Between-Frames-master/src/log.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/log.hpp create mode 100644 mods-v2/Click-Between-Frames-master/src/main.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/options.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/options.hpp create mode 100644 mods-v2/Click-Between-Frames-master/src/settings.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/settings.hpp create mode 100644 mods-v2/Click-Between-Frames-master/src/thunks.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/windows.cpp create mode 100644 mods-v2/Click-Between-Frames-master/src/windows.hpp diff --git a/mods-v2/Click-Between-Frames-master/.editorconfig b/mods-v2/Click-Between-Frames-master/.editorconfig new file mode 100644 index 00000000..91cc997d --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[CMakeLists.txt] +indent_style = space +indent_size = 2 + +[*.{cpp, hpp, h}] +indent_style = space +indent_size = 4 diff --git a/mods-v2/Click-Between-Frames-master/.github/workflows/build.yml b/mods-v2/Click-Between-Frames-master/.github/workflows/build.yml new file mode 100644 index 00000000..978af8b3 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/.github/workflows/build.yml @@ -0,0 +1,52 @@ +name: Build + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure + run: cmake -B build -A Win32 "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + + - name: Build + run: cmake --build build --config Release + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ClickBetweenFrames + path: build/Release/ClickBetweenFrames.dll + if-no-files-found: error + + - name: Move nightly tag + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + run: | + git tag -f nightly ${{ github.sha }} + git push -f origin nightly + + - name: Release + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: softprops/action-gh-release@v2 + with: + tag_name: nightly + name: Nightly build + body: | + Automated build of `${{ github.sha }}`. + + ${{ github.event.head_commit.message }} + files: build/Release/ClickBetweenFrames.dll + prerelease: true diff --git a/mods-v2/Click-Between-Frames-master/.gitignore b/mods-v2/Click-Between-Frames-master/.gitignore new file mode 100644 index 00000000..6f31401f --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/.gitignore @@ -0,0 +1,2 @@ +build/ +.vscode/ diff --git a/mods-v2/Click-Between-Frames-master/.gitmodules b/mods-v2/Click-Between-Frames-master/.gitmodules new file mode 100644 index 00000000..d951fe02 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/.gitmodules @@ -0,0 +1,9 @@ +[submodule "libraries/minhook"] + path = libraries/minhook + url = https://github.com/TsudaKageyu/minhook.git +[submodule "libraries/cocos-headers"] + path = libraries/cocos-headers + url = https://github.com/HJfod/cocos-headers +[submodule "libraries/gd.h"] + path = libraries/gd.h + url = https://github.com/poweredbypie/gd.h diff --git a/mods-v2/Click-Between-Frames-master/CMakeLists.txt b/mods-v2/Click-Between-Frames-master/CMakeLists.txt new file mode 100644 index 00000000..2fa12146 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.10) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +project(ClickBetweenFrames) + +file(GLOB SOURCE_FILES CONFIGURE_DEPENDS src/*.cpp) +add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES}) + +# this is so stupid +set(CMAKE_SIZEOF_VOID_P 4) + +if (${CMAKE_CXX_COMPILER_ID} STREQUAL Clang) + # ensure 32 bit on clang + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -target i386-pc-windows-msvc") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -target i386-pc-windows-msvc") + add_definitions("--target=i386-pc-windows-msvc") +endif() + +target_include_directories(${PROJECT_NAME} PRIVATE + libraries/minhook/include +) + +add_subdirectory(libraries/minhook) +add_subdirectory(libraries/cocos-headers) + +target_link_libraries(${PROJECT_NAME} minhook cocos2d) diff --git a/mods-v2/Click-Between-Frames-master/LICENSE b/mods-v2/Click-Between-Frames-master/LICENSE new file mode 100644 index 00000000..928855f6 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 theyareonit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mods-v2/Click-Between-Frames-master/README.md b/mods-v2/Click-Between-Frames-master/README.md new file mode 100644 index 00000000..35936163 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/README.md @@ -0,0 +1,45 @@ +I'm not gonna bother updating the README for this mod since it's a bit of a mess, but here it is anyway. + +# Installation + +Install [Geode](https://geode-sdk.org/install/), then install this mod from the Geode index. + +# What does it do + +This mod allows your inputs to register in between visual frames, which drastically increases input precision on low framerates like 60FPS. + +If you like the mod, please consider [donating](https://www.paypal.com/donate/?hosted_button_id=U2LWN9H395TF8)! + +# Why not just use Click Between Steps? + +Because CBS is limited to 480TPS precision, while this mod does not have a hard limit on input precision (well, it technically does, but it's an extremely large number that would be hard to calculate). Also, this mod has a few fixes to improve the precision even further than Robtop's implementation, even if you ignore the 480TPS limit of Rob's version. + +# How to use + +If the icon is automatically jumping when you respawn, disable the "Stop Triggers on Death" hack in Mega Hack. + +To edit keybinds, go to the GD options menu and click the "Keys" button in the top right (requires Custom Keybinds). + +It is recommended to use either Physics Bypass or one of these FPS values: 60, 80, 120, or 240. \ +This is because 2.2 has stutters on FPS values that aren't factors or multiples of 240 unless you enable Physics Bypass. + +Disable TPS Bypass/Draw Divide when using this mod, because they're pointless. \ +This mod automatically overrides the vanilla "Click On Steps" and "Click Between Steps" options, so you don't need to worry about those. + +The mod comes with its own version of Physics Bypass in the mod options (on Windows/Linux). Be warned that not all lists or leaderboards that allow CBF will consider this legit! + +If on Linux, and the mod doesn't work, please try running the command sudo usermod -aG input $USER (this will make your system slightly less secure). + +# Known issues + +- This mod does not work with bots +- Linux controller support is experimental + +# Credits + +Icon by alex/sincos. + +Android, macOS, & iOS code based off [Click on Steps by zmx](https://github.com/qimiko/click-on-steps), used with permission. \ +Android port by [mat](https://github.com/matcool), macOS & iOS ports by [Jasmine](https://github.com/hiimjasmine00). + +(these credits are slightly outdated since the mod just uses the vanilla game's input handling now but oh well) \ No newline at end of file diff --git a/mods-v2/Click-Between-Frames-master/src/dllmain.cpp b/mods-v2/Click-Between-Frames-master/src/dllmain.cpp new file mode 100644 index 00000000..9d9046a8 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/dllmain.cpp @@ -0,0 +1,29 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +HMODULE g_module = nullptr; + +void modLoaded(); + +DWORD WINAPI thread_func(void*) { + // quickldr can get us in before cocos is around and we hook one of its exports + for (int i = 0; i < 200 && !GetModuleHandleA("libcocos2d.dll"); i++) Sleep(50); + + modLoaded(); + return 0; +} + +BOOL APIENTRY DllMain(HMODULE handle, DWORD reason, LPVOID reserved) { + if (reason == DLL_PROCESS_ATTACH) { + g_module = handle; + DisableThreadLibraryCalls(handle); + + auto h = CreateThread(0, 0, thread_func, handle, 0, 0); + if (h) + CloseHandle(h); + else + return FALSE; + } + return TRUE; +} diff --git a/mods-v2/Click-Between-Frames-master/src/gd.hpp b/mods-v2/Click-Between-Frames-master/src/gd.hpp new file mode 100644 index 00000000..c63857b9 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/gd.hpp @@ -0,0 +1,147 @@ +#pragma once + +// minimal 2.113 bindings, offsets from the windows binary + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include + +using namespace cocos2d; + +inline uintptr_t gdBase() { + static uintptr_t base = reinterpret_cast(GetModuleHandleA(nullptr)); + return base; +} + +inline uintptr_t cocosBase() { + static uintptr_t base = reinterpret_cast(GetModuleHandleA("libcocos2d.dll")); + return base; +} + +enum class PlayerButton : int { + Jump = 1, + Left = 2, + Right = 3 +}; + +// our own ids for controller buttons, they just have to be unique +enum ControllerKeyCodes : int { + CONTROLLER_A = 1000, + CONTROLLER_B = 1001, + CONTROLLER_X = 1002, + CONTROLLER_Y = 1003, + CONTROLLER_Start = 1004, + CONTROLLER_Back = 1005, + CONTROLLER_RB = 1006, + CONTROLLER_LB = 1007, + CONTROLLER_RT = 1008, + CONTROLLER_LT = 1009, + CONTROLLER_Up = 1010, + CONTROLLER_Down = 1011, + CONTROLLER_Left = 1012, + CONTROLLER_Right = 1013, + CONTROLLER_LTHUMBSTICK_UP = 1014, + CONTROLLER_LTHUMBSTICK_DOWN = 1015, + CONTROLLER_LTHUMBSTICK_LEFT = 1016, + CONTROLLER_LTHUMBSTICK_RIGHT = 1017, + CONTROLLER_RTHUMBSTICK_UP = 1018, + CONTROLLER_RTHUMBSTICK_DOWN = 1019, + CONTROLLER_RTHUMBSTICK_LEFT = 1020, + CONTROLLER_RTHUMBSTICK_RIGHT = 1021 +}; + +class PlayerObject { +public: + char m_pad000[0x488]; + CCDictionary* m_collisionLogBottom; // 0x488 + CCDictionary* m_collisionLogTop; // 0x48C + char m_pad490[0x4B8 - 0x490]; + int m_lastCollisionBottom; // 0x4B8 + int m_lastCollisionTop; // 0x4BC + char m_pad4C0[0x628 - 0x4C0]; + double m_yVelocity; // 0x628 + bool m_isOnSlope; // 0x630 + bool m_wasOnSlope; // 0x631 + char m_pad632[0x638 - 0x632]; + bool m_isShip; // 0x638 + bool m_isBird; // 0x639 + bool m_isBall; // 0x63A + bool m_isDart; // 0x63B + bool m_isRobot; // 0x63C + bool m_isSpider; // 0x63D + bool m_isUpsideDown; // 0x63E + char m_pad63F[1]; + bool m_isOnGround; // 0x640 + bool m_isDashing; // 0x641 + char m_pad642[0x66C - 0x642]; + CCArray* m_touchingRings; // 0x66C + char m_pad670[0x67C - 0x670]; + CCPoint m_position; // 0x67C +}; + +class PlayLayer { +public: + char m_pad000[0x224]; + PlayerObject* m_player1; // 0x224 + PlayerObject* m_player2; // 0x228 + char m_pad22C[0x2A9 - 0x22C]; + bool m_isDualMode; // 0x2A9 + char m_pad2AA[0x39C - 0x2AA]; + bool m_playerDied; // 0x39C + char m_pad39D[0x494 - 0x39D]; + bool m_isTestMode; // 0x494 + char m_pad495[0x4BD - 0x495]; + bool m_hasLevelCompleteMenu; // 0x4BD + char m_pad4BE[0x52F - 0x4BE]; + bool m_isPaused; // 0x52F + + static PlayLayer* get(); + + // 2.1 takes the delta in xmm2 and the player on the stack, so this needs a thunk + void checkCollisions(PlayerObject* player, float dt); + + // 2.1's equivalent of GJBaseGameLayer::handleButton; the int argument is unused + void pushButton(int button, bool isPlayer1) { + reinterpret_cast(gdBase() + 0x111500)(this, button, isPlayer1); + } + void releaseButton(int button, bool isPlayer1) { + reinterpret_cast(gdBase() + 0x111660)(this, button, isPlayer1); + } +}; + +class GameManager { +public: + char m_pad000[0x164]; + PlayLayer* m_playLayer; // 0x164 + void* m_editorLayer; // 0x168 + + static GameManager* sharedState() { + return reinterpret_cast(gdBase() + 0xC4A50)(); + } + + PlayLayer* getPlayLayer() { return m_playLayer; } + void* getEditorLayer() { return m_editorLayer; } +}; + +#pragma runtime_checks("s", off) +class FLAlertLayer { +public: + // the caption is passed by value as an std::string so the caller has to clean the stack + static FLAlertLayer* create(void* target, const char* title, const char* btn1, const char* btn2, std::string caption) { + auto ret = reinterpret_cast( + gdBase() + 0x22680)(target, title, btn1, btn2, caption); + __asm add esp, 0x20 + return ret; + } + void show() { + reinterpret_cast(gdBase() + 0x23560)(this); + } +}; +#pragma runtime_checks("s", restore) + +inline PlayLayer* PlayLayer::get() { + return GameManager::sharedState()->getPlayLayer(); +} diff --git a/mods-v2/Click-Between-Frames-master/src/includes.hpp b/mods-v2/Click-Between-Frames-master/src/includes.hpp new file mode 100644 index 00000000..0cc6e6e1 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/includes.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gd.hpp" +#include "settings.hpp" + +using TimestampType = int64_t; +TimestampType getCurrentTimestamp(); + +enum GameAction : int { + p1Jump = 0, + p1Left = 1, + p1Right = 2, + p2Jump = 3, + p2Left = 4, + p2Right = 5 +}; + +enum State : bool { + Release = 0, + Press = 1 +}; + +struct InputEvent { + TimestampType time; + PlayerButton inputType; + bool inputState; + bool isPlayer1; +}; + +struct Step { + InputEvent input; + double deltaFactor; + bool endStep; +}; + +extern std::deque inputQueue; +extern std::deque inputQueueCopy; + +extern std::array, 6> inputBinds; +extern std::unordered_set heldInputs; + +extern std::mutex inputQueueLock; +extern std::mutex keybindsLock; + +extern std::atomic enableRightClick; +// true -> cbf disabled, confusing i know +extern std::atomic softToggle; + +extern bool threadPriority; + +// some windows only global variables +#include "windows.hpp" diff --git a/mods-v2/Click-Between-Frames-master/src/linux/build.sh b/mods-v2/Click-Between-Frames-master/src/linux/build.sh new file mode 100644 index 00000000..62957b1b --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/linux/build.sh @@ -0,0 +1,4 @@ +# too lazy to make this into a github workflow +winemaker -iws2_32 -ievdev . --single-target linux-input +make +cp linux-input.exe.so ../../resources/linux-input.so diff --git a/mods-v2/Click-Between-Frames-master/src/linux/linux-input.cpp b/mods-v2/Click-Between-Frames-master/src/linux/linux-input.cpp new file mode 100644 index 00000000..bb086176 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/linux/linux-input.cpp @@ -0,0 +1,388 @@ +#include // thankfully this can be put before windows.h, it bugs otherwise +#include +#include // winelib lets you use both windows and linux apis + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +enum DeviceType : int8_t { + MOUSE, + TOUCHPAD, + KEYBOARD, + TOUCHSCREEN, + CONTROLLER, + UNKNOWN +}; + +struct __attribute__((packed)) LinuxInputEvent { + LARGE_INTEGER time; + USHORT type; + USHORT code; + int value; + DeviceType deviceType; +}; + +constexpr size_t BUFFER_SIZE = 20; +constexpr int MAX_EVENTS = 10; + +#define INOTIFY_EVENT_SIZE ( sizeof (struct inotify_event) ) +#define INOTIFY_BUF_LEN ( 1024 * ( INOTIFY_EVENT_SIZE + 16 ) ) + +std::atomic should_quit{false}; + +void stop(int i) { + should_quit.store(true); +} + +LARGE_INTEGER convert_time(timeval t) { // convert timeval to windows file time + LARGE_INTEGER wft; + wft.QuadPart = ((static_cast(t.tv_sec) + 11644473600) * 10000000) + (t.tv_usec * 10); // hopefully wine doesnt change how this is calculated + return wft; +} + +USHORT convert_scan_code(USHORT code) { + static const std::array special_codes = []() { + std::array map{}; + map[96 - 96] = 0xE01C; // KPENTER + map[97 - 96] = 0xE01D; // RIGHTCTRL + map[98 - 96] = 0xE035; // KPSLASH + map[100 - 96] = 0xE038; // RIGHTALT + map[102 - 96] = 0xE047; // HOME + map[103 - 96] = 0xE048; // UP + map[104 - 96] = 0xE049; // PAGEUP + map[105 - 96] = 0xE04B; // LEFT + map[106 - 96] = 0xE04D; // RIGHT + map[107 - 96] = 0xE04F; // END + map[108 - 96] = 0xE050; // DOWN + map[109 - 96] = 0xE051; // PAGEDOWN + map[110 - 96] = 0xE052; // INSERT + map[111 - 96] = 0xE053; // DELETE + map[113 - 96] = 0xE020; // MUTE + map[114 - 96] = 0xE02E; // VOLUMEDOWN + map[115 - 96] = 0xE030; // VOLUMEUP + return map; + }(); + + return (code > 96) && (code < 116) ? special_codes[code - 96] : code; +} + +DWORD WINAPI gd_watchdog(LPVOID) { // CreateProcess doesn't return a handle for linux-input.exe, so a job object to auto-close linux-input.exe isn't an option + HANDLE gdMutex = OpenMutex(SYNCHRONIZE, FALSE, "CBFWatchdogMutex"); + if (gdMutex == NULL) { + std::cerr << "[CBF] Failed to open mutex: " << GetLastError() << std::endl; + should_quit.store(true); + return 1; + } + WaitForSingleObject(gdMutex, INFINITE); + ReleaseMutex(gdMutex); + should_quit.store(true); + return 0; +} + +void add_input_device(std::string path, int epoll_fd, std::vector &devices, std::vector &devices_paths){ + int fd = open(path.c_str(), O_RDONLY); + if (fd == -1) { + // We ignore errno if its 2 because when a device is disconnected, an IN_ATTRIB signal will still be sent, + // causing it to try to add the now deleted device. And we ignore errno 13 because it means that the IN_ATTRIB + // signal that we catched is not the right one and we can't access the device yet. More information below. + if(errno == 2 || errno == 13) return; + std::cerr << "[CBF] Failed to open " << path << ": " << strerror(errno) << std::endl; + return; + } + + libevdev* dev = nullptr; + int rc = libevdev_new_from_fd(fd, &dev); + if (rc < 0) { + std::cerr << "[CBF] Failed to create evdev device for " << path << ": " << strerror(-rc) << std::endl; + close(fd); + return; + } + + int bus = libevdev_get_id_bustype(dev); + if (bus == BUS_USB || bus == BUS_BLUETOOTH || bus == BUS_I8042 || bus == BUS_VIRTUAL) { + epoll_event ev; + ev.events = EPOLLIN; + ev.data.ptr = dev; + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { + std::cerr << "[CBF] Failed to add fd to epoll for " << path << ": " << strerror(errno) << std::endl; + libevdev_free(dev); + close(fd); + return; + } + + devices.push_back(dev); + devices_paths.push_back(path); + std::cerr << "[CBF] Added device: " << path << std::endl; + } else { + libevdev_free(dev); + close(fd); + } +} + +void remove_input_device(std::string path, std::vector &devices, std::vector &devices_paths){ + auto finder = std::find(devices_paths.begin(), devices_paths.end(), path); + int index = std::distance(devices_paths.begin(), finder); + if(finder == devices_paths.end()){ + std::cerr << "[CBF] Input device scheduled to be removed was not found." << std::endl; + return; + } + + close(libevdev_get_fd(devices[index])); + libevdev_free(devices[index]); + devices.erase(devices.begin() + index); + devices_paths.erase(devices_paths.begin() + index); + + std::cerr << "[CBF] Removed device: " << path << std::endl; +} + +// ensure an axis is consistent among all controllers +int32_t normalize_axis(struct libevdev* dev, int code, int val, int min, int max) { + int abs_min = libevdev_get_abs_minimum(dev, code); + int abs_max = libevdev_get_abs_maximum(dev, code); + float normalized = static_cast(val - abs_min) / static_cast(abs_max - abs_min); + int32_t scaled = static_cast(normalized * (max - min)) + min; + return scaled; +} + +int main() { + std::cerr << "[CBF] Linux input program started" << std::endl; + std::vector devices; + // To my knowledge, there is not a proper way to access a device's path using libevdev, so we need to store + // their paths in a separate vector. + std::vector devices_paths; + + const char* input_dir = "/dev/input/"; + + int epoll_fd = epoll_create1(0); + if (epoll_fd == -1) { + std::cerr << "[CBF] Failed to create epoll instance: " << strerror(errno) << std::endl; + return 1; + } + + int inotify_fd = inotify_init1(IN_NONBLOCK); + if (inotify_fd < 0){ + std::cerr << "[CBF] Failed to create inotify instance: " << strerror(errno) << std::endl; + return 1; + } + + int inotify_watch = inotify_add_watch(inotify_fd, input_dir, IN_DELETE | IN_ATTRIB); + if(inotify_watch < 0){ + std::cerr << "[CBF] Failed to create an inotify watch: " << strerror(errno) << std::endl; + return 1; + } + char inotify_buffer[INOTIFY_BUF_LEN]; + + DIR* dir = opendir(input_dir); + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + std::string filename(entry->d_name); + if (filename.find("event") == 0) { + std::string path = std::string(input_dir) + filename; + add_input_device(path, epoll_fd, devices, devices_paths); + } + } + closedir(dir); + + signal(SIGINT, stop); + signal(SIGTERM, stop); + + HANDLE hSharedMem = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "LinuxSharedMemory"); + if (hSharedMem == NULL) { + std::cerr << "[CBF] Failed to open file mapping: " << GetLastError() << std::endl; + return 1; + } + + LPVOID pBuf = MapViewOfFile(hSharedMem, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(LinuxInputEvent) * BUFFER_SIZE); + if (pBuf == NULL) { + std::cerr << "[CBF] Failed to map view of file: " << GetLastError() << std::endl; + CloseHandle(hSharedMem); + return 1; + } + + LinuxInputEvent* shared_events = static_cast(pBuf); + + HANDLE hMutex = OpenMutex(SYNCHRONIZE, FALSE, "CBFLinuxMutex"); + if (hMutex == NULL) { + std::cerr << "[CBF] Failed to open mutex: " << GetLastError() << std::endl; + UnmapViewOfFile(pBuf); + CloseHandle(hSharedMem); + return 1; + } + + if (devices.empty()) { + std::cerr << "[CBF] No input devices" << std::endl; + close(epoll_fd); + + DWORD waitResult = WaitForSingleObject(hMutex, 1000); + if (waitResult == WAIT_OBJECT_0) { + shared_events[0].type = 3; // cant access input devices + ReleaseMutex(hMutex); + } + else if (waitResult != WAIT_TIMEOUT) { + std::cerr << "[CBF] Failed to acquire mutex: " << GetLastError() << std::endl; + } + + return 1; + } + + std::cerr << "[CBF] Waiting for input events" << std::endl; + CreateThread(NULL, 0, gd_watchdog, NULL, 0, NULL); + + epoll_event events[MAX_EVENTS]; + + while (!should_quit.load()) { + + // Reads if there's an inotify event ready + int inotify_len = read(inotify_fd, inotify_buffer, INOTIFY_BUF_LEN); + if(inotify_len > 0){ + int i = 0; + + while(i < inotify_len){ + struct inotify_event *event = ( struct inotify_event * ) &inotify_buffer[ i ]; + + if(event->len) { + i+= INOTIFY_EVENT_SIZE + event->len; + + // Saves the current device path and name in a string and skips the loop if it's is not a keyboard + std::string device_name = std::string(event->name); + std::string path = std::string(input_dir) + device_name; + if(device_name.find("event") != 0) continue; + + // This signal is sent whenever a file (in this case, device) attributes are modified. + // We call the add_input_device function in here and not in IN_CREATE because we + // cannot access the device inmediatly after creation, and we have to wait for the proper + // IN_ATTRIB signal (it doesn't neccesarily have to be the first one). + if(event->mask & IN_ATTRIB) { + add_input_device(path, epoll_fd, devices, devices_paths); + } + // This is called when a device is disconnected. Before IN_DELETE is called, IN_ATTRIB is also + // called, but we ignore that signal with the conditional found in add_input_device. + else if(event->mask & IN_DELETE){ + remove_input_device(path, devices, devices_paths); + } + } + } + } + + int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, 100); + if (nfds == -1) { + if (errno == EINTR) continue; // timeout + std::cerr << "[CBF] Failed to epoll_wait: " << strerror(errno) << std::endl; + break; + } + + for (int n = 0; n < nfds; ++n) { + struct libevdev* dev = static_cast(events[n].data.ptr); + struct input_event ev; + + while (libevdev_has_event_pending(dev)) { + int rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); + if (rc != -EAGAIN && rc != 0) { + // Seems like even after removing the device, if there is a libevdev event pending + // it will still try to access it, but that is not possible anymore. + if (rc == -ENODEV) break; + + std::cerr << "[CBF] Error reading event: " << strerror(-rc) << std::endl; + break; + } + + LARGE_INTEGER time = convert_time(ev.time); + USHORT code = ev.code; + int value = ev.value; + DeviceType device_type; + + if (libevdev_has_event_type(dev, EV_REL)) { + if (ev.type != EV_KEY || ev.value == 2) { // Exclude autorepeat + continue; + } + device_type = MOUSE; + } + else if (libevdev_has_event_code(dev, EV_KEY, KEY_1)) { + if (ev.type != EV_KEY || ev.value == 2) { // Exclude autorepeat + continue; + } + device_type = KEYBOARD; + code = convert_scan_code(ev.code); + } + else if (libevdev_has_property(dev, INPUT_PROP_DIRECT)) { + if (ev.type != EV_KEY || ev.value == 2) { // Exclude autorepeat + continue; + } + device_type = TOUCHSCREEN; + } + else if (libevdev_has_property(dev, INPUT_PROP_BUTTONPAD)) { + if (ev.type != EV_KEY || ev.value == 2) { // Exclude autorepeat + continue; + } + device_type = TOUCHPAD; + } + else if (libevdev_has_event_code(dev, EV_KEY, BTN_GAMEPAD)) { + device_type = CONTROLLER; + if (ev.type == EV_ABS) { + if (ev.code == ABS_Z || ev.code == ABS_RZ) value = normalize_axis(dev, code, value, 0, 255); // different range for lt and rt + else value = normalize_axis(dev, code, value, -32768, 32767); + } + else if (ev.type != EV_KEY || ev.value == 2) { // Exclude autorepeat + continue; + } + } + else { + if (ev.type != EV_KEY || ev.value == 2) { // Exclude autorepeat + continue; + } + device_type = UNKNOWN; + } + + DWORD waitResult = WaitForSingleObject(hMutex, 1000); + if (waitResult == WAIT_OBJECT_0) { + for (int i = 0; i < BUFFER_SIZE; i++) { + if (shared_events[i].type == 0) { // if there is room in the buffer + shared_events[i].time = time; + shared_events[i].type = ev.type; + shared_events[i].code = code; + shared_events[i].value = value; + shared_events[i].deviceType = device_type; + break; + } + } + ReleaseMutex(hMutex); + } + else if (waitResult != WAIT_TIMEOUT) { + std::cerr << "[CBF] Failed to acquire mutex: " << GetLastError() << std::endl; + } + } + } + } + + for (auto dev : devices) { + int fd = libevdev_get_fd(dev); + epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, nullptr); + libevdev_free(dev); + close(fd); + } + + close(epoll_fd); + inotify_rm_watch(inotify_fd, inotify_watch); + close(inotify_fd); + + UnmapViewOfFile(pBuf); + CloseHandle(hSharedMem); + CloseHandle(hMutex); + + std::cerr << "[CBF] Linux input program exiting" << std::endl; + return 0; +} + diff --git a/mods-v2/Click-Between-Frames-master/src/linuxeventcodes.hpp b/mods-v2/Click-Between-Frames-master/src/linuxeventcodes.hpp new file mode 100644 index 00000000..e2c032b8 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/linuxeventcodes.hpp @@ -0,0 +1,33 @@ +#pragma once + +constexpr int EV_SYN = 0x00; +constexpr int EV_KEY = 0x01; +constexpr int EV_REL = 0x02; +constexpr int EV_ABS = 0x03; + +constexpr int ABS_X = 0x00; +constexpr int ABS_Y = 0x01; +constexpr int ABS_Z = 0x02; +constexpr int ABS_RX = 0x03; +constexpr int ABS_RY = 0x04; +constexpr int ABS_RZ = 0x05; +constexpr int ABS_HAT0X = 0x10; +constexpr int ABS_HAT0Y = 0x11; + +constexpr int BUTTON_LEFT = 0x110; +constexpr int BUTTON_RIGHT = 0x111; + +constexpr int BTN_GAMEPAD = 0x130; +constexpr int BTN_A = 0x130; +constexpr int BTN_B = 0x131; +constexpr int BTN_X = 0x133; +constexpr int BTN_Y = 0x134; +constexpr int BTN_TL = 0x136; +constexpr int BTN_TR = 0x137; +constexpr int BTN_SELECT = 0x13a; +constexpr int BTN_START = 0x13b; +constexpr int BTN_MODE = 0x13c; +constexpr int BTN_THUMBL = 0x13d; +constexpr int BTN_THUMBR = 0x13e; +constexpr int BTN_TOUCH = 0x14a; + diff --git a/mods-v2/Click-Between-Frames-master/src/log.cpp b/mods-v2/Click-Between-Frames-master/src/log.cpp new file mode 100644 index 00000000..5c50cc39 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/log.cpp @@ -0,0 +1,59 @@ +#define _CRT_SECURE_NO_WARNINGS +#include "log.hpp" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +#include +#include +#include +#include + +extern HMODULE g_module; + +static std::mutex logLock; + +static void writeLog(const char* level, const char* fmt, va_list args) { + char message[1024]; + vsnprintf(message, sizeof(message), fmt, args); + + char line[1152]; + snprintf(line, sizeof(line), "[CBF] [%s] %s\n", level, message); + + OutputDebugStringA(line); + + static std::string path = []() { + char buffer[MAX_PATH]; + GetModuleFileNameA(g_module, buffer, MAX_PATH); + std::string dir = buffer; + return dir.substr(0, dir.find_last_of("\\/") + 1) + "ClickBetweenFrames.log"; + }(); + + std::lock_guard lock(logLock); + if (FILE* file = fopen(path.c_str(), "a")) { + fputs(line, file); + fclose(file); + } +} + +void cbf::log::info(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeLog("info", fmt, args); + va_end(args); +} + +void cbf::log::debug(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeLog("debug", fmt, args); + va_end(args); +} + +void cbf::log::error(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeLog("error", fmt, args); + va_end(args); +} diff --git a/mods-v2/Click-Between-Frames-master/src/log.hpp b/mods-v2/Click-Between-Frames-master/src/log.hpp new file mode 100644 index 00000000..4e6b1d57 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/log.hpp @@ -0,0 +1,11 @@ +#pragma once + +// writes next to the dll and to the debugger +// nested in a namespace because the crt already has a global `log` +namespace cbf { + namespace log { + void info(const char* fmt, ...); + void debug(const char* fmt, ...); + void error(const char* fmt, ...); + } +} diff --git a/mods-v2/Click-Between-Frames-master/src/main.cpp b/mods-v2/Click-Between-Frames-master/src/main.cpp new file mode 100644 index 00000000..97bab7c0 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/main.cpp @@ -0,0 +1,468 @@ +#include "includes.hpp" +#include "log.hpp" +#include "options.hpp" + +#include +#include +#include +#include + +#include + +constexpr double SMALLEST_FLOAT = std::numeric_limits::min(); + +constexpr InputEvent EMPTY_INPUT = InputEvent { + .time = 0, + .inputType = PlayerButton::Jump, + .inputState = false, + .isPlayer1 = false, +}; +constexpr Step EMPTY_STEP = Step { + .input = EMPTY_INPUT, + .deltaFactor = 1.0, + .endStep = true, +}; + +std::deque inputQueue; +std::deque inputQueueCopy; +std::deque stepQueue; + +std::atomic softToggle; + +InputEvent nextInput = EMPTY_INPUT; + +TimestampType lastFrameTime; +TimestampType currentFrameTime; + +bool firstFrame = true; // necessary to prevent accidental inputs at the start of the level or when unpausing +bool skipUpdate = true; // true -> dont split steps during PlayerObject::update() +bool enableInput = false; +bool linuxNative = false; +bool lateCutoff; // false -> ignore inputs that happen after the start of the frame; true -> check for inputs at the latest possible moment + +std::array, 6> inputBinds; +std::unordered_set heldInputs; + +std::mutex inputQueueLock; +std::mutex keybindsLock; + +std::atomic enableRightClick; +bool threadPriority; + +void (__thiscall* PlayLayer_update)(PlayLayer*, float); +void (__thiscall* PlayerObject_update)(PlayerObject*, float); + +/* +this function copies over the inputQueue from the input thread and uses it to build a queue of physics steps +based on when each input happened relative to the start of the frame +(and also calculates the associated stepDelta multipliers for each step) +*/ +void buildStepQueue(int stepCount) { + PlayLayer* playLayer = PlayLayer::get(); + nextInput = EMPTY_INPUT; + stepQueue = {}; // shouldnt be necessary, but just in case + + if (lateCutoff) { // copy all inputs in queue, use current time as the frame boundary + currentFrameTime = getCurrentTimestamp(); + if (linuxNative) { + linuxCheckInputs(); + } + + std::lock_guard lock(inputQueueLock); + inputQueueCopy = inputQueue; + inputQueue = {}; + } + else { // only copy inputs that happened before the start of the frame + if (linuxNative) linuxCheckInputs(); + + std::lock_guard lock(inputQueueLock); + while (!inputQueue.empty() && inputQueue.front().time <= currentFrameTime) { + inputQueueCopy.push_back(inputQueue.front()); + inputQueue.pop_front(); + } + } + + skipUpdate = false; + if (firstFrame) { + skipUpdate = true; + firstFrame = false; + lastFrameTime = currentFrameTime; + if (!lateCutoff) inputQueueCopy = {}; + return; + } + + TimestampType deltaTime = currentFrameTime - lastFrameTime; + TimestampType stepDelta = (deltaTime / stepCount) + 1; // the +1 is to prevent dropped inputs caused by integer division + + for (int i = 0; i < stepCount; i++) { // for each physics step of the frame + double elapsedTime = 0.0; + while (!inputQueueCopy.empty()) { // while loop to account for multiple inputs on the same step + InputEvent front = inputQueueCopy.front(); + + if (front.time - lastFrameTime < stepDelta * (i + 1)) { // if the first input in the queue happened on the current step + double inputTime = static_cast((front.time - lastFrameTime) % stepDelta) / stepDelta; // proportion of step elapsed at the time the input was made + stepQueue.emplace_back(Step{ front, std::clamp(inputTime - elapsedTime, SMALLEST_FLOAT, 1.0), false }); + inputQueueCopy.pop_front(); + elapsedTime = inputTime; + } + else break; // no more inputs this step, more later in the frame + } + + stepQueue.emplace_back(Step{ EMPTY_INPUT, std::max(SMALLEST_FLOAT, 1.0 - elapsedTime), true }); + } + + lastFrameTime = currentFrameTime; +} + +/* +return the first step in the queue, +also check if an input happened on the previous step, if so run pushButton/releaseButton +(2.1's split version of handleButton). +tbh this doesnt need to be a separate function from the PlayerObject::update() hook +*/ +Step popStepQueue() { + if (stepQueue.empty()) return EMPTY_STEP; + + Step front = stepQueue.front(); + double deltaFactor = front.deltaFactor; + + if (nextInput.time != 0) { + PlayLayer* playLayer = PlayLayer::get(); + + enableInput = true; + if (nextInput.inputState == Press) playLayer->pushButton(0, nextInput.isPlayer1); + else playLayer->releaseButton(0, nextInput.isPlayer1); + enableInput = false; + } + + nextInput = front.input; + stepQueue.pop_front(); + + return front; +} + +/* +send list of keybinds to the input thread. +2.1 has no rebindable controls, so this is just what vanilla UILayer::keyDown reacts to +*/ +void updateKeybinds() { + std::array, 6> binds; + + enableRightClick.store(settings::getBool("right-click", false)); + + binds[p1Jump] = { KEY_Space, CONTROLLER_A, CONTROLLER_RB }; + binds[p2Jump] = { KEY_Up, CONTROLLER_LB }; + + { + std::lock_guard lock(keybindsLock); + inputBinds = binds; + } +} + +// rewritten PlayerObject::resetCollisionLog() since its inlined in 2.113 +void decomp_resetCollisionLog(PlayerObject* p) { + p->m_collisionLogTop->removeAllObjects(); + p->m_collisionLogBottom->removeAllObjects(); + p->m_lastCollisionBottom = 0; + p->m_lastCollisionTop = 0; +} + +bool safeMode; +bool mouseFix; +bool clickOnSteps = false; + +int stepCount; +double frameDelta; +bool stepsBuilt = false; + +// work the step count back out of the per step delta, so TPS/FPS bypasses still work +void calculateSteps(float stepDelta) { + PlayLayer* pl = PlayLayer::get(); + stepsBuilt = true; + + stepCount = static_cast(std::round((frameDelta * 60.0) / stepDelta)); + if (stepCount < 1) stepCount = 1; + + if (pl->m_playerDied || GameManager::sharedState()->getEditorLayer() || softToggle.load()) { + enableInput = true; + skipUpdate = true; + firstFrame = true; + } + else if (frameDelta > 0.0) buildStepQueue(stepCount); + else skipUpdate = true; +} + +void onFrameStart() { + PlayLayer* playLayer = PlayLayer::get(); + + if (!lateCutoff) { + currentFrameTime = getCurrentTimestamp(); + } + + if (softToggle.load() // CBF disabled + || !GetFocus() // GD is minimized + || !playLayer // not in level + || playLayer->m_isPaused // if paused + || playLayer->m_hasLevelCompleteMenu) // if on endscreen + { + firstFrame = true; + skipUpdate = true; + enableInput = true; + + inputQueueCopy = {}; + + if (!linuxNative) { // clearing the queue isnt necessary on Linux since its fixed size anyway, but on Windows memory leaks are possible + std::lock_guard lock(inputQueueLock); + inputQueue = {}; + } + } + if (mouseFix && !skipUpdate) { // reduce lag with high polling rate mice by limiting the number of mouse movements per frame to 1 + MSG msg; + int index = 1; + while (PeekMessage(&msg, NULL, WM_MOUSEFIRST + index, WM_MOUSELAST, PM_NOREMOVE)) { // check for mouse inputs in the queue + if (msg.message == WM_MOUSEMOVE || msg.message == WM_NCMOUSEMOVE) { + PeekMessage(&msg, NULL, WM_MOUSEFIRST + index, WM_MOUSELAST, PM_REMOVE); // remove mouse movements from queue + } + else index++; + } + } +} + +void (__thiscall* CCEGLView_pollEvents)(void*); +void __fastcall CCEGLView_pollEvents_H(void* self, void*) { + onFrameStart(); + + CCEGLView_pollEvents(self); +} + +// no getModifiedDelta in 2.1, so just record the delta and notice if the step loop didnt run +void __fastcall PlayLayer_update_H(PlayLayer* self, void*, float dt) { + frameDelta = dt; + stepsBuilt = false; + + PlayLayer_update(self, dt); + + if (!stepsBuilt) { // no physics steps happened this frame (dead, paused, level not started yet, ...) + enableInput = true; + skipUpdate = true; + firstFrame = true; + } +} + +// disable regular inputs while CBF is active +void (__thiscall* GJBaseGameLayer_pushButton)(PlayLayer*, int, bool); +void __fastcall GJBaseGameLayer_pushButton_H(PlayLayer* self, void*, int button, bool isPlayer1) { + if (enableInput) GJBaseGameLayer_pushButton(self, button, isPlayer1); +} + +void (__thiscall* GJBaseGameLayer_releaseButton)(PlayLayer*, int, bool); +void __fastcall GJBaseGameLayer_releaseButton_H(PlayLayer* self, void*, int button, bool isPlayer1) { + if (enableInput) GJBaseGameLayer_releaseButton(self, button, isPlayer1); +} + +bool inputThisStep = false; +bool p1Split = false; +bool p2Split = false; +bool midStep = false; + +// split a single step based on the entries in stepQueue +void __fastcall PlayerObject_update_H(PlayerObject* self, void*, float stepDelta) { + PlayLayer* pl = PlayLayer::get(); + if (!skipUpdate) enableInput = false; + + if (pl && self != pl->m_player1 || midStep) { // do all of the logic during the P1 update for simplicity + if (midStep || !inputThisStep || self != pl->m_player2) PlayerObject_update(self, stepDelta); + return; + } + + // this is the first player update of the frame, so it's the earliest point the step count is known + if (pl && !stepsBuilt && stepDelta > 0.0f) calculateSteps(stepDelta); + + if (clickOnSteps && !stepQueue.empty()) { + Step step; + do step = popStepQueue(); while (!stepQueue.empty() && !step.endStep); // process 1 step (or more if theres an input) + } + + inputThisStep = stepQueue.empty() ? false : !stepQueue.front().endStep; + if (!stepQueue.empty() && !inputThisStep && !clickOnSteps) stepQueue.pop_front(); + + if (skipUpdate + || !pl + || !inputThisStep + || clickOnSteps) + { + p1Split = false; + p2Split = false; + inputThisStep = false; + PlayerObject_update(self, stepDelta); + return; + } + + PlayerObject* p2 = pl->m_player2; + bool isDual = pl->m_isDualMode; + bool p1StartedOnGround = self->m_isOnGround; + bool p2StartedOnGround = p2->m_isOnGround; + + bool p1NotBuffering = p1StartedOnGround + || self->m_touchingRings->count() + || self->m_isDashing + || (self->m_isDart || self->m_isBird || self->m_isShip); + + bool p2NotBuffering = p2StartedOnGround + || p2->m_touchingRings->count() + || p2->m_isDashing + || (p2->m_isDart || p2->m_isBird || p2->m_isShip); + + p1Split = p1NotBuffering; + p2Split = p2NotBuffering && isDual; + + Step step; + bool firstLoop = true; + midStep = true; + + do { + step = popStepQueue(); + const float substepDelta = stepDelta * step.deltaFactor; + + if (p1Split) { + PlayerObject_update(self, substepDelta); + if (!step.endStep) { + if (firstLoop && ((self->m_yVelocity < 0) ^ self->m_isUpsideDown)) self->m_isOnGround = p1StartedOnGround; // this fixes delayed inputs on platforms moving down for some reason + if (!self->m_isOnSlope || self->m_isDart) pl->checkCollisions(self, 0.0f); // moving platforms will launch u really high if this is anything other than 0.0, idk why + else pl->checkCollisions(self, stepDelta); // slopes will launch you really high if the 2nd argument is lower than like 0.01, idk why + decomp_resetCollisionLog(self); // necessary for wave + } + } + else if (step.endStep) PlayerObject_update(self, stepDelta); // revert to click-on-steps mode when buffering to reduce bugs + + if (p2Split) { + PlayerObject_update(p2, substepDelta); + if (!step.endStep) { + if (firstLoop && ((p2->m_yVelocity < 0) ^ p2->m_isUpsideDown)) p2->m_isOnGround = p2StartedOnGround; + if (!p2->m_isOnSlope || p2->m_isDart) pl->checkCollisions(p2, 0.0f); + else pl->checkCollisions(p2, stepDelta); + decomp_resetCollisionLog(p2); + } + } + else if (step.endStep && isDual) PlayerObject_update(p2, stepDelta); // 2.1 only updates P2 in dual mode + + firstLoop = false; + } while (!step.endStep); + + midStep = false; +} + +// update keybinds when you enter a level +bool (__thiscall* PlayLayer_init)(PlayLayer*, void*); +bool __fastcall PlayLayer_init_H(PlayLayer* self, void*, void* level) { + updateKeybinds(); + return PlayLayer_init(self, level); +} + +// disable progress in safe mode +void (__thiscall* PlayLayer_levelComplete)(PlayLayer*); +void __fastcall PlayLayer_levelComplete_H(PlayLayer* self, void*) { + bool testMode = self->m_isTestMode; + if (safeMode && !softToggle.load()) self->m_isTestMode = true; + + PlayLayer_levelComplete(self); + + self->m_isTestMode = testMode; +} + +// disable new best popup in safe mode +void (__thiscall* PlayLayer_showNewBest)(PlayLayer*, bool, int, int, bool, bool, bool); +void __fastcall PlayLayer_showNewBest_H(PlayLayer* self, void*, bool p0, int p1, int p2, bool p3, bool p4, bool p5) { + if (!safeMode || softToggle.load()) PlayLayer_showNewBest(self, p0, p1, p2, p3, p4, p5); +} + +// CBF endscreen watermark +void (__thiscall* EndLevelLayer_customSetup)(void*); +void __fastcall EndLevelLayer_customSetup_H(void* self, void*) { + EndLevelLayer_customSetup(self); + + if (!softToggle.load() && !clickOnSteps) { + cocos2d::CCSize size = cocos2d::CCDirector::sharedDirector()->getWinSize(); + CCLabelBMFont* indicator = CCLabelBMFont::create("CBF", "bigFont.fnt"); + + indicator->setPosition({ size.width, size.height }); + indicator->setAnchorPoint({ 1.0f, 1.0f }); + indicator->setOpacity(30); + indicator->setScale(0.2f); + + reinterpret_cast(self)->addChild(indicator); + } +} + +// notify the player if theres an issue with input on Linux +bool (__thiscall* CreatorLayer_init)(void*); +bool __fastcall CreatorLayer_init_H(void* self, void*) { + if (!CreatorLayer_init(self)) return false; + + showLinuxInputError(); + return true; +} + +void toggleMod(bool disable) { + // 2.1 has no vanilla click-between-steps/click-on-steps to patch out, so there is nothing to disable here + softToggle.store(disable); +} + +// push a value changed in the options page into the running mod +void applySetting(const char* key, bool value) { + std::string setting = key; + + if (setting == "soft-toggle") toggleMod(value); + else if (setting == "safe-mode") safeMode = value; + else if (setting == "click-on-steps") clickOnSteps = value; + else if (setting == "mouse-fix") mouseFix = value; + else if (setting == "late-cutoff") lateCutoff = value; + else if (setting == "right-click") enableRightClick.store(value); + // thread-priority and wine-workaround are only read while the mod is loading +} + +static void hook(uintptr_t address, void* detour, void* trampoline) { + MH_STATUS status = MH_CreateHook(reinterpret_cast(address), detour, reinterpret_cast(trampoline)); + if (status != MH_OK) cbf::log::error("Failed to hook %p: %s", reinterpret_cast(address), MH_StatusToString(status)); +} + +void modLoaded() { + settings::load(); + + toggleMod(settings::getBool("soft-toggle", false)); + + safeMode = settings::getBool("safe-mode", false); + clickOnSteps = settings::getBool("click-on-steps", false); + mouseFix = settings::getBool("mouse-fix", false); + lateCutoff = settings::getBool("late-cutoff", false); + threadPriority = settings::getBool("thread-priority", false); + + updateKeybinds(); + + MH_Initialize(); + + const uintptr_t base = gdBase(); + + hook(base + 0x2029C0, &PlayLayer_update_H, &PlayLayer_update); + hook(base + 0x1E8200, &PlayerObject_update_H, &PlayerObject_update); + hook(base + 0x111500, &GJBaseGameLayer_pushButton_H, &GJBaseGameLayer_pushButton); + hook(base + 0x111660, &GJBaseGameLayer_releaseButton_H, &GJBaseGameLayer_releaseButton); + hook(base + 0x1FB780, &PlayLayer_init_H, &PlayLayer_init); + hook(base + 0x1FD3D0, &PlayLayer_levelComplete_H, &PlayLayer_levelComplete); + hook(base + 0x1FE3A0, &PlayLayer_showNewBest_H, &PlayLayer_showNewBest); + hook(base + 0x94CB0, &EndLevelLayer_customSetup_H, &EndLevelLayer_customSetup); + hook(base + 0x4DE40, &CreatorLayer_init_H, &CreatorLayer_init); + + if (HMODULE cocos = GetModuleHandleA("libcocos2d.dll")) { + if (void* pollEvents = reinterpret_cast(GetProcAddress(cocos, "?pollEvents@CCEGLView@cocos2d@@QAEXXZ"))) { + hook(reinterpret_cast(pollEvents), &CCEGLView_pollEvents_H, &CCEGLView_pollEvents); + } + else cbf::log::error("Failed to find CCEGLView::pollEvents"); + } + + setupOptionsHook(); + + MH_EnableHook(MH_ALL_HOOKS); + + windowsSetup(); +} diff --git a/mods-v2/Click-Between-Frames-master/src/options.cpp b/mods-v2/Click-Between-Frames-master/src/options.cpp new file mode 100644 index 00000000..80ed0072 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/options.cpp @@ -0,0 +1,110 @@ +#include "options.hpp" +#include "settings.hpp" +#include "log.hpp" + +#include + +#include + +struct Option { + const char* gameVariable; + const char* key; + const char* name; + const char* description; + bool defaultValue; +}; + +static const Option OPTIONS[] = { + { "cbf01", "soft-toggle", "Disable CBF", + "Turn Click Between Frames off without restarting the game.", false }, + { "cbf02", "safe-mode", "Safe Mode", + "Disable progress and stats while CBF is active.", false }, + { "cbf03", "click-on-steps", "Click on Steps", + "Only register inputs on physics steps. Should have identical physics to vanilla.", false }, + { "cbf04", "mouse-fix", "Reduce Mouse Lag", + "Reduce lag when using high polling rate mice.\nExperimental, may break things.", false }, + { "cbf05", "late-cutoff", "Late Cutoff", + "Check for inputs at the latest possible moment instead of at the start of the frame.", false }, + { "cbf06", "right-click", "Right Click P2", + "Use right click for player 2 jump.", false }, + { "cbf07", "thread-priority", "CBF Thread Priority", + "Run the input thread at a higher priority.\nTakes effect after a restart.", false }, + { "cbf08", "wine-workaround", "CBF Wine Workaround", + "Read input devices directly when running under Wine.\nTakes effect after a restart.", true } +}; + +constexpr int OPTION_COUNT = sizeof(OPTIONS) / sizeof(OPTIONS[0]); +constexpr int TOGGLES_PER_PAGE = 10; // what MoreOptionsLayer::addToggle paginates at + +static void (__thiscall* GameManager_setGameVariable)(GameManager*, const char*, bool); + +static bool seeding = false; + +// keep the ini and the running mod in step with whatever the toggle wrote +static void __fastcall GameManager_setGameVariable_H(GameManager* self, void*, const char* key, bool value) { + GameManager_setGameVariable(self, key, value); + if (seeding) return; + + for (int i = 0; i < OPTION_COUNT; i++) { + if (strcmp(OPTIONS[i].gameVariable, key) != 0) continue; + settings::setBool(OPTIONS[i].key, value); + applySetting(OPTIONS[i].key, value); + return; + } +} + +static void (__thiscall* MoreOptionsLayer_addToggle)(void*, const char*, const char*, const char*); + +// number of toggles the layer has added so far, which is what decides the page they land on +static int& toggleCount(void* layer) { + return *reinterpret_cast(reinterpret_cast(layer) + 0x1D8); +} + +static bool injectPending = false; + +static void injectToggles(void* layer) { + GameManager* gm = GameManager::sharedState(); + + seeding = true; + for (int i = 0; i < OPTION_COUNT; i++) { + // the toggle reads its state out of the game variable, so seed it from the ini first + GameManager_setGameVariable(gm, OPTIONS[i].gameVariable, settings::getBool(OPTIONS[i].key, OPTIONS[i].defaultValue)); + MoreOptionsLayer_addToggle(layer, OPTIONS[i].name, OPTIONS[i].gameVariable, OPTIONS[i].description); + } + + seeding = false; + + int& count = toggleCount(layer); + count = ((count + TOGGLES_PER_PAGE - 1) / TOGGLES_PER_PAGE) * TOGGLES_PER_PAGE; +} + +static void __fastcall MoreOptionsLayer_addToggle_H(void* self, void*, const char* name, const char* key, const char* description) { + if (injectPending) { // put our page in front of the first vanilla toggle + injectPending = false; + injectToggles(self); + } + + MoreOptionsLayer_addToggle(self, name, key, description); +} + +static bool (__thiscall* MoreOptionsLayer_init)(void*); +static bool __fastcall MoreOptionsLayer_init_H(void* self, void*) { + injectPending = true; + bool result = MoreOptionsLayer_init(self); + injectPending = false; + + return result; +} + +static void hook(uintptr_t address, void* detour, void* trampoline, const char* name) { + MH_STATUS status = MH_CreateHook(reinterpret_cast(address), detour, reinterpret_cast(trampoline)); + if (status != MH_OK) cbf::log::error("Failed to hook %s: %s", name, MH_StatusToString(status)); +} + +void setupOptionsHook() { + const uintptr_t base = gdBase(); + + hook(base + 0xC9B50, &GameManager_setGameVariable_H, &GameManager_setGameVariable, "GameManager::setGameVariable"); + hook(base + 0x1DE8F0, &MoreOptionsLayer_init_H, &MoreOptionsLayer_init, "MoreOptionsLayer::init"); + hook(base + 0x1DF6B0, &MoreOptionsLayer_addToggle_H, &MoreOptionsLayer_addToggle, "MoreOptionsLayer::addToggle"); +} diff --git a/mods-v2/Click-Between-Frames-master/src/options.hpp b/mods-v2/Click-Between-Frames-master/src/options.hpp new file mode 100644 index 00000000..94733728 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/options.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include "gd.hpp" + +// the ini gets a page inside the vanilla options popup +void applySetting(const char* key, bool value); + +void setupOptionsHook(); diff --git a/mods-v2/Click-Between-Frames-master/src/settings.cpp b/mods-v2/Click-Between-Frames-master/src/settings.cpp new file mode 100644 index 00000000..3d420f64 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/settings.cpp @@ -0,0 +1,90 @@ +#include "settings.hpp" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +#include +#include +#include +#include + +static std::map g_values; +static std::string g_path; + +static std::string trim(const std::string& s) { + size_t start = s.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) return ""; + size_t end = s.find_last_not_of(" \t\r\n"); + return s.substr(start, end - start + 1); +} + +static const std::pair DEFAULTS[] = { + { "soft-toggle", "false" }, + { "safe-mode", "false" }, + { "click-on-steps", "false" }, + { "mouse-fix", "false" }, + { "late-cutoff", "false" }, + { "thread-priority", "false" }, + { "wine-workaround", "true" }, + { "right-click", "false" } +}; + +extern HMODULE g_module; + +void settings::load() { + char path[MAX_PATH]; + GetModuleFileNameA(g_module, path, MAX_PATH); + std::string dir = path; + dir = dir.substr(0, dir.find_last_of("\\/") + 1); + g_path = dir + "ClickBetweenFrames.ini"; + + for (auto& [key, value] : DEFAULTS) g_values[key] = value; + + std::ifstream file(g_path); + if (file) { + std::string line; + while (std::getline(file, line)) { + if (line.empty() || line[0] == ';' || line[0] == '#' || line[0] == '[') continue; + size_t eq = line.find('='); + if (eq == std::string::npos) continue; + g_values[trim(line.substr(0, eq))] = trim(line.substr(eq + 1)); + } + return; + } + + // write out a default config + std::ofstream out(g_path); + if (!out) return; + out << "; Click Between Frames" << std::endl; + for (auto& [key, value] : DEFAULTS) out << key << " = " << value << "\n"; +} + +bool settings::getBool(const std::string& key, bool defaultValue) { + auto it = g_values.find(key); + if (it == g_values.end()) return defaultValue; + std::string value = it->second; + std::transform(value.begin(), value.end(), value.begin(), ::tolower); + return value == "true" || value == "1" || value == "yes"; +} + +std::string settings::getString(const std::string& key, const std::string& defaultValue) { + auto it = g_values.find(key); + return it == g_values.end() ? defaultValue : it->second; +} + +void settings::setBool(const std::string& key, bool value) { + g_values[key] = value ? "true" : "false"; + save(); +} + +void settings::save() { + std::ofstream out(g_path); + if (!out) return; + out << "; Click Between Frames 2.1" << std::endl; + for (auto& [key, value] : g_values) out << key << " = " << value << std::endl; +} + +void settings::setSavedBool(const std::string& key, bool value) { + g_values[key] = value ? "true" : "false"; +} diff --git a/mods-v2/Click-Between-Frames-master/src/settings.hpp b/mods-v2/Click-Between-Frames-master/src/settings.hpp new file mode 100644 index 00000000..2652a8a8 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/settings.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +// values live in ClickBetweenFrames.ini next to the dll, read once on load +namespace settings { + void load(); + + bool getBool(const std::string& key, bool defaultValue); + std::string getString(const std::string& key, const std::string& defaultValue); + + void setBool(const std::string& key, bool value); + void save(); + + void setSavedBool(const std::string& key, bool value); +} diff --git a/mods-v2/Click-Between-Frames-master/src/thunks.cpp b/mods-v2/Click-Between-Frames-master/src/thunks.cpp new file mode 100644 index 00000000..9b233870 --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/thunks.cpp @@ -0,0 +1,17 @@ +#include "gd.hpp" + +// checkCollisions takes the player on the stack and the delta in xmm2, so it needs asm. +// the callee cleans up the pushed argument itself +#pragma runtime_checks("s", off) +void PlayLayer::checkCollisions(PlayerObject* player, float dt) { + static uintptr_t address = gdBase() + 0x203CD0; + PlayLayer* self = this; + + __asm { + movss xmm2, dt + push player + mov ecx, self + call address + } +} +#pragma runtime_checks("s", restore) diff --git a/mods-v2/Click-Between-Frames-master/src/windows.cpp b/mods-v2/Click-Between-Frames-master/src/windows.cpp new file mode 100644 index 00000000..0833e91d --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/windows.cpp @@ -0,0 +1,578 @@ +#include "includes.hpp" +#include "log.hpp" + +#include + +extern HMODULE g_module; + +TimestampType getCurrentTimestamp() { + LARGE_INTEGER t; + if (linuxNative) { + // used instead of QPC to make it possible to convert between Linux and Windows timestamps + GetSystemTimePreciseAsFileTime((FILETIME*)&t); + } else { + QueryPerformanceCounter(&t); + } + return t.QuadPart; +} + +LPVOID pBuf; +HANDLE hSharedMem = NULL; +HANDLE hMutex = NULL; + +LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + LARGE_INTEGER time; + PlayerButton inputType; + bool inputState; + bool player1; + + LPVOID pData; + switch (uMsg) { + case WM_INPUT: { + + UINT dwSize; + GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER)); + + auto lpb = std::unique_ptr(new BYTE[dwSize]); + if (!lpb) { + return 0; + } + if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb.get(), &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) { + cbf::log::debug("GetRawInputData does not return correct size"); + } + + RAWINPUT* raw = (RAWINPUT*)lpb.get(); + switch (raw->header.dwType) { + case RIM_TYPEKEYBOARD: { + QueryPerformanceCounter(&time); + + USHORT vkey = raw->data.keyboard.VKey; + inputState = raw->data.keyboard.Flags & RI_KEY_BREAK ? Release : Press; + + if (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9) vkey -= 0x30; // make numpad numbers work with customkeybinds + + // cocos2d::enumKeyCodes corresponds directly to vkeys + if (heldInputs.contains(vkey)) { + if (inputState) return 0; + else heldInputs.erase(vkey); + } + + bool shouldEmplace = true; + player1 = true; + + { + std::lock_guard lock(keybindsLock); + + if (inputBinds[p1Jump].contains(vkey)) inputType = PlayerButton::Jump; + else if (inputBinds[p1Left].contains(vkey)) inputType = PlayerButton::Left; + else if (inputBinds[p1Right].contains(vkey)) inputType = PlayerButton::Right; + else { + player1 = false; + if (inputBinds[p2Jump].contains(vkey)) inputType = PlayerButton::Jump; + else if (inputBinds[p2Left].contains(vkey)) inputType = PlayerButton::Left; + else if (inputBinds[p2Right].contains(vkey)) inputType = PlayerButton::Right; + else shouldEmplace = false; + } + } + + if (inputState) heldInputs.emplace(vkey); + if (!shouldEmplace) return 0; + + break; + } + case RIM_TYPEMOUSE: { + USHORT flags = raw->data.mouse.usButtonFlags; + bool shouldEmplace = true; + + player1 = true; + inputType = PlayerButton::Jump; + + if (flags & RI_MOUSE_BUTTON_1_DOWN) inputState = Press; + else if (flags & RI_MOUSE_BUTTON_1_UP) inputState = Release; + else { + player1 = false; + if (!enableRightClick.load()) return 0; + if (flags & RI_MOUSE_BUTTON_2_DOWN) inputState = Press; + else if (flags & RI_MOUSE_BUTTON_2_UP) inputState = Release; + else return 0; + } + + QueryPerformanceCounter(&time); // dont call on mouse move events + break; + } + default: + return 0; + } + break; + } + default: + return DefWindowProcA(hwnd, uMsg, wParam, lParam); + } + + { + std::lock_guard lock(inputQueueLock); + inputQueue.emplace_back(InputEvent{ timestampFromLarge(time), inputType, inputState, player1 }); + } + + return 0; +} + +void rawInputThread() { + WNDCLASS wc = {}; + wc.lpfnWndProc = WindowProc; + wc.hInstance = GetModuleHandleA(NULL); + wc.lpszClassName = "CBF"; + + RegisterClass(&wc); + HWND hwnd = CreateWindow("CBF", "Raw Input Window", 0, 0, 0, 0, 0, HWND_MESSAGE, 0, wc.hInstance, 0); + if (!hwnd) { + const DWORD err = GetLastError(); + cbf::log::error("Failed to create raw input window: %lu", err); + return; + } + + RAWINPUTDEVICE dev[2]; + dev[0].usUsagePage = 0x01; // generic desktop controls + dev[0].usUsage = 0x02; // mouse + dev[0].dwFlags = RIDEV_INPUTSINK; // allow inputs without being in the foreground + dev[0].hwndTarget = hwnd; // raw input window + + dev[1].usUsagePage = 0x01; + dev[1].usUsage = 0x06; // keyboard + dev[1].dwFlags = RIDEV_INPUTSINK; + dev[1].hwndTarget = hwnd; + + if (!RegisterRawInputDevices(dev, 2, sizeof(dev[0]))) { + cbf::log::error("Failed to register raw input devices"); + return; + } + + if (threadPriority) SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST); + + MSG msg; + while (GetMessage(&msg, hwnd, 0, 0)) { + DispatchMessage(&msg); + while (softToggle.load()) { // reduce lag while mod is disabled + Sleep(2000); + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)); // clear all pending messages + } + } +} + +void xinputThread() { + HMODULE xinputLib = LoadLibrary("Xinput1_4.dll"); + if (xinputLib == NULL) xinputLib = LoadLibrary("XInput9_1_0.dll"); // the version 2.1 itself links against + if (xinputLib == NULL) { + cbf::log::error("Failed to load Xinput"); + return; + } + typedef DWORD(WINAPI* XInputGetState_t)(DWORD, XINPUT_STATE*); + const auto XInputGetState = (XInputGetState_t)GetProcAddress(xinputLib, "XInputGetState"); + + // cocos2d doesn't match with Xinput + std::array, 22> xinputToCCKey = { + std::pair { XINPUT_GAMEPAD_DPAD_UP, CONTROLLER_Up }, + std::pair { XINPUT_GAMEPAD_DPAD_DOWN, CONTROLLER_Down }, + std::pair { XINPUT_GAMEPAD_DPAD_LEFT, CONTROLLER_Left }, + std::pair { XINPUT_GAMEPAD_DPAD_RIGHT, CONTROLLER_Right }, + std::pair { XINPUT_GAMEPAD_START, CONTROLLER_Start }, + std::pair { XINPUT_GAMEPAD_BACK, CONTROLLER_Back }, + std::pair { XINPUT_GAMEPAD_LEFT_SHOULDER, CONTROLLER_LB }, + std::pair { XINPUT_GAMEPAD_RIGHT_SHOULDER, CONTROLLER_RB }, + std::pair { XINPUT_GAMEPAD_A, CONTROLLER_A }, + std::pair { XINPUT_GAMEPAD_B, CONTROLLER_B }, + std::pair { XINPUT_GAMEPAD_X, CONTROLLER_X }, + std::pair { XINPUT_GAMEPAD_Y, CONTROLLER_Y }, + std::pair { -1, CONTROLLER_LT }, + std::pair { -1, CONTROLLER_RT }, + std::pair { -1, CONTROLLER_LTHUMBSTICK_UP }, + std::pair { -1, CONTROLLER_LTHUMBSTICK_DOWN }, + std::pair { -1, CONTROLLER_LTHUMBSTICK_LEFT }, + std::pair { -1, CONTROLLER_LTHUMBSTICK_RIGHT }, + std::pair { -1, CONTROLLER_RTHUMBSTICK_UP }, + std::pair { -1, CONTROLLER_RTHUMBSTICK_DOWN }, + std::pair { -1, CONTROLLER_RTHUMBSTICK_LEFT }, + std::pair { -1, CONTROLLER_RTHUMBSTICK_RIGHT } + }; + + if (threadPriority) SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST); + + bool xinputWorks = false; + + do { + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); + + DWORD dwResult; + for (DWORD i = 0; i < XUSER_MAX_COUNT; i++ ) { + XINPUT_STATE state; + dwResult = XInputGetState(i, &state); + + // controller not connected + if (dwResult != ERROR_SUCCESS) { + continue; + } + + xinputWorks = true; + + for (auto& [xinputButton, ccButton] : xinputToCCKey) { + bool inputState; + + bool buttonPressed = heldInputs.contains(ccButton); + // if it's not a joystick or trigger, we can just use & to check if the button is pressed + if (xinputButton != -1) { + buttonPressed = state.Gamepad.wButtons & xinputButton; + } else { // otherwise we have to check the values + switch (ccButton) { + case CONTROLLER_LT: + buttonPressed = state.Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + break; + case CONTROLLER_RT: + buttonPressed = state.Gamepad.bRightTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + break; + case CONTROLLER_LTHUMBSTICK_UP: + buttonPressed = state.Gamepad.sThumbLY > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + break; + case CONTROLLER_LTHUMBSTICK_DOWN: + buttonPressed = state.Gamepad.sThumbLY < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + break; + case CONTROLLER_LTHUMBSTICK_LEFT: + buttonPressed = state.Gamepad.sThumbLX < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + break; + case CONTROLLER_LTHUMBSTICK_RIGHT: + buttonPressed = state.Gamepad.sThumbLX > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + break; + case CONTROLLER_RTHUMBSTICK_UP: + buttonPressed = state.Gamepad.sThumbRY > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + break; + case CONTROLLER_RTHUMBSTICK_DOWN: + buttonPressed = state.Gamepad.sThumbRY < -XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + break; + case CONTROLLER_RTHUMBSTICK_LEFT: + buttonPressed = state.Gamepad.sThumbRX < -XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + break; + case CONTROLLER_RTHUMBSTICK_RIGHT: + buttonPressed = state.Gamepad.sThumbRX > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + break; + default: + // shouldn't be possible, just to prevent warnings + continue; + } + } + + if (buttonPressed) { + if (heldInputs.contains(ccButton)) continue; // skip if already held + heldInputs.emplace(ccButton); + inputState = Press; + } else { + if (!heldInputs.contains(ccButton)) continue; // skip if not held + heldInputs.erase(ccButton); + inputState = Release; + } + + LARGE_INTEGER time; + QueryPerformanceCounter(&time); + PlayerButton inputType; + bool player1 = true; + + { + std::lock_guard lock(keybindsLock); + + if (inputBinds[p1Jump].contains(ccButton)) inputType = PlayerButton::Jump; + else if (inputBinds[p1Left].contains(ccButton)) inputType = PlayerButton::Left; + else if (inputBinds[p1Right].contains(ccButton)) inputType = PlayerButton::Right; + else { + player1 = false; + if (inputBinds[p2Jump].contains(ccButton)) inputType = PlayerButton::Jump; + else if (inputBinds[p2Left].contains(ccButton)) inputType = PlayerButton::Left; + else if (inputBinds[p2Right].contains(ccButton)) inputType = PlayerButton::Right; + else continue; + } + } + { + std::lock_guard lock(inputQueueLock); + inputQueue.emplace_back(InputEvent{ timestampFromLarge(time), inputType, inputState, player1 }); + } + } + } + std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); + std::chrono::nanoseconds elapsed = end - start; + // reduce lag, inputs should still be accurate to 1/2000th of a second + std::this_thread::sleep_for(std::chrono::nanoseconds(500000) - elapsed); + while (softToggle.load()) { // reduce lag while mod is disabled + Sleep(2000); + } + } while (xinputWorks); + + if (!xinputWorks) { + cbf::log::error("Xinput failed to read any controllers"); + } +} + +// notify the player if theres an issue with input on Linux, called from the CreatorLayer::init hook +void showLinuxInputError() { + if (!linuxNative) return; + + DWORD waitResult = WaitForSingleObject(hMutex, 5); + if (waitResult == WAIT_OBJECT_0) { + if (static_cast(pBuf)[0].type == 3 && !softToggle.load()) { + cbf::log::error("Linux input failed"); + FLAlertLayer* popup = FLAlertLayer::create( + nullptr, + "CBF Linux", + "OK", + nullptr, + "Failed to read input devices.\nOn most distributions, this can be resolved with the following command: sudo usermod -aG input $USER (reboot afterward; this will make your system slightly less secure).\nIf the issue persists, please contact the mod developer." + ); + popup->show(); + } + ReleaseMutex(hMutex); + } + else if (waitResult == WAIT_TIMEOUT) { + cbf::log::error("Mutex stalling"); + } +} + +void linuxCheckInputs() { + static std::unordered_map linuxToCCKey = { + { BTN_A, CONTROLLER_A }, + { BTN_B, CONTROLLER_B }, + { BTN_X, CONTROLLER_X }, + { BTN_Y, CONTROLLER_Y }, + { BTN_TL, CONTROLLER_LB }, + { BTN_TR, CONTROLLER_RB }, + { BTN_SELECT, CONTROLLER_Back }, + { BTN_START, CONTROLLER_Start }, + }; + + DWORD waitResult = WaitForSingleObject(hMutex, 1); + if (waitResult == WAIT_OBJECT_0) { + LinuxInputEvent* events = static_cast(pBuf); + for (int i = 0; i < BUFFER_SIZE; i++) { + if (events[i].type == 0) break; // if there are no more events + + InputEvent input; + bool player1 = true; + USHORT scanCode = events[i].code; + int value = events[i].value; + + switch (events[i].deviceType) { + case MOUSE: + case TOUCHPAD: + if (scanCode == BUTTON_LEFT) { + input.inputType = PlayerButton::Jump; + } + else if (scanCode == BUTTON_RIGHT) { + if (!enableRightClick.load()) continue; + input.inputType = PlayerButton::Jump; + player1 = false; + } + break; + case KEYBOARD: { + USHORT keyCode = MapVirtualKeyExA(scanCode, MAPVK_VSC_TO_VK, GetKeyboardLayout(0)); + if (inputBinds[p1Jump].contains(keyCode)) input.inputType = PlayerButton::Jump; + else if (inputBinds[p1Left].contains(keyCode)) input.inputType = PlayerButton::Left; + else if (inputBinds[p1Right].contains(keyCode)) input.inputType = PlayerButton::Right; + else { + player1 = false; + if (inputBinds[p2Jump].contains(keyCode)) input.inputType = PlayerButton::Jump; + else if (inputBinds[p2Left].contains(keyCode)) input.inputType = PlayerButton::Left; + else if (inputBinds[p2Right].contains(keyCode)) input.inputType = PlayerButton::Right; + else continue; + } + break; + } + case TOUCHSCREEN: + if (scanCode == BTN_TOUCH) { // touching screen + input.inputType = PlayerButton::Jump; + } + break; + case CONTROLLER: { + int keyCode = -1; + if (events[i].type == EV_KEY) { + keyCode = linuxToCCKey[scanCode]; + } else if (events[i].type == EV_ABS) { + bool continueLoop = false; + auto analyze4Directions = [&] (int deadzone, int negative, int positive) { + if (events[i].value < -deadzone) { + keyCode = negative; + if (heldInputs.contains(negative)) { + continueLoop = true; // already held, ignore + } + value = Press; + } else if (events[i].value > deadzone) { + keyCode = positive; + if (heldInputs.contains(positive)) { + continueLoop = true; // already held, ignore + } + value = Press; + } else { + value = Release; + if (heldInputs.contains(negative)) { + keyCode = negative; + } else if (heldInputs.contains(positive)) { + keyCode = positive; + } else { + continueLoop = true; // continue cuz button was already released + } + } + }; + + switch (events[i].code) { + case ABS_X: // left thumbstick x + analyze4Directions(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, CONTROLLER_LTHUMBSTICK_LEFT, CONTROLLER_LTHUMBSTICK_RIGHT); + break; + case ABS_Y: // left thumbstick y + analyze4Directions(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, CONTROLLER_LTHUMBSTICK_UP, CONTROLLER_LTHUMBSTICK_DOWN); + break; + case ABS_RX: // right thumbstick x + analyze4Directions(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE, CONTROLLER_RTHUMBSTICK_LEFT, CONTROLLER_RTHUMBSTICK_RIGHT); + break; + case ABS_RY: // right thumbstick y + analyze4Directions(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE, CONTROLLER_RTHUMBSTICK_UP, CONTROLLER_RTHUMBSTICK_DOWN); + break; + case ABS_HAT0X: // dpadx + analyze4Directions(10, CONTROLLER_Left, CONTROLLER_Right); + break; + case ABS_HAT0Y: // dpady + analyze4Directions(10, CONTROLLER_Up, CONTROLLER_Down); + break; + case ABS_Z: + keyCode = CONTROLLER_LT; + if (events[i].value > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) { + value = Press; + } else { + value = Release; + } + break; + case ABS_RZ: + keyCode = CONTROLLER_RT; + if (events[i].value > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) { + value = Press; + } else { + value = Release; + } + break; + } + if (continueLoop) continue; + } + if (inputBinds[p1Jump].contains(keyCode)) input.inputType = PlayerButton::Jump; + else if (inputBinds[p1Left].contains(keyCode)) input.inputType = PlayerButton::Left; + else if (inputBinds[p1Right].contains(keyCode)) input.inputType = PlayerButton::Right; + else { + player1 = false; + if (inputBinds[p2Jump].contains(keyCode)) input.inputType = PlayerButton::Jump; + else if (inputBinds[p2Left].contains(keyCode)) input.inputType = PlayerButton::Left; + else if (inputBinds[p2Right].contains(keyCode)) input.inputType = PlayerButton::Right; + else continue; + } + if (value == Press) { + if (heldInputs.contains(keyCode)) { + continue; // already held, ignore + } else { + heldInputs.emplace(keyCode); + } + } else { + if (!heldInputs.contains(keyCode)) { + continue; // already released, ignore + } else { + heldInputs.erase(keyCode); + } + } + break; + } + default: + continue; + } + + input.inputState = value; + input.time = timestampFromLarge(events[i].time); + input.isPlayer1 = player1; + + inputQueue.emplace_back(input); + } + ZeroMemory(events, sizeof(LinuxInputEvent[BUFFER_SIZE])); + ReleaseMutex(hMutex); + } + else if (waitResult != WAIT_TIMEOUT) { + cbf::log::error("WaitForSingleObject failed: %lu", GetLastError()); + } +} + +void windowsSetup() { + HANDLE gdMutex; + + HMODULE ntdll = GetModuleHandle("ntdll.dll"); + typedef void (*wine_get_host_version)(const char **sysname, const char **release); + wine_get_host_version wghv = (wine_get_host_version)GetProcAddress(ntdll, "wine_get_host_version"); + if (wghv) { // if this function exists, the user is on Wine + const char* sysname; + const char* release; + wghv(&sysname, &release); + + std::string sys = sysname; + cbf::log::info("Wine %s", sys.c_str()); + + if (sys == "Linux") settings::setSavedBool("you-must-be-on-linux-to-change-this", true); + if (sys == "Linux" && settings::getBool("wine-workaround", true)) { // background raw keyboard input doesn't work in Wine + linuxNative = true; + cbf::log::info("Linux native"); + + hSharedMem = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(LinuxInputEvent[BUFFER_SIZE]), "LinuxSharedMemory"); + if (hSharedMem == NULL) { + cbf::log::error("Failed to create file mapping: %lu", GetLastError()); + return; + } + + pBuf = MapViewOfFile(hSharedMem, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(LinuxInputEvent[BUFFER_SIZE])); + if (pBuf == NULL) { + cbf::log::error("Failed to map view of file: %lu", GetLastError()); + CloseHandle(hSharedMem); + return; + } + + hMutex = CreateMutex(NULL, FALSE, "CBFLinuxMutex"); // used to gate access to the shared memory buffer for inputs + if (hMutex == NULL) { + cbf::log::error("Failed to create shared memory mutex: %lu", GetLastError()); + CloseHandle(hSharedMem); + return; + } + + gdMutex = CreateMutex(NULL, TRUE, "CBFWatchdogMutex"); // will be released when gd closes + if (gdMutex == NULL) { + cbf::log::error("Failed to create watchdog mutex: %lu", GetLastError()); + CloseHandle(hMutex); + CloseHandle(hSharedMem); + return; + } + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = NULL; + + STARTUPINFO si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + char buffer[MAX_PATH]; + GetModuleFileNameA(g_module, buffer, MAX_PATH); + std::string path = buffer; + path = path.substr(0, path.find_last_of("\\/") + 1) + "linux-input.so"; + + if (!CreateProcess(path.c_str(), NULL, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { + cbf::log::error("Failed to launch Linux input program: %lu", GetLastError()); + CloseHandle(hMutex); + CloseHandle(gdMutex); + CloseHandle(hSharedMem); + return; + } + } + } + + if (!linuxNative) { + std::thread(rawInputThread).detach(); + std::thread(xinputThread).detach(); + } +} diff --git a/mods-v2/Click-Between-Frames-master/src/windows.hpp b/mods-v2/Click-Between-Frames-master/src/windows.hpp new file mode 100644 index 00000000..d8c2eb4f --- /dev/null +++ b/mods-v2/Click-Between-Frames-master/src/windows.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "gd.hpp" +#include "linuxeventcodes.hpp" + +enum DeviceType : int8_t { + MOUSE, + TOUCHPAD, + KEYBOARD, + TOUCHSCREEN, + CONTROLLER, + UNKNOWN +}; + +#pragma pack(push, 1) +struct LinuxInputEvent { + LARGE_INTEGER time; + USHORT type; + USHORT code; + int value; + DeviceType deviceType; +}; +#pragma pack(pop) + +extern HANDLE hSharedMem; +extern HANDLE hMutex; +extern LPVOID pBuf; + +extern bool linuxNative; + +inline LARGE_INTEGER largeFromTimestamp(TimestampType t) { + LARGE_INTEGER res; + res.QuadPart = t; + return res; +} + +inline TimestampType timestampFromLarge(LARGE_INTEGER l) { + return l.QuadPart; +} + +constexpr size_t BUFFER_SIZE = 20; + +void windowsSetup(); +void linuxCheckInputs(); +void rawInputThread(); +void showLinuxInputError();