Muninn has one body: drop the workspace copy

The three muninn crates, their seventeen actuator scripts, the two Raven
capture-mux wrappers that called one of them, and the two Muninn docs all
leave this workspace. GameCult/Muninn has carried them since 2026-09-09 and
has moved on (media stream advertisement, receivers dialling the producer);
this copy was a fork drifting behind under the same name, and gamecult-ops
now binds every Muninn target to the Muninn repository. Odin keeps the
Muninn records in odin-core until they are moved; that is the remaining
piece of the extraction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-10 23:39:16 +02:00
commit d41c744b2e
34 changed files with 4 additions and 22638 deletions

View file

@ -51,9 +51,8 @@ ssh ygg 'sudo docker run --rm -v /srv/build/odin:/w -v /etc/machine-id:/etc/mach
with `Linux machine-id is unavailable` — a wall of failures that looks like
broken code and is a missing mount.
`muninn-psmoveapi-tracker` fails to link in that image for want of
`libpsmoveapi`. It declares no CultLib dependency; scope test runs to the crates
you are changing.
Scope test runs to the crates you are changing. Muninn's crates live in
`GameCult/Muninn` now, not in this workspace.
## Session Bootstrap And Re-entry Protocol

27
Cargo.lock generated
View file

@ -573,33 +573,6 @@ version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "muninn-daemon"
version = "0.1.0"
dependencies = [
"anyhow",
"cultmesh-rs",
"cultnet-rs",
"libc",
"muninn-move-tracker",
"muninn-psmoveapi-tracker",
"odin-core",
"rmp-serde",
"serde",
"serde_bytes",
"serde_json",
"socket2",
"windows-sys 0.59.0",
]
[[package]]
name = "muninn-move-tracker"
version = "0.1.0"
[[package]]
name = "muninn-psmoveapi-tracker"
version = "0.1.0"
[[package]]
name = "num-traits"
version = "0.2.19"

View file

@ -1,9 +1,6 @@
[workspace]
members = [
"crates/sleipnir-daemon",
"crates/muninn-daemon",
"crates/muninn-move-tracker",
"crates/muninn-psmoveapi-tracker",
"crates/odin-core",
"crates/odin-daemon",
]

View file

@ -1,40 +0,0 @@
[package]
name = "muninn-daemon"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish.workspace = true
[features]
default = []
psmoveapi-tracker = ["dep:muninn-psmoveapi-tracker"]
[[bin]]
name = "muninn"
path = "src/main.rs"
[dependencies]
anyhow = "1"
cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a8aeddacab7dd4b9e634b4420867ea3b402cb858", package = "cultmesh-rs" }
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a8aeddacab7dd4b9e634b4420867ea3b402cb858", package = "cultnet-rs" }
libc = "0.2"
muninn-move-tracker = { path = "../muninn-move-tracker" }
muninn-psmoveapi-tracker = { path = "../muninn-psmoveapi-tracker", optional = true }
odin-core = { path = "../odin-core" }
rmp-serde = "1.3"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11"
serde_json = "1"
socket2 = "0.5"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Devices_DeviceAndDriverInstallation",
"Win32_Devices_HumanInterfaceDevice",
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_IO",
"Win32_System_Threading",
"Win32_UI_Input_XboxController",
] }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
[package]
name = "muninn-move-tracker"
version = "0.1.0"
edition = "2024"
description = "Muninn-owned Rust orchestration and CPU mirror for PS Move marker candidate extraction"
[lib]
name = "muninn_move_tracker"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]

View file

@ -1,31 +0,0 @@
# Muninn Move Tracker
Muninn owns PS Move sensor stream exposure. This crate is Muninn's first native
optical marker candidate extractor: Rust owns orchestration, FFI, validation,
and the CPU mirror; the compute shader owns the parallel image reduction stage.
The shader includes CultMath's canonical HLSL mirror, so shader compilation must
add `E:\Projects\CultLib\packages\cultmath\shaders` or the packaged CultMath shader content to
the include path.
This crate deliberately stops at per-frame marker candidates. Final pose,
stereo triangulation, controller association, IMU fusion, prediction, and
calibration remain separate later owners. Mimir consumes Muninn-emitted typed
candidate/observation streams; it does not own raw optical extraction.
## Current Contract
- Input: one luma frame (`Y8`) with width, height, stride, source hash, and frame
sequence.
- GPU stage: one 16px tile per thread group, emitting at most one candidate per
tile.
- Output: `MoveMarkerCandidate` records containing weighted centroid, radius,
area, peak/mean luma, and score.
- Rust mirror: deterministic CPU implementation and FFI wrappers for tests and
non-GPU smoke paths.
- FFI prefix: `muninn_move_tracker_*`.
- Shader IDs are carried as `uint2` low/high pairs to avoid making 64-bit HLSL
integer support a dependency of the first extraction pass.
This is the first stage required for PlayStation-class wand tracking. The
remaining stack needs calibrated camera rays, controlled sphere colors/exposure,
multi-camera association, triangulation, IMU fusion, and latency prediction.

View file

@ -1,106 +0,0 @@
// Muninn PS Move sphere candidate extraction.
// One thread group owns one image tile. The shader emits at most one bright
// marker candidate per 16px tile; Rust owns dispatch sizing and downstream ranking.
#include "CultMath.hlsl"
struct MoveSphereCandidate
{
uint2 sourceIdHash;
uint2 frameSequence;
uint tileX;
uint tileY;
float centerXPx;
float centerYPx;
float radiusPx;
uint areaPx;
float meanLuma;
uint peakLuma;
float score;
};
cbuffer MoveTrackerConfig : register(b0)
{
uint width;
uint height;
uint strideBytes;
uint tileSize;
uint thresholdMin;
uint minAreaPx;
uint maxCandidates;
uint _pad0;
uint2 sourceIdHash;
uint2 frameSequence;
};
ByteAddressBuffer frameY8 : register(t0);
RWStructuredBuffer<MoveSphereCandidate> candidates : register(u0);
RWByteAddressBuffer candidateCounter : register(u1);
groupshared uint gArea;
groupshared uint gSumX;
groupshared uint gSumY;
groupshared uint gSumW;
groupshared uint gPeak;
[numthreads(16, 16, 1)]
void main(uint3 groupId : SV_GroupID, uint3 groupThreadId : SV_GroupThreadID)
{
if (groupThreadId.x == 0 && groupThreadId.y == 0)
{
gArea = 0;
gSumX = 0;
gSumY = 0;
gSumW = 0;
gPeak = 0;
}
GroupMemoryBarrierWithGroupSync();
uint x = groupId.x * tileSize + groupThreadId.x;
uint y = groupId.y * tileSize + groupThreadId.y;
if (groupThreadId.x < tileSize && groupThreadId.y < tileSize && x < width && y < height)
{
uint byteOffset = y * strideBytes + x;
uint packed = frameY8.Load(byteOffset & ~3u);
uint shift = (byteOffset & 3u) * 8u;
uint luma = (packed >> shift) & 0xffu;
if (luma >= thresholdMin)
{
uint weight = luma - thresholdMin + 1u;
InterlockedAdd(gArea, 1u);
InterlockedAdd(gSumX, x * weight);
InterlockedAdd(gSumY, y * weight);
InterlockedAdd(gSumW, weight);
InterlockedMax(gPeak, luma);
}
}
GroupMemoryBarrierWithGroupSync();
if (groupThreadId.x == 0 && groupThreadId.y == 0 && gArea >= minAreaPx && gSumW > 0)
{
uint index;
candidateCounter.InterlockedAdd(0, 1, index);
if (index < maxCandidates)
{
float area = (float)gArea;
float tilePixels = (float)(tileSize * tileSize);
float areaScore = cultmath_saturate(area / tilePixels);
float brightnessScore = (float)gPeak / 255.0;
MoveSphereCandidate candidate;
candidate.sourceIdHash = sourceIdHash;
candidate.frameSequence = frameSequence;
candidate.tileX = groupId.x;
candidate.tileY = groupId.y;
candidate.centerXPx = (float)gSumX / (float)gSumW;
candidate.centerYPx = (float)gSumY / (float)gSumW;
candidate.radiusPx = sqrt(area / 3.14159265358979323846);
candidate.areaPx = gArea;
candidate.meanLuma = (float)thresholdMin + (((float)gSumW / area) - 1.0);
candidate.peakLuma = gPeak;
candidate.score = brightnessScore * (0.65 + areaScore * 0.35);
candidates[index] = candidate;
}
}
}

View file

@ -1,308 +0,0 @@
#![deny(unsafe_op_in_unsafe_fn)]
use core::ffi::c_int;
pub const MUNINN_MOVE_TRACKER_OK: c_int = 0;
pub const MUNINN_MOVE_TRACKER_NULL: c_int = -1;
pub const MUNINN_MOVE_TRACKER_INVALID: c_int = -2;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MoveTrackerConfig {
pub width: u32,
pub height: u32,
pub stride_bytes: u32,
pub tile_size: u32,
pub threshold_min: u8,
pub min_area_px: u32,
pub max_candidates: u32,
pub source_id_hash: u64,
pub frame_sequence: u64,
}
impl Default for MoveTrackerConfig {
fn default() -> Self {
Self {
width: 320,
height: 240,
stride_bytes: 320,
tile_size: 16,
threshold_min: 180,
min_area_px: 4,
max_candidates: 64,
source_id_hash: 0,
frame_sequence: 0,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MoveTrackerDispatchPlan {
pub group_size_x: u32,
pub group_size_y: u32,
pub groups_x: u32,
pub groups_y: u32,
pub max_candidates: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MoveMarkerCandidate {
pub source_id_hash: u64,
pub frame_sequence: u64,
pub tile_x: u32,
pub tile_y: u32,
pub center_x_px: f32,
pub center_y_px: f32,
pub radius_px: f32,
pub area_px: u32,
pub mean_luma: f32,
pub peak_luma: u32,
pub score: f32,
}
pub fn dispatch_plan(config: MoveTrackerConfig) -> Option<MoveTrackerDispatchPlan> {
if !config_is_valid(config) {
return None;
}
let tile = config.tile_size;
Some(MoveTrackerDispatchPlan {
group_size_x: tile,
group_size_y: tile,
groups_x: config.width.div_ceil(tile),
groups_y: config.height.div_ceil(tile),
max_candidates: config.max_candidates,
})
}
pub fn extract_luma_candidates(
frame: &[u8],
config: MoveTrackerConfig,
) -> Option<Vec<MoveMarkerCandidate>> {
if !config_is_valid(config) {
return None;
}
let required = required_frame_bytes(config)?;
if frame.len() < required {
return None;
}
let mut candidates = Vec::new();
let tile = config.tile_size;
let groups_x = config.width.div_ceil(tile);
let groups_y = config.height.div_ceil(tile);
for tile_y in 0..groups_y {
for tile_x in 0..groups_x {
if candidates.len() >= config.max_candidates as usize {
return Some(candidates);
}
if let Some(candidate) = extract_tile(frame, config, tile_x, tile_y) {
candidates.push(candidate);
}
}
}
candidates.sort_by(|left, right| {
right
.score
.partial_cmp(&left.score)
.unwrap_or(core::cmp::Ordering::Equal)
});
Some(candidates)
}
fn extract_tile(
frame: &[u8],
config: MoveTrackerConfig,
tile_x: u32,
tile_y: u32,
) -> Option<MoveMarkerCandidate> {
let x0 = tile_x * config.tile_size;
let y0 = tile_y * config.tile_size;
let x1 = (x0 + config.tile_size).min(config.width);
let y1 = (y0 + config.tile_size).min(config.height);
let threshold = u32::from(config.threshold_min);
let mut area = 0u32;
let mut sum_x = 0u64;
let mut sum_y = 0u64;
let mut sum_w = 0u64;
let mut peak = 0u32;
for y in y0..y1 {
let row = y as usize * config.stride_bytes as usize;
for x in x0..x1 {
let luma = u32::from(frame[row + x as usize]);
if luma < threshold {
continue;
}
area += 1;
peak = peak.max(luma);
let weight = u64::from(luma - threshold + 1);
sum_x += u64::from(x) * weight;
sum_y += u64::from(y) * weight;
sum_w += weight;
}
}
if area < config.min_area_px || sum_w == 0 {
return None;
}
let center_x = sum_x as f32 / sum_w as f32;
let center_y = sum_y as f32 / sum_w as f32;
let radius = (area as f32 / core::f32::consts::PI).sqrt();
let mean_luma = threshold as f32 + ((sum_w as f32 / area as f32) - 1.0);
let area_score = (area as f32 / (config.tile_size * config.tile_size) as f32).min(1.0);
let brightness_score = peak as f32 / 255.0;
Some(MoveMarkerCandidate {
source_id_hash: config.source_id_hash,
frame_sequence: config.frame_sequence,
tile_x,
tile_y,
center_x_px: center_x,
center_y_px: center_y,
radius_px: radius,
area_px: area,
mean_luma,
peak_luma: peak,
score: brightness_score * (0.65 + area_score * 0.35),
})
}
fn config_is_valid(config: MoveTrackerConfig) -> bool {
config.width > 0
&& config.height > 0
&& config.stride_bytes >= config.width
&& config.tile_size == 16
&& config.min_area_px > 0
&& config.max_candidates > 0
}
fn required_frame_bytes(config: MoveTrackerConfig) -> Option<usize> {
let rows = usize::try_from(config.height).ok()?;
let stride = usize::try_from(config.stride_bytes).ok()?;
rows.checked_mul(stride)
}
#[unsafe(no_mangle)]
pub extern "C" fn muninn_move_tracker_dispatch_plan(
config: MoveTrackerConfig,
out_plan: *mut MoveTrackerDispatchPlan,
) -> c_int {
if out_plan.is_null() {
return MUNINN_MOVE_TRACKER_NULL;
}
let Some(plan) = dispatch_plan(config) else {
return MUNINN_MOVE_TRACKER_INVALID;
};
unsafe {
*out_plan = plan;
}
MUNINN_MOVE_TRACKER_OK
}
#[unsafe(no_mangle)]
pub extern "C" fn muninn_move_tracker_extract_luma_cpu(
config: MoveTrackerConfig,
frame: *const u8,
frame_len: usize,
out_candidates: *mut MoveMarkerCandidate,
out_capacity: usize,
out_count: *mut usize,
) -> c_int {
if frame.is_null() || out_candidates.is_null() || out_count.is_null() {
return MUNINN_MOVE_TRACKER_NULL;
}
let frame = unsafe { core::slice::from_raw_parts(frame, frame_len) };
let Some(candidates) = extract_luma_candidates(frame, config) else {
return MUNINN_MOVE_TRACKER_INVALID;
};
let written = candidates.len().min(out_capacity);
unsafe {
core::ptr::copy_nonoverlapping(candidates.as_ptr(), out_candidates, written);
*out_count = written;
}
MUNINN_MOVE_TRACKER_OK
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispatch_plan_covers_partial_tiles() {
let config = MoveTrackerConfig {
width: 33,
height: 17,
stride_bytes: 40,
tile_size: 16,
..MoveTrackerConfig::default()
};
let plan = dispatch_plan(config).expect("valid plan");
assert_eq!(3, plan.groups_x);
assert_eq!(2, plan.groups_y);
assert_eq!(16, plan.group_size_x);
assert_eq!(16, plan.group_size_y);
}
#[test]
fn extracts_weighted_marker_candidate() {
let config = MoveTrackerConfig {
width: 32,
height: 24,
stride_bytes: 32,
tile_size: 16,
threshold_min: 180,
min_area_px: 3,
max_candidates: 8,
source_id_hash: 42,
frame_sequence: 7,
};
let mut frame = vec![0u8; (config.stride_bytes * config.height) as usize];
for (x, y, luma) in [
(10u32, 8u32, 230u8),
(11, 8, 250),
(10, 9, 240),
(11, 9, 255),
] {
frame[(y * config.stride_bytes + x) as usize] = luma;
}
let candidates = extract_luma_candidates(&frame, config).expect("candidates");
assert_eq!(1, candidates.len());
let candidate = candidates[0];
assert_eq!(42, candidate.source_id_hash);
assert_eq!(7, candidate.frame_sequence);
assert_eq!(0, candidate.tile_x);
assert_eq!(0, candidate.tile_y);
assert_eq!(4, candidate.area_px);
assert_eq!(255, candidate.peak_luma);
assert!(candidate.center_x_px > 10.4 && candidate.center_x_px < 10.7);
assert!(candidate.center_y_px > 8.4 && candidate.center_y_px < 8.7);
assert!(candidate.score > 0.65);
}
#[test]
fn rejects_short_frames_and_empty_config() {
assert!(extract_luma_candidates(&[], MoveTrackerConfig::default()).is_none());
assert!(
dispatch_plan(MoveTrackerConfig {
width: 0,
..MoveTrackerConfig::default()
})
.is_none()
);
}
}

View file

@ -1,10 +0,0 @@
[package]
name = "muninn-psmoveapi-tracker"
version = "0.1.0"
edition = "2024"
description = "Safe Muninn ownership wrapper for the upstream PSMoveAPI tracker ABI"
[lib]
name = "muninn_psmoveapi_tracker"
[dependencies]

View file

@ -1,359 +0,0 @@
#[derive(Clone, Debug, PartialEq)]
pub struct PsmoveApiObservation {
pub move_id: String,
pub center_x_px: f32,
pub center_y_px: f32,
pub radius_px: f32,
pub age_ms: i32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PsmoveApiCameraInfo {
pub name: String,
pub api: String,
pub width: u32,
pub height: u32,
pub exposure: f32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PsmoveApiTrackerError(pub String);
impl core::fmt::Display for PsmoveApiTrackerError {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for PsmoveApiTrackerError {}
#[cfg(target_os = "linux")]
mod linux {
use super::{PsmoveApiCameraInfo, PsmoveApiObservation, PsmoveApiTrackerError};
use core::ffi::{c_char, c_float, c_int, c_uchar, c_void};
use std::ffi::CStr;
const TRACKER_CALIBRATED: c_int = 2;
#[repr(C)]
struct CameraInfo {
camera_name: *const c_char,
camera_api: *const c_char,
width: c_int,
height: c_int,
}
#[repr(C)]
struct TrackerRgbImage {
data: *const u8,
width: c_int,
height: c_int,
}
#[link(name = "psmoveapi")]
#[link(name = "psmoveapi_tracker")]
unsafe extern "C" {
fn psmove_count_connected() -> c_int;
fn psmove_connect_by_id(id: c_int) -> *mut c_void;
fn psmove_get_serial(controller: *mut c_void) -> *const c_char;
fn psmove_disconnect(controller: *mut c_void);
fn psmove_tracker_new_with_camera(camera: c_int) -> *mut c_void;
fn psmove_tracker_free(tracker: *mut c_void);
fn psmove_tracker_enable_color_observer(
tracker: *mut c_void,
controller: *mut c_void,
red: c_uchar,
green: c_uchar,
blue: c_uchar,
) -> c_int;
fn psmove_tracker_set_expected_color(
tracker: *mut c_void,
controller: *mut c_void,
red: c_uchar,
green: c_uchar,
blue: c_uchar,
) -> bool;
fn psmove_tracker_disable(tracker: *mut c_void, controller: *mut c_void);
fn psmove_tracker_set_exposure(tracker: *mut c_void, exposure: c_float);
fn psmove_tracker_get_exposure(tracker: *mut c_void) -> c_float;
fn psmove_tracker_update_image(tracker: *mut c_void);
fn psmove_tracker_update(tracker: *mut c_void, controller: *mut c_void) -> c_int;
fn psmove_tracker_get_position(
tracker: *mut c_void,
controller: *mut c_void,
x: *mut c_float,
y: *mut c_float,
radius: *mut c_float,
) -> c_int;
fn psmove_tracker_get_camera_info(tracker: *mut c_void) -> *const CameraInfo;
fn psmove_tracker_get_image(tracker: *mut c_void) -> TrackerRgbImage;
}
struct Controller {
handle: *mut c_void,
move_id: String,
}
pub struct PsmoveApiTracker {
tracker: *mut c_void,
controllers: Vec<Controller>,
colors: Vec<(String, [u8; 3])>,
camera_info: PsmoveApiCameraInfo,
}
impl PsmoveApiTracker {
pub fn open(
camera_index: i32,
exposure: f32,
colors: &[(String, [u8; 3])],
) -> Result<Self, PsmoveApiTrackerError> {
if !(0.0..=1.0).contains(&exposure) {
return Err(PsmoveApiTrackerError(format!(
"PSMoveAPI exposure must be within 0..=1, got {exposure}"
)));
}
let tracker = unsafe { psmove_tracker_new_with_camera(camera_index) };
if tracker.is_null() {
return Err(PsmoveApiTrackerError(format!(
"PSMoveAPI could not open camera index {camera_index}"
)));
}
unsafe { psmove_tracker_set_exposure(tracker, exposure) };
let mut result = Self {
tracker,
controllers: Vec::new(),
colors: colors.to_vec(),
camera_info: PsmoveApiCameraInfo {
name: "unknown".to_string(),
api: "unknown".to_string(),
width: 0,
height: 0,
exposure,
},
};
result.observe_connected();
result.camera_info = unsafe {
let info = psmove_tracker_get_camera_info(tracker);
if info.is_null() {
PsmoveApiCameraInfo {
exposure: psmove_tracker_get_exposure(tracker),
..result.camera_info.clone()
}
} else {
PsmoveApiCameraInfo {
name: text((*info).camera_name),
api: text((*info).camera_api),
width: (*info).width.max(0) as u32,
height: (*info).height.max(0) as u32,
exposure: psmove_tracker_get_exposure(tracker),
}
}
};
Ok(result)
}
pub fn calibrate_connected(&mut self) -> usize {
self.observe_connected()
}
pub fn observe_connected(&mut self) -> usize {
let count = unsafe { psmove_count_connected() }.max(0);
for index in 0..count {
let handle = unsafe { psmove_connect_by_id(index) };
if handle.is_null() {
continue;
}
let serial = unsafe { text(psmove_get_serial(handle)) };
let move_id = format!("move-{}", serial.replace(':', "").to_ascii_lowercase());
if self
.controllers
.iter()
.any(|controller| controller.move_id == move_id)
{
unsafe { psmove_disconnect(handle) };
continue;
}
let Some((_, color)) = self
.colors
.iter()
.find(|(identity, _)| identity.eq_ignore_ascii_case(&move_id))
else {
unsafe { psmove_disconnect(handle) };
continue;
};
let status = unsafe {
psmove_tracker_enable_color_observer(
self.tracker,
handle,
color[0],
color[1],
color[2],
)
};
if status == TRACKER_CALIBRATED {
self.controllers.push(Controller { handle, move_id });
} else {
unsafe { psmove_disconnect(handle) };
}
}
self.controllers.len()
}
pub fn set_expected_color(&mut self, move_id: &str, color: [u8; 3]) -> bool {
let Some(controller) = self
.controllers
.iter()
.find(|controller| controller.move_id.eq_ignore_ascii_case(move_id))
else {
return false;
};
unsafe {
psmove_tracker_set_expected_color(
self.tracker,
controller.handle,
color[0],
color[1],
color[2],
)
}
}
pub fn camera_info(&self) -> &PsmoveApiCameraInfo {
&self.camera_info
}
pub fn tracked_controller_count(&self) -> usize {
self.controllers.len()
}
pub fn update(&mut self) -> Vec<PsmoveApiObservation> {
unsafe { psmove_tracker_update_image(self.tracker) };
let mut observations = Vec::new();
for controller in &self.controllers {
unsafe { psmove_tracker_update(self.tracker, controller.handle) };
let mut x = 0.0;
let mut y = 0.0;
let mut radius = 0.0;
let age_ms = unsafe {
psmove_tracker_get_position(
self.tracker,
controller.handle,
&mut x,
&mut y,
&mut radius,
)
};
if age_ms >= 0 && radius > 0.0 {
observations.push(PsmoveApiObservation {
move_id: controller.move_id.clone(),
center_x_px: x,
center_y_px: y,
radius_px: radius,
age_ms,
});
}
}
observations
}
pub fn rgb_image(&self) -> Option<(u32, u32, Vec<u8>)> {
let image = unsafe { psmove_tracker_get_image(self.tracker) };
if image.data.is_null() || image.width <= 0 || image.height <= 0 {
return None;
}
let byte_len = (image.width as usize)
.checked_mul(image.height as usize)?
.checked_mul(3)?;
let bytes = unsafe { core::slice::from_raw_parts(image.data, byte_len) };
Some((image.width as u32, image.height as u32, bytes.to_vec()))
}
}
impl Drop for PsmoveApiTracker {
fn drop(&mut self) {
for controller in self.controllers.drain(..) {
unsafe {
psmove_tracker_disable(self.tracker, controller.handle);
psmove_disconnect(controller.handle);
}
}
unsafe { psmove_tracker_free(self.tracker) };
}
}
unsafe fn text(value: *const c_char) -> String {
if value.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(value) }
.to_string_lossy()
.into_owned()
}
}
#[cfg(target_os = "linux")]
pub use linux::PsmoveApiTracker;
#[cfg(not(target_os = "linux"))]
pub struct PsmoveApiTracker;
#[cfg(not(target_os = "linux"))]
impl PsmoveApiTracker {
pub fn open(
_camera_index: i32,
_exposure: f32,
_colors: &[(String, [u8; 3])],
) -> Result<Self, PsmoveApiTrackerError> {
Err(PsmoveApiTrackerError(
"PSMoveAPI tracker backend is currently available on Linux".to_string(),
))
}
pub fn camera_info(&self) -> &PsmoveApiCameraInfo {
unreachable!()
}
pub fn calibrate_connected(&mut self) -> usize {
0
}
pub fn observe_connected(&mut self) -> usize {
0
}
pub fn set_expected_color(&mut self, _move_id: &str, _color: [u8; 3]) -> bool {
false
}
pub fn tracked_controller_count(&self) -> usize {
0
}
pub fn update(&mut self) -> Vec<PsmoveApiObservation> {
Vec::new()
}
pub fn rgb_image(&self) -> Option<(u32, u32, Vec<u8>)> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observation_preserves_stable_move_identity() {
let observation = PsmoveApiObservation {
move_id: "move-000704a39772".to_string(),
center_x_px: 369.0,
center_y_px: 430.0,
radius_px: 10.6,
age_ms: 1,
};
assert_eq!(observation.move_id, "move-000704a39772");
assert!(observation.radius_px > 0.0);
}
}

View file

@ -109,8 +109,8 @@ delivery, or renderer layout. Keepalive loops belong in Idunn, not Odin's
coordinator or individual daemons.
Muninn is the portable local telemetry Verse assembler. Its Rust body lives in
`crates/muninn-daemon` and publishes `muninn.telemetry_surface.v1` through
CultMesh/CultCache. Muninn may run on Raven, Nightwing, Starfire, or any future
the `GameCult/Muninn` repository and publishes `muninn.telemetry_surface.v1`
through CultMesh/CultCache. Muninn may run on Raven, Nightwing, Starfire, or any future
device body. It names locally accessible telemetry affordances: screen capture,
loopback audio, microphones, cameras, and future sensors. Muninn does not start
expensive capture streams merely because the daemon is alive. The default

View file

@ -1,374 +0,0 @@
# Muninn Media Streaming
Muninn live A/V is the stress organ for CultMesh transport. The goal is not
"make OBS show something once." The goal is encoded game video and audio moving
between nearby PCs at LAN speed with bounded latency, observable ownership, and
transport behavior that teaches CultMesh how to carry hot media.
## Objective
Raven Muninn captures screen video and Realtek loopback audio, encodes them with
hardware-first low-latency settings, publishes them through CultNet/CultMesh
media streams, and lets the Muninn OBS plugin render the stream without owning
capture, daemon lifecycle, or transport truth.
## Current Mechanism
The current Raven A/V path is a compatibility path:
1. OBS reads Raven's `muninn.obs_stream_catalog` from the synced CultCache
store.
2. The OBS plugin sends a typed `muninn.capture_stream_command` to Raven's
Muninn daemon over CultNet RUDP.
3. Raven Muninn `serve` owns command acceptance and spawns a daemon-owned
activation child for the requested stream.
4. The activation child starts WASAPI loopback capture and FFmpeg.
5. FFmpeg muxes encoded video and audio into MPEG-TS on stdout.
6. Muninn slices that byte stream into fixed-size chunks and sends those chunks
over the RUDP `media` channel.
7. The OBS plugin forwards received bytes into a local FFmpeg source.
That path proved activation, capture, hardware encode, and cross-machine
delivery. It is not the final media architecture. MPEG-TS byte chunks over an
unordered lossy hot path give CultMesh no frame identity, no deadline, no media
dependency graph, and no clean way to choose between retransmit, conceal,
discard, keyframe request, or audio/video resync.
## Invariants
- Muninn owns local capture and stream activation for the machine body where
the sensors live.
- OBS and Mimir are consumers. They may request a stream and report receiver
health, but they do not start capture by implication and do not own Raven
daemon state.
- Idunn owns daemon supervision and health pressure. It does not infer that
liveness means screen/audio capture should be burning.
- CultMesh owns live media stream semantics: stream identity, frame identity,
timestamps, deadlines, dependencies, channel policy, and receiver feedback.
- CultCache records are receipts, catalog entries, and operator/debug state.
They are not the hot media lane.
- Audio and video have separate clocks and recovery policy. They may share a
session, but they must not be hidden inside an opaque transport byte soup.
- Transport reliability is deadline-bound. Late media is damage, not treasure.
## Intended Change
Replace "MPEG-TS stdout sliced into RUDP packets" with "codec access units and
audio packets published as typed CultMesh media frames."
The stream owner should emit media documents shaped around decisions the
transport and decoder actually need:
```text
muninn.media_video_access_unit.v1
stream_id
session_id
frame_id
codec
pts_ticks
duration_ticks
timebase_num
timebase_den
keyframe
dependency_frame_id
deadline_ticks
chunk_index
chunk_count
payload
muninn.media_audio_packet.v1
stream_id
session_id
packet_id
codec
pts_ticks
duration_ticks
timebase_num
timebase_den
deadline_ticks
payload
muninn.media_receiver_feedback.v1
stream_id
session_id
receiver_id
highest_decodable_frame_id
missing_frame_ids
missing_video_chunk_keys
late_frame_ids
requested_keyframe
jitter_us
decode_queue_us
```
The exact schema names can move when promoted into the shared document catalog,
but the ownership shape should not: frame/access-unit identity is load-bearing.
## Authority Map
- Owner: Muninn capture runtime owns source capture, encode configuration, and
emission of video access units plus audio packets for a requested stream.
- Inputs: explicit `muninn.capture_stream_command`, local capture devices,
encoder capabilities, receiver feedback, stream policy, and Idunn-supervised
daemon runtime state.
- Outputs: typed active stream receipts in CultCache, CultMesh media frames over
CultNet RUDP, and typed receiver/transport health receipts.
- Derived state: OBS catalog entries are discovery hints; local FFmpeg bridge
URLs are compatibility lowering details; packet counters and logs are
observability only.
- Forbidden writers: OBS source settings, local UDP bridge behavior, scheduled
task wrappers, health scripts, and replayed command receipts must not decide
capture state, stream identity, frame order, or media repair policy.
- Shared paths: direct operator requests, OBS plugin requests, future Mimir
requests, restart recovery, and reconnect recovery must all publish the same
typed capture command and consume the same active stream receipts.
- Deletion line: delete the hot-path assumption that a media stream is an
MPEG-TS byte stream. Keep it only as a named compatibility lowering until the
OBS receiver can consume typed media frames directly.
## Boring Stream Discovery Contract
The desired operator experience is boring: OBS asks the Verse for Muninns, the
plugin shows live sources, and a selected source either activates or reports the
specific owner that refused it. No local plugin fallback may invent Raven
devices. A fake `display:0` or Realtek loopback row is worse than an empty list
because it makes stale state look selectable.
Live ownership:
- Owner: Muninn `serve` owns source inventory for the body where the devices
exist. It publishes video and audio source ids, labels, command boundary,
media profile, and current active sessions.
- Inputs: local display/audio enumeration, explicit configured source hints,
daemon health, and accepted stream commands.
- Outputs: `muninn.telemetry_surface.v1`, `gamecult.eve.provider_advertisement.v1`,
`muninn.command_boundary.v1`, `muninn.transport_profile.v1`, active
`muninn.capture_stream.v1` receipts, and the temporary
`muninn.obs_stream_catalog.v1` compatibility record.
- Derived state: the OBS dropdown is a lowering of discovered Muninn state. It
is not an inventory owner, activation owner, or health owner.
- Forbidden writers: OBS defaults, local hard-coded device names, synced stale
CultCache mirrors, scheduled-task launchers, and previous activation receipts
must not create selectable devices or active streams.
- Shared paths: initial OBS load, refresh button, periodic refresh, source
selection, reconnect, and OBS restart must all read the same live-discovered
source list and issue the same typed capture command when activation is
needed.
- Deletion line: the plugin may display `discovery missing`, but it must not
synthesize `Display 1` or `Realtek loopback` as if Raven advertised them.
Until Odin/Verse discovery is the direct plugin input, the OBS catalog remains
a compatibility projection of Muninn-owned live state, not its own truth.
Compatibility store fallback is allowed only as availability fallback: the
plugin must use the first current store that yields Muninn telemetry and must
not merge stale stores into a more attractive fake inventory.
A store older than the Muninn health freshness budget is not a discovery
source; it is an outage receipt.
This contract also splits the media session truth:
- Video session: selected video source id, encoder profile, bitrate, frame
timestamps, access-unit ids, keyframe policy, feedback pressure, and video
receiver queue.
- Audio session: selected audio source id, sample format, packet timestamps,
reorder budget, continuity/drop policy, and OBS audio output cadence.
- Combined OBS source: a convenience lowering that may request one video session
and one audio session together. It does not merge their clocks or transport
state into one opaque "A/V target".
## Known-Good Checkpoint (2026-06-23)
The current system is in a usable compatibility state worth preserving before
the next transport experiment.
What is working:
- Raven Muninn runs continuously as the capture owner and accepts stream
activation over CultNet RUDP.
- OBS on Starfire shows the live-discovered Muninn source inventory instead of
synthesizing fake devices from stale local defaults.
- Video and audio sessions activate separately through the combined OBS source
lowering and the stream comes up cleanly again after reconnect.
- The OBS plugin currently receives the temporary live
`muninn.obs_stream_catalog.v1` projection over UDP `17874` and uses it as the
live discovery source before any CultCache fallback.
Current compatibility details:
- Raven command ingress is currently bound to `0.0.0.0:17883`.
- Starfire OBS listens for the temporary live OBS catalog on UDP `17874`.
- The daemon `serve` loop must continue publishing the OBS catalog while idle;
otherwise OBS falls back to stale store state and reports `discovery-missing`
or shows dead inventory.
- The current OBS plugin is still a compatibility client. It does not yet host
the Rust CultMesh runtime directly and it is not yet consuming Odin/Verse
discovery as a first-class Verse client.
Known debt that remains intentionally visible:
- Idunn health publication can still time out independently of the media path.
That is operational debt, not proof that Muninn discovery or streaming is
down.
- The OBS catalog is still a temporary projection. The boring end state is live
Verse discovery with no plugin-owned catalog receiver and no stale store
merge behavior.
## Codec Direction
Use vendor hardware encode before inventing a codec. NVIDIA's practical answer
in this space is NVENC/NVDEC, low-latency encode presets, high LAN bitrate,
short GOP or intra-refresh, no B-frames, no lookahead, and application-level
transport that understands frame deadlines.
Shipping order:
1. H.264 NVENC low-latency as the compatibility baseline for broad decoder
support, simple failure analysis, and OBS bridge stability.
2. HEVC NVENC low-latency when Raven encode and OBS decode both prove the
target latency budget.
3. AV1 NVENC only after the exact Raven-to-OBS path proves encode, decode, and
queue latency inside budget. AV1 can be the quality/bandwidth winner, but it
must earn the low-latency lane on our hardware instead of getting promoted by
codec fashion.
The codec should produce elementary video access units, not a muxed MPEG-TS
program. Audio should use Opus low-delay packets unless the OBS lowering needs
a temporary PCM/AAC compatibility bridge.
Recommended first LAN profile:
- Profile id: `muninn.rudp.low_latency_h264_lan.v1`.
- Video: H.264 NVENC, `p1`, ultra-low-latency tune, CBR high enough that LAN
bandwidth is not the constraint, no B-frames, no lookahead, short GOP or
intra-refresh, periodic IDR/keyframe on feedback pressure.
- VBV: derive `bufsize` from roughly one frame of bitrate
(`bitrate_kbps / framerate`) instead of a large fixed encoder reservoir.
The point is bounded latency, not hoarding compressed video in a nicer hat.
- Audio: Opus low-delay when OBS lowering can consume it directly; otherwise a
temporary PCM/AAC bridge with separate packet identity and clock.
- Transport: payload elementary access units as typed media records, never a
muxed container as the unit of truth.
- Packet size: default typed media chunks are `480` bytes. That leaves headroom
for CultNet/MessagePack/RUDP headers inside a normal Ethernet MTU so the OBS
bridge does not depend on IP fragmentation or private RUDP fragment
reassembly. The sender also sets CultNet RUDP `max_fragment_bytes` to `1431`
bytes (`1472` byte IPv4 UDP payload budget minus `36` RUDP fixed header bytes
minus the five-byte `media` channel id), so oversized typed records fragment
in CultNet deliberately instead of falling through to accidental IP
fragmentation.
- Resend cadence: sender RUDP media resends are scheduled every `5` ms while
the packet is still inside the `75` ms reliable-expiry budget.
- Control: sender may adapt bitrate, keyframe cadence, chunk size, and playout
budget from receiver feedback; the receiver must not silently stretch latency
to preserve visual perfection.
- Receiver URL policy: RUDP stream URLs carry `profile`,
`sender_resend_delay_ms`, `reliable_expire_after_ms`,
`assembly_deadline_ms`, and `gap_wait_ms` so the OBS bridge receives timing
budget from Muninn's stream profile instead of hard-coding receiver policy
privately.
### Motion Vectors And Reconstruction
Do not make CultMesh a bespoke game-streaming codec in the first shipping path.
The motion-vector/delta idea is real, but it belongs as an augmentation around
the hardware codec, not as the primary authority.
NVIDIA's relevant public pattern is:
- NVENC/NVDEC for hardware video encode and decode.
- Low-latency encode tuning, high refresh rate, adaptive bitrate, and explicit
network/frame timing for cloud gaming.
- Foveated streaming for bandwidth reduction when a trusted focus region exists.
- Optical Flow / motion estimation hardware for interpolation, extrapolation,
analysis, or concealment experiments.
For Muninn, that means:
- If we only have desktop capture pixels, trust NVENC for motion estimation and
compression. Do not compute our own patch stream before the codec has failed a
measured requirement.
- If a game or renderer we control can expose motion vectors, depth, regions of
interest, or dirty rectangles, publish them as optional sideband media hints.
They may guide encoder ROI, decoder concealment, frame extrapolation, or
foveated quality allocation. They do not replace the encoded video access
unit unless a separate research path proves that decoder under the same OBS
latency budget.
- Stochastic reconstruction is allowed only inside a bounded playout budget.
When the deadline expires, the receiver displays the best available frame,
conceals damage, or requests a keyframe. It must not preserve dead packets by
growing latency.
Useful references for this direction:
- NVIDIA Video Codec SDK presets describe low-latency and ultra-low-latency
tuning plus preset tradeoffs for encoder tools such as GOP structure,
B-frames, and lookahead:
<https://developer.nvidia.com/blog/introducing-video-codec-sdk-10-presets/>
- NVIDIA CloudXR publicly frames low-latency spatial streaming around NVENC,
adaptive bitrate, foveated streaming, diagnostics, and bidirectional data
channels:
<https://docs.nvidia.com/cloudxr-sdk/latest/overview/overview.html>
- NVIDIA's Optical Flow SDK exposes hardware motion vectors for real-time
interpolation/extrapolation and related video processing:
<https://developer.nvidia.com/optical-flow-sdk>
## CultNet RUDP Media Policy
The RUDP `media` channel should become a media-aware lane, not merely a reliable
unordered byte lane.
Required behavior:
- Frame-level fragmentation and reassembly keyed by `(stream_id, session_id,
frame_id)`.
- Deadline-aware retransmission: resend while useful, drop when the playout
budget is gone.
- Receiver feedback that can NACK missing chunks, request a keyframe, and
report jitter/decode queue pressure.
- Small playout buffer chosen by policy, for example 8-20 ms on a wired LAN.
- Optional FEC/parity for video chunks where one repair packet beats a resend.
- Separate audio policy that favors continuity and bounded drift over perfect
packet recovery.
## OBS Lowering
The OBS plugin should lower typed media frames into OBS-friendly video/audio.
The clean target is direct decode from elementary streams using libavcodec or
NVDEC where available, then hand decoded video frames and audio packets to OBS.
Temporary bridge allowed:
- Reconstruct an elementary stream from typed video access units.
- Feed the local FFmpeg source only as a compatibility decoder.
- Keep audio as a separate OBS audio path so audio sync is explicit instead of
being hidden inside a damaged MPEG-TS stream.
The compatibility bridge must not own stream truth. It is a lowering target,
not the protocol.
## Cut Plan
1. Promote media-frame schemas in Odin/CultLib and add unit tests for
encode/decode round trips.
2. Teach Muninn activation to emit video access units and audio packets instead
of arbitrary MPEG-TS byte chunks.
3. Teach CultNet RUDP media receive to reassemble by frame and deadline, with
explicit NACK/keyframe feedback.
4. Teach the OBS plugin to consume typed media frames and feed OBS through a
direct decoder or a narrowly named compatibility elementary-stream bridge.
5. Remove the MPEG-TS byte-chunk hot path once the typed receiver is stable.
## Verification
The rebuild is not proven by "OBS shows video once." Minimum proof:
- Raven `serve` remains idle until an explicit capture command arrives.
- OBS request activates Raven without manual Raven terminals.
- Sender logs and receiver telemetry agree on stream id, session id, frame id,
codec, PTS, deadlines, drops, NACKs, and keyframe requests.
- A forced packet drop causes bounded damage and recovery, not OBS freeze.
- Audio remains present as an OBS audio source and stays within the chosen
sync budget.
- Restarting OBS does not strand Raven capture or poison the next request with
stale command receipts.
- Restarting Raven Muninn restores catalog/health without starting capture
until requested.

View file

@ -1,262 +0,0 @@
# Muninn
Muninn is Odin's portable local telemetry Verse assembler.
It runs on a device body and publishes a typed CultMesh/CultCache surface for
what that body can sense or expose locally: screen capture, loopback audio,
microphones, cameras, and future sensors.
## Authority
- Owner: Muninn owns local telemetry discovery, stream affordance publication,
and explicit activation of requested local streams.
- Inputs: cheap local probes, operator or Verse activation requests, and local
capture tools such as FFmpeg or WASAPI helpers.
- Outputs: `muninn.telemetry_surface.v1`, active `muninn.capture_stream.v1`,
Quest access records, Move receipt records, and a CultMesh bytes stream
carrying live Move evidence frames for Mimir.
- Forbidden writers: daemon startup, Idunn keepalive, Mimir ingest, OBS, Odin,
Gjallar, and renderer bodies must not start capture by implication.
## Runtime
```powershell
cargo build -p muninn-daemon
muninn serve --store C:\Meta\Odin\state\muninn.telemetry.cc --interval-seconds 15 --idunn-rudp-health $env:IDUNN_RUDP_HEALTH --idunn-daemon starfire-muninn --idunn-health-contract muninn.cultnet-rudp-local-telemetry-and-quest-access
muninn --health --store C:\Meta\Odin\state\muninn.telemetry.cc
```
`serve` is cheap and idle. It publishes affordances, consumes addressed pending
`muninn.move_light_command.v1` records, and keeps the local Verse surface fresh
without starting FFmpeg, screen capture, microphone capture, or loopback
capture.
Muninn writes its stream affordance catalog as typed CultCache state inside:
```text
C:\Meta\Odin\state\muninn.telemetry.cc
```
The record is type `muninn.obs_stream_catalog` with schema
`muninn.obs_stream_catalog.v1` at key `obs`. Consumers discover the catalog
through Odin/CultMesh; Muninn does not publish a parallel OBS RUDP catalog.
Activation is explicit typed state:
```powershell
muninn request-stream --target-host cultmesh://odin/media/muninn-raven-av --media-transport rudp
```
`target_host` in the legacy command record field is a CultMesh URI, not a host
or IP. The activation child resolves `muninn.media.rudp.v1` endpoints from
Odin's provider catalog before opening the CultNet RUDP media lane. SRT, OBS
target flags, and standalone Raven activation scripts are archived; Mimir,
OBS, and other renderers are consumers of Odin/CultMesh discovery, not transport
owners.
## Quest Access And Unity Return Video
Quest hardware attached to Starfire is a Muninn local telemetry surface, not a
Mimir-owned preflight. Enable it by running Muninn on Starfire with ADB probing:
```powershell
muninn serve `
--store C:\Meta\Odin\state\muninn.telemetry.cc `
--host starfire `
--quest-adb `
--quest-serial 1WMHHB68PG1515
```
When `adb devices -l` reports the Quest as `device`, Muninn publishes
`muninn.quest_access.v1` at `quest-access` and
`muninn:<host>:quest-access:<serial>`. The record advertises:
- `muninn:<host>:quest-input`: Quest buttons/analog/controller input once a
Quest/OpenXR witness is running.
- `muninn:<host>:quest-poses`: headset and controller poses from that same
witness.
- `muninn:<host>:quest-warped-video-input`: warp-corrected video frames that
Brokkr can route from Starfire Unity editor play mode toward the Quest device.
ADB authorization proves local USB access only. It does not by itself expose
OpenXR poses or accept video frames. A Quest/OpenXR witness still owns headset
runtime sampling, while Brokkr owns the Unity editor adapter that sends
warp-corrected play-mode frames to Muninn's advertised Quest video input.
Read the current record with:
```powershell
muninn quest-access-status --store C:\Meta\Odin\state\muninn.telemetry.cc
```
For Starfire's local Quest-attached daemon, Idunn supervises the
`starfire-muninn` target through:
```powershell
E:\Projects\Odin\scripts\restart-starfire-muninn.cmd
E:\Projects\Odin\scripts\health-starfire-muninn.cmd
```
The restart script launches Muninn hidden with `--host starfire --quest-adb`,
`--idunn-rudp-health` from explicit `-IdunnRudpHealth` or
`IDUNN_RUDP_HEALTH`, `--idunn-daemon starfire-muninn`, and
`--idunn-health-contract muninn.cultnet-rudp-local-telemetry-and-quest-access`.
If the CultCache store at `C:\Meta\Odin\state\starfire.muninn.telemetry.cc`
fails MessagePack decode on boot, the restart path archives the corrupt file,
clears the stale `.lock`, and relaunches the daemon instead of leaving the lane
dead.
The deployed loopback helper must accept Muninn's command contract:
```powershell
wasapi-loopback-capture.ps1 -Output stdout -Device Realtek -SampleRate 48000 -Channels 2
```
`stdout` is an alias for binary standard output, and `Device` is the requested
render-device hint. Current helper builds may ignore the hint and use the
default render endpoint, but they must accept the argument so Muninn's generated
mux command remains executable.
## Move Marker Candidates
`crates/muninn-move-tracker` is Muninn's native/Rust PS Move optical candidate
extractor. It owns dispatch planning, FFI, configuration validation, a CPU
mirror, and the HLSL 16px-tile luma reduction shader. It emits marker
candidates only: weighted centroid, radius, area, peak/mean luma, and score for
one camera frame.
Muninn owns publishing those candidates as `muninn.move_marker_candidate.v1`.
Muninn also publishes USB controller facts as
`muninn.move_controller_state.v1`: accelerometer, gyro, magnetometer, button,
trigger, battery, sequence, and source timestamp. Mimir is the consumer of both
sensor streams. Odin may discover and project the schemas, but Odin does not
own raw capture, candidate extraction, calibration, triangulation, IMU fusion,
prediction, or final 6DoF pose. Muninn does not synthesize wand pose; it
reports what the local body saw and read from USB.
The hot tracking path is a CultMesh stream, not CultCache polling. When
`serve` has one or more `--move-state` sources, it declares
`muninn:<host>:move-evidence` in Verse `mimir-live` and publishes
MessagePack `mimir.muninn_move_evidence_stream_frame.v1` bytes through a
shared-memory frame ring. The frame contains any marker candidates available
from local optical extraction plus the controller states read from USB. The
`muninn.move_marker_candidate.v1` and `muninn.move_controller_state.v1` records
remain receipts/debug state in the `.cc` store; Mimir drinks the stream and
owns association, calibration, fusion, prediction, and final pose.
On Linux hosts, enable the controller-state feed by passing one or more Move
joystick sources to `serve`:
```bash
muninn serve \
--store ~/.local/state/gamecult/muninn/muninn.telemetry.cc \
--host nightwing \
--move-state move-usb=/dev/input/by-id/usb-Sony_Computer_Entertainment_Motion_Controller-joystick
```
The published values are raw Linux joystick/HID counts. Mimir owns calibration,
axis interpretation, unit conversion, fusion, prediction, and resolved pose
publication. Hidraw remains the local output path for LED reports.
Use `--move-evidence-stream <stream-id>` or `--move-evidence-verse <verse-id>`
only when the default `muninn:<host>:move-evidence` / `mimir-live` address is
not the desired Mimir-facing stream identity.
## Move Light Commands
Muninn is also the local output owner for USB-attached PS Moves. When Mimir
wants structured light pulses for calibration or tracking, it publishes a typed
`muninn.move_light_command.v1` command over CultNet/CultMesh to the Muninn
daemon on the host that owns the Move. `serve` consumes `pending` commands
whose `host_id` matches the local Muninn host, writes PS Move HID report `0x06`
to the command's `hidraw_path`, and updates the same command record to
`running`, `completed`, or `failed`.
For operator smoke and bring-up, `request-move-light` writes that typed command
into the local Muninn store without touching HID directly:
```bash
muninn request-move-light \
--store ~/.local/state/gamecult/muninn/muninn.telemetry.cc \
--host nightwing \
--move move-usb \
--hidraw /dev/hidraw1 \
--color 35ff6c \
--duration-ms 0 \
--repeat-count 1
muninn move-light-status \
--store ~/.local/state/gamecult/muninn/muninn.telemetry.cc \
--host nightwing
```
Idunn keeps the Muninn daemon alive. Idunn does not learn a Move-specific
watcher, and Mimir does not write HID directly except through temporary smoke
scripts used to prove hardware behavior before a Muninn daemon is available.
When `serve` is launched with `--idunn-rudp-health`, `--idunn-daemon`, and
`--idunn-health-contract`, the long-running Muninn body publishes
`idunn.daemon_health` directly to Idunn over `cultnet.transport.rudp.v0` on its
normal cadence. `--health` keeps the same publication path for manual proof and
compatibility probes, but the live owner is the daemon's `serve` process.
Quest ADB probing is a telemetry input, not daemon liveness. If `adb` is
missing or the Quest is unavailable, Muninn publishes `muninn.quest_access` as
`unavailable` and keeps serving the local telemetry surface.
## Host Deployments
Raven runs Muninn from `C:\Meta\Odin\Muninn`. `scripts/restart-muninn.ps1`
recreates only the `GameCult-Muninn` scheduled task. It also unregisters the
obsolete `GameCult-Muninn-Activate` and `GameCult-Muninn-VideoProof` tasks if
they are present. The serve launcher starts `muninn.exe` with
`-WindowStyle Hidden`, requires `--idunn-rudp-health` from explicit
`-IdunnRudpHealth` or `IDUNN_RUDP_HEALTH`, passes `--idunn-daemon muninn`, and
`--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-health`, and
requires `-MediaTargetUri` / `MUNINN_MEDIA_TARGET_URI` to be a `cultmesh://`
URI resolved through Odin at activation time. It
redirects logs under `C:\Meta\Odin\logs\muninn`.
Raven is an operator-consented host: Muninn operations on Raven must be
background-only and must not create visible terminal windows. `.cmd` files may
exist only as manual compatibility entrypoints that call the same hidden VBS
launchers; neither Task Scheduler nor the hidden VBS layer may route through a
`cmdPath` trampoline on Raven.
Health must fail if the obsolete activation or video-proof tasks still exist on
Raven. If Raven is unreachable, the repo is prepared but the live scheduler is
not clean.
Nightwing Muninn is kept alive by the single Idunn supervisor through the
`nightwing-muninn` daemon target. Idunn learns that target through Odin's typed
daemon surface and invokes `scripts/health-nightwing-muninn.ps1` and
`scripts/restart-nightwing-muninn.ps1`; those scripts are health/restart
actuators only, not lifecycle owners. The binary is installed at
`/home/metacrat/.local/bin/muninn`, the store lives at
`/home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc`, and the
restart actuator launches `serve --host nightwing --interval-seconds 15` with
`--move-state move-usb=/dev/input/by-id/usb-Sony_Computer_Entertainment_Motion_Controller-joystick`,
`--idunn-rudp-health` supplied by explicit `-IdunnRudpHealth` or
`IDUNN_RUDP_HEALTH`,
`--idunn-daemon nightwing-muninn`,
`--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-and-move-hid`,
PID, and logs under
`/home/metacrat/.local/state/gamecult/muninn`.
With that Move state source attached, `serve` also publishes the
`muninn:nightwing:move-evidence` CultMesh stream frame body that Mimir's native
Move evidence reservoir consumes.
Nightwing source discovery must emit one Move source per controller id. The USB
pairing collection and Bluetooth joystick can expose the same controller as
separate `/dev/input/js*` paths; `nightwing-move-state-sources.sh` prefers the
Bluetooth `HID_ID=0005:0000054C:000003D5` path so Idunn health does not demand
fresh records from two faces of the same controller. The Nightwing health
actuator requires the same configured Idunn RUDP endpoint, but that command is
fallback proof only; the live keepalive contract is now published by the
long-running Nightwing `serve` process.
Each private PSMoveAPI Eye worker owns optical observation admission before the
parent aggregates evidence. Positions older than 50 ms, blobs below 2 px,
out-of-frame coordinates, implausible radius jumps, and frame-to-frame
teleports are rejected locally. Typed `muninn.move_tracker_health.v1` records
publish rejection counts by stale/radius/bounds/continuity reason.
PSMoveAPI's position age is freshness, not optical confidence. The aggregate
marker candidate therefore carries an explicit uncalibrated score of `0.5`
until the observer exposes a real fit-quality measure. Consumers must not treat
the thresholded color-blob radius as a calibrated physical sphere silhouette;
per-Eye radius response belongs to Mimir's calibration evidence.

View file

@ -1,2 +0,0 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0activate-muninn-raven-av-srt.ps1

View file

@ -1 +0,0 @@
throw "activate-muninn-raven-media.ps1 archived: Muninn media activation is daemon-owned and target resolution is through Odin/CultMesh. Redeploy Muninn through Idunn and publish a typed muninn.capture_stream_command with a cultmesh:// media target URI."

View file

@ -1,321 +0,0 @@
param(
[string] $RavenHost = "raven.Home",
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
[string] $LocalMuninnExe = "",
[int] $ConnectTimeoutSeconds = 10,
[string] $SshUser = "madman's lullaby",
[string] $IdentityFile = "C:\Users\Meta\.ssh\id_ed25519_192_168_1_84",
[switch] $SkipBuild,
[switch] $SkipRestart,
[switch] $PreflightOnly
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
if ([string]::IsNullOrWhiteSpace($LocalMuninnExe)) {
$LocalMuninnExe = Join-Path $repoRoot "target\release\muninn.exe"
}
if (-not $SkipBuild) {
Push-Location $repoRoot
try {
cargo build -p muninn-daemon --release
if ($LASTEXITCODE -ne 0) {
throw "cargo build -p muninn-daemon --release failed with exit code $LASTEXITCODE"
}
} finally {
Pop-Location
}
}
if (-not (Test-Path -LiteralPath $LocalMuninnExe)) {
throw "Local Muninn executable was not found at '$LocalMuninnExe'."
}
if ($ConnectTimeoutSeconds -lt 1) {
throw "ConnectTimeoutSeconds must be at least 1."
}
if (-not $SkipRestart) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "verify-muninn-media-profile.ps1") `
-RestartScript (Join-Path $PSScriptRoot "restart-muninn.ps1")
if ($LASTEXITCODE -ne 0) {
throw "verify-muninn-media-profile.ps1 failed with exit code $LASTEXITCODE"
}
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
[Parameter(Mandatory = $true)] [string] $Content
)
[System.IO.File]::WriteAllText($Path, ($Content -replace "`r?`n", "`r`n"), [System.Text.Encoding]::ASCII)
}
function ConvertTo-PowerShellStringLiteral {
param([Parameter(Mandatory = $true)] [string] $Value)
return "'" + $Value.Replace("'", "''") + "'"
}
function Get-SshCommonArgs {
param([int] $TimeoutSeconds = $ConnectTimeoutSeconds)
$args = @(
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=$TimeoutSeconds",
"-o", "ConnectionAttempts=1"
)
if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) {
$args += @("-i", $IdentityFile)
}
return $args
}
function Get-SshTarget {
param([Parameter(Mandatory = $true)] [string] $Target)
if ([string]::IsNullOrWhiteSpace($SshUser)) {
return $Target
}
return "${SshUser}@${Target}"
}
function Get-SshEffectiveConfig {
param([Parameter(Mandatory = $true)] [string] $Target)
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$commonArgs = Get-SshCommonArgs
$sshTarget = Get-SshTarget -Target $Target
$output = & ssh.exe -G @commonArgs $sshTarget 2>&1
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
$config = @{
hostname = $Target
user = $null
identityfile = @()
}
if ($LASTEXITCODE -ne 0) {
$config.error = ($output -join " ")
return $config
}
foreach ($line in $output) {
if ($line -match "^hostname\s+(.+)$") {
$config.hostname = $Matches[1]
} elseif ($line -match "^user\s+(.+)$") {
$config.user = $Matches[1]
} elseif ($line -match "^identityfile\s+(.+)$") {
$config.identityfile += $Matches[1]
}
}
return $config
}
function Test-TcpPort {
param(
[Parameter(Mandatory = $true)] [string] $HostName,
[Parameter(Mandatory = $true)] [int] $Port,
[Parameter(Mandatory = $true)] [int] $TimeoutSeconds
)
$client = [System.Net.Sockets.TcpClient]::new()
try {
$async = $client.BeginConnect($HostName, $Port, $null, $null)
if (-not $async.AsyncWaitHandle.WaitOne([TimeSpan]::FromSeconds($TimeoutSeconds))) {
return "timeout"
}
$client.EndConnect($async)
return "reachable"
} catch {
return $_.Exception.Message
} finally {
$client.Close()
}
}
function Test-IcmpHost {
param(
[Parameter(Mandatory = $true)] [string] $HostName,
[Parameter(Mandatory = $true)] [int] $TimeoutSeconds
)
$ping = [System.Net.NetworkInformation.Ping]::new()
try {
$reply = $ping.Send($HostName, [Math]::Max(1, $TimeoutSeconds) * 1000)
if ($null -ne $reply -and $reply.Status -eq [System.Net.NetworkInformation.IPStatus]::Success) {
return "reachable"
}
if ($null -ne $reply) {
return $reply.Status.ToString()
}
return "unreachable"
} catch {
return $_.Exception.Message
} finally {
$ping.Dispose()
}
}
function Get-RavenSshDiagnostic {
param(
[Parameter(Mandatory = $true)] [string] $Target,
[Parameter(Mandatory = $true)] [int] $TimeoutSeconds
)
$config = Get-SshEffectiveConfig -Target $Target
$hostName = [string] $config.hostname
$icmp = Test-IcmpHost -HostName $hostName -TimeoutSeconds $TimeoutSeconds
$tcp22 = Test-TcpPort -HostName $hostName -Port 22 -TimeoutSeconds $TimeoutSeconds
$identity = if ($config.identityfile.Count -gt 0) {
($config.identityfile -join ",")
} else {
"default"
}
$user = if ([string]::IsNullOrWhiteSpace($config.user)) { "default" } else { $config.user }
$configError = if ($config.ContainsKey("error")) { "; ssh_config_error=$($config.error)" } else { "" }
return "resolved_host=$hostName user=$user identityfile=$identity icmp=$icmp tcp22=$tcp22$configError"
}
function Invoke-RavenSshPreflight {
param(
[Parameter(Mandatory = $true)] [string] $Target,
[Parameter(Mandatory = $true)] [int] $TimeoutSeconds
)
$sshArgs = @(
(Get-SshTarget -Target $Target),
"cmd /c echo raven-ssh-ready"
)
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$commonArgs = Get-SshCommonArgs -TimeoutSeconds $TimeoutSeconds
$output = & ssh.exe @commonArgs @sshArgs 2>&1
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
if ($LASTEXITCODE -ne 0) {
$diagnostic = Get-RavenSshDiagnostic -Target $Target -TimeoutSeconds $TimeoutSeconds
throw "Raven SSH preflight failed for '$Target' within ${TimeoutSeconds}s: $($output -join ' '); $diagnostic"
}
}
Invoke-RavenSshPreflight -Target $RavenHost -TimeoutSeconds $ConnectTimeoutSeconds
if ($PreflightOnly) {
Write-Host "Raven SSH preflight succeeded for '$RavenHost'."
exit 0
}
$deployId = [guid]::NewGuid().ToString("N")
$localTempRoot = Join-Path $env:TEMP "odin-raven-muninn-binary-$deployId"
$localRemoteScript = Join-Path $localTempRoot "deploy-raven-muninn-binary.ps1"
$localSftpBatch = Join-Path $localTempRoot "deploy-raven-muninn-binary.sftp"
$remoteExeSftpPath = "C:/Windows/Temp/muninn-$deployId.exe"
$remoteScriptSftpPath = "C:/Windows/Temp/deploy-raven-muninn-binary-$deployId.ps1"
$remoteExePath = "C:\Windows\Temp\muninn-$deployId.exe"
$remoteScriptPath = "C:\Windows\Temp\deploy-raven-muninn-binary-$deployId.ps1"
$remoteBackupPath = "$MuninnExe.bak-$deployId"
try {
New-Item -ItemType Directory -Force -Path $localTempRoot | Out-Null
$remoteScript = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
`$target = $(ConvertTo-PowerShellStringLiteral $MuninnExe)
`$incoming = $(ConvertTo-PowerShellStringLiteral $remoteExePath)
`$backup = $(ConvertTo-PowerShellStringLiteral $remoteBackupPath)
if (-not (Test-Path -LiteralPath `$incoming)) {
throw "Uploaded Muninn executable was not found at `$incoming"
}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent `$target) | Out-Null
Get-CimInstance Win32_Process |
Where-Object { `$_.Name -ieq "muninn.exe" } |
ForEach-Object {
& cmd.exe /c ("taskkill /PID {0} /T /F >nul 2>&1" -f `$_.ProcessId) | Out-Null
}
if (Test-Path -LiteralPath `$target) {
Copy-Item -LiteralPath `$target -Destination `$backup -Force
}
Copy-Item -LiteralPath `$incoming -Destination `$target -Force
Remove-Item -LiteralPath `$incoming -Force -ErrorAction SilentlyContinue
`$item = Get-Item -LiteralPath `$target
Write-Output ("muninn.exe deployed length={0} path={1}" -f `$item.Length, `$item.FullName)
`$global:LASTEXITCODE = 0
"@
Set-AsciiFile -Path $localRemoteScript -Content $remoteScript
Set-AsciiFile -Path $localSftpBatch -Content (@(
'put "{0}" "{1}"' -f $LocalMuninnExe, $remoteExeSftpPath
'put "{0}" "{1}"' -f $localRemoteScript, $remoteScriptSftpPath
) -join "`r`n")
$sftpArgs = @(
"-b", $localSftpBatch,
(Get-SshTarget -Target $RavenHost)
)
$commonArgs = Get-SshCommonArgs
& sftp.exe @commonArgs @sftpArgs
if ($LASTEXITCODE -ne 0) {
throw "sftp upload to Raven failed with exit code $LASTEXITCODE"
}
$remoteRunner = @"
`$ErrorActionPreference = "Stop"
try {
& "$remoteScriptPath"
exit 0
} finally {
Remove-Item -LiteralPath "$remoteScriptPath" -Force -ErrorAction SilentlyContinue
}
"@
$encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner))
$commonArgs = Get-SshCommonArgs
$sshTarget = Get-SshTarget -Target $RavenHost
& ssh.exe @commonArgs $sshTarget "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand $encodedRunner"
if ($LASTEXITCODE -ne 0) {
throw "remote Muninn binary deploy failed with exit code $LASTEXITCODE"
}
} finally {
Remove-Item -LiteralPath $localTempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
if (-not $SkipRestart) {
$restartArgs = @(
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
(Join-Path $PSScriptRoot "restart-muninn.ps1"),
"-RavenHost",
$RavenHost,
"-MuninnExe",
$MuninnExe,
"-ConnectTimeoutSeconds",
$ConnectTimeoutSeconds.ToString()
)
if (-not [string]::IsNullOrWhiteSpace($SshUser)) {
$restartArgs += @("-SshUser", $SshUser)
}
if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) {
$restartArgs += @("-IdentityFile", $IdentityFile)
}
& powershell.exe @restartArgs
if ($LASTEXITCODE -ne 0) {
throw "restart-muninn.ps1 failed with exit code $LASTEXITCODE"
}
}

View file

@ -1,3 +0,0 @@
@echo off
echo health-muninn.cmd archived: Raven Muninn health is daemon-published over CultNet/RUDP as muninn.cultnet-rudp-remote-telemetry-health. Use health-muninn.ps1 only as an explicit witness inspection, not Idunn health truth.
exit /b 2

View file

@ -1,217 +0,0 @@
param(
[string] $RavenHost = "raven-local",
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
[string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc",
[string] $ActivateStorePath = "C:\Meta\Odin\state\muninn.activate.cc",
[string] $LogRoot = "C:\Meta\Odin\logs\muninn",
[int] $MaxStoreAgeSeconds = 180,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[int] $ConnectTimeoutSeconds = 10,
[string] $SshUser = "madman's lullaby",
[string] $IdentityFile = ""
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
[Parameter(Mandatory = $true)] [string] $Content
)
[System.IO.File]::WriteAllText($Path, ($Content -replace "`r?`n", "`r`n"), [System.Text.Encoding]::ASCII)
}
function Get-SshCommonArgs {
$args = @(
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=$ConnectTimeoutSeconds",
"-o", "ConnectionAttempts=1"
)
if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) {
$args += @("-i", $IdentityFile)
}
return $args
}
function Get-SshTarget {
param([Parameter(Mandatory = $true)] [string] $Target)
if ([string]::IsNullOrWhiteSpace($SshUser)) {
return $Target
}
return "${SshUser}@${Target}"
}
function Invoke-RavenUploadedPowerShell {
param(
[Parameter(Mandatory = $true)] [string] $RavenHost,
[Parameter(Mandatory = $true)] [string] $RemoteScriptContent,
[Parameter(Mandatory = $true)] [string] $TempPrefix
)
$uploadId = [guid]::NewGuid().ToString("N")
$localTempRoot = Join-Path $env:TEMP "$TempPrefix-$uploadId"
$localRemoteScript = Join-Path $localTempRoot "$TempPrefix-$uploadId.ps1"
$localSftpBatch = Join-Path $localTempRoot "$TempPrefix-$uploadId.sftp"
$remoteSftpPath = "C:/Windows/Temp/$TempPrefix-$uploadId.ps1"
$remotePsPath = "C:\Windows\Temp\$TempPrefix-$uploadId.ps1"
try {
New-Item -ItemType Directory -Force -Path $localTempRoot | Out-Null
Set-AsciiFile -Path $localRemoteScript -Content $RemoteScriptContent
Set-AsciiFile -Path $localSftpBatch -Content ('put "{0}" "{1}"' -f $localRemoteScript, $remoteSftpPath)
$commonArgs = Get-SshCommonArgs
$sshTarget = Get-SshTarget -Target $RavenHost
& sftp.exe @commonArgs -b $localSftpBatch $sshTarget
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$remoteRunner = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
try {
& "$remotePsPath"
exit 0
} finally {
Remove-Item -LiteralPath "$remotePsPath" -Force -ErrorAction SilentlyContinue
}
"@
$encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner))
$commonArgs = Get-SshCommonArgs
$sshTarget = Get-SshTarget -Target $RavenHost
& ssh.exe @commonArgs $sshTarget "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand $encodedRunner"
} finally {
Remove-Item -LiteralPath $localTempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
$remoteScript = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
if (-not (Test-Path -LiteralPath "$MuninnExe")) {
throw "Muninn executable not found at $MuninnExe"
}
`$muninnDir = Split-Path -Parent "$MuninnExe"
`$taskExpectations = @(
@{ Name = "GameCult-Muninn"; Launcher = "powershell"; Vbs = Join-Path `$muninnDir "start-muninn-serve-hidden.vbs"; Ps = Join-Path `$muninnDir "start-muninn-serve.ps1" }
)
foreach (`$expectation in `$taskExpectations) {
`$task = Get-ScheduledTask -TaskName `$expectation.Name -ErrorAction Stop
`$action = @(`$task.Actions)[0]
if (`$expectation.Launcher -eq "powershell") {
if (`$action.Execute -notmatch 'powershell\.exe$') {
throw "`$(`$expectation.Name) action executes `$(`$action.Execute), expected powershell.exe"
}
if (`$action.Arguments -notlike "*`$(`$expectation.Ps)*") {
throw "`$(`$expectation.Name) action arguments `$(`$action.Arguments) do not reference `$(`$expectation.Ps)"
}
if (`$action.Arguments -notlike "*-File*") {
throw "`$(`$expectation.Name) action arguments `$(`$action.Arguments) do not execute a PowerShell launcher"
}
} else {
if (`$action.Execute -notmatch '(^|\\)wscript\.exe$') {
throw "`$(`$expectation.Name) action executes `$(`$action.Execute), expected wscript.exe"
}
if (`$action.Arguments -notlike "*`$(`$expectation.Vbs)*") {
throw "`$(`$expectation.Name) action arguments `$(`$action.Arguments) do not reference `$(`$expectation.Vbs)"
}
if (`$action.Arguments -notlike "*//B*" -or `$action.Arguments -notlike "*//Nologo*") {
throw "`$(`$expectation.Name) action arguments `$(`$action.Arguments) do not force hidden WScript execution"
}
}
if (-not (Test-Path -LiteralPath `$expectation.Vbs)) {
throw "Missing hidden launcher for `$(`$expectation.Name) at `$(`$expectation.Vbs)"
}
if (-not (Test-Path -LiteralPath `$expectation.Ps)) {
throw "Missing PowerShell launcher for `$(`$expectation.Name) at `$(`$expectation.Ps)"
}
`$psContent = Get-Content -LiteralPath `$expectation.Ps -Raw
if (`$psContent -like "*'' | ConvertFrom-Json*") {
throw "`$(`$expectation.Ps) has an empty encoded-arguments payload"
}
if (`$psContent -notmatch 'Start-Process') {
throw "`$(`$expectation.Ps) does not launch a hidden process"
}
`$vbs = Get-Content -LiteralPath `$expectation.Vbs -Raw
if (`$vbs -match 'cmdPath\s*=') {
throw "`$(`$expectation.Vbs) still routes through a cmdPath trampoline"
}
if (`$vbs -notmatch '\.ps1') {
throw "`$(`$expectation.Vbs) does not reference a PowerShell launcher"
}
}
if (Get-ScheduledTask -TaskName "GameCult-Muninn-Activate" -ErrorAction SilentlyContinue) {
throw "Obsolete GameCult-Muninn-Activate task is still present on Raven"
}
if (Get-ScheduledTask -TaskName "GameCult-Muninn-VideoProof" -ErrorAction SilentlyContinue) {
throw "Obsolete GameCult-Muninn-VideoProof task is still present on Raven"
}
`$servePidPath = Join-Path "$LogRoot" "muninn-serve.pid"
`$servePid = `$null
if (Test-Path -LiteralPath `$servePidPath) {
`$servePidText = (Get-Content -LiteralPath `$servePidPath -Raw).Trim()
if (`$servePidText -match '^\d+$') {
`$servePid = [int] `$servePidText
}
}
`$process = `$null
if (`$null -ne `$servePid) {
`$process = Get-CimInstance Win32_Process -Filter ("ProcessId = {0}" -f `$servePid) -ErrorAction SilentlyContinue |
Where-Object { `$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*serve*" } |
Select-Object -First 1
}
if (`$null -eq `$process) {
`$process = Get-CimInstance Win32_Process |
Where-Object { `$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*serve*" } |
Select-Object -First 1
}
if (`$null -eq `$process) {
throw "Muninn serve process is not running"
}
foreach (`$pattern in @(
"--host raven",
"--activate-store $ActivateStorePath",
"--idunn-rudp-health $IdunnRudpHealth",
"--idunn-daemon muninn",
"--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-health"
)) {
if (`$process.CommandLine -notlike "*`$pattern*") {
throw "Muninn serve process is missing expected command-line segment `${pattern}: `$(`$process.CommandLine)"
}
}
`$conflictingWriter = Get-CimInstance Win32_Process |
Where-Object {
`$_.Name -like "muninn*.exe" -and
`$_.ProcessId -ne `$process.ProcessId -and
`$_.ParentProcessId -ne `$process.ProcessId -and
`$_.CommandLine -like "*activate*" -and
`$_.CommandLine -like "*$StorePath*"
} |
Select-Object -First 1
if (`$null -ne `$conflictingWriter) {
throw "Conflicting Muninn activate writer still targets ${StorePath}: `$(`$conflictingWriter.CommandLine)"
}
if (-not (Test-Path -LiteralPath "$StorePath")) {
throw "Muninn telemetry store is missing at $StorePath"
}
`$storeAgeSeconds = (([DateTime]::UtcNow) - (Get-Item -LiteralPath "$StorePath").LastWriteTimeUtc).TotalSeconds
if (`$storeAgeSeconds -gt $MaxStoreAgeSeconds) {
throw "Muninn telemetry store is stale (`$([math]::Round(`$storeAgeSeconds))s old)"
}
if (-not (Test-Path -LiteralPath `$servePidPath)) {
throw "Muninn serve PID file is missing at `$servePidPath"
}
if (`$null -ne `$servePid -and `$process.ProcessId -ne `$servePid) {
throw "Muninn serve PID file references `$servePid but live Muninn serve process is `$(`$process.ProcessId)"
}
"@
Invoke-RavenUploadedPowerShell -RavenHost $RavenHost -RemoteScriptContent $remoteScript -TempPrefix "odin-raven-muninn-health"
exit $LASTEXITCODE

View file

@ -1,3 +0,0 @@
@echo off
echo health-nightwing-muninn.cmd archived: Nightwing Muninn health is daemon-published over CultNet/RUDP as muninn.cultnet-rudp-remote-telemetry-and-move-hid. Use health-nightwing-muninn.ps1 only as an explicit witness inspection, not Idunn health truth.
exit /b 2

View file

@ -1,114 +0,0 @@
param(
[string] $SshTarget = "nightwing",
[string] $MuninnExe = "/home/metacrat/.local/bin/muninn",
[string] $StorePath = "/home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc",
[string] $PidPath = "/home/metacrat/.local/state/gamecult/muninn/muninn.pid",
[string[]] $MoveState = @(),
[int] $IntervalSeconds = 15,
[int] $MaxStoreAgeSeconds = 180,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no WireGuard endpoint default is allowed."
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
[Parameter(Mandatory = $true)] [string] $Content
)
[System.IO.File]::WriteAllText($Path, ($Content -replace "`r?`n", "`n"), [System.Text.Encoding]::ASCII)
}
function Invoke-NightwingUploadedShell {
param(
[Parameter(Mandatory = $true)] [string] $SshTarget,
[Parameter(Mandatory = $true)] [string] $RemoteScriptContent,
[Parameter(Mandatory = $true)] [string] $TempPrefix
)
$uploadId = [guid]::NewGuid().ToString("N")
$localTempRoot = Join-Path $env:TEMP "$TempPrefix-$uploadId"
$localRemoteScript = Join-Path $localTempRoot "$TempPrefix-$uploadId.sh"
$localSftpBatch = Join-Path $localTempRoot "$TempPrefix-$uploadId.sftp"
$remoteScriptPath = "/tmp/$TempPrefix-$uploadId.sh"
try {
New-Item -ItemType Directory -Force -Path $localTempRoot | Out-Null
Set-AsciiFile -Path $localRemoteScript -Content $RemoteScriptContent
Set-AsciiFile -Path $localSftpBatch -Content ('put "{0}" "{1}"' -f $localRemoteScript, $remoteScriptPath)
& sftp.exe -b $localSftpBatch $SshTarget
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "chmod +x '$remoteScriptPath' && bash '$remoteScriptPath'"
$remoteExit = $LASTEXITCODE
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "rm -f '$remoteScriptPath'"
exit $remoteExit
} finally {
Remove-Item -LiteralPath $localTempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
$remoteScript = @"
#!/usr/bin/env bash
set -euo pipefail
if [ ! -x '$MuninnExe' ]; then
echo 'Muninn executable not found at $MuninnExe' >&2
exit 1
fi
if [ -f '$PidPath' ] && kill -0 "`$(cat '$PidPath')" 2>/dev/null; then
:
elif pgrep -f '[m]uninn serve .*--host nightwing' >/dev/null 2>&1; then
:
else
echo 'Muninn serve process is not running on Nightwing' >&2
exit 1
fi
process_line="`$(pgrep -af '[m]uninn serve .*--host nightwing' | head -n1 || true)"
if [ -z "`$process_line" ]; then
echo 'Nightwing Muninn serve command line is unavailable' >&2
exit 1
fi
for pattern in \
'--idunn-rudp-health $IdunnRudpHealth' \
'--idunn-daemon nightwing-muninn' \
'--idunn-health-contract muninn.cultnet-rudp-remote-telemetry-and-move-hid'
do
if ! printf '%s\n' "`$process_line" | grep -F -- "`$pattern" >/dev/null 2>&1; then
echo "Nightwing Muninn serve command line is missing `$pattern" >&2
exit 1
fi
done
if [ ! -f '$StorePath' ]; then
echo 'Muninn telemetry store is missing on Nightwing' >&2
exit 1
fi
store_mtime="`$(stat -c %Y '$StorePath')"
now="`$(date +%s)"
store_age="`$((now - store_mtime))"
if [ "`$store_age" -gt '$MaxStoreAgeSeconds' ]; then
echo "Muninn telemetry store is stale on Nightwing (`$store_age s old)" >&2
exit 1
fi
if printf '%s\n' "`$process_line" | grep -F -- '--move-state' >/dev/null 2>&1 || \
printf '%s\n' "`$process_line" | grep -F -- '--move-evidence-stream' >/dev/null 2>&1; then
identity_output="`$('$MuninnExe' move-identity-status --store '$StorePath' --host nightwing)"
if printf '%s\n' "`$identity_output" | grep -Fq 'No Muninn Move identity records found'; then
echo 'Muninn Move identity roster is empty on Nightwing' >&2
exit 1
fi
identity_count="`$(printf '%s\n' "`$identity_output" | grep -c 'move-identity move=')"
echo "Muninn healthy: nightwing store_age=`${store_age}s move_runtime=explicit move_identities=`${identity_count}"
else
echo "Muninn healthy: nightwing store_age=`${store_age}s move_runtime=disabled"
fi
"@
Invoke-NightwingUploadedShell -SshTarget $SshTarget -RemoteScriptContent $remoteScript -TempPrefix "odin-nightwing-muninn-health"

View file

@ -1,3 +0,0 @@
@echo off
echo health-starfire-muninn.cmd archived: Starfire Muninn health is daemon-published over CultNet/RUDP as muninn.cultnet-rudp-local-telemetry-and-quest-access. Use health-starfire-muninn.ps1 only as an explicit witness inspection, not Idunn health truth.
exit /b 2

View file

@ -1,50 +0,0 @@
param(
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
[string] $StorePath = "C:\Meta\Odin\state\starfire.muninn.telemetry.cc",
[string] $LogRoot = "C:\Meta\Odin\logs\starfire-muninn",
[string[]] $MoveState = @(),
[int] $MaxStoreAgeSeconds = 180,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if (-not (Test-Path -LiteralPath $MuninnExe)) {
throw "Muninn executable not found at $MuninnExe"
}
$process = Get-CimInstance Win32_Process |
Where-Object {
$_.Name -ieq "muninn.exe" -and
$_.CommandLine -like "*serve*" -and
$_.CommandLine -like "*--host*starfire*" -and
$_.CommandLine -like "*--quest-adb*"
} |
Select-Object -First 1
if ($null -eq $process) {
throw "Starfire Muninn serve process is not running with Quest access enabled"
}
foreach ($pattern in @(
"--idunn-rudp-health $IdunnRudpHealth",
"--idunn-daemon starfire-muninn",
"--idunn-health-contract muninn.cultnet-rudp-local-telemetry-and-quest-access"
)) {
if ($process.CommandLine -notlike "*$pattern*") {
throw "Starfire Muninn serve process is missing expected command-line segment ${pattern}: $($process.CommandLine)"
}
}
if (-not (Test-Path -LiteralPath $StorePath)) {
throw "Starfire Muninn telemetry store is missing at $StorePath"
}
$storeAgeSeconds = (([DateTime]::UtcNow) - (Get-Item -LiteralPath $StorePath).LastWriteTimeUtc).TotalSeconds
if ($storeAgeSeconds -gt $MaxStoreAgeSeconds) {
throw "Starfire Muninn telemetry store is stale ($([math]::Round($storeAgeSeconds))s old)"
}
$pidPath = Join-Path $LogRoot "muninn-serve.pid"
if (-not (Test-Path -LiteralPath $pidPath)) {
throw "Starfire Muninn serve PID file is missing at $pidPath"
}

View file

@ -1,156 +0,0 @@
param(
[string] $RavenHost = "raven",
[string] $MuninnDir = "C:\Meta\Odin\Muninn"
)
$ErrorActionPreference = "Stop"
$remoteScript = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
function Register-HiddenVbsTask {
param(
[Parameter(Mandatory = `$true)] [string] `$TaskName,
[Parameter(Mandatory = `$true)] [string] `$VbsPath
)
if (-not (Test-Path -LiteralPath `$VbsPath)) {
throw "Hidden launcher not found for `$TaskName at `$VbsPath"
}
`$action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "//B //Nologo ""`$VbsPath"""
`$trigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Today.AddHours(23).AddMinutes(59))
`$principal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited
`$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName `$TaskName -Action `$action -Trigger `$trigger -Principal `$principal -Settings `$settings -Force | Out-Null
}
function Assert-HiddenVbsTask {
param(
[Parameter(Mandatory = `$true)] [string] `$TaskName,
[Parameter(Mandatory = `$true)] [string] `$VbsPath
)
`$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction Stop
`$action = @(`$task.Actions)[0]
if (`$action.Execute -notmatch '(^|\\)wscript\.exe$') {
throw "`$TaskName action executes `$(`$action.Execute), expected wscript.exe"
}
if (`$action.Arguments -notlike "*`$VbsPath*") {
throw "`$TaskName action arguments `$(`$action.Arguments) do not reference `$VbsPath"
}
if (`$action.Arguments -notlike "*//B*" -or `$action.Arguments -notlike "*//Nologo*") {
throw "`$TaskName action arguments `$(`$action.Arguments) do not force background WScript execution"
}
`$vbs = Get-Content -LiteralPath `$VbsPath -Raw
if (`$vbs -match 'cmdPath\s*=') {
throw "`$TaskName hidden launcher at `$VbsPath still routes through a cmdPath trampoline"
}
if (`$vbs -notmatch '\.ps1') {
throw "`$TaskName hidden launcher at `$VbsPath does not reference a PowerShell launcher"
}
}
`$serveVbs = Join-Path "$MuninnDir" "start-muninn-serve-hidden.vbs"
`$activateCmd = Join-Path "$MuninnDir" "activate-raven-av-srt.cmd"
`$activatePs = Join-Path "$MuninnDir" "activate-raven-av-srt.ps1"
`$activateVbs = Join-Path "$MuninnDir" "activate-raven-av-srt-hidden.vbs"
`$videoProofCmd = Join-Path "$MuninnDir" "muninn-raven-video-to-starfire-obs.cmd"
`$videoProofPs = Join-Path "$MuninnDir" "muninn-raven-video-to-starfire-obs.ps1"
`$videoProofVbs = Join-Path "$MuninnDir" "muninn-raven-video-to-starfire-obs-hidden.vbs"
function Write-HiddenPowerShellVbsLauncher {
param(
[Parameter(Mandatory = `$true)] [string] `$PsPath,
[Parameter(Mandatory = `$true)] [string] `$VbsPath
)
if (-not (Test-Path -LiteralPath `$PsPath)) {
throw "PowerShell launcher not found at `$PsPath"
}
`$lines = @(
'Set fso = CreateObject("Scripting.FileSystemObject")',
'scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)',
'Set shell = CreateObject("WScript.Shell")',
"psLauncher = ""`$PsPath""",
'shell.CurrentDirectory = scriptDir',
'shell.Run "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & psLauncher & """", 0, False'
)
Set-Content -LiteralPath `$VbsPath -Value `$lines -Encoding ASCII
}
function Write-WscriptCmdLauncher {
param(
[Parameter(Mandatory = `$true)] [string] `$CmdPath,
[Parameter(Mandatory = `$true)] [string] `$WorkingDirectory,
[Parameter(Mandatory = `$true)] [string] `$VbsPath
)
`$cmdLines = @(
'@echo off',
"cd /d ""`$WorkingDirectory""",
"wscript.exe //B //Nologo ""`$VbsPath"""
)
Set-Content -LiteralPath `$CmdPath -Value `$cmdLines -Encoding ASCII
}
if (Test-Path -LiteralPath `$activatePs) {
Write-HiddenPowerShellVbsLauncher -PsPath `$activatePs -VbsPath `$activateVbs
if (Test-Path -LiteralPath `$activateCmd) {
Write-WscriptCmdLauncher -CmdPath `$activateCmd -WorkingDirectory "$MuninnDir" -VbsPath `$activateVbs
}
}
if (Test-Path -LiteralPath `$videoProofPs) {
Write-HiddenPowerShellVbsLauncher -PsPath `$videoProofPs -VbsPath `$videoProofVbs
if (Test-Path -LiteralPath `$videoProofCmd) {
Write-WscriptCmdLauncher -CmdPath `$videoProofCmd -WorkingDirectory "$MuninnDir" -VbsPath `$videoProofVbs
}
}
Register-HiddenVbsTask -TaskName "GameCult-Muninn" -VbsPath `$serveVbs
if (Test-Path -LiteralPath `$activateVbs) {
Register-HiddenVbsTask -TaskName "GameCult-Muninn-Activate" -VbsPath `$activateVbs
}
if (Test-Path -LiteralPath `$videoProofVbs) {
Register-HiddenVbsTask -TaskName "GameCult-Muninn-VideoProof" -VbsPath `$videoProofVbs
}
Assert-HiddenVbsTask -TaskName "GameCult-Muninn" -VbsPath `$serveVbs
if (Test-Path -LiteralPath `$activateVbs) {
Assert-HiddenVbsTask -TaskName "GameCult-Muninn-Activate" -VbsPath `$activateVbs
}
if (Test-Path -LiteralPath `$videoProofVbs) {
Assert-HiddenVbsTask -TaskName "GameCult-Muninn-VideoProof" -VbsPath `$videoProofVbs
}
"@
$uploadId = [guid]::NewGuid().ToString("N")
$localRemoteScript = Join-Path $env:TEMP "odin-raven-muninn-task-repair-$uploadId.ps1"
$localSftpBatch = Join-Path $env:TEMP "odin-raven-muninn-task-repair-$uploadId.sftp"
$remoteSftpPath = "C:/Windows/Temp/odin-raven-muninn-task-repair-$uploadId.ps1"
$remotePsPath = "C:\Windows\Temp\odin-raven-muninn-task-repair-$uploadId.ps1"
try {
Set-Content -LiteralPath $localRemoteScript -Encoding ASCII -Value $remoteScript
Set-Content -LiteralPath $localSftpBatch -Encoding ASCII -Value "put ""$localRemoteScript"" ""$remoteSftpPath"""
& sftp.exe -b $localSftpBatch $RavenHost
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$remoteRunner = @"
`$ErrorActionPreference = "Stop"
try {
& "$remotePsPath"
`$code = `$LASTEXITCODE
} finally {
Remove-Item -LiteralPath "$remotePsPath" -Force -ErrorAction SilentlyContinue
}
exit `$code
"@
$encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner))
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedRunner"
} finally {
Remove-Item -LiteralPath $localRemoteScript, $localSftpBatch -Force -ErrorAction SilentlyContinue
}
exit $LASTEXITCODE

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-muninn.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-muninn.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-muninn.ps1

View file

@ -1,897 +0,0 @@
param(
[string] $RavenHost = "raven.Home",
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
[string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc",
[string] $ActivateStorePath = "C:\Meta\Odin\state\muninn.activate.cc",
[string] $LogRoot = "C:\Meta\Odin\logs\muninn",
[string] $LocalLoopbackScript = (Join-Path $PSScriptRoot "wasapi-loopback-capture.ps1"),
[string] $LoopbackScript = "C:\Meta\Odin\Muninn\scripts\wasapi-loopback-capture.ps1",
[string] $Ffmpeg = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe",
[string] $MediaTargetUri = $env:MUNINN_MEDIA_TARGET_URI,
[string] $AudioDevice = "Realtek",
[string[]] $VideoSources = @(),
[string[]] $AudioSources = @(),
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[string] $IdunnDaemon = "muninn",
[string] $IdunnHealthContract = "muninn.cultnet-rudp-remote-telemetry-health",
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" }),
[string] $OdinCultMeshRudpEndpoint = $env:CULTMESH_URI_ODIN_RUDP,
[string[]] $MoveStates = @("xbox-raven=xinput://0"),
[string] $HidControllerRudpTarget = "",
[string] $HidControllerRudpBind = "0.0.0.0:17887",
[string] $HidControllerRudpAdvertise = $env:MUNINN_HID_CONTROLLER_RUDP_ADVERTISE,
[int] $ConnectTimeoutSeconds = 10,
[int] $ServeStartTimeoutSeconds = 20,
[string] $SshUser = "madman's lullaby",
[string] $IdentityFile = "C:\Users\Meta\.ssh\id_ed25519_192_168_1_84"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-muninn.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
if (-not (Test-Path -LiteralPath $LocalLoopbackScript)) {
throw "Local Muninn loopback script not found at $LocalLoopbackScript"
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if ([string]::IsNullOrWhiteSpace($MediaTargetUri) -or -not $MediaTargetUri.StartsWith("cultmesh://", [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Muninn media target URI must be supplied by -MediaTargetUri or MUNINN_MEDIA_TARGET_URI and must start with cultmesh://."
}
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri) -and $OdinCultMeshUri.StartsWith("cultmesh://", [System.StringComparison]::OrdinalIgnoreCase) -and [string]::IsNullOrWhiteSpace($OdinCultMeshRudpEndpoint)) {
throw "Odin CultMesh RUDP endpoint must be supplied by -OdinCultMeshRudpEndpoint or CULTMESH_URI_ODIN_RUDP when -OdinCultMeshUri is a cultmesh:// URI."
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind) -and [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
throw "HID controller RUDP advertise endpoint must be supplied by -HidControllerRudpAdvertise or MUNINN_HID_CONTROLLER_RUDP_ADVERTISE when HID RUDP bind is enabled; no Raven LAN default is allowed."
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
[Parameter(Mandatory = $true)] [string] $Content
)
[System.IO.File]::WriteAllText($Path, ($Content -replace "`r?`n", "`r`n"), [System.Text.Encoding]::ASCII)
}
function ConvertTo-PowerShellStringLiteral {
param(
[Parameter(Mandatory = $true)] [string] $Value
)
return "'" + $Value.Replace("'", "''") + "'"
}
function ConvertTo-PowerShellArrayLiteral {
param(
[Parameter(Mandatory = $true)] [string[]] $Values
)
$lines = $Values | ForEach-Object { " {0}" -f (ConvertTo-PowerShellStringLiteral $_) }
return "@(`r`n{0}`r`n)" -f ($lines -join ",`r`n")
}
function ConvertTo-PowerShellEnvAssignmentLines {
param(
[Parameter(Mandatory = $true)] [hashtable] $Environment
)
$lines = @()
foreach ($key in ($Environment.Keys | Sort-Object)) {
$name = [string] $key
if ($name -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') {
throw "Invalid environment variable name for Muninn launcher: $name"
}
$value = [string] $Environment[$key]
$lines += '$env:{0} = {1}' -f $name, (ConvertTo-PowerShellStringLiteral $value)
}
return ($lines -join "`r`n")
}
function Get-SshCommonArgs {
$args = @(
"-o", "BatchMode=yes",
"-o", "ConnectTimeout=$ConnectTimeoutSeconds",
"-o", "ConnectionAttempts=1"
)
if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) {
$args += @("-i", $IdentityFile)
}
return $args
}
function Get-SshTarget {
param([Parameter(Mandatory = $true)] [string] $Target)
if ([string]::IsNullOrWhiteSpace($SshUser)) {
return $Target
}
return "${SshUser}@${Target}"
}
function Test-LikelyVirtualDisplayToken {
param([Parameter(Mandatory = $true)] [string] $Token)
$trimmed = $Token.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed)) {
return $true
}
return (
$trimmed.StartsWith('MSBDD_', [System.StringComparison]::OrdinalIgnoreCase) -or
$trimmed.StartsWith('MSNIL', [System.StringComparison]::OrdinalIgnoreCase) -or
$trimmed.StartsWith('NOEDID', [System.StringComparison]::OrdinalIgnoreCase) -or
$trimmed.StartsWith('UGD', [System.StringComparison]::OrdinalIgnoreCase)
)
}
function Get-GraphicsDriverDisplayTokens {
$connectivityRoot = 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity'
if (-not (Test-Path -LiteralPath $connectivityRoot)) {
return @()
}
$bestTokens = @()
$bestScore = [int]::MinValue
foreach ($key in Get-ChildItem -LiteralPath $connectivityRoot -ErrorAction SilentlyContinue) {
$item = Get-ItemProperty -LiteralPath $key.PSPath -ErrorAction SilentlyContinue
if ($null -eq $item) {
continue
}
foreach ($property in $item.PSObject.Properties) {
if ($property.Name -like 'PS*' -or $property.Value -isnot [string]) {
continue
}
$tokens = @(
$property.Value.Split('+') |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($tokens.Count -lt 1) {
continue
}
$realCount = @($tokens | Where-Object { -not (Test-LikelyVirtualDisplayToken $_) }).Count
$score = ($realCount * 100) + $tokens.Count
if ($score -gt $bestScore) {
$bestScore = $score
$bestTokens = $tokens
}
}
}
return @($bestTokens | Select-Object -Unique)
}
function Get-DefaultMuninnVideoSourceLabels {
param([Parameter(Mandatory = $true)] [string] $HostLabel)
$displayNames = @(
Get-CimInstance Win32_DesktopMonitor -ErrorAction SilentlyContinue |
Where-Object { [string]::IsNullOrWhiteSpace($_.Status) -or $_.Status -eq 'OK' } |
ForEach-Object {
if ([string]::IsNullOrWhiteSpace($_.Name)) {
'Display'
} else {
$_.Name.Trim()
}
}
)
$useRegistryFallback = (
$displayNames.Count -lt 1 -or
($displayNames.Count -eq 1 -and (
$displayNames[0] -match '^\s*WinDisc\s*$' -or
$displayNames[0] -match '^\s*Generic PnP Monitor\s*$' -or
$displayNames[0] -match '^\s*Display\s*$'
))
)
if ($useRegistryFallback) {
$displayTokens = @(Get-GraphicsDriverDisplayTokens)
if ($displayTokens.Count -gt 0) {
return @(for ($index = 0; $index -lt $displayTokens.Count; $index++) {
'{0} display {1}' -f $HostLabel, ($index + 1)
})
}
}
if ($displayNames.Count -lt 1) {
$displayNames = @('Display')
}
return @(for ($index = 0; $index -lt $displayNames.Count; $index++) {
$monitorName = $displayNames[$index]
if ($monitorName -eq 'Display') {
'{0} display {1}' -f $HostLabel, ($index + 1)
} else {
'{0} display {1} ({2})' -f $HostLabel, ($index + 1), $monitorName
}
})
}
function New-HiddenNativeLauncherPowerShellContent {
param(
[Parameter(Mandatory = $false)] [string] $StorePath,
[Parameter(Mandatory = $true)] [string] $LogRoot,
[Parameter(Mandatory = $true)] [string[]] $Arguments,
[Parameter(Mandatory = $true)] [string] $FilePath,
[Parameter(Mandatory = $true)] [string] $StdoutPath,
[Parameter(Mandatory = $true)] [string] $StderrPath,
[Parameter(Mandatory = $true)] [string] $PidPath,
[Parameter(Mandatory = $false)] [string] $WorkingDirectory,
[Parameter(Mandatory = $false)] [hashtable] $Environment = @{},
[Parameter(Mandatory = $false)] [string] $DynamicArgumentsScript,
[Parameter(Mandatory = $false)] [switch] $WaitForExit
)
$template = @'
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
__STORE_DIR_LINE__
New-Item -ItemType Directory -Force -Path __LOG_ROOT__ | Out-Null
$launcherLog = __PID_PATH__ + '.launcher.log'
('launcher-start ' + [DateTime]::UtcNow.ToString('o')) | Set-Content -Encoding ASCII -LiteralPath $launcherLog
__ENVIRONMENT_LINES__
$arguments = __ARGUMENTS__
__DYNAMIC_ARGUMENTS_SCRIPT__
function Quote-NativeArgument([string] $Value) {
if ($Value -match '[\s"]') {
return '"' + $Value.Replace('"', '\"') + '"'
}
return $Value
}
$argumentLine = ($arguments | ForEach-Object { Quote-NativeArgument $_ }) -join ' '
('launcher-args ' + $argumentLine) | Add-Content -Encoding ASCII -LiteralPath $launcherLog
if (__RUN_INLINE__) {
$PID | Set-Content -Encoding ASCII -LiteralPath __PID_PATH__
('launcher-pid ' + $PID + ' powershell-inline') | Add-Content -Encoding ASCII -LiteralPath $launcherLog
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
& __FILE_PATH__ @arguments > __STDOUT_PATH__ 2> __STDERR_PATH__
$exitCode = $LASTEXITCODE
} catch {
('launcher-error ' + $_.Exception.Message) | Add-Content -Encoding ASCII -LiteralPath $launcherLog
throw
} finally {
$ErrorActionPreference = $previousErrorActionPreference
}
('launcher-exit ' + $exitCode) | Add-Content -Encoding ASCII -LiteralPath $launcherLog
exit $exitCode
}
try {
$process = Start-Process -FilePath __FILE_PATH__ -ArgumentList $argumentLine __WORKING_DIRECTORY_CLAUSE__-WindowStyle Hidden -PassThru -RedirectStandardOutput __STDOUT_PATH__ -RedirectStandardError __STDERR_PATH__
$process.Id | Set-Content -Encoding ASCII -LiteralPath __PID_PATH__
('launcher-pid ' + $process.Id) | Add-Content -Encoding ASCII -LiteralPath $launcherLog
} catch {
('launcher-error ' + $_.Exception.Message) | Add-Content -Encoding ASCII -LiteralPath $launcherLog
throw
}
__WAIT_FOR_EXIT_LINE__
'@
$storeDirLine = if ([string]::IsNullOrWhiteSpace($StorePath)) {
""
} else {
"New-Item -ItemType Directory -Force -Path (Split-Path -Parent {0}) | Out-Null" -f (ConvertTo-PowerShellStringLiteral $StorePath)
}
$workingDirectoryClause = if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) {
""
} else {
"-WorkingDirectory {0} " -f (ConvertTo-PowerShellStringLiteral $WorkingDirectory)
}
return $template.
Replace("__STORE_DIR_LINE__", $storeDirLine).
Replace("__LOG_ROOT__", (ConvertTo-PowerShellStringLiteral $LogRoot)).
Replace("__ENVIRONMENT_LINES__", (ConvertTo-PowerShellEnvAssignmentLines $Environment)).
Replace("__ARGUMENTS__", (ConvertTo-PowerShellArrayLiteral $Arguments)).
Replace("__DYNAMIC_ARGUMENTS_SCRIPT__", $DynamicArgumentsScript).
Replace("__FILE_PATH__", (ConvertTo-PowerShellStringLiteral $FilePath)).
Replace("__WORKING_DIRECTORY_CLAUSE__", $workingDirectoryClause).
Replace("__STDOUT_PATH__", (ConvertTo-PowerShellStringLiteral $StdoutPath)).
Replace("__STDERR_PATH__", (ConvertTo-PowerShellStringLiteral $StderrPath)).
Replace("__PID_PATH__", (ConvertTo-PowerShellStringLiteral $PidPath)).
Replace("__RUN_INLINE__", $(if ($WaitForExit) { '$true' } else { '$false' })).
Replace("__WAIT_FOR_EXIT_LINE__", $(if ($WaitForExit) { '$process.WaitForExit(); exit $process.ExitCode' } else { "" }))
}
function New-HiddenPowerShellVbsLauncherContent {
param(
[Parameter(Mandatory = $true)] [string] $PsPath,
[Parameter(Mandatory = $false)] [switch] $WaitForExit
)
return (@'
Set fso = CreateObject("Scripting.FileSystemObject")
scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
psLauncher = "{0}"
logPath = psLauncher & ".vbs.log"
Set logFile = fso.OpenTextFile(logPath, 8, True)
logFile.WriteLine "vbs-start " & Now
logFile.Close
Set shell = CreateObject("WScript.Shell")
shell.CurrentDirectory = scriptDir
exitCode = shell.Run("powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & psLauncher & """", 0, {1})
Set logFile = fso.OpenTextFile(logPath, 8, True)
logFile.WriteLine "vbs-exit " & exitCode & " " & Now
logFile.Close
'@) -f $PsPath, ($(if ($WaitForExit) { "True" } else { "False" }))
}
function New-WscriptCmdLauncherContent {
param(
[Parameter(Mandatory = $true)] [string] $WorkingDirectory,
[Parameter(Mandatory = $true)] [string] $VbsPath
)
return (@'
@echo off
cd /d "{0}"
wscript.exe //B //Nologo "{1}"
'@) -f $WorkingDirectory, $VbsPath
}
function Invoke-RavenUploadedPowerShell {
param(
[Parameter(Mandatory = $true)] [string] $RavenHost,
[Parameter(Mandatory = $true)] [string] $RemoteScriptContent,
[Parameter(Mandatory = $true)] [object[]] $UploadSpecs,
[Parameter(Mandatory = $true)] [string] $TempPrefix
)
$uploadId = [guid]::NewGuid().ToString("N")
$localTempRoot = Join-Path $env:TEMP "$TempPrefix-$uploadId"
$localRemoteScript = Join-Path $localTempRoot "$TempPrefix-$uploadId.ps1"
$localSftpBatch = Join-Path $localTempRoot "$TempPrefix-$uploadId.sftp"
$remoteSftpPath = "C:/Windows/Temp/$TempPrefix-$uploadId.ps1"
$remotePsPath = "C:\Windows\Temp\$TempPrefix-$uploadId.ps1"
try {
New-Item -ItemType Directory -Force -Path $localTempRoot | Out-Null
Set-AsciiFile -Path $localRemoteScript -Content $RemoteScriptContent
$batchLines = @()
foreach ($spec in $UploadSpecs) {
$batchLines += 'put "{0}" "{1}"' -f $spec.LocalPath, $spec.RemotePath
}
$batchLines += 'put "{0}" "{1}"' -f $localRemoteScript, $remoteSftpPath
Set-AsciiFile -Path $localSftpBatch -Content ($batchLines -join "`r`n")
$commonArgs = Get-SshCommonArgs
$sshTarget = Get-SshTarget -Target $RavenHost
& sftp.exe @commonArgs -b $localSftpBatch $sshTarget
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$remoteRunner = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
try {
& "$remotePsPath"
exit 0
} finally {
Remove-Item -LiteralPath "$remotePsPath" -Force -ErrorAction SilentlyContinue
}
"@
$encodedRunner = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteRunner))
$commonArgs = Get-SshCommonArgs
$sshTarget = Get-SshTarget -Target $RavenHost
& ssh.exe @commonArgs $sshTarget "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -OutputFormat Text -EncodedCommand $encodedRunner"
} finally {
Remove-Item -LiteralPath $localTempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
$muninnDir = Split-Path -Parent $MuninnExe
$serveCmdPath = Join-Path $muninnDir "start-muninn-serve.cmd"
$servePsPath = Join-Path $muninnDir "start-muninn-serve.ps1"
$serveVbsPath = Join-Path $muninnDir "start-muninn-serve-hidden.vbs"
$serveArguments = @(
"serve",
"--store", $StorePath,
"--activate-store", $ActivateStorePath,
"--log-root", $LogRoot,
"--host", "raven",
"--stream", "muninn.raven.av.rudp",
"--target-host", $MediaTargetUri,
"--media-transport", "rudp"
)
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri)) {
$serveArguments += @("--odin-cultmesh-uri", $OdinCultMeshUri)
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpTarget)) {
$serveArguments += @("--hid-controller-rudp-target", $HidControllerRudpTarget)
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind)) {
$serveArguments += @("--hid-controller-rudp-bind", $HidControllerRudpBind)
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
$serveArguments += @("--hid-controller-rudp-advertise", $HidControllerRudpAdvertise)
}
foreach ($videoSource in $VideoSources) {
$serveArguments += @("--video-source", $videoSource)
}
foreach ($audioSource in $AudioSources) {
$serveArguments += @("--audio-source", $audioSource)
}
foreach ($moveState in $MoveStates) {
if (-not [string]::IsNullOrWhiteSpace($moveState)) {
$serveArguments += @("--move-state", $moveState)
}
}
$serveArguments += @(
"--audio-device", $AudioDevice,
"--ffmpeg", $Ffmpeg,
"--loopback-script", $LoopbackScript,
"--interval-seconds", "15",
"--idunn-rudp-health", $IdunnRudpHealth,
"--idunn-daemon", $IdunnDaemon,
"--idunn-health-contract", $IdunnHealthContract
)
$serveDynamicArgumentsScript = @'
function Test-LikelyVirtualDisplayToken {
param([Parameter(Mandatory = $true)] [string] $Token)
$trimmed = $Token.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed)) {
return $true
}
return (
$trimmed.StartsWith('MSBDD_', [System.StringComparison]::OrdinalIgnoreCase) -or
$trimmed.StartsWith('MSNIL', [System.StringComparison]::OrdinalIgnoreCase) -or
$trimmed.StartsWith('NOEDID', [System.StringComparison]::OrdinalIgnoreCase) -or
$trimmed.StartsWith('UGD', [System.StringComparison]::OrdinalIgnoreCase)
)
}
function Get-GraphicsDriverDisplayTokens {
$connectivityRoot = 'HKLM:\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity'
if (-not (Test-Path -LiteralPath $connectivityRoot)) {
return @()
}
$bestTokens = @()
$bestScore = [int]::MinValue
foreach ($key in Get-ChildItem -LiteralPath $connectivityRoot -ErrorAction SilentlyContinue) {
$item = Get-ItemProperty -LiteralPath $key.PSPath -ErrorAction SilentlyContinue
if ($null -eq $item) {
continue
}
foreach ($property in $item.PSObject.Properties) {
if ($property.Name -like 'PS*' -or $property.Value -isnot [string]) {
continue
}
$tokens = @(
$property.Value.Split('+') |
ForEach-Object { $_.Trim() } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($tokens.Count -lt 1) {
continue
}
$realCount = @($tokens | Where-Object { -not (Test-LikelyVirtualDisplayToken $_) }).Count
$score = ($realCount * 100) + $tokens.Count
if ($score -gt $bestScore) {
$bestScore = $score
$bestTokens = $tokens
}
}
}
return @($bestTokens | Select-Object -Unique)
}
function Get-DefaultMuninnVideoSourceLabels {
param([Parameter(Mandatory = $true)] [string] $HostLabel)
$displayNames = @(
Get-CimInstance Win32_DesktopMonitor -ErrorAction SilentlyContinue |
Where-Object { [string]::IsNullOrWhiteSpace($_.Status) -or $_.Status -eq 'OK' } |
ForEach-Object {
if ([string]::IsNullOrWhiteSpace($_.Name)) {
'Display'
} else {
$_.Name.Trim()
}
}
)
$useRegistryFallback = (
$displayNames.Count -lt 1 -or
($displayNames.Count -eq 1 -and (
$displayNames[0] -match '^\s*WinDisc\s*$' -or
$displayNames[0] -match '^\s*Generic PnP Monitor\s*$' -or
$displayNames[0] -match '^\s*Display\s*$'
))
)
if ($useRegistryFallback) {
$displayTokens = @(Get-GraphicsDriverDisplayTokens)
if ($displayTokens.Count -gt 0) {
return @(for ($index = 0; $index -lt $displayTokens.Count; $index++) {
'{0} display {1}' -f $HostLabel, ($index + 1)
})
}
}
if ($displayNames.Count -lt 1) {
$displayNames = @('Display')
}
return @(for ($index = 0; $index -lt $displayNames.Count; $index++) {
$monitorName = $displayNames[$index]
if ($monitorName -eq 'Display') {
'{0} display {1}' -f $HostLabel, ($index + 1)
} else {
'{0} display {1} ({2})' -f $HostLabel, ($index + 1), $monitorName
}
})
}
$hostArgumentIndex = [Array]::IndexOf($arguments, '--host')
$hostLabel = if ($hostArgumentIndex -ge 0 -and ($hostArgumentIndex + 1) -lt $arguments.Count) {
$arguments[$hostArgumentIndex + 1]
} else {
'host'
}
$hasExplicitVideoSources = @($arguments | Where-Object { $_ -eq '--video-source' }).Count -gt 0
$hasExplicitAudioSources = @($arguments | Where-Object { $_ -eq '--audio-source' }).Count -gt 0
if (-not ('Muninn.AudioEndpointDiscovery' -as [type])) {
Add-Type -TypeDefinition @"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Muninn {
public static class AudioEndpointDiscovery {
const int eRender = 0;
const int eCapture = 1;
const uint DEVICE_STATE_ACTIVE = 0x00000001;
static readonly PROPERTYKEY DeviceFriendlyNameKey = new PROPERTYKEY(
new Guid("A45C254E-DF1C-4EFD-8020-67D146A850E0"),
14);
[StructLayout(LayoutKind.Sequential)]
struct PROPERTYKEY {
public Guid fmtid;
public uint pid;
public PROPERTYKEY(Guid formatId, uint propertyId) {
fmtid = formatId;
pid = propertyId;
}
}
[StructLayout(LayoutKind.Sequential)]
struct PROPVARIANT {
public ushort vt;
public ushort wReserved1;
public ushort wReserved2;
public ushort wReserved3;
public IntPtr pointerValue;
public int intValue;
}
[ComImport]
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
class MMDeviceEnumerator { }
[ComImport]
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
[PreserveSig]
int EnumAudioEndpoints(int dataFlow, uint stateMask, out IMMDeviceCollection devices);
}
[ComImport]
[Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceCollection {
[PreserveSig]
int GetCount(out uint count);
[PreserveSig]
int Item(uint index, out IMMDevice device);
}
[ComImport]
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
[PreserveSig]
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, out object audioClient);
[PreserveSig]
int OpenPropertyStore(uint access, out IPropertyStore properties);
}
[ComImport]
[Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPropertyStore {
[PreserveSig]
int GetCount(out uint propertyCount);
[PreserveSig]
int GetAt(uint propertyIndex, out PROPERTYKEY key);
[PreserveSig]
int GetValue(ref PROPERTYKEY key, out PROPVARIANT value);
[PreserveSig]
int SetValue(ref PROPERTYKEY key, ref PROPVARIANT value);
[PreserveSig]
int Commit();
}
[DllImport("ole32.dll")]
static extern int PropVariantClear(ref PROPVARIANT variant);
public static string[] FriendlyNames(int dataFlow) {
var names = new List<string>();
IMMDeviceEnumerator enumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator());
IMMDeviceCollection devices;
Marshal.ThrowExceptionForHR(enumerator.EnumAudioEndpoints(dataFlow, DEVICE_STATE_ACTIVE, out devices));
try {
uint count;
Marshal.ThrowExceptionForHR(devices.GetCount(out count));
for (uint index = 0; index < count; index++) {
IMMDevice device;
Marshal.ThrowExceptionForHR(devices.Item(index, out device));
try {
var name = DeviceFriendlyName(device);
if (!String.IsNullOrWhiteSpace(name)) {
names.Add(name.Trim());
}
} finally {
Marshal.ReleaseComObject(device);
}
}
} finally {
Marshal.ReleaseComObject(devices);
Marshal.ReleaseComObject(enumerator);
}
return names.ToArray();
}
static string DeviceFriendlyName(IMMDevice device) {
IPropertyStore store;
Marshal.ThrowExceptionForHR(device.OpenPropertyStore(0, out store));
try {
PROPERTYKEY key = DeviceFriendlyNameKey;
PROPVARIANT value;
Marshal.ThrowExceptionForHR(store.GetValue(ref key, out value));
try {
if (value.vt == 31 && value.pointerValue != IntPtr.Zero) {
return Marshal.PtrToStringUni(value.pointerValue) ?? "";
}
return "";
} finally {
PropVariantClear(ref value);
}
} finally {
Marshal.ReleaseComObject(store);
}
}
}
}
"@
}
if (-not $hasExplicitVideoSources) {
$videoSourceLabels = @(Get-DefaultMuninnVideoSourceLabels -HostLabel $hostLabel)
for ($index = 0; $index -lt $videoSourceLabels.Count; $index++) {
$arguments += @('--video-source', ('display:{0}={1}' -f $index, $videoSourceLabels[$index]))
}
}
if (-not $hasExplicitAudioSources) {
$loopbackNames = @([Muninn.AudioEndpointDiscovery]::FriendlyNames(0) | Sort-Object -Unique)
$inputNames = @([Muninn.AudioEndpointDiscovery]::FriendlyNames(1) | Sort-Object -Unique)
if ($loopbackNames.Count -lt 1 -and $inputNames.Count -lt 1) {
$audioDeviceIndex = [Array]::IndexOf($arguments, '--audio-device')
if ($audioDeviceIndex -ge 0 -and ($audioDeviceIndex + 1) -lt $arguments.Count) {
$loopbackNames = @($arguments[$audioDeviceIndex + 1])
}
}
foreach ($audioName in $loopbackNames) {
$arguments += @('--audio-source', ('wasapi-loopback:{0}={1} loopback ({0})' -f $audioName.Trim(), $hostLabel))
}
foreach ($audioName in $inputNames) {
$arguments += @('--audio-source', ('wasapi-input:{0}={1} input ({0})' -f $audioName.Trim(), $hostLabel))
}
}
'@
$servePsContent = New-HiddenNativeLauncherPowerShellContent `
-StorePath $StorePath `
-LogRoot $LogRoot `
-Arguments $serveArguments `
-FilePath $MuninnExe `
-StdoutPath (Join-Path $LogRoot "muninn-serve.out.log") `
-StderrPath (Join-Path $LogRoot "muninn-serve.err.log") `
-PidPath (Join-Path $LogRoot "muninn-serve.pid") `
-Environment @{ CULTMESH_URI_ODIN_RUDP = $OdinCultMeshRudpEndpoint } `
-DynamicArgumentsScript $serveDynamicArgumentsScript
$uploadRoot = Join-Path $env:TEMP ("odin-raven-muninn-launchers-" + [guid]::NewGuid().ToString("N"))
try {
New-Item -ItemType Directory -Force -Path $uploadRoot | Out-Null
$localServePs = Join-Path $uploadRoot "start-muninn-serve.ps1"
$localServeVbs = Join-Path $uploadRoot "start-muninn-serve-hidden.vbs"
$localServeCmd = Join-Path $uploadRoot "start-muninn-serve.cmd"
Set-AsciiFile -Path $localServePs -Content $servePsContent
Set-AsciiFile -Path $localServeVbs -Content (New-HiddenPowerShellVbsLauncherContent -PsPath $servePsPath)
Set-AsciiFile -Path $localServeCmd -Content (New-WscriptCmdLauncherContent -WorkingDirectory $muninnDir -VbsPath $serveVbsPath)
$remoteScript = @"
`$ErrorActionPreference = "Stop"
`$ProgressPreference = "SilentlyContinue"
if (-not (Test-Path -LiteralPath "$MuninnExe")) {
throw "Muninn executable not found at $MuninnExe"
}
if (-not (Test-Path -LiteralPath "$LoopbackScript")) {
throw "Muninn loopback script not found at $LoopbackScript"
}
if (-not (Test-Path -LiteralPath "$Ffmpeg")) {
throw "FFmpeg executable not found at $Ffmpeg"
}
foreach (`$path in @(
"$servePsPath",
"$serveVbsPath"
)) {
if (-not (Test-Path -LiteralPath `$path)) {
throw "Required launcher path not found at `$path"
}
}
`$obsoleteActivateTask = Get-ScheduledTask -TaskName "GameCult-Muninn-Activate" -ErrorAction SilentlyContinue
if (`$null -ne `$obsoleteActivateTask) {
Unregister-ScheduledTask -TaskName "GameCult-Muninn-Activate" -Confirm:`$false | Out-Null
}
`$obsoleteVideoProofTask = Get-ScheduledTask -TaskName "GameCult-Muninn-VideoProof" -ErrorAction SilentlyContinue
if (`$null -ne `$obsoleteVideoProofTask) {
Unregister-ScheduledTask -TaskName "GameCult-Muninn-VideoProof" -Confirm:`$false | Out-Null
}
Get-CimInstance Win32_Process |
Where-Object { `$_.Name -like "muninn*.exe" } |
ForEach-Object {
& cmd.exe /c ("taskkill /PID {0} /T /F >nul 2>&1" -f `$_.ProcessId) | Out-Null
}
New-Item -ItemType Directory -Force -Path "$LogRoot" | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent "$StorePath") | Out-Null
& netsh.exe advfirewall firewall delete rule name="GameCult Muninn Capture Command RUDP" | Out-Null 2>`$null
if (-not [string]::IsNullOrWhiteSpace("$HidControllerRudpBind")) {
`$hidControllerUdpPort = ([string] "$HidControllerRudpBind").Split(':')[-1]
& netsh.exe advfirewall firewall delete rule name="GameCult Muninn HID Controller RUDP" | Out-Null 2>`$null
& netsh.exe advfirewall firewall add rule name="GameCult Muninn HID Controller RUDP" dir=in action=allow protocol=UDP localport=`$hidControllerUdpPort | Out-Null
}
function Register-HiddenVbsTask {
param(
[Parameter(Mandatory = `$true)] [string] `$TaskName,
[Parameter(Mandatory = `$true)] [string] `$VbsPath
)
`$taskAction = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\wscript.exe" -Argument "//B //Nologo ""`$VbsPath""" -WorkingDirectory (Split-Path -Parent `$VbsPath)
`$taskTrigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Today.AddHours(23).AddMinutes(59))
`$taskPrincipal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest
`$taskSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName `$TaskName -Action `$taskAction -Trigger `$taskTrigger -Principal `$taskPrincipal -Settings `$taskSettings -Force | Out-Null
}
function Register-HiddenPowerShellTask {
param(
[Parameter(Mandatory = `$true)] [string] `$TaskName,
[Parameter(Mandatory = `$true)] [string] `$PsPath
)
`$taskArguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + `$PsPath + '"'
`$taskAction = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -Argument `$taskArguments -WorkingDirectory (Split-Path -Parent `$PsPath)
`$taskTrigger = New-ScheduledTaskTrigger -Once -At ([DateTime]::Today.AddHours(23).AddMinutes(59))
`$taskPrincipal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest
`$taskSettings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName `$TaskName -Action `$taskAction -Trigger `$taskTrigger -Principal `$taskPrincipal -Settings `$taskSettings -Force | Out-Null
}
function Assert-HiddenVbsTask {
param(
[Parameter(Mandatory = `$true)] [string] `$TaskName,
[Parameter(Mandatory = `$true)] [string] `$VbsPath,
[Parameter(Mandatory = `$true)] [string] `$PsPath
)
`$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction Stop
`$action = @(`$task.Actions)[0]
if (`$action.Execute -notmatch '(^|\\)wscript\.exe$') {
throw "`$TaskName action executes `$(`$action.Execute), expected wscript.exe"
}
if (`$action.Arguments -notlike "*`$VbsPath*") {
throw "`$TaskName action arguments `$(`$action.Arguments) do not reference `$VbsPath"
}
if (`$action.Arguments -notlike "*//B*" -or `$action.Arguments -notlike "*//Nologo*") {
throw "`$TaskName action arguments `$(`$action.Arguments) do not force background WScript execution"
}
if (-not (Test-Path -LiteralPath `$PsPath)) {
throw "`$TaskName PowerShell launcher not found at `$PsPath"
}
`$vbs = Get-Content -LiteralPath `$VbsPath -Raw
if (`$vbs -match 'cmdPath\s*=') {
throw "`$TaskName hidden launcher at `$VbsPath still routes through a cmdPath trampoline"
}
if (`$vbs -notmatch '\.ps1') {
throw "`$TaskName hidden launcher at `$VbsPath does not reference a PowerShell launcher"
}
}
function Assert-HiddenPowerShellTask {
param(
[Parameter(Mandatory = `$true)] [string] `$TaskName,
[Parameter(Mandatory = `$true)] [string] `$PsPath
)
`$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction Stop
`$action = @(`$task.Actions)[0]
if (`$action.Execute -notmatch 'powershell\.exe$') {
throw "`$TaskName action executes `$(`$action.Execute), expected powershell.exe"
}
if (`$action.Arguments -notlike "*`$PsPath*") {
throw "`$TaskName action arguments `$(`$action.Arguments) do not reference `$PsPath"
}
if (`$action.Arguments -notlike "*-File*") {
throw "`$TaskName action arguments `$(`$action.Arguments) do not execute a PowerShell launcher"
}
if (-not (Test-Path -LiteralPath `$PsPath)) {
throw "`$TaskName PowerShell launcher not found at `$PsPath"
}
}
Register-HiddenPowerShellTask -TaskName "GameCult-Muninn" -PsPath "$servePsPath"
Assert-HiddenPowerShellTask -TaskName "GameCult-Muninn" -PsPath "$servePsPath"
Start-ScheduledTask -TaskName "GameCult-Muninn"
`$deadline = [DateTime]::UtcNow.AddSeconds($ServeStartTimeoutSeconds)
`$process = `$null
do {
Start-Sleep -Milliseconds 500
`$process = Get-CimInstance Win32_Process |
Where-Object { `$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*serve*" -and `$_.CommandLine -like "*--host*raven*" } |
Select-Object -First 1
} while (`$null -eq `$process -and [DateTime]::UtcNow -lt `$deadline)
if (`$null -eq `$process) {
throw "Muninn serve process did not start on Raven within $ServeStartTimeoutSeconds seconds"
}
foreach (`$pattern in @(
"--idunn-rudp-health",
"$IdunnRudpHealth",
"--idunn-daemon",
"$IdunnDaemon",
"--idunn-health-contract",
"$IdunnHealthContract"
)) {
if (`$process.CommandLine -notlike "*`$pattern*") {
throw "Muninn Raven serve command line is missing `${pattern}: `$(`$process.CommandLine)"
}
}
"@
$uploadSpecs = @(
@{ LocalPath = $LocalLoopbackScript; RemotePath = ($LoopbackScript -replace "\\", "/") },
@{ LocalPath = $localServePs; RemotePath = ($servePsPath -replace "\\", "/") },
@{ LocalPath = $localServeVbs; RemotePath = ($serveVbsPath -replace "\\", "/") },
@{ LocalPath = $localServeCmd; RemotePath = ($serveCmdPath -replace "\\", "/") }
)
Invoke-RavenUploadedPowerShell -RavenHost $RavenHost -RemoteScriptContent $remoteScript -UploadSpecs $uploadSpecs -TempPrefix "odin-raven-muninn-restart"
} finally {
Remove-Item -LiteralPath $uploadRoot -Recurse -Force -ErrorAction SilentlyContinue
}
exit $LASTEXITCODE

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-nightwing-muninn.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-nightwing-muninn.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-nightwing-muninn.ps1 -DiscoverMoveState -ClaimUsbMoves

View file

@ -1,190 +0,0 @@
param(
[string] $SshTarget = "nightwing",
[string] $MuninnExe = "/home/metacrat/.local/bin/muninn",
[string] $StorePath = "/home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc",
[string] $LogRoot = "/home/metacrat/.local/state/gamecult/muninn",
[string[]] $MoveState = @(),
[switch] $DiscoverMoveState = $true,
[switch] $ClaimUsbMoves = $true,
[string] $MoveEvidenceStream = "muninn:nightwing:move-evidence",
[string[]] $MoveMarkerCamera = @(
"nightwing-eye-0=/dev/video2",
"nightwing-eye-1=/dev/video3"
),
[int] $MoveTrackerExposureMilli = 100,
[hashtable] $MoveTrackerCameraExposure = @{
"nightwing-eye-0" = 300
"nightwing-eye-1" = 100
},
[int] $IntervalSeconds = 15,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[string] $IdunnDaemon = "nightwing-muninn",
[string] $IdunnHealthContract = "muninn.cultnet-rudp-remote-telemetry-and-move-hid",
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" }),
[string] $OdinCultMeshRudpEndpoint = $(if ($env:CULTMESH_URI_ODIN_RUDP) { $env:CULTMESH_URI_ODIN_RUDP } else { "10.77.0.2:17871" }),
[string] $HidControllerRudpBind = "0.0.0.0:17888",
[string] $HidControllerRudpAdvertise = $(if ($env:MUNINN_HID_CONTROLLER_RUDP_ADVERTISE) { $env:MUNINN_HID_CONTROLLER_RUDP_ADVERTISE } else { "10.77.0.3:17888" }),
[string] $CommandRudpBind = "0.0.0.0:17889",
[string] $CommandRudpAdvertise = "10.77.0.3:17889"
)
$ErrorActionPreference = "Stop"
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-nightwing-muninn.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if ($IdunnRudpHealth -match '^(127\.0\.0\.1|localhost):(\d+)$') {
$IdunnRudpHealth = "10.77.0.2:$($Matches[2])"
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind) -and [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
throw "HID controller RUDP advertise endpoint must be supplied by -HidControllerRudpAdvertise or MUNINN_HID_CONTROLLER_RUDP_ADVERTISE when HID RUDP bind is enabled; no Nightwing LAN default is allowed."
}
function Quote-ShSingle([string] $Value) {
return "'" + ($Value -replace "'", "'\''") + "'"
}
function Set-AsciiFile {
param(
[Parameter(Mandatory = $true)] [string] $Path,
[Parameter(Mandatory = $true)] [string] $Content
)
[System.IO.File]::WriteAllText($Path, ($Content -replace "`r?`n", "`n"), [System.Text.Encoding]::ASCII)
}
function Invoke-UploadedShell {
param(
[Parameter(Mandatory = $true)] [string] $SshTarget,
[Parameter(Mandatory = $true)] [string] $RemoteScriptContent
)
$uploadId = [guid]::NewGuid().ToString("N")
$localTempRoot = Join-Path $env:TEMP "odin-nightwing-muninn-restart-$uploadId"
$localRemoteScript = Join-Path $localTempRoot "restart-nightwing-muninn-$uploadId.sh"
$localSftpBatch = Join-Path $localTempRoot "restart-nightwing-muninn-$uploadId.sftp"
$remoteScriptPath = "/tmp/restart-nightwing-muninn-$uploadId.sh"
try {
New-Item -ItemType Directory -Force -Path $localTempRoot | Out-Null
Set-AsciiFile -Path $localRemoteScript -Content $RemoteScriptContent
Set-AsciiFile -Path $localSftpBatch -Content ('put "{0}" "{1}"' -f $localRemoteScript, $remoteScriptPath)
& sftp.exe -b $localSftpBatch $SshTarget
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "chmod +x '$remoteScriptPath' && bash '$remoteScriptPath'"
$remoteExit = $LASTEXITCODE
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $SshTarget "rm -f '$remoteScriptPath'"
exit $remoteExit
} finally {
Remove-Item -LiteralPath $localTempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
$moveStateSpecs = @($MoveState | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
if ($ClaimUsbMoves.IsPresent -and -not $DiscoverMoveState.IsPresent -and $moveStateSpecs.Count -eq 0) {
throw "-ClaimUsbMoves requires -DiscoverMoveState or at least one explicit -MoveState value."
}
if ($DiscoverMoveState.IsPresent) {
$claimScript = Join-Path $PSScriptRoot "claim-nightwing-usb-moves.ps1"
if ($ClaimUsbMoves.IsPresent) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $claimScript -SshTarget $SshTarget
}
$sourceScript = Join-Path $PSScriptRoot "get-nightwing-move-state-sources.ps1"
$moveStateSpecs = @(& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $sourceScript -SshTarget $SshTarget)
if ($moveStateSpecs.Count -eq 0) {
throw "Nightwing Move runtime was explicitly requested, but no live Move state sources were discovered."
}
}
$moveStateSetLines = ($moveStateSpecs | ForEach-Object {
"set -- ""`$@"" --move-state $(Quote-ShSingle $_)"
}) -join "`n"
$moveRuntimeSetLines = @()
if ($moveStateSpecs.Count -gt 0) {
$moveRuntimeSetLines += $moveStateSetLines
$moveRuntimeSetLines += "set -- ""`$@"" --move-evidence-stream $(Quote-ShSingle $MoveEvidenceStream)"
if ($MoveMarkerCamera.Count -gt 0) {
foreach ($camera in $MoveMarkerCamera) {
$moveRuntimeSetLines += "set -- ""`$@"" --move-marker-camera $(Quote-ShSingle $camera)"
}
$moveRuntimeSetLines += "set -- ""`$@"" --move-psmoveapi-tracker"
$moveRuntimeSetLines += "set -- ""`$@"" --move-tracker-exposure-milli $(Quote-ShSingle ([string]$MoveTrackerExposureMilli))"
foreach ($cameraId in @($MoveTrackerCameraExposure.Keys | Sort-Object)) {
$moveRuntimeSetLines += "set -- ""`$@"" --move-tracker-camera-exposure $(Quote-ShSingle ("{0}={1}" -f $cameraId, $MoveTrackerCameraExposure[$cameraId]))"
}
}
}
$moveRuntimeSetBlock = ($moveRuntimeSetLines -join "`n")
$odinCultMeshUriSetBlock = ""
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri)) {
$odinCultMeshUriSetBlock = "set -- ""`$@"" --odin-cultmesh-uri $(Quote-ShSingle $OdinCultMeshUri)"
}
$hidControllerRudpSetLines = @()
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind)) {
$hidControllerRudpSetLines += "set -- ""`$@"" --hid-controller-rudp-bind $(Quote-ShSingle $HidControllerRudpBind)"
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
$hidControllerRudpSetLines += "set -- ""`$@"" --hid-controller-rudp-advertise $(Quote-ShSingle $HidControllerRudpAdvertise)"
}
$hidControllerRudpSetBlock = ($hidControllerRudpSetLines -join "`n")
$commandRudpSetBlock = "set -- ""`$@"" --command-rudp-bind $(Quote-ShSingle $CommandRudpBind) --command-rudp-advertise $(Quote-ShSingle $CommandRudpAdvertise)"
$remoteScript = @"
set -eu
mkdir -p '$LogRoot'
if [ ! -x '$MuninnExe' ]; then
echo 'Muninn executable not found at $MuninnExe' >&2
exit 1
fi
if [ -f '$LogRoot/muninn.pid' ] && kill -0 "`$(cat '$LogRoot/muninn.pid')" 2>/dev/null; then
kill "`$(cat '$LogRoot/muninn.pid')" 2>/dev/null || true
sleep 1
fi
for pid in `$(pgrep -f '[m]uninn move-tracker-worker .*--host nightwing' 2>/dev/null || true); do
kill "`$pid" 2>/dev/null || true
done
for pid in `$(pgrep -f '[m]uninn serve .*--host nightwing' 2>/dev/null || true); do
kill "`$pid" 2>/dev/null || true
done
for pid in `$(pgrep -f '[m]uninn serve .*--host nightwing-eye0' 2>/dev/null || true); do
kill "`$pid" 2>/dev/null || true
done
set -- serve \
--store '$StorePath' \
--log-root '$LogRoot' \
--host nightwing
$moveRuntimeSetBlock
set -- "`$@" \
--interval-seconds '$IntervalSeconds' \
--idunn-rudp-health '$IdunnRudpHealth' \
--idunn-daemon '$IdunnDaemon' \
--idunn-health-contract '$IdunnHealthContract'
$odinCultMeshUriSetBlock
$hidControllerRudpSetBlock
$commandRudpSetBlock
CULTNET_RUDP_TRACE=1 CULTMESH_URI_ODIN_RUDP='$OdinCultMeshRudpEndpoint' nohup '$MuninnExe' "`$@" \
> '$LogRoot/muninn-serve.out.log' \
2> '$LogRoot/muninn-serve.err.log' \
< /dev/null &
echo `$! > '$LogRoot/muninn.pid'
sleep 1
muninn_pid="`$(cat '$LogRoot/muninn.pid')"
kill -0 "`$muninn_pid" 2>/dev/null
muninn_cmdline="`$(tr '\0' ' ' < "/proc/`$muninn_pid/cmdline")"
if ! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--host nightwing' >/dev/null 2>&1 ||
! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--idunn-rudp-health $IdunnRudpHealth' >/dev/null 2>&1 ||
! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--move-marker-camera nightwing-eye-0=/dev/video2' >/dev/null 2>&1 ||
! printf '%s\n' "`$muninn_cmdline" | grep -F -- '--move-marker-camera nightwing-eye-1=/dev/video3' >/dev/null 2>&1; then
echo 'Nightwing Muninn serve command line is missing Idunn health or dual-camera arguments' >&2
exit 1
fi
"@
Invoke-UploadedShell -SshTarget $SshTarget -RemoteScriptContent $remoteScript

View file

@ -1,2 +0,0 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-muninn.ps1

View file

@ -1,7 +0,0 @@
param(
[string] $RavenHost = "raven"
)
$script = Join-Path $PSScriptRoot "restart-muninn.ps1"
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script -RavenHost $RavenHost
exit $LASTEXITCODE

View file

@ -1,10 +0,0 @@
@echo off
if not "%IDUNN_ACTUATOR%"=="1" (
echo restart-starfire-muninn.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
if /I not "%IDUNN_COMMAND_AUTHORITY%"=="idunn-daemon" (
echo restart-starfire-muninn.cmd is an Idunn actuator wrapper. Redeploy by poking Idunn; direct service restart is not an owned path. 1>&2
exit /b 1
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File %~dp0restart-starfire-muninn.ps1

View file

@ -1,174 +0,0 @@
param(
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn-d4d0616.exe",
[string] $StorePath = "C:\Meta\Odin\state\starfire.muninn.telemetry.cc",
[string] $LogRoot = "C:\Meta\Odin\logs\starfire-muninn",
[string] $QuestSerial = "1WMHHB68PG1515",
[string] $MoveBluetoothHost = "",
[string[]] $MoveState = @(),
[switch] $EnableUsbMoveState,
[string] $IdunnRudpHealth = $env:IDUNN_RUDP_HEALTH,
[string] $IdunnDaemon = "starfire-muninn",
[string] $IdunnHealthContract = "muninn.cultnet-rudp-local-telemetry-and-quest-access",
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" }),
[string] $OdinCultMeshRudpEndpoint = $(if ($env:CULTMESH_URI_ODIN_RUDP) { $env:CULTMESH_URI_ODIN_RUDP } else { "127.0.0.1:17871" }),
[string] $HidControllerRudpBind = "",
[string] $HidControllerRudpAdvertise = ""
)
$ErrorActionPreference = "Stop"
$restartMutex = [System.Threading.Mutex]::new($false, "Global\GameCult-Starfire-Muninn-Restart")
if (-not $restartMutex.WaitOne([TimeSpan]::FromMinutes(2))) {
throw "Timed out waiting for the Starfire Muninn restart actuator mutex."
}
if ($env:IDUNN_ACTUATOR -ne "1" -or $env:IDUNN_COMMAND_AUTHORITY -ne "idunn-daemon") {
throw "restart-starfire-muninn.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path."
}
if (-not (Test-Path -LiteralPath $MuninnExe)) {
throw "Muninn executable not found at $MuninnExe"
}
if ([string]::IsNullOrWhiteSpace($IdunnRudpHealth)) {
throw "Idunn RUDP health endpoint must be supplied by -IdunnRudpHealth or IDUNN_RUDP_HEALTH; no Starfire LAN default is allowed."
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind) -and [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
throw "HID controller RUDP advertise endpoint must be supplied by -HidControllerRudpAdvertise or MUNINN_HID_CONTROLLER_RUDP_ADVERTISE when HID RUDP bind is enabled; no Starfire LAN default is allowed."
}
Get-CimInstance Win32_Process |
Where-Object {
$_.Name -like "muninn*.exe" -and
$_.CommandLine -like "*serve*" -and
$_.CommandLine -like "*--host*starfire*"
} |
ForEach-Object {
& taskkill.exe /PID $_.ProcessId /T /F | Out-Null
}
New-Item -ItemType Directory -Force -Path $LogRoot | Out-Null
$storeParent = Split-Path -Parent $StorePath
if (-not [string]::IsNullOrWhiteSpace($storeParent)) {
New-Item -ItemType Directory -Force -Path $storeParent | Out-Null
}
if (-not [string]::IsNullOrWhiteSpace($MoveBluetoothHost)) {
& $MuninnExe claim-move-host --move-host $MoveBluetoothHost
}
$arguments = @(
"serve",
"--store", $StorePath,
"--log-root", $LogRoot,
"--host", "starfire",
"--interval-seconds", "15",
"--quest-adb",
"--quest-serial", $QuestSerial,
"--idunn-rudp-health", $IdunnRudpHealth,
"--idunn-daemon", $IdunnDaemon,
"--idunn-health-contract", $IdunnHealthContract
)
if (-not [string]::IsNullOrWhiteSpace($OdinCultMeshUri)) {
$arguments += @("--odin-cultmesh-uri", $OdinCultMeshUri)
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpBind)) {
$arguments += @("--hid-controller-rudp-bind", $HidControllerRudpBind)
}
if (-not [string]::IsNullOrWhiteSpace($HidControllerRudpAdvertise)) {
$arguments += @("--hid-controller-rudp-advertise", $HidControllerRudpAdvertise)
}
if (-not [string]::IsNullOrWhiteSpace($MoveBluetoothHost)) {
$arguments += @("--move-host", $MoveBluetoothHost)
}
foreach ($source in $MoveState) {
if (-not [string]::IsNullOrWhiteSpace($source)) {
$arguments += @("--move-state", $source)
}
}
if ($EnableUsbMoveState) {
$arguments += @("--move-state", "move-starfire-usb=windows-psmove")
}
$serveOutLog = Join-Path $LogRoot "muninn-serve.out.log"
$serveErrLog = Join-Path $LogRoot "muninn-serve.err.log"
$pidPath = Join-Path $LogRoot "muninn-serve.pid"
$lockPath = "$StorePath.lock"
function Start-MuninnServeProcess {
param([string[]] $ArgumentList)
$previousOdinEndpoint = $env:CULTMESH_URI_ODIN_RUDP
try {
$env:CULTMESH_URI_ODIN_RUDP = $OdinCultMeshRudpEndpoint
$process = Start-Process `
-FilePath $MuninnExe `
-ArgumentList $ArgumentList `
-WindowStyle Hidden `
-PassThru `
-RedirectStandardOutput $serveOutLog `
-RedirectStandardError $serveErrLog
} finally {
$env:CULTMESH_URI_ODIN_RUDP = $previousOdinEndpoint
}
$process.Id | Set-Content -Encoding ASCII -LiteralPath $pidPath
return $process
}
function Get-StarfireMuninnServeProcess {
return Get-CimInstance Win32_Process |
Where-Object {
$_.Name -like "muninn*.exe" -and
$_.CommandLine -like "*serve*" -and
$_.CommandLine -like "*--host*starfire*"
} |
Select-Object -First 1
}
function Reset-CorruptMuninnStore {
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
if (Test-Path -LiteralPath $StorePath) {
Move-Item -LiteralPath $StorePath -Destination "$StorePath.corrupt-$timestamp" -Force
}
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
$storeLeaf = Split-Path -Leaf $StorePath
Get-ChildItem -LiteralPath $storeParent -ErrorAction SilentlyContinue | Where-Object {
$_.Name -like "$storeLeaf.*.tmp"
} | ForEach-Object {
Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
}
}
Remove-Item -LiteralPath $lockPath -Force -ErrorAction SilentlyContinue
$null = Start-MuninnServeProcess -ArgumentList $arguments
Start-Sleep -Seconds 2
$processCheck = Get-StarfireMuninnServeProcess
if ($null -eq $processCheck) {
$serveError = if (Test-Path -LiteralPath $serveErrLog) {
Get-Content -LiteralPath $serveErrLog -Raw
} else {
""
}
if ($serveError -like "*failed to decode MessagePack*") {
Reset-CorruptMuninnStore
$null = Start-MuninnServeProcess -ArgumentList $arguments
Start-Sleep -Seconds 2
$processCheck = Get-StarfireMuninnServeProcess
}
}
if ($null -eq $processCheck) {
throw "Starfire Muninn serve process is not running after restart"
}
foreach ($pattern in @(
"--idunn-rudp-health",
$IdunnRudpHealth,
"--idunn-daemon",
$IdunnDaemon,
"--idunn-health-contract",
$IdunnHealthContract
)) {
if ($processCheck.CommandLine -notlike "*$pattern*") {
throw "Starfire Muninn serve command line is missing ${pattern}: $($processCheck.CommandLine)"
}
}

View file

@ -1,121 +0,0 @@
param(
[string] $RestartScript = (Join-Path $PSScriptRoot "restart-muninn.ps1")
)
$ErrorActionPreference = "Stop"
function Fail-MuninnMediaProfile {
param([string] $Message)
throw "Muninn media profile verification failed: $Message"
}
function Get-AssignmentAst {
param(
[System.Management.Automation.Language.Ast] $Ast,
[string] $VariableName
)
$matches = @($Ast.FindAll({
param($Node)
if ($Node -isnot [System.Management.Automation.Language.AssignmentStatementAst]) {
return $false
}
$left = $Node.Left
return $left -is [System.Management.Automation.Language.VariableExpressionAst] -and
$left.VariablePath.UserPath -eq $VariableName
}, $true))
if ($matches.Count -ne 1) {
Fail-MuninnMediaProfile "expected exactly one assignment to `$$VariableName, found $($matches.Count)"
}
return $matches[0]
}
function Get-ConstantAssignmentValue {
param(
[System.Management.Automation.Language.Ast] $Ast,
[string] $VariableName
)
$assignment = Get-AssignmentAst -Ast $Ast -VariableName $VariableName
$right = $assignment.Right
if ($right -is [System.Management.Automation.Language.CommandExpressionAst]) {
$right = $right.Expression
}
if ($right -isnot [System.Management.Automation.Language.ConstantExpressionAst]) {
Fail-MuninnMediaProfile "`$$VariableName is not a constant assignment"
}
return $right.Value
}
function Assert-TokenPair {
param(
[string[]] $Tokens,
[string] $Name,
[string] $Value
)
for ($index = 0; $index -lt ($Tokens.Count - 1); $index++) {
if ($Tokens[$index] -eq $Name -and $Tokens[$index + 1] -eq $Value) {
return
}
}
Fail-MuninnMediaProfile "video proof arguments are missing '$Name $Value'"
}
if (-not (Test-Path -LiteralPath $RestartScript)) {
Fail-MuninnMediaProfile "restart script not found at $RestartScript"
}
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path -LiteralPath $RestartScript),
[ref] $tokens,
[ref] $errors
)
if ($errors.Count -gt 0) {
Fail-MuninnMediaProfile (($errors | ForEach-Object { $_.ToString() }) -join "; ")
}
$framerate = Get-ConstantAssignmentValue -Ast $ast -VariableName "videoProofFramerate"
$bitrateKbps = Get-ConstantAssignmentValue -Ast $ast -VariableName "videoProofBitrateKbps"
if ($framerate -ne 30) {
Fail-MuninnMediaProfile "video proof framerate is $framerate, expected 30"
}
if ($bitrateKbps -ne 12000) {
Fail-MuninnMediaProfile "video proof bitrate is $bitrateKbps, expected 12000"
}
$videoProofAssignment = Get-AssignmentAst -Ast $ast -VariableName "videoProofArguments"
$stringConstants = @($videoProofAssignment.Right.FindAll({
param($Node)
$Node -is [System.Management.Automation.Language.StringConstantExpressionAst]
}, $true))
$literalTokens = @($stringConstants | ForEach-Object { $_.Value })
Assert-TokenPair -Tokens $literalTokens -Name "-fflags" -Value "nobuffer"
Assert-TokenPair -Tokens $literalTokens -Name "-flags" -Value "low_delay"
Assert-TokenPair -Tokens $literalTokens -Name "-c:v" -Value "h264_nvenc"
Assert-TokenPair -Tokens $literalTokens -Name "-preset" -Value "p1"
Assert-TokenPair -Tokens $literalTokens -Name "-tune" -Value "ull"
Assert-TokenPair -Tokens $literalTokens -Name "-zerolatency" -Value "1"
Assert-TokenPair -Tokens $literalTokens -Name "-bf" -Value "0"
Assert-TokenPair -Tokens $literalTokens -Name "-delay" -Value "0"
Assert-TokenPair -Tokens $literalTokens -Name "-rc" -Value "cbr"
Assert-TokenPair -Tokens $literalTokens -Name "-rc-lookahead" -Value "0"
Assert-TokenPair -Tokens $literalTokens -Name "-forced-idr" -Value "1"
$videoProofText = $videoProofAssignment.Extent.Text
if ($videoProofText -notmatch '\$videoProofVbvKbits') {
Fail-MuninnMediaProfile "video proof bufsize is not derived from the one-frame VBV budget"
}
if ($videoProofText -match '"p4"|''p4''|24000k') {
Fail-MuninnMediaProfile "video proof still contains legacy p4 or 24000k latency settings"
}
Write-Host "Muninn media profile verification passed for $RestartScript"