1use crate::particles::ParticleSystem;
6use crate::world::Linearity;
7use crate::{Clip, Component, Entity, Geometry, JsonError, Linear, Transform, World};
8use serde::Serialize;
9use serde_json::Value;
10
11#[derive(Clone, Debug, Default, PartialEq, Serialize)]
13pub struct ClipEffects {
14 pub origin: Option<String>,
16 pub spawns: Vec<ClipSpawn>,
18 pub lost: Vec<(String, u32)>,
20}
21
22#[derive(Clone, Debug, PartialEq, Serialize)]
24pub struct ClipSpawn {
25 pub system: Entity,
27 pub start: f32,
29 pub end: Option<f32>,
31 pub socket: Option<String>,
36 pub span: Vec<String>,
41 pub follow: bool,
43 pub event: Option<u32>,
45}
46
47impl Component for ClipEffects {
48 const NAME: &'static str = "core.clip_effects";
49 const GEOMETRY: Geometry = Geometry::None;
50 fn to_json(&self) -> Result<Option<Value>, JsonError> {
51 crate::json::to_value(self).map(Some)
52 }
53 fn references(&self, out: &mut Vec<Entity>) {
54 out.extend(self.spawns.iter().map(|spawn| spawn.system));
55 }
56 fn verify(&self, entity: Entity, world: &World) -> Result<(), String> {
59 if !world.has::<Clip>(entity) {
60 return Err("core.clip_effects must be on a core.clip".into());
61 }
62 if self
63 .origin
64 .as_ref()
65 .is_some_and(|origin| origin.trim().is_empty())
66 {
67 return Err("clip effects origin is empty".into());
68 }
69 if self.lost.iter().any(|(reason, _)| reason.trim().is_empty()) {
70 return Err("a lost count without a reason".into());
71 }
72 for (i, spawn) in self.spawns.iter().enumerate() {
73 if !world.has::<ParticleSystem>(spawn.system) {
74 return Err(format!("spawn {i}: {:?} is not a particle system", spawn.system));
75 }
76 if world.has::<Transform>(spawn.system) {
77 return Err(format!(
78 "spawn {i}: the system {:?} is placed; a clip places the systems it starts",
79 spawn.system
80 ));
81 }
82 if !spawn.start.is_finite() || spawn.start < 0.0 {
83 return Err(format!("spawn {i}: a start of {} seconds", spawn.start));
84 }
85 if spawn.end.is_some_and(|end| !end.is_finite() || end < spawn.start) {
86 return Err(format!("spawn {i}: an end before its start or not finite"));
87 }
88 if spawn
89 .socket
90 .as_ref()
91 .is_some_and(|socket| socket.trim().is_empty())
92 {
93 return Err(format!("spawn {i}: a socket name that is empty"));
94 }
95 if !spawn.span.is_empty() && spawn.socket.is_none() {
96 return Err(format!("spawn {i}: sockets it runs through with no first"));
97 }
98 if spawn.span.iter().any(|s| s.trim().is_empty()) {
99 return Err(format!("spawn {i}: a socket it runs through whose name is empty"));
100 }
101 let mut through = std::collections::BTreeSet::new();
102 if let Some(again) = spawn
103 .socket
104 .iter()
105 .chain(&spawn.span)
106 .find(|s| !through.insert(*s))
107 {
108 return Err(format!("spawn {i}: it runs through the socket {again} twice"));
109 }
110 }
111 Ok(())
112 }
113 fn apply_linear(&self, _linear: &Linear) -> Linearity<Self> {
114 Linearity::Invariant
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::Extrapolation;
122 use crate::particles::Termination;
123
124 fn system() -> ParticleSystem {
125 ParticleSystem {
126 seed: 0,
127 emitters: Vec::new(),
128 inputs: Vec::new(),
129 states: 1,
130 warmup: 0.0,
131 time_scale: None,
132 max_view_distance: None,
133 termination: Termination::Natural,
134 end: None,
135 lost: Vec::new(),
136 }
137 }
138
139 fn fixture() -> (World, Entity, Entity, ClipEffects) {
140 let mut world = World::new();
141 let clip = world.spawn();
142 world.insert(
143 clip,
144 Clip {
145 name: "clip".into(),
146 duration: 1.0,
147 extrapolation: Extrapolation::Hold,
148 channels: vec![],
149 root_motion: None,
150 additive: None,
151 },
152 );
153 let spark = world.spawn();
154 world.insert(spark, system());
155 let effects = ClipEffects {
156 origin: Some("source event record".into()),
157 spawns: vec![ClipSpawn {
158 system: spark,
159 start: 0.25,
160 end: Some(0.5),
161 socket: Some("dummy 100".into()),
162 span: Vec::new(),
163 follow: true,
164 event: Some(3),
165 }],
166 lost: vec![("an effect named through a param row".into(), 2)],
167 };
168 (world, clip, spark, effects)
169 }
170
171 #[test]
172 fn a_spawn_of_an_unplaced_system_on_a_clip_verifies_and_names_its_system() {
173 let (world, clip, spark, effects) = fixture();
174 assert_eq!(effects.verify(clip, &world), Ok(()));
175 let mut referenced = Vec::new();
176 effects.references(&mut referenced);
177 assert_eq!(referenced, [spark]);
178 let json = effects.to_json().unwrap().unwrap();
179 assert_eq!(json["spawns"][0]["socket"], "dummy 100");
180 assert_eq!(json["lost"][0][1], 2);
181 }
182
183 #[test]
184 fn what_is_not_a_clip_a_system_or_a_time_is_refused() {
185 let (mut world, clip, spark, effects) = fixture();
186 assert!(effects.verify(spark, &world).is_err());
188 let mut not_a_system = effects.clone();
190 not_a_system.spawns[0].system = clip;
191 assert!(not_a_system.verify(clip, &world).is_err());
192 world.insert(spark, Transform::IDENTITY);
193 assert!(effects.verify(clip, &world).is_err());
194 world.remove::<Transform>(spark);
195 for (start, end) in [
196 (f32::NAN, None),
197 (-0.5, None),
198 (0.5, Some(0.25)),
199 (0.0, Some(f32::INFINITY)),
200 ] {
201 let mut bad = effects.clone();
202 bad.spawns[0].start = start;
203 bad.spawns[0].end = end;
204 assert!(bad.verify(clip, &world).is_err(), "{start} {end:?}");
205 }
206 let mut bad = effects.clone();
207 bad.spawns[0].socket = Some(" ".into());
208 assert!(bad.verify(clip, &world).is_err());
209 let mut bad = effects.clone();
211 bad.spawns[0].span = vec!["dummy 101".into(), "dummy 102".into()];
212 assert_eq!(bad.verify(clip, &world), Ok(()));
213 bad.spawns[0].socket = None;
214 assert!(bad.verify(clip, &world).is_err());
215 bad.spawns[0].socket = Some("dummy 100".into());
216 bad.spawns[0].span = vec![String::new()];
217 assert!(bad.verify(clip, &world).is_err());
218 bad.spawns[0].span = vec!["dummy 101".into(), "dummy 101".into()];
219 assert!(bad.verify(clip, &world).is_err());
220 bad.spawns[0].span = vec!["dummy 100".into()];
221 assert!(bad.verify(clip, &world).is_err());
222 let mut bad = effects;
223 bad.lost.push((String::new(), 1));
224 assert!(bad.verify(clip, &world).is_err());
225 }
226}