owned_lab/
lib.rs

1//! Playable first-party reference scene. Platform code consumes the same Rust state on native/Web.
2mod peer;
3use ferravex_core::FixedClock;
4use ferravex_engine::play::particles::{ParticleDesc, ParticlePool};
5use ferravex_engine::{
6    audio::{Clip, Mixer},
7    dynamics::{BodyDesc, BodyHandle, PhysicsWorld, Shape},
8    replay::{Frame, Replay, checksum},
9};
10use ferravex_fps::{Arena, GameEvent, InputFrame, Match, forward};
11use ferravex_graphics::{
12    Camera, ClipDepth, Color, Mat4, Mesh, MeshCache, MeshId, RenderItem, SoftwareRenderer, Vec3,
13    vec3,
14};
15use std::sync::Mutex;
16#[derive(Clone)]
17struct Object {
18    mesh: MeshId,
19    position: Vec3,
20    scale: Vec3,
21    solid: Option<usize>,
22}
23pub struct Lab {
24    pub game: Match,
25    pub cache: MeshCache,
26    objects: Vec<Object>,
27    physics: PhysicsWorld<3>,
28    balls: Vec<(BodyHandle, MeshId)>,
29    terrain: Vec<BodyHandle>,
30    clock: FixedClock,
31    time: f32,
32    pub camera: Camera,
33    pub items: Vec<RenderItem>,
34    pub instances: Vec<f32>,
35    pub shadow_instances: Vec<f32>,
36    pub matrix: [f32; 16],
37    pub light: [f32; 16],
38    pub status: [f32; 11],
39    meshes: Vec<MeshId>,
40    mesh_data: Vec<Vec<f32>>,
41    mixer: Mixer,
42    pub audio: Vec<[f32; 2]>,
43    shot: Clip,
44    replay: Replay,
45    player_mesh: MeshId,
46    head_mesh: MeshId,
47    pub kills: u32,
48    recording: bool,
49    export: Vec<u8>,
50    particles: ParticlePool,
51    peer: peer::Peer,
52    network_out: Vec<u8>,
53    network_in: Vec<u8>,
54}
55impl Default for Lab {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60impl Lab {
61    pub fn new() -> Self {
62        let mut cache = MeshCache::new();
63        let colors = [
64            0xcbbfa6, 0xa29071, 0x586b67, 0x8c613c, 0x3d4547, 0xc58658, 0xe4d7bd, 0x6b7650,
65            0x4d6875, 0xd3b56b,
66        ];
67        let mut meshes: Vec<_> = colors
68            .into_iter()
69            .map(|c| {
70                cache
71                    .insert(Mesh::cuboid(Vec3::ONE, Color::from_hex(c)))
72                    .unwrap()
73            })
74            .collect();
75        let player_mesh = meshes[8];
76        let head_mesh = cache
77            .insert(Mesh::sphere(0.5, 4, 6, Color::from_hex(0xc5ad8b)))
78            .unwrap();
79        meshes.push(head_mesh);
80        let round = cache
81            .insert(Mesh::sphere(1., 5, 8, Color::from_hex(0xd3b56b)))
82            .unwrap();
83        meshes.push(round);
84        let mut objects = Vec::new();
85        let mut add = |material: usize, p: [f32; 3], s: [f32; 3], solid| {
86            objects.push(Object {
87                mesh: meshes[material],
88                position: p.into(),
89                scale: s.into(),
90                solid,
91            })
92        };
93        add(0, [0., -0.25, 0.], [48., 0.5, 56.], None);
94        let arena = Arena::default();
95        for (i, wall) in arena.solids.iter().enumerate() {
96            add(
97                if i < 4 { 0 } else { 2 },
98                wall.center.to_array(),
99                (wall.half_extents * 2.).to_array(),
100                Some(i),
101            );
102        }
103        // Layered stone facades and blue-gray shutters give simple geometry a believable scale.
104        for side in [-1., 1.] {
105            for z in [-21., -12., -3., 6., 15., 24.] {
106                add(1, [side * 21.95, 2.8, z], [0.18, 0.2, 7.5], None);
107                add(6, [side * 21.85, 5.8, z], [0.3, 0.35, 8.5], None);
108                add(4, [side * 21.84, 3.9, z], [0.18, 1.55, 1.3], None);
109                add(2, [side * 21.7, 3.9, z], [0.18, 1.35, 1.1], None);
110                add(1, [side * 21.7, 3.0, z], [0.5, 0.15, 1.7], None);
111            }
112        }
113        for x in [-18., -12., -6., 0., 6., 12., 18.] {
114            add(6, [x, 5.9, -25.9], [5.8, 0.4, 0.4], None);
115            add(2, [x, 4.4, -25.85], [1.45, 1.4, 0.2], None);
116        }
117        // Timber cargo, inset doors, lintels, terracotta and a striped shade.
118        for (x, z) in [
119            (-16., 8.),
120            (-14., 8.),
121            (-16., 10.),
122            (13., -5.),
123            (14., -7.),
124            (-5., -18.),
125        ] {
126            add(3, [x, 0.65, z], [1.35, 1.3, 1.35], None);
127            for y in [0.16, 1.12] {
128                add(1, [x, y, z + 0.69], [1.4, 0.09, 0.1], None);
129            }
130        }
131        for x in [-12., 12.] {
132            add(4, [x, 1.5, -25.8], [2.5, 3., 0.25], None);
133            add(1, [x, 3.1, -25.6], [3., 0.35, 0.45], None);
134        }
135        for i in 0..8 {
136            add(
137                if i % 2 == 0 { 0 } else { 5 },
138                [-18. + i as f32 * 0.55, 4.2, -8.],
139                [0.55, 0.09, 5.],
140                None,
141            );
142        }
143        for z in [-10., -6.] {
144            add(3, [-18., 2., z], [0.13, 4., 0.13], None);
145        }
146        add(9, [7., 0.012, 18.], [6., 0.025, 0.13], None);
147        add(9, [10., 0.012, 15.], [0.13, 0.025, 6.], None);
148        let mut physics = PhysicsWorld::<3>::new([0., -9.81, 0.]);
149        physics
150            .insert(BodyDesc::fixed(
151                Shape::cuboid([24., 0.5, 28.]),
152                [0., -0.5, 0.],
153            ))
154            .unwrap();
155        let terrain = arena
156            .solids
157            .iter()
158            .map(|wall| {
159                physics
160                    .insert(BodyDesc::fixed(
161                        Shape::cuboid(wall.half_extents.to_array()),
162                        wall.center.to_array(),
163                    ))
164                    .unwrap()
165            })
166            .collect();
167        let mut balls = Vec::new();
168        for i in 0..6 {
169            let mut desc = BodyDesc::dynamic(
170                Shape::ball(0.32),
171                [5. + i as f32 * 0.74, 3. + i as f32 * 0.5, 15.],
172                1.,
173            );
174            desc.restitution = 0.65;
175            desc.ccd = true;
176            let id = physics.insert(desc).unwrap();
177            balls.push((id, round));
178        }
179        let mesh_data = meshes
180            .iter()
181            .map(|id| {
182                let mesh = cache.get(*id).unwrap();
183                mesh.indices
184                    .iter()
185                    .flat_map(|&i| {
186                        let v = mesh.vertices[i as usize];
187                        v.position
188                            .to_array()
189                            .into_iter()
190                            .chain(v.normal.to_array())
191                            .chain(v.color.to_array())
192                    })
193                    .collect()
194            })
195            .collect();
196        let mut game = Match::new(arena);
197        for id in 0..5 {
198            game.spawn_player(id);
199        }
200        let camera = Camera::perspective(
201            vec3(-12., 10., 24.),
202            vec3(0., 1., 0.),
203            Vec3::Y,
204            1.05,
205            16. / 9.,
206            0.08,
207            120.,
208        );
209        let mut app = Self {
210            game,
211            cache,
212            objects,
213            physics,
214            balls,
215            terrain,
216            clock: FixedClock::new(64., 8),
217            time: 0.,
218            camera,
219            items: Vec::with_capacity(256),
220            instances: Vec::with_capacity(4096),
221            shadow_instances: Vec::with_capacity(4096),
222            matrix: [0.; 16],
223            light: (Mat4::orthographic_rh_gl(-40., 40., -40., 40., 1., 140.)
224                * Mat4::look_at_rh(vec3(-30., 51., 24.), Vec3::ZERO, Vec3::Y))
225            .to_cols_array(),
226            status: [0.; 11],
227            meshes,
228            mesh_data,
229            mixer: Mixer::new(48000, 32).unwrap(),
230            audio: Vec::new(),
231            shot: Clip::tone(48000, 95., 0.09).unwrap(),
232            replay: Replay::default(),
233            player_mesh,
234            head_mesh,
235            kills: 0,
236            recording: false,
237            export: Vec::new(),
238            particles: ParticlePool::new(96),
239            peer: peer::Peer::default(),
240            network_out: Vec::new(),
241            network_in: vec![0; 1200],
242        };
243        app.rebuild(16. / 9., 0);
244        app
245    }
246    pub fn update(&mut self, seconds: f64, input: InputFrame, view: u32, aspect: f32) {
247        let step = self.clock.advance(seconds);
248        for _ in 0..step.steps {
249            self.time += step.dt;
250            let command = self.peer.command(input.sanitized());
251            let mut inputs = vec![(0, command)];
252            self.peer.elapsed += step.dt;
253            if self.peer.mode == 1 {
254                if let Some(remote) = self.peer.queue.pop_latest() {
255                    self.peer.last = remote;
256                    self.peer.acknowledged = true;
257                }
258                let remote = if self.peer.elapsed < 2. {
259                    self.peer.last
260                } else {
261                    InputFrame::default()
262                };
263                inputs.push((1, remote));
264            }
265            for bot in self
266                .game
267                .players
268                .iter()
269                .filter(|p| p.id > 0 && !(self.peer.mode == 1 && p.id == 1))
270            {
271                let phase = self.time * 0.3 + bot.id as f32 * 1.4;
272                inputs.push((
273                    bot.id,
274                    InputFrame {
275                        move_x: phase.sin() * 0.7,
276                        move_z: 0.25,
277                        yaw: phase.sin() * 2.,
278                        ..Default::default()
279                    },
280                ));
281            }
282            let events = if self.peer.mode == 2 {
283                let arena = &self.game.arena;
284                if ferravex_fps::step_player(
285                    self.game.players.iter_mut().find(|p| p.id == 0).unwrap(),
286                    command,
287                    arena,
288                ) {
289                    self.mixer.play(
290                        self.shot.clone(),
291                        self.game.player(0).unwrap().eye_position().to_array(),
292                        0.8,
293                        false,
294                    );
295                }
296                self.game.tick = self.game.tick.wrapping_add(1);
297                Vec::new()
298            } else {
299                self.game.step(&inputs)
300            };
301            for event in events {
302                match event {
303                    GameEvent::Shot { origin, end, .. } => {
304                        self.mixer
305                            .play(self.shot.clone(), origin.to_array(), 0.8, false);
306                        for i in 0..6 {
307                            let phase = i as f32 * 2.4 + self.time;
308                            let _ = self.particles.spawn(ParticleDesc {
309                                position: end.to_array(),
310                                velocity: [phase.sin() * 2., 1.8, phase.cos() * 2.],
311                                acceleration: [0., -8., 0.],
312                                lifetime: 0.4,
313                                start_size: 0.07,
314                                end_size: 0.01,
315                                ..Default::default()
316                            });
317                        }
318                    }
319                    GameEvent::Killed { attacker: 0, .. } => self.kills += 1,
320                    _ => {}
321                }
322            }
323            self.physics.step(step.dt).unwrap();
324            self.particles.update(step.dt).unwrap();
325            if self.recording {
326                let player = self.game.player(0).unwrap();
327                let bytes: Vec<_> = player
328                    .position
329                    .to_array()
330                    .iter()
331                    .flat_map(|v| v.to_le_bytes())
332                    .collect();
333                self.replay.record(Frame {
334                    tick: u64::from(self.game.tick),
335                    input: inputs[0].1,
336                    state_hash: checksum(&bytes),
337                });
338            }
339        }
340        self.rebuild(aspect, view);
341        let count = if seconds.is_finite() {
342            (seconds.clamp(0., 0.1) * 48000.) as usize
343        } else {
344            0
345        };
346        self.audio.resize(count, [0.; 2]);
347        self.mixer.listener = self.camera.position.to_array();
348        self.mixer.right = vec3(input.yaw.cos(), 0., input.yaw.sin()).to_array();
349        self.mixer.render(&mut self.audio);
350    }
351    fn rebuild(&mut self, aspect: f32, view: u32) {
352        let aspect = if aspect.is_finite() {
353            aspect.clamp(0.2, 5.)
354        } else {
355            16. / 9.
356        };
357        let player = self.game.player(0).unwrap();
358        self.camera = match view {
359            1 => {
360                let eye = player.eye_position();
361                Camera::perspective(
362                    eye,
363                    eye + forward(player.yaw, player.pitch),
364                    Vec3::Y,
365                    1.15,
366                    aspect,
367                    0.06,
368                    120.,
369                )
370            }
371            2 => Camera {
372                view_projection: Mat4::orthographic_rh(
373                    -31. * aspect,
374                    31. * aspect,
375                    -31.,
376                    31.,
377                    0.1,
378                    100.,
379                ) * Mat4::look_at_rh(vec3(0., 60., 0.), Vec3::ZERO, -Vec3::Z),
380                position: vec3(0., 60., 0.),
381                clip_depth: ClipDepth::ZeroToOne,
382            },
383            _ => {
384                let angle = self.time * 0.025;
385                let eye = vec3(-13. + angle.sin() * 8., 11., 24.);
386                Camera::perspective(eye, vec3(0., 1., 0.), Vec3::Y, 1., aspect, 0.08, 120.)
387            }
388        };
389        self.items.clear();
390        for o in &self.objects {
391            self.items.push(RenderItem::new(
392                o.mesh,
393                Mat4::from_scale_rotation_translation(
394                    o.scale,
395                    ferravex_graphics::Quat::IDENTITY,
396                    o.position,
397                ),
398            ));
399        }
400        for (handle, mesh) in &self.balls {
401            let p = self.physics.body(*handle).unwrap().position();
402            self.items.push(RenderItem::new(
403                *mesh,
404                Mat4::from_scale_rotation_translation(
405                    Vec3::splat(0.32),
406                    ferravex_graphics::Quat::IDENTITY,
407                    p.into(),
408                ),
409            ));
410        }
411        for p in self
412            .game
413            .players
414            .iter()
415            .filter(|p| p.is_alive() && (view != 1 || p.id != 0))
416        {
417            let rot = ferravex_graphics::Quat::from_rotation_y(-p.yaw);
418            let sway = (self.time * 8. + p.id as f32).sin() * 0.06;
419            for (offset, size, mesh) in [
420                (vec3(0., 1., 0.), vec3(0.56, 0.72, 0.32), self.player_mesh),
421                (vec3(0., 1.65, 0.), Vec3::splat(0.35), self.head_mesh),
422                (
423                    vec3(-0.17, 0.34 + sway, 0.),
424                    vec3(0.19, 0.68, 0.22),
425                    self.meshes[4],
426                ),
427                (
428                    vec3(0.17, 0.34 - sway, 0.),
429                    vec3(0.19, 0.68, 0.22),
430                    self.meshes[4],
431                ),
432                (
433                    vec3(0.34, 1.12, -0.2),
434                    vec3(0.12, 0.16, 0.65),
435                    self.meshes[4],
436                ),
437            ] {
438                self.items.push(RenderItem::new(
439                    mesh,
440                    Mat4::from_scale_rotation_translation(size, rot, p.position + rot * offset),
441                ));
442            }
443        }
444        for p in self.particles.iter() {
445            self.items.push(RenderItem::new(
446                self.meshes[9],
447                Mat4::from_scale_rotation_translation(
448                    Vec3::splat(p.size),
449                    ferravex_graphics::Quat::IDENTITY,
450                    p.position.into(),
451                ),
452            ));
453        }
454        if view == 1 {
455            let rot = ferravex_graphics::Quat::from_rotation_y(-player.yaw)
456                * ferravex_graphics::Quat::from_rotation_x(player.pitch);
457            let bob = (self.time * 10.).sin() * player.velocity.length().min(7.) * 0.0008;
458            let eye = player.eye_position();
459            for (offset, size, material) in [
460                (vec3(0.22, -0.22 + bob, -0.43), vec3(0.12, 0.15, 0.35), 4),
461                (vec3(0.22, -0.18 + bob, -0.68), vec3(0.045, 0.045, 0.32), 4),
462                (vec3(0.22, -0.32 + bob, -0.37), vec3(0.08, 0.16, 0.09), 3),
463            ] {
464                self.items.push(RenderItem::new(
465                    self.meshes[material],
466                    Mat4::from_scale_rotation_translation(size, rot, eye + rot * offset),
467                ));
468            }
469        }
470        self.shadow_instances.clear();
471        for item in &self.items {
472            self.shadow_instances.push(item.mesh.index as f32);
473            self.shadow_instances.extend(item.transform.to_cols_array());
474        }
475        self.instances.clear();
476        let frustum = self.camera.frustum().unwrap();
477        for item in &self.items {
478            if frustum.intersects_bounds(self.cache.bounds(item.mesh).unwrap(), item.transform) {
479                self.instances.push(item.mesh.index as f32);
480                self.instances.extend(item.transform.to_cols_array());
481            }
482        }
483        self.matrix = self.camera.view_projection.to_cols_array();
484        self.status = [
485            player.health as f32,
486            player.ammo as f32,
487            self.kills as f32,
488            self.game.tick as f32,
489            (self.instances.len() / 17) as f32,
490            self.items.len() as f32,
491            player.reload_ticks as f32,
492            self.time,
493            self.camera.position.x,
494            self.camera.position.y,
495            self.camera.position.z,
496        ];
497    }
498    pub fn render(&self, width: u32, height: u32) -> SoftwareRenderer {
499        let mut renderer = SoftwareRenderer::new(width, height).unwrap();
500        renderer.settings.clear_color = Color::from_hex(0xa9b8ba);
501        renderer.settings.ambient = 0.5;
502        renderer.settings.diffuse = 0.5;
503        renderer.settings.fog = Some(ferravex_graphics::Fog {
504            color: Color::from_hex(0xa9b8ba),
505            start: 40.,
506            end: 120.,
507        });
508        renderer
509            .render(&self.cache, &self.items, self.camera)
510            .unwrap();
511        renderer
512    }
513    pub fn edit(&mut self, index: usize, x: f32, y: f32, z: f32) -> bool {
514        if index == 0
515            || index >= self.objects.len()
516            || ![x, y, z].iter().all(|v| v.is_finite() && v.abs() < 100.)
517        {
518            return false;
519        }
520        let o = &mut self.objects[index];
521        o.position = vec3(x, y, z);
522        if let Some(i) = o.solid {
523            self.game.arena.solids[i].center = o.position;
524            self.physics
525                .body_mut(self.terrain[i])
526                .unwrap()
527                .set_position(o.position.to_array())
528                .unwrap();
529        }
530        true
531    }
532    pub fn object_count(&self) -> usize {
533        self.objects.len()
534    }
535    pub fn object(&self, index: usize) -> Option<[f32; 6]> {
536        self.objects.get(index).map(|o| {
537            [
538                o.position.x,
539                o.position.y,
540                o.position.z,
541                o.scale.x,
542                o.scale.y,
543                o.scale.z,
544            ]
545        })
546    }
547    pub fn replay_bytes(&self) -> Vec<u8> {
548        self.replay.encode()
549    }
550}
551static LAB: Mutex<Option<Lab>> = Mutex::new(None);
552#[unsafe(no_mangle)]
553pub extern "C" fn ferravex_button_mask(kind: u32) -> u32 {
554    match kind {
555        0 => u32::from(ferravex_fps::BUTTON_FIRE),
556        1 => u32::from(ferravex_fps::BUTTON_JUMP),
557        2 => u32::from(ferravex_fps::BUTTON_RELOAD),
558        _ => 0,
559    }
560}
561#[unsafe(no_mangle)]
562pub extern "C" fn ferravex_init() {
563    *LAB.lock().unwrap() = Some(Lab::new());
564}
565#[unsafe(no_mangle)]
566pub extern "C" fn ferravex_step(
567    dt: f64,
568    x: f32,
569    z: f32,
570    yaw: f32,
571    pitch: f32,
572    buttons: u32,
573    view: u32,
574    aspect: f32,
575) {
576    if let Some(lab) = LAB.lock().unwrap().as_mut() {
577        lab.update(
578            dt,
579            InputFrame {
580                move_x: x,
581                move_z: z,
582                yaw,
583                pitch,
584                buttons: buttons as u8,
585                ..Default::default()
586            },
587            view,
588            aspect,
589        );
590    }
591}
592macro_rules! buffer {
593    ($ptr:ident,$len:ident,$field:ident,$ty:ty) => {
594        #[unsafe(no_mangle)]
595        pub extern "C" fn $ptr() -> *const $ty {
596            LAB.lock().unwrap().as_ref().unwrap().$field.as_ptr()
597        }
598        #[unsafe(no_mangle)]
599        pub extern "C" fn $len() -> usize {
600            LAB.lock().unwrap().as_ref().unwrap().$field.len()
601        }
602    };
603}
604buffer!(
605    ferravex_shadow_instances,
606    ferravex_shadow_instances_len,
607    shadow_instances,
608    f32
609);
610buffer!(ferravex_instances, ferravex_instances_len, instances, f32);
611buffer!(ferravex_matrix, ferravex_matrix_len, matrix, f32);
612buffer!(ferravex_light, ferravex_light_len, light, f32);
613buffer!(ferravex_status, ferravex_status_len, status, f32);
614buffer!(ferravex_audio, ferravex_audio_len, audio, [f32; 2]);
615#[unsafe(no_mangle)]
616pub extern "C" fn ferravex_mesh_count() -> usize {
617    LAB.lock().unwrap().as_ref().unwrap().mesh_data.len()
618}
619#[unsafe(no_mangle)]
620pub extern "C" fn ferravex_mesh(index: usize) -> *const f32 {
621    LAB.lock().unwrap().as_ref().unwrap().mesh_data[index].as_ptr()
622}
623#[unsafe(no_mangle)]
624pub extern "C" fn ferravex_mesh_len(index: usize) -> usize {
625    LAB.lock().unwrap().as_ref().unwrap().mesh_data[index].len()
626}
627#[unsafe(no_mangle)]
628pub extern "C" fn ferravex_object_count() -> usize {
629    LAB.lock().unwrap().as_ref().unwrap().object_count()
630}
631#[unsafe(no_mangle)]
632pub extern "C" fn ferravex_object(index: usize, axis: usize) -> f32 {
633    LAB.lock()
634        .unwrap()
635        .as_ref()
636        .unwrap()
637        .object(index)
638        .and_then(|v| v.get(axis).copied())
639        .unwrap_or(0.)
640}
641#[unsafe(no_mangle)]
642pub extern "C" fn ferravex_edit(index: usize, x: f32, y: f32, z: f32) -> u32 {
643    u32::from(LAB.lock().unwrap().as_mut().unwrap().edit(index, x, y, z))
644}
645#[unsafe(no_mangle)]
646pub extern "C" fn ferravex_record(enabled: u32) {
647    LAB.lock().unwrap().as_mut().unwrap().recording = enabled != 0;
648}
649#[unsafe(no_mangle)]
650pub extern "C" fn ferravex_export_replay() -> usize {
651    let mut lock = LAB.lock().unwrap();
652    let lab = lock.as_mut().unwrap();
653    lab.export = lab.replay_bytes();
654    lab.export.len()
655}
656#[unsafe(no_mangle)]
657pub extern "C" fn ferravex_replay_bytes() -> *const u8 {
658    LAB.lock().unwrap().as_ref().unwrap().export.as_ptr()
659}
660#[unsafe(no_mangle)]
661pub extern "C" fn ferravex_peer_mode(mode: u32) {
662    LAB.lock().unwrap().as_mut().unwrap().peer.set_mode(mode);
663}
664#[unsafe(no_mangle)]
665pub extern "C" fn ferravex_peer_out() -> *const u8 {
666    let mut lock = LAB.lock().unwrap();
667    let lab = lock.as_mut().unwrap();
668    lab.network_out = lab.peer.packet(&lab.game);
669    lab.network_out.as_ptr()
670}
671#[unsafe(no_mangle)]
672pub extern "C" fn ferravex_peer_out_len() -> usize {
673    LAB.lock().unwrap().as_ref().unwrap().network_out.len()
674}
675#[unsafe(no_mangle)]
676pub extern "C" fn ferravex_peer_in() -> *mut u8 {
677    LAB.lock()
678        .unwrap()
679        .as_mut()
680        .unwrap()
681        .network_in
682        .as_mut_ptr()
683}
684#[unsafe(no_mangle)]
685pub extern "C" fn ferravex_peer_receive(len: usize) -> u32 {
686    let mut lock = LAB.lock().unwrap();
687    let lab = lock.as_mut().unwrap();
688    if len > 1200 {
689        return 0;
690    }
691    u32::from(lab.peer.receive(&lab.network_in[..len], &mut lab.game))
692}
693#[cfg(test)]
694mod tests {
695    use super::*;
696    #[test]
697    fn scene_runs_with_owned_physics_and_renders_nonempty() {
698        let mut lab = Lab::new();
699        for _ in 0..640 {
700            lab.update(1. / 64., InputFrame::default(), 0, 16. / 9.);
701        }
702        assert_eq!(lab.game.tick, 640);
703        assert_eq!(lab.status[0], 100.);
704        let renderer = lab.render(160, 90);
705        assert!(
706            renderer
707                .framebuffer
708                .depths()
709                .iter()
710                .filter(|d| d.is_finite())
711                .count()
712                > 2000
713        );
714        assert!(!lab.edit(usize::MAX, 0., 0., 0.));
715    }
716    #[test]
717    fn movement_recording_and_editor_apply_real_state() {
718        let mut lab = Lab::new();
719        lab.recording = true;
720        let start = lab.game.player(0).unwrap().position;
721        for _ in 0..64 {
722            lab.update(
723                1. / 64.,
724                InputFrame {
725                    move_z: 1.,
726                    ..Default::default()
727                },
728                1,
729                1.,
730            );
731        }
732        assert!((lab.game.player(0).unwrap().position - start).length() > 5.);
733        assert_eq!(
734            Replay::decode(&lab.replay_bytes()).unwrap().frames().len(),
735            64
736        );
737        let wall = lab.objects.iter().position(|o| o.solid == Some(0)).unwrap();
738        assert!(lab.edit(wall, -20., 3., 0.));
739        assert_eq!(lab.game.arena.solids[0].center.x, -20.);
740    }
741}