From 47098554652ca9b5a536134320f22a625cb5f7a3 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 27 Aug 2021 15:52:52 +0300
Subject: [PATCH 01/14] Don't forget about pets in PvP checks

---
 common/src/combat.rs                     |  9 +++----
 common/src/comp/agent.rs                 | 13 ++++++++--
 common/systems/src/aura.rs               | 24 ++++++++++++------
 common/systems/src/beam.rs               | 21 ++++++++++++----
 common/systems/src/melee.rs              | 24 +++++++++++++-----
 common/systems/src/projectile.rs         | 21 ++++++++++++----
 common/systems/src/shockwave.rs          | 22 ++++++++++++----
 server/src/events/entity_manipulation.rs | 32 ++++++++++++++++++------
 8 files changed, 123 insertions(+), 43 deletions(-)

diff --git a/common/src/combat.rs b/common/src/combat.rs
index e8fb6270da..bc73dc91bb 100644
--- a/common/src/combat.rs
+++ b/common/src/combat.rs
@@ -469,11 +469,10 @@ impl Attack {
     }
 }
 
-/// Checks if we should allow negative effects from one player to another
-// FIXME: handle pets?
-// This code works only with players.
-// You still can kill someone's pet and
-// you still can be killed by someone's pet
+/// Says if we should allow negative effects from one player to another
+///
+/// NOTE: this function doesn't handle pets or friendly-fire, you will need to
+/// figure it out on call-side.
 pub fn may_harm(attacker: Option<&Player>, target: Option<&Player>) -> bool {
     attacker
         .zip(target)
diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs
index 72cd5bc641..3760b03544 100644
--- a/common/src/comp/agent.rs
+++ b/common/src/comp/agent.rs
@@ -3,10 +3,10 @@ use crate::{
     path::Chaser,
     rtsim::RtSimController,
     trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
-    uid::Uid,
+    uid::{Uid, UidAllocator},
 };
 use serde::Deserialize;
-use specs::{Component, Entity as EcsEntity};
+use specs::{saveload::MarkerAllocator, Component, Entity as EcsEntity};
 use specs_idvs::IdvStorage;
 use std::{collections::VecDeque, fmt};
 use strum::IntoEnumIterator;
@@ -34,6 +34,15 @@ pub enum Alignment {
     Passive,
 }
 
+// Helper function to get owner
+pub fn owner_of(alignment: Option<Alignment>, uid_allocator: &UidAllocator) -> Option<EcsEntity> {
+    if let Some(Alignment::Owned(uid)) = alignment {
+        uid_allocator.retrieve_entity_internal(uid.into())
+    } else {
+        None
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub enum Mark {
     Merchant,
diff --git a/common/systems/src/aura.rs b/common/systems/src/aura.rs
index f9201b3d8d..31264e647a 100644
--- a/common/systems/src/aura.rs
+++ b/common/systems/src/aura.rs
@@ -1,10 +1,11 @@
 use common::{
     combat,
     comp::{
+        agent::owner_of,
         aura::{AuraChange, AuraKey, AuraKind, AuraTarget},
         buff::{Buff, BuffCategory, BuffChange, BuffSource},
         group::Group,
-        Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos,
+        Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos,
     },
     event::{Emitter, EventBus, ServerEvent},
     resources::DeltaTime,
@@ -27,6 +28,7 @@ pub struct ReadData<'a> {
     cached_spatial_grid: Read<'a, common::CachedSpatialGrid>,
     positions: ReadStorage<'a, Pos>,
     char_states: ReadStorage<'a, CharacterState>,
+    alignments: ReadStorage<'a, Alignment>,
     healths: ReadStorage<'a, Health>,
     groups: ReadStorage<'a, Group>,
     uids: ReadStorage<'a, Uid>,
@@ -174,7 +176,7 @@ fn activate_aura(
             // TODO: this check will disable friendly fire with PvE switch.
             //
             // Which means that you can't apply debuffs on you and your group
-            // even if it's intented mechanics.
+            // even if it's intented mechanic.
             //
             // Not that we have this for now, but think about this
             // when we will add this.
@@ -185,11 +187,19 @@ fn activate_aura(
                     },
                     _ => None,
                 };
-                owner.map_or(true, |attacker| {
-                    let attacker = read_data.players.get(attacker);
-                    let target = read_data.players.get(target);
-                    combat::may_harm(attacker, target)
-                })
+                let owner_if_pet = |entity| {
+                    // Return owner entity if pet,
+                    // or just return entity back otherwise
+                    owner_of(
+                        read_data.alignments.get(entity).copied(),
+                        &read_data.uid_allocator,
+                    )
+                    .unwrap_or(entity)
+                };
+                combat::may_harm(
+                    owner.and_then(|owner| read_data.players.get(owner_if_pet(owner))),
+                    read_data.players.get(owner_if_pet(target)),
+                )
             };
 
             conditions_held && (kind.is_buff() || may_harm())
diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs
index 86854bd814..8b2403c3a4 100644
--- a/common/systems/src/beam.rs
+++ b/common/systems/src/beam.rs
@@ -1,9 +1,9 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{Sound, SoundKind},
-        Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
-        Inventory, Ori, Player, Pos, Scale, Stats,
+        agent::{owner_of, Sound, SoundKind},
+        Alignment, Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health,
+        HealthSource, Inventory, Ori, Player, Pos, Scale, Stats,
     },
     event::{EventBus, ServerEvent},
     outcome::Outcome,
@@ -36,6 +36,7 @@ pub struct ReadData<'a> {
     uids: ReadStorage<'a, Uid>,
     positions: ReadStorage<'a, Pos>,
     orientations: ReadStorage<'a, Ori>,
+    alignments: ReadStorage<'a, Alignment>,
     scales: ReadStorage<'a, Scale>,
     bodies: ReadStorage<'a, Body>,
     healths: ReadStorage<'a, Health>,
@@ -214,9 +215,19 @@ impl<'a> System<'a> for Sys {
                     };
 
 
+                    // PvP check
+                    let owner_if_pet = |entity| {
+                        // Return owner entity if pet,
+                        // or just return entity back otherwise
+                        owner_of(
+                            read_data.alignments.get(entity).copied(),
+                            &read_data.uid_allocator,
+                        )
+                        .unwrap_or(entity)
+                    };
                     let may_harm = combat::may_harm(
-                        beam_owner.and_then(|owner| read_data.players.get(owner)),
-                        read_data.players.get(target),
+                        beam_owner.and_then(|owner| read_data.players.get(owner_if_pet(owner))),
+                        read_data.players.get(owner_if_pet(target)),
                     );
                     let attack_options = AttackOptions {
                         // No luck with dodging beams
diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs
index 433f6b48c5..005efad509 100644
--- a/common/systems/src/melee.rs
+++ b/common/systems/src/melee.rs
@@ -1,14 +1,14 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{Sound, SoundKind},
-        Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Player, Pos,
-        Scale, Stats,
+        agent::{owner_of, Sound, SoundKind},
+        Alignment, Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori,
+        Player, Pos, Scale, Stats,
     },
     event::{EventBus, ServerEvent},
     outcome::Outcome,
     resources::Time,
-    uid::Uid,
+    uid::{Uid, UidAllocator},
     util::Dir,
     GroupTarget,
 };
@@ -21,11 +21,13 @@ use vek::*;
 #[derive(SystemData)]
 pub struct ReadData<'a> {
     time: Read<'a, Time>,
+    uid_allocator: Read<'a, UidAllocator>,
     entities: Entities<'a>,
     players: ReadStorage<'a, Player>,
     uids: ReadStorage<'a, Uid>,
     positions: ReadStorage<'a, Pos>,
     orientations: ReadStorage<'a, Ori>,
+    alignments: ReadStorage<'a, Alignment>,
     scales: ReadStorage<'a, Scale>,
     bodies: ReadStorage<'a, Body>,
     healths: ReadStorage<'a, Health>,
@@ -162,9 +164,19 @@ impl<'a> System<'a> for Sys {
                         char_state: read_data.char_states.get(target),
                     };
 
+                    // PvP check
+                    let owner_if_pet = |entity| {
+                        // Return owner entity if pet,
+                        // or just return entity back otherwise
+                        owner_of(
+                            read_data.alignments.get(entity).copied(),
+                            &read_data.uid_allocator,
+                        )
+                        .unwrap_or(entity)
+                    };
                     let may_harm = combat::may_harm(
-                        read_data.players.get(attacker),
-                        read_data.players.get(target),
+                        read_data.players.get(owner_if_pet(attacker)),
+                        read_data.players.get(owner_if_pet(target)),
                     );
 
                     let attack_options = AttackOptions {
diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs
index 44f7fe66cd..f27ac95466 100644
--- a/common/systems/src/projectile.rs
+++ b/common/systems/src/projectile.rs
@@ -1,9 +1,9 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{Sound, SoundKind},
-        projectile, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
-        Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel,
+        agent::{owner_of, Sound, SoundKind},
+        projectile, Alignment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
+        Inventory, Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel,
     },
     event::{Emitter, EventBus, ServerEvent},
     outcome::Outcome,
@@ -31,6 +31,7 @@ pub struct ReadData<'a> {
     server_bus: Read<'a, EventBus<ServerEvent>>,
     uids: ReadStorage<'a, Uid>,
     positions: ReadStorage<'a, Pos>,
+    alignments: ReadStorage<'a, Alignment>,
     physics_states: ReadStorage<'a, PhysicsState>,
     velocities: ReadStorage<'a, Vel>,
     inventories: ReadStorage<'a, Inventory>,
@@ -299,9 +300,19 @@ fn dispatch_hit(
                 });
             }
 
+            // PvP check
+            let owner_if_pet = |entity| {
+                // Return owner entity if pet,
+                // or just return entity back otherwise
+                owner_of(
+                    read_data.alignments.get(entity).copied(),
+                    &read_data.uid_allocator,
+                )
+                .unwrap_or(entity)
+            };
             let may_harm = combat::may_harm(
-                owner.and_then(|owner| read_data.players.get(owner)),
-                read_data.players.get(target),
+                owner.and_then(|owner| read_data.players.get(owner_if_pet(owner))),
+                read_data.players.get(owner_if_pet(target)),
             );
 
             let attack_options = AttackOptions {
diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs
index 86ad74eca7..75fd5db924 100644
--- a/common/systems/src/shockwave.rs
+++ b/common/systems/src/shockwave.rs
@@ -1,9 +1,9 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{Sound, SoundKind},
-        Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory, Ori,
-        PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
+        agent::{owner_of, Sound, SoundKind},
+        Alignment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
+        Ori, PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
     },
     event::{EventBus, ServerEvent},
     outcome::Outcome,
@@ -31,6 +31,7 @@ pub struct ReadData<'a> {
     uids: ReadStorage<'a, Uid>,
     positions: ReadStorage<'a, Pos>,
     orientations: ReadStorage<'a, Ori>,
+    alignments: ReadStorage<'a, Alignment>,
     scales: ReadStorage<'a, Scale>,
     bodies: ReadStorage<'a, Body>,
     healths: ReadStorage<'a, Health>,
@@ -209,9 +210,20 @@ impl<'a> System<'a> for Sys {
                         char_state: read_data.character_states.get(target),
                     };
 
+                    // PvP check
+                    let owner_if_pet = |entity| {
+                        // Return owner entity if pet,
+                        // or just return entity back otherwise
+                        owner_of(
+                            read_data.alignments.get(entity).copied(),
+                            &read_data.uid_allocator,
+                        )
+                        .unwrap_or(entity)
+                    };
                     let may_harm = combat::may_harm(
-                        shockwave_owner.and_then(|owner| read_data.players.get(owner)),
-                        read_data.players.get(target),
+                        shockwave_owner
+                            .and_then(|owner| read_data.players.get(owner_if_pet(owner))),
+                        read_data.players.get(owner_if_pet(target)),
                     );
                     let attack_options = AttackOptions {
                         // Trying roll during earthquake isn't the best idea
diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs
index 56978c7380..5d81428093 100644
--- a/server/src/events/entity_manipulation.rs
+++ b/server/src/events/entity_manipulation.rs
@@ -1,7 +1,7 @@
 use crate::{
     client::Client,
     comp::{
-        agent::{Agent, AgentEvent, Sound, SoundKind},
+        agent::{owner_of, Agent, AgentEvent, Sound, SoundKind},
         biped_large, bird_large, quadruped_low, quadruped_medium, quadruped_small,
         skills::SkillGroupKind,
         theropod, BuffKind, BuffSource, PhysicsState,
@@ -873,6 +873,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                 let energies = &ecs.read_storage::<comp::Energy>();
                 let combos = &ecs.read_storage::<comp::Combo>();
                 let inventories = &ecs.read_storage::<comp::Inventory>();
+                let alignments = &ecs.read_storage::<Alignment>();
+                let uid_allocator = &ecs.read_resource::<UidAllocator>();
                 let players = &ecs.read_storage::<comp::Player>();
                 for (
                     entity_b,
@@ -942,9 +944,16 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                             char_state: char_state_b_maybe,
                         };
 
+                        // PvP check
+                        let owner_if_pet = |entity| {
+                            // Return owner entity if pet,
+                            // or just return entity back otherwise
+                            owner_of(alignments.get(entity).copied(), uid_allocator)
+                                .unwrap_or(entity)
+                        };
                         let may_harm = combat::may_harm(
-                            owner_entity.and_then(|owner| players.get(owner)),
-                            players.get(entity_b),
+                            owner_entity.and_then(|owner| players.get(owner_if_pet(owner))),
+                            players.get(owner_if_pet(entity_b)),
                         );
                         let attack_options = combat::AttackOptions {
                             // cool guyz maybe don't look at explosions
@@ -968,6 +977,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                 }
             },
             RadiusEffect::Entity(mut effect) => {
+                let alignments = &ecs.read_storage::<Alignment>();
+                let uid_allocator = &ecs.read_resource::<UidAllocator>();
                 let players = &ecs.read_storage::<comp::Player>();
                 for (entity_b, pos_b, body_b_maybe) in (
                     &ecs.entities(),
@@ -994,12 +1005,17 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                     // you want to harm yourself.
                     //
                     // This can be changed later.
+                    // PvP check
+                    let owner_if_pet = |entity| {
+                        // Return owner entity if pet,
+                        // or just return entity back otherwise
+                        owner_of(alignments.get(entity).copied(), uid_allocator).unwrap_or(entity)
+                    };
                     let may_harm = || {
-                        owner_entity.map_or(false, |attacker| {
-                            let attacker_player = players.get(attacker);
-                            let target_player = players.get(entity_b);
-                            combat::may_harm(attacker_player, target_player) || attacker == entity_b
-                        })
+                        combat::may_harm(
+                            owner_entity.and_then(|owner| players.get(owner_if_pet(owner))),
+                            players.get(owner_if_pet(entity_b)),
+                        ) || owner_entity.map_or(true, |entity_a| entity_a == entity_b)
                     };
                     if strength > 0.0 {
                         let is_alive = ecs

From e5e3349fb6978920740d5c5041cc96fb1e688e9c Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 27 Aug 2021 17:08:18 +0300
Subject: [PATCH 02/14] Add PerPlayer server flag for BattleMode

---
 server/src/settings.rs         | 10 ++++++++--
 server/src/sys/msg/register.rs | 10 +++++++++-
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/server/src/settings.rs b/server/src/settings.rs
index 5c17adb5e1..7e45e3d668 100644
--- a/server/src/settings.rs
+++ b/server/src/settings.rs
@@ -40,6 +40,12 @@ pub struct X509FilePair {
     pub key: PathBuf,
 }
 
+#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
+pub enum ServerBattleMode {
+    Global(BattleMode),
+    PerPlayer { default: BattleMode },
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize)]
 #[serde(default)]
 pub struct Settings {
@@ -49,7 +55,7 @@ pub struct Settings {
     pub quic_files: Option<X509FilePair>,
     pub max_players: usize,
     pub world_seed: u32,
-    pub battle_mode: BattleMode,
+    pub battle_mode: ServerBattleMode,
     pub server_name: String,
     pub start_time: f64,
     /// When set to None, loads the default map file (if available); otherwise,
@@ -79,7 +85,7 @@ impl Default for Settings {
             world_seed: DEFAULT_WORLD_SEED,
             server_name: "Veloren Alpha".into(),
             max_players: 100,
-            battle_mode: BattleMode::PvP,
+            battle_mode: ServerBattleMode::Global(BattleMode::PvP),
             start_time: 9.0 * 3600.0,
             map_file: None,
             max_view_distance: Some(65),
diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs
index 816640be05..a2a5b3cbf9 100644
--- a/server/src/sys/msg/register.rs
+++ b/server/src/sys/msg/register.rs
@@ -2,6 +2,7 @@ use crate::{
     client::Client,
     login_provider::{LoginProvider, PendingLogin},
     metrics::PlayerMetrics,
+    settings::ServerBattleMode,
     EditableSettings, Settings,
 };
 use common::{
@@ -173,7 +174,14 @@ impl<'a> System<'a> for Sys {
                     return Ok(());
                 }
 
-                let player = Player::new(username, read_data.settings.battle_mode, uuid);
+                let battle_mode = match read_data.settings.battle_mode {
+                    ServerBattleMode::Global(mode) => mode,
+                    // FIXME:
+                    // Should this use just default battle_mode
+                    // or should we take it from last change?
+                    ServerBattleMode::PerPlayer { default: mode } => mode,
+                };
+                let player = Player::new(username, battle_mode, uuid);
                 let admin = read_data.editable_settings.admins.get(&uuid);
 
                 if !player.is_valid() {

From 1cedaa44176c1d2e757cbb9f8910dd66a9ad6781 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 27 Aug 2021 19:25:05 +0300
Subject: [PATCH 03/14] Implement /battlemode_force command

+ add placeholder for /battlemode command (currently can only show your
  battle mode)
---
 common/src/cmd.rs | 23 ++++++++++++++++++
 server/src/cmd.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 82 insertions(+), 1 deletion(-)

diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index 7c1a2d6ebf..fc3352c6af 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -47,6 +47,8 @@ pub enum ChatCommand {
     Alias,
     ApplyBuff,
     Ban,
+    BattleMode,
+    BattleModeForce,
     Build,
     BuildAreaAdd,
     BuildAreaList,
@@ -321,6 +323,25 @@ impl ChatCommand {
                  true for overwrite to alter an existing ban..",
                 Some(Moderator),
             ),
+            ChatCommand::BattleMode => cmd(
+                vec![Enum(
+                    "battle mode",
+                    vec!["pvp".to_owned(), "pve".to_owned()],
+                    Optional,
+                )],
+                "Set your battle mode to pvp/pve.\n\
+                If called without arguments will show current battle mode.",
+                None,
+            ),
+            ChatCommand::BattleModeForce => cmd(
+                vec![Enum(
+                    "battle mode",
+                    vec!["pvp".to_owned(), "pve".to_owned()],
+                    Required,
+                )],
+                "Change your battle mode flag without any checks",
+                Some(Admin),
+            ),
             ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None),
             ChatCommand::BuildAreaAdd => cmd(
                 vec![
@@ -623,6 +644,8 @@ impl ChatCommand {
             ChatCommand::Alias => "alias",
             ChatCommand::ApplyBuff => "buff",
             ChatCommand::Ban => "ban",
+            ChatCommand::BattleMode => "battlemode",
+            ChatCommand::BattleModeForce => "battlemode_force",
             ChatCommand::Build => "build",
             ChatCommand::BuildAreaAdd => "build_area_add",
             ChatCommand::BuildAreaList => "build_area_list",
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 960044b230..6e71021f01 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -31,7 +31,7 @@ use common::{
     event::{EventBus, ServerEvent},
     generation::EntityInfo,
     npc::{self, get_npc_name},
-    resources::{PlayerPhysicsSettings, TimeOfDay},
+    resources::{PlayerPhysicsSettings, TimeOfDay, BattleMode},
     terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
     uid::Uid,
     vol::RectVolSize,
@@ -113,6 +113,8 @@ fn do_command(
         ChatCommand::Alias => handle_alias,
         ChatCommand::ApplyBuff => handle_apply_buff,
         ChatCommand::Ban => handle_ban,
+        ChatCommand::BattleMode => handle_battlemode,
+        ChatCommand::BattleModeForce => handle_battlemode_force,
         ChatCommand::Build => handle_build,
         ChatCommand::BuildAreaAdd => handle_build_area_add,
         ChatCommand::BuildAreaList => handle_build_area_list,
@@ -3094,6 +3096,62 @@ fn handle_ban(
     }
 }
 
+fn handle_battlemode(
+    server: &mut Server,
+    client: EcsEntity,
+    target: EcsEntity,
+    args: Vec<String>,
+    _action: &ChatCommand,
+) -> CmdResult<()> {
+    let ecs = &server.state.ecs();
+    if let Some(mode) = parse_args!(args, String) {
+        Err("Seting mode isn't implemented".to_owned())
+    } else {
+        let players = ecs.read_storage::<comp::Player>();
+        let player = players
+            .get(target)
+            .ok_or_else(|| "Cannot get player component for target".to_string())?;
+        server.notify_client(
+            client,
+            ServerGeneral::server_msg(
+                ChatType::CommandInfo,
+                format!("Battle mode is {:?}", player.battle_mode),
+            ),
+        );
+        Ok(())
+    }
+}
+
+fn handle_battlemode_force(
+    server: &mut Server,
+    client: EcsEntity,
+    target: EcsEntity,
+    args: Vec<String>,
+    action: &ChatCommand,
+) -> CmdResult<()> {
+    let ecs = &server.state.ecs();
+    let mut players = ecs.write_storage::<comp::Player>();
+    let mode = parse_args!(args, String).ok_or_else(|| action.help_string())?;
+    let mode = match mode.as_str() {
+        "pvp" => BattleMode::PvP,
+        "pve" => BattleMode::PvE,
+        _ => return Err("Available modes: pvp, pve".to_owned()),
+    };
+    if let Some(ref mut player) = players.get_mut(target) {
+        player.battle_mode = mode;
+        server.notify_client(
+            client,
+            ServerGeneral::server_msg(
+                ChatType::CommandInfo,
+                format!("Set battle_mode to {:?}", player.battle_mode),
+            ),
+        );
+        Ok(())
+    } else {
+        Err("Cannot get player component for target".to_owned())
+    }
+}
+
 fn handle_unban(
     server: &mut Server,
     client: EcsEntity,

From 7be1c4d14a26ba75266a4db69d41a92888543e46 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 27 Aug 2021 21:49:58 +0300
Subject: [PATCH 04/14] Deduplicate pvp-checks

---
 common/src/cmd.rs                        |  1 +
 common/src/combat.rs                     | 64 ++++++++++++++++++++----
 common/src/comp/agent.rs                 | 13 +----
 common/systems/src/aura.rs               | 19 +++----
 common/systems/src/beam.rs               | 18 +++----
 common/systems/src/melee.rs              | 18 +++----
 common/systems/src/projectile.rs         | 18 +++----
 common/systems/src/shockwave.rs          | 19 +++----
 server/src/cmd.rs                        |  2 +-
 server/src/events/entity_manipulation.rs | 36 ++++++-------
 10 files changed, 101 insertions(+), 107 deletions(-)

diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index fc3352c6af..b1a6890b74 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -323,6 +323,7 @@ impl ChatCommand {
                  true for overwrite to alter an existing ban..",
                 Some(Moderator),
             ),
+            #[rustfmt::skip]
             ChatCommand::BattleMode => cmd(
                 vec![Enum(
                     "battle mode",
diff --git a/common/src/combat.rs b/common/src/combat.rs
index bc73dc91bb..f2d02722b7 100644
--- a/common/src/combat.rs
+++ b/common/src/combat.rs
@@ -12,13 +12,13 @@ use crate::{
         },
         poise::PoiseChange,
         skills::SkillGroupKind,
-        Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange,
-        HealthSource, Inventory, Ori, Player, Poise, SkillSet, Stats,
+        Alignment, Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health,
+        HealthChange, HealthSource, Inventory, Ori, Player, Poise, SkillSet, Stats,
     },
     event::ServerEvent,
     outcome::Outcome,
     states::utils::StageSection,
-    uid::Uid,
+    uid::{Uid, UidAllocator},
     util::Dir,
 };
 
@@ -28,7 +28,7 @@ use rand::{thread_rng, Rng};
 use serde::{Deserialize, Serialize};
 
 #[cfg(not(target_arch = "wasm32"))]
-use specs::Entity as EcsEntity;
+use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage};
 #[cfg(not(target_arch = "wasm32"))]
 use std::{ops::MulAssign, time::Duration};
 #[cfg(not(target_arch = "wasm32"))] use vek::*;
@@ -469,14 +469,56 @@ impl Attack {
     }
 }
 
-/// Says if we should allow negative effects from one player to another
+/// Function that checks for unintentional PvP between players.
 ///
-/// NOTE: this function doesn't handle pets or friendly-fire, you will need to
-/// figure it out on call-side.
-pub fn may_harm(attacker: Option<&Player>, target: Option<&Player>) -> bool {
-    attacker
-        .zip(target)
-        .map_or(true, |(attacker, target)| attacker.may_harm(target))
+/// Returns `false` if attack will create unintentional conflict,
+/// e.g. if player with PvE mode will harm pets of other players
+/// or other players will do the same to such player.
+///
+/// If both players have PvP mode enabled, interact with NPC and
+/// in any other case, this function will return `true`
+// TODO: add parameter for doing self-harm?
+pub fn may_harm(
+    alignments: &ReadStorage<Alignment>,
+    players: &ReadStorage<Player>,
+    uid_allocator: &UidAllocator,
+    attacker: Option<EcsEntity>,
+    target: EcsEntity,
+) -> bool {
+    // Return owner entity if pet,
+    // or just return entity back otherwise
+    let owner_if_pet = |entity| {
+        let alignment = alignments.get(entity).copied();
+        if let Some(Alignment::Owned(uid)) = alignment {
+            // return original entity
+            // if can't get owner
+            uid_allocator
+                .retrieve_entity_internal(uid.into())
+                .unwrap_or(entity)
+        } else {
+            entity
+        }
+    };
+
+    // Just return ok if attacker is unknown, it's probably
+    // environment or command.
+    let attacker = match attacker {
+        Some(attacker) => attacker,
+        None => return true,
+    };
+
+    // "Dereference" to owner if this is a pet.
+    let attacker = owner_if_pet(attacker);
+    let target = owner_if_pet(target);
+
+    // Get player components
+    let attacker_info = players.get(attacker);
+    let target_info = players.get(target);
+
+    // Return `true` if not players.
+    attacker_info
+        .zip(target_info)
+        .map_or(true, |(a, t)| a.may_harm(t))
 }
 
 #[cfg(not(target_arch = "wasm32"))]
diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs
index 3760b03544..72cd5bc641 100644
--- a/common/src/comp/agent.rs
+++ b/common/src/comp/agent.rs
@@ -3,10 +3,10 @@ use crate::{
     path::Chaser,
     rtsim::RtSimController,
     trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult},
-    uid::{Uid, UidAllocator},
+    uid::Uid,
 };
 use serde::Deserialize;
-use specs::{saveload::MarkerAllocator, Component, Entity as EcsEntity};
+use specs::{Component, Entity as EcsEntity};
 use specs_idvs::IdvStorage;
 use std::{collections::VecDeque, fmt};
 use strum::IntoEnumIterator;
@@ -34,15 +34,6 @@ pub enum Alignment {
     Passive,
 }
 
-// Helper function to get owner
-pub fn owner_of(alignment: Option<Alignment>, uid_allocator: &UidAllocator) -> Option<EcsEntity> {
-    if let Some(Alignment::Owned(uid)) = alignment {
-        uid_allocator.retrieve_entity_internal(uid.into())
-    } else {
-        None
-    }
-}
-
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub enum Mark {
     Merchant,
diff --git a/common/systems/src/aura.rs b/common/systems/src/aura.rs
index 31264e647a..f316e6fde7 100644
--- a/common/systems/src/aura.rs
+++ b/common/systems/src/aura.rs
@@ -1,7 +1,6 @@
 use common::{
     combat,
     comp::{
-        agent::owner_of,
         aura::{AuraChange, AuraKey, AuraKind, AuraTarget},
         buff::{Buff, BuffCategory, BuffChange, BuffSource},
         group::Group,
@@ -178,7 +177,7 @@ fn activate_aura(
             // Which means that you can't apply debuffs on you and your group
             // even if it's intented mechanic.
             //
-            // Not that we have this for now, but think about this
+            // We don't have this for now, but think about this
             // when we will add this.
             let may_harm = || {
                 let owner = match source {
@@ -187,18 +186,12 @@ fn activate_aura(
                     },
                     _ => None,
                 };
-                let owner_if_pet = |entity| {
-                    // Return owner entity if pet,
-                    // or just return entity back otherwise
-                    owner_of(
-                        read_data.alignments.get(entity).copied(),
-                        &read_data.uid_allocator,
-                    )
-                    .unwrap_or(entity)
-                };
                 combat::may_harm(
-                    owner.and_then(|owner| read_data.players.get(owner_if_pet(owner))),
-                    read_data.players.get(owner_if_pet(target)),
+                    &read_data.alignments,
+                    &read_data.players,
+                    &read_data.uid_allocator,
+                    owner,
+                    target,
                 )
             };
 
diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs
index 8b2403c3a4..06182c3953 100644
--- a/common/systems/src/beam.rs
+++ b/common/systems/src/beam.rs
@@ -1,7 +1,7 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{owner_of, Sound, SoundKind},
+        agent::{Sound, SoundKind},
         Alignment, Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health,
         HealthSource, Inventory, Ori, Player, Pos, Scale, Stats,
     },
@@ -216,18 +216,12 @@ impl<'a> System<'a> for Sys {
 
 
                     // PvP check
-                    let owner_if_pet = |entity| {
-                        // Return owner entity if pet,
-                        // or just return entity back otherwise
-                        owner_of(
-                            read_data.alignments.get(entity).copied(),
-                            &read_data.uid_allocator,
-                        )
-                        .unwrap_or(entity)
-                    };
                     let may_harm = combat::may_harm(
-                        beam_owner.and_then(|owner| read_data.players.get(owner_if_pet(owner))),
-                        read_data.players.get(owner_if_pet(target)),
+                        &read_data.alignments,
+                        &read_data.players,
+                        &read_data.uid_allocator,
+                        beam_owner,
+                        target,
                     );
                     let attack_options = AttackOptions {
                         // No luck with dodging beams
diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs
index 005efad509..5067b79b53 100644
--- a/common/systems/src/melee.rs
+++ b/common/systems/src/melee.rs
@@ -1,7 +1,7 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{owner_of, Sound, SoundKind},
+        agent::{Sound, SoundKind},
         Alignment, Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori,
         Player, Pos, Scale, Stats,
     },
@@ -165,18 +165,12 @@ impl<'a> System<'a> for Sys {
                     };
 
                     // PvP check
-                    let owner_if_pet = |entity| {
-                        // Return owner entity if pet,
-                        // or just return entity back otherwise
-                        owner_of(
-                            read_data.alignments.get(entity).copied(),
-                            &read_data.uid_allocator,
-                        )
-                        .unwrap_or(entity)
-                    };
                     let may_harm = combat::may_harm(
-                        read_data.players.get(owner_if_pet(attacker)),
-                        read_data.players.get(owner_if_pet(target)),
+                        &read_data.alignments,
+                        &read_data.players,
+                        &read_data.uid_allocator,
+                        Some(attacker),
+                        target,
                     );
 
                     let attack_options = AttackOptions {
diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs
index f27ac95466..83bb98e102 100644
--- a/common/systems/src/projectile.rs
+++ b/common/systems/src/projectile.rs
@@ -1,7 +1,7 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{owner_of, Sound, SoundKind},
+        agent::{Sound, SoundKind},
         projectile, Alignment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
         Inventory, Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel,
     },
@@ -301,18 +301,12 @@ fn dispatch_hit(
             }
 
             // PvP check
-            let owner_if_pet = |entity| {
-                // Return owner entity if pet,
-                // or just return entity back otherwise
-                owner_of(
-                    read_data.alignments.get(entity).copied(),
-                    &read_data.uid_allocator,
-                )
-                .unwrap_or(entity)
-            };
             let may_harm = combat::may_harm(
-                owner.and_then(|owner| read_data.players.get(owner_if_pet(owner))),
-                read_data.players.get(owner_if_pet(target)),
+                &read_data.alignments,
+                &read_data.players,
+                &read_data.uid_allocator,
+                owner,
+                target,
             );
 
             let attack_options = AttackOptions {
diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs
index 75fd5db924..f44b965d9d 100644
--- a/common/systems/src/shockwave.rs
+++ b/common/systems/src/shockwave.rs
@@ -1,7 +1,7 @@
 use common::{
     combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
     comp::{
-        agent::{owner_of, Sound, SoundKind},
+        agent::{Sound, SoundKind},
         Alignment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
         Ori, PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
     },
@@ -211,19 +211,12 @@ impl<'a> System<'a> for Sys {
                     };
 
                     // PvP check
-                    let owner_if_pet = |entity| {
-                        // Return owner entity if pet,
-                        // or just return entity back otherwise
-                        owner_of(
-                            read_data.alignments.get(entity).copied(),
-                            &read_data.uid_allocator,
-                        )
-                        .unwrap_or(entity)
-                    };
                     let may_harm = combat::may_harm(
-                        shockwave_owner
-                            .and_then(|owner| read_data.players.get(owner_if_pet(owner))),
-                        read_data.players.get(owner_if_pet(target)),
+                        &read_data.alignments,
+                        &read_data.players,
+                        &read_data.uid_allocator,
+                        shockwave_owner,
+                        target,
                     );
                     let attack_options = AttackOptions {
                         // Trying roll during earthquake isn't the best idea
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 6e71021f01..9bdae67c03 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -31,7 +31,7 @@ use common::{
     event::{EventBus, ServerEvent},
     generation::EntityInfo,
     npc::{self, get_npc_name},
-    resources::{PlayerPhysicsSettings, TimeOfDay, BattleMode},
+    resources::{BattleMode, PlayerPhysicsSettings, TimeOfDay},
     terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
     uid::Uid,
     vol::RectVolSize,
diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs
index 5d81428093..42a69a54fc 100644
--- a/server/src/events/entity_manipulation.rs
+++ b/server/src/events/entity_manipulation.rs
@@ -1,7 +1,7 @@
 use crate::{
     client::Client,
     comp::{
-        agent::{owner_of, Agent, AgentEvent, Sound, SoundKind},
+        agent::{Agent, AgentEvent, Sound, SoundKind},
         biped_large, bird_large, quadruped_low, quadruped_medium, quadruped_small,
         skills::SkillGroupKind,
         theropod, BuffKind, BuffSource, PhysicsState,
@@ -945,15 +945,12 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                         };
 
                         // PvP check
-                        let owner_if_pet = |entity| {
-                            // Return owner entity if pet,
-                            // or just return entity back otherwise
-                            owner_of(alignments.get(entity).copied(), uid_allocator)
-                                .unwrap_or(entity)
-                        };
                         let may_harm = combat::may_harm(
-                            owner_entity.and_then(|owner| players.get(owner_if_pet(owner))),
-                            players.get(owner_if_pet(entity_b)),
+                            alignments,
+                            players,
+                            uid_allocator,
+                            owner_entity,
+                            entity_b,
                         );
                         let attack_options = combat::AttackOptions {
                             // cool guyz maybe don't look at explosions
@@ -994,27 +991,22 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                         1.0 - distance_squared / explosion.radius.powi(2)
                     };
 
-                    // Player check only accounts for PvP/PvE flag.
+                    // Player check only accounts for PvP/PvE flag, but bombs
+                    // are intented to do friendly fire.
                     //
-                    // But bombs are intented to do
-                    // friendly fire.
-                    //
-                    // What exactly friendly fire is subject to discussion.
+                    // What exactly is friendly fire is subject to discussion.
                     // As we probably want to minimize possibility of being dick
                     // even to your group members, the only exception is when
                     // you want to harm yourself.
                     //
                     // This can be changed later.
-                    // PvP check
-                    let owner_if_pet = |entity| {
-                        // Return owner entity if pet,
-                        // or just return entity back otherwise
-                        owner_of(alignments.get(entity).copied(), uid_allocator).unwrap_or(entity)
-                    };
                     let may_harm = || {
                         combat::may_harm(
-                            owner_entity.and_then(|owner| players.get(owner_if_pet(owner))),
-                            players.get(owner_if_pet(entity_b)),
+                            alignments,
+                            players,
+                            uid_allocator,
+                            owner_entity,
+                            entity_b,
                         ) || owner_entity.map_or(true, |entity_a| entity_a == entity_b)
                     };
                     if strength > 0.0 {

From d43f34aea87e20e7f79d1b312f82590574870af6 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 28 Aug 2021 00:08:48 +0300
Subject: [PATCH 05/14] Allow changing battle_mode only in towns

---
 server/src/cmd.rs                        | 75 ++++++++++++++++++------
 server/src/events/entity_manipulation.rs |  9 +--
 2 files changed, 58 insertions(+), 26 deletions(-)

diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 9bdae67c03..db62540b17 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -50,9 +50,10 @@ use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt}
 use std::str::FromStr;
 use vek::*;
 use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement};
-use world::util::Sampler;
+use world::{site::SiteKind, util::Sampler};
 
 use crate::{client::Client, login_provider::LoginProvider, wiring};
+use std::ops::DerefMut;
 use tracing::{error, info, warn};
 
 pub trait ChatCommandExt {
@@ -3103,9 +3104,37 @@ fn handle_battlemode(
     args: Vec<String>,
     _action: &ChatCommand,
 ) -> CmdResult<()> {
-    let ecs = &server.state.ecs();
+    let ecs = server.state.ecs();
     if let Some(mode) = parse_args!(args, String) {
-        Err("Seting mode isn't implemented".to_owned())
+        let world = &server.world;
+        let index = &server.index;
+        let sim = world.sim();
+        let pos = position(server, target, "target")?;
+        let chunk_pos = Vec2::from(pos.0).map(|x: f32| x as i32);
+        let chunk = sim
+            .get(chunk_pos)
+            .ok_or_else(|| "Cannot get current chunk for target")?;
+        let site_ids = &chunk.sites;
+        let mut in_town = false;
+        for site_id in site_ids.iter() {
+            let site = index.sites.get(*site_id);
+            if matches!(site.kind, SiteKind::Settlement(_)) {
+                in_town = true;
+                break;
+            }
+        }
+
+        if !in_town {
+            return Err("You can change battle_mode only in town".to_owned());
+        }
+
+        let mut players = ecs.write_storage::<comp::Player>();
+        let mut player = players
+            .get_mut(target)
+            .ok_or_else(|| "Cannot get player component for target".to_owned())?;
+        // FIXME: handle cooldown before merge here!
+        //
+        set_battlemode(&mode, player.deref_mut(), server, client)
     } else {
         let players = ecs.read_storage::<comp::Player>();
         let player = players
@@ -3129,27 +3158,35 @@ fn handle_battlemode_force(
     args: Vec<String>,
     action: &ChatCommand,
 ) -> CmdResult<()> {
-    let ecs = &server.state.ecs();
-    let mut players = ecs.write_storage::<comp::Player>();
     let mode = parse_args!(args, String).ok_or_else(|| action.help_string())?;
-    let mode = match mode.as_str() {
+    let ecs = server.state.ecs();
+    let mut players = ecs.write_storage::<comp::Player>();
+    let mut player = players
+        .get_mut(target)
+        .ok_or_else(|| "Cannot get player component for target".to_owned())?;
+    set_battlemode(&mode, player.deref_mut(), server, client)
+}
+
+fn set_battlemode(
+    mode: &str,
+    player_info: &mut comp::Player,
+    server: &Server,
+    client: EcsEntity,
+) -> CmdResult<()> {
+    let mode = match mode {
         "pvp" => BattleMode::PvP,
         "pve" => BattleMode::PvE,
         _ => return Err("Available modes: pvp, pve".to_owned()),
     };
-    if let Some(ref mut player) = players.get_mut(target) {
-        player.battle_mode = mode;
-        server.notify_client(
-            client,
-            ServerGeneral::server_msg(
-                ChatType::CommandInfo,
-                format!("Set battle_mode to {:?}", player.battle_mode),
-            ),
-        );
-        Ok(())
-    } else {
-        Err("Cannot get player component for target".to_owned())
-    }
+    player_info.battle_mode = mode;
+    server.notify_client(
+        client,
+        ServerGeneral::server_msg(
+            ChatType::CommandInfo,
+            format!("Set battle_mode to {:?}", player_info.battle_mode),
+        ),
+    );
+    Ok(())
 }
 
 fn handle_unban(
diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs
index 42a69a54fc..f25657913d 100644
--- a/server/src/events/entity_manipulation.rs
+++ b/server/src/events/entity_manipulation.rs
@@ -1001,13 +1001,8 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
                     //
                     // This can be changed later.
                     let may_harm = || {
-                        combat::may_harm(
-                            alignments,
-                            players,
-                            uid_allocator,
-                            owner_entity,
-                            entity_b,
-                        ) || owner_entity.map_or(true, |entity_a| entity_a == entity_b)
+                        combat::may_harm(alignments, players, uid_allocator, owner_entity, entity_b)
+                            || owner_entity.map_or(true, |entity_a| entity_a == entity_b)
                     };
                     if strength > 0.0 {
                         let is_alive = ecs

From deacec94d7c6e6c04df05be6c60306c03a6c7f8a Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 28 Aug 2021 01:42:42 +0300
Subject: [PATCH 06/14] Allow changing mode only with enabled settings

- send warning in force mode
---
 server/src/cmd.rs      | 28 ++++++++++++++++++++++------
 server/src/settings.rs |  9 +++++++++
 2 files changed, 31 insertions(+), 6 deletions(-)

diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index db62540b17..832289ec9f 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -3,12 +3,15 @@
 //! in [do_command].
 
 use crate::{
+    client::Client,
+    login_provider::LoginProvider,
     settings::{
         Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord,
     },
     sys::terrain::NpcData,
+    wiring,
     wiring::{Logic, OutputFormula},
-    Server, SpawnPoint, StateExt,
+    Server, Settings, SpawnPoint, StateExt,
 };
 use assets::AssetExt;
 use authc::Uuid;
@@ -52,7 +55,6 @@ use vek::*;
 use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement};
 use world::{site::SiteKind, util::Sampler};
 
-use crate::{client::Client, login_provider::LoginProvider, wiring};
 use std::ops::DerefMut;
 use tracing::{error, info, warn};
 
@@ -3105,6 +3107,10 @@ fn handle_battlemode(
     _action: &ChatCommand,
 ) -> CmdResult<()> {
     let ecs = server.state.ecs();
+    let settings = ecs.read_resource::<Settings>();
+    if !settings.battle_mode.allow_choosing() {
+        return Err("Toggling battlemode is disabled.".to_owned());
+    }
     if let Some(mode) = parse_args!(args, String) {
         let world = &server.world;
         let index = &server.index;
@@ -3113,7 +3119,7 @@ fn handle_battlemode(
         let chunk_pos = Vec2::from(pos.0).map(|x: f32| x as i32);
         let chunk = sim
             .get(chunk_pos)
-            .ok_or_else(|| "Cannot get current chunk for target")?;
+            .ok_or("Cannot get current chunk for target")?;
         let site_ids = &chunk.sites;
         let mut in_town = false;
         for site_id in site_ids.iter() {
@@ -3131,7 +3137,7 @@ fn handle_battlemode(
         let mut players = ecs.write_storage::<comp::Player>();
         let mut player = players
             .get_mut(target)
-            .ok_or_else(|| "Cannot get player component for target".to_owned())?;
+            .ok_or("Cannot get player component for target")?;
         // FIXME: handle cooldown before merge here!
         //
         set_battlemode(&mode, player.deref_mut(), server, client)
@@ -3139,7 +3145,7 @@ fn handle_battlemode(
         let players = ecs.read_storage::<comp::Player>();
         let player = players
             .get(target)
-            .ok_or_else(|| "Cannot get player component for target".to_string())?;
+            .ok_or("Cannot get player component for target")?;
         server.notify_client(
             client,
             ServerGeneral::server_msg(
@@ -3160,10 +3166,20 @@ fn handle_battlemode_force(
 ) -> CmdResult<()> {
     let mode = parse_args!(args, String).ok_or_else(|| action.help_string())?;
     let ecs = server.state.ecs();
+    let settings = ecs.read_resource::<Settings>();
+    if !settings.battle_mode.allow_choosing() {
+        server.notify_client(
+            client,
+            ServerGeneral::server_msg(
+                ChatType::CommandInfo,
+                "Warning! Forcing battle_mode while not enabled in settings!".to_owned(),
+            ),
+        );
+    }
     let mut players = ecs.write_storage::<comp::Player>();
     let mut player = players
         .get_mut(target)
-        .ok_or_else(|| "Cannot get player component for target".to_owned())?;
+        .ok_or("Cannot get player component for target")?;
     set_battlemode(&mode, player.deref_mut(), server, client)
 }
 
diff --git a/server/src/settings.rs b/server/src/settings.rs
index 7e45e3d668..f2c7004d29 100644
--- a/server/src/settings.rs
+++ b/server/src/settings.rs
@@ -46,6 +46,15 @@ pub enum ServerBattleMode {
     PerPlayer { default: BattleMode },
 }
 
+impl ServerBattleMode {
+    pub fn allow_choosing(&self) -> bool {
+        match self {
+            ServerBattleMode::Global { .. } => false,
+            ServerBattleMode::PerPlayer { .. } => true,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize)]
 #[serde(default)]
 pub struct Settings {

From 6c1095884aa9d2846c2d2f7901cdde0277d5c361 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 28 Aug 2021 13:36:33 +0300
Subject: [PATCH 07/14] Make it compile without feature(worldgen)

+ move settings check to toggling mode segment
---
 server/src/cmd.rs | 46 +++++++++++++++++++++++++++-------------------
 1 file changed, 27 insertions(+), 19 deletions(-)

diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 832289ec9f..5d95344c40 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -3108,28 +3108,36 @@ fn handle_battlemode(
 ) -> CmdResult<()> {
     let ecs = server.state.ecs();
     let settings = ecs.read_resource::<Settings>();
-    if !settings.battle_mode.allow_choosing() {
-        return Err("Toggling battlemode is disabled.".to_owned());
-    }
     if let Some(mode) = parse_args!(args, String) {
-        let world = &server.world;
-        let index = &server.index;
-        let sim = world.sim();
-        let pos = position(server, target, "target")?;
-        let chunk_pos = Vec2::from(pos.0).map(|x: f32| x as i32);
-        let chunk = sim
-            .get(chunk_pos)
-            .ok_or("Cannot get current chunk for target")?;
-        let site_ids = &chunk.sites;
-        let mut in_town = false;
-        for site_id in site_ids.iter() {
-            let site = index.sites.get(*site_id);
-            if matches!(site.kind, SiteKind::Settlement(_)) {
-                in_town = true;
-                break;
-            }
+        if !settings.battle_mode.allow_choosing() {
+            return Err("Toggling battlemode is disabled.".to_owned());
         }
 
+        #[cfg(feature = "worldgen")]
+        let in_town = {
+            let world = &server.world;
+            let index = &server.index;
+            let sim = world.sim();
+            let pos = position(server, target, "target")?;
+            let chunk_pos = Vec2::from(pos.0).map(|x: f32| x as i32);
+            let chunk = sim
+                .get(chunk_pos)
+                .ok_or("Cannot get current chunk for target")?;
+            let site_ids = &chunk.sites;
+            let mut in_town = false;
+            for site_id in site_ids.iter() {
+                let site = index.sites.get(*site_id);
+                if matches!(site.kind, SiteKind::Settlement(_)) {
+                    in_town = true;
+                    break;
+                }
+            }
+            in_town
+        };
+        // just skip this check, if worldgen is disabled
+        #[cfg(not(feature = "worldgen"))]
+        let in_town = true;
+
         if !in_town {
             return Err("You can change battle_mode only in town".to_owned());
         }

From b96de1adf7f15c78c916f9a3625268ce141f1f80 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 28 Aug 2021 16:32:18 +0300
Subject: [PATCH 08/14] Fix town detection

---
 server/src/cmd.rs | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 5d95344c40..6b3a821136 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -3110,7 +3110,7 @@ fn handle_battlemode(
     let settings = ecs.read_resource::<Settings>();
     if let Some(mode) = parse_args!(args, String) {
         if !settings.battle_mode.allow_choosing() {
-            return Err("Toggling battlemode is disabled.".to_owned());
+            return Err("Command disabled in server settings".to_owned());
         }
 
         #[cfg(feature = "worldgen")]
@@ -3118,11 +3118,19 @@ fn handle_battlemode(
             let world = &server.world;
             let index = &server.index;
             let sim = world.sim();
+            // get chunk position
             let pos = position(server, target, "target")?;
-            let chunk_pos = Vec2::from(pos.0).map(|x: f32| x as i32);
+            let wpos = pos.0.xy().map(|x| x as i32);
+            let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |pos, size: u32| {
+                pos / size as i32
+            });
             let chunk = sim
                 .get(chunk_pos)
                 .ok_or("Cannot get current chunk for target")?;
+            // search for towns in chunk
+            //
+            // NOTE: this code finds town even if it is far from it.
+            // Does it count plant fields?
             let site_ids = &chunk.sites;
             let mut in_town = false;
             for site_id in site_ids.iter() {
@@ -3139,7 +3147,7 @@ fn handle_battlemode(
         let in_town = true;
 
         if !in_town {
-            return Err("You can change battle_mode only in town".to_owned());
+            return Err("Too far from town".to_owned());
         }
 
         let mut players = ecs.write_storage::<comp::Player>();
@@ -3158,7 +3166,7 @@ fn handle_battlemode(
             client,
             ServerGeneral::server_msg(
                 ChatType::CommandInfo,
-                format!("Battle mode is {:?}", player.battle_mode),
+                format!("Current battle mode: {:?}", player.battle_mode),
             ),
         );
         Ok(())
@@ -3180,7 +3188,7 @@ fn handle_battlemode_force(
             client,
             ServerGeneral::server_msg(
                 ChatType::CommandInfo,
-                "Warning! Forcing battle_mode while not enabled in settings!".to_owned(),
+                "Warning! Forcing battle mode while not enabled in settings!".to_owned(),
             ),
         );
     }
@@ -3207,7 +3215,7 @@ fn set_battlemode(
         client,
         ServerGeneral::server_msg(
             ChatType::CommandInfo,
-            format!("Set battle_mode to {:?}", player_info.battle_mode),
+            format!("New battle mode: {:?}", player_info.battle_mode),
         ),
     );
     Ok(())

From df066f364acc88b73b080f547b2868412c6fff27 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Tue, 31 Aug 2021 18:55:28 +0300
Subject: [PATCH 09/14] Cooldowns

- Add last_battlemode_change to Player component
- check on last_battlemode_change in handle_battlemode
- set last_battlemode_change after setting battlemode
- still are not persisted in any way
---
 common/src/comp/player.rs      | 11 ++++-
 server/src/cmd.rs              | 75 +++++++++++++++++++---------------
 server/src/sys/msg/register.rs |  9 ++--
 3 files changed, 57 insertions(+), 38 deletions(-)

diff --git a/common/src/comp/player.rs b/common/src/comp/player.rs
index 76db8ddc55..8eecf6b90d 100644
--- a/common/src/comp/player.rs
+++ b/common/src/comp/player.rs
@@ -3,7 +3,7 @@ use specs::{Component, DerefFlaggedStorage, NullStorage};
 use specs_idvs::IdvStorage;
 use uuid::Uuid;
 
-use crate::resources::BattleMode;
+use crate::resources::{BattleMode, Time};
 
 const MAX_ALIAS_LEN: usize = 32;
 
@@ -20,6 +20,7 @@ pub enum DisconnectReason {
 pub struct Player {
     pub alias: String,
     pub battle_mode: BattleMode,
+    pub last_battlemode_change: Option<Time>,
     uuid: Uuid,
 }
 
@@ -30,10 +31,16 @@ impl BattleMode {
 }
 
 impl Player {
-    pub fn new(alias: String, battle_mode: BattleMode, uuid: Uuid) -> Self {
+    pub fn new(
+        alias: String,
+        battle_mode: BattleMode,
+        uuid: Uuid,
+        last_battlemode_change: Option<Time>,
+    ) -> Self {
         Self {
             alias,
             battle_mode,
+            last_battlemode_change,
             uuid,
         }
     }
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 6b3a821136..c51cfcc9f8 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -34,7 +34,7 @@ use common::{
     event::{EventBus, ServerEvent},
     generation::EntityInfo,
     npc::{self, get_npc_name},
-    resources::{BattleMode, PlayerPhysicsSettings, TimeOfDay},
+    resources::{BattleMode, PlayerPhysicsSettings, Time, TimeOfDay},
     terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
     uid::Uid,
     vol::RectVolSize,
@@ -55,7 +55,6 @@ use vek::*;
 use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement};
 use world::{site::SiteKind, util::Sampler};
 
-use std::ops::DerefMut;
 use tracing::{error, info, warn};
 
 pub trait ChatCommandExt {
@@ -3128,12 +3127,11 @@ fn handle_battlemode(
                 .get(chunk_pos)
                 .ok_or("Cannot get current chunk for target")?;
             // search for towns in chunk
-            //
-            // NOTE: this code finds town even if it is far from it.
-            // Does it count plant fields?
             let site_ids = &chunk.sites;
             let mut in_town = false;
             for site_id in site_ids.iter() {
+                // NOTE: this code finds town even if it is far from actual
+                // houses in settlement. Is it because of plant fields?
                 let site = index.sites.get(*site_id);
                 if matches!(site.kind, SiteKind::Settlement(_)) {
                     in_town = true;
@@ -3151,12 +3149,40 @@ fn handle_battlemode(
         }
 
         let mut players = ecs.write_storage::<comp::Player>();
-        let mut player = players
+        let mut player_info = players
             .get_mut(target)
             .ok_or("Cannot get player component for target")?;
-        // FIXME: handle cooldown before merge here!
-        //
-        set_battlemode(&mode, player.deref_mut(), server, client)
+        let time = ecs.read_resource::<Time>();
+        if let Some(Time(last_change)) = player_info.last_battlemode_change {
+            const COOLDOWN: f64 = 60.0 * 5.0;
+
+            let Time(time) = *time;
+            let elapsed = time - last_change;
+            if elapsed < COOLDOWN {
+                #[rustfmt::skip]
+                let msg = format!(
+                    "You can switch battlemode only once in {:.0} seconds. \
+                    Last change was {:.0} seconds before",
+                    COOLDOWN, elapsed,
+                );
+                return Err(msg);
+            }
+        }
+        let mode = match mode.as_str() {
+            "pvp" => BattleMode::PvP,
+            "pve" => BattleMode::PvE,
+            _ => return Err("Available modes: pvp, pve".to_owned()),
+        };
+        player_info.battle_mode = mode;
+        player_info.last_battlemode_change = Some(*time);
+        server.notify_client(
+            client,
+            ServerGeneral::server_msg(
+                ChatType::CommandInfo,
+                format!("New battle mode: {:?}", mode),
+            ),
+        );
+        Ok(())
     } else {
         let players = ecs.read_storage::<comp::Player>();
         let player = players
@@ -3180,42 +3206,27 @@ fn handle_battlemode_force(
     args: Vec<String>,
     action: &ChatCommand,
 ) -> CmdResult<()> {
-    let mode = parse_args!(args, String).ok_or_else(|| action.help_string())?;
     let ecs = server.state.ecs();
     let settings = ecs.read_resource::<Settings>();
     if !settings.battle_mode.allow_choosing() {
-        server.notify_client(
-            client,
-            ServerGeneral::server_msg(
-                ChatType::CommandInfo,
-                "Warning! Forcing battle mode while not enabled in settings!".to_owned(),
-            ),
-        );
+        return Err("Command disabled in server settings".to_owned());
     }
-    let mut players = ecs.write_storage::<comp::Player>();
-    let mut player = players
-        .get_mut(target)
-        .ok_or("Cannot get player component for target")?;
-    set_battlemode(&mode, player.deref_mut(), server, client)
-}
-
-fn set_battlemode(
-    mode: &str,
-    player_info: &mut comp::Player,
-    server: &Server,
-    client: EcsEntity,
-) -> CmdResult<()> {
-    let mode = match mode {
+    let mode = parse_args!(args, String).ok_or_else(|| action.help_string())?;
+    let mode = match mode.as_str() {
         "pvp" => BattleMode::PvP,
         "pve" => BattleMode::PvE,
         _ => return Err("Available modes: pvp, pve".to_owned()),
     };
+    let mut players = ecs.write_storage::<comp::Player>();
+    let mut player_info = players
+        .get_mut(target)
+        .ok_or("Cannot get player component for target")?;
     player_info.battle_mode = mode;
     server.notify_client(
         client,
         ServerGeneral::server_msg(
             ChatType::CommandInfo,
-            format!("New battle mode: {:?}", player_info.battle_mode),
+            format!("Set battle mode to: {:?}", mode),
         ),
     );
     Ok(())
diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs
index a2a5b3cbf9..a32100a00e 100644
--- a/server/src/sys/msg/register.rs
+++ b/server/src/sys/msg/register.rs
@@ -174,14 +174,15 @@ impl<'a> System<'a> for Sys {
                     return Ok(());
                 }
 
+                // FIXME:
+                // Take last battle_mode and last battle_mode change
+                // from in-memory persistence
                 let battle_mode = match read_data.settings.battle_mode {
                     ServerBattleMode::Global(mode) => mode,
-                    // FIXME:
-                    // Should this use just default battle_mode
-                    // or should we take it from last change?
                     ServerBattleMode::PerPlayer { default: mode } => mode,
                 };
-                let player = Player::new(username, battle_mode, uuid);
+                let player = Player::new(username, battle_mode, uuid, None);
+
                 let admin = read_data.editable_settings.admins.get(&uuid);
 
                 if !player.is_valid() {

From 91539a3b67403a31c1817dc9d71c63ca80ea66f9 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Fri, 3 Sep 2021 23:49:13 +0300
Subject: [PATCH 10/14] Add in-memory persistence for battlemode changes

---
 common/src/resources.rs        | 18 ++++++++++++++++++
 server/src/events/player.rs    | 10 ++++++++++
 server/src/lib.rs              |  4 +++-
 server/src/state_ext.rs        | 23 ++++++++++++++++++++---
 server/src/sys/msg/register.rs |  7 ++++---
 5 files changed, 55 insertions(+), 7 deletions(-)

diff --git a/common/src/resources.rs b/common/src/resources.rs
index 2b08b6407b..688562c081 100644
--- a/common/src/resources.rs
+++ b/common/src/resources.rs
@@ -1,5 +1,7 @@
+use crate::character::CharacterId;
 #[cfg(not(target_arch = "wasm32"))]
 use crate::comp::Pos;
+use hashbrown::HashMap;
 use serde::{Deserialize, Serialize};
 #[cfg(not(target_arch = "wasm32"))]
 use specs::Entity;
@@ -75,6 +77,22 @@ pub struct PlayerPhysicsSettings {
     pub settings: hashbrown::HashMap<uuid::Uuid, PlayerPhysicsSetting>,
 }
 
+/// Store of BattleMode cooldowns for players while they go offline
+#[derive(Clone, Default, Debug)]
+pub struct BattleModeBuffer {
+    map: HashMap<CharacterId, (BattleMode, Time)>,
+}
+
+impl BattleModeBuffer {
+    pub fn push(&mut self, char_id: CharacterId, save: (BattleMode, Time)) {
+        self.map.insert(char_id, save);
+    }
+
+    pub fn pop(&mut self, char_id: &CharacterId) -> Option<(BattleMode, Time)> {
+        self.map.remove(char_id)
+    }
+}
+
 /// Describe how players interact with other players.
 ///
 /// May be removed when we will discover better way
diff --git a/server/src/events/player.rs b/server/src/events/player.rs
index 667dd5fe39..517ec11819 100644
--- a/server/src/events/player.rs
+++ b/server/src/events/player.rs
@@ -6,6 +6,7 @@ use crate::{
 use common::{
     comp,
     comp::{group, pet::is_tameable},
+    resources::BattleModeBuffer,
     uid::{Uid, UidAllocator},
 };
 use common_base::span;
@@ -201,12 +202,14 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
         Some(skill_set),
         Some(inventory),
         Some(player_uid),
+        Some(player_info),
         mut character_updater,
     ) = (
         state.read_storage::<Presence>().get(entity),
         state.read_storage::<comp::SkillSet>().get(entity),
         state.read_storage::<comp::Inventory>().get(entity),
         state.read_storage::<Uid>().get(entity),
+        state.read_storage::<comp::Player>().get(entity),
         state.ecs().fetch_mut::<CharacterUpdater>(),
     ) {
         match presence.kind {
@@ -216,6 +219,13 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
                     .read_storage::<common::comp::Waypoint>()
                     .get(entity)
                     .cloned();
+                // Store last battle mode change
+                let mut battlemode_buffer = state.ecs().fetch_mut::<BattleModeBuffer>();
+                if let Some(change) = player_info.last_battlemode_change {
+                    let mode = player_info.battle_mode;
+                    let save = (mode, change);
+                    battlemode_buffer.push(char_id, save);
+                }
 
                 // Get player's pets
                 let alignments = state.ecs().read_storage::<comp::Alignment>();
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 97ecf22a93..227a9b4379 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -69,7 +69,7 @@ use common::{
     comp::{item::MaterialStatManifest, CharacterAbility},
     event::{EventBus, ServerEvent},
     recipe::default_recipe_book,
-    resources::TimeOfDay,
+    resources::{BattleModeBuffer, TimeOfDay},
     rtsim::RtSimEntity,
     slowjob::SlowJobPool,
     terrain::{TerrainChunk, TerrainChunkSize},
@@ -196,8 +196,10 @@ impl Server {
         let ecs_system_metrics = EcsSystemMetrics::new(&registry).unwrap();
         let tick_metrics = TickMetrics::new(&registry).unwrap();
         let physics_metrics = PhysicsMetrics::new(&registry).unwrap();
+        let battlemode_buffer = BattleModeBuffer::default();
 
         let mut state = State::server();
+        state.ecs_mut().insert(battlemode_buffer);
         state.ecs_mut().insert(settings.clone());
         state.ecs_mut().insert(editable_settings);
         state.ecs_mut().insert(DataDir {
diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs
index 9d91e90357..08ff295c36 100644
--- a/server/src/state_ext.rs
+++ b/server/src/state_ext.rs
@@ -16,7 +16,7 @@ use common::{
         Group, Inventory, Poise,
     },
     effect::Effect,
-    resources::TimeOfDay,
+    resources::{BattleModeBuffer, TimeOfDay},
     slowjob::SlowJobPool,
     uid::{Uid, UidAllocator},
 };
@@ -496,8 +496,8 @@ impl StateExt for State {
                 }),
             ));
 
-            // NOTE: By fetching the player_uid, we validated that the entity exists, and we
-            // call nothing that can delete it in any of the subsequent
+            // NOTE: By fetching the player_uid, we validated that the entity exists,
+            // and we call nothing that can delete it in any of the subsequent
             // commands, so we can assume that all of these calls succeed,
             // justifying ignoring the result of insertion.
             self.write_component_ignore_entity_dead(entity, comp::Collider::Box {
@@ -567,6 +567,23 @@ impl StateExt for State {
             } else {
                 warn!("Player has no pos, cannot load {} pets", pets.len());
             }
+
+            let presences = self.ecs().read_storage::<Presence>();
+            let presence = presences.get(entity);
+            if let Some(Presence {
+                kind: PresenceKind::Character(char_id),
+                ..
+            }) = presence
+            {
+                let mut battlemode_buffer = self.ecs().fetch_mut::<BattleModeBuffer>();
+                if let Some((mode, change)) = battlemode_buffer.pop(char_id) {
+                    let mut players = self.ecs().write_storage::<comp::Player>();
+                    if let Some(mut player_info) = players.get_mut(entity) {
+                        player_info.battle_mode = mode;
+                        player_info.last_battlemode_change = Some(change);
+                    }
+                }
+            }
         }
     }
 
diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs
index a32100a00e..682b8405a0 100644
--- a/server/src/sys/msg/register.rs
+++ b/server/src/sys/msg/register.rs
@@ -174,9 +174,10 @@ impl<'a> System<'a> for Sys {
                     return Ok(());
                 }
 
-                // FIXME:
-                // Take last battle_mode and last battle_mode change
-                // from in-memory persistence
+                // NOTE: this is just default value.
+                //
+                // It will be overwritten in ServerExt::update_character_data
+                // from stored last battlemode change if such exists.
                 let battle_mode = match read_data.settings.battle_mode {
                     ServerBattleMode::Global(mode) => mode,
                     ServerBattleMode::PerPlayer { default: mode } => mode,

From c87905305d0a836d5928041fe1aa73d4e3038ba6 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 4 Sep 2021 00:30:31 +0300
Subject: [PATCH 11/14] Improve UX of /battlemode

---
 server/src/cmd.rs | 29 ++++++++++++++++++++++-------
 1 file changed, 22 insertions(+), 7 deletions(-)

diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index c51cfcc9f8..66a0586a86 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -3105,7 +3105,11 @@ fn handle_battlemode(
     args: Vec<String>,
     _action: &ChatCommand,
 ) -> CmdResult<()> {
+    // TODO: discuss time
+    const COOLDOWN: f64 = 60.0 * 5.0;
+
     let ecs = server.state.ecs();
+    let time = ecs.read_resource::<Time>();
     let settings = ecs.read_resource::<Settings>();
     if let Some(mode) = parse_args!(args, String) {
         if !settings.battle_mode.allow_choosing() {
@@ -3152,10 +3156,7 @@ fn handle_battlemode(
         let mut player_info = players
             .get_mut(target)
             .ok_or("Cannot get player component for target")?;
-        let time = ecs.read_resource::<Time>();
         if let Some(Time(last_change)) = player_info.last_battlemode_change {
-            const COOLDOWN: f64 = 60.0 * 5.0;
-
             let Time(time) = *time;
             let elapsed = time - last_change;
             if elapsed < COOLDOWN {
@@ -3173,6 +3174,9 @@ fn handle_battlemode(
             "pve" => BattleMode::PvE,
             _ => return Err("Available modes: pvp, pve".to_owned()),
         };
+        if player_info.battle_mode == mode {
+            return Err("Attempted to set the same battlemode".to_owned());
+        }
         player_info.battle_mode = mode;
         player_info.last_battlemode_change = Some(*time);
         server.notify_client(
@@ -3188,12 +3192,23 @@ fn handle_battlemode(
         let player = players
             .get(target)
             .ok_or("Cannot get player component for target")?;
+        let mut msg = format!("Current battle mode: {:?}.", player.battle_mode);
+        if settings.battle_mode.allow_choosing() {
+            msg.push_str(" Possible to change.");
+        } else {
+            msg.push_str(" Global.");
+        }
+        if let Some(change) = player.last_battlemode_change {
+            let Time(time) = *time;
+            let Time(change) = change;
+            let elapsed = time - change;
+            let next = COOLDOWN - elapsed;
+            let notice = format!(" Next change will be available in: {:.0} seconds", next);
+            msg.push_str(&notice);
+        }
         server.notify_client(
             client,
-            ServerGeneral::server_msg(
-                ChatType::CommandInfo,
-                format!("Current battle mode: {:?}", player.battle_mode),
-            ),
+            ServerGeneral::server_msg(ChatType::CommandInfo, msg),
         );
         Ok(())
     }

From 182d3c4815f9b69a5d56cdc09b82603834c01919 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 4 Sep 2021 01:40:02 +0300
Subject: [PATCH 12/14] Workaround of bug with global Player component

* Set default value of battle_mode and last_battlemode_change explicitly
if isn't found in battlemode_buffer
---
 common/src/resources.rs        | 20 +-------------------
 server/src/events/player.rs    |  6 +++---
 server/src/lib.rs              | 24 +++++++++++++++++++++++-
 server/src/settings.rs         |  7 +++++++
 server/src/state_ext.rs        | 25 +++++++++++++++++++------
 server/src/sys/msg/register.rs |  9 ++-------
 6 files changed, 55 insertions(+), 36 deletions(-)

diff --git a/common/src/resources.rs b/common/src/resources.rs
index 688562c081..fee169e388 100644
--- a/common/src/resources.rs
+++ b/common/src/resources.rs
@@ -1,7 +1,5 @@
-use crate::character::CharacterId;
 #[cfg(not(target_arch = "wasm32"))]
 use crate::comp::Pos;
-use hashbrown::HashMap;
 use serde::{Deserialize, Serialize};
 #[cfg(not(target_arch = "wasm32"))]
 use specs::Entity;
@@ -77,27 +75,11 @@ pub struct PlayerPhysicsSettings {
     pub settings: hashbrown::HashMap<uuid::Uuid, PlayerPhysicsSetting>,
 }
 
-/// Store of BattleMode cooldowns for players while they go offline
-#[derive(Clone, Default, Debug)]
-pub struct BattleModeBuffer {
-    map: HashMap<CharacterId, (BattleMode, Time)>,
-}
-
-impl BattleModeBuffer {
-    pub fn push(&mut self, char_id: CharacterId, save: (BattleMode, Time)) {
-        self.map.insert(char_id, save);
-    }
-
-    pub fn pop(&mut self, char_id: &CharacterId) -> Option<(BattleMode, Time)> {
-        self.map.remove(char_id)
-    }
-}
-
 /// Describe how players interact with other players.
 ///
 /// May be removed when we will discover better way
 /// to handle duels and murders
-#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
+#[derive(PartialEq, Eq, Copy, Clone, Debug, Deserialize, Serialize)]
 pub enum BattleMode {
     PvP,
     PvE,
diff --git a/server/src/events/player.rs b/server/src/events/player.rs
index 517ec11819..0bd57edab1 100644
--- a/server/src/events/player.rs
+++ b/server/src/events/player.rs
@@ -1,12 +1,11 @@
 use super::Event;
 use crate::{
     client::Client, metrics::PlayerMetrics, persistence::character_updater::CharacterUpdater,
-    presence::Presence, state_ext::StateExt, Server,
+    presence::Presence, state_ext::StateExt, BattleModeBuffer, Server,
 };
 use common::{
     comp,
     comp::{group, pet::is_tameable},
-    resources::BattleModeBuffer,
     uid::{Uid, UidAllocator},
 };
 use common_base::span;
@@ -204,6 +203,7 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
         Some(player_uid),
         Some(player_info),
         mut character_updater,
+        mut battlemode_buffer,
     ) = (
         state.read_storage::<Presence>().get(entity),
         state.read_storage::<comp::SkillSet>().get(entity),
@@ -211,6 +211,7 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
         state.read_storage::<Uid>().get(entity),
         state.read_storage::<comp::Player>().get(entity),
         state.ecs().fetch_mut::<CharacterUpdater>(),
+        state.ecs().fetch_mut::<BattleModeBuffer>(),
     ) {
         match presence.kind {
             PresenceKind::Character(char_id) => {
@@ -220,7 +221,6 @@ fn persist_entity(state: &mut State, entity: EcsEntity) -> EcsEntity {
                     .get(entity)
                     .cloned();
                 // Store last battle mode change
-                let mut battlemode_buffer = state.ecs().fetch_mut::<BattleModeBuffer>();
                 if let Some(change) = player_info.last_battlemode_change {
                     let mode = player_info.battle_mode;
                     let save = (mode, change);
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 227a9b4379..736f2e7392 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -64,12 +64,13 @@ use crate::{
 use common::grid::Grid;
 use common::{
     assets::AssetExt,
+    character::CharacterId,
     cmd::ChatCommand,
     comp,
     comp::{item::MaterialStatManifest, CharacterAbility},
     event::{EventBus, ServerEvent},
     recipe::default_recipe_book,
-    resources::{BattleModeBuffer, TimeOfDay},
+    resources::{BattleMode, Time, TimeOfDay},
     rtsim::RtSimEntity,
     slowjob::SlowJobPool,
     terrain::{TerrainChunk, TerrainChunkSize},
@@ -109,6 +110,7 @@ use crate::{
     persistence::{DatabaseSettings, SqlLogMode},
     sys::terrain,
 };
+use hashbrown::HashMap;
 use std::sync::RwLock;
 
 #[cfg(feature = "plugins")]
@@ -152,6 +154,26 @@ enum DisconnectType {
 #[derive(Copy, Clone)]
 pub struct TickStart(Instant);
 
+/// Store of BattleMode cooldowns for players while they go offline
+#[derive(Clone, Default, Debug)]
+pub struct BattleModeBuffer {
+    map: HashMap<CharacterId, (BattleMode, Time)>,
+}
+
+impl BattleModeBuffer {
+    pub fn push(&mut self, char_id: CharacterId, save: (BattleMode, Time)) {
+        self.map.insert(char_id, save);
+    }
+
+    pub fn get(&self, char_id: &CharacterId) -> Option<&(BattleMode, Time)> {
+        self.map.get(char_id)
+    }
+
+    pub fn pop(&mut self, char_id: &CharacterId) -> Option<(BattleMode, Time)> {
+        self.map.remove(char_id)
+    }
+}
+
 pub struct Server {
     state: State,
     world: Arc<World>,
diff --git a/server/src/settings.rs b/server/src/settings.rs
index f2c7004d29..d3cd0ace6c 100644
--- a/server/src/settings.rs
+++ b/server/src/settings.rs
@@ -53,6 +53,13 @@ impl ServerBattleMode {
             ServerBattleMode::PerPlayer { .. } => true,
         }
     }
+
+    pub fn default_mode(&self) -> BattleMode {
+        match self {
+            ServerBattleMode::Global(mode) => *mode,
+            ServerBattleMode::PerPlayer { default: mode } => *mode,
+        }
+    }
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs
index 08ff295c36..d51463ae7e 100644
--- a/server/src/state_ext.rs
+++ b/server/src/state_ext.rs
@@ -5,7 +5,7 @@ use crate::{
     presence::{Presence, RepositionOnChunkLoad},
     settings::Settings,
     sys::sentinel::DeletedEntities,
-    wiring, SpawnPoint,
+    wiring, BattleModeBuffer, SpawnPoint,
 };
 use common::{
     character::CharacterId,
@@ -16,7 +16,7 @@ use common::{
         Group, Inventory, Poise,
     },
     effect::Effect,
-    resources::{BattleModeBuffer, TimeOfDay},
+    resources::TimeOfDay,
     slowjob::SlowJobPool,
     uid::{Uid, UidAllocator},
 };
@@ -575,12 +575,25 @@ impl StateExt for State {
                 ..
             }) = presence
             {
-                let mut battlemode_buffer = self.ecs().fetch_mut::<BattleModeBuffer>();
-                if let Some((mode, change)) = battlemode_buffer.pop(char_id) {
-                    let mut players = self.ecs().write_storage::<comp::Player>();
+                let battlemode_buffer = self.ecs().fetch::<BattleModeBuffer>();
+                let mut players = self.ecs().write_storage::<comp::Player>();
+                if let Some((mode, change)) = battlemode_buffer.get(char_id) {
+                    if let Some(mut player_info) = players.get_mut(entity) {
+                        player_info.battle_mode = *mode;
+                        player_info.last_battlemode_change = Some(*change);
+                    }
+                } else {
+                    // FIXME:
+                    // ???
+                    //
+                    // This probably shouldn't exist,
+                    // but without this code, character gets battle_mode from
+                    // another character on this account.
+                    let settings = self.ecs().read_resource::<Settings>();
+                    let mode = settings.battle_mode.default_mode();
                     if let Some(mut player_info) = players.get_mut(entity) {
                         player_info.battle_mode = mode;
-                        player_info.last_battlemode_change = Some(change);
+                        player_info.last_battlemode_change = None;
                     }
                 }
             }
diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs
index 682b8405a0..e5e0f5ea57 100644
--- a/server/src/sys/msg/register.rs
+++ b/server/src/sys/msg/register.rs
@@ -2,7 +2,6 @@ use crate::{
     client::Client,
     login_provider::{LoginProvider, PendingLogin},
     metrics::PlayerMetrics,
-    settings::ServerBattleMode,
     EditableSettings, Settings,
 };
 use common::{
@@ -176,12 +175,8 @@ impl<'a> System<'a> for Sys {
 
                 // NOTE: this is just default value.
                 //
-                // It will be overwritten in ServerExt::update_character_data
-                // from stored last battlemode change if such exists.
-                let battle_mode = match read_data.settings.battle_mode {
-                    ServerBattleMode::Global(mode) => mode,
-                    ServerBattleMode::PerPlayer { default: mode } => mode,
-                };
+                // It will be overwritten in ServerExt::update_character_data.
+                let battle_mode = read_data.settings.battle_mode.default_mode();
                 let player = Player::new(username, battle_mode, uuid, None);
 
                 let admin = read_data.editable_settings.admins.get(&uuid);

From 134699e1db568cadc5dd79725f4128e183fcb70d Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 4 Sep 2021 20:38:53 +0300
Subject: [PATCH 13/14] Adress review:

- explanation of what pvp/pve means in /battlemode help
- check for radius from town instead of town in chunks (because it count
  plant fields, which is kinda meh)
- better error displaying
---
 common/src/cmd.rs |  2 +-
 server/src/cmd.rs | 56 +++++++++++++++++++----------------------------
 2 files changed, 24 insertions(+), 34 deletions(-)

diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index b1a6890b74..caab209624 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -330,7 +330,7 @@ impl ChatCommand {
                     vec!["pvp".to_owned(), "pve".to_owned()],
                     Optional,
                 )],
-                "Set your battle mode to pvp/pve.\n\
+                "Set your battle mode to pvp (player vs player) or pve (player vs environment).\n\
                 If called without arguments will show current battle mode.",
                 None,
             ),
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 66a0586a86..160b06dfc0 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -53,7 +53,7 @@ use specs::{storage::StorageEntry, Builder, Entity as EcsEntity, Join, WorldExt}
 use std::str::FromStr;
 use vek::*;
 use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement};
-use world::{site::SiteKind, util::Sampler};
+use world::util::Sampler;
 
 use tracing::{error, info, warn};
 
@@ -3118,53 +3118,42 @@ fn handle_battlemode(
 
         #[cfg(feature = "worldgen")]
         let in_town = {
-            let world = &server.world;
-            let index = &server.index;
-            let sim = world.sim();
             // get chunk position
             let pos = position(server, target, "target")?;
             let wpos = pos.0.xy().map(|x| x as i32);
-            let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |pos, size: u32| {
-                pos / size as i32
+            let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |wpos, size: u32| {
+                wpos / size as i32
             });
-            let chunk = sim
-                .get(chunk_pos)
-                .ok_or("Cannot get current chunk for target")?;
-            // search for towns in chunk
-            let site_ids = &chunk.sites;
-            let mut in_town = false;
-            for site_id in site_ids.iter() {
-                // NOTE: this code finds town even if it is far from actual
-                // houses in settlement. Is it because of plant fields?
-                let site = index.sites.get(*site_id);
-                if matches!(site.kind, SiteKind::Settlement(_)) {
-                    in_town = true;
-                    break;
-                }
-            }
-            in_town
+            server.world.civs().sites().any(|site| {
+                // empirical
+                const RADIUS: f32 = 9.0;
+                let delta = site
+                    .center
+                    .map(|x| x as f32)
+                    .distance(chunk_pos.map(|x| x as f32));
+                delta < RADIUS
+            })
         };
         // just skip this check, if worldgen is disabled
         #[cfg(not(feature = "worldgen"))]
         let in_town = true;
 
         if !in_town {
-            return Err("Too far from town".to_owned());
+            return Err("You need to be in town to change battle mode!".to_owned());
         }
 
         let mut players = ecs.write_storage::<comp::Player>();
-        let mut player_info = players
-            .get_mut(target)
-            .ok_or("Cannot get player component for target")?;
+        let mut player_info = players.get_mut(target).ok_or_else(|| {
+            error!("Can't get player component for player");
+            "Error!"
+        })?;
         if let Some(Time(last_change)) = player_info.last_battlemode_change {
             let Time(time) = *time;
             let elapsed = time - last_change;
             if elapsed < COOLDOWN {
-                #[rustfmt::skip]
                 let msg = format!(
-                    "You can switch battlemode only once in {:.0} seconds. \
-                    Last change was {:.0} seconds before",
-                    COOLDOWN, elapsed,
+                    "Cooldown period active. Try again in {} second",
+                    COOLDOWN - elapsed,
                 );
                 return Err(msg);
             }
@@ -3189,9 +3178,10 @@ fn handle_battlemode(
         Ok(())
     } else {
         let players = ecs.read_storage::<comp::Player>();
-        let player = players
-            .get(target)
-            .ok_or("Cannot get player component for target")?;
+        let player = players.get(target).ok_or_else(|| {
+            error!("Can't get player component for player");
+            "Error!"
+        })?;
         let mut msg = format!("Current battle mode: {:?}.", player.battle_mode);
         if settings.battle_mode.allow_choosing() {
             msg.push_str(" Possible to change.");

From 69f18c8fe660d02af4463df9bc4a9ac13c77a4b0 Mon Sep 17 00:00:00 2001
From: juliancoffee <lightdarkdaughter@gmail.com>
Date: Sat, 4 Sep 2021 20:56:55 +0300
Subject: [PATCH 14/14] Better formatting

---
 common/src/cmd.rs | 4 +++-
 server/src/cmd.rs | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/common/src/cmd.rs b/common/src/cmd.rs
index caab209624..614d86bb72 100644
--- a/common/src/cmd.rs
+++ b/common/src/cmd.rs
@@ -330,7 +330,9 @@ impl ChatCommand {
                     vec!["pvp".to_owned(), "pve".to_owned()],
                     Optional,
                 )],
-                "Set your battle mode to pvp (player vs player) or pve (player vs environment).\n\
+                "Set your battle mode to:\n\
+                * pvp (player vs player)\n\
+                * pve (player vs environment).\n\
                 If called without arguments will show current battle mode.",
                 None,
             ),
diff --git a/server/src/cmd.rs b/server/src/cmd.rs
index 160b06dfc0..e17ef01967 100644
--- a/server/src/cmd.rs
+++ b/server/src/cmd.rs
@@ -3152,7 +3152,7 @@ fn handle_battlemode(
             let elapsed = time - last_change;
             if elapsed < COOLDOWN {
                 let msg = format!(
-                    "Cooldown period active. Try again in {} second",
+                    "Cooldown period active. Try again in {:.0} seconds",
                     COOLDOWN - elapsed,
                 );
                 return Err(msg);