Skip to main content

veloren_common/
generation.rs

1use std::ops::RangeInclusive;
2
3use crate::{
4    assets::{self, AssetExt, Error, Ron},
5    calendar::Calendar,
6    combat::{DeathEffects, RiderEffects, StatEffect},
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<StatEffect>,
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::RngExt,
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::RngExt,
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::RngExt,
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!("failed to parse body {string:?} in {config_asset:}. Err: {err:?}")
638                    });
639                let _ = body_creator();
640            },
641            BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
642        }
643    }
644
645    #[cfg(test)]
646    fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
647        let InventorySpec { loadout, items } = inventory;
648
649        match loadout {
650            LoadoutKind::FromBody => {
651                if body.clone() == BodyBuilder::Uninit {
652                    // there is a big chance to call automatic name
653                    // when body is yet undefined
654                    panic!("Used FromBody loadout with Uninit body in {}", config_asset);
655                }
656            },
657            LoadoutKind::Asset(asset) => {
658                let loadout: LoadoutSpec = Ron::load_cloned(&asset)
659                    .expect("failed to load loadout asset")
660                    .into_inner();
661                loadout
662                    .validate(vec![asset])
663                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
664            },
665            LoadoutKind::Inline(spec) => {
666                spec.validate(Vec::new())
667                    .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
668            },
669        }
670
671        // TODO: check for number of items
672        //
673        // 1) just with 16 default slots?
674        // - well, keep in mind that not every item can stack to infinite amount
675        //
676        // 2) discover total capacity from loadout?
677        for (num, item_str) in items {
678            let item = Item::new_from_asset(&item_str);
679            let mut item = item.unwrap_or_else(|err| {
680                panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
681            });
682            item.set_amount(num).unwrap_or_else(|err| {
683                panic!(
684                    "can't set amount {} for {} in {}: {:?}",
685                    num, item_str, config_asset, err
686                );
687            });
688        }
689    }
690
691    #[cfg(test)]
692    fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
693        if (name == NameKind::Automatic || matches!(name, NameKind::Translate(_)))
694            && body == BodyBuilder::Uninit
695        {
696            // there is a big chance to call automatic name
697            // when body is yet undefined
698            //
699            // use .with_automatic_name() in code explicitly
700            panic!(
701                "Used Automatic/Translate name with Uninit body in {}",
702                config_asset
703            );
704        }
705    }
706
707    #[cfg(test)]
708    fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
709        use crate::lottery;
710        lottery::tests::validate_loot_spec(&loot);
711    }
712
713    #[cfg(test)]
714    fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
715        let mut meta_counter = HashMap::new();
716        for field in meta {
717            meta_counter
718                .entry(field.id())
719                .and_modify(|c| *c += 1)
720                .or_insert(1);
721
722            match field {
723                Meta::SkillSetAsset(asset) => {
724                    drop(SkillSetBuilder::from_asset_expect(&asset));
725                },
726            }
727        }
728        for (meta_id, counter) in meta_counter {
729            if counter > 1 {
730                panic!("Duplicate {:?} in {}", meta_id, config_asset);
731            }
732        }
733    }
734
735    #[cfg(test)]
736    fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
737        for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
738            (
739                Ron::<EntityConfig>::load_cloned(&pet_asset)
740                    .unwrap_or_else(|_| {
741                        panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
742                    })
743                    .into_inner(),
744                amount,
745            )
746        }) {
747            assert!(
748                amount.end() >= amount.start(),
749                "Invalid pet spawn range ({}..={}), in {}",
750                amount.start(),
751                amount.end(),
752                config_asset
753            );
754            if !pet.pets.is_empty() {
755                panic!("Pets must not be owners of pets: {config_asset}");
756            }
757        }
758    }
759
760    #[cfg(test)]
761    fn validate_death_effects(effects: Vec<StatEffect>, config_asset: &str) {
762        for effect in effects {
763            use crate::combat::CombatEffect;
764            match effect.effect {
765                // We only care about testing transform, since that is the only one that can be
766                // failable at runtime (since it references an asset file)
767                CombatEffect::Transform {
768                    entity_spec,
769                    allow_players: _,
770                } => {
771                    if let Err(error) = Ron::<EntityConfig>::load(&entity_spec) {
772                        panic!(
773                            "Error while loading transform entity spec ({entity_spec}) for entity \
774                             {config_asset}: {error:?}"
775                        );
776                    }
777                },
778                CombatEffect::Heal(_)
779                | CombatEffect::Buff(_)
780                | CombatEffect::Knockback(_)
781                | CombatEffect::EnergyReward(_)
782                | CombatEffect::Lifesteal(_)
783                | CombatEffect::Poise(_)
784                | CombatEffect::Combo(_)
785                | CombatEffect::StageVulnerable(_, _)
786                | CombatEffect::RefreshBuff(_, _)
787                | CombatEffect::BuffsVulnerable(_, _)
788                | CombatEffect::StunnedVulnerable(_)
789                | CombatEffect::SelfBuff(_)
790                | CombatEffect::Energy(_)
791                | CombatEffect::DebuffsVulnerable { .. } => {},
792            }
793        }
794    }
795
796    fn validate_rider(rider: Option<String>, config_asset: &str) {
797        if let Some(rider) = rider {
798            Ron::<EntityConfig>::load_cloned(&rider).unwrap_or_else(|_| {
799                panic!("Rider asset path invalid: \"{rider}\", in {config_asset}")
800            });
801        }
802    }
803
804    #[cfg(test)]
805    pub fn validate_entity_config(config_asset: &str) {
806        let EntityConfig {
807            body,
808            inventory,
809            name,
810            loot,
811            pets,
812            rider,
813            meta,
814            death_effects,
815            alignment: _, // can't fail if serialized, it's a boring enum
816            rider_effects: _,
817            scale,
818            agent: _,
819        } = EntityConfig::from_asset_expect_owned(config_asset);
820
821        assert!(
822            scale.is_finite() && scale > 0.0,
823            "Scale has to be finite and greater than zero"
824        );
825
826        validate_body(&body, config_asset);
827        // body dependent stuff
828        validate_inventory(inventory, &body, config_asset);
829        validate_name(name, body, config_asset);
830        // misc
831        validate_loot(loot, config_asset);
832        validate_meta(meta, config_asset);
833        validate_pets(pets, config_asset);
834        validate_rider(rider, config_asset);
835        validate_death_effects(death_effects, config_asset);
836    }
837
838    #[test]
839    fn test_all_entity_assets() {
840        // Get list of entity configs, load everything, validate content.
841        let entity_configs =
842            try_all_entity_configs().expect("Failed to access entity configs directory");
843        for config_asset in entity_configs {
844            validate_entity_config(&config_asset)
845        }
846    }
847}