veloren_common/
generation.rs

1use std::ops::RangeInclusive;
2
3use crate::{
4    assets::{self, AssetExt, Error},
5    calendar::Calendar,
6    combat::{DeathEffect, DeathEffects, RiderEffects},
7    comp::{
8        self, Alignment, Body, Item, agent, humanoid,
9        inventory::loadout_builder::{LoadoutBuilder, LoadoutSpec},
10        misc::PortalData,
11    },
12    effect::BuffEffect,
13    lottery::LootSpec,
14    npc::{self, NPC_NAMES},
15    resources::TimeOfDay,
16    rtsim,
17    trade::SiteInformation,
18};
19use enum_map::EnumMap;
20use serde::Deserialize;
21use tracing::error;
22use vek::*;
23
24#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
25pub enum NameKind {
26    Name(String),
27    Automatic,
28    Uninit,
29}
30
31#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
32pub enum BodyBuilder {
33    RandomWith(String),
34    Exact(Body),
35    Uninit,
36}
37
38#[derive(Debug, Deserialize, Clone)]
39pub enum AlignmentMark {
40    Alignment(Alignment),
41    Uninit,
42}
43
44impl Default for AlignmentMark {
45    fn default() -> Self { Self::Alignment(Alignment::Wild) }
46}
47
48#[derive(Default, Debug, Deserialize, Clone)]
49#[serde(default)]
50pub struct AgentConfig {
51    pub has_agency: Option<bool>,
52    pub no_flee: Option<bool>,
53    pub idle_wander_factor: Option<f32>,
54    pub aggro_range_multiplier: Option<f32>,
55}
56
57#[derive(Debug, Deserialize, Clone)]
58pub enum LoadoutKind {
59    FromBody,
60    Asset(String),
61    Inline(Box<LoadoutSpec>),
62}
63
64#[derive(Debug, Deserialize, Clone)]
65pub struct InventorySpec {
66    pub loadout: LoadoutKind,
67    #[serde(default)]
68    pub items: Vec<(u32, String)>,
69}
70
71#[derive(Debug, Deserialize, Clone)]
72pub enum Meta {
73    SkillSetAsset(String),
74}
75
76// FIXME: currently this is used for both base definition
77// and extension manifest.
78// This is why all fields have Uninit kind which is means
79// that this field should be either Default or Unchanged
80// depending on how it is used.
81//
82// When we will use extension manifests more, it would be nicer to
83// split EntityBase and EntityExtension to different structs.
84//
85// Fields which have Uninit enum kind
86// should be optional (or renamed to Unchanged) in EntityExtension
87// and required (or renamed to Default) in EntityBase
88/// Struct for EntityInfo manifest.
89///
90/// Intended to use with .ron files as base definition or
91/// in rare cases as extension manifest.
92/// Pure data struct, doesn't do anything until evaluated with EntityInfo.
93///
94/// Check assets/common/entity/template.ron or other examples.
95///
96/// # Example
97/// ```
98/// use vek::Vec3;
99/// use veloren_common::generation::EntityInfo;
100///
101/// // create new EntityInfo at dummy position
102/// // and fill it with template config
103/// let dummy_position = Vec3::new(0.0, 0.0, 0.0);
104/// // rng is required because some elements may be randomly generated
105/// let mut dummy_rng = rand::thread_rng();
106/// let entity = EntityInfo::at(dummy_position).with_asset_expect(
107///     "common.entity.template",
108///     &mut dummy_rng,
109///     None,
110/// );
111/// ```
112#[derive(Debug, Deserialize, Clone)]
113#[serde(deny_unknown_fields)]
114pub struct EntityConfig {
115    /// Name of Entity
116    /// Can be Name(String) with given name
117    /// or Automatic which will call automatic name depend on Body
118    /// or Uninit (means it should be specified somewhere in code)
119    // Hidden, because its behaviour depends on `body` field.
120    name: NameKind,
121
122    /// Body
123    /// Can be Exact (Body with all fields e.g BodyType, Species, Hair color and
124    /// such) or RandomWith (will generate random body or species)
125    /// or Uninit (means it should be specified somewhere in code)
126    pub body: BodyBuilder,
127
128    /// Alignment, can be Uninit
129    pub alignment: AlignmentMark,
130
131    /// Parameterises agent behaviour
132    #[serde(default)]
133    pub agent: AgentConfig,
134
135    /// Loot
136    /// See LootSpec in lottery
137    pub loot: LootSpec<String>,
138
139    /// Loadout & Inventory
140    /// Check docs for `InventorySpec` struct in this file.
141    pub inventory: InventorySpec,
142
143    /// Pets to spawn with this entity (specified as a list of asset paths).
144    /// The range represents how many pets will be spawned.
145    #[serde(default)]
146    pub pets: Vec<(String, RangeInclusive<usize>)>,
147
148    /// If this entity spawns with a rider.
149    #[serde(default)]
150    pub rider: Option<String>,
151
152    /// Buffs this entity gives to whatever is riding it.
153    #[serde(default)]
154    pub rider_effects: Vec<BuffEffect>,
155
156    #[serde(default = "num_traits::One::one")]
157    pub scale: f32,
158
159    #[serde(default)]
160    pub death_effects: Vec<DeathEffect>,
161
162    /// Meta Info for optional fields
163    /// Possible fields:
164    /// SkillSetAsset(String) with asset_specifier for skillset
165    #[serde(default)]
166    pub meta: Vec<Meta>,
167}
168
169impl assets::Asset for EntityConfig {
170    type Loader = assets::RonLoader;
171
172    const EXTENSION: &'static str = "ron";
173}
174
175impl EntityConfig {
176    pub fn from_asset_expect_owned(asset_specifier: &str) -> Self {
177        Self::load_owned(asset_specifier)
178            .unwrap_or_else(|e| panic!("Failed to load {}. Error: {:?}", asset_specifier, e))
179    }
180
181    #[must_use]
182    pub fn with_body(mut self, body: BodyBuilder) -> Self {
183        self.body = body;
184
185        self
186    }
187}
188
189/// Return all entity config specifiers
190pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
191    let configs = assets::load_rec_dir::<EntityConfig>("common.entity")?;
192    Ok(configs.read().ids().map(|id| id.to_string()).collect())
193}
194
195#[derive(Clone, Debug)]
196pub enum SpecialEntity {
197    Waypoint,
198    Teleporter(PortalData),
199    /// Totem with FriendlyFire and ForcePvP auras
200    ArenaTotem {
201        range: f32,
202    },
203}
204
205#[derive(Clone)]
206pub struct EntityInfo {
207    pub pos: Vec3<f32>,
208    pub alignment: Alignment,
209    /// Parameterises agent behaviour
210    pub has_agency: bool,
211    pub agent_mark: Option<agent::Mark>,
212    pub no_flee: bool,
213    pub idle_wander_factor: f32,
214    pub aggro_range_multiplier: f32,
215    // Stats
216    pub body: Body,
217    pub name: Option<String>,
218    pub scale: f32,
219    // Loot
220    pub loot: LootSpec<String>,
221    // Loadout
222    pub inventory: Vec<(u32, Item)>,
223    pub loadout: LoadoutBuilder,
224    pub make_loadout: Option<
225        fn(
226            LoadoutBuilder,
227            Option<&SiteInformation>,
228            time: Option<&(TimeOfDay, Calendar)>,
229        ) -> LoadoutBuilder,
230    >,
231    // Skills
232    pub skillset_asset: Option<String>,
233    pub death_effects: Option<DeathEffects>,
234    pub rider_effects: Option<RiderEffects>,
235
236    pub pets: Vec<EntityInfo>,
237
238    pub rider: Option<Box<EntityInfo>>,
239
240    // Economy
241    // we can't use DHashMap, do we want to move that into common?
242    pub trading_information: Option<SiteInformation>,
243    //Option<hashbrown::HashMap<crate::trade::Good, (f32, f32)>>, /* price and available amount */
244
245    // Edge cases, override everything else
246    pub special_entity: Option<SpecialEntity>,
247}
248
249impl EntityInfo {
250    pub fn at(pos: Vec3<f32>) -> Self {
251        Self {
252            pos,
253            alignment: Alignment::Wild,
254
255            has_agency: true,
256            agent_mark: None,
257            no_flee: false,
258            idle_wander_factor: 1.0,
259            aggro_range_multiplier: 1.0,
260
261            body: Body::Humanoid(humanoid::Body::random()),
262            name: None,
263            scale: 1.0,
264            loot: LootSpec::Nothing,
265            inventory: Vec::new(),
266            loadout: LoadoutBuilder::empty(),
267            make_loadout: None,
268            death_effects: None,
269            rider_effects: None,
270            skillset_asset: None,
271            pets: Vec::new(),
272            rider: None,
273            trading_information: None,
274            special_entity: None,
275        }
276    }
277
278    /// Helper function for applying config from asset
279    /// with specified Rng for managing loadout.
280    #[must_use]
281    pub fn with_asset_expect<R>(
282        self,
283        asset_specifier: &str,
284        loadout_rng: &mut R,
285        time: Option<&(TimeOfDay, Calendar)>,
286    ) -> Self
287    where
288        R: rand::Rng,
289    {
290        let config = EntityConfig::load_expect_cloned(asset_specifier);
291
292        self.with_entity_config(config, Some(asset_specifier), loadout_rng, time)
293    }
294
295    /// Evaluate and apply EntityConfig
296    #[must_use]
297    pub fn with_entity_config<R>(
298        mut self,
299        config: EntityConfig,
300        config_asset: Option<&str>,
301        loadout_rng: &mut R,
302        time: Option<&(TimeOfDay, Calendar)>,
303    ) -> Self
304    where
305        R: rand::Rng,
306    {
307        let EntityConfig {
308            name,
309            body,
310            alignment,
311            agent,
312            inventory,
313            loot,
314            meta,
315            scale,
316            pets,
317            rider,
318            death_effects,
319            rider_effects,
320        } = config;
321
322        match body {
323            BodyBuilder::RandomWith(string) => {
324                let npc::NpcBody(_body_kind, mut body_creator) =
325                    string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
326                        panic!("failed to parse body {:?}. Err: {:?}", &string, err)
327                    });
328                let body = body_creator();
329                self = self.with_body(body);
330            },
331            BodyBuilder::Exact(body) => {
332                self = self.with_body(body);
333            },
334            BodyBuilder::Uninit => {},
335        }
336
337        // NOTE: set name after body, as it's used with automatic name
338        match name {
339            NameKind::Name(name) => {
340                self = self.with_name(name);
341            },
342            NameKind::Automatic => {
343                self = self.with_automatic_name(None);
344            },
345            NameKind::Uninit => {},
346        }
347
348        if let AlignmentMark::Alignment(alignment) = alignment {
349            self = self.with_alignment(alignment);
350        }
351
352        self = self.with_loot_drop(loot);
353
354        // NOTE: set loadout after body, as it's used with default equipement
355        self = self.with_inventory(inventory, config_asset, loadout_rng, time);
356
357        let mut pet_infos: Vec<EntityInfo> = Vec::new();
358        for (pet_asset, amount) in pets {
359            let config = EntityConfig::load_expect(&pet_asset).read();
360            let (start, mut end) = amount.into_inner();
361            if start > end {
362                error!("Invalid range for pet count start: {start}, end: {end}");
363                end = start;
364            }
365
366            pet_infos.extend((0..loadout_rng.gen_range(start..=end)).map(|_| {
367                EntityInfo::at(self.pos).with_entity_config(
368                    config.clone(),
369                    config_asset,
370                    loadout_rng,
371                    time,
372                )
373            }));
374        }
375        self.scale = scale;
376
377        self.pets = pet_infos;
378
379        self.rider = rider.map(|rider| {
380            let config = EntityConfig::load_expect(&rider).read();
381            Box::new(EntityInfo::at(self.pos).with_entity_config(
382                config.clone(),
383                config_asset,
384                loadout_rng,
385                time,
386            ))
387        });
388
389        // Prefer the new configuration, if possible
390        let AgentConfig {
391            has_agency,
392            no_flee,
393            idle_wander_factor,
394            aggro_range_multiplier,
395        } = agent;
396        self.has_agency = has_agency.unwrap_or(self.has_agency);
397        self.no_flee = no_flee.unwrap_or(self.no_flee);
398        self.idle_wander_factor = idle_wander_factor.unwrap_or(self.idle_wander_factor);
399        self.aggro_range_multiplier = aggro_range_multiplier.unwrap_or(self.aggro_range_multiplier);
400        self.death_effects = (!death_effects.is_empty()).then_some(DeathEffects(death_effects));
401        self.rider_effects = (!rider_effects.is_empty()).then_some(RiderEffects(rider_effects));
402
403        for field in meta {
404            match field {
405                Meta::SkillSetAsset(asset) => {
406                    self = self.with_skillset_asset(asset);
407                },
408            }
409        }
410
411        self
412    }
413
414    /// Return EntityInfo with LoadoutBuilder and items overwritten
415    // NOTE: helper function, think twice before exposing it
416    #[must_use]
417    fn with_inventory<R>(
418        mut self,
419        inventory: InventorySpec,
420        config_asset: Option<&str>,
421        rng: &mut R,
422        time: Option<&(TimeOfDay, Calendar)>,
423    ) -> Self
424    where
425        R: rand::Rng,
426    {
427        let config_asset = config_asset.unwrap_or("???");
428        let InventorySpec { loadout, items } = inventory;
429
430        // FIXME: this shouldn't always overwrite
431        // inventory. Think about this when we get to
432        // entity config inheritance.
433        self.inventory = items
434            .into_iter()
435            .map(|(num, i)| (num, Item::new_from_asset_expect(&i)))
436            .collect();
437
438        match loadout {
439            LoadoutKind::FromBody => {
440                self = self.with_default_equip();
441            },
442            LoadoutKind::Asset(loadout) => {
443                let loadout = LoadoutBuilder::from_asset(&loadout, rng, time).unwrap_or_else(|e| {
444                    panic!("failed to load loadout for {config_asset}: {e:?}");
445                });
446                self.loadout = loadout;
447            },
448            LoadoutKind::Inline(loadout_spec) => {
449                let loadout = LoadoutBuilder::from_loadout_spec(*loadout_spec, rng, time)
450                    .unwrap_or_else(|e| {
451                        panic!("failed to load loadout for {config_asset}: {e:?}");
452                    });
453                self.loadout = loadout;
454            },
455        }
456
457        self
458    }
459
460    /// Return EntityInfo with LoadoutBuilder overwritten
461    // NOTE: helper function, think twice before exposing it
462    #[must_use]
463    fn with_default_equip(mut self) -> Self {
464        let loadout_builder = LoadoutBuilder::from_default(&self.body);
465        self.loadout = loadout_builder;
466
467        self
468    }
469
470    #[must_use]
471    pub fn do_if(mut self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
472        if cond {
473            self = f(self);
474        }
475        self
476    }
477
478    #[must_use]
479    pub fn into_special(mut self, special: SpecialEntity) -> Self {
480        self.special_entity = Some(special);
481        self
482    }
483
484    #[must_use]
485    pub fn with_alignment(mut self, alignment: Alignment) -> Self {
486        self.alignment = alignment;
487        self
488    }
489
490    #[must_use]
491    pub fn with_body(mut self, body: Body) -> Self {
492        self.body = body;
493        self
494    }
495
496    #[must_use]
497    pub fn with_name(mut self, name: impl Into<String>) -> Self {
498        self.name = Some(name.into());
499        self
500    }
501
502    #[must_use]
503    pub fn with_agency(mut self, agency: bool) -> Self {
504        self.has_agency = agency;
505        self
506    }
507
508    #[must_use]
509    pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
510        self.agent_mark = agent_mark.into();
511        self
512    }
513
514    #[must_use]
515    pub fn with_loot_drop(mut self, loot_drop: LootSpec<String>) -> Self {
516        self.loot = loot_drop;
517        self
518    }
519
520    #[must_use]
521    pub fn with_scale(mut self, scale: f32) -> Self {
522        self.scale = scale;
523        self
524    }
525
526    #[must_use]
527    pub fn with_lazy_loadout(
528        mut self,
529        creator: fn(
530            LoadoutBuilder,
531            Option<&SiteInformation>,
532            time: Option<&(TimeOfDay, Calendar)>,
533        ) -> LoadoutBuilder,
534    ) -> Self {
535        self.make_loadout = Some(creator);
536        self
537    }
538
539    #[must_use]
540    pub fn with_skillset_asset(mut self, asset: String) -> Self {
541        self.skillset_asset = Some(asset);
542        self
543    }
544
545    #[must_use]
546    pub fn with_automatic_name(mut self, alias: Option<String>) -> Self {
547        let npc_names = NPC_NAMES.read();
548        let name = match &self.body {
549            Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)),
550            Body::QuadrupedMedium(body) => {
551                Some(get_npc_name(&npc_names.quadruped_medium, body.species))
552            },
553            Body::BirdMedium(body) => Some(get_npc_name(&npc_names.bird_medium, body.species)),
554            Body::BirdLarge(body) => Some(get_npc_name(&npc_names.bird_large, body.species)),
555            Body::FishSmall(body) => Some(get_npc_name(&npc_names.fish_small, body.species)),
556            Body::FishMedium(body) => Some(get_npc_name(&npc_names.fish_medium, body.species)),
557            Body::Theropod(body) => Some(get_npc_name(&npc_names.theropod, body.species)),
558            Body::QuadrupedSmall(body) => {
559                Some(get_npc_name(&npc_names.quadruped_small, body.species))
560            },
561            Body::Dragon(body) => Some(get_npc_name(&npc_names.dragon, body.species)),
562            Body::QuadrupedLow(body) => Some(get_npc_name(&npc_names.quadruped_low, body.species)),
563            Body::Golem(body) => Some(get_npc_name(&npc_names.golem, body.species)),
564            Body::BipedLarge(body) => Some(get_npc_name(&npc_names.biped_large, body.species)),
565            Body::Arthropod(body) => Some(get_npc_name(&npc_names.arthropod, body.species)),
566            Body::Crustacean(body) => Some(get_npc_name(&npc_names.crustacean, body.species)),
567            Body::Plugin(body) => Some(get_npc_name(&npc_names.plugin, body.species)),
568            _ => None,
569        };
570        self.name = name.map(|name| {
571            if let Some(alias) = alias {
572                format!("{alias} ({name})")
573            } else {
574                name.to_string()
575            }
576        });
577        self
578    }
579
580    #[must_use]
581    pub fn with_alias(mut self, alias: String) -> Self {
582        self.name = Some(if let Some(name) = self.name {
583            format!("{alias} ({name})")
584        } else {
585            alias
586        });
587        self
588    }
589
590    /// map contains price+amount
591    #[must_use]
592    pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
593        self.trading_information = e.into().cloned();
594        self
595    }
596
597    #[must_use]
598    pub fn with_no_flee(mut self) -> Self {
599        self.no_flee = true;
600        self
601    }
602
603    #[must_use]
604    pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
605        self.loadout = loadout;
606        self
607    }
608}
609
610#[derive(Default)]
611pub struct ChunkSupplement {
612    pub entities: Vec<EntityInfo>,
613    pub rtsim_max_resources: EnumMap<rtsim::ChunkResource, usize>,
614}
615
616impl ChunkSupplement {
617    pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); }
618}
619
620pub fn get_npc_name<
621    'a,
622    Species,
623    SpeciesData: for<'b> core::ops::Index<&'b Species, Output = npc::SpeciesNames>,
624>(
625    body_data: &'a comp::BodyData<npc::BodyNames, SpeciesData>,
626    species: Species,
627) -> &'a str {
628    &body_data.species[&species].generic
629}
630
631#[cfg(test)]
632pub mod tests {
633    use super::*;
634    use crate::SkillSetBuilder;
635    use hashbrown::HashMap;
636
637    #[derive(Debug, Eq, Hash, PartialEq)]
638    enum MetaId {
639        SkillSetAsset,
640    }
641
642    impl Meta {
643        fn id(&self) -> MetaId {
644            match self {
645                Meta::SkillSetAsset(_) => MetaId::SkillSetAsset,
646            }
647        }
648    }
649
650    #[cfg(test)]
651    fn validate_body(body: &BodyBuilder, config_asset: &str) {
652        match body {
653            BodyBuilder::RandomWith(string) => {
654                let npc::NpcBody(_body_kind, mut body_creator) =
655                    string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
656                        panic!(
657                            "failed to parse body {:?} in {}. Err: {:?}",
658                            &string, config_asset, err
659                        )
660                    });
661                let _ = body_creator();
662            },
663            BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
664        }
665    }
666
667    #[cfg(test)]
668    fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
669        let InventorySpec { loadout, items } = inventory;
670
671        match loadout {
672            LoadoutKind::FromBody => {
673                if body.clone() == BodyBuilder::Uninit {
674                    // there is a big chance to call automatic name
675                    // when body is yet undefined
676                    panic!("Used FromBody loadout with Uninit body in {}", config_asset);
677                }
678            },
679            LoadoutKind::Asset(asset) => {
680                let loadout =
681                    LoadoutSpec::load_cloned(&asset).expect("failed to load loadout asset");
682                loadout
683                    .validate(vec![asset])
684                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
685            },
686            LoadoutKind::Inline(spec) => {
687                spec.validate(Vec::new())
688                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
689            },
690        }
691
692        // TODO: check for number of items
693        //
694        // 1) just with 16 default slots?
695        // - well, keep in mind that not every item can stack to infinite amount
696        //
697        // 2) discover total capacity from loadout?
698        for (num, item_str) in items {
699            let item = Item::new_from_asset(&item_str);
700            let mut item = item.unwrap_or_else(|err| {
701                panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
702            });
703            item.set_amount(num).unwrap_or_else(|err| {
704                panic!(
705                    "can't set amount {} for {} in {}: {:?}",
706                    num, item_str, config_asset, err
707                );
708            });
709        }
710    }
711
712    #[cfg(test)]
713    fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
714        if name == NameKind::Automatic && body == BodyBuilder::Uninit {
715            // there is a big chance to call automatic name
716            // when body is yet undefined
717            //
718            // use .with_automatic_name() in code explicitly
719            panic!("Used Automatic name with Uninit body in {}", config_asset);
720        }
721    }
722
723    #[cfg(test)]
724    fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
725        use crate::lottery;
726        lottery::tests::validate_loot_spec(&loot);
727    }
728
729    #[cfg(test)]
730    fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
731        let mut meta_counter = HashMap::new();
732        for field in meta {
733            meta_counter
734                .entry(field.id())
735                .and_modify(|c| *c += 1)
736                .or_insert(1);
737
738            match field {
739                Meta::SkillSetAsset(asset) => {
740                    drop(SkillSetBuilder::from_asset_expect(&asset));
741                },
742            }
743        }
744        for (meta_id, counter) in meta_counter {
745            if counter > 1 {
746                panic!("Duplicate {:?} in {}", meta_id, config_asset);
747            }
748        }
749    }
750
751    #[cfg(test)]
752    fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
753        for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
754            (
755                EntityConfig::load_cloned(&pet_asset).unwrap_or_else(|_| {
756                    panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
757                }),
758                amount,
759            )
760        }) {
761            assert!(
762                amount.end() >= amount.start(),
763                "Invalid pet spawn range ({}..={}), in {}",
764                amount.start(),
765                amount.end(),
766                config_asset
767            );
768            if !pet.pets.is_empty() {
769                panic!("Pets must not be owners of pets: {config_asset}");
770            }
771        }
772    }
773
774    #[cfg(test)]
775    fn validate_death_effects(effects: Vec<DeathEffect>, config_asset: &str) {
776        for effect in effects {
777            match effect {
778                DeathEffect::AttackerBuff {
779                    kind: _,
780                    strength: _,
781                    duration: _,
782                } => {},
783                DeathEffect::Transform {
784                    entity_spec,
785                    allow_players: _,
786                } => {
787                    if let Err(error) = EntityConfig::load(&entity_spec) {
788                        panic!(
789                            "Error while loading transform entity spec ({entity_spec}) for entity \
790                             {config_asset}: {error:?}"
791                        );
792                    }
793                },
794            }
795        }
796    }
797
798    fn validate_rider(rider: Option<String>, config_asset: &str) {
799        if let Some(rider) = rider {
800            EntityConfig::load_cloned(&rider).unwrap_or_else(|_| {
801                panic!("Rider asset path invalid: \"{rider}\", in {config_asset}")
802            });
803        }
804    }
805
806    #[cfg(test)]
807    pub fn validate_entity_config(config_asset: &str) {
808        let EntityConfig {
809            body,
810            inventory,
811            name,
812            loot,
813            pets,
814            rider,
815            meta,
816            death_effects,
817            alignment: _, // can't fail if serialized, it's a boring enum
818            rider_effects: _,
819            scale,
820            agent: _,
821        } = EntityConfig::from_asset_expect_owned(config_asset);
822
823        assert!(
824            scale.is_finite() && scale > 0.0,
825            "Scale has to be finite and greater than zero"
826        );
827
828        validate_body(&body, config_asset);
829        // body dependent stuff
830        validate_inventory(inventory, &body, config_asset);
831        validate_name(name, body, config_asset);
832        // misc
833        validate_loot(loot, config_asset);
834        validate_meta(meta, config_asset);
835        validate_pets(pets, config_asset);
836        validate_rider(rider, config_asset);
837        validate_death_effects(death_effects, config_asset);
838    }
839
840    #[test]
841    fn test_all_entity_assets() {
842        // Get list of entity configs, load everything, validate content.
843        let entity_configs =
844            try_all_entity_configs().expect("Failed to access entity configs directory");
845        for config_asset in entity_configs {
846            validate_entity_config(&config_asset)
847        }
848    }
849}