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