Skip to content

simulatecraft.minecraft.connection

simulatecraft.minecraft.connection

Mineflayer IPC bridge — Python side.

Architecture

A Node.js process runs bot/bot.js which connects to Minecraft via Mineflayer. This module spawns that process and communicates with it over a local TCP socket using newline-delimited JSON (one JSON object per line).

Python sends → {"id": "", "method": "", "params": {...}} Node responds ← {"id": "", "result": {...}} or {"id": "", "error": "..."} Node also pushes unsolicited events: ← {"event": "", "data": {...}}

Usage

bridge = MinecraftBridge(host="localhost", minecraft_port=25565,
                         username="SimBot", bot_script=None)
await bridge.connect()
state = await bridge.get_state()
await bridge.perform_action({"kind": "chat", "text": "hello!"})
await bridge.close()

bot_script defaults to the bundled bot/bot.js.

BridgeError

Bases: Exception

Raised when the bot process crashes or returns an error response.

MinecraftBridge

MinecraftBridge(*, host: str = 'localhost', minecraft_port: int = 25565, username: str = 'SimBot', password: str = '', version: str | None = None, ipc_port: int = _DEFAULT_IPC_PORT, bot_script: str | Path | None = None, node_executable: str = 'node', connect_timeout: float = 30.0, request_timeout: float = 45.0, auth: str = 'offline')

Manages the Node.js Mineflayer subprocess and the JSON-RPC socket.

Source code in src/simulatecraft/minecraft/connection.py
def __init__(
    self,
    *,
    host: str = "localhost",
    minecraft_port: int = 25565,
    username: str = "SimBot",
    password: str = "",
    version: str | None = None,
    ipc_port: int = _DEFAULT_IPC_PORT,
    bot_script: str | Path | None = None,
    node_executable: str = "node",
    connect_timeout: float = 30.0,
    request_timeout: float = 45.0,
    auth: str = "offline",
) -> None:
    self.host = host
    self.minecraft_port = minecraft_port
    self.username = username
    self.password = password
    self.version = version
    self.ipc_port = ipc_port
    self.bot_script = Path(bot_script) if bot_script else _DEFAULT_BOT_SCRIPT
    self.node_executable = node_executable
    self.connect_timeout = connect_timeout
    self.request_timeout = request_timeout
    self.auth = auth

    self._process: subprocess.Popen | None = None
    self._reader: asyncio.StreamReader | None = None
    self._writer: asyncio.StreamWriter | None = None
    self._pending: dict[str, asyncio.Future[Any]] = {}
    self._event_handlers: dict[str, list[Any]] = {}
    self._read_task: asyncio.Task | None = None
    self._connected = False

connect async

connect() -> None

Spawn the Node bot and wait until it signals it has joined the server.

Source code in src/simulatecraft/minecraft/connection.py
async def connect(self) -> None:
    """Spawn the Node bot and wait until it signals it has joined the server."""
    if self._connected:
        return

    if not self.bot_script.exists():
        raise FileNotFoundError(
            f"Bot script not found: {self.bot_script}\n"
            "Run: cd src/simulatecraft/minecraft/bot && npm install"
        )

    env = {**os.environ, "IPC_PORT": str(self.ipc_port)}
    cmd = [
        self.node_executable,
        str(self.bot_script),
        "--host",
        self.host,
        "--port",
        str(self.minecraft_port),
        "--username",
        self.username,
        "--ipc-port",
        str(self.ipc_port),
        "--auth",
        self.auth,
    ]
    if self.password:
        cmd += ["--password", self.password]
    if self.version:
        cmd += ["--version", self.version]

    log.info("Spawning bot process: %s", " ".join(cmd))
    self._process = subprocess.Popen(  # noqa: S603
        cmd,
        env=env,
        cwd=str(self.bot_script.parent),
        stdout=None,
        stderr=None,
    )

    await self._tcp_connect()

    spawned: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()

    def _on_spawned(data: dict[str, Any]) -> None:
        if not spawned.done():
            spawned.set_result(data)

    def _on_failed(data: dict[str, Any]) -> None:
        if spawned.done():
            return
        reason = data.get("message") or data.get("reason") or "disconnected"
        spawned.set_exception(BridgeError(self._minecraft_connect_hint(str(reason))))

    self.on_event("bot.spawned", _on_spawned)
    self.on_event("bot.error", _on_failed)
    self.on_event("bot.disconnected", _on_failed)
    try:
        await asyncio.wait_for(spawned, timeout=self.connect_timeout)
    except TimeoutError as exc:
        await self.close()
        raise BridgeError(
            f"Bot did not spawn within {self.connect_timeout}s. "
            f"Is a Minecraft Java server running at {self.host}:{self.minecraft_port}?"
        ) from exc
    except BridgeError:
        await self.close()
        raise

    self._connected = True
    log.info("Bot '%s' spawned in Minecraft.", self.username)

close async

close() -> None

Gracefully shut down the bot and the subprocess.

Source code in src/simulatecraft/minecraft/connection.py
async def close(self) -> None:
    """Gracefully shut down the bot and the subprocess."""
    self._connected = False
    if self._read_task and not self._read_task.done():
        self._read_task.cancel()
    if self._writer:
        try:
            self._writer.close()
            await self._writer.wait_closed()
        except Exception:
            pass
    if self._process and self._process.poll() is None:
        self._process.terminate()
        try:
            self._process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            self._process.kill()
    # Fail any pending requests
    for fut in self._pending.values():
        if not fut.done():
            fut.set_exception(BridgeError("bridge closed"))
    self._pending.clear()

call async

call(method: str, **params: Any) -> Any

Send an RPC request and await its response.

Source code in src/simulatecraft/minecraft/connection.py
async def call(self, method: str, **params: Any) -> Any:
    """Send an RPC request and await its response."""
    if self._writer is None:
        raise BridgeError("Bridge not connected. Call connect() first.")
    req_id = str(uuid.uuid4())
    loop = asyncio.get_event_loop()
    future: asyncio.Future[Any] = loop.create_future()
    self._pending[req_id] = future
    msg = json.dumps({"id": req_id, "method": method, "params": params}) + "\n"
    self._writer.write(msg.encode())
    await self._writer.drain()
    try:
        return await asyncio.wait_for(future, timeout=self.request_timeout)
    except TimeoutError as exc:
        self._pending.pop(req_id, None)
        raise BridgeError(f"RPC '{method}' timed out after {self.request_timeout}s") from exc

get_state async

get_state() -> dict[str, Any]

Return a full world-state snapshot from the bot.

Source code in src/simulatecraft/minecraft/connection.py
async def get_state(self) -> dict[str, Any]:
    """Return a full world-state snapshot from the bot."""
    return await self.call("get_state")

perform_action async

perform_action(action: dict[str, Any]) -> dict[str, Any]

Execute one action dict (matches Action.model_dump()) on the bot.

Source code in src/simulatecraft/minecraft/connection.py
async def perform_action(self, action: dict[str, Any]) -> dict[str, Any]:
    """Execute one action dict (matches Action.model_dump()) on the bot."""
    return await self.call("perform_action", action=action)

configure_presence async

configure_presence(*, x: float | None = None, y: float | None = None, z: float | None = None, gamemode: str | None = None) -> dict[str, Any]

Teleport after spawn via chat (RCON is preferred for reliability).

Source code in src/simulatecraft/minecraft/connection.py
async def configure_presence(
    self,
    *,
    x: float | None = None,
    y: float | None = None,
    z: float | None = None,
    gamemode: str | None = None,
) -> dict[str, Any]:
    """Teleport after spawn via chat (RCON is preferred for reliability)."""
    _ = gamemode  # agents never set gamemode; kept for call-site compatibility
    return await self.call(
        "configure_presence",
        x=x,
        y=y,
        z=z,
    )