diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad39e4695..2f495ea27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed Perforate icon not displaying - Make cave entrances easier to follow - Renamed Twiggy Shoulders to match the Twig Armor set +- No longer stack buffs of the same kind with equal attributes, this could lead to a DoS if ie. an entity stayed long enough in lava. ## [0.15.0] - 2023-07-01 diff --git a/Cargo.lock b/Cargo.lock index 07cfe7aa8c..86350ff101 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7088,6 +7088,7 @@ dependencies = [ "tracing", "vek 0.15.8", "veloren-common", + "veloren-common-assets", "veloren-common-base", "veloren-common-ecs", "veloren-common-net", diff --git a/README.md b/README.md index 5c95e65561..647f6ab8b2 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ Most Veloren servers require you to register with the official authentication se - [Website](https://veloren.net) - [Discord](https://discord.gg/veloren-community-449602562165833758) - [Matrix](https://matrix.to/#/#veloren-space:fachschaften.org) -- [Twitter](https://twitter.com/velorenproject) - [Mastodon](https://floss.social/@veloren) - [Reddit](https://www.reddit.com/r/Veloren) - [YouTube](https://youtube.com/@Veloren) diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl index cbc6007b08..4b76ba0b6a 100644 --- a/assets/voxygen/i18n/en/command.ftl +++ b/assets/voxygen/i18n/en/command.ftl @@ -64,6 +64,8 @@ command-battlemode-available-modes = Available modes: pvp, pve command-battlemode-same = Attempted to set the same battlemode command-battlemode-updated = New battlemode: { $battlemode } command-buff-unknown = Unknown buff: { $buff } +command-buff-data = Buff argument '{ $buff }' requires additional data +command-buff-body-unknown = Unknown body spec: { $spec } command-skillpreset-load-error = Error while loading presets command-skillpreset-broken = Skill preset is broken command-skillpreset-missing = Preset does not exist: { $preset } @@ -95,4 +97,4 @@ command-you-dont-exist = You do not exist, so you cannot use this command command-destroyed-tethers = All tethers destroyed! You are now free command-destroyed-no-tethers = You're not connected to any tethers command-dismounted = Dismounted -command-no-dismount = You're not riding or being ridden \ No newline at end of file +command-no-dismount = You're not riding or being ridden diff --git a/common/assets/src/lib.rs b/common/assets/src/lib.rs index 952c196e9d..f6368a20c9 100644 --- a/common/assets/src/lib.rs +++ b/common/assets/src/lib.rs @@ -31,14 +31,14 @@ pub use walk::{walk_tree, Walk}; #[cfg(feature = "plugins")] lazy_static! { -/// The HashMap where all loaded assets are stored in. -static ref ASSETS: plugin_cache::CombinedCache = plugin_cache::CombinedCache::new().unwrap(); + /// The HashMap where all loaded assets are stored in. + static ref ASSETS: plugin_cache::CombinedCache = plugin_cache::CombinedCache::new().unwrap(); } #[cfg(not(feature = "plugins"))] lazy_static! { -/// The HashMap where all loaded assets are stored in. -static ref ASSETS: AssetCache = - AssetCache::with_source(fs::FileSystem::new().unwrap()); + /// The HashMap where all loaded assets are stored in. + static ref ASSETS: AssetCache = + AssetCache::with_source(fs::FileSystem::new().unwrap()); } #[cfg(feature = "hot-reloading")] diff --git a/common/src/cmd.rs b/common/src/cmd.rs index a41a9c93b7..b26059550e 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -201,9 +201,10 @@ lazy_static! { }; static ref BUFFS: Vec = { - let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect(); - // Add all as valid command - buff_pack.push("all".to_string()); + let mut buff_pack: Vec = BUFF_PARSER.keys().cloned().collect(); + + // Add `all` as valid command + buff_pack.push("all".to_owned()); buff_pack }; @@ -429,6 +430,7 @@ impl ServerChatCommand { Enum("buff", BUFFS.clone(), Required), Float("strength", 0.01, Optional), Float("duration", 10.0, Optional), + Any("buff data spec", Optional), ], "Cast a buff on player", Some(Admin), diff --git a/common/src/combat.rs b/common/src/combat.rs index 1a13ba314d..1d7f881eb4 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -791,7 +791,7 @@ impl AttackDamage { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct AttackEffect { target: Option, effect: CombatEffect, @@ -890,7 +890,7 @@ impl CombatEffect { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum CombatRequirement { AnyDamage, Energy(f32), @@ -898,7 +898,7 @@ pub enum CombatRequirement { TargetHasBuff(BuffKind), } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum DamagedEffect { Combo(i32), } diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs index cef10d70c3..e4c883188e 100644 --- a/common/src/comp/buff.rs +++ b/common/src/comp/buff.rs @@ -165,19 +165,38 @@ pub enum BuffKind { /// Results from drinking a potion. /// Decreases the health gained from subsequent potions. PotionSickness, - /// Changed into another body. - Polymorphed, /// Slows movement speed and reduces energy reward. /// Both scales non-linearly to strength, 0.5 lead to movespeed reduction /// by 25% and energy reward reduced by 150%, 1.0 lead to MS reduction by /// 33.3% and energy reward reduced by 200%. Energy reward can't be /// reduced by more than 200%, to a minimum value of -100%. Heatstroke, + // Complex, non-obvious buffs + /// Changed into another body. + Polymorphed, +} + +/// Tells a little more about the buff kind than simple buff/debuff +pub enum BuffDescriptor { + /// Simple positive buffs, like `BuffKind::Saturation` + SimplePositive, + /// Simple negative buffs, like `BuffKind::Bleeding` + SimpleNegative, + /// Buffs that require unusual data that can't be governed just by strength + /// and duration, like `BuffKind::Polymorhped` + Complex, + // For future additions, we may want to tell about non-obvious buffs, + // like Agility. + // Also maybe extend Complex to differentiate between Positive, Negative + // and Neutral buffs? + // For now, Complex is assumed to be neutral/non-obvious. } impl BuffKind { - /// Checks if buff is buff or debuff. - pub fn is_buff(self) -> bool { + /// Tells a little more about buff kind than simple buff/debuff + /// + /// Read more in [BuffDescriptor]. + pub fn differentiate(self) -> BuffDescriptor { match self { BuffKind::Regeneration | BuffKind::Saturation @@ -202,7 +221,7 @@ impl BuffKind { | BuffKind::Sunderer | BuffKind::Defiance | BuffKind::Bloodfeast - | BuffKind::Berserk => true, + | BuffKind::Berserk => BuffDescriptor::SimplePositive, BuffKind::Bleeding | BuffKind::Cursed | BuffKind::Burning @@ -213,8 +232,23 @@ impl BuffKind { | BuffKind::Poisoned | BuffKind::Parried | BuffKind::PotionSickness - | BuffKind::Polymorphed - | BuffKind::Heatstroke => false, + | BuffKind::Heatstroke => BuffDescriptor::SimpleNegative, + BuffKind::Polymorphed => BuffDescriptor::Complex, + } + } + + /// Checks if buff is buff or debuff. + pub fn is_buff(self) -> bool { + match self.differentiate() { + BuffDescriptor::SimplePositive => true, + BuffDescriptor::SimpleNegative | BuffDescriptor::Complex => false, + } + } + + pub fn is_simple(self) -> bool { + match self.differentiate() { + BuffDescriptor::SimplePositive | BuffDescriptor::SimpleNegative => true, + BuffDescriptor::Complex => false, } } @@ -460,9 +494,6 @@ pub struct BuffData { pub strength: f32, pub duration: Option, pub delay: Option, - /// Force the buff effects to be applied each tick, ignoring num_ticks - #[serde(default)] - pub force_immediate: bool, /// Used for buffs that have rider buffs (e.g. Flame, Frigid) pub secondary_duration: Option, /// Used to add random data to buffs if needed (e.g. polymorphed) @@ -479,7 +510,6 @@ impl BuffData { Self { strength, duration, - force_immediate: false, delay: None, secondary_duration: None, misc_data: None, @@ -496,12 +526,6 @@ impl BuffData { self } - /// Force the buff effects to be applied each tick, ignoring num_ticks - pub fn with_force_immediate(mut self, force_immediate: bool) -> Self { - self.force_immediate = force_immediate; - self - } - pub fn with_misc_data(mut self, misc_data: MiscBuffData) -> Self { self.misc_data = Some(misc_data); self @@ -524,14 +548,14 @@ pub enum BuffCategory { SelfBuff, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum ModifierKind { Additive, Multiplicative, } /// Data indicating and configuring behaviour of a de/buff. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BuffEffect { /// Periodically damages or heals entity HealthChangeOverTime { @@ -782,11 +806,44 @@ impl Buffs { pub fn insert(&mut self, buff: Buff, current_time: Time) -> BuffKey { let kind = buff.kind; - let key = self.buffs.insert(buff); - self.kinds[kind] - .get_or_insert_with(|| (Vec::new(), current_time)) - .0 - .push(key); + // Try to find another overlaping non-queueable buff with same data, cat_ids and + // source. + let other_key = if kind.queues() { + None + } else { + self.kinds[kind].as_ref().and_then(|(keys, _)| { + keys.iter() + .find(|key| { + self.buffs.get(**key).map_or(false, |other_buff| { + other_buff.data == buff.data + && other_buff.cat_ids == buff.cat_ids + && other_buff.source == buff.source + && other_buff + .end_time + .map_or(true, |end_time| end_time.0 >= buff.start_time.0) + }) + }) + .copied() + }) + }; + + // If another buff with the same fields is found, update end_time and effects + let key = if let Some((other_buff, key)) = + other_key.and_then(|key| Some((self.buffs.get_mut(key)?, key))) + { + other_buff.end_time = buff.end_time; + other_buff.effects = buff.effects; + key + // Otherwise, insert a new buff + } else { + let key = self.buffs.insert(buff); + self.kinds[kind] + .get_or_insert_with(|| (Vec::new(), current_time)) + .0 + .push(key); + key + }; + self.sort_kind(kind); if kind.queues() { self.delay_queueable_buffs(kind, current_time); diff --git a/common/state/Cargo.toml b/common/state/Cargo.toml index 2e71eb301d..a94ba64808 100644 --- a/common/state/Cargo.toml +++ b/common/state/Cargo.toml @@ -6,7 +6,7 @@ version = "0.10.0" [features] simd = ["vek/platform_intrinsics"] -plugins = ["toml", "tar", "wasmer", "wasmer-wasix-types", "bincode", "plugin-api", "serde"] +plugins = ["common-assets/plugins", "toml", "tar", "wasmer", "wasmer-wasix-types", "bincode", "plugin-api", "serde"] default = ["simd"] @@ -15,6 +15,7 @@ common = { package = "veloren-common", path = ".." } common-net = { package = "veloren-common-net", path = "../net" } common-ecs = { package = "veloren-common-ecs", path = "../ecs" } common-base = { package = "veloren-common-base", path = "../base" } +common-assets = { package = "veloren-common-assets", path = "../assets", optional = true} rayon = { workspace = true } num_cpus = "1.0" diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index f7d48fcb18..c4f9ea769a 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -161,7 +161,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(1.0, Some(Secs(6.0))).with_force_immediate(true), + BuffData::new(1.0, Some(Secs(6.0))), Vec::new(), BuffSource::World, *read_data.time, @@ -179,7 +179,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(5.0, Some(Secs(3.0))).with_force_immediate(true), + BuffData::new(5.0, Some(Secs(3.0))), Vec::new(), BuffSource::World, *read_data.time, @@ -215,7 +215,7 @@ impl<'a> System<'a> for Sys { entity, buff_change: BuffChange::Add(Buff::new( BuffKind::Bleeding, - BuffData::new(15.0, Some(Secs(0.1))).with_force_immediate(true), + BuffData::new(15.0, Some(Secs(0.1))), Vec::new(), BuffSource::World, *read_data.time, @@ -420,7 +420,6 @@ impl<'a> System<'a> for Sys { execute_effect( effect, buff.kind, - &buff.data, buff.start_time, kind_start_time, &read_data, @@ -478,7 +477,6 @@ impl<'a> System<'a> for Sys { fn execute_effect( effect: &BuffEffect, buff_kind: BuffKind, - buff_data: &BuffData, buff_start_time: Time, buff_kind_start_time: Time, read_data: &ReadData, @@ -516,9 +514,7 @@ fn execute_effect( let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor(); let whole_ticks = curr_tick - prev_tick; - if buff_data.force_immediate { - Some((1.0 / tick_dur.0 * dt) as f32) - } else if buff_will_expire { + if buff_will_expire { // If the buff is ending, include the fraction of progress towards the next // tick. let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0; diff --git a/server/src/cmd.rs b/server/src/cmd.rs index d7c7ae24c2..a478ca4108 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -26,7 +26,7 @@ use common::{ }, comp::{ self, - buff::{Buff, BuffData, BuffKind, BuffSource}, + buff::{Buff, BuffData, BuffKind, BuffSource, MiscBuffData}, inventory::{ item::{tool::AbilityMap, MaterialStatManifest, Quality}, slot::Slot, @@ -4140,49 +4140,128 @@ fn handle_buff( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) { - let strength = strength.unwrap_or(0.01); - let duration = duration.unwrap_or(1.0); + let (Some(buff), strength, duration, misc_data_spec) = + parse_cmd_args!(args, String, f32, f64, String) + else { + return Err(Content::Plain(action.help_string())); + }; + + let strength = strength.unwrap_or(0.01); + + if buff == "all" { + let duration = duration.unwrap_or(5.0); let buffdata = BuffData::new(strength, Some(Secs(duration))); - if buff != "all" { - cast_buff(&buff, buffdata, server, target) - } else { - for kind in BUFF_PACK.iter() { - cast_buff(kind, buffdata, server, target)?; - } - Ok(()) - } + + // apply every(*) non-complex buff + // + // (*) BUFF_PACK contains all buffs except + // invulnerability + BUFF_PACK + .iter() + .filter_map(|kind_key| parse_buffkind(kind_key)) + .filter(|buffkind| buffkind.is_simple()) + .for_each(|buffkind| cast_buff(buffkind, buffdata, server, target)); + Ok(()) } else { - Err(Content::Plain(action.help_string())) + let buffkind = parse_buffkind(&buff).ok_or_else(|| { + Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())]) + })?; + + if buffkind.is_simple() { + let duration = duration.unwrap_or(10.0); + let buffdata = BuffData::new(strength, Some(Secs(duration))); + cast_buff(buffkind, buffdata, server, target); + Ok(()) + } else { + // default duration is longer for complex buffs + let duration = duration.unwrap_or(20.0); + let spec = misc_data_spec.ok_or_else(|| { + Content::localized_with_args("command-buff-data", [("buff", buff.clone())]) + })?; + cast_buff_complex(buffkind, server, target, spec, strength, duration) + } } } -fn cast_buff(kind: &str, data: BuffData, server: &mut Server, target: EcsEntity) -> CmdResult<()> { - if let Some(buffkind) = parse_buffkind(kind) { - let ecs = &server.state.ecs(); - let mut buffs_all = ecs.write_storage::(); - let stats = ecs.read_storage::(); - let healths = ecs.read_storage::(); - let time = ecs.read_resource::