Skip to main content

pfx_play/
rigs.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use pfx_core::rig::{Bob, Bounds, Follow, FollowTuning, Person, PersonTuning, Rig, Third};
5use pfx_load::scene::project_root;
6use serde::Deserialize;
7
8pub const FILE: &str = "rigs.toml";
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Kind {
12    Follow2d,
13    Follow3d,
14    FirstPerson,
15    ThirdPerson,
16}
17
18impl Kind {
19    pub fn word(self) -> &'static str {
20        match self {
21            Kind::Follow2d => "follow2d",
22            Kind::Follow3d => "follow3d",
23            Kind::FirstPerson => "first_person",
24            Kind::ThirdPerson => "third_person",
25        }
26    }
27
28    fn of(word: &str) -> Option<Kind> {
29        Some(match word {
30            "follow2d" => Kind::Follow2d,
31            "follow3d" => Kind::Follow3d,
32            "first_person" => Kind::FirstPerson,
33            "third_person" => Kind::ThirdPerson,
34            _ => return None,
35        })
36    }
37
38    fn follows(self) -> bool {
39        matches!(self, Kind::Follow2d | Kind::Follow3d)
40    }
41}
42
43#[derive(Clone, Debug, PartialEq)]
44pub struct RigDesc {
45    pub name: String,
46    pub kind: Kind,
47    pub target: Option<String>,
48    pub look: String,
49    pub mouse: bool,
50    pub blend: f32,
51    pub collide: u32,
52    pub height: Option<f32>,
53    pub fit_view: bool,
54    pub rig: Rig,
55}
56
57#[derive(Clone, Debug, Default, PartialEq)]
58pub struct Rigs {
59    file: Option<PathBuf>,
60    list: BTreeMap<String, RigDesc>,
61}
62
63#[derive(Deserialize)]
64#[serde(untagged)]
65enum Numbers {
66    One(f32),
67    Many(Vec<f32>),
68}
69
70#[derive(Deserialize)]
71#[serde(deny_unknown_fields)]
72struct BobEntry {
73    stride: Option<f32>,
74    vertical: Option<f32>,
75    sway: Option<f32>,
76    full_speed: Option<f32>,
77    ease: Option<f32>,
78}
79
80#[derive(Deserialize)]
81#[serde(deny_unknown_fields)]
82struct ThirdEntry {
83    distance: Option<f32>,
84    radius: Option<f32>,
85    margin: Option<f32>,
86    min_distance: Option<f32>,
87    recover: Option<f32>,
88    shoulder: Option<Numbers>,
89}
90
91#[derive(Deserialize)]
92#[serde(deny_unknown_fields)]
93struct BoundsEntry {
94    min: Numbers,
95    max: Numbers,
96}
97
98#[derive(Deserialize)]
99#[serde(deny_unknown_fields)]
100struct Entry {
101    name: String,
102    kind: String,
103    target: Option<String>,
104    look: Option<String>,
105    mouse: Option<bool>,
106    blend: Option<f32>,
107    collide: Option<u32>,
108    offset: Option<Numbers>,
109    arm: Option<Numbers>,
110    dead_zone: Option<Numbers>,
111    look_ahead: Option<f32>,
112    look_ahead_speed: Option<f32>,
113    look_ahead_damping: Option<f32>,
114    damping: Option<Numbers>,
115    bounds: Option<BoundsEntry>,
116    handover: Option<f32>,
117    pixels_per_unit: Option<f32>,
118    height: Option<f32>,
119    fit_view: Option<bool>,
120    eye: Option<Numbers>,
121    yaw: Option<f32>,
122    pitch: Option<f32>,
123    mouse_sensitivity: Option<f32>,
124    stick_sensitivity: Option<f32>,
125    invert_x: Option<bool>,
126    invert_y: Option<bool>,
127    smoothing: Option<f32>,
128    pitch_min: Option<f32>,
129    pitch_max: Option<f32>,
130    bob: Option<BobEntry>,
131    third: Option<ThirdEntry>,
132    view: Option<String>,
133}
134
135#[derive(Deserialize)]
136#[serde(deny_unknown_fields)]
137struct File {
138    #[serde(default)]
139    rig: Vec<Entry>,
140}
141
142fn vec3(value: &Numbers, fill: f32) -> Result<[f32; 3], String> {
143    let parts = match value {
144        Numbers::One(value) => vec![*value; 3],
145        Numbers::Many(parts) => parts.clone(),
146    };
147    let out = match parts.as_slice() {
148        [x, y] => [*x, *y, fill],
149        [x, y, z] => [*x, *y, *z],
150        _ => return Err("needs 2 or 3 numbers".into()),
151    };
152    if out.iter().any(|part| !part.is_finite()) {
153        return Err("needs finite numbers".into());
154    }
155    Ok(out)
156}
157
158fn pair(value: &Numbers) -> Result<[f32; 2], String> {
159    match value {
160        Numbers::Many(parts) if parts.len() == 2 && parts.iter().all(|part| part.is_finite()) => {
161            Ok([parts[0], parts[1]])
162        }
163        _ => Err("needs 2 numbers".into()),
164    }
165}
166
167fn number(value: f32, least: f32) -> Result<f32, String> {
168    if value.is_finite() && value >= least {
169        Ok(value)
170    } else {
171        Err(format!("needs a number of {least} or more"))
172    }
173}
174
175fn above(value: f32) -> Result<f32, String> {
176    if value.is_finite() && value > 0.0 {
177        Ok(value)
178    } else {
179        Err("needs a number above 0".into())
180    }
181}
182
183fn field<T>(key: &str, result: Result<T, String>) -> Result<T, String> {
184    result.map_err(|message| format!("{key}: {message}"))
185}
186
187fn follows_only(entry: &Entry) -> Option<&'static str> {
188    [
189        ("dead_zone", entry.dead_zone.is_some()),
190        ("look_ahead", entry.look_ahead.is_some()),
191        ("look_ahead_speed", entry.look_ahead_speed.is_some()),
192        ("look_ahead_damping", entry.look_ahead_damping.is_some()),
193        ("damping", entry.damping.is_some()),
194        ("bounds", entry.bounds.is_some()),
195        ("arm", entry.arm.is_some()),
196        ("pixels_per_unit", entry.pixels_per_unit.is_some()),
197        ("height", entry.height.is_some()),
198        ("fit_view", entry.fit_view.is_some()),
199    ]
200    .into_iter()
201    .find_map(|(key, given)| given.then_some(key))
202}
203
204fn people_only(entry: &Entry) -> Option<&'static str> {
205    [
206        ("eye", entry.eye.is_some()),
207        ("yaw", entry.yaw.is_some()),
208        ("pitch", entry.pitch.is_some()),
209        ("mouse_sensitivity", entry.mouse_sensitivity.is_some()),
210        ("stick_sensitivity", entry.stick_sensitivity.is_some()),
211        ("invert_x", entry.invert_x.is_some()),
212        ("invert_y", entry.invert_y.is_some()),
213        ("smoothing", entry.smoothing.is_some()),
214        ("pitch_min", entry.pitch_min.is_some()),
215        ("pitch_max", entry.pitch_max.is_some()),
216        ("bob", entry.bob.is_some()),
217        ("third", entry.third.is_some()),
218        ("view", entry.view.is_some()),
219    ]
220    .into_iter()
221    .find_map(|(key, given)| given.then_some(key))
222}
223
224fn build(entry: Entry) -> Result<RigDesc, String> {
225    let kind = Kind::of(&entry.kind).ok_or_else(|| {
226        format!(
227            "kind: {:?} is not follow2d, follow3d, first_person or third_person",
228            entry.kind
229        )
230    })?;
231    if kind.follows() {
232        if let Some(key) = people_only(&entry) {
233            return Err(format!("{key}: a {} rig has no {key}", kind.word()));
234        }
235    } else if let Some(key) = follows_only(&entry) {
236        return Err(format!("{key}: a {} rig has no {key}", kind.word()));
237    }
238    let plane = kind == Kind::Follow2d;
239    if !plane
240        && let Some(key) = [
241            ("height", entry.height.is_some()),
242            ("pixels_per_unit", entry.pixels_per_unit.is_some()),
243            ("fit_view", entry.fit_view.is_some()),
244        ]
245        .into_iter()
246        .find_map(|(key, given)| given.then_some(key))
247    {
248        return Err(format!("{key}: only a follow2d rig has {key}"));
249    }
250    let blend = field("blend", number(entry.blend.unwrap_or(0.4), 0.0))?;
251    let look = entry.look.clone().unwrap_or_else(|| "look".into());
252    let mouse = entry.mouse.unwrap_or(!kind.follows());
253    let collide = entry.collide.unwrap_or(u32::MAX);
254    let offset = |value: &Option<Numbers>, key: &str, default: [f32; 3]| match value {
255        Some(value) => field(key, vec3(value, 0.0)),
256        None => Ok(default),
257    };
258    let rig = if kind.follows() {
259        let mut tuning = if plane {
260            FollowTuning::follow2()
261        } else {
262            FollowTuning::follow3()
263        };
264        tuning.offset = offset(&entry.offset, "offset", tuning.offset)?;
265        tuning.arm = offset(&entry.arm, "arm", tuning.arm)?;
266        let zone = offset(&entry.dead_zone, "dead_zone", tuning.dead_zone)?;
267        for part in zone {
268            field("dead_zone", number(part, 0.0))?;
269        }
270        tuning.dead_zone = zone;
271        let damping = offset(&entry.damping, "damping", tuning.damping)?;
272        for part in damping {
273            field("damping", number(part, 0.0))?;
274        }
275        tuning.damping = damping;
276        if let Some(value) = entry.look_ahead {
277            tuning.look_ahead = field("look_ahead", number(value, 0.0))?;
278        }
279        if let Some(value) = entry.look_ahead_speed {
280            tuning.look_ahead_speed = field("look_ahead_speed", above(value))?;
281        }
282        if let Some(value) = entry.look_ahead_damping {
283            tuning.look_ahead_damping = field("look_ahead_damping", number(value, 0.0))?;
284        }
285        if let Some(value) = entry.handover {
286            tuning.handover = field("handover", number(value, 0.0))?;
287        }
288        if let Some(value) = entry.pixels_per_unit {
289            tuning.pixels_per_unit = Some(field("pixels_per_unit", above(value))?);
290        }
291        if let Some(bounds) = &entry.bounds {
292            let min = field("bounds.min", vec3(&bounds.min, 0.0))?;
293            let max = field("bounds.max", vec3(&bounds.max, 0.0))?;
294            if (0..3).any(|axis| min[axis] > max[axis]) {
295                return Err("bounds: min is above max".into());
296            }
297            tuning.bounds = Some(Bounds::new(min, max));
298        }
299        Rig::Follow(Follow::new(tuning))
300    } else {
301        let mut tuning = PersonTuning {
302            eye: offset(&entry.eye, "eye", PersonTuning::default().eye)?,
303            ..PersonTuning::default()
304        };
305        if let Some(value) = entry.yaw {
306            tuning.yaw = field("yaw", number(value, f32::MIN))?;
307        }
308        if let Some(value) = entry.pitch {
309            tuning.pitch = field("pitch", number(value, f32::MIN))?;
310        }
311        if let Some(value) = entry.mouse_sensitivity {
312            tuning.mouse = field("mouse_sensitivity", above(value))?;
313        }
314        if let Some(value) = entry.stick_sensitivity {
315            tuning.stick = field("stick_sensitivity", above(value))?;
316        }
317        tuning.invert_x = entry.invert_x.unwrap_or(false);
318        tuning.invert_y = entry.invert_y.unwrap_or(false);
319        if let Some(value) = entry.smoothing {
320            tuning.smoothing = field("smoothing", number(value, 0.0))?;
321        }
322        let low = entry.pitch_min.unwrap_or(tuning.pitch_range[0]);
323        let high = entry.pitch_max.unwrap_or(tuning.pitch_range[1]);
324        if !(low.is_finite() && high.is_finite() && low < high) {
325            return Err("pitch_min: needs numbers with pitch_min below pitch_max".into());
326        }
327        tuning.pitch_range = [low, high];
328        if let Some(value) = entry.handover {
329            tuning.handover = field("handover", number(value, 0.0))?;
330        }
331        if let Some(bob) = &entry.bob {
332            let mut set = Bob::default();
333            if let Some(value) = bob.stride {
334                set.stride = field("bob.stride", above(value))?;
335            }
336            if let Some(value) = bob.vertical {
337                set.vertical = field("bob.vertical", number(value, 0.0))?;
338            }
339            if let Some(value) = bob.sway {
340                set.sway = field("bob.sway", number(value, 0.0))?;
341            }
342            if let Some(value) = bob.full_speed {
343                set.full_speed = field("bob.full_speed", above(value))?;
344            }
345            if let Some(value) = bob.ease {
346                set.ease = field("bob.ease", number(value, 0.0))?;
347            }
348            tuning.bob = Some(set);
349        }
350        if let Some(third) = &entry.third {
351            if kind == Kind::FirstPerson {
352                return Err("third: a first_person rig has no third".into());
353            }
354            tuning.third = Some(third_of(third)?);
355        } else if kind == Kind::ThirdPerson {
356            tuning.third = Some(Third::default());
357        }
358        let mut person = Person::new(tuning);
359        let third = match entry.view.as_deref() {
360            Some("first") => false,
361            Some("third") => true,
362            None => kind == Kind::ThirdPerson,
363            Some(other) => return Err(format!("view: {other:?} is not first or third")),
364        };
365        person.set_third(third);
366        Rig::Person(person)
367    };
368    let height = match entry.height {
369        Some(value) => Some(field("height", above(value))?),
370        None => plane.then_some(10.0),
371    };
372    Ok(RigDesc {
373        name: entry.name,
374        kind,
375        target: entry.target,
376        look,
377        mouse,
378        blend,
379        collide,
380        height,
381        fit_view: entry.fit_view.unwrap_or(false),
382        rig,
383    })
384}
385
386fn third_of(entry: &ThirdEntry) -> Result<Third, String> {
387    let mut third = Third::default();
388    if let Some(value) = entry.distance {
389        third.distance = field("third.distance", above(value))?;
390    }
391    if let Some(value) = entry.radius {
392        third.radius = field("third.radius", number(value, 0.0))?;
393    }
394    if let Some(value) = entry.margin {
395        third.margin = field("third.margin", number(value, 0.0))?;
396    }
397    if let Some(value) = entry.min_distance {
398        third.min_distance = field("third.min_distance", number(value, 0.0))?;
399    }
400    if let Some(value) = entry.recover {
401        third.recover = field("third.recover", number(value, 0.0))?;
402    }
403    if let Some(value) = &entry.shoulder {
404        third.shoulder = field("third.shoulder", pair(value))?;
405    }
406    Ok(third)
407}
408
409impl Rigs {
410    pub fn parse(text: &str, file: &Path) -> Result<Rigs, String> {
411        let shown = file.display();
412        let parsed: File = toml::from_str(text).map_err(|error| format!("{shown}: {error}"))?;
413        let mut list = BTreeMap::new();
414        for entry in parsed.rig {
415            let name = entry.name.clone();
416            if name.is_empty() {
417                return Err(format!("{shown}: [[rig]] needs a name"));
418            }
419            let desc =
420                build(entry).map_err(|message| format!("{shown}: [[rig]] {name}: {message}"))?;
421            if list.insert(name.clone(), desc).is_some() {
422                return Err(format!("{shown}: [[rig]] {name}: two rigs share the name"));
423            }
424        }
425        Ok(Rigs {
426            file: Some(file.to_path_buf()),
427            list,
428        })
429    }
430
431    pub fn open(file: &Path) -> Result<Rigs, String> {
432        let text = std::fs::read_to_string(file)
433            .map_err(|error| format!("{}: {error}", file.display()))?;
434        Self::parse(&text, file)
435    }
436
437    pub fn find(root: &Path) -> Option<PathBuf> {
438        let named = std::fs::read_to_string(root.join("project.toml"))
439            .ok()
440            .and_then(|text| toml::from_str::<toml::Table>(&text).ok())
441            .and_then(|table| table.get("rigs")?.as_str().map(PathBuf::from));
442        let file = root.join(named.unwrap_or_else(|| PathBuf::from(FILE)));
443        Some(file).filter(|file| file.is_file())
444    }
445
446    pub fn of_scene(scene: &Path) -> Result<Rigs, String> {
447        match Self::find(&project_root(scene)) {
448            Some(file) => Self::open(&file),
449            None => Ok(Rigs::default()),
450        }
451    }
452
453    pub fn file(&self) -> Option<&Path> {
454        self.file.as_deref()
455    }
456
457    pub fn get(&self, name: &str) -> Option<&RigDesc> {
458        self.list.get(name)
459    }
460
461    pub fn len(&self) -> usize {
462        self.list.len()
463    }
464
465    pub fn is_empty(&self) -> bool {
466        self.list.is_empty()
467    }
468
469    pub(crate) fn into_map(self) -> BTreeMap<String, RigDesc> {
470        self.list
471    }
472
473    pub fn iter(&self) -> impl Iterator<Item = &RigDesc> {
474        self.list.values()
475    }
476
477    pub fn names(&self) -> impl Iterator<Item = &str> {
478        self.list.keys().map(String::as_str)
479    }
480}