# 雷电模拟器脚本循环、停止条件与定时运行

> 让 LDPlayer 或 BlueStacks 上的宏带停止条件循环，在运行之间保存进度，用状态码退出，并在 MAS 定时任务中每天运行。

Source: https://automationmacro.com/zh-CN/docs/guides/loops-and-scheduling (教程, updated 2026-09-05)

一个无人值守运行的宏需要三样东西：知道何时停止的循环、记住进行到哪里的方法，以及不用你动手就启动它的定时。这篇指南为 Macro Automation Studio（MAS）中的雷电模拟器脚本循环（LDPlayer 或 BlueStacks 上都一样）讲清这三样。你会得到循环模式、停止条件、运行之间的存储、Webhook 可以报告的退出码，以及定时任务。适合把宏从"我点运行"变成"它每天早上自己跑"的人。

## 开始之前

- 已安装并登录 MAS。试用期内可[免费下载](/download)。
- 一个代码型项目能从 Code Editor 在模拟器上运行。参见[快速开始](/docs/getting-started)。
- 模拟器已添加为设备，端口可以在它的卡片上找到。参见[设备](/docs/devices)。
- 你的模板和区域已经截取。参见[图像识别指南](/docs/guides/image-recognition-macros)。

## LDPlayer 和 BlueStacks 的脚本循环模式

三种形状覆盖几乎所有的宏。

**带停止条件的 while。** 循环一直跑，直到屏幕上或计数器里的某样东西说停。

```python
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 += 1
```

**固定次数的 for。** 你知道这件事要做多少次。

```python
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)
```

**直到模板出现。** 一个带截止时间的等待，用于加载页面和长计时器。

```python
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 None
```

上面每个循环都有出口。`find_object` 找不到时返回 `None`，从不抛异常，所以模板缺失本身不能结束一个 `while True`。给每个循环一个边界。

## 脚本循环的停止条件

至少组合其中两个。一个防游戏，另一个防你自己的 bug。

- **计数器。** `while collected < 50`。便宜且可预测。
- **时间预算。** `time.monotonic()` 按秒计数，时钟变化时也不会跳变。把它用在外层循环，这样定时运行总会在下一次开始前结束。
- **屏幕状态。** 一个模板，比如"体力不足"、"背包已满"或每日上限提示。像其他模板一样在 Asset Lab 中截取，在每一轮开头检查。
- **连续失败次数。** 数一数连续多少轮什么都没找到，五次之后放弃。一个你没有计划到的屏幕看起来正是这样。

```python
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)
```

## 停顿与拟人化节奏

`click` 在点击后已经等待 `delay_ms=1000`，`swipe` 默认持续 `duration_ms=1000`。在轮次之间，加入一个有变化的停顿。

```python
import random

def pause(low=0.8, high=2.5):
    time.sleep(random.uniform(low, high))
```

固定间隔看起来很机械，还会和游戏抢时间。点击后 900 毫秒才出现的弹窗，在慢的日子里会从固定的 1000 毫秒延迟中溜过去。随机停顿两者都能缓和。`click` 上的 `delay_ms` 留给应用自己的反应时间，`pause()` 用于人的节奏。

## 用存储保存进度

中途停止的定时运行，下一次应该从上次的位置继续。存储函数为每个任务保存一个小的 JSON 文档，键由这台电脑、设备的端口和你选的任务名组成。

```python
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 time
```

`save(task_name, data)` 写入或替换条目；`retrieve(task_name)` 返回字典或空字典；`clear(task_name)` 删除它。`data` 必须可以 JSON 序列化。因为端口是键的一部分，运行同一个宏的两个模拟器实例永远不会共享计数器。想要总数时，`retrieve_all(task_name)` 返回每个实例的条目。参见[存储](/docs/sdk/storage)。

> [!TIP]
> 把日期和计数器一起存。每日宏就能区分恢复的运行和新的一天，并自行重置。

## 用退出码退出，让 Webhook 报告状态

脚本结束时 MAS 读取进程的退出码。零表示运行完成；其他任何值都标记为失败。Webhook 订阅者相应地收到 `macro.completed` 或 `macro.failed`，载荷里带 `exit_code`。

```python
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.completed
```

未处理的异常也会以非零退出，所以崩溃无需额外代码就会被报告为失败。对于只有你的脚本知道的事件，`mas.webhook("level.reached", {"level": 40})` 会向订阅了自定义事件的端点发送一个 `custom.level.reached` 事件。在 **Webhook** 页设置端点；参见 [Webhook](/docs/webhooks)。

## 一个完整的示例

这个宏每天最多领取五十个奖励，记住自己的计数，在"体力不足"或二十分钟后停止，并通过退出码报告。把 ID 换成你的。

```python
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()
```

先在 Code Editor 中用**运行**（<kbd>F5</kbd>）跑一次，确认它能自己结束，再设置定时。

## 在 BlueStacks 或 LDPlayer 上用 MAS 定时任务运行宏

定时任务位于桌面应用内部，在你设定的时间在模拟器上启动宏。

1. 打开**定时任务**，点击**新建计划**。
2. 输入**名称**（3 到 100 个字符），选择**宏**。
3. 在**模拟器端口**下点击**扫描**并选择实例的端口，或者直接输入。
4. 设置**日期**和**时间**。时间是你电脑的本地时间。
5. 设置**重复**：**无**表示只运行一次，**每天**，**每周**并勾选**星期几**，或者**每月**。
6. **重复次数**保持为 1，除非你想让宏在每次触发时连续运行多次。
7. 保持**启用**打开，点击**创建计划**。

依赖它之前要知道的事：

- 应用必须保持打开并登录。定时任务不是系统服务，也没有 cron 语法。
- 任务每 30 秒检查一次，所以运行可能比设定时间晚最多半分钟开始。
- 一个计划针对一个端口。到时间时端口正被另一次运行占用，定时任务会跳过这一轮，下一轮再试。
- 两个计划不能在同一个端口共用一个时间段；应用会拒绝并提示 "Time slot is occupied on this port"。
- 最多 20 个定时任务可以同时运行。
- **重复次数**大于 1 时，一次失败的运行（非零退出）会停止剩余的重复，并把任务标记为失败。
- 定时运行使用设备卡片上保存的参数，或设备跟随的设置方案。参见[设置方案](/docs/settings-profiles)。
- 任务上的**查看历史**显示每次运行及其状态和日志。

[定时任务](/docs/scheduler)页介绍编辑、跳过和取消任务。

## 在组上运行

要手动在多个实例上运行同一个宏，把它们放进一个设备组，给每台设备分配宏，点击**全部启动**。设备依次启动，间隔半秒，每台都有自己的运行、日志和退出码。要为组设置定时，为每个端口创建一个宏和时间相同的定时任务。[多开指南](/docs/guides/multi-instance)介绍按设备的参数、代理和存储。

## 可能出什么问题

### 循环永远不结束

一个没有边界的 `while True`，或者一个依赖永远不会出现的模板的停止条件。加上用 `time.monotonic()` 的时间预算和连续失败计数器；这两个能结束任何循环。

### 定时没有触发

到时间时应用已关闭或处于休眠、端口被占用，或者任务没有**启用**。在**查看历史**中查找跳过或失败的条目，并在定时窗口内让电脑保持唤醒。

### Time slot is occupied on this port

同一个端口上的另一个任务已经占用了那个时间。把一个任务挪开几分钟，或者用**重复次数**把第二个宏放进第一个里。

### 运行显示失败，但宏完成了

脚本在最后一个动作之后以非零退出码或未处理的异常结束。阅读日志的最后几行；清理路径里的 `sys.exit(1)`，或存储状态上的 `KeyError`，是常见原因。

没有任何自动化工具是 100% 无风险的，请负责任地使用自动化，并自行斟酌决定。
