Skip to main content

pfx_play/world/
edits.rs

1use pfx_load::scene::{BodyShape, Projection, Target, Value};
2use pfx_materials::Material;
3
4use super::{ObjectId, World, body_volume, collider};
5use crate::live::Edit;
6use crate::math::{self, number};
7
8fn numbers<const N: usize>(value: &Value) -> Option<[f32; N]> {
9    let Value::Array(items) = value else {
10        return None;
11    };
12    if items.len() != N {
13        return None;
14    }
15    let mut out = [0.0; N];
16    for (slot, item) in out.iter_mut().zip(items) {
17        *slot = number(item)?;
18    }
19    Some(out)
20}
21
22fn toggle(value: &Value) -> Option<bool> {
23    match value {
24        Value::Bool(value) => Some(*value),
25        _ => None,
26    }
27}
28
29fn finite(value: f32) -> Option<f32> {
30    value.is_finite().then_some(value)
31}
32
33fn at_least(value: &Value, low: f32) -> Option<f32> {
34    number(value).and_then(finite).filter(|value| *value >= low)
35}
36
37fn above(value: &Value, low: f32) -> Option<f32> {
38    number(value).and_then(finite).filter(|value| *value > low)
39}
40
41fn vector(value: &Value) -> Option<[f32; 3]> {
42    numbers::<3>(value).filter(|parts| parts.iter().all(|part| part.is_finite()))
43}
44
45fn toml_of(value: &Value) -> toml::Value {
46    match value {
47        Value::Bool(value) => toml::Value::Boolean(*value),
48        Value::Int(value) => toml::Value::Integer(*value),
49        Value::Float(value) => toml::Value::Float(math::shortest(*value)),
50        Value::Text(text) => toml::Value::String(text.clone()),
51        Value::Array(items) => toml::Value::Array(items.iter().map(toml_of).collect()),
52        Value::Table(entries) => toml::Value::Table(
53            entries
54                .iter()
55                .map(|(key, value)| (key.clone(), toml_of(value)))
56                .collect(),
57        ),
58    }
59}
60
61pub(crate) fn set_key(
62    material: Material,
63    path: &[String],
64    value: &Value,
65) -> Result<Material, String> {
66    let text = material.to_toml()?;
67    let mut table: toml::Table = toml::from_str(&text).map_err(|error| error.to_string())?;
68    let Some((last, parents)) = path.split_last() else {
69        return Err("no key".to_string());
70    };
71    let mut at = &mut table;
72    for key in parents {
73        at = at
74            .entry(key.clone())
75            .or_insert_with(|| toml::Value::Table(toml::Table::new()))
76            .as_table_mut()
77            .ok_or_else(|| format!("{key} is not a table"))?;
78    }
79    at.insert(last.clone(), toml_of(value));
80    let text = toml::to_string(&table).map_err(|error| error.to_string())?;
81    Material::from_toml(&text)
82}
83
84fn sized(shape: BodyShape, key: &str, value: &Value) -> Option<BodyShape> {
85    Some(match (shape, key) {
86        (BodyShape::Box { .. }, "half") => BodyShape::Box {
87            half: vector(value).filter(|half| half.iter().all(|part| *part > 0.0))?,
88        },
89        (BodyShape::Sphere { .. }, "radius") => BodyShape::Sphere {
90            radius: above(value, 0.0)?,
91        },
92        (BodyShape::Capsule { half_height, .. }, "radius") => BodyShape::Capsule {
93            half_height,
94            radius: above(value, 0.0)?,
95        },
96        (BodyShape::Capsule { radius, .. }, "half_height") => BodyShape::Capsule {
97            half_height: above(value, 0.0)?,
98            radius,
99        },
100        (BodyShape::Cylinder { half_height, .. }, "radius") => BodyShape::Cylinder {
101            half_height,
102            radius: above(value, 0.0)?,
103        },
104        (BodyShape::Cylinder { radius, .. }, "half_height") => BodyShape::Cylinder {
105            half_height: above(value, 0.0)?,
106            radius,
107        },
108        _ => return None,
109    })
110}
111
112impl World {
113    pub fn apply(&mut self, edit: &Edit) -> Result<Edit, String> {
114        match edit {
115            Edit::Tunable { name, value } => {
116                let value = self.set_tunable(name, *value)?;
117                Ok(Edit::tunable(name, value))
118            }
119            Edit::Scene {
120                target,
121                path,
122                value,
123            } => {
124                let refuse = |why: &str| Err(format!("{}: {why}", edit.key()));
125                let key: Vec<&str> = path.iter().map(String::as_str).collect();
126                let kept = match (target, key.as_slice()) {
127                    (Target::Object(name), [key, rest @ ..]) => {
128                        let Some(id) = self.object(name) else {
129                            return refuse("no such object in the running world");
130                        };
131                        let kept = match (*key, rest) {
132                            ("body", [key]) => self.edit_body(id, edit, key, value)?,
133                            ("character", [key]) => {
134                                self.edit_character(id, key, value)
135                                    .map_err(|why| format!("{}: {why}", edit.key()))?;
136                                edit.clone()
137                            }
138                            (_, []) => {
139                                self.edit_object(id, key, value)
140                                    .map_err(|why| format!("{}: {why}", edit.key()))?;
141                                edit.clone()
142                            }
143                            _ => return refuse("not a live key"),
144                        };
145                        self.refresh_physics();
146                        kept
147                    }
148                    (Target::Material(name), [_, ..]) => {
149                        let Some(material) = self.material(name) else {
150                            return refuse("no such material");
151                        };
152                        let changed = set_key(material, path, value)
153                            .map_err(|why| format!("{}: {why}", edit.key()))?;
154                        self.materials.insert(name.clone(), changed);
155                        edit.clone()
156                    }
157                    (Target::Light(name), [key]) => {
158                        let Some(mut light) = self.lights.get(name).copied() else {
159                            return refuse("no such light");
160                        };
161                        match *key {
162                            "position" => {
163                                light.position = vector(value).ok_or("needs 3 numbers")?
164                            }
165                            "color" => light.color = vector(value).ok_or("needs 3 numbers")?,
166                            "intensity" => {
167                                light.intensity =
168                                    at_least(value, 0.0).ok_or("needs a number of 0 or more")?
169                            }
170                            "radius" => {
171                                light.radius =
172                                    at_least(value, 0.0).ok_or("needs a number of 0 or more")?
173                            }
174                            "range" => {
175                                light.range =
176                                    at_least(value, 0.0).ok_or("needs a number of 0 or more")?
177                            }
178                            "shadow" => {
179                                light.shadow = toggle(value).ok_or("needs true or false")?
180                            }
181                            _ => return refuse("not a live key"),
182                        }
183                        self.set_light(name, light);
184                        edit.clone()
185                    }
186                    (Target::Camera, [key]) => {
187                        self.edit_camera(key, value)
188                            .map_err(|why| format!("{}: {why}", edit.key()))?;
189                        edit.clone()
190                    }
191                    (Target::Finish, ["exposure"]) => {
192                        let exposure = above(value, 0.0)
193                            .ok_or_else(|| format!("{}: needs a number above 0", edit.key()))?;
194                        self.set_exposure(exposure);
195                        edit.clone()
196                    }
197                    (Target::Sound(name), [key]) => {
198                        self.edit_sound(name, key, value)
199                            .map_err(|why| format!("{}: {why}", edit.key()))?;
200                        edit.clone()
201                    }
202                    _ => return refuse("not a live key"),
203                };
204                Ok(kept)
205            }
206        }
207    }
208
209    fn edit_object(&mut self, id: ObjectId, key: &str, value: &Value) -> Result<(), &'static str> {
210        match key {
211            "hidden" => {
212                let hidden = toggle(value).ok_or("needs true or false")?;
213                if !self.set_hidden(id, hidden) {
214                    return Err("is hidden in the scene; a game shows it by declaring it visible");
215                }
216            }
217            "material" => {
218                let Value::Text(name) = value else {
219                    return Err("needs a material name");
220                };
221                if self.material(name).is_none() {
222                    return Err("names no material in the library");
223                }
224                let mut look = self.look(id).clone();
225                look.material = Some(name.clone());
226                self.set_look(id, look);
227            }
228            "at" | "rotate" | "scale" => {
229                let parts = match (key, value) {
230                    ("scale", value) if number(value).is_some() => {
231                        number(value).map(|scale| [scale; 3])
232                    }
233                    (_, value) => vector(value),
234                }
235                .ok_or("needs 3 numbers")?;
236                let object = &self.scene.objects[id.0];
237                let rest = [object.at, object.rotate, object.scale];
238                let pose = self.poses.entry(id.0).or_insert(rest);
239                let slot = match key {
240                    "at" => 0,
241                    "rotate" => 1,
242                    _ => 2,
243                };
244                pose[slot] = parts;
245                let [at, rotate, scale] = *pose;
246                self.objects[id.0].scale = scale;
247                self.set_local(id, pfx_load::scene::model(at, rotate, scale));
248                if let Some(physical) = self.bodies.get(&id.0) {
249                    let world = self.model(id);
250                    let body = physical.id;
251                    self.physics.set_pose(body, math::pose(world));
252                    self.physics.wake(body);
253                }
254            }
255            _ => return Err("not a live key"),
256        }
257        Ok(())
258    }
259
260    fn edit_body(
261        &mut self,
262        id: ObjectId,
263        edit: &Edit,
264        key: &str,
265        value: &Value,
266    ) -> Result<Edit, String> {
267        let refuse = |why: &str| format!("{}: {why}", edit.key());
268        let Some(physical) = self.bodies.get(&id.0).copied() else {
269            return Err(refuse("the object has no body"));
270        };
271        let mut desc = physical.desc;
272        let mut kept = edit.clone();
273        let mut rebuild = false;
274        match key {
275            "mass" => {
276                let mass = above(value, 0.0).ok_or_else(|| refuse("needs a number above 0"))?;
277                desc.density = mass / body_volume(desc.shape);
278                if let Edit::Scene { target, .. } = edit {
279                    kept = Edit::scene(target.clone(), &["body", "density"], desc.density);
280                }
281                rebuild = true;
282            }
283            "density" => {
284                desc.density =
285                    at_least(value, 0.0).ok_or_else(|| refuse("needs a number of 0 or more"))?;
286                rebuild = true;
287            }
288            "friction" => {
289                desc.friction =
290                    at_least(value, 0.0).ok_or_else(|| refuse("needs a number of 0 or more"))?;
291                rebuild = true;
292            }
293            "restitution" => {
294                desc.restitution =
295                    at_least(value, 0.0).ok_or_else(|| refuse("needs a number of 0 or more"))?;
296                rebuild = true;
297            }
298            "offset" => {
299                desc.offset = vector(value).ok_or_else(|| refuse("needs 3 numbers"))?;
300                rebuild = true;
301            }
302            "half" | "radius" | "half_height" => {
303                desc.shape = sized(desc.shape, key, value)
304                    .ok_or_else(|| refuse("needs a size above 0 that the body's shape has"))?;
305                rebuild = true;
306            }
307            "damping" => {
308                let damping = numbers::<2>(value)
309                    .filter(|parts| parts.iter().all(|part| part.is_finite() && *part >= 0.0))
310                    .ok_or_else(|| refuse("needs 2 numbers of 0 or more"))?;
311                desc.damping = damping;
312                self.physics
313                    .set_damping(physical.id, damping[0], damping[1]);
314            }
315            "gravity_scale" => {
316                desc.gravity_scale = number(value)
317                    .and_then(finite)
318                    .ok_or_else(|| refuse("needs a number"))?;
319                self.physics
320                    .set_gravity_scale(physical.id, desc.gravity_scale);
321            }
322            "velocity" | "spin" => {
323                let parts = vector(value).ok_or_else(|| refuse("needs 3 numbers"))?;
324                let state = self
325                    .physics
326                    .body(physical.id)
327                    .ok_or_else(|| refuse("the body is gone"))?;
328                let (linear, angular) = if key == "velocity" {
329                    desc.velocity = parts;
330                    (parts, state.angular_velocity)
331                } else {
332                    desc.spin = parts;
333                    (state.linear_velocity, parts.map(f32::to_radians))
334                };
335                self.physics.set_velocity(physical.id, linear, angular);
336            }
337            _ => return Err(refuse("not a live key")),
338        }
339        if rebuild {
340            self.physics.remove_collider(physical.collider);
341            self.owners.remove(&physical.collider);
342            let made = match self
343                .physics
344                .add_collider(physical.id, collider(&desc).with_layers(physical.groups))
345            {
346                Some(made) => made,
347                None => {
348                    let back = self
349                        .physics
350                        .add_collider(
351                            physical.id,
352                            collider(&physical.desc).with_layers(physical.groups),
353                        )
354                        .ok_or_else(|| refuse("the body lost its shape"))?;
355                    self.owners.insert(back, id.0);
356                    if let Some(slot) = self.bodies.get_mut(&id.0) {
357                        slot.collider = back;
358                    }
359                    return Err(refuse("the shape is not usable"));
360                }
361            };
362            self.owners.insert(made, id.0);
363            if let Some(slot) = self.bodies.get_mut(&id.0) {
364                slot.collider = made;
365            }
366        }
367        if let Some(slot) = self.bodies.get_mut(&id.0) {
368            slot.desc = desc;
369        }
370        self.physics.wake(physical.id);
371        Ok(kept)
372    }
373
374    fn edit_camera(&mut self, key: &str, value: &Value) -> Result<(), &'static str> {
375        let camera = &mut self.camera;
376        match key {
377            "at" => camera.at = vector(value).ok_or("needs 3 numbers")?,
378            "look_at" => camera.look_at = vector(value).ok_or("needs 3 numbers")?,
379            "up" => camera.up = vector(value).ok_or("needs 3 numbers")?,
380            "near" => camera.near = above(value, 0.0).ok_or("needs a number above 0")?,
381            "far" => camera.far = above(value, 0.0).ok_or("needs a number above 0")?,
382            "fov" => match &mut camera.projection {
383                Projection::Perspective { fov } => {
384                    *fov = above(value, 0.0)
385                        .filter(|fov| *fov < 180.0)
386                        .ok_or("needs degrees between 0 and 180")?
387                }
388                Projection::Orthographic { .. } => return Err("the camera is orthographic"),
389            },
390            "height" => match &mut camera.projection {
391                Projection::Orthographic { height } => {
392                    *height = above(value, 0.0).ok_or("needs a number above 0")?
393                }
394                Projection::Perspective { .. } => return Err("the camera is a perspective one"),
395            },
396            "fstop" | "focus" => {
397                let Some(depth) = &mut camera.depth_of_field else {
398                    return Err("the camera has no depth of field");
399                };
400                let amount = above(value, 0.0).ok_or("needs a number above 0")?;
401                if key == "fstop" {
402                    depth.fstop = amount;
403                } else {
404                    depth.distance = amount;
405                }
406            }
407            _ => return Err("not a live key"),
408        }
409        Ok(())
410    }
411
412    fn edit_sound(&mut self, name: &str, key: &str, value: &Value) -> Result<(), &'static str> {
413        let Some(level) = self.levels.get_mut(name) else {
414            return Err("no such sound");
415        };
416        let voice = self.sounds.voices.get(name).copied();
417        match key {
418            "volume" => {
419                level.volume = at_least(value, 0.0).ok_or("needs a number of 0 or more")?;
420                if let Some(voice) = voice {
421                    self.sounds.mixer.set_volume(voice, level.volume);
422                }
423            }
424            "pan" => {
425                level.pan = number(value)
426                    .filter(|pan| (-1.0..=1.0).contains(pan))
427                    .ok_or("needs a number from -1 to 1")?;
428                if let Some(voice) = voice {
429                    self.sounds.mixer.set_pan(voice, level.pan);
430                }
431            }
432            "loop" => level.looping = toggle(value).ok_or("needs true or false")?,
433            _ => return Err("not a live key"),
434        }
435        Ok(())
436    }
437}