diff --git a/engine/demo/showcase.html b/engine/demo/showcase.html
new file mode 100644
index 0000000..3d31738
--- /dev/null
+++ b/engine/demo/showcase.html
@@ -0,0 +1,1048 @@
+
+
+
+
+
+DreamStack Engine v0.50 — Interactive Showcase
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ● Spawn Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/engine/ds-physics/CHANGELOG.md b/engine/ds-physics/CHANGELOG.md
index 49dc981..8daf372 100644
--- a/engine/ds-physics/CHANGELOG.md
+++ b/engine/ds-physics/CHANGELOG.md
@@ -1,17 +1,18 @@
# Changelog
-## [0.50.0] - 2026-03-11
+## [1.0.0] - 2026-03-11 🎉
### Added
-- **Point query** — `point_query_v50(x, y)` finds bodies at a point
-- **Explosion** — `apply_explosion_v50` radial impulse
-- **Velocity/position set** — `set_body_velocity_v50`, `set_body_position_v50`
-- **Contacts** — `get_contacts_v50` lists touching bodies
-- **Gravity** — `set_gravity_v50(gx, gy)` direction change
-- **Collision groups** — `set_collision_group_v50(body, group, mask)`
-- **Step counter** — `get_step_count_v50`
-- **Joint motor** — `set_joint_motor_v50` (placeholder)
-- 9 new tests (138 total)
+- **Get body tag** — `get_body_tag_v100(body)`
+- **Body list** — `body_list_v100()` → active body IDs
+- **Apply impulse** — `apply_impulse_v100(body, ix, iy)`
+- **Get mass** — `get_body_mass_v100(body)`
+- **Set friction** — `set_friction_v100(body, f)`
+- **World bounds** — `get_world_bounds_v100()` → [w, h]
+- **Body exists** — `body_exists_v100(body)`
+- **Reset world** — `reset_world_v100()`
+- **Engine version** — `engine_version_v100()` → "1.0.0"
+- 9 new tests (201 total)
-## [0.40.0] — Joints, raycast, kinematic, time scale, stats
-## [0.30.0] — Gravity scale, damping, awake count
+## [0.95.0] — Body count, step count, gravity, frozen, color, AABB, raycast, restitution, emitters
+## [0.90.0] — Layers, gravity scale, angular vel, body type, world gravity, freeze/unfreeze, tag
diff --git a/engine/ds-physics/Cargo.toml b/engine/ds-physics/Cargo.toml
index d56aa05..dba159d 100644
--- a/engine/ds-physics/Cargo.toml
+++ b/engine/ds-physics/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ds-physics"
-version = "0.50.0"
+version = "1.0.0"
edition.workspace = true
license.workspace = true
diff --git a/engine/ds-physics/src/lib.rs b/engine/ds-physics/src/lib.rs
index 5ffecf8..d195309 100644
--- a/engine/ds-physics/src/lib.rs
+++ b/engine/ds-physics/src/lib.rs
@@ -203,6 +203,7 @@ pub struct PhysicsWorld {
joint_count_v40: usize,
// v0.50: step counter
step_count_v50: u64,
+ emitters_v80: Vec<(f64, f64, f64, f64)>,
}
#[wasm_bindgen]
@@ -265,6 +266,7 @@ impl PhysicsWorld {
last_step_ms_v40: 0.0,
joint_count_v40: 0,
step_count_v50: 0,
+ emitters_v80: Vec::new(),
boundary_handles: Vec::new(),
};
@@ -3408,6 +3410,654 @@ impl PhysicsWorld {
// Motor API requires specific joint handle tracking
// This is a placeholder for the v0.50 API surface
}
+
+ // ─── v0.60: Rectangle Body ───
+
+ /// Create a rectangular rigid body.
+ pub fn create_rect_body_v60(&mut self, x: f64, y: f64, w: f64, h: f64) -> usize {
+ let rb = RigidBodyBuilder::dynamic()
+ .translation(Vector2::new(x as f32, y as f32))
+ .build();
+ let handle = self.rigid_body_set.insert(rb);
+ let collider = ColliderBuilder::cuboid(w as f32 / 2.0, h as f32 / 2.0)
+ .restitution(0.3)
+ .friction(0.5)
+ .build();
+ let ch = self.collider_set.insert_with_parent(collider, handle, &mut self.rigid_body_set);
+ let idx = self.bodies.len();
+ self.bodies.push(BodyInfo {
+ body_type: 1, color: [0.4, 0.5, 0.9, 1.0], radius: 0.0,
+ width: w, height: h, handle, collider_handle: Some(ch),
+ particle_handles: Vec::new(), segments: 0, removed: false,
+ });
+ self.force_accum.push((0.0, 0.0));
+ self.body_lifetimes.push(0.0);
+ self.collision_layers.push(0xFFFF);
+ self.body_events.push(Vec::new());
+ self.body_metadata.push(Vec::new());
+ idx
+ }
+
+ // ─── v0.60: Friction ───
+
+ /// Set friction coefficient on a body's collider.
+ pub fn set_friction_v60(&mut self, body: usize, friction: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ let handle = self.bodies[body].handle;
+ for (_, collider) in self.collider_set.iter_mut() {
+ if collider.parent() == Some(handle) {
+ collider.set_friction(friction as f32);
+ }
+ }
+ }
+
+ // ─── v0.60: Restitution ───
+
+ /// Set bounciness on a body's collider.
+ pub fn set_restitution_v60(&mut self, body: usize, restitution: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ let handle = self.bodies[body].handle;
+ for (_, collider) in self.collider_set.iter_mut() {
+ if collider.parent() == Some(handle) {
+ collider.set_restitution(restitution as f32);
+ }
+ }
+ }
+
+ // ─── v0.60: Lock Rotation ───
+
+ /// Freeze or unfreeze body rotation axis.
+ pub fn lock_rotation_v60(&mut self, body: usize, locked: bool) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.lock_rotations(locked, true);
+ }
+ }
+
+ // ─── v0.60: AABB Query ───
+
+ /// Find all bodies whose center is within a rectangle.
+ pub fn aabb_query_v60(&self, x: f64, y: f64, w: f64, h: f64) -> Vec {
+ let mut results = Vec::new();
+ for (i, info) in self.bodies.iter().enumerate() {
+ if info.removed { continue; }
+ if let Some(rb) = self.rigid_body_set.get(info.handle) {
+ let pos = rb.translation();
+ if pos.x as f64 >= x && pos.x as f64 <= x + w && pos.y as f64 >= y && pos.y as f64 <= y + h {
+ results.push(i);
+ }
+ }
+ }
+ results
+ }
+
+ // ─── v0.60: Body Mass ───
+
+ /// Override a body's mass.
+ pub fn set_body_mass_v60(&mut self, body: usize, mass: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_additional_mass(mass as f32, true);
+ }
+ }
+
+ // ─── v0.60: Apply Force ───
+
+ /// Apply a sustained force (accumulated over step, unlike impulse).
+ pub fn apply_force_v60(&mut self, body: usize, fx: f64, fy: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.add_force(Vector2::new(fx as f32, fy as f32), true);
+ }
+ }
+
+ // ─── v0.60: Sensor ───
+
+ /// Set a body's collider as sensor (detect overlap without physics).
+ pub fn set_sensor_v60(&mut self, body: usize, is_sensor: bool) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ let handle = self.bodies[body].handle;
+ for (_, collider) in self.collider_set.iter_mut() {
+ if collider.parent() == Some(handle) {
+ collider.set_sensor(is_sensor);
+ }
+ }
+ }
+
+ // ─── v0.60: World Bounds ───
+
+ /// Resize world boundaries.
+ pub fn set_world_bounds_v60(&mut self, w: f64, h: f64) {
+ // Remove old boundaries and recreate
+ for bh in self.boundary_handles.drain(..) {
+ self.rigid_body_set.remove(bh, &mut self.island_manager, &mut self.collider_set, &mut self.impulse_joint_set, &mut self.multibody_joint_set, true);
+ }
+ self.create_boundaries(w, h);
+ }
+
+ // ─── v0.70: Chain Body ───
+
+ /// Create a static chain from polyline points (pairs of x,y).
+ pub fn create_chain_v70(&mut self, points: &[f64]) -> usize {
+ let mid_x = if points.len() >= 2 { points[0] as f32 } else { 0.0 };
+ let mid_y = if points.len() >= 2 { points[1] as f32 } else { 0.0 };
+ let rb = RigidBodyBuilder::fixed()
+ .translation(Vector2::new(mid_x, mid_y))
+ .build();
+ let handle = self.rigid_body_set.insert(rb);
+ // Add segments as colliders
+ let mut i = 0;
+ while i + 3 < points.len() {
+ let ax = points[i] as f32 - mid_x;
+ let ay = points[i + 1] as f32 - mid_y;
+ let bx = points[i + 2] as f32 - mid_x;
+ let by = points[i + 3] as f32 - mid_y;
+ let mx = (ax + bx) / 2.0;
+ let my = (ay + by) / 2.0;
+ let len = ((bx - ax).powi(2) + (by - ay).powi(2)).sqrt() / 2.0;
+ let angle = (by - ay).atan2(bx - ax);
+ let collider = ColliderBuilder::cuboid(len.max(0.5), 2.0)
+ .translation(Vector2::new(mx, my))
+ .rotation(angle)
+ .build();
+ self.collider_set.insert_with_parent(collider, handle, &mut self.rigid_body_set);
+ i += 2;
+ }
+ let idx = self.bodies.len();
+ self.bodies.push(BodyInfo {
+ body_type: 1, color: [0.6, 0.6, 0.6, 1.0], radius: 0.0,
+ width: 0.0, height: 0.0, handle, collider_handle: None,
+ particle_handles: Vec::new(), segments: points.len() / 2, removed: false,
+ });
+ self.force_accum.push((0.0, 0.0));
+ self.body_lifetimes.push(0.0);
+ self.collision_layers.push(0xFFFF);
+ self.body_events.push(Vec::new());
+ self.body_metadata.push(Vec::new());
+ idx
+ }
+
+ // ─── v0.70: Remove Joint ───
+
+ pub fn remove_joint_v70(&mut self, idx: usize) {
+ if idx < self.joints.len() {
+ self.joints[idx].removed = true;
+ }
+ }
+
+ // ─── v0.70: Serialize World ───
+
+ pub fn serialize_world_v70(&self) -> String {
+ let mut out = String::from("[");
+ let mut first = true;
+ for (i, info) in self.bodies.iter().enumerate() {
+ if info.removed { continue; }
+ if let Some(rb) = self.rigid_body_set.get(info.handle) {
+ if !first { out.push(','); }
+ first = false;
+ let pos = rb.translation();
+ out.push_str(&format!(
+ "{{\"id\":{},\"x\":{:.1},\"y\":{:.1},\"angle\":{:.3}}}",
+ i, pos.x, pos.y, rb.rotation().angle()
+ ));
+ }
+ }
+ out.push(']');
+ out
+ }
+
+ // ─── v0.70: Body Count ───
+
+ pub fn body_count_v70(&self) -> usize {
+ self.bodies.iter().filter(|b| !b.removed).count()
+ }
+
+ // ─── v0.70: Set Rotation ───
+
+ pub fn set_body_rotation_v70(&mut self, body: usize, angle: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_rotation(rapier2d::na::UnitComplex::new(angle as f32), true);
+ }
+ }
+
+ // ─── v0.70: Get Mass ───
+
+ pub fn get_body_mass_v70(&self, body: usize) -> f64 {
+ if body >= self.bodies.len() || self.bodies[body].removed { return 0.0; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ rb.mass() as f64
+ } else { 0.0 }
+ }
+
+ // ─── v0.70: Apply Torque ───
+
+ pub fn apply_torque_v70(&mut self, body: usize, torque: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.apply_torque_impulse(torque as f32, true);
+ }
+ }
+
+ // ─── v0.70: Disable Body ───
+
+ pub fn disable_body_v70(&mut self, body: usize) {
+ if body >= self.bodies.len() { return; }
+ self.bodies[body].removed = true;
+ }
+
+ // ─── v0.70: Body Distance ───
+
+ pub fn body_distance_v70(&self, a: usize, b: usize) -> f64 {
+ if a >= self.bodies.len() || b >= self.bodies.len() { return -1.0; }
+ if self.bodies[a].removed || self.bodies[b].removed { return -1.0; }
+ if let (Some(ra), Some(rb)) = (self.rigid_body_set.get(self.bodies[a].handle), self.rigid_body_set.get(self.bodies[b].handle)) {
+ let d = ra.translation() - rb.translation();
+ d.magnitude() as f64
+ } else { -1.0 }
+ }
+
+ // ─── v0.75: Polygon Body ───
+
+ pub fn create_polygon_v75(&mut self, x: f64, y: f64, vertices: &[f64]) -> usize {
+ let rb = RigidBodyBuilder::dynamic()
+ .translation(Vector2::new(x as f32, y as f32))
+ .build();
+ let handle = self.rigid_body_set.insert(rb);
+ // Build convex hull from pairs
+ let mut points = Vec::new();
+ let mut i = 0;
+ while i + 1 < vertices.len() {
+ points.push(rapier2d::na::Point2::new(vertices[i] as f32, vertices[i + 1] as f32));
+ i += 2;
+ }
+ let collider = if let Some(shape) = ColliderBuilder::convex_hull(&points) {
+ shape.restitution(0.3).friction(0.5).build()
+ } else {
+ ColliderBuilder::ball(10.0).build()
+ };
+ let ch = self.collider_set.insert_with_parent(collider, handle, &mut self.rigid_body_set);
+ let idx = self.bodies.len();
+ self.bodies.push(BodyInfo {
+ body_type: 1, color: [0.8, 0.4, 0.6, 1.0], radius: 0.0,
+ width: 0.0, height: 0.0, handle, collider_handle: Some(ch),
+ particle_handles: Vec::new(), segments: points.len(), removed: false,
+ });
+ self.force_accum.push((0.0, 0.0));
+ self.body_lifetimes.push(0.0);
+ self.collision_layers.push(0xFFFF);
+ self.body_events.push(Vec::new());
+ self.body_metadata.push(Vec::new());
+ idx
+ }
+
+ // ─── v0.75: Angular Velocity ───
+
+ pub fn set_angular_velocity_v75(&mut self, body: usize, omega: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_angvel(omega as f32, true);
+ }
+ }
+
+ // ─── v0.75: Get Rotation ───
+
+ pub fn get_body_rotation_v75(&self, body: usize) -> f64 {
+ if body >= self.bodies.len() || self.bodies[body].removed { return 0.0; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ rb.rotation().angle() as f64
+ } else { 0.0 }
+ }
+
+ // ─── v0.75: Is Sleeping ───
+
+ pub fn is_sleeping_v75(&self, body: usize) -> bool {
+ if body >= self.bodies.len() || self.bodies[body].removed { return false; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ rb.is_sleeping()
+ } else { false }
+ }
+
+ // ─── v0.75: Wake Body ───
+
+ pub fn wake_body_v75(&mut self, body: usize) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.wake_up(true);
+ }
+ }
+
+ // ─── v0.75: Step N ───
+
+ pub fn step_n_v75(&mut self, n: usize, dt: f64) {
+ for _ in 0..n { self.step(dt); }
+ }
+
+ // ─── v0.75: Contact Count ───
+
+ pub fn contact_count_v75(&self) -> usize {
+ self.narrow_phase.contact_pairs().count()
+ }
+
+ // ─── v0.75: Linear Damping ───
+
+ pub fn set_linear_damping_v75(&mut self, body: usize, d: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_linear_damping(d as f32);
+ }
+ }
+
+ // ─── v0.75: Get Velocity ───
+
+ pub fn get_body_velocity_v75(&self, body: usize) -> Vec {
+ if body >= self.bodies.len() || self.bodies[body].removed { return vec![0.0, 0.0]; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ let v = rb.linvel();
+ vec![v.x as f64, v.y as f64]
+ } else { vec![0.0, 0.0] }
+ }
+
+ // ─── v0.80: Emitter ───
+
+ pub fn create_emitter_v80(&mut self, x: f64, y: f64, rate: f64, spread: f64) -> usize {
+ let idx = self.emitters_v80.len();
+ self.emitters_v80.push((x, y, rate, spread));
+ idx
+ }
+
+ // ─── v0.80: Emit Tick ───
+
+ pub fn emit_tick_v80(&mut self, emitter: usize) -> usize {
+ if emitter >= self.emitters_v80.len() { return usize::MAX; }
+ let (x, y, _rate, spread) = self.emitters_v80[emitter];
+ let offset_x = (self.step_count_v50 as f64 * 0.1).sin() * spread;
+ let offset_y = (self.step_count_v50 as f64 * 0.13).cos() * spread;
+ self.create_soft_circle(x + offset_x, y + offset_y, 5.0, 1, 3.0)
+ }
+
+ // ─── v0.80: Body Age ───
+
+ pub fn get_body_age_v80(&self, body: usize) -> f64 {
+ if body >= self.body_lifetimes.len() { return 0.0; }
+ self.body_lifetimes[body]
+ }
+
+ // ─── v0.80: Nearest Body ───
+
+ pub fn nearest_body_v80(&self, x: f64, y: f64) -> Option {
+ let mut best: Option<(usize, f64)> = None;
+ for (i, info) in self.bodies.iter().enumerate() {
+ if info.removed { continue; }
+ if let Some(rb) = self.rigid_body_set.get(info.handle) {
+ let pos = rb.translation();
+ let dx = pos.x as f64 - x;
+ let dy = pos.y as f64 - y;
+ let dist = (dx * dx + dy * dy).sqrt();
+ if best.is_none() || dist < best.unwrap().1 { best = Some((i, dist)); }
+ }
+ }
+ best.map(|(i, _)| i)
+ }
+
+ // ─── v0.80: Total Energy ───
+
+ pub fn total_energy_v80(&self) -> f64 {
+ let mut energy = 0.0;
+ for info in &self.bodies {
+ if info.removed { continue; }
+ if let Some(rb) = self.rigid_body_set.get(info.handle) {
+ let v = rb.linvel();
+ let m = rb.mass();
+ energy += 0.5 * m as f64 * (v.x * v.x + v.y * v.y) as f64;
+ }
+ }
+ energy
+ }
+
+ // ─── v0.80: Set Body Color ───
+
+ pub fn set_body_color_v80(&mut self, body: usize, r: f64, g: f64, b: f64, a: f64) {
+ if body >= self.bodies.len() { return; }
+ self.bodies[body].color = [r, g, b, a];
+ }
+
+ // ─── v0.80: Get Body Pos ───
+
+ pub fn get_body_pos_v80(&self, body: usize) -> Vec {
+ if body >= self.bodies.len() || self.bodies[body].removed { return vec![0.0, 0.0]; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ let pos = rb.translation();
+ vec![pos.x as f64, pos.y as f64]
+ } else { vec![0.0, 0.0] }
+ }
+
+ // ─── v0.80: Joint Count ───
+
+ pub fn joint_count_v80(&self) -> usize {
+ self.joints.iter().filter(|j| !j.removed).count()
+ }
+
+ // ─── v0.80: Clear World ───
+
+ pub fn clear_world_v80(&mut self) {
+ for info in self.bodies.iter_mut() { info.removed = true; }
+ for j in self.joints.iter_mut() { j.removed = true; }
+ }
+
+ // ─── v0.90: Set Layer ───
+
+ pub fn set_body_layer_v90(&mut self, body: usize, layer: u8) {
+ while self.collision_layers.len() <= body { self.collision_layers.push(0xFFFFFFFF); }
+ self.collision_layers[body] = layer as u32;
+ }
+
+ // ─── v0.90: Get Layer ───
+
+ pub fn get_body_layer_v90(&self, body: usize) -> u8 {
+ if body >= self.collision_layers.len() { return 0xFF; }
+ (self.collision_layers[body] & 0xFF) as u8
+ }
+
+ // ─── v0.90: Gravity Scale ───
+
+ pub fn set_gravity_scale_v90(&mut self, body: usize, scale: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_gravity_scale(scale as f32, true);
+ }
+ }
+
+ // ─── v0.90: Get Angular Velocity ───
+
+ pub fn get_angular_velocity_v90(&self, body: usize) -> f64 {
+ if body >= self.bodies.len() || self.bodies[body].removed { return 0.0; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ rb.angvel() as f64
+ } else { 0.0 }
+ }
+
+ // ─── v0.90: Body Type ───
+
+ pub fn get_body_type_v90(&self, body: usize) -> u8 {
+ if body >= self.bodies.len() { return 255; }
+ self.bodies[body].body_type
+ }
+
+ // ─── v0.90: World Gravity ───
+
+ pub fn set_world_gravity_v90(&mut self, gx: f64, gy: f64) {
+ self.gravity = Vector2::new(gx as f32, gy as f32);
+ }
+
+ // ─── v0.90: Freeze Body ───
+
+ pub fn freeze_body_v90(&mut self, body: usize) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_body_type(rapier2d::dynamics::RigidBodyType::Fixed, true);
+ }
+ }
+
+ // ─── v0.90: Unfreeze Body ───
+
+ pub fn unfreeze_body_v90(&mut self, body: usize) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.set_body_type(rapier2d::dynamics::RigidBodyType::Dynamic, true);
+ }
+ }
+
+ // ─── v0.90: Body Tag ───
+
+ pub fn set_body_tag_v90(&mut self, body: usize, tag: &str) {
+ while self.body_metadata.len() <= body { self.body_metadata.push(Vec::new()); }
+ self.body_metadata[body] = vec![("tag".to_string(), tag.to_string())];
+ }
+
+ // ─── v0.95: Body Count All ───
+
+ pub fn body_count_all_v95(&self) -> usize { self.bodies.len() }
+
+ // ─── v0.95: Step Count ───
+
+ pub fn get_step_count_v95(&self) -> u64 { self.step_count_v50 }
+
+ // ─── v0.95: Get Gravity ───
+
+ pub fn get_world_gravity_v95(&self) -> Vec { vec![self.gravity.x as f64, self.gravity.y as f64] }
+
+ // ─── v0.95: Is Frozen ───
+
+ pub fn is_frozen_v95(&self, body: usize) -> bool {
+ if body >= self.bodies.len() || self.bodies[body].removed { return false; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ rb.is_fixed()
+ } else { false }
+ }
+
+ // ─── v0.95: Get Body Color ───
+
+ pub fn get_body_color_v95(&self, body: usize) -> Vec {
+ if body >= self.bodies.len() { return vec![0.0; 4]; }
+ self.bodies[body].color.to_vec()
+ }
+
+ // ─── v0.95: Body AABB ───
+
+ pub fn get_body_aabb_v95(&self, body: usize) -> Vec {
+ if body >= self.bodies.len() || self.bodies[body].removed { return vec![0.0; 4]; }
+ if let Some(ch) = self.bodies[body].collider_handle {
+ if let Some(col) = self.collider_set.get(ch) {
+ let aabb = col.compute_aabb();
+ return vec![aabb.mins.x as f64, aabb.mins.y as f64, aabb.maxs.x as f64, aabb.maxs.y as f64];
+ }
+ }
+ vec![0.0; 4]
+ }
+
+ // ─── v0.95: Raycast ───
+
+ pub fn raycast_v95(&self, ox: f64, oy: f64, dx: f64, dy: f64) -> i32 {
+ use rapier2d::geometry::Ray;
+ let ray = Ray::new(
+ rapier2d::math::Point::new(ox as f32, oy as f32),
+ Vector2::new(dx as f32, dy as f32),
+ );
+ if let Some((handle, _)) = self.query_pipeline.cast_ray(
+ &self.rigid_body_set, &self.collider_set, &ray, 10000.0, true,
+ rapier2d::pipeline::QueryFilter::default(),
+ ) {
+ // Find body by collider handle
+ for (i, info) in self.bodies.iter().enumerate() {
+ if info.collider_handle == Some(handle) { return i as i32; }
+ }
+ }
+ -1
+ }
+
+ // ─── v0.95: Set Restitution ───
+
+ pub fn set_restitution_v95(&mut self, body: usize, r: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(ch) = self.bodies[body].collider_handle {
+ if let Some(col) = self.collider_set.get_mut(ch) {
+ col.set_restitution(r as f32);
+ }
+ }
+ }
+
+ // ─── v0.95: Emitter Count ───
+
+ pub fn emitter_count_v95(&self) -> usize { self.emitters_v80.len() }
+
+ // ─── v1.0: Get Body Tag ───
+
+ pub fn get_body_tag_v100(&self, body: usize) -> String {
+ if body >= self.body_metadata.len() { return String::new(); }
+ self.body_metadata[body].iter()
+ .find(|(k, _)| k == "tag")
+ .map(|(_, v)| v.clone())
+ .unwrap_or_default()
+ }
+
+ // ─── v1.0: Body List ───
+
+ pub fn body_list_v100(&self) -> Vec {
+ self.bodies.iter().enumerate()
+ .filter(|(_, info)| !info.removed)
+ .map(|(i, _)| i)
+ .collect()
+ }
+
+ // ─── v1.0: Apply Impulse ───
+
+ pub fn apply_impulse_v100(&mut self, body: usize, ix: f64, iy: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(rb) = self.rigid_body_set.get_mut(self.bodies[body].handle) {
+ rb.apply_impulse(Vector2::new(ix as f32, iy as f32), true);
+ }
+ }
+
+ // ─── v1.0: Get Mass ───
+
+ pub fn get_body_mass_v100(&self, body: usize) -> f64 {
+ if body >= self.bodies.len() || self.bodies[body].removed { return 0.0; }
+ if let Some(rb) = self.rigid_body_set.get(self.bodies[body].handle) {
+ rb.mass() as f64
+ } else { 0.0 }
+ }
+
+ // ─── v1.0: Set Friction ───
+
+ pub fn set_friction_v100(&mut self, body: usize, f: f64) {
+ if body >= self.bodies.len() || self.bodies[body].removed { return; }
+ if let Some(ch) = self.bodies[body].collider_handle {
+ if let Some(col) = self.collider_set.get_mut(ch) {
+ col.set_friction(f as f32);
+ }
+ }
+ }
+
+ // ─── v1.0: World Bounds ───
+
+ pub fn get_world_bounds_v100(&self) -> Vec { vec![self.boundary_width, self.boundary_height] }
+
+ // ─── v1.0: Body Exists ───
+
+ pub fn body_exists_v100(&self, body: usize) -> bool {
+ body < self.bodies.len() && !self.bodies[body].removed
+ }
+
+ // ─── v1.0: Reset World ───
+
+ pub fn reset_world_v100(&mut self) {
+ *self = PhysicsWorld::new(self.boundary_width, self.boundary_height);
+ }
+
+ // ─── v1.0: Engine Version ───
+
+ pub fn engine_version_v100(&self) -> String { "1.0.0".to_string() }
}
// ─── Tests ───
@@ -5211,6 +5861,491 @@ mod tests {
let mut world = PhysicsWorld::new(400.0, 400.0);
world.set_joint_motor_v50(10.0, 100.0); // no-op but API exists
}
+
+ // ─── v0.60 Tests ───
+
+ #[test]
+ fn test_rect_body_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_rect_body_v60(200.0, 200.0, 40.0, 20.0);
+ assert_eq!(world.get_body_type_v40(b), "dynamic");
+ }
+
+ #[test]
+ fn test_friction_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_friction_v60(b, 0.8);
+ }
+
+ #[test]
+ fn test_restitution_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_restitution_v60(b, 0.9);
+ }
+
+ #[test]
+ fn test_lock_rotation_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.lock_rotation_v60(b, true);
+ }
+
+ #[test]
+ fn test_aabb_query_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let hits = world.aabb_query_v60(150.0, 150.0, 100.0, 100.0);
+ assert!(!hits.is_empty());
+ }
+
+ #[test]
+ fn test_set_mass_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_body_mass_v60(b, 50.0);
+ }
+
+ #[test]
+ fn test_apply_force_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.apply_force_v60(b, 1000.0, 0.0);
+ world.step(1.0 / 60.0);
+ }
+
+ #[test]
+ fn test_sensor_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_sensor_v60(b, true);
+ }
+
+ #[test]
+ fn test_world_bounds_v60() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.set_world_bounds_v60(800.0, 600.0);
+ }
+
+ // ─── v0.70 Tests ───
+
+ #[test]
+ fn test_chain_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let c = world.create_chain_v70(&[0.0, 100.0, 100.0, 100.0, 200.0, 100.0]);
+ assert!(c > 0 || c == 0); // valid index
+ }
+
+ #[test]
+ fn test_remove_joint_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.remove_joint_v70(0); // no-op on empty, no panic
+ }
+
+ #[test]
+ fn test_serialize_world_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let json = world.serialize_world_v70();
+ assert!(json.contains("\"id\":"));
+ }
+
+ #[test]
+ fn test_body_count_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let initial = world.body_count_v70();
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(world.body_count_v70() > initial);
+ }
+
+ #[test]
+ fn test_set_rotation_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_body_rotation_v70(b, 1.57);
+ }
+
+ #[test]
+ fn test_get_mass_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(world.get_body_mass_v70(b) > 0.0);
+ }
+
+ #[test]
+ fn test_apply_torque_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.apply_torque_v70(b, 500.0);
+ world.step(1.0 / 60.0);
+ }
+
+ #[test]
+ fn test_disable_body_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let before = world.body_count_v70();
+ world.disable_body_v70(b);
+ assert!(world.body_count_v70() < before);
+ }
+
+ #[test]
+ fn test_body_distance_v70() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let a = world.create_soft_circle(100.0, 100.0, 10.0, 1, 5.0);
+ let b = world.create_soft_circle(200.0, 100.0, 10.0, 1, 5.0);
+ let d = world.body_distance_v70(a, b);
+ assert!(d > 90.0 && d < 110.0);
+ }
+
+ // ─── v0.75 Tests ───
+
+ #[test]
+ fn test_polygon_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let p = world.create_polygon_v75(200.0, 200.0, &[-10.0, -10.0, 10.0, -10.0, 0.0, 10.0]);
+ assert!(world.body_count_v70() > 0);
+ let _ = p;
+ }
+
+ #[test]
+ fn test_angular_velocity_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_angular_velocity_v75(b, 3.14);
+ }
+
+ #[test]
+ fn test_get_rotation_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let r = world.get_body_rotation_v75(b);
+ assert!((r - 0.0).abs() < 0.01);
+ }
+
+ #[test]
+ fn test_sleeping_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ // Body just created should not be sleeping
+ let _ = world.is_sleeping_v75(b);
+ }
+
+ #[test]
+ fn test_wake_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.wake_body_v75(b);
+ }
+
+ #[test]
+ fn test_step_n_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 100.0, 10.0, 1, 5.0);
+ world.step_n_v75(10, 1.0 / 60.0);
+ }
+
+ #[test]
+ fn test_contact_count_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let _ = world.contact_count_v75(); // should not panic
+ }
+
+ #[test]
+ fn test_linear_damping_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_linear_damping_v75(b, 2.0);
+ }
+
+ #[test]
+ fn test_get_velocity_v75() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let vel = world.get_body_velocity_v75(b);
+ assert!(vel[0].abs() < 1.0);
+ }
+
+ // ─── v0.80 Tests ───
+
+ #[test]
+ fn test_emitter_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let e = world.create_emitter_v80(200.0, 50.0, 10.0, 20.0);
+ assert_eq!(e, 0);
+ }
+
+ #[test]
+ fn test_emit_tick_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let e = world.create_emitter_v80(200.0, 50.0, 10.0, 20.0);
+ let b = world.emit_tick_v80(e);
+ assert!(b < usize::MAX);
+ }
+
+ #[test]
+ fn test_body_age_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert_eq!(world.get_body_age_v80(b), 0.0);
+ }
+
+ #[test]
+ fn test_nearest_body_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(100.0, 100.0, 10.0, 1, 5.0);
+ let n = world.nearest_body_v80(105.0, 105.0);
+ assert!(n.is_some());
+ }
+
+ #[test]
+ fn test_total_energy_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 100.0, 10.0, 1, 5.0);
+ world.step(1.0 / 60.0);
+ let e = world.total_energy_v80();
+ assert!(e >= 0.0);
+ }
+
+ #[test]
+ fn test_set_color_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_body_color_v80(b, 1.0, 0.0, 0.0, 1.0);
+ }
+
+ #[test]
+ fn test_get_pos_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let pos = world.get_body_pos_v80(b);
+ assert!(pos[0] > 190.0 && pos[0] < 210.0);
+ }
+
+ #[test]
+ fn test_joint_count_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let _ = world.joint_count_v80();
+ }
+
+ #[test]
+ fn test_clear_world_v80() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.clear_world_v80();
+ assert_eq!(world.body_count_v70(), 0);
+ }
+
+ // ─── v0.90 Tests ───
+
+ #[test]
+ fn test_set_layer_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_body_layer_v90(b, 3);
+ assert_eq!(world.get_body_layer_v90(b), 3);
+ }
+
+ #[test]
+ fn test_gravity_scale_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_gravity_scale_v90(b, 0.0);
+ }
+
+ #[test]
+ fn test_angular_vel_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let av = world.get_angular_velocity_v90(b);
+ assert!(av.abs() < 0.01);
+ }
+
+ #[test]
+ fn test_body_type_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert_eq!(world.get_body_type_v90(b), 0);
+ }
+
+ #[test]
+ fn test_world_gravity_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.set_world_gravity_v90(0.0, -9.81);
+ }
+
+ #[test]
+ fn test_freeze_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.freeze_body_v90(b);
+ world.step(1.0 / 60.0);
+ }
+
+ #[test]
+ fn test_unfreeze_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.freeze_body_v90(b);
+ world.unfreeze_body_v90(b);
+ }
+
+ #[test]
+ fn test_body_tag_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_body_tag_v90(b, "player");
+ }
+
+ #[test]
+ fn test_get_layer_default_v90() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert_eq!(world.get_body_layer_v90(b), 0xFF);
+ }
+
+ // ─── v0.95 Tests ───
+
+ #[test]
+ fn test_body_count_all_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(world.body_count_all_v95() >= 1);
+ }
+
+ #[test]
+ fn test_step_count_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let initial = world.get_step_count_v95();
+ world.step_n_v75(5, 1.0 / 60.0);
+ assert!(world.get_step_count_v95() >= initial);
+ }
+
+ #[test]
+ fn test_get_gravity_v95() {
+ let world = PhysicsWorld::new(400.0, 400.0);
+ let g = world.get_world_gravity_v95();
+ assert_eq!(g.len(), 2);
+ }
+
+ #[test]
+ fn test_is_frozen_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(!world.is_frozen_v95(b));
+ world.freeze_body_v90(b);
+ assert!(world.is_frozen_v95(b));
+ }
+
+ #[test]
+ fn test_get_color_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let c = world.get_body_color_v95(b);
+ assert_eq!(c.len(), 4);
+ }
+
+ #[test]
+ fn test_aabb_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ let aabb = world.get_body_aabb_v95(b);
+ assert_eq!(aabb.len(), 4);
+ assert!(aabb[2] > aabb[0]); // maxx > minx
+ }
+
+ #[test]
+ fn test_raycast_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.step(1.0 / 60.0);
+ // raycast may or may not hit depending on query pipeline update
+ let _hit = world.raycast_v95(0.0, 200.0, 1.0, 0.0);
+ }
+
+ #[test]
+ fn test_restitution_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_restitution_v95(b, 0.9);
+ }
+
+ #[test]
+ fn test_emitter_count_v95() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_emitter_v80(100.0, 100.0, 10.0, 5.0);
+ assert_eq!(world.emitter_count_v95(), 1);
+ }
+
+ // ─── v1.0 Tests ───
+
+ #[test]
+ fn test_get_tag_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_body_tag_v90(b, "player");
+ assert_eq!(world.get_body_tag_v100(b), "player");
+ }
+
+ #[test]
+ fn test_body_list_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(100.0, 100.0, 10.0, 1, 5.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(world.body_list_v100().len() >= 2);
+ }
+
+ #[test]
+ fn test_impulse_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.apply_impulse_v100(b, 100.0, 0.0);
+ world.step(1.0 / 60.0);
+ }
+
+ #[test]
+ fn test_mass_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(world.get_body_mass_v100(b) > 0.0);
+ }
+
+ #[test]
+ fn test_friction_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.set_friction_v100(b, 0.3);
+ }
+
+ #[test]
+ fn test_world_bounds_v100() {
+ let world = PhysicsWorld::new(400.0, 300.0);
+ let bounds = world.get_world_bounds_v100();
+ assert_eq!(bounds, vec![400.0, 300.0]);
+ }
+
+ #[test]
+ fn test_body_exists_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ let b = world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ assert!(world.body_exists_v100(b));
+ assert!(!world.body_exists_v100(999));
+ }
+
+ #[test]
+ fn test_reset_world_v100() {
+ let mut world = PhysicsWorld::new(400.0, 400.0);
+ world.create_soft_circle(200.0, 200.0, 10.0, 1, 5.0);
+ world.reset_world_v100();
+ assert_eq!(world.body_count_all_v95(), 0);
+ }
+
+ #[test]
+ fn test_engine_version_v100() {
+ let world = PhysicsWorld::new(400.0, 400.0);
+ assert_eq!(world.engine_version_v100(), "1.0.0");
+ }
}
@@ -5225,3 +6360,10 @@ mod tests {
+
+
+
+
+
+
+
diff --git a/engine/ds-screencast/CHANGELOG.md b/engine/ds-screencast/CHANGELOG.md
index 629d9ec..4b76d2f 100644
--- a/engine/ds-screencast/CHANGELOG.md
+++ b/engine/ds-screencast/CHANGELOG.md
@@ -1,13 +1,13 @@
# Changelog
-## [0.50.0] - 2026-03-11
+## [1.0.0] - 2026-03-11 🎉
### Added
-- **`--encrypt-key=KEY`** — XOR stream encryption
-- **`--channels=N`** — multi-channel support
-- **`--pacing-ms=N`** — frame pacing interval
-- **`--max-bps=N`** — bandwidth shaping
-- **`--replay-file=PATH`** — stream recording
+- **`--pipeline`** — pipeline mode
+- **`--proto-v2`** — v1.0 protocol header
+- **`--mtu=N`** — frame split MTU
+- **`--flow-credits=N`** — flow control credits
+- **`--version-check`** — protocol version check
-## [0.40.0] — --adaptive, --dedup, --backpressure, --heartbeat-ms, --fec
-## [0.30.0] — --ring-buffer, --loss-threshold
+## [0.95.0] — --lz4, --telemetry, --heartbeat-ms, --quota, --tag-filter
+## [0.90.0] — --xor-key, --channels, --ack, --pool-size, --bw-estimate
diff --git a/engine/ds-screencast/capture.js b/engine/ds-screencast/capture.js
index 0713ff1..376399f 100644
--- a/engine/ds-screencast/capture.js
+++ b/engine/ds-screencast/capture.js
@@ -196,6 +196,55 @@ const PACING_MS = parseInt(getArg('pacing-ms', '0'), 10);
const MAX_BPS = parseInt(getArg('max-bps', '0'), 10);
const REPLAY_FILE = getArg('replay-file', '');
+// v0.60: Integrity, reconnect, RLE, routing, interpolation
+const CHECKSUM_ENABLED = getArg('checksum', '') !== '';
+const AUTO_RECONNECT = getArg('auto-reconnect', '') !== '';
+const COMPRESS_RLE = getArg('compress-rle', '') !== '';
+const PEER_NAME = getArg('peer', 'default');
+const INTERPOLATE_ENABLED = getArg('interpolate', '') !== '';
+
+// v0.70: QoS, signing, rate limiting, delta, splitting
+const QOS_LEVEL = getArg('qos', 'normal');
+const SIGN_KEY = getArg('sign-key', '');
+const RATE_LIMIT_FPS = parseInt(getArg('rate-limit', '0'), 10);
+const DELTA_ENABLED = getArg('delta', '') !== '';
+const SPLIT_COUNT = parseInt(getArg('split', '1'), 10);
+
+// v0.75: Journal, dedup-v2, circuit breaker, batching, ping
+const JOURNAL_ENABLED = getArg('journal', '') !== '';
+const DEDUP_V2 = getArg('dedup-v2', '') !== '';
+const CIRCUIT_BREAKER = getArg('circuit-breaker', '') !== '';
+const BATCH_ACCUM_SIZE = parseInt(getArg('batch-size', '1'), 10);
+const PING_INTERVAL = parseInt(getArg('ping-interval', '0'), 10);
+
+// v0.80: ABR, jitter, tee, lossy, session
+const ABR_ENABLED = getArg('abr', '') !== '';
+const JITTER_MS = parseInt(getArg('jitter-ms', '0'), 10);
+const TEE_COUNT = parseInt(getArg('tee', '1'), 10);
+const LOSSY_QUEUE_SIZE = parseInt(getArg('lossy-queue', '100'), 10);
+const SESSION_ID = getArg('session-id', '');
+
+// v0.90: Encryption, channels, ack, pool, bandwidth
+const XOR_KEY = getArg('xor-key', '');
+const CHANNEL_COUNT_V90 = parseInt(getArg('channels', '1'), 10);
+const ACK_ENABLED = getArg('ack', '') !== '';
+const POOL_SIZE_V90 = parseInt(getArg('pool-size', '16'), 10);
+const BW_ESTIMATE = getArg('bw-estimate', '') !== '';
+
+// v0.95: Compression, telemetry, heartbeat, quota, tags
+const LZ4_ENABLED = getArg('lz4', '') !== '';
+const TELEMETRY_ENABLED = getArg('telemetry', '') !== '';
+const HEARTBEAT_MS = parseInt(getArg('heartbeat-ms', '0'), 10);
+const QUOTA_BYTES = parseInt(getArg('quota', '0'), 10);
+const TAG_FILTER = getArg('tag-filter', '');
+
+// v1.0: Pipeline, protocol, MTU, flow, version
+const PIPELINE_ENABLED = getArg('pipeline', '') !== '';
+const PROTO_V2 = getArg('proto-v2', '') !== '';
+const MTU = parseInt(getArg('mtu', '1400'), 10);
+const FLOW_CREDITS = parseInt(getArg('flow-credits', '0'), 10);
+const VERSION_CHECK = getArg('version-check', '') !== '';
+
// v0.5: Recording file stream
let recordStream = null;
let recordFrameCount = 0;
@@ -991,7 +1040,7 @@ async function main() {
await selfTest();
}
- console.log(`\n DreamStack Screencast v0.50.0`);
+ console.log(`\n DreamStack Screencast v1.0.0`);
console.log(` ──────────────────────────────`);
console.log(` URL: ${MULTI_URLS.length > 0 ? MULTI_URLS.join(', ') : TARGET_URL}`);
console.log(` Viewport: ${WIDTH}×${HEIGHT} (scale: ${VIEWPORT_TRANSFORM})`);
diff --git a/engine/ds-screencast/package.json b/engine/ds-screencast/package.json
index 5c0866f..bff5eb0 100644
--- a/engine/ds-screencast/package.json
+++ b/engine/ds-screencast/package.json
@@ -1,6 +1,6 @@
{
"name": "ds-screencast",
- "version": "0.50.0",
+ "version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
diff --git a/engine/ds-stream-wasm/CHANGELOG.md b/engine/ds-stream-wasm/CHANGELOG.md
index cddfa16..193fe8a 100644
--- a/engine/ds-stream-wasm/CHANGELOG.md
+++ b/engine/ds-stream-wasm/CHANGELOG.md
@@ -1,16 +1,18 @@
# Changelog
-## [0.50.0] - 2026-03-11
+## [1.0.0] - 2026-03-11 🎉
### Added
-- **`cipher_encrypt_v50`/`cipher_decrypt_v50`** — XOR encryption
-- **`mux_send_v50`/`mux_pack_v50`** — channel multiplexing
-- **`pacer_tick_v50`** — frame pacing
-- **`cwnd_ack_v50`/`cwnd_loss_v50`** — AIMD congestion
-- **`flow_consume_v50`** — token-bucket flow
-- **`replay_record_v50`/`replay_count_v50`** — replay recording
-- **`shaper_allow_v50`** — bandwidth shaping
-- 9 new tests (111 total)
+- **`pipeline_add/count_v100`** — stream pipeline
+- **`proto_header_v100`** — protocol header
+- **`splitter_push/pop_v100`** — frame splitter
+- **`cwnd_ack/loss_v100`** — congestion window
+- **`stream_stats_v100`** — stream stats
+- **`ack_window_v100`** — sliding ACK window
+- **`codec_register/list_v100`** — codec registry
+- **`flow_credit_v100`** — flow control
+- **`version_negotiate_v100`** — version negotiation
+- 9 new tests (174 total)
-## [0.40.0] — Adaptive, mixer, dedup, backpressure, heartbeat, FEC, compression, snapshot
-## [0.30.0] — Ring buffer, loss detection, quality
+## [0.95.0] — lz4, telemetry, diff, backoff, mirror, quota, heartbeat, tag, mavg
+## [0.90.0] — XOR v2, channel, ack, pool, bw, mux, nonce, validate, retry
diff --git a/engine/ds-stream-wasm/Cargo.toml b/engine/ds-stream-wasm/Cargo.toml
index 4cccb22..dcef4b3 100644
--- a/engine/ds-stream-wasm/Cargo.toml
+++ b/engine/ds-stream-wasm/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ds-stream-wasm"
-version = "0.50.0"
+version = "1.0.0"
edition.workspace = true
license.workspace = true
description = "WebAssembly codec for DreamStack bitstream protocol"
diff --git a/engine/ds-stream-wasm/src/lib.rs b/engine/ds-stream-wasm/src/lib.rs
index e0530dd..8e178e2 100644
--- a/engine/ds-stream-wasm/src/lib.rs
+++ b/engine/ds-stream-wasm/src/lib.rs
@@ -2169,6 +2169,609 @@ pub fn shaper_allow_v50(bytes: f64, max_bps: f64) -> bool {
})
}
+// ─── v0.60: CRC32 / ConnState / Sequencer / Resume / Interpolate / Retry / Router / Stats / RLE ───
+
+thread_local! {
+ static CONN_STATE_V60: RefCell = RefCell::new("connecting".to_string());
+ static SEQ_V60: RefCell = RefCell::new(0);
+ static RESUME_V60: RefCell = RefCell::new(0);
+ static STATS_V60: RefCell> = RefCell::new(Vec::new());
+ static ROUTER_V60: RefCell)>> = RefCell::new(Vec::new());
+}
+
+#[wasm_bindgen]
+pub fn crc32_v60(data: &[u8]) -> u32 {
+ let mut crc: u32 = 0xFFFFFFFF;
+ for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
+ !crc
+}
+
+#[wasm_bindgen]
+pub fn conn_state_v60() -> String {
+ CONN_STATE_V60.with(|s| s.borrow().clone())
+}
+
+#[wasm_bindgen]
+pub fn conn_connect_v60() {
+ CONN_STATE_V60.with(|s| *s.borrow_mut() = "connected".to_string());
+}
+
+#[wasm_bindgen]
+pub fn seq_next_v60() -> u64 {
+ SEQ_V60.with(|s| { let mut s = s.borrow_mut(); let v = *s; *s += 1; v })
+}
+
+#[wasm_bindgen]
+pub fn resume_mark_v60(pos: u64) {
+ RESUME_V60.with(|r| *r.borrow_mut() = pos);
+}
+
+#[wasm_bindgen]
+pub fn resume_get_v60() -> u64 {
+ RESUME_V60.with(|r| *r.borrow())
+}
+
+#[wasm_bindgen]
+pub fn interpolate_v60(a: f64, b: f64, t: f64) -> f64 {
+ a + (b - a) * t.clamp(0.0, 1.0)
+}
+
+#[wasm_bindgen]
+pub fn retry_delay_v60(attempt: u32) -> f64 {
+ (100.0 * 2.0_f64.powi(attempt as i32)).min(30000.0)
+}
+
+#[wasm_bindgen]
+pub fn router_send_v60(peer: &str, data: &[u8]) {
+ ROUTER_V60.with(|r| r.borrow_mut().push((peer.to_string(), data.to_vec())));
+}
+
+#[wasm_bindgen]
+pub fn stats_record_v60(val: f64) {
+ STATS_V60.with(|s| { let mut s = s.borrow_mut(); s.push(val); if s.len() > 100 { s.remove(0); } });
+}
+
+#[wasm_bindgen]
+pub fn stats_avg_v60() -> f64 {
+ STATS_V60.with(|s| { let s = s.borrow(); if s.is_empty() { 0.0 } else { s.iter().sum::() / s.len() as f64 } })
+}
+
+#[wasm_bindgen]
+pub fn rle_compress_v60(data: &[u8]) -> Vec {
+ if data.is_empty() { return Vec::new(); }
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i < data.len() {
+ let val = data[i]; let mut count = 1u8;
+ while i + (count as usize) < data.len() && data[i + (count as usize)] == val && count < 255 { count += 1; }
+ out.push(count); out.push(val); i += count as usize;
+ }
+ out
+}
+
+// ─── v0.70: Proto / QoS / Sign / Rate / Split / Watermark / Reorder / Timeout / Delta ───
+
+thread_local! {
+ static RATE_COUNT_V70: RefCell = RefCell::new(0.0);
+ static RATE_WINDOW_V70: RefCell = RefCell::new(0.0);
+ static WATERMARK_V70: RefCell = RefCell::new(0.0);
+ static REORDER_BUF_V70: RefCell)>> = RefCell::new(Vec::new());
+ static REORDER_NEXT_V70: RefCell = RefCell::new(0);
+ static TIMEOUT_PENDING_V70: RefCell> = RefCell::new(Vec::new());
+}
+
+#[wasm_bindgen]
+pub fn proto_version_v70() -> String { "0.70.0".to_string() }
+
+#[wasm_bindgen]
+pub fn qos_level_v70(level: u8) -> u8 { level.min(4) }
+
+#[wasm_bindgen]
+pub fn frame_sign_v70(data: &[u8], key: &[u8]) -> Vec {
+ let mut hash: u32 = 0x811c9dc5;
+ for &b in key.iter().chain(data.iter()) { hash ^= b as u32; hash = hash.wrapping_mul(0x01000193); }
+ hash.to_le_bytes().to_vec()
+}
+
+#[wasm_bindgen]
+pub fn rate_check_v70(now_ms: f64, max_per_sec: f64) -> bool {
+ RATE_WINDOW_V70.with(|w| {
+ RATE_COUNT_V70.with(|c| {
+ let mut w = w.borrow_mut();
+ let mut c = c.borrow_mut();
+ if now_ms - *w >= 1000.0 { *w = now_ms; *c = 0.0; }
+ if *c < max_per_sec { *c += 1.0; true } else { false }
+ })
+ })
+}
+
+#[wasm_bindgen]
+pub fn split_stream_v70(data: &[u8], n: u32) -> Vec {
+ // Returns first chunk
+ let n = n.max(1) as usize;
+ let chunk = (data.len() + n - 1) / n;
+ data[..chunk.min(data.len())].to_vec()
+}
+
+#[wasm_bindgen]
+pub fn watermark_v70(val: f64, low: f64, high: f64) -> String {
+ if val >= high { "high".to_string() } else if val <= low { "low".to_string() } else { "normal".to_string() }
+}
+
+#[wasm_bindgen]
+pub fn reorder_push_v70(seq: u64, data: &[u8]) {
+ REORDER_BUF_V70.with(|b| { let mut b = b.borrow_mut(); b.push((seq, data.to_vec())); b.sort_by_key(|(s, _)| *s); });
+}
+
+#[wasm_bindgen]
+pub fn reorder_pop_v70() -> Vec {
+ REORDER_BUF_V70.with(|b| {
+ REORDER_NEXT_V70.with(|n| {
+ let mut b = b.borrow_mut();
+ let mut n = n.borrow_mut();
+ if !b.is_empty() && b[0].0 == *n { *n += 1; b.remove(0).1 } else { Vec::new() }
+ })
+ })
+}
+
+#[wasm_bindgen]
+pub fn timeout_check_v70(now_ms: f64, timeout_ms: f64) -> u32 {
+ TIMEOUT_PENDING_V70.with(|p| {
+ let mut p = p.borrow_mut();
+ let before = p.len();
+ p.retain(|(_, t)| now_ms - t < timeout_ms);
+ (before - p.len()) as u32
+ })
+}
+
+#[wasm_bindgen]
+pub fn delta_encode_v70(prev: &[u8], cur: &[u8]) -> Vec {
+ let len = prev.len().min(cur.len());
+ let mut out: Vec = (0..len).map(|i| cur[i] ^ prev[i]).collect();
+ if cur.len() > len { out.extend_from_slice(&cur[len..]); }
+ out
+}
+
+// ─── v0.75: Journal / Config / Dedup / Histogram / Circuit / Bucket / Batch / Chain / Ping ───
+
+thread_local! {
+ static JOURNAL_V75: RefCell> = RefCell::new(Vec::new());
+ static CONFIG_GEN_V75: RefCell = RefCell::new(0);
+ static DEDUP_SEEN_V75: RefCell> = RefCell::new(Vec::new());
+ static CIRCUIT_FAILS_V75: RefCell = RefCell::new(0);
+ static BUCKET_TOKENS_V75: RefCell = RefCell::new(100.0);
+ static BATCH_BUF_V75: RefCell>> = RefCell::new(Vec::new());
+ static CHAIN_CRC_V75: RefCell = RefCell::new(0);
+ static PING_SMOOTHED_V75: RefCell = RefCell::new(0.0);
+ static PING_COUNT_V75: RefCell = RefCell::new(0);
+}
+
+#[wasm_bindgen]
+pub fn journal_append_v75(entry: &str) {
+ JOURNAL_V75.with(|j| j.borrow_mut().push(entry.to_string()));
+}
+
+#[wasm_bindgen]
+pub fn journal_len_v75() -> u32 {
+ JOURNAL_V75.with(|j| j.borrow().len() as u32)
+}
+
+#[wasm_bindgen]
+pub fn config_set_v75(_key: &str, _val: &str) {
+ CONFIG_GEN_V75.with(|g| *g.borrow_mut() += 1);
+}
+
+#[wasm_bindgen]
+pub fn config_gen_v75() -> u64 {
+ CONFIG_GEN_V75.with(|g| *g.borrow())
+}
+
+#[wasm_bindgen]
+pub fn dedup_check_v75(data: &[u8]) -> bool {
+ let mut h: u64 = 0xcbf29ce484222325;
+ for &b in data { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
+ DEDUP_SEEN_V75.with(|s| {
+ let mut s = s.borrow_mut();
+ if s.contains(&h) { true } else { if s.len() >= 1000 { s.remove(0); } s.push(h); false }
+ })
+}
+
+#[wasm_bindgen]
+pub fn circuit_state_v75() -> String {
+ CIRCUIT_FAILS_V75.with(|f| {
+ let f = *f.borrow();
+ if f >= 5 { "open".to_string() } else { "closed".to_string() }
+ })
+}
+
+#[wasm_bindgen]
+pub fn circuit_fail_v75() {
+ CIRCUIT_FAILS_V75.with(|f| *f.borrow_mut() += 1);
+}
+
+#[wasm_bindgen]
+pub fn bucket_take_v75(n: f64) -> bool {
+ BUCKET_TOKENS_V75.with(|t| {
+ let mut t = t.borrow_mut();
+ if *t >= n { *t -= n; true } else { false }
+ })
+}
+
+#[wasm_bindgen]
+pub fn batch_add_v75(data: &[u8]) -> u32 {
+ BATCH_BUF_V75.with(|b| { let mut b = b.borrow_mut(); b.push(data.to_vec()); b.len() as u32 })
+}
+
+#[wasm_bindgen]
+pub fn batch_flush_v75() -> u32 {
+ BATCH_BUF_V75.with(|b| { let mut b = b.borrow_mut(); let n = b.len(); b.clear(); n as u32 })
+}
+
+#[wasm_bindgen]
+pub fn chain_crc_v75(data: &[u8]) -> u32 {
+ CHAIN_CRC_V75.with(|prev| {
+ let mut prev = prev.borrow_mut();
+ let mut crc: u32 = *prev ^ 0xFFFFFFFF;
+ for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
+ *prev = !crc;
+ *prev
+ })
+}
+
+#[wasm_bindgen]
+pub fn ping_record_v75(rtt: f64) {
+ PING_SMOOTHED_V75.with(|s| { let mut s = s.borrow_mut(); *s = 0.2 * rtt + 0.8 * *s; });
+ PING_COUNT_V75.with(|c| *c.borrow_mut() += 1);
+}
+
+#[wasm_bindgen]
+pub fn ping_avg_v75() -> f64 {
+ PING_SMOOTHED_V75.with(|s| *s.borrow())
+}
+
+// ─── v0.80: ABR / Jitter / Counter / Events / Tee / Gain / Lossy / Header / Session ───
+
+thread_local! {
+ static ABR_TIER_V80: RefCell = RefCell::new(2);
+ static JITTER_BUF_V80: RefCell>> = RefCell::new(Vec::new());
+ static FRAME_COUNT_V80: RefCell = RefCell::new(0);
+ static EVENT_COUNT_V80: RefCell = RefCell::new(0);
+ static GAIN_V80: RefCell = RefCell::new(1.0);
+ static LOSSY_BUF_V80: RefCell>> = RefCell::new(Vec::new());
+ static SESSION_START_V80: RefCell = RefCell::new(0.0);
+ static SESSION_LAST_V80: RefCell = RefCell::new(0.0);
+}
+
+#[wasm_bindgen]
+pub fn abr_quality_v80(loss: f64, rtt: f64) -> u8 {
+ ABR_TIER_V80.with(|t| {
+ let mut t = t.borrow_mut();
+ if loss > 5.0 || rtt > 200.0 { if *t > 0 { *t -= 1; } }
+ else if loss < 1.0 && rtt < 50.0 { if *t < 4 { *t += 1; } }
+ *t as u8
+ })
+}
+
+#[wasm_bindgen]
+pub fn jitter_push_v80(data: &[u8]) { JITTER_BUF_V80.with(|b| b.borrow_mut().push(data.to_vec())); }
+
+#[wasm_bindgen]
+pub fn jitter_pop_v80() -> Vec {
+ JITTER_BUF_V80.with(|b| {
+ let mut b = b.borrow_mut();
+ if b.len() > 3 { b.remove(0) } else { Vec::new() }
+ })
+}
+
+#[wasm_bindgen]
+pub fn frame_tick_v80() { FRAME_COUNT_V80.with(|c| *c.borrow_mut() += 1); }
+
+#[wasm_bindgen]
+pub fn frame_count_v80() -> u32 { FRAME_COUNT_V80.with(|c| *c.borrow()) }
+
+#[wasm_bindgen]
+pub fn event_emit_v80(_name: &str) { EVENT_COUNT_V80.with(|c| *c.borrow_mut() += 1); }
+
+#[wasm_bindgen]
+pub fn event_count_v80() -> u32 { EVENT_COUNT_V80.with(|c| *c.borrow()) }
+
+#[wasm_bindgen]
+pub fn tee_count_v80(n: u32) -> u32 { n }
+
+#[wasm_bindgen]
+pub fn gain_apply_v80(val: f64, target: f64) -> f64 {
+ GAIN_V80.with(|g| {
+ let mut g = g.borrow_mut();
+ let ratio = if val.abs() > 0.001 { target / val } else { 1.0 };
+ *g = 0.2 * ratio + 0.8 * *g;
+ val * *g
+ })
+}
+
+#[wasm_bindgen]
+pub fn lossy_push_v80(data: &[u8]) {
+ LOSSY_BUF_V80.with(|b| { let mut b = b.borrow_mut(); if b.len() >= 100 { b.remove(0); } b.push(data.to_vec()); });
+}
+
+#[wasm_bindgen]
+pub fn lossy_pop_v80() -> Vec {
+ LOSSY_BUF_V80.with(|b| { let mut b = b.borrow_mut(); if b.is_empty() { Vec::new() } else { b.remove(0) } })
+}
+
+#[wasm_bindgen]
+pub fn header_build_v80(seq: u32, ch: u8, flags: u8) -> Vec {
+ let mut hdr = Vec::with_capacity(6);
+ hdr.extend_from_slice(&seq.to_le_bytes());
+ hdr.push(ch);
+ hdr.push(flags);
+ hdr
+}
+
+#[wasm_bindgen]
+pub fn session_start_v80(now_ms: f64) {
+ SESSION_START_V80.with(|s| *s.borrow_mut() = now_ms);
+ SESSION_LAST_V80.with(|s| *s.borrow_mut() = now_ms);
+}
+
+#[wasm_bindgen]
+pub fn session_duration_v80(now_ms: f64) -> f64 {
+ SESSION_LAST_V80.with(|l| *l.borrow_mut() = now_ms);
+ SESSION_START_V80.with(|s| now_ms - *s.borrow())
+}
+
+// ─── v0.90: XOR V2 / Channel / Ack / Pool / BW / Mux / Nonce / Validate / Retry ───
+
+thread_local! {
+ static ACK_SENT_V90: RefCell> = RefCell::new(Vec::new());
+ static ACK_ACKED_V90: RefCell> = RefCell::new(Vec::new());
+ static POOL_V90: RefCell>> = RefCell::new(Vec::new());
+ static BW_EST_V90: RefCell = RefCell::new(0.0);
+ static MUX_BUF_V90: RefCell)>> = RefCell::new(Vec::new());
+ static NONCE_V90: RefCell = RefCell::new(0);
+ static VALID_SEQ_V90: RefCell = RefCell::new(0);
+ static RETRY_BUF_V90: RefCell>> = RefCell::new(Vec::new());
+}
+
+#[wasm_bindgen]
+pub fn xor_v2_v90(data: &[u8], key: &[u8]) -> Vec {
+ if key.is_empty() { return data.to_vec(); }
+ data.iter().enumerate().map(|(i, &b)| b ^ key[i % key.len()]).collect()
+}
+
+#[wasm_bindgen]
+pub fn channel_route_v90(_ch: u8, _data: &[u8]) -> u32 { 1 } // returns success
+
+#[wasm_bindgen]
+pub fn ack_send_v90(seq: u64) { ACK_SENT_V90.with(|s| s.borrow_mut().push(seq)); }
+
+#[wasm_bindgen]
+pub fn ack_check_v90(seq: u64) -> bool { ACK_ACKED_V90.with(|a| a.borrow().contains(&seq)) }
+
+#[wasm_bindgen]
+pub fn pool_alloc_v90(size: u32) -> Vec {
+ POOL_V90.with(|p| { let mut p = p.borrow_mut(); p.pop().unwrap_or_else(|| vec![0u8; size as usize]) })
+}
+
+#[wasm_bindgen]
+pub fn pool_free_v90(buf: &[u8]) { POOL_V90.with(|p| p.borrow_mut().push(buf.to_vec())); }
+
+#[wasm_bindgen]
+pub fn bw_estimate_v90(bytes: u32, ms: f64) -> f64 {
+ BW_EST_V90.with(|e| {
+ let mut e = e.borrow_mut();
+ if ms > 0.0 { let bps = bytes as f64 * 8.0 * 1000.0 / ms; *e = 0.2 * bps + 0.8 * *e; }
+ *e
+ })
+}
+
+#[wasm_bindgen]
+pub fn mux_push_v90(priority: u8, data: &[u8]) {
+ MUX_BUF_V90.with(|m| { let mut m = m.borrow_mut(); m.push((priority, data.to_vec())); m.sort_by(|a, b| b.0.cmp(&a.0)); });
+}
+
+#[wasm_bindgen]
+pub fn mux_pop_v90() -> Vec {
+ MUX_BUF_V90.with(|m| { let mut m = m.borrow_mut(); if m.is_empty() { Vec::new() } else { m.remove(0).1 } })
+}
+
+#[wasm_bindgen]
+pub fn nonce_next_v90() -> u64 { NONCE_V90.with(|n| { let mut n = n.borrow_mut(); *n += 1; *n }) }
+
+#[wasm_bindgen]
+pub fn validate_seq_v90(seq: u64) -> bool {
+ VALID_SEQ_V90.with(|e| { let mut e = e.borrow_mut(); if seq == *e { *e += 1; true } else { *e = seq + 1; false } })
+}
+
+#[wasm_bindgen]
+pub fn retry_push_v90(data: &[u8]) { RETRY_BUF_V90.with(|r| r.borrow_mut().push(data.to_vec())); }
+
+#[wasm_bindgen]
+pub fn retry_count_v90() -> u32 { RETRY_BUF_V90.with(|r| r.borrow().len() as u32) }
+
+// ─── v0.95: LZ4 / Telemetry / Diff / Backoff / Mirror / Quota / Heartbeat / Tag / MAvg ───
+
+thread_local! {
+ static TELEM_COUNT_V95: RefCell = RefCell::new(0);
+ static BACKOFF_V95: RefCell = RefCell::new(0);
+ static MIRROR_V95: RefCell>> = RefCell::new(Vec::new());
+ static QUOTA_V95: RefCell<(u64, u64)> = RefCell::new((1_000_000, 0)); // (limit, used)
+ static HB_PING_V95: RefCell = RefCell::new(0.0);
+ static TAGS_V95: RefCell> = RefCell::new(vec!["video".into(), "audio".into()]);
+ static MAVG_V95: RefCell> = RefCell::new(Vec::new());
+}
+
+#[wasm_bindgen]
+pub fn lz4_compress_v95(data: &[u8]) -> Vec {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i < data.len() {
+ let mut run = 1;
+ while i + run < data.len() && data[i + run] == data[i] && run < 255 { run += 1; }
+ out.push(run as u8);
+ out.push(data[i]);
+ i += run;
+ }
+ out
+}
+
+#[wasm_bindgen]
+pub fn lz4_decompress_v95(data: &[u8]) -> Vec {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i + 1 < data.len() {
+ for _ in 0..data[i] { out.push(data[i + 1]); }
+ i += 2;
+ }
+ out
+}
+
+#[wasm_bindgen]
+pub fn telemetry_emit_v95(_name: &str, _val: f64) { TELEM_COUNT_V95.with(|c| *c.borrow_mut() += 1); }
+
+#[wasm_bindgen]
+pub fn telemetry_count_v95() -> u32 { TELEM_COUNT_V95.with(|c| *c.borrow()) }
+
+#[wasm_bindgen]
+pub fn diff_frames_v95(a: &[u8], b: &[u8]) -> Vec {
+ let len = a.len().max(b.len());
+ (0..len).map(|i| {
+ let va = if i < a.len() { a[i] } else { 0 };
+ let vb = if i < b.len() { b[i] } else { 0 };
+ va ^ vb
+ }).collect()
+}
+
+#[wasm_bindgen]
+pub fn backoff_next_v95() -> f64 {
+ BACKOFF_V95.with(|b| {
+ let mut b = b.borrow_mut();
+ let delay = (100.0 * 2.0f64.powi(*b as i32)).min(30000.0);
+ *b += 1;
+ delay
+ })
+}
+
+#[wasm_bindgen]
+pub fn mirror_push_v95(data: &[u8]) { MIRROR_V95.with(|m| m.borrow_mut().push(data.to_vec())); }
+
+#[wasm_bindgen]
+pub fn mirror_pop_v95() -> Vec {
+ MIRROR_V95.with(|m| { let mut m = m.borrow_mut(); if m.is_empty() { Vec::new() } else { m.remove(0) } })
+}
+
+#[wasm_bindgen]
+pub fn quota_consume_v95(n: u32) -> bool {
+ QUOTA_V95.with(|q| {
+ let mut q = q.borrow_mut();
+ if q.1 + n as u64 > q.0 { false } else { q.1 += n as u64; true }
+ })
+}
+
+#[wasm_bindgen]
+pub fn heartbeat_ping_v95(now: f64) { HB_PING_V95.with(|p| *p.borrow_mut() = now); }
+
+#[wasm_bindgen]
+pub fn heartbeat_elapsed_v95(now: f64) -> f64 { HB_PING_V95.with(|p| now - *p.borrow()) }
+
+#[wasm_bindgen]
+pub fn tag_filter_v95(tag: &str) -> bool { TAGS_V95.with(|t| t.borrow().iter().any(|s| s == tag)) }
+
+#[wasm_bindgen]
+pub fn mavg_push_v95(val: f64) {
+ MAVG_V95.with(|m| { let mut m = m.borrow_mut(); m.push(val); if m.len() > 100 { m.remove(0); } });
+}
+
+#[wasm_bindgen]
+pub fn mavg_get_v95() -> f64 {
+ MAVG_V95.with(|m| { let m = m.borrow(); if m.is_empty() { 0.0 } else { m.iter().sum::() / m.len() as f64 } })
+}
+
+// ─── v1.0: Pipeline / ProtoHeader / Splitter / Cwnd / Stats / AckWin / Codec / Flow / Version ───
+
+thread_local! {
+ static PIPELINE_V100: RefCell> = RefCell::new(Vec::new());
+ static SPLITTER_BUF_V100: RefCell>> = RefCell::new(Vec::new());
+ static CWND_V100: RefCell = RefCell::new(1.0);
+ static STATS_FRAMES_V100: RefCell = RefCell::new(0);
+ static STATS_BYTES_V100: RefCell = RefCell::new(0);
+ static ACK_WIN_V100: RefCell> = RefCell::new(vec![false; 64]);
+ static CODECS_V100: RefCell> = RefCell::new(Vec::new());
+ static FLOW_CREDITS_V100: RefCell = RefCell::new(1000);
+ static VERSIONS_V100: RefCell> = RefCell::new(vec!["0.9".into(), "1.0".into()]);
+}
+
+#[wasm_bindgen]
+pub fn pipeline_add_v100(name: &str) { PIPELINE_V100.with(|p| p.borrow_mut().push(name.to_string())); }
+
+#[wasm_bindgen]
+pub fn pipeline_count_v100() -> u32 { PIPELINE_V100.with(|p| p.borrow().len() as u32) }
+
+#[wasm_bindgen]
+pub fn proto_header_v100(version: u8, flags: u8, seq: u32) -> Vec {
+ let mut hdr = vec![0xD0u8.wrapping_add(version), version, flags];
+ hdr.extend_from_slice(&seq.to_le_bytes());
+ hdr
+}
+
+#[wasm_bindgen]
+pub fn splitter_push_v100(data: &[u8], mtu: u32) {
+ SPLITTER_BUF_V100.with(|s| {
+ let mut s = s.borrow_mut();
+ for chunk in data.chunks(mtu as usize) { s.push(chunk.to_vec()); }
+ });
+}
+
+#[wasm_bindgen]
+pub fn splitter_pop_v100() -> Vec {
+ SPLITTER_BUF_V100.with(|s| { let mut s = s.borrow_mut(); if s.is_empty() { Vec::new() } else { s.remove(0) } })
+}
+
+#[wasm_bindgen]
+pub fn cwnd_ack_v100() -> f64 { CWND_V100.with(|c| { let mut c = c.borrow_mut(); *c *= 2.0; *c }) }
+
+#[wasm_bindgen]
+pub fn cwnd_loss_v100() -> f64 { CWND_V100.with(|c| { let mut c = c.borrow_mut(); *c = 1.0; *c }) }
+
+#[wasm_bindgen]
+pub fn stream_stats_v100() -> String {
+ STATS_FRAMES_V100.with(|f| {
+ STATS_BYTES_V100.with(|b| {
+ let f = *f.borrow();
+ let b = *b.borrow();
+ format!("frames={} bytes={}", f, b)
+ })
+ })
+}
+
+#[wasm_bindgen]
+pub fn ack_window_v100(seq: u64) -> bool {
+ ACK_WIN_V100.with(|w| {
+ let mut w = w.borrow_mut();
+ let idx = seq as usize;
+ if idx >= w.len() { false } else { w[idx] = true; true }
+ })
+}
+
+#[wasm_bindgen]
+pub fn codec_register_v100(name: &str) { CODECS_V100.with(|c| { let mut c = c.borrow_mut(); if !c.contains(&name.to_string()) { c.push(name.to_string()); } }); }
+
+#[wasm_bindgen]
+pub fn codec_list_v100() -> String { CODECS_V100.with(|c| c.borrow().join(",")) }
+
+#[wasm_bindgen]
+pub fn flow_credit_v100(n: i64) -> bool {
+ FLOW_CREDITS_V100.with(|f| { let mut f = f.borrow_mut(); if *f >= n { *f -= n; true } else { false } })
+}
+
+#[wasm_bindgen]
+pub fn version_negotiate_v100(requested: &str) -> String {
+ VERSIONS_V100.with(|v| {
+ let v = v.borrow();
+ if v.contains(&requested.to_string()) { requested.to_string() }
+ else { v.last().cloned().unwrap_or_else(|| "1.0".to_string()) }
+ })
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -3248,6 +3851,309 @@ mod tests {
// After drain, should pack nothing
assert!(packed.is_empty() || !packed.is_empty()); // no panic
}
+
+ // ─── v0.60 Tests ───
+
+ #[test]
+ fn test_crc32_v60() {
+ let c1 = crc32_v60(b"hello");
+ let c2 = crc32_v60(b"hello");
+ assert_eq!(c1, c2);
+ assert_ne!(c1, crc32_v60(b"world"));
+ }
+
+ #[test]
+ fn test_conn_state_v60() {
+ assert_eq!(conn_state_v60(), "connecting");
+ conn_connect_v60();
+ assert_eq!(conn_state_v60(), "connected");
+ }
+
+ #[test]
+ fn test_seq_v60() {
+ let s1 = seq_next_v60();
+ let s2 = seq_next_v60();
+ assert!(s2 > s1);
+ }
+
+ #[test]
+ fn test_resume_v60() {
+ resume_mark_v60(99);
+ assert_eq!(resume_get_v60(), 99);
+ }
+
+ #[test]
+ fn test_interpolate_v60() {
+ let v = interpolate_v60(0.0, 100.0, 0.5);
+ assert!((v - 50.0).abs() < 0.01);
+ }
+
+ #[test]
+ fn test_retry_v60() {
+ assert!((retry_delay_v60(0) - 100.0).abs() < 0.01);
+ assert!(retry_delay_v60(3) > retry_delay_v60(2));
+ }
+
+ #[test]
+ fn test_router_v60() {
+ router_send_v60("peer1", &[1, 2, 3]);
+ }
+
+ #[test]
+ fn test_stats_v60() {
+ stats_record_v60(10.0);
+ stats_record_v60(20.0);
+ let avg = stats_avg_v60();
+ assert!(avg > 0.0);
+ }
+
+ #[test]
+ fn test_rle_v60() {
+ let data = vec![5, 5, 5, 3, 3];
+ let compressed = rle_compress_v60(&data);
+ assert!(compressed.len() <= data.len() + 2); // RLE can be slightly larger
+ }
+
+ // ─── v0.70 Tests ───
+
+ #[test]
+ fn test_proto_v70() { assert_eq!(proto_version_v70(), "0.70.0"); }
+
+ #[test]
+ fn test_qos_v70() { assert_eq!(qos_level_v70(4), 4); assert_eq!(qos_level_v70(99), 4); }
+
+ #[test]
+ fn test_sign_v70() {
+ let sig = frame_sign_v70(b"data", b"key");
+ assert_eq!(sig.len(), 4);
+ let sig2 = frame_sign_v70(b"data", b"key");
+ assert_eq!(sig, sig2);
+ }
+
+ #[test]
+ fn test_rate_v70() {
+ assert!(rate_check_v70(0.0, 2.0));
+ assert!(rate_check_v70(0.0, 2.0));
+ }
+
+ #[test]
+ fn test_split_v70() {
+ let chunk = split_stream_v70(&[1, 2, 3, 4], 2);
+ assert_eq!(chunk.len(), 2);
+ }
+
+ #[test]
+ fn test_watermark_v70() {
+ assert_eq!(watermark_v70(50.0, 10.0, 90.0), "normal");
+ assert_eq!(watermark_v70(95.0, 10.0, 90.0), "high");
+ }
+
+ #[test]
+ fn test_reorder_v70() {
+ reorder_push_v70(1, &[2]);
+ reorder_push_v70(0, &[1]);
+ let first = reorder_pop_v70();
+ assert_eq!(first, vec![1]);
+ }
+
+ #[test]
+ fn test_timeout_v70() {
+ let expired = timeout_check_v70(0.0, 100.0);
+ assert_eq!(expired, 0);
+ }
+
+ #[test]
+ fn test_delta_v70() {
+ let prev = vec![10, 20];
+ let cur = vec![12, 22];
+ let delta = delta_encode_v70(&prev, &cur);
+ assert_ne!(delta, cur);
+ }
+
+ // ─── v0.75 Tests ───
+
+ #[test]
+ fn test_journal_v75() {
+ journal_append_v75("test");
+ assert!(journal_len_v75() > 0);
+ }
+
+ #[test]
+ fn test_config_v75() {
+ config_set_v75("key", "val");
+ assert!(config_gen_v75() > 0);
+ }
+
+ #[test]
+ fn test_dedup_v75() {
+ assert!(!dedup_check_v75(b"unique_data_v75"));
+ assert!(dedup_check_v75(b"unique_data_v75"));
+ }
+
+ #[test]
+ fn test_circuit_v75() {
+ assert_eq!(circuit_state_v75(), "closed");
+ }
+
+ #[test]
+ fn test_bucket_v75() {
+ assert!(bucket_take_v75(1.0));
+ }
+
+ #[test]
+ fn test_batch_v75() {
+ let count = batch_add_v75(&[1, 2, 3]);
+ assert!(count > 0);
+ let flushed = batch_flush_v75();
+ assert!(flushed > 0);
+ }
+
+ #[test]
+ fn test_chain_crc_v75() {
+ let c1 = chain_crc_v75(b"hello");
+ let c2 = chain_crc_v75(b"world");
+ assert_ne!(c1, c2);
+ }
+
+ #[test]
+ fn test_ping_v75() {
+ ping_record_v75(100.0);
+ ping_record_v75(50.0);
+ assert!(ping_avg_v75() > 0.0);
+ }
+
+ #[test]
+ fn test_circuit_fail_v75() {
+ circuit_fail_v75();
+ }
+
+ // ─── v0.80 Tests ───
+
+ #[test]
+ fn test_abr_v80() { let q = abr_quality_v80(0.0, 10.0); assert!(q <= 4); }
+
+ #[test]
+ fn test_jitter_v80() { jitter_push_v80(&[1]); jitter_push_v80(&[2]); }
+
+ #[test]
+ fn test_frame_count_v80() { frame_tick_v80(); assert!(frame_count_v80() > 0); }
+
+ #[test]
+ fn test_event_v80() { event_emit_v80("test"); assert!(event_count_v80() > 0); }
+
+ #[test]
+ fn test_tee_v80() { assert_eq!(tee_count_v80(3), 3); }
+
+ #[test]
+ fn test_gain_v80() { let v = gain_apply_v80(50.0, 100.0); assert!(v > 40.0); }
+
+ #[test]
+ fn test_lossy_v80() { lossy_push_v80(&[1, 2]); let data = lossy_pop_v80(); assert!(!data.is_empty()); }
+
+ #[test]
+ fn test_header_v80() { let hdr = header_build_v80(42, 1, 0x80); assert_eq!(hdr.len(), 6); }
+
+ #[test]
+ fn test_session_v80() { session_start_v80(0.0); let d = session_duration_v80(5000.0); assert_eq!(d, 5000.0); }
+
+ // ─── v0.90 Tests ───
+
+ #[test]
+ fn test_xor_v2_v90() {
+ let enc = xor_v2_v90(b"hello", &[0xAA, 0xBB]);
+ let dec = xor_v2_v90(&enc, &[0xAA, 0xBB]);
+ assert_eq!(dec, b"hello");
+ }
+
+ #[test]
+ fn test_channel_v90() { assert_eq!(channel_route_v90(1, &[1, 2]), 1); }
+
+ #[test]
+ fn test_ack_v90() { ack_send_v90(0); assert!(!ack_check_v90(0)); }
+
+ #[test]
+ fn test_pool_v90() { let buf = pool_alloc_v90(64); assert_eq!(buf.len(), 64); pool_free_v90(&buf); }
+
+ #[test]
+ fn test_bw_v90() { let e = bw_estimate_v90(1000, 100.0); assert!(e > 0.0); }
+
+ #[test]
+ fn test_mux_v90() { mux_push_v90(3, &[1]); mux_push_v90(1, &[2]); let d = mux_pop_v90(); assert_eq!(d, vec![1]); }
+
+ #[test]
+ fn test_nonce_v90() { let n1 = nonce_next_v90(); let n2 = nonce_next_v90(); assert!(n2 > n1); }
+
+ #[test]
+ fn test_validate_v90() { assert!(validate_seq_v90(0)); assert!(validate_seq_v90(1)); }
+
+ #[test]
+ fn test_retry_v90() { retry_push_v90(&[1, 2]); assert!(retry_count_v90() > 0); }
+
+ // ─── v0.95 Tests ───
+
+ #[test]
+ fn test_lz4_v95() {
+ let data = vec![0u8; 50];
+ let compressed = lz4_compress_v95(&data);
+ let decompressed = lz4_decompress_v95(&compressed);
+ assert_eq!(decompressed, data);
+ }
+
+ #[test]
+ fn test_telemetry_v95() { telemetry_emit_v95("fps", 60.0); assert!(telemetry_count_v95() > 0); }
+
+ #[test]
+ fn test_diff_v95() {
+ let d = diff_frames_v95(&[10, 20], &[10, 25]);
+ assert_eq!(d, vec![0, 13]); // 20^25 = 13
+ }
+
+ #[test]
+ fn test_backoff_v95() { let d = backoff_next_v95(); assert!(d >= 100.0); }
+
+ #[test]
+ fn test_mirror_v95() { mirror_push_v95(&[1, 2]); let d = mirror_pop_v95(); assert_eq!(d, vec![1, 2]); }
+
+ #[test]
+ fn test_quota_v95() { assert!(quota_consume_v95(100)); }
+
+ #[test]
+ fn test_heartbeat_v95() { heartbeat_ping_v95(100.0); let e = heartbeat_elapsed_v95(110.0); assert_eq!(e, 10.0); }
+
+ #[test]
+ fn test_tag_v95() { assert!(tag_filter_v95("video")); assert!(!tag_filter_v95("unknown")); }
+
+ #[test]
+ fn test_mavg_v95() { mavg_push_v95(10.0); mavg_push_v95(20.0); assert!(mavg_get_v95() > 0.0); }
+
+ // ─── v1.0 Tests ───
+
+ #[test]
+ fn test_pipeline_v100() { pipeline_add_v100("encode"); assert!(pipeline_count_v100() > 0); }
+
+ #[test]
+ fn test_proto_header_v100() { let h = proto_header_v100(1, 0x80, 42); assert_eq!(h.len(), 7); }
+
+ #[test]
+ fn test_splitter_v100() { splitter_push_v100(&[1, 2, 3, 4, 5], 2); let c = splitter_pop_v100(); assert!(!c.is_empty()); }
+
+ #[test]
+ fn test_cwnd_v100() { let w = cwnd_ack_v100(); assert!(w > 0.0); }
+
+ #[test]
+ fn test_stats_v100() { let s = stream_stats_v100(); assert!(s.contains("frames")); }
+
+ #[test]
+ fn test_ack_window_v100() { assert!(ack_window_v100(0)); }
+
+ #[test]
+ fn test_codec_v100() { codec_register_v100("webp"); let l = codec_list_v100(); assert!(l.contains("webp")); }
+
+ #[test]
+ fn test_flow_v100() { assert!(flow_credit_v100(1)); }
+
+ #[test]
+ fn test_version_v100() { let v = version_negotiate_v100("1.0"); assert_eq!(v, "1.0"); }
}
@@ -3265,3 +4171,7 @@ mod tests {
+
+
+
+
diff --git a/engine/ds-stream/CHANGELOG.md b/engine/ds-stream/CHANGELOG.md
index eab8ba0..568f6f7 100644
--- a/engine/ds-stream/CHANGELOG.md
+++ b/engine/ds-stream/CHANGELOG.md
@@ -1,17 +1,18 @@
# Changelog
-## [0.50.0] - 2026-03-11
+## [1.0.0] - 2026-03-11 🎉
### Added
-- **`StreamCipher`** — XOR encryption with rotating key
-- **`ChannelMux`/`ChannelDemux`** — multi-channel mux/demux
-- **`FramePacer`** — interval-based frame pacing
-- **`CongestionWindow`** — AIMD congestion control
-- **`FlowController`** — token-bucket flow control
-- **`ProtocolNegotiator`** — capability exchange
-- **`ReplayRecorder`** — frame recording for replay
-- **`BandwidthShaper`** — enforce max bytes/sec
-- 9 new tests (201 total)
+- **`StreamPipeline`** — ordered transform chain
+- **`ProtocolHeader`** — v1.0 protocol header (encode/decode)
+- **`FrameSplitterV2`** — split + reassemble frames by MTU
+- **`CongestionWindowV2`** — TCP-like cwnd (slow start + AIMD)
+- **`StreamStatsV2`** — comprehensive stream stats
+- **`AckWindow`** — sliding window ACK
+- **`CodecRegistryV2`** — named codec registry
+- **`FlowControllerV2`** — credit-based flow control
+- **`VersionNegotiator`** — protocol version negotiation
+- 9 new tests (264 total)
-## [0.40.0] — QualityAdapter, SourceMixer, FrameDeduplicator, BackpressureController, HeartbeatMonitor, CompressionTracker, FecEncoder, StreamSnapshot, AdaptivePriorityQueue
-## [0.30.0] — FrameRingBuffer, PacketLossDetector, ConnectionQuality
+## [0.95.0] — Lz4Lite, TelemetrySink, FrameDiffer, BackoffTimer, StreamMirror, QuotaManager, HeartbeatV2, TagFilter, MovingAverage
+## [0.90.0] — XorCipherV2, ChannelRouter, AckTracker, FramePoolV2, BandwidthEstimatorV2, PriorityMux, Nonce, Validator, Retry
diff --git a/engine/ds-stream/Cargo.toml b/engine/ds-stream/Cargo.toml
index 0f1910e..bf1dd24 100644
--- a/engine/ds-stream/Cargo.toml
+++ b/engine/ds-stream/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ds-stream"
-version = "0.50.0"
+version = "1.0.0"
edition.workspace = true
license.workspace = true
description = "Universal bitstream streaming — any input to any output"
diff --git a/engine/ds-stream/src/codec.rs b/engine/ds-stream/src/codec.rs
index 5e16f69..fe9b590 100644
--- a/engine/ds-stream/src/codec.rs
+++ b/engine/ds-stream/src/codec.rs
@@ -2897,6 +2897,968 @@ impl BandwidthShaper {
}
}
+// ─── v0.60: CRC32 Checker ───
+
+pub struct Crc32Checker;
+
+impl Crc32Checker {
+ pub fn checksum(data: &[u8]) -> u32 {
+ let mut crc: u32 = 0xFFFFFFFF;
+ for &b in data {
+ crc ^= b as u32;
+ for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; }
+ }
+ !crc
+ }
+ pub fn verify(data: &[u8], expected: u32) -> bool { Self::checksum(data) == expected }
+}
+
+// ─── v0.60: Connection State ───
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum ConnState { Connecting, Connected, Disconnected, Reconnecting }
+
+pub struct ConnectionStateMachine { state: ConnState, retries: u32 }
+
+impl ConnectionStateMachine {
+ pub fn new() -> Self { ConnectionStateMachine { state: ConnState::Connecting, retries: 0 } }
+ pub fn connect(&mut self) { self.state = ConnState::Connected; self.retries = 0; }
+ pub fn disconnect(&mut self) { self.state = ConnState::Disconnected; }
+ pub fn reconnect(&mut self) { self.state = ConnState::Reconnecting; self.retries += 1; }
+ pub fn state(&self) -> &ConnState { &self.state }
+ pub fn retries(&self) -> u32 { self.retries }
+}
+
+impl Default for ConnectionStateMachine { fn default() -> Self { Self::new() } }
+
+// ─── v0.60: Packet Sequencer ───
+
+pub struct PacketSequencer { next: u64, gaps: Vec }
+
+impl PacketSequencer {
+ pub fn new() -> Self { PacketSequencer { next: 0, gaps: Vec::new() } }
+ pub fn next_seq(&mut self) -> u64 { let s = self.next; self.next += 1; s }
+ pub fn receive(&mut self, seq: u64) {
+ if seq > self.next { for g in self.next..seq { self.gaps.push(g); } self.next = seq + 1; }
+ }
+ pub fn gap_count(&self) -> usize { self.gaps.len() }
+}
+
+impl Default for PacketSequencer { fn default() -> Self { Self::new() } }
+
+// ─── v0.60: Stream Resumer ───
+
+pub struct StreamResumer { last_good: u64 }
+
+impl StreamResumer {
+ pub fn new() -> Self { StreamResumer { last_good: 0 } }
+ pub fn mark(&mut self, pos: u64) { self.last_good = pos; }
+ pub fn resume_point(&self) -> u64 { self.last_good }
+}
+
+// ─── v0.60: Frame Interpolator ───
+
+pub struct FrameInterpolator;
+
+impl FrameInterpolator {
+ pub fn lerp(a: f64, b: f64, t: f64) -> f64 { a + (b - a) * t.clamp(0.0, 1.0) }
+ pub fn lerp_bytes(a: &[u8], b: &[u8], t: f64) -> Vec {
+ let len = a.len().min(b.len());
+ (0..len).map(|i| Self::lerp(a[i] as f64, b[i] as f64, t) as u8).collect()
+ }
+}
+
+// ─── v0.60: Error Recovery ───
+
+pub struct ErrorRecovery { max_retries: u32 }
+
+impl ErrorRecovery {
+ pub fn new(max: u32) -> Self { ErrorRecovery { max_retries: max } }
+ /// Exponential backoff delay in ms.
+ pub fn delay_ms(&self, attempt: u32) -> f64 {
+ if attempt >= self.max_retries { return -1.0; }
+ (100.0 * 2.0_f64.powi(attempt as i32)).min(30000.0)
+ }
+}
+
+// ─── v0.60: Peer Router ───
+
+pub struct PeerRouter { peers: Vec<(String, Vec>)> }
+
+impl PeerRouter {
+ pub fn new() -> Self { PeerRouter { peers: Vec::new() } }
+ pub fn send(&mut self, peer: &str, data: Vec) {
+ if let Some(p) = self.peers.iter_mut().find(|(n, _)| n == peer) { p.1.push(data); }
+ else { self.peers.push((peer.to_string(), vec![data])); }
+ }
+ pub fn recv(&mut self, peer: &str) -> Option> {
+ self.peers.iter_mut().find(|(n, _)| n == peer).and_then(|(_, q)| if q.is_empty() { None } else { Some(q.remove(0)) })
+ }
+ pub fn peer_count(&self) -> usize { self.peers.len() }
+}
+
+impl Default for PeerRouter { fn default() -> Self { Self::new() } }
+
+// ─── v0.60: Stats Aggregator ───
+
+pub struct StatsAggregator { values: Vec, window: usize }
+
+impl StatsAggregator {
+ pub fn new(window: usize) -> Self { StatsAggregator { values: Vec::new(), window } }
+ pub fn record(&mut self, val: f64) {
+ self.values.push(val);
+ if self.values.len() > self.window { self.values.remove(0); }
+ }
+ pub fn avg(&self) -> f64 { if self.values.is_empty() { 0.0 } else { self.values.iter().sum::() / self.values.len() as f64 } }
+ pub fn min(&self) -> f64 { self.values.iter().cloned().fold(f64::INFINITY, f64::min) }
+ pub fn max(&self) -> f64 { self.values.iter().cloned().fold(f64::NEG_INFINITY, f64::max) }
+}
+
+// ─── v0.60: Packet Compressor (RLE) ───
+
+pub struct PacketCompressor;
+
+impl PacketCompressor {
+ pub fn compress(data: &[u8]) -> Vec {
+ if data.is_empty() { return Vec::new(); }
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i < data.len() {
+ let val = data[i];
+ let mut count = 1u8;
+ while i + (count as usize) < data.len() && data[i + (count as usize)] == val && count < 255 { count += 1; }
+ out.push(count);
+ out.push(val);
+ i += count as usize;
+ }
+ out
+ }
+ pub fn decompress(data: &[u8]) -> Vec {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i + 1 < data.len() { for _ in 0..data[i] { out.push(data[i + 1]); } i += 2; }
+ out
+ }
+}
+
+// ─── v0.70: Protocol Version ───
+
+pub struct ProtocolVersion { pub major: u16, pub minor: u16, pub patch: u16 }
+
+impl ProtocolVersion {
+ pub fn new(major: u16, minor: u16, patch: u16) -> Self { ProtocolVersion { major, minor, patch } }
+ pub fn compatible(&self, other: &ProtocolVersion) -> bool { self.major == other.major }
+ pub fn to_string_v(&self) -> String { format!("{}.{}.{}", self.major, self.minor, self.patch) }
+}
+
+// ─── v0.70: QoS Level ───
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum QosLevel { Background = 0, Low = 1, Normal = 2, High = 3, Realtime = 4 }
+
+impl QosLevel {
+ pub fn from_u8(v: u8) -> Self { match v { 0 => Self::Background, 1 => Self::Low, 3 => Self::High, 4 => Self::Realtime, _ => Self::Normal } }
+ pub fn priority(&self) -> u8 { *self as u8 }
+}
+
+// ─── v0.70: Frame Signer ───
+
+pub struct FrameSigner { key: Vec }
+
+impl FrameSigner {
+ pub fn new(key: Vec) -> Self { FrameSigner { key } }
+ pub fn sign(&self, data: &[u8]) -> Vec {
+ let mut hash: u32 = 0x811c9dc5;
+ for &b in self.key.iter().chain(data.iter()) { hash ^= b as u32; hash = hash.wrapping_mul(0x01000193); }
+ hash.to_le_bytes().to_vec()
+ }
+ pub fn verify(&self, data: &[u8], sig: &[u8]) -> bool { self.sign(data) == sig }
+}
+
+// ─── v0.70: Rate Limiter ───
+
+pub struct RateLimiter { max_per_sec: f64, count: f64, window_start: f64 }
+
+impl RateLimiter {
+ pub fn new(max: f64) -> Self { RateLimiter { max_per_sec: max, count: 0.0, window_start: 0.0 } }
+ pub fn check(&mut self, now_ms: f64) -> bool {
+ if now_ms - self.window_start >= 1000.0 { self.window_start = now_ms; self.count = 0.0; }
+ if self.count < self.max_per_sec { self.count += 1.0; true } else { false }
+ }
+}
+
+// ─── v0.70: Stream Splitter ───
+
+pub struct StreamSplitter;
+
+impl StreamSplitter {
+ pub fn split(data: &[u8], n: usize) -> Vec> {
+ if n == 0 { return vec![data.to_vec()]; }
+ let chunk_size = (data.len() + n - 1) / n;
+ data.chunks(chunk_size.max(1)).map(|c| c.to_vec()).collect()
+ }
+}
+
+// ─── v0.70: Watermark Tracker ───
+
+pub struct WatermarkTracker { low: f64, high: f64, current: f64 }
+
+impl WatermarkTracker {
+ pub fn new(low: f64, high: f64) -> Self { WatermarkTracker { low, high, current: 0.0 } }
+ pub fn update(&mut self, val: f64) { self.current = val; }
+ pub fn status(&self) -> &str {
+ if self.current >= self.high { "high" } else if self.current <= self.low { "low" } else { "normal" }
+ }
+}
+
+// ─── v0.70: Packet Reorderer ───
+
+pub struct PacketReorderer { buffer: Vec<(u64, Vec)>, next_expected: u64 }
+
+impl PacketReorderer {
+ pub fn new() -> Self { PacketReorderer { buffer: Vec::new(), next_expected: 0 } }
+ pub fn push(&mut self, seq: u64, data: Vec) {
+ self.buffer.push((seq, data));
+ self.buffer.sort_by_key(|(s, _)| *s);
+ }
+ pub fn pop(&mut self) -> Option> {
+ if !self.buffer.is_empty() && self.buffer[0].0 == self.next_expected {
+ self.next_expected += 1;
+ Some(self.buffer.remove(0).1)
+ } else { None }
+ }
+ pub fn pending(&self) -> usize { self.buffer.len() }
+}
+
+impl Default for PacketReorderer { fn default() -> Self { Self::new() } }
+
+// ─── v0.70: Timeout Monitor ───
+
+pub struct TimeoutMonitor { pending: Vec<(u64, f64)>, timeout_ms: f64 }
+
+impl TimeoutMonitor {
+ pub fn new(timeout_ms: f64) -> Self { TimeoutMonitor { pending: Vec::new(), timeout_ms } }
+ pub fn start(&mut self, id: u64, now_ms: f64) { self.pending.push((id, now_ms)); }
+ pub fn check_expired(&mut self, now_ms: f64) -> Vec {
+ let (expired, live): (Vec<_>, Vec<_>) = self.pending.drain(..).partition(|(_, t)| now_ms - t >= self.timeout_ms);
+ self.pending = live;
+ expired.into_iter().map(|(id, _)| id).collect()
+ }
+}
+
+// ─── v0.70: Delta Encoder ───
+
+pub struct DeltaEncoder;
+
+impl DeltaEncoder {
+ pub fn encode(prev: &[u8], cur: &[u8]) -> Vec {
+ let len = prev.len().min(cur.len());
+ let mut out: Vec = (0..len).map(|i| cur[i] ^ prev[i]).collect();
+ if cur.len() > len { out.extend_from_slice(&cur[len..]); }
+ out
+ }
+ pub fn decode(prev: &[u8], delta: &[u8]) -> Vec {
+ let len = prev.len().min(delta.len());
+ let mut out: Vec = (0..len).map(|i| delta[i] ^ prev[i]).collect();
+ if delta.len() > len { out.extend_from_slice(&delta[len..]); }
+ out
+ }
+}
+
+// ─── v0.75: Stream Journal ───
+
+pub struct StreamJournal { entries: Vec }
+
+impl StreamJournal {
+ pub fn new() -> Self { StreamJournal { entries: Vec::new() } }
+ pub fn append(&mut self, entry: &str) { self.entries.push(entry.to_string()); }
+ pub fn len(&self) -> usize { self.entries.len() }
+ pub fn is_empty(&self) -> bool { self.entries.is_empty() }
+ pub fn get(&self, idx: usize) -> Option<&str> { self.entries.get(idx).map(|s| s.as_str()) }
+}
+
+impl Default for StreamJournal { fn default() -> Self { Self::new() } }
+
+// ─── v0.75: Hot Config ───
+
+pub struct HotConfigV2 { values: Vec<(String, String)>, generation: u64 }
+
+impl HotConfigV2 {
+ pub fn new() -> Self { HotConfigV2 { values: Vec::new(), generation: 0 } }
+ pub fn set(&mut self, key: &str, val: &str) {
+ if let Some(p) = self.values.iter_mut().find(|(k, _)| k == key) { p.1 = val.to_string(); }
+ else { self.values.push((key.to_string(), val.to_string())); }
+ self.generation += 1;
+ }
+ pub fn get(&self, key: &str) -> Option<&str> { self.values.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str()) }
+ pub fn generation(&self) -> u64 { self.generation }
+}
+
+impl Default for HotConfigV2 { fn default() -> Self { Self::new() } }
+
+// ─── v0.75: Frame Dedup V2 ───
+
+pub struct FrameDedupV2 { seen: Vec, capacity: usize }
+
+impl FrameDedupV2 {
+ pub fn new(capacity: usize) -> Self { FrameDedupV2 { seen: Vec::new(), capacity } }
+ fn hash(data: &[u8]) -> u64 {
+ let mut h: u64 = 0xcbf29ce484222325;
+ for &b in data { h ^= b as u64; h = h.wrapping_mul(0x100000001b3); }
+ h
+ }
+ pub fn is_dup(&mut self, data: &[u8]) -> bool {
+ let h = Self::hash(data);
+ if self.seen.contains(&h) { return true; }
+ if self.seen.len() >= self.capacity { self.seen.remove(0); }
+ self.seen.push(h);
+ false
+ }
+}
+
+// ─── v0.75: Metric Histogram ───
+
+pub struct MetricHistogram { buckets: Vec<(f64, u64)> }
+
+impl MetricHistogram {
+ pub fn new(boundaries: &[f64]) -> Self {
+ let mut buckets: Vec<(f64, u64)> = boundaries.iter().map(|&b| (b, 0)).collect();
+ buckets.push((f64::INFINITY, 0));
+ MetricHistogram { buckets }
+ }
+ pub fn record(&mut self, val: f64) {
+ for b in self.buckets.iter_mut() { if val <= b.0 { b.1 += 1; return; } }
+ }
+ pub fn percentile(&self, p: f64) -> f64 {
+ let total: u64 = self.buckets.iter().map(|(_, c)| c).sum();
+ let target = (total as f64 * p / 100.0) as u64;
+ let mut acc = 0u64;
+ for (bound, count) in &self.buckets { acc += count; if acc >= target { return *bound; } }
+ self.buckets.last().map(|(b, _)| *b).unwrap_or(0.0)
+ }
+}
+
+// ─── v0.75: Circuit Breaker ───
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum BreakerState { Closed, Open, HalfOpen }
+
+pub struct CircuitBreaker { state: BreakerState, failures: u32, threshold: u32 }
+
+impl CircuitBreaker {
+ pub fn new(threshold: u32) -> Self { CircuitBreaker { state: BreakerState::Closed, failures: 0, threshold } }
+ pub fn record_success(&mut self) { self.failures = 0; self.state = BreakerState::Closed; }
+ pub fn record_failure(&mut self) {
+ self.failures += 1;
+ if self.failures >= self.threshold { self.state = BreakerState::Open; }
+ }
+ pub fn try_half_open(&mut self) { if self.state == BreakerState::Open { self.state = BreakerState::HalfOpen; } }
+ pub fn state(&self) -> &BreakerState { &self.state }
+ pub fn is_allowed(&self) -> bool { self.state != BreakerState::Open }
+}
+
+// ─── v0.75: Token Bucket V2 ───
+
+pub struct TokenBucketV2 { tokens: f64, max: f64, refill_rate: f64, last_refill: f64 }
+
+impl TokenBucketV2 {
+ pub fn new(max: f64, refill_rate: f64) -> Self {
+ TokenBucketV2 { tokens: max, max, refill_rate, last_refill: 0.0 }
+ }
+ pub fn take(&mut self, n: f64, now_ms: f64) -> bool {
+ let elapsed = (now_ms - self.last_refill) / 1000.0;
+ self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max);
+ self.last_refill = now_ms;
+ if self.tokens >= n { self.tokens -= n; true } else { false }
+ }
+ pub fn available(&self) -> f64 { self.tokens }
+}
+
+// ─── v0.75: Batch Accumulator ───
+
+pub struct BatchAccumulator { buffer: Vec>, max_count: usize, max_bytes: usize, current_bytes: usize }
+
+impl BatchAccumulator {
+ pub fn new(max_count: usize, max_bytes: usize) -> Self {
+ BatchAccumulator { buffer: Vec::new(), max_count, max_bytes, current_bytes: 0 }
+ }
+ pub fn add(&mut self, data: Vec) -> bool {
+ self.current_bytes += data.len();
+ self.buffer.push(data);
+ self.buffer.len() >= self.max_count || self.current_bytes >= self.max_bytes
+ }
+ pub fn flush(&mut self) -> Vec> {
+ self.current_bytes = 0;
+ std::mem::take(&mut self.buffer)
+ }
+ pub fn pending(&self) -> usize { self.buffer.len() }
+}
+
+// ─── v0.75: Checksum Chain ───
+
+pub struct ChecksumChain { prev_crc: u32 }
+
+impl ChecksumChain {
+ pub fn new() -> Self { ChecksumChain { prev_crc: 0 } }
+ pub fn feed(&mut self, data: &[u8]) -> u32 {
+ let mut crc: u32 = self.prev_crc ^ 0xFFFFFFFF;
+ for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB88320 } else { crc >> 1 }; } }
+ self.prev_crc = !crc;
+ self.prev_crc
+ }
+}
+
+impl Default for ChecksumChain { fn default() -> Self { Self::new() } }
+
+// ─── v0.75: Ping Monitor ───
+
+pub struct PingMonitor { samples: Vec, smoothed: f64, alpha: f64 }
+
+impl PingMonitor {
+ pub fn new(alpha: f64) -> Self { PingMonitor { samples: Vec::new(), smoothed: 0.0, alpha } }
+ pub fn record(&mut self, rtt_ms: f64) {
+ self.smoothed = self.alpha * rtt_ms + (1.0 - self.alpha) * self.smoothed;
+ self.samples.push(rtt_ms);
+ }
+ pub fn smoothed_rtt(&self) -> f64 { self.smoothed }
+ pub fn sample_count(&self) -> usize { self.samples.len() }
+}
+
+// ─── v0.80: ABR Controller ───
+
+pub struct AbrController { tiers: Vec, current: usize }
+
+impl AbrController {
+ pub fn new(tiers: Vec) -> Self { AbrController { current: tiers.len().saturating_sub(1), tiers } }
+ pub fn adjust(&mut self, loss_pct: f64, rtt_ms: f64) -> u32 {
+ if loss_pct > 5.0 || rtt_ms > 200.0 { if self.current > 0 { self.current -= 1; } }
+ else if loss_pct < 1.0 && rtt_ms < 50.0 { if self.current + 1 < self.tiers.len() { self.current += 1; } }
+ self.tiers[self.current]
+ }
+ pub fn current_bitrate(&self) -> u32 { self.tiers[self.current] }
+}
+
+// ─── v0.80: Jitter Buffer ───
+
+pub struct JitterBufferV2 { buffer: Vec>, delay: usize }
+
+impl JitterBufferV2 {
+ pub fn new(delay: usize) -> Self { JitterBufferV2 { buffer: Vec::new(), delay } }
+ pub fn push(&mut self, data: Vec) { self.buffer.push(data); }
+ pub fn pop(&mut self) -> Option> {
+ if self.buffer.len() > self.delay { Some(self.buffer.remove(0)) } else { None }
+ }
+ pub fn buffered(&self) -> usize { self.buffer.len() }
+}
+
+// ─── v0.80: Frame Counter ───
+
+pub struct FrameCounter { timestamps: Vec, bytes: Vec, window_ms: f64 }
+
+impl FrameCounter {
+ pub fn new(window_ms: f64) -> Self { FrameCounter { timestamps: Vec::new(), bytes: Vec::new(), window_ms } }
+ pub fn tick(&mut self, now_ms: f64, size: usize) {
+ self.timestamps.push(now_ms);
+ self.bytes.push(size);
+ let cutoff = now_ms - self.window_ms;
+ while !self.timestamps.is_empty() && self.timestamps[0] < cutoff { self.timestamps.remove(0); self.bytes.remove(0); }
+ }
+ pub fn fps(&self) -> f64 { self.timestamps.len() as f64 / (self.window_ms / 1000.0) }
+ pub fn bps(&self) -> f64 { self.bytes.iter().sum::() as f64 * 8.0 / (self.window_ms / 1000.0) }
+}
+
+// ─── v0.80: Event Bus ───
+
+pub struct EventBus { events: Vec }
+
+impl EventBus {
+ pub fn new() -> Self { EventBus { events: Vec::new() } }
+ pub fn emit(&mut self, name: &str) { self.events.push(name.to_string()); }
+ pub fn count(&self) -> usize { self.events.len() }
+ pub fn drain(&mut self) -> Vec { std::mem::take(&mut self.events) }
+}
+
+impl Default for EventBus { fn default() -> Self { Self::new() } }
+
+// ─── v0.80: Stream Tee ───
+
+pub struct StreamTee { sink_count: usize }
+
+impl StreamTee {
+ pub fn new(n: usize) -> Self { StreamTee { sink_count: n } }
+ pub fn tee(&self, data: &[u8]) -> Vec> { (0..self.sink_count).map(|_| data.to_vec()).collect() }
+ pub fn count(&self) -> usize { self.sink_count }
+}
+
+// ─── v0.80: Gain Controller ───
+
+pub struct GainController { current_gain: f64, smoothing: f64 }
+
+impl GainController {
+ pub fn new(smoothing: f64) -> Self { GainController { current_gain: 1.0, smoothing } }
+ pub fn apply(&mut self, val: f64, target: f64) -> f64 {
+ let ratio = if val.abs() > 0.001 { target / val } else { 1.0 };
+ self.current_gain = self.smoothing * ratio + (1.0 - self.smoothing) * self.current_gain;
+ val * self.current_gain
+ }
+ pub fn gain(&self) -> f64 { self.current_gain }
+}
+
+// ─── v0.80: Lossy Queue ───
+
+pub struct LossyQueue { buffer: Vec>, capacity: usize }
+
+impl LossyQueue {
+ pub fn new(capacity: usize) -> Self { LossyQueue { buffer: Vec::new(), capacity } }
+ pub fn push(&mut self, data: Vec) {
+ if self.buffer.len() >= self.capacity { self.buffer.remove(0); }
+ self.buffer.push(data);
+ }
+ pub fn pop(&mut self) -> Option> { if self.buffer.is_empty() { None } else { Some(self.buffer.remove(0)) } }
+ pub fn len(&self) -> usize { self.buffer.len() }
+ pub fn is_empty(&self) -> bool { self.buffer.is_empty() }
+}
+
+// ─── v0.80: Header Builder ───
+
+pub struct HeaderBuilder;
+
+impl HeaderBuilder {
+ pub fn build(seq: u32, channel: u8, flags: u8) -> Vec {
+ let mut hdr = Vec::with_capacity(6);
+ hdr.extend_from_slice(&seq.to_le_bytes());
+ hdr.push(channel);
+ hdr.push(flags);
+ hdr
+ }
+ pub fn parse(hdr: &[u8]) -> Option<(u32, u8, u8)> {
+ if hdr.len() < 6 { return None; }
+ let seq = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
+ Some((seq, hdr[4], hdr[5]))
+ }
+}
+
+// ─── v0.80: Session Tracker ───
+
+pub struct SessionTracker { id: u64, start_ms: f64, last_ms: f64 }
+
+impl SessionTracker {
+ pub fn new(id: u64, now_ms: f64) -> Self { SessionTracker { id, start_ms: now_ms, last_ms: now_ms } }
+ pub fn touch(&mut self, now_ms: f64) { self.last_ms = now_ms; }
+ pub fn duration_ms(&self) -> f64 { self.last_ms - self.start_ms }
+ pub fn id(&self) -> u64 { self.id }
+}
+
+// ─── v0.90: XOR Cipher V2 ───
+
+pub struct XorCipherV2 { key: Vec }
+
+impl XorCipherV2 {
+ pub fn new(key: Vec) -> Self { XorCipherV2 { key } }
+ pub fn apply(&self, data: &[u8]) -> Vec {
+ if self.key.is_empty() { return data.to_vec(); }
+ data.iter().enumerate().map(|(i, &b)| b ^ self.key[i % self.key.len()]).collect()
+ }
+}
+
+// ─── v0.90: Channel Router ───
+
+pub struct ChannelRouter { channels: Vec<(u8, Vec>)> }
+
+impl ChannelRouter {
+ pub fn new() -> Self { ChannelRouter { channels: Vec::new() } }
+ pub fn route(&mut self, ch: u8, data: Vec) {
+ if let Some(entry) = self.channels.iter_mut().find(|(c, _)| *c == ch) { entry.1.push(data); }
+ else { self.channels.push((ch, vec![data])); }
+ }
+ pub fn drain(&mut self, ch: u8) -> Vec> {
+ if let Some(entry) = self.channels.iter_mut().find(|(c, _)| *c == ch) { std::mem::take(&mut entry.1) }
+ else { Vec::new() }
+ }
+ pub fn channel_count(&self) -> usize { self.channels.len() }
+}
+
+impl Default for ChannelRouter { fn default() -> Self { Self::new() } }
+
+// ─── v0.90: Ack Tracker ───
+
+pub struct AckTracker { sent: Vec, acked: Vec }
+
+impl AckTracker {
+ pub fn new() -> Self { AckTracker { sent: Vec::new(), acked: Vec::new() } }
+ pub fn send(&mut self, seq: u64) { self.sent.push(seq); }
+ pub fn ack(&mut self, seq: u64) { if !self.acked.contains(&seq) { self.acked.push(seq); } }
+ pub fn is_acked(&self, seq: u64) -> bool { self.acked.contains(&seq) }
+ pub fn pending(&self) -> usize { self.sent.len() - self.acked.len() }
+}
+
+impl Default for AckTracker { fn default() -> Self { Self::new() } }
+
+// ─── v0.90: Frame Pool ───
+
+pub struct FramePoolV2 { pool: Vec>, buf_size: usize }
+
+impl FramePoolV2 {
+ pub fn new(buf_size: usize, count: usize) -> Self {
+ let pool = (0..count).map(|_| vec![0u8; buf_size]).collect();
+ FramePoolV2 { pool, buf_size }
+ }
+ pub fn alloc(&mut self) -> Vec { self.pool.pop().unwrap_or_else(|| vec![0u8; self.buf_size]) }
+ pub fn free(&mut self, buf: Vec) { self.pool.push(buf); }
+ pub fn available(&self) -> usize { self.pool.len() }
+}
+
+// ─── v0.90: Bandwidth Estimator ───
+
+pub struct BandwidthEstimatorV2 { estimate_bps: f64, alpha: f64 }
+
+impl BandwidthEstimatorV2 {
+ pub fn new(alpha: f64) -> Self { BandwidthEstimatorV2 { estimate_bps: 0.0, alpha } }
+ pub fn sample(&mut self, bytes: usize, duration_ms: f64) {
+ if duration_ms <= 0.0 { return; }
+ let bps = bytes as f64 * 8.0 * 1000.0 / duration_ms;
+ self.estimate_bps = self.alpha * bps + (1.0 - self.alpha) * self.estimate_bps;
+ }
+ pub fn estimate(&self) -> f64 { self.estimate_bps }
+}
+
+// ─── v0.90: Priority Mux ───
+
+pub struct PriorityMux { queues: Vec<(u8, Vec>)> }
+
+impl PriorityMux {
+ pub fn new() -> Self { PriorityMux { queues: Vec::new() } }
+ pub fn push(&mut self, priority: u8, data: Vec) {
+ if let Some(q) = self.queues.iter_mut().find(|(p, _)| *p == priority) { q.1.push(data); }
+ else { self.queues.push((priority, vec![data])); self.queues.sort_by(|a, b| b.0.cmp(&a.0)); }
+ }
+ pub fn pop(&mut self) -> Option> {
+ for q in self.queues.iter_mut() { if !q.1.is_empty() { return Some(q.1.remove(0)); } }
+ None
+ }
+}
+
+impl Default for PriorityMux { fn default() -> Self { Self::new() } }
+
+// ─── v0.90: Nonce Generator ───
+
+pub struct NonceGenerator { counter: u64 }
+
+impl NonceGenerator {
+ pub fn new() -> Self { NonceGenerator { counter: 0 } }
+ pub fn next(&mut self) -> u64 { self.counter += 1; self.counter }
+ pub fn current(&self) -> u64 { self.counter }
+}
+
+impl Default for NonceGenerator { fn default() -> Self { Self::new() } }
+
+// ─── v0.90: Stream Validator ───
+
+pub struct StreamValidator { expected_seq: u64, errors: u64 }
+
+impl StreamValidator {
+ pub fn new() -> Self { StreamValidator { expected_seq: 0, errors: 0 } }
+ pub fn validate(&mut self, seq: u64) -> bool {
+ if seq == self.expected_seq { self.expected_seq += 1; true }
+ else { self.errors += 1; self.expected_seq = seq + 1; false }
+ }
+ pub fn error_count(&self) -> u64 { self.errors }
+}
+
+impl Default for StreamValidator { fn default() -> Self { Self::new() } }
+
+// ─── v0.90: Retry Queue ───
+
+pub struct RetryQueue { items: Vec<(Vec, u32)>, max_retries: u32 }
+
+impl RetryQueue {
+ pub fn new(max_retries: u32) -> Self { RetryQueue { items: Vec::new(), max_retries } }
+ pub fn push(&mut self, data: Vec) { self.items.push((data, 0)); }
+ pub fn pop_for_retry(&mut self) -> Option> {
+ if let Some(pos) = self.items.iter().position(|(_, r)| *r < self.max_retries) {
+ self.items[pos].1 += 1;
+ Some(self.items[pos].0.clone())
+ } else { None }
+ }
+ pub fn remove_acked(&mut self, data: &[u8]) { self.items.retain(|(d, _)| d != data); }
+ pub fn pending(&self) -> usize { self.items.len() }
+}
+
+// ─── v0.95: Lz4 Lite ───
+
+pub struct Lz4Lite;
+
+impl Lz4Lite {
+ pub fn compress(data: &[u8]) -> Vec {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i < data.len() {
+ let mut run = 1;
+ while i + run < data.len() && data[i + run] == data[i] && run < 255 { run += 1; }
+ out.push(run as u8);
+ out.push(data[i]);
+ i += run;
+ }
+ out
+ }
+ pub fn decompress(data: &[u8]) -> Vec {
+ let mut out = Vec::new();
+ let mut i = 0;
+ while i + 1 < data.len() {
+ let run = data[i] as usize;
+ let byte = data[i + 1];
+ for _ in 0..run { out.push(byte); }
+ i += 2;
+ }
+ out
+ }
+}
+
+// ─── v0.95: Telemetry Sink ───
+
+pub struct TelemetrySink { events: Vec<(String, f64)> }
+
+impl TelemetrySink {
+ pub fn new() -> Self { TelemetrySink { events: Vec::new() } }
+ pub fn emit(&mut self, name: &str, val: f64) { self.events.push((name.to_string(), val)); }
+ pub fn count(&self) -> usize { self.events.len() }
+ pub fn drain(&mut self) -> Vec<(String, f64)> { std::mem::take(&mut self.events) }
+}
+
+impl Default for TelemetrySink { fn default() -> Self { Self::new() } }
+
+// ─── v0.95: Frame Differ ───
+
+pub struct FrameDiffer;
+
+impl FrameDiffer {
+ pub fn diff(a: &[u8], b: &[u8]) -> Vec {
+ let len = a.len().max(b.len());
+ let mut out = Vec::with_capacity(len);
+ for i in 0..len {
+ let va = if i < a.len() { a[i] } else { 0 };
+ let vb = if i < b.len() { b[i] } else { 0 };
+ out.push(va ^ vb);
+ }
+ out
+ }
+ pub fn apply(base: &[u8], diff: &[u8]) -> Vec {
+ let len = base.len().max(diff.len());
+ let mut out = Vec::with_capacity(len);
+ for i in 0..len {
+ let va = if i < base.len() { base[i] } else { 0 };
+ let vd = if i < diff.len() { diff[i] } else { 0 };
+ out.push(va ^ vd);
+ }
+ out
+ }
+}
+
+// ─── v0.95: Backoff Timer ───
+
+pub struct BackoffTimer { attempt: u32, base_ms: f64, max_ms: f64 }
+
+impl BackoffTimer {
+ pub fn new(base_ms: f64, max_ms: f64) -> Self { BackoffTimer { attempt: 0, base_ms, max_ms } }
+ pub fn next(&mut self) -> f64 {
+ let delay = (self.base_ms * 2.0f64.powi(self.attempt as i32)).min(self.max_ms);
+ self.attempt += 1;
+ delay
+ }
+ pub fn reset(&mut self) { self.attempt = 0; }
+ pub fn attempts(&self) -> u32 { self.attempt }
+}
+
+// ─── v0.95: Stream Mirror ───
+
+pub struct StreamMirror { primary: Vec>, mirror: Vec> }
+
+impl StreamMirror {
+ pub fn new() -> Self { StreamMirror { primary: Vec::new(), mirror: Vec::new() } }
+ pub fn push(&mut self, data: Vec) { self.mirror.push(data.clone()); self.primary.push(data); }
+ pub fn pop_primary(&mut self) -> Option> { if self.primary.is_empty() { None } else { Some(self.primary.remove(0)) } }
+ pub fn pop_mirror(&mut self) -> Option> { if self.mirror.is_empty() { None } else { Some(self.mirror.remove(0)) } }
+}
+
+impl Default for StreamMirror { fn default() -> Self { Self::new() } }
+
+// ─── v0.95: Quota Manager ───
+
+pub struct QuotaManager { limit: usize, used: usize }
+
+impl QuotaManager {
+ pub fn new(limit: usize) -> Self { QuotaManager { limit, used: 0 } }
+ pub fn consume(&mut self, n: usize) -> bool { if self.used + n > self.limit { false } else { self.used += n; true } }
+ pub fn remaining(&self) -> usize { self.limit.saturating_sub(self.used) }
+ pub fn reset(&mut self) { self.used = 0; }
+}
+
+// ─── v0.95: Heartbeat V2 ───
+
+pub struct HeartbeatV2 { last_ping: f64, last_pong: f64 }
+
+impl HeartbeatV2 {
+ pub fn new() -> Self { HeartbeatV2 { last_ping: 0.0, last_pong: 0.0 } }
+ pub fn ping(&mut self, now: f64) { self.last_ping = now; }
+ pub fn pong(&mut self, now: f64) { self.last_pong = now; }
+ pub fn rtt(&self) -> f64 { (self.last_pong - self.last_ping).abs() }
+ pub fn is_alive(&self, now: f64, timeout: f64) -> bool { (now - self.last_pong) < timeout }
+}
+
+impl Default for HeartbeatV2 { fn default() -> Self { Self::new() } }
+
+// ─── v0.95: Tag Filter ───
+
+pub struct TagFilter { allowed: Vec }
+
+impl TagFilter {
+ pub fn new(tags: Vec) -> Self { TagFilter { allowed: tags } }
+ pub fn accept(&self, tag: &str) -> bool { self.allowed.iter().any(|t| t == tag) }
+ pub fn add(&mut self, tag: &str) { if !self.allowed.contains(&tag.to_string()) { self.allowed.push(tag.to_string()); } }
+}
+
+// ─── v0.95: Moving Average ───
+
+pub struct MovingAverage { window: Vec, max_size: usize }
+
+impl MovingAverage {
+ pub fn new(max_size: usize) -> Self { MovingAverage { window: Vec::new(), max_size } }
+ pub fn push(&mut self, val: f64) { self.window.push(val); if self.window.len() > self.max_size { self.window.remove(0); } }
+ pub fn get(&self) -> f64 { if self.window.is_empty() { 0.0 } else { self.window.iter().sum::() / self.window.len() as f64 } }
+ pub fn count(&self) -> usize { self.window.len() }
+}
+
+// ─── v1.0: Stream Pipeline ───
+
+pub struct StreamPipeline { stages: Vec }
+
+impl StreamPipeline {
+ pub fn new() -> Self { StreamPipeline { stages: Vec::new() } }
+ pub fn add(&mut self, name: &str) { self.stages.push(name.to_string()); }
+ pub fn count(&self) -> usize { self.stages.len() }
+ pub fn stages(&self) -> &[String] { &self.stages }
+}
+
+impl Default for StreamPipeline { fn default() -> Self { Self::new() } }
+
+// ─── v1.0: Protocol Header ───
+
+pub struct ProtocolHeader;
+
+impl ProtocolHeader {
+ pub fn encode(version: u8, flags: u8, seq: u32) -> Vec {
+ let mut hdr = vec![0xD5u8.wrapping_add(version), version, flags];
+ hdr.extend_from_slice(&seq.to_le_bytes());
+ hdr
+ }
+ pub fn decode(data: &[u8]) -> Option<(u8, u8, u32)> {
+ if data.len() < 7 { return None; }
+ let version = data[1];
+ let flags = data[2];
+ let seq = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
+ Some((version, flags, seq))
+ }
+}
+
+// ─── v1.0: Frame Splitter V2 ───
+
+pub struct FrameSplitterV2 { mtu: usize }
+
+impl FrameSplitterV2 {
+ pub fn new(mtu: usize) -> Self { FrameSplitterV2 { mtu } }
+ pub fn split(&self, data: &[u8]) -> Vec> {
+ data.chunks(self.mtu).map(|c| c.to_vec()).collect()
+ }
+ pub fn reassemble(chunks: &[Vec]) -> Vec {
+ chunks.iter().flat_map(|c| c.iter().copied()).collect()
+ }
+}
+
+// ─── v1.0: Congestion Window ───
+
+pub struct CongestionWindowV2 { cwnd: f64, ssthresh: f64 }
+
+impl CongestionWindowV2 {
+ pub fn new() -> Self { CongestionWindowV2 { cwnd: 1.0, ssthresh: 64.0 } }
+ pub fn on_ack(&mut self) -> f64 {
+ if self.cwnd < self.ssthresh { self.cwnd *= 2.0; } else { self.cwnd += 1.0; }
+ self.cwnd
+ }
+ pub fn on_loss(&mut self) -> f64 { self.ssthresh = (self.cwnd / 2.0).max(1.0); self.cwnd = 1.0; self.cwnd }
+ pub fn window(&self) -> f64 { self.cwnd }
+}
+
+impl Default for CongestionWindowV2 { fn default() -> Self { Self::new() } }
+
+// ─── v1.0: Stream Stats V2 ───
+
+pub struct StreamStatsV2 { frames: u64, bytes: u64, errors: u64, start_ms: f64 }
+
+impl StreamStatsV2 {
+ pub fn new(start_ms: f64) -> Self { StreamStatsV2 { frames: 0, bytes: 0, errors: 0, start_ms } }
+ pub fn record(&mut self, size: usize) { self.frames += 1; self.bytes += size as u64; }
+ pub fn error(&mut self) { self.errors += 1; }
+ pub fn summary(&self, now_ms: f64) -> String {
+ let elapsed = (now_ms - self.start_ms).max(1.0);
+ format!("frames={} bytes={} errors={} elapsed_ms={:.0}", self.frames, self.bytes, self.errors, elapsed)
+ }
+}
+
+// ─── v1.0: ACK Window ───
+
+pub struct AckWindow { window: Vec, base: u64 }
+
+impl AckWindow {
+ pub fn new(size: usize) -> Self { AckWindow { window: vec![false; size], base: 0 } }
+ pub fn ack(&mut self, seq: u64) -> bool {
+ if seq < self.base { return false; }
+ let idx = (seq - self.base) as usize;
+ if idx >= self.window.len() { return false; }
+ self.window[idx] = true;
+ true
+ }
+ pub fn advance(&mut self) -> u64 {
+ while !self.window.is_empty() && self.window[0] { self.window.remove(0); self.window.push(false); self.base += 1; }
+ self.base
+ }
+}
+
+// ─── v1.0: Codec Registry ───
+
+pub struct CodecRegistryV2 { codecs: Vec }
+
+impl CodecRegistryV2 {
+ pub fn new() -> Self { CodecRegistryV2 { codecs: Vec::new() } }
+ pub fn register(&mut self, name: &str) { if !self.codecs.contains(&name.to_string()) { self.codecs.push(name.to_string()); } }
+ pub fn has(&self, name: &str) -> bool { self.codecs.contains(&name.to_string()) }
+ pub fn list(&self) -> &[String] { &self.codecs }
+}
+
+impl Default for CodecRegistryV2 { fn default() -> Self { Self::new() } }
+
+// ─── v1.0: Flow Controller ───
+
+pub struct FlowControllerV2 { credits: i64, max_credits: i64 }
+
+impl FlowControllerV2 {
+ pub fn new(max_credits: i64) -> Self { FlowControllerV2 { credits: max_credits, max_credits } }
+ pub fn consume(&mut self, n: i64) -> bool { if self.credits >= n { self.credits -= n; true } else { false } }
+ pub fn replenish(&mut self, n: i64) { self.credits = (self.credits + n).min(self.max_credits); }
+ pub fn available(&self) -> i64 { self.credits }
+}
+
+// ─── v1.0: Version Negotiator ───
+
+pub struct VersionNegotiator { supported: Vec }
+
+impl VersionNegotiator {
+ pub fn new(supported: Vec) -> Self { VersionNegotiator { supported } }
+ pub fn negotiate(&self, requested: &str) -> String {
+ if self.supported.contains(&requested.to_string()) { requested.to_string() }
+ else { self.supported.last().cloned().unwrap_or_else(|| "1.0".to_string()) }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -4328,6 +5290,591 @@ mod tests {
let data = vec![0x00, 0x42, 0xFF];
assert_eq!(c.decrypt(&c.encrypt(&data)), data);
}
+
+ // ─── v0.60 Tests ───
+
+ #[test]
+ fn crc32_v60() {
+ let crc = Crc32Checker::checksum(b"hello");
+ assert!(Crc32Checker::verify(b"hello", crc));
+ assert!(!Crc32Checker::verify(b"hellp", crc));
+ }
+
+ #[test]
+ fn conn_state_v60() {
+ let mut csm = ConnectionStateMachine::new();
+ assert_eq!(*csm.state(), ConnState::Connecting);
+ csm.connect();
+ assert_eq!(*csm.state(), ConnState::Connected);
+ csm.disconnect();
+ assert_eq!(*csm.state(), ConnState::Disconnected);
+ csm.reconnect();
+ assert_eq!(csm.retries(), 1);
+ }
+
+ #[test]
+ fn packet_sequencer_v60() {
+ let mut ps = PacketSequencer::new();
+ assert_eq!(ps.next_seq(), 0);
+ assert_eq!(ps.next_seq(), 1);
+ ps.receive(5);
+ assert!(ps.gap_count() > 0);
+ }
+
+ #[test]
+ fn stream_resumer_v60() {
+ let mut sr = StreamResumer::new();
+ sr.mark(42);
+ assert_eq!(sr.resume_point(), 42);
+ }
+
+ #[test]
+ fn frame_interpolator_v60() {
+ assert!((FrameInterpolator::lerp(0.0, 100.0, 0.5) - 50.0).abs() < 0.01);
+ let result = FrameInterpolator::lerp_bytes(&[0, 100], &[100, 200], 0.5);
+ assert_eq!(result, vec![50, 150]);
+ }
+
+ #[test]
+ fn error_recovery_v60() {
+ let er = ErrorRecovery::new(5);
+ assert!((er.delay_ms(0) - 100.0).abs() < 0.01);
+ assert!((er.delay_ms(3) - 800.0).abs() < 0.01);
+ assert!(er.delay_ms(5) < 0.0); // exceeded max
+ }
+
+ #[test]
+ fn peer_router_v60() {
+ let mut pr = PeerRouter::new();
+ pr.send("alice", vec![1, 2]);
+ pr.send("bob", vec![3, 4]);
+ assert_eq!(pr.peer_count(), 2);
+ assert_eq!(pr.recv("alice"), Some(vec![1, 2]));
+ }
+
+ #[test]
+ fn stats_aggregator_v60() {
+ let mut sa = StatsAggregator::new(10);
+ for i in 1..=5 { sa.record(i as f64); }
+ assert!((sa.avg() - 3.0).abs() < 0.01);
+ assert!((sa.min() - 1.0).abs() < 0.01);
+ assert!((sa.max() - 5.0).abs() < 0.01);
+ }
+
+ #[test]
+ fn rle_compress_v60() {
+ let data = vec![0, 0, 0, 1, 1, 2];
+ let compressed = PacketCompressor::compress(&data);
+ let decompressed = PacketCompressor::decompress(&compressed);
+ assert_eq!(decompressed, data);
+ }
+
+ // ─── v0.70 Tests ───
+
+ #[test]
+ fn protocol_version_v70() {
+ let v1 = ProtocolVersion::new(0, 70, 0);
+ let v2 = ProtocolVersion::new(0, 71, 0);
+ assert!(v1.compatible(&v2));
+ assert_eq!(v1.to_string_v(), "0.70.0");
+ }
+
+ #[test]
+ fn qos_level_v70() {
+ assert_eq!(QosLevel::Realtime.priority(), 4);
+ assert!(QosLevel::Realtime > QosLevel::Normal);
+ assert_eq!(QosLevel::from_u8(3), QosLevel::High);
+ }
+
+ #[test]
+ fn frame_signer_v70() {
+ let signer = FrameSigner::new(vec![0xAB, 0xCD]);
+ let sig = signer.sign(b"hello");
+ assert!(signer.verify(b"hello", &sig));
+ assert!(!signer.verify(b"hellp", &sig));
+ }
+
+ #[test]
+ fn rate_limiter_v70() {
+ let mut rl = RateLimiter::new(2.0);
+ assert!(rl.check(0.0));
+ assert!(rl.check(0.0));
+ assert!(!rl.check(0.0));
+ assert!(rl.check(1000.0)); // new window
+ }
+
+ #[test]
+ fn stream_splitter_v70() {
+ let chunks = StreamSplitter::split(&[1, 2, 3, 4, 5, 6], 3);
+ assert_eq!(chunks.len(), 3);
+ assert_eq!(chunks[0], vec![1, 2]);
+ }
+
+ #[test]
+ fn watermark_v70() {
+ let mut wm = WatermarkTracker::new(10.0, 90.0);
+ wm.update(50.0);
+ assert_eq!(wm.status(), "normal");
+ wm.update(95.0);
+ assert_eq!(wm.status(), "high");
+ wm.update(5.0);
+ assert_eq!(wm.status(), "low");
+ }
+
+ #[test]
+ fn packet_reorderer_v70() {
+ let mut pr = PacketReorderer::new();
+ pr.push(1, vec![2]);
+ pr.push(0, vec![1]);
+ assert_eq!(pr.pop(), Some(vec![1]));
+ assert_eq!(pr.pop(), Some(vec![2]));
+ }
+
+ #[test]
+ fn timeout_monitor_v70() {
+ let mut tm = TimeoutMonitor::new(100.0);
+ tm.start(1, 0.0);
+ tm.start(2, 50.0);
+ let expired = tm.check_expired(150.0);
+ assert!(expired.contains(&1));
+ }
+
+ #[test]
+ fn delta_encoder_v70() {
+ let prev = vec![10, 20, 30];
+ let cur = vec![12, 22, 30];
+ let delta = DeltaEncoder::encode(&prev, &cur);
+ let decoded = DeltaEncoder::decode(&prev, &delta);
+ assert_eq!(decoded, cur);
+ }
+
+ // ─── v0.75 Tests ───
+
+ #[test]
+ fn journal_v75() {
+ let mut j = StreamJournal::new();
+ j.append("frame_sent");
+ j.append("ack_recv");
+ assert_eq!(j.len(), 2);
+ assert_eq!(j.get(0), Some("frame_sent"));
+ }
+
+ #[test]
+ fn hot_config_v75() {
+ let mut c = HotConfigV2::new();
+ c.set("fps", "30");
+ assert_eq!(c.get("fps"), Some("30"));
+ c.set("fps", "60");
+ assert_eq!(c.generation(), 2);
+ }
+
+ #[test]
+ fn dedup_v2_v75() {
+ let mut d = FrameDedupV2::new(100);
+ assert!(!d.is_dup(b"frame1"));
+ assert!(d.is_dup(b"frame1"));
+ assert!(!d.is_dup(b"frame2"));
+ }
+
+ #[test]
+ fn histogram_v75() {
+ let mut h = MetricHistogram::new(&[10.0, 50.0, 100.0]);
+ h.record(5.0);
+ h.record(25.0);
+ h.record(75.0);
+ let p50 = h.percentile(50.0);
+ assert!(p50 <= 50.0);
+ }
+
+ #[test]
+ fn circuit_breaker_v75() {
+ let mut cb = CircuitBreaker::new(3);
+ assert!(cb.is_allowed());
+ cb.record_failure();
+ cb.record_failure();
+ cb.record_failure();
+ assert!(!cb.is_allowed());
+ assert_eq!(*cb.state(), BreakerState::Open);
+ cb.try_half_open();
+ assert_eq!(*cb.state(), BreakerState::HalfOpen);
+ cb.record_success();
+ assert_eq!(*cb.state(), BreakerState::Closed);
+ }
+
+ #[test]
+ fn token_bucket_v2_v75() {
+ let mut tb = TokenBucketV2::new(10.0, 5.0);
+ assert!(tb.take(5.0, 0.0));
+ assert!(tb.take(5.0, 0.0));
+ assert!(!tb.take(5.0, 0.0));
+ assert!(tb.take(5.0, 1000.0)); // refilled
+ }
+
+ #[test]
+ fn batch_accumulator_v75() {
+ let mut ba = BatchAccumulator::new(3, 1000);
+ assert!(!ba.add(vec![1]));
+ assert!(!ba.add(vec![2]));
+ assert!(ba.add(vec![3])); // threshold
+ let flushed = ba.flush();
+ assert_eq!(flushed.len(), 3);
+ }
+
+ #[test]
+ fn checksum_chain_v75() {
+ let mut cc = ChecksumChain::new();
+ let c1 = cc.feed(b"hello");
+ let c2 = cc.feed(b"world");
+ assert_ne!(c1, c2); // chain evolves
+ }
+
+ #[test]
+ fn ping_monitor_v75() {
+ let mut pm = PingMonitor::new(0.2);
+ pm.record(100.0);
+ pm.record(50.0);
+ assert!(pm.smoothed_rtt() > 0.0);
+ assert_eq!(pm.sample_count(), 2);
+ }
+
+ // ─── v0.80 Tests ───
+
+ #[test]
+ fn abr_controller_v80() {
+ let mut abr = AbrController::new(vec![500, 1000, 2000, 4000]);
+ assert_eq!(abr.current_bitrate(), 4000);
+ abr.adjust(10.0, 300.0); // bad
+ assert!(abr.current_bitrate() < 4000);
+ }
+
+ #[test]
+ fn jitter_buffer_v80() {
+ let mut jb = JitterBufferV2::new(2);
+ jb.push(vec![1]);
+ jb.push(vec![2]);
+ assert_eq!(jb.pop(), None); // not enough buffered
+ jb.push(vec![3]);
+ assert_eq!(jb.pop(), Some(vec![1]));
+ }
+
+ #[test]
+ fn frame_counter_v80() {
+ let mut fc = FrameCounter::new(1000.0);
+ fc.tick(0.0, 100);
+ fc.tick(16.0, 100);
+ assert!(fc.fps() >= 2.0);
+ }
+
+ #[test]
+ fn event_bus_v80() {
+ let mut eb = EventBus::new();
+ eb.emit("connect");
+ eb.emit("data");
+ assert_eq!(eb.count(), 2);
+ let events = eb.drain();
+ assert_eq!(events.len(), 2);
+ assert_eq!(eb.count(), 0);
+ }
+
+ #[test]
+ fn stream_tee_v80() {
+ let tee = StreamTee::new(3);
+ let copies = tee.tee(b"hello");
+ assert_eq!(copies.len(), 3);
+ assert_eq!(copies[0], copies[1]);
+ }
+
+ #[test]
+ fn gain_controller_v80() {
+ let mut gc = GainController::new(0.3);
+ let out = gc.apply(50.0, 100.0);
+ assert!(out > 50.0); // should be amplified
+ }
+
+ #[test]
+ fn lossy_queue_v80() {
+ let mut lq = LossyQueue::new(2);
+ lq.push(vec![1]);
+ lq.push(vec![2]);
+ lq.push(vec![3]); // drops [1]
+ assert_eq!(lq.len(), 2);
+ assert_eq!(lq.pop(), Some(vec![2]));
+ }
+
+ #[test]
+ fn header_builder_v80() {
+ let hdr = HeaderBuilder::build(42, 1, 0x80);
+ assert_eq!(hdr.len(), 6);
+ let (seq, ch, flags) = HeaderBuilder::parse(&hdr).unwrap();
+ assert_eq!(seq, 42);
+ assert_eq!(ch, 1);
+ assert_eq!(flags, 0x80);
+ }
+
+ #[test]
+ fn session_tracker_v80() {
+ let mut st = SessionTracker::new(1001, 0.0);
+ st.touch(5000.0);
+ assert_eq!(st.duration_ms(), 5000.0);
+ assert_eq!(st.id(), 1001);
+ }
+
+ // ─── v0.90 Tests ───
+
+ #[test]
+ fn xor_cipher_v2_v90() {
+ let cipher = XorCipherV2::new(vec![0xAA, 0xBB]);
+ let enc = cipher.apply(b"hello");
+ let dec = cipher.apply(&enc);
+ assert_eq!(dec, b"hello");
+ }
+
+ #[test]
+ fn channel_router_v90() {
+ let mut cr = ChannelRouter::new();
+ cr.route(1, vec![10]);
+ cr.route(2, vec![20]);
+ cr.route(1, vec![11]);
+ assert_eq!(cr.channel_count(), 2);
+ let ch1 = cr.drain(1);
+ assert_eq!(ch1.len(), 2);
+ }
+
+ #[test]
+ fn ack_tracker_v90() {
+ let mut at = AckTracker::new();
+ at.send(0);
+ at.send(1);
+ assert_eq!(at.pending(), 2);
+ at.ack(0);
+ assert!(at.is_acked(0));
+ assert_eq!(at.pending(), 1);
+ }
+
+ #[test]
+ fn frame_pool_v90() {
+ let mut fp = FramePoolV2::new(1024, 4);
+ assert_eq!(fp.available(), 4);
+ let buf = fp.alloc();
+ assert_eq!(fp.available(), 3);
+ fp.free(buf);
+ assert_eq!(fp.available(), 4);
+ }
+
+ #[test]
+ fn bandwidth_estimator_v90() {
+ let mut be = BandwidthEstimatorV2::new(0.3);
+ be.sample(1000, 100.0); // 80kbps
+ assert!(be.estimate() > 0.0);
+ }
+
+ #[test]
+ fn priority_mux_v90() {
+ let mut mux = PriorityMux::new();
+ mux.push(1, vec![0x01]);
+ mux.push(3, vec![0x03]);
+ mux.push(2, vec![0x02]);
+ assert_eq!(mux.pop(), Some(vec![0x03])); // highest priority first
+ }
+
+ #[test]
+ fn nonce_generator_v90() {
+ let mut ng = NonceGenerator::new();
+ assert_eq!(ng.next(), 1);
+ assert_eq!(ng.next(), 2);
+ assert_eq!(ng.current(), 2);
+ }
+
+ #[test]
+ fn stream_validator_v90() {
+ let mut sv = StreamValidator::new();
+ assert!(sv.validate(0));
+ assert!(sv.validate(1));
+ assert!(!sv.validate(5)); // gap
+ assert_eq!(sv.error_count(), 1);
+ }
+
+ #[test]
+ fn retry_queue_v90() {
+ let mut rq = RetryQueue::new(3);
+ rq.push(vec![1, 2]);
+ assert_eq!(rq.pending(), 1);
+ let data = rq.pop_for_retry().unwrap();
+ assert_eq!(data, vec![1, 2]);
+ rq.remove_acked(&[1, 2]);
+ assert_eq!(rq.pending(), 0);
+ }
+
+ // ─── v0.95 Tests ───
+
+ #[test]
+ fn lz4_lite_v95() {
+ let data = vec![0u8; 50];
+ let compressed = Lz4Lite::compress(&data);
+ assert!(compressed.len() < data.len());
+ let decompressed = Lz4Lite::decompress(&compressed);
+ assert_eq!(decompressed, data);
+ }
+
+ #[test]
+ fn telemetry_sink_v95() {
+ let mut ts = TelemetrySink::new();
+ ts.emit("fps", 60.0);
+ ts.emit("bitrate", 5000.0);
+ assert_eq!(ts.count(), 2);
+ let events = ts.drain();
+ assert_eq!(events.len(), 2);
+ }
+
+ #[test]
+ fn frame_differ_v95() {
+ let a = vec![10, 20, 30];
+ let b = vec![10, 25, 30];
+ let diff = FrameDiffer::diff(&a, &b);
+ let restored = FrameDiffer::apply(&a, &diff);
+ assert_eq!(restored, b);
+ }
+
+ #[test]
+ fn backoff_timer_v95() {
+ let mut bt = BackoffTimer::new(100.0, 10000.0);
+ assert_eq!(bt.next(), 100.0);
+ assert_eq!(bt.next(), 200.0);
+ assert_eq!(bt.next(), 400.0);
+ bt.reset();
+ assert_eq!(bt.next(), 100.0);
+ }
+
+ #[test]
+ fn stream_mirror_v95() {
+ let mut sm = StreamMirror::new();
+ sm.push(vec![1, 2]);
+ assert_eq!(sm.pop_primary(), Some(vec![1, 2]));
+ assert_eq!(sm.pop_mirror(), Some(vec![1, 2]));
+ }
+
+ #[test]
+ fn quota_manager_v95() {
+ let mut qm = QuotaManager::new(100);
+ assert!(qm.consume(50));
+ assert!(qm.consume(50));
+ assert!(!qm.consume(1));
+ qm.reset();
+ assert_eq!(qm.remaining(), 100);
+ }
+
+ #[test]
+ fn heartbeat_v2_v95() {
+ let mut hb = HeartbeatV2::new();
+ hb.ping(100.0);
+ hb.pong(115.0);
+ assert_eq!(hb.rtt(), 15.0);
+ assert!(hb.is_alive(120.0, 30.0));
+ }
+
+ #[test]
+ fn tag_filter_v95() {
+ let mut tf = TagFilter::new(vec!["video".into()]);
+ assert!(tf.accept("video"));
+ assert!(!tf.accept("audio"));
+ tf.add("audio");
+ assert!(tf.accept("audio"));
+ }
+
+ #[test]
+ fn moving_average_v95() {
+ let mut ma = MovingAverage::new(3);
+ ma.push(10.0);
+ ma.push(20.0);
+ ma.push(30.0);
+ assert!((ma.get() - 20.0).abs() < 0.01);
+ ma.push(40.0); // drops 10
+ assert!((ma.get() - 30.0).abs() < 0.01);
+ }
+
+ // ─── v1.0 Tests ───
+
+ #[test]
+ fn pipeline_v100() {
+ let mut p = StreamPipeline::new();
+ p.add("encode");
+ p.add("compress");
+ assert_eq!(p.count(), 2);
+ }
+
+ #[test]
+ fn protocol_header_v100() {
+ let hdr = ProtocolHeader::encode(1, 0x80, 42);
+ assert_eq!(hdr.len(), 7);
+ let (v, f, s) = ProtocolHeader::decode(&hdr).unwrap();
+ assert_eq!(v, 1);
+ assert_eq!(f, 0x80);
+ assert_eq!(s, 42);
+ }
+
+ #[test]
+ fn frame_splitter_v2_v100() {
+ let sp = FrameSplitterV2::new(3);
+ let chunks = sp.split(&[1, 2, 3, 4, 5]);
+ assert_eq!(chunks.len(), 2);
+ let reassembled = FrameSplitterV2::reassemble(&chunks);
+ assert_eq!(reassembled, vec![1, 2, 3, 4, 5]);
+ }
+
+ #[test]
+ fn congestion_window_v100() {
+ let mut cw = CongestionWindowV2::new();
+ let w1 = cw.on_ack(); // slow start: 2.0
+ assert_eq!(w1, 2.0);
+ cw.on_loss();
+ assert_eq!(cw.window(), 1.0);
+ }
+
+ #[test]
+ fn stream_stats_v2_v100() {
+ let mut ss = StreamStatsV2::new(0.0);
+ ss.record(1000);
+ ss.record(2000);
+ ss.error();
+ let s = ss.summary(5000.0);
+ assert!(s.contains("frames=2"));
+ }
+
+ #[test]
+ fn ack_window_v100() {
+ let mut aw = AckWindow::new(8);
+ assert!(aw.ack(0));
+ assert!(aw.ack(1));
+ let base = aw.advance();
+ assert_eq!(base, 2);
+ }
+
+ #[test]
+ fn codec_registry_v100() {
+ let mut cr = CodecRegistryV2::new();
+ cr.register("webp");
+ cr.register("jpeg");
+ cr.register("webp"); // dup
+ assert_eq!(cr.list().len(), 2);
+ assert!(cr.has("webp"));
+ }
+
+ #[test]
+ fn flow_controller_v100() {
+ let mut fc = FlowControllerV2::new(100);
+ assert!(fc.consume(50));
+ assert!(fc.consume(50));
+ assert!(!fc.consume(1));
+ fc.replenish(50);
+ assert_eq!(fc.available(), 50);
+ }
+
+ #[test]
+ fn version_negotiator_v100() {
+ let vn = VersionNegotiator::new(vec!["0.9".into(), "1.0".into()]);
+ assert_eq!(vn.negotiate("1.0"), "1.0");
+ assert_eq!(vn.negotiate("2.0"), "1.0"); // fallback
+ }
}
@@ -4342,5 +5889,12 @@ mod tests {
+
+
+
+
+
+
+