A line-for-line port of the original AutoHotkey macro
(Fisch_Cream_s_Macro_free_edition.Ahk by Cweamya,
https://github.com/Cweamy/Fisch-Cream-s-Macro) to Python, so it runs on
Debian/X11 instead of Windows.
It works the same way the original did: it looks at on-screen pixel colors and clicks/holds the mouse accordingly. It does not touch Roblox's memory or process in any way. Unlike XTernal which does. XTernal calls us bad, that other macros dont offer their useless features Learn More
System package (X11 window control, this will not work under Wayland):
sudo apt install xdotoolPython packages:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt- Roblox (or your Linux Roblox client, the official client isn't available on Linux, so you're likely using something like Sober) needs to be fullscreen with Camera Mode enabled for the fishing minigame.
- Set your display scale to 100% (e.g.
xrandr --dpi 96, or your desktop environment's scaling setting). Like the original AHK script, this assumes 1:1 pixel coordinates. - Change
Settings.inito your control in the game. e.g0.2 - Change
Webhook.inito your actual discord webhook. NOT REQUIRED
python3 fisch_macro.pyOn startup it will print "Click on the game window to select it...", click
anywhere on your Roblox/Sober window and it selects and activates that
window (via xdotool selectwindow). This is more reliable than matching by
window title, since Linux Roblox clients don't necessarily title their
window "Roblox".
If you'd rather match by title instead of clicking, you can still do that:
python3 fisch_macro.py --window-title "Sober"Press F8 at any time to stop (default). This hotkey is system-wide,
it works even if the game isn't the focused window, so pick something you
won't press by accident elsewhere. Change it with --exit-key, e.g.:
python3 fisch_macro.py --exit-key escA: You need to test that argument how ever you want, the default is 1.0, but if that value is really unstable (e.g loses fish really frequently) then you can use --hold-scale 0.2459, as that's my config. You probably shouldn't use this because the hold scale might depend on your system hardware, which if its weak then you might want to adjust the value to make it work as nicely.
A: Making 0.2459 default would introduce more problems on weaker hardware, as I know there are some users that use weaker hardware (as some players might be using, e.g: a laptop from 2014.). Additionally it might make the macro slower, which is quite needed to react quickly to the minigame. That being said if that value does work, then you can use it.
python3 fisch_macro.py --shake-min-pixels 12 --hold-scale 1.0--shake-min-pixels(default 12): minimum matched white pixels before a shake-button detection counts. If the macro seems "stuck" doing nothing and never re-casts, raise this, it likely means something else in the shake region (glare, UI text) is matching white and keeps resetting the fail-safe timer forever. If real shakes are being missed, lower it. Camera angle matters here too: color matching can't distinguish a real shake button from a gray rock/object of similar color sitting in the background. Pointing the camera at something low-contrast and non-gray (water works well) instead of gray terrain/objects reduces false positives significantly, on top of whatever--shake-min-pixelsyou're using.--hold-scale(default 1.0): multiplier on every minigame hold duration. On low-Control rods, even the shortest possible pulse can overshoot a narrow target zone, so the bar oscillates instead of converging. Try 0.6-0.8 to shorten pulses. This is experimental, I can't tune it against the live game myself, so treat it as a starting point to adjust per rod. Ignored if--pulse-msis set.--pulse-ms(default: unset): skip the variable hold-time formula entirely and run a simple, direction-aware bang-bang loop instead: check which side the fish is on, hold or release, sleep exactly--pulse-ms, repeat. No nested waits, no extra delays, the cadence you set is the cadence you get. Useful when a rod's Control is so low the fish sits centered and the formula-based approach can't converge on it at all.--deadzone-px(default 6, fixed-pulse mode only): minimum fish-to-bar offset in pixels before switching direction. Without this, a naive bang-bang controller flips direction on every pixel of noise right around the setpoint, which looks like it's constantly "going left and right" instead of settling near center. Raise it if it still chatters, lower it toward 0 if correction feels sluggish.
If capture benchmarks fast (confirmed via benchmark_capture.py and the
live capture Xms status readout) but lowering --pulse-ms still doesn't
visibly speed anything up in-game, the most likely explanation left is that
Roblox itself only samples input at its own tick rate, toggling faster
than the game polls for it simply won't be visible, no matter how fast
Python sends the events. Test this by trying something like --pulse-ms 20
vs --pulse-ms 300: if there's truly no difference across that whole
range, that's strong evidence it's a game-side limit rather than anything
fixable in this script.
If instead the symptom is the bar visibly "going left and right" near
center rather than settling, that's direction chattering, see
--deadzone-px above.
PixelSearch→mss(screenshot capture) +numpy(vectorized color matching), same hex colors/tolerances as the original.Click/ mouse down-up →pyautogui.WinActivate/ window geometry →xdotool(X11 only).- AHK's stacked on-screen
Tooltips are replaced with a single live status line printed to the terminal (task, click count, catch total, etc.). This is the one place I simplified rather than pixel-matched, an overlay GUI (e.g. a small always-on-top Tk window) would be a natural follow-up if you want an on-screen HUD instead of a terminal line. - The
Controlbar-size auto-detection, hold-time interpolation table, and minigame steering logic are preserved as-is from the original script. - The subscribe/Discord-prompt block from the original was inert (wrapped
in a
/* */comment block in the source) and isn't reproduced. - The macro was tested on the SOULREAPER rod with -0.1 control, you're free
to modify the
--hold-scaleargument to your liking.
-
Added Webhooks you can now use webhooks. That's all
-
Fixed (major): the reticle box's position was being detected incorrectly most of the time. Analyzing real screenshots showed the box's own fill color actually shifts dynamically (observed brown, olive-green, and near-white depending on distance from target) - so the old "look for pure white, fall back to something else" logic only worked in the rare near-aligned state and gave inconsistent, jittery position reads the rest of the time. This is very likely what was causing "goes left and right, can't reach the target." Detection is now based on the two arrow glyphs inside the box, which stay a constant gray regardless of the box's fill color - confirmed against 3 real screenshots in 3 different box states. Handles both arrows visible (take the midpoint) and only one visible (box pushed against a track edge - estimate the far edge using the known box width). Also fixed the same theme-color problem in the minigame-start detection trigger in the main loop.
-
Added:
--deadzone-px(default 6) for fixed-pulse mode, to stop the bang-bang controller from chattering direction on every pixel of noise near the setpoint (the "going left and right" symptom). -
Fixed (the real cause):
--pulse-msbeing effectively capped around ~100ms no matter how low it was set. Screenshot capture was ruled out viabenchmark_capture.py(sub-millisecond on this system). The actual cause: pyautogui inserts a 0.1 second pause after every single call by default (pyautogui.PAUSE), as a built-in safety default, everymouseDown/mouseUp/keyDown/keyUpin the steering loop was silently eating ~100ms on top of whatever pulse length was requested. This was never explicitly disabled. Now set topyautogui.PAUSE = 0at startup; every other call site already has its own explicittime.sleep()where a delay is actually needed, so this doesn't remove any intended pacing. -
Added: live capture-timing shown in the status line during fixed-pulse mode, plus a standalone
benchmark_capture.pydiagnostic script, to figure out why--pulse-msmight have no visible effect - most likely cause is screenshot capture itself being the bottleneck, not the sleep. See "Diagnosing" section above. -
Fixed:
--pulse-msnot actually producing a fixed cadence, it was reusing the formula-mode loop's machinery (nestedwait()polling, plus a0.6×sleep tacked on after every release), so the real on/off timing drifted around (e.g. alternating ~100ms/~300ms) instead of holding steady, causing overshoot.--pulse-msnow runs its own dedicated, minimal loop: decide direction, hold or release, sleep exactly the requested duration, repeat. Nothing else in the timing. -
Reverted: minigame steering back to mouse (was briefly switched to spacebar). Real-world testing with a hardware autoclicker (100ms interval, 100ms hold, on the mouse button) confirmed mouse works and spacebar didn't, the earlier guess that keyboard events would have lower latency didn't hold up. Selectable via
--steering mouse|spaceif worth revisiting later; default ismouse. -
Fixed:
--pulse-msbeing silently ignored. The crossing-safety clamp (added to fix the fish-tracking bug earlier) was checked before the fixed-pulse override, so on low-Control rods, where the fish crosses paths constantly, it kept forcing 0ms and--pulse-msnever got a chance to apply. Fixed-pulse mode now always returns the constant value, since that clamp only ever existed to guard the variable formula. -
Changed: minigame steering (moving the reel bar left/right) now holds spacebar instead of the mouse button, key events seem to have lower input latency than simulated mouse-down/up for this. Casting the rod (
reels()) still uses the mouse, unchanged. -
Added:
--pulse-msto bypass the variable hold-time formula with a fixed-length pulse (e.g.--pulse-ms 100), for rods where Control is too low for the formula to converge on a centered fish. See Tuning flags above. -
Fixed: macro getting permanently "stuck" acting like it's mid-shake and never re-casting until you gave manual input. Cause: any white-ish pixel match in the shake region (even a single stray pixel from glare or UI) reset the fail-safe timer, so a persistent false positive meant the fail-safe never tripped and it never re-cast. Now requires a minimum matched-pixel blob size (
--shake-min-pixels, default 12) before counting a detection as real. -
Added:
--hold-scalemultiplier for minigame hold durations, to help tune overshoot on low-Control rods (see Tuning flags above). This is an inherent limitation carried over from the original's fixed hold-time table, not something introduced by the port - the table's pixel steps don't scale with how narrow a given rod's control bar is. -
Changed: shaking now presses Enter instead of mouse-clicking the shake button's on-screen position. No more coordinate calculation needed, and every detected shake button gets pressed (the old version skipped a button if its position happened to match the previously clicked one).
-
Fixed: crash when
Settings.inihas a fractionalControlvalue (e.g.Control = 0.23, which is normal/expected). The fallback loader was reading it withgetint(), which only accepts whole numbers. Now reads it as a float, matching the formula it feeds into (320 * Control + 97) and matching how the original AHKIniRead(which doesn't distinguish int vs float) treated it. -
Fixed: the macro stopping unexpectedly after a few catches with no error. It wasn't crashing, the exit hotkey (spacebar) is a system-wide listener, not scoped to the game window (same as the original AHK
$space::exitapp). Any spacebar press anywhere, alt-tabbing to pause a video, scroll a page, etc., silently stopped it, and there was no log message explaining why. Now: the default exit key is F8 instead of space (configurable via--exit-key), and pressing it prints[Exit key pressed - stopping macro]so it's unmistakable next time. -
Fixed: fish-tracking loss. Two causes:
hold_formula()could receive a negative pixel distance when the fish crossed to the other side of the bar mid-hold. Python's negative list indexing (data[-1]) silently wrapped to the last table entry instead of erroring like AHK would, producing wildly wrong hold times right at the moment the fish changed direction. Now negative distances return a 0ms hold (stop immediately), matching the original's intent.- Each tracking loop iteration was taking 2-3 separate screenshots (one per color checked) instead of one shared screenshot. That added avoidable latency on every step. Fish/white/bar checks now reuse a single captured frame per iteration.
They tell it on their website that we (other macros) never update our macros, OpenMacro (the founder of XTernal and Swift) calls us like we are pointless, like puppets on their website, that we dont offer that all rods are supported, well WHAT are we supposed to do? Break the roblox TOS? NO, we don't want to do that. We know it's better but it's going to make us have hell trying to find memory values of each new Sober release, and if I wanted to I would port over XTernal but it would have taken more time to transfer AutoHotKey API's. Now about Swift. Swift is a that has alot of features, BUT guess what? It's locked behind a hell pay wall, which who the heck wants to pay for a MACRO? No one, only people that wanted to support OpenMacro's work. If a openmacro dev is reading this then screw you, ban me if u can. Instead porting them over, you decide to be greedy. It's yet to say that creamy macro is quite outdated, but it's atleast not breaking the TOS. I'll only change my mind if yall contact me personally, and iI'll give yall a challenge, but again
Making your macro open-source doesn't help it only solves the issue of not being able to get the source code to port over. But then why wouldn't you port over the macro to Linux? Popularity won't save you here.