Guides
LDPlayer Macro Loop, Stop Conditions and Scheduling
Loop a macro on LDPlayer or BlueStacks with stop conditions, keep progress between runs, exit with a status code, and schedule it daily in the MAS Scheduler.
- Windows
- Mac
- Emulator
- Studio
- Python SDK
On this page
- Before you start
- Macro loop patterns for LDPlayer and BlueStacks
- Macro loop stop conditions
- Sleeps and humanized timing
- Persist progress with storage
- Exit with a code so webhooks report status
- A complete example
- Schedule the macro in the MAS Scheduler on BlueStacks or LDPlayer
- Run it on a group
- What can go wrong
- The loop never ends
- The schedule did not fire
- Time slot is occupied on this port
- The run shows Failed but the macro finished
A macro that runs unattended needs three things: a loop that knows when to stop, a way to remember where it got to, and a schedule that starts it without you. This guide covers all three for an LDPlayer or BlueStacks macro in Macro Automation Studio (MAS). You get the loop patterns, the stop conditions, storage between runs, exit codes that webhooks can report, and the Scheduler. It is for anyone moving a macro from “I click Run” to “it runs every morning”.
Before you start
- MAS is installed and signed in. It is a free download for the trial.
- A Code-Based project runs on an emulator from the Code Editor. See Getting started.
- The emulator is added as a device with a port you can find on its card. See Devices.
- Your templates and regions are captured. See the image recognition guide.
Macro loop patterns for LDPlayer and BlueStacks
Three shapes cover nearly every macro.
While, with a stop condition. The loop runs until something on screen or in your counters says stop.
import mas
images = mas.images({"collect": 401, "done": 402})
collected = 0
while collected < 50:
if mas.find_object(images.done):
break
button = mas.find_object_retry(images.collect, total_tries=3, time_sleep=2.0)
if button is None:
continue
mas.click(button.x, button.y, delay_ms=1000)
collected += 1For a fixed count. You know how many times to do the thing.
for round_no in range(1, 11):
mas.log(f"Round {round_no} of 10")
button = mas.find_object_retry(images.collect)
if button is None:
mas.log("Nothing to collect, stopping early", level="warning")
break
mas.click(button.x, button.y)Until a template appears. A wait with a deadline, used for loading screens and long timers.
import time
def wait_for(image, timeout_s=120, every_s=3.0):
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
match = mas.find_object(image)
if match:
return match
time.sleep(every_s)
return NoneEvery loop above has an exit. find_object returns None on a miss and never raises, so a missing template on its own cannot end a while True. Give every loop a bound.
Macro loop stop conditions
Combine at least two of these. One guards against the game, the other against your own bug.
- A counter.
while collected < 50. Cheap and predictable. - A time budget.
time.monotonic()counts seconds and never jumps when the clock changes. Use it for the outer loop so a scheduled run always ends before the next one starts. - A screen state. A template such as “out of energy”, “inventory full” or a daily-limit message. Capture it in Asset Lab like any other template and check it at the top of each pass.
- Consecutive misses. Count how many passes in a row found nothing and give up after five. A screen you did not plan for looks exactly like this.
started = time.monotonic()
misses = 0
while time.monotonic() - started < 20 * 60:
if mas.find_object(images.out_of_energy):
break
button = mas.find_object_retry(images.collect)
if button is None:
misses += 1
if misses >= 5:
break
continue
misses = 0
mas.click(button.x, button.y)Sleeps and humanized timing
click already waits delay_ms=1000 after the tap and swipe takes duration_ms=1000 by default. Between passes, add a pause that varies.
import random
def pause(low=0.8, high=2.5):
time.sleep(random.uniform(low, high))Fixed intervals look mechanical and also race the game. A popup that appears 900 ms after a tap slips past a fixed 1000 ms delay on a slow day. A random pause smooths both. Keep delay_ms on click for the app’s own reaction time and use pause() for the human rhythm.
Persist progress with storage
A scheduled run that stops halfway should pick up where it left off the next time. The storage functions keep a small JSON document per task, keyed by this computer, the device’s port and a task name you choose.
TASK = "daily_collect"
state = mas.retrieve(TASK) # {} on the first run
collected = state.get("collected", 0)
last_day = state.get("day")
mas.save(TASK, {"collected": collected, "day": today}) # after each pass of the loop
if collected >= 50:
mas.clear(TASK) # start fresh next timesave(task_name, data) writes or replaces the entry; retrieve(task_name) returns the dictionary or an empty one; clear(task_name) deletes it. data must be JSON serialisable. Because the port is part of the key, two emulator instances running the same macro never share a counter. retrieve_all(task_name) returns every instance’s entry when you want a total. See Storage.
Exit with a code so webhooks report status
MAS reads the process exit code when your script ends. Zero means the run completed; anything else marks it as failed. Webhook subscribers receive macro.completed or macro.failed accordingly, with the exit_code in the payload.
import sys
if energy is None:
mas.log("Could not read energy", level="error")
sys.exit(2) # macro.failed, exit_code 2
mas.log("All done")
sys.exit(0) # macro.completedAn unhandled exception also exits non-zero, so a crash is reported as a failure without any extra code. For events only your script knows about, mas.webhook("level.reached", {"level": 40}) sends a custom.level.reached event to endpoints subscribed to custom events. Set endpoints up on the Webhooks page; see Webhooks.
A complete example
The macro collects up to fifty rewards a day, remembers its count, stops on “out of energy” or after twenty minutes, and reports through its exit code. Replace the IDs with yours.
import datetime
import random
import sys
import time
import mas
images = mas.images({
"collect": 401,
"close_popup": 402,
"out_of_energy": 403,
})
TASK = "daily_collect"
DAILY_LIMIT = 50
TIME_BUDGET_S = 20 * 60
def pause(low=0.8, high=2.5):
time.sleep(random.uniform(low, high))
def main():
today = datetime.date.today().isoformat()
state = mas.retrieve(TASK)
collected = state.get("collected", 0) if state.get("day") == today else 0
mas.log(f"{today}: starting at {collected} of {DAILY_LIMIT}")
started = time.monotonic()
misses = 0
while collected < DAILY_LIMIT:
if time.monotonic() - started > TIME_BUDGET_S:
mas.log("Time budget reached", level="warning")
break
popup = mas.find_object(images.close_popup)
if popup:
mas.click(popup.x, popup.y, delay_ms=800)
continue
if mas.find_object(images.out_of_energy):
mas.log("Out of energy")
break
button = mas.find_object_retry(images.collect, total_tries=3, time_sleep=2.0)
if button is None:
misses += 1
if misses >= 5:
mas.log("Five misses in a row, giving up", level="error")
mas.save(TASK, {"collected": collected, "day": today})
sys.exit(2)
continue
misses = 0
mas.click(button.x, button.y, delay_ms=1000)
collected += 1
mas.save(TASK, {"collected": collected, "day": today})
pause()
mas.log(f"Finished at {collected} of {DAILY_LIMIT}")
sys.exit(0)
if __name__ == "__main__":
main()Run it once from the Code Editor with Run (F5) and confirm it ends on its own before you schedule it.
Schedule the macro in the MAS Scheduler on BlueStacks or LDPlayer
The Scheduler lives inside the desktop app and starts macros on your emulators at the times you set.
- Open Scheduler and click Create New Schedule.
- Enter a Name (3 to 100 characters) and pick the Macro.
- Under Emulator Port, click Scan and choose the port of the instance, or type it.
- Set Date and Time. The time is your computer’s local time.
- Set Recurrence: None for a single run, Daily, Weekly with the Days of Week ticked, or Monthly.
- Leave Repeat Count at 1 unless you want the macro to run several times back to back at each firing.
- Keep Active on and click Create Schedule.
What to know before you rely on it:
- The app must stay open and signed in. The scheduler is not a system service and there is no cron syntax.
- Jobs are checked every 30 seconds, so a run can start up to half a minute after its time.
- A schedule targets one port. If the port is busy with another run when the time comes, the scheduler skips that cycle and tries again on the next one.
- Two schedules cannot share a time slot on the same port; the app refuses with “Time slot is occupied on this port”.
- Up to 20 scheduled jobs can run at the same time.
- With Repeat Count above 1, a failed run (non-zero exit) stops the remaining repeats and marks the job as Failed.
- Scheduled runs use the arguments saved on the device card, or the settings profile the device follows. See Settings profiles.
- View History on a job shows every run with its status and logs.
The Scheduler page covers editing, skipping and cancelling jobs.
Run it on a group
To run the same macro on several instances by hand, put them in one device group, assign the macro to each device and click Start All. Devices start one after another, half a second apart, and each gets its own run, logs and exit code. To schedule the group, create one scheduled job per port with the same macro and time. The multi-instance guide covers per-device arguments, proxies and storage.
What can go wrong
The loop never ends
A while True with no bound, or a stop condition that depends on a template that never appears. Add a time budget with time.monotonic() and a consecutive-miss counter; those two end every loop.
The schedule did not fire
The app was closed or asleep at the time, the port was busy, or the job is not Active. Check View History for a skipped or failed entry, and keep the computer awake for the scheduled window.
Time slot is occupied on this port
Another job on the same port already owns that time. Move one job a few minutes, or put the second macro inside the first with Repeat Count.
The run shows Failed but the macro finished
The script ended with a non-zero exit code or an unhandled exception after its last action. Read the last lines of the log; a sys.exit(1) in a cleanup path or a KeyError on the stored state are the usual causes.
No automation tool is 100% risk-free, so automate responsibly and at your own discretion.
Next steps
Related pages
Thanks. If something is wrong, tell us in Discord.
Questions? Ask in Discord