From e243aa07f5e53da5c6367a9f4b45ce9d863ad560 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Javier=20P=C3=A9rez?=
 <3212100-walpo@users.noreply.gitlab.com>
Date: Sun, 19 Nov 2023 22:54:21 +0100
Subject: [PATCH 01/10] docs: remove references to the Twitter account

---
 README.md | 1 -
 1 file changed, 1 deletion(-)

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)

From 5514df330b47474e9b6d14269ae4dc3020b5eedc Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 5 Jan 2024 19:56:41 +0200
Subject: [PATCH 02/10] Add BuffDescriptor enum

---
 common/src/comp/buff.rs | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs
index cef10d70c3..d0ccf30d76 100644
--- a/common/src/comp/buff.rs
+++ b/common/src/comp/buff.rs
@@ -175,6 +175,21 @@ pub enum BuffKind {
     Heatstroke,
 }
 
+/// Tells how buffs influence the target
+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?
+}
+
 impl BuffKind {
     /// Checks if buff is buff or debuff.
     pub fn is_buff(self) -> bool {

From f4939220ccccfe443e7eeb07de1dfd8ec75ce680 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 5 Jan 2024 20:15:59 +0200
Subject: [PATCH 03/10] Add BuffKind::differentiate

---
 common/src/comp/buff.rs | 30 +++++++++++++++++++++---------
 1 file changed, 21 insertions(+), 9 deletions(-)

diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs
index d0ccf30d76..bc3c8ca93c 100644
--- a/common/src/comp/buff.rs
+++ b/common/src/comp/buff.rs
@@ -165,18 +165,19 @@ 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 how buffs influence the target
-enum BuffDescriptor {
+/// 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`
@@ -188,11 +189,14 @@ enum BuffDescriptor {
     // 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
@@ -217,7 +221,7 @@ impl BuffKind {
             | BuffKind::Sunderer
             | BuffKind::Defiance
             | BuffKind::Bloodfeast
-            | BuffKind::Berserk => true,
+            | BuffKind::Berserk => BuffDescriptor::SimplePositive,
             BuffKind::Bleeding
             | BuffKind::Cursed
             | BuffKind::Burning
@@ -228,8 +232,16 @@ 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,
         }
     }
 

From 18742bc7fba2b7995ee2dd2aae266f3a79b5bdc8 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 5 Jan 2024 21:36:09 +0200
Subject: [PATCH 04/10] Fix veloren-server compilation

As veloren-server enables plugin feature automatically, it results in
veloren-common-state inherit this feature, which enables
common/state/plugin/mod.rs which asks for common::assets function that
is enabled only if plugin feature is enabled, but because
veloren-common-state doesn't depend on common::assets, this feature is
kind of lost half-way.

This commit fixes this by adding explicit optional dependency on
common-assets in common-state that is enabled by plugin feature.
---
 Cargo.lock               |  1 +
 common/assets/src/lib.rs | 10 +++++-----
 common/state/Cargo.toml  |  3 ++-
 3 files changed, 8 insertions(+), 6 deletions(-)

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/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<fs::FileSystem> =
-        AssetCache::with_source(fs::FileSystem::new().unwrap());
+    /// The HashMap where all loaded assets are stored in.
+    static ref ASSETS: AssetCache<fs::FileSystem> =
+            AssetCache::with_source(fs::FileSystem::new().unwrap());
 }
 
 #[cfg(feature = "hot-reloading")]
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"

From 5aa30b017558065bdd5b227d796c69479b3ae5b6 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 5 Jan 2024 21:40:34 +0200
Subject: [PATCH 05/10] Warn about complex buffs when using /buff

---
 assets/voxygen/i18n/en/command.ftl |  3 +-
 common/src/comp/buff.rs            |  7 +++
 server/src/cmd.rs                  | 76 +++++++++++++++++++-----------
 3 files changed, 57 insertions(+), 29 deletions(-)

diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl
index cbc6007b08..b840ca39cd 100644
--- a/assets/voxygen/i18n/en/command.ftl
+++ b/assets/voxygen/i18n/en/command.ftl
@@ -64,6 +64,7 @@ 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-complex = /buff doesn't work with this buff, use /buff_complex
 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 +96,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/src/comp/buff.rs b/common/src/comp/buff.rs
index bc3c8ca93c..70f9b47905 100644
--- a/common/src/comp/buff.rs
+++ b/common/src/comp/buff.rs
@@ -245,6 +245,13 @@ impl BuffKind {
         }
     }
 
+    pub fn is_simple(self) -> bool {
+        match self.differentiate() {
+            BuffDescriptor::SimplePositive | BuffDescriptor::SimpleNegative => true,
+            BuffDescriptor::Complex => false,
+        }
+    }
+
     /// Checks if buff should queue.
     pub fn queues(self) -> bool { matches!(self, BuffKind::Saturation) }
 
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index d7c7ae24c2..8c559a80ee 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -4145,10 +4145,30 @@ fn handle_buff(
         let duration = duration.unwrap_or(1.0);
         let buffdata = BuffData::new(strength, Some(Secs(duration)));
         if buff != "all" {
-            cast_buff(&buff, buffdata, server, target)
+            let buffkind = parse_buffkind(&buff).ok_or_else(|| {
+                Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
+            })?;
+
+            if buffkind.is_simple() {
+                cast_buff(buffkind, buffdata, server, target)
+            } else {
+                return Err(Content::localized_with_args("command-buff-complex", [(
+                    "buff", buff,
+                )]));
+            }
         } else {
-            for kind in BUFF_PACK.iter() {
-                cast_buff(kind, buffdata, server, target)?;
+            for kind_key in BUFF_PACK.iter() {
+                let buffkind = parse_buffkind(kind_key).ok_or_else(|| {
+                    Content::localized_with_args("command-buff-unknown", [(
+                        "buff",
+                        kind_key.to_owned(),
+                    )])
+                })?;
+
+                // Execute only simple buffs, ignore complex
+                if buffkind.is_simple() {
+                    cast_buff(buffkind, buffdata, server, target)?;
+                }
             }
             Ok(())
         }
@@ -4157,33 +4177,33 @@ fn handle_buff(
     }
 }
 
-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::<comp::Buffs>();
-        let stats = ecs.read_storage::<comp::Stats>();
-        let healths = ecs.read_storage::<comp::Health>();
-        let time = ecs.read_resource::<Time>();
-        if let Some(mut buffs) = buffs_all.get_mut(target) {
-            buffs.insert(
-                Buff::new(
-                    buffkind,
-                    data,
-                    vec![],
-                    BuffSource::Command,
-                    *time,
-                    stats.get(target),
-                    healths.get(target),
-                ),
+fn cast_buff(
+    buffkind: BuffKind,
+    data: BuffData,
+    server: &mut Server,
+    target: EcsEntity,
+) -> CmdResult<()> {
+    let ecs = &server.state.ecs();
+    let mut buffs_all = ecs.write_storage::<comp::Buffs>();
+    let stats = ecs.read_storage::<comp::Stats>();
+    let healths = ecs.read_storage::<comp::Health>();
+    let time = ecs.read_resource::<Time>();
+    if let Some(mut buffs) = buffs_all.get_mut(target) {
+        buffs.insert(
+            Buff::new(
+                buffkind,
+                data,
+                vec![],
+                BuffSource::Command,
                 *time,
-            );
-        }
-        Ok(())
-    } else {
-        Err(Content::localized_with_args("command-buff-unknown", [(
-            "buff", kind,
-        )]))
+                stats.get(target),
+                healths.get(target),
+            ),
+            *time,
+        );
     }
+
+    Ok(())
 }
 
 fn parse_buffkind(buff: &str) -> Option<BuffKind> { BUFF_PARSER.get(buff).copied() }

From 2746a98f40f6621cd01c18173138128e11c1fce9 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 6 Jan 2024 18:00:03 +0200
Subject: [PATCH 06/10] Add /buff_complex command

---
 assets/voxygen/i18n/en/command.ftl |   4 +-
 common/src/cmd.rs                  |  30 ++++++-
 server/src/cmd.rs                  | 140 +++++++++++++++++++++--------
 3 files changed, 132 insertions(+), 42 deletions(-)

diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl
index b840ca39cd..963caa7360 100644
--- a/assets/voxygen/i18n/en/command.ftl
+++ b/assets/voxygen/i18n/en/command.ftl
@@ -64,7 +64,9 @@ 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-complex = /buff doesn't work with this buff, use /buff_complex
+command-buff-complex = /buff doesn't work with [{ $buff }], use /buff_complex
+command-buff-simple = /buff_complex doesn't work with [{ $buff }], use /buff
+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 }
diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index a41a9c93b7..a7d7228490 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -200,13 +200,23 @@ lazy_static! {
         buff_pack
     };
 
-    static ref BUFFS: Vec<String> = {
-        let mut buff_pack: Vec<_> = BUFF_PARSER.keys().cloned().collect();
-        // Add all as valid command
+    static ref BUFFS_SIMPLE: Vec<String> = {
+        let mut buff_pack: Vec<String> = BUFF_PARSER
+            .iter()
+            .filter_map(|(key, kind)| kind.is_simple().then_some(key.clone()))
+            .collect();
+
+        // Add `all` as valid command
         buff_pack.push("all".to_string());
         buff_pack
     };
 
+    static ref BUFFS_COMPLEX: Vec<String> =
+        BUFF_PARSER
+            .iter()
+            .filter_map(|(key, kind)| (!kind.is_simple()).then_some(key.clone()))
+            .collect();
+
     static ref BLOCK_KINDS: Vec<String> = terrain::block::BlockKind::iter()
         .map(|bk| bk.to_string())
         .collect();
@@ -312,6 +322,7 @@ pub enum ServerChatCommand {
     BattleModeForce,
     Body,
     Buff,
+    BuffComplex,
     Build,
     Campfire,
     CreateLocation,
@@ -426,13 +437,23 @@ impl ServerChatCommand {
             ),
             ServerChatCommand::Buff => cmd(
                 vec![
-                    Enum("buff", BUFFS.clone(), Required),
+                    Enum("buff", BUFFS_SIMPLE.clone(), Required),
                     Float("strength", 0.01, Optional),
                     Float("duration", 10.0, Optional),
                 ],
                 "Cast a buff on player",
                 Some(Admin),
             ),
+            ServerChatCommand::BuffComplex => cmd(
+                vec![
+                    Enum("buff", BUFFS_COMPLEX.clone(), Required),
+                    Any("buff data spec", Required),
+                    Float("strength", 0.01, Optional),
+                    Float("duration", 10.0, Optional),
+                ],
+                "Cast a complex buff on player",
+                Some(Admin),
+            ),
             ServerChatCommand::Ban => cmd(
                 vec![
                     PlayerName(Required),
@@ -934,6 +955,7 @@ impl ServerChatCommand {
             ServerChatCommand::BattleModeForce => "battlemode_force",
             ServerChatCommand::Body => "body",
             ServerChatCommand::Buff => "buff",
+            ServerChatCommand::BuffComplex => "buff_complex",
             ServerChatCommand::Build => "build",
             ServerChatCommand::AreaAdd => "area_add",
             ServerChatCommand::AreaList => "area_list",
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 8c559a80ee..afe0a243cc 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,
@@ -131,6 +131,7 @@ fn do_command(
         ServerChatCommand::BattleModeForce => handle_battlemode_force,
         ServerChatCommand::Body => handle_body,
         ServerChatCommand::Buff => handle_buff,
+        ServerChatCommand::BuffComplex => handle_buff_complex,
         ServerChatCommand::Build => handle_build,
         ServerChatCommand::AreaAdd => handle_area_add,
         ServerChatCommand::AreaList => handle_area_list,
@@ -4140,49 +4141,116 @@ fn handle_buff(
     args: Vec<String>,
     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 buffdata = BuffData::new(strength, Some(Secs(duration)));
-        if buff != "all" {
-            let buffkind = parse_buffkind(&buff).ok_or_else(|| {
-                Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
-            })?;
+    let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) else {
+        return Err(Content::Plain(action.help_string()));
+    };
 
-            if buffkind.is_simple() {
-                cast_buff(buffkind, buffdata, server, target)
-            } else {
-                return Err(Content::localized_with_args("command-buff-complex", [(
-                    "buff", buff,
-                )]));
-            }
-        } else {
-            for kind_key in BUFF_PACK.iter() {
-                let buffkind = parse_buffkind(kind_key).ok_or_else(|| {
-                    Content::localized_with_args("command-buff-unknown", [(
-                        "buff",
-                        kind_key.to_owned(),
-                    )])
-                })?;
+    let strength = strength.unwrap_or(0.01);
+    let duration = duration.unwrap_or(1.0);
+    let buffdata = BuffData::new(strength, Some(Secs(duration)));
 
-                // Execute only simple buffs, ignore complex
-                if buffkind.is_simple() {
-                    cast_buff(buffkind, buffdata, server, target)?;
-                }
-            }
-            Ok(())
-        }
+    if buff == "all" {
+        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));
     } 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() {
+            return Err(Content::localized_with_args("command-buff-complex", [(
+                "buff", buff,
+            )]));
+        }
+
+        cast_buff(buffkind, buffdata, server, target);
     }
+
+    Ok(())
 }
 
-fn cast_buff(
-    buffkind: BuffKind,
-    data: BuffData,
+fn handle_buff_complex(
     server: &mut Server,
+    _client: EcsEntity,
     target: EcsEntity,
+    args: Vec<String>,
+    action: &ServerChatCommand,
 ) -> CmdResult<()> {
+    let (Some(buff), Some(spec), strength, duration) =
+        parse_cmd_args!(args, String, String, f32, f64)
+    else {
+        return 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() {
+        return Err(Content::localized_with_args("command-buff-simple", [(
+            "buff", buff,
+        )]));
+    }
+
+    // explicit match to remember that this function exists
+    let misc_data = match buffkind {
+        BuffKind::Polymorphed => {
+            let Ok(npc::NpcBody(_id, mut body)) = spec.parse() else {
+                return Err(Content::localized_with_args("command-buff-body-unknown", [
+                    ("spec", spec.clone()),
+                ]));
+            };
+            MiscBuffData::Body(body())
+        },
+        BuffKind::Regeneration
+        | BuffKind::Saturation
+        | BuffKind::Potion
+        | BuffKind::Agility
+        | BuffKind::CampfireHeal
+        | BuffKind::Frenzied
+        | BuffKind::EnergyRegen
+        | BuffKind::IncreaseMaxEnergy
+        | BuffKind::IncreaseMaxHealth
+        | BuffKind::Invulnerability
+        | BuffKind::ProtectingWard
+        | BuffKind::Hastened
+        | BuffKind::Fortitude
+        | BuffKind::Reckless
+        | BuffKind::Flame
+        | BuffKind::Frigid
+        | BuffKind::Lifesteal
+        | BuffKind::ImminentCritical
+        | BuffKind::Fury
+        | BuffKind::Sunderer
+        | BuffKind::Defiance
+        | BuffKind::Bloodfeast
+        | BuffKind::Berserk
+        | BuffKind::Bleeding
+        | BuffKind::Cursed
+        | BuffKind::Burning
+        | BuffKind::Crippled
+        | BuffKind::Frozen
+        | BuffKind::Wet
+        | BuffKind::Ensnared
+        | BuffKind::Poisoned
+        | BuffKind::Parried
+        | BuffKind::PotionSickness
+        | BuffKind::Heatstroke => unreachable!("is_simple() above"),
+    };
+
+    let strength = strength.unwrap_or(0.01);
+    let duration = duration.unwrap_or(20.0);
+
+    let buffdata = BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data);
+
+    cast_buff(buffkind, buffdata, server, target);
+    Ok(())
+}
+
+fn cast_buff(buffkind: BuffKind, data: BuffData, server: &mut Server, target: EcsEntity) {
     let ecs = &server.state.ecs();
     let mut buffs_all = ecs.write_storage::<comp::Buffs>();
     let stats = ecs.read_storage::<comp::Stats>();
@@ -4202,8 +4270,6 @@ fn cast_buff(
             *time,
         );
     }
-
-    Ok(())
 }
 
 fn parse_buffkind(buff: &str) -> Option<BuffKind> { BUFF_PARSER.get(buff).copied() }

From 296f70c1b2c17667e10a8756cdcb2ce82f89bfd2 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Mon, 8 Jan 2024 19:55:10 +0200
Subject: [PATCH 07/10] Unify /buff_complex and /buff

Turns out parse_cmd_args allows omitting arguments, /buff <buff>
[misc_data] will be idential to /buff <buff> [strength] [duration]
[misc_data]
---
 assets/voxygen/i18n/en/command.ftl |  3 +-
 common/src/cmd.rs                  | 30 +++-----------
 server/src/cmd.rs                  | 63 +++++++++++++-----------------
 3 files changed, 34 insertions(+), 62 deletions(-)

diff --git a/assets/voxygen/i18n/en/command.ftl b/assets/voxygen/i18n/en/command.ftl
index 963caa7360..4b76ba0b6a 100644
--- a/assets/voxygen/i18n/en/command.ftl
+++ b/assets/voxygen/i18n/en/command.ftl
@@ -64,8 +64,7 @@ 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-complex = /buff doesn't work with [{ $buff }], use /buff_complex
-command-buff-simple = /buff_complex doesn't work with [{ $buff }], use /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
diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index a7d7228490..b26059550e 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -200,23 +200,14 @@ lazy_static! {
         buff_pack
     };
 
-    static ref BUFFS_SIMPLE: Vec<String> = {
-        let mut buff_pack: Vec<String> = BUFF_PARSER
-            .iter()
-            .filter_map(|(key, kind)| kind.is_simple().then_some(key.clone()))
-            .collect();
+    static ref BUFFS: Vec<String> = {
+        let mut buff_pack: Vec<String> = BUFF_PARSER.keys().cloned().collect();
 
         // Add `all` as valid command
-        buff_pack.push("all".to_string());
+        buff_pack.push("all".to_owned());
         buff_pack
     };
 
-    static ref BUFFS_COMPLEX: Vec<String> =
-        BUFF_PARSER
-            .iter()
-            .filter_map(|(key, kind)| (!kind.is_simple()).then_some(key.clone()))
-            .collect();
-
     static ref BLOCK_KINDS: Vec<String> = terrain::block::BlockKind::iter()
         .map(|bk| bk.to_string())
         .collect();
@@ -322,7 +313,6 @@ pub enum ServerChatCommand {
     BattleModeForce,
     Body,
     Buff,
-    BuffComplex,
     Build,
     Campfire,
     CreateLocation,
@@ -437,23 +427,14 @@ impl ServerChatCommand {
             ),
             ServerChatCommand::Buff => cmd(
                 vec![
-                    Enum("buff", BUFFS_SIMPLE.clone(), Required),
+                    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),
             ),
-            ServerChatCommand::BuffComplex => cmd(
-                vec![
-                    Enum("buff", BUFFS_COMPLEX.clone(), Required),
-                    Any("buff data spec", Required),
-                    Float("strength", 0.01, Optional),
-                    Float("duration", 10.0, Optional),
-                ],
-                "Cast a complex buff on player",
-                Some(Admin),
-            ),
             ServerChatCommand::Ban => cmd(
                 vec![
                     PlayerName(Required),
@@ -955,7 +936,6 @@ impl ServerChatCommand {
             ServerChatCommand::BattleModeForce => "battlemode_force",
             ServerChatCommand::Body => "body",
             ServerChatCommand::Buff => "buff",
-            ServerChatCommand::BuffComplex => "buff_complex",
             ServerChatCommand::Build => "build",
             ServerChatCommand::AreaAdd => "area_add",
             ServerChatCommand::AreaList => "area_list",
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index afe0a243cc..a478ca4108 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -131,7 +131,6 @@ fn do_command(
         ServerChatCommand::BattleModeForce => handle_battlemode_force,
         ServerChatCommand::Body => handle_body,
         ServerChatCommand::Buff => handle_buff,
-        ServerChatCommand::BuffComplex => handle_buff_complex,
         ServerChatCommand::Build => handle_build,
         ServerChatCommand::AreaAdd => handle_area_add,
         ServerChatCommand::AreaList => handle_area_list,
@@ -4141,60 +4140,57 @@ fn handle_buff(
     args: Vec<String>,
     action: &ServerChatCommand,
 ) -> CmdResult<()> {
-    let (Some(buff), strength, duration) = parse_cmd_args!(args, String, f32, f64) else {
+    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);
-    let duration = duration.unwrap_or(1.0);
-    let buffdata = BuffData::new(strength, Some(Secs(duration)));
 
     if buff == "all" {
+        let duration = duration.unwrap_or(5.0);
+        let buffdata = BuffData::new(strength, Some(Secs(duration)));
+
+        // 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 {
         let buffkind = parse_buffkind(&buff).ok_or_else(|| {
             Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
         })?;
 
-        if !buffkind.is_simple() {
-            return Err(Content::localized_with_args("command-buff-complex", [(
-                "buff", buff,
-            )]));
+        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)
         }
-
-        cast_buff(buffkind, buffdata, server, target);
     }
-
-    Ok(())
 }
 
-fn handle_buff_complex(
+fn cast_buff_complex(
+    buffkind: BuffKind,
     server: &mut Server,
-    _client: EcsEntity,
     target: EcsEntity,
-    args: Vec<String>,
-    action: &ServerChatCommand,
+    spec: String,
+    strength: f32,
+    duration: f64,
 ) -> CmdResult<()> {
-    let (Some(buff), Some(spec), strength, duration) =
-        parse_cmd_args!(args, String, String, f32, f64)
-    else {
-        return 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() {
-        return Err(Content::localized_with_args("command-buff-simple", [(
-            "buff", buff,
-        )]));
-    }
-
     // explicit match to remember that this function exists
     let misc_data = match buffkind {
         BuffKind::Polymorphed => {
@@ -4241,9 +4237,6 @@ fn handle_buff_complex(
         | BuffKind::Heatstroke => unreachable!("is_simple() above"),
     };
 
-    let strength = strength.unwrap_or(0.01);
-    let duration = duration.unwrap_or(20.0);
-
     let buffdata = BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data);
 
     cast_buff(buffkind, buffdata, server, target);

From da08376e9aec818df75aad4b9da51c3f437581b4 Mon Sep 17 00:00:00 2001
From: Maxicarlos08 <maxicarlos08@gmail.com>
Date: Sat, 13 Jan 2024 18:45:54 +0100
Subject: [PATCH 08/10] Don't stack buffs of the same kind with equal
 attributes

---
 common/src/combat.rs       |  6 ++---
 common/src/comp/buff.rs    | 49 +++++++++++++++++++++++++-------------
 common/systems/src/buff.rs | 12 ++++------
 3 files changed, 39 insertions(+), 28 deletions(-)

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<GroupTarget>,
     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..ec58a4c0af 100644
--- a/common/src/comp/buff.rs
+++ b/common/src/comp/buff.rs
@@ -460,9 +460,6 @@ pub struct BuffData {
     pub strength: f32,
     pub duration: Option<Secs>,
     pub delay: Option<Secs>,
-    /// 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<Secs>,
     /// Used to add random data to buffs if needed (e.g. polymorphed)
@@ -479,7 +476,6 @@ impl BuffData {
         Self {
             strength,
             duration,
-            force_immediate: false,
             delay: None,
             secondary_duration: None,
             misc_data: None,
@@ -496,12 +492,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 +514,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 +772,36 @@ 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 buff with same data, cat_ids and source
+        let other_key = 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
+                    })
+                })
+                .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/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;

From df7c2ee70a2d9b1b7e6e8efa9f0b9fd4316fd2c6 Mon Sep 17 00:00:00 2001
From: Maxicarlos08 <maxicarlos08@gmail.com>
Date: Sat, 13 Jan 2024 18:50:51 +0100
Subject: [PATCH 09/10] changelog

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

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
 

From 3a7bb698fc376fddd5e397050bd5e8e75b6f87d7 Mon Sep 17 00:00:00 2001
From: Maxicarlos08 <maxicarlos08@gmail.com>
Date: Sat, 13 Jan 2024 19:53:02 +0100
Subject: [PATCH 10/10] handle overlap and queueing correctly

---
 common/src/comp/buff.rs | 30 +++++++++++++++++++-----------
 1 file changed, 19 insertions(+), 11 deletions(-)

diff --git a/common/src/comp/buff.rs b/common/src/comp/buff.rs
index ec58a4c0af..8b98f38284 100644
--- a/common/src/comp/buff.rs
+++ b/common/src/comp/buff.rs
@@ -772,18 +772,26 @@ impl Buffs {
 
     pub fn insert(&mut self, buff: Buff, current_time: Time) -> BuffKey {
         let kind = buff.kind;
-        // Try to find another buff with same data, cat_ids and source
-        let other_key = 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
+        // 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()
-        });
+                    .copied()
+            })
+        };
 
         // If another buff with the same fields is found, update end_time and effects
         let key = if let Some((other_buff, key)) =