X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=src%2Fcontent_sao.cpp;h=5119223a7c43493addcdfd1b02c74eaf27f4362b;hb=ac5f53e364838b52096f86a0313cae452c7d62b8;hp=81c6902f58b4c2b438ac38b157fa3122d0ea8e49;hpb=2818d3f2244d2146a5cdb61cd41f6561c514f97c;p=dragonfireclient.git diff --git a/src/content_sao.cpp b/src/content_sao.cpp index 81c6902f5..5119223a7 100644 --- a/src/content_sao.cpp +++ b/src/content_sao.cpp @@ -28,6 +28,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server.h" #include "scripting_server.h" #include "genericobject.h" +#include "settings.h" +#include +#include std::map ServerActiveObject::m_types; @@ -59,7 +62,7 @@ class TestSAO : public ServerActiveObject m_age += dtime; if(m_age > 10) { - m_removed = true; + m_pending_removal = true; return; } @@ -67,7 +70,7 @@ class TestSAO : public ServerActiveObject if(m_base_position.Y > 8*BS) m_base_position.Y = 2*BS; - if(send_recommended == false) + if (!send_recommended) return; m_timer1 -= dtime; @@ -91,6 +94,9 @@ class TestSAO : public ServerActiveObject } bool getCollisionBox(aabb3f *toset) const { return false; } + + virtual bool getSelectionBox(aabb3f *toset) const { return false; } + bool collideWithObjects() const { return false; } private: @@ -106,33 +112,20 @@ TestSAO proto_TestSAO(NULL, v3f(0,0,0)); */ UnitSAO::UnitSAO(ServerEnvironment *env, v3f pos): - ServerActiveObject(env, pos), - m_hp(-1), - m_yaw(0), - m_properties_sent(true), - m_armor_groups_sent(false), - m_animation_range(0,0), - m_animation_speed(0), - m_animation_blend(0), - m_animation_loop(true), - m_animation_sent(false), - m_bone_position_sent(false), - m_attachment_parent_id(0), - m_attachment_sent(false) + ServerActiveObject(env, pos) { // Initialize something to armor groups m_armor_groups["fleshy"] = 100; } -bool UnitSAO::isAttached() const +ServerActiveObject *UnitSAO::getParent() const { if (!m_attachment_parent_id) - return false; + return nullptr; // Check if the parent still exists ServerActiveObject *obj = m_env->getActiveObject(m_attachment_parent_id); - if (obj) - return true; - return false; + + return obj; } void UnitSAO::setArmorGroups(const ItemGroupList &armor_groups) @@ -141,7 +134,7 @@ void UnitSAO::setArmorGroups(const ItemGroupList &armor_groups) m_armor_groups_sent = false; } -const ItemGroupList &UnitSAO::getArmorGroups() +const ItemGroupList &UnitSAO::getArmorGroups() const { return m_armor_groups; } @@ -164,6 +157,12 @@ void UnitSAO::getAnimation(v2f *frame_range, float *frame_speed, float *frame_bl *frame_loop = m_animation_loop; } +void UnitSAO::setAnimationSpeed(float frame_speed) +{ + m_animation_speed = frame_speed; + m_animation_speed_sent = false; +} + void UnitSAO::setBonePosition(const std::string &bone, v3f position, v3f rotation) { // store these so they can be updated to clients @@ -187,15 +186,21 @@ void UnitSAO::setAttachment(int parent_id, const std::string &bone, v3f position // This breaks some things so we also give the server the most accurate representation // even if players only see the client changes. + int old_parent = m_attachment_parent_id; m_attachment_parent_id = parent_id; m_attachment_bone = bone; m_attachment_position = position; m_attachment_rotation = rotation; m_attachment_sent = false; + + if (parent_id != old_parent) { + onDetach(old_parent); + onAttach(parent_id); + } } void UnitSAO::getAttachment(int *parent_id, std::string *bone, v3f *position, - v3f *rotation) + v3f *rotation) const { *parent_id = m_attachment_parent_id; *bone = m_attachment_bone; @@ -203,6 +208,30 @@ void UnitSAO::getAttachment(int *parent_id, std::string *bone, v3f *position, *rotation = m_attachment_rotation; } +void UnitSAO::clearChildAttachments() +{ + for (int child_id : m_attachment_child_ids) { + // Child can be NULL if it was deleted earlier + if (ServerActiveObject *child = m_env->getActiveObject(child_id)) + child->setAttachment(0, "", v3f(0, 0, 0), v3f(0, 0, 0)); + } + m_attachment_child_ids.clear(); +} + +void UnitSAO::clearParentAttachment() +{ + ServerActiveObject *parent = nullptr; + if (m_attachment_parent_id) { + parent = m_env->getActiveObject(m_attachment_parent_id); + setAttachment(0, "", m_attachment_position, m_attachment_rotation); + } else { + setAttachment(0, "", v3f(0, 0, 0), v3f(0, 0, 0)); + } + // Do it + if (parent) + parent->removeAttachmentChild(m_id); +} + void UnitSAO::addAttachmentChild(int child_id) { m_attachment_child_ids.insert(child_id); @@ -213,11 +242,43 @@ void UnitSAO::removeAttachmentChild(int child_id) m_attachment_child_ids.erase(child_id); } -const UNORDERED_SET &UnitSAO::getAttachmentChildIds() +const std::unordered_set &UnitSAO::getAttachmentChildIds() const { return m_attachment_child_ids; } +void UnitSAO::onAttach(int parent_id) +{ + if (!parent_id) + return; + + ServerActiveObject *parent = m_env->getActiveObject(parent_id); + + if (!parent || parent->isGone()) + return; // Do not try to notify soon gone parent + + if (parent->getType() == ACTIVEOBJECT_TYPE_LUAENTITY) { + // Call parent's on_attach field + m_env->getScriptIface()->luaentity_on_attach_child(parent_id, this); + } +} + +void UnitSAO::onDetach(int parent_id) +{ + if (!parent_id) + return; + + ServerActiveObject *parent = m_env->getActiveObject(parent_id); + if (getType() == ACTIVEOBJECT_TYPE_LUAENTITY) + m_env->getScriptIface()->luaentity_on_detach(m_id, parent); + + if (!parent || parent->isGone()) + return; // Do not try to notify soon gone parent + + if (parent->getType() == ACTIVEOBJECT_TYPE_LUAENTITY) + m_env->getScriptIface()->luaentity_on_detach_child(parent_id, this); +} + ObjectProperties* UnitSAO::accessObjectProperties() { return &m_prop; @@ -239,16 +300,7 @@ LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &name, const std::string &state): UnitSAO(env, pos), m_init_name(name), - m_init_state(state), - m_registered(false), - m_velocity(0,0,0), - m_acceleration(0,0,0), - m_last_sent_yaw(0), - m_last_sent_position(0,0,0), - m_last_sent_velocity(0,0,0), - m_last_sent_position_timer(0), - m_last_sent_move_precision(0), - m_current_texture_modifier("") + m_init_state(state) { // Only register type if no environment supplied if(env == NULL){ @@ -263,9 +315,8 @@ LuaEntitySAO::~LuaEntitySAO() m_env->getScriptIface()->luaentity_Remove(m_id); } - for (UNORDERED_SET::iterator it = m_attached_particle_spawners.begin(); - it != m_attached_particle_spawners.end(); ++it) { - m_env->deleteParticleSpawner(*it, false); + for (u32 attached_particle_spawner : m_attached_particle_spawners) { + m_env->deleteParticleSpawner(attached_particle_spawner, false); } } @@ -280,12 +331,12 @@ void LuaEntitySAO::addedToEnvironment(u32 dtime_s) if(m_registered){ // Get properties m_env->getScriptIface()-> - luaentity_GetProperties(m_id, &m_prop); + luaentity_GetProperties(m_id, this, &m_prop); // Initialize HP from properties m_hp = m_prop.hp_max; // Activate entity, supplying serialized state m_env->getScriptIface()-> - luaentity_Activate(m_id, m_init_state.c_str(), dtime_s); + luaentity_Activate(m_id, m_init_state, dtime_s); } else { m_prop.infotext = m_init_name; } @@ -296,33 +347,50 @@ ServerActiveObject* LuaEntitySAO::create(ServerEnvironment *env, v3f pos, { std::string name; std::string state; - s16 hp = 1; + u16 hp = 1; v3f velocity; - float yaw = 0; - if(data != ""){ + v3f rotation; + + while (!data.empty()) { // breakable, run for one iteration std::istringstream is(data, std::ios::binary); - // read version + // 'version' does not allow to incrementally extend the parameter list thus + // we need another variable to build on top of 'version=1'. Ugly hack but works™ + u8 version2 = 0; u8 version = readU8(is); - // check if version is supported - if(version == 0){ - name = deSerializeString(is); - state = deSerializeLongString(is); - } - else if(version == 1){ - name = deSerializeString(is); - state = deSerializeLongString(is); - hp = readS16(is); - velocity = readV3F1000(is); - yaw = readF1000(is); - } + + name = deSerializeString(is); + state = deSerializeLongString(is); + + if (version < 1) + break; + + hp = readU16(is); + velocity = readV3F1000(is); + // yaw must be yaw to be backwards-compatible + rotation.Y = readF1000(is); + + if (is.good()) // EOF for old formats + version2 = readU8(is); + + if (version2 < 1) // PROTOCOL_VERSION < 37 + break; + + // version2 >= 1 + rotation.X = readF1000(is); + rotation.Z = readF1000(is); + + // if (version2 < 2) + // break; + // + break; } // create object - infostream<<"LuaEntitySAO::create(name=\""<m_hp = hp; sao->m_velocity = velocity; - sao->m_yaw = yaw; + sao->m_rotation = rotation; return sao; } @@ -384,20 +452,20 @@ void LuaEntitySAO::step(float dtime, bool send_recommended) m_velocity += dtime * m_acceleration; } - if((m_prop.automatic_face_movement_dir) && - (fabs(m_velocity.Z) > 0.001 || fabs(m_velocity.X) > 0.001)) - { - float optimal_yaw = atan2(m_velocity.Z,m_velocity.X) * 180 / M_PI - + m_prop.automatic_face_movement_dir_offset; - float max_rotation_delta = - dtime * m_prop.automatic_face_movement_max_rotation_per_sec; - - if ((m_prop.automatic_face_movement_max_rotation_per_sec > 0) && - (fabs(m_yaw - optimal_yaw) > max_rotation_delta)) { - - m_yaw = optimal_yaw < m_yaw ? m_yaw - max_rotation_delta : m_yaw + max_rotation_delta; + if (m_prop.automatic_face_movement_dir && + (fabs(m_velocity.Z) > 0.001 || fabs(m_velocity.X) > 0.001)) { + float target_yaw = atan2(m_velocity.Z, m_velocity.X) * 180 / M_PI + + m_prop.automatic_face_movement_dir_offset; + float max_rotation_per_sec = + m_prop.automatic_face_movement_max_rotation_per_sec; + + if (max_rotation_per_sec > 0) { + m_rotation.Y = wrapDegrees_0_360(m_rotation.Y); + wrappedApproachShortest(m_rotation.Y, target_yaw, + dtime * max_rotation_per_sec, 360.f); } else { - m_yaw = optimal_yaw; + // Negative values of max_rotation_per_sec mean disabled. + m_rotation.Y = target_yaw; } } } @@ -406,7 +474,7 @@ void LuaEntitySAO::step(float dtime, bool send_recommended) m_env->getScriptIface()->luaentity_Step(m_id, dtime); } - if(send_recommended == false) + if (!send_recommended) return; if(!isAttached()) @@ -421,13 +489,16 @@ void LuaEntitySAO::step(float dtime, bool send_recommended) float move_d = m_base_position.getDistanceFrom(m_last_sent_position); move_d += m_last_sent_move_precision; float vel_d = m_velocity.getDistanceFrom(m_last_sent_velocity); - if(move_d > minchange || vel_d > minchange || - fabs(m_yaw - m_last_sent_yaw) > 1.0){ + if (move_d > minchange || vel_d > minchange || + std::fabs(m_rotation.X - m_last_sent_rotation.X) > 1.0f || + std::fabs(m_rotation.Y - m_last_sent_rotation.Y) > 1.0f || + std::fabs(m_rotation.Z - m_last_sent_rotation.Z) > 1.0f) { + sendPosition(true, false); } } - if(m_armor_groups_sent == false){ + if (!m_armor_groups_sent) { m_armor_groups_sent = true; std::string str = gob_cmd_update_armor_groups( m_armor_groups); @@ -436,7 +507,7 @@ void LuaEntitySAO::step(float dtime, bool send_recommended) m_messages_out.push(aom); } - if(m_animation_sent == false){ + if (!m_animation_sent) { m_animation_sent = true; std::string str = gob_cmd_update_animation( m_animation_range, m_animation_speed, m_animation_blend, m_animation_loop); @@ -445,9 +516,17 @@ void LuaEntitySAO::step(float dtime, bool send_recommended) m_messages_out.push(aom); } - if(m_bone_position_sent == false){ + if (!m_animation_speed_sent) { + m_animation_speed_sent = true; + std::string str = gob_cmd_update_animation_speed(m_animation_speed); + // create message and add to list + ActiveObjectMessage aom(getId(), true, str); + m_messages_out.push(aom); + } + + if (!m_bone_position_sent) { m_bone_position_sent = true; - for (UNORDERED_MAP >::const_iterator + for (std::unordered_map>::const_iterator ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii){ std::string str = gob_cmd_update_bone_position((*ii).first, (*ii).second.X, (*ii).second.Y); @@ -457,7 +536,7 @@ void LuaEntitySAO::step(float dtime, bool send_recommended) } } - if(m_attachment_sent == false){ + if (!m_attachment_sent) { m_attachment_sent = true; std::string str = gob_cmd_update_attachment(m_attachment_parent_id, m_attachment_bone, m_attachment_position, m_attachment_rotation); // create message and add to list @@ -470,21 +549,21 @@ std::string LuaEntitySAO::getClientInitializationData(u16 protocol_version) { std::ostringstream os(std::ios::binary); - // protocol >= 14 + // PROTOCOL_VERSION >= 37 writeU8(os, 1); // version os << serializeString(""); // name writeU8(os, 0); // is_player - writeS16(os, getId()); //id - writeV3F1000(os, m_base_position); - writeF1000(os, m_yaw); - writeS16(os, m_hp); + writeU16(os, getId()); //id + writeV3F32(os, m_base_position); + writeV3F32(os, m_rotation); + writeU16(os, m_hp); std::ostringstream msg_os(std::ios::binary); msg_os << serializeLongString(getPropertyPacket()); // message 1 msg_os << serializeLongString(gob_cmd_update_armor_groups(m_armor_groups)); // 2 msg_os << serializeLongString(gob_cmd_update_animation( m_animation_range, m_animation_speed, m_animation_blend, m_animation_loop)); // 3 - for (UNORDERED_MAP >::const_iterator + for (std::unordered_map>::const_iterator ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii) { msg_os << serializeLongString(gob_cmd_update_bone_position((*ii).first, (*ii).second.X, (*ii).second.Y)); // m_bone_position.size @@ -492,10 +571,12 @@ std::string LuaEntitySAO::getClientInitializationData(u16 protocol_version) msg_os << serializeLongString(gob_cmd_update_attachment(m_attachment_parent_id, m_attachment_bone, m_attachment_position, m_attachment_rotation)); // 4 int message_count = 4 + m_bone_position.size(); - for (UNORDERED_SET::const_iterator ii = m_attachment_child_ids.begin(); + for (std::unordered_set::const_iterator ii = m_attachment_child_ids.begin(); (ii != m_attachment_child_ids.end()); ++ii) { if (ServerActiveObject *obj = m_env->getActiveObject(*ii)) { message_count++; + // TODO after a protocol bump: only send the object initialization data + // to older clients (superfluous since this message exists) msg_os << serializeLongString(gob_cmd_update_infant(*ii, obj->getSendType(), obj->getClientInitializationData(protocol_version))); } @@ -515,7 +596,7 @@ void LuaEntitySAO::getStaticData(std::string *result) const { verbosestream<= 37 + + writeF1000(os, m_rotation.X); + writeF1000(os, m_rotation.Z); + + // + *result = os.str(); } -int LuaEntitySAO::punch(v3f dir, +u16 LuaEntitySAO::punch(v3f dir, const ToolCapabilities *toolcap, ServerActiveObject *puncher, float time_from_last_punch) { - if (!m_registered){ + if (!m_registered) { // Delete unknown LuaEntities when punched - m_removed = true; + m_pending_removal = true; return 0; } - // It's best that attachments cannot be punched - if (isAttached()) - return 0; + FATAL_ERROR_IF(!puncher, "Punch action called without SAO"); - ItemStack *punchitem = NULL; - ItemStack punchitem_static; - if (puncher) { - punchitem_static = puncher->getWieldedItem(); - punchitem = &punchitem_static; - } + s32 old_hp = getHP(); + ItemStack selected_item, hand_item; + ItemStack tool_item = puncher->getWieldedItem(&selected_item, &hand_item); PunchDamageResult result = getPunchDamage( m_armor_groups, toolcap, - punchitem, + &tool_item, time_from_last_punch); bool damage_handled = m_env->getScriptIface()->luaentity_Punch(m_id, puncher, @@ -569,27 +652,28 @@ int LuaEntitySAO::punch(v3f dir, if (!damage_handled) { if (result.did_punch) { - setHP(getHP() - result.damage); - - if (result.damage > 0) { - std::string punchername = puncher ? puncher->getDescription() : "nil"; + setHP((s32)getHP() - result.damage, + PlayerHPChangeReason(PlayerHPChangeReason::SET_HP)); - actionstream << getDescription() << " punched by " - << punchername << ", damage " << result.damage - << " hp, health now " << getHP() << " hp" << std::endl; - } - - std::string str = gob_cmd_punched(result.damage, getHP()); + std::string str = gob_cmd_punched(getHP()); // create message and add to list ActiveObjectMessage aom(getId(), true, str); m_messages_out.push(aom); } } - if (getHP() == 0) - m_removed = true; - + if (getHP() == 0 && !isGone()) { + m_pending_removal = true; + clearParentAttachment(); + clearChildAttachments(); + m_env->getScriptIface()->luaentity_on_death(m_id, puncher); + } + actionstream << puncher->getDescription() << " (id=" << puncher->getId() << + ", hp=" << puncher->getHP() << ") punched " << + getDescription() << " (id=" << m_id << ", hp=" << m_hp << + "), damage=" << (old_hp - (s32)getHP()) << + (damage_handled ? " (handled by Lua)" : "") << std::endl; return result.wear; } @@ -598,9 +682,7 @@ void LuaEntitySAO::rightClick(ServerActiveObject *clicker) { if (!m_registered) return; - // It's best that attachments cannot be clicked - if (isAttached()) - return; + m_env->getScriptIface()->luaentity_Rightclick(m_id, clicker); } @@ -637,13 +719,12 @@ std::string LuaEntitySAO::getDescription() return os.str(); } -void LuaEntitySAO::setHP(s16 hp) +void LuaEntitySAO::setHP(s32 hp, const PlayerHPChangeReason &reason) { - if(hp < 0) hp = 0; - m_hp = hp; + m_hp = rangelim(hp, 0, U16_MAX); } -s16 LuaEntitySAO::getHP() const +u16 LuaEntitySAO::getHP() const { return m_hp; } @@ -715,10 +796,10 @@ void LuaEntitySAO::sendPosition(bool do_interpolate, bool is_movement_end) m_last_sent_move_precision = m_base_position.getDistanceFrom( m_last_sent_position); m_last_sent_position_timer = 0; - m_last_sent_yaw = m_yaw; m_last_sent_position = m_base_position; m_last_sent_velocity = m_velocity; //m_last_sent_acceleration = m_acceleration; + m_last_sent_rotation = m_rotation; float update_interval = m_env->getSendRecommendedInterval(); @@ -726,7 +807,7 @@ void LuaEntitySAO::sendPosition(bool do_interpolate, bool is_movement_end) m_base_position, m_velocity, m_acceleration, - m_yaw, + m_rotation, do_interpolate, is_movement_end, update_interval @@ -753,6 +834,18 @@ bool LuaEntitySAO::getCollisionBox(aabb3f *toset) const return false; } +bool LuaEntitySAO::getSelectionBox(aabb3f *toset) const +{ + if (!m_prop.is_visible || !m_prop.pointable) { + return false; + } + + toset->MinEdge = m_prop.selectionbox.MinEdge * BS; + toset->MaxEdge = m_prop.selectionbox.MaxEdge * BS; + + return true; +} + bool LuaEntitySAO::collideWithObjects() const { return m_prop.collideWithObjects; @@ -764,60 +857,43 @@ bool LuaEntitySAO::collideWithObjects() const // No prototype, PlayerSAO does not need to be deserialized -PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, u16 peer_id_, +PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, session_t peer_id_, bool is_singleplayer): UnitSAO(env_, v3f(0,0,0)), m_player(player_), m_peer_id(peer_id_), - m_inventory(NULL), - m_damage(0), - m_last_good_position(0,0,0), - m_time_from_last_teleport(0), - m_time_from_last_punch(0), - m_nocheat_dig_pos(32767, 32767, 32767), - m_nocheat_dig_time(0), - m_wield_index(0), - m_position_not_sent(false), - m_is_singleplayer(is_singleplayer), - m_breath(PLAYER_MAX_BREATH), - m_pitch(0), - m_fov(0), - m_wanted_range(0), - m_extended_attributes_modified(false), - // public - m_physics_override_speed(1), - m_physics_override_jump(1), - m_physics_override_gravity(1), - m_physics_override_sneak(true), - m_physics_override_sneak_glitch(false), - m_physics_override_new_move(true), - m_physics_override_sent(false) -{ - assert(m_peer_id != 0); // pre-condition - - m_prop.hp_max = PLAYER_MAX_HP; + m_is_singleplayer(is_singleplayer) +{ + SANITY_CHECK(m_peer_id != PEER_ID_INEXISTENT); + + m_prop.hp_max = PLAYER_MAX_HP_DEFAULT; + m_prop.breath_max = PLAYER_MAX_BREATH_DEFAULT; m_prop.physical = false; - m_prop.weight = 75; - m_prop.collisionbox = aabb3f(-1/3.,-1.0,-1/3., 1/3.,1.0,1/3.); - // start of default appearance, this should be overwritten by LUA + m_prop.collisionbox = aabb3f(-0.3f, 0.0f, -0.3f, 0.3f, 1.77f, 0.3f); + m_prop.selectionbox = aabb3f(-0.3f, 0.0f, -0.3f, 0.3f, 1.77f, 0.3f); + m_prop.pointable = true; + // Start of default appearance, this should be overwritten by Lua m_prop.visual = "upright_sprite"; - m_prop.visual_size = v2f(1, 2); + m_prop.visual_size = v3f(1, 2, 1); m_prop.textures.clear(); - m_prop.textures.push_back("player.png"); - m_prop.textures.push_back("player_back.png"); + m_prop.textures.emplace_back("player.png"); + m_prop.textures.emplace_back("player_back.png"); m_prop.colors.clear(); - m_prop.colors.push_back(video::SColor(255, 255, 255, 255)); + m_prop.colors.emplace_back(255, 255, 255, 255); m_prop.spritediv = v2s16(1,1); - // end of default appearance + m_prop.eye_height = 1.625f; + // End of default appearance m_prop.is_visible = true; + m_prop.backface_culling = false; m_prop.makes_footstep_sound = true; - m_hp = PLAYER_MAX_HP; -} + m_prop.stepheight = PLAYER_DEFAULT_STEPHEIGHT * BS; + m_hp = m_prop.hp_max; + m_breath = m_prop.breath_max; + // Disable zoom in survival mode using a value of 0 + m_prop.zoom_fov = g_settings->getBool("creative_mode") ? 15.0f : 0.0f; -PlayerSAO::~PlayerSAO() -{ - if(m_inventory != &m_player->inventory) - delete m_inventory; + if (!g_settings->getBool("enable_damage")) + m_armor_groups["immortal"] = 1; } void PlayerSAO::finalize(RemotePlayer *player, const std::set &privs) @@ -825,12 +901,11 @@ void PlayerSAO::finalize(RemotePlayer *player, const std::set &priv assert(player); m_player = player; m_privs = privs; - m_inventory = &m_player->inventory; } v3f PlayerSAO::getEyeOffset() const { - return v3f(0, BS * 1.625f, 0); + return v3f(0, BS * m_prop.eye_height, 0); } std::string PlayerSAO::getDescription() @@ -844,7 +919,7 @@ void PlayerSAO::addedToEnvironment(u32 dtime_s) ServerActiveObject::addedToEnvironment(dtime_s); ServerActiveObject::setBasePosition(m_base_position); m_player->setPlayerSAO(this); - m_player->peer_id = m_peer_id; + m_player->setPeerId(m_peer_id); m_last_good_position = m_base_position; } @@ -854,9 +929,8 @@ void PlayerSAO::removingFromEnvironment() ServerActiveObject::removingFromEnvironment(); if (m_player->getPlayerSAO() == this) { unlinkPlayerSessionAndSave(); - for (UNORDERED_SET::iterator it = m_attached_particle_spawners.begin(); - it != m_attached_particle_spawners.end(); ++it) { - m_env->deleteParticleSpawner(*it, false); + for (u32 attached_particle_spawner : m_attached_particle_spawners) { + m_env->deleteParticleSpawner(attached_particle_spawner, false); } } } @@ -869,17 +943,17 @@ std::string PlayerSAO::getClientInitializationData(u16 protocol_version) writeU8(os, 1); // version os << serializeString(m_player->getName()); // name writeU8(os, 1); // is_player - writeS16(os, getId()); //id - writeV3F1000(os, m_base_position + v3f(0,BS*1,0)); - writeF1000(os, m_yaw); - writeS16(os, getHP()); + writeS16(os, getId()); // id + writeV3F32(os, m_base_position); + writeV3F32(os, m_rotation); + writeU16(os, getHP()); std::ostringstream msg_os(std::ios::binary); msg_os << serializeLongString(getPropertyPacket()); // message 1 msg_os << serializeLongString(gob_cmd_update_armor_groups(m_armor_groups)); // 2 msg_os << serializeLongString(gob_cmd_update_animation( m_animation_range, m_animation_speed, m_animation_blend, m_animation_loop)); // 3 - for (UNORDERED_MAP >::const_iterator + for (std::unordered_map>::const_iterator ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii) { msg_os << serializeLongString(gob_cmd_update_bone_position((*ii).first, (*ii).second.X, (*ii).second.Y)); // m_bone_position.size @@ -892,7 +966,7 @@ std::string PlayerSAO::getClientInitializationData(u16 protocol_version) // (GENERIC_CMD_UPDATE_NAMETAG_ATTRIBUTES) : Deprecated, for backwards compatibility only. msg_os << serializeLongString(gob_cmd_update_nametag_attributes(m_prop.nametag_color)); // 6 int message_count = 6 + m_bone_position.size(); - for (UNORDERED_SET::const_iterator ii = m_attachment_child_ids.begin(); + for (std::unordered_set::const_iterator ii = m_attachment_child_ids.begin(); ii != m_attachment_child_ids.end(); ++ii) { if (ServerActiveObject *obj = m_env->getActiveObject(*ii)) { message_count++; @@ -908,17 +982,17 @@ std::string PlayerSAO::getClientInitializationData(u16 protocol_version) return os.str(); } -void PlayerSAO::getStaticData(std::string *result) const +void PlayerSAO::getStaticData(std::string * result) const { - FATAL_ERROR("Deprecated function"); + FATAL_ERROR("Obsolete function"); } void PlayerSAO::step(float dtime, bool send_recommended) { - if (m_drowning_interval.step(dtime, 2.0)) { - // get head position - v3s16 p = floatToInt(m_base_position + v3f(0, BS * 1.6, 0), BS); - MapNode n = m_env->getMap().getNodeNoEx(p); + if (!isImmortal() && m_drowning_interval.step(dtime, 2.0f)) { + // Get nose/mouth position, approximate with eye position + v3s16 p = floatToInt(getEyePosition(), BS); + MapNode n = m_env->getMap().getNode(p); const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n); // If node generates drown if (c.drowning > 0 && m_hp > 0) { @@ -927,43 +1001,58 @@ void PlayerSAO::step(float dtime, bool send_recommended) // No more breath, damage player if (m_breath == 0) { - setHP(m_hp - c.drowning); - m_env->getGameDef()->SendPlayerHPOrDie(this); + PlayerHPChangeReason reason(PlayerHPChangeReason::DROWNING); + setHP(m_hp - c.drowning, reason); + m_env->getGameDef()->SendPlayerHPOrDie(this, reason); } } } - if (m_breathing_interval.step(dtime, 0.5)) { - // get head position - v3s16 p = floatToInt(m_base_position + v3f(0, BS * 1.6, 0), BS); - MapNode n = m_env->getMap().getNodeNoEx(p); + if (m_breathing_interval.step(dtime, 0.5f) && !isImmortal()) { + // Get nose/mouth position, approximate with eye position + v3s16 p = floatToInt(getEyePosition(), BS); + MapNode n = m_env->getMap().getNode(p); const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n); - // If player is alive & no drowning, breath - if (m_hp > 0 && m_breath < PLAYER_MAX_BREATH && c.drowning == 0) + // If player is alive & not drowning & not in ignore & not immortal, breathe + if (m_breath < m_prop.breath_max && c.drowning == 0 && + n.getContent() != CONTENT_IGNORE && m_hp > 0) setBreath(m_breath + 1); } - if (m_node_hurt_interval.step(dtime, 1.0)) { - // Feet, middle and head - v3s16 p1 = floatToInt(m_base_position + v3f(0, BS*0.1, 0), BS); - MapNode n1 = m_env->getMap().getNodeNoEx(p1); - v3s16 p2 = floatToInt(m_base_position + v3f(0, BS*0.8, 0), BS); - MapNode n2 = m_env->getMap().getNodeNoEx(p2); - v3s16 p3 = floatToInt(m_base_position + v3f(0, BS*1.6, 0), BS); - MapNode n3 = m_env->getMap().getNodeNoEx(p3); - + if (!isImmortal() && m_node_hurt_interval.step(dtime, 1.0f)) { u32 damage_per_second = 0; - damage_per_second = MYMAX(damage_per_second, - m_env->getGameDef()->ndef()->get(n1).damage_per_second); - damage_per_second = MYMAX(damage_per_second, - m_env->getGameDef()->ndef()->get(n2).damage_per_second); - damage_per_second = MYMAX(damage_per_second, - m_env->getGameDef()->ndef()->get(n3).damage_per_second); + std::string nodename; + // Lowest and highest damage points are 0.1 within collisionbox + float dam_top = m_prop.collisionbox.MaxEdge.Y - 0.1f; + + // Sequence of damage points, starting 0.1 above feet and progressing + // upwards in 1 node intervals, stopping below top damage point. + for (float dam_height = 0.1f; dam_height < dam_top; dam_height++) { + v3s16 p = floatToInt(m_base_position + + v3f(0.0f, dam_height * BS, 0.0f), BS); + MapNode n = m_env->getMap().getNode(p); + const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n); + if (c.damage_per_second > damage_per_second) { + damage_per_second = c.damage_per_second; + nodename = c.name; + } + } + + // Top damage point + v3s16 ptop = floatToInt(m_base_position + + v3f(0.0f, dam_top * BS, 0.0f), BS); + MapNode ntop = m_env->getMap().getNode(ptop); + const ContentFeatures &c = m_env->getGameDef()->ndef()->get(ntop); + if (c.damage_per_second > damage_per_second) { + damage_per_second = c.damage_per_second; + nodename = c.name; + } if (damage_per_second != 0 && m_hp > 0) { - s16 newhp = ((s32) damage_per_second > m_hp ? 0 : m_hp - damage_per_second); - setHP(newhp); - m_env->getGameDef()->SendPlayerHPOrDie(this); + s32 newhp = (s32)m_hp - (s32)damage_per_second; + PlayerHPChangeReason reason(PlayerHPChangeReason::NODE_DAMAGE, nodename); + setHP(newhp, reason); + m_env->getGameDef()->SendPlayerHPOrDie(this, reason); } } @@ -973,15 +1062,15 @@ void PlayerSAO::step(float dtime, bool send_recommended) // create message and add to list ActiveObjectMessage aom(getId(), true, str); m_messages_out.push(aom); + m_env->getScriptIface()->player_event(this, "properties_changed"); } // If attached, check that our parent is still there. If it isn't, detach. - if(m_attachment_parent_id && !isAttached()) - { + if (m_attachment_parent_id && !isAttached()) { m_attachment_parent_id = 0; m_attachment_bone = ""; - m_attachment_position = v3f(0,0,0); - m_attachment_rotation = v3f(0,0,0); + m_attachment_position = v3f(0.0f, 0.0f, 0.0f); + m_attachment_rotation = v3f(0.0f, 0.0f, 0.0f); setBasePosition(m_last_good_position); m_env->getGameDef()->SendMovePlayer(m_peer_id); } @@ -989,8 +1078,8 @@ void PlayerSAO::step(float dtime, bool send_recommended) //dstream<<"PlayerSAO::step: dtime: "<getMaxLagEstimate() * 2.0; + const float LAG_POOL_MIN = 5.0f; + float lag_pool_max = m_env->getMaxLagEstimate() * 2.0f; if(lag_pool_max < LAG_POOL_MIN) lag_pool_max = LAG_POOL_MIN; m_dig_pool.setMax(lag_pool_max); @@ -1002,9 +1091,12 @@ void PlayerSAO::step(float dtime, bool send_recommended) m_time_from_last_teleport += dtime; m_time_from_last_punch += dtime; m_nocheat_dig_time += dtime; + m_max_speed_override_time = MYMAX(m_max_speed_override_time - dtime, 0.0f); - // Each frame, parent position is copied if the object is attached, otherwise it's calculated normally - // If the object gets detached this comes into effect automatically from the last known origin + // Each frame, parent position is copied if the object is attached, + // otherwise it's calculated normally. + // If the object gets detached this comes into effect automatically from + // the last known origin. if (isAttached()) { v3f pos = m_env->getActiveObject(m_attachment_parent_id)->getBasePosition(); m_last_good_position = pos; @@ -1014,21 +1106,22 @@ void PlayerSAO::step(float dtime, bool send_recommended) if (!send_recommended) return; - // If the object is attached client-side, don't waste bandwidth sending its position to clients - if(m_position_not_sent && !isAttached()) - { + if (m_position_not_sent) { m_position_not_sent = false; float update_interval = m_env->getSendRecommendedInterval(); v3f pos; - if(isAttached()) // Just in case we ever do send attachment position too - pos = m_env->getActiveObject(m_attachment_parent_id)->getBasePosition(); + // When attached, the position is only sent to clients where the + // parent isn't known + if (isAttached()) + pos = m_last_good_position; else - pos = m_base_position + v3f(0,BS*1,0); + pos = m_base_position; + std::string str = gob_cmd_update_position( pos, - v3f(0,0,0), - v3f(0,0,0), - m_yaw, + v3f(0.0f, 0.0f, 0.0f), + v3f(0.0f, 0.0f, 0.0f), + m_rotation, true, false, update_interval @@ -1069,7 +1162,7 @@ void PlayerSAO::step(float dtime, bool send_recommended) if (!m_bone_position_sent) { m_bone_position_sent = true; - for (UNORDERED_MAP >::const_iterator + for (std::unordered_map>::const_iterator ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii) { std::string str = gob_cmd_update_bone_position((*ii).first, (*ii).second.X, (*ii).second.Y); @@ -1079,7 +1172,7 @@ void PlayerSAO::step(float dtime, bool send_recommended) } } - if (!m_attachment_sent){ + if (!m_attachment_sent) { m_attachment_sent = true; std::string str = gob_cmd_update_attachment(m_attachment_parent_id, m_attachment_bone, m_attachment_position, m_attachment_rotation); @@ -1096,7 +1189,11 @@ void PlayerSAO::setBasePosition(const v3f &position) // This needs to be ran for attachments too ServerActiveObject::setBasePosition(position); - m_position_not_sent = true; + + // Updating is not wanted/required for player migration + if (m_env) { + m_position_not_sent = true; + } } void PlayerSAO::setPos(const v3f &pos) @@ -1104,6 +1201,10 @@ void PlayerSAO::setPos(const v3f &pos) if(isAttached()) return; + // Send mapblock of target location + v3s16 blockpos = v3s16(pos.X / MAP_BLOCKSIZE, pos.Y / MAP_BLOCKSIZE, pos.Z / MAP_BLOCKSIZE); + m_env->getGameDef()->SendBlock(m_peer_id, blockpos); + setBasePosition(pos); // Movement caused by this command is always valid m_last_good_position = pos; @@ -1125,12 +1226,14 @@ void PlayerSAO::moveTo(v3f pos, bool continuous) m_env->getGameDef()->SendMovePlayer(m_peer_id); } -void PlayerSAO::setYaw(const float yaw) +void PlayerSAO::setPlayerYaw(const float yaw) { - if (m_player && yaw != m_yaw) + v3f rotation(0, yaw, 0); + if (m_player && yaw != m_rotation.Y) m_player->setDirty(true); - UnitSAO::setYaw(yaw); + // Set player model yaw, not look view + UnitSAO::setRotation(rotation); } void PlayerSAO::setFov(const float fov) @@ -1149,13 +1252,13 @@ void PlayerSAO::setWantedRange(const s16 range) m_wanted_range = range; } -void PlayerSAO::setYawAndSend(const float yaw) +void PlayerSAO::setPlayerYawAndSend(const float yaw) { - setYaw(yaw); + setPlayerYaw(yaw); m_env->getGameDef()->SendMovePlayer(m_peer_id); } -void PlayerSAO::setPitch(const float pitch) +void PlayerSAO::setLookPitch(const float pitch) { if (m_player && pitch != m_pitch) m_player->setDirty(true); @@ -1163,28 +1266,26 @@ void PlayerSAO::setPitch(const float pitch) m_pitch = pitch; } -void PlayerSAO::setPitchAndSend(const float pitch) +void PlayerSAO::setLookPitchAndSend(const float pitch) { - setPitch(pitch); + setLookPitch(pitch); m_env->getGameDef()->SendMovePlayer(m_peer_id); } -int PlayerSAO::punch(v3f dir, +u16 PlayerSAO::punch(v3f dir, const ToolCapabilities *toolcap, ServerActiveObject *puncher, float time_from_last_punch) { - // It's best that attachments cannot be punched - if (isAttached()) - return 0; - if (!toolcap) return 0; - // No effect if PvP disabled - if (g_settings->getBool("enable_pvp") == false) { + FATAL_ERROR_IF(!puncher, "Punch action called without SAO"); + + // No effect if PvP disabled or if immortal + if (isImmortal() || !g_settings->getBool("enable_pvp")) { if (puncher->getType() == ACTIVEOBJECT_TYPE_PLAYER) { - std::string str = gob_cmd_punched(0, getHP()); + std::string str = gob_cmd_punched(getHP()); // create message and add to list ActiveObjectMessage aom(getId(), true, str); m_messages_out.push(aom); @@ -1192,14 +1293,10 @@ int PlayerSAO::punch(v3f dir, } } + s32 old_hp = getHP(); HitParams hitparams = getHitParams(m_armor_groups, toolcap, time_from_last_punch); - std::string punchername = "nil"; - - if (puncher != 0) - punchername = puncher->getDescription(); - PlayerSAO *playersao = m_player->getPlayerSAO(); bool damage_handled = m_env->getScriptIface()->on_punchplayer(playersao, @@ -1207,59 +1304,45 @@ int PlayerSAO::punch(v3f dir, hitparams.hp); if (!damage_handled) { - setHP(getHP() - hitparams.hp); + setHP((s32)getHP() - (s32)hitparams.hp, + PlayerHPChangeReason(PlayerHPChangeReason::PLAYER_PUNCH, puncher)); } else { // override client prediction if (puncher->getType() == ACTIVEOBJECT_TYPE_PLAYER) { - std::string str = gob_cmd_punched(0, getHP()); + std::string str = gob_cmd_punched(getHP()); // create message and add to list ActiveObjectMessage aom(getId(), true, str); m_messages_out.push(aom); } } - - actionstream << "Player " << m_player->getName() << " punched by " - << punchername; - if (!damage_handled) { - actionstream << ", damage " << hitparams.hp << " HP"; - } else { - actionstream << ", damage handled by lua"; - } - actionstream << std::endl; + actionstream << puncher->getDescription() << " (id=" << puncher->getId() << + ", hp=" << puncher->getHP() << ") punched " << + getDescription() << " (id=" << m_id << ", hp=" << m_hp << + "), damage=" << (old_hp - (s32)getHP()) << + (damage_handled ? " (handled by Lua)" : "") << std::endl; return hitparams.wear; } -s16 PlayerSAO::readDamage() +void PlayerSAO::setHP(s32 hp, const PlayerHPChangeReason &reason) { - s16 damage = m_damage; - m_damage = 0; - return damage; -} + s32 oldhp = m_hp; -void PlayerSAO::setHP(s16 hp) -{ - s16 oldhp = m_hp; + hp = rangelim(hp, 0, m_prop.hp_max); - s16 hp_change = m_env->getScriptIface()->on_player_hpchange(this, hp - oldhp); - if (hp_change == 0) - return; - hp = oldhp + hp_change; + if (oldhp != hp) { + s32 hp_change = m_env->getScriptIface()->on_player_hpchange(this, hp - oldhp, reason); + if (hp_change == 0) + return; - if (hp < 0) - hp = 0; - else if (hp > PLAYER_MAX_HP) - hp = PLAYER_MAX_HP; + hp = rangelim(oldhp + hp_change, 0, m_prop.hp_max); + } - if (hp < oldhp && !g_settings->getBool("enable_damage")) { + if (hp < oldhp && isImmortal()) return; - } m_hp = hp; - if (oldhp > hp) - m_damage += (oldhp - hp); - // Update properties on death if ((hp == 0) != (oldhp == 0)) m_properties_sent = false; @@ -1270,19 +1353,15 @@ void PlayerSAO::setBreath(const u16 breath, bool send) if (m_player && breath != m_breath) m_player->setDirty(true); - m_breath = MYMIN(breath, PLAYER_MAX_BREATH); + m_breath = rangelim(breath, 0, m_prop.breath_max); if (send) m_env->getGameDef()->SendPlayerBreath(this); } -Inventory* PlayerSAO::getInventory() -{ - return m_inventory; -} -const Inventory* PlayerSAO::getInventory() const +Inventory *PlayerSAO::getInventory() const { - return m_inventory; + return m_player ? &m_player->inventory : nullptr; } InventoryLocation PlayerSAO::getInventoryLocation() const @@ -1292,72 +1371,36 @@ InventoryLocation PlayerSAO::getInventoryLocation() const return loc; } -std::string PlayerSAO::getWieldList() const -{ - return "main"; -} - -ItemStack PlayerSAO::getWieldedItem() const +u16 PlayerSAO::getWieldIndex() const { - const Inventory *inv = getInventory(); - ItemStack ret; - const InventoryList *mlist = inv->getList(getWieldList()); - if (mlist && getWieldIndex() < (s32)mlist->getSize()) - ret = mlist->getItem(getWieldIndex()); - return ret; + return m_player->getWieldIndex(); } -ItemStack PlayerSAO::getWieldedItemOrHand() const +ItemStack PlayerSAO::getWieldedItem(ItemStack *selected, ItemStack *hand) const { - const Inventory *inv = getInventory(); - ItemStack ret; - const InventoryList *mlist = inv->getList(getWieldList()); - if (mlist && getWieldIndex() < (s32)mlist->getSize()) - ret = mlist->getItem(getWieldIndex()); - if (ret.name.empty()) { - const InventoryList *hlist = inv->getList("hand"); - if (hlist) - ret = hlist->getItem(0); - } - return ret; + return m_player->getWieldedItem(selected, hand); } bool PlayerSAO::setWieldedItem(const ItemStack &item) { - Inventory *inv = getInventory(); - if (inv) { - InventoryList *mlist = inv->getList(getWieldList()); - if (mlist) { - mlist->changeItem(getWieldIndex(), item); - return true; - } + InventoryList *mlist = m_player->inventory.getList(getWieldList()); + if (mlist) { + mlist->changeItem(m_player->getWieldIndex(), item); + return true; } return false; } -int PlayerSAO::getWieldIndex() const -{ - return m_wield_index; -} - -void PlayerSAO::setWieldIndex(int i) -{ - if(i != m_wield_index) { - m_wield_index = i; - } -} - -// Erase the peer id and make the object for removal void PlayerSAO::disconnected() { - m_peer_id = 0; - m_removed = true; + m_peer_id = PEER_ID_INEXISTENT; + m_pending_removal = true; } void PlayerSAO::unlinkPlayerSessionAndSave() { assert(m_player->getPlayerSAO() == this); - m_player->peer_id = 0; + m_player->setPeerId(PEER_ID_INEXISTENT); m_env->savePlayer(m_player); m_player->setPlayerSAO(NULL); m_env->removePlayer(m_player); @@ -1369,6 +1412,19 @@ std::string PlayerSAO::getPropertyPacket() return gob_cmd_set_properties(m_prop); } +void PlayerSAO::setMaxSpeedOverride(const v3f &vel) +{ + if (m_max_speed_override_time == 0.0f) + m_max_speed_override = vel; + else + m_max_speed_override += vel; + if (m_player) { + float accel = MYMIN(m_player->movement_acceleration_default, + m_player->movement_acceleration_air); + m_max_speed_override_time = m_max_speed_override.getLength() / accel / BS; + } +} + bool PlayerSAO::checkMovementCheat() { if (isAttached() || m_is_singleplayer || @@ -1388,26 +1444,49 @@ bool PlayerSAO::checkMovementCheat() too, and much more lightweight. */ - float player_max_speed = 0; - - if (m_privs.count("fast") != 0) { - // Fast speed - player_max_speed = m_player->movement_speed_fast * m_physics_override_speed; + float override_max_H, override_max_V; + if (m_max_speed_override_time > 0.0f) { + override_max_H = MYMAX(fabs(m_max_speed_override.X), fabs(m_max_speed_override.Z)); + override_max_V = fabs(m_max_speed_override.Y); } else { - // Normal speed - player_max_speed = m_player->movement_speed_walk * m_physics_override_speed; + override_max_H = override_max_V = 0.0f; } - // Tolerance. The lag pool does this a bit. - //player_max_speed *= 2.5; + + float player_max_walk = 0; // horizontal movement + float player_max_jump = 0; // vertical upwards movement + + if (m_privs.count("fast") != 0) + player_max_walk = m_player->movement_speed_fast; // Fast speed + else + player_max_walk = m_player->movement_speed_walk; // Normal speed + player_max_walk *= m_physics_override_speed; + player_max_walk = MYMAX(player_max_walk, override_max_H); + + player_max_jump = m_player->movement_speed_jump * m_physics_override_jump; + // FIXME: Bouncy nodes cause practically unbound increase in Y speed, + // until this can be verified correctly, tolerate higher jumping speeds + player_max_jump *= 2.0; + player_max_jump = MYMAX(player_max_jump, override_max_V); + + // Don't divide by zero! + if (player_max_walk < 0.0001f) + player_max_walk = 0.0001f; + if (player_max_jump < 0.0001f) + player_max_jump = 0.0001f; v3f diff = (m_base_position - m_last_good_position); float d_vert = diff.Y; diff.Y = 0; float d_horiz = diff.getLength(); - float required_time = d_horiz / player_max_speed; - - if (d_vert > 0 && d_vert / player_max_speed > required_time) - required_time = d_vert / player_max_speed; // Moving upwards + float required_time = d_horiz / player_max_walk; + + // FIXME: Checking downwards movement is not easily possible currently, + // the server could calculate speed differences to examine the gravity + if (d_vert > 0) { + // In certain cases (water, ladders) walking speed is applied vertically + float s = MYMAX(player_max_jump, player_max_walk); + required_time = MYMAX(required_time, d_vert / s); + } if (m_move_pool.grab(required_time)) { m_last_good_position = m_base_position; @@ -1428,8 +1507,28 @@ bool PlayerSAO::checkMovementCheat() bool PlayerSAO::getCollisionBox(aabb3f *toset) const { - *toset = aabb3f(-BS * 0.30, 0.0, -BS * 0.30, BS * 0.30, BS * 1.75, BS * 0.30); + //update collision box + toset->MinEdge = m_prop.collisionbox.MinEdge * BS; + toset->MaxEdge = m_prop.collisionbox.MaxEdge * BS; + toset->MinEdge += m_base_position; toset->MaxEdge += m_base_position; return true; } + +bool PlayerSAO::getSelectionBox(aabb3f *toset) const +{ + if (!m_prop.is_visible || !m_prop.pointable) { + return false; + } + + toset->MinEdge = m_prop.selectionbox.MinEdge * BS; + toset->MaxEdge = m_prop.selectionbox.MaxEdge * BS; + + return true; +} + +float PlayerSAO::getZoomFOV() const +{ + return m_prop.zoom_fov; +}