From f06b3ed601cceb68f79f6b1b2e0d98e8d97fa839 Mon Sep 17 00:00:00 2001 From: jdoda Date: Tue, 13 Aug 2024 10:48:54 -0400 Subject: [PATCH] Add optional acceleration to the mouse macro. (#931) * Add optional acceleration to the mouse macro. * Make accelerated mouse motion more accurate by accumulating fractional component of the speed. * Apply suggestions from code review Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com> --------- Co-authored-by: Tobi <28510156+sezanzeb@users.noreply.github.com> --- inputremapper/injection/macros/macro.py | 32 ++++++++++++++++++++++--- readme/macros.md | 5 +++- tests/unit/test_macros.py | 14 +++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/inputremapper/injection/macros/macro.py b/inputremapper/injection/macros/macro.py index 4be5b9566..88e5c4a39 100644 --- a/inputremapper/injection/macros/macro.py +++ b/inputremapper/injection/macros/macro.py @@ -469,10 +469,16 @@ def add_event(self, type_: Union[str, int], code: Union[str, int], value: int): self.tasks.append(lambda handler: handler(type_, code, value)) self.tasks.append(self._keycode_pause) - def add_mouse(self, direction: str, speed: int): + def add_mouse( + self, + direction: str, + speed: int, + acceleration: Optional[float] = None, + ): """Move the mouse cursor.""" _type_check(direction, [str], "mouse", 1) speed = _type_check(speed, [int], "mouse", 2) + acceleration = _type_check(acceleration, [float, None], "mouse", 3) code, value = { "up": (REL_Y, -1), @@ -482,9 +488,29 @@ def add_mouse(self, direction: str, speed: int): }[direction.lower()] async def task(handler: Callable): - resolved_speed = value * _resolve(speed, [int]) + resolved_speed = _resolve(speed, [int]) + resolved_accel = _resolve(acceleration, [float, None]) + + if resolved_accel: + current_speed = 0.0 + displacement_accumulator = 0.0 + displacement = 0 + else: + displacement = resolved_speed + while self.is_holding(): - handler(EV_REL, code, resolved_speed) + # Cursors can only move by integers. To get smooth acceleration for + # small acceleration values, the cursor needs to move by a pixel every + # few iterations. This can be achieved by remembering the decimal + # places that were cast away, and using them for the next iteration. + if resolved_accel and current_speed < resolved_speed: + current_speed += resolved_accel + current_speed = min(current_speed, resolved_speed) + displacement_accumulator += current_speed + displacement = int(displacement_accumulator) + displacement_accumulator -= displacement + + handler(EV_REL, code, value * displacement) await asyncio.sleep(1 / self.mapping.rel_rate) self.tasks.append(task) diff --git a/readme/macros.md b/readme/macros.md index f7f715d4f..949549ac9 100644 --- a/readme/macros.md +++ b/readme/macros.md @@ -124,9 +124,11 @@ Bear in mind that anti-cheat software might detect macros in games. ### mouse > Moves the mouse cursor +> +> If `acceleration` is provided then the cursor will accelerate from zero to a maximum speed of `speed`. > > ```c# -> mouse(direction: str, speed: int) +> mouse(direction: str, speed: int, acceleration: float | None) > ``` > > Examples: @@ -134,6 +136,7 @@ Bear in mind that anti-cheat software might detect macros in games. > ```c# > mouse(up, 1) > mouse(left, 2) +> mouse(down, 10, 0.3) > ``` ### wheel diff --git a/tests/unit/test_macros.py b/tests/unit/test_macros.py index e6e6cc1f2..939ac6526 100644 --- a/tests/unit/test_macros.py +++ b/tests/unit/test_macros.py @@ -1012,6 +1012,20 @@ async def test_mouse(self): actual_wheel_hi_res_event_count, expected_wheel_hi_res_event_count * 1.1 ) + async def test_mouse_accel(self): + macro_1 = parse("mouse(up, 10, 0.9)", self.context, DummyMapping) + macro_1.press_trigger() + asyncio.ensure_future(macro_1.run(self.handler)) + + sleep = 0.1 + await asyncio.sleep(sleep) + self.assertTrue(macro_1.is_holding()) + macro_1.release_trigger() + self.assertEqual( + [(2, 1, 0), (2, 1, -2), (2, 1, -3), (2, 1, -4), (2, 1, -4), (2, 1, -5)], + self.result, + ) + async def test_event_1(self): macro = parse("e(EV_KEY, KEY_A, 1)", self.context, DummyMapping) a_code = system_mapping.get("a")