Compare commits
1 Commits
mimo/code/
...
mimo/creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1294f37b44 |
@@ -1,132 +1,169 @@
|
|||||||
# Legacy Matrix Audit
|
# Legacy Matrix Audit — Migration Table
|
||||||
|
|
||||||
Purpose:
|
Purpose:
|
||||||
Preserve useful work from `/Users/apayne/the-matrix` before the Nexus browser shell is rebuilt.
|
Preserve quality work from `/Users/apayne/the-matrix` before the Nexus browser shell is rebuilt.
|
||||||
|
|
||||||
Canonical rule:
|
Canonical rule:
|
||||||
- `Timmy_Foundation/the-nexus` is the only canonical 3D repo.
|
- `Timmy_Foundation/the-nexus` is the only canonical 3D repo.
|
||||||
- `/Users/apayne/the-matrix` is legacy source material, not a parallel product.
|
- `/Users/apayne/the-matrix` is legacy source material, not a parallel product.
|
||||||
|
- This document is the authoritative migration table for issue #685.
|
||||||
|
|
||||||
## Verified Legacy Matrix State
|
## Verified Legacy State
|
||||||
|
|
||||||
Local legacy repo:
|
Local legacy repo: `/Users/apayne/the-matrix`
|
||||||
- `/Users/apayne/the-matrix`
|
|
||||||
|
|
||||||
Observed facts:
|
Observed facts:
|
||||||
- Vite browser app exists
|
- Vite browser app, vanilla JS + Three.js 0.171.0
|
||||||
- `npm test` passes with `87 passed, 0 failed`
|
- 24 JS modules under `js/`
|
||||||
- 23 JS modules under `js/`
|
- Smoke suite: 87 passed, 0 failed
|
||||||
- package scripts include `dev`, `build`, `preview`, and `test`
|
- Package scripts: dev, build, preview, test
|
||||||
|
- PWA manifest + service worker
|
||||||
|
- Vite config with code-splitting (Three.js in separate chunk)
|
||||||
|
- Quality-tier system for hardware detection
|
||||||
|
- WebSocket client with reconnection, heartbeat, mock mode
|
||||||
|
- Full avatar FPS movement + PiP camera
|
||||||
|
- Sub-world portal system with zone triggers
|
||||||
|
|
||||||
## Known historical Nexus snapshot
|
## Migration Table
|
||||||
|
|
||||||
Useful in-repo reference point:
|
Decision key:
|
||||||
- `0518a1c3ae3c1d0afeb24dea9772102f5a3d9a66`
|
- **CARRY** = transplant concepts and patterns into Nexus vNext
|
||||||
|
- **ARCHIVE** = keep as reference, do not directly transplant
|
||||||
|
- **DROP** = do not preserve unless re-justified
|
||||||
|
|
||||||
That snapshot still contains browser-world root files such as:
|
### Core Modules
|
||||||
- `index.html`
|
|
||||||
- `app.js`
|
|
||||||
- `style.css`
|
|
||||||
- `package.json`
|
|
||||||
- `tests/`
|
|
||||||
|
|
||||||
## Rescue Candidates
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
|
|------|-------|------------|----------|---------------|
|
||||||
|
| `js/main.js` | 180 | App bootstrap, render loop, WebGL context recovery | **CARRY** | Architectural pattern. Shows clean init/teardown lifecycle, context-loss recovery, visibility pause. Nexus needs this loop but should not copy the monolithic wiring. |
|
||||||
|
| `js/world.js` | 95 | Scene, camera, renderer, grid, lights | **CARRY** | Foundational. Quality-tier-aware renderer setup, grid floor, lighting. Nexus already has a world but should adopt the tier-aware antialiasing and pixel-ratio capping. |
|
||||||
|
| `js/config.js` | 68 | Connection config via URL params + env vars | **ARCHIVE** | Pattern reference only. Nexus config should route through Hermes harness, not Vite env vars. The URL-override pattern (ws, token, mock) is worth remembering. |
|
||||||
|
| `js/quality.js` | 90 | Hardware detection, quality tier (low/medium/high) | **CARRY** | Directly useful. DPR capping, core/memory/screen heuristics, WebGL renderer sniffing. Nexus needs this for graceful degradation on Mac/iPad. |
|
||||||
|
| `js/storage.js` | 39 | Safe localStorage with in-memory fallback | **CARRY** | Small, robust, sandbox-proof. Nexus should use this or equivalent. Prevents crashes in sandboxed iframes. |
|
||||||
|
|
||||||
### Carry forward into Nexus vNext
|
### Agent System
|
||||||
|
|
||||||
1. `agent-defs.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- agent identity definitions
|
|------|-------|------------|----------|---------------|
|
||||||
- useful as seed data/model for visible entities in the world
|
| `js/agent-defs.js` | 30 | Agent identity data (id, label, color, role, position) | **CARRY** | Seed data model. Nexus agents should be defined similarly — data-driven, not hardcoded in render logic. Color hex helper is trivial but useful. |
|
||||||
|
| `js/agents.js` | 523 | Agent 3D objects, movement, state, connection lines, hot-add/remove | **CARRY** | Core visual system. Shared geometries (perf), movement interpolation, wallet-health stress glow, auto-placement algorithm, connection-line pulse. All valuable. Needs integration with real agent state from Hermes. |
|
||||||
|
| `js/behaviors.js` | 413 | Autonomous agent behavior state machine | **ARCHIVE** | Pattern reference. The personality-weighted behavior selection, conversation pairing, and artifact-placement system are well-designed. But Nexus behaviors should be driven by Hermes, not a client-side simulation. Keep the architecture, drop the fake-autonomy. |
|
||||||
|
| `js/presence.js` | 139 | Agent presence HUD (online/offline, uptime, state) | **CARRY** | Valuable UX. Live "who's here" panel with uptime tickers and state indicators. Needs real backend state, not mock assumptions. |
|
||||||
|
|
||||||
2. `agents.js`
|
### Visitor & Interaction
|
||||||
- agent objects, state machine, connection lines
|
|
||||||
- useful for visualizing Timmy / subagents / system processes in a world-native way
|
|
||||||
|
|
||||||
3. `avatar.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- visitor embodiment, movement, camera handling
|
|------|-------|------------|----------|---------------|
|
||||||
- strongly aligned with "training ground" and "walk the world" goals
|
| `js/visitor.js` | 141 | Visitor enter/leave protocol, chat input | **CARRY** | Session lifecycle. Device detection, visibility-based leave/return, chat input wiring. Directly applicable to Nexus visitor tracking. |
|
||||||
|
| `js/avatar.js` | 360 | FPS movement, PiP dual-camera, touch input | **CARRY** | Visitor embodiment. WASD + arrow movement, first/third person swap, PiP canvas, touch joystick, right-click mouse-look. Strong work. Needs tuning for Nexus world bounds. |
|
||||||
|
| `js/interaction.js` | 296 | Raycasting, click-to-select agents, info popup | **CARRY** | Essential for any browser world. OrbitControls, pointer/tap detection, agent popup with state/role, TALK button. The popup-anchoring-to-3D-position logic is particularly well done. |
|
||||||
|
| `js/zones.js` | 161 | Proximity trigger zones (portal enter/exit, events) | **CARRY** | Spatial event system. Portal traversal, event triggers, once-only zones. Nexus portals (#672) need this exact pattern. |
|
||||||
|
|
||||||
4. `ui.js`
|
### Chat & Communication
|
||||||
- HUD, chat surfaces, overlays
|
|
||||||
- useful if rebuilt against real harness data instead of stale fake state
|
|
||||||
|
|
||||||
5. `websocket.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- browser-side live bridge patterns
|
|------|-------|------------|----------|---------------|
|
||||||
- useful if retethered to Hermes-facing transport
|
| `js/bark.js` | 141 | Speech bubble system with typing animation | **CARRY** | Timmy's voice in-world. Typing animation, queue, auto-dismiss, emotion tags, demo bark lines. Strong expressive presence. The demo lines ("The Tower watches. The Tower remembers.") are good seed content. |
|
||||||
|
| `js/ui.js` | 285 | Chat panel, agent list, HUD, streaming tokens | **CARRY** | Chat infrastructure. Rolling chat buffer, per-agent localStorage history, streaming token display with cursor animation, HTML escaping. Needs reconnection to Hermes chat instead of WS mock. |
|
||||||
|
| `js/transcript.js` | 183 | Conversation transcript logger, export | **ARCHIVE** | Pattern reference. The rolling buffer, structured JSON entries, TXT/JSON download, HUD badge are all solid. But transcript authority should live in Hermes, not browser localStorage. Keep the UX pattern, rebuild storage layer. |
|
||||||
|
|
||||||
6. `transcript.js`
|
### Visual Effects
|
||||||
- local transcript capture pattern
|
|
||||||
- useful if durable truth still routes through Hermes and browser cache remains secondary
|
|
||||||
|
|
||||||
7. `ambient.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- mood / atmosphere system
|
|------|-------|------------|----------|---------------|
|
||||||
- directly supports wizardly presentation without changing system authority
|
| `js/effects.js` | 195 | Matrix rain particles + starfield | **CARRY** | Atmospheric foundation. Quality-tier particle counts, frame-skip optimization, adaptive draw-range (FPS-budget recovery), bounding-sphere pre-compute. This is production-grade particle work. |
|
||||||
|
| `js/ambient.js` | 212 | Mood-driven atmosphere (lighting, fog, rain, stars) | **CARRY** | Scene mood engine. Smooth eased transitions between mood states (calm, focused, excited, contemplative, stressed), per-mood lighting/fog/rain/star parameters. Directly supports Nexus atmosphere. |
|
||||||
|
| `js/satflow.js` | 261 | Lightning payment particle flow | **CARRY** | Economy visualization. Bezier-arc particles, staggered travel, burst-on-arrival, pooling. If Nexus shows any payment/economy flow, this is the pattern. |
|
||||||
|
|
||||||
8. `satflow.js`
|
### Economy & Scene
|
||||||
- visual economy / payment flow motifs
|
|
||||||
- useful if Timmy's economy/agent interactions become a real visible layer
|
|
||||||
|
|
||||||
9. `economy.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- treasury / wallet panel ideas
|
|------|-------|------------|----------|---------------|
|
||||||
- useful if later backed by real sovereign metrics
|
| `js/economy.js` | 100 | Wallet/treasury HUD panel | **ARCHIVE** | UI pattern reference. Clean sats formatting, per-agent balance rows, health-colored dots, recent transactions. Worth rebuilding when backed by real sovereign metrics. |
|
||||||
|
| `js/scene-objects.js` | 718 | Dynamic 3D object registry, portals, sub-worlds | **CARRY** | Critical. Geometry/material factories, animation system (rotate/bob/pulse/orbit), portal visual (torus ring + glow disc + zone), sub-world load/unload, text sprites, compound groups. This is the most complex and valuable module. Nexus portals (#672) should build on this. |
|
||||||
|
|
||||||
10. `presence.js`
|
### Backend Bridge
|
||||||
- who-is-here / online-state UI
|
|
||||||
- useful for showing human + agent + process presence in the world
|
|
||||||
|
|
||||||
11. `interaction.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- clicking, inspecting, selecting world entities
|
|------|-------|------------|----------|---------------|
|
||||||
- likely needed in any real browser-facing Nexus shell
|
| `js/websocket.js` | 598 | WebSocket client, message dispatcher, mock mode | **ARCHIVE** | Pattern reference only. Reconnection with exponential backoff, heartbeat/zombie detection, rich message dispatch (40+ message types), streaming chat support. The architecture is sound but must be reconnected to Hermes transport, not copied wholesale. The message-type catalog is the most valuable reference artifact. |
|
||||||
|
| `js/demo.js` | ~300 | Demo autopilot (mock mode simulation) | **DROP** | Fake activity simulation. Deliberately creates the illusion of live data. Do not preserve. If Nexus needs a demo mode, build a clearly-labeled one that doesn't pretend to be real. |
|
||||||
|
|
||||||
12. `quality.js`
|
### Testing & Build
|
||||||
- hardware-aware quality tiering
|
|
||||||
- useful for local-first graceful degradation on Mac hardware
|
|
||||||
|
|
||||||
13. `bark.js`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- prominent speech / bark system
|
|------|-------|------------|----------|---------------|
|
||||||
- strong fit for Timmy's expressive presence in-world
|
| `test/smoke.mjs` | 235 | Automated browser smoke test suite | **CARRY** | Testing discipline. Module inventory check, export verification, HTML structure validation, Vite build test, bundle-size budget, PWA manifest check. Nexus should adopt this pattern (adapted for its own module structure). |
|
||||||
|
| `vite.config.js` | 53 | Build config with code splitting, SW generation | **ARCHIVE** | Build tooling reference. manualChunks for Three.js, SW precache generation plugin. Relevant if Nexus re-commits to Vite. |
|
||||||
|
| `sw.js` | ~40 | Service worker with precache | **ARCHIVE** | PWA reference. Relevant only if Nexus pursues offline-first PWA. |
|
||||||
|
| `manifest.json` | ~20 | PWA manifest | **ARCHIVE** | PWA reference. |
|
||||||
|
|
||||||
14. `world.js`, `effects.js`, `scene-objects.js`, `zones.js`
|
### Server-Side (Python)
|
||||||
- broad visual foundation work
|
|
||||||
- should be mined for patterns, not blindly transplanted
|
|
||||||
|
|
||||||
15. `test/smoke.mjs`
|
| File | Lines | Capability | Decision | Why for Nexus |
|
||||||
- browser smoke discipline
|
|------|-------|------------|----------|---------------|
|
||||||
- should inform rebuilt validation in canonical Nexus repo
|
| `server/bridge.py` | ~900 | WebSocket bridge server | **ARCHIVE** | Reference. Hermes replaces this role. Keep for protocol schema reference. |
|
||||||
|
| `server/gateway.py` | ~400 | HTTP gateway | **ARCHIVE** | Reference. |
|
||||||
|
| `server/ollama_client.py` | ~280 | Ollama integration | **ARCHIVE** | Reference. Relevant if Nexus needs local model calls. |
|
||||||
|
| `server/research.py` | ~450 | Research pipeline | **ARCHIVE** | Reference. |
|
||||||
|
| `server/webhooks.py` | ~350 | Webhook handler | **ARCHIVE** | Reference. |
|
||||||
|
| `server/test_*.py` | ~5 files | Server test suites | **ARCHIVE** | Testing patterns worth studying. |
|
||||||
|
|
||||||
### Archive as reference, not direct carry-forward
|
## Summary by Decision
|
||||||
|
|
||||||
- demo/autopilot assumptions that pretend fake backend activity is real
|
### CARRY FORWARD (17 modules)
|
||||||
- any websocket schema that no longer matches Hermes truth
|
These modules contain patterns, algorithms, or entire implementations that should move into the Nexus browser shell:
|
||||||
- Vite-specific plumbing that is only useful if we consciously recommit to Vite
|
|
||||||
|
|
||||||
### Deliberately drop unless re-justified
|
- `quality.js` — hardware detection
|
||||||
|
- `storage.js` — safe persistence
|
||||||
|
- `world.js` — scene foundation
|
||||||
|
- `agent-defs.js` — agent data model
|
||||||
|
- `agents.js` — agent visualization + movement
|
||||||
|
- `presence.js` — online presence HUD
|
||||||
|
- `visitor.js` — session lifecycle
|
||||||
|
- `avatar.js` — FPS embodiment
|
||||||
|
- `interaction.js` — click/select/raycast
|
||||||
|
- `zones.js` — spatial triggers
|
||||||
|
- `bark.js` — speech bubbles
|
||||||
|
- `ui.js` — chat/HUD
|
||||||
|
- `effects.js` — particle effects
|
||||||
|
- `ambient.js` — mood atmosphere
|
||||||
|
- `satflow.js` — payment flow particles
|
||||||
|
- `scene-objects.js` — dynamic objects + portals
|
||||||
|
- `test/smoke.mjs` — smoke test discipline
|
||||||
|
|
||||||
- anything that presents mock data as if it were live
|
### ARCHIVE AS REFERENCE (9 modules/files)
|
||||||
- anything that duplicates a better Hermes-native telemetry path
|
Keep for patterns, protocol schemas, and architectural reference. Do not directly transplant:
|
||||||
- anything that turns the browser into the system of record
|
|
||||||
|
- `config.js` — config pattern (use Hermes instead)
|
||||||
|
- `behaviors.js` — behavior architecture (use Hermes-driven state)
|
||||||
|
- `transcript.js` — transcript UX (use Hermes storage)
|
||||||
|
- `economy.js` — economy UI pattern (use real metrics)
|
||||||
|
- `websocket.js` — message protocol catalog + reconnection patterns
|
||||||
|
- `vite.config.js` — build tooling
|
||||||
|
- `sw.js`, `manifest.json` — PWA reference
|
||||||
|
- `server/*.py` — server protocol schemas
|
||||||
|
|
||||||
|
### DELIBERATELY DROP (2)
|
||||||
|
Do not preserve unless re-justified:
|
||||||
|
|
||||||
|
- `demo.js` — fake activity simulation; creates false impression of live system
|
||||||
|
- `main.js` monolithic wiring — the init pattern carries, the specific module wiring does not
|
||||||
|
|
||||||
## Concern Separation for Nexus vNext
|
## Concern Separation for Nexus vNext
|
||||||
|
|
||||||
When rebuilding inside `the-nexus`, keep concerns separated:
|
When rebuilding inside `the-nexus`, keep these concerns in separate modules:
|
||||||
|
|
||||||
1. World shell / rendering
|
1. **World shell** — scene, camera, renderer, grid, lights, fog
|
||||||
- scene, camera, movement, atmosphere
|
2. **Effects layer** — rain, stars, ambient mood transitions
|
||||||
|
3. **Agent visualization** — 3D objects, labels, connection lines, movement
|
||||||
2. Presence and embodiment
|
4. **Visitor embodiment** — avatar, FPS controls, PiP camera
|
||||||
- avatar, agent placement, selection, bark/chat surfaces
|
5. **Interaction layer** — raycasting, selection, zones, portal traversal
|
||||||
|
6. **Communication surface** — bark, chat panel, streaming tokens
|
||||||
3. Harness bridge
|
7. **Presence & HUD** — who's-online, economy panel, transcript controls
|
||||||
- websocket / API bridge from Hermes truth into browser state
|
8. **Harness bridge** — WebSocket/API transport to Hermes (NOT a copy of websocket.js)
|
||||||
|
9. **Quality & config** — hardware detection, runtime configuration
|
||||||
4. Visualization panels
|
10. **Smoke tests** — automated validation
|
||||||
- metrics, presence, economy, portal states, transcripts
|
|
||||||
|
|
||||||
5. Validation
|
|
||||||
- smoke tests, screenshot proof, provenance checks
|
|
||||||
|
|
||||||
6. Game portal layer
|
|
||||||
- Morrowind / portal-specific interaction surfaces
|
|
||||||
|
|
||||||
Do not collapse all of this into one giant app file again.
|
Do not collapse all of this into one giant app file again.
|
||||||
Do not let visual shell code become telemetry authority.
|
Do not let visual shell code become telemetry authority.
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
# 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
|
|
||||||
101
index.html
101
index.html
@@ -318,6 +318,107 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||||
|
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||||
|
<ul style="margin:0; padding-left:15px;">
|
||||||
|
<li>• Require PR for merge ✅</li>
|
||||||
|
<li>• Require 1 approval ✅</li>
|
||||||
|
<li>• Dismiss stale approvals ✅</li>
|
||||||
|
<li>• Require CI ✅ (where available)</li>
|
||||||
|
<li>• Block force push ✅</li>
|
||||||
|
<li>• Block branch deletion ✅</li>
|
||||||
|
<li>• Weekly audit for unreviewed merges ✅</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="mem-palace-container" class="mem-palace-ui">
|
||||||
|
<div class="mem-palace-header">
|
||||||
|
<span id="mem-palace-status">MEMPALACE</span>
|
||||||
|
<button onclick="mineMemPalaceContent()" class="mem-palace-btn">Mine Chat</button>
|
||||||
|
</div>
|
||||||
|
<div class="mem-palace-stats">
|
||||||
|
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||||
|
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||||
|
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="mem-palace-logs" id="mem-palace-logs"></div>
|
||||||
|
</div>
|
||||||
|
<div class="default-reviewers" style="margin-top: 8px; font-size: 12px; color: #aaa;">
|
||||||
|
<strong>DEFAULT REVIEWERS</strong><br>
|
||||||
|
<ul style="margin:0; padding-left:15px;">
|
||||||
|
<li>• <span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
|
||||||
|
<li>• <span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="implementation-status" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||||
|
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||||
|
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
|
||||||
|
<div>• <span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
|
||||||
|
<div>• <span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠️ (CI disabled)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="mem-palace-status" style="position:fixed; right:24px; top:64px; background:rgba(74,240,192,0.1); color:#4af0c0; padding:6px 12px; border-radius:4px; font-family:'Orbitron', sans-serif; font-size:10px; letter-spacing:0.1em;">
|
||||||
|
MEMPALACE INIT
|
||||||
|
</div>
|
||||||
|
<div>• <span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
|
||||||
|
<div>• <span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="mem-palace-container" class="mem-palace-ui">
|
||||||
|
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
|
||||||
|
<div class="mem-palace-stats">
|
||||||
|
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||||
|
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||||
|
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="mem-palace-actions">
|
||||||
|
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||||
|
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
|
||||||
|
</div>
|
||||||
|
<div id="mem-palace-logs" class="mem-palace-logs"></div>
|
||||||
|
</div>
|
||||||
|
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:11px; border-left:2px solid #4af0c0;">
|
||||||
|
<button onclick="mineMemPalace()">Mine Chat</button>
|
||||||
|
<button onclick="searchMemPalace()">Search</button>
|
||||||
|
</div>
|
||||||
|
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||||
|
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:10px; border-left:2px solid #4af0c0;">
|
||||||
|
<button class="mem-palace-mining-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||||
|
<button onclick="searchMemPalace()">Search</button>
|
||||||
|
</div>
|
||||||
|
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||||
|
>>>>>>> replace
|
||||||
|
```
|
||||||
|
|
||||||
|
index.html
|
||||||
|
```html
|
||||||
|
<<<<<<< search
|
||||||
|
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||||
|
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||||
|
<ul style="margin:0; padding-left:15px;">
|
||||||
|
<li>• Require PR for merge ✅</li>
|
||||||
|
<li>• Require 1 approval ✅</li>
|
||||||
|
<li>• Dismiss stale approvals ✅</li>
|
||||||
|
<li>• Require CI ✅ (where available)</li>
|
||||||
|
<li>• Block force push ✅</li>
|
||||||
|
<li>• Block branch deletion ✅</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="default-reviewers" style="margin-top: 8px;">
|
||||||
|
<strong>DEFAULT REVIEWERS</strong><br>
|
||||||
|
<ul style="margin:0; padding-left:15px;">
|
||||||
|
<li>• <span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
|
||||||
|
<li>• <span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="implementation-status" style="margin-top: 10px;">
|
||||||
|
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||||
|
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
|
||||||
|
<div>• <span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
|
||||||
|
<div>• <span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠<> (CI disabled)</div>
|
||||||
|
<div>• <span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
|
||||||
|
<div>• <span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="./app.js"></script>
|
<script type="module" src="./app.js"></script>
|
||||||
|
|||||||
@@ -836,43 +836,8 @@ 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,
|
||||||
|
|||||||
@@ -1,394 +0,0 @@
|
|||||||
#!/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,22 +23,13 @@
|
|||||||
"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": "local",
|
"environment": "production",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
#!/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