use crate::{ assets::{self, AssetExt, Error}, calendar::Calendar, comp::{ self, agent, humanoid, inventory::loadout_builder::{LoadoutBuilder, LoadoutSpec}, misc::PortalData, Alignment, Body, Item, }, lottery::LootSpec, npc::{self, NPC_NAMES}, resources::TimeOfDay, rtsim, trade::SiteInformation, }; use enum_map::EnumMap; use serde::Deserialize; use vek::*; #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] pub enum NameKind { Name(String), Automatic, Uninit, } #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] pub enum BodyBuilder { RandomWith(String), Exact(Body), Uninit, } #[derive(Debug, Deserialize, Clone)] pub enum AlignmentMark { Alignment(Alignment), Uninit, } impl Default for AlignmentMark { fn default() -> Self { Self::Alignment(Alignment::Wild) } } #[derive(Default, Debug, Deserialize, Clone)] #[serde(default)] pub struct AgentConfig { pub has_agency: Option, pub no_flee: Option, pub idle_wander_factor: Option, pub aggro_range_multiplier: Option, } #[derive(Debug, Deserialize, Clone)] pub enum LoadoutKind { FromBody, Asset(String), Inline(Box), } #[derive(Debug, Deserialize, Clone)] pub struct InventorySpec { loadout: LoadoutKind, #[serde(default)] items: Vec<(u32, String)>, } #[derive(Debug, Deserialize, Clone)] pub enum Meta { SkillSetAsset(String), } // FIXME: currently this is used for both base definition // and extension manifest. // This is why all fields have Uninit kind which is means // that this field should be either Default or Unchanged // depending on how it is used. // // When we will use extension manifests more, it would be nicer to // split EntityBase and EntityExtension to different structs. // // Fields which have Uninit enum kind // should be optional (or renamed to Unchanged) in EntityExtension // and required (or renamed to Default) in EntityBase /// Struct for EntityInfo manifest. /// /// Intended to use with .ron files as base definition or /// in rare cases as extension manifest. /// Pure data struct, doesn't do anything until evaluated with EntityInfo. /// /// Check assets/common/entity/template.ron or other examples. /// /// # Example /// ``` /// use vek::Vec3; /// use veloren_common::generation::EntityInfo; /// /// // create new EntityInfo at dummy position /// // and fill it with template config /// let dummy_position = Vec3::new(0.0, 0.0, 0.0); /// // rng is required because some elements may be randomly generated /// let mut dummy_rng = rand::thread_rng(); /// let entity = EntityInfo::at(dummy_position).with_asset_expect( /// "common.entity.template", /// &mut dummy_rng, /// None, /// ); /// ``` #[derive(Debug, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub struct EntityConfig { /// Name of Entity /// Can be Name(String) with given name /// or Automatic which will call automatic name depend on Body /// or Uninit (means it should be specified somewhere in code) // Hidden, because its behaviour depends on `body` field. name: NameKind, /// Body /// Can be Exact (Body with all fields e.g BodyType, Species, Hair color and /// such) or RandomWith (will generate random body or species) /// or Uninit (means it should be specified somewhere in code) pub body: BodyBuilder, /// Alignment, can be Uninit pub alignment: AlignmentMark, /// Parameterises agent behaviour #[serde(default)] pub agent: AgentConfig, /// Loot /// See LootSpec in lottery pub loot: LootSpec, /// Loadout & Inventory /// Check docs for `InventorySpec` struct in this file. pub inventory: InventorySpec, /// Meta Info for optional fields /// Possible fields: /// SkillSetAsset(String) with asset_specifier for skillset #[serde(default)] pub meta: Vec, } impl assets::Asset for EntityConfig { type Loader = assets::RonLoader; const EXTENSION: &'static str = "ron"; } impl EntityConfig { pub fn from_asset_expect_owned(asset_specifier: &str) -> Self { Self::load_owned(asset_specifier) .unwrap_or_else(|e| panic!("Failed to load {}. Error: {:?}", asset_specifier, e)) } #[must_use] pub fn with_body(mut self, body: BodyBuilder) -> Self { self.body = body; self } } /// Return all entity config specifiers pub fn try_all_entity_configs() -> Result, Error> { let configs = assets::load_dir::("common.entity", true)?; Ok(configs.ids().map(|id| id.to_string()).collect()) } #[derive(Clone)] pub enum SpecialEntity { Waypoint, Teleporter(PortalData), } #[derive(Clone)] pub struct EntityInfo { pub pos: Vec3, pub alignment: Alignment, /// Parameterises agent behaviour pub has_agency: bool, pub agent_mark: Option, pub no_flee: bool, pub idle_wander_factor: f32, pub aggro_range_multiplier: f32, // Stats pub body: Body, pub name: Option, pub scale: f32, // Loot pub loot: LootSpec, // Loadout pub inventory: Vec<(u32, Item)>, pub loadout: LoadoutBuilder, pub make_loadout: Option< fn( LoadoutBuilder, Option<&SiteInformation>, time: Option<&(TimeOfDay, Calendar)>, ) -> LoadoutBuilder, >, // Skills pub skillset_asset: Option, // Not implemented pub pet: Option>, // Economy // we can't use DHashMap, do we want to move that into common? pub trading_information: Option, //Option>, /* price and available amount */ // Edge cases, override everything else pub special_entity: Option, } impl EntityInfo { pub fn at(pos: Vec3) -> Self { Self { pos, alignment: Alignment::Wild, has_agency: true, agent_mark: None, no_flee: false, idle_wander_factor: 1.0, aggro_range_multiplier: 1.0, body: Body::Humanoid(humanoid::Body::random()), name: None, scale: 1.0, loot: LootSpec::Nothing, inventory: Vec::new(), loadout: LoadoutBuilder::empty(), make_loadout: None, skillset_asset: None, pet: None, trading_information: None, special_entity: None, } } /// Helper function for applying config from asset /// with specified Rng for managing loadout. #[must_use] pub fn with_asset_expect( self, asset_specifier: &str, loadout_rng: &mut R, time: Option<&(TimeOfDay, Calendar)>, ) -> Self where R: rand::Rng, { let config = EntityConfig::load_expect_cloned(asset_specifier); self.with_entity_config(config, Some(asset_specifier), loadout_rng, time) } /// Evaluate and apply EntityConfig #[must_use] pub fn with_entity_config( mut self, config: EntityConfig, config_asset: Option<&str>, loadout_rng: &mut R, time: Option<&(TimeOfDay, Calendar)>, ) -> Self where R: rand::Rng, { let EntityConfig { name, body, alignment, agent, inventory, loot, meta, } = config; match body { BodyBuilder::RandomWith(string) => { let npc::NpcBody(_body_kind, mut body_creator) = string.parse::().unwrap_or_else(|err| { panic!("failed to parse body {:?}. Err: {:?}", &string, err) }); let body = body_creator(); self = self.with_body(body); }, BodyBuilder::Exact(body) => { self = self.with_body(body); }, BodyBuilder::Uninit => {}, } // NOTE: set name after body, as it's used with automatic name match name { NameKind::Name(name) => { self = self.with_name(name); }, NameKind::Automatic => { self = self.with_automatic_name(None); }, NameKind::Uninit => {}, } if let AlignmentMark::Alignment(alignment) = alignment { self = self.with_alignment(alignment); } self = self.with_loot_drop(loot); // NOTE: set loadout after body, as it's used with default equipement self = self.with_inventory(inventory, config_asset, loadout_rng, time); // Prefer the new configuration, if possible let AgentConfig { has_agency, no_flee, idle_wander_factor, aggro_range_multiplier, } = agent; self.has_agency = has_agency.unwrap_or(self.has_agency); self.no_flee = no_flee.unwrap_or(self.no_flee); self.idle_wander_factor = idle_wander_factor.unwrap_or(self.idle_wander_factor); self.aggro_range_multiplier = aggro_range_multiplier.unwrap_or(self.aggro_range_multiplier); for field in meta { match field { Meta::SkillSetAsset(asset) => { self = self.with_skillset_asset(asset); }, } } self } /// Return EntityInfo with LoadoutBuilder and items overwritten // NOTE: helper function, think twice before exposing it #[must_use] fn with_inventory( mut self, inventory: InventorySpec, config_asset: Option<&str>, rng: &mut R, time: Option<&(TimeOfDay, Calendar)>, ) -> Self where R: rand::Rng, { let config_asset = config_asset.unwrap_or("???"); let InventorySpec { loadout, items } = inventory; // FIXME: this shouldn't always overwrite // inventory. Think about this when we get to // entity config inheritance. self.inventory = items .into_iter() .map(|(num, i)| (num, Item::new_from_asset_expect(&i))) .collect(); match loadout { LoadoutKind::FromBody => { self = self.with_default_equip(); }, LoadoutKind::Asset(loadout) => { let loadout = LoadoutBuilder::from_asset(&loadout, rng, time).unwrap_or_else(|e| { panic!("failed to load loadout for {config_asset}: {e:?}"); }); self.loadout = loadout; }, LoadoutKind::Inline(loadout_spec) => { let loadout = LoadoutBuilder::from_loadout_spec(*loadout_spec, rng, time) .unwrap_or_else(|e| { panic!("failed to load loadout for {config_asset}: {e:?}"); }); self.loadout = loadout; }, } self } /// Return EntityInfo with LoadoutBuilder overwritten // NOTE: helper function, think twice before exposing it #[must_use] fn with_default_equip(mut self) -> Self { let loadout_builder = LoadoutBuilder::from_default(&self.body); self.loadout = loadout_builder; self } #[must_use] pub fn do_if(mut self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self { if cond { self = f(self); } self } #[must_use] pub fn into_special(mut self, special: SpecialEntity) -> Self { self.special_entity = Some(special); self } #[must_use] pub fn with_alignment(mut self, alignment: Alignment) -> Self { self.alignment = alignment; self } #[must_use] pub fn with_body(mut self, body: Body) -> Self { self.body = body; self } #[must_use] pub fn with_name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } #[must_use] pub fn with_agency(mut self, agency: bool) -> Self { self.has_agency = agency; self } #[must_use] pub fn with_agent_mark(mut self, agent_mark: impl Into>) -> Self { self.agent_mark = agent_mark.into(); self } #[must_use] pub fn with_loot_drop(mut self, loot_drop: LootSpec) -> Self { self.loot = loot_drop; self } #[must_use] pub fn with_scale(mut self, scale: f32) -> Self { self.scale = scale; self } #[must_use] pub fn with_lazy_loadout( mut self, creator: fn( LoadoutBuilder, Option<&SiteInformation>, time: Option<&(TimeOfDay, Calendar)>, ) -> LoadoutBuilder, ) -> Self { self.make_loadout = Some(creator); self } #[must_use] pub fn with_skillset_asset(mut self, asset: String) -> Self { self.skillset_asset = Some(asset); self } #[must_use] pub fn with_automatic_name(mut self, alias: Option) -> Self { let npc_names = NPC_NAMES.read(); let name = match &self.body { Body::Humanoid(body) => Some(get_npc_name(&npc_names.humanoid, body.species)), Body::QuadrupedMedium(body) => { Some(get_npc_name(&npc_names.quadruped_medium, body.species)) }, Body::BirdMedium(body) => Some(get_npc_name(&npc_names.bird_medium, body.species)), Body::BirdLarge(body) => Some(get_npc_name(&npc_names.bird_large, body.species)), Body::FishSmall(body) => Some(get_npc_name(&npc_names.fish_small, body.species)), Body::FishMedium(body) => Some(get_npc_name(&npc_names.fish_medium, body.species)), Body::Theropod(body) => Some(get_npc_name(&npc_names.theropod, body.species)), Body::QuadrupedSmall(body) => { Some(get_npc_name(&npc_names.quadruped_small, body.species)) }, Body::Dragon(body) => Some(get_npc_name(&npc_names.dragon, body.species)), Body::QuadrupedLow(body) => Some(get_npc_name(&npc_names.quadruped_low, body.species)), Body::Golem(body) => Some(get_npc_name(&npc_names.golem, body.species)), Body::BipedLarge(body) => Some(get_npc_name(&npc_names.biped_large, body.species)), Body::Arthropod(body) => Some(get_npc_name(&npc_names.arthropod, body.species)), Body::Crustacean(body) => Some(get_npc_name(&npc_names.crustacean, body.species)), _ => None, }; self.name = name.map(|name| { if let Some(alias) = alias { format!("{alias} ({name})") } else { name.to_string() } }); self } #[must_use] pub fn with_alias(mut self, alias: String) -> Self { self.name = Some(if let Some(name) = self.name { format!("{alias} ({name})") } else { alias }); self } /// map contains price+amount #[must_use] pub fn with_economy<'a>(mut self, e: impl Into>) -> Self { self.trading_information = e.into().cloned(); self } #[must_use] pub fn with_no_flee(mut self) -> Self { self.no_flee = true; self } #[must_use] pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self { self.loadout = loadout; self } } #[derive(Default)] pub struct ChunkSupplement { pub entities: Vec, pub rtsim_max_resources: EnumMap, } impl ChunkSupplement { pub fn add_entity(&mut self, entity: EntityInfo) { self.entities.push(entity); } } pub fn get_npc_name< 'a, Species, SpeciesData: for<'b> core::ops::Index<&'b Species, Output = npc::SpeciesNames>, >( body_data: &'a comp::BodyData, species: Species, ) -> &'a str { &body_data.species[&species].generic } #[cfg(test)] mod tests { use super::*; use crate::SkillSetBuilder; use hashbrown::HashMap; #[derive(Debug, Eq, Hash, PartialEq)] enum MetaId { SkillSetAsset, } impl Meta { fn id(&self) -> MetaId { match self { Meta::SkillSetAsset(_) => MetaId::SkillSetAsset, } } } #[cfg(test)] fn validate_body(body: &BodyBuilder, config_asset: &str) { match body { BodyBuilder::RandomWith(string) => { let npc::NpcBody(_body_kind, mut body_creator) = string.parse::().unwrap_or_else(|err| { panic!( "failed to parse body {:?} in {}. Err: {:?}", &string, config_asset, err ) }); let _ = body_creator(); }, BodyBuilder::Uninit | BodyBuilder::Exact { .. } => {}, } } #[cfg(test)] fn validate_inventory(inventory: InventorySpec, body: &BodyBuilder, config_asset: &str) { let InventorySpec { loadout, items } = inventory; match loadout { LoadoutKind::FromBody => { if body.clone() == BodyBuilder::Uninit { // there is a big chance to call automatic name // when body is yet undefined panic!("Used FromBody loadout with Uninit body in {}", config_asset); } }, LoadoutKind::Asset(asset) => { let loadout = LoadoutSpec::load_cloned(&asset).expect("failed to load loadout asset"); loadout .validate(vec![asset]) .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}")); }, LoadoutKind::Inline(spec) => { spec.validate(Vec::new()) .unwrap_or_else(|e| panic!("Config {config_asset} is broken: {e:?}")); }, } // TODO: check for number of items // // 1) just with 16 default slots? // - well, keep in mind that not every item can stack to infinite amount // // 2) discover total capacity from loadout? for (num, item_str) in items { let item = Item::new_from_asset(&item_str); let mut item = item.unwrap_or_else(|err| { panic!("can't load {} in {}: {:?}", item_str, config_asset, err); }); item.set_amount(num).unwrap_or_else(|err| { panic!( "can't set amount {} for {} in {}: {:?}", num, item_str, config_asset, err ); }); } } #[cfg(test)] fn validate_name(name: NameKind, body: BodyBuilder, config_asset: &str) { if name == NameKind::Automatic && body == BodyBuilder::Uninit { // there is a big chance to call automatic name // when body is yet undefined // // use .with_automatic_name() in code explicitly panic!("Used Automatic name with Uninit body in {}", config_asset); } } #[cfg(test)] fn validate_loot(loot: LootSpec, _config_asset: &str) { use crate::lottery; lottery::tests::validate_loot_spec(&loot); } #[cfg(test)] fn validate_meta(meta: Vec, config_asset: &str) { let mut meta_counter = HashMap::new(); for field in meta { meta_counter .entry(field.id()) .and_modify(|c| *c += 1) .or_insert(1); match field { Meta::SkillSetAsset(asset) => { drop(SkillSetBuilder::from_asset_expect(&asset)); }, } } for (meta_id, counter) in meta_counter { if counter > 1 { panic!("Duplicate {:?} in {}", meta_id, config_asset); } } } #[test] fn test_all_entity_assets() { // Get list of entity configs, load everything, validate content. let entity_configs = try_all_entity_configs().expect("Failed to access entity configs directory"); for config_asset in entity_configs { let EntityConfig { body, agent: _, inventory, name, loot, meta, alignment: _, // can't fail if serialized, it's a boring enum } = EntityConfig::from_asset_expect_owned(&config_asset); validate_body(&body, &config_asset); // body dependent stuff validate_inventory(inventory, &body, &config_asset); validate_name(name, body, &config_asset); // misc validate_loot(loot, &config_asset); validate_meta(meta, &config_asset); } } }