veloren_common/
generation.rs

1use std::ops::RangeInclusive;
2
3use crate::{
4    assets::{self, AssetExt, Error, Ron},
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::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 EntityConfig {
176    pub fn from_asset_expect_owned(asset_specifier: &str) -> Self {
177        Ron::load_owned(asset_specifier)
178            .unwrap_or_else(|e| panic!("Failed to load {}. Error: {:?}", asset_specifier, e))
179            .into_inner()
180    }
181
182    #[must_use]
183    pub fn with_body(mut self, body: BodyBuilder) -> Self {
184        self.body = body;
185
186        self
187    }
188}
189
190/// Return all entity config specifiers
191pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
192    let configs = assets::load_rec_dir::<Ron<EntityConfig>>("common.entity")?;
193    Ok(configs.read().ids().map(|id| id.to_string()).collect())
194}
195
196pub enum EntitySpawn {
197    Entity(Box<EntityInfo>),
198    Group(Vec<EntityInfo>),
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 = Ron::load_expect_cloned(asset_specifier).into_inner();
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 = Ron::<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.random_range(start..=end)).map(|_| {
375                EntityInfo::at(self.pos).with_entity_config(
376                    config.clone().into_inner(),
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 = Ron::<EntityConfig>::load_expect(&rider).read();
389            Box::new(EntityInfo::at(self.pos).with_entity_config(
390                config.clone().into_inner(),
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: impl Into<Option<String>>) -> Self {
562        if let Some(alias) = alias.into() {
563            self.name = Some(Content::localized_with_args(
564                "name-misc-with-alias-template",
565                [
566                    ("alias", Content::Plain(alias)),
567                    (
568                        "old_name",
569                        self.name.unwrap_or_else(|| {
570                            dev_panic!("no name present to use with with_alias");
571                            Content::Plain("??".to_owned())
572                        }),
573                    ),
574                ],
575            ));
576        }
577        self
578    }
579
580    /// map contains price+amount
581    #[must_use]
582    pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
583        self.trading_information = e.into().cloned();
584        self
585    }
586
587    #[must_use]
588    pub fn with_no_flee(mut self) -> Self {
589        self.no_flee = true;
590        self
591    }
592
593    #[must_use]
594    pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
595        self.loadout = loadout;
596        self
597    }
598}
599
600#[derive(Default)]
601pub struct ChunkSupplement {
602    pub entity_spawns: Vec<EntitySpawn>,
603    pub rtsim_max_resources: EnumMap<rtsim::TerrainResource, usize>,
604}
605
606impl ChunkSupplement {
607    pub fn add_entity_spawn(&mut self, entity_spawn: EntitySpawn) {
608        self.entity_spawns.push(entity_spawn);
609    }
610}
611
612#[cfg(test)]
613pub mod tests {
614    use super::*;
615    use crate::SkillSetBuilder;
616    use hashbrown::HashMap;
617
618    #[derive(Debug, Eq, Hash, PartialEq)]
619    enum MetaId {
620        SkillSetAsset,
621    }
622
623    impl Meta {
624        fn id(&self) -> MetaId {
625            match self {
626                Meta::SkillSetAsset(_) => MetaId::SkillSetAsset,
627            }
628        }
629    }
630
631    #[cfg(test)]
632    fn validate_body(body: &BodyBuilder, config_asset: &str) {
633        match body {
634            BodyBuilder::RandomWith(string) => {
635                let npc::NpcBody(_body_kind, mut body_creator) =
636                    string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
637                        panic!(
638                            "failed to parse body {:?} in {}. Err: {:?}",
639                            &string, config_asset, err
640                        )
641                    });
642                let _ = body_creator();
643            },
644            BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
645        }
646    }
647
648    #[cfg(test)]
649    fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
650        let InventorySpec { loadout, items } = inventory;
651
652        match loadout {
653            LoadoutKind::FromBody => {
654                if body.clone() == BodyBuilder::Uninit {
655                    // there is a big chance to call automatic name
656                    // when body is yet undefined
657                    panic!("Used FromBody loadout with Uninit body in {}", config_asset);
658                }
659            },
660            LoadoutKind::Asset(asset) => {
661                let loadout: LoadoutSpec = Ron::load_cloned(&asset)
662                    .expect("failed to load loadout asset")
663                    .into_inner();
664                loadout
665                    .validate(vec![asset])
666                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
667            },
668            LoadoutKind::Inline(spec) => {
669                spec.validate(Vec::new())
670                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
671            },
672        }
673
674        // TODO: check for number of items
675        //
676        // 1) just with 16 default slots?
677        // - well, keep in mind that not every item can stack to infinite amount
678        //
679        // 2) discover total capacity from loadout?
680        for (num, item_str) in items {
681            let item = Item::new_from_asset(&item_str);
682            let mut item = item.unwrap_or_else(|err| {
683                panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
684            });
685            item.set_amount(num).unwrap_or_else(|err| {
686                panic!(
687                    "can't set amount {} for {} in {}: {:?}",
688                    num, item_str, config_asset, err
689                );
690            });
691        }
692    }
693
694    #[cfg(test)]
695    fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
696        if (name == NameKind::Automatic || matches!(name, NameKind::Translate(_)))
697            && body == BodyBuilder::Uninit
698        {
699            // there is a big chance to call automatic name
700            // when body is yet undefined
701            //
702            // use .with_automatic_name() in code explicitly
703            panic!(
704                "Used Automatic/Translate name with Uninit body in {}",
705                config_asset
706            );
707        }
708    }
709
710    #[cfg(test)]
711    fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
712        use crate::lottery;
713        lottery::tests::validate_loot_spec(&loot);
714    }
715
716    #[cfg(test)]
717    fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
718        let mut meta_counter = HashMap::new();
719        for field in meta {
720            meta_counter
721                .entry(field.id())
722                .and_modify(|c| *c += 1)
723                .or_insert(1);
724
725            match field {
726                Meta::SkillSetAsset(asset) => {
727                    drop(SkillSetBuilder::from_asset_expect(&asset));
728                },
729            }
730        }
731        for (meta_id, counter) in meta_counter {
732            if counter > 1 {
733                panic!("Duplicate {:?} in {}", meta_id, config_asset);
734            }
735        }
736    }
737
738    #[cfg(test)]
739    fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
740        for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
741            (
742                Ron::<EntityConfig>::load_cloned(&pet_asset)
743                    .unwrap_or_else(|_| {
744                        panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
745                    })
746                    .into_inner(),
747                amount,
748            )
749        }) {
750            assert!(
751                amount.end() >= amount.start(),
752                "Invalid pet spawn range ({}..={}), in {}",
753                amount.start(),
754                amount.end(),
755                config_asset
756            );
757            if !pet.pets.is_empty() {
758                panic!("Pets must not be owners of pets: {config_asset}");
759            }
760        }
761    }
762
763    #[cfg(test)]
764    fn validate_death_effects(effects: Vec<DeathEffect>, config_asset: &str) {
765        for effect in effects {
766            match effect {
767                DeathEffect::AttackerBuff {
768                    kind: _,
769                    strength: _,
770                    duration: _,
771                } => {},
772                DeathEffect::Transform {
773                    entity_spec,
774                    allow_players: _,
775                } => {
776                    if let Err(error) = Ron::<EntityConfig>::load(&entity_spec) {
777                        panic!(
778                            "Error while loading transform entity spec ({entity_spec}) for entity \
779                             {config_asset}: {error:?}"
780                        );
781                    }
782                },
783            }
784        }
785    }
786
787    fn validate_rider(rider: Option<String>, config_asset: &str) {
788        if let Some(rider) = rider {
789            Ron::<EntityConfig>::load_cloned(&rider).unwrap_or_else(|_| {
790                panic!("Rider asset path invalid: \"{rider}\", in {config_asset}")
791            });
792        }
793    }
794
795    #[cfg(test)]
796    pub fn validate_entity_config(config_asset: &str) {
797        let EntityConfig {
798            body,
799            inventory,
800            name,
801            loot,
802            pets,
803            rider,
804            meta,
805            death_effects,
806            alignment: _, // can't fail if serialized, it's a boring enum
807            rider_effects: _,
808            scale,
809            agent: _,
810        } = EntityConfig::from_asset_expect_owned(config_asset);
811
812        assert!(
813            scale.is_finite() && scale > 0.0,
814            "Scale has to be finite and greater than zero"
815        );
816
817        validate_body(&body, config_asset);
818        // body dependent stuff
819        validate_inventory(inventory, &body, config_asset);
820        validate_name(name, body, config_asset);
821        // misc
822        validate_loot(loot, config_asset);
823        validate_meta(meta, config_asset);
824        validate_pets(pets, config_asset);
825        validate_rider(rider, config_asset);
826        validate_death_effects(death_effects, config_asset);
827    }
828
829    #[test]
830    fn test_all_entity_assets() {
831        // Get list of entity configs, load everything, validate content.
832        let entity_configs =
833            try_all_entity_configs().expect("Failed to access entity configs directory");
834        for config_asset in entity_configs {
835            validate_entity_config(&config_asset)
836        }
837    }
838}