Compare commits
1 Commits
mimo/creat
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73bc86d3a2 |
151
docs/BANNERLORD_LOCAL_MAC.md
Normal file
151
docs/BANNERLORD_LOCAL_MAC.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Bannerlord Local Mac Setup
|
||||||
|
|
||||||
|
> **Status:** READY FOR TESTING
|
||||||
|
> **Platform:** macOS (Apple Silicon / Intel)
|
||||||
|
> **Source:** GOG (not Steam)
|
||||||
|
> **Last Updated:** 2026-04-10
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Bannerlord is a Windows game. Alexander has it from GOG on macOS.
|
||||||
|
We need it running locally through emulation before the harness can observe it.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LOCAL BANNERLORD ON MAC │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ Bannerlord │ │ Emulator │ │ macOS Desktop │ │
|
||||||
|
│ │ (GOG) │───►│ Wine/Whisky/ │───►│ (the screen) │ │
|
||||||
|
│ │ │ │ CrossOver │ │ │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Bannerlord Harness │ │
|
||||||
|
│ │ ┌────────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||||
|
│ │ │ capture_ │ │ execute_ │ │ bannerlord│ │ │
|
||||||
|
│ │ │ state() │ │ action() │ │ _local.py │ │ │
|
||||||
|
│ │ └────────────┘ └───────────┘ └───────────┘ │ │
|
||||||
|
│ │ │ ▲ │ │ │
|
||||||
|
│ │ ▼ │ ▼ │ │
|
||||||
|
│ │ ┌─────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ MCP Servers (desktop-control) │ │ │
|
||||||
|
│ │ │ Screenshots + keyboard/mouse │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Hermes WebSocket │ │
|
||||||
|
│ │ Telemetry + ODA loop │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `scripts/bannerlord_launcher.sh` | Shell launcher — detects emulator + game, launches |
|
||||||
|
| `nexus/bannerlord_local.py` | Python module — programmatic readiness + launch control |
|
||||||
|
| `nexus/bannerlord_harness.py` | Existing harness — extended with `--local` and `--launch-local` |
|
||||||
|
| `portals.json` | Portal metadata — updated with `local_launch` block |
|
||||||
|
|
||||||
|
## Emulator Priority
|
||||||
|
|
||||||
|
1. **Whisky** — `/Applications/Whisky.app` (preferred, best macOS integration)
|
||||||
|
2. **CrossOver** — `/Applications/CrossOver.app` (good, paid)
|
||||||
|
3. **Homebrew Wine** — `wine64` / `wine` on PATH (free, may need Rosetta on ARM)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Check Readiness
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Shell
|
||||||
|
./scripts/bannerlord_launcher.sh --check --verbose
|
||||||
|
|
||||||
|
# Python
|
||||||
|
python3 -m nexus.bannerlord_local --check --json
|
||||||
|
|
||||||
|
# Through harness
|
||||||
|
python3 -m nexus.bannerlord_harness --local --mock
|
||||||
|
```
|
||||||
|
|
||||||
|
### Launch Game
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Shell
|
||||||
|
./scripts/bannerlord_launcher.sh --launch
|
||||||
|
|
||||||
|
# Python
|
||||||
|
python3 -m nexus.bannerlord_local --launch --json
|
||||||
|
|
||||||
|
# Through harness (launches game, then runs ODA)
|
||||||
|
python3 -m nexus.bannerlord_harness --launch-local --mock
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop Game
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m nexus.bannerlord_local --stop
|
||||||
|
```
|
||||||
|
|
||||||
|
## GOG Install Paths Searched
|
||||||
|
|
||||||
|
The launcher checks these paths in order:
|
||||||
|
|
||||||
|
1. `/Applications/Games/Mount & Blade II Bannerlord`
|
||||||
|
2. `~/GOG Games/Mount and Blade II Bannerlord`
|
||||||
|
3. `~/Games/Mount & Blade II Bannerlord`
|
||||||
|
4. `/Applications/Mount & Blade II Bannerlord`
|
||||||
|
5. `~/Library/Application Support/GOG.com/Galaxy/Applications/*/`
|
||||||
|
6. Recursive `find` as last resort
|
||||||
|
|
||||||
|
The game must have `bin/Generic/Bannerlord.exe` relative to the install root.
|
||||||
|
|
||||||
|
## Portal Metadata
|
||||||
|
|
||||||
|
The `portals.json` bannerlord entry now includes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"environment": "local",
|
||||||
|
"local_launch": {
|
||||||
|
"platform": "macos",
|
||||||
|
"source": "gog",
|
||||||
|
"emulator_required": true,
|
||||||
|
"emulator_options": ["whisky", "crossover", "wine"],
|
||||||
|
"launcher": "scripts/bannerlord_launcher.sh",
|
||||||
|
"harness_bridge": "nexus/bannerlord_local.py",
|
||||||
|
"check_command": "python3 -m nexus.bannerlord_local --check --json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Honest Status
|
||||||
|
|
||||||
|
| Component | Status |
|
||||||
|
|-----------|--------|
|
||||||
|
| Launcher script | Written, needs Mac testing |
|
||||||
|
| Python local module | Written, needs Mac testing |
|
||||||
|
| Harness integration | Added `--local`/`--launch-local` flags |
|
||||||
|
| Portal metadata | Updated |
|
||||||
|
| MCP observation of emulated window | Untested — depends on emulator window visibility |
|
||||||
|
| ODA loop with emulated game | Untested — needs game actually running |
|
||||||
|
|
||||||
|
## What Could Go Wrong
|
||||||
|
|
||||||
|
- **Emulator not installed:** User must install Whisky, CrossOver, or wine
|
||||||
|
- **Game not found:** User must install GOG Bannerlord to a known path
|
||||||
|
- **Performance:** Wine on Apple Silicon requires Rosetta + possible DXVK setup
|
||||||
|
- **Window title:** The emulated window may not match "Mount & Blade II: Bannerlord" — the harness may need to detect the actual window title
|
||||||
|
- **MCP desktop-control on macOS:** pyautogui on macOS needs Accessibility permissions
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Alexander runs `./scripts/bannerlord_launcher.sh --check --verbose` on his Mac
|
||||||
|
2. If missing emulator, install Whisky (`brew install --cask whisky`)
|
||||||
|
3. If missing game, install GOG Bannerlord
|
||||||
|
4. Run `--launch` to verify the game opens
|
||||||
|
5. Run `--launch-local --mock` to verify harness integration
|
||||||
|
6. Test MCP screenshots of the emulated window
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Media Production — Veo/Flow Prototypes
|
|
||||||
|
|
||||||
Issue #681: [MEDIA] Veo/Flow flythrough prototypes for The Nexus and Timmy.
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
|
|
||||||
- `veo-storyboard.md` — Full storyboard for 5 clips with shot sequences, prompts, and design focus areas
|
|
||||||
- `clip-metadata.json` — Durable metadata for each clip (prompts, model, outputs, insights)
|
|
||||||
|
|
||||||
## Clips Overview
|
|
||||||
|
|
||||||
| ID | Title | Audience | Purpose |
|
|
||||||
|----|-------|----------|---------|
|
|
||||||
| clip-001 | First Light | PUBLIC | The Nexus reveal teaser |
|
|
||||||
| clip-002 | Between Worlds | INTERNAL | Portal activation UX study |
|
|
||||||
| clip-003 | The Guardian's View | PUBLIC | Timmy's presence promo |
|
|
||||||
| clip-004 | The Void Between | INTERNAL | Ambient environment study |
|
|
||||||
| clip-005 | Command Center | INTERNAL | Terminal UI readability |
|
|
||||||
|
|
||||||
## How to Generate
|
|
||||||
|
|
||||||
### Via Flow (labs.google/flow)
|
|
||||||
1. Open `veo-storyboard.md`, copy the prompt for your clip
|
|
||||||
2. Go to labs.google/flow
|
|
||||||
3. Paste the prompt, select Veo 3.1
|
|
||||||
4. Generate (8-second clips)
|
|
||||||
5. Download output, update `clip-metadata.json` with output path and findings
|
|
||||||
|
|
||||||
### Via Gemini App
|
|
||||||
1. Type "generate a video of [prompt text]" in Gemini
|
|
||||||
2. Uses Veo 3.1 Fast (slightly lower quality, faster)
|
|
||||||
3. Good for quick iteration on prompts
|
|
||||||
|
|
||||||
### Via API (programmatic)
|
|
||||||
```python
|
|
||||||
from google import genai
|
|
||||||
client = genai.Client()
|
|
||||||
|
|
||||||
# See: ai.google.dev/gemini-api/docs/video
|
|
||||||
response = client.models.generate_content(
|
|
||||||
model="veo-3.1",
|
|
||||||
contents="[prompt from storyboard]"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## After Generation
|
|
||||||
|
|
||||||
For each clip:
|
|
||||||
1. Save output file to `outputs/clip-XXX.mp4`
|
|
||||||
2. Update `clip-metadata.json`:
|
|
||||||
- Add output file path to `output_files[]`
|
|
||||||
- Fill in `design_insights.findings` with observations
|
|
||||||
- Add `threejs_changes_suggested` if the clip reveals needed changes
|
|
||||||
3. Share internal clips with the team for design review
|
|
||||||
4. Use public clips in README, social media, project communication
|
|
||||||
|
|
||||||
## Design Insight Workflow
|
|
||||||
|
|
||||||
Each clip has specific questions it's designed to answer:
|
|
||||||
|
|
||||||
**clip-001 (First Light)**
|
|
||||||
- Scale perception: platform vs. portals vs. terminal
|
|
||||||
- Color hierarchy: teal primary, purple secondary, gold accent
|
|
||||||
- Camera movement: cinematic or disorienting?
|
|
||||||
|
|
||||||
**clip-002 (Between Worlds)**
|
|
||||||
- Activation distance: when does interaction become available?
|
|
||||||
- Transition feel: travel or teleportation?
|
|
||||||
- Overlay readability against portal glow
|
|
||||||
|
|
||||||
**clip-003 (The Guardian's View)**
|
|
||||||
- Agent presence: alive or decorative?
|
|
||||||
- Crystal hologram readability
|
|
||||||
- Wide shot: world or tech demo?
|
|
||||||
|
|
||||||
**clip-004 (The Void Between)**
|
|
||||||
- Void atmosphere: alive or empty?
|
|
||||||
- Particle systems: enhance or distract?
|
|
||||||
- Lighting hierarchy clarity
|
|
||||||
|
|
||||||
**clip-005 (Command Center)**
|
|
||||||
- Text readability at 1080p
|
|
||||||
- Color-coded panel hierarchy
|
|
||||||
- Scan-line effect: retro or futuristic?
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
|
|
||||||
- 8-second clips max (Veo/Flow limitation)
|
|
||||||
- Queued generation (not instant)
|
|
||||||
- Content policies apply
|
|
||||||
- Ultra tier gets highest rate limits
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
{
|
|
||||||
"clips": [
|
|
||||||
{
|
|
||||||
"id": "clip-001",
|
|
||||||
"title": "First Light — The Nexus Reveal",
|
|
||||||
"purpose": "Public-facing teaser. Establishes the Nexus as a place worth visiting.",
|
|
||||||
"audience": "public",
|
|
||||||
"priority": "HIGH",
|
|
||||||
"duration_seconds": 8,
|
|
||||||
"shots": [
|
|
||||||
{
|
|
||||||
"shot": 1,
|
|
||||||
"timeframe": "0-2s",
|
|
||||||
"description": "Void Approach — camera drifts through nebula, hexagonal glow appears",
|
|
||||||
"design_focus": "isolation before connection"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 2,
|
|
||||||
"timeframe": "2-4s",
|
|
||||||
"description": "Platform Reveal — camera descends to hexagonal platform, grid pulses",
|
|
||||||
"design_focus": "structure emerges from chaos"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 3,
|
|
||||||
"timeframe": "4-6s",
|
|
||||||
"description": "Portal Array — sweep low showing multiple colored portals",
|
|
||||||
"design_focus": "infinite worlds, one home"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 4,
|
|
||||||
"timeframe": "6-8s",
|
|
||||||
"description": "Timmy's Terminal — rise to batcave terminal, holographic panels",
|
|
||||||
"design_focus": "someone is home"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prompt": "Cinematic flythrough of a futuristic digital nexus hub. Start in deep space with a dark purple nebula, stars twinkling. Camera descends toward a glowing hexagonal platform with pulsing teal grid lines and a luminous ring border. Sweep low across the platform revealing multiple glowing portal archways in orange, teal, gold, and blue — each with flickering holographic labels. Rise toward a central command terminal with holographic data panels showing scrolling status text. Camera pushes into a teal light flare. Cyberpunk aesthetic, volumetric lighting, 8-second sequence, smooth camera movement, concept art quality.",
|
|
||||||
"prompt_variants": [],
|
|
||||||
"model_tool": "veo-3.1",
|
|
||||||
"access_point": "flow",
|
|
||||||
"output_files": [],
|
|
||||||
"design_insights": {
|
|
||||||
"questions": [
|
|
||||||
"Does the scale feel right? (platform vs. portals vs. terminal)",
|
|
||||||
"Does the color hierarchy work? (teal primary, purple secondary, gold accent)",
|
|
||||||
"Is the camera movement cinematic or disorienting?"
|
|
||||||
],
|
|
||||||
"findings": null,
|
|
||||||
"threejs_changes_suggested": []
|
|
||||||
},
|
|
||||||
"status": "pending",
|
|
||||||
"created_at": "2026-04-10T20:15:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "clip-002",
|
|
||||||
"title": "Between Worlds — Portal Activation",
|
|
||||||
"purpose": "Internal design reference. Tests portal activation sequence and spatial relationships.",
|
|
||||||
"audience": "internal",
|
|
||||||
"priority": "HIGH",
|
|
||||||
"duration_seconds": 8,
|
|
||||||
"shots": [
|
|
||||||
{
|
|
||||||
"shot": 1,
|
|
||||||
"timeframe": "0-2.5s",
|
|
||||||
"description": "Approach — first-person walk toward Morrowind portal (orange, x:15, z:-10)",
|
|
||||||
"design_focus": "proximity feel, portal scale relative to player"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 2,
|
|
||||||
"timeframe": "2.5-5.5s",
|
|
||||||
"description": "Activation — portal brightens, energy vortex, particles accelerate, overlay text",
|
|
||||||
"design_focus": "activation UX, visual feedback timing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 3,
|
|
||||||
"timeframe": "5.5-8s",
|
|
||||||
"description": "Stepping Through — camera pushes in, world dissolves, flash, 'VVARDENFELL' text",
|
|
||||||
"design_focus": "transition smoothness, immersion break points"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prompt": "First-person perspective walking toward a glowing orange portal archway in a futuristic digital space. The portal ring has inner energy glow with rising particle effects. A holographic label \"MORROWIND\" flickers above. Camera stops, portal interior brightens into an energy vortex, particles accelerate inward. Camera pushes forward into the portal, world dissolves into an orange energy tunnel, flash to black with text \"VVARDENFELL\". Dark ambient environment with teal grid floor. Cyberpunk aesthetic, volumetric effects, smooth camera movement.",
|
|
||||||
"prompt_variants": [],
|
|
||||||
"model_tool": "veo-3.1",
|
|
||||||
"access_point": "flow",
|
|
||||||
"output_files": [],
|
|
||||||
"design_insights": {
|
|
||||||
"questions": [
|
|
||||||
"Is the activation distance clear? (when does interaction become available?)",
|
|
||||||
"Does the transition feel like travel or teleportation?",
|
|
||||||
"Is the overlay text readable against the portal glow?"
|
|
||||||
],
|
|
||||||
"findings": null,
|
|
||||||
"threejs_changes_suggested": []
|
|
||||||
},
|
|
||||||
"status": "pending",
|
|
||||||
"created_at": "2026-04-10T20:15:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "clip-003",
|
|
||||||
"title": "The Guardian's View — Timmy's Perspective",
|
|
||||||
"purpose": "Public-facing. Establishes Timmy as the guardian/presence of the Nexus.",
|
|
||||||
"audience": "public",
|
|
||||||
"priority": "MEDIUM",
|
|
||||||
"duration_seconds": 8,
|
|
||||||
"shots": [
|
|
||||||
{
|
|
||||||
"shot": 1,
|
|
||||||
"timeframe": "0-2s",
|
|
||||||
"description": "Agent Presence — floating glowing orb with trailing particles",
|
|
||||||
"design_focus": "consciousness without body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 2,
|
|
||||||
"timeframe": "2-4s",
|
|
||||||
"description": "Vision Crystal — rotating octahedron with holographic 'SOVEREIGNTY' text",
|
|
||||||
"design_focus": "values inscribed in space"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 3,
|
|
||||||
"timeframe": "4-6s",
|
|
||||||
"description": "Harness Pulse — thought stream ribbon, agent orbs drifting",
|
|
||||||
"design_focus": "the system breathes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 4,
|
|
||||||
"timeframe": "6-8s",
|
|
||||||
"description": "Wide View — full Nexus visible, text overlay 'THE NEXUS — Timmy's Sovereign Home'",
|
|
||||||
"design_focus": "this is a world, not a page"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prompt": "Cinematic sequence in a futuristic digital nexus. Start with eye-level view of a floating glowing orb (teal-gold light, trailing particles) pulsing gently — an AI agent presence. Shift to a rotating octahedron crystal refracting light, with holographic text \"SOVEREIGNTY — No masters, no chains\" and a ring of light pulsing beneath. Pull back to reveal flowing ribbons of light (thought streams) crossing a hexagonal platform, with agent orbs drifting. Rise to high orbit showing the full nexus: hexagonal platform, multiple colored portal archways, central command terminal, floating crystals, all framed by a dark purple nebula skybox. End with text overlay \"THE NEXUS — Timmy's Sovereign Home\". Cyberpunk aesthetic, volumetric lighting, contemplative pacing.",
|
|
||||||
"prompt_variants": [],
|
|
||||||
"model_tool": "veo-3.1",
|
|
||||||
"access_point": "flow",
|
|
||||||
"output_files": [],
|
|
||||||
"design_insights": {
|
|
||||||
"questions": [
|
|
||||||
"Do agent presences read as 'alive' or decorative?",
|
|
||||||
"Is the crystal-to-text hologram readable?",
|
|
||||||
"Does the wide shot communicate 'world' or 'tech demo'?"
|
|
||||||
],
|
|
||||||
"findings": null,
|
|
||||||
"threejs_changes_suggested": []
|
|
||||||
},
|
|
||||||
"status": "pending",
|
|
||||||
"created_at": "2026-04-10T20:15:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "clip-004",
|
|
||||||
"title": "The Void Between — Ambient Environment Study",
|
|
||||||
"purpose": "Internal design reference. Tests ambient environment systems: particles, dust, lighting, skybox.",
|
|
||||||
"audience": "internal",
|
|
||||||
"priority": "MEDIUM",
|
|
||||||
"duration_seconds": 8,
|
|
||||||
"shots": [
|
|
||||||
{
|
|
||||||
"shot": 1,
|
|
||||||
"timeframe": "0-4s",
|
|
||||||
"description": "Particle Systems — static camera, view from platform edge into void, particles visible",
|
|
||||||
"design_focus": "does the void feel alive or empty?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 2,
|
|
||||||
"timeframe": "4-8s",
|
|
||||||
"description": "Lighting Study — slow orbit showing teal/purple point lights on grid floor",
|
|
||||||
"design_focus": "lighting hierarchy, mood consistency"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prompt": "Ambient environment study in a futuristic digital void. Static camera with slight drift, viewing from the edge of a hexagonal platform into deep space. Dark purple nebula with twinkling distant stars, subtle color shifts. Floating particles and dust drift slowly. No structures, no portals — pure atmosphere. Then camera slowly orbits showing teal and purple point lights casting volumetric glow on a dark hexagonal grid floor. Ambient lighting fills shadows. Contemplative, moody, atmospheric. Cyberpunk aesthetic, minimal movement, focus on light and particle behavior.",
|
|
||||||
"prompt_variants": [],
|
|
||||||
"model_tool": "veo-3.1",
|
|
||||||
"access_point": "flow",
|
|
||||||
"output_files": [],
|
|
||||||
"design_insights": {
|
|
||||||
"questions": [
|
|
||||||
"Is the void atmospheric or just dark?",
|
|
||||||
"Do the particle systems enhance or distract?",
|
|
||||||
"Is the lighting hierarchy (teal primary, purple secondary) clear?"
|
|
||||||
],
|
|
||||||
"findings": null,
|
|
||||||
"threejs_changes_suggested": []
|
|
||||||
},
|
|
||||||
"status": "pending",
|
|
||||||
"created_at": "2026-04-10T20:15:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "clip-005",
|
|
||||||
"title": "Command Center — Batcave Terminal Focus",
|
|
||||||
"purpose": "Internal design reference. Tests readability and hierarchy of holographic terminal panels.",
|
|
||||||
"audience": "internal",
|
|
||||||
"priority": "LOW",
|
|
||||||
"duration_seconds": 8,
|
|
||||||
"shots": [
|
|
||||||
{
|
|
||||||
"shot": 1,
|
|
||||||
"timeframe": "0-2.5s",
|
|
||||||
"description": "Terminal Overview — 5 holographic panels in arc with distinct colors",
|
|
||||||
"design_focus": "panel arrangement, color distinction"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 2,
|
|
||||||
"timeframe": "2.5-5.5s",
|
|
||||||
"description": "Panel Detail — zoom into METRICS panel, scrolling text, scan lines",
|
|
||||||
"design_focus": "text readability, information density"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shot": 3,
|
|
||||||
"timeframe": "5.5-8s",
|
|
||||||
"description": "Agent Status — shift to panel, pulsing green dots, pull back",
|
|
||||||
"design_focus": "status indication clarity"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prompt": "Approach a futuristic holographic command terminal in a dark digital space. Five curved holographic panels float in an arc: \"NEXUS COMMAND\" (teal), \"DEV QUEUE\" (gold), \"METRICS\" (purple), \"SOVEREIGNTY\" (gold), \"AGENT STATUS\" (teal). Camera zooms into the METRICS panel showing scrolling data: \"CPU: 12%\", \"MEM: 4.2GB\", \"COMMITS: 842\" with scan lines and glow effects. Shift to AGENT STATUS panel showing \"TIMMY: ● RUNNING\", \"KIMI: ○ STANDBY\", \"CLAUDE: ● ACTIVE\" with pulsing green dots. Pull back to show full terminal context. Dark ambient environment, cyberpunk aesthetic, holographic UI focus.",
|
|
||||||
"prompt_variants": [],
|
|
||||||
"model_tool": "veo-3.1",
|
|
||||||
"access_point": "flow",
|
|
||||||
"output_files": [],
|
|
||||||
"design_insights": {
|
|
||||||
"questions": [
|
|
||||||
"Can you read the text at 1080p?",
|
|
||||||
"Do the color-coded panels communicate hierarchy?",
|
|
||||||
"Is the scan-line effect too retro or appropriately futuristic?"
|
|
||||||
],
|
|
||||||
"findings": null,
|
|
||||||
"threejs_changes_suggested": []
|
|
||||||
},
|
|
||||||
"status": "pending",
|
|
||||||
"created_at": "2026-04-10T20:15:00Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"project": "Timmy_Foundation/the-nexus",
|
|
||||||
"issue": 681,
|
|
||||||
"source_plan": "~/google-ai-ultra-plan.md",
|
|
||||||
"tools_available": ["veo-3.1", "flow", "nano-banana-pro"],
|
|
||||||
"max_clip_duration": 8,
|
|
||||||
"created_by": "mimo-v2-pro swarm",
|
|
||||||
"created_at": "2026-04-10T20:15:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
# Veo/Flow Flythrough Prototypes — Storyboard
|
|
||||||
## The Nexus & Timmy (Issue #681)
|
|
||||||
|
|
||||||
Source: `google-ai-ultra-plan.md` Veo/Flow section.
|
|
||||||
|
|
||||||
Purpose: Turn the current Nexus vision into short promo/concept clips for design leverage and communication.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Clip 1: "First Light" — The Nexus Reveal (PUBLIC PROMO)
|
|
||||||
|
|
||||||
**Duration:** 8 seconds
|
|
||||||
**Purpose:** Public-facing teaser. Establishes the Nexus as a place worth visiting.
|
|
||||||
**Tone:** Awe. Discovery. "What is this?"
|
|
||||||
|
|
||||||
### Shot Sequence (4 shots, ~2s each)
|
|
||||||
|
|
||||||
1. **0–2s | Void Approach**
|
|
||||||
- Camera drifts through deep space nebula (dark purples, teals)
|
|
||||||
- Distant stars twinkle
|
|
||||||
- A faint hexagonal glow appears below
|
|
||||||
- *Narrative hook: isolation before connection*
|
|
||||||
|
|
||||||
2. **2–4s | Platform Reveal**
|
|
||||||
- Camera descends toward the hexagonal platform
|
|
||||||
- Grid lines pulse with teal energy
|
|
||||||
- The ring border glows at the edge
|
|
||||||
- *Narrative hook: structure emerges from chaos*
|
|
||||||
|
|
||||||
3. **4–6s | Portal Array**
|
|
||||||
- Camera sweeps low across the platform
|
|
||||||
- 3–4 portals visible: Morrowind (orange), Workshop (teal), Chapel (gold), Archive (blue)
|
|
||||||
- Each portal ring hums with colored light, holographic labels flicker
|
|
||||||
- *Narrative hook: infinite worlds, one home*
|
|
||||||
|
|
||||||
4. **6–8s | Timmy's Terminal**
|
|
||||||
- Camera rises to the batcave terminal
|
|
||||||
- Holographic panels glow: NEXUS COMMAND, METRICS, AGENT STATUS
|
|
||||||
- Text scrolls: "> STATUS: NOMINAL"
|
|
||||||
- Final frame: teal light floods the lens
|
|
||||||
- *Narrative hook: someone is home*
|
|
||||||
|
|
||||||
### Veo Prompt (text-to-video)
|
|
||||||
```
|
|
||||||
Cinematic flythrough of a futuristic digital nexus hub. Start in deep space with a dark purple nebula, stars twinkling. Camera descends toward a glowing hexagonal platform with pulsing teal grid lines and a luminous ring border. Sweep low across the platform revealing multiple glowing portal archways in orange, teal, gold, and blue — each with flickering holographic labels. Rise toward a central command terminal with holographic data panels showing scrolling status text. Camera pushes into a teal light flare. Cyberpunk aesthetic, volumetric lighting, 8-second sequence, smooth camera movement, concept art quality.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Insight Target
|
|
||||||
- Does the scale feel right? (platform vs. portals vs. terminal)
|
|
||||||
- Does the color hierarchy work? (teal primary, purple secondary, gold accent)
|
|
||||||
- Is the camera movement cinematic or disorienting?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Clip 2: "Between Worlds" — Portal Activation (INTERNAL DESIGN)
|
|
||||||
|
|
||||||
**Duration:** 8 seconds
|
|
||||||
**Purpose:** Internal design reference. Tests the portal activation sequence and spatial relationships.
|
|
||||||
**Tone:** Energy. Connection. "What happens when you step through?"
|
|
||||||
|
|
||||||
### Shot Sequence (3 shots, ~2.5s each)
|
|
||||||
|
|
||||||
1. **0–2.5s | Approach**
|
|
||||||
- First-person perspective walking toward the Morrowind portal (orange, position x:15, z:-10)
|
|
||||||
- Portal ring visible: inner glow, particle effects rising
|
|
||||||
- Holographic label "MORROWIND" flickers above
|
|
||||||
- *Design focus: proximity feel, portal scale relative to player*
|
|
||||||
|
|
||||||
2. **2.5–5.5s | Activation**
|
|
||||||
- Player stops at activation distance
|
|
||||||
- Portal interior brightens — energy vortex forms
|
|
||||||
- Camera tilts up to show the full portal height
|
|
||||||
- Particles accelerate into the portal center
|
|
||||||
- Overlay text appears: "ENTER MORROWIND?"
|
|
||||||
- *Design focus: activation UX, visual feedback timing*
|
|
||||||
|
|
||||||
3. **5.5–8s | Stepping Through**
|
|
||||||
- Camera pushes forward into the portal
|
|
||||||
- World dissolves into orange energy tunnel
|
|
||||||
- Brief flash — then fade to black with "VVARDENFELL" text
|
|
||||||
- *Design focus: transition smoothness, immersion break points*
|
|
||||||
|
|
||||||
### Veo Prompt (text-to-video)
|
|
||||||
```
|
|
||||||
First-person perspective walking toward a glowing orange portal archway in a futuristic digital space. The portal ring has inner energy glow with rising particle effects. A holographic label "MORROWIND" flickers above. Camera stops, portal interior brightens into an energy vortex, particles accelerate inward. Camera pushes forward into the portal, world dissolves into an orange energy tunnel, flash to black with text "VVARDENFELL". Dark ambient environment with teal grid floor. Cyberpunk aesthetic, volumetric effects, smooth camera movement.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Insight Target
|
|
||||||
- Is the activation distance clear? (when does interaction become available?)
|
|
||||||
- Does the transition feel like travel or teleportation?
|
|
||||||
- Is the overlay text readable against the portal glow?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Clip 3: "The Guardian's View" — Timmy's Perspective (PUBLIC PROMO)
|
|
||||||
|
|
||||||
**Duration:** 8 seconds
|
|
||||||
**Purpose:** Public-facing. Establishes Timmy as the guardian/presence of the Nexus.
|
|
||||||
**Tone:** Contemplative. Sovereign. "Who lives here?"
|
|
||||||
|
|
||||||
### Shot Sequence (4 shots, ~2s each)
|
|
||||||
|
|
||||||
1. **0–2s | Agent Presence**
|
|
||||||
- Camera at eye-level, looking at a floating agent presence (glowing orb with trailing particles)
|
|
||||||
- The orb pulses gently, teal-gold light
|
|
||||||
- Background: the Nexus platform, slightly out of focus
|
|
||||||
- *Narrative hook: consciousness without body*
|
|
||||||
|
|
||||||
2. **2–4s | Vision Crystal**
|
|
||||||
- Camera shifts to a floating octahedron crystal (Sovereignty vision point)
|
|
||||||
- Crystal rotates slowly, refracting light
|
|
||||||
- Text hologram appears: "SOVEREIGNTY — No masters, no chains"
|
|
||||||
- Ring of light pulses beneath
|
|
||||||
- *Narrative hook: values inscribed in space*
|
|
||||||
|
|
||||||
3. **4–6s | The Harness Pulse**
|
|
||||||
- Camera pulls back to show the thought stream — a flowing ribbon of light across the platform
|
|
||||||
- Harness pulse mesh glows at the center
|
|
||||||
- Agent orbs drift along the stream
|
|
||||||
- *Narrative hook: the system breathes*
|
|
||||||
|
|
||||||
4. **6–8s | Wide View**
|
|
||||||
- Camera rises to high orbit view
|
|
||||||
- Entire Nexus visible: platform, portals, terminal, crystals, agents
|
|
||||||
- Nebula skybox frames everything
|
|
||||||
- Final frame: "THE NEXUS — Timmy's Sovereign Home" text overlay
|
|
||||||
- *Narrative hook: this is a world, not a page*
|
|
||||||
|
|
||||||
### Veo Prompt (text-to-video)
|
|
||||||
```
|
|
||||||
Cinematic sequence in a futuristic digital nexus. Start with eye-level view of a floating glowing orb (teal-gold light, trailing particles) pulsing gently — an AI agent presence. Shift to a rotating octahedron crystal refracting light, with holographic text "SOVEREIGNTY — No masters, no chains" and a ring of light pulsing beneath. Pull back to reveal flowing ribbons of light (thought streams) crossing a hexagonal platform, with agent orbs drifting. Rise to high orbit showing the full nexus: hexagonal platform, multiple colored portal archways, central command terminal, floating crystals, all framed by a dark purple nebula skybox. End with text overlay "THE NEXUS — Timmy's Sovereign Home". Cyberpunk aesthetic, volumetric lighting, contemplative pacing.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Insight Target
|
|
||||||
- Do agent presences read as "alive" or decorative?
|
|
||||||
- Is the crystal-to-text hologram readable?
|
|
||||||
- Does the wide shot communicate "world" or "tech demo"?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Clip 4: "The Void Between" — Ambient Environment Study (INTERNAL DESIGN)
|
|
||||||
|
|
||||||
**Duration:** 8 seconds
|
|
||||||
**Purpose:** Internal design reference. Tests the ambient environment systems: particles, dust, lighting, skybox.
|
|
||||||
**Tone:** Atmosphere. Mood. "What does the Nexus feel like when nothing is happening?"
|
|
||||||
|
|
||||||
### Shot Sequence (2 shots, ~4s each)
|
|
||||||
|
|
||||||
1. **0–4s | Particle Systems**
|
|
||||||
- Static camera, slight drift
|
|
||||||
- View from platform edge, looking out into the void
|
|
||||||
- Particle systems visible: ambient particles, dust particles
|
|
||||||
- Nebula skybox: dark purples, distant stars, subtle color shifts
|
|
||||||
- No portals, no terminals — just the environment
|
|
||||||
- *Design focus: does the void feel alive or empty?*
|
|
||||||
|
|
||||||
2. **4–8s | Lighting Study**
|
|
||||||
- Camera slowly orbits a point on the platform
|
|
||||||
- Teal point light (position 0,1,-5) creates warm glow
|
|
||||||
- Purple point light (position -8,3,-8) adds depth
|
|
||||||
- Ambient light (0x1a1a3a) fills shadows
|
|
||||||
- Grid lines catch the light
|
|
||||||
- *Design focus: lighting hierarchy, mood consistency*
|
|
||||||
|
|
||||||
### Veo Prompt (text-to-video)
|
|
||||||
```
|
|
||||||
Ambient environment study in a futuristic digital void. Static camera with slight drift, viewing from the edge of a hexagonal platform into deep space. Dark purple nebula with twinkling distant stars, subtle color shifts. Floating particles and dust drift slowly. No structures, no portals — pure atmosphere. Then camera slowly orbits showing teal and purple point lights casting volumetric glow on a dark hexagonal grid floor. Ambient lighting fills shadows. Contemplative, moody, atmospheric. Cyberpunk aesthetic, minimal movement, focus on light and particle behavior.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Insight Target
|
|
||||||
- Is the void atmospheric or just dark?
|
|
||||||
- Do the particle systems enhance or distract?
|
|
||||||
- Is the lighting hierarchy (teal primary, purple secondary) clear?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Clip 5: "Command Center" — Batcave Terminal Focus (INTERNAL DESIGN)
|
|
||||||
|
|
||||||
**Duration:** 8 seconds
|
|
||||||
**Purpose:** Internal design reference. Tests readability and hierarchy of the holographic terminal panels.
|
|
||||||
**Tone:** Information density. Control. "What can you see from here?"
|
|
||||||
|
|
||||||
### Shot Sequence (3 shots, ~2.5s each)
|
|
||||||
|
|
||||||
1. **0–2.5s | Terminal Overview**
|
|
||||||
- Camera approaches the batcave terminal from the front
|
|
||||||
- 5 holographic panels visible in arc: NEXUS COMMAND, DEV QUEUE, METRICS, SOVEREIGNTY, AGENT STATUS
|
|
||||||
- Each panel has distinct color (teal, gold, purple, gold, teal)
|
|
||||||
- *Design focus: panel arrangement, color distinction*
|
|
||||||
|
|
||||||
2. **2.5–5.5s | Panel Detail**
|
|
||||||
- Camera zooms into METRICS panel
|
|
||||||
- Text scrolls: "> CPU: 12% [||....]", "> MEM: 4.2GB", "> COMMITS: 842"
|
|
||||||
- Panel background glows, scan lines visible
|
|
||||||
- *Design focus: text readability, information density*
|
|
||||||
|
|
||||||
3. **5.5–8s | Agent Status**
|
|
||||||
- Camera shifts to AGENT STATUS panel
|
|
||||||
- Text: "> TIMMY: ● RUNNING", "> KIMI: ○ STANDBY", "> CLAUDE: ● ACTIVE"
|
|
||||||
- Green dot pulses next to active agents
|
|
||||||
- Pull back to show panel in context
|
|
||||||
- *Design focus: status indication clarity*
|
|
||||||
|
|
||||||
### Veo Prompt (text-to-video)
|
|
||||||
```
|
|
||||||
Approach a futuristic holographic command terminal in a dark digital space. Five curved holographic panels float in an arc: "NEXUS COMMAND" (teal), "DEV QUEUE" (gold), "METRICS" (purple), "SOVEREIGNTY" (gold), "AGENT STATUS" (teal). Camera zooms into the METRICS panel showing scrolling data: "CPU: 12%", "MEM: 4.2GB", "COMMITS: 842" with scan lines and glow effects. Shift to AGENT STATUS panel showing "TIMMY: ● RUNNING", "KIMI: ○ STANDBY", "CLAUDE: ● ACTIVE" with pulsing green dots. Pull back to show full terminal context. Dark ambient environment, cyberpunk aesthetic, holographic UI focus.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Design Insight Target
|
|
||||||
- Can you read the text at 1080p?
|
|
||||||
- Do the color-coded panels communicate hierarchy?
|
|
||||||
- Is the scan-line effect too retro or appropriately futuristic?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Matrix
|
|
||||||
|
|
||||||
| Clip | Title | Purpose | Audience | Priority |
|
|
||||||
|------|-------|---------|----------|----------|
|
|
||||||
| 1 | First Light | Public teaser | External | HIGH |
|
|
||||||
| 2 | Between Worlds | Portal UX design | Internal | HIGH |
|
|
||||||
| 3 | The Guardian's View | Public promo | External | MEDIUM |
|
|
||||||
| 4 | The Void Between | Environment design | Internal | MEDIUM |
|
|
||||||
| 5 | Command Center | Terminal UI design | Internal | LOW |
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Generate each clip using Veo/Flow (text-to-video prompts above)
|
|
||||||
2. Review outputs — update prompts based on what works
|
|
||||||
3. Record metadata in `docs/media/clip-metadata.json`
|
|
||||||
4. Iterate: refine prompts, regenerate, compare
|
|
||||||
5. Use internal design clips to inform Three.js implementation changes
|
|
||||||
6. Use public promo clips for README, social media, project communication
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Generated for Issue #681 — Timmy_Foundation/the-nexus*
|
|
||||||
@@ -836,8 +836,43 @@ async def main():
|
|||||||
default=1.0,
|
default=1.0,
|
||||||
help="Delay between iterations in seconds (default: 1.0)",
|
help="Delay between iterations in seconds (default: 1.0)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--local",
|
||||||
|
action="store_true",
|
||||||
|
help="Check local macOS Bannerlord readiness before starting",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--launch-local",
|
||||||
|
action="store_true",
|
||||||
|
help="Launch local Bannerlord on macOS via emulator before ODA loop",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle local macOS Bannerlord
|
||||||
|
if args.local or args.launch_local:
|
||||||
|
try:
|
||||||
|
from nexus.bannerlord_local import (
|
||||||
|
check_local_readiness, launch_bannerlord, LocalStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
state = check_local_readiness()
|
||||||
|
log.info(f"Local check: {state.status.value}")
|
||||||
|
log.info(f" Emulator: {state.emulator.name or 'none'}")
|
||||||
|
log.info(f" Game: {state.game.game_dir or 'not found'}")
|
||||||
|
log.info(f" Message: {state.message}")
|
||||||
|
|
||||||
|
if args.launch_local:
|
||||||
|
if state.status == LocalStatus.READY:
|
||||||
|
state = launch_bannerlord(state)
|
||||||
|
log.info(f"Launch result: {state.status.value} — {state.message}")
|
||||||
|
elif state.status == LocalStatus.RUNNING:
|
||||||
|
log.info(f"Already running (PID: {state.process_id})")
|
||||||
|
else:
|
||||||
|
log.error(f"Cannot launch: {state.message}")
|
||||||
|
return
|
||||||
|
except ImportError:
|
||||||
|
log.warning("bannerlord_local module not available — skipping local check")
|
||||||
|
|
||||||
# Create harness
|
# Create harness
|
||||||
harness = BannerlordHarness(
|
harness = BannerlordHarness(
|
||||||
hermes_ws_url=args.hermes_ws,
|
hermes_ws_url=args.hermes_ws,
|
||||||
|
|||||||
394
nexus/bannerlord_local.py
Normal file
394
nexus/bannerlord_local.py
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Bannerlord Local Manager — macOS Emulator Bridge
|
||||||
|
|
||||||
|
Detects and manages a local Bannerlord installation on macOS.
|
||||||
|
Provides status queries, launch control, and process monitoring
|
||||||
|
for the Bannerlord harness.
|
||||||
|
|
||||||
|
This module bridges the gap between:
|
||||||
|
- The GamePortal Protocol (MCP-based observation/action)
|
||||||
|
- A local GOG Bannerlord running through Wine/Whisky/CrossOver on macOS
|
||||||
|
|
||||||
|
The harness does NOT change — this module just manages the game process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
log = logging.getLogger("bannerlord.local")
|
||||||
|
|
||||||
|
|
||||||
|
class EmulatorType(Enum):
|
||||||
|
WHISKY = "whisky"
|
||||||
|
CROSSOVER = "crossover"
|
||||||
|
WINE = "wine"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class LocalStatus(Enum):
|
||||||
|
READY = "ready"
|
||||||
|
MISSING_EMULATOR = "missing_emulator"
|
||||||
|
MISSING_GAME = "missing_game"
|
||||||
|
RUNNING = "running"
|
||||||
|
CRASHED = "crashed"
|
||||||
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
# Standard GOG install paths on macOS
|
||||||
|
GOG_SEARCH_PATHS = [
|
||||||
|
Path("/Applications/Games/Mount & Blade II Bannerlord"),
|
||||||
|
Path.home() / "GOG Games" / "Mount and Blade II Bannerlord",
|
||||||
|
Path.home() / "Games" / "Mount & Blade II Bannerlord",
|
||||||
|
Path("/Applications/Mount & Blade II Bannerlord"),
|
||||||
|
]
|
||||||
|
|
||||||
|
BANNERLORD_EXE_RELATIVE = "bin/Generic/Bannerlord.exe"
|
||||||
|
|
||||||
|
LAUNCHER_SCRIPT = Path(__file__).parent.parent / "scripts" / "bannerlord_launcher.sh"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EmulatorInfo:
|
||||||
|
"""Detected Windows emulator on macOS."""
|
||||||
|
name: str = ""
|
||||||
|
path: str = ""
|
||||||
|
emulator_type: EmulatorType = EmulatorType.UNKNOWN
|
||||||
|
found: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameInstall:
|
||||||
|
"""Detected Bannerlord GOG installation."""
|
||||||
|
game_dir: str = ""
|
||||||
|
game_exe: str = ""
|
||||||
|
found: bool = False
|
||||||
|
source: str = "" # "gog", "gog-galaxy", "manual"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocalState:
|
||||||
|
"""Full local Bannerlord state."""
|
||||||
|
status: LocalStatus = LocalStatus.ERROR
|
||||||
|
emulator: EmulatorInfo = field(default_factory=EmulatorInfo)
|
||||||
|
game: GameInstall = field(default_factory=GameInstall)
|
||||||
|
process_id: Optional[int] = None
|
||||||
|
message: str = ""
|
||||||
|
is_macos: bool = False
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"status": self.status.value,
|
||||||
|
"emulator": {
|
||||||
|
"name": self.emulator.name,
|
||||||
|
"path": self.emulator.path,
|
||||||
|
"type": self.emulator.emulator_type.value,
|
||||||
|
"found": self.emulator.found,
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"game_dir": self.game.game_dir,
|
||||||
|
"game_exe": self.game.game_exe,
|
||||||
|
"found": self.game.found,
|
||||||
|
"source": self.game.source,
|
||||||
|
},
|
||||||
|
"process_id": self.process_id,
|
||||||
|
"message": self.message,
|
||||||
|
"is_macos": self.is_macos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_macos() -> bool:
|
||||||
|
"""Check if running on macOS."""
|
||||||
|
return platform.system() == "Darwin"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_emulator() -> EmulatorInfo:
|
||||||
|
"""Find a Windows emulator on macOS."""
|
||||||
|
info = EmulatorInfo()
|
||||||
|
|
||||||
|
# Whisky
|
||||||
|
whisky_path = "/Applications/Whisky.app/Contents/Resources/Libraries/wine/bin/wine64"
|
||||||
|
if os.path.isfile(whisky_path) and os.access(whisky_path, os.X_OK):
|
||||||
|
info.name = "Whisky"
|
||||||
|
info.path = whisky_path
|
||||||
|
info.emulator_type = EmulatorType.WHISKY
|
||||||
|
info.found = True
|
||||||
|
return info
|
||||||
|
|
||||||
|
# CrossOver
|
||||||
|
cx_path = "/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine"
|
||||||
|
if os.path.isfile(cx_path) and os.access(cx_path, os.X_OK):
|
||||||
|
info.name = "CrossOver"
|
||||||
|
info.path = cx_path
|
||||||
|
info.emulator_type = EmulatorType.CROSSOVER
|
||||||
|
info.found = True
|
||||||
|
return info
|
||||||
|
|
||||||
|
# Homebrew wine
|
||||||
|
for candidate in ["wine64", "wine"]:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["which", candidate],
|
||||||
|
capture_output=True, text=True, timeout=5,
|
||||||
|
)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
info.name = candidate
|
||||||
|
info.path = result.stdout.strip()
|
||||||
|
info.emulator_type = EmulatorType.WINE
|
||||||
|
info.found = True
|
||||||
|
return info
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def detect_game() -> GameInstall:
|
||||||
|
"""Find the Bannerlord GOG installation."""
|
||||||
|
install = GameInstall()
|
||||||
|
|
||||||
|
# Check standard paths
|
||||||
|
for path in GOG_SEARCH_PATHS:
|
||||||
|
exe_path = path / BANNERLORD_EXE_RELATIVE
|
||||||
|
if exe_path.is_file():
|
||||||
|
install.game_dir = str(path)
|
||||||
|
install.game_exe = str(exe_path)
|
||||||
|
install.found = True
|
||||||
|
install.source = "gog"
|
||||||
|
return install
|
||||||
|
|
||||||
|
# Check GOG Galaxy paths
|
||||||
|
galaxy_base = Path.home() / "Library/Application Support/GOG.com/Galaxy/Applications"
|
||||||
|
if galaxy_base.is_dir():
|
||||||
|
for child in galaxy_base.iterdir():
|
||||||
|
candidate = child / "Mount & Blade II Bannerlord" / BANNERLORD_EXE_RELATIVE
|
||||||
|
if candidate.is_file():
|
||||||
|
install.game_dir = str(candidate.parent.parent)
|
||||||
|
install.game_exe = str(candidate)
|
||||||
|
install.found = True
|
||||||
|
install.source = "gog-galaxy"
|
||||||
|
return install
|
||||||
|
|
||||||
|
# Last resort: find
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["find", "/Applications", str(Path.home() / "GOG Games"),
|
||||||
|
str(Path.home() / "Games"), "-name", "Bannerlord.exe",
|
||||||
|
"-type", "f"],
|
||||||
|
capture_output=True, text=True, timeout=15,
|
||||||
|
)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
first_line = result.stdout.strip().split("\n")[0]
|
||||||
|
install.game_exe = first_line
|
||||||
|
install.game_dir = str(Path(first_line).parent.parent)
|
||||||
|
install.found = True
|
||||||
|
install.source = "search"
|
||||||
|
return install
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return install
|
||||||
|
|
||||||
|
|
||||||
|
def check_local_readiness() -> LocalState:
|
||||||
|
"""Full local readiness check. Returns complete state."""
|
||||||
|
state = LocalState()
|
||||||
|
state.is_macos = detect_macos()
|
||||||
|
|
||||||
|
if not state.is_macos:
|
||||||
|
state.status = LocalStatus.ERROR
|
||||||
|
state.message = "Not macOS — local manager is Mac-only"
|
||||||
|
return state
|
||||||
|
|
||||||
|
state.emulator = detect_emulator()
|
||||||
|
if not state.emulator.found:
|
||||||
|
state.status = LocalStatus.MISSING_EMULATOR
|
||||||
|
state.message = "No Windows emulator found (install Whisky, CrossOver, or wine)"
|
||||||
|
return state
|
||||||
|
|
||||||
|
state.game = detect_game()
|
||||||
|
if not state.game.found:
|
||||||
|
state.status = LocalStatus.MISSING_GAME
|
||||||
|
state.message = "Bannerlord GOG installation not found in known paths"
|
||||||
|
return state
|
||||||
|
|
||||||
|
# Check if already running
|
||||||
|
pid = _read_pid()
|
||||||
|
if pid and _is_process_running(pid):
|
||||||
|
state.status = LocalStatus.RUNNING
|
||||||
|
state.process_id = pid
|
||||||
|
state.message = f"Bannerlord already running (PID: {pid})"
|
||||||
|
else:
|
||||||
|
state.status = LocalStatus.READY
|
||||||
|
state.message = "Ready to launch"
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def launch_bannerlord(state: Optional[LocalState] = None) -> LocalState:
|
||||||
|
"""Launch Bannerlord via the emulator. Returns updated state."""
|
||||||
|
if state is None:
|
||||||
|
state = check_local_readiness()
|
||||||
|
|
||||||
|
if state.status not in (LocalStatus.READY, LocalStatus.RUNNING):
|
||||||
|
return state
|
||||||
|
|
||||||
|
if state.status == LocalStatus.RUNNING:
|
||||||
|
state.message = f"Already running (PID: {state.process_id})"
|
||||||
|
return state
|
||||||
|
|
||||||
|
# Check if launcher script exists
|
||||||
|
if LAUNCHER_SCRIPT.is_file():
|
||||||
|
log.info(f"Using launcher script: {LAUNCHER_SCRIPT}")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["bash", str(LAUNCHER_SCRIPT), "--launch"],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
cwd=str(LAUNCHER_SCRIPT.parent.parent),
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Parse PID from output
|
||||||
|
for line in result.stdout.strip().split("\n"):
|
||||||
|
if "PID:" in line:
|
||||||
|
try:
|
||||||
|
pid = int(line.split("PID:")[1].strip().rstrip(")"))
|
||||||
|
state.process_id = pid
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
state.status = LocalStatus.RUNNING
|
||||||
|
state.message = "Launched via launcher script"
|
||||||
|
return state
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
||||||
|
log.warning(f"Launcher script failed: {e}, falling back to direct launch")
|
||||||
|
|
||||||
|
# Direct launch fallback
|
||||||
|
try:
|
||||||
|
log.info(f"Launching Bannerlord directly via {state.emulator.name}")
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[state.emulator.path, state.game.game_exe],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
cwd=state.game.game_dir,
|
||||||
|
)
|
||||||
|
state.process_id = proc.pid
|
||||||
|
state.status = LocalStatus.RUNNING
|
||||||
|
state.message = f"Launched (PID: {proc.pid})"
|
||||||
|
_write_pid(proc.pid)
|
||||||
|
except Exception as e:
|
||||||
|
state.status = LocalStatus.CRASHED
|
||||||
|
state.message = f"Launch failed: {e}"
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def stop_bannerlord() -> bool:
|
||||||
|
"""Stop a running Bannerlord process."""
|
||||||
|
pid = _read_pid()
|
||||||
|
if not pid or not _is_process_running(pid):
|
||||||
|
_clear_pid()
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.kill(pid, 15) # SIGTERM
|
||||||
|
time.sleep(1)
|
||||||
|
if _is_process_running(pid):
|
||||||
|
os.kill(pid, 9) # SIGKILL
|
||||||
|
_clear_pid()
|
||||||
|
log.info(f"Stopped Bannerlord (PID: {pid})")
|
||||||
|
return True
|
||||||
|
except ProcessLookupError:
|
||||||
|
_clear_pid()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# PID FILE MANAGEMENT
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
PID_FILE = Path("/tmp/bannerlord.pid")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_pid() -> Optional[int]:
|
||||||
|
try:
|
||||||
|
if PID_FILE.is_file():
|
||||||
|
return int(PID_FILE.read_text().strip())
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_pid(pid: int):
|
||||||
|
try:
|
||||||
|
PID_FILE.write_text(str(pid))
|
||||||
|
except OSError as e:
|
||||||
|
log.warning(f"Failed to write PID file: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_pid():
|
||||||
|
try:
|
||||||
|
if PID_FILE.is_file():
|
||||||
|
PID_FILE.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _is_process_running(pid: int) -> bool:
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
return True
|
||||||
|
except (ProcessLookupError, PermissionError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# CLI
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Bannerlord Local Manager — macOS")
|
||||||
|
parser.add_argument("--check", action="store_true", help="Check readiness")
|
||||||
|
parser.add_argument("--launch", action="store_true", help="Launch the game")
|
||||||
|
parser.add_argument("--stop", action="store_true", help="Stop running game")
|
||||||
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.stop:
|
||||||
|
stopped = stop_bannerlord()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"stopped": stopped}))
|
||||||
|
else:
|
||||||
|
print("Stopped." if stopped else "Not running.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.launch:
|
||||||
|
state = launch_bannerlord()
|
||||||
|
else:
|
||||||
|
state = check_local_readiness()
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(state.to_dict(), indent=2))
|
||||||
|
else:
|
||||||
|
print(f"Status: {state.status.value}")
|
||||||
|
print(f"Emulator: {state.emulator.name or 'none'} ({state.emulator.emulator_type.value})")
|
||||||
|
print(f"Game: {state.game.game_dir or 'not found'}")
|
||||||
|
if state.process_id:
|
||||||
|
print(f"PID: {state.process_id}")
|
||||||
|
print(f"Message: {state.message}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
11
portals.json
11
portals.json
@@ -23,13 +23,22 @@
|
|||||||
"rotation": { "y": 0.5 },
|
"rotation": { "y": 0.5 },
|
||||||
"portal_type": "game-world",
|
"portal_type": "game-world",
|
||||||
"world_category": "strategy-rpg",
|
"world_category": "strategy-rpg",
|
||||||
"environment": "production",
|
"environment": "local",
|
||||||
"access_mode": "operator",
|
"access_mode": "operator",
|
||||||
"readiness_state": "active",
|
"readiness_state": "active",
|
||||||
"telemetry_source": "hermes-harness:bannerlord",
|
"telemetry_source": "hermes-harness:bannerlord",
|
||||||
"owner": "Timmy",
|
"owner": "Timmy",
|
||||||
"app_id": 261550,
|
"app_id": 261550,
|
||||||
"window_title": "Mount & Blade II: Bannerlord",
|
"window_title": "Mount & Blade II: Bannerlord",
|
||||||
|
"local_launch": {
|
||||||
|
"platform": "macos",
|
||||||
|
"source": "gog",
|
||||||
|
"emulator_required": true,
|
||||||
|
"emulator_options": ["whisky", "crossover", "wine"],
|
||||||
|
"launcher": "scripts/bannerlord_launcher.sh",
|
||||||
|
"harness_bridge": "nexus/bannerlord_local.py",
|
||||||
|
"check_command": "python3 -m nexus.bannerlord_local --check --json"
|
||||||
|
},
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": "https://bannerlord.timmy.foundation",
|
"url": "https://bannerlord.timmy.foundation",
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
|
|||||||
223
scripts/bannerlord_launcher.sh
Executable file
223
scripts/bannerlord_launcher.sh
Executable file
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bannerlord Local Launcher for macOS
|
||||||
|
# Detects Wine/Whisky/CrossOver, finds GOG Bannerlord install, launches it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/bannerlord_launcher.sh [--check] [--launch] [--verbose]
|
||||||
|
#
|
||||||
|
# Modes:
|
||||||
|
# --check Check environment only (no launch). Exits 0 if ready.
|
||||||
|
# --launch Launch the game (default if no flags)
|
||||||
|
# --verbose Print detailed diagnostic info
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# CONFIGURATION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
BANNERLORD_EXE="bin/Generic/Bannerlord.exe"
|
||||||
|
GOG_PATHS=(
|
||||||
|
"/Applications/Games/Mount & Blade II Bannerlord"
|
||||||
|
"$HOME/GOG Games/Mount and Blade II Bannerlord"
|
||||||
|
"$HOME/Games/Mount & Blade II Bannerlord"
|
||||||
|
"/Applications/Mount & Blade II Bannerlord"
|
||||||
|
)
|
||||||
|
# Also check common GOG Galaxy paths
|
||||||
|
GOG_GALAXY_PATHS=(
|
||||||
|
"$HOME/Library/Application Support/GOG.com/Galaxy/Applications/*/Mount & Blade II Bannerlord"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Emulator priority: Whisky > CrossOver > Homebrew Wine > system wine
|
||||||
|
EMULATOR_NAMES=("Whisky" "CrossOver" "Wine" "wine64" "wine")
|
||||||
|
|
||||||
|
VERBOSE=0
|
||||||
|
CHECK_ONLY=0
|
||||||
|
LAUNCH=0
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# ARGUMENT PARSING
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--check) CHECK_ONLY=1 ;;
|
||||||
|
--launch) LAUNCH=1 ;;
|
||||||
|
--verbose) VERBOSE=1 ;;
|
||||||
|
*) echo "Unknown arg: $arg"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$CHECK_ONLY" -eq 0 ] && [ "$LAUNCH" -eq 0 ]; then
|
||||||
|
LAUNCH=1 # Default to launch mode
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() { echo "[bannerlord] $*"; }
|
||||||
|
vlog() { [ "$VERBOSE" -eq 1 ] && echo "[bannerlord:debug] $*" || true; }
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# EMULATOR DETECTION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
find_emulator() {
|
||||||
|
local emulator_path=""
|
||||||
|
local emulator_name=""
|
||||||
|
local emulator_type=""
|
||||||
|
|
||||||
|
# Check for Whisky (macOS Wine wrapper)
|
||||||
|
if [ -d "/Applications/Whisky.app" ]; then
|
||||||
|
emulator_path="/Applications/Whisky.app/Contents/Resources/Libraries/wine/bin/wine64"
|
||||||
|
if [ -x "$emulator_path" ]; then
|
||||||
|
emulator_name="Whisky"
|
||||||
|
emulator_type="whisky"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for CrossOver
|
||||||
|
if [ -z "$emulator_path" ] && [ -d "/Applications/CrossOver.app" ]; then
|
||||||
|
emulator_path="/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine"
|
||||||
|
if [ -x "$emulator_path" ]; then
|
||||||
|
emulator_name="CrossOver"
|
||||||
|
emulator_type="crossover"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for Homebrew wine
|
||||||
|
if [ -z "$emulator_path" ]; then
|
||||||
|
for candidate in wine64 wine; do
|
||||||
|
if command -v "$candidate" >/dev/null 2>&1; then
|
||||||
|
emulator_path="$(command -v "$candidate")"
|
||||||
|
emulator_name="$candidate"
|
||||||
|
emulator_type="wine"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$emulator_path" ]; then
|
||||||
|
EMULATOR_PATH="$emulator_path"
|
||||||
|
EMULATOR_NAME="$emulator_name"
|
||||||
|
EMULATOR_TYPE="$emulator_type"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# GAME DETECTION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
find_bannerlord() {
|
||||||
|
# Check standard GOG paths
|
||||||
|
for path in "${GOG_PATHS[@]}"; do
|
||||||
|
if [ -f "$path/$BANNERLORD_EXE" ]; then
|
||||||
|
GAME_DIR="$path"
|
||||||
|
GAME_EXE="$path/$BANNERLORD_EXE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check GOG Galaxy paths (glob expansion)
|
||||||
|
for pattern in "${GOG_GALAXY_PATHS[@]}"; do
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
for path in $pattern; do
|
||||||
|
if [ -d "$path" ] && [ -f "$path/$BANNERLORD_EXE" ]; then
|
||||||
|
GAME_DIR="$path"
|
||||||
|
GAME_EXE="$path/$BANNERLORD_EXE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Search with find as last resort
|
||||||
|
local found
|
||||||
|
found=$(find /Applications "$HOME/GOG Games" "$HOME/Games" -name "Bannerlord.exe" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -n "$found" ]; then
|
||||||
|
GAME_EXE="$found"
|
||||||
|
GAME_DIR="$(dirname "$(dirname "$found")")"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# STATUS REPORTING
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
emit_status() {
|
||||||
|
local status="$1"
|
||||||
|
local message="$2"
|
||||||
|
# JSON output for harness consumption
|
||||||
|
echo "{\"status\":\"$status\",\"emulator\":\"${EMULATOR_NAME:-none}\",\"emulator_type\":\"${EMULATOR_TYPE:-none}\",\"game_dir\":\"${GAME_DIR:-}\",\"game_exe\":\"${GAME_EXE:-}\",\"message\":\"$message\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# MAIN
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Verify macOS
|
||||||
|
if [ "$(uname)" != "Darwin" ]; then
|
||||||
|
emit_status "error" "Not macOS — this launcher is Mac-only"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Bannerlord Local Launcher — macOS"
|
||||||
|
|
||||||
|
# Find emulator
|
||||||
|
if find_emulator; then
|
||||||
|
log "Emulator found: $EMULATOR_NAME ($EMULATOR_PATH)"
|
||||||
|
vlog " Type: $EMULATOR_TYPE"
|
||||||
|
else
|
||||||
|
log "ERROR: No Windows emulator found."
|
||||||
|
log "Install one of: Whisky, CrossOver, or wine (brew install --cask wine-stable)"
|
||||||
|
emit_status "missing_emulator" "No Windows emulator installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find game
|
||||||
|
if find_bannerlord; then
|
||||||
|
log "Bannerlord found: $GAME_DIR"
|
||||||
|
vlog " Exe: $GAME_EXE"
|
||||||
|
else
|
||||||
|
log "ERROR: Bannerlord not found in known GOG paths."
|
||||||
|
log "Checked: ${GOG_PATHS[*]}"
|
||||||
|
emit_status "missing_game" "Bannerlord GOG installation not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check mode
|
||||||
|
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||||
|
log "Check passed. Ready to launch."
|
||||||
|
emit_status "ready" "Emulator and game both found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Launch
|
||||||
|
if [ "$LAUNCH" -eq 1 ]; then
|
||||||
|
log "Launching Bannerlord via $EMULATOR_NAME..."
|
||||||
|
emit_status "launching" "Starting Bannerlord through $EMULATOR_NAME"
|
||||||
|
|
||||||
|
cd "$GAME_DIR"
|
||||||
|
# Launch in background, redirect output
|
||||||
|
"$EMULATOR_PATH" "$GAME_EXE" "$@" >/dev/null 2>&1 &
|
||||||
|
local pid=$!
|
||||||
|
log "Bannerlord started (PID: $pid)"
|
||||||
|
echo "$pid" > /tmp/bannerlord.pid
|
||||||
|
|
||||||
|
# Wait a moment and check it's still running
|
||||||
|
sleep 2
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log "Bannerlord is running."
|
||||||
|
emit_status "running" "Bannerlord PID $pid"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
log "WARNING: Bannerlord process exited quickly. Check Wine logs."
|
||||||
|
emit_status "crashed" "Process exited within 2 seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user