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
196#[derive(Clone, Debug)]
197pub enum SpecialEntity {
198    Waypoint,
199    Teleporter(PortalData),
200    /// Totem with FriendlyFire and ForcePvP auras
201    ArenaTotem {
202        range: f32,
203    },
204}
205
206#[derive(Clone)]
207pub struct EntityInfo {
208    pub pos: Vec3<f32>,
209    pub alignment: Alignment,
210    /// Parameterises agent behaviour
211    pub has_agency: bool,
212    pub agent_mark: Option<agent::Mark>,
213    pub no_flee: bool,
214    pub idle_wander_factor: f32,
215    pub aggro_range_multiplier: f32,
216    // Stats
217    pub body: Body,
218    pub name: Option<Content>,
219    pub scale: f32,
220    // Loot
221    pub loot: LootSpec<String>,
222    // Loadout
223    pub inventory: Vec<(u32, Item)>,
224    pub loadout: LoadoutBuilder,
225    pub make_loadout: Option<
226        fn(
227            LoadoutBuilder,
228            Option<&SiteInformation>,
229            time: Option<&(TimeOfDay, Calendar)>,
230        ) -> LoadoutBuilder,
231    >,
232    // Skills
233    pub skillset_asset: Option<String>,
234    pub death_effects: Option<DeathEffects>,
235    pub rider_effects: Option<RiderEffects>,
236
237    pub pets: Vec<EntityInfo>,
238
239    pub rider: Option<Box<EntityInfo>>,
240
241    // Economy
242    // we can't use DHashMap, do we want to move that into common?
243    pub trading_information: Option<SiteInformation>,
244    //Option<hashbrown::HashMap<crate::trade::Good, (f32, f32)>>, /* price and available amount */
245
246    // Edge cases, override everything else
247    pub special_entity: Option<SpecialEntity>,
248}
249
250impl EntityInfo {
251    pub fn at(pos: Vec3<f32>) -> Self {
252        Self {
253            pos,
254            alignment: Alignment::Wild,
255
256            has_agency: true,
257            agent_mark: None,
258            no_flee: false,
259            idle_wander_factor: 1.0,
260            aggro_range_multiplier: 1.0,
261
262            body: Body::Humanoid(humanoid::Body::random()),
263            name: None,
264            scale: 1.0,
265            loot: LootSpec::Nothing,
266            inventory: Vec::new(),
267            loadout: LoadoutBuilder::empty(),
268            make_loadout: None,
269            death_effects: None,
270            rider_effects: None,
271            skillset_asset: None,
272            pets: Vec::new(),
273            rider: None,
274            trading_information: None,
275            special_entity: None,
276        }
277    }
278
279    /// Helper function for applying config from asset
280    /// with specified Rng for managing loadout.
281    #[must_use]
282    pub fn with_asset_expect<R>(
283        self,
284        asset_specifier: &str,
285        loadout_rng: &mut R,
286        time: Option<&(TimeOfDay, Calendar)>,
287    ) -> Self
288    where
289        R: rand::Rng,
290    {
291        let config: EntityConfig = Ron::load_expect_cloned(asset_specifier).into_inner();
292
293        self.with_entity_config(config, Some(asset_specifier), loadout_rng, time)
294    }
295
296    /// Evaluate and apply EntityConfig
297    #[must_use]
298    pub fn with_entity_config<R>(
299        mut self,
300        config: EntityConfig,
301        config_asset: Option<&str>,
302        loadout_rng: &mut R,
303        time: Option<&(TimeOfDay, Calendar)>,
304    ) -> Self
305    where
306        R: rand::Rng,
307    {
308        let EntityConfig {
309            name,
310            body,
311            alignment,
312            agent,
313            inventory,
314            loot,
315            meta,
316            scale,
317            pets,
318            rider,
319            death_effects,
320            rider_effects,
321        } = config;
322
323        match body {
324            BodyBuilder::RandomWith(string) => {
325                let npc::NpcBody(_body_kind, mut body_creator) =
326                    string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
327                        panic!("failed to parse body {:?}. Err: {:?}", &string, err)
328                    });
329                let body = body_creator();
330                self = self.with_body(body);
331            },
332            BodyBuilder::Exact(body) => {
333                self = self.with_body(body);
334            },
335            BodyBuilder::Uninit => {},
336        }
337
338        // NOTE: set name after body, as body is needed used with for both
339        // automatic and translated names
340        match name {
341            NameKind::Translate(key) => {
342                let name = Content::with_attr(key, self.body.gender_attr());
343                self = self.with_name(name);
344            },
345            NameKind::Automatic => {
346                self = self.with_automatic_name();
347            },
348            NameKind::Uninit => {},
349        }
350
351        if let AlignmentMark::Alignment(alignment) = alignment {
352            self = self.with_alignment(alignment);
353        }
354
355        self = self.with_loot_drop(loot);
356
357        // NOTE: set loadout after body, as it's used with default equipement
358        self = self.with_inventory(inventory, config_asset, loadout_rng, time);
359
360        let mut pet_infos: Vec<EntityInfo> = Vec::new();
361        for (pet_asset, amount) in pets {
362            let config = Ron::<EntityConfig>::load_expect(&pet_asset).read();
363            let (start, mut end) = amount.into_inner();
364            if start > end {
365                error!("Invalid range for pet count start: {start}, end: {end}");
366                end = start;
367            }
368
369            pet_infos.extend((0..loadout_rng.random_range(start..=end)).map(|_| {
370                EntityInfo::at(self.pos).with_entity_config(
371                    config.clone().into_inner(),
372                    config_asset,
373                    loadout_rng,
374                    time,
375                )
376            }));
377        }
378        self.scale = scale;
379
380        self.pets = pet_infos;
381
382        self.rider = rider.map(|rider| {
383            let config = Ron::<EntityConfig>::load_expect(&rider).read();
384            Box::new(EntityInfo::at(self.pos).with_entity_config(
385                config.clone().into_inner(),
386                config_asset,
387                loadout_rng,
388                time,
389            ))
390        });
391
392        // Prefer the new configuration, if possible
393        let AgentConfig {
394            has_agency,
395            no_flee,
396            idle_wander_factor,
397            aggro_range_multiplier,
398        } = agent;
399        self.has_agency = has_agency.unwrap_or(self.has_agency);
400        self.no_flee = no_flee.unwrap_or(self.no_flee);
401        self.idle_wander_factor = idle_wander_factor.unwrap_or(self.idle_wander_factor);
402        self.aggro_range_multiplier = aggro_range_multiplier.unwrap_or(self.aggro_range_multiplier);
403        self.death_effects = (!death_effects.is_empty()).then_some(DeathEffects(death_effects));
404        self.rider_effects = (!rider_effects.is_empty()).then_some(RiderEffects(rider_effects));
405
406        for field in meta {
407            match field {
408                Meta::SkillSetAsset(asset) => {
409                    self = self.with_skillset_asset(asset);
410                },
411            }
412        }
413
414        self
415    }
416
417    /// Return EntityInfo with LoadoutBuilder and items overwritten
418    // NOTE: helper function, think twice before exposing it
419    #[must_use]
420    fn with_inventory<R>(
421        mut self,
422        inventory: InventorySpec,
423        config_asset: Option<&str>,
424        rng: &mut R,
425        time: Option<&(TimeOfDay, Calendar)>,
426    ) -> Self
427    where
428        R: rand::Rng,
429    {
430        let config_asset = config_asset.unwrap_or("???");
431        let InventorySpec { loadout, items } = inventory;
432
433        // FIXME: this shouldn't always overwrite
434        // inventory. Think about this when we get to
435        // entity config inheritance.
436        self.inventory = items
437            .into_iter()
438            .map(|(num, i)| (num, Item::new_from_asset_expect(&i)))
439            .collect();
440
441        match loadout {
442            LoadoutKind::FromBody => {
443                self = self.with_default_equip();
444            },
445            LoadoutKind::Asset(loadout) => {
446                let loadout = LoadoutBuilder::from_asset(&loadout, rng, time).unwrap_or_else(|e| {
447                    panic!("failed to load loadout for {config_asset}: {e:?}");
448                });
449                self.loadout = loadout;
450            },
451            LoadoutKind::Inline(loadout_spec) => {
452                let loadout = LoadoutBuilder::from_loadout_spec(*loadout_spec, rng, time)
453                    .unwrap_or_else(|e| {
454                        panic!("failed to load loadout for {config_asset}: {e:?}");
455                    });
456                self.loadout = loadout;
457            },
458        }
459
460        self
461    }
462
463    /// Return EntityInfo with LoadoutBuilder overwritten
464    // NOTE: helper function, think twice before exposing it
465    #[must_use]
466    fn with_default_equip(mut self) -> Self {
467        let loadout_builder = LoadoutBuilder::from_default(&self.body);
468        self.loadout = loadout_builder;
469
470        self
471    }
472
473    #[must_use]
474    pub fn do_if(mut self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
475        if cond {
476            self = f(self);
477        }
478        self
479    }
480
481    #[must_use]
482    pub fn into_special(mut self, special: SpecialEntity) -> Self {
483        self.special_entity = Some(special);
484        self
485    }
486
487    #[must_use]
488    pub fn with_alignment(mut self, alignment: Alignment) -> Self {
489        self.alignment = alignment;
490        self
491    }
492
493    #[must_use]
494    pub fn with_body(mut self, body: Body) -> Self {
495        self.body = body;
496        self
497    }
498
499    #[must_use]
500    pub fn with_name(mut self, name: Content) -> Self {
501        self.name = Some(name);
502        self
503    }
504
505    #[must_use]
506    pub fn with_agency(mut self, agency: bool) -> Self {
507        self.has_agency = agency;
508        self
509    }
510
511    #[must_use]
512    pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
513        self.agent_mark = agent_mark.into();
514        self
515    }
516
517    #[must_use]
518    pub fn with_loot_drop(mut self, loot_drop: LootSpec<String>) -> Self {
519        self.loot = loot_drop;
520        self
521    }
522
523    #[must_use]
524    pub fn with_scale(mut self, scale: f32) -> Self {
525        self.scale = scale;
526        self
527    }
528
529    #[must_use]
530    pub fn with_lazy_loadout(
531        mut self,
532        creator: fn(
533            LoadoutBuilder,
534            Option<&SiteInformation>,
535            time: Option<&(TimeOfDay, Calendar)>,
536        ) -> LoadoutBuilder,
537    ) -> Self {
538        self.make_loadout = Some(creator);
539        self
540    }
541
542    #[must_use]
543    pub fn with_skillset_asset(mut self, asset: String) -> Self {
544        self.skillset_asset = Some(asset);
545        self
546    }
547
548    #[must_use]
549    pub fn with_automatic_name(mut self) -> Self {
550        let npc_names = NPC_NAMES.read();
551        self.name = npc_names.get_default_name(&self.body);
552        self
553    }
554
555    #[must_use]
556    pub fn with_alias(mut self, alias: impl Into<Option<String>>) -> Self {
557        if let Some(alias) = alias.into() {
558            self.name = Some(Content::localized_with_args(
559                "name-misc-with-alias-template",
560                [
561                    ("alias", Content::Plain(alias)),
562                    (
563                        "old_name",
564                        self.name.unwrap_or_else(|| {
565                            dev_panic!("no name present to use with with_alias");
566                            Content::Plain("??".to_owned())
567                        }),
568                    ),
569                ],
570            ));
571        }
572        self
573    }
574
575    /// map contains price+amount
576    #[must_use]
577    pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
578        self.trading_information = e.into().cloned();
579        self
580    }
581
582    #[must_use]
583    pub fn with_no_flee(mut self) -> Self {
584        self.no_flee = true;
585        self
586    }
587
588    #[must_use]
589    pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
590        self.loadout = loadout;
591        self
592    }
593}
594
595#[derive(Default)]
596pub struct ChunkSupplement {
597    pub entities: Vec<EntityInfo>,
598    pub rtsim_max_resources: EnumMap<rtsim::TerrainResource, usize>,
599}
600
601impl ChunkSupplement {
602    pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); }
603}
604
605#[cfg(test)]
606pub mod tests {
607    use super::*;
608    use crate::SkillSetBuilder;
609    use hashbrown::HashMap;
610
611    #[derive(Debug, Eq, Hash, PartialEq)]
612    enum MetaId {
613        SkillSetAsset,
614    }
615
616    impl Meta {
617        fn id(&self) -> MetaId {
618            match self {
619                Meta::SkillSetAsset(_) => MetaId::SkillSetAsset,
620            }
621        }
622    }
623
624    #[cfg(test)]
625    fn validate_body(body: &BodyBuilder, config_asset: &str) {
626        match body {
627            BodyBuilder::RandomWith(string) => {
628                let npc::NpcBody(_body_kind, mut body_creator) =
629                    string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
630                        panic!(
631                            "failed to parse body {:?} in {}. Err: {:?}",
632                            &string, config_asset, err
633                        )
634                    });
635                let _ = body_creator();
636            },
637            BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
638        }
639    }
640
641    #[cfg(test)]
642    fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
643        let InventorySpec { loadout, items } = inventory;
644
645        match loadout {
646            LoadoutKind::FromBody => {
647                if body.clone() == BodyBuilder::Uninit {
648                    // there is a big chance to call automatic name
649                    // when body is yet undefined
650                    panic!("Used FromBody loadout with Uninit body in {}", config_asset);
651                }
652            },
653            LoadoutKind::Asset(asset) => {
654                let loadout: LoadoutSpec = Ron::load_cloned(&asset)
655                    .expect("failed to load loadout asset")
656                    .into_inner();
657                loadout
658                    .validate(vec![asset])
659                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
660            },
661            LoadoutKind::Inline(spec) => {
662                spec.validate(Vec::new())
663                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
664            },
665        }
666
667        // TODO: check for number of items
668        //
669        // 1) just with 16 default slots?
670        // - well, keep in mind that not every item can stack to infinite amount
671        //
672        // 2) discover total capacity from loadout?
673        for (num, item_str) in items {
674            let item = Item::new_from_asset(&item_str);
675            let mut item = item.unwrap_or_else(|err| {
676                panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
677            });
678            item.set_amount(num).unwrap_or_else(|err| {
679                panic!(
680                    "can't set amount {} for {} in {}: {:?}",
681                    num, item_str, config_asset, err
682                );
683            });
684        }
685    }
686
687    #[cfg(test)]
688    fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
689        if (name == NameKind::Automatic || matches!(name, NameKind::Translate(_)))
690            && body == BodyBuilder::Uninit
691        {
692            // there is a big chance to call automatic name
693            // when body is yet undefined
694            //
695            // use .with_automatic_name() in code explicitly
696            panic!(
697                "Used Automatic/Translate name with Uninit body in {}",
698                config_asset
699            );
700        }
701    }
702
703    #[cfg(test)]
704    fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
705        use crate::lottery;
706        lottery::tests::validate_loot_spec(&loot);
707    }
708
709    #[cfg(test)]
710    fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
711        let mut meta_counter = HashMap::new();
712        for field in meta {
713            meta_counter
714                .entry(field.id())
715                .and_modify(|c| *c += 1)
716                .or_insert(1);
717
718            match field {
719                Meta::SkillSetAsset(asset) => {
720                    drop(SkillSetBuilder::from_asset_expect(&asset));
721                },
722            }
723        }
724        for (meta_id, counter) in meta_counter {
725            if counter > 1 {
726                panic!("Duplicate {:?} in {}", meta_id, config_asset);
727            }
728        }
729    }
730
731    #[cfg(test)]
732    fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
733        for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
734            (
735                Ron::<EntityConfig>::load_cloned(&pet_asset)
736                    .unwrap_or_else(|_| {
737                        panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
738                    })
739                    .into_inner(),
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) = Ron::<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            Ron::<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}