veloren_common/
generation.rs

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