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 { + + + + + + +