3 Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4 Copyright (C) 2013-2020 Minetest core developers & community
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU Lesser General Public License as published by
8 the Free Software Foundation; either version 2.1 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU Lesser General Public License for more details.
16 You should have received a copy of the GNU Lesser General Public License along
17 with this program; if not, write to the Free Software Foundation, Inc.,
18 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #include "player_sao.h"
23 #include "remoteplayer.h"
24 #include "scripting_server.h"
26 #include "serverenvironment.h"
28 PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, session_t peer_id_,
29 bool is_singleplayer):
30 UnitSAO(env_, v3f(0,0,0)),
33 m_is_singleplayer(is_singleplayer)
35 SANITY_CHECK(m_peer_id != PEER_ID_INEXISTENT);
37 m_prop.hp_max = PLAYER_MAX_HP_DEFAULT;
38 m_prop.breath_max = PLAYER_MAX_BREATH_DEFAULT;
39 m_prop.physical = false;
40 m_prop.collisionbox = aabb3f(-0.3f, 0.0f, -0.3f, 0.3f, 1.77f, 0.3f);
41 m_prop.selectionbox = aabb3f(-0.3f, 0.0f, -0.3f, 0.3f, 1.77f, 0.3f);
42 m_prop.pointable = true;
43 // Start of default appearance, this should be overwritten by Lua
44 m_prop.visual = "upright_sprite";
45 m_prop.visual_size = v3f(1, 2, 1);
46 m_prop.textures.clear();
47 m_prop.textures.emplace_back("player.png");
48 m_prop.textures.emplace_back("player_back.png");
49 m_prop.colors.clear();
50 m_prop.colors.emplace_back(255, 255, 255, 255);
51 m_prop.spritediv = v2s16(1,1);
52 m_prop.eye_height = 1.625f;
53 // End of default appearance
54 m_prop.is_visible = true;
55 m_prop.backface_culling = false;
56 m_prop.makes_footstep_sound = true;
57 m_prop.stepheight = PLAYER_DEFAULT_STEPHEIGHT * BS;
59 m_breath = m_prop.breath_max;
60 // Disable zoom in survival mode using a value of 0
61 m_prop.zoom_fov = g_settings->getBool("creative_mode") ? 15.0f : 0.0f;
63 if (!g_settings->getBool("enable_damage"))
64 m_armor_groups["immortal"] = 1;
67 void PlayerSAO::finalize(RemotePlayer *player, const std::set<std::string> &privs)
74 v3f PlayerSAO::getEyeOffset() const
76 return v3f(0, BS * m_prop.eye_height, 0);
79 std::string PlayerSAO::getDescription()
81 return std::string("player ") + m_player->getName();
84 // Called after id has been set and has been inserted in environment
85 void PlayerSAO::addedToEnvironment(u32 dtime_s)
87 ServerActiveObject::addedToEnvironment(dtime_s);
88 ServerActiveObject::setBasePosition(m_base_position);
89 m_player->setPlayerSAO(this);
90 m_player->setPeerId(m_peer_id);
91 m_last_good_position = m_base_position;
94 // Called before removing from environment
95 void PlayerSAO::removingFromEnvironment()
97 ServerActiveObject::removingFromEnvironment();
98 if (m_player->getPlayerSAO() == this) {
99 unlinkPlayerSessionAndSave();
100 for (u32 attached_particle_spawner : m_attached_particle_spawners) {
101 m_env->deleteParticleSpawner(attached_particle_spawner, false);
106 std::string PlayerSAO::getClientInitializationData(u16 protocol_version)
108 std::ostringstream os(std::ios::binary);
111 writeU8(os, 1); // version
112 os << serializeString(m_player->getName()); // name
113 writeU8(os, 1); // is_player
114 writeS16(os, getId()); // id
115 writeV3F32(os, m_base_position);
116 writeV3F32(os, m_rotation);
117 writeU16(os, getHP());
119 std::ostringstream msg_os(std::ios::binary);
120 msg_os << serializeLongString(getPropertyPacket()); // message 1
121 msg_os << serializeLongString(generateUpdateArmorGroupsCommand()); // 2
122 msg_os << serializeLongString(generateUpdateAnimationCommand()); // 3
123 for (std::unordered_map<std::string, core::vector2d<v3f>>::const_iterator
124 ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii) {
125 msg_os << serializeLongString(generateUpdateBonePositionCommand((*ii).first,
126 (*ii).second.X, (*ii).second.Y)); // m_bone_position.size
128 msg_os << serializeLongString(generateUpdateAttachmentCommand()); // 4
129 msg_os << serializeLongString(generateUpdatePhysicsOverrideCommand()); // 5
130 // (AO_CMD_UPDATE_NAMETAG_ATTRIBUTES) : Deprecated, for backwards compatibility only.
131 msg_os << serializeLongString(generateUpdateNametagAttributesCommand(m_prop.nametag_color)); // 6
132 int message_count = 6 + m_bone_position.size();
133 for (std::unordered_set<int>::const_iterator ii = m_attachment_child_ids.begin();
134 ii != m_attachment_child_ids.end(); ++ii) {
135 if (ServerActiveObject *obj = m_env->getActiveObject(*ii)) {
137 msg_os << serializeLongString(obj->generateUpdateInfantCommand(*ii, protocol_version));
141 writeU8(os, message_count);
142 os.write(msg_os.str().c_str(), msg_os.str().size());
148 void PlayerSAO::getStaticData(std::string * result) const
150 FATAL_ERROR("Obsolete function");
153 void PlayerSAO::step(float dtime, bool send_recommended)
155 if (!isImmortal() && m_drowning_interval.step(dtime, 2.0f)) {
156 // Get nose/mouth position, approximate with eye position
157 v3s16 p = floatToInt(getEyePosition(), BS);
158 MapNode n = m_env->getMap().getNode(p);
159 const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n);
160 // If node generates drown
161 if (c.drowning > 0 && m_hp > 0) {
163 setBreath(m_breath - 1);
165 // No more breath, damage player
167 PlayerHPChangeReason reason(PlayerHPChangeReason::DROWNING);
168 setHP(m_hp - c.drowning, reason);
169 m_env->getGameDef()->SendPlayerHPOrDie(this, reason);
174 if (m_breathing_interval.step(dtime, 0.5f) && !isImmortal()) {
175 // Get nose/mouth position, approximate with eye position
176 v3s16 p = floatToInt(getEyePosition(), BS);
177 MapNode n = m_env->getMap().getNode(p);
178 const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n);
179 // If player is alive & not drowning & not in ignore & not immortal, breathe
180 if (m_breath < m_prop.breath_max && c.drowning == 0 &&
181 n.getContent() != CONTENT_IGNORE && m_hp > 0)
182 setBreath(m_breath + 1);
185 if (!isImmortal() && m_node_hurt_interval.step(dtime, 1.0f)) {
186 u32 damage_per_second = 0;
187 std::string nodename;
188 // Lowest and highest damage points are 0.1 within collisionbox
189 float dam_top = m_prop.collisionbox.MaxEdge.Y - 0.1f;
191 // Sequence of damage points, starting 0.1 above feet and progressing
192 // upwards in 1 node intervals, stopping below top damage point.
193 for (float dam_height = 0.1f; dam_height < dam_top; dam_height++) {
194 v3s16 p = floatToInt(m_base_position +
195 v3f(0.0f, dam_height * BS, 0.0f), BS);
196 MapNode n = m_env->getMap().getNode(p);
197 const ContentFeatures &c = m_env->getGameDef()->ndef()->get(n);
198 if (c.damage_per_second > damage_per_second) {
199 damage_per_second = c.damage_per_second;
205 v3s16 ptop = floatToInt(m_base_position +
206 v3f(0.0f, dam_top * BS, 0.0f), BS);
207 MapNode ntop = m_env->getMap().getNode(ptop);
208 const ContentFeatures &c = m_env->getGameDef()->ndef()->get(ntop);
209 if (c.damage_per_second > damage_per_second) {
210 damage_per_second = c.damage_per_second;
214 if (damage_per_second != 0 && m_hp > 0) {
215 s32 newhp = (s32)m_hp - (s32)damage_per_second;
216 PlayerHPChangeReason reason(PlayerHPChangeReason::NODE_DAMAGE, nodename);
217 setHP(newhp, reason);
218 m_env->getGameDef()->SendPlayerHPOrDie(this, reason);
222 if (!m_properties_sent) {
223 m_properties_sent = true;
224 std::string str = getPropertyPacket();
225 // create message and add to list
226 m_messages_out.emplace(getId(), true, str);
227 m_env->getScriptIface()->player_event(this, "properties_changed");
230 // If attached, check that our parent is still there. If it isn't, detach.
231 if (m_attachment_parent_id && !isAttached()) {
232 m_attachment_parent_id = 0;
233 m_attachment_bone = "";
234 m_attachment_position = v3f(0.0f, 0.0f, 0.0f);
235 m_attachment_rotation = v3f(0.0f, 0.0f, 0.0f);
236 setBasePosition(m_last_good_position);
237 m_env->getGameDef()->SendMovePlayer(m_peer_id);
240 //dstream<<"PlayerSAO::step: dtime: "<<dtime<<std::endl;
242 // Set lag pool maximums based on estimated lag
243 const float LAG_POOL_MIN = 5.0f;
244 float lag_pool_max = m_env->getMaxLagEstimate() * 2.0f;
245 if(lag_pool_max < LAG_POOL_MIN)
246 lag_pool_max = LAG_POOL_MIN;
247 m_dig_pool.setMax(lag_pool_max);
248 m_move_pool.setMax(lag_pool_max);
250 // Increment cheat prevention timers
251 m_dig_pool.add(dtime);
252 m_move_pool.add(dtime);
253 m_time_from_last_teleport += dtime;
254 m_time_from_last_punch += dtime;
255 m_nocheat_dig_time += dtime;
256 m_max_speed_override_time = MYMAX(m_max_speed_override_time - dtime, 0.0f);
258 // Each frame, parent position is copied if the object is attached,
259 // otherwise it's calculated normally.
260 // If the object gets detached this comes into effect automatically from
261 // the last known origin.
263 v3f pos = m_env->getActiveObject(m_attachment_parent_id)->getBasePosition();
264 m_last_good_position = pos;
265 setBasePosition(pos);
268 if (!send_recommended)
271 if (m_position_not_sent) {
272 m_position_not_sent = false;
273 float update_interval = m_env->getSendRecommendedInterval();
275 // When attached, the position is only sent to clients where the
276 // parent isn't known
278 pos = m_last_good_position;
280 pos = m_base_position;
282 std::string str = generateUpdatePositionCommand(
284 v3f(0.0f, 0.0f, 0.0f),
285 v3f(0.0f, 0.0f, 0.0f),
291 // create message and add to list
292 m_messages_out.emplace(getId(), false, str);
295 if (!m_armor_groups_sent) {
296 m_armor_groups_sent = true;
297 // create message and add to list
298 m_messages_out.emplace(getId(), true, generateUpdateArmorGroupsCommand());
301 if (!m_physics_override_sent) {
302 m_physics_override_sent = true;
303 // create message and add to list
304 m_messages_out.emplace(getId(), true, generateUpdatePhysicsOverrideCommand());
307 if (!m_animation_sent) {
308 m_animation_sent = true;
309 // create message and add to list
310 m_messages_out.emplace(getId(), true, generateUpdateAnimationCommand());
313 if (!m_bone_position_sent) {
314 m_bone_position_sent = true;
315 for (std::unordered_map<std::string, core::vector2d<v3f>>::const_iterator
316 ii = m_bone_position.begin(); ii != m_bone_position.end(); ++ii) {
317 std::string str = generateUpdateBonePositionCommand((*ii).first,
318 (*ii).second.X, (*ii).second.Y);
319 // create message and add to list
320 m_messages_out.emplace(getId(), true, str);
324 if (!m_attachment_sent) {
325 m_attachment_sent = true;
326 // create message and add to list
327 m_messages_out.emplace(getId(), true, generateUpdateAttachmentCommand());
331 std::string PlayerSAO::generateUpdatePhysicsOverrideCommand() const
333 std::ostringstream os(std::ios::binary);
335 writeU8(os, AO_CMD_SET_PHYSICS_OVERRIDE);
337 writeF32(os, m_physics_override_speed);
338 writeF32(os, m_physics_override_jump);
339 writeF32(os, m_physics_override_gravity);
340 // these are sent inverted so we get true when the server sends nothing
341 writeU8(os, !m_physics_override_sneak);
342 writeU8(os, !m_physics_override_sneak_glitch);
343 writeU8(os, !m_physics_override_new_move);
347 void PlayerSAO::setBasePosition(const v3f &position)
349 if (m_player && position != m_base_position)
350 m_player->setDirty(true);
352 // This needs to be ran for attachments too
353 ServerActiveObject::setBasePosition(position);
355 // Updating is not wanted/required for player migration
357 m_position_not_sent = true;
361 void PlayerSAO::setPos(const v3f &pos)
366 // Send mapblock of target location
367 v3s16 blockpos = v3s16(pos.X / MAP_BLOCKSIZE, pos.Y / MAP_BLOCKSIZE, pos.Z / MAP_BLOCKSIZE);
368 m_env->getGameDef()->SendBlock(m_peer_id, blockpos);
370 setBasePosition(pos);
371 // Movement caused by this command is always valid
372 m_last_good_position = pos;
374 m_time_from_last_teleport = 0.0;
375 m_env->getGameDef()->SendMovePlayer(m_peer_id);
378 void PlayerSAO::moveTo(v3f pos, bool continuous)
383 setBasePosition(pos);
384 // Movement caused by this command is always valid
385 m_last_good_position = pos;
387 m_time_from_last_teleport = 0.0;
388 m_env->getGameDef()->SendMovePlayer(m_peer_id);
391 void PlayerSAO::setPlayerYaw(const float yaw)
393 v3f rotation(0, yaw, 0);
394 if (m_player && yaw != m_rotation.Y)
395 m_player->setDirty(true);
397 // Set player model yaw, not look view
398 UnitSAO::setRotation(rotation);
401 void PlayerSAO::setFov(const float fov)
403 if (m_player && fov != m_fov)
404 m_player->setDirty(true);
409 void PlayerSAO::setWantedRange(const s16 range)
411 if (m_player && range != m_wanted_range)
412 m_player->setDirty(true);
414 m_wanted_range = range;
417 void PlayerSAO::setPlayerYawAndSend(const float yaw)
420 m_env->getGameDef()->SendMovePlayer(m_peer_id);
423 void PlayerSAO::setLookPitch(const float pitch)
425 if (m_player && pitch != m_pitch)
426 m_player->setDirty(true);
431 void PlayerSAO::setLookPitchAndSend(const float pitch)
434 m_env->getGameDef()->SendMovePlayer(m_peer_id);
437 u16 PlayerSAO::punch(v3f dir,
438 const ToolCapabilities *toolcap,
439 ServerActiveObject *puncher,
440 float time_from_last_punch)
445 FATAL_ERROR_IF(!puncher, "Punch action called without SAO");
447 // No effect if PvP disabled or if immortal
448 if (isImmortal() || !g_settings->getBool("enable_pvp")) {
449 if (puncher->getType() == ACTIVEOBJECT_TYPE_PLAYER) {
450 // create message and add to list
456 s32 old_hp = getHP();
457 HitParams hitparams = getHitParams(m_armor_groups, toolcap,
458 time_from_last_punch);
460 PlayerSAO *playersao = m_player->getPlayerSAO();
462 bool damage_handled = m_env->getScriptIface()->on_punchplayer(playersao,
463 puncher, time_from_last_punch, toolcap, dir,
466 if (!damage_handled) {
467 setHP((s32)getHP() - (s32)hitparams.hp,
468 PlayerHPChangeReason(PlayerHPChangeReason::PLAYER_PUNCH, puncher));
469 } else { // override client prediction
470 if (puncher->getType() == ACTIVEOBJECT_TYPE_PLAYER) {
471 // create message and add to list
476 actionstream << puncher->getDescription() << " (id=" << puncher->getId() <<
477 ", hp=" << puncher->getHP() << ") punched " <<
478 getDescription() << " (id=" << m_id << ", hp=" << m_hp <<
479 "), damage=" << (old_hp - (s32)getHP()) <<
480 (damage_handled ? " (handled by Lua)" : "") << std::endl;
482 return hitparams.wear;
485 void PlayerSAO::setHP(s32 hp, const PlayerHPChangeReason &reason)
489 hp = rangelim(hp, 0, m_prop.hp_max);
492 s32 hp_change = m_env->getScriptIface()->on_player_hpchange(this, hp - oldhp, reason);
496 hp = rangelim(oldhp + hp_change, 0, m_prop.hp_max);
499 if (hp < oldhp && isImmortal())
504 // Update properties on death
505 if ((hp == 0) != (oldhp == 0))
506 m_properties_sent = false;
509 void PlayerSAO::setBreath(const u16 breath, bool send)
511 if (m_player && breath != m_breath)
512 m_player->setDirty(true);
514 m_breath = rangelim(breath, 0, m_prop.breath_max);
517 m_env->getGameDef()->SendPlayerBreath(this);
520 Inventory *PlayerSAO::getInventory() const
522 return m_player ? &m_player->inventory : nullptr;
525 InventoryLocation PlayerSAO::getInventoryLocation() const
527 InventoryLocation loc;
528 loc.setPlayer(m_player->getName());
532 u16 PlayerSAO::getWieldIndex() const
534 return m_player->getWieldIndex();
537 ItemStack PlayerSAO::getWieldedItem(ItemStack *selected, ItemStack *hand) const
539 return m_player->getWieldedItem(selected, hand);
542 bool PlayerSAO::setWieldedItem(const ItemStack &item)
544 InventoryList *mlist = m_player->inventory.getList(getWieldList());
546 mlist->changeItem(m_player->getWieldIndex(), item);
552 void PlayerSAO::disconnected()
554 m_peer_id = PEER_ID_INEXISTENT;
555 m_pending_removal = true;
558 void PlayerSAO::unlinkPlayerSessionAndSave()
560 assert(m_player->getPlayerSAO() == this);
561 m_player->setPeerId(PEER_ID_INEXISTENT);
562 m_env->savePlayer(m_player);
563 m_player->setPlayerSAO(NULL);
564 m_env->removePlayer(m_player);
567 std::string PlayerSAO::getPropertyPacket()
569 m_prop.is_visible = (true);
570 return generateSetPropertiesCommand(m_prop);
573 void PlayerSAO::setMaxSpeedOverride(const v3f &vel)
575 if (m_max_speed_override_time == 0.0f)
576 m_max_speed_override = vel;
578 m_max_speed_override += vel;
580 float accel = MYMIN(m_player->movement_acceleration_default,
581 m_player->movement_acceleration_air);
582 m_max_speed_override_time = m_max_speed_override.getLength() / accel / BS;
586 bool PlayerSAO::checkMovementCheat()
588 if (isAttached() || m_is_singleplayer ||
589 g_settings->getBool("disable_anticheat")) {
590 m_last_good_position = m_base_position;
594 bool cheated = false;
596 Check player movements
598 NOTE: Actually the server should handle player physics like the
599 client does and compare player's position to what is calculated
600 on our side. This is required when eg. players fly due to an
601 explosion. Altough a node-based alternative might be possible
602 too, and much more lightweight.
605 float override_max_H, override_max_V;
606 if (m_max_speed_override_time > 0.0f) {
607 override_max_H = MYMAX(fabs(m_max_speed_override.X), fabs(m_max_speed_override.Z));
608 override_max_V = fabs(m_max_speed_override.Y);
610 override_max_H = override_max_V = 0.0f;
613 float player_max_walk = 0; // horizontal movement
614 float player_max_jump = 0; // vertical upwards movement
616 if (m_privs.count("fast") != 0)
617 player_max_walk = m_player->movement_speed_fast; // Fast speed
619 player_max_walk = m_player->movement_speed_walk; // Normal speed
620 player_max_walk *= m_physics_override_speed;
621 player_max_walk = MYMAX(player_max_walk, override_max_H);
623 player_max_jump = m_player->movement_speed_jump * m_physics_override_jump;
624 // FIXME: Bouncy nodes cause practically unbound increase in Y speed,
625 // until this can be verified correctly, tolerate higher jumping speeds
626 player_max_jump *= 2.0;
627 player_max_jump = MYMAX(player_max_jump, override_max_V);
629 // Don't divide by zero!
630 if (player_max_walk < 0.0001f)
631 player_max_walk = 0.0001f;
632 if (player_max_jump < 0.0001f)
633 player_max_jump = 0.0001f;
635 v3f diff = (m_base_position - m_last_good_position);
636 float d_vert = diff.Y;
638 float d_horiz = diff.getLength();
639 float required_time = d_horiz / player_max_walk;
641 // FIXME: Checking downwards movement is not easily possible currently,
642 // the server could calculate speed differences to examine the gravity
644 // In certain cases (water, ladders) walking speed is applied vertically
645 float s = MYMAX(player_max_jump, player_max_walk);
646 required_time = MYMAX(required_time, d_vert / s);
649 if (m_move_pool.grab(required_time)) {
650 m_last_good_position = m_base_position;
652 const float LAG_POOL_MIN = 5.0;
653 float lag_pool_max = m_env->getMaxLagEstimate() * 2.0;
654 lag_pool_max = MYMAX(lag_pool_max, LAG_POOL_MIN);
655 if (m_time_from_last_teleport > lag_pool_max) {
656 actionstream << "Server: " << m_player->getName()
657 << " moved too fast: V=" << d_vert << ", H=" << d_horiz
658 << "; resetting position." << std::endl;
661 setBasePosition(m_last_good_position);
666 bool PlayerSAO::getCollisionBox(aabb3f *toset) const
668 //update collision box
669 toset->MinEdge = m_prop.collisionbox.MinEdge * BS;
670 toset->MaxEdge = m_prop.collisionbox.MaxEdge * BS;
672 toset->MinEdge += m_base_position;
673 toset->MaxEdge += m_base_position;
677 bool PlayerSAO::getSelectionBox(aabb3f *toset) const
679 if (!m_prop.is_visible || !m_prop.pointable) {
683 toset->MinEdge = m_prop.selectionbox.MinEdge * BS;
684 toset->MaxEdge = m_prop.selectionbox.MaxEdge * BS;
689 float PlayerSAO::getZoomFOV() const
691 return m_prop.zoom_fov;