Skip to content

ldplayer

LDPlayerAdapter

Bases: Emulator


              flowchart TD
              src.ai_coc.adapters.ldplayer.LDPlayerAdapter[LDPlayerAdapter]
              src.ai_coc.adapters.emulator.Emulator[Emulator]

                              src.ai_coc.adapters.emulator.Emulator --> src.ai_coc.adapters.ldplayer.LDPlayerAdapter
                


              click src.ai_coc.adapters.ldplayer.LDPlayerAdapter href "" "src.ai_coc.adapters.ldplayer.LDPlayerAdapter"
              click src.ai_coc.adapters.emulator.Emulator href "" "src.ai_coc.adapters.emulator.Emulator"
            

Methods:

Name Description
version

Off the banner ldconsole prints with no command, ldplayer v14.0.29.0 ....

serial_for
enumerate_instances
launch_instance

Start the instance, unless it is already on its way up.

restart_instance

Quit, let the VM wind down, and launch, returning once the launch has taken.

close_instance
serial_of

What the instance reports while it is up, and where it will listen while it is down.

controller

One adbutils-backed controller per instance, keyed by its own ADB port.

launch_coc
instance

One instance by index, or None while the emulator leaves it out of its listing.

ensure_coc

Bring one instance to a running CoC screen, recovering stale launches.

restart_coc
screenshot
tap
swipe

label

label: str = 'LDPlayer'

install_root

install_root: Path = Field(default_factory=detect_install_path)

cli

cli: Path

version

version() -> str

Off the banner ldconsole prints with no command, ldplayer v14.0.29.0 ....

Source code in src/ai_coc/adapters/ldplayer.py
def version(self) -> str:
    """Off the banner `ldconsole` prints with no command, `ldplayer v14.0.29.0 ...`."""
    found = re.search(r"v(\d+(?:\.\d+)+)", self._console())
    return found.group(1) if found else "unknown"

serial_for

serial_for(index: int) -> str
Source code in src/ai_coc/adapters/ldplayer.py
def serial_for(self, index: int) -> str:
    return f"127.0.0.1:{FIRST_ADB_PORT + ADB_PORT_STEP * index}"

enumerate_instances

enumerate_instances() -> list[EmulatorInstance]
Source code in src/ai_coc/adapters/ldplayer.py
def enumerate_instances(self) -> list[EmulatorInstance]:
    instances: list[EmulatorInstance] = []
    for line in self._console("list2").splitlines():
        if not line.strip():
            continue
        info = LDPlayerInstanceInfo.parse(line)
        serial = self.serial_for(info.index)
        answered = coc_running = False
        # **Android says it is up well before it can start anything**:
        # watched through a boot, `list2` said 1 ten seconds before `adb`
        # answered at all, and `adb` answered a moment before the boot
        # completed — a game launched there came up and died. So the
        # serial goes out as port 0 until the boot has completed, which is
        # what MuMu's CLI reports in the same window and what `_booted`
        # waits on.
        #
        # **And `list2` can lose an instance that is up**, so ADB is asked
        # whatever it says. Seen twice on 2026-09-26: an instance that had
        # just farmed for an hour, and one opened from its icon minutes
        # earlier, both read `0,…,-1,-1` with the player and the VM running
        # and `adb` answering. Believing `list2` there had `ensure_coc`
        # launch an emulator that was already up, wait out the boot and
        # fail on port 0, without ever reaching the game.
        try:
            adb = self.controller(serial)
            answered = adb.shell(["getprop", "sys.boot_completed"], timeout=5).strip() == "1"
            coc_running = answered and adb.is_running(COC_PACKAGE)
        except AdbControlError:
            logger.debug("LDPlayer instance %s has no ADB bridge", info.index)
        if answered and not info.android_started:
            logger.warning(
                "LDPlayer lists instance %s as down, but its ADB answers; driving it anyway",
                info.index,
            )
        android = 1 if answered else info.android
        instances.append(
            EmulatorInstance(
                emulator_id=f"ldplayer:{info.index}",
                index=info.index,
                name=info.title or f"LDPlayer {info.index}",
                android_version="unknown",
                adb_serial=serial if answered else AdbEndpoint().serial,
                process_started=info.pid > 0,
                android_started=android == 1,
                state=STATES.get(android, "unknown"),
                pid=info.pid,
                main_hwnd=info.top_hwnd,
                render_hwnd=info.bind_hwnd,
                resolution=f"{info.width}x{info.height}",
                dpi=str(info.dpi),
                coc_running=coc_running,
            )
        )
    logger.info("LDPlayer reported %d instance(s)", len(instances))
    return sorted(instances, key=lambda item: item.index)

launch_instance

launch_instance(index: int) -> None

Start the instance, unless it is already on its way up.

ensure_coc asks for a launch whenever Android is not up, which takes in an instance still booting — straight after restart_instance, every time — and a second launch into one is not something to find out about on an emulator whose reboot already wipes its settings.

Source code in src/ai_coc/adapters/ldplayer.py
def launch_instance(self, index: int) -> None:
    """Start the instance, unless it is already on its way up.

    `ensure_coc` asks for a launch whenever Android is not up, which takes
    in an instance still booting — straight after `restart_instance`, every
    time — and a second launch into one is not something to find out about
    on an emulator whose `reboot` already wipes its settings.
    """
    if self._running(index):
        logger.info("LDPlayer instance %s is already starting; not launching it twice", index)
        return
    self._console("launch", "--index", str(index), timeout=30)

restart_instance

restart_instance(index: int) -> None

Quit, let the VM wind down, and launch, returning once the launch has taken.

Never reboot: twice out of three it rewrote the instance's settings file to LDPlayer's defaults, which puts the instance at 1920x1080 and turns local ADB debugging off — nothing here is measured at the one and nothing here can reach the other. quit and launch left the file alone every time.

isrunning still says stop straight after a launch and running two seconds later, which is the window ensure_coc would launch into again, so this returns only once it has closed.

Source code in src/ai_coc/adapters/ldplayer.py
def restart_instance(self, index: int) -> None:
    """Quit, let the VM wind down, and launch, returning once the launch has taken.

    **Never `reboot`**: twice out of three it rewrote the instance's settings
    file to LDPlayer's defaults, which puts the instance at 1920x1080 and
    turns local ADB debugging off — nothing here is measured at the one and
    nothing here can reach the other. `quit` and `launch` left the file
    alone every time.

    `isrunning` still says stop straight after a `launch` and running two
    seconds later, which is the window `ensure_coc` would launch into
    again, so this returns only once it has closed.
    """
    self._console("quit", "--index", str(index), timeout=30)
    self._settle(index, running=False)
    time.sleep(QUIT_SETTLE)
    self._console("launch", "--index", str(index), timeout=30)
    self._settle(index, running=True)

close_instance

close_instance(index: int) -> None
Source code in src/ai_coc/adapters/ldplayer.py
def close_instance(self, index: int) -> None:
    self._console("quit", "--index", str(index), timeout=30)

serial_of

serial_of(instance: EmulatorInstance) -> str

What the instance reports while it is up, and where it will listen while it is down.

Source code in src/ai_coc/adapters/emulator.py
def serial_of(self, instance: EmulatorInstance) -> str:
    """What the instance reports while it is up, and where it will listen while it is down."""
    return instance.adb_serial if instance.endpoint.ready else self.serial_for(instance.index)

controller

controller(serial: str) -> AdbController

One adbutils-backed controller per instance, keyed by its own ADB port.

Source code in src/ai_coc/adapters/emulator.py
def controller(self, serial: str) -> AdbController:
    """One adbutils-backed controller per instance, keyed by its own ADB port."""
    if serial not in self._controllers:
        self._controllers[serial] = AdbController(endpoint=AdbEndpoint.parse(serial))
    return self._controllers[serial]

launch_coc

launch_coc(instance: EmulatorInstance) -> None
Source code in src/ai_coc/adapters/emulator.py
def launch_coc(self, instance: EmulatorInstance) -> None:
    self.controller(instance.adb_serial).launch_app(COC_PACKAGE)

instance

instance(index: int) -> EmulatorInstance | None

One instance by index, or None while the emulator leaves it out of its listing.

None is a real state rather than an error: MuMu drops an instance from the listing for a moment while it restarts, so a caller waiting on one keeps whatever it last saw rather than giving up.

Source code in src/ai_coc/adapters/emulator.py
def instance(self, index: int) -> EmulatorInstance | None:
    """One instance by index, or None while the emulator leaves it out of its listing.

    None is a real state rather than an error: MuMu drops an instance from
    the listing for a moment while it restarts, so a caller waiting on one
    keeps whatever it last saw rather than giving up.
    """
    return next((item for item in self.enumerate_instances() if item.index == index), None)

ensure_coc

ensure_coc(index: int) -> EmulatorInstance

Bring one instance to a running CoC screen, recovering stale launches.

Source code in src/ai_coc/adapters/emulator.py
def ensure_coc(self, index: int) -> EmulatorInstance:
    """Bring one instance to a running CoC screen, recovering stale launches."""
    logger.info("Ensuring Clash of Clans is running on %s instance %s", self.label, index)
    current = self.instance(index)
    if current is None:
        raise EmulatorError(f"找不到 {self.label} instance {index}")
    if not current.android_started:
        self.launch_instance(index)
        current = self._booted(index, current)
    self.launch_coc(current)
    for _ in range(LAUNCH_POLLS):
        time.sleep(POLL_GAP)
        current = self.instance(index) or current
        if current.coc_running:
            return current
    # MuMu can report Android ready while the first launch is ignored.
    logger.warning("Clash of Clans did not come up on %s; restarting the instance", index)
    self.restart_instance(index)
    current = self._booted(index, current)
    self.launch_coc(current)
    time.sleep(RELAUNCH_SETTLE)
    refreshed = self.instance(index) or current
    if not refreshed.coc_running:
        raise EmulatorError("已重啟模擬器,但部落衝突仍未啟動")
    return refreshed

restart_coc

restart_coc(instance: EmulatorInstance) -> None
Source code in src/ai_coc/adapters/emulator.py
def restart_coc(self, instance: EmulatorInstance) -> None:
    adb = self.controller(instance.adb_serial)
    adb.stop_app(COC_PACKAGE)
    adb.launch_app(COC_PACKAGE)

screenshot

screenshot(instance: EmulatorInstance) -> bytes
Source code in src/ai_coc/adapters/emulator.py
def screenshot(self, instance: EmulatorInstance) -> bytes:
    adb = self.controller(instance.adb_serial)
    return adb.screenshot(adb.display_for(COC_PACKAGE))

tap

tap(instance: EmulatorInstance, x: int, y: int) -> None
Source code in src/ai_coc/adapters/emulator.py
def tap(self, instance: EmulatorInstance, x: int, y: int) -> None:
    adb = self.controller(instance.adb_serial)
    adb.tap(x, y, adb.display_for(COC_PACKAGE))

swipe

swipe(
    instance: EmulatorInstance, start: tuple[int, int], end: tuple[int, int], duration_ms: int
) -> None
Source code in src/ai_coc/adapters/emulator.py
def swipe(
    self,
    instance: EmulatorInstance,
    start: tuple[int, int],
    end: tuple[int, int],
    duration_ms: int,
) -> None:
    adb = self.controller(instance.adb_serial)
    adb.swipe(start, end, duration_ms, adb.display_for(COC_PACKAGE))