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 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 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
190pub 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 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 = Ron::load_expect_cloned(asset_specifier).into_inner();
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 = 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 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: 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 #[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!(
638 "failed to parse body {:?} in {}. Err: {:?}",
639 &string, config_asset, err
640 )
641 });
642 let _ = body_creator();
643 },
644 BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
645 }
646 }
647
648 #[cfg(test)]
649 fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
650 let InventorySpec { loadout, items } = inventory;
651
652 match loadout {
653 LoadoutKind::FromBody => {
654 if body.clone() == BodyBuilder::Uninit {
655 panic!("Used FromBody loadout with Uninit body in {}", config_asset);
658 }
659 },
660 LoadoutKind::Asset(asset) => {
661 let loadout: LoadoutSpec = Ron::load_cloned(&asset)
662 .expect("failed to load loadout asset")
663 .into_inner();
664 loadout
665 .validate(vec![asset])
666 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
667 },
668 LoadoutKind::Inline(spec) => {
669 spec.validate(Vec::new())
670 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
671 },
672 }
673
674 for (num, item_str) in items {
681 let item = Item::new_from_asset(&item_str);
682 let mut item = item.unwrap_or_else(|err| {
683 panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
684 });
685 item.set_amount(num).unwrap_or_else(|err| {
686 panic!(
687 "can't set amount {} for {} in {}: {:?}",
688 num, item_str, config_asset, err
689 );
690 });
691 }
692 }
693
694 #[cfg(test)]
695 fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
696 if (name == NameKind::Automatic || matches!(name, NameKind::Translate(_)))
697 && body == BodyBuilder::Uninit
698 {
699 panic!(
704 "Used Automatic/Translate name with Uninit body in {}",
705 config_asset
706 );
707 }
708 }
709
710 #[cfg(test)]
711 fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
712 use crate::lottery;
713 lottery::tests::validate_loot_spec(&loot);
714 }
715
716 #[cfg(test)]
717 fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
718 let mut meta_counter = HashMap::new();
719 for field in meta {
720 meta_counter
721 .entry(field.id())
722 .and_modify(|c| *c += 1)
723 .or_insert(1);
724
725 match field {
726 Meta::SkillSetAsset(asset) => {
727 drop(SkillSetBuilder::from_asset_expect(&asset));
728 },
729 }
730 }
731 for (meta_id, counter) in meta_counter {
732 if counter > 1 {
733 panic!("Duplicate {:?} in {}", meta_id, config_asset);
734 }
735 }
736 }
737
738 #[cfg(test)]
739 fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
740 for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
741 (
742 Ron::<EntityConfig>::load_cloned(&pet_asset)
743 .unwrap_or_else(|_| {
744 panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
745 })
746 .into_inner(),
747 amount,
748 )
749 }) {
750 assert!(
751 amount.end() >= amount.start(),
752 "Invalid pet spawn range ({}..={}), in {}",
753 amount.start(),
754 amount.end(),
755 config_asset
756 );
757 if !pet.pets.is_empty() {
758 panic!("Pets must not be owners of pets: {config_asset}");
759 }
760 }
761 }
762
763 #[cfg(test)]
764 fn validate_death_effects(effects: Vec<DeathEffect>, config_asset: &str) {
765 for effect in effects {
766 match effect {
767 DeathEffect::AttackerBuff {
768 kind: _,
769 strength: _,
770 duration: _,
771 } => {},
772 DeathEffect::Transform {
773 entity_spec,
774 allow_players: _,
775 } => {
776 if let Err(error) = Ron::<EntityConfig>::load(&entity_spec) {
777 panic!(
778 "Error while loading transform entity spec ({entity_spec}) for entity \
779 {config_asset}: {error:?}"
780 );
781 }
782 },
783 }
784 }
785 }
786
787 fn validate_rider(rider: Option<String>, config_asset: &str) {
788 if let Some(rider) = rider {
789 Ron::<EntityConfig>::load_cloned(&rider).unwrap_or_else(|_| {
790 panic!("Rider asset path invalid: \"{rider}\", in {config_asset}")
791 });
792 }
793 }
794
795 #[cfg(test)]
796 pub fn validate_entity_config(config_asset: &str) {
797 let EntityConfig {
798 body,
799 inventory,
800 name,
801 loot,
802 pets,
803 rider,
804 meta,
805 death_effects,
806 alignment: _, rider_effects: _,
808 scale,
809 agent: _,
810 } = EntityConfig::from_asset_expect_owned(config_asset);
811
812 assert!(
813 scale.is_finite() && scale > 0.0,
814 "Scale has to be finite and greater than zero"
815 );
816
817 validate_body(&body, config_asset);
818 validate_inventory(inventory, &body, config_asset);
820 validate_name(name, body, config_asset);
821 validate_loot(loot, config_asset);
823 validate_meta(meta, config_asset);
824 validate_pets(pets, config_asset);
825 validate_rider(rider, config_asset);
826 validate_death_effects(death_effects, config_asset);
827 }
828
829 #[test]
830 fn test_all_entity_assets() {
831 let entity_configs =
833 try_all_entity_configs().expect("Failed to access entity configs directory");
834 for config_asset in entity_configs {
835 validate_entity_config(&config_asset)
836 }
837 }
838}