Browse Source

drop "primary engine"; drop window renaming functionality; rename/refactor functions/methods/classes

Fabian Peter Hammerle 4 years ago
parent
commit
f66b714799
6 changed files with 125 additions and 215 deletions
  1. 0 16
      Pipfile.lock
  2. 6 7
      README.md
  3. 90 140
      rescriptoon/__init__.py
  4. 4 16
      rescriptoon/_cli.py
  5. 24 35
      rescriptoon/actions.py
  6. 1 1
      setup.py

+ 0 - 16
Pipfile.lock

@@ -16,22 +16,6 @@
         ]
     },
     "default": {
-        "psutil": {
-            "hashes": [
-                "sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b",
-                "sha256:10b7f75cc8bd676cfc6fa40cd7d5c25b3f45a0e06d43becd7c2d2871cbb5e806",
-                "sha256:1b1575240ca9a90b437e5a40db662acd87bbf181f6aa02f0204978737b913c6b",
-                "sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995",
-                "sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd",
-                "sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73",
-                "sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465",
-                "sha256:b74b43fecce384a57094a83d2778cdfc2e2d9a6afaadd1ebecb2e75e0d34e10d",
-                "sha256:e85f727ffb21539849e6012f47b12f6dd4c44965e56591d8dec6e8bc9ab96f4a",
-                "sha256:fd2e09bb593ad9bdd7429e779699d2d47c1268cbde4dda95fcd1bd17544a0217",
-                "sha256:ffad8eb2ac614518bbe3c0b8eb9dffdb3a8d2e3a7d5da51c5b974fb723a5c5aa"
-            ],
-            "version": "==5.6.7"
-        },
         "rescriptoon": {
             "editable": true,
             "path": "."

+ 6 - 7
README.md

@@ -10,20 +10,19 @@ TODO add description
 Compatible with X Window System (X11)
 
 ```sh
-$ sudo apt-get install python3-xlib python3-psutil `# optional`
+$ sudo apt-get install python3-xlib `# optional`
 $ pip3 install --user --upgrade rescriptoon
 ```
 
 ## Usage
 
-```sh
-$ rescriptoon TODO
-```
+1. Launch one or two Toontown engines
+2. Run `rescriptoon`
 
 `rescriptoon --help` shows all available options.
 
-While in the game press `` ` `` (grave) to enable extended controls.
-Press `` ` `` again to disable.
+While in the game press `` ` `` (grave) to disable key remapping.
+Press `` ` `` again to re-enable.
 
 ```sh
 $ rescriptoon --toggle slash
@@ -39,7 +38,7 @@ $ rescriptoon --toggle slash
 
 | Key   | Action                          | Target Engine |
 | ----- | ------------------------------- | ------------- |
-| `     | turn Extended Controls on / off |               |
+| `     | turn key remapping on / off     |               |
 | w     | walk forward                    | 0             |
 | s     | walk backward                   | 0             |
 | a     | turn left                       | 0             |

+ 90 - 140
rescriptoon/__init__.py

@@ -1,16 +1,16 @@
 import copy
+import logging
 import os
 import select
 import time
+import typing
 
-import psutil
 import Xlib.display
 from Xlib import XK, X, Xatom
 
 from rescriptoon.actions import *
 
 EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME = "grave"
-EXTENDED_CONTROLS_PID_XPROPERTY_NAME = "_TOONCHER_EXTENDED_CONTROLS_PID"
 TOONTOWN_WINDOW_NAME = "Toontown Rewritten"
 
 EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS = {
@@ -28,9 +28,7 @@ EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS = {
     XK.XK_l: RewriteKeyEventAction(keysym=XK.XK_Right, target_engine_index=1),
     XK.XK_slash: RewriteKeyEventAction(keysym=XK.XK_Control_L, target_engine_index=1),
     XK.XK_n: RewriteKeyEventAction(keysym=XK.XK_Delete, target_engine_index=1),
-    XK.XK_space: RewriteKeyEventAction(
-        keysym=XK.XK_Control_L, target_engine_index=TargetEngine.All
-    ),
+    XK.XK_space: RewriteKeyEventAction(keysym=XK.XK_Control_L),
     XK.XK_e: SelectGagAction(
         target_engine_index=0, column_index=4, factor_y=-0.047
     ),  # elephant trunk
@@ -46,26 +44,16 @@ EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS = {
 }
 
 
-def x_find_window(parent_window, filter_callback):
-    matching = []
+def _x_walk_children_windows(
+    parent_window: "Xlib.display.Window",
+) -> typing.Iterator["Xlib.display.Window"]:
+    yield parent_window
     for child_window in parent_window.query_tree().children:
-        if filter_callback(child_window):
-            matching.append(child_window)
-        matching += x_find_window(child_window, filter_callback)
-    return matching
-
-
-def x_find_window_by_pid(display, pid):
-    pid_prop = display.intern_atom("_NET_WM_PID")
-
-    def filter_callback(window):
-        prop = window.get_full_property(pid_prop, X.AnyPropertyType)
-        return prop and prop.value.tolist() == [pid]
-
-    return x_find_window(display.screen().root, filter_callback)
+        for subchild_window in _x_walk_children_windows(child_window):
+            yield subchild_window
 
 
-def x_wait_for_event(xdisplay, timeout_seconds):
+def _x_wait_for_event(xdisplay, timeout_seconds):
     """ Wait up to `timeout_seconds` seconds for a xevent.
         Return True, if a xevent is available.
         Return False, if the timeout was reached. """
@@ -78,122 +66,104 @@ def x_wait_for_event(xdisplay, timeout_seconds):
     return len(rlist) > 0
 
 
-class Rescriptoon:
+class Overlay:
     def __init__(
-        self,
-        primary_engine_pid,
-        primary_engine_window_name=None,
-        toggle_keysym_name=EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME,
+        self, toggle_keysym_name=EXTENDED_CONTROLS_DEFAULT_TOGGLE_KEYSYM_NAME,
     ):
-        self._primary_engine_pid = primary_engine_pid
-        self._primary_engine_window_name = primary_engine_window_name
         self._xdisplay = Xlib.display.Display()
         self._toggle_keysym = XK.string_to_keysym(toggle_keysym_name)
         if self._toggle_keysym == X.NoSymbol:
-            raise Exception(
-                "Extended keyboard controls toggle:"
-                + " Unknown keysym name '{}'".format(toggle_keysym_name)
+            raise ValueError(
+                "rescriptoon controls toggle:"
+                + " unknown keysym name '{}'".format(toggle_keysym_name)
             )
         self._keysym_mappings = copy.deepcopy(
             EXTENDED_CONTROLS_DEFAULT_KEYSYM_MAPPINGS,
         )
         if self._toggle_keysym in self._keysym_mappings:
-            print(
-                "INFO Extended Controls:"
-                + " Ignoring mapping for toggle key '{}'".format(toggle_keysym_name)
-            )
-        self._keysym_mappings[self._toggle_keysym] = ToggleExtendedControlsAction()
-        self._default_action = ForwardKeyEventAction()
-        self._primary_engine_window = None
-        self._engine_windows_by_target_index = None
+            logging.warning("ignoring mapping for toggle key %s", toggle_keysym_name)
+        self._keysym_mappings[self._toggle_keysym] = ToggleOverlayAction()
         self._active_key_registry = {}
         self._enabled = False
-
-    @property
-    def engine_running(self):
-        return psutil.pid_exists(self._primary_engine_pid)
+        self._engine_windows = None
 
     @property
     def xdisplay(self):
         return self._xdisplay
 
     @property
-    def primary_engine_window(self):
-        return self._primary_engine_window
+    def engine_windows(self) -> typing.List["Xlib.display.Window"]:
+        return self._engine_windows
 
-    def _wait_for_engine_window(self, timeout_seconds=20, search_interval_seconds=2):
-        start_epoch = time.time()
-        while self.engine_running and (time.time() - start_epoch) <= timeout_seconds:
-            windows = x_find_window_by_pid(self._xdisplay, self._primary_engine_pid,)
-            assert len(windows) <= 1
-            if len(windows) == 1:
-                return windows[0]
-            time.sleep(search_interval_seconds)
-        return None
-
-    def run(self):
-        self._primary_engine_window = self._wait_for_engine_window()
-        if not self._primary_engine_window:
-            raise Exception("Could not find the game's window.")
-        self._grab_key(self._xdisplay.keysym_to_keycode(self._toggle_keysym),)
-        self._primary_engine_window.change_property(
-            self.xdisplay.intern_atom(EXTENDED_CONTROLS_PID_XPROPERTY_NAME),
-            Xatom.CARDINAL,
-            format=32,
-            data=[os.getpid()],
-            mode=X.PropModeReplace,
-        )
-        if self._primary_engine_window_name:
-            self._primary_engine_window.set_wm_name(self._primary_engine_window_name,)
-            print(
-                "INFO Changed engine's window name to {!r}".format(
-                    self._primary_engine_window_name,
-                )
-            )
-        if not self.enabled:
-            keysym_name = XK.keysym_to_string(self._toggle_keysym)
-            print(
-                "INFO Extended Controls are currently disabled."
-                + " Press key '{}' to enable.".format(keysym_name)
-            )
-        while self.engine_running:
+    @property
+    def _engine_windows_open(self) -> bool:
+        for window in self._engine_windows:
+            try:
+                window.get_wm_state()
+            except Xlib.error.BadWindow:
+                logging.info("engine window %x is no longer available", window.id)
+                return False
+        return True
+
+    def run(self) -> None:
+        self._engine_windows = self._find_engine_windows()
+        logging.debug("engine window ids %r", [hex(w.id) for w in self._engine_windows])
+        if not self._engine_windows:
+            raise Exception("no toontown window found")
+        self._grab_key(self.xdisplay.keysym_to_keycode(self._toggle_keysym),)
+        self.enable()
+        while self._engine_windows_open:
             while self.xdisplay.pending_events():
-                self._handle_xevent(self._xdisplay.next_event())
+                self._handle_xevent(self.xdisplay.next_event())
             self._check_active_key_registry()
             # keep timeout low for _check_active_key_registry()
             # to be called frequently
-            x_wait_for_event(self.xdisplay, timeout_seconds=0.05)
+            _x_wait_for_event(self.xdisplay, timeout_seconds=0.05)
+        self._disable()
 
-    def _handle_xevent(self, xevent):
-        if isinstance(xevent, Xlib.protocol.event.KeyPress) or isinstance(
-            xevent, Xlib.protocol.event.KeyRelease
+    def _handle_xevent(self, xevent: Xlib.protocol.rq.Event) -> None:
+        if isinstance(
+            xevent, (Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease)
         ):
             self._handle_xkeyevent(xevent)
 
-    def _handle_xkeyevent(self, xkeyevent):
+    def _handle_xkeyevent(
+        self,
+        xkeyevent: typing.Union[
+            Xlib.protocol.event.KeyPress, Xlib.protocol.event.KeyRelease
+        ],
+    ) -> None:
         self._update_active_key_registry(xkeyevent)
-        keysym_in = self._xdisplay.keycode_to_keysym(xkeyevent.detail, index=0,)
-        if keysym_in in self._keysym_mappings:
-            action = self._keysym_mappings[keysym_in]
-        else:
-            action = self._default_action
+        keysym_in = self.xdisplay.keycode_to_keysym(xkeyevent.detail, index=0,)
+        action = self._keysym_mappings[keysym_in]
         action.execute(self, xkeyevent)
 
+    @property
+    def _toggle_keysym_name(self) -> str:
+        return XK.keysym_to_string(self._toggle_keysym)
+
     def enable(self):
         for keysym in self._keysym_mappings.keys():
             if keysym != self._toggle_keysym:
-                self._grab_key(self._xdisplay.keysym_to_keycode(keysym),)
+                self._grab_key(self.xdisplay.keysym_to_keycode(keysym),)
         self._enabled = True
-        # reset cache
-        self._engine_windows_by_target_index = None
-        print("INFO Enabled Extended Controls")
+        logging.info(
+            "rescriptoon is now enabled. press %s to disable.",
+            self._toggle_keysym_name,
+        )
 
-    def disable(self):
+    def _disable(self):
         for keysym in self._keysym_mappings.keys():
             if keysym != self._toggle_keysym:
-                self._ungrab_key(self._xdisplay.keysym_to_keycode(keysym),)
+                self._ungrab_key(self.xdisplay.keysym_to_keycode(keysym),)
         self._enabled = False
-        print("INFO Disabled Extended Controls")
+
+    def disable(self):
+        self._disable()
+        logging.info(
+            "rescriptoon is now disabled. press %s to enable.",
+            self._toggle_keysym_name,
+        )
 
     @property
     def enabled(self):
@@ -206,50 +176,30 @@ class Rescriptoon:
             self.enable()
 
     def _grab_key(self, keycode):
-        self.primary_engine_window.grab_key(
-            keycode,
-            X.AnyModifier,
-            # owner_events
-            # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event
-            # False,
-            True,
-            X.GrabModeAsync,
-            X.GrabModeAsync,
-        )
+        for window in self._engine_windows:
+            window.grab_key(
+                keycode,
+                X.AnyModifier,
+                # owner_events
+                # https://stackoverflow.com/questions/32122360/x11-will-xgrabpointer-prevent-other-apps-from-any-mouse-event
+                # False,
+                True,
+                X.GrabModeAsync,
+                X.GrabModeAsync,
+            )
 
     def _ungrab_key(self, keycode):
-        self.primary_engine_window.ungrab_key(keycode, X.AnyModifier)
-
-    def find_engine_windows(self):
-        controls_xprop = self.xdisplay.intern_atom(
-            EXTENDED_CONTROLS_PID_XPROPERTY_NAME,
-        )
-        return x_find_window(
-            self.xdisplay.screen().root,
-            lambda w: w.get_wm_name() == TOONTOWN_WINDOW_NAME
-            or w.get_full_property(controls_xprop, X.AnyPropertyType),
+        for window in self._engine_windows:
+            window.ungrab_key(keycode, X.AnyModifier)
+
+    def _find_engine_windows(self) -> typing.List["Xlib.display.Window"]:
+        return list(
+            filter(
+                lambda w: w.get_wm_name() == TOONTOWN_WINDOW_NAME,
+                _x_walk_children_windows(self.xdisplay.screen().root),
+            )
         )
 
-    @property
-    def engine_windows_by_target_index(self):
-        if not self._engine_windows_by_target_index:
-            win_by_index = {}
-            for target_index, win in enumerate(self.find_engine_windows()):
-                print(
-                    "INFO Engine window {} has no target index, assuming {}".format(
-                        win.id, target_index,
-                    )
-                )
-                if not target_index in win_by_index:
-                    win_by_index[target_index] = []
-                win_by_index[target_index].append(win)
-            self._engine_windows_by_target_index = win_by_index
-        return self._engine_windows_by_target_index
-
-    @property
-    def engine_windows(self):
-        return [w for g in self.engine_windows_by_target_index.values() for w in g]
-
     def _update_active_key_registry(self, xkeyevent):
         # see self._check_active_key_registry
         keycode = xkeyevent.detail

+ 4 - 16
rescriptoon/_cli.py

@@ -1,14 +1,13 @@
 import argparse
+import logging
 
 import rescriptoon
 
 
 def main() -> None:
+    logging.basicConfig(level=logging.DEBUG)
     argparser = argparse.ArgumentParser(
-        description="Attach Extended Controls to an already running Toontown engine.",
-    )
-    argparser.add_argument(
-        "engine_pid", type=int, help="process id of engine to attach to",
+        description="Attach resriptoon to running Toontown Rewritten engines.",
     )
     argparser.add_argument(
         "--toggle",
@@ -21,16 +20,5 @@ def main() -> None:
         + " (see XStringToKeysym & X11/keysymdef.h, "
         + " default: %(default)s)",
     )
-    argparser.add_argument(
-        "--change-window-name",
-        metavar="ENGINE_WINDOW_NAME",
-        dest="engine_window_name",
-        default=None,
-        help="change window name of engine after launch",
-    )
     args = argparser.parse_args()
-    rescriptoon.Rescriptoon(
-        primary_engine_pid=args.engine_pid,
-        toggle_keysym_name=args.toggle_keysym_name,
-        primary_engine_window_name=args.engine_window_name,
-    ).run()
+    rescriptoon.Overlay(toggle_keysym_name=args.toggle_keysym_name).run()

+ 24 - 35
rescriptoon/actions.py

@@ -1,33 +1,29 @@
-try:
-    import Xlib.X
-    import Xlib.protocol.event
-except ImportError:
-    pass
+import abc
+import logging
+import typing
 
-
-class TargetEngine:
-    Primary = -1
-    All = -3
+import Xlib.protocol.event
+import Xlib.X
 
 
 class EngineAction:
-    def __init__(self, target_engine_index):
+    def __init__(self, target_engine_index: typing.Optional[int] = None):
         self._target_engine_index = target_engine_index
 
-    def execute_on_window(self, extended_controls, xkeyevent, engine_window):
-        raise Exception("abstract method")
+    @abc.abstractmethod
+    def execute_on_window(self, overlay, xkeyevent, engine_window):
+        raise NotImplementedError()
 
-    def execute(self, extended_controls, xkeyevent):
-        if self._target_engine_index == TargetEngine.Primary:
-            target_windows = [extended_controls.primary_engine_window]
-        elif self._target_engine_index == TargetEngine.All:
-            target_windows = extended_controls.engine_windows
+    def execute(self, overlay, xkeyevent):
+        if not self._target_engine_index:
+            for target_window in overlay.engine_windows:
+                self.execute_on_window(overlay, xkeyevent, target_window)
+        elif self._target_engine_index >= len(overlay.engine_windows):
+            logging.warning("target engine index out of bounds")
         else:
-            target_windows = extended_controls.engine_windows_by_target_index.get(
-                self._target_engine_index, [],
+            self.execute_on_window(
+                overlay, xkeyevent, overlay.engine_windows[self._target_engine_index]
             )
-        for target_window in target_windows:
-            self.execute_on_window(extended_controls, xkeyevent, target_window)
 
 
 class CenterClickAction(EngineAction):
@@ -37,7 +33,7 @@ class CenterClickAction(EngineAction):
         self._factor_x = factor_x
         self._factor_y = factor_y
 
-    def execute_on_window(self, extended_controls, xkeyevent, engine_window):
+    def execute_on_window(self, overlay, xkeyevent, engine_window):
         engine_geometry = engine_window.get_geometry()
         smaller_dimension = min(engine_geometry.width, engine_geometry.height)
         attr = dict(
@@ -78,15 +74,15 @@ class SelectGagAction(CenterClickAction):
 
 
 class RewriteKeyEventAction(EngineAction):
-    def __init__(self, target_engine_index, keysym=None):
+    def __init__(self, target_engine_index: typing.Optional[int] = None, keysym=None):
         super().__init__(target_engine_index=target_engine_index,)
         self._keysym = keysym
 
-    def execute_on_window(self, extended_controls, xkeyevent, engine_window):
+    def execute_on_window(self, overlay, xkeyevent, engine_window):
         engine_window.send_event(
             type(xkeyevent)(
                 window=engine_window,
-                detail=extended_controls.xdisplay.keysym_to_keycode(self._keysym)
+                detail=overlay.xdisplay.keysym_to_keycode(self._keysym)
                 if self._keysym
                 else xkeyevent.detail,
                 state=xkeyevent.state,
@@ -102,14 +98,7 @@ class RewriteKeyEventAction(EngineAction):
         )
 
 
-class ForwardKeyEventAction(RewriteKeyEventAction):
-    def __init__(self):
-        super().__init__(
-            keysym=None, target_engine_index=TargetEngine.Primary,
-        )
-
-
-class ToggleExtendedControlsAction:
-    def execute(self, extended_controls, xkeyevent):
+class ToggleOverlayAction:
+    def execute(self, overlay, xkeyevent):
         if isinstance(xkeyevent, Xlib.protocol.event.KeyPress):
-            extended_controls.toggle()
+            overlay.toggle()

+ 1 - 1
setup.py

@@ -23,7 +23,7 @@ setuptools.setup(
         "Topic :: Utilities",
     ],
     entry_points={"console_scripts": ["rescriptoon = rescriptoon._cli:main"]},
-    install_requires=["xlib", "psutil"],
+    install_requires=["xlib"],
     setup_requires=["setuptools_scm"],
     tests_require=["pytest"],
 )