# LD플레이어 매크로 반복, 정지 조건, 예약 실행

> LDPlayer나 BlueStacks에서 매크로를 정지 조건과 함께 반복하고, 실행 사이에 진행 상황을 저장하며, 종료 코드를 남기고 MAS 스케줄러로 매일 예약 실행합니다.

Source: https://automationmacro.com/ko/docs/guides/loops-and-scheduling (가이드, updated 2026-09-05)

혼자 돌아가는 매크로에는 세 가지가 필요합니다: 언제 멈출지 아는 반복문, 어디까지 했는지 기억하는 방법, 그리고 사용자 없이 시작하는 예약. 이 가이드는 Macro Automation Studio(MAS)의 LD플레이어 매크로 반복과 BlueStacks 매크로 반복에 대해 이 세 가지를 모두 다룹니다. LD플레이어 매크로 반복 패턴, 정지 조건, 실행 사이의 스토리지, 웹훅이 보고할 수 있는 종료 코드, 그리고 스케줄러를 얻게 됩니다. 매크로를 "내가 Run을 클릭한다"에서 "매일 아침 알아서 돈다"로 옮기려는 분을 위한 가이드입니다.

## 시작하기 전에

- MAS가 설치되어 있고 로그인한 상태. 체험용은 [무료 다운로드](/download)입니다.
- Code-Based 프로젝트가 Code Editor에서 에뮬레이터로 실행되는 상태. [시작하기](/docs/getting-started)를 참고하세요.
- 에뮬레이터가 카드에서 포트를 확인할 수 있는 기기로 추가된 상태. [기기](/docs/devices)를 참고하세요.
- 템플릿과 영역이 캡처된 상태. [이미지 인식 가이드](/docs/guides/image-recognition-macros)를 참고하세요.

## LD플레이어와 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`를 끝낼 수 없습니다. 모든 반복문에 한계를 주세요.

## 매크로 반복 정지 조건

다음 중 최소 두 개를 조합하세요. 하나는 게임을, 다른 하나는 본인의 버그를 막습니다.

- **카운터.** `while collected < 50`. 저렴하고 예측 가능합니다.
- **시간 예산.** `time.monotonic()`은 초를 세고 시계가 바뀌어도 건너뛰지 않습니다. 바깥 반복문에 써서 예약 실행이 다음 실행 전에 항상 끝나게 하세요.
- **화면 상태.** "out of energy", "inventory full" 또는 일일 한도 메시지 같은 템플릿. 다른 템플릿처럼 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 ms 뒤에 나타나는 팝업은 느린 날에 고정 1000 ms 지연을 빠져나갑니다. 무작위 쉼이 둘 다 부드럽게 합니다. 앱 자체 반응 시간에는 `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]
> 카운터와 함께 날짜를 저장하세요. 그러면 일일 매크로가 이어진 실행과 새 날을 구별해 스스로 초기화할 수 있습니다.

## 웹훅이 상태를 보고하도록 코드와 함께 종료

스크립트가 끝나면 MAS가 프로세스 종료 코드를 읽습니다. 0은 실행 완료를 뜻하고, 그 밖의 값은 실패로 표시됩니다. 웹훅 구독자는 그에 따라 페이로드에 `exit_code`가 담긴 `macro.completed` 또는 `macro.failed`를 받습니다.

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

처리되지 않은 예외도 0이 아닌 값으로 종료되므로 추가 코드 없이 충돌이 실패로 보고됩니다. 스크립트만 아는 이벤트에는 `mas.webhook("level.reached", {"level": 40})`이 사용자 지정 이벤트를 구독한 엔드포인트에 `custom.level.reached` 이벤트를 보냅니다. 엔드포인트는 **Webhooks** 페이지에서 설정합니다. [웹훅](/docs/webhooks)을 참고하세요.

## 완전한 예제

매크로는 하루에 보상을 최대 50개 모으고, 개수를 기억하고, "out of energy"나 20분 뒤에 멈추고, 종료 코드로 보고합니다. 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에서 **Run**(<kbd>F5</kbd>)으로 한 번 실행해 스스로 끝나는지 확인하세요.

## BlueStacks나 LDPlayer에서 MAS 스케줄러로 매크로 예약 실행

스케줄러는 데스크톱 앱 안에 있으며 설정한 시간에 에뮬레이터에서 매크로를 시작합니다.

1. **Scheduler**를 열고 **Create New Schedule**을 클릭합니다.
2. **Name**(3자에서 100자)을 입력하고 **Macro**를 고릅니다.
3. **Emulator Port**에서 **Scan**을 클릭해 인스턴스의 포트를 고르거나 직접 입력합니다.
4. **Date**와 **Time**을 설정합니다. 시간은 컴퓨터의 현지 시간입니다.
5. **Recurrence**를 설정합니다: 한 번 실행은 **None**, **Daily**, **Days of Week**를 체크한 **Weekly**, 또는 **Monthly**.
6. 매 실행 시각마다 매크로를 여러 번 연달아 실행하고 싶지 않다면 **Repeat Count**를 1로 둡니다.
7. **Active**를 켠 채 **Create Schedule**을 클릭합니다.

의지하기 전에 알아 둘 것:

- 앱이 열려 있고 로그인되어 있어야 합니다. 스케줄러는 시스템 서비스가 아니며 cron 문법도 없습니다.
- 작업은 30초마다 확인되므로 실행이 지정 시간보다 최대 30초 늦게 시작할 수 있습니다.
- 예약은 포트 하나를 대상으로 합니다. 시간이 되었을 때 포트가 다른 실행으로 바쁘면 스케줄러는 그 회차를 건너뛰고 다음 회차에 다시 시도합니다.
- 같은 포트에서 두 예약이 같은 시간대를 공유할 수 없습니다. 앱이 "Time slot is occupied on this port"로 거부합니다.
- 예약된 작업은 동시에 최대 20개까지 실행할 수 있습니다.
- **Repeat Count**가 1보다 크면 실패한 실행(0이 아닌 종료)이 남은 반복을 멈추고 작업을 Failed로 표시합니다.
- 예약 실행은 기기 카드에 저장된 인수, 또는 기기가 따르는 설정 프로필을 사용합니다. [설정 프로필](/docs/settings-profiles)을 참고하세요.
- 작업의 **View History**는 모든 실행을 상태와 로그와 함께 보여 줍니다.

작업 편집, 건너뛰기, 취소는 [스케줄러](/docs/scheduler) 페이지에서 다룹니다.

## 그룹에서 실행

같은 매크로를 여러 인스턴스에서 손으로 실행하려면 기기 그룹 하나에 넣고, 각 기기에 매크로를 지정하고, **Start All**을 클릭하세요. 기기가 0.5초 간격으로 차례로 시작되며 각각 자기 실행, 로그, 종료 코드를 가집니다. 그룹을 예약하려면 같은 매크로와 시간으로 포트마다 예약 작업 하나를 만드세요. [멀티 인스턴스 가이드](/docs/guides/multi-instance)에서 기기별 인수, 프록시, 스토리지를 다룹니다.

## 잘못될 수 있는 것

### 반복문이 끝나지 않음

한계 없는 `while True`이거나, 끝내 나타나지 않는 템플릿에 의존하는 정지 조건입니다. `time.monotonic()` 시간 예산과 연속 실패 카운터를 추가하세요. 그 둘이 모든 반복문을 끝냅니다.

### 예약이 실행되지 않음

그 시간에 앱이 닫혀 있거나 절전 상태였거나, 포트가 바빴거나, 작업이 **Active**가 아닙니다. **View History**에서 건너뛴 항목이나 실패한 항목을 확인하고, 예약된 시간대에 컴퓨터를 깨어 있게 하세요.

### Time slot is occupied on this port

같은 포트의 다른 작업이 이미 그 시간을 갖고 있습니다. 한 작업을 몇 분 옮기거나, 두 번째 매크로를 **Repeat Count**로 첫 번째 안에 넣으세요.

### 매크로는 끝났는데 실행이 Failed로 표시됨

스크립트가 마지막 동작 뒤에 0이 아닌 종료 코드나 처리되지 않은 예외로 끝났습니다. 로그의 마지막 줄을 읽어 보세요. 정리 경로의 `sys.exit(1)`이나 저장된 상태의 `KeyError`가 흔한 원인입니다.

100% 안전한 자동화 도구는 없으므로, 책임감을 가지고 본인의 판단에 따라 자동화하세요.
