Me? Cheating in a Single Player game? I would nev... oh wait, that's exactly what we are trying to do here. Ok, fine, no AI supported cheating. Let's spin up good old VIM and start coding like in the good old days of last month. Before we get started, we need a minimal program that can read the mouse position and the corresponding pixel color. Just so we have all the coordinates and stuff we need to click:
#!/usr/bin/env perl
use v5.36;
use strict;
use X11::GUITest qw[GetMousePos MoveMouseAbs ClickMouseButton :CONST];
use Imager::Screenshot qw[screenshot];
my $w = 1920;
my $h = 1080;
checker();
sub checker {
my $img = screenshot();
while(1) {
my ($x, $y, $scr_num) = GetMousePos();
if($x < $w && $y < $h) {
my ($r, $g, $b) = $img->getpixel(x => $x, y => $y)->rgba()
+;
print join(' | ', $x, $y, $scr_num, $r, $g, $b), "\n";
} else {
#print "\n";
}
}
}
So i started Spaceplan in windowed mode, moved it to the left upper position to my screen and wrote down a bunch of numbers (coordinates, RGB values) on a piece of paper. If you want to replicate the stuff with your own copy of spaceplan (or any other game), you'll have to do the same.
Now that the boring part is out of the way, let's get coding. First we need to declare some stuff and hardcode the pixel positions in some global variables:
#!/usr/bin/env perl
use v5.36;
use strict;
use X11::GUITest qw[GetMousePos MoveMouseAbs ClickMouseButton :CONST];
use Imager::Screenshot qw[screenshot];
use Carp;
use Time::HiRes qw(sleep);
################# TUNEABLES #############
my $w = 1920; # Screen width
my $h = 1080; # Screen height
my $thingmakerx = 55; # leftmost X position of "Thing maker" list
my $idealisterx = 1380; # leftmost X position of "Idea lister" list
my $liststarty = 450; # Top Y of lists
my $listendy = 1048; # Bottom Y of lists
my $buttonx = 186; # Power button thingy X
my $buttony = 369; # Power button thingy Y
################# TUNEABLES #############
Next step is to write our main loop:
my $lastx = -1;
my $lasty = -1;
while(1) {
for(1..50) {
click($buttonx, $buttony);
}
my $img = screenshot();
# Check idea lister
my $ideasfound = findClickableStuff($img, $idealisterx, 'Idea list
+er', 1);
if($ideasfound) {
# Don't spend watts on thing maker when we still have ideas
print "Still have ideas left!\n";
next;
}
# Check thing maker
findClickableStuff($img, $thingmakerx, 'Thing maker', 1);
}
The strategy i choose here is to always mash the power button thingy. If there are any ideas in the idea lister (which has all sorts of multipliers, some of which increase the power button thingy effectiveness), we only want to click on those. Only if there are no more ideas listed, do we want to click on the thing maker stuff. This makes for a very slow start, but increasing the power button multiplier early on can speed things up later in the game. But it's easy to alter the strategy mid-game, or even sneak in a few manual click once in a while. Next, we need a way to click on the screen.
sub click($x, $y) {
if($x != $lastx || $y != $lasty) {
MoveMouseAbs($x, $y, 0);
#sleep(0.5);
$lastx = $x;
$lasty = $y;
} else {
my ($mx, $my) = GetMousePos();
my $dist = abs($mx - $x) + abs($my - $y);
if($dist > 10) {
croak("USER ABORT!");
}
}
ClickMouseButton(M_LEFT);
return;
}
The click() function also serves as our abort function. Since we mostly click on the power button, we can just move the mouse once and detect if the mouse position was moved by the user. We don't want to accidently buy every item on ebay because we can't turn off the autoclicker, right?
And finally, we need to detect clickable items in the "idea lister" and "thing maker" lists - and click them:
sub findClickableStuff($img, $startx, $name, $clickonlyone) {
my $listitemsfound = 0;
for(my $iy = $listendy; $iy > $liststarty; $iy--) {
my $found = 0;
for(my $ix = 0; $ix < 90; $ix++) {
my ($r, $g, $b) = $img->getpixel(x => $ix + $startx, y =>
+$iy)->rgba();
if($r == 255 && $g == 255 && $b == 255) {
# White text (or lines): List has at least one entry
$listitemsfound = 1;
}
#next if($r > 180);
#next if($g < 180);
# Search for somewhat green text (means we can affort the
+item)
if($r < 50 && $g > 220 && $b > 150) {
$found = 1;
last;
}
}
if($found) {
print $name, "item found!\n";
for(1..3) {
click($startx + 45, $iy);
}
$iy -= 50;
#$listitemsfound = 1;
if($clickonlyone) {
last;
}
}
}
return $listitemsfound;
}
I found that calling click() multiple times after moving the mouse seems to have a higher change of actually
clicking. Or rather having the game understand that we clicked at that position after moving the mouse there a nanosecond earlier.
But it mostly works! Here is the full listing of autoclicker.pl for easier download:
#!/usr/bin/env perl
use v5.36;
use strict;
use X11::GUITest qw[GetMousePos MoveMouseAbs ClickMouseButton :CONST];
use Imager::Screenshot qw[screenshot];
use Carp;
use Time::HiRes qw(sleep);
################# TUNEABLES #############
my $w = 1920; # Screen width
my $h = 1080; # Screen height
my $thingmakerx = 55; # leftmost X position of "Thing maker" list
my $idealisterx = 1380; # leftmost X position of "Idea lister" list
my $liststarty = 450; # Top Y of lists
my $listendy = 1048; # Bottom Y of lists
my $buttonx = 186; # Power button thingy X
my $buttony = 369; # Power button thingy Y
################# TUNEABLES #############
my $lastx = -1;
my $lasty = -1;
while(1) {
for(1..50) {
click($buttonx, $buttony);
}
my $img = screenshot();
# Check idea lister
my $ideasfound = findClickableStuff($img, $idealisterx, 'Idea list
+er', 1);
if($ideasfound) {
# Don't spend watts on thing maker when we still have ideas
print "Still have ideas left!\n";
next;
}
# Check thing maker
findClickableStuff($img, $thingmakerx, 'Thing maker', 1);
}
sub findClickableStuff($img, $startx, $name, $clickonlyone) {
my $listitemsfound = 0;
for(my $iy = $listendy; $iy > $liststarty; $iy--) {
my $found = 0;
for(my $ix = 0; $ix < 90; $ix++) {
my ($r, $g, $b) = $img->getpixel(x => $ix + $startx, y =>
+$iy)->rgba();
if($r == 255 && $g == 255 && $b == 255) {
# White text (or lines): List has at least one entry
$listitemsfound = 1;
}
#next if($r > 180);
#next if($g < 180);
# Search for somewhat green text (means we can affort the
+item)
if($r < 50 && $g > 220 && $b > 150) {
$found = 1;
last;
}
}
if($found) {
print $name, "item found!\n";
for(1..3) {
click($startx + 45, $iy);
}
$iy -= 50;
#$listitemsfound = 1;
if($clickonlyone) {
last;
}
}
}
return $listitemsfound;
}
sub click($x, $y) {
if($x != $lastx || $y != $lasty) {
MoveMouseAbs($x, $y, 0);
#sleep(0.5);
$lastx = $x;
$lasty = $y;
} else {
my ($mx, $my) = GetMousePos();
my $dist = abs($mx - $x) + abs($my - $y);
if($dist > 10) {
croak("USER ABORT!");
}
}
ClickMouseButton(M_LEFT);
return;
}
exit(0);
Version 2
Although, there is one very annoying unfeature: Every time the script needs to take a screenshot and
scan it for clickable items, it pauses clicking on the power button for 0.3 seconds (probably less on your
computer, but my rig is 10+ years old). If only there was a way to, i don't know split the clicking and the screenshot-scanning between different programs - without using Perl threads, which i just don't like. Unix
domain sockets to the rescue! (Windows folks: IO::Socket::IP with TCP will work with a small changes. And you would need to switch to the windows version of GUITest anyway).
I don't want to write two separate programs, so we'll just fork, make our socket connection and pass messages between them:
#!/usr/bin/env perl
use v5.36;
use strict;
use X11::GUITest qw[GetMousePos MoveMouseAbs ClickMouseButton :CONST];
use Imager::Screenshot qw[screenshot];
use Carp;
use Time::HiRes qw(sleep);
use IO::Socket::UNIX;
my $keepRunning = 1;
$SIG{CHLD} = sub {
print "Child exit!\n";
$keepRunning = 0;
};
################# TUNEABLES #############
my $w = 1920; # Screen width
my $h = 1080; # Screen height
my $thingmakerx = 55; # leftmost X position of "Thing maker" list
my $idealisterx = 1380; # leftmost X position of "Idea lister" list
my $liststarty = 490; # Top Y of lists
my $listendy = 1048; # Bottom Y of lists
my $buttonx = 186; # Power button thingy X
my $buttony = 369; # Power button thingy Y
my $socketpath = 'clicker.socket';
################# TUNEABLES #############
my $lastx = -1;
my $lasty = -1;
unlink $socketpath;
my $childpid = fork();
if(!defined($childpid)) {
croak("Fork failed");
}
if($childpid) {
# Parent
MouseClicker();
} else {
ScreenLooker();
}
exit(0);
The "main" process, e.g. the parent runs the MouseClicker() code:
sub MouseClicker() {
# MouseClicker is the server
my $server = IO::Socket::UNIX->new(
Type => SOCK_STREAM,
Local => $socketpath,
Listen => 1,
) or croak($!);
$server->blocking(0);
my $socket;
while(!defined($socket)) {
$socket = $server->accept();
}
$socket->blocking(0);
print "Screenlooker connection established!\n";
while($keepRunning) {
if(!click($buttonx, $buttony)) {
# USER ABORT
print "USER ABORT (mouse moved manually)\n";
syswrite($socket, "ABORT\n");
last;
}
my $getline = readSocket($socket);
if($getline ne '') {
my ($itemx, $itemy) = split/\§/, $getline;
print "Item click on $itemx / $itemy\n";
click($itemx, $itemy);
}
}
print "Mail loop exit\n";
while($keepRunning) {
print "Waiting for ScreenLooker to exit...\n";
sleep(0.1);
}
exit(0);
}
It's similar to the logic of the first program, except we get our information for where and when to click on list items via the socket connection. Plus, we handle user aborts a bit differently, since we need to inform our child process about it.
The logic of the child process also stays largely the same to what we had before, except that we have to handle the ABORT and don't actually do any clicking ourselves:
sub ScreenLooker() {
# ScreenLooker is the client
# Give server time to set up socket server
sleep(1);
my $socket = IO::Socket::UNIX->new(
Type => SOCK_STREAM,
Peer => $socketpath,
) or croak($!);
$socket->blocking(0);
while($keepRunning) {
my $img = screenshot();
# Check idea lister
my $ideasfound = findClickableStuff($img, $idealisterx, 'Idea
+lister', 1, $socket);
if($ideasfound) {
# Don't spend watts on thing maker when we still have idea
+s
#print "Still have ideas left!\n";
} else {
# Check thing maker
findClickableStuff($img, $thingmakerx, 'Thing maker', 1, $
+socket);
}
my $getline = readSocket($socket);
if($getline eq 'ABORT') {
print "MouseClicker wants to abort, exiting ScreenLooker\n
+";
$keepRunning = 0;
}
}
exit(0);
}
We also need a common function to do some non-blocking readline emulation on websockets. This is a slimmed down version of stuff i use in other programs. For clarity, i removed most of that "error handling" clutter. It's an auto-clicker, not a high-availability production server backend.
my $input = '';
sub readSocket($socket) {
my $retval = '';
while(1) {
my $buf;
my $readok = 0;
eval {
sysread($socket, $buf, 1); # Read one byte
$readok = 1;
};
if(!$readok) {
croak("Socket error");
}
last if(!defined($buf) || $buf eq '');
if($buf eq "\n") {
$retval = '' . $input;
$input = '';
last;
}
$input .= $buf;
}
return $retval;
}
The findClickableStuff() function also gets a slight tuneup, basically we change
click($startx + 45, $iy);
to
syswrite($socket, $startx + 45 . '§' . $iy . "\n");
And here is the full listing of autoclicker2.pl:
#!/usr/bin/env perl
use v5.36;
use strict;
use X11::GUITest qw[GetMousePos MoveMouseAbs ClickMouseButton :CONST];
use Imager::Screenshot qw[screenshot];
use Carp;
use Time::HiRes qw(sleep);
use IO::Socket::UNIX;
my $keepRunning = 1;
$SIG{CHLD} = sub {
print "Child exit!\n";
$keepRunning = 0;
};
################# TUNEABLES #############
my $w = 1920; # Screen width
my $h = 1080; # Screen height
my $thingmakerx = 55; # leftmost X position of "Thing maker" list
my $idealisterx = 1380; # leftmost X position of "Idea lister" list
my $liststarty = 490; # Top Y of lists
my $listendy = 1048; # Bottom Y of lists
my $buttonx = 186; # Power button thingy X
my $buttony = 369; # Power button thingy Y
my $socketpath = 'clicker.socket';
################# TUNEABLES #############
my $lastx = -1;
my $lasty = -1;
unlink $socketpath;
my $childpid = fork();
if(!defined($childpid)) {
croak("Fork failed");
}
if($childpid) {
# Parent
MouseClicker();
} else {
ScreenLooker();
}
exit(0);
sub MouseClicker() {
# MouseClicker is the server
my $server = IO::Socket::UNIX->new(
Type => SOCK_STREAM,
Local => $socketpath,
Listen => 1,
) or croak($!);
$server->blocking(0);
my $socket;
while(!defined($socket)) {
$socket = $server->accept();
}
$socket->blocking(0);
print "Screenlooker connection established!\n";
while($keepRunning) {
if(!click($buttonx, $buttony)) {
# USER ABORT
print "USER ABORT (mouse moved manually)\n";
syswrite($socket, "ABORT\n");
last;
}
my $getline = readSocket($socket);
if($getline ne '') {
my ($itemx, $itemy) = split/\§/, $getline;
print "Item click on $itemx / $itemy\n";
click($itemx, $itemy);
}
}
print "Mail loop exit\n";
while($keepRunning) {
print "Waiting for ScreenLooker to exit...\n";
sleep(0.1);
}
exit(0);
}
sub ScreenLooker() {
# ScreenLooker is the client
# Give server time to set up socket server
sleep(1);
my $socket = IO::Socket::UNIX->new(
Type => SOCK_STREAM,
Peer => $socketpath,
) or croak($!);
$socket->blocking(0);
while($keepRunning) {
my $img = screenshot();
# Check idea lister
my $ideasfound = findClickableStuff($img, $idealisterx, 'Idea
+lister', 1, $socket);
if($ideasfound) {
# Don't spend watts on thing maker when we still have idea
+s
#print "Still have ideas left!\n";
} else {
# Check thing maker
findClickableStuff($img, $thingmakerx, 'Thing maker', 1, $
+socket);
}
my $getline = readSocket($socket);
if($getline eq 'ABORT') {
print "MouseClicker wants to abort, exiting ScreenLooker\n
+";
$keepRunning = 0;
}
}
exit(0);
}
my $input = '';
sub readSocket($socket) {
my $retval = '';
while(1) {
my $buf;
my $readok = 0;
eval {
sysread($socket, $buf, 1); # Read one byte
$readok = 1;
};
if(!$readok) {
croak("Socket error");
}
last if(!defined($buf) || $buf eq '');
if($buf eq "\n") {
$retval = '' . $input;
$input = '';
last;
}
$input .= $buf;
}
return $retval;
}
sub findClickableStuff($img, $startx, $name, $clickonlyone, $socket) {
my $listitemsfound = 0;
for(my $iy = $listendy; $iy > $liststarty; $iy--) {
my $found = 0;
for(my $ix = 0; $ix < 90; $ix++) {
my ($r, $g, $b) = $img->getpixel(x => $ix + $startx, y =>
+$iy)->rgba();
if($r == 255 && $g == 255 && $b == 255) {
# White text (or lines): List has at least one entry
$listitemsfound = 1;
}
#next if($r > 180);
#next if($g < 180);
# Search for somewhat green text (means we can affort the
+item)
if($r < 50 && $g > 220 && $b > 150) {
$found = 1;
last;
}
}
if($found) {
print $name, "item found!\n";
#for(1..3) {
# click($startx + 45, $iy);
#}
syswrite($socket, $startx + 45 . '§' . $iy . "\n");
$iy -= 50;
#$listitemsfound = 1;
if($clickonlyone) {
last;
}
}
}
return $listitemsfound;
}
sub click($x, $y) {
if($x != $lastx || $y != $lasty) {
MoveMouseAbs($x, $y, 0);
#sleep(0.5);
$lastx = $x;
$lasty = $y;
} else {
my ($mx, $my) = GetMousePos();
my $dist = abs($mx - $x) + abs($my - $y);
if($dist > 10) {
print("USER ABORT!\n");
return 0;
}
}
ClickMouseButton(M_LEFT);
return 1;
}
exit(0);
Have fun.