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
196#[derive(Clone, Debug)]
197pub enum SpecialEntity {
198 Waypoint,
199 Teleporter(PortalData),
200 ArenaTotem {
202 range: f32,
203 },
204}
205
206#[derive(Clone)]
207pub struct EntityInfo {
208 pub pos: Vec3<f32>,
209 pub alignment: Alignment,
210 pub has_agency: bool,
212 pub agent_mark: Option<agent::Mark>,
213 pub no_flee: bool,
214 pub idle_wander_factor: f32,
215 pub aggro_range_multiplier: f32,
216 pub body: Body,
218 pub name: Option<Content>,
219 pub scale: f32,
220 pub loot: LootSpec<String>,
222 pub inventory: Vec<(u32, Item)>,
224 pub loadout: LoadoutBuilder,
225 pub make_loadout: Option<
226 fn(
227 LoadoutBuilder,
228 Option<&SiteInformation>,
229 time: Option<&(TimeOfDay, Calendar)>,
230 ) -> LoadoutBuilder,
231 >,
232 pub skillset_asset: Option<String>,
234 pub death_effects: Option<DeathEffects>,
235 pub rider_effects: Option<RiderEffects>,
236
237 pub pets: Vec<EntityInfo>,
238
239 pub rider: Option<Box<EntityInfo>>,
240
241 pub trading_information: Option<SiteInformation>,
244 pub special_entity: Option<SpecialEntity>,
248}
249
250impl EntityInfo {
251 pub fn at(pos: Vec3<f32>) -> Self {
252 Self {
253 pos,
254 alignment: Alignment::Wild,
255
256 has_agency: true,
257 agent_mark: None,
258 no_flee: false,
259 idle_wander_factor: 1.0,
260 aggro_range_multiplier: 1.0,
261
262 body: Body::Humanoid(humanoid::Body::random()),
263 name: None,
264 scale: 1.0,
265 loot: LootSpec::Nothing,
266 inventory: Vec::new(),
267 loadout: LoadoutBuilder::empty(),
268 make_loadout: None,
269 death_effects: None,
270 rider_effects: None,
271 skillset_asset: None,
272 pets: Vec::new(),
273 rider: None,
274 trading_information: None,
275 special_entity: None,
276 }
277 }
278
279 #[must_use]
282 pub fn with_asset_expect<R>(
283 self,
284 asset_specifier: &str,
285 loadout_rng: &mut R,
286 time: Option<&(TimeOfDay, Calendar)>,
287 ) -> Self
288 where
289 R: rand::Rng,
290 {
291 let config: EntityConfig = Ron::load_expect_cloned(asset_specifier).into_inner();
292
293 self.with_entity_config(config, Some(asset_specifier), loadout_rng, time)
294 }
295
296 #[must_use]
298 pub fn with_entity_config<R>(
299 mut self,
300 config: EntityConfig,
301 config_asset: Option<&str>,
302 loadout_rng: &mut R,
303 time: Option<&(TimeOfDay, Calendar)>,
304 ) -> Self
305 where
306 R: rand::Rng,
307 {
308 let EntityConfig {
309 name,
310 body,
311 alignment,
312 agent,
313 inventory,
314 loot,
315 meta,
316 scale,
317 pets,
318 rider,
319 death_effects,
320 rider_effects,
321 } = config;
322
323 match body {
324 BodyBuilder::RandomWith(string) => {
325 let npc::NpcBody(_body_kind, mut body_creator) =
326 string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
327 panic!("failed to parse body {:?}. Err: {:?}", &string, err)
328 });
329 let body = body_creator();
330 self = self.with_body(body);
331 },
332 BodyBuilder::Exact(body) => {
333 self = self.with_body(body);
334 },
335 BodyBuilder::Uninit => {},
336 }
337
338 match name {
341 NameKind::Translate(key) => {
342 let name = Content::with_attr(key, self.body.gender_attr());
343 self = self.with_name(name);
344 },
345 NameKind::Automatic => {
346 self = self.with_automatic_name();
347 },
348 NameKind::Uninit => {},
349 }
350
351 if let AlignmentMark::Alignment(alignment) = alignment {
352 self = self.with_alignment(alignment);
353 }
354
355 self = self.with_loot_drop(loot);
356
357 self = self.with_inventory(inventory, config_asset, loadout_rng, time);
359
360 let mut pet_infos: Vec<EntityInfo> = Vec::new();
361 for (pet_asset, amount) in pets {
362 let config = Ron::<EntityConfig>::load_expect(&pet_asset).read();
363 let (start, mut end) = amount.into_inner();
364 if start > end {
365 error!("Invalid range for pet count start: {start}, end: {end}");
366 end = start;
367 }
368
369 pet_infos.extend((0..loadout_rng.random_range(start..=end)).map(|_| {
370 EntityInfo::at(self.pos).with_entity_config(
371 config.clone().into_inner(),
372 config_asset,
373 loadout_rng,
374 time,
375 )
376 }));
377 }
378 self.scale = scale;
379
380 self.pets = pet_infos;
381
382 self.rider = rider.map(|rider| {
383 let config = Ron::<EntityConfig>::load_expect(&rider).read();
384 Box::new(EntityInfo::at(self.pos).with_entity_config(
385 config.clone().into_inner(),
386 config_asset,
387 loadout_rng,
388 time,
389 ))
390 });
391
392 let AgentConfig {
394 has_agency,
395 no_flee,
396 idle_wander_factor,
397 aggro_range_multiplier,
398 } = agent;
399 self.has_agency = has_agency.unwrap_or(self.has_agency);
400 self.no_flee = no_flee.unwrap_or(self.no_flee);
401 self.idle_wander_factor = idle_wander_factor.unwrap_or(self.idle_wander_factor);
402 self.aggro_range_multiplier = aggro_range_multiplier.unwrap_or(self.aggro_range_multiplier);
403 self.death_effects = (!death_effects.is_empty()).then_some(DeathEffects(death_effects));
404 self.rider_effects = (!rider_effects.is_empty()).then_some(RiderEffects(rider_effects));
405
406 for field in meta {
407 match field {
408 Meta::SkillSetAsset(asset) => {
409 self = self.with_skillset_asset(asset);
410 },
411 }
412 }
413
414 self
415 }
416
417 #[must_use]
420 fn with_inventory<R>(
421 mut self,
422 inventory: InventorySpec,
423 config_asset: Option<&str>,
424 rng: &mut R,
425 time: Option<&(TimeOfDay, Calendar)>,
426 ) -> Self
427 where
428 R: rand::Rng,
429 {
430 let config_asset = config_asset.unwrap_or("???");
431 let InventorySpec { loadout, items } = inventory;
432
433 self.inventory = items
437 .into_iter()
438 .map(|(num, i)| (num, Item::new_from_asset_expect(&i)))
439 .collect();
440
441 match loadout {
442 LoadoutKind::FromBody => {
443 self = self.with_default_equip();
444 },
445 LoadoutKind::Asset(loadout) => {
446 let loadout = LoadoutBuilder::from_asset(&loadout, rng, time).unwrap_or_else(|e| {
447 panic!("failed to load loadout for {config_asset}: {e:?}");
448 });
449 self.loadout = loadout;
450 },
451 LoadoutKind::Inline(loadout_spec) => {
452 let loadout = LoadoutBuilder::from_loadout_spec(*loadout_spec, rng, time)
453 .unwrap_or_else(|e| {
454 panic!("failed to load loadout for {config_asset}: {e:?}");
455 });
456 self.loadout = loadout;
457 },
458 }
459
460 self
461 }
462
463 #[must_use]
466 fn with_default_equip(mut self) -> Self {
467 let loadout_builder = LoadoutBuilder::from_default(&self.body);
468 self.loadout = loadout_builder;
469
470 self
471 }
472
473 #[must_use]
474 pub fn do_if(mut self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
475 if cond {
476 self = f(self);
477 }
478 self
479 }
480
481 #[must_use]
482 pub fn into_special(mut self, special: SpecialEntity) -> Self {
483 self.special_entity = Some(special);
484 self
485 }
486
487 #[must_use]
488 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
489 self.alignment = alignment;
490 self
491 }
492
493 #[must_use]
494 pub fn with_body(mut self, body: Body) -> Self {
495 self.body = body;
496 self
497 }
498
499 #[must_use]
500 pub fn with_name(mut self, name: Content) -> Self {
501 self.name = Some(name);
502 self
503 }
504
505 #[must_use]
506 pub fn with_agency(mut self, agency: bool) -> Self {
507 self.has_agency = agency;
508 self
509 }
510
511 #[must_use]
512 pub fn with_agent_mark(mut self, agent_mark: impl Into<Option<agent::Mark>>) -> Self {
513 self.agent_mark = agent_mark.into();
514 self
515 }
516
517 #[must_use]
518 pub fn with_loot_drop(mut self, loot_drop: LootSpec<String>) -> Self {
519 self.loot = loot_drop;
520 self
521 }
522
523 #[must_use]
524 pub fn with_scale(mut self, scale: f32) -> Self {
525 self.scale = scale;
526 self
527 }
528
529 #[must_use]
530 pub fn with_lazy_loadout(
531 mut self,
532 creator: fn(
533 LoadoutBuilder,
534 Option<&SiteInformation>,
535 time: Option<&(TimeOfDay, Calendar)>,
536 ) -> LoadoutBuilder,
537 ) -> Self {
538 self.make_loadout = Some(creator);
539 self
540 }
541
542 #[must_use]
543 pub fn with_skillset_asset(mut self, asset: String) -> Self {
544 self.skillset_asset = Some(asset);
545 self
546 }
547
548 #[must_use]
549 pub fn with_automatic_name(mut self) -> Self {
550 let npc_names = NPC_NAMES.read();
551 self.name = npc_names.get_default_name(&self.body);
552 self
553 }
554
555 #[must_use]
556 pub fn with_alias(mut self, alias: impl Into<Option<String>>) -> Self {
557 if let Some(alias) = alias.into() {
558 self.name = Some(Content::localized_with_args(
559 "name-misc-with-alias-template",
560 [
561 ("alias", Content::Plain(alias)),
562 (
563 "old_name",
564 self.name.unwrap_or_else(|| {
565 dev_panic!("no name present to use with with_alias");
566 Content::Plain("??".to_owned())
567 }),
568 ),
569 ],
570 ));
571 }
572 self
573 }
574
575 #[must_use]
577 pub fn with_economy<'a>(mut self, e: impl Into<Option<&'a SiteInformation>>) -> Self {
578 self.trading_information = e.into().cloned();
579 self
580 }
581
582 #[must_use]
583 pub fn with_no_flee(mut self) -> Self {
584 self.no_flee = true;
585 self
586 }
587
588 #[must_use]
589 pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self {
590 self.loadout = loadout;
591 self
592 }
593}
594
595#[derive(Default)]
596pub struct ChunkSupplement {
597 pub entities: Vec<EntityInfo>,
598 pub rtsim_max_resources: EnumMap<rtsim::TerrainResource, usize>,
599}
600
601impl ChunkSupplement {
602 pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); }
603}
604
605#[cfg(test)]
606pub mod tests {
607 use super::*;
608 use crate::SkillSetBuilder;
609 use hashbrown::HashMap;
610
611 #[derive(Debug, Eq, Hash, PartialEq)]
612 enum MetaId {
613 SkillSetAsset,
614 }
615
616 impl Meta {
617 fn id(&self) -> MetaId {
618 match self {
619 Meta::SkillSetAsset(_) => MetaId::SkillSetAsset,
620 }
621 }
622 }
623
624 #[cfg(test)]
625 fn validate_body(body: &BodyBuilder, config_asset: &str) {
626 match body {
627 BodyBuilder::RandomWith(string) => {
628 let npc::NpcBody(_body_kind, mut body_creator) =
629 string.parse::<npc::NpcBody>().unwrap_or_else(|err| {
630 panic!(
631 "failed to parse body {:?} in {}. Err: {:?}",
632 &string, config_asset, err
633 )
634 });
635 let _ = body_creator();
636 },
637 BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {},
638 }
639 }
640
641 #[cfg(test)]
642 fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) {
643 let InventorySpec { loadout, items } = inventory;
644
645 match loadout {
646 LoadoutKind::FromBody => {
647 if body.clone() == BodyBuilder::Uninit {
648 panic!("Used FromBody loadout with Uninit body in {}", config_asset);
651 }
652 },
653 LoadoutKind::Asset(asset) => {
654 let loadout: LoadoutSpec = Ron::load_cloned(&asset)
655 .expect("failed to load loadout asset")
656 .into_inner();
657 loadout
658 .validate(vec![asset])
659 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
660 },
661 LoadoutKind::Inline(spec) => {
662 spec.validate(Vec::new())
663 .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}"));
664 },
665 }
666
667 for (num, item_str) in items {
674 let item = Item::new_from_asset(&item_str);
675 let mut item = item.unwrap_or_else(|err| {
676 panic!("can't load {} in {}: {:?}", item_str, config_asset, err);
677 });
678 item.set_amount(num).unwrap_or_else(|err| {
679 panic!(
680 "can't set amount {} for {} in {}: {:?}",
681 num, item_str, config_asset, err
682 );
683 });
684 }
685 }
686
687 #[cfg(test)]
688 fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) {
689 if (name == NameKind::Automatic || matches!(name, NameKind::Translate(_)))
690 && body == BodyBuilder::Uninit
691 {
692 panic!(
697 "Used Automatic/Translate name with Uninit body in {}",
698 config_asset
699 );
700 }
701 }
702
703 #[cfg(test)]
704 fn validate_loot(loot: LootSpec<String>, _config_asset: &str) {
705 use crate::lottery;
706 lottery::tests::validate_loot_spec(&loot);
707 }
708
709 #[cfg(test)]
710 fn validate_meta(meta: Vec<Meta>, config_asset: &str) {
711 let mut meta_counter = HashMap::new();
712 for field in meta {
713 meta_counter
714 .entry(field.id())
715 .and_modify(|c| *c += 1)
716 .or_insert(1);
717
718 match field {
719 Meta::SkillSetAsset(asset) => {
720 drop(SkillSetBuilder::from_asset_expect(&asset));
721 },
722 }
723 }
724 for (meta_id, counter) in meta_counter {
725 if counter > 1 {
726 panic!("Duplicate {:?} in {}", meta_id, config_asset);
727 }
728 }
729 }
730
731 #[cfg(test)]
732 fn validate_pets(pets: Vec<(String, RangeInclusive<usize>)>, config_asset: &str) {
733 for (pet, amount) in pets.into_iter().map(|(pet_asset, amount)| {
734 (
735 Ron::<EntityConfig>::load_cloned(&pet_asset)
736 .unwrap_or_else(|_| {
737 panic!("Pet asset path invalid: \"{pet_asset}\", in {config_asset}")
738 })
739 .into_inner(),
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) = Ron::<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 Ron::<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}