Skip to main content

pfx_load/scene/
play.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use pfx_scene::Entry;
6use pfx_scene::types as file;
7
8use super::build::{Places, Reader, Step, inner};
9use super::{Object, SceneError, SceneMesh, multiply};
10use crate::sha256;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Physics {
14    pub gravity: [f32; 3],
15    pub rate: f32,
16    pub substeps: u32,
17    pub iterations: u32,
18}
19
20impl Default for Physics {
21    fn default() -> Self {
22        Self {
23            gravity: [0.0, -9.81, 0.0],
24            rate: 60.0,
25            substeps: 1,
26            iterations: 4,
27        }
28    }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum BodyKind {
33    Dynamic,
34    Kinematic,
35    Fixed,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum BodyShape {
40    Box { half: [f32; 3] },
41    Sphere { radius: f32 },
42    Capsule { half_height: f32, radius: f32 },
43    Cylinder { half_height: f32, radius: f32 },
44}
45
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct Body {
48    pub kind: BodyKind,
49    pub shape: BodyShape,
50    pub offset: [f32; 3],
51    pub density: f32,
52    pub friction: f32,
53    pub restitution: f32,
54    pub velocity: [f32; 3],
55    pub spin: [f32; 3],
56    pub gravity_scale: f32,
57    pub damping: [f32; 2],
58    pub ccd: bool,
59}
60
61#[derive(Clone, Debug, PartialEq)]
62pub struct Trigger {
63    pub shape: BodyShape,
64    pub offset: [f32; 3],
65    pub layer: Option<String>,
66    pub mask: Option<Vec<String>>,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum CharacterPlane {
71    Xy,
72    Yz,
73}
74
75#[derive(Clone, Debug, PartialEq)]
76pub struct Character {
77    pub plane: Option<CharacterPlane>,
78    pub radius: f32,
79    pub height: f32,
80    pub max_climb: f32,
81    pub max_step: f32,
82    pub snap: f32,
83    pub coyote: u32,
84    pub jump_buffer: u32,
85    pub jump_speed: f32,
86    pub variable_jump: f32,
87    pub wall_slide: Option<f32>,
88    pub wall_jump: Option<[f32; 2]>,
89    pub layer: Option<String>,
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum Cue {
94    Start,
95    Hit,
96    Game,
97}
98
99#[derive(Clone, Debug, PartialEq)]
100pub struct Sound {
101    pub file: PathBuf,
102    pub bytes: Arc<Vec<u8>>,
103    pub hash: [u8; 32],
104    pub volume: f32,
105    pub pan: f32,
106    pub looping: bool,
107    pub cue: Cue,
108    pub object: Option<String>,
109}
110
111pub(super) fn build_physics(file: &file::Physics) -> Physics {
112    let base = Physics::default();
113    Physics {
114        gravity: file.gravity.unwrap_or(base.gravity),
115        rate: file.rate.unwrap_or(base.rate),
116        substeps: file.substeps.unwrap_or(base.substeps),
117        iterations: file.iterations.unwrap_or(base.iterations),
118    }
119}
120
121pub(super) fn build_sound(
122    places: &Places,
123    entry: &Entry<file::Sound>,
124    reader: &mut Reader,
125) -> Result<Sound, SceneError> {
126    let name = entry.key.as_str();
127    let steps = [Step::Key("sound"), Step::Key(inner(name))];
128    let sound = &entry.value;
129    let path = places.root.join(&sound.file);
130    let bytes = reader
131        .read(&path)
132        .map_err(|message| places.refusal(&entry.file, &steps, message))?;
133    if bytes.starts_with(b"OggS") {
134        if let Err(message) = check_vorbis(&bytes) {
135            return places.error(
136                &entry.file,
137                &steps,
138                format!("sound {name}: {} {message}", sound.file),
139            );
140        }
141    } else if bytes.len() < 12 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
142        return places.error(
143            &entry.file,
144            &steps,
145            format!(
146                "sound {name}: {} is not a WAV or an Ogg Vorbis file",
147                sound.file
148            ),
149        );
150    }
151    Ok(Sound {
152        file: path,
153        hash: sha256(&bytes),
154        bytes: Arc::new(bytes),
155        volume: sound.volume.unwrap_or(1.0),
156        pan: sound.pan.unwrap_or(0.0),
157        looping: sound.looping,
158        cue: match sound.play.unwrap_or_default() {
159            file::Cue::Start => Cue::Start,
160            file::Cue::Hit => Cue::Hit,
161            file::Cue::Game => Cue::Game,
162        },
163        object: sound.object.clone(),
164    })
165}
166
167fn check_vorbis(bytes: &[u8]) -> Result<(), String> {
168    let mut at = 0usize;
169    let mut first = true;
170    let mut frames = 0i64;
171    let mut rate = 0u32;
172    while at < bytes.len() {
173        let head = bytes
174            .get(at..at + 27)
175            .filter(|head| &head[0..4] == b"OggS" && head[4] == 0)
176            .ok_or("is a damaged Ogg file")?;
177        let segments = usize::from(head[26]);
178        let table = bytes
179            .get(at + 27..at + 27 + segments)
180            .ok_or("is a truncated Ogg file")?;
181        let body_len: usize = table.iter().map(|len| usize::from(*len)).sum();
182        let body_at = at + 27 + segments;
183        let body = bytes
184            .get(body_at..body_at + body_len)
185            .ok_or("is a truncated Ogg file")?;
186        if first {
187            if body.len() < 30 || &body[0..7] != b"\x01vorbis" {
188                return Err("is an Ogg file with no Vorbis stream".into());
189            }
190            let version = u32::from_le_bytes([body[7], body[8], body[9], body[10]]);
191            let channels = body[11];
192            rate = u32::from_le_bytes([body[12], body[13], body[14], body[15]]);
193            if version != 0 || channels == 0 || rate == 0 {
194                return Err("has a Vorbis header with no audio".into());
195            }
196            first = false;
197        }
198        let granule = i64::from_le_bytes([
199            head[6], head[7], head[8], head[9], head[10], head[11], head[12], head[13],
200        ]);
201        if granule > 0 {
202            frames = granule;
203        }
204        at = body_at + body_len;
205    }
206    if first {
207        return Err("is not a WAV or an Ogg Vorbis file".into());
208    }
209    if frames == 0 || rate == 0 {
210        return Err("is an Ogg Vorbis file with no duration".into());
211    }
212    Ok(())
213}
214
215pub(super) fn build_bodies(
216    places: &Places,
217    entries: &[Entry<file::Object>],
218    objects: &[Object],
219    meshes: &BTreeMap<String, SceneMesh>,
220) -> Result<BTreeMap<String, Body>, SceneError> {
221    let mut out = BTreeMap::new();
222    for (entry, object) in entries.iter().zip(objects) {
223        let Some(file) = &entry.value.body else {
224            continue;
225        };
226        let name = entry.key.as_str();
227        let steps = [Step::Named("object", inner(name)), Step::Key("body")];
228        let Some(mesh) = meshes.get(&object.mesh) else {
229            return places.error(
230                &entry.file,
231                &steps,
232                format!("object {name} has a body and no mesh"),
233            );
234        };
235        let body = build_body(name, file, object, mesh)
236            .map_err(|message| places.refusal(&entry.file, &steps, message))?;
237        out.insert(name.to_string(), body);
238    }
239    Ok(out)
240}
241
242pub(super) fn build_triggers(
243    places: &Places,
244    entries: &[Entry<file::Object>],
245    objects: &[Object],
246    meshes: &BTreeMap<String, SceneMesh>,
247) -> Result<BTreeMap<String, Trigger>, SceneError> {
248    let mut out = BTreeMap::new();
249    for (entry, object) in entries.iter().zip(objects) {
250        let Some(file) = &entry.value.trigger else {
251            continue;
252        };
253        let name = entry.key.as_str();
254        let steps = [Step::Named("object", inner(name)), Step::Key("trigger")];
255        let Some(mesh) = meshes.get(&object.mesh) else {
256            return places.error(
257                &entry.file,
258                &steps,
259                format!("object {name} has a trigger and no mesh"),
260            );
261        };
262        let (shape, offset) = volume(
263            name,
264            "trigger",
265            Volume {
266                shape: file.shape.unwrap_or_default(),
267                half: file.half,
268                radius: file.radius,
269                half_height: file.half_height,
270                offset: file.offset,
271            },
272            object,
273            mesh,
274        )
275        .map_err(|message| places.refusal(&entry.file, &steps, message))?;
276        out.insert(
277            name.to_string(),
278            Trigger {
279                shape,
280                offset,
281                layer: file.layer.clone(),
282                mask: file.mask.clone(),
283            },
284        );
285    }
286    Ok(out)
287}
288
289pub(super) fn build_characters(
290    places: &Places,
291    entries: &[Entry<file::Object>],
292    objects: &[Object],
293    meshes: &BTreeMap<String, SceneMesh>,
294) -> Result<BTreeMap<String, Character>, SceneError> {
295    let mut out = BTreeMap::new();
296    for (entry, object) in entries.iter().zip(objects) {
297        let Some(file) = &entry.value.character else {
298            continue;
299        };
300        let name = entry.key.as_str();
301        let steps = [Step::Named("object", inner(name)), Step::Key("character")];
302        let Some(mesh) = meshes.get(&object.mesh) else {
303            return places.error(
304                &entry.file,
305                &steps,
306                format!("object {name} has a character and no mesh"),
307            );
308        };
309        let character = build_character(name, file, object, mesh)
310            .map_err(|message| places.refusal(&entry.file, &steps, message))?;
311        out.insert(name.to_string(), character);
312    }
313    Ok(out)
314}
315
316fn build_character(
317    name: &str,
318    file: &file::Character,
319    object: &Object,
320    mesh: &SceneMesh,
321) -> Result<Character, String> {
322    let (low, high) = bounds(mesh);
323    let scale = object.scale.map(f32::abs);
324    let half: [f32; 3] = std::array::from_fn(|axis| (high[axis] - low[axis]) * 0.5 * scale[axis]);
325    let radius = file.radius.unwrap_or(half[0].max(half[2]));
326    let height = file.height.unwrap_or(half[1] * 2.0);
327    if !(radius.is_finite() && radius > 0.0 && height.is_finite() && height > 0.0) {
328        return Err(format!(
329            "object {name}: its character has no size; give it a positive radius and height"
330        ));
331    }
332    if height < radius * 2.0 {
333        return Err(format!(
334            "object {name}: its character's height {height} is less than twice its radius {radius}"
335        ));
336    }
337    let plane = match file.kind.unwrap_or_default() {
338        file::CharacterKind::ThreeD => None,
339        file::CharacterKind::TwoD => Some(match file.plane.unwrap_or_default() {
340            file::Plane::Xy => CharacterPlane::Xy,
341            file::Plane::Yz => CharacterPlane::Yz,
342        }),
343    };
344    Ok(Character {
345        plane,
346        radius,
347        height,
348        max_climb: file.max_climb.unwrap_or(file::Character::MAX_CLIMB),
349        max_step: file.max_step.unwrap_or(file::Character::MAX_STEP),
350        snap: file.snap.unwrap_or(file::Character::SNAP),
351        coyote: file.coyote.unwrap_or(file::Character::COYOTE),
352        jump_buffer: file.jump_buffer.unwrap_or(file::Character::JUMP_BUFFER),
353        jump_speed: file.jump_speed.unwrap_or(file::Character::JUMP_SPEED),
354        variable_jump: file.variable_jump.unwrap_or(file::Character::VARIABLE_JUMP),
355        wall_slide: file.wall_slide,
356        wall_jump: file.wall_jump,
357        layer: file.layer.clone(),
358    })
359}
360
361pub(super) fn body_layers(entries: &[Entry<file::Object>]) -> BTreeMap<String, String> {
362    entries
363        .iter()
364        .filter_map(|entry| {
365            let layer = entry.value.body.as_ref()?.layer.clone()?;
366            Some((entry.key.clone(), layer))
367        })
368        .collect()
369}
370
371fn build_body(
372    name: &str,
373    file: &file::Body,
374    object: &Object,
375    mesh: &SceneMesh,
376) -> Result<Body, String> {
377    let kind = match file.kind.unwrap_or_default() {
378        file::BodyKind::Dynamic => BodyKind::Dynamic,
379        file::BodyKind::Kinematic => BodyKind::Kinematic,
380        file::BodyKind::Fixed => BodyKind::Fixed,
381    };
382    let (shape, offset) = volume(
383        name,
384        "body",
385        Volume {
386            shape: file.shape.unwrap_or_default(),
387            half: file.half,
388            radius: file.radius,
389            half_height: file.half_height,
390            offset: file.offset,
391        },
392        object,
393        mesh,
394    )?;
395    Ok(Body {
396        kind,
397        shape,
398        offset,
399        density: file.density.unwrap_or(file::Body::DENSITY),
400        friction: file.friction.unwrap_or(file::Body::FRICTION),
401        restitution: file.restitution.unwrap_or(file::Body::RESTITUTION),
402        velocity: file.velocity.unwrap_or([0.0; 3]),
403        spin: file.spin.unwrap_or([0.0; 3]),
404        gravity_scale: file.gravity_scale.unwrap_or(file::Body::GRAVITY_SCALE),
405        damping: file.damping.unwrap_or([0.0; 2]),
406        ccd: file.ccd,
407    })
408}
409
410struct Volume {
411    shape: file::BodyShape,
412    half: Option<[f32; 3]>,
413    radius: Option<f32>,
414    half_height: Option<f32>,
415    offset: Option<[f32; 3]>,
416}
417
418fn volume(
419    name: &str,
420    what: &str,
421    file: Volume,
422    object: &Object,
423    mesh: &SceneMesh,
424) -> Result<(BodyShape, [f32; 3]), String> {
425    let (low, high) = bounds(mesh);
426    let scale = object.scale.map(f32::abs);
427    let half: [f32; 3] = std::array::from_fn(|axis| (high[axis] - low[axis]) * 0.5 * scale[axis]);
428    let centre: [f32; 3] =
429        std::array::from_fn(|axis| (high[axis] + low[axis]) * 0.5 * object.scale[axis]);
430    let round = half[0].max(half[2]);
431    let (shape, shape_name) = match file.shape {
432        file::BodyShape::Box => (
433            BodyShape::Box {
434                half: file.half.unwrap_or(half),
435            },
436            "box",
437        ),
438        file::BodyShape::Sphere => (
439            BodyShape::Sphere {
440                radius: file
441                    .radius
442                    .unwrap_or_else(|| half[0].max(half[1]).max(half[2])),
443            },
444            "sphere",
445        ),
446        file::BodyShape::Capsule => {
447            let radius = file.radius.unwrap_or(round);
448            (
449                BodyShape::Capsule {
450                    half_height: file
451                        .half_height
452                        .unwrap_or_else(|| (half[1] - radius).max(0.0)),
453                    radius,
454                },
455                "capsule",
456            )
457        }
458        file::BodyShape::Cylinder => (
459            BodyShape::Cylinder {
460                half_height: file.half_height.unwrap_or(half[1]),
461                radius: file.radius.unwrap_or(round),
462            },
463            "cylinder",
464        ),
465    };
466    let sizes = match shape {
467        BodyShape::Box { half } => half.to_vec(),
468        BodyShape::Sphere { radius } => vec![radius],
469        BodyShape::Capsule {
470            half_height,
471            radius,
472        } => vec![half_height + radius, radius],
473        BodyShape::Cylinder {
474            half_height,
475            radius,
476        } => vec![half_height, radius],
477    };
478    if sizes.iter().any(|size| !(size.is_finite() && *size > 0.0)) {
479        return Err(format!(
480            "object {name}: its {what} has a {shape_name} with no size; give it a positive size"
481        ));
482    }
483    Ok((shape, file.offset.unwrap_or(centre)))
484}
485
486fn bounds(mesh: &SceneMesh) -> ([f32; 3], [f32; 3]) {
487    let mut low = [f32::INFINITY; 3];
488    let mut high = [f32::NEG_INFINITY; 3];
489    for part in &mesh.parts {
490        for position in &part.geometry.positions {
491            let point = multiply(
492                part.transform,
493                [
494                    [position[0], position[1], position[2], 1.0],
495                    [0.0; 4],
496                    [0.0; 4],
497                    [0.0; 4],
498                ],
499            )[0];
500            for axis in 0..3 {
501                low[axis] = low[axis].min(point[axis]);
502                high[axis] = high[axis].max(point[axis]);
503            }
504        }
505    }
506    if low[0] > high[0] {
507        return ([0.0; 3], [0.0; 3]);
508    }
509    (low, high)
510}