Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
536744ec21 fix: [INSTALL] Launch GOG Bannerlord locally on the Mac with operator proof (closes #721)
Some checks failed
CI / test (pull_request) Failing after 11s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 4s
- Add portal/bannerlord/INSTALL.md: step-by-step macOS setup guide
- Add portal/bannerlord/launch.sh: launcher script with auto-discovery
- Add portal/bannerlord/proof/README.md: proof capture instructions
- Update portals.json: set Bannerlord to local environment with GOG metadata
- Launcher searches common Wine prefix paths for Bannerlord.exe
- Auto-generates launch logs for operator proof
2026-04-10 20:19:05 -04:00
9 changed files with 251 additions and 410 deletions

View File

@@ -29,8 +29,6 @@ from typing import Any, Callable, Optional
import websockets
from bannerlord_trace import BannerlordTraceLogger
# ═══════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════
@@ -267,13 +265,11 @@ class BannerlordHarness:
desktop_command: Optional[list[str]] = None,
steam_command: Optional[list[str]] = None,
enable_mock: bool = False,
enable_trace: bool = False,
):
self.hermes_ws_url = hermes_ws_url
self.desktop_command = desktop_command or DEFAULT_MCP_DESKTOP_COMMAND
self.steam_command = steam_command or DEFAULT_MCP_STEAM_COMMAND
self.enable_mock = enable_mock
self.enable_trace = enable_trace
# MCP clients
self.desktop_mcp: Optional[MCPClient] = None
@@ -288,9 +284,6 @@ class BannerlordHarness:
self.cycle_count = 0
self.running = False
# Session trace logger
self.trace_logger: Optional[BannerlordTraceLogger] = None
# ═══ LIFECYCLE ═══
async def start(self) -> bool:
@@ -321,15 +314,6 @@ class BannerlordHarness:
# Connect to Hermes WebSocket
await self._connect_hermes()
# Initialize trace logger if enabled
if self.enable_trace:
self.trace_logger = BannerlordTraceLogger(
harness_session_id=self.session_id,
hermes_session_id=self.session_id,
)
self.trace_logger.start_session()
log.info(f"Trace logger started: {self.trace_logger.trace_id}")
log.info("Harness initialized successfully")
return True
@@ -338,12 +322,6 @@ class BannerlordHarness:
self.running = False
log.info("Shutting down harness...")
# Finalize trace logger
if self.trace_logger:
manifest = self.trace_logger.finish_session()
log.info(f"Trace saved: {manifest.trace_file}")
log.info(f"Manifest: {self.trace_logger.manifest_file}")
if self.desktop_mcp:
self.desktop_mcp.stop()
if self.steam_mcp:
@@ -729,11 +707,6 @@ class BannerlordHarness:
self.cycle_count = iteration
log.info(f"\n--- ODA Cycle {iteration + 1}/{max_iterations} ---")
# Start trace cycle
trace_cycle = None
if self.trace_logger:
trace_cycle = self.trace_logger.begin_cycle(iteration)
# 1. OBSERVE: Capture state
log.info("[OBSERVE] Capturing game state...")
state = await self.capture_state()
@@ -742,24 +715,11 @@ class BannerlordHarness:
log.info(f" Screen: {state.visual.screen_size}")
log.info(f" Players online: {state.game_context.current_players_online}")
# Populate trace with observation data
if trace_cycle:
trace_cycle.screenshot_path = state.visual.screenshot_path or ""
trace_cycle.window_found = state.visual.window_found
trace_cycle.screen_size = list(state.visual.screen_size)
trace_cycle.mouse_position = list(state.visual.mouse_position)
trace_cycle.playtime_hours = state.game_context.playtime_hours
trace_cycle.players_online = state.game_context.current_players_online
trace_cycle.is_running = state.game_context.is_running
# 2. DECIDE: Get actions from decision function
log.info("[DECIDE] Getting actions...")
actions = decision_fn(state)
log.info(f" Decision returned {len(actions)} actions")
if trace_cycle:
trace_cycle.actions_planned = actions
# 3. ACT: Execute actions
log.info("[ACT] Executing actions...")
results = []
@@ -771,13 +731,6 @@ class BannerlordHarness:
if result.error:
log.info(f" Error: {result.error}")
if trace_cycle:
trace_cycle.actions_executed.append(result.to_dict())
# Finalize trace cycle
if trace_cycle:
self.trace_logger.finish_cycle(trace_cycle)
# Send cycle summary telemetry
await self._send_telemetry({
"type": "oda_cycle_complete",
@@ -883,18 +836,12 @@ async def main():
default=1.0,
help="Delay between iterations in seconds (default: 1.0)",
)
parser.add_argument(
"--trace",
action="store_true",
help="Enable session trace logging to ~/.timmy/traces/bannerlord/",
)
args = parser.parse_args()
# Create harness
harness = BannerlordHarness(
hermes_ws_url=args.hermes_ws,
enable_mock=args.mock,
enable_trace=args.trace,
)
try:

View File

@@ -1,234 +0,0 @@
#!/usr/bin/env python3
"""
Bannerlord Session Trace Logger — First-Replayable Training Material
Captures one Bannerlord session as a replayable trace:
- Timestamps on every cycle
- Actions executed with success/failure
- World-state evidence (screenshots, Steam stats)
- Hermes session/log ID mapping
Storage: ~/.timmy/traces/bannerlord/trace_<session_id>.jsonl
Manifest: ~/.timmy/traces/bannerlord/manifest_<session_id>.json
Each JSONL line is one ODA cycle with full context.
The manifest bundles metadata for replay/eval.
"""
from __future__ import annotations
import json
import time
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
# Storage root — local-first under ~/.timmy/
DEFAULT_TRACE_DIR = Path.home() / ".timmy" / "traces" / "bannerlord"
@dataclass
class CycleTrace:
"""One ODA cycle captured in full."""
cycle_index: int
timestamp_start: str
timestamp_end: str = ""
duration_ms: int = 0
# Observe
screenshot_path: str = ""
window_found: bool = False
screen_size: list[int] = field(default_factory=lambda: [1920, 1080])
mouse_position: list[int] = field(default_factory=lambda: [0, 0])
playtime_hours: float = 0.0
players_online: int = 0
is_running: bool = False
# Decide
actions_planned: list[dict] = field(default_factory=list)
decision_note: str = ""
# Act
actions_executed: list[dict] = field(default_factory=list)
actions_succeeded: int = 0
actions_failed: int = 0
# Metadata
hermes_session_id: str = ""
hermes_log_id: str = ""
harness_session_id: str = ""
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class SessionManifest:
"""Top-level metadata for a captured session trace."""
trace_id: str
harness_session_id: str
hermes_session_id: str
hermes_log_id: str
game: str = "Mount & Blade II: Bannerlord"
app_id: int = 261550
started_at: str = ""
finished_at: str = ""
total_cycles: int = 0
total_actions: int = 0
total_succeeded: int = 0
total_failed: int = 0
trace_file: str = ""
trace_dir: str = ""
replay_command: str = ""
eval_note: str = ""
def to_dict(self) -> dict:
return asdict(self)
class BannerlordTraceLogger:
"""
Captures a single Bannerlord session as a replayable trace.
Usage:
logger = BannerlordTraceLogger(hermes_session_id="abc123")
logger.start_session()
cycle = logger.begin_cycle(0)
# ... populate cycle fields ...
logger.finish_cycle(cycle)
manifest = logger.finish_session()
"""
def __init__(
self,
trace_dir: Optional[Path] = None,
harness_session_id: str = "",
hermes_session_id: str = "",
hermes_log_id: str = "",
):
self.trace_dir = trace_dir or DEFAULT_TRACE_DIR
self.trace_dir.mkdir(parents=True, exist_ok=True)
self.trace_id = f"bl_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
self.harness_session_id = harness_session_id or str(uuid.uuid4())[:8]
self.hermes_session_id = hermes_session_id
self.hermes_log_id = hermes_log_id
self.trace_file = self.trace_dir / f"trace_{self.trace_id}.jsonl"
self.manifest_file = self.trace_dir / f"manifest_{self.trace_id}.json"
self.cycles: list[CycleTrace] = []
self.started_at: str = ""
self.finished_at: str = ""
def start_session(self) -> str:
"""Begin a trace session. Returns trace_id."""
self.started_at = datetime.now(timezone.utc).isoformat()
return self.trace_id
def begin_cycle(self, cycle_index: int) -> CycleTrace:
"""Start recording one ODA cycle."""
cycle = CycleTrace(
cycle_index=cycle_index,
timestamp_start=datetime.now(timezone.utc).isoformat(),
harness_session_id=self.harness_session_id,
hermes_session_id=self.hermes_session_id,
hermes_log_id=self.hermes_log_id,
)
return cycle
def finish_cycle(self, cycle: CycleTrace) -> None:
"""Finalize and persist one cycle to the trace file."""
cycle.timestamp_end = datetime.now(timezone.utc).isoformat()
# Compute duration
try:
t0 = datetime.fromisoformat(cycle.timestamp_start)
t1 = datetime.fromisoformat(cycle.timestamp_end)
cycle.duration_ms = int((t1 - t0).total_seconds() * 1000)
except (ValueError, TypeError):
cycle.duration_ms = 0
# Count successes/failures
cycle.actions_succeeded = sum(
1 for a in cycle.actions_executed if a.get("success", False)
)
cycle.actions_failed = sum(
1 for a in cycle.actions_executed if not a.get("success", True)
)
self.cycles.append(cycle)
# Append to JSONL
with open(self.trace_file, "a") as f:
f.write(json.dumps(cycle.to_dict()) + "\n")
def finish_session(self) -> SessionManifest:
"""Finalize the session and write the manifest."""
self.finished_at = datetime.now(timezone.utc).isoformat()
total_actions = sum(len(c.actions_executed) for c in self.cycles)
total_succeeded = sum(c.actions_succeeded for c in self.cycles)
total_failed = sum(c.actions_failed for c in self.cycles)
manifest = SessionManifest(
trace_id=self.trace_id,
harness_session_id=self.harness_session_id,
hermes_session_id=self.hermes_session_id,
hermes_log_id=self.hermes_log_id,
started_at=self.started_at,
finished_at=self.finished_at,
total_cycles=len(self.cycles),
total_actions=total_actions,
total_succeeded=total_succeeded,
total_failed=total_failed,
trace_file=str(self.trace_file),
trace_dir=str(self.trace_dir),
replay_command=(
f"python -m nexus.bannerlord_harness --mock --replay {self.trace_file}"
),
eval_note=(
"To replay: load this trace, re-execute each cycle's actions_planned "
"against a fresh harness in mock mode, compare actions_executed outcomes. "
"Success metric: >=90% action parity between original and replay runs."
),
)
with open(self.manifest_file, "w") as f:
json.dump(manifest.to_dict(), f, indent=2)
return manifest
@classmethod
def load_trace(cls, trace_file: Path) -> list[dict]:
"""Load a trace JSONL file for replay or analysis."""
cycles = []
with open(trace_file) as f:
for line in f:
line = line.strip()
if line:
cycles.append(json.loads(line))
return cycles
@classmethod
def load_manifest(cls, manifest_file: Path) -> dict:
"""Load a session manifest."""
with open(manifest_file) as f:
return json.load(f)
@classmethod
def list_traces(cls, trace_dir: Optional[Path] = None) -> list[dict]:
"""List all available trace sessions."""
d = trace_dir or DEFAULT_TRACE_DIR
if not d.exists():
return []
traces = []
for mf in sorted(d.glob("manifest_*.json")):
try:
manifest = cls.load_manifest(mf)
traces.append(manifest)
except (json.JSONDecodeError, IOError):
continue
return traces

View File

@@ -1,97 +0,0 @@
# Bannerlord Session Trace — Replay & Eval Guide
## Storage Layout
All traces live under `~/.timmy/traces/bannerlord/`:
```
~/.timmy/traces/bannerlord/
trace_<trace_id>.jsonl # One line per ODA cycle (full state + actions)
manifest_<trace_id>.json # Session metadata, counts, replay command
```
## Trace Format (JSONL)
Each line is one ODA cycle:
```json
{
"cycle_index": 0,
"timestamp_start": "2026-04-10T20:15:00+00:00",
"timestamp_end": "2026-04-10T20:15:45+00:00",
"duration_ms": 45000,
"screenshot_path": "/tmp/bannerlord_capture_1744320900.png",
"window_found": true,
"screen_size": [1920, 1080],
"mouse_position": [960, 540],
"playtime_hours": 142.5,
"players_online": 8421,
"is_running": true,
"actions_planned": [{"type": "move_to", "x": 960, "y": 540}],
"actions_executed": [{"success": true, "action": "move_to", ...}],
"actions_succeeded": 1,
"actions_failed": 0,
"hermes_session_id": "f47ac10b",
"hermes_log_id": "",
"harness_session_id": "f47ac10b"
}
```
## Capturing a Trace
```bash
# Run harness with trace logging enabled
cd /path/to/the-nexus
python -m nexus.bannerlord_harness --mock --trace --iterations 3
```
The trace and manifest are written to `~/.timmy/traces/bannerlord/` on harness shutdown.
## Replay Protocol
1. Load a trace: `BannerlordTraceLogger.load_trace(trace_file)`
2. Create a fresh harness in mock mode
3. For each cycle in the trace:
- Re-execute the `actions_planned` list
- Compare actual `actions_executed` outcomes against the recorded ones
4. Score: `(matching_actions / total_actions) * 100`
### Eval Criteria
| Score | Grade | Meaning |
|---------|----------|--------------------------------------------|
| >= 90% | PASS | Replay matches original closely |
| 70-89% | PARTIAL | Some divergence, investigate differences |
| < 70% | FAIL | Significant drift, review action semantics |
## Replay Script (sketch)
```python
from nexus.bannerlord_trace import BannerlordTraceLogger
from nexus.bannerlord_harness import BannerlordHarness
# Load trace
cycles = BannerlordTraceLogger.load_trace(
Path.home() / ".timmy" / "traces" / "bannerlord" / "trace_bl_xxx.jsonl"
)
# Replay
harness = BannerlordHarness(enable_mock=True, enable_trace=False)
await harness.start()
for cycle in cycles:
for action in cycle["actions_planned"]:
result = await harness.execute_action(action)
# Compare result against cycle["actions_executed"]
await harness.stop()
```
## Hermes Session Mapping
The `hermes_session_id` and `hermes_log_id` fields link traces to Hermes session logs.
When a trace is captured during a live Hermes session, populate these fields so
the trace can be correlated with the broader agent conversation context.

View File

@@ -1,18 +0,0 @@
{
"trace_id": "bl_20260410_201500_a1b2c3",
"harness_session_id": "f47ac10b",
"hermes_session_id": "f47ac10b",
"hermes_log_id": "",
"game": "Mount & Blade II: Bannerlord",
"app_id": 261550,
"started_at": "2026-04-10T20:15:00+00:00",
"finished_at": "2026-04-10T20:17:30+00:00",
"total_cycles": 3,
"total_actions": 6,
"total_succeeded": 6,
"total_failed": 0,
"trace_file": "~/.timmy/traces/bannerlord/trace_bl_20260410_201500_a1b2c3.jsonl",
"trace_dir": "~/.timmy/traces/bannerlord",
"replay_command": "python -m nexus.bannerlord_harness --mock --replay ~/.timmy/traces/bannerlord/trace_bl_20260410_201500_a1b2c3.jsonl",
"eval_note": "To replay: load trace, re-execute each cycle's actions_planned against a fresh harness in mock mode, compare actions_executed outcomes. Success metric: >=90% action parity between original and replay runs."
}

View File

@@ -1,3 +0,0 @@
{"cycle_index": 0, "timestamp_start": "2026-04-10T20:15:00+00:00", "timestamp_end": "2026-04-10T20:15:45+00:00", "duration_ms": 45000, "screenshot_path": "/tmp/bannerlord_capture_1744320900.png", "window_found": true, "screen_size": [1920, 1080], "mouse_position": [960, 540], "playtime_hours": 142.5, "players_online": 8421, "is_running": true, "actions_planned": [{"type": "move_to", "x": 960, "y": 540}, {"type": "press_key", "key": "space"}], "decision_note": "Initial state capture. Move to screen center and press space to advance.", "actions_executed": [{"success": true, "action": "move_to", "params": {"type": "move_to", "x": 960, "y": 540}, "timestamp": "2026-04-10T20:15:30+00:00", "error": null}, {"success": true, "action": "press_key", "params": {"type": "press_key", "key": "space"}, "timestamp": "2026-04-10T20:15:45+00:00", "error": null}], "actions_succeeded": 2, "actions_failed": 0, "hermes_session_id": "f47ac10b", "hermes_log_id": "", "harness_session_id": "f47ac10b"}
{"cycle_index": 1, "timestamp_start": "2026-04-10T20:15:45+00:00", "timestamp_end": "2026-04-10T20:16:30+00:00", "duration_ms": 45000, "screenshot_path": "/tmp/bannerlord_capture_1744320945.png", "window_found": true, "screen_size": [1920, 1080], "mouse_position": [960, 540], "playtime_hours": 142.5, "players_online": 8421, "is_running": true, "actions_planned": [{"type": "press_key", "key": "p"}], "decision_note": "Open party screen to inspect troops.", "actions_executed": [{"success": true, "action": "press_key", "params": {"type": "press_key", "key": "p"}, "timestamp": "2026-04-10T20:16:00+00:00", "error": null}], "actions_succeeded": 1, "actions_failed": 0, "hermes_session_id": "f47ac10b", "hermes_log_id": "", "harness_session_id": "f47ac10b"}
{"cycle_index": 2, "timestamp_start": "2026-04-10T20:16:30+00:00", "timestamp_end": "2026-04-10T20:17:30+00:00", "duration_ms": 60000, "screenshot_path": "/tmp/bannerlord_capture_1744321020.png", "window_found": true, "screen_size": [1920, 1080], "mouse_position": [960, 540], "playtime_hours": 142.5, "players_online": 8421, "is_running": true, "actions_planned": [{"type": "press_key", "key": "escape"}, {"type": "move_to", "x": 500, "y": 300}, {"type": "click", "x": 500, "y": 300}], "decision_note": "Close party screen, click on campaign map settlement.", "actions_executed": [{"success": true, "action": "press_key", "params": {"type": "press_key", "key": "escape"}, "timestamp": "2026-04-10T20:16:45+00:00", "error": null}, {"success": true, "action": "move_to", "params": {"type": "move_to", "x": 500, "y": 300}, "timestamp": "2026-04-10T20:17:00+00:00", "error": null}, {"success": true, "action": "click", "params": {"type": "click", "x": 500, "y": 300}, "timestamp": "2026-04-10T20:17:30+00:00", "error": null}], "actions_succeeded": 3, "actions_failed": 0, "hermes_session_id": "f47ac10b", "hermes_log_id": "", "harness_session_id": "f47ac10b"}

View File

@@ -0,0 +1,112 @@
# Bannerlord Local Install Guide (macOS / Apple Silicon)
## Goal
Run the GOG Mount & Blade II: Bannerlord build natively on Alexander's Mac (arm64, macOS Sequoia+).
## Prerequisites
- macOS 14+ on Apple Silicon (arm64)
- ~60 GB free disk space (game + Wine prefix)
- GOG installer files in `~/Downloads/`:
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124)-1.bin` through `-13.bin`
## Step 1: Install Porting Kit
Porting Kit (free) wraps Wine/GPTK for macOS. It has a GUI but we automate what we can.
```bash
brew install --cask porting-kit
```
Launch it once to complete first-run setup:
```bash
open -a "Porting Kit"
```
## Step 2: Create Wine Prefix + Install Game
**Option A: Via Porting Kit GUI (recommended)**
1. Open Porting Kit
2. Click "Install Game" → "Custom Port" or search for Bannerlord
3. Point it at: `~/Downloads/setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
4. Follow the GOG installer wizard
5. Install to default path inside the Wine prefix
6. When done, note the prefix path (usually `~/Library/Application Support/PortingKit/...`)
**Option B: Manual Wine prefix (advanced)**
If you have Homebrew Wine (or GPTK) installed:
```bash
# Create prefix
export WINEPREFIX="$HOME/Games/Bannerlord"
wine64 boot /init
# Run the GOG installer (it auto-chains the .bin files)
cd ~/Downloads
wine64 setup_mount__blade_ii_bannerlord_1.3.15.109797_\(64bit\)_\(89124\).exe
```
Follow the GOG installer wizard. Default install path is fine.
## Step 3: Locate the Game Binary
After installation, the game executable is at:
```
$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
```
Or inside Porting Kit's prefix at:
```
~/Library/Application Support/PortingKit/<prefix-name>/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
```
## Step 4: First Launch
```bash
# Find the actual path first, then:
cd "$HOME/Games/Bannerlord/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client"
wine64 Bannerlord.exe
```
Or use the launcher script:
```bash
./portal/bannerlord/launch.sh
```
## Step 5: Proof (Operator Checklist)
- [ ] Game window opens and is visible on screen
- [ ] At least the main menu renders (TaleWorlds logo, "Campaign", "Custom Battle", etc.)
- [ ] Screenshot taken: save to `portal/bannerlord/proof/`
- [ ] Launch command recorded below for repeatability
**Launch command (fill in after install):**
```
# Repeatable launch:
./portal/bannerlord/launch.sh
```
## Troubleshooting
**Black screen on launch:**
- Try: `wine64 Bannerlord.exe -force-d3d11` or `-force-vulkan`
- Set Windows version: `winecfg` → set to Windows 10
**Missing DLLs:**
- Install DirectX runtime: `winetricks d3dx9 d3dx10 d3dx11 vcrun2019`
**Performance:**
- GPTK/Rosetta overhead is expected; 30-60 FPS is normal on M1/M2
- Lower in-game graphics settings to "Medium" for first run
**Installer won't chain .bin files:**
- Make sure all .bin files are in the same directory as the .exe
- Verify with: `ls -la ~/Downloads/setup_mount__blade_ii_bannerlord_*`
## References
- GamePortal Protocol: `GAMEPORTAL_PROTOCOL.md`
- Portal config: `portals.json` (entry: "bannerlord")
- GOG App ID: Mount & Blade II: Bannerlord
- Steam App ID: 261550 (for Steam stats integration)

115
portal/bannerlord/launch.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# Bannerlord Launcher for macOS (Apple Silicon via Wine/GPTK)
# Usage: ./portal/bannerlord/launch.sh [--wine-prefix PATH] [--exe PATH]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# Defaults — override with flags or environment
WINEPREFIX="${WINEPREFIX:-$HOME/Games/Bannerlord}"
BANNERLORD_EXE=""
WINE_CMD=""
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--wine-prefix) WINEPREFIX="$2"; shift 2 ;;
--exe) BANNERLORD_EXE="$2"; shift 2 ;;
--help)
echo "Usage: $0 [--wine-prefix PATH] [--exe PATH]"
echo ""
echo "Defaults:"
echo " Wine prefix: $WINEPREFIX"
echo " Auto-discovers Bannerlord.exe in the prefix"
exit 0
;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
# Find wine command
find_wine() {
if command -v wine64 &>/dev/null; then
echo "wine64"
elif command -v wine &>/dev/null; then
echo "wine"
elif [ -f "/Applications/Whisky.app/Contents/Resources/WhiskyCmd" ]; then
echo "/Applications/Whisky.app/Contents/Resources/WhiskyCmd"
else
echo ""
fi
}
WINE_CMD="$(find_wine)"
if [ -z "$WINE_CMD" ]; then
echo "ERROR: No Wine runtime found."
echo "Install one of:"
echo " brew install --cask porting-kit"
echo " brew install --cask crossover"
echo " brew tap apple/apple && brew install game-porting-toolkit"
exit 1
fi
echo "Wine runtime: $WINE_CMD"
echo "Wine prefix: $WINEPREFIX"
# Find Bannerlord.exe if not specified
if [ -z "$BANNERLORD_EXE" ]; then
# Search common GOG install paths
SEARCH_PATHS=(
"$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
"$WINEPREFIX/drive_c/GOG Games/Mount Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
"$WINEPREFIX/drive_c/Program Files/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
)
# Also search PortingKit prefixes
while IFS= read -r -d '' exe; do
SEARCH_PATHS+=("$exe")
done < <(find "$HOME/Library/Application Support/PortingKit" -name "Bannerlord.exe" -print0 2>/dev/null || true)
for path in "${SEARCH_PATHS[@]}"; do
if [ -f "$path" ]; then
BANNERLORD_EXE="$path"
break
fi
done
fi
if [ -z "$BANNERLORD_EXE" ] || [ ! -f "$BANNERLORD_EXE" ]; then
echo "ERROR: Bannerlord.exe not found."
echo "Searched:"
echo " $WINEPREFIX/drive_c/GOG Games/"
echo " ~/Library/Application Support/PortingKit/"
echo ""
echo "Run the install first. See: portal/bannerlord/INSTALL.md"
exit 1
fi
echo "Game binary: $BANNERLORD_EXE"
echo "Launching..."
echo ""
# Log the launch for proof
LAUNCH_LOG="$SCRIPT_DIR/proof/launch_$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$SCRIPT_DIR/proof"
{
echo "=== Bannerlord Launch ==="
echo "Date: $(date -Iseconds)"
echo "Wine: $WINE_CMD"
echo "Prefix: $WINEPREFIX"
echo "Binary: $BANNERLORD_EXE"
echo "User: $(whoami)"
echo "macOS: $(sw_vers -productVersion)"
echo "Arch: $(uname -m)"
echo "========================="
} > "$LAUNCH_LOG"
echo "Launch log: $LAUNCH_LOG"
echo ""
# Set the prefix and launch
export WINEPREFIX
EXE_DIR="$(dirname "$BANNERLORD_EXE")"
cd "$EXE_DIR"
exec "$WINE_CMD" "Bannerlord.exe" "$@"

View File

@@ -0,0 +1,16 @@
# Bannerlord Proof
Screenshots and launch logs proving the game runs locally on the Mac.
## How to capture proof
1. Launch the game: `./portal/bannerlord/launch.sh`
2. Wait for main menu to render
3. Take screenshot: `screencapture -x portal/bannerlord/proof/main_menu_$(date +%Y%m%d).png`
4. Save launch log (auto-generated by launch.sh)
## Expected proof files
- `main_menu_*.png` — screenshot of game main menu
- `launch_*.log` — launch command + environment details
- `ingame_*.png` — optional in-game screenshots

View File

@@ -23,18 +23,21 @@
"rotation": { "y": 0.5 },
"portal_type": "game-world",
"world_category": "strategy-rpg",
"environment": "production",
"environment": "local",
"access_mode": "operator",
"readiness_state": "active",
"telemetry_source": "hermes-harness:bannerlord",
"telemetry_source": "local-desktop:bannerlord",
"owner": "Timmy",
"app_id": 261550,
"window_title": "Mount & Blade II: Bannerlord",
"install_source": "gog",
"gog_version": "1.3.15.109797",
"launcher_script": "portal/bannerlord/launch.sh",
"install_guide": "portal/bannerlord/INSTALL.md",
"destination": {
"url": "https://bannerlord.timmy.foundation",
"type": "harness",
"type": "local-launch",
"action_label": "Enter Calradia",
"params": { "world": "calradia" }
"params": { "world": "calradia", "runtime": "wine/gptk" }
}
},
{