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