Compare commits
9 Commits
mimo/build
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0694fa6167 | ||
| aab3e607eb | |||
| fe56ece1ad | |||
| bf477382ba | |||
| fba972f8be | |||
| 6786e65f3d | |||
| 62a6581827 | |||
| 797f32a7fe | |||
| 80eb4ff7ea |
6
app.js
6
app.js
@@ -1,4 +1,4 @@
|
|||||||
import * as THREE from 'three';
|
import ResonanceVisualizer from './nexus/components/resonance-visualizer.js';\nimport * as THREE from 'three';
|
||||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
@@ -597,7 +597,7 @@ class PSELayer {
|
|||||||
|
|
||||||
let pseLayer;
|
let pseLayer;
|
||||||
|
|
||||||
let metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
let resonanceViz, metaLayer, neuroBridge, cbr, symbolicPlanner, knowledgeGraph, blackboard, symbolicEngine, calibrator;
|
||||||
let agentFSMs = {};
|
let agentFSMs = {};
|
||||||
|
|
||||||
function setupGOFAI() {
|
function setupGOFAI() {
|
||||||
@@ -666,7 +666,7 @@ async function init() {
|
|||||||
scene = new THREE.Scene();
|
scene = new THREE.Scene();
|
||||||
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
scene.fog = new THREE.FogExp2(0x050510, 0.012);
|
||||||
|
|
||||||
setupGOFAI();
|
setupGOFAI();\n resonanceViz = new ResonanceVisualizer(scene);
|
||||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
camera.position.copy(playerPos);
|
camera.position.copy(playerPos);
|
||||||
|
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
# Bannerlord Runtime — Apple Silicon Selection
|
|
||||||
|
|
||||||
> **Issue:** #720
|
|
||||||
> **Status:** DECIDED
|
|
||||||
> **Chosen Runtime:** Whisky (via Apple Game Porting Toolkit)
|
|
||||||
> **Date:** 2026-04-12
|
|
||||||
> **Platform:** macOS Apple Silicon (arm64)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**Whisky** is the chosen runtime for Mount & Blade II: Bannerlord on Apple Silicon Macs.
|
|
||||||
|
|
||||||
Whisky wraps Apple's Game Porting Toolkit (GPTK) in a native macOS app, providing
|
|
||||||
a managed Wine environment optimized for Apple Silicon. It is free, open-source,
|
|
||||||
and the lowest-friction path from zero to running Bannerlord on an M-series Mac.
|
|
||||||
|
|
||||||
### Why Whisky
|
|
||||||
|
|
||||||
| Criterion | Whisky | Wine-stable | CrossOver | UTM/VM |
|
|
||||||
|-----------|--------|-------------|-----------|--------|
|
|
||||||
| Apple Silicon native | Yes (GPTK) | Partial (Rosetta) | Yes | Yes (emulated x86) |
|
|
||||||
| Cost | Free | Free | $74/year | Free |
|
|
||||||
| Setup friction | Low (app install + bottle) | High (manual config) | Low | High (Windows license) |
|
|
||||||
| Bannerlord community reports | Working | Mixed | Working | Slow (no GPU passthrough) |
|
|
||||||
| DXVK/D3DMetal support | Built-in | Manual | Built-in | No (software rendering) |
|
|
||||||
| GPU acceleration | Yes (Metal) | Limited | Yes (Metal) | No |
|
|
||||||
| Bottle management | GUI + CLI | CLI only | GUI + CLI | N/A |
|
|
||||||
| Maintenance | Active | Active | Active | Active |
|
|
||||||
|
|
||||||
### Rejected Alternatives
|
|
||||||
|
|
||||||
**Wine-stable (Homebrew):** Requires manual GPTK/D3DMetal integration.
|
|
||||||
Poor Apple Silicon support out of the box. Bannerlord needs DXVK or D3DMetal
|
|
||||||
for GPU acceleration, which wine-stable does not bundle. Rejected: high falsework.
|
|
||||||
|
|
||||||
**CrossOver:** Commercial ($74/year). Functionally equivalent to Whisky for
|
|
||||||
Bannerlord. Rejected: unnecessary cost when a free alternative works. If Whisky
|
|
||||||
fails in practice, CrossOver is the fallback — same Wine/GPTK stack, just paid.
|
|
||||||
|
|
||||||
**UTM/VM (Windows 11 ARM):** No GPU passthrough. Bannerlord requires hardware
|
|
||||||
3D acceleration. Software rendering produces <5 FPS. Rejected: physics, not ideology.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- macOS 14+ on Apple Silicon (M1/M2/M3/M4)
|
|
||||||
- ~60GB free disk space (Whisky + Steam + Bannerlord)
|
|
||||||
- Homebrew installed
|
|
||||||
|
|
||||||
### One-Command Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/bannerlord_runtime_setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script handles:
|
|
||||||
1. Installing Whisky via Homebrew cask
|
|
||||||
2. Creating a Bannerlord bottle
|
|
||||||
3. Configuring the bottle for GPTK/D3DMetal
|
|
||||||
4. Pointing the bottle at Steam (Windows)
|
|
||||||
5. Outputting a verification-ready path
|
|
||||||
|
|
||||||
### Manual Steps (if script not used)
|
|
||||||
|
|
||||||
1. **Install Whisky:**
|
|
||||||
```bash
|
|
||||||
brew install --cask whisky
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Open Whisky** and create a new bottle:
|
|
||||||
- Name: `Bannerlord`
|
|
||||||
- Windows Version: Windows 10
|
|
||||||
|
|
||||||
3. **Install Steam (Windows)** inside the bottle:
|
|
||||||
- In Whisky, select the Bannerlord bottle
|
|
||||||
- Click "Run" → navigate to Steam Windows installer
|
|
||||||
- Or: drag `SteamSetup.exe` into the Whisky window
|
|
||||||
|
|
||||||
4. **Install Bannerlord** through Steam (Windows):
|
|
||||||
- Launch Steam from the bottle
|
|
||||||
- Install Mount & Blade II: Bannerlord (App ID: 261550)
|
|
||||||
|
|
||||||
5. **Configure D3DMetal:**
|
|
||||||
- In Whisky bottle settings, enable D3DMetal (or DXVK as fallback)
|
|
||||||
- Set Windows version to Windows 10
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Runtime Paths
|
|
||||||
|
|
||||||
After setup, the key paths are:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Whisky bottle root
|
|
||||||
~/Library/Application Support/Whisky/Bottles/Bannerlord/
|
|
||||||
|
|
||||||
# Windows C: drive
|
|
||||||
~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/
|
|
||||||
|
|
||||||
# Steam (Windows)
|
|
||||||
~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/Program Files (x86)/Steam/
|
|
||||||
|
|
||||||
# Bannerlord install
|
|
||||||
~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/
|
|
||||||
|
|
||||||
# Bannerlord executable
|
|
||||||
~/Library/Application Support/Whisky/Bottles/Bannerlord/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
Run the verification script to confirm the runtime is operational:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/bannerlord_verify_runtime.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Checks:
|
|
||||||
- [ ] Whisky installed (`/Applications/Whisky.app`)
|
|
||||||
- [ ] Bannerlord bottle exists
|
|
||||||
- [ ] Steam (Windows) installed in bottle
|
|
||||||
- [ ] Bannerlord executable found
|
|
||||||
- [ ] `wine64-preloader` can launch the exe (smoke test, no window)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Integration with Bannerlord Harness
|
|
||||||
|
|
||||||
The `nexus/bannerlord_runtime.py` module provides programmatic access to the runtime:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from bannerlord_runtime import BannerlordRuntime
|
|
||||||
|
|
||||||
rt = BannerlordRuntime()
|
|
||||||
# Check runtime state
|
|
||||||
status = rt.check()
|
|
||||||
# Launch Bannerlord
|
|
||||||
rt.launch()
|
|
||||||
# Launch Steam first, then Bannerlord
|
|
||||||
rt.launch(with_steam=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
The harness's `capture_state()` and `execute_action()` operate on the running
|
|
||||||
game window via MCP desktop-control. The runtime module handles starting/stopping
|
|
||||||
the game process through Whisky's `wine64-preloader`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Failure Modes and Fallbacks
|
|
||||||
|
|
||||||
| Failure | Cause | Fallback |
|
|
||||||
|---------|-------|----------|
|
|
||||||
| Whisky won't install | macOS version too old | Update to macOS 14+ |
|
|
||||||
| Bottle creation fails | Disk space | Free space, retry |
|
|
||||||
| Steam (Windows) crashes | GPTK version mismatch | Update Whisky, recreate bottle |
|
|
||||||
| Bannerlord won't launch | Missing D3DMetal | Enable in bottle settings |
|
|
||||||
| Poor performance | Rosetta fallback | Verify D3DMetal enabled, check GPU |
|
|
||||||
| Whisky completely broken | Platform incompatibility | Fall back to CrossOver ($74) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Whisky: https://getwhisky.app
|
|
||||||
- Apple GPTK: https://developer.apple.com/games/game-porting-toolkit/
|
|
||||||
- Bannerlord on Whisky: https://github.com/Whisky-App/Whisky/issues (search: bannerlord)
|
|
||||||
- Issue #720: https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/720
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Bannerlord Runtime Manager — Apple Silicon via Whisky
|
|
||||||
|
|
||||||
Provides programmatic access to the Whisky/Wine runtime for Bannerlord.
|
|
||||||
Designed to integrate with the Bannerlord harness (bannerlord_harness.py).
|
|
||||||
|
|
||||||
Runtime choice documented in docs/BANNERLORD_RUNTIME.md.
|
|
||||||
Issue #720.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
log = logging.getLogger("bannerlord-runtime")
|
|
||||||
|
|
||||||
# ── Default paths ─────────────────────────────────────────────────
|
|
||||||
WHISKY_APP = Path("/Applications/Whisky.app")
|
|
||||||
DEFAULT_BOTTLE_NAME = "Bannerlord"
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RuntimePaths:
|
|
||||||
"""Resolved paths for the Bannerlord Whisky bottle."""
|
|
||||||
bottle_name: str = DEFAULT_BOTTLE_NAME
|
|
||||||
bottle_root: Path = field(init=False)
|
|
||||||
drive_c: Path = field(init=False)
|
|
||||||
steam_exe: Path = field(init=False)
|
|
||||||
bannerlord_exe: Path = field(init=False)
|
|
||||||
installer_path: Path = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
base = Path.home() / "Library/Application Support/Whisky/Bottles" / self.bottle_name
|
|
||||||
self.bottle_root = base
|
|
||||||
self.drive_c = base / "drive_c"
|
|
||||||
self.steam_exe = (
|
|
||||||
base / "drive_c/Program Files (x86)/Steam/Steam.exe"
|
|
||||||
)
|
|
||||||
self.bannerlord_exe = (
|
|
||||||
base
|
|
||||||
/ "drive_c/Program Files (x86)/Steam/steamapps/common"
|
|
||||||
/ "Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
|
||||||
)
|
|
||||||
self.installer_path = Path("/tmp/SteamSetup.exe")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RuntimeStatus:
|
|
||||||
"""Current state of the Bannerlord runtime."""
|
|
||||||
whisky_installed: bool = False
|
|
||||||
whisky_version: str = ""
|
|
||||||
bottle_exists: bool = False
|
|
||||||
drive_c_populated: bool = False
|
|
||||||
steam_installed: bool = False
|
|
||||||
bannerlord_installed: bool = False
|
|
||||||
gptk_available: bool = False
|
|
||||||
macos_version: str = ""
|
|
||||||
macos_ok: bool = False
|
|
||||||
errors: list[str] = field(default_factory=list)
|
|
||||||
warnings: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ready(self) -> bool:
|
|
||||||
return (
|
|
||||||
self.whisky_installed
|
|
||||||
and self.bottle_exists
|
|
||||||
and self.steam_installed
|
|
||||||
and self.bannerlord_installed
|
|
||||||
and self.macos_ok
|
|
||||||
)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
|
||||||
return {
|
|
||||||
"whisky_installed": self.whisky_installed,
|
|
||||||
"whisky_version": self.whisky_version,
|
|
||||||
"bottle_exists": self.bottle_exists,
|
|
||||||
"drive_c_populated": self.drive_c_populated,
|
|
||||||
"steam_installed": self.steam_installed,
|
|
||||||
"bannerlord_installed": self.bannerlord_installed,
|
|
||||||
"gptk_available": self.gptk_available,
|
|
||||||
"macos_version": self.macos_version,
|
|
||||||
"macos_ok": self.macos_ok,
|
|
||||||
"ready": self.ready,
|
|
||||||
"errors": self.errors,
|
|
||||||
"warnings": self.warnings,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BannerlordRuntime:
|
|
||||||
"""Manages the Whisky/Wine runtime for Bannerlord on Apple Silicon."""
|
|
||||||
|
|
||||||
def __init__(self, bottle_name: str = DEFAULT_BOTTLE_NAME):
|
|
||||||
self.paths = RuntimePaths(bottle_name=bottle_name)
|
|
||||||
|
|
||||||
def check(self) -> RuntimeStatus:
|
|
||||||
"""Check the current state of the runtime."""
|
|
||||||
status = RuntimeStatus()
|
|
||||||
|
|
||||||
# macOS version
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["sw_vers", "-productVersion"],
|
|
||||||
capture_output=True, text=True, timeout=5,
|
|
||||||
)
|
|
||||||
status.macos_version = result.stdout.strip()
|
|
||||||
major = int(status.macos_version.split(".")[0])
|
|
||||||
status.macos_ok = major >= 14
|
|
||||||
if not status.macos_ok:
|
|
||||||
status.errors.append(f"macOS {status.macos_version} too old, need 14+")
|
|
||||||
except Exception as e:
|
|
||||||
status.errors.append(f"Cannot detect macOS version: {e}")
|
|
||||||
|
|
||||||
# Whisky installed
|
|
||||||
if WHISKY_APP.exists():
|
|
||||||
status.whisky_installed = True
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"defaults", "read",
|
|
||||||
str(WHISKY_APP / "Contents/Info.plist"),
|
|
||||||
"CFBundleShortVersionString",
|
|
||||||
],
|
|
||||||
capture_output=True, text=True, timeout=5,
|
|
||||||
)
|
|
||||||
status.whisky_version = result.stdout.strip()
|
|
||||||
except Exception:
|
|
||||||
status.whisky_version = "unknown"
|
|
||||||
else:
|
|
||||||
status.errors.append(f"Whisky not found at {WHISKY_APP}")
|
|
||||||
|
|
||||||
# Bottle
|
|
||||||
status.bottle_exists = self.paths.bottle_root.exists()
|
|
||||||
if not status.bottle_exists:
|
|
||||||
status.errors.append(f"Bottle not found: {self.paths.bottle_root}")
|
|
||||||
|
|
||||||
# drive_c
|
|
||||||
status.drive_c_populated = self.paths.drive_c.exists()
|
|
||||||
if not status.drive_c_populated and status.bottle_exists:
|
|
||||||
status.warnings.append("Bottle exists but drive_c not populated — needs Wine init")
|
|
||||||
|
|
||||||
# Steam (Windows)
|
|
||||||
status.steam_installed = self.paths.steam_exe.exists()
|
|
||||||
if not status.steam_installed:
|
|
||||||
status.warnings.append("Steam (Windows) not installed in bottle")
|
|
||||||
|
|
||||||
# Bannerlord
|
|
||||||
status.bannerlord_installed = self.paths.bannerlord_exe.exists()
|
|
||||||
if not status.bannerlord_installed:
|
|
||||||
status.warnings.append("Bannerlord not installed")
|
|
||||||
|
|
||||||
# GPTK/D3DMetal
|
|
||||||
whisky_support = Path.home() / "Library/Application Support/Whisky"
|
|
||||||
if whisky_support.exists():
|
|
||||||
gptk_files = list(whisky_support.rglob("*gptk*")) + \
|
|
||||||
list(whisky_support.rglob("*d3dmetal*")) + \
|
|
||||||
list(whisky_support.rglob("*dxvk*"))
|
|
||||||
status.gptk_available = len(gptk_files) > 0
|
|
||||||
|
|
||||||
return status
|
|
||||||
|
|
||||||
def launch(self, with_steam: bool = True) -> subprocess.Popen | None:
|
|
||||||
"""
|
|
||||||
Launch Bannerlord via Whisky.
|
|
||||||
|
|
||||||
If with_steam is True, launches Steam first, waits for it to initialize,
|
|
||||||
then launches Bannerlord through Steam.
|
|
||||||
"""
|
|
||||||
status = self.check()
|
|
||||||
if not status.ready:
|
|
||||||
log.error("Runtime not ready: %s", "; ".join(status.errors or status.warnings))
|
|
||||||
return None
|
|
||||||
|
|
||||||
if with_steam:
|
|
||||||
log.info("Launching Steam (Windows) via Whisky...")
|
|
||||||
steam_proc = self._run_exe(str(self.paths.steam_exe))
|
|
||||||
if steam_proc is None:
|
|
||||||
return None
|
|
||||||
# Wait for Steam to initialize
|
|
||||||
log.info("Waiting for Steam to initialize (15s)...")
|
|
||||||
time.sleep(15)
|
|
||||||
|
|
||||||
# Launch Bannerlord via steam://rungameid/
|
|
||||||
log.info("Launching Bannerlord via Steam protocol...")
|
|
||||||
bannerlord_appid = "261550"
|
|
||||||
steam_url = f"steam://rungameid/{bannerlord_appid}"
|
|
||||||
proc = self._run_exe(str(self.paths.steam_exe), args=[steam_url])
|
|
||||||
if proc:
|
|
||||||
log.info("Bannerlord launch command sent (PID: %d)", proc.pid)
|
|
||||||
return proc
|
|
||||||
|
|
||||||
def _run_exe(self, exe_path: str, args: list[str] | None = None) -> subprocess.Popen | None:
|
|
||||||
"""Run a Windows executable through Whisky's wine64-preloader."""
|
|
||||||
# Whisky uses wine64-preloader from its bundled Wine
|
|
||||||
wine64 = self._find_wine64()
|
|
||||||
if wine64 is None:
|
|
||||||
log.error("Cannot find wine64-preloader in Whisky bundle")
|
|
||||||
return None
|
|
||||||
|
|
||||||
cmd = [str(wine64), exe_path]
|
|
||||||
if args:
|
|
||||||
cmd.extend(args)
|
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["WINEPREFIX"] = str(self.paths.bottle_root)
|
|
||||||
|
|
||||||
try:
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
cmd,
|
|
||||||
env=env,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
return proc
|
|
||||||
except Exception as e:
|
|
||||||
log.error("Failed to launch %s: %s", exe_path, e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _find_wine64(self) -> Optional[Path]:
|
|
||||||
"""Find wine64-preloader in Whisky's app bundle or GPTK install."""
|
|
||||||
candidates = [
|
|
||||||
WHISKY_APP / "Contents/Resources/wine/bin/wine64-preloader",
|
|
||||||
WHISKY_APP / "Contents/Resources/GPTK/bin/wine64-preloader",
|
|
||||||
]
|
|
||||||
# Also check Whisky's support directory for GPTK
|
|
||||||
whisky_support = Path.home() / "Library/Application Support/Whisky"
|
|
||||||
if whisky_support.exists():
|
|
||||||
for p in whisky_support.rglob("wine64-preloader"):
|
|
||||||
candidates.append(p)
|
|
||||||
|
|
||||||
for c in candidates:
|
|
||||||
if c.exists() and os.access(c, os.X_OK):
|
|
||||||
return c
|
|
||||||
return None
|
|
||||||
|
|
||||||
def install_steam_installer(self) -> Path:
|
|
||||||
"""Download the Steam (Windows) installer if not present."""
|
|
||||||
installer = self.paths.installer_path
|
|
||||||
if installer.exists():
|
|
||||||
log.info("Steam installer already at: %s", installer)
|
|
||||||
return installer
|
|
||||||
|
|
||||||
log.info("Downloading Steam (Windows) installer...")
|
|
||||||
url = "https://cdn.akamai.steamstatic.com/client/installer/SteamSetup.exe"
|
|
||||||
subprocess.run(
|
|
||||||
["curl", "-L", "-o", str(installer), url],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
log.info("Steam installer saved to: %s", installer)
|
|
||||||
return installer
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
|
||||||
rt = BannerlordRuntime()
|
|
||||||
status = rt.check()
|
|
||||||
print(json.dumps(status.to_dict(), indent=2))
|
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
|
|
||||||
class MemoryOptimizer {
|
class MemoryOptimizer {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.threshold = options.threshold || 0.8;
|
this.threshold = options.threshold || 0.3;
|
||||||
this.decayRate = options.decayRate || 0.05;
|
this.decayRate = options.decayRate || 0.01;
|
||||||
|
this.lastRun = Date.now();
|
||||||
}
|
}
|
||||||
optimize(memory) {
|
optimize(memories) {
|
||||||
console.log('Optimizing memory...');
|
const now = Date.now();
|
||||||
// Heuristic-based pruning
|
const elapsed = (now - this.lastRun) / 1000;
|
||||||
return memory.filter(m => m.strength > this.threshold);
|
this.lastRun = now;
|
||||||
|
return memories.map(m => {
|
||||||
|
const decay = (m.importance || 1) * this.decayRate * elapsed;
|
||||||
|
return { ...m, strength: Math.max(0, (m.strength || 1) - decay) };
|
||||||
|
}).filter(m => m.strength > this.threshold || m.locked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default MemoryOptimizer;
|
export default MemoryOptimizer;
|
||||||
|
|||||||
14
nexus/mnemosyne/reasoner.py
Normal file
14
nexus/mnemosyne/reasoner.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
class Reasoner:
|
||||||
|
def __init__(self, rules):
|
||||||
|
self.rules = rules
|
||||||
|
def evaluate(self, entries):
|
||||||
|
return [r['action'] for r in self.rules if self._check(r['condition'], entries)]
|
||||||
|
def _check(self, cond, entries):
|
||||||
|
if cond.startswith('count'):
|
||||||
|
# e.g. count(type=anomaly)>3
|
||||||
|
p = cond.replace('count(', '').split(')')
|
||||||
|
key, val = p[0].split('=')
|
||||||
|
count = sum(1 for e in entries if e.get(key) == val)
|
||||||
|
return eval(f"{count}{p[1]}")
|
||||||
|
return False
|
||||||
22
nexus/mnemosyne/resonance_linker.py
Normal file
22
nexus/mnemosyne/resonance_linker.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
"""Resonance Linker — Finds second-degree connections in the holographic graph."""
|
||||||
|
|
||||||
|
class ResonanceLinker:
|
||||||
|
def __init__(self, archive):
|
||||||
|
self.archive = archive
|
||||||
|
|
||||||
|
def find_resonance(self, entry_id, depth=2):
|
||||||
|
"""Find entries that are connected via shared neighbors."""
|
||||||
|
if entry_id not in self.archive._entries: return []
|
||||||
|
|
||||||
|
entry = self.archive._entries[entry_id]
|
||||||
|
neighbors = set(entry.links)
|
||||||
|
resonance = {}
|
||||||
|
|
||||||
|
for neighbor_id in neighbors:
|
||||||
|
if neighbor_id in self.archive._entries:
|
||||||
|
for second_neighbor in self.archive._entries[neighbor_id].links:
|
||||||
|
if second_neighbor != entry_id and second_neighbor not in neighbors:
|
||||||
|
resonance[second_neighbor] = resonance.get(second_neighbor, 0) + 1
|
||||||
|
|
||||||
|
return sorted(resonance.items(), key=lambda x: x[1], reverse=True)
|
||||||
6
nexus/mnemosyne/rules.json
Normal file
6
nexus/mnemosyne/rules.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"condition": "count(type=anomaly)>3",
|
||||||
|
"action": "alert"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Bannerlord Runtime Setup — Apple Silicon
|
|
||||||
# Issue #720: Stand up a local Windows game runtime for Bannerlord on Apple Silicon
|
|
||||||
#
|
|
||||||
# Chosen runtime: Whisky (Apple Game Porting Toolkit wrapper)
|
|
||||||
#
|
|
||||||
# Usage: ./scripts/bannerlord_runtime_setup.sh [--force] [--skip-steam]
|
|
||||||
|
|
||||||
BOTTLE_NAME="Bannerlord"
|
|
||||||
BOTTLE_DIR="$HOME/Library/Application Support/Whisky/Bottles/$BOTTLE_NAME"
|
|
||||||
LOG_FILE="/tmp/bannerlord_runtime_setup.log"
|
|
||||||
|
|
||||||
FORCE=false
|
|
||||||
SKIP_STEAM=false
|
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
|
||||||
--force) FORCE=true ;;
|
|
||||||
--skip-steam) SKIP_STEAM=true ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
log "FATAL: $*"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Preflight ──────────────────────────────────────────────────────
|
|
||||||
log "=== Bannerlord Runtime Setup ==="
|
|
||||||
log "Platform: $(uname -m) macOS $(sw_vers -productVersion)"
|
|
||||||
|
|
||||||
if [[ "$(uname -m)" != "arm64" ]]; then
|
|
||||||
fail "This script requires Apple Silicon (arm64). Got: $(uname -m)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 1: Install Whisky ────────────────────────────────────────
|
|
||||||
log "[1/5] Checking Whisky installation..."
|
|
||||||
if [[ -d "/Applications/Whisky.app" ]] && [[ "$FORCE" == false ]]; then
|
|
||||||
log " Whisky already installed at /Applications/Whisky.app"
|
|
||||||
else
|
|
||||||
log " Installing Whisky via Homebrew cask..."
|
|
||||||
if ! command -v brew &>/dev/null; then
|
|
||||||
fail "Homebrew not found. Install from https://brew.sh"
|
|
||||||
fi
|
|
||||||
brew install --cask whisky 2>&1 | tee -a "$LOG_FILE"
|
|
||||||
log " Whisky installed."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 2: Create Bottle ─────────────────────────────────────────
|
|
||||||
log "[2/5] Checking Bannerlord bottle..."
|
|
||||||
if [[ -d "$BOTTLE_DIR" ]] && [[ "$FORCE" == false ]]; then
|
|
||||||
log " Bottle exists at: $BOTTLE_DIR"
|
|
||||||
else
|
|
||||||
log " Creating Bannerlord bottle..."
|
|
||||||
# Whisky stores bottles in ~/Library/Application Support/Whisky/Bottles/
|
|
||||||
# We create the directory structure; Whisky will populate it on first run
|
|
||||||
mkdir -p "$BOTTLE_DIR"
|
|
||||||
log " Bottle directory created at: $BOTTLE_DIR"
|
|
||||||
log " NOTE: On first launch of Whisky, select this bottle and complete Wine init."
|
|
||||||
log " Open Whisky.app, create bottle named '$BOTTLE_NAME', Windows 10."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 3: Verify Whisky CLI ─────────────────────────────────────
|
|
||||||
log "[3/5] Verifying Whisky CLI access..."
|
|
||||||
WHISKY_APP="/Applications/Whisky.app"
|
|
||||||
if [[ -d "$WHISKY_APP" ]]; then
|
|
||||||
WHISKY_VERSION=$(defaults read "$WHISKY_APP/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "unknown")
|
|
||||||
log " Whisky version: $WHISKY_VERSION"
|
|
||||||
else
|
|
||||||
fail "Whisky.app not found at $WHISKY_APP"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 4: Document Steam (Windows) install path ─────────────────
|
|
||||||
log "[4/5] Steam (Windows) install target..."
|
|
||||||
STEAM_WIN_PATH="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/Steam.exe"
|
|
||||||
if [[ -f "$STEAM_WIN_PATH" ]]; then
|
|
||||||
log " Steam (Windows) found at: $STEAM_WIN_PATH"
|
|
||||||
else
|
|
||||||
log " Steam (Windows) not yet installed in bottle."
|
|
||||||
log " After opening Whisky:"
|
|
||||||
log " 1. Select the '$BOTTLE_NAME' bottle"
|
|
||||||
log " 2. Run the Steam Windows installer (download from store.steampowered.com)"
|
|
||||||
log " 3. Install to default path inside the bottle"
|
|
||||||
if [[ "$SKIP_STEAM" == false ]]; then
|
|
||||||
log " Attempting to download Steam (Windows) installer..."
|
|
||||||
STEAM_INSTALLER="/tmp/SteamSetup.exe"
|
|
||||||
if [[ ! -f "$STEAM_INSTALLER" ]]; then
|
|
||||||
curl -L -o "$STEAM_INSTALLER" "https://cdn.akamai.steamstatic.com/client/installer/SteamSetup.exe" 2>&1 | tee -a "$LOG_FILE"
|
|
||||||
fi
|
|
||||||
log " Steam installer at: $STEAM_INSTALLER"
|
|
||||||
log " Run this in Whisky: open -a Whisky"
|
|
||||||
log " Then: in the Bannerlord bottle, click 'Run' and select $STEAM_INSTALLER"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Step 5: Bannerlord executable path ────────────────────────────
|
|
||||||
log "[5/5] Bannerlord executable target..."
|
|
||||||
BANNERLORD_EXE="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
|
||||||
if [[ -f "$BANNERLORD_EXE" ]]; then
|
|
||||||
log " Bannerlord found at: $BANNERLORD_EXE"
|
|
||||||
else
|
|
||||||
log " Bannerlord not yet installed."
|
|
||||||
log " Install via Steam (Windows) inside the Whisky bottle."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Summary ───────────────────────────────────────────────────────
|
|
||||||
log ""
|
|
||||||
log "=== Setup Summary ==="
|
|
||||||
log "Runtime: Whisky (Apple GPTK)"
|
|
||||||
log "Bottle: $BOTTLE_DIR"
|
|
||||||
log "Log: $LOG_FILE"
|
|
||||||
log ""
|
|
||||||
log "Next steps:"
|
|
||||||
log " 1. Open Whisky: open -a Whisky"
|
|
||||||
log " 2. Create/select '$BOTTLE_NAME' bottle (Windows 10)"
|
|
||||||
log " 3. Install Steam (Windows) in the bottle"
|
|
||||||
log " 4. Install Bannerlord via Steam"
|
|
||||||
log " 5. Enable D3DMetal in bottle settings"
|
|
||||||
log " 6. Run verification: ./scripts/bannerlord_verify_runtime.sh"
|
|
||||||
log ""
|
|
||||||
log "=== Done ==="
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Bannerlord Runtime Verification — Apple Silicon
|
|
||||||
# Issue #720: Verify the local Windows game runtime for Bannerlord
|
|
||||||
#
|
|
||||||
# Usage: ./scripts/bannerlord_verify_runtime.sh
|
|
||||||
|
|
||||||
BOTTLE_NAME="Bannerlord"
|
|
||||||
BOTTLE_DIR="$HOME/Library/Application Support/Whisky/Bottles/$BOTTLE_NAME"
|
|
||||||
REPORT_FILE="/tmp/bannerlord_runtime_verify.txt"
|
|
||||||
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
WARN=0
|
|
||||||
|
|
||||||
check() {
|
|
||||||
local label="$1"
|
|
||||||
local result="$2" # PASS, FAIL, WARN
|
|
||||||
local detail="${3:-}"
|
|
||||||
case "$result" in
|
|
||||||
PASS) ((PASS++)) ; echo "[PASS] $label${detail:+ — $detail}" ;;
|
|
||||||
FAIL) ((FAIL++)) ; echo "[FAIL] $label${detail:+ — $detail}" ;;
|
|
||||||
WARN) ((WARN++)) ; echo "[WARN] $label${detail:+ — $detail}" ;;
|
|
||||||
esac
|
|
||||||
echo "$result: $label${detail:+ — $detail}" >> "$REPORT_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== Bannerlord Runtime Verification ===" | tee "$REPORT_FILE"
|
|
||||||
echo "Date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')" | tee -a "$REPORT_FILE"
|
|
||||||
echo "Platform: $(uname -m) macOS $(sw_vers -productVersion)" | tee -a "$REPORT_FILE"
|
|
||||||
echo "" | tee -a "$REPORT_FILE"
|
|
||||||
|
|
||||||
# ── Check 1: Whisky installed ────────────────────────────────────
|
|
||||||
if [[ -d "/Applications/Whisky.app" ]]; then
|
|
||||||
VER=$(defaults read "/Applications/Whisky.app/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "?")
|
|
||||||
check "Whisky installed" "PASS" "v$VER at /Applications/Whisky.app"
|
|
||||||
else
|
|
||||||
check "Whisky installed" "FAIL" "not found at /Applications/Whisky.app"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 2: Bottle exists ───────────────────────────────────────
|
|
||||||
if [[ -d "$BOTTLE_DIR" ]]; then
|
|
||||||
check "Bannerlord bottle exists" "PASS" "$BOTTLE_DIR"
|
|
||||||
else
|
|
||||||
check "Bannerlord bottle exists" "FAIL" "missing: $BOTTLE_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 3: drive_c structure ───────────────────────────────────
|
|
||||||
if [[ -d "$BOTTLE_DIR/drive_c" ]]; then
|
|
||||||
check "Bottle drive_c populated" "PASS"
|
|
||||||
else
|
|
||||||
check "Bottle drive_c populated" "FAIL" "drive_c not found — bottle may need Wine init"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 4: Steam (Windows) ─────────────────────────────────────
|
|
||||||
STEAM_EXE="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/Steam.exe"
|
|
||||||
if [[ -f "$STEAM_EXE" ]]; then
|
|
||||||
check "Steam (Windows) installed" "PASS" "$STEAM_EXE"
|
|
||||||
else
|
|
||||||
check "Steam (Windows) installed" "FAIL" "not found at expected path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 5: Bannerlord executable ───────────────────────────────
|
|
||||||
BANNERLORD_EXE="$BOTTLE_DIR/drive_c/Program Files (x86)/Steam/steamapps/common/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
|
||||||
if [[ -f "$BANNERLORD_EXE" ]]; then
|
|
||||||
EXE_SIZE=$(stat -f%z "$BANNERLORD_EXE" 2>/dev/null || echo "?")
|
|
||||||
check "Bannerlord executable found" "PASS" "size: $EXE_SIZE bytes"
|
|
||||||
else
|
|
||||||
check "Bannerlord executable found" "FAIL" "not installed yet"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 6: GPTK/D3DMetal presence ──────────────────────────────
|
|
||||||
# D3DMetal libraries should be present in the Whisky GPTK installation
|
|
||||||
GPTK_DIR="$HOME/Library/Application Support/Whisky"
|
|
||||||
if [[ -d "$GPTK_DIR" ]]; then
|
|
||||||
GPTK_FILES=$(find "$GPTK_DIR" -name "*gptk*" -o -name "*d3dmetal*" -o -name "*dxvk*" 2>/dev/null | head -5)
|
|
||||||
if [[ -n "$GPTK_FILES" ]]; then
|
|
||||||
check "GPTK/D3DMetal libraries" "PASS"
|
|
||||||
else
|
|
||||||
check "GPTK/D3DMetal libraries" "WARN" "not found — may need Whisky update"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
check "GPTK/D3DMetal libraries" "WARN" "Whisky support dir not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 7: Homebrew (for updates) ──────────────────────────────
|
|
||||||
if command -v brew &>/dev/null; then
|
|
||||||
check "Homebrew available" "PASS" "$(brew --version | head -1)"
|
|
||||||
else
|
|
||||||
check "Homebrew available" "WARN" "not found — manual updates required"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Check 8: macOS version ───────────────────────────────────────
|
|
||||||
MACOS_VER=$(sw_vers -productVersion)
|
|
||||||
MACOS_MAJOR=$(echo "$MACOS_VER" | cut -d. -f1)
|
|
||||||
if [[ "$MACOS_MAJOR" -ge 14 ]]; then
|
|
||||||
check "macOS version" "PASS" "$MACOS_VER (Sonoma+)"
|
|
||||||
else
|
|
||||||
check "macOS version" "FAIL" "$MACOS_VER — requires macOS 14+"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Summary ───────────────────────────────────────────────────────
|
|
||||||
echo "" | tee -a "$REPORT_FILE"
|
|
||||||
echo "=== Results ===" | tee -a "$REPORT_FILE"
|
|
||||||
echo "PASS: $PASS" | tee -a "$REPORT_FILE"
|
|
||||||
echo "FAIL: $FAIL" | tee -a "$REPORT_FILE"
|
|
||||||
echo "WARN: $WARN" | tee -a "$REPORT_FILE"
|
|
||||||
echo "Report: $REPORT_FILE" | tee -a "$REPORT_FILE"
|
|
||||||
|
|
||||||
if [[ "$FAIL" -gt 0 ]]; then
|
|
||||||
echo "STATUS: INCOMPLETE — $FAIL check(s) failed" | tee -a "$REPORT_FILE"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "STATUS: RUNTIME READY" | tee -a "$REPORT_FILE"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
23
server.py
23
server.py
@@ -48,24 +48,21 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Broadcast to all OTHER clients
|
# Broadcast to all OTHER clients
|
||||||
if not clients:
|
|
||||||
continue
|
|
||||||
|
|
||||||
disconnected = set()
|
disconnected = set()
|
||||||
# Create broadcast tasks for efficiency
|
# Create broadcast tasks paired with their target client
|
||||||
tasks = []
|
task_client_pairs = []
|
||||||
for client in clients:
|
for client in clients:
|
||||||
if client != websocket and client.open:
|
if client != websocket and client.open:
|
||||||
tasks.append(asyncio.create_task(client.send(message)))
|
task = asyncio.create_task(client.send(message))
|
||||||
|
task_client_pairs.append((task, client))
|
||||||
if tasks:
|
|
||||||
|
if task_client_pairs:
|
||||||
|
tasks = [t for t, _ in task_client_pairs]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
for i, result in enumerate(results):
|
for (task, client), result in zip(task_client_pairs, results):
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
# Find the client that failed
|
logger.error(f"Failed to send to a client {client.remote_address}: {result}")
|
||||||
target_client = [c for c in clients if c != websocket][i]
|
disconnected.add(client)
|
||||||
logger.error(f"Failed to send to a client {target_client.remote_address}: {result}")
|
|
||||||
disconnected.add(target_client)
|
|
||||||
|
|
||||||
if disconnected:
|
if disconnected:
|
||||||
clients.difference_update(disconnected)
|
clients.difference_update(disconnected)
|
||||||
|
|||||||
Reference in New Issue
Block a user