1use std::ops::RangeInclusive;
2
3use crate::{
4 assets::{self, AssetExt, Error},
5 calendar::Calendar,
6 combat::{DeathEffect, DeathEffects, RiderEffects},
7 comp::{
8 self, 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 enum_map::EnumMap;
20use serde::Deserialize;
21use tracing::error;
22use vek::*;
23
24#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
25pub enum NameKind {
26 Name(String),
27 Automatic,
28 Uninit,
29}
30
31#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
32pub enum BodyBuilder {
33 RandomWith(String),
34 Exact(Body),
35 Uninit,
36}
37
38#[derive(Debug, Deserialize, Clone)]
39pub enum AlignmentMark {
40 Alignment(Alignment),
41 Uninit,
42}
43
44impl Default for AlignmentMark {
45 fn default() -> Self { Self::Alignment(Alignment::Wild) }
46}
47
48#[derive(Default, Debug, Deserialize, Clone)]
49#[serde(default)]
50pub struct AgentConfig {
51 pub has_agency: Option<bool>,
52 pub no_flee: Option<bool>,
53 pub idle_wander_factor: Option<f32>,
54 pub aggro_range_multiplier: Option<f32>,
55}
56
57#[derive(Debug, Deserialize, Clone)]
58pub enum LoadoutKind {
59 FromBody,
60 Asset(String),
61 Inline(Box<LoadoutSpec>),
62}
63
64#[derive(Debug, Deserialize, Clone)]
65pub struct InventorySpec {
66 pub loadout: LoadoutKind,
67 #[serde(default)]
68 pub items: Vec<(u32, String)>,
69}
70
71#[derive(Debug, Deserialize, Clone)]
72pub enum Meta {
73 SkillSetAsset(String),
74}
75
76#[derive(Debug, Deserialize, Clone)]
113#[serde(deny_unknown_fields)]
114pub struct EntityConfig {
115 name: NameKind,
121
122 pub body: BodyBuilder,
127
128 pub alignment: AlignmentMark,
130
131 #[serde(default)]
133 pub agent: AgentConfig,
134
135 pub loot: LootSpec<String>,
138
139 pub inventory: InventorySpec,
142
143 #[serde(default)]
146 pub pets: Vec<(String, RangeInclusive<usize>)>,
147
148 #[serde(default)]
150 pub rider: Option<String>,
151
152 #[serde(default)]
154 pub rider_effects: Vec<BuffEffect>,
155
156 #[serde(default = "num_traits::One::one")]
157 pub scale: f32,
158
159 #[serde(default)]
160 pub death_effects: Vec<DeathEffect>,
161
162 #[serde(default)]
166 pub meta: Vec<Meta>,
167}
168
169impl assets::Asset for EntityConfig {
170 type Loader = assets::RonLoader;
171
172 const EXTENSION: &'static str = "ron";
173}
174
175impl EntityConfig {
176 pub fn from_asset_expect_owned(asset_specifier: &str) -> Self {
177 Self::load_owned(asset_specifier)
178 .unwrap_or_else(|e| panic!("Failed to load {}. Error: {:?}", asset_specifier, e))
179 }
180
181 #[must_use]
182 pub fn with_body(mut self, body: BodyBuilder) -> Self {
183 self.body = body;
184
185 self
186 }
187}
188
189pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
191 let configs = assets::load_rec_dir::<EntityConfig>("common.entity")?;
192 Ok(configs.read().ids().map(|id| id.to_string()).collect())
193}
194
195#[derive(Clone, Debug)]
196pub enum SpecialEntity {
197 Waypoint,
198 Teleporter(PortalData),
199 ArenaTotem {
201 range: f32,
202 },
203}
204
205#[derive(Clone)]
206pub struct EntityInfo {
207 pub pos: Vec3<f32>,
208 pub alignment: Alignment,
209 pub has_agency: bool,
211 pub agent_mark: Option<agent::Mark>,
212 pub no_flee: bool,
213 pub idle_wander_factor: f32,
214 pub aggro_range_multiplier: f32,
215 pub body: Body,
217 pub name: Option<String>,
218 pub scale: f32,
219 pub loot: LootSpec<String>,
221 pub inventory: Vec<(u32, Item)>,
223 pub loadout: LoadoutBuilder,
224 pub make_loadout: Option<
225 fn(
226 LoadoutBuilder,
227 Option<&SiteInformation>,
228 time: Option<&(TimeOfDay, Calendar)>,
229 ) -> LoadoutBuilder,
230 >,
231 pub skillset_asset: Option<String>,
233 pub death_effects: Option<DeathEffects>,
234 pub rider_effects: Option<RiderEffects>,
235
236 pub pets: Vec<EntityInfo>,
237
238 pub rider: Option<Box<EntityInfo>>,
239
240 pub trading_information: Option<SiteInformation>,
243 pub special_entity: Option<SpecialEntity>,
247}
248
249impl EntityInfo {
250 pub fn at(pos: Vec3<f32>) -> Self {
251 Self {
252 pos,
253 alignment: Alignment::Wild,
254
255 has_agency: true,
256 agent_mark: None,
257 no_flee: false,
258 idle_wander_factor: 1.0,
259 aggro_range_multiplier: 1.0,
260
261 body: Body::Humanoid(humanoid::Body::random()),
262 name: None,
263 scale: 1.0,
264 loot: LootSpec::Nothing,
265 inventory: Vec::new(),
266 loadout: LoadoutBuilder::empty(),
267 make_loadout: None,
268 death_effects: None,
269 rider_effects: None,
270 skillset_asset: None,
271 pets: Vec::new(),
272 rider: None,
273 trading_information: None,
274 special_entity: None,
275 }
276 }
277
278 #[must_use]
281 pub fn with_asset_expect<R>(
282 self,
283 asset_specifier: &str,
284 loadout_rng: &mut R,
285 time: Option<&(TimeOfDay, Calendar)>,
286 ) -> Self
287 where
288 R: rand::Rng,
289 {
290 let config = EntityConfig::load_expect_cloned(asset_specifier);
291
292 self.with_entity_config(config, Some(asset_specifier), loadout_rng, time)
293 }
294
295 #[must_use]
297 pub fn with_entity_config<R>(
298 mut self,
299 config: EntityConfig,
300 config_asset: Option<&str>,
301 loadout_rng: &mut R,
302 time: Option<&(TimeOfDay, Calendar)>,
303 ) -> Self
304 where
305 R: rand::Rng,
306 {
307 let EntityConfig {
308 name,
309 body,
310 alignment,
311 agent,
312 inventory,
313 loot,
314 meta,
315 scale,
316 pets,
317 rider,
318 death_effects,
319 rider_effects,
320 } = config;
321
322 match body {
323 BodyBuilder::RandomWith(string) => {
324 let npc::NpcBody(_body_kind, mut body_creator) =
325 string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
326 panic!("failed to parse body {:?}. Err: {:?}", &string, err)
327 });
328 let body = body_creator();
329 self = self.with_body(body);
330 },
331 BodyBuilder::Exact(body) => {
332 self = self.with_body(body);
333 },
334 BodyBuilder::Uninit => {},
335 }
336
337 match name {
339 NameKind::Name(name) => {
340 self = self.with_name(name);
341 },
342 NameKind::Automatic => {
343 self = self.with_automatic_name(None);
344 },
345 NameKind::Uninit => {},
346 }
347
348 if let AlignmentMark::Alignment(alignment) = alignment {
349 self = self.with_alignment(alignment);
350 }
351
352 self = self.with_loot_drop(loot);
353
354 self = self.with_inventory(inventory, config_asset, loadout_rng, time);
356
357 let mut pet_infos: Vec<EntityInfo> = Vec::new();
358 for (pet_asset, amount) in pets {
359 let config = EntityConfig::load_expect(&pet_asset).read();
360 let (start, mut end) = amount.into_inner();
361 if start > end {
362 error!("Invalid range for pet count start: {start}, end: {end}");
363 end = start;
364 }
365
366 pet_infos.extend((0..loadout_rng.gen_range(start..=end)).map(|_| {
367 EntityInfo::at(self.pos).with_entity_config(
368 config.clone(),
369 config_asset,
370 loadout_rng,
371 time,
372 )
373 }));
374 }
375 self.scale = scale;
376
377 self.pets = pet_infos;
378
379 self.rider = rider.map(|rider| {
380 let config = EntityConfig::load_expect(&rider).read();
381 Box::new(EntityInfo::at(self.pos).with_entity_config(
382 config.clone(),
383 config_asset,
384 loadout_rng,
385 time,
386 ))
387 });
388
389 let AgentConfig {
391 has_agency,
392 no_flee,
393 idle_wander_factor,
394 aggro_range_multiplier,
395 } = agent;
396 self.has_agency = has_agency.unwrap_or(self.has_agency);
397 self.no_flee = no_flee.unwrap_or(self.no_flee);
398 self.idle_wander_factor = idle_wander_factor.unwrap_or(self.idle_wander_factor);
399 self.aggro_range_multiplier = aggro_range_multiplier.unwrap_or(self.aggro_range_multiplier);
400 self.death_effects = (!death_effects.is_empty()).then_some(DeathEffects(death_effects));
401 self.rider_effects = (!rider_effects.is_empty()).then_some(RiderEffects(rider_effects));
402
403 for field in meta {
404 match field {
405 Meta::SkillSetAsset(asset) => {
406 self = self.with_skillset_asset(asset);
407 },
408 }
409 }
410
411 self
412 }
413
414 #[must_use]
417 fn with_inventory<R>(
418 mut self,
419 inventory: InventorySpec,
420 config_asset: Option<&str>,
421 rng: &mut R,
422 time: Option<&(TimeOfDay, Calendar)>,
423 ) -> Self
424 where
425 R: rand::Rng,
426 {
427 let config_asset = config_asset.unwrap_or("???");
428 let InventorySpec { loadout, items } = inventory;
429
430 self.inventory = items
434 .into_iter()
435 .map(|(num, i)| (num, Item::new_from_asset_expect(&i)))
436 .collect();
437
438 match loadout {
439 LoadoutKind::FromBody => {
440 self = self.with_default_equip();
441 },
442 LoadoutKind::Asset(loadout) => {
443 let loadout = LoadoutBuilder::from_asset(&loadout, rng, time).unwrap_or_else(|e| {
444 panic!("failed to load loadout for {config_asset}: {e:?}");
445 });
446 self.loadout = loadout;
447 },
448 LoadoutKind::Inline(loadout_spec) => {
449 let loadout = LoadoutBuilder::from_loadout_spec(*loadout_spec, rng, time)
450 .unwrap_or_else(|e| {
451 panic!("failed to load loadout for {config_asset}: {e:?}");
452 });
453 self.loadout = loadout;
454 },
455 }
456
457 self
458 }
459
460 #[must_use]
463 fn with_default_equip(mut self) -> Self {
464 let loadout_builder = LoadoutBuilder::from_default(&self.body);
465 self.loadout = loadout_builder;
466
467 self
468 }
469
470 #[must_use]
471 pub fn do_if(mut self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
472 if cond {
473 self = f(self);
474 }
475 self
476 }
477
478 #[must_use]
479 pub fn into_special(mut self, special: SpecialEntity) -> Self {
480 self.special_entity = Some(special);
481 self
482 }
483
484 #[must_use]
485 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
486 self.alignment = alignment;
487 self
488 }
489
490 #[must_use]
491 pub fn with_body(mut self, body: Body) -> Self {
492 self.body = body;
493 self
494 }
495
496 #[must_use]
497 pub fn with_name(mut self, name: impl Into<String>) -> Self {
498 self.name = Some(name.into());
499 self
500 }
501
502 #[must_use]
503 pub fn with_agency(mut self, agency: bool) -> Self {
504 self.has_agency = agency;
505 self
506 }
507
508 #[must_use]
509 pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
510 self.agent_mark = agent_mark.into();
511 self
512 }
513
514 #[must_use]
515 pub fn with_loot_drop(mut self, loot_drop: LootSpec<String>) -> Self {
516 self.loot = loot_drop;
517 self
518 }
519
520 #[must_use]
521 pub fn with_scale(mut self, scale: f32) -> Self {
522 self.scale = scale;
523 self
524 }
525
526 #[must_use]
527 pub fn with_lazy_loadout(
528 mut self,
529 creator: fn(
530 LoadoutBuilder,
531 Option<&SiteInformation>,
532 time: Option<&(TimeOfDay, Calendar)>,
533 ) -> LoadoutBuilder,
534 ) -> Self {
535 self.make_loadout = Some(creator);
536 self
537 }
538
539 #[must_use]
540 pub fn with_skillset_asset(mut self, asset: String) -> Self {
541 self.skillset_asset = Some(asset);
542 self
543 }
544
545 #[must_use]
546 pub fn with_automatic_name(mut self, alias: Option<String>) -> Self {
547 let npc_names = NPC_NAMES.read();
548 let name = match &self.body {
549 Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)),
550 Body::QuadrupedMedium(body) => {
551 Some(get_npc_name(&npc_names.quadruped_medium, body.species))
552 },
553 Body::BirdMedium(body) => Some(get_npc_name(&npc_names.bird_medium, body.species)),
554 Body::BirdLarge(body) => Some(get_npc_name(&npc_names.bird_large, body.species)),
555 Body::FishSmall(body) => Some(get_npc_name(&npc_names.fish_small, body.species)),
556 Body::FishMedium(body) => Some(get_npc_name(&npc_names.fish_medium, body.species)),
557 Body::Theropod(body) => Some(get_npc_name(&npc_names.theropod, body.species)),
558 Body::QuadrupedSmall(body) => {
559 Some(get_npc_name(&npc_names.quadruped_small, body.species))
560 },
561 Body::Dragon(body) => Some(get_npc_name(&npc_names.dragon, body.species)),
562 Body::QuadrupedLow(body) => Some(get_npc_name(&npc_names.quadruped_low, body.species)),
563 Body::Golem(body) => Some(get_npc_name(&npc_names.golem, body.species)),
564 Body::BipedLarge(body) => Some(get_npc_name(&npc_names.biped_large, body.species)),
565 Body::Arthropod(body) => Some(get_npc_name(&npc_names.arthropod, body.species)),
566 Body::Crustacean(body) => Some(get_npc_name(&npc_names.crustacean, body.species)),
567 Body::Plugin(body) => Some(get_npc_name(&npc_names.plugin, body.species)),
568 _ => None,
569 };
570 self.name = name.map(|name| {
571 if let Some(alias) = alias {
572 format!("{alias} ({name})")
573 } else {
574 name.to_string()
575 }
576 });
577 self
578 }
579
580 #[must_use]
581 pub fn with_alias(mut self, alias: String) -> Self {
582 self.name = Some(if let Some(name) = self.name {
583 format!("{alias} ({name})")
584 } else {
585 alias
586 });
587 self
588 }
589
590 #[must_use]
592 pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
593 self.trading_information = e.into().cloned();
594 self
595 }
596
597 #[must_use]
598 pub fn with_no_flee(mut self) -> Self {
599 self.no_flee = true;
600 self
601 }
602
603 #[must_use]
604 pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
605 self.loadout = loadout;
606 self
607 }
608}
609
610#[derive(Default)]
611pub struct ChunkSupplement {
612 pub entities: Vec<EntityInfo>,
613 pub rtsim_max_resources: EnumMap<rtsim::ChunkResource, usize>,
614}
615
616impl ChunkSupplement {
617 pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); }
618}
619
620pub fn get_npc_name<
621 'a,
622 Species,
623 SpeciesData: for<'b> core::ops::Index<&'b Species, Output = npc::SpeciesNames>,
624>(
625 body_data: &'a comp::BodyData<npc::BodyNames, SpeciesData>,
626 species: Species,
627) -> &'a str {
628 &body_data.species[&species].generic
629}
630
631#[cfg(test)]
632pub mod tests {
633 use super::*;
634 use crate::SkillSetBuilder;
635 use hashbrown::HashMap;
636
637 #[derive(Debug, Eq, Hash, PartialEq)]
638 enum MetaId {
639 SkillSetAsset,
640 }
641
642 impl Meta {
643 fn id(&self) -> MetaId {
644 match self {
645 Meta::SkillSetAsset(_) => MetaId::SkillSetAsset,
646 }
647 }
648 }
649
650 #[cfg(test)]
651 fn validate_body(body: &BodyBuilder, config_asset: &str) {
652 match body {
653 BodyBuilder::RandomWith(string) => {
654 let npc::NpcBody(_body_kind, mut body_creator) =
655 string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
656 panic!(
657 "failed to parse body {:?} in {}. Err: {:?}",
658 &string, config_asset, err
659 )
660 });
661 let _ = body_creator();
662 },
663 BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
664 }
665 }
666
667 #[cfg(test)]
668 fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
669 let InventorySpec { loadout, items } = inventory;
670
671 match loadout {
672 LoadoutKind::FromBody => {
673 if body.clone() == BodyBuilder::Uninit {
674 panic!("Used FromBody loadout with Uninit body in {}", config_asset);
677 }
678 },
679 LoadoutKind::Asset(asset) => {
680 let loadout =
681 LoadoutSpec::load_cloned(&asset).expect("failed to load loadout asset");
682 loadout
683 .validate(vec![asset])
684 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
685 },
686 LoadoutKind::Inline(spec) => {
687 spec.validate(Vec::new())
688 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
689 },
690 }
691
692 for (num, item_str) in items {
699 let item = Item::new_from_asset(&item_str);
700 let mut item = item.unwrap_or_else(|err| {
701 panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
702 });
703 item.set_amount(num).unwrap_or_else(|err| {
704 panic!(
705 "can't set amount {} for {} in {}: {:?}",
706 num, item_str, config_asset, err
707 );
708 });
709 }
710 }
711
712 #[cfg(test)]
713 fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
714 if name == NameKind::Automatic && body == BodyBuilder::Uninit {
715 panic!("Used Automatic name with Uninit body in {}", config_asset);
720 }
721 }
722
723 #[cfg(test)]
724 fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
725 use crate::lottery;
726 lottery::tests::validate_loot_spec(&loot);
727 }
728
729 #[cfg(test)]
730 fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
731 let mut meta_counter = HashMap::new();
732 for field in meta {
733 meta_counter
734 .entry(field.id())
735 .and_modify(|c| *c += 1)
736 .or_insert(1);
737
738 match field {
739 Meta::SkillSetAsset(asset) => {
740 drop(SkillSetBuilder::from_asset_expect(&asset));
741 },
742 }
743 }
744 for (meta_id, counter) in meta_counter {
745 if counter > 1 {
746 panic!("Duplicate {:?} in {}", meta_id, config_asset);
747 }
748 }
749 }
750
751 #[cfg(test)]
752 fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
753 for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
754 (
755 EntityConfig::load_cloned(&pet_asset).unwrap_or_else(|_| {
756 panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
757 }),
758 amount,
759 )
760 }) {
761 assert!(
762 amount.end() >= amount.start(),
763 "Invalid pet spawn range ({}..={}), in {}",
764 amount.start(),
765 amount.end(),
766 config_asset
767 );
768 if !pet.pets.is_empty() {
769 panic!("Pets must not be owners of pets: {config_asset}");
770 }
771 }
772 }
773
774 #[cfg(test)]
775 fn validate_death_effects(effects: Vec<DeathEffect>, config_asset: &str) {
776 for effect in effects {
777 match effect {
778 DeathEffect::AttackerBuff {
779 kind: _,
780 strength: _,
781 duration: _,
782 } => {},
783 DeathEffect::Transform {
784 entity_spec,
785 allow_players: _,
786 } => {
787 if let Err(error) = EntityConfig::load(&entity_spec) {
788 panic!(
789 "Error while loading transform entity spec ({entity_spec}) for entity \
790 {config_asset}: {error:?}"
791 );
792 }
793 },
794 }
795 }
796 }
797
798 fn validate_rider(rider: Option<String>, config_asset: &str) {
799 if let Some(rider) = rider {
800 EntityConfig::load_cloned(&rider).unwrap_or_else(|_| {
801 panic!("Rider asset path invalid: \"{rider}\", in {config_asset}")
802 });
803 }
804 }
805
806 #[cfg(test)]
807 pub fn validate_entity_config(config_asset: &str) {
808 let EntityConfig {
809 body,
810 inventory,
811 name,
812 loot,
813 pets,
814 rider,
815 meta,
816 death_effects,
817 alignment: _, rider_effects: _,
819 scale,
820 agent: _,
821 } = EntityConfig::from_asset_expect_owned(config_asset);
822
823 assert!(
824 scale.is_finite() && scale > 0.0,
825 "Scale has to be finite and greater than zero"
826 );
827
828 validate_body(&body, config_asset);
829 validate_inventory(inventory, &body, config_asset);
831 validate_name(name, body, config_asset);
832 validate_loot(loot, config_asset);
834 validate_meta(meta, config_asset);
835 validate_pets(pets, config_asset);
836 validate_rider(rider, config_asset);
837 validate_death_effects(death_effects, config_asset);
838 }
839
840 #[test]
841 fn test_all_entity_assets() {
842 let entity_configs =
844 try_all_entity_configs().expect("Failed to access entity configs directory");
845 for config_asset in entity_configs {
846 validate_entity_config(&config_asset)
847 }
848 }
849}