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