1use std::ops::RangeInclusive;
2
3use crate::{
4 assets::{self, AssetExt, Error},
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 Translate(String),
30 Automatic,
33 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#[derive(Debug, Deserialize, Clone)]
119#[serde(deny_unknown_fields)]
120pub struct EntityConfig {
121 name: NameKind,
127
128 pub body: BodyBuilder,
133
134 pub alignment: AlignmentMark,
136
137 #[serde(default)]
139 pub agent: AgentConfig,
140
141 pub loot: LootSpec<String>,
144
145 pub inventory: InventorySpec,
148
149 #[serde(default)]
152 pub pets: Vec<(String, RangeInclusive<usize>)>,
153
154 #[serde(default)]
156 pub rider: Option<String>,
157
158 #[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 #[serde(default)]
172 pub meta: Vec<Meta>,
173}
174
175impl assets::Asset for EntityConfig {
176 type Loader = assets::RonLoader;
177
178 const EXTENSION: &'static str = "ron";
179}
180
181impl EntityConfig {
182 pub fn from_asset_expect_owned(asset_specifier: &str) -> Self {
183 Self::load_owned(asset_specifier)
184 .unwrap_or_else(|e| panic!("Failed to load {}. Error: {:?}", asset_specifier, e))
185 }
186
187 #[must_use]
188 pub fn with_body(mut self, body: BodyBuilder) -> Self {
189 self.body = body;
190
191 self
192 }
193}
194
195pub fn try_all_entity_configs() -> Result<Vec<String>, Error> {
197 let configs = assets::load_rec_dir::<EntityConfig>("common.entity")?;
198 Ok(configs.read().ids().map(|id| id.to_string()).collect())
199}
200
201#[derive(Clone, Debug)]
202pub enum SpecialEntity {
203 Waypoint,
204 Teleporter(PortalData),
205 ArenaTotem {
207 range: f32,
208 },
209}
210
211#[derive(Clone)]
212pub struct EntityInfo {
213 pub pos: Vec3<f32>,
214 pub alignment: Alignment,
215 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 pub body: Body,
223 pub name: Option<Content>,
224 pub scale: f32,
225 pub loot: LootSpec<String>,
227 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 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 pub trading_information: Option<SiteInformation>,
249 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 #[must_use]
287 pub fn with_asset_expect<R>(
288 self,
289 asset_specifier: &str,
290 loadout_rng: &mut R,
291 time: Option<&(TimeOfDay, Calendar)>,
292 ) -> Self
293 where
294 R: rand::Rng,
295 {
296 let config = EntityConfig::load_expect_cloned(asset_specifier);
297
298 self.with_entity_config(config, Some(asset_specifier), loadout_rng, time)
299 }
300
301 #[must_use]
303 pub fn with_entity_config<R>(
304 mut self,
305 config: EntityConfig,
306 config_asset: Option<&str>,
307 loadout_rng: &mut R,
308 time: Option<&(TimeOfDay, Calendar)>,
309 ) -> Self
310 where
311 R: rand::Rng,
312 {
313 let EntityConfig {
314 name,
315 body,
316 alignment,
317 agent,
318 inventory,
319 loot,
320 meta,
321 scale,
322 pets,
323 rider,
324 death_effects,
325 rider_effects,
326 } = config;
327
328 match body {
329 BodyBuilder::RandomWith(string) => {
330 let npc::NpcBody(_body_kind, mut body_creator) =
331 string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
332 panic!("failed to parse body {:?}. Err: {:?}", &string, err)
333 });
334 let body = body_creator();
335 self = self.with_body(body);
336 },
337 BodyBuilder::Exact(body) => {
338 self = self.with_body(body);
339 },
340 BodyBuilder::Uninit => {},
341 }
342
343 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 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 = 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.gen_range(start..=end)).map(|_| {
375 EntityInfo::at(self.pos).with_entity_config(
376 config.clone(),
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 = EntityConfig::load_expect(&rider).read();
389 Box::new(EntityInfo::at(self.pos).with_entity_config(
390 config.clone(),
391 config_asset,
392 loadout_rng,
393 time,
394 ))
395 });
396
397 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 #[must_use]
425 fn with_inventory<R>(
426 mut self,
427 inventory: InventorySpec,
428 config_asset: Option<&str>,
429 rng: &mut R,
430 time: Option<&(TimeOfDay, Calendar)>,
431 ) -> Self
432 where
433 R: rand::Rng,
434 {
435 let config_asset = config_asset.unwrap_or("???");
436 let InventorySpec { loadout, items } = inventory;
437
438 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 #[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: String) -> Self {
562 self.name = Some(Content::localized_with_args(
563 "name-misc-with-alias-template",
564 [
565 ("alias", Content::Plain(alias)),
566 (
567 "old_name",
568 self.name.unwrap_or_else(|| {
569 dev_panic!("no name present to use with with_alias");
570 Content::Plain("??".to_owned())
571 }),
572 ),
573 ],
574 ));
575 self
576 }
577
578 #[must_use]
580 pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
581 self.trading_information = e.into().cloned();
582 self
583 }
584
585 #[must_use]
586 pub fn with_no_flee(mut self) -> Self {
587 self.no_flee = true;
588 self
589 }
590
591 #[must_use]
592 pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
593 self.loadout = loadout;
594 self
595 }
596}
597
598#[derive(Default)]
599pub struct ChunkSupplement {
600 pub entities: Vec<EntityInfo>,
601 pub rtsim_max_resources: EnumMap<rtsim::ChunkResource, usize>,
602}
603
604impl ChunkSupplement {
605 pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); }
606}
607
608#[cfg(test)]
609pub mod tests {
610 use super::*;
611 use crate::SkillSetBuilder;
612 use hashbrown::HashMap;
613
614 #[derive(Debug, Eq, Hash, PartialEq)]
615 enum MetaId {
616 SkillSetAsset,
617 }
618
619 impl Meta {
620 fn id(&self) -> MetaId {
621 match self {
622 Meta::SkillSetAsset(_) => MetaId::SkillSetAsset,
623 }
624 }
625 }
626
627 #[cfg(test)]
628 fn validate_body(body: &BodyBuilder, config_asset: &str) {
629 match body {
630 BodyBuilder::RandomWith(string) => {
631 let npc::NpcBody(_body_kind, mut body_creator) =
632 string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
633 panic!(
634 "failed to parse body {:?} in {}. Err: {:?}",
635 &string, config_asset, err
636 )
637 });
638 let _ = body_creator();
639 },
640 BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
641 }
642 }
643
644 #[cfg(test)]
645 fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
646 let InventorySpec { loadout, items } = inventory;
647
648 match loadout {
649 LoadoutKind::FromBody => {
650 if body.clone() == BodyBuilder::Uninit {
651 panic!("Used FromBody loadout with Uninit body in {}", config_asset);
654 }
655 },
656 LoadoutKind::Asset(asset) => {
657 let loadout =
658 LoadoutSpec::load_cloned(&asset).expect("failed to load loadout asset");
659 loadout
660 .validate(vec![asset])
661 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
662 },
663 LoadoutKind::Inline(spec) => {
664 spec.validate(Vec::new())
665 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
666 },
667 }
668
669 for (num, item_str) in items {
676 let item = Item::new_from_asset(&item_str);
677 let mut item = item.unwrap_or_else(|err| {
678 panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
679 });
680 item.set_amount(num).unwrap_or_else(|err| {
681 panic!(
682 "can't set amount {} for {} in {}: {:?}",
683 num, item_str, config_asset, err
684 );
685 });
686 }
687 }
688
689 #[cfg(test)]
690 fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
691 if (name == NameKind::Automatic || matches!(name, NameKind::Translate(_)))
692 && body == BodyBuilder::Uninit
693 {
694 panic!(
699 "Used Automatic/Translate name with Uninit body in {}",
700 config_asset
701 );
702 }
703 }
704
705 #[cfg(test)]
706 fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
707 use crate::lottery;
708 lottery::tests::validate_loot_spec(&loot);
709 }
710
711 #[cfg(test)]
712 fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
713 let mut meta_counter = HashMap::new();
714 for field in meta {
715 meta_counter
716 .entry(field.id())
717 .and_modify(|c| *c += 1)
718 .or_insert(1);
719
720 match field {
721 Meta::SkillSetAsset(asset) => {
722 drop(SkillSetBuilder::from_asset_expect(&asset));
723 },
724 }
725 }
726 for (meta_id, counter) in meta_counter {
727 if counter > 1 {
728 panic!("Duplicate {:?} in {}", meta_id, config_asset);
729 }
730 }
731 }
732
733 #[cfg(test)]
734 fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
735 for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
736 (
737 EntityConfig::load_cloned(&pet_asset).unwrap_or_else(|_| {
738 panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
739 }),
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) = 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 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: _, 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 validate_inventory(inventory, &body, config_asset);
813 validate_name(name, body, config_asset);
814 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 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}