Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,41 @@ This produces `build\RaidClient.dll` exporting:

- `BOOL clickRaid(int x, int y)` — post a left click to the game window
- `BOOL captureRaid(const char* path)` — save the client area as a BMP file

## Python farmer

A minimal farmer script inspired by [rslhelper](https://github.com/KSPOG/rslhelper)
resides in `rsl_farmer.py`. It uses [PyAutoGUI](https://pyautogui.readthedocs.io/)
to click the "Start"/"Replay" button and detect when a run has finished.
PyAutoGUI is available on PyPI and can be installed from your terminal with
`python -m pip install pyautogui`, or via the requirements file below.

Install dependencies and run the farmer:

Run these commands from a shell (not the Python `help>` prompt):

```bash
python -m pip install -r requirements.txt # installs PyAutoGUI and other dependencies
python rsl_farmer.py --start-x 1000 --start-y 800 --complete-img victory.png --runs 10
```

`complete-img` should point to an image that appears when the run ends (for example, a
screenshot of the "Victory" banner).

Instead of supplying command line arguments, you can launch a small
configuration window:

```bash
python rsl_farmer.py --gui
# or simply:
python rsl_farmer.py
```

The GUI lets you capture the start button coordinates, choose the completion
image and tweak run settings.

On Windows you can also launch the GUI with a batch file:

```
run_farmer_gui.bat
```
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyautogui>=0.9.50
215 changes: 215 additions & 0 deletions rsl_farmer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Simple Raid: Shadow Legends farmer.

This script uses :mod:`pyautogui` to click the "Start"/"Replay" button of a
Raid: Shadow Legends dungeon and waits for an image indicating the run has
finished. It is inspired by the automation features of
`rslhelper <https://github.com/KSPOG/rslhelper>`_.

The game client must be visible on screen and the coordinates of the
"Start"/"Replay" button supplied. An image of the "Victory" (or "Defeat")
screen is required so the script knows when to start the next run.

Requires the [PyAutoGUI](https://pyautogui.readthedocs.io/) package, which can
be installed with ``python -m pip install pyautogui``.
"""

from __future__ import annotations

import argparse
import sys
import time
from dataclasses import dataclass
from typing import Tuple

try: # pragma: no cover - tkinter may be unavailable in some environments
import tkinter as tk
from tkinter import filedialog, messagebox
except Exception: # pragma: no cover
tk = None # type: ignore

try:
import pyautogui
except Exception: # pragma: no cover - environment may not have pyautogui
pyautogui = None # type: ignore


@dataclass
class FarmerConfig:
"""Configuration for :class:`RSLFarmer`.

Attributes
----------
start_button:
Coordinates of the "Start"/"Replay" button.
complete_img:
Path to an image on disk that appears when a run is complete,
e.g. a screenshot of the "Victory" banner.
run_delay:
Delay between checks in seconds while waiting for a run to finish.
timeout:
Maximum time in seconds to wait for a run to finish before aborting.
"""

start_button: Tuple[int, int]
complete_img: str
run_delay: float = 2.0
timeout: float = 120.0


class RSLFarmer:
"""Automates repeated runs in Raid: Shadow Legends."""

def __init__(self, config: FarmerConfig):
if pyautogui is None:
raise RuntimeError(
"pyautogui is required but not installed. Install it with 'python -m pip install pyautogui'."
)
self.config = config

def _wait_for_complete(self) -> None:
start_time = time.time()
while True:
if pyautogui.locateOnScreen(self.config.complete_img, confidence=0.8):
return
if time.time() - start_time > self.config.timeout:
raise RuntimeError("Run timed out waiting for completion image.")
time.sleep(self.config.run_delay)

def run(self, runs: int) -> None:
"""Execute a number of runs.

Parameters
----------
runs:
Number of runs to perform.
"""

for i in range(1, runs + 1):
pyautogui.click(*self.config.start_button)
self._wait_for_complete()
# click the "Replay" button, assumed to be at the same coordinates
pyautogui.click(*self.config.start_button)
time.sleep(self.config.run_delay)


def launch_gui() -> None:
"""Open a simple Tkinter window to configure and start the farmer."""
if tk is None:
raise RuntimeError("tkinter is required for the GUI but is not available.")
if pyautogui is None:
raise RuntimeError(
"pyautogui is required but not installed. Install it with 'python -m pip install pyautogui'."
)

root = tk.Tk()
root.title("RSL Farmer")

start_x_var = tk.StringVar()
start_y_var = tk.StringVar()
img_var = tk.StringVar()
runs_var = tk.StringVar(value="1")
delay_var = tk.StringVar(value="2.0")
timeout_var = tk.StringVar(value="120.0")

def capture_start() -> None:
root.withdraw()
time.sleep(3)
x, y = pyautogui.position()
start_x_var.set(str(x))
start_y_var.set(str(y))
root.deiconify()

def browse_img() -> None:
path = filedialog.askopenfilename()
if path:
img_var.set(path)

def start_runs() -> None:
try:
config = FarmerConfig(
start_button=(int(start_x_var.get()), int(start_y_var.get())),
complete_img=img_var.get(),
run_delay=float(delay_var.get()),
timeout=float(timeout_var.get()),
)
runs = int(runs_var.get())
except Exception as exc: # pragma: no cover - user input errors
messagebox.showerror("Invalid input", str(exc))
return

root.withdraw()
try:
RSLFarmer(config).run(runs)
except Exception as exc: # pragma: no cover - runtime errors
messagebox.showerror("Error", str(exc))
finally:
root.deiconify()

# Layout
frm = tk.Frame(root)
frm.pack(padx=10, pady=10)

tk.Label(frm, text="Start X").grid(row=0, column=0, sticky="e")
tk.Entry(frm, textvariable=start_x_var, width=6).grid(row=0, column=1)
tk.Label(frm, text="Start Y").grid(row=0, column=2, sticky="e")
tk.Entry(frm, textvariable=start_y_var, width=6).grid(row=0, column=3)
tk.Button(frm, text="Capture", command=capture_start).grid(row=0, column=4, padx=(5, 0))

tk.Label(frm, text="Completion image").grid(row=1, column=0, sticky="e")
tk.Entry(frm, textvariable=img_var, width=25).grid(row=1, column=1, columnspan=3)
tk.Button(frm, text="Browse", command=browse_img).grid(row=1, column=4, padx=(5, 0))

tk.Label(frm, text="Runs").grid(row=2, column=0, sticky="e")
tk.Entry(frm, textvariable=runs_var, width=6).grid(row=2, column=1)

tk.Label(frm, text="Delay").grid(row=2, column=2, sticky="e")
tk.Entry(frm, textvariable=delay_var, width=6).grid(row=2, column=3)

tk.Label(frm, text="Timeout").grid(row=3, column=0, sticky="e")
tk.Entry(frm, textvariable=timeout_var, width=6).grid(row=3, column=1)

tk.Button(frm, text="Start", command=start_runs).grid(row=4, column=0, columnspan=5, pady=(10, 0))

root.mainloop()


def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--gui", action="store_true", help="Launch configuration window")
parser.add_argument("--start-x", type=int, help="X coordinate of Start/Replay button")
parser.add_argument("--start-y", type=int, help="Y coordinate of Start/Replay button")
parser.add_argument("--complete-img", help="Path to an image that signifies a completed run")
parser.add_argument("--runs", type=int, default=1, help="Number of runs to perform")
parser.add_argument("--delay", type=float, default=2.0, help="Delay between checks in seconds")
parser.add_argument("--timeout", type=float, default=120.0, help="Maximum time to wait for completion image")

argv = sys.argv[1:]
if not argv:
return argparse.Namespace(gui=True, start_x=None, start_y=None,
complete_img=None, runs=1, delay=2.0, timeout=120.0)
args = parser.parse_args(argv)
if not args.gui and (
args.start_x is None or args.start_y is None or args.complete_img is None
):
parser.error("--start-x, --start-y and --complete-img are required unless --gui is used")
return args


def main() -> None:
args = _parse_args()
if args.gui:
launch_gui()
return
config = FarmerConfig(
start_button=(args.start_x, args.start_y),
complete_img=args.complete_img,
run_delay=args.delay,
timeout=args.timeout,
)
farmer = RSLFarmer(config)
farmer.run(args.runs)


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions run_farmer_gui.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off
cd /d %~dp0
python rsl_farmer.py --gui %*