Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,61 @@ The CLI automatically:
- Sets up the correct environment variables
- Gracefully shuts down on Ctrl+C

## Native Platform (Linux Only)
## Platform-Specific Details

### Windows Platform

The `windows` platform launches Balatro via Steam on Windows. The CLI auto-detects the Steam installation paths:

**Auto-Detected Paths:**

- `BALATROBOT_LOVE_PATH`: `C:\Program Files (x86)\Steam\steamapps\common\Balatro\Balatro.exe`
- `BALATROBOT_LOVELY_PATH`: `C:\Program Files (x86)\Steam\steamapps\common\Balatro\version.dll`

**Requirements:**

- Balatro installed via Steam
- [Lovely Injector](https://github.com/ethangreen-dev/lovely-injector) `version.dll` placed in the Balatro game directory
- Mods directory: `%AppData%\Balatro\Mods`

**Launch:**

```powershell
# Auto-detects paths
balatrobot --fast

# Or specify custom paths
balatrobot --love-path "C:\Custom\Path\Balatro.exe" --lovely-path "C:\Custom\Path\version.dll"
```

### macOS Platform

The `darwin` platform launches Balatro via Steam on macOS. The CLI auto-detects the Steam installation paths:

**Auto-Detected Paths:**

- `BALATROBOT_LOVE_PATH`: `~/Library/Application Support/Steam/steamapps/common/Balatro/Balatro.app/Contents/MacOS/love`
- `BALATROBOT_LOVELY_PATH`: `~/Library/Application Support/Steam/steamapps/common/Balatro/liblovely.dylib`

**Requirements:**

- Balatro installed via Steam
- [Lovely Injector](https://github.com/ethangreen-dev/lovely-injector) `liblovely.dylib` and `run_lovely_macos.sh` in the Balatro game directory
- Mods directory: `~/Library/Application Support/Balatro/Mods`

**Note:** You cannot run the game through Steam on macOS due to a Steam client bug. The CLI handles this by directly executing the LOVE runtime with proper environment variables.

**Launch:**

```bash
# Auto-detects paths
balatrobot --fast

# Or specify custom paths
balatrobot --love-path "/path/to/love" --lovely-path "/path/to/liblovely.dylib"
```

### Native Platform (Linux Only)

The `native` platform runs Balatro from source code using the LÖVE framework installed via package manager. This requires specific directory structure:

Expand Down
25 changes: 21 additions & 4 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

Guide for contributing to BalatroBot development.

!!! warning "Help Needed: Windows & Linux (Proton) Support"
!!! warning "Help Needed: Linux (Proton) Support"

We currently lack CLI support for **Windows** and **Linux (Proton)**. Contributions to implement these platforms are highly welcome!
We currently lack CLI support for **Linux (Proton)**. Contributions to implement this platform are highly welcome!

Please refer to the existing implementations for guidance:

- **macOS:** `src/balatrobot/platforms/macos.py`
- **Windows:** `src/balatrobot/platforms/windows.py`
- **Linux (Native):** `src/balatrobot/platforms/native.py`

## Prerequisites
Expand Down Expand Up @@ -49,7 +50,23 @@ ln -s "$(pwd)" ~/.local/share/Steam/steamapps/compatdata/2379780/pfx/drive_c/use
New-Item -ItemType SymbolicLink -Path "$env:APPDATA\Balatro\Mods\balatrobot" -Target (Get-Location)
```

### 3. Launch Balatro
### 3. Activate Virtual Environment

Activate the virtual environment to use the `balatrobot` command:

**macOS/Linux:**

```bash
source .venv/bin/activate
```

**Windows (PowerShell):**

```powershell
.venv\Scripts\Activate.ps1
```

### 4. Launch Balatro

Start with debug and fast mode for development:

Expand All @@ -59,7 +76,7 @@ balatrobot --debug --fast

For detailed CLI options, see the [CLI Reference](cli.md).

### 4. Running Tests
### 5. Running Tests

Tests use Python + pytest to communicate with the Lua API.

Expand Down
4 changes: 3 additions & 1 deletion src/balatrobot/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def get_launcher(platform: str | None = None) -> "BaseLauncher":
case "linux":
raise NotImplementedError("Linux launcher not yet implemented")
case "windows":
raise NotImplementedError("Windows launcher not yet implemented")
from balatrobot.platforms.windows import WindowsLauncher

return WindowsLauncher()
case "native":
from balatrobot.platforms.native import NativeLauncher

Expand Down
41 changes: 41 additions & 0 deletions src/balatrobot/platforms/windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Windows platform launcher."""

import os
from pathlib import Path

from balatrobot.config import Config
from balatrobot.platforms.base import BaseLauncher


class WindowsLauncher(BaseLauncher):
"""Windows-specific Balatro launcher via Steam."""

def validate_paths(self, config: Config) -> None:
"""Validate paths, apply Windows defaults if None."""
if config.love_path is None:
config.love_path = (
r"C:\Program Files (x86)\Steam\steamapps\common\Balatro\Balatro.exe"
)
if config.lovely_path is None:
config.lovely_path = (
r"C:\Program Files (x86)\Steam\steamapps\common\Balatro\version.dll"
)

love = Path(config.love_path)
lovely = Path(config.lovely_path)

if not love.exists():
raise RuntimeError(f"Balatro executable not found: {love}")
if not lovely.exists():
raise RuntimeError(f"version.dll not found: {lovely}")

def build_env(self, config: Config) -> dict[str, str]:
"""Build environment."""
env = os.environ.copy()
env.update(config.to_env())
return env

def build_cmd(self, config: Config) -> list[str]:
"""Build Windows launch command."""
assert config.love_path is not None
return [config.love_path]
57 changes: 53 additions & 4 deletions tests/cli/test_platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
from balatrobot.platforms import VALID_PLATFORMS, get_launcher
from balatrobot.platforms.macos import MacOSLauncher
from balatrobot.platforms.native import NativeLauncher
from balatrobot.platforms.windows import WindowsLauncher

IS_MACOS = platform_module.system() == "Darwin"
IS_LINUX = platform_module.system() == "Linux"
IS_WINDOWS = platform_module.system() == "Windows"


class TestGetLauncher:
Expand All @@ -31,10 +33,10 @@ def test_native_returns_native_launcher(self):
launcher = get_launcher("native")
assert isinstance(launcher, NativeLauncher)

def test_windows_not_implemented(self):
"""'windows' raises NotImplementedError."""
with pytest.raises(NotImplementedError):
get_launcher("windows")
def test_windows_returns_windows_launcher(self):
"""'windows' returns WindowsLauncher."""
launcher = get_launcher("windows")
assert isinstance(launcher, WindowsLauncher)

def test_linux_not_implemented(self):
"""'linux' raises NotImplementedError."""
Expand Down Expand Up @@ -127,3 +129,50 @@ def test_build_cmd(self, tmp_path):
cmd = launcher.build_cmd(config)

assert cmd == ["/usr/bin/love", "/path/to/balatro"]


@pytest.mark.skipif(not IS_WINDOWS, reason="Windows only")
class TestWindowsLauncher:
"""Tests for WindowsLauncher (Windows only)."""

def test_validate_paths_missing_balatro_exe(self, tmp_path):
"""Raises RuntimeError when Balatro.exe missing."""
launcher = WindowsLauncher()
config = Config(love_path=str(tmp_path / "nonexistent.exe"))

with pytest.raises(RuntimeError, match="Balatro executable not found"):
launcher.validate_paths(config)

def test_validate_paths_missing_version_dll(self, tmp_path):
"""Raises RuntimeError when version.dll missing."""
# Create a fake Balatro.exe
exe_path = tmp_path / "Balatro.exe"
exe_path.touch()

launcher = WindowsLauncher()
config = Config(
love_path=str(exe_path),
lovely_path=str(tmp_path / "nonexistent.dll"),
)

with pytest.raises(RuntimeError, match="version.dll not found"):
launcher.validate_paths(config)

def test_build_env_no_dll_injection_var(self, tmp_path):
"""build_env does not include DLL injection environment variable."""
launcher = WindowsLauncher()
config = Config(lovely_path=r"C:\path\to\version.dll")

env = launcher.build_env(config)

assert "DYLD_INSERT_LIBRARIES" not in env
assert "LD_PRELOAD" not in env

def test_build_cmd(self, tmp_path):
"""build_cmd returns Balatro.exe path."""
launcher = WindowsLauncher()
config = Config(love_path=r"C:\path\to\Balatro.exe")

cmd = launcher.build_cmd(config)

assert cmd == [r"C:\path\to\Balatro.exe"]