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}