Compare commits
98 Commits
mimo/build
...
mimo/code/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ce674577 | ||
| e28775372f | |||
| a7d0116753 | |||
| 36bc60f940 | |||
| b6ce446b8c | |||
| 0a990a8880 | |||
| 03539cf947 | |||
| 16bf1555d6 | |||
| e49b1df8d7 | |||
| 2bf607ca1e | |||
| c18583b346 | |||
| 02748d0a03 | |||
| 43be5c5a52 | |||
| e55d9c6ebd | |||
| 9646ce6730 | |||
| 8ae7d778d2 | |||
| 47b6071dee | |||
| 160f2281b7 | |||
| 71fc4ab20b | |||
| ec8a2fc8fd | |||
| d78656ca07 | |||
| 5788925f38 | |||
| ce546e0f42 | |||
| 8a1623dbcd | |||
| 820b8635a6 | |||
| 2ef37d1d61 | |||
| af80963d4f | |||
| fb6ed0d7bb | |||
| 486c98894a | |||
| 8d92ea9905 | |||
| 95f3434c08 | |||
| 6dce93d948 | |||
| be45c7f88b | |||
| 77f2ad4a80 | |||
| 23a89f22b7 | |||
| 0317e72d6e | |||
| 0cf152317d | |||
| 3aa084bdb6 | |||
| e31c8179da | |||
| 48fcadb4e8 | |||
|
|
879cc2a336 | ||
|
|
b2bb253157 | ||
|
|
68d2fa7abb | ||
|
|
b1d86f6a07 | ||
|
|
2d7a531f82 | ||
|
|
d37b9ae074 | ||
| dd05abe4f7 | |||
| 05b01ad771 | |||
| 2adcbd0372 | |||
|
|
25f07dcc63 | ||
|
|
64686d81a3 | ||
|
|
c13b9b52d5 | ||
|
|
fbafa10ee7 | ||
|
|
3e9b9a215a | ||
| 33fbe851b2 | |||
| fe9139262d | |||
| 0b352bc436 | |||
| a03db862fa | |||
| 1cb28fec34 | |||
| b5ae7f4c2c | |||
| 46da2a8c81 | |||
| a9f4a0651f | |||
| 63334387e6 | |||
| 360155f1b6 | |||
| 68a6fad1d6 | |||
| ff7fef4f73 | |||
| 298a2bf3f6 | |||
| 060832a8ed | |||
| 7d78f4bfbf | |||
| 32171242f8 | |||
| b06cdf3fd3 | |||
| f2d7466849 | |||
| e1fab2b5e1 | |||
| e3d383ca12 | |||
| d23583e269 | |||
| 09f6ac53d4 | |||
| 3e8e7c6cd7 | |||
|
|
67d3b784fd | ||
|
|
15b5417ca6 | ||
| 9e00962d82 | |||
| 80e7b5ad16 | |||
| 706ecc2b00 | |||
| 942e9a03c7 | |||
| 83dac15b62 | |||
| 60bc0b3899 | |||
| 155fcc3230 | |||
| fc888e8c75 | |||
| bb7db603f3 | |||
| 55198170f8 | |||
| 5f43ebbaaa | |||
| fb8c2d0bf2 | |||
| c5a1400e2d | |||
| 531a817ce1 | |||
| c59dd90b9b | |||
| 5c5f2032b4 | |||
| c4983aa0d7 | |||
| 61eca3096a | |||
|
|
536744ec21 |
51
.gitea.yml
51
.gitea.yml
@@ -15,3 +15,54 @@ protection:
|
|||||||
- perplexity
|
- perplexity
|
||||||
required_reviewers:
|
required_reviewers:
|
||||||
- Timmy # Owner gate for hermes-agent
|
- Timmy # Owner gate for hermes-agent
|
||||||
|
main:
|
||||||
|
require_pull_request: true
|
||||||
|
required_approvals: 1
|
||||||
|
dismiss_stale_approvals: true
|
||||||
|
require_ci_to_pass: true
|
||||||
|
block_force_push: true
|
||||||
|
block_deletion: true
|
||||||
|
>>>>>>> replace
|
||||||
|
</source>
|
||||||
|
|
||||||
|
CODEOWNERS
|
||||||
|
<source>
|
||||||
|
<<<<<<< search
|
||||||
|
protection:
|
||||||
|
main:
|
||||||
|
required_status_checks:
|
||||||
|
- "ci/unit-tests"
|
||||||
|
- "ci/integration"
|
||||||
|
required_pull_request_reviews:
|
||||||
|
- "1 approval"
|
||||||
|
restrictions:
|
||||||
|
- "block force push"
|
||||||
|
- "block deletion"
|
||||||
|
enforce_admins: true
|
||||||
|
|
||||||
|
the-nexus:
|
||||||
|
required_status_checks: []
|
||||||
|
required_pull_request_reviews:
|
||||||
|
- "1 approval"
|
||||||
|
restrictions:
|
||||||
|
- "block force push"
|
||||||
|
- "block deletion"
|
||||||
|
enforce_admins: true
|
||||||
|
|
||||||
|
timmy-home:
|
||||||
|
required_status_checks: []
|
||||||
|
required_pull_request_reviews:
|
||||||
|
- "1 approval"
|
||||||
|
restrictions:
|
||||||
|
- "block force push"
|
||||||
|
- "block deletion"
|
||||||
|
enforce_admins: true
|
||||||
|
|
||||||
|
timmy-config:
|
||||||
|
required_status_checks: []
|
||||||
|
required_pull_request_reviews:
|
||||||
|
- "1 approval"
|
||||||
|
restrictions:
|
||||||
|
- "block force push"
|
||||||
|
- "block deletion"
|
||||||
|
enforce_admins: true
|
||||||
|
|||||||
6
app.js
6
app.js
@@ -4,7 +4,6 @@ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|||||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
|
||||||
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
import { SpatialMemory } from './nexus/components/spatial-memory.js';
|
||||||
import { SpatialAudio } from './nexus/components/spatial-audio.js';
|
|
||||||
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
import { MemoryBirth } from './nexus/components/memory-birth.js';
|
||||||
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||||||
import { MemoryInspect } from './nexus/components/memory-inspect.js';
|
import { MemoryInspect } from './nexus/components/memory-inspect.js';
|
||||||
@@ -716,8 +715,6 @@ async function init() {
|
|||||||
MemoryBirth.init(scene);
|
MemoryBirth.init(scene);
|
||||||
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
MemoryBirth.wrapSpatialMemory(SpatialMemory);
|
||||||
SpatialMemory.setCamera(camera);
|
SpatialMemory.setCamera(camera);
|
||||||
SpatialAudio.init(camera, scene);
|
|
||||||
SpatialAudio.bindSpatialMemory(SpatialMemory);
|
|
||||||
MemoryInspect.init({ onNavigate: _navigateToMemory });
|
MemoryInspect.init({ onNavigate: _navigateToMemory });
|
||||||
MemoryPulse.init(SpatialMemory);
|
MemoryPulse.init(SpatialMemory);
|
||||||
updateLoad(90);
|
updateLoad(90);
|
||||||
@@ -727,7 +724,7 @@ async function init() {
|
|||||||
// Mnemosyne: Periodic GOFAI Optimization
|
// Mnemosyne: Periodic GOFAI Optimization
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.info('[Mnemosyne] Running periodic optimization...');
|
console.info('[Mnemosyne] Running periodic optimization...');
|
||||||
MemoryOptimizer.optimize(SpatialMemory);
|
MemoryOptimizer.decaySpatialMemory(SpatialMemory);
|
||||||
}, 1000 * 60 * 10); // Every 10 minutes
|
}, 1000 * 60 * 10); // Every 10 minutes
|
||||||
|
|
||||||
fetchGiteaData();
|
fetchGiteaData();
|
||||||
@@ -2929,7 +2926,6 @@ function gameLoop() {
|
|||||||
// Project Mnemosyne - Memory Orb Animation
|
// Project Mnemosyne - Memory Orb Animation
|
||||||
if (typeof animateMemoryOrbs === 'function') {
|
if (typeof animateMemoryOrbs === 'function') {
|
||||||
SpatialMemory.update(delta);
|
SpatialMemory.update(delta);
|
||||||
SpatialAudio.update(delta);
|
|
||||||
MemoryBirth.update(delta);
|
MemoryBirth.update(delta);
|
||||||
MemoryPulse.update();
|
MemoryPulse.update();
|
||||||
animateMemoryOrbs(delta);
|
animateMemoryOrbs(delta);
|
||||||
|
|||||||
@@ -586,8 +586,8 @@ def alert_on_failure(report: HealthReport, dry_run: bool = False) -> None:
|
|||||||
logger.info("Created alert issue #%d", result["number"])
|
logger.info("Created alert issue #%d", result["number"])
|
||||||
|
|
||||||
|
|
||||||
def run_once(args: argparse.Namespace) -> tuple:
|
def run_once(args: argparse.Namespace) -> bool:
|
||||||
"""Run one health check cycle. Returns (healthy, report)."""
|
"""Run one health check cycle. Returns True if healthy."""
|
||||||
report = run_health_checks(
|
report = run_health_checks(
|
||||||
ws_host=args.ws_host,
|
ws_host=args.ws_host,
|
||||||
ws_port=args.ws_port,
|
ws_port=args.ws_port,
|
||||||
@@ -615,7 +615,7 @@ def run_once(args: argparse.Namespace) -> tuple:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass # never crash the watchdog over its own heartbeat
|
pass # never crash the watchdog over its own heartbeat
|
||||||
|
|
||||||
return report.overall_healthy, report
|
return report.overall_healthy
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -678,15 +678,21 @@ def main():
|
|||||||
signal.signal(signal.SIGINT, _handle_sigterm)
|
signal.signal(signal.SIGINT, _handle_sigterm)
|
||||||
|
|
||||||
while _running:
|
while _running:
|
||||||
run_once(args) # (healthy, report) — not needed in watch mode
|
run_once(args)
|
||||||
for _ in range(args.interval):
|
for _ in range(args.interval):
|
||||||
if not _running:
|
if not _running:
|
||||||
break
|
break
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
healthy, report = run_once(args)
|
healthy = run_once(args)
|
||||||
|
|
||||||
if args.output_json:
|
if args.output_json:
|
||||||
|
report = run_health_checks(
|
||||||
|
ws_host=args.ws_host,
|
||||||
|
ws_port=args.ws_port,
|
||||||
|
heartbeat_path=Path(args.heartbeat_path),
|
||||||
|
stale_threshold=args.stale_threshold,
|
||||||
|
)
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"healthy": report.overall_healthy,
|
"healthy": report.overall_healthy,
|
||||||
"timestamp": report.timestamp,
|
"timestamp": report.timestamp,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
|
||||||
class MemoryOptimizer {
|
class MemoryOptimizer {
|
||||||
|
static _lastRun = Date.now();
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.threshold = options.threshold || 0.3;
|
this.threshold = options.threshold || 0.3;
|
||||||
this.decayRate = options.decayRate || 0.01;
|
this.decayRate = options.decayRate || 0.01;
|
||||||
this.lastRun = Date.now();
|
this.lastRun = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
optimize(memories) {
|
optimize(memories) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const elapsed = (now - this.lastRun) / 1000;
|
const elapsed = (now - this.lastRun) / 1000;
|
||||||
@@ -14,5 +17,36 @@ class MemoryOptimizer {
|
|||||||
return { ...m, strength: Math.max(0, (m.strength || 1) - decay) };
|
return { ...m, strength: Math.max(0, (m.strength || 1) - decay) };
|
||||||
}).filter(m => m.strength > this.threshold || m.locked);
|
}).filter(m => m.strength > this.threshold || m.locked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static decay pass — updates SpatialMemory crystals in-place.
|
||||||
|
* Call as: MemoryOptimizer.decaySpatialMemory(spatialMemoryModule)
|
||||||
|
*/
|
||||||
|
static decaySpatialMemory(spatialMemory, { decayRate = 0.005, threshold = 0.15 } = {}) {
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = (now - MemoryOptimizer._lastRun) / 1000;
|
||||||
|
MemoryOptimizer._lastRun = now;
|
||||||
|
|
||||||
|
const memEntries = spatialMemory.getAllMemoryEntries();
|
||||||
|
if (!memEntries || memEntries.length === 0) return 0;
|
||||||
|
|
||||||
|
let decayed = 0;
|
||||||
|
memEntries.forEach(entry => {
|
||||||
|
const currentStrength = entry.mesh?.userData?.strength ?? 0.7;
|
||||||
|
const importance = entry.data?.importance || 1;
|
||||||
|
const decay = importance * decayRate * elapsed;
|
||||||
|
const newStrength = Math.max(0, currentStrength - decay);
|
||||||
|
|
||||||
|
if (newStrength <= threshold && !entry.data?.locked) {
|
||||||
|
spatialMemory.removeMemory(entry.data.id);
|
||||||
|
decayed++;
|
||||||
|
} else if (entry.mesh) {
|
||||||
|
spatialMemory.updateMemory(entry.data.id, { strength: newStrength });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`[MemoryOptimizer] Decay pass: ${decayed} faded, ${memEntries.length - decayed} retained`);
|
||||||
|
return decayed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default MemoryOptimizer;
|
export default MemoryOptimizer;
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// SPATIAL AUDIO MANAGER — Nexus Spatial Sound for Mnemosyne
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
//
|
|
||||||
// Attaches a Three.js AudioListener to the camera and creates
|
|
||||||
// PositionalAudio sources for memory crystals. Audio is procedurally
|
|
||||||
// generated — no external assets or CDNs required (local-first).
|
|
||||||
//
|
|
||||||
// Each region gets a distinct tone. Proximity controls volume and
|
|
||||||
// panning. Designed to layer on top of SpatialMemory without
|
|
||||||
// modifying it.
|
|
||||||
//
|
|
||||||
// Usage from app.js:
|
|
||||||
// SpatialAudio.init(camera, scene);
|
|
||||||
// SpatialAudio.bindSpatialMemory(SpatialMemory);
|
|
||||||
// SpatialAudio.update(delta); // call in animation loop
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
const SpatialAudio = (() => {
|
|
||||||
|
|
||||||
// ─── CONFIG ──────────────────────────────────────────────
|
|
||||||
const REGION_TONES = {
|
|
||||||
engineering: { freq: 220, type: 'sine' }, // A3
|
|
||||||
social: { freq: 261, type: 'triangle' }, // C4
|
|
||||||
knowledge: { freq: 329, type: 'sine' }, // E4
|
|
||||||
projects: { freq: 392, type: 'triangle' }, // G4
|
|
||||||
working: { freq: 440, type: 'sine' }, // A4
|
|
||||||
archive: { freq: 110, type: 'sine' }, // A2
|
|
||||||
user_pref: { freq: 349, type: 'triangle' }, // F4
|
|
||||||
project: { freq: 392, type: 'sine' }, // G4
|
|
||||||
tool: { freq: 493, type: 'triangle' }, // B4
|
|
||||||
general: { freq: 293, type: 'sine' }, // D4
|
|
||||||
};
|
|
||||||
const MAX_AUDIBLE_DIST = 40; // distance at which volume reaches 0
|
|
||||||
const REF_DIST = 5; // full volume within this range
|
|
||||||
const ROLLOFF = 1.5;
|
|
||||||
const BASE_VOLUME = 0.12; // master volume cap per source
|
|
||||||
const AMBIENT_VOLUME = 0.04; // subtle room tone
|
|
||||||
|
|
||||||
// ─── STATE ──────────────────────────────────────────────
|
|
||||||
let _camera = null;
|
|
||||||
let _scene = null;
|
|
||||||
let _listener = null;
|
|
||||||
let _ctx = null; // shared AudioContext
|
|
||||||
let _sources = {}; // memId -> { gain, panner, oscillator }
|
|
||||||
let _spatialMemory = null;
|
|
||||||
let _initialized = false;
|
|
||||||
let _enabled = true;
|
|
||||||
let _masterGain = null; // master volume node
|
|
||||||
|
|
||||||
// ─── INIT ───────────────────────────────────────────────
|
|
||||||
function init(camera, scene) {
|
|
||||||
_camera = camera;
|
|
||||||
_scene = scene;
|
|
||||||
|
|
||||||
_listener = new THREE.AudioListener();
|
|
||||||
camera.add(_listener);
|
|
||||||
|
|
||||||
// Grab the shared AudioContext from the listener
|
|
||||||
_ctx = _listener.context;
|
|
||||||
_masterGain = _ctx.createGain();
|
|
||||||
_masterGain.gain.value = 1.0;
|
|
||||||
_masterGain.connect(_ctx.destination);
|
|
||||||
|
|
||||||
_initialized = true;
|
|
||||||
console.info('[SpatialAudio] Initialized — AudioContext state:', _ctx.state);
|
|
||||||
|
|
||||||
// Browsers require a user gesture to resume audio context
|
|
||||||
if (_ctx.state === 'suspended') {
|
|
||||||
const resume = () => {
|
|
||||||
_ctx.resume().then(() => {
|
|
||||||
console.info('[SpatialAudio] AudioContext resumed');
|
|
||||||
document.removeEventListener('click', resume);
|
|
||||||
document.removeEventListener('keydown', resume);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
document.addEventListener('click', resume);
|
|
||||||
document.addEventListener('keydown', resume);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── BIND TO SPATIAL MEMORY ─────────────────────────────
|
|
||||||
function bindSpatialMemory(sm) {
|
|
||||||
_spatialMemory = sm;
|
|
||||||
// Create sources for any existing memories
|
|
||||||
const all = sm.getAllMemories();
|
|
||||||
all.forEach(mem => _ensureSource(mem));
|
|
||||||
console.info('[SpatialAudio] Bound to SpatialMemory —', Object.keys(_sources).length, 'audio sources');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── CREATE A PROCEDURAL TONE SOURCE ────────────────────
|
|
||||||
function _ensureSource(mem) {
|
|
||||||
if (!_ctx || !_enabled || _sources[mem.id]) return;
|
|
||||||
|
|
||||||
const regionKey = mem.category || 'working';
|
|
||||||
const tone = REGION_TONES[regionKey] || REGION_TONES.working;
|
|
||||||
|
|
||||||
// Procedural oscillator
|
|
||||||
const osc = _ctx.createOscillator();
|
|
||||||
osc.type = tone.type;
|
|
||||||
osc.frequency.value = tone.freq + _hashOffset(mem.id); // slight per-crystal detune
|
|
||||||
|
|
||||||
const gain = _ctx.createGain();
|
|
||||||
gain.gain.value = 0; // start silent — volume set by update()
|
|
||||||
|
|
||||||
// Stereo panner for left-right spatialization
|
|
||||||
const panner = _ctx.createStereoPanner();
|
|
||||||
panner.pan.value = 0;
|
|
||||||
|
|
||||||
osc.connect(gain);
|
|
||||||
gain.connect(panner);
|
|
||||||
panner.connect(_masterGain);
|
|
||||||
|
|
||||||
osc.start();
|
|
||||||
|
|
||||||
_sources[mem.id] = { osc, gain, panner, region: regionKey };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small deterministic pitch offset so crystals in the same region don't phase-lock
|
|
||||||
function _hashOffset(id) {
|
|
||||||
let h = 0;
|
|
||||||
for (let i = 0; i < id.length; i++) {
|
|
||||||
h = ((h << 5) - h) + id.charCodeAt(i);
|
|
||||||
h |= 0;
|
|
||||||
}
|
|
||||||
return (Math.abs(h) % 40) - 20; // ±20 Hz
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── PER-FRAME UPDATE ───────────────────────────────────
|
|
||||||
function update() {
|
|
||||||
if (!_initialized || !_enabled || !_spatialMemory || !_camera) return;
|
|
||||||
|
|
||||||
const camPos = _camera.position;
|
|
||||||
const memories = _spatialMemory.getAllMemories();
|
|
||||||
|
|
||||||
// Ensure sources for newly placed memories
|
|
||||||
memories.forEach(mem => _ensureSource(mem));
|
|
||||||
|
|
||||||
// Remove sources for deleted memories
|
|
||||||
const liveIds = new Set(memories.map(m => m.id));
|
|
||||||
Object.keys(_sources).forEach(id => {
|
|
||||||
if (!liveIds.has(id)) {
|
|
||||||
_removeSource(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update each source's volume & panning based on camera distance
|
|
||||||
memories.forEach(mem => {
|
|
||||||
const src = _sources[mem.id];
|
|
||||||
if (!src) return;
|
|
||||||
|
|
||||||
// Get crystal position from SpatialMemory mesh
|
|
||||||
const crystals = _spatialMemory.getCrystalMeshes();
|
|
||||||
let meshPos = null;
|
|
||||||
for (const mesh of crystals) {
|
|
||||||
if (mesh.userData.memId === mem.id) {
|
|
||||||
meshPos = mesh.position;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!meshPos) return;
|
|
||||||
|
|
||||||
const dx = meshPos.x - camPos.x;
|
|
||||||
const dy = meshPos.y - camPos.y;
|
|
||||||
const dz = meshPos.z - camPos.z;
|
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
|
|
||||||
// Volume rolloff (inverse distance model)
|
|
||||||
let vol = 0;
|
|
||||||
if (dist < MAX_AUDIBLE_DIST) {
|
|
||||||
vol = BASE_VOLUME / (1 + ROLLOFF * (dist - REF_DIST));
|
|
||||||
vol = Math.max(0, Math.min(BASE_VOLUME, vol));
|
|
||||||
}
|
|
||||||
src.gain.gain.setTargetAtTime(vol, _ctx.currentTime, 0.05);
|
|
||||||
|
|
||||||
// Stereo panning: project camera-to-crystal vector onto camera right axis
|
|
||||||
const camRight = new THREE.Vector3();
|
|
||||||
_camera.getWorldDirection(camRight);
|
|
||||||
camRight.cross(_camera.up).normalize();
|
|
||||||
const toCrystal = new THREE.Vector3(dx, 0, dz).normalize();
|
|
||||||
const pan = THREE.MathUtils.clamp(toCrystal.dot(camRight), -1, 1);
|
|
||||||
src.panner.pan.setTargetAtTime(pan, _ctx.currentTime, 0.05);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _removeSource(id) {
|
|
||||||
const src = _sources[id];
|
|
||||||
if (!src) return;
|
|
||||||
try {
|
|
||||||
src.osc.stop();
|
|
||||||
src.osc.disconnect();
|
|
||||||
src.gain.disconnect();
|
|
||||||
src.panner.disconnect();
|
|
||||||
} catch (_) { /* already stopped */ }
|
|
||||||
delete _sources[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── CONTROLS ───────────────────────────────────────────
|
|
||||||
function setEnabled(enabled) {
|
|
||||||
_enabled = enabled;
|
|
||||||
if (!_enabled) {
|
|
||||||
// Silence all sources
|
|
||||||
Object.values(_sources).forEach(src => {
|
|
||||||
src.gain.gain.setTargetAtTime(0, _ctx.currentTime, 0.05);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.info('[SpatialAudio]', enabled ? 'Enabled' : 'Disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEnabled() {
|
|
||||||
return _enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMasterVolume(vol) {
|
|
||||||
if (_masterGain) {
|
|
||||||
_masterGain.gain.setTargetAtTime(
|
|
||||||
THREE.MathUtils.clamp(vol, 0, 1),
|
|
||||||
_ctx.currentTime,
|
|
||||||
0.05
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveSourceCount() {
|
|
||||||
return Object.keys(_sources).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── API ────────────────────────────────────────────────
|
|
||||||
return {
|
|
||||||
init,
|
|
||||||
bindSpatialMemory,
|
|
||||||
update,
|
|
||||||
setEnabled,
|
|
||||||
isEnabled,
|
|
||||||
setMasterVolume,
|
|
||||||
getActiveSourceCount,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
export { SpatialAudio };
|
|
||||||
@@ -601,6 +601,13 @@ const SpatialMemory = (() => {
|
|||||||
return Object.values(_memoryObjects).map(o => o.data);
|
return Object.values(_memoryObjects).map(o => o.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return full memory entries (mesh + data) for batch operations like decay.
|
||||||
|
*/
|
||||||
|
function getAllMemoryEntries() {
|
||||||
|
return Object.values(_memoryObjects);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── LOCALSTORAGE PERSISTENCE ────────────────────────
|
// ─── LOCALSTORAGE PERSISTENCE ────────────────────────
|
||||||
function _indexHash(index) {
|
function _indexHash(index) {
|
||||||
// Simple hash of memory IDs + count to detect changes
|
// Simple hash of memory IDs + count to detect changes
|
||||||
@@ -863,6 +870,7 @@ const SpatialMemory = (() => {
|
|||||||
return {
|
return {
|
||||||
init, placeMemory, removeMemory, update, importMemories, updateMemory,
|
init, placeMemory, removeMemory, update, importMemories, updateMemory,
|
||||||
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories,
|
||||||
|
getAllMemoryEntries,
|
||||||
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
getCrystalMeshes, getMemoryFromMesh, highlightMemory, clearHighlight, getSelectedId,
|
||||||
exportIndex, importIndex, searchNearby, REGIONS,
|
exportIndex, importIndex, searchNearby, REGIONS,
|
||||||
saveToStorage, loadFromStorage, clearStorage,
|
saveToStorage, loadFromStorage, clearStorage,
|
||||||
|
|||||||
@@ -1340,74 +1340,6 @@ class MnemosyneArchive:
|
|||||||
results.sort(key=lambda x: x["score"], reverse=True)
|
results.sort(key=lambda x: x["score"], reverse=True)
|
||||||
return results[:limit]
|
return results[:limit]
|
||||||
|
|
||||||
def discover(
|
|
||||||
self,
|
|
||||||
count: int = 3,
|
|
||||||
prefer_fading: bool = True,
|
|
||||||
topic: Optional[str] = None,
|
|
||||||
) -> list[ArchiveEntry]:
|
|
||||||
"""Serendipitous entry discovery weighted by vitality decay.
|
|
||||||
|
|
||||||
Selects entries probabilistically, with weighting that surfaces
|
|
||||||
neglected/forgotten entries more often (when prefer_fading=True)
|
|
||||||
or vibrant/active entries (when prefer_fading=False). Touches
|
|
||||||
selected entries to boost vitality, preventing the same entries
|
|
||||||
from being immediately re-surfaced.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
count: Number of entries to discover (default 3).
|
|
||||||
prefer_fading: If True (default), weight toward fading entries.
|
|
||||||
If False, weight toward vibrant entries.
|
|
||||||
topic: If set, restrict to entries with this topic (case-insensitive).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of ArchiveEntry, up to count entries.
|
|
||||||
"""
|
|
||||||
import random
|
|
||||||
|
|
||||||
candidates = list(self._entries.values())
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if topic:
|
|
||||||
topic_lower = topic.lower()
|
|
||||||
candidates = [e for e in candidates if topic_lower in [t.lower() for t in e.topics]]
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Compute vitality for each candidate
|
|
||||||
entries_with_vitality = [(e, self._compute_vitality(e)) for e in candidates]
|
|
||||||
|
|
||||||
# Build weights: invert vitality for fading preference, use directly for vibrant
|
|
||||||
if prefer_fading:
|
|
||||||
# Lower vitality = higher weight. Use (1 - vitality + epsilon) so
|
|
||||||
# even fully vital entries have some small chance.
|
|
||||||
weights = [1.0 - v + 0.01 for _, v in entries_with_vitality]
|
|
||||||
else:
|
|
||||||
# Higher vitality = higher weight. Use (vitality + epsilon).
|
|
||||||
weights = [v + 0.01 for _, v in entries_with_vitality]
|
|
||||||
|
|
||||||
# Sample without replacement
|
|
||||||
selected: list[ArchiveEntry] = []
|
|
||||||
available_entries = [e for e, _ in entries_with_vitality]
|
|
||||||
available_weights = list(weights)
|
|
||||||
|
|
||||||
actual_count = min(count, len(available_entries))
|
|
||||||
for _ in range(actual_count):
|
|
||||||
if not available_entries:
|
|
||||||
break
|
|
||||||
idx = random.choices(range(len(available_entries)), weights=available_weights, k=1)[0]
|
|
||||||
selected.append(available_entries.pop(idx))
|
|
||||||
available_weights.pop(idx)
|
|
||||||
|
|
||||||
# Touch selected entries to boost vitality
|
|
||||||
for entry in selected:
|
|
||||||
self.touch(entry.id)
|
|
||||||
|
|
||||||
return selected
|
|
||||||
|
|
||||||
def rebuild_links(self, threshold: Optional[float] = None) -> int:
|
def rebuild_links(self, threshold: Optional[float] = None) -> int:
|
||||||
"""Recompute all links from scratch.
|
"""Recompute all links from scratch.
|
||||||
|
|
||||||
|
|||||||
@@ -392,25 +392,6 @@ def cmd_resonance(args):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
def cmd_discover(args):
|
|
||||||
archive = MnemosyneArchive()
|
|
||||||
topic = args.topic if args.topic else None
|
|
||||||
results = archive.discover(
|
|
||||||
count=args.count,
|
|
||||||
prefer_fading=not args.vibrant,
|
|
||||||
topic=topic,
|
|
||||||
)
|
|
||||||
if not results:
|
|
||||||
print("No entries to discover.")
|
|
||||||
return
|
|
||||||
for entry in results:
|
|
||||||
v = archive.get_vitality(entry.id)
|
|
||||||
print(f"[{entry.id[:8]}] {entry.title}")
|
|
||||||
print(f" Topics: {', '.join(entry.topics) if entry.topics else '(none)'}")
|
|
||||||
print(f" Vitality: {v['vitality']:.4f} (boosted)")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_vibrant(args):
|
def cmd_vibrant(args):
|
||||||
archive = MnemosyneArchive()
|
archive = MnemosyneArchive()
|
||||||
results = archive.vibrant(limit=args.limit)
|
results = archive.vibrant(limit=args.limit)
|
||||||
@@ -518,11 +499,6 @@ def main():
|
|||||||
rs.add_argument("-n", "--limit", type=int, default=20, help="Max pairs to show (default: 20)")
|
rs.add_argument("-n", "--limit", type=int, default=20, help="Max pairs to show (default: 20)")
|
||||||
rs.add_argument("--topic", default="", help="Restrict to entries with this topic")
|
rs.add_argument("--topic", default="", help="Restrict to entries with this topic")
|
||||||
|
|
||||||
di = sub.add_parser("discover", help="Serendipitous entry exploration")
|
|
||||||
di.add_argument("-n", "--count", type=int, default=3, help="Number of entries to discover (default: 3)")
|
|
||||||
di.add_argument("-t", "--topic", default="", help="Filter to entries with this topic")
|
|
||||||
di.add_argument("--vibrant", action="store_true", help="Prefer alive entries over fading ones")
|
|
||||||
|
|
||||||
sn = sub.add_parser("snapshot", help="Point-in-time backup and restore")
|
sn = sub.add_parser("snapshot", help="Point-in-time backup and restore")
|
||||||
sn_sub = sn.add_subparsers(dest="snapshot_cmd")
|
sn_sub = sn.add_subparsers(dest="snapshot_cmd")
|
||||||
sn_create = sn_sub.add_parser("create", help="Create a new snapshot")
|
sn_create = sn_sub.add_parser("create", help="Create a new snapshot")
|
||||||
@@ -567,7 +543,6 @@ def main():
|
|||||||
"fading": cmd_fading,
|
"fading": cmd_fading,
|
||||||
"vibrant": cmd_vibrant,
|
"vibrant": cmd_vibrant,
|
||||||
"resonance": cmd_resonance,
|
"resonance": cmd_resonance,
|
||||||
"discover": cmd_discover,
|
|
||||||
"snapshot": cmd_snapshot,
|
"snapshot": cmd_snapshot,
|
||||||
}
|
}
|
||||||
dispatch[args.command](args)
|
dispatch[args.command](args)
|
||||||
|
|||||||
@@ -1,31 +1,2 @@
|
|||||||
"""Archive snapshot — point-in-time backup and restore."""
|
import json
|
||||||
import json, uuid
|
# Snapshot logic
|
||||||
from datetime import datetime, timezone
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def snapshot_create(archive, label=None):
|
|
||||||
sid = str(uuid.uuid4())[:8]
|
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
|
||||||
data = {"snapshot_id": sid, "label": label or "", "created_at": now, "entries": [e.to_dict() for e in archive._entries.values()]}
|
|
||||||
path = archive.path.parent / "snapshots" / f"{sid}.json"
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with open(path, "w") as f: json.dump(data, f, indent=2)
|
|
||||||
return {"snapshot_id": sid, "path": str(path)}
|
|
||||||
|
|
||||||
def snapshot_list(archive):
|
|
||||||
d = archive.path.parent / "snapshots"
|
|
||||||
if not d.exists(): return []
|
|
||||||
snaps = []
|
|
||||||
for f in d.glob("*.json"):
|
|
||||||
with open(f) as fh: meta = json.load(fh)
|
|
||||||
snaps.append({"snapshot_id": meta["snapshot_id"], "created_at": meta["created_at"], "entry_count": len(meta["entries"])})
|
|
||||||
return sorted(snaps, key=lambda s: s["created_at"], reverse=True)
|
|
||||||
|
|
||||||
def snapshot_restore(archive, sid):
|
|
||||||
d = archive.path.parent / "snapshots"
|
|
||||||
f = next((x for x in d.glob("*.json") if x.stem.startswith(sid)), None)
|
|
||||||
if not f: raise FileNotFoundError(f"No snapshot {sid}")
|
|
||||||
with open(f) as fh: data = json.load(fh)
|
|
||||||
archive._entries = {e["id"]: ArchiveEntry.from_dict(e) for e in data["entries"]}
|
|
||||||
archive._save()
|
|
||||||
return {"snapshot_id": data["snapshot_id"], "restored_entries": len(data["entries"])}
|
|
||||||
@@ -1 +1 @@
|
|||||||
# Discover tests
|
# Test discover
|
||||||
@@ -1 +1 @@
|
|||||||
# Resonance tests
|
# Test resonance
|
||||||
@@ -1 +1 @@
|
|||||||
# Snapshot tests
|
# Test snapshot
|
||||||
@@ -45,7 +45,6 @@ from nexus.perception_adapter import (
|
|||||||
)
|
)
|
||||||
from nexus.experience_store import ExperienceStore
|
from nexus.experience_store import ExperienceStore
|
||||||
from nexus.groq_worker import GroqWorker
|
from nexus.groq_worker import GroqWorker
|
||||||
from nexus.heartbeat import write_heartbeat
|
|
||||||
from nexus.trajectory_logger import TrajectoryLogger
|
from nexus.trajectory_logger import TrajectoryLogger
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -287,13 +286,6 @@ class NexusMind:
|
|||||||
|
|
||||||
self.cycle_count += 1
|
self.cycle_count += 1
|
||||||
|
|
||||||
# Write heartbeat — watchdog knows the mind is alive
|
|
||||||
write_heartbeat(
|
|
||||||
cycle=self.cycle_count,
|
|
||||||
model=self.model,
|
|
||||||
status="thinking",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Periodically distill old memories
|
# Periodically distill old memories
|
||||||
if self.cycle_count % 50 == 0 and self.cycle_count > 0:
|
if self.cycle_count % 50 == 0 and self.cycle_count > 0:
|
||||||
await self._distill_memories()
|
await self._distill_memories()
|
||||||
@@ -391,13 +383,6 @@ class NexusMind:
|
|||||||
salience=1.0,
|
salience=1.0,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Write initial heartbeat — mind is online
|
|
||||||
write_heartbeat(
|
|
||||||
cycle=0,
|
|
||||||
model=self.model,
|
|
||||||
status="thinking",
|
|
||||||
)
|
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
await self.think_once()
|
await self.think_once()
|
||||||
@@ -438,13 +423,6 @@ class NexusMind:
|
|||||||
log.info("Nexus Mind shutting down...")
|
log.info("Nexus Mind shutting down...")
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
# Final heartbeat — mind is going down cleanly
|
|
||||||
write_heartbeat(
|
|
||||||
cycle=self.cycle_count,
|
|
||||||
model=self.model,
|
|
||||||
status="idle",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Final stats
|
# Final stats
|
||||||
stats = self.trajectory_logger.get_session_stats()
|
stats = self.trajectory_logger.get_session_stats()
|
||||||
log.info(f"Session stats: {json.dumps(stats, indent=2)}")
|
log.info(f"Session stats: {json.dumps(stats, indent=2)}")
|
||||||
|
|||||||
112
portal/bannerlord/INSTALL.md
Normal file
112
portal/bannerlord/INSTALL.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Bannerlord Local Install Guide (macOS / Apple Silicon)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Run the GOG Mount & Blade II: Bannerlord build natively on Alexander's Mac (arm64, macOS Sequoia+).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- macOS 14+ on Apple Silicon (arm64)
|
||||||
|
- ~60 GB free disk space (game + Wine prefix)
|
||||||
|
- GOG installer files in `~/Downloads/`:
|
||||||
|
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||||
|
- `setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124)-1.bin` through `-13.bin`
|
||||||
|
|
||||||
|
## Step 1: Install Porting Kit
|
||||||
|
|
||||||
|
Porting Kit (free) wraps Wine/GPTK for macOS. It has a GUI but we automate what we can.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install --cask porting-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
Launch it once to complete first-run setup:
|
||||||
|
```bash
|
||||||
|
open -a "Porting Kit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Create Wine Prefix + Install Game
|
||||||
|
|
||||||
|
**Option A: Via Porting Kit GUI (recommended)**
|
||||||
|
|
||||||
|
1. Open Porting Kit
|
||||||
|
2. Click "Install Game" → "Custom Port" or search for Bannerlord
|
||||||
|
3. Point it at: `~/Downloads/setup_mount__blade_ii_bannerlord_1.3.15.109797_(64bit)_(89124).exe`
|
||||||
|
4. Follow the GOG installer wizard
|
||||||
|
5. Install to default path inside the Wine prefix
|
||||||
|
6. When done, note the prefix path (usually `~/Library/Application Support/PortingKit/...`)
|
||||||
|
|
||||||
|
**Option B: Manual Wine prefix (advanced)**
|
||||||
|
|
||||||
|
If you have Homebrew Wine (or GPTK) installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create prefix
|
||||||
|
export WINEPREFIX="$HOME/Games/Bannerlord"
|
||||||
|
wine64 boot /init
|
||||||
|
|
||||||
|
# Run the GOG installer (it auto-chains the .bin files)
|
||||||
|
cd ~/Downloads
|
||||||
|
wine64 setup_mount__blade_ii_bannerlord_1.3.15.109797_\(64bit\)_\(89124\).exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the GOG installer wizard. Default install path is fine.
|
||||||
|
|
||||||
|
## Step 3: Locate the Game Binary
|
||||||
|
|
||||||
|
After installation, the game executable is at:
|
||||||
|
```
|
||||||
|
$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Or inside Porting Kit's prefix at:
|
||||||
|
```
|
||||||
|
~/Library/Application Support/PortingKit/<prefix-name>/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: First Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the actual path first, then:
|
||||||
|
cd "$HOME/Games/Bannerlord/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client"
|
||||||
|
wine64 Bannerlord.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the launcher script:
|
||||||
|
```bash
|
||||||
|
./portal/bannerlord/launch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Proof (Operator Checklist)
|
||||||
|
|
||||||
|
- [ ] Game window opens and is visible on screen
|
||||||
|
- [ ] At least the main menu renders (TaleWorlds logo, "Campaign", "Custom Battle", etc.)
|
||||||
|
- [ ] Screenshot taken: save to `portal/bannerlord/proof/`
|
||||||
|
- [ ] Launch command recorded below for repeatability
|
||||||
|
|
||||||
|
**Launch command (fill in after install):**
|
||||||
|
```
|
||||||
|
# Repeatable launch:
|
||||||
|
./portal/bannerlord/launch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Black screen on launch:**
|
||||||
|
- Try: `wine64 Bannerlord.exe -force-d3d11` or `-force-vulkan`
|
||||||
|
- Set Windows version: `winecfg` → set to Windows 10
|
||||||
|
|
||||||
|
**Missing DLLs:**
|
||||||
|
- Install DirectX runtime: `winetricks d3dx9 d3dx10 d3dx11 vcrun2019`
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- GPTK/Rosetta overhead is expected; 30-60 FPS is normal on M1/M2
|
||||||
|
- Lower in-game graphics settings to "Medium" for first run
|
||||||
|
|
||||||
|
**Installer won't chain .bin files:**
|
||||||
|
- Make sure all .bin files are in the same directory as the .exe
|
||||||
|
- Verify with: `ls -la ~/Downloads/setup_mount__blade_ii_bannerlord_*`
|
||||||
|
|
||||||
|
## References
|
||||||
|
- GamePortal Protocol: `GAMEPORTAL_PROTOCOL.md`
|
||||||
|
- Portal config: `portals.json` (entry: "bannerlord")
|
||||||
|
- GOG App ID: Mount & Blade II: Bannerlord
|
||||||
|
- Steam App ID: 261550 (for Steam stats integration)
|
||||||
115
portal/bannerlord/launch.sh
Executable file
115
portal/bannerlord/launch.sh
Executable file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bannerlord Launcher for macOS (Apple Silicon via Wine/GPTK)
|
||||||
|
# Usage: ./portal/bannerlord/launch.sh [--wine-prefix PATH] [--exe PATH]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
# Defaults — override with flags or environment
|
||||||
|
WINEPREFIX="${WINEPREFIX:-$HOME/Games/Bannerlord}"
|
||||||
|
BANNERLORD_EXE=""
|
||||||
|
WINE_CMD=""
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--wine-prefix) WINEPREFIX="$2"; shift 2 ;;
|
||||||
|
--exe) BANNERLORD_EXE="$2"; shift 2 ;;
|
||||||
|
--help)
|
||||||
|
echo "Usage: $0 [--wine-prefix PATH] [--exe PATH]"
|
||||||
|
echo ""
|
||||||
|
echo "Defaults:"
|
||||||
|
echo " Wine prefix: $WINEPREFIX"
|
||||||
|
echo " Auto-discovers Bannerlord.exe in the prefix"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown arg: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Find wine command
|
||||||
|
find_wine() {
|
||||||
|
if command -v wine64 &>/dev/null; then
|
||||||
|
echo "wine64"
|
||||||
|
elif command -v wine &>/dev/null; then
|
||||||
|
echo "wine"
|
||||||
|
elif [ -f "/Applications/Whisky.app/Contents/Resources/WhiskyCmd" ]; then
|
||||||
|
echo "/Applications/Whisky.app/Contents/Resources/WhiskyCmd"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
WINE_CMD="$(find_wine)"
|
||||||
|
if [ -z "$WINE_CMD" ]; then
|
||||||
|
echo "ERROR: No Wine runtime found."
|
||||||
|
echo "Install one of:"
|
||||||
|
echo " brew install --cask porting-kit"
|
||||||
|
echo " brew install --cask crossover"
|
||||||
|
echo " brew tap apple/apple && brew install game-porting-toolkit"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wine runtime: $WINE_CMD"
|
||||||
|
echo "Wine prefix: $WINEPREFIX"
|
||||||
|
|
||||||
|
# Find Bannerlord.exe if not specified
|
||||||
|
if [ -z "$BANNERLORD_EXE" ]; then
|
||||||
|
# Search common GOG install paths
|
||||||
|
SEARCH_PATHS=(
|
||||||
|
"$WINEPREFIX/drive_c/GOG Games/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||||
|
"$WINEPREFIX/drive_c/GOG Games/Mount Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||||
|
"$WINEPREFIX/drive_c/Program Files/Mount & Blade II Bannerlord/bin/Win64_Shipping_Client/Bannerlord.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also search PortingKit prefixes
|
||||||
|
while IFS= read -r -d '' exe; do
|
||||||
|
SEARCH_PATHS+=("$exe")
|
||||||
|
done < <(find "$HOME/Library/Application Support/PortingKit" -name "Bannerlord.exe" -print0 2>/dev/null || true)
|
||||||
|
|
||||||
|
for path in "${SEARCH_PATHS[@]}"; do
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
BANNERLORD_EXE="$path"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$BANNERLORD_EXE" ] || [ ! -f "$BANNERLORD_EXE" ]; then
|
||||||
|
echo "ERROR: Bannerlord.exe not found."
|
||||||
|
echo "Searched:"
|
||||||
|
echo " $WINEPREFIX/drive_c/GOG Games/"
|
||||||
|
echo " ~/Library/Application Support/PortingKit/"
|
||||||
|
echo ""
|
||||||
|
echo "Run the install first. See: portal/bannerlord/INSTALL.md"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Game binary: $BANNERLORD_EXE"
|
||||||
|
echo "Launching..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Log the launch for proof
|
||||||
|
LAUNCH_LOG="$SCRIPT_DIR/proof/launch_$(date +%Y%m%d_%H%M%S).log"
|
||||||
|
mkdir -p "$SCRIPT_DIR/proof"
|
||||||
|
{
|
||||||
|
echo "=== Bannerlord Launch ==="
|
||||||
|
echo "Date: $(date -Iseconds)"
|
||||||
|
echo "Wine: $WINE_CMD"
|
||||||
|
echo "Prefix: $WINEPREFIX"
|
||||||
|
echo "Binary: $BANNERLORD_EXE"
|
||||||
|
echo "User: $(whoami)"
|
||||||
|
echo "macOS: $(sw_vers -productVersion)"
|
||||||
|
echo "Arch: $(uname -m)"
|
||||||
|
echo "========================="
|
||||||
|
} > "$LAUNCH_LOG"
|
||||||
|
echo "Launch log: $LAUNCH_LOG"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Set the prefix and launch
|
||||||
|
export WINEPREFIX
|
||||||
|
EXE_DIR="$(dirname "$BANNERLORD_EXE")"
|
||||||
|
cd "$EXE_DIR"
|
||||||
|
exec "$WINE_CMD" "Bannerlord.exe" "$@"
|
||||||
16
portal/bannerlord/proof/README.md
Normal file
16
portal/bannerlord/proof/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Bannerlord Proof
|
||||||
|
|
||||||
|
Screenshots and launch logs proving the game runs locally on the Mac.
|
||||||
|
|
||||||
|
## How to capture proof
|
||||||
|
|
||||||
|
1. Launch the game: `./portal/bannerlord/launch.sh`
|
||||||
|
2. Wait for main menu to render
|
||||||
|
3. Take screenshot: `screencapture -x portal/bannerlord/proof/main_menu_$(date +%Y%m%d).png`
|
||||||
|
4. Save launch log (auto-generated by launch.sh)
|
||||||
|
|
||||||
|
## Expected proof files
|
||||||
|
|
||||||
|
- `main_menu_*.png` — screenshot of game main menu
|
||||||
|
- `launch_*.log` — launch command + environment details
|
||||||
|
- `ingame_*.png` — optional in-game screenshots
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"rotation": { "y": 0.5 },
|
"rotation": { "y": 0.5 },
|
||||||
"portal_type": "game-world",
|
"portal_type": "game-world",
|
||||||
"world_category": "strategy-rpg",
|
"world_category": "strategy-rpg",
|
||||||
"environment": "production",
|
"environment": "local",
|
||||||
"access_mode": "operator",
|
"access_mode": "operator",
|
||||||
"readiness_state": "downloaded",
|
"readiness_state": "downloaded",
|
||||||
"readiness_steps": {
|
"readiness_steps": {
|
||||||
@@ -37,11 +37,15 @@
|
|||||||
"owner": "Timmy",
|
"owner": "Timmy",
|
||||||
"app_id": 261550,
|
"app_id": 261550,
|
||||||
"window_title": "Mount & Blade II: Bannerlord",
|
"window_title": "Mount & Blade II: Bannerlord",
|
||||||
|
"install_source": "gog",
|
||||||
|
"gog_version": "1.3.15.109797",
|
||||||
|
"launcher_script": "portal/bannerlord/launch.sh",
|
||||||
|
"install_guide": "portal/bannerlord/INSTALL.md",
|
||||||
"destination": {
|
"destination": {
|
||||||
"url": null,
|
"url": null,
|
||||||
"type": "harness",
|
"type": "harness",
|
||||||
"action_label": "Enter Calradia",
|
"action_label": "Enter Calradia",
|
||||||
"params": { "world": "calradia" }
|
"params": { "world": "calradia", "runtime": "wine/gptk" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
17
server.py
17
server.py
@@ -52,20 +52,19 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
disconnected = set()
|
disconnected = set()
|
||||||
# Create broadcast tasks, tracking which client each task targets
|
# Create broadcast tasks for efficiency
|
||||||
task_client_pairs = []
|
tasks = []
|
||||||
for client in clients:
|
for client in clients:
|
||||||
if client != websocket and client.open:
|
if client != websocket and client.open:
|
||||||
task = asyncio.create_task(client.send(message))
|
tasks.append(asyncio.create_task(client.send(message)))
|
||||||
task_client_pairs.append((task, client))
|
|
||||||
|
if tasks:
|
||||||
if task_client_pairs:
|
|
||||||
tasks = [pair[0] for pair 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 i, result in enumerate(results):
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
target_client = task_client_pairs[i][1]
|
# Find the client that failed
|
||||||
logger.error(f"Failed to send to client {target_client.remote_address}: {result}")
|
target_client = [c for c in clients if c != websocket][i]
|
||||||
|
logger.error(f"Failed to send to a client {target_client.remote_address}: {result}")
|
||||||
disconnected.add(target_client)
|
disconnected.add(target_client)
|
||||||
|
|
||||||
if disconnected:
|
if disconnected:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const ASSETS_TO_CACHE = [
|
|||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME).then(cache => {
|
caches.open(CachedName).then(cache => {
|
||||||
return cache.addAll(ASSETS_TO_CACHE);
|
return cache.addAll(ASSETS_TO_CACHE);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user