1use crate::{CONFIG, IndexRef, column::ColumnSample, sim::SimChunk, util::close};
2use common::{
3 assets::{AssetExt, Ron},
4 calendar::{Calendar, CalendarEvent},
5 generation::{ChunkSupplement, EntityInfo},
6 resources::TimeOfDay,
7 terrain::{BiomeKind, Block},
8 time::DayPeriod,
9 vol::{ReadVol, RectSizedVol, WriteVol},
10};
11use rand::prelude::*;
12use serde::Deserialize;
13use std::f32;
14use vek::*;
15
16type Weight = u32;
17type Min = u8;
18type Max = u8;
19
20#[derive(Clone, Debug, Deserialize)]
21pub struct SpawnEntry {
22 pub name: String,
24 pub note: String,
25 pub rules: Vec<Pack>,
27}
28
29impl SpawnEntry {
30 pub fn from(asset_specifier: &str) -> Self {
31 Ron::load_expect_cloned(asset_specifier).into_inner()
32 }
33
34 pub fn request(
35 &self,
36 requested_period: DayPeriod,
37 calendar: Option<&Calendar>,
38 is_underwater: bool,
39 is_ice: bool,
40 ) -> Option<Pack> {
41 self.rules
42 .iter()
43 .find(|pack| {
44 let time_match = pack.day_period.contains(&requested_period);
45 let calendar_match = if let Some(calendar) = calendar {
46 pack.calendar_events
47 .as_ref()
48 .is_none_or(|events| events.iter().any(|event| calendar.is_event(*event)))
49 } else {
50 false
51 };
52 let mode_match = match pack.spawn_mode {
53 SpawnMode::Land => !is_underwater,
54 SpawnMode::Ice => is_ice,
55 SpawnMode::Water | SpawnMode::Underwater => is_underwater,
56 SpawnMode::Air(_) => true,
57 };
58 time_match && calendar_match && mode_match
59 })
60 .cloned()
61 }
62}
63
64#[derive(Clone, Debug, Deserialize)]
106pub struct Pack {
107 pub groups: Vec<(Weight, (Min, Max, String))>,
108 pub spawn_mode: SpawnMode,
109 pub day_period: Vec<DayPeriod>,
110 #[serde(default)]
111 pub calendar_events: Option<Vec<CalendarEvent>>, }
114
115#[derive(Copy, Clone, Debug, Deserialize)]
116pub enum SpawnMode {
117 Land,
118 Ice,
119 Water,
120 Underwater,
121 Air(f32),
122}
123
124impl Pack {
125 pub fn generate(&self, pos: Vec3<f32>, dynamic_rng: &mut impl Rng) -> (EntityInfo, u8) {
126 let (_, (from, to, entity_asset)) = self
127 .groups
128 .choose_weighted(dynamic_rng, |(p, _group)| *p)
129 .expect("Failed to choose group");
130 let entity = EntityInfo::at(pos).with_asset_expect(entity_asset, dynamic_rng, None);
131 let group_size = dynamic_rng.random_range(*from..=*to);
132
133 (entity, group_size)
134 }
135}
136
137pub type DensityFn = fn(&SimChunk, &ColumnSample) -> f32;
138
139pub fn spawn_manifest() -> Vec<(&'static str, DensityFn)> {
140 const BASE_DENSITY: f32 = 1.0e-5; vec![
145 ("world.wildlife.spawn.tundra.rock", |c, col| {
148 close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * col.rock_density * 1.0
149 }),
150 ("world.wildlife.spawn.tundra.core", |c, _col| {
152 close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5
153 }),
154 (
156 "world.wildlife.spawn.calendar.christmas.tundra.core",
157 |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
158 ),
159 (
160 "world.wildlife.spawn.calendar.halloween.tundra.core",
161 |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 1.0,
162 ),
163 (
164 "world.wildlife.spawn.calendar.april_fools.tundra.core",
165 |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
166 ),
167 (
168 "world.wildlife.spawn.calendar.easter.tundra.core",
169 |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
170 ),
171 ("world.wildlife.spawn.tundra.snow", |c, col| {
173 close(c.temp, CONFIG.snow_temp, 0.3) * BASE_DENSITY * col.snow_cover as i32 as f32 * 1.0
174 }),
175 (
177 "world.wildlife.spawn.calendar.christmas.tundra.snow",
178 |c, col| {
179 close(c.temp, CONFIG.snow_temp, 0.3)
180 * BASE_DENSITY
181 * col.snow_cover as i32 as f32
182 * 1.0
183 },
184 ),
185 (
186 "world.wildlife.spawn.calendar.halloween.tundra.snow",
187 |c, col| {
188 close(c.temp, CONFIG.snow_temp, 0.3)
189 * BASE_DENSITY
190 * col.snow_cover as i32 as f32
191 * 1.5
192 },
193 ),
194 (
195 "world.wildlife.spawn.calendar.april_fools.tundra.snow",
196 |c, col| {
197 close(c.temp, CONFIG.snow_temp, 0.3)
198 * BASE_DENSITY
199 * col.snow_cover as i32 as f32
200 * 1.0
201 },
202 ),
203 (
204 "world.wildlife.spawn.calendar.easter.tundra.snow",
205 |c, col| {
206 close(c.temp, CONFIG.snow_temp, 0.3)
207 * BASE_DENSITY
208 * col.snow_cover as i32 as f32
209 * 1.0
210 },
211 ),
212 ("world.wildlife.spawn.tundra.forest", |c, col| {
214 close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4
215 }),
216 ("world.wildlife.spawn.tundra.river", |c, col| {
218 close(col.temp, CONFIG.snow_temp, 0.3)
219 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
220 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
221 && c.alt > CONFIG.sea_level + 20.0
222 {
223 0.001
224 } else {
225 0.0
226 }
227 }),
228 (
230 "world.wildlife.spawn.calendar.christmas.tundra.forest",
231 |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
232 ),
233 (
234 "world.wildlife.spawn.calendar.halloween.tundra.forest",
235 |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 2.0,
236 ),
237 (
238 "world.wildlife.spawn.calendar.april_fools.tundra.forest",
239 |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
240 ),
241 (
242 "world.wildlife.spawn.calendar.easter.tundra.forest",
243 |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
244 ),
245 ("world.wildlife.spawn.taiga.core_forest", |c, col| {
248 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
249 }),
250 (
252 "world.wildlife.spawn.calendar.christmas.taiga.core_forest",
253 |c, col| {
254 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
255 },
256 ),
257 (
258 "world.wildlife.spawn.calendar.halloween.taiga.core",
259 |c, col| {
260 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.8
261 },
262 ),
263 (
264 "world.wildlife.spawn.calendar.april_fools.taiga.core",
265 |c, col| {
266 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
267 },
268 ),
269 (
270 "world.wildlife.spawn.calendar.easter.taiga.core",
271 |c, col| {
272 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
273 },
274 ),
275 ("world.wildlife.spawn.taiga.core", |c, _col| {
277 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * BASE_DENSITY * 1.0
278 }),
279 ("world.wildlife.spawn.taiga.forest", |c, col| {
281 close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * col.tree_density * BASE_DENSITY * 0.9
282 }),
283 ("world.wildlife.spawn.taiga.area", |c, _col| {
285 close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * BASE_DENSITY * 5.0
286 }),
287 ("world.wildlife.spawn.taiga.water", |c, col| {
289 close(c.temp, CONFIG.snow_temp, 0.15) * col.tree_density * BASE_DENSITY * 5.0
290 }),
291 ("world.wildlife.spawn.taiga.river", |c, col| {
293 close(col.temp, CONFIG.snow_temp + 0.2, 0.6)
294 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
295 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
296 && c.alt > CONFIG.sea_level + 20.0
297 {
298 0.001
299 } else {
300 0.0
301 }
302 }),
303 ("world.wildlife.spawn.temperate.rare", |c, _col| {
306 close(c.temp, CONFIG.temperate_temp, 0.8) * BASE_DENSITY * 0.08
307 }),
308 ("world.wildlife.spawn.temperate.plains", |c, _col| {
310 close(c.temp, CONFIG.temperate_temp, 0.8)
311 * close(c.tree_density, 0.0, 0.1)
312 * BASE_DENSITY
313 * 5.0
314 }),
315 ("world.wildlife.spawn.temperate.river", |c, col| {
317 close(col.temp, CONFIG.temperate_temp, 0.6)
318 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
319 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
320 && c.alt > CONFIG.sea_level + 20.0
321 {
322 0.001
323 } else {
324 0.0
325 }
326 }),
327 ("world.wildlife.spawn.temperate.wood", |c, col| {
329 close(c.temp, CONFIG.temperate_temp + 0.1, 0.5) * col.tree_density * BASE_DENSITY * 5.0
330 }),
331 ("world.wildlife.spawn.temperate.rainforest", |c, _col| {
333 close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
334 * close(c.humidity, CONFIG.forest_hum, 0.6)
335 * BASE_DENSITY
336 * 5.0
337 }),
338 (
340 "world.wildlife.spawn.calendar.halloween.temperate.rainforest",
341 |c, _col| {
342 close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
343 * close(c.humidity, CONFIG.forest_hum, 0.6)
344 * BASE_DENSITY
345 * 5.0
346 },
347 ),
348 (
349 "world.wildlife.spawn.calendar.april_fools.temperate.rainforest",
350 |c, _col| {
351 close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
352 * close(c.humidity, CONFIG.forest_hum, 0.6)
353 * BASE_DENSITY
354 * 4.0
355 },
356 ),
357 (
358 "world.wildlife.spawn.calendar.easter.temperate.rainforest",
359 |c, _col| {
360 close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
361 * close(c.humidity, CONFIG.forest_hum, 0.6)
362 * BASE_DENSITY
363 * 4.0
364 },
365 ),
366 ("world.wildlife.spawn.temperate.ocean", |_c, col| {
368 close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
369 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
370 && matches!(col.chunk.get_biome(), BiomeKind::Ocean)
371 {
372 0.001
373 } else {
374 0.0
375 }
376 }),
377 ("world.wildlife.spawn.temperate.beach", |c, col| {
379 close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
380 * if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
381 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
382 && c.alt < CONFIG.sea_level + 2.0
383 {
384 0.001
385 } else {
386 0.0
387 }
388 }),
389 ("world.wildlife.spawn.jungle.rainforest", |c, _col| {
392 close(c.temp, CONFIG.tropical_temp + 0.2, 0.2)
393 * close(c.humidity, CONFIG.jungle_hum, 0.2)
394 * BASE_DENSITY
395 * 2.8
396 }),
397 ("world.wildlife.spawn.jungle.rainforest_area", |c, _col| {
399 close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
400 * close(c.humidity, CONFIG.jungle_hum, 0.2)
401 * BASE_DENSITY
402 * 8.0
403 }),
404 (
406 "world.wildlife.spawn.calendar.halloween.jungle.area",
407 |c, _col| {
408 close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
409 * close(c.humidity, CONFIG.jungle_hum, 0.2)
410 * BASE_DENSITY
411 * 10.0
412 },
413 ),
414 (
415 "world.wildlife.spawn.calendar.april_fools.jungle.area",
416 |c, _col| {
417 close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
418 * close(c.humidity, CONFIG.jungle_hum, 0.2)
419 * BASE_DENSITY
420 * 8.0
421 },
422 ),
423 (
424 "world.wildlife.spawn.calendar.easter.jungle.area",
425 |c, _col| {
426 close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
427 * close(c.humidity, CONFIG.jungle_hum, 0.2)
428 * BASE_DENSITY
429 * 8.0
430 },
431 ),
432 ("world.wildlife.spawn.tropical.river", |c, col| {
435 close(col.temp, CONFIG.tropical_temp, 0.5)
436 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
437 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
438 && c.alt > CONFIG.sea_level + 20.0
439 {
440 0.001
441 } else {
442 0.0
443 }
444 }),
445 ("world.wildlife.spawn.tropical.ocean", |_c, col| {
447 close(col.temp, CONFIG.tropical_temp, 0.1) / 10.0
448 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
449 && matches!(col.chunk.get_biome(), BiomeKind::Ocean)
450 {
451 0.001
452 } else {
453 0.0
454 }
455 }),
456 ("world.wildlife.spawn.tropical.beach", |c, col| {
458 close(col.temp, CONFIG.tropical_temp, 1.0) / 10.0
459 * if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
460 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
461 && c.alt < CONFIG.sea_level + 2.0
462 {
463 0.001
464 } else {
465 0.0
466 }
467 }),
468 ("world.wildlife.spawn.arctic.ocean", |_c, col| {
470 close(col.temp, CONFIG.snow_temp, 0.25) / 10.0
471 * if matches!(col.chunk.get_biome(), BiomeKind::Ocean) {
472 0.001
473 } else {
474 0.0
475 }
476 }),
477 ("world.wildlife.spawn.tropical.rainforest", |c, _col| {
479 close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
480 * close(c.humidity, CONFIG.jungle_hum, 0.4)
481 * BASE_DENSITY
482 * 2.0
483 }),
484 (
486 "world.wildlife.spawn.calendar.halloween.tropical.rainforest",
487 |c, _col| {
488 close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
489 * close(c.humidity, CONFIG.jungle_hum, 0.4)
490 * BASE_DENSITY
491 * 3.5
492 },
493 ),
494 (
495 "world.wildlife.spawn.calendar.april_fools.tropical.rainforest",
496 |c, _col| {
497 close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
498 * close(c.humidity, CONFIG.jungle_hum, 0.4)
499 * BASE_DENSITY
500 * 2.0
501 },
502 ),
503 ("world.wildlife.spawn.tropical.rock", |c, col| {
505 close(c.temp, CONFIG.tropical_temp + 0.1, 0.5) * col.rock_density * BASE_DENSITY * 5.0
506 }),
507 ("world.wildlife.spawn.desert.area", |c, _col| {
510 close(c.temp, CONFIG.desert_temp + 0.1, 0.4)
511 * close(c.humidity, CONFIG.desert_hum, 0.4)
512 * BASE_DENSITY
513 * 0.8
514 }),
515 ("world.wildlife.spawn.desert.wasteland", |c, _col| {
517 close(c.temp, CONFIG.desert_temp + 0.2, 0.3)
518 * close(c.humidity, CONFIG.desert_hum, 0.5)
519 * BASE_DENSITY
520 * 1.3
521 }),
522 ("world.wildlife.spawn.desert.river", |c, col| {
524 close(col.temp, CONFIG.desert_temp + 0.2, 0.3)
525 * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
526 && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
527 && c.alt > CONFIG.sea_level + 20.0
528 {
529 0.001
530 } else {
531 0.0
532 }
533 }),
534 ("world.wildlife.spawn.desert.hot", |c, _col| {
536 close(c.temp, CONFIG.desert_temp + 0.2, 0.3) * BASE_DENSITY * 3.8
537 }),
538 ("world.wildlife.spawn.desert.rock", |c, col| {
540 close(c.temp, CONFIG.desert_temp + 0.2, 0.05) * col.rock_density * BASE_DENSITY * 4.0
541 }),
542 ]
543}
544
545pub fn apply_wildlife_supplement<'a, R: Rng>(
546 dynamic_rng: &mut R,
548 wpos2d: Vec2<i32>,
549 mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
550 vol: &(impl RectSizedVol<Vox = Block> + ReadVol + WriteVol),
551 index: IndexRef,
552 chunk: &SimChunk,
553 supplement: &mut ChunkSupplement,
554 time: Option<&(TimeOfDay, Calendar)>,
555) {
556 let scatter = &index.wildlife_spawns;
557 let wildlife_density_modifier = index.features.wildlife_density;
559
560 for y in 0..vol.size_xy().y as i32 {
561 for x in 0..vol.size_xy().x as i32 {
562 let offs = Vec2::new(x, y);
563
564 let wpos2d = wpos2d + offs;
565
566 let col_sample = if let Some(col_sample) = get_column(offs) {
568 col_sample
569 } else {
570 continue;
571 };
572
573 let is_underwater = col_sample.water_level > col_sample.alt;
574 let is_ice = col_sample.ice_depth > 0.5 && is_underwater;
575 let (current_day_period, calendar) = if let Some((time, calendar)) = time {
576 (DayPeriod::from(time.0), Some(calendar))
577 } else {
578 (DayPeriod::Noon, None)
579 };
580
581 let entity_group = scatter
582 .iter()
583 .filter_map(|(entry, get_density)| {
584 let density = get_density(chunk, col_sample) * wildlife_density_modifier;
585 (density > 0.0)
586 .then(|| {
587 entry
588 .read()
589 .0
590 .request(current_day_period, calendar, is_underwater, is_ice)
591 .and_then(|pack| {
592 (dynamic_rng.random::<f32>() < density * col_sample.spawn_rate
593 && col_sample.gradient < Some(1.3))
594 .then_some(pack)
595 })
596 })
597 .flatten()
598 })
599 .collect::<Vec<_>>() .choose_mut(dynamic_rng)
601 .cloned();
602
603 if let Some(pack) = entity_group {
604 let desired_alt = match pack.spawn_mode {
605 SpawnMode::Land | SpawnMode::Underwater => col_sample.alt,
606 SpawnMode::Ice => col_sample.water_level + 1.0 + col_sample.ice_depth,
607 SpawnMode::Water => dynamic_rng.random_range(
608 col_sample.alt..col_sample.water_level.max(col_sample.alt + 0.1),
609 ),
610 SpawnMode::Air(height) => {
611 col_sample.alt.max(col_sample.water_level)
612 + dynamic_rng.random::<f32>() * height
613 },
614 };
615
616 let (entity, group_size) = pack.generate(
617 (wpos2d.map(|e| e as f32) + 0.5).with_z(desired_alt),
618 dynamic_rng,
619 );
620 for e in 0..group_size {
621 let offs_wpos2d = (Vec2::new(
623 (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).sin(),
624 (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).cos(),
625 ) * (5.0 + dynamic_rng.random::<f32>().powf(0.5) * 5.0))
626 .map(|e| e as i32);
627 let offs_wpos2d = (offs + offs_wpos2d)
629 .clamped(Vec2::zero(), vol.size_xy().map(|e| e as i32) - 1)
630 - offs;
631
632 let z_offset = (0..16)
635 .map(|z| if z % 2 == 0 { z } else { -z } / 2)
636 .find(|z| {
637 (0..2).all(|z2| {
638 vol.get(
639 Vec3::new(offs.x, offs.y, desired_alt as i32)
640 + offs_wpos2d.with_z(z + z2),
641 )
642 .map(|b| !b.is_solid())
643 .unwrap_or(true)
644 })
645 });
646
647 if let Some(z_offset) = z_offset {
648 let mut entity = entity.clone();
649 entity.pos += offs_wpos2d.with_z(z_offset).map(|e| e as f32);
650 supplement.add_entity(entity);
651 }
652 }
653 }
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use hashbrown::HashMap;
662
663 #[test]
665 fn test_load_entries() {
666 let scatter = spawn_manifest();
667 for (entry, _) in scatter.into_iter() {
668 drop(SpawnEntry::from(entry));
669 }
670 }
671
672 #[test]
674 fn test_name_uniqueness() {
675 let scatter = spawn_manifest();
676 let mut names = HashMap::new();
677 for (entry, _) in scatter.into_iter() {
678 let SpawnEntry { name, .. } = SpawnEntry::from(entry);
679 if let Some(old_entry) = names.insert(name, entry) {
680 panic!("{}: Found name duplicate with {}", entry, old_entry);
681 }
682 }
683 }
684
685 #[test]
687 fn test_load_entities() {
688 let scatter = spawn_manifest();
689 for (entry, _) in scatter.into_iter() {
690 let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
691 for pack in rules {
692 let Pack { groups, .. } = pack;
693 for group in &groups {
694 println!("{}:", entry);
695 let (_, (_, _, asset)) = group;
696 let dummy_pos = Vec3::new(0.0, 0.0, 0.0);
697 let mut dummy_rng = rand::rng();
698 let entity =
699 EntityInfo::at(dummy_pos).with_asset_expect(asset, &mut dummy_rng, None);
700 drop(entity);
701 }
702 }
703 }
704 }
705
706 #[test]
708 fn test_group_choose() {
709 let scatter = spawn_manifest();
710 for (entry, _) in scatter.into_iter() {
711 let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
712 for pack in rules {
713 let Pack { groups, .. } = pack;
714 let dynamic_rng = &mut rand::rng();
715 let _ = groups
716 .choose_weighted(dynamic_rng, |(p, _group)| *p)
717 .unwrap_or_else(|err| {
718 panic!("{}: Failed to choose random group. Err: {}", entry, err)
719 });
720 }
721 }
722 }
723}