feat: Linux bulk importer for the Allflame Voyage Solver
Mirrors the solver's Windows-only voyage-import.ahk on KDE Wayland: - poe-clip-bridge.py: clipboard watcher that keeps only PoE item text, serves it on loopback and can trigger a sweep from the browser - voyage-sweep.py: hovers every cell of the in-game chart grid, Ctrl+C's it and hands over the whole batch in one go - userscript: feeds the batch into the solver page without focusing it Includes notes on the four Wayland pointer pitfalls this ran into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# Notes on driving the pointer under KDE Wayland
|
||||
|
||||
Everything here was measured on KDE Plasma 6 / `kwin_wayland`, multi-monitor,
|
||||
game running as an XWayland client through Proton. If you are automating input
|
||||
on Wayland, these four are what will bite you.
|
||||
|
||||
## 1. `ydotool mousemove --absolute` is useless on multi-monitor
|
||||
|
||||
Absolute positioning gets clamped into whatever output the pointer currently
|
||||
sits on, so the mapping is neither linear nor monotonic:
|
||||
|
||||
| requested | landed on |
|
||||
|---|---|
|
||||
| `0,0` | `302,1081` |
|
||||
| `1000,500` | `1855,747` |
|
||||
| `5000,2500` | `1418,1411` |
|
||||
| `6000,3000` | `301,1080` |
|
||||
|
||||
`xdotool mousemove` (XWarpPointer) does not work either — KWin ignores warp
|
||||
requests from clients that do not own the pointer focus.
|
||||
|
||||
**Use relative motion.**
|
||||
|
||||
## 2. Injected relative motion is not applied 1:1
|
||||
|
||||
On one of the test monitors every injected delta came out multiplied by exactly
|
||||
2.00 — not acceleration, a constant factor:
|
||||
|
||||
```
|
||||
sent=-25,-96 moved=-50,-192 ratio=2.00,2.00
|
||||
sent=+24,+99 moved=+48,+198 ratio=2.00,2.00
|
||||
```
|
||||
|
||||
A naive "move by the remaining error" loop therefore oscillates forever around
|
||||
the target, roughly ±100 px, and never converges.
|
||||
|
||||
**Fix:** learn the factor at runtime instead of hardcoding it. Send
|
||||
`error / gain`, measure what actually happened, update `gain` with an
|
||||
exponential moving average. Converges in a handful of iterations regardless of
|
||||
scale factor or acceleration profile — see `move_to()` in `bin/voyage-sweep.py`.
|
||||
|
||||
Large single deltas additionally get distorted by pointer acceleration, so cap
|
||||
each step (150 px works well).
|
||||
|
||||
## 3. `xdotool getmouselocation` freezes outside XWayland surfaces
|
||||
|
||||
X only learns the pointer position while the pointer is over an XWayland
|
||||
surface. Anywhere else it keeps returning a stale value — for hours, with no
|
||||
error:
|
||||
|
||||
```
|
||||
KWin says: 5102,2532
|
||||
xdotool says: 4300,2662 # stale, and stays that way
|
||||
```
|
||||
|
||||
That silently breaks any control loop that starts outside the game window.
|
||||
|
||||
**Fix:** two-phase. Coarse approach with KWin's own cursor position, which is
|
||||
always correct, then fine control with `xdotool` once the pointer is inside the
|
||||
(XWayland) game window, where it is live and fast.
|
||||
|
||||
Reading KWin's cursor position without a compositor plugin:
|
||||
|
||||
```bash
|
||||
echo 'print("PROBE " + workspace.cursorPos.x + "," + workspace.cursorPos.y);' > /tmp/probe.js
|
||||
id=$(qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript /tmp/probe.js probe)
|
||||
qdbus6 org.kde.KWin /Scripting/Script$id org.kde.kwin.Script.run
|
||||
journalctl _COMM=kwin_wayland -n 5 -o cat | grep PROBE
|
||||
qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript probe
|
||||
```
|
||||
|
||||
Roughly 300 ms per reading — fine for a coarse approach, too slow for a loop.
|
||||
|
||||
## 4. `wl-copy --clear` is asynchronous
|
||||
|
||||
Clearing the clipboard and then polling for "something appeared" is a race: the
|
||||
poll still reads the *previous* owner's data. In a sweep this looks like every
|
||||
cell copying the same item — 48 identical charts, all of them the first one.
|
||||
|
||||
**Fix:** write a unique sentinel token into the clipboard, then wait for it to be
|
||||
replaced:
|
||||
|
||||
```python
|
||||
token = f"sweep-{row}-{col}-{time.monotonic_ns()}"
|
||||
subprocess.run(["wl-copy"], input=token, text=True)
|
||||
send_ctrl_c()
|
||||
# ... poll until clipboard != token
|
||||
```
|
||||
|
||||
## Bonus: do not home the pointer into a screen corner
|
||||
|
||||
The obvious bootstrap for "get to a known position" — send a huge negative delta
|
||||
and let it clamp in the corner — triggers KDE's hot corners. Ours opened the
|
||||
application overview mid-sweep. Approach a known target directly instead.
|
||||
|
||||
## Bonus: measure the grid from a screenshot, do not eyeball it
|
||||
|
||||
Hand-calibrating cell corners is imprecise. Take a window screenshot, run a
|
||||
brightness projection per axis to get the cell pitch, then draw the computed
|
||||
centres back onto the screenshot and look at it:
|
||||
|
||||
```bash
|
||||
spectacle -b -n -a -o window.png
|
||||
```
|
||||
|
||||
That is how the 66.8 × 66.6 px pitch in this repo was derived — and how a wrong
|
||||
row count (which silently changes the computed pitch) was caught.
|
||||
Reference in New Issue
Block a user