From 253a18dbfede1e5e267657d31d9bc99d922cde4e Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Tue, 12 Oct 2021 02:06:12 -0400
Subject: [PATCH 1/8] Add more profiling outputs to the .gitignore

---
 .gitignore | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/.gitignore b/.gitignore
index 5621834e78..0c3b49c28a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,13 @@ maps
 screenshots
 todo.txt
 userdata
+
+# Profiling and traces
 heaptrack.*
+cargo-timing*
+flamegraph.*
+perf.*
+wgpu-trace/
 
 # allow asset hud settings
 !assets/voxygen/i18n/*/hud/settings.ron

From 4330a8c24b01d029b389d29143463f06e5edc98d Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Tue, 12 Oct 2021 02:10:42 -0400
Subject: [PATCH 2/8] Improve efficiency of states::utils::handle_orientation
 by reducing the conversions between Ori/Dir less frequent and optimizing the
 conversion of Dir -> Ori, also added a method to compute the angle between
 two Ori so that they don't need to be converted to Dir

---
 common/src/comp/character_state.rs       |  1 +
 common/src/comp/ori.rs                   | 56 ++++++++++++++----------
 common/src/states/utils.rs               | 17 ++++---
 common/systems/src/character_behavior.rs |  2 +
 4 files changed, 48 insertions(+), 28 deletions(-)

diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs
index 1feb6ed264..9d27425d39 100644
--- a/common/src/comp/character_state.rs
+++ b/common/src/comp/character_state.rs
@@ -37,6 +37,7 @@ pub struct StateUpdate {
 
 impl From<&JoinData<'_>> for StateUpdate {
     fn from(data: &JoinData) -> Self {
+        common_base::prof_span!("StateUpdate::from");
         StateUpdate {
             pos: *data.pos,
             vel: *data.vel,
diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs
index 5531c94829..8ac46732d6 100644
--- a/common/src/comp/ori.rs
+++ b/common/src/comp/ori.rs
@@ -170,6 +170,18 @@ impl Ori {
             )
     }
 
+    /// Find the angle between two `Ori`s
+    ///
+    /// Returns angle in radians
+    pub fn angle_between(self, other: Self) -> f32 {
+        // Compute quaternion from one ori to the other
+        // https://www.mathworks.com/matlabcentral/answers/476474-how-to-find-the-angle-between-two-quaternions#answer_387973
+        let between = self.to_quat().conjugate() * other.to_quat();
+        // Then compute it's angle
+        // http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/
+        2.0 * between.w.acos()
+    }
+
     pub fn pitched_up(self, angle_radians: f32) -> Self {
         self.rotated(Quaternion::rotation_x(angle_radians))
     }
@@ -239,38 +251,37 @@ impl Ori {
     /// let ang2 = pd_rr.up().angle_between(zenith);
     /// assert!((ang1 - ang2).abs() <= std::f32::EPSILON);
     /// ```
-    pub fn uprighted(self) -> Self {
-        let fw = self.look_dir();
-        match Dir::up().projected(&Plane::from(fw)) {
-            Some(dir_p) => {
-                let up = self.up();
-                let go_right_s = fw.cross(*up).dot(*dir_p).signum();
-                self.rolled_right(up.angle_between(*dir_p) * go_right_s)
-            },
-            None => self,
-        }
-    }
+    pub fn uprighted(self) -> Self { self.look_dir().into() }
 
     fn is_normalized(&self) -> bool { self.0.into_vec4().is_normalized() }
 }
-
 impl From<Dir> for Ori {
     fn from(dir: Dir) -> Self {
-        let from = Dir::default();
-        let q = Quaternion::<f32>::rotation_from_to_3d(*from, *dir).normalized();
+        // Check that dir is not straight up/down
+        // Uses a multiple of EPSILON to be safe
+        let quat = if dir.z.abs() - 1.0 > f32::EPSILON * 4.0 {
+            // Compute rotation that will give an "upright" orientation (no rolling):
 
-        Self(q).uprighted()
+            // Rotation to get to this projected point from the default direction of y+
+            let yaw = dir.xy().normalized().y.acos() * dir.x.signum();
+            // Rotation to then rotate up/down to the match the input direction
+            let pitch = dir.z.asin();
+
+            (Quaternion::rotation_z(yaw) * Quaternion::rotation_x(pitch)).normalized()
+        } else {
+            // Nothing in particular can be considered upright if facing up or down
+            // so we just produce a quaternion that will rotate to that direction
+            let from = Dir::default();
+            // This calls normalized() internally
+            Quaternion::<f32>::rotation_from_to_3d(*from, *dir)
+        };
+
+        Self(quat)
     }
 }
 
 impl From<Vec3<f32>> for Ori {
-    fn from(dir: Vec3<f32>) -> Self {
-        let dir = Dir::from_unnormalized(dir).unwrap_or_default();
-        let from = Dir::default();
-        let q = Quaternion::<f32>::rotation_from_to_3d(*from, *dir).normalized();
-
-        Self(q).uprighted()
-    }
+    fn from(dir: Vec3<f32>) -> Self { Dir::from_unnormalized(dir).unwrap_or_default().into() }
 }
 
 impl From<Quaternion<f32>> for Ori {
@@ -359,6 +370,7 @@ mod tests {
         let from_to = |dir: Dir| {
             let ori = Ori::from(dir);
 
+            assert!(ori.is_normalized(), "ori {:?}\ndir {:?}", ori, dir);
             approx::assert_relative_eq!(ori.look_dir().dot(*dir), 1.0);
             approx::assert_relative_eq!((ori.to_quat() * Dir::default()).dot(*dir), 1.0);
         };
diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs
index 3b554b4f07..0a4f599120 100644
--- a/common/src/states/utils.rs
+++ b/common/src/states/utils.rs
@@ -387,24 +387,29 @@ pub fn handle_orientation(
     efficiency: f32,
     dir_override: Option<Dir>,
 ) {
+    common_base::prof_span!("handle_orientation");
     // Direction is set to the override if one is provided, else if entity is
     // strafing or attacking the horiontal component of the look direction is used,
     // else the current horizontal movement direction is used
-    let dir = if let Some(dir_override) = dir_override {
-        dir_override
+    let target_ori = if let Some(dir_override) = dir_override {
+        dir_override.into()
     } else if is_strafing(data, update) || update.character.is_attack() {
-        data.inputs.look_dir.to_horizontal().unwrap_or_default()
+        data.inputs
+            .look_dir
+            .to_horizontal()
+            .unwrap_or_default()
+            .into()
     } else {
         Dir::from_unnormalized(data.inputs.move_dir.into())
-            .unwrap_or_else(|| data.ori.to_horizontal().look_dir())
+            .map_or_else(|| data.ori.to_horizontal(), |dir| dir.into())
     };
     let rate = {
-        let angle = update.ori.look_dir().angle_between(*dir);
+        let angle = update.ori.angle_between(target_ori);
         data.body.base_ori_rate() * efficiency * std::f32::consts::PI / angle
     };
     update.ori = update
         .ori
-        .slerped_towards(dir.into(), (data.dt.0 * rate).min(1.0));
+        .slerped_towards(target_ori, (data.dt.0 * rate).min(1.0));
 }
 
 /// Updates components to move player as if theyre swimming
diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs
index f43fc53a05..d3ee92c2ba 100644
--- a/common/systems/src/character_behavior.rs
+++ b/common/systems/src/character_behavior.rs
@@ -16,6 +16,7 @@ use common::{
     terrain::TerrainGrid,
     uid::Uid,
 };
+use common_base::prof_span;
 use common_ecs::{Job, Origin, Phase, System};
 use std::time::Duration;
 
@@ -123,6 +124,7 @@ impl<'a> System<'a> for Sys {
         )
             .join()
         {
+            prof_span!("entity");
             // Being dead overrides all other states
             if health.map_or(false, |h| h.is_dead) {
                 // Do nothing

From 33af9a8bbdd04be687878a76b9dbe653f9bbd072 Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Tue, 12 Oct 2021 12:43:46 -0400
Subject: [PATCH 3/8] Rewrite Ori::to_horizontal to reduce redundant
 normalization and directly calculate the needed yaw instead of using the more
 general Ori::from(dir), fix bugs in Ori::from(dir) and optimize the up/down
 case, add tests for Ori::to_horizontal and Ori::angle_between

---
 common/src/comp/ori.rs | 122 ++++++++++++++++++++++++++++-------------
 1 file changed, 83 insertions(+), 39 deletions(-)

diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs
index 8ac46732d6..ab9ba00fba 100644
--- a/common/src/comp/ori.rs
+++ b/common/src/comp/ori.rs
@@ -149,25 +149,25 @@ impl Ori {
     }
 
     pub fn to_horizontal(self) -> Self {
-        let fw = self.look_dir();
-        Dir::from_unnormalized(fw.xy().into())
-            .or_else(|| {
-                // if look_dir is straight down, pitch up, or if straight up, pitch down
-                Dir::from_unnormalized(
-                    if fw.dot(Vec3::unit_z()) < 0.0 {
-                        self.up()
-                    } else {
-                        self.down()
-                    }
-                    .xy()
-                    .into(),
-                )
-            })
-            .map(|dir| dir.into())
-            .expect(
-                "If the horizontal component of a Dir can not be normalized, the horizontal \
-                 component of a Dir perpendicular to it must be",
-            )
+        // We don't use Self::look_dir to avoid the extra normalization step within
+        // Dir's Quaternion Mul impl (since we will normalize later below)
+        let fw = self.to_quat() * Dir::default().to_vec();
+        // Check that dir is not straight up/down
+        // Uses a multiple of EPSILON to be safe
+        // We can just check z since beyond floating point errors `fw` should be
+        // normalized
+        let xy = if 1.0 - fw.z.abs() > f32::EPSILON * 4.0 {
+            fw.xy().normalized()
+        } else {
+            // if look_dir is straight down, pitch up, or if straight up, pitch down
+            // xy should essentially be normalized so no need to normalize
+            if fw.z < 0.0 { self.up() } else { self.down() }.xy()
+        };
+        // We know direction lies in the xy plane so we only need to compute a rotation
+        // about the z-axis
+        let yaw = xy.y.acos() * fw.x.signum() * -1.0;
+
+        Self(Quaternion::rotation_z(yaw))
     }
 
     /// Find the angle between two `Ori`s
@@ -255,15 +255,16 @@ impl Ori {
 
     fn is_normalized(&self) -> bool { self.0.into_vec4().is_normalized() }
 }
+
 impl From<Dir> for Ori {
     fn from(dir: Dir) -> Self {
         // Check that dir is not straight up/down
         // Uses a multiple of EPSILON to be safe
-        let quat = if dir.z.abs() - 1.0 > f32::EPSILON * 4.0 {
+        let quat = if 1.0 - dir.z.abs() > f32::EPSILON * 4.0 {
             // Compute rotation that will give an "upright" orientation (no rolling):
 
             // Rotation to get to this projected point from the default direction of y+
-            let yaw = dir.xy().normalized().y.acos() * dir.x.signum();
+            let yaw = dir.xy().normalized().y.acos() * dir.x.signum() * -1.0;
             // Rotation to then rotate up/down to the match the input direction
             let pitch = dir.z.asin();
 
@@ -271,9 +272,9 @@ impl From<Dir> for Ori {
         } else {
             // Nothing in particular can be considered upright if facing up or down
             // so we just produce a quaternion that will rotate to that direction
-            let from = Dir::default();
-            // This calls normalized() internally
-            Quaternion::<f32>::rotation_from_to_3d(*from, *dir)
+            // (once again rotating from y+)
+            let pitch = PI / 2.0 * dir.z.signum();
+            Quaternion::rotation_x(pitch)
         };
 
         Self(quat)
@@ -365,33 +366,76 @@ impl Component for Ori {
 mod tests {
     use super::*;
 
+    // Helper method to produce Dirs at different angles to test
+    fn dirs() -> impl Iterator<Item = Dir> {
+        let angles = 32;
+        (0..angles).flat_map(move |i| {
+            let theta = PI * 2.0 * (i as f32) / (angles as f32);
+
+            let v = Vec3::unit_y();
+            let q = Quaternion::rotation_x(theta);
+            let dir_1 = Dir::new(q * v);
+
+            let v = Vec3::unit_z();
+            let q = Quaternion::rotation_y(theta);
+            let dir_2 = Dir::new(q * v);
+
+            let v = Vec3::unit_x();
+            let q = Quaternion::rotation_z(theta);
+            let dir_3 = Dir::new(q * v);
+
+            [dir_1, dir_2, dir_3]
+        })
+    }
+
+    #[test]
+    fn to_horizontal() {
+        let to_horizontal = |dir: Dir| {
+            let ori = Ori::from(dir);
+
+            let horizontal = ori.to_horizontal();
+
+            approx::assert_relative_eq!(horizontal.look_dir().xy().magnitude(), 1.0);
+            approx::assert_relative_eq!(horizontal.look_dir().z, 0.0);
+        };
+
+        dirs().for_each(to_horizontal);
+    }
+
+    #[test]
+    fn angle_between() {
+        let angle_between = |(dir_a, dir_b): (Dir, Dir)| {
+            let ori_a = Ori::from(dir_a);
+            let ori_b = Ori::from(dir_b);
+
+            approx::assert_relative_eq!(ori_a.angle_between(ori_b), dir_a.angle_between(*dir_b));
+        };
+
+        dirs()
+            .flat_map(|dir| dirs().map(move |dir_two| (dir, dir_two)))
+            .for_each(angle_between)
+    }
+
     #[test]
     fn from_to_dir() {
         let from_to = |dir: Dir| {
             let ori = Ori::from(dir);
 
             assert!(ori.is_normalized(), "ori {:?}\ndir {:?}", ori, dir);
-            approx::assert_relative_eq!(ori.look_dir().dot(*dir), 1.0);
+            assert!(
+                approx::relative_eq!(ori.look_dir().dot(*dir), 1.0),
+                "Ori::from(dir).look_dir() != dir\ndir: {:?}\nOri::from(dir).look_dir(): {:?}",
+                dir,
+                ori.look_dir(),
+            );
             approx::assert_relative_eq!((ori.to_quat() * Dir::default()).dot(*dir), 1.0);
         };
 
-        let angles = 32;
-        for i in 0..angles {
-            let theta = PI * 2. * (i as f32) / (angles as f32);
-            let v = Vec3::unit_y();
-            let q = Quaternion::rotation_x(theta);
-            from_to(Dir::new(q * v));
-            let v = Vec3::unit_z();
-            let q = Quaternion::rotation_y(theta);
-            from_to(Dir::new(q * v));
-            let v = Vec3::unit_x();
-            let q = Quaternion::rotation_z(theta);
-            from_to(Dir::new(q * v));
-        }
+        dirs().for_each(from_to);
     }
 
     #[test]
-    fn dirs() {
+    fn orthogonal_dirs() {
         let ori = Ori::default();
         let def = Dir::default();
         for dir in &[ori.up(), ori.down(), ori.left(), ori.right()] {

From 7b35f14043fe0a1f1c891c7af194729c666e1571 Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Tue, 12 Oct 2021 22:03:18 -0400
Subject: [PATCH 4/8] Remove per entity VecDeque's of events from character
 StateUpdate and instead pass in external Vecs that can be pushed to (saves
 significant time not allocating for VecDeque::new)

---
 common/src/comp/character_state.rs       | 168 +++++++++++++----------
 common/src/comp/mod.rs                   |   2 +-
 common/src/event.rs                      |   3 +
 common/src/lib.rs                        |   3 +-
 common/src/states/basic_aura.rs          |   9 +-
 common/src/states/basic_beam.rs          |  11 +-
 common/src/states/basic_block.rs         |   4 +-
 common/src/states/basic_melee.rs         |  22 ++-
 common/src/states/basic_ranged.rs        |  27 +++-
 common/src/states/basic_summon.rs        |   7 +-
 common/src/states/behavior.rs            |  85 ++++++++----
 common/src/states/blink.rs               |   6 +-
 common/src/states/boost.rs               |  20 ++-
 common/src/states/charged_melee.rs       |  18 ++-
 common/src/states/charged_ranged.rs      |   9 +-
 common/src/states/climb.rs               |   5 +-
 common/src/states/combo_melee.rs         |  19 ++-
 common/src/states/dance.rs               |  21 +--
 common/src/states/dash_melee.rs          |   4 +-
 common/src/states/equipping.rs           |   6 +-
 common/src/states/glide.rs               |   8 +-
 common/src/states/glide_wield.rs         |  26 ++--
 common/src/states/idle.rs                |  29 ++--
 common/src/states/leap_melee.rs          |   6 +-
 common/src/states/repeater_ranged.rs     |  11 +-
 common/src/states/roll.rs                |   9 +-
 common/src/states/self_buff.rs           |   7 +-
 common/src/states/shockwave.rs           |   6 +-
 common/src/states/sit.rs                 |  21 +--
 common/src/states/sneak.rs               |  27 ++--
 common/src/states/spin_melee.rs          |   4 +-
 common/src/states/sprite_interact.rs     |   8 +-
 common/src/states/sprite_summon.rs       |   6 +-
 common/src/states/stunned.rs             |   4 +-
 common/src/states/talk.rs                |  21 +--
 common/src/states/use_item.rs            |  17 ++-
 common/src/states/utils.rs               |  38 +++--
 common/src/states/wielding.rs            |  26 ++--
 common/systems/src/character_behavior.rs |  42 +++---
 39 files changed, 464 insertions(+), 301 deletions(-)

diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs
index 9d27425d39..8c312489d9 100644
--- a/common/src/comp/character_state.rs
+++ b/common/src/comp/character_state.rs
@@ -15,7 +15,7 @@ use crate::{
 use serde::{Deserialize, Serialize};
 use specs::{Component, DerefFlaggedStorage, VecStorage};
 use specs_idvs::IdvStorage;
-use std::collections::{BTreeMap, VecDeque};
+use std::collections::BTreeMap;
 use strum_macros::Display;
 use vek::*;
 
@@ -31,8 +31,21 @@ pub struct StateUpdate {
     pub should_strafe: bool,
     pub queued_inputs: BTreeMap<InputKind, InputAttr>,
     pub removed_inputs: Vec<InputKind>,
-    pub local_events: VecDeque<LocalEvent>,
-    pub server_events: VecDeque<ServerEvent>,
+}
+
+pub struct OutputEvents<'a> {
+    local: &'a mut Vec<LocalEvent>,
+    server: &'a mut Vec<ServerEvent>,
+}
+
+impl<'a> OutputEvents<'a> {
+    pub fn new(local: &'a mut Vec<LocalEvent>, server: &'a mut Vec<ServerEvent>) -> Self {
+        Self { local, server }
+    }
+
+    pub fn emit_local(&mut self, event: LocalEvent) { self.local.push(event); }
+
+    pub fn emit_server(&mut self, event: ServerEvent) { self.server.push(event); }
 }
 
 impl From<&JoinData<'_>> for StateUpdate {
@@ -49,8 +62,6 @@ impl From<&JoinData<'_>> for StateUpdate {
             character: data.character.clone(),
             queued_inputs: BTreeMap::new(),
             removed_inputs: Vec::new(),
-            local_events: VecDeque::new(),
-            server_events: VecDeque::new(),
         }
     }
 }
@@ -246,81 +257,96 @@ impl CharacterState {
         std::mem::discriminant(self) == std::mem::discriminant(other)
     }
 
-    pub fn behavior(&self, j: &JoinData) -> StateUpdate {
+    pub fn behavior(&self, j: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         match &self {
-            CharacterState::Idle => states::idle::Data.behavior(j),
-            CharacterState::Talk => states::talk::Data.behavior(j),
-            CharacterState::Climb(data) => data.behavior(j),
-            CharacterState::Glide(data) => data.behavior(j),
-            CharacterState::GlideWield(data) => data.behavior(j),
-            CharacterState::Stunned(data) => data.behavior(j),
-            CharacterState::Sit => states::sit::Data::behavior(&states::sit::Data, j),
-            CharacterState::Dance => states::dance::Data::behavior(&states::dance::Data, j),
-            CharacterState::Sneak => states::sneak::Data::behavior(&states::sneak::Data, j),
-            CharacterState::BasicBlock(data) => data.behavior(j),
-            CharacterState::Roll(data) => data.behavior(j),
-            CharacterState::Wielding => states::wielding::Data.behavior(j),
-            CharacterState::Equipping(data) => data.behavior(j),
-            CharacterState::ComboMelee(data) => data.behavior(j),
-            CharacterState::BasicMelee(data) => data.behavior(j),
-            CharacterState::BasicRanged(data) => data.behavior(j),
-            CharacterState::Boost(data) => data.behavior(j),
-            CharacterState::DashMelee(data) => data.behavior(j),
-            CharacterState::LeapMelee(data) => data.behavior(j),
-            CharacterState::SpinMelee(data) => data.behavior(j),
-            CharacterState::ChargedMelee(data) => data.behavior(j),
-            CharacterState::ChargedRanged(data) => data.behavior(j),
-            CharacterState::RepeaterRanged(data) => data.behavior(j),
-            CharacterState::Shockwave(data) => data.behavior(j),
-            CharacterState::BasicBeam(data) => data.behavior(j),
-            CharacterState::BasicAura(data) => data.behavior(j),
-            CharacterState::Blink(data) => data.behavior(j),
-            CharacterState::BasicSummon(data) => data.behavior(j),
-            CharacterState::SelfBuff(data) => data.behavior(j),
-            CharacterState::SpriteSummon(data) => data.behavior(j),
-            CharacterState::UseItem(data) => data.behavior(j),
-            CharacterState::SpriteInteract(data) => data.behavior(j),
+            CharacterState::Idle => states::idle::Data.behavior(j, output_events),
+            CharacterState::Talk => states::talk::Data.behavior(j, output_events),
+            CharacterState::Climb(data) => data.behavior(j, output_events),
+            CharacterState::Glide(data) => data.behavior(j, output_events),
+            CharacterState::GlideWield(data) => data.behavior(j, output_events),
+            CharacterState::Stunned(data) => data.behavior(j, output_events),
+            CharacterState::Sit => {
+                states::sit::Data::behavior(&states::sit::Data, j, output_events)
+            },
+            CharacterState::Dance => {
+                states::dance::Data::behavior(&states::dance::Data, j, output_events)
+            },
+            CharacterState::Sneak => {
+                states::sneak::Data::behavior(&states::sneak::Data, j, output_events)
+            },
+            CharacterState::BasicBlock(data) => data.behavior(j, output_events),
+            CharacterState::Roll(data) => data.behavior(j, output_events),
+            CharacterState::Wielding => states::wielding::Data.behavior(j, output_events),
+            CharacterState::Equipping(data) => data.behavior(j, output_events),
+            CharacterState::ComboMelee(data) => data.behavior(j, output_events),
+            CharacterState::BasicMelee(data) => data.behavior(j, output_events),
+            CharacterState::BasicRanged(data) => data.behavior(j, output_events),
+            CharacterState::Boost(data) => data.behavior(j, output_events),
+            CharacterState::DashMelee(data) => data.behavior(j, output_events),
+            CharacterState::LeapMelee(data) => data.behavior(j, output_events),
+            CharacterState::SpinMelee(data) => data.behavior(j, output_events),
+            CharacterState::ChargedMelee(data) => data.behavior(j, output_events),
+            CharacterState::ChargedRanged(data) => data.behavior(j, output_events),
+            CharacterState::RepeaterRanged(data) => data.behavior(j, output_events),
+            CharacterState::Shockwave(data) => data.behavior(j, output_events),
+            CharacterState::BasicBeam(data) => data.behavior(j, output_events),
+            CharacterState::BasicAura(data) => data.behavior(j, output_events),
+            CharacterState::Blink(data) => data.behavior(j, output_events),
+            CharacterState::BasicSummon(data) => data.behavior(j, output_events),
+            CharacterState::SelfBuff(data) => data.behavior(j, output_events),
+            CharacterState::SpriteSummon(data) => data.behavior(j, output_events),
+            CharacterState::UseItem(data) => data.behavior(j, output_events),
+            CharacterState::SpriteInteract(data) => data.behavior(j, output_events),
         }
     }
 
-    pub fn handle_event(&self, j: &JoinData, action: ControlAction) -> StateUpdate {
+    pub fn handle_event(
+        &self,
+        j: &JoinData,
+        output_events: &mut OutputEvents,
+        action: ControlAction,
+    ) -> StateUpdate {
         match &self {
-            CharacterState::Idle => states::idle::Data.handle_event(j, action),
-            CharacterState::Talk => states::talk::Data.handle_event(j, action),
-            CharacterState::Climb(data) => data.handle_event(j, action),
-            CharacterState::Glide(data) => data.handle_event(j, action),
-            CharacterState::GlideWield(data) => data.handle_event(j, action),
-            CharacterState::Stunned(data) => data.handle_event(j, action),
-            CharacterState::Sit => states::sit::Data::handle_event(&states::sit::Data, j, action),
+            CharacterState::Idle => states::idle::Data.handle_event(j, output_events, action),
+            CharacterState::Talk => states::talk::Data.handle_event(j, output_events, action),
+            CharacterState::Climb(data) => data.handle_event(j, output_events, action),
+            CharacterState::Glide(data) => data.handle_event(j, output_events, action),
+            CharacterState::GlideWield(data) => data.handle_event(j, output_events, action),
+            CharacterState::Stunned(data) => data.handle_event(j, output_events, action),
+            CharacterState::Sit => {
+                states::sit::Data::handle_event(&states::sit::Data, j, output_events, action)
+            },
             CharacterState::Dance => {
-                states::dance::Data::handle_event(&states::dance::Data, j, action)
+                states::dance::Data::handle_event(&states::dance::Data, j, output_events, action)
             },
             CharacterState::Sneak => {
-                states::sneak::Data::handle_event(&states::sneak::Data, j, action)
+                states::sneak::Data::handle_event(&states::sneak::Data, j, output_events, action)
             },
-            CharacterState::BasicBlock(data) => data.handle_event(j, action),
-            CharacterState::Roll(data) => data.handle_event(j, action),
-            CharacterState::Wielding => states::wielding::Data.handle_event(j, action),
-            CharacterState::Equipping(data) => data.handle_event(j, action),
-            CharacterState::ComboMelee(data) => data.handle_event(j, action),
-            CharacterState::BasicMelee(data) => data.handle_event(j, action),
-            CharacterState::BasicRanged(data) => data.handle_event(j, action),
-            CharacterState::Boost(data) => data.handle_event(j, action),
-            CharacterState::DashMelee(data) => data.handle_event(j, action),
-            CharacterState::LeapMelee(data) => data.handle_event(j, action),
-            CharacterState::SpinMelee(data) => data.handle_event(j, action),
-            CharacterState::ChargedMelee(data) => data.handle_event(j, action),
-            CharacterState::ChargedRanged(data) => data.handle_event(j, action),
-            CharacterState::RepeaterRanged(data) => data.handle_event(j, action),
-            CharacterState::Shockwave(data) => data.handle_event(j, action),
-            CharacterState::BasicBeam(data) => data.handle_event(j, action),
-            CharacterState::BasicAura(data) => data.handle_event(j, action),
-            CharacterState::Blink(data) => data.handle_event(j, action),
-            CharacterState::BasicSummon(data) => data.handle_event(j, action),
-            CharacterState::SelfBuff(data) => data.handle_event(j, action),
-            CharacterState::SpriteSummon(data) => data.handle_event(j, action),
-            CharacterState::UseItem(data) => data.handle_event(j, action),
-            CharacterState::SpriteInteract(data) => data.handle_event(j, action),
+            CharacterState::BasicBlock(data) => data.handle_event(j, output_events, action),
+            CharacterState::Roll(data) => data.handle_event(j, output_events, action),
+            CharacterState::Wielding => {
+                states::wielding::Data.handle_event(j, output_events, action)
+            },
+            CharacterState::Equipping(data) => data.handle_event(j, output_events, action),
+            CharacterState::ComboMelee(data) => data.handle_event(j, output_events, action),
+            CharacterState::BasicMelee(data) => data.handle_event(j, output_events, action),
+            CharacterState::BasicRanged(data) => data.handle_event(j, output_events, action),
+            CharacterState::Boost(data) => data.handle_event(j, output_events, action),
+            CharacterState::DashMelee(data) => data.handle_event(j, output_events, action),
+            CharacterState::LeapMelee(data) => data.handle_event(j, output_events, action),
+            CharacterState::SpinMelee(data) => data.handle_event(j, output_events, action),
+            CharacterState::ChargedMelee(data) => data.handle_event(j, output_events, action),
+            CharacterState::ChargedRanged(data) => data.handle_event(j, output_events, action),
+            CharacterState::RepeaterRanged(data) => data.handle_event(j, output_events, action),
+            CharacterState::Shockwave(data) => data.handle_event(j, output_events, action),
+            CharacterState::BasicBeam(data) => data.handle_event(j, output_events, action),
+            CharacterState::BasicAura(data) => data.handle_event(j, output_events, action),
+            CharacterState::Blink(data) => data.handle_event(j, output_events, action),
+            CharacterState::BasicSummon(data) => data.handle_event(j, output_events, action),
+            CharacterState::SelfBuff(data) => data.handle_event(j, output_events, action),
+            CharacterState::SpriteSummon(data) => data.handle_event(j, output_events, action),
+            CharacterState::UseItem(data) => data.handle_event(j, output_events, action),
+            CharacterState::SpriteInteract(data) => data.handle_event(j, output_events, action),
         }
     }
 }
diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs
index 12575c6d71..677b5c17e2 100644
--- a/common/src/comp/mod.rs
+++ b/common/src/comp/mod.rs
@@ -8,7 +8,7 @@ pub mod anchor;
 #[cfg(not(target_arch = "wasm32"))] pub mod body;
 pub mod buff;
 #[cfg(not(target_arch = "wasm32"))]
-mod character_state;
+pub mod character_state;
 #[cfg(not(target_arch = "wasm32"))] pub mod chat;
 #[cfg(not(target_arch = "wasm32"))] pub mod combo;
 pub mod compass;
diff --git a/common/src/event.rs b/common/src/event.rs
index 4c7d1bc896..0c551ed39d 100644
--- a/common/src/event.rs
+++ b/common/src/event.rs
@@ -242,6 +242,9 @@ impl<'a, E> Emitter<'a, E> {
     pub fn emit(&mut self, event: E) { self.events.push_back(event); }
 
     pub fn append(&mut self, other: &mut VecDeque<E>) { self.events.append(other) }
+
+    // TODO: allow just emitting the whole vec of events at once? without copying
+    pub fn append_vec(&mut self, vec: Vec<E>) { self.events.extend(vec) }
 }
 
 impl<'a, E> Drop for Emitter<'a, E> {
diff --git a/common/src/lib.rs b/common/src/lib.rs
index a337c57b88..01cd3e8dde 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -10,7 +10,8 @@
     label_break_value,
     option_zip,
     trait_alias,
-    type_alias_impl_trait
+    type_alias_impl_trait,
+    extend_one
 )]
 
 /// Re-exported crates
diff --git a/common/src/states/basic_aura.rs b/common/src/states/basic_aura.rs
index 0c2f084c67..2ee79957b7 100644
--- a/common/src/states/basic_aura.rs
+++ b/common/src/states/basic_aura.rs
@@ -2,6 +2,7 @@ use crate::{
     combat::GroupTarget,
     comp::{
         aura::{AuraBuffConstructor, AuraChange, AuraKind, AuraTarget, Specifier},
+        character_state::OutputEvents,
         CharacterState, StateUpdate,
     },
     event::ServerEvent,
@@ -52,12 +53,12 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.8);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Buildup => {
@@ -89,12 +90,12 @@ impl CharacterBehavior for Data {
                                     1.0 + (self.static_data.combo_at_cast.max(1) as f32).log(2.0);
                             },
                         }
-                        update.server_events.push_front(ServerEvent::ComboChange {
+                        output_events.emit_server(ServerEvent::ComboChange {
                             entity: data.entity,
                             change: -(self.static_data.combo_at_cast as i32),
                         });
                     }
-                    update.server_events.push_front(ServerEvent::Aura {
+                    output_events.emit_server(ServerEvent::Aura {
                         entity: data.entity,
                         aura_change: AuraChange::Add(aura),
                     });
diff --git a/common/src/states/basic_beam.rs b/common/src/states/basic_beam.rs
index deda921aa3..ffc7b4ad2e 100644
--- a/common/src/states/basic_beam.rs
+++ b/common/src/states/basic_beam.rs
@@ -3,7 +3,10 @@ use crate::{
         Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
         DamageSource, GroupTarget,
     },
-    comp::{beam, body::biped_large, Body, CharacterState, Ori, Pos, StateUpdate},
+    comp::{
+        beam, body::biped_large, character_state::OutputEvents, Body, CharacterState, Ori, Pos,
+        StateUpdate,
+    },
     event::ServerEvent,
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -60,14 +63,14 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         let ori_rate = self.static_data.ori_rate;
 
         handle_orientation(data, &mut update, ori_rate, None);
         handle_move(data, &mut update, 0.4);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Buildup => {
@@ -171,7 +174,7 @@ impl CharacterBehavior for Data {
                     let pos = Pos(data.pos.0 + body_offsets);
 
                     // Create beam segment
-                    update.server_events.push_front(ServerEvent::BeamSegment {
+                    output_events.emit_server(ServerEvent::BeamSegment {
                         properties,
                         pos,
                         ori: beam_ori,
diff --git a/common/src/states/basic_block.rs b/common/src/states/basic_block.rs
index 273f9d8ab3..f0719ba15e 100644
--- a/common/src/states/basic_block.rs
+++ b/common/src/states/basic_block.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, InputKind, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, InputKind, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
@@ -35,7 +35,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
diff --git a/common/src/states/basic_melee.rs b/common/src/states/basic_melee.rs
index 982a93f5d4..314bd4e1f7 100644
--- a/common/src/states/basic_melee.rs
+++ b/common/src/states/basic_melee.rs
@@ -3,7 +3,7 @@ use crate::{
         Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
         DamageSource, GroupTarget, Knockback,
     },
-    comp::{tool::ToolKind, CharacterState, Melee, StateUpdate},
+    comp::{character_state::OutputEvents, tool::ToolKind, CharacterState, Melee, StateUpdate},
     states::{
         behavior::{CharacterBehavior, JoinData},
         utils::*,
@@ -53,12 +53,12 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.7);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Buildup => {
@@ -161,7 +161,7 @@ impl CharacterBehavior for Data {
                 } else {
                     // Done
                     if input_is_pressed(data, self.static_data.ability_info.input) {
-                        reset_state(self, data, &mut update);
+                        reset_state(self, data, output_events, &mut update);
                     } else {
                         update.character = CharacterState::Wielding;
                     }
@@ -184,6 +184,16 @@ impl CharacterBehavior for Data {
     }
 }
 
-fn reset_state(data: &Data, join: &JoinData, update: &mut StateUpdate) {
-    handle_input(join, update, data.static_data.ability_info.input);
+fn reset_state(
+    data: &Data,
+    join: &JoinData,
+    output_events: &mut OutputEvents,
+    update: &mut StateUpdate,
+) {
+    handle_input(
+        join,
+        output_events,
+        update,
+        data.static_data.ability_info.input,
+    );
 }
diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs
index 2b487ba2ef..2e936d74f3 100644
--- a/common/src/states/basic_ranged.rs
+++ b/common/src/states/basic_ranged.rs
@@ -1,5 +1,8 @@
 use crate::{
-    comp::{Body, CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate},
+    comp::{
+        character_state::OutputEvents, Body, CharacterState, LightEmitter, Pos,
+        ProjectileConstructor, StateUpdate,
+    },
     event::ServerEvent,
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -45,12 +48,12 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.3);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Buildup => {
@@ -96,7 +99,7 @@ impl CharacterBehavior for Data {
                         }))
                         .unwrap_or(data.inputs.look_dir);
                         // Tells server to create and shoot the projectile
-                        update.server_events.push_front(ServerEvent::Shoot {
+                        output_events.emit_server(ServerEvent::Shoot {
                             entity: data.entity,
                             pos,
                             dir,
@@ -121,7 +124,7 @@ impl CharacterBehavior for Data {
                 } else {
                     // Done
                     if input_is_pressed(data, self.static_data.ability_info.input) {
-                        reset_state(self, data, &mut update);
+                        reset_state(self, data, output_events, &mut update);
                     } else {
                         update.character = CharacterState::Wielding;
                     }
@@ -142,6 +145,16 @@ impl CharacterBehavior for Data {
     }
 }
 
-fn reset_state(data: &Data, join: &JoinData, update: &mut StateUpdate) {
-    handle_input(join, update, data.static_data.ability_info.input);
+fn reset_state(
+    data: &Data,
+    join: &JoinData,
+    output_events: &mut OutputEvents,
+    update: &mut StateUpdate,
+) {
+    handle_input(
+        join,
+        output_events,
+        update,
+        data.static_data.ability_info.input,
+    );
 }
diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs
index 9ad22acf23..cec7765b68 100644
--- a/common/src/states/basic_summon.rs
+++ b/common/src/states/basic_summon.rs
@@ -1,6 +1,7 @@
 use crate::{
     comp::{
         self,
+        character_state::OutputEvents,
         inventory::loadout_builder::{self, LoadoutBuilder},
         Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate,
     },
@@ -54,7 +55,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         match self.stage_section {
@@ -167,7 +168,7 @@ impl CharacterBehavior for Data {
                         });
 
                         // Send server event to create npc
-                        update.server_events.push_front(ServerEvent::CreateNpc {
+                        output_events.emit_server(ServerEvent::CreateNpc {
                             pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
                             stats,
                             skill_set,
@@ -193,7 +194,7 @@ impl CharacterBehavior for Data {
                         });
 
                         // Send local event used for frontend shenanigans
-                        update.local_events.push_front(LocalEvent::CreateOutcome(
+                        output_events.emit_local(LocalEvent::CreateOutcome(
                             Outcome::SummonedCreature {
                                 pos: data.pos.0,
                                 body,
diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs
index b54ff433e6..d98f1a0b84 100644
--- a/common/src/states/behavior.rs
+++ b/common/src/states/behavior.rs
@@ -1,8 +1,9 @@
 use crate::{
     comp::{
-        self, item::MaterialStatManifest, Beam, Body, CharacterState, Combo, ControlAction,
-        Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory,
-        InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, StateUpdate, Stats, Vel,
+        self, character_state::OutputEvents, item::MaterialStatManifest, Beam, Body,
+        CharacterState, Combo, ControlAction, Controller, ControllerInputs, Density, Energy,
+        Health, InputAttr, InputKind, Inventory, InventoryAction, Mass, Melee, Ori, PhysicsState,
+        Pos, SkillSet, StateUpdate, Stats, Vel,
     },
     resources::DeltaTime,
     terrain::TerrainGrid,
@@ -12,20 +13,47 @@ use specs::{storage::FlaggedAccessMut, Entity, LazyUpdate};
 use vek::*;
 
 pub trait CharacterBehavior {
-    fn behavior(&self, data: &JoinData) -> StateUpdate;
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate;
     // Impl these to provide behavior for these inputs
-    fn swap_equipped_weapons(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn manipulate_loadout(&self, data: &JoinData, _inv_action: InventoryAction) -> StateUpdate {
+    fn swap_equipped_weapons(
+        &self,
+        data: &JoinData,
+        _output_events: &mut OutputEvents,
+    ) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        _output_events: &mut OutputEvents,
+        _inv_action: InventoryAction,
+    ) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn wield(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn glide_wield(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn unwield(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn sit(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn dance(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn sneak(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn stand(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
+        StateUpdate::from(data)
+    }
+    fn talk(&self, data: &JoinData, _output_events: &mut OutputEvents) -> StateUpdate {
         StateUpdate::from(data)
     }
-    fn wield(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn glide_wield(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn unwield(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn sit(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn dance(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn sneak(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn stand(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
-    fn talk(&self, data: &JoinData) -> StateUpdate { StateUpdate::from(data) }
     fn start_input(
         &self,
         data: &JoinData,
@@ -45,18 +73,25 @@ pub trait CharacterBehavior {
         update.removed_inputs.push(input);
         update
     }
-    fn handle_event(&self, data: &JoinData, event: ControlAction) -> StateUpdate {
+    fn handle_event(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        event: ControlAction,
+    ) -> StateUpdate {
         match event {
-            ControlAction::SwapEquippedWeapons => self.swap_equipped_weapons(data),
-            ControlAction::InventoryAction(inv_action) => self.manipulate_loadout(data, inv_action),
-            ControlAction::Wield => self.wield(data),
-            ControlAction::GlideWield => self.glide_wield(data),
-            ControlAction::Unwield => self.unwield(data),
-            ControlAction::Sit => self.sit(data),
-            ControlAction::Dance => self.dance(data),
-            ControlAction::Sneak => self.sneak(data),
-            ControlAction::Stand => self.stand(data),
-            ControlAction::Talk => self.talk(data),
+            ControlAction::SwapEquippedWeapons => self.swap_equipped_weapons(data, output_events),
+            ControlAction::InventoryAction(inv_action) => {
+                self.manipulate_loadout(data, output_events, inv_action)
+            },
+            ControlAction::Wield => self.wield(data, output_events),
+            ControlAction::GlideWield => self.glide_wield(data, output_events),
+            ControlAction::Unwield => self.unwield(data, output_events),
+            ControlAction::Sit => self.sit(data, output_events),
+            ControlAction::Dance => self.dance(data, output_events),
+            ControlAction::Sneak => self.sneak(data, output_events),
+            ControlAction::Stand => self.stand(data, output_events),
+            ControlAction::Talk => self.talk(data, output_events),
             ControlAction::StartInput {
                 input,
                 target_entity,
diff --git a/common/src/states/blink.rs b/common/src/states/blink.rs
index fae3bbef76..24623e0673 100644
--- a/common/src/states/blink.rs
+++ b/common/src/states/blink.rs
@@ -1,5 +1,5 @@
 use crate::{
-    comp::{CharacterState, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, StateUpdate},
     event::ServerEvent,
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -34,7 +34,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
@@ -52,7 +52,7 @@ impl CharacterBehavior for Data {
                     // provided
                     if let Some(input_attr) = self.static_data.ability_info.input_attr {
                         if let Some(target) = input_attr.target_entity {
-                            update.server_events.push_front(ServerEvent::TeleportTo {
+                            output_events.emit_server(ServerEvent::TeleportTo {
                                 entity: data.entity,
                                 target,
                                 max_range: Some(self.static_data.max_range),
diff --git a/common/src/states/boost.rs b/common/src/states/boost.rs
index 9323cc2f3a..4e1ca96fac 100644
--- a/common/src/states/boost.rs
+++ b/common/src/states/boost.rs
@@ -1,5 +1,5 @@
 use crate::{
-    comp::{CharacterState, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, StateUpdate},
     states::{
         behavior::{CharacterBehavior, JoinData},
         utils::*,
@@ -28,7 +28,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_move(data, &mut update, 1.0);
@@ -47,7 +47,7 @@ impl CharacterBehavior for Data {
         } else {
             // Done
             if input_is_pressed(data, self.static_data.ability_info.input) {
-                reset_state(self, data, &mut update);
+                reset_state(self, data, output_events, &mut update);
             } else {
                 update.vel.0 = update.vel.0.try_normalized().unwrap_or_default()
                     * update
@@ -63,6 +63,16 @@ impl CharacterBehavior for Data {
     }
 }
 
-fn reset_state(data: &Data, join: &JoinData, update: &mut StateUpdate) {
-    handle_input(join, update, data.static_data.ability_info.input);
+fn reset_state(
+    data: &Data,
+    join: &JoinData,
+    output_events: &mut OutputEvents,
+    update: &mut StateUpdate,
+) {
+    handle_input(
+        join,
+        output_events,
+        update,
+        data.static_data.ability_info.input,
+    );
 }
diff --git a/common/src/states/charged_melee.rs b/common/src/states/charged_melee.rs
index 5d3ca259e4..e4bc23107f 100644
--- a/common/src/states/charged_melee.rs
+++ b/common/src/states/charged_melee.rs
@@ -1,6 +1,6 @@
 use crate::{
     combat::{Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement},
-    comp::{tool::ToolKind, CharacterState, Melee, StateUpdate},
+    comp::{character_state::OutputEvents, tool::ToolKind, CharacterState, Melee, StateUpdate},
     event::LocalEvent,
     outcome::Outcome,
     states::{
@@ -69,12 +69,12 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.7);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Charge => {
@@ -190,13 +190,11 @@ impl CharacterBehavior for Data {
 
                     if let Some(FrontendSpecifier::GroundCleave) = self.static_data.specifier {
                         // Send local event used for frontend shenanigans
-                        update.local_events.push_front(LocalEvent::CreateOutcome(
-                            Outcome::GroundSlam {
-                                pos: data.pos.0
-                                    + *data.ori.look_dir()
-                                        * (data.body.max_radius() + self.static_data.range),
-                            },
-                        ));
+                        output_events.emit_local(LocalEvent::CreateOutcome(Outcome::GroundSlam {
+                            pos: data.pos.0
+                                + *data.ori.look_dir()
+                                    * (data.body.max_radius() + self.static_data.range),
+                        }));
                     }
                 } else if self.timer < self.static_data.swing_duration {
                     // Swings
diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs
index cbe3dfc482..9f5ca30b82 100644
--- a/common/src/states/charged_ranged.rs
+++ b/common/src/states/charged_ranged.rs
@@ -1,6 +1,7 @@
 use crate::{
     comp::{
-        projectile::ProjectileConstructor, Body, CharacterState, LightEmitter, Pos, StateUpdate,
+        character_state::OutputEvents, projectile::ProjectileConstructor, Body, CharacterState,
+        LightEmitter, Pos, StateUpdate,
     },
     event::ServerEvent,
     states::{
@@ -70,12 +71,12 @@ impl Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, self.static_data.move_speed);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Buildup => {
@@ -118,7 +119,7 @@ impl CharacterBehavior for Data {
                         crit_mult,
                         buff_strength,
                     );
-                    update.server_events.push_front(ServerEvent::Shoot {
+                    output_events.emit_server(ServerEvent::Shoot {
                         entity: data.entity,
                         pos,
                         dir: data.inputs.look_dir,
diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs
index 0ccd3bd106..4857e40242 100644
--- a/common/src/states/climb.rs
+++ b/common/src/states/climb.rs
@@ -1,5 +1,6 @@
 use crate::{
     comp::{
+        character_state::OutputEvents,
         skills::{ClimbSkill::*, Skill, SKILL_MODIFIERS},
         CharacterState, Climb, InputKind, Ori, StateUpdate,
     },
@@ -54,7 +55,7 @@ impl Default for Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         // If no wall is in front of character or we stopped climbing;
@@ -72,7 +73,7 @@ impl CharacterBehavior for Data {
                 // How strong the climb boost is relative to a normal jump
                 const CLIMB_BOOST_JUMP_FACTOR: f32 = 0.5;
                 // They've climbed atop something, give them a boost
-                update.local_events.push_front(LocalEvent::Jump(
+                output_events.emit_local(LocalEvent::Jump(
                     data.entity,
                     CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0,
                 ));
diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs
index df55b12d89..17efb030b6 100644
--- a/common/src/states/combo_melee.rs
+++ b/common/src/states/combo_melee.rs
@@ -1,6 +1,7 @@
 use crate::{
     combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
     comp::{
+        character_state::OutputEvents,
         tool::{Stats, ToolKind},
         CharacterState, Melee, StateUpdate,
     },
@@ -151,7 +152,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_move(data, &mut update, 0.4);
@@ -340,7 +341,7 @@ impl CharacterBehavior for Data {
                 } else {
                     // Done
                     if input_is_pressed(data, self.static_data.ability_info.input) {
-                        reset_state(self, data, &mut update);
+                        reset_state(self, data, output_events, &mut update);
                     } else {
                         update.character = CharacterState::Wielding;
                     }
@@ -363,8 +364,18 @@ impl CharacterBehavior for Data {
     }
 }
 
-fn reset_state(data: &Data, join: &JoinData, update: &mut StateUpdate) {
-    handle_input(join, update, data.static_data.ability_info.input);
+fn reset_state(
+    data: &Data,
+    join: &JoinData,
+    output_events: &mut OutputEvents,
+    update: &mut StateUpdate,
+) {
+    handle_input(
+        join,
+        output_events,
+        update,
+        data.static_data.ability_info.input,
+    );
 
     if let CharacterState::ComboMelee(c) = &mut update.character {
         c.stage = (data.stage % data.static_data.num_stages) + 1;
diff --git a/common/src/states/dance.rs b/common/src/states/dance.rs
index 8a13b84d10..ccaf6a4c39 100644
--- a/common/src/states/dance.rs
+++ b/common/src/states/dance.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, InventoryAction, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, InventoryAction, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
@@ -9,11 +9,11 @@ use serde::{Deserialize, Serialize};
 pub struct Data;
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_wield(data, &mut update);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         // Try to Fall/Stand up/Move
         if data.physics.on_ground.is_none() || data.inputs.move_dir.magnitude_squared() > 0.0 {
@@ -23,25 +23,30 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn wield(&self, data: &JoinData) -> StateUpdate {
+    fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_wield(data, &mut update);
         update
     }
 
-    fn sit(&self, data: &JoinData) -> StateUpdate {
+    fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sit(data, &mut update);
         update
     }
 
-    fn stand(&self, data: &JoinData) -> StateUpdate {
+    fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         // Try to Fall/Stand up/Move
         update.character = CharacterState::Idle;
diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs
index efe2ddcfdc..d009305cb8 100644
--- a/common/src/states/dash_melee.rs
+++ b/common/src/states/dash_melee.rs
@@ -1,6 +1,6 @@
 use crate::{
     combat::{Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement},
-    comp::{tool::ToolKind, CharacterState, Melee, StateUpdate},
+    comp::{character_state::OutputEvents, tool::ToolKind, CharacterState, Melee, StateUpdate},
     states::{
         behavior::{CharacterBehavior, JoinData},
         utils::*,
@@ -74,7 +74,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_move(data, &mut update, 0.1);
diff --git a/common/src/states/equipping.rs b/common/src/states/equipping.rs
index 609acf48ae..78061755c6 100644
--- a/common/src/states/equipping.rs
+++ b/common/src/states/equipping.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
@@ -23,12 +23,12 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 1.0);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         if self.timer < self.static_data.buildup_duration {
             // Draw weapon
diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs
index 399110dc3c..1c66e519cf 100644
--- a/common/src/states/glide.rs
+++ b/common/src/states/glide.rs
@@ -1,8 +1,8 @@
 use super::utils::handle_climb;
 use crate::{
     comp::{
-        fluid_dynamics::angle_of_attack, inventory::slot::EquipSlot, CharacterState, Ori,
-        StateUpdate, Vel,
+        character_state::OutputEvents, fluid_dynamics::angle_of_attack, inventory::slot::EquipSlot,
+        CharacterState, Ori, StateUpdate, Vel,
     },
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -73,7 +73,7 @@ impl Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         // If player is on ground, end glide
@@ -199,7 +199,7 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn unwield(&self, data: &JoinData) -> StateUpdate {
+    fn unwield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         update.character = CharacterState::Idle;
         update
diff --git a/common/src/states/glide_wield.rs b/common/src/states/glide_wield.rs
index 88da23275b..961c9bb2aa 100644
--- a/common/src/states/glide_wield.rs
+++ b/common/src/states/glide_wield.rs
@@ -1,6 +1,9 @@
 use super::utils::*;
 use crate::{
-    comp::{slot::EquipSlot, CharacterState, InventoryAction, Ori, StateUpdate},
+    comp::{
+        character_state::OutputEvents, slot::EquipSlot, CharacterState, InventoryAction, Ori,
+        StateUpdate,
+    },
     states::{
         behavior::{CharacterBehavior, JoinData},
         glide,
@@ -32,12 +35,12 @@ impl From<&JoinData<'_>> for Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 1.0);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
         handle_dodge_input(data, &mut update);
         handle_wield(data, &mut update);
 
@@ -76,31 +79,36 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn unwield(&self, data: &JoinData) -> StateUpdate {
+    fn unwield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         update.character = CharacterState::Idle;
         update
     }
 
-    fn sit(&self, data: &JoinData) -> StateUpdate {
+    fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sit(data, &mut update);
         update
     }
 
-    fn dance(&self, data: &JoinData) -> StateUpdate {
+    fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_dance(data, &mut update);
         update
     }
 
-    fn sneak(&self, data: &JoinData) -> StateUpdate {
+    fn sneak(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sneak(data, &mut update);
         update
diff --git a/common/src/states/idle.rs b/common/src/states/idle.rs
index d1f02390b1..43b95c996c 100644
--- a/common/src/states/idle.rs
+++ b/common/src/states/idle.rs
@@ -1,18 +1,18 @@
 use super::utils::*;
 use crate::{
-    comp::{InventoryAction, StateUpdate},
+    comp::{character_state::OutputEvents, InventoryAction, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 
 pub struct Data;
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 1.0);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
         handle_wield(data, &mut update);
         handle_climb(data, &mut update);
         handle_dodge_input(data, &mut update);
@@ -20,49 +20,54 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn swap_equipped_weapons(&self, data: &JoinData) -> StateUpdate {
+    fn swap_equipped_weapons(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_swap_equipped_weapons(data, &mut update);
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn wield(&self, data: &JoinData) -> StateUpdate {
+    fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_wield(data, &mut update);
         update
     }
 
-    fn glide_wield(&self, data: &JoinData) -> StateUpdate {
+    fn glide_wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_glide_wield(data, &mut update);
         update
     }
 
-    fn sit(&self, data: &JoinData) -> StateUpdate {
+    fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sit(data, &mut update);
         update
     }
 
-    fn dance(&self, data: &JoinData) -> StateUpdate {
+    fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_dance(data, &mut update);
         update
     }
 
-    fn sneak(&self, data: &JoinData) -> StateUpdate {
+    fn sneak(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sneak(data, &mut update);
         update
     }
 
-    fn talk(&self, data: &JoinData) -> StateUpdate {
+    fn talk(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_talk(data, &mut update);
         update
diff --git a/common/src/states/leap_melee.rs b/common/src/states/leap_melee.rs
index bc9f09be6b..2c0b773cb2 100644
--- a/common/src/states/leap_melee.rs
+++ b/common/src/states/leap_melee.rs
@@ -1,6 +1,6 @@
 use crate::{
     combat::{Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement},
-    comp::{tool::ToolKind, CharacterState, Melee, StateUpdate},
+    comp::{character_state::OutputEvents, tool::ToolKind, CharacterState, Melee, StateUpdate},
     states::{
         behavior::{CharacterBehavior, JoinData},
         utils::{StageSection, *},
@@ -57,12 +57,12 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.3);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             // Delay before leaping into the air
diff --git a/common/src/states/repeater_ranged.rs b/common/src/states/repeater_ranged.rs
index 8e498d3ecb..180599a00f 100644
--- a/common/src/states/repeater_ranged.rs
+++ b/common/src/states/repeater_ranged.rs
@@ -1,5 +1,8 @@
 use crate::{
-    comp::{Body, CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate},
+    comp::{
+        character_state::OutputEvents, Body, CharacterState, LightEmitter, Pos,
+        ProjectileConstructor, StateUpdate,
+    },
     event::ServerEvent,
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -49,7 +52,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.3);
@@ -97,7 +100,7 @@ impl CharacterBehavior for Data {
                         crit_mult,
                         buff_strength,
                     );
-                    update.server_events.push_front(ServerEvent::Shoot {
+                    output_events.emit_server(ServerEvent::Shoot {
                         entity: data.entity,
                         pos,
                         dir: data.inputs.look_dir,
@@ -109,7 +112,7 @@ impl CharacterBehavior for Data {
                     });
 
                     // Removes energy from character when arrow is fired
-                    update.server_events.push_front(ServerEvent::EnergyChange {
+                    output_events.emit_server(ServerEvent::EnergyChange {
                         entity: data.entity,
                         change: -self.static_data.energy_cost,
                     });
diff --git a/common/src/states/roll.rs b/common/src/states/roll.rs
index d91e0526f9..d87d599a41 100644
--- a/common/src/states/roll.rs
+++ b/common/src/states/roll.rs
@@ -1,6 +1,7 @@
 use crate::{
     comp::{
         buff::{BuffChange, BuffKind},
+        character_state::OutputEvents,
         CharacterState, InputKind, StateUpdate,
     },
     event::ServerEvent,
@@ -47,7 +48,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         // You should not be able to strafe while rolling
@@ -67,7 +68,7 @@ impl CharacterBehavior for Data {
                     });
                 } else {
                     // Remove burning effect if active
-                    update.server_events.push_front(ServerEvent::Buff {
+                    output_events.emit_server(ServerEvent::Buff {
                         entity: data.entity,
                         buff_change: BuffChange::RemoveByKind(BuffKind::Burning),
                     });
@@ -109,7 +110,7 @@ impl CharacterBehavior for Data {
                 handle_move(data, &mut update, 1.0);
                 // Allows for jumps to interrupt recovery in roll
                 if self.timer < self.static_data.recover_duration
-                    && !handle_jump(data, &mut update, 1.5)
+                    && !handle_jump(data, output_events, &mut update, 1.5)
                 {
                     // Recover
                     update.character = CharacterState::Roll(Data {
@@ -120,7 +121,7 @@ impl CharacterBehavior for Data {
                     // Done
                     if let Some((input, stage)) = self.was_combo {
                         if input_is_pressed(data, input) {
-                            handle_input(data, &mut update, input);
+                            handle_input(data, output_events, &mut update, input);
                             // If other states are introduced that progress through stages, add them
                             // here
                             if let CharacterState::ComboMelee(c) = &mut update.character {
diff --git a/common/src/states/self_buff.rs b/common/src/states/self_buff.rs
index 6f60e283d7..b8a3bd1d4a 100644
--- a/common/src/states/self_buff.rs
+++ b/common/src/states/self_buff.rs
@@ -1,6 +1,7 @@
 use crate::{
     comp::{
         buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource},
+        character_state::OutputEvents,
         CharacterState, StateUpdate,
     },
     event::ServerEvent,
@@ -43,11 +44,11 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_move(data, &mut update, 0.8);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         match self.stage_section {
             StageSection::Buildup => {
@@ -68,7 +69,7 @@ impl CharacterBehavior for Data {
                         Vec::new(),
                         BuffSource::Character { by: *data.uid },
                     );
-                    update.server_events.push_front(ServerEvent::Buff {
+                    output_events.emit_server(ServerEvent::Buff {
                         entity: data.entity,
                         buff_change: BuffChange::Add(buff),
                     });
diff --git a/common/src/states/shockwave.rs b/common/src/states/shockwave.rs
index 70d3eee546..d7ba8a66b2 100644
--- a/common/src/states/shockwave.rs
+++ b/common/src/states/shockwave.rs
@@ -3,7 +3,7 @@ use crate::{
         Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
         DamageSource, GroupTarget, Knockback,
     },
-    comp::{shockwave, CharacterState, StateUpdate},
+    comp::{character_state::OutputEvents, shockwave, CharacterState, StateUpdate},
     event::ServerEvent,
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -62,7 +62,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
@@ -117,7 +117,7 @@ impl CharacterBehavior for Data {
                         owner: Some(*data.uid),
                         specifier: self.static_data.specifier,
                     };
-                    update.server_events.push_front(ServerEvent::Shockwave {
+                    output_events.emit_server(ServerEvent::Shockwave {
                         properties,
                         pos: *data.pos,
                         ori: *data.ori,
diff --git a/common/src/states/sit.rs b/common/src/states/sit.rs
index afafb3e743..cb8183f196 100644
--- a/common/src/states/sit.rs
+++ b/common/src/states/sit.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, InventoryAction, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, InventoryAction, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
@@ -9,11 +9,11 @@ use serde::{Deserialize, Serialize};
 pub struct Data;
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_wield(data, &mut update);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
 
         // Try to Fall/Stand up/Move
         if data.physics.on_ground.is_none() || data.inputs.move_dir.magnitude_squared() > 0.0 {
@@ -23,25 +23,30 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn wield(&self, data: &JoinData) -> StateUpdate {
+    fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_wield(data, &mut update);
         update
     }
 
-    fn dance(&self, data: &JoinData) -> StateUpdate {
+    fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_dance(data, &mut update);
         update
     }
 
-    fn stand(&self, data: &JoinData) -> StateUpdate {
+    fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         // Try to Fall/Stand up/Move
         update.character = CharacterState::Idle;
diff --git a/common/src/states/sneak.rs b/common/src/states/sneak.rs
index 537c7a9836..d92f40f9fa 100644
--- a/common/src/states/sneak.rs
+++ b/common/src/states/sneak.rs
@@ -1,18 +1,18 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, InventoryAction, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, InventoryAction, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 
 pub struct Data;
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 0.4);
-        handle_jump(data, &mut update, 1.0);
+        handle_jump(data, output_events, &mut update, 1.0);
         handle_wield(data, &mut update);
         handle_climb(data, &mut update);
         handle_dodge_input(data, &mut update);
@@ -25,43 +25,48 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn swap_equipped_weapons(&self, data: &JoinData) -> StateUpdate {
+    fn swap_equipped_weapons(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_swap_equipped_weapons(data, &mut update);
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn wield(&self, data: &JoinData) -> StateUpdate {
+    fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_wield(data, &mut update);
         update
     }
 
-    fn glide_wield(&self, data: &JoinData) -> StateUpdate {
+    fn glide_wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_glide_wield(data, &mut update);
         update
     }
 
-    fn sit(&self, data: &JoinData) -> StateUpdate {
+    fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sit(data, &mut update);
         update
     }
 
-    fn dance(&self, data: &JoinData) -> StateUpdate {
+    fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_dance(data, &mut update);
         update
     }
 
-    fn stand(&self, data: &JoinData) -> StateUpdate {
+    fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         update.character = CharacterState::Idle;
         update
diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs
index 5baf929409..5ec0e2c88d 100644
--- a/common/src/states/spin_melee.rs
+++ b/common/src/states/spin_melee.rs
@@ -3,7 +3,7 @@ use crate::{
         Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageKind,
         DamageSource, GroupTarget, Knockback,
     },
-    comp::{tool::ToolKind, CharacterState, Melee, StateUpdate},
+    comp::{character_state::OutputEvents, tool::ToolKind, CharacterState, Melee, StateUpdate},
     consts::GRAVITY,
     states::{
         behavior::{CharacterBehavior, JoinData},
@@ -71,7 +71,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         match self.static_data.movement_behavior {
diff --git a/common/src/states/sprite_interact.rs b/common/src/states/sprite_interact.rs
index e34b34cb04..e825d519b8 100644
--- a/common/src/states/sprite_interact.rs
+++ b/common/src/states/sprite_interact.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, InventoryManip, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, InventoryManip, StateUpdate},
     event::ServerEvent,
     states::behavior::{CharacterBehavior, JoinData},
     terrain::SpriteKind,
@@ -41,7 +41,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         let ori_dir = Dir::from_unnormalized(Vec3::from(
@@ -93,9 +93,7 @@ impl CharacterBehavior for Data {
                 } else {
                     // Create inventory manipulation event
                     let inv_manip = InventoryManip::Collect(self.static_data.sprite_pos);
-                    update
-                        .server_events
-                        .push_front(ServerEvent::InventoryManip(data.entity, inv_manip));
+                    output_events.emit_server(ServerEvent::InventoryManip(data.entity, inv_manip));
                     // Done
                     if self.static_data.was_wielded {
                         update.character = CharacterState::Wielding;
diff --git a/common/src/states/sprite_summon.rs b/common/src/states/sprite_summon.rs
index df354f1013..2a81fe8f44 100644
--- a/common/src/states/sprite_summon.rs
+++ b/common/src/states/sprite_summon.rs
@@ -1,5 +1,5 @@
 use crate::{
-    comp::{CharacterState, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, StateUpdate},
     event::ServerEvent,
     spiral::Spiral2d,
     states::{
@@ -47,7 +47,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         match self.stage_section {
@@ -119,7 +119,7 @@ impl CharacterBehavior for Data {
                                     Vec3::new(sprite_pos.x as i32, sprite_pos.y as i32, z);
 
                                 // Send server event to create sprite
-                                update.server_events.push_front(ServerEvent::CreateSprite {
+                                output_events.emit_server(ServerEvent::CreateSprite {
                                     pos: sprite_pos,
                                     sprite: self.static_data.sprite,
                                 });
diff --git a/common/src/states/stunned.rs b/common/src/states/stunned.rs
index 97bd5e7937..0641ce0ad4 100644
--- a/common/src/states/stunned.rs
+++ b/common/src/states/stunned.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, PoiseState, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, PoiseState, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
@@ -33,7 +33,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
diff --git a/common/src/states/talk.rs b/common/src/states/talk.rs
index 59abfc8e69..b069e2dfd8 100644
--- a/common/src/states/talk.rs
+++ b/common/src/states/talk.rs
@@ -1,6 +1,6 @@
 use super::utils::*;
 use crate::{
-    comp::{CharacterState, InventoryAction, StateUpdate},
+    comp::{character_state::OutputEvents, CharacterState, InventoryAction, StateUpdate},
     states::behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
@@ -11,7 +11,7 @@ const TURN_RATE: f32 = 40.0;
 pub struct Data;
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_wield(data, &mut update);
@@ -20,33 +20,38 @@ impl CharacterBehavior for Data {
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn wield(&self, data: &JoinData) -> StateUpdate {
+    fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_wield(data, &mut update);
         update
     }
 
-    fn sit(&self, data: &JoinData) -> StateUpdate {
+    fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         update.character = CharacterState::Idle;
         attempt_sit(data, &mut update);
         update
     }
 
-    fn dance(&self, data: &JoinData) -> StateUpdate {
+    fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         update.character = CharacterState::Idle;
         attempt_dance(data, &mut update);
         update
     }
 
-    fn stand(&self, data: &JoinData) -> StateUpdate {
+    fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         // Try to Fall/Stand up/Move
         update.character = CharacterState::Idle;
diff --git a/common/src/states/use_item.rs b/common/src/states/use_item.rs
index 10d490eec2..db56d7f6a5 100644
--- a/common/src/states/use_item.rs
+++ b/common/src/states/use_item.rs
@@ -2,6 +2,7 @@ use super::utils::*;
 use crate::{
     comp::{
         buff::{BuffChange, BuffKind},
+        character_state::OutputEvents,
         inventory::{
             item::{ConsumableKind, ItemKind},
             slot::{InvSlotId, Slot},
@@ -47,7 +48,7 @@ pub struct Data {
 }
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         match self.static_data.item_kind {
@@ -86,7 +87,7 @@ impl CharacterBehavior for Data {
                     });
                     if let UsePoint::BuildupUse = use_point {
                         // Create inventory manipulation event
-                        use_item(data, &mut update, self);
+                        use_item(data, output_events, self);
                     }
                 }
             },
@@ -107,7 +108,7 @@ impl CharacterBehavior for Data {
                     });
                     if let UsePoint::UseRecover = use_point {
                         // Create inventory manipulation event
-                        use_item(data, &mut update, self);
+                        use_item(data, output_events, self);
                     }
                 }
             },
@@ -141,11 +142,11 @@ impl CharacterBehavior for Data {
 
         if matches!(update.character, CharacterState::Roll(_)) {
             // Remove potion/saturation effect if left the use item state early by rolling
-            update.server_events.push_front(ServerEvent::Buff {
+            output_events.emit_server(ServerEvent::Buff {
                 entity: data.entity,
                 buff_change: BuffChange::RemoveByKind(BuffKind::Potion),
             });
-            update.server_events.push_front(ServerEvent::Buff {
+            output_events.emit_server(ServerEvent::Buff {
                 entity: data.entity,
                 buff_change: BuffChange::RemoveByKind(BuffKind::Saturation),
             });
@@ -201,7 +202,7 @@ enum UsePoint {
     UseRecover,
 }
 
-fn use_item(data: &JoinData, update: &mut StateUpdate, state: &Data) {
+fn use_item(data: &JoinData, output_events: &mut OutputEvents, state: &Data) {
     // Check if the same item is in the slot
     let item_is_same = data
         .inventory
@@ -212,8 +213,6 @@ fn use_item(data: &JoinData, update: &mut StateUpdate, state: &Data) {
     if item_is_same {
         // Create inventory manipulation event
         let inv_manip = InventoryManip::Use(Slot::Inventory(state.static_data.inv_slot));
-        update
-            .server_events
-            .push_front(ServerEvent::InventoryManip(data.entity, inv_manip));
+        output_events.emit_server(ServerEvent::InventoryManip(data.entity, inv_manip));
     }
 }
diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs
index 0a4f599120..5e35f043a5 100644
--- a/common/src/states/utils.rs
+++ b/common/src/states/utils.rs
@@ -3,6 +3,7 @@ use crate::{
     combat,
     comp::{
         biped_large, biped_small,
+        character_state::OutputEvents,
         inventory::slot::{EquipSlot, Slot},
         item::{Hands, ItemKind, Tool, ToolKind},
         quadruped_low, quadruped_medium, quadruped_small,
@@ -632,6 +633,7 @@ pub fn attempt_swap_equipped_weapons(data: &JoinData<'_>, update: &mut StateUpda
 /// Handles inventory manipulations that affect the loadout
 pub fn handle_manipulate_loadout(
     data: &JoinData<'_>,
+    output_events: &mut OutputEvents,
     update: &mut StateUpdate,
     inv_action: InventoryAction,
 ) {
@@ -664,9 +666,8 @@ pub fn handle_manipulate_loadout(
                 });
             } else {
                 // Else emit inventory action instantnaneously
-                update
-                    .server_events
-                    .push_front(ServerEvent::InventoryManip(data.entity, inv_action.into()));
+                output_events
+                    .emit_server(ServerEvent::InventoryManip(data.entity, inv_action.into()));
             }
         },
         InventoryAction::Collect(sprite_pos) => {
@@ -773,9 +774,7 @@ pub fn handle_manipulate_loadout(
         },
         _ => {
             // Else just do event instantaneously
-            update
-                .server_events
-                .push_front(ServerEvent::InventoryManip(data.entity, inv_action.into()));
+            output_events.emit_server(ServerEvent::InventoryManip(data.entity, inv_action.into()));
         },
     }
 }
@@ -798,12 +797,18 @@ pub fn attempt_glide_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
 }
 
 /// Checks that player can jump and sends jump event if so
-pub fn handle_jump(data: &JoinData<'_>, update: &mut StateUpdate, strength: f32) -> bool {
+pub fn handle_jump(
+    data: &JoinData<'_>,
+    output_events: &mut OutputEvents,
+    // TODO: remove?
+    _update: &mut StateUpdate,
+    strength: f32,
+) -> bool {
     (input_is_pressed(data, InputKind::Jump) && data.physics.on_ground.is_some())
         .then(|| data.body.jump_impulse())
         .flatten()
         .map(|impulse| {
-            update.local_events.push_front(LocalEvent::Jump(
+            output_events.emit_local(LocalEvent::Jump(
                 data.entity,
                 strength * impulse / data.mass.0 * data.stats.move_speed_modifier,
             ));
@@ -881,24 +886,33 @@ pub fn handle_ability_input(data: &JoinData<'_>, update: &mut StateUpdate) {
     }
 }
 
-pub fn handle_input(data: &JoinData<'_>, update: &mut StateUpdate, input: InputKind) {
+pub fn handle_input(
+    data: &JoinData<'_>,
+    output_events: &mut OutputEvents,
+    update: &mut StateUpdate,
+    input: InputKind,
+) {
     match input {
         InputKind::Primary | InputKind::Secondary | InputKind::Ability(_) => {
             handle_ability(data, update, input)
         },
         InputKind::Roll => handle_dodge_input(data, update),
         InputKind::Jump => {
-            handle_jump(data, update, 1.0);
+            handle_jump(data, output_events, update, 1.0);
         },
         InputKind::Block => handle_block_input(data, update),
         InputKind::Fly => {},
     }
 }
 
-pub fn attempt_input(data: &JoinData<'_>, update: &mut StateUpdate) {
+pub fn attempt_input(
+    data: &JoinData<'_>,
+    output_events: &mut OutputEvents,
+    update: &mut StateUpdate,
+) {
     // TODO: look into using first() when it becomes stable
     if let Some(input) = data.controller.queued_inputs.keys().next() {
-        handle_input(data, update, *input);
+        handle_input(data, output_events, update, *input);
     }
 }
 
diff --git a/common/src/states/wielding.rs b/common/src/states/wielding.rs
index 68c90aa4fd..7c7fd20993 100644
--- a/common/src/states/wielding.rs
+++ b/common/src/states/wielding.rs
@@ -1,6 +1,7 @@
 use super::utils::*;
 use crate::{
     comp::{
+        character_state::OutputEvents,
         slot::{EquipSlot, Slot},
         CharacterState, InventoryAction, StateUpdate,
     },
@@ -10,24 +11,29 @@ use crate::{
 pub struct Data;
 
 impl CharacterBehavior for Data {
-    fn behavior(&self, data: &JoinData) -> StateUpdate {
+    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
 
         handle_orientation(data, &mut update, 1.0, None);
         handle_move(data, &mut update, 1.0);
         handle_climb(data, &mut update);
-        attempt_input(data, &mut update);
+        attempt_input(data, output_events, &mut update);
 
         update
     }
 
-    fn swap_equipped_weapons(&self, data: &JoinData) -> StateUpdate {
+    fn swap_equipped_weapons(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_swap_equipped_weapons(data, &mut update);
         update
     }
 
-    fn manipulate_loadout(&self, data: &JoinData, inv_action: InventoryAction) -> StateUpdate {
+    fn manipulate_loadout(
+        &self,
+        data: &JoinData,
+        output_events: &mut OutputEvents,
+        inv_action: InventoryAction,
+    ) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         match inv_action {
             InventoryAction::Drop(EquipSlot::ActiveMainhand | EquipSlot::ActiveOffhand)
@@ -40,35 +46,35 @@ impl CharacterBehavior for Data {
             },
             _ => (),
         }
-        handle_manipulate_loadout(data, &mut update, inv_action);
+        handle_manipulate_loadout(data, output_events, &mut update, inv_action);
         update
     }
 
-    fn glide_wield(&self, data: &JoinData) -> StateUpdate {
+    fn glide_wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_glide_wield(data, &mut update);
         update
     }
 
-    fn unwield(&self, data: &JoinData) -> StateUpdate {
+    fn unwield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         update.character = CharacterState::Idle;
         update
     }
 
-    fn sit(&self, data: &JoinData) -> StateUpdate {
+    fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sit(data, &mut update);
         update
     }
 
-    fn dance(&self, data: &JoinData) -> StateUpdate {
+    fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_dance(data, &mut update);
         update
     }
 
-    fn sneak(&self, data: &JoinData) -> StateUpdate {
+    fn sneak(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate {
         let mut update = StateUpdate::from(data);
         attempt_sneak(data, &mut update);
         update
diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs
index d3ee92c2ba..2cdc3b602f 100644
--- a/common/systems/src/character_behavior.rs
+++ b/common/systems/src/character_behavior.rs
@@ -5,11 +5,12 @@ use specs::{
 
 use common::{
     comp::{
-        self, inventory::item::MaterialStatManifest, Beam, Body, CharacterState, Combo, Controller,
-        Density, Energy, Health, Inventory, InventoryManip, Mass, Melee, Mounting, Ori,
-        PhysicsState, Poise, PoiseState, Pos, SkillSet, StateUpdate, Stats, Vel,
+        self, character_state::OutputEvents, inventory::item::MaterialStatManifest, Beam, Body,
+        CharacterState, Combo, Controller, Density, Energy, Health, Inventory, InventoryManip,
+        Mass, Melee, Mounting, Ori, PhysicsState, Poise, PoiseState, Pos, SkillSet, StateUpdate,
+        Stats, Vel,
     },
-    event::{Emitter, EventBus, LocalEvent, ServerEvent},
+    event::{EventBus, LocalEvent, ServerEvent},
     outcome::Outcome,
     resources::DeltaTime,
     states::behavior::{JoinData, JoinStruct},
@@ -87,6 +88,10 @@ impl<'a> System<'a> for Sys {
         let mut server_emitter = read_data.server_bus.emitter();
         let mut local_emitter = read_data.local_bus.emitter();
 
+        let mut local_events = Vec::new();
+        let mut server_events = Vec::new();
+        let mut output_events = OutputEvents::new(&mut local_events, &mut server_events);
+
         for (
             entity,
             uid,
@@ -259,13 +264,8 @@ impl<'a> System<'a> for Sys {
                     &read_data.dt,
                     &read_data.msm,
                 );
-                let state_update = j.character.handle_event(&j, action);
-                Self::publish_state_update(
-                    &mut join_struct,
-                    state_update,
-                    &mut local_emitter,
-                    &mut server_emitter,
-                );
+                let state_update = j.character.handle_event(&j, &mut output_events, action);
+                Self::publish_state_update(&mut join_struct, state_update, &mut output_events);
             }
 
             // Mounted occurs after control actions have been handled
@@ -285,14 +285,12 @@ impl<'a> System<'a> for Sys {
                 &read_data.msm,
             );
 
-            let state_update = j.character.behavior(&j);
-            Self::publish_state_update(
-                &mut join_struct,
-                state_update,
-                &mut local_emitter,
-                &mut server_emitter,
-            );
+            let state_update = j.character.behavior(&j, &mut output_events);
+            Self::publish_state_update(&mut join_struct, state_update, &mut output_events);
         }
+
+        local_emitter.append_vec(local_events);
+        server_emitter.append_vec(server_events);
     }
 }
 
@@ -300,12 +298,8 @@ impl Sys {
     fn publish_state_update(
         join: &mut JoinStruct,
         mut state_update: StateUpdate,
-        local_emitter: &mut Emitter<LocalEvent>,
-        server_emitter: &mut Emitter<ServerEvent>,
+        output_events: &mut OutputEvents,
     ) {
-        local_emitter.append(&mut state_update.local_events);
-        server_emitter.append(&mut state_update.server_events);
-
         // TODO: if checking equality is expensive use optional field in StateUpdate
         if *join.char_state != state_update.character {
             *join.char_state = state_update.character
@@ -322,7 +316,7 @@ impl Sys {
             join.controller.queued_inputs.remove(&input);
         }
         if state_update.swap_equipped_weapons {
-            server_emitter.emit(ServerEvent::InventoryManip(
+            output_events.emit_server(ServerEvent::InventoryManip(
                 join.entity,
                 InventoryManip::SwapEquippedWeapons,
             ));

From 54ed63e359510ba995bfe5c44346d6b324707666 Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Fri, 15 Oct 2021 00:57:20 -0400
Subject: [PATCH 5/8] Add alias for running the server with tracy with the
 releasedebuginfo profile, this is potentially useful for some of Tracy's
 source view features

---
 .cargo/config | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.cargo/config b/.cargo/config
index 686f74a4b0..f85f7ca37c 100644
--- a/.cargo/config
+++ b/.cargo/config
@@ -10,6 +10,7 @@ csv-import = "run --manifest-path common/Cargo.toml --features=bin_csv --bin csv
 test-server = "run --bin veloren-server-cli --no-default-features"
 tracy-server = "-Zunstable-options run --bin veloren-server-cli --no-default-features --features tracy,simd --profile no_overflow"
 tracy-world-server = "-Zunstable-options run --bin veloren-server-cli --features tracy,simd --profile no_overflow"
+tracy-world-server-releasedebuginfo = "-Zunstable-options run --bin veloren-server-cli --features tracy,simd --profile releasedebuginfo"
 test-voxygen = "run --bin veloren-voxygen --no-default-features --features simd,egui-ui"
 tracy-voxygen = "-Zunstable-options run --bin veloren-voxygen --no-default-features --features tracy,simd,egui-ui --profile no_overflow"
 server = "run --bin veloren-server-cli"

From 0b115af398b303ea0ea3555e80fcfb761a146083 Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Fri, 15 Oct 2021 01:03:37 -0400
Subject: [PATCH 6/8] Improve Ori impl of From<Dir> and Ori::to_horizontal by
 avoiding acos/asin calls by constructing the quaternions in a more direct
 fashion

---
 common/src/comp/ori.rs | 102 +++++++++++++++++++++++++++++++++++------
 1 file changed, 88 insertions(+), 14 deletions(-)

diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs
index ab9ba00fba..0ff078e40d 100644
--- a/common/src/comp/ori.rs
+++ b/common/src/comp/ori.rs
@@ -2,7 +2,7 @@ use crate::util::{Dir, Plane, Projection};
 use serde::{Deserialize, Serialize};
 use specs::Component;
 use specs_idvs::IdvStorage;
-use std::f32::consts::PI;
+use std::f32::consts::{FRAC_PI_2, PI};
 use vek::{Quaternion, Vec2, Vec3};
 
 // Orientation
@@ -150,24 +150,29 @@ impl Ori {
 
     pub fn to_horizontal(self) -> Self {
         // We don't use Self::look_dir to avoid the extra normalization step within
-        // Dir's Quaternion Mul impl (since we will normalize later below)
+        // Dir's Quaternion Mul impl
         let fw = self.to_quat() * Dir::default().to_vec();
         // Check that dir is not straight up/down
         // Uses a multiple of EPSILON to be safe
         // We can just check z since beyond floating point errors `fw` should be
         // normalized
-        let xy = if 1.0 - fw.z.abs() > f32::EPSILON * 4.0 {
-            fw.xy().normalized()
-        } else {
-            // if look_dir is straight down, pitch up, or if straight up, pitch down
-            // xy should essentially be normalized so no need to normalize
-            if fw.z < 0.0 { self.up() } else { self.down() }.xy()
-        };
-        // We know direction lies in the xy plane so we only need to compute a rotation
-        // about the z-axis
-        let yaw = xy.y.acos() * fw.x.signum() * -1.0;
+        if 1.0 - fw.z.abs() > f32::EPSILON * 4.0 {
+            // We know direction lies in the xy plane so we only need to compute a rotation
+            // about the z-axis
+            let Vec2 { x, y } = fw.xy().normalized();
+            // Negate x and swap coords since we want to compute the angle from y+
+            let quat = rotation_2d(Vec2::new(y, -x), Vec3::unit_z());
 
-        Self(Quaternion::rotation_z(yaw))
+            Self(quat)
+        } else {
+            // TODO: optimize this more (see asm)
+            // if the direction is straight down, pitch up, or if straight up, pitch down
+            if fw.z < 0.0 {
+                self.pitched_up(FRAC_PI_2)
+            } else {
+                self.pitched_down(FRAC_PI_2)
+            }
+        }
     }
 
     /// Find the angle between two `Ori`s
@@ -256,19 +261,67 @@ impl Ori {
     fn is_normalized(&self) -> bool { self.0.into_vec4().is_normalized() }
 }
 
+/// Produce a quaternion from an axis to rotate about and a 2D point on the unit
+/// circle to rotate to
+///
+/// NOTE: the provided axis and 2D vector must be normalized
+fn rotation_2d(Vec2 { x, y }: Vec2<f32>, axis: Vec3<f32>) -> Quaternion<f32> {
+    // Skip needing the angle for quaternion construction by computing cos/sin
+    // directly from the normalized x value
+    //
+    // scalar = cos(theta / 2)
+    // vector = axis * sin(theta / 2)
+    //
+    // cos(a / 2) = +/- ((1 + cos(a)) / 2)^0.5
+    // sin(a / 2) = +/- ((1 - cos(a)) / 2)^0.5
+    //
+    // scalar = +/- sqrt((1 + cos(a)) / 2)
+    // vector = vec3(0, 0, 1) * +/- sqrt((1 - cos(a)) / 2)
+    //
+    // cos(a) = x / |xy| => x (when normalized)
+    let scalar = ((1.0 + x) / 2.0).sqrt() * y.signum();
+    let vector = axis * ((1.0 - x) / 2.0).sqrt();
+
+    // This is normalized by our construction above
+    Quaternion::from_scalar_and_vec3((scalar, vector))
+}
+
 impl From<Dir> for Ori {
     fn from(dir: Dir) -> Self {
         // Check that dir is not straight up/down
         // Uses a multiple of EPSILON to be safe
         let quat = if 1.0 - dir.z.abs() > f32::EPSILON * 4.0 {
+            // handle_orientation: mean: 168, median: 121
+            // move_dir(no subspans): mean: 74, median: 42
+            // move_dir: mean: 226, median: 197
+            // mean: 105, median: 90
             // Compute rotation that will give an "upright" orientation (no rolling):
-
+            /*
             // Rotation to get to this projected point from the default direction of y+
             let yaw = dir.xy().normalized().y.acos() * dir.x.signum() * -1.0;
             // Rotation to then rotate up/down to the match the input direction
             let pitch = dir.z.asin();
 
             (Quaternion::rotation_z(yaw) * Quaternion::rotation_x(pitch)).normalized()
+
+            // handle_orientation: mean: 167, median: 151
+            // move_dir(no subspans): mean: 83, median: 83
+            // move_dir: mean: 209, median: 186
+            // mean: 60, median: 46
+            // Compute rotation that will give an "upright" orientation (no
+            // rolling):
+            */
+            let xy_len = dir.xy().magnitude();
+            let xy_norm = dir.xy() / xy_len;
+            // Rotation to get to this projected point from the default direction of y+
+            // Negate x and swap coords since we want to compute the angle from y+
+            let yaw = rotation_2d(Vec2::new(xy_norm.y, -xy_norm.x), Vec3::unit_z());
+            // Rotation to then rotate up/down to the match the input direction
+            // In this rotated space the xy_len becomes the distance along the x axis
+            // And since we rotated around the z-axis the z value is unchanged
+            let pitch = rotation_2d(Vec2::new(xy_len, dir.z), Vec3::unit_x());
+
+            (yaw * pitch).normalized()
         } else {
             // Nothing in particular can be considered upright if facing up or down
             // so we just produce a quaternion that will rotate to that direction
@@ -397,6 +450,27 @@ mod tests {
 
             approx::assert_relative_eq!(horizontal.look_dir().xy().magnitude(), 1.0);
             approx::assert_relative_eq!(horizontal.look_dir().z, 0.0);
+            // Check correctness by comparing with Dir::to_horizontal
+            if let Some(dir_h) = ori.look_dir().to_horizontal() {
+                let quat_correct = Quaternion::<f32>::rotation_from_to_3d(Dir::default(), dir_h);
+                #[rustfmt::skip]
+                assert!(
+                    dir_h
+                        .map2(*horizontal.look_dir(), |d, o| approx::relative_eq!(d, o, epsilon = f32::EPSILON * 4.0))
+                        .reduce_and(),
+                    "\n\
+                    Original: {:?}\n\
+                    Dir::to_horizontal: {:?}\n\
+                    Ori::to_horizontal(as dir): {:?}\n\
+                    Ori::to_horizontal(as quat): {:?}\n\
+                    Correct quaternion {:?}",
+                    ori.look_dir(),
+                    dir_h,
+                    horizontal.look_dir(),
+                    horizontal,
+                    quat_correct,
+                );
+            }
         };
 
         dirs().for_each(to_horizontal);

From 4ea6842932f599a8f116f406724e16290d965828 Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Fri, 15 Oct 2021 01:06:41 -0400
Subject: [PATCH 7/8] Remove timings and commented code (separate from the
 previous commit so we can keep these notes in the git history), add
 potentially better version of the straight up/down case for to_horizontal as
 a comment for testing in the future, remove fine grained spans

---
 common/src/comp/character_state.rs       |  1 -
 common/src/comp/ori.rs                   | 32 ++++++++++--------------
 common/src/states/utils.rs               |  1 -
 common/systems/src/character_behavior.rs |  2 --
 4 files changed, 13 insertions(+), 23 deletions(-)

diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs
index 8c312489d9..d34b4624e3 100644
--- a/common/src/comp/character_state.rs
+++ b/common/src/comp/character_state.rs
@@ -50,7 +50,6 @@ impl<'a> OutputEvents<'a> {
 
 impl From<&JoinData<'_>> for StateUpdate {
     fn from(data: &JoinData) -> Self {
-        common_base::prof_span!("StateUpdate::from");
         StateUpdate {
             pos: *data.pos,
             vel: *data.vel,
diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs
index 0ff078e40d..e042ccf695 100644
--- a/common/src/comp/ori.rs
+++ b/common/src/comp/ori.rs
@@ -165,13 +165,25 @@ impl Ori {
 
             Self(quat)
         } else {
-            // TODO: optimize this more (see asm)
             // if the direction is straight down, pitch up, or if straight up, pitch down
             if fw.z < 0.0 {
                 self.pitched_up(FRAC_PI_2)
             } else {
                 self.pitched_down(FRAC_PI_2)
             }
+            // TODO: test this alternative for speed and correctness compared to
+            // current impl
+            //
+            // removes a branch
+            //
+            // use core::f32::consts::FRAC_1_SQRT_2;
+            // let cos = FRAC_1_SQRT_2;
+            // let sin = -FRAC_1_SQRT_2 * fw.z.signum();
+            // let axis = Vec3::unit_x();
+            // let scalar = cos;
+            // let vector = sin * axis;
+            // Self((self.0 * Quaternion::from_scalar_and_vec3((scalar,
+            // vector))).normalized())
         }
     }
 
@@ -291,26 +303,8 @@ impl From<Dir> for Ori {
         // Check that dir is not straight up/down
         // Uses a multiple of EPSILON to be safe
         let quat = if 1.0 - dir.z.abs() > f32::EPSILON * 4.0 {
-            // handle_orientation: mean: 168, median: 121
-            // move_dir(no subspans): mean: 74, median: 42
-            // move_dir: mean: 226, median: 197
-            // mean: 105, median: 90
-            // Compute rotation that will give an "upright" orientation (no rolling):
-            /*
-            // Rotation to get to this projected point from the default direction of y+
-            let yaw = dir.xy().normalized().y.acos() * dir.x.signum() * -1.0;
-            // Rotation to then rotate up/down to the match the input direction
-            let pitch = dir.z.asin();
-
-            (Quaternion::rotation_z(yaw) * Quaternion::rotation_x(pitch)).normalized()
-
-            // handle_orientation: mean: 167, median: 151
-            // move_dir(no subspans): mean: 83, median: 83
-            // move_dir: mean: 209, median: 186
-            // mean: 60, median: 46
             // Compute rotation that will give an "upright" orientation (no
             // rolling):
-            */
             let xy_len = dir.xy().magnitude();
             let xy_norm = dir.xy() / xy_len;
             // Rotation to get to this projected point from the default direction of y+
diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs
index 5e35f043a5..6bfe1a8885 100644
--- a/common/src/states/utils.rs
+++ b/common/src/states/utils.rs
@@ -388,7 +388,6 @@ pub fn handle_orientation(
     efficiency: f32,
     dir_override: Option<Dir>,
 ) {
-    common_base::prof_span!("handle_orientation");
     // Direction is set to the override if one is provided, else if entity is
     // strafing or attacking the horiontal component of the look direction is used,
     // else the current horizontal movement direction is used
diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs
index 2cdc3b602f..7f0db06778 100644
--- a/common/systems/src/character_behavior.rs
+++ b/common/systems/src/character_behavior.rs
@@ -17,7 +17,6 @@ use common::{
     terrain::TerrainGrid,
     uid::Uid,
 };
-use common_base::prof_span;
 use common_ecs::{Job, Origin, Phase, System};
 use std::time::Duration;
 
@@ -129,7 +128,6 @@ impl<'a> System<'a> for Sys {
         )
             .join()
         {
-            prof_span!("entity");
             // Being dead overrides all other states
             if health.map_or(false, |h| h.is_dead) {
                 // Do nothing

From 1ad6b5adb4833c0c459b03f907c5f0f4f820b882 Mon Sep 17 00:00:00 2001
From: Imbris <imbrisf@gmail.com>
Date: Fri, 15 Oct 2021 03:17:30 -0400
Subject: [PATCH 8/8] Fix issue with angle between going over PI and remove
 faulty test (angle between Quaternions isn't equivalent to the angle between
 Dir vectors since Quaternions involve rolling as well)

---
 common/src/comp/ori.rs | 23 +++++++----------------
 1 file changed, 7 insertions(+), 16 deletions(-)

diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs
index e042ccf695..5c04d27c23 100644
--- a/common/src/comp/ori.rs
+++ b/common/src/comp/ori.rs
@@ -1,8 +1,8 @@
 use crate::util::{Dir, Plane, Projection};
+use core::f32::consts::{FRAC_PI_2, PI, TAU};
 use serde::{Deserialize, Serialize};
 use specs::Component;
 use specs_idvs::IdvStorage;
-use std::f32::consts::{FRAC_PI_2, PI};
 use vek::{Quaternion, Vec2, Vec3};
 
 // Orientation
@@ -189,6 +189,10 @@ impl Ori {
 
     /// Find the angle between two `Ori`s
     ///
+    /// NOTE: This finds the angle of the quaternion between the two `Ori`s
+    /// which can involve rolling and thus can be larger than simply the
+    /// angle between vectors at the start and end points.
+    ///
     /// Returns angle in radians
     pub fn angle_between(self, other: Self) -> f32 {
         // Compute quaternion from one ori to the other
@@ -196,7 +200,8 @@ impl Ori {
         let between = self.to_quat().conjugate() * other.to_quat();
         // Then compute it's angle
         // http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/
-        2.0 * between.w.acos()
+        let angle = 2.0 * between.w.acos();
+        if angle < PI { angle } else { TAU - angle }
     }
 
     pub fn pitched_up(self, angle_radians: f32) -> Self {
@@ -470,20 +475,6 @@ mod tests {
         dirs().for_each(to_horizontal);
     }
 
-    #[test]
-    fn angle_between() {
-        let angle_between = |(dir_a, dir_b): (Dir, Dir)| {
-            let ori_a = Ori::from(dir_a);
-            let ori_b = Ori::from(dir_b);
-
-            approx::assert_relative_eq!(ori_a.angle_between(ori_b), dir_a.angle_between(*dir_b));
-        };
-
-        dirs()
-            .flat_map(|dir| dirs().map(move |dir_two| (dir, dir_two)))
-            .for_each(angle_between)
-    }
-
     #[test]
     fn from_to_dir() {
         let from_to = |dir: Dir| {