aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/CMakeLists.txt5
-rw-r--r--src/server/activeobjectmgr.cpp10
-rw-r--r--src/server/activeobjectmgr.h7
-rw-r--r--src/server/luaentity_sao.cpp541
-rw-r--r--src/server/luaentity_sao.h93
-rw-r--r--src/server/mods.cpp10
-rw-r--r--src/server/mods.h3
-rw-r--r--src/server/player_sao.cpp665
-rw-r--r--src/server/player_sao.h300
-rw-r--r--src/server/serveractiveobject.cpp75
-rw-r--r--src/server/serveractiveobject.h255
-rw-r--r--src/server/serverinventorymgr.cpp192
-rw-r--r--src/server/serverinventorymgr.h60
-rw-r--r--src/server/unit_sao.cpp345
-rw-r--r--src/server/unit_sao.h136
15 files changed, 2684 insertions, 13 deletions
diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt
index e964c69ff..0a5a8f3a7 100644
--- a/src/server/CMakeLists.txt
+++ b/src/server/CMakeLists.txt
@@ -1,4 +1,9 @@
set(server_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/activeobjectmgr.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/luaentity_sao.cpp
${CMAKE_CURRENT_SOURCE_DIR}/mods.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/player_sao.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/serveractiveobject.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/serverinventorymgr.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/unit_sao.cpp
PARENT_SCOPE)
diff --git a/src/server/activeobjectmgr.cpp b/src/server/activeobjectmgr.cpp
index 984ae7794..1b8e31409 100644
--- a/src/server/activeobjectmgr.cpp
+++ b/src/server/activeobjectmgr.cpp
@@ -111,17 +111,19 @@ void ActiveObjectMgr::removeObject(u16 id)
}
// clang-format on
-void ActiveObjectMgr::getObjectsInsideRadius(
- const v3f &pos, float radius, std::vector<u16> &result)
+void ActiveObjectMgr::getObjectsInsideRadius(const v3f &pos, float radius,
+ std::vector<ServerActiveObject *> &result,
+ std::function<bool(ServerActiveObject *obj)> include_obj_cb)
{
float r2 = radius * radius;
for (auto &activeObject : m_active_objects) {
ServerActiveObject *obj = activeObject.second;
- u16 id = activeObject.first;
const v3f &objectpos = obj->getBasePosition();
if (objectpos.getDistanceFromSQ(pos) > r2)
continue;
- result.push_back(id);
+
+ if (!include_obj_cb || include_obj_cb(obj))
+ result.push_back(obj);
}
}
diff --git a/src/server/activeobjectmgr.h b/src/server/activeobjectmgr.h
index a502ac6ed..bc2085499 100644
--- a/src/server/activeobjectmgr.h
+++ b/src/server/activeobjectmgr.h
@@ -22,7 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include <functional>
#include <vector>
#include "../activeobjectmgr.h"
-#include "serverobject.h"
+#include "serveractiveobject.h"
namespace server
{
@@ -35,8 +35,9 @@ public:
bool registerObject(ServerActiveObject *obj) override;
void removeObject(u16 id) override;
- void getObjectsInsideRadius(
- const v3f &pos, float radius, std::vector<u16> &result);
+ void getObjectsInsideRadius(const v3f &pos, float radius,
+ std::vector<ServerActiveObject *> &result,
+ std::function<bool(ServerActiveObject *obj)> include_obj_cb);
void getAddedActiveObjectsAroundPos(const v3f &player_pos, f32 radius,
f32 player_radius, std::set<u16> &current_objects,
diff --git a/src/server/luaentity_sao.cpp b/src/server/luaentity_sao.cpp
new file mode 100644
index 000000000..d504c42ca
--- /dev/null
+++ b/src/server/luaentity_sao.cpp
@@ -0,0 +1,541 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013-2020 Minetest core developers & community
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "luaentity_sao.h"
+#include "collision.h"
+#include "constants.h"
+#include "player_sao.h"
+#include "scripting_server.h"
+#include "server.h"
+#include "serverenvironment.h"
+
+LuaEntitySAO::LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &data)
+ : UnitSAO(env, pos)
+{
+ std::string name;
+ std::string state;
+ u16 hp = 1;
+ v3f velocity;
+ v3f rotation;
+
+ while (!data.empty()) { // breakable, run for one iteration
+ std::istringstream is(data, std::ios::binary);
+ // '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);
+
+ 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;
+ // <read new values>
+ break;
+ }
+ // create object
+ infostream << "LuaEntitySAO::create(name=\"" << name << "\" state=\""
+ << state << "\")" << std::endl;
+
+ m_init_name = name;
+ m_init_state = state;
+ m_hp = hp;
+ m_velocity = velocity;
+ m_rotation = rotation;
+}
+
+LuaEntitySAO::~LuaEntitySAO()
+{
+ if(m_registered){
+ m_env->getScriptIface()->luaentity_Remove(m_id);
+ }
+
+ for (u32 attached_particle_spawner : m_attached_particle_spawners) {
+ m_env->deleteParticleSpawner(attached_particle_spawner, false);
+ }
+}
+
+void LuaEntitySAO::addedToEnvironment(u32 dtime_s)
+{
+ ServerActiveObject::addedToEnvironment(dtime_s);
+
+ // Create entity from name
+ m_registered = m_env->getScriptIface()->
+ luaentity_Add(m_id, m_init_name.c_str());
+
+ if(m_registered){
+ // Get properties
+ m_env->getScriptIface()->
+ 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, dtime_s);
+ } else {
+ m_prop.infotext = m_init_name;
+ }
+}
+
+void LuaEntitySAO::step(float dtime, bool send_recommended)
+{
+ if(!m_properties_sent)
+ {
+ m_properties_sent = true;
+ std::string str = getPropertyPacket();
+ // create message and add to list
+ m_messages_out.emplace(getId(), true, str);
+ }
+
+ // If attached, check that our parent is still there. If it isn't, detach.
+ if (m_attachment_parent_id && !isAttached()) {
+ // This is handled when objects are removed from the map
+ warningstream << "LuaEntitySAO::step() id=" << m_id <<
+ " is attached to nonexistent parent. This is a bug." << std::endl;
+ clearParentAttachment();
+ sendPosition(false, true);
+ }
+
+ m_last_sent_position_timer += dtime;
+
+ collisionMoveResult moveresult, *moveresult_p = nullptr;
+
+ // 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_base_position = pos;
+ m_velocity = v3f(0,0,0);
+ m_acceleration = v3f(0,0,0);
+ }
+ else
+ {
+ if(m_prop.physical){
+ aabb3f box = m_prop.collisionbox;
+ box.MinEdge *= BS;
+ box.MaxEdge *= BS;
+ f32 pos_max_d = BS*0.25; // Distance per iteration
+ v3f p_pos = m_base_position;
+ v3f p_velocity = m_velocity;
+ v3f p_acceleration = m_acceleration;
+ moveresult = collisionMoveSimple(m_env, m_env->getGameDef(),
+ pos_max_d, box, m_prop.stepheight, dtime,
+ &p_pos, &p_velocity, p_acceleration,
+ this, m_prop.collideWithObjects);
+ moveresult_p = &moveresult;
+
+ // Apply results
+ m_base_position = p_pos;
+ m_velocity = p_velocity;
+ m_acceleration = p_acceleration;
+ } else {
+ m_base_position += dtime * m_velocity + 0.5 * dtime
+ * dtime * m_acceleration;
+ 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 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 {
+ // Negative values of max_rotation_per_sec mean disabled.
+ m_rotation.Y = target_yaw;
+ }
+ }
+ }
+
+ if(m_registered) {
+ m_env->getScriptIface()->luaentity_Step(m_id, dtime, moveresult_p);
+ }
+
+ if (!send_recommended)
+ return;
+
+ if(!isAttached())
+ {
+ // TODO: force send when acceleration changes enough?
+ float minchange = 0.2*BS;
+ if(m_last_sent_position_timer > 1.0){
+ minchange = 0.01*BS;
+ } else if(m_last_sent_position_timer > 0.2){
+ minchange = 0.05*BS;
+ }
+ 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 ||
+ 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);
+ }
+ }
+
+ sendOutdatedData();
+}
+
+std::string LuaEntitySAO::getClientInitializationData(u16 protocol_version)
+{
+ std::ostringstream os(std::ios::binary);
+
+ // PROTOCOL_VERSION >= 37
+ writeU8(os, 1); // version
+ os << serializeString(""); // name
+ writeU8(os, 0); // is_player
+ 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(generateUpdateArmorGroupsCommand()); // 2
+ msg_os << serializeLongString(generateUpdateAnimationCommand()); // 3
+ for (const auto &bone_pos : m_bone_position) {
+ msg_os << serializeLongString(generateUpdateBonePositionCommand(
+ bone_pos.first, bone_pos.second.X, bone_pos.second.Y)); // m_bone_position.size
+ }
+ msg_os << serializeLongString(generateUpdateAttachmentCommand()); // 4
+
+ int message_count = 4 + m_bone_position.size();
+
+ for (const auto &id : getAttachmentChildIds()) {
+ if (ServerActiveObject *obj = m_env->getActiveObject(id)) {
+ message_count++;
+ msg_os << serializeLongString(obj->generateUpdateInfantCommand(
+ id, protocol_version));
+ }
+ }
+
+ msg_os << serializeLongString(generateSetTextureModCommand());
+ message_count++;
+
+ writeU8(os, message_count);
+ std::string serialized = msg_os.str();
+ os.write(serialized.c_str(), serialized.size());
+
+ // return result
+ return os.str();
+}
+
+void LuaEntitySAO::getStaticData(std::string *result) const
+{
+ verbosestream<<FUNCTION_NAME<<std::endl;
+ std::ostringstream os(std::ios::binary);
+ // version must be 1 to keep backwards-compatibility. See version2
+ writeU8(os, 1);
+ // name
+ os<<serializeString(m_init_name);
+ // state
+ if(m_registered){
+ std::string state = m_env->getScriptIface()->
+ luaentity_GetStaticdata(m_id);
+ os<<serializeLongString(state);
+ } else {
+ os<<serializeLongString(m_init_state);
+ }
+ writeU16(os, m_hp);
+ writeV3F1000(os, m_velocity);
+ // yaw
+ writeF1000(os, m_rotation.Y);
+
+ // version2. Increase this variable for new values
+ writeU8(os, 1); // PROTOCOL_VERSION >= 37
+
+ writeF1000(os, m_rotation.X);
+ writeF1000(os, m_rotation.Z);
+
+ // <write new values>
+
+ *result = os.str();
+}
+
+u16 LuaEntitySAO::punch(v3f dir,
+ const ToolCapabilities *toolcap,
+ ServerActiveObject *puncher,
+ float time_from_last_punch)
+{
+ if (!m_registered) {
+ // Delete unknown LuaEntities when punched
+ m_pending_removal = true;
+ return 0;
+ }
+
+ FATAL_ERROR_IF(!puncher, "Punch action called without SAO");
+
+ 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,
+ &tool_item,
+ time_from_last_punch);
+
+ bool damage_handled = m_env->getScriptIface()->luaentity_Punch(m_id, puncher,
+ time_from_last_punch, toolcap, dir, result.did_punch ? result.damage : 0);
+
+ if (!damage_handled) {
+ if (result.did_punch) {
+ setHP((s32)getHP() - result.damage,
+ PlayerHPChangeReason(PlayerHPChangeReason::PLAYER_PUNCH, puncher));
+
+ // create message and add to list
+ sendPunchCommand();
+ }
+ }
+
+ if (getHP() == 0 && !isGone()) {
+ clearParentAttachment();
+ clearChildAttachments();
+ m_env->getScriptIface()->luaentity_on_death(m_id, puncher);
+ m_pending_removal = true;
+ }
+
+ 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;
+
+ // TODO: give Lua control over wear
+ return result.wear;
+}
+
+void LuaEntitySAO::rightClick(ServerActiveObject *clicker)
+{
+ if (!m_registered)
+ return;
+
+ m_env->getScriptIface()->luaentity_Rightclick(m_id, clicker);
+}
+
+void LuaEntitySAO::setPos(const v3f &pos)
+{
+ if(isAttached())
+ return;
+ m_base_position = pos;
+ sendPosition(false, true);
+}
+
+void LuaEntitySAO::moveTo(v3f pos, bool continuous)
+{
+ if(isAttached())
+ return;
+ m_base_position = pos;
+ if(!continuous)
+ sendPosition(true, true);
+}
+
+float LuaEntitySAO::getMinimumSavedMovement()
+{
+ return 0.1 * BS;
+}
+
+std::string LuaEntitySAO::getDescription()
+{
+ std::ostringstream oss;
+ oss << "LuaEntitySAO \"" << m_init_name << "\" ";
+ auto pos = floatToInt(m_base_position, BS);
+ oss << "at " << PP(pos);
+ return oss.str();
+}
+
+void LuaEntitySAO::setHP(s32 hp, const PlayerHPChangeReason &reason)
+{
+ m_hp = rangelim(hp, 0, U16_MAX);
+}
+
+u16 LuaEntitySAO::getHP() const
+{
+ return m_hp;
+}
+
+void LuaEntitySAO::setVelocity(v3f velocity)
+{
+ m_velocity = velocity;
+}
+
+v3f LuaEntitySAO::getVelocity()
+{
+ return m_velocity;
+}
+
+void LuaEntitySAO::setAcceleration(v3f acceleration)
+{
+ m_acceleration = acceleration;
+}
+
+v3f LuaEntitySAO::getAcceleration()
+{
+ return m_acceleration;
+}
+
+void LuaEntitySAO::setTextureMod(const std::string &mod)
+{
+ m_current_texture_modifier = mod;
+ // create message and add to list
+ m_messages_out.emplace(getId(), true, generateSetTextureModCommand());
+}
+
+std::string LuaEntitySAO::getTextureMod() const
+{
+ return m_current_texture_modifier;
+}
+
+
+std::string LuaEntitySAO::generateSetTextureModCommand() const
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SET_TEXTURE_MOD);
+ // parameters
+ os << serializeString(m_current_texture_modifier);
+ return os.str();
+}
+
+std::string LuaEntitySAO::generateSetSpriteCommand(v2s16 p, u16 num_frames,
+ f32 framelength, bool select_horiz_by_yawpitch)
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SET_SPRITE);
+ // parameters
+ writeV2S16(os, p);
+ writeU16(os, num_frames);
+ writeF32(os, framelength);
+ writeU8(os, select_horiz_by_yawpitch);
+ return os.str();
+}
+
+void LuaEntitySAO::setSprite(v2s16 p, int num_frames, float framelength,
+ bool select_horiz_by_yawpitch)
+{
+ std::string str = generateSetSpriteCommand(
+ p,
+ num_frames,
+ framelength,
+ select_horiz_by_yawpitch
+ );
+ // create message and add to list
+ m_messages_out.emplace(getId(), true, str);
+}
+
+std::string LuaEntitySAO::getName()
+{
+ return m_init_name;
+}
+
+std::string LuaEntitySAO::getPropertyPacket()
+{
+ return generateSetPropertiesCommand(m_prop);
+}
+
+void LuaEntitySAO::sendPosition(bool do_interpolate, bool is_movement_end)
+{
+ // If the object is attached client-side, don't waste bandwidth sending its position to clients
+ if(isAttached())
+ return;
+
+ m_last_sent_move_precision = m_base_position.getDistanceFrom(
+ m_last_sent_position);
+ m_last_sent_position_timer = 0;
+ 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();
+
+ std::string str = generateUpdatePositionCommand(
+ m_base_position,
+ m_velocity,
+ m_acceleration,
+ m_rotation,
+ do_interpolate,
+ is_movement_end,
+ update_interval
+ );
+ // create message and add to list
+ m_messages_out.emplace(getId(), false, str);
+}
+
+bool LuaEntitySAO::getCollisionBox(aabb3f *toset) const
+{
+ if (m_prop.physical)
+ {
+ //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;
+ }
+
+ 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;
+}
diff --git a/src/server/luaentity_sao.h b/src/server/luaentity_sao.h
new file mode 100644
index 000000000..2520c8f5d
--- /dev/null
+++ b/src/server/luaentity_sao.h
@@ -0,0 +1,93 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013-2020 Minetest core developers & community
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "unit_sao.h"
+
+class LuaEntitySAO : public UnitSAO
+{
+public:
+ LuaEntitySAO() = delete;
+ // Used by the environment to load SAO
+ LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &data);
+ // Used by the Lua API
+ LuaEntitySAO(ServerEnvironment *env, v3f pos, const std::string &name,
+ const std::string &state) :
+ UnitSAO(env, pos),
+ m_init_name(name), m_init_state(state)
+ {
+ }
+ ~LuaEntitySAO();
+ ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_LUAENTITY; }
+ ActiveObjectType getSendType() const { return ACTIVEOBJECT_TYPE_GENERIC; }
+ virtual void addedToEnvironment(u32 dtime_s);
+ void step(float dtime, bool send_recommended);
+ std::string getClientInitializationData(u16 protocol_version);
+ bool isStaticAllowed() const { return m_prop.static_save; }
+ void getStaticData(std::string *result) const;
+ u16 punch(v3f dir, const ToolCapabilities *toolcap = nullptr,
+ ServerActiveObject *puncher = nullptr,
+ float time_from_last_punch = 1000000.0f);
+ void rightClick(ServerActiveObject *clicker);
+ void setPos(const v3f &pos);
+ void moveTo(v3f pos, bool continuous);
+ float getMinimumSavedMovement();
+ std::string getDescription();
+ void setHP(s32 hp, const PlayerHPChangeReason &reason);
+ u16 getHP() const;
+
+ /* LuaEntitySAO-specific */
+ void setVelocity(v3f velocity);
+ void addVelocity(v3f velocity) { m_velocity += velocity; }
+ v3f getVelocity();
+ void setAcceleration(v3f acceleration);
+ v3f getAcceleration();
+
+ void setTextureMod(const std::string &mod);
+ std::string getTextureMod() const;
+ void setSprite(v2s16 p, int num_frames, float framelength,
+ bool select_horiz_by_yawpitch);
+ std::string getName();
+ bool getCollisionBox(aabb3f *toset) const;
+ bool getSelectionBox(aabb3f *toset) const;
+ bool collideWithObjects() const;
+
+private:
+ std::string getPropertyPacket();
+ void sendPosition(bool do_interpolate, bool is_movement_end);
+ std::string generateSetTextureModCommand() const;
+ static std::string generateSetSpriteCommand(v2s16 p, u16 num_frames,
+ f32 framelength, bool select_horiz_by_yawpitch);
+
+ std::string m_init_name;
+ std::string m_init_state;
+ bool m_registered = false;
+
+ v3f m_velocity;
+ v3f m_acceleration;
+
+ v3f m_last_sent_position;
+ v3f m_last_sent_velocity;
+ v3f m_last_sent_rotation;
+ float m_last_sent_position_timer = 0.0f;
+ float m_last_sent_move_precision = 0.0f;
+ std::string m_current_texture_modifier = "";
+};
diff --git a/src/server/mods.cpp b/src/server/mods.cpp
index c5616dcd6..6ac530739 100644
--- a/src/server/mods.cpp
+++ b/src/server/mods.cpp
@@ -22,6 +22,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "log.h"
#include "scripting_server.h"
#include "content/subgames.h"
+#include "porting.h"
+#include "util/metricsbackend.h"
/**
* Manage server mods
@@ -66,14 +68,10 @@ void ServerModManager::loadMods(ServerScripting *script)
"Only characters [a-z0-9_] are allowed.");
}
std::string script_path = mod.path + DIR_DELIM + "init.lua";
- infostream << " [" << padStringRight(mod.name, 12) << "] [\""
- << script_path << "\"]" << std::endl;
- auto t = std::chrono::steady_clock::now();
+ auto t = porting::getTimeMs();
script->loadMod(script_path, mod.name);
infostream << "Mod \"" << mod.name << "\" loaded after "
- << std::chrono::duration_cast<std::chrono::milliseconds>(
- std::chrono::steady_clock::now() - t).count() * 0.001f
- << " seconds" << std::endl;
+ << (porting::getTimeMs() - t) << " ms" << std::endl;
}
// Run a callback when mods are loaded
diff --git a/src/server/mods.h b/src/server/mods.h
index 2bc1aa22f..54774bd86 100644
--- a/src/server/mods.h
+++ b/src/server/mods.h
@@ -20,7 +20,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#include "content/mods.h"
+#include <memory>
+class MetricsBackend;
+class MetricCounter;
class ServerScripting;
/**
diff --git a/src/server/player_sao.cpp b/src/server/player_sao.cpp
new file mode 100644
index 000000000..67efed210
--- /dev/null
+++ b/src/server/player_sao.cpp
@@ -0,0 +1,665 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013-2020 Minetest core developers & community
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "player_sao.h"
+#include "nodedef.h"
+#include "remoteplayer.h"
+#include "scripting_server.h"
+#include "server.h"
+#include "serverenvironment.h"
+
+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_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.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 = v3f(1, 2, 1);
+ m_prop.textures.clear();
+ m_prop.textures.emplace_back("player.png");
+ m_prop.textures.emplace_back("player_back.png");
+ m_prop.colors.clear();
+ m_prop.colors.emplace_back(255, 255, 255, 255);
+ m_prop.spritediv = v2s16(1,1);
+ 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_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;
+
+ if (!g_settings->getBool("enable_damage"))
+ m_armor_groups["immortal"] = 1;
+}
+
+void PlayerSAO::finalize(RemotePlayer *player, const std::set<std::string> &privs)
+{
+ assert(player);
+ m_player = player;
+ m_privs = privs;
+}
+
+v3f PlayerSAO::getEyeOffset() const
+{
+ return v3f(0, BS * m_prop.eye_height, 0);
+}
+
+std::string PlayerSAO::getDescription()
+{
+ return std::string("player ") + m_player->getName();
+}
+
+// Called after id has been set and has been inserted in environment
+void PlayerSAO::addedToEnvironment(u32 dtime_s)
+{
+ ServerActiveObject::addedToEnvironment(dtime_s);
+ ServerActiveObject::setBasePosition(m_base_position);
+ m_player->setPlayerSAO(this);
+ m_player->setPeerId(m_peer_id);
+ m_last_good_position = m_base_position;
+}
+
+// Called before removing from environment
+void PlayerSAO::removingFromEnvironment()
+{
+ ServerActiveObject::removingFromEnvironment();
+ if (m_player->getPlayerSAO() == this) {
+ unlinkPlayerSessionAndSave();
+ for (u32 attached_particle_spawner : m_attached_particle_spawners) {
+ m_env->deleteParticleSpawner(attached_particle_spawner, false);
+ }
+ }
+}
+
+std::string PlayerSAO::getClientInitializationData(u16 protocol_version)
+{
+ std::ostringstream os(std::ios::binary);
+
+ // Protocol >= 15
+ writeU8(os, 1); // version
+ os << serializeString(m_player->getName()); // name
+ writeU8(os, 1); // is_player
+ 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(generateUpdateArmorGroupsCommand()); // 2
+ msg_os << serializeLongString(generateUpdateAnimationCommand()); // 3
+ for (const auto &bone_pos : m_bone_position) {
+ msg_os << serializeLongString(generateUpdateBonePositionCommand(
+ bone_pos.first, bone_pos.second.X, bone_pos.second.Y)); // m_bone_position.size
+ }
+ msg_os << serializeLongString(generateUpdateAttachmentCommand()); // 4
+ msg_os << serializeLongString(generateUpdatePhysicsOverrideCommand()); // 5
+
+ int message_count = 5 + m_bone_position.size();
+
+ for (const auto &id : getAttachmentChildIds()) {
+ if (ServerActiveObject *obj = m_env->getActiveObject(id)) {
+ message_count++;
+ msg_os << serializeLongString(obj->generateUpdateInfantCommand(
+ id, protocol_version));
+ }
+ }
+
+ writeU8(os, message_count);
+ std::string serialized = msg_os.str();
+ os.write(serialized.c_str(), serialized.size());
+
+ // return result
+ return os.str();
+}
+
+void PlayerSAO::getStaticData(std::string * result) const
+{
+ FATAL_ERROR("Obsolete function");
+}
+
+void PlayerSAO::step(float dtime, bool send_recommended)
+{
+ 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) {
+ if (m_breath > 0)
+ setBreath(m_breath - 1);
+
+ // No more breath, damage player
+ if (m_breath == 0) {
+ PlayerHPChangeReason reason(PlayerHPChangeReason::DROWNING);
+ setHP(m_hp - c.drowning, reason);
+ m_env->getGameDef()->SendPlayerHPOrDie(this, reason);
+ }
+ }
+ }
+
+ 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 & 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 (!isImmortal() && m_node_hurt_interval.step(dtime, 1.0f)) {
+ u32 damage_per_second = 0;
+ 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) {
+ s32 newhp = (s32)m_hp - (s32)damage_per_second;
+ PlayerHPChangeReason reason(PlayerHPChangeReason::NODE_DAMAGE, nodename);
+ setHP(newhp, reason);
+ m_env->getGameDef()->SendPlayerHPOrDie(this, reason);
+ }
+ }
+
+ if (!m_properties_sent) {
+ m_properties_sent = true;
+ std::string str = getPropertyPacket();
+ // create message and add to list
+ m_messages_out.emplace(getId(), true, str);
+ 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()) {
+ // This is handled when objects are removed from the map
+ warningstream << "PlayerSAO::step() id=" << m_id <<
+ " is attached to nonexistent parent. This is a bug." << std::endl;
+ clearParentAttachment();
+ setBasePosition(m_last_good_position);
+ m_env->getGameDef()->SendMovePlayer(m_peer_id);
+ }
+
+ //dstream<<"PlayerSAO::step: dtime: "<<dtime<<std::endl;
+
+ // Set lag pool maximums based on estimated lag
+ 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);
+ m_move_pool.setMax(lag_pool_max);
+
+ // Increment cheat prevention timers
+ m_dig_pool.add(dtime);
+ m_move_pool.add(dtime);
+ 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.
+ if (isAttached()) {
+ v3f pos = m_env->getActiveObject(m_attachment_parent_id)->getBasePosition();
+ m_last_good_position = pos;
+ setBasePosition(pos);
+ }
+
+ if (!send_recommended)
+ return;
+
+ if (m_position_not_sent) {
+ m_position_not_sent = false;
+ float update_interval = m_env->getSendRecommendedInterval();
+ v3f pos;
+ // 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;
+
+ std::string str = generateUpdatePositionCommand(
+ pos,
+ v3f(0.0f, 0.0f, 0.0f),
+ v3f(0.0f, 0.0f, 0.0f),
+ m_rotation,
+ true,
+ false,
+ update_interval
+ );
+ // create message and add to list
+ m_messages_out.emplace(getId(), false, str);
+ }
+
+ if (!m_physics_override_sent) {
+ m_physics_override_sent = true;
+ // create message and add to list
+ m_messages_out.emplace(getId(), true, generateUpdatePhysicsOverrideCommand());
+ }
+
+ sendOutdatedData();
+}
+
+std::string PlayerSAO::generateUpdatePhysicsOverrideCommand() const
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SET_PHYSICS_OVERRIDE);
+ // parameters
+ writeF32(os, m_physics_override_speed);
+ writeF32(os, m_physics_override_jump);
+ writeF32(os, m_physics_override_gravity);
+ // these are sent inverted so we get true when the server sends nothing
+ writeU8(os, !m_physics_override_sneak);
+ writeU8(os, !m_physics_override_sneak_glitch);
+ writeU8(os, !m_physics_override_new_move);
+ return os.str();
+}
+
+void PlayerSAO::setBasePosition(const v3f &position)
+{
+ if (m_player && position != m_base_position)
+ m_player->setDirty(true);
+
+ // This needs to be ran for attachments too
+ ServerActiveObject::setBasePosition(position);
+
+ // Updating is not wanted/required for player migration
+ if (m_env) {
+ m_position_not_sent = true;
+ }
+}
+
+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;
+ m_move_pool.empty();
+ m_time_from_last_teleport = 0.0;
+ m_env->getGameDef()->SendMovePlayer(m_peer_id);
+}
+
+void PlayerSAO::moveTo(v3f pos, bool continuous)
+{
+ if(isAttached())
+ return;
+
+ setBasePosition(pos);
+ // Movement caused by this command is always valid
+ m_last_good_position = pos;
+ m_move_pool.empty();
+ m_time_from_last_teleport = 0.0;
+ m_env->getGameDef()->SendMovePlayer(m_peer_id);
+}
+
+void PlayerSAO::setPlayerYaw(const float yaw)
+{
+ v3f rotation(0, yaw, 0);
+ if (m_player && yaw != m_rotation.Y)
+ m_player->setDirty(true);
+
+ // Set player model yaw, not look view
+ UnitSAO::setRotation(rotation);
+}
+
+void PlayerSAO::setFov(const float fov)
+{
+ if (m_player && fov != m_fov)
+ m_player->setDirty(true);
+
+ m_fov = fov;
+}
+
+void PlayerSAO::setWantedRange(const s16 range)
+{
+ if (m_player && range != m_wanted_range)
+ m_player->setDirty(true);
+
+ m_wanted_range = range;
+}
+
+void PlayerSAO::setPlayerYawAndSend(const float yaw)
+{
+ setPlayerYaw(yaw);
+ m_env->getGameDef()->SendMovePlayer(m_peer_id);
+}
+
+void PlayerSAO::setLookPitch(const float pitch)
+{
+ if (m_player && pitch != m_pitch)
+ m_player->setDirty(true);
+
+ m_pitch = pitch;
+}
+
+void PlayerSAO::setLookPitchAndSend(const float pitch)
+{
+ setLookPitch(pitch);
+ m_env->getGameDef()->SendMovePlayer(m_peer_id);
+}
+
+u16 PlayerSAO::punch(v3f dir,
+ const ToolCapabilities *toolcap,
+ ServerActiveObject *puncher,
+ float time_from_last_punch)
+{
+ if (!toolcap)
+ return 0;
+
+ 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) {
+ // create message and add to list
+ sendPunchCommand();
+ return 0;
+ }
+ }
+
+ s32 old_hp = getHP();
+ HitParams hitparams = getHitParams(m_armor_groups, toolcap,
+ time_from_last_punch);
+
+ PlayerSAO *playersao = m_player->getPlayerSAO();
+
+ bool damage_handled = m_env->getScriptIface()->on_punchplayer(playersao,
+ puncher, time_from_last_punch, toolcap, dir,
+ hitparams.hp);
+
+ if (!damage_handled) {
+ setHP((s32)getHP() - (s32)hitparams.hp,
+ PlayerHPChangeReason(PlayerHPChangeReason::PLAYER_PUNCH, puncher));
+ } else { // override client prediction
+ if (puncher->getType() == ACTIVEOBJECT_TYPE_PLAYER) {
+ // create message and add to list
+ sendPunchCommand();
+ }
+ }
+
+ 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;
+}
+
+void PlayerSAO::setHP(s32 hp, const PlayerHPChangeReason &reason)
+{
+ s32 oldhp = m_hp;
+
+ hp = rangelim(hp, 0, m_prop.hp_max);
+
+ if (oldhp != hp) {
+ s32 hp_change = m_env->getScriptIface()->on_player_hpchange(this, hp - oldhp, reason);
+ if (hp_change == 0)
+ return;
+
+ hp = rangelim(oldhp + hp_change, 0, m_prop.hp_max);
+ }
+
+ if (hp < oldhp && isImmortal())
+ return;
+
+ m_hp = hp;
+
+ // Update properties on death
+ if ((hp == 0) != (oldhp == 0))
+ m_properties_sent = false;
+}
+
+void PlayerSAO::setBreath(const u16 breath, bool send)
+{
+ if (m_player && breath != m_breath)
+ m_player->setDirty(true);
+
+ m_breath = rangelim(breath, 0, m_prop.breath_max);
+
+ if (send)
+ m_env->getGameDef()->SendPlayerBreath(this);
+}
+
+Inventory *PlayerSAO::getInventory() const
+{
+ return m_player ? &m_player->inventory : nullptr;
+}
+
+InventoryLocation PlayerSAO::getInventoryLocation() const
+{
+ InventoryLocation loc;
+ loc.setPlayer(m_player->getName());
+ return loc;
+}
+
+u16 PlayerSAO::getWieldIndex() const
+{
+ return m_player->getWieldIndex();
+}
+
+ItemStack PlayerSAO::getWieldedItem(ItemStack *selected, ItemStack *hand) const
+{
+ return m_player->getWieldedItem(selected, hand);
+}
+
+bool PlayerSAO::setWieldedItem(const ItemStack &item)
+{
+ InventoryList *mlist = m_player->inventory.getList(getWieldList());
+ if (mlist) {
+ mlist->changeItem(m_player->getWieldIndex(), item);
+ return true;
+ }
+ return false;
+}
+
+void PlayerSAO::disconnected()
+{
+ m_peer_id = PEER_ID_INEXISTENT;
+ m_pending_removal = true;
+}
+
+void PlayerSAO::unlinkPlayerSessionAndSave()
+{
+ assert(m_player->getPlayerSAO() == this);
+ m_player->setPeerId(PEER_ID_INEXISTENT);
+ m_env->savePlayer(m_player);
+ m_player->setPlayerSAO(NULL);
+ m_env->removePlayer(m_player);
+}
+
+std::string PlayerSAO::getPropertyPacket()
+{
+ m_prop.is_visible = (true);
+ return generateSetPropertiesCommand(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 ||
+ g_settings->getBool("disable_anticheat")) {
+ m_last_good_position = m_base_position;
+ return false;
+ }
+
+ bool cheated = false;
+ /*
+ Check player movements
+
+ NOTE: Actually the server should handle player physics like the
+ client does and compare player's position to what is calculated
+ on our side. This is required when eg. players fly due to an
+ explosion. Altough a node-based alternative might be possible
+ too, and much more lightweight.
+ */
+
+ 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 {
+ override_max_H = override_max_V = 0.0f;
+ }
+
+ 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_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;
+ } else {
+ const float LAG_POOL_MIN = 5.0;
+ float lag_pool_max = m_env->getMaxLagEstimate() * 2.0;
+ lag_pool_max = MYMAX(lag_pool_max, LAG_POOL_MIN);
+ if (m_time_from_last_teleport > lag_pool_max) {
+ actionstream << "Server: " << m_player->getName()
+ << " moved too fast: V=" << d_vert << ", H=" << d_horiz
+ << "; resetting position." << std::endl;
+ cheated = true;
+ }
+ setBasePosition(m_last_good_position);
+ }
+ return cheated;
+}
+
+bool PlayerSAO::getCollisionBox(aabb3f *toset) const
+{
+ //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;
+}
diff --git a/src/server/player_sao.h b/src/server/player_sao.h
new file mode 100644
index 000000000..8571bd4f9
--- /dev/null
+++ b/src/server/player_sao.h
@@ -0,0 +1,300 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013-2020 Minetest core developers & community
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "constants.h"
+#include "network/networkprotocol.h"
+#include "unit_sao.h"
+#include "util/numeric.h"
+
+/*
+ PlayerSAO needs some internals exposed.
+*/
+
+class LagPool
+{
+ float m_pool = 15.0f;
+ float m_max = 15.0f;
+
+public:
+ LagPool() = default;
+
+ void setMax(float new_max)
+ {
+ m_max = new_max;
+ if (m_pool > new_max)
+ m_pool = new_max;
+ }
+
+ void add(float dtime)
+ {
+ m_pool -= dtime;
+ if (m_pool < 0)
+ m_pool = 0;
+ }
+
+ void empty() { m_pool = m_max; }
+
+ bool grab(float dtime)
+ {
+ if (dtime <= 0)
+ return true;
+ if (m_pool + dtime > m_max)
+ return false;
+ m_pool += dtime;
+ return true;
+ }
+};
+
+class RemotePlayer;
+
+class PlayerSAO : public UnitSAO
+{
+public:
+ PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, session_t peer_id_,
+ bool is_singleplayer);
+
+ ActiveObjectType getType() const { return ACTIVEOBJECT_TYPE_PLAYER; }
+ ActiveObjectType getSendType() const { return ACTIVEOBJECT_TYPE_GENERIC; }
+ std::string getDescription();
+
+ /*
+ Active object <-> environment interface
+ */
+
+ void addedToEnvironment(u32 dtime_s);
+ void removingFromEnvironment();
+ bool isStaticAllowed() const { return false; }
+ std::string getClientInitializationData(u16 protocol_version);
+ void getStaticData(std::string *result) const;
+ void step(float dtime, bool send_recommended);
+ void setBasePosition(const v3f &position);
+ void setPos(const v3f &pos);
+ void moveTo(v3f pos, bool continuous);
+ void setPlayerYaw(const float yaw);
+ // Data should not be sent at player initialization
+ void setPlayerYawAndSend(const float yaw);
+ void setLookPitch(const float pitch);
+ // Data should not be sent at player initialization
+ void setLookPitchAndSend(const float pitch);
+ f32 getLookPitch() const { return m_pitch; }
+ f32 getRadLookPitch() const { return m_pitch * core::DEGTORAD; }
+ // Deprecated
+ f32 getRadLookPitchDep() const { return -1.0 * m_pitch * core::DEGTORAD; }
+ void setFov(const float pitch);
+ f32 getFov() const { return m_fov; }
+ void setWantedRange(const s16 range);
+ s16 getWantedRange() const { return m_wanted_range; }
+
+ /*
+ Interaction interface
+ */
+
+ u16 punch(v3f dir, const ToolCapabilities *toolcap, ServerActiveObject *puncher,
+ float time_from_last_punch);
+ void rightClick(ServerActiveObject *clicker) {}
+ void setHP(s32 hp, const PlayerHPChangeReason &reason);
+ void setHPRaw(u16 hp) { m_hp = hp; }
+ s16 readDamage();
+ u16 getBreath() const { return m_breath; }
+ void setBreath(const u16 breath, bool send = true);
+
+ /*
+ Inventory interface
+ */
+ Inventory *getInventory() const;
+ InventoryLocation getInventoryLocation() const;
+ void setInventoryModified() {}
+ std::string getWieldList() const { return "main"; }
+ u16 getWieldIndex() const;
+ ItemStack getWieldedItem(ItemStack *selected, ItemStack *hand = nullptr) const;
+ bool setWieldedItem(const ItemStack &item);
+
+ /*
+ PlayerSAO-specific
+ */
+
+ void disconnected();
+
+ RemotePlayer *getPlayer() { return m_player; }
+ session_t getPeerID() const { return m_peer_id; }
+
+ // Cheat prevention
+
+ v3f getLastGoodPosition() const { return m_last_good_position; }
+ float resetTimeFromLastPunch()
+ {
+ float r = m_time_from_last_punch;
+ m_time_from_last_punch = 0.0;
+ return r;
+ }
+ void noCheatDigStart(const v3s16 &p)
+ {
+ m_nocheat_dig_pos = p;
+ m_nocheat_dig_time = 0;
+ }
+ v3s16 getNoCheatDigPos() { return m_nocheat_dig_pos; }
+ float getNoCheatDigTime() { return m_nocheat_dig_time; }
+ void noCheatDigEnd() { m_nocheat_dig_pos = v3s16(32767, 32767, 32767); }
+ LagPool &getDigPool() { return m_dig_pool; }
+ void setMaxSpeedOverride(const v3f &vel);
+ // Returns true if cheated
+ bool checkMovementCheat();
+
+ // Other
+
+ void updatePrivileges(const std::set<std::string> &privs, bool is_singleplayer)
+ {
+ m_privs = privs;
+ m_is_singleplayer = is_singleplayer;
+ }
+
+ bool getCollisionBox(aabb3f *toset) const;
+ bool getSelectionBox(aabb3f *toset) const;
+ bool collideWithObjects() const { return true; }
+
+ void finalize(RemotePlayer *player, const std::set<std::string> &privs);
+
+ v3f getEyePosition() const { return m_base_position + getEyeOffset(); }
+ v3f getEyeOffset() const;
+ float getZoomFOV() const;
+
+ inline Metadata &getMeta() { return m_meta; }
+
+private:
+ std::string getPropertyPacket();
+ void unlinkPlayerSessionAndSave();
+ std::string generateUpdatePhysicsOverrideCommand() const;
+
+ RemotePlayer *m_player = nullptr;
+ session_t m_peer_id = 0;
+
+ // Cheat prevention
+ LagPool m_dig_pool;
+ LagPool m_move_pool;
+ v3f m_last_good_position;
+ float m_time_from_last_teleport = 0.0f;
+ float m_time_from_last_punch = 0.0f;
+ v3s16 m_nocheat_dig_pos = v3s16(32767, 32767, 32767);
+ float m_nocheat_dig_time = 0.0f;
+ float m_max_speed_override_time = 0.0f;
+ v3f m_max_speed_override = v3f(0.0f, 0.0f, 0.0f);
+
+ // Timers
+ IntervalLimiter m_breathing_interval;
+ IntervalLimiter m_drowning_interval;
+ IntervalLimiter m_node_hurt_interval;
+
+ bool m_position_not_sent = false;
+
+ // Cached privileges for enforcement
+ std::set<std::string> m_privs;
+ bool m_is_singleplayer;
+
+ u16 m_breath = PLAYER_MAX_BREATH_DEFAULT;
+ f32 m_pitch = 0.0f;
+ f32 m_fov = 0.0f;
+ s16 m_wanted_range = 0.0f;
+
+ Metadata m_meta;
+
+public:
+ float m_physics_override_speed = 1.0f;
+ float m_physics_override_jump = 1.0f;
+ float m_physics_override_gravity = 1.0f;
+ bool m_physics_override_sneak = true;
+ bool m_physics_override_sneak_glitch = false;
+ bool m_physics_override_new_move = true;
+ bool m_physics_override_sent = false;
+};
+
+struct PlayerHPChangeReason
+{
+ enum Type : u8
+ {
+ SET_HP,
+ PLAYER_PUNCH,
+ FALL,
+ NODE_DAMAGE,
+ DROWNING,
+ RESPAWN
+ };
+
+ Type type = SET_HP;
+ bool from_mod = false;
+ int lua_reference = -1;
+
+ // For PLAYER_PUNCH
+ ServerActiveObject *object = nullptr;
+ // For NODE_DAMAGE
+ std::string node;
+
+ inline bool hasLuaReference() const { return lua_reference >= 0; }
+
+ bool setTypeFromString(const std::string &typestr)
+ {
+ if (typestr == "set_hp")
+ type = SET_HP;
+ else if (typestr == "punch")
+ type = PLAYER_PUNCH;
+ else if (typestr == "fall")
+ type = FALL;
+ else if (typestr == "node_damage")
+ type = NODE_DAMAGE;
+ else if (typestr == "drown")
+ type = DROWNING;
+ else if (typestr == "respawn")
+ type = RESPAWN;
+ else
+ return false;
+
+ return true;
+ }
+
+ std::string getTypeAsString() const
+ {
+ switch (type) {
+ case PlayerHPChangeReason::SET_HP:
+ return "set_hp";
+ case PlayerHPChangeReason::PLAYER_PUNCH:
+ return "punch";
+ case PlayerHPChangeReason::FALL:
+ return "fall";
+ case PlayerHPChangeReason::NODE_DAMAGE:
+ return "node_damage";
+ case PlayerHPChangeReason::DROWNING:
+ return "drown";
+ case PlayerHPChangeReason::RESPAWN:
+ return "respawn";
+ default:
+ return "?";
+ }
+ }
+
+ PlayerHPChangeReason(Type type) : type(type) {}
+
+ PlayerHPChangeReason(Type type, ServerActiveObject *object) :
+ type(type), object(object)
+ {
+ }
+
+ PlayerHPChangeReason(Type type, std::string node) : type(type), node(node) {}
+};
diff --git a/src/server/serveractiveobject.cpp b/src/server/serveractiveobject.cpp
new file mode 100644
index 000000000..3341dc008
--- /dev/null
+++ b/src/server/serveractiveobject.cpp
@@ -0,0 +1,75 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "serveractiveobject.h"
+#include <fstream>
+#include "inventory.h"
+#include "constants.h" // BS
+#include "log.h"
+
+ServerActiveObject::ServerActiveObject(ServerEnvironment *env, v3f pos):
+ ActiveObject(0),
+ m_env(env),
+ m_base_position(pos)
+{
+}
+
+float ServerActiveObject::getMinimumSavedMovement()
+{
+ return 2.0*BS;
+}
+
+ItemStack ServerActiveObject::getWieldedItem(ItemStack *selected, ItemStack *hand) const
+{
+ *selected = ItemStack();
+ if (hand)
+ *hand = ItemStack();
+
+ return ItemStack();
+}
+
+bool ServerActiveObject::setWieldedItem(const ItemStack &item)
+{
+ return false;
+}
+
+std::string ServerActiveObject::generateUpdateInfantCommand(u16 infant_id, u16 protocol_version)
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SPAWN_INFANT);
+ // parameters
+ writeU16(os, infant_id);
+ writeU8(os, getSendType());
+ if (protocol_version < 38) {
+ // Clients since 4aa9a66 so no longer need this data
+ // Version 38 is the first bump after that commit.
+ // See also: ClientEnvironment::addActiveObject
+ os << serializeLongString(getClientInitializationData(protocol_version));
+ }
+ return os.str();
+}
+
+void ServerActiveObject::dumpAOMessagesToQueue(std::queue<ActiveObjectMessage> &queue)
+{
+ while (!m_messages_out.empty()) {
+ queue.push(std::move(m_messages_out.front()));
+ m_messages_out.pop();
+ }
+}
diff --git a/src/server/serveractiveobject.h b/src/server/serveractiveobject.h
new file mode 100644
index 000000000..927009aef
--- /dev/null
+++ b/src/server/serveractiveobject.h
@@ -0,0 +1,255 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include <unordered_set>
+#include "irrlichttypes_bloated.h"
+#include "activeobject.h"
+#include "inventorymanager.h"
+#include "itemgroup.h"
+#include "util/container.h"
+
+/*
+
+Some planning
+-------------
+
+* Server environment adds an active object, which gets the id 1
+* The active object list is scanned for each client once in a while,
+ and it finds out what objects have been added that are not known
+ by the client yet. This scan is initiated by the Server class and
+ the result ends up directly to the server.
+* A network packet is created with the info and sent to the client.
+* Environment converts objects to static data and static data to
+ objects, based on how close players are to them.
+
+*/
+
+class ServerEnvironment;
+struct ItemStack;
+struct ToolCapabilities;
+struct ObjectProperties;
+struct PlayerHPChangeReason;
+
+class ServerActiveObject : public ActiveObject
+{
+public:
+ /*
+ NOTE: m_env can be NULL, but step() isn't called if it is.
+ Prototypes are used that way.
+ */
+ ServerActiveObject(ServerEnvironment *env, v3f pos);
+ virtual ~ServerActiveObject() = default;
+
+ virtual ActiveObjectType getSendType() const
+ { return getType(); }
+
+ // Called after id has been set and has been inserted in environment
+ virtual void addedToEnvironment(u32 dtime_s){};
+ // Called before removing from environment
+ virtual void removingFromEnvironment(){};
+ // Returns true if object's deletion is the job of the
+ // environment
+ virtual bool environmentDeletes() const
+ { return true; }
+
+ // Create a certain type of ServerActiveObject
+ static ServerActiveObject* create(ActiveObjectType type,
+ ServerEnvironment *env, u16 id, v3f pos,
+ const std::string &data);
+
+ /*
+ Some simple getters/setters
+ */
+ v3f getBasePosition() const { return m_base_position; }
+ void setBasePosition(v3f pos){ m_base_position = pos; }
+ ServerEnvironment* getEnv(){ return m_env; }
+
+ /*
+ Some more dynamic interface
+ */
+
+ virtual void setPos(const v3f &pos)
+ { setBasePosition(pos); }
+ // continuous: if true, object does not stop immediately at pos
+ virtual void moveTo(v3f pos, bool continuous)
+ { setBasePosition(pos); }
+ // If object has moved less than this and data has not changed,
+ // saving to disk may be omitted
+ virtual float getMinimumSavedMovement();
+
+ virtual std::string getDescription(){return "SAO";}
+
+ /*
+ Step object in time.
+ Messages added to messages are sent to client over network.
+
+ send_recommended:
+ True at around 5-10 times a second, same for all objects.
+ This is used to let objects send most of the data at the
+ same time so that the data can be combined in a single
+ packet.
+ */
+ virtual void step(float dtime, bool send_recommended){}
+
+ /*
+ The return value of this is passed to the client-side object
+ when it is created
+ */
+ virtual std::string getClientInitializationData(u16 protocol_version) {return "";}
+
+ /*
+ The return value of this is passed to the server-side object
+ when it is created (converted from static to active - actually
+ the data is the static form)
+ */
+ virtual void getStaticData(std::string *result) const
+ {
+ assert(isStaticAllowed());
+ *result = "";
+ }
+ /*
+ Return false in here to never save and instead remove object
+ on unload. getStaticData() will not be called in that case.
+ */
+ virtual bool isStaticAllowed() const
+ {return true;}
+
+ // Returns tool wear
+ virtual u16 punch(v3f dir,
+ const ToolCapabilities *toolcap = nullptr,
+ ServerActiveObject *puncher = nullptr,
+ float time_from_last_punch = 1000000.0f)
+ { return 0; }
+ virtual void rightClick(ServerActiveObject *clicker)
+ {}
+ virtual void setHP(s32 hp, const PlayerHPChangeReason &reason)
+ {}
+ virtual u16 getHP() const
+ { return 0; }
+
+ virtual void setArmorGroups(const ItemGroupList &armor_groups)
+ {}
+ virtual const ItemGroupList &getArmorGroups() const
+ { static ItemGroupList rv; return rv; }
+ virtual void setPhysicsOverride(float physics_override_speed, float physics_override_jump, float physics_override_gravity)
+ {}
+ virtual void setAnimation(v2f frames, float frame_speed, float frame_blend, bool frame_loop)
+ {}
+ virtual void getAnimation(v2f *frames, float *frame_speed, float *frame_blend, bool *frame_loop)
+ {}
+ virtual void setAnimationSpeed(float frame_speed)
+ {}
+ virtual void setBonePosition(const std::string &bone, v3f position, v3f rotation)
+ {}
+ virtual void getBonePosition(const std::string &bone, v3f *position, v3f *lotation)
+ {}
+ virtual const std::unordered_set<int> &getAttachmentChildIds() const
+ { static std::unordered_set<int> rv; return rv; }
+ virtual ServerActiveObject *getParent() const { return nullptr; }
+ virtual ObjectProperties* accessObjectProperties()
+ { return NULL; }
+ virtual void notifyObjectPropertiesModified()
+ {}
+
+ // Inventory and wielded item
+ virtual Inventory *getInventory() const
+ { return NULL; }
+ virtual InventoryLocation getInventoryLocation() const
+ { return InventoryLocation(); }
+ virtual void setInventoryModified()
+ {}
+ virtual std::string getWieldList() const
+ { return ""; }
+ virtual u16 getWieldIndex() const
+ { return 0; }
+ virtual ItemStack getWieldedItem(ItemStack *selected,
+ ItemStack *hand = nullptr) const;
+ virtual bool setWieldedItem(const ItemStack &item);
+ inline void attachParticleSpawner(u32 id)
+ {
+ m_attached_particle_spawners.insert(id);
+ }
+ inline void detachParticleSpawner(u32 id)
+ {
+ m_attached_particle_spawners.erase(id);
+ }
+
+ std::string generateUpdateInfantCommand(u16 infant_id, u16 protocol_version);
+ std::string generateUpdateNametagAttributesCommand(const video::SColor &color) const;
+
+ void dumpAOMessagesToQueue(std::queue<ActiveObjectMessage> &queue);
+
+ /*
+ Number of players which know about this object. Object won't be
+ deleted until this is 0 to keep the id preserved for the right
+ object.
+ */
+ u16 m_known_by_count = 0;
+
+ /*
+ - Whether this object is to be removed when nobody knows about
+ it anymore.
+ - Removal is delayed to preserve the id for the time during which
+ it could be confused to some other object by some client.
+ - This is usually set to true by the step() method when the object wants
+ to be deleted but can be set by anything else too.
+ */
+ bool m_pending_removal = false;
+
+ /*
+ Same purpose as m_pending_removal but for deactivation.
+ deactvation = save static data in block, remove active object
+
+ If this is set alongside with m_pending_removal, removal takes
+ priority.
+ */
+ bool m_pending_deactivation = false;
+
+ /*
+ A getter that unifies the above to answer the question:
+ "Can the environment still interact with this object?"
+ */
+ inline bool isGone() const
+ { return m_pending_removal || m_pending_deactivation; }
+
+ /*
+ Whether the object's static data has been stored to a block
+ */
+ bool m_static_exists = false;
+ /*
+ The block from which the object was loaded from, and in which
+ a copy of the static data resides.
+ */
+ v3s16 m_static_block = v3s16(1337,1337,1337);
+
+protected:
+ virtual void onAttach(int parent_id) {}
+ virtual void onDetach(int parent_id) {}
+
+ ServerEnvironment *m_env;
+ v3f m_base_position;
+ std::unordered_set<u32> m_attached_particle_spawners;
+
+ /*
+ Queue of messages to be sent to the client
+ */
+ std::queue<ActiveObjectMessage> m_messages_out;
+};
diff --git a/src/server/serverinventorymgr.cpp b/src/server/serverinventorymgr.cpp
new file mode 100644
index 000000000..555e01ec6
--- /dev/null
+++ b/src/server/serverinventorymgr.cpp
@@ -0,0 +1,192 @@
+/*
+Minetest
+Copyright (C) 2010-2020 Minetest core development team
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "serverinventorymgr.h"
+#include "map.h"
+#include "nodemetadata.h"
+#include "player_sao.h"
+#include "remoteplayer.h"
+#include "server.h"
+#include "serverenvironment.h"
+
+ServerInventoryManager::ServerInventoryManager() : InventoryManager()
+{
+}
+
+ServerInventoryManager::~ServerInventoryManager()
+{
+ // Delete detached inventories
+ for (auto &detached_inventory : m_detached_inventories) {
+ delete detached_inventory.second.inventory;
+ }
+}
+
+Inventory *ServerInventoryManager::getInventory(const InventoryLocation &loc)
+{
+ switch (loc.type) {
+ case InventoryLocation::UNDEFINED:
+ case InventoryLocation::CURRENT_PLAYER:
+ break;
+ case InventoryLocation::PLAYER: {
+ RemotePlayer *player = m_env->getPlayer(loc.name.c_str());
+ if (!player)
+ return NULL;
+ PlayerSAO *playersao = player->getPlayerSAO();
+ if (!playersao)
+ return NULL;
+ return playersao->getInventory();
+ } break;
+ case InventoryLocation::NODEMETA: {
+ NodeMetadata *meta = m_env->getMap().getNodeMetadata(loc.p);
+ if (!meta)
+ return NULL;
+ return meta->getInventory();
+ } break;
+ case InventoryLocation::DETACHED: {
+ auto it = m_detached_inventories.find(loc.name);
+ if (it == m_detached_inventories.end())
+ return nullptr;
+ return it->second.inventory;
+ } break;
+ default:
+ sanity_check(false); // abort
+ break;
+ }
+ return NULL;
+}
+
+void ServerInventoryManager::setInventoryModified(const InventoryLocation &loc)
+{
+ switch (loc.type) {
+ case InventoryLocation::UNDEFINED:
+ break;
+ case InventoryLocation::PLAYER: {
+
+ RemotePlayer *player = m_env->getPlayer(loc.name.c_str());
+
+ if (!player)
+ return;
+
+ player->setModified(true);
+ player->inventory.setModified(true);
+ // Updates are sent in ServerEnvironment::step()
+ } break;
+ case InventoryLocation::NODEMETA: {
+ MapEditEvent event;
+ event.type = MEET_BLOCK_NODE_METADATA_CHANGED;
+ event.p = loc.p;
+ m_env->getMap().dispatchEvent(event);
+ } break;
+ case InventoryLocation::DETACHED: {
+ // Updates are sent in ServerEnvironment::step()
+ } break;
+ default:
+ sanity_check(false); // abort
+ break;
+ }
+}
+
+Inventory *ServerInventoryManager::createDetachedInventory(
+ const std::string &name, IItemDefManager *idef, const std::string &player)
+{
+ if (m_detached_inventories.count(name) > 0) {
+ infostream << "Server clearing detached inventory \"" << name << "\""
+ << std::endl;
+ delete m_detached_inventories[name].inventory;
+ } else {
+ infostream << "Server creating detached inventory \"" << name << "\""
+ << std::endl;
+ }
+
+ Inventory *inv = new Inventory(idef);
+ sanity_check(inv);
+ m_detached_inventories[name].inventory = inv;
+ if (!player.empty()) {
+ m_detached_inventories[name].owner = player;
+
+ if (!m_env)
+ return inv; // Mods are not loaded yet, ignore
+
+ RemotePlayer *p = m_env->getPlayer(name.c_str());
+
+ // if player is connected, send him the inventory
+ if (p && p->getPeerId() != PEER_ID_INEXISTENT) {
+ m_env->getGameDef()->sendDetachedInventory(
+ inv, name, p->getPeerId());
+ }
+ } else {
+ if (!m_env)
+ return inv; // Mods are not loaded yet, don't send
+
+ // Inventory is for everybody, broadcast
+ m_env->getGameDef()->sendDetachedInventory(inv, name, PEER_ID_INEXISTENT);
+ }
+
+ return inv;
+}
+
+bool ServerInventoryManager::removeDetachedInventory(const std::string &name)
+{
+ const auto &inv_it = m_detached_inventories.find(name);
+ if (inv_it == m_detached_inventories.end())
+ return false;
+
+ delete inv_it->second.inventory;
+ const std::string &owner = inv_it->second.owner;
+
+ if (!owner.empty()) {
+ RemotePlayer *player = m_env->getPlayer(owner.c_str());
+
+ if (player && player->getPeerId() != PEER_ID_INEXISTENT)
+ m_env->getGameDef()->sendDetachedInventory(
+ nullptr, name, player->getPeerId());
+
+ } else {
+ // Notify all players about the change
+ m_env->getGameDef()->sendDetachedInventory(
+ nullptr, name, PEER_ID_INEXISTENT);
+ }
+
+ m_detached_inventories.erase(inv_it);
+
+ return true;
+}
+
+void ServerInventoryManager::sendDetachedInventories(const std::string &peer_name,
+ bool incremental,
+ std::function<void(const std::string &, Inventory *)> apply_cb)
+{
+ for (const auto &detached_inventory : m_detached_inventories) {
+ const DetachedInventory &dinv = detached_inventory.second;
+ if (incremental) {
+ if (!dinv.inventory || !dinv.inventory->checkModified())
+ continue;
+ }
+
+ // if we are pushing inventories to a specific player
+ // we should filter to send only the right inventories
+ if (!peer_name.empty()) {
+ const std::string &attached_player = dinv.owner;
+ if (!attached_player.empty() && peer_name != attached_player)
+ continue;
+ }
+
+ apply_cb(detached_inventory.first, detached_inventory.second.inventory);
+ }
+}
diff --git a/src/server/serverinventorymgr.h b/src/server/serverinventorymgr.h
new file mode 100644
index 000000000..ccf6d3b2e
--- /dev/null
+++ b/src/server/serverinventorymgr.h
@@ -0,0 +1,60 @@
+/*
+Minetest
+Copyright (C) 2010-2020 Minetest core development team
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "inventorymanager.h"
+#include <functional>
+
+class ServerEnvironment;
+
+class ServerInventoryManager : public InventoryManager
+{
+public:
+ ServerInventoryManager();
+ virtual ~ServerInventoryManager();
+
+ void setEnv(ServerEnvironment *env)
+ {
+ assert(!m_env);
+ m_env = env;
+ }
+
+ Inventory *getInventory(const InventoryLocation &loc);
+ void setInventoryModified(const InventoryLocation &loc);
+
+ // Creates or resets inventory
+ Inventory *createDetachedInventory(const std::string &name, IItemDefManager *idef,
+ const std::string &player = "");
+ bool removeDetachedInventory(const std::string &name);
+
+ void sendDetachedInventories(const std::string &peer_name, bool incremental,
+ std::function<void(const std::string &, Inventory *)> apply_cb);
+
+private:
+ struct DetachedInventory
+ {
+ Inventory *inventory;
+ std::string owner;
+ };
+
+ ServerEnvironment *m_env = nullptr;
+
+ std::unordered_map<std::string, DetachedInventory> m_detached_inventories;
+};
diff --git a/src/server/unit_sao.cpp b/src/server/unit_sao.cpp
new file mode 100644
index 000000000..ef0e87f2c
--- /dev/null
+++ b/src/server/unit_sao.cpp
@@ -0,0 +1,345 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013-2020 Minetest core developers & community
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "unit_sao.h"
+#include "scripting_server.h"
+#include "serverenvironment.h"
+
+UnitSAO::UnitSAO(ServerEnvironment *env, v3f pos) : ServerActiveObject(env, pos)
+{
+ // Initialize something to armor groups
+ m_armor_groups["fleshy"] = 100;
+}
+
+ServerActiveObject *UnitSAO::getParent() const
+{
+ if (!m_attachment_parent_id)
+ return nullptr;
+ // Check if the parent still exists
+ ServerActiveObject *obj = m_env->getActiveObject(m_attachment_parent_id);
+
+ return obj;
+}
+
+void UnitSAO::setArmorGroups(const ItemGroupList &armor_groups)
+{
+ m_armor_groups = armor_groups;
+ m_armor_groups_sent = false;
+}
+
+const ItemGroupList &UnitSAO::getArmorGroups() const
+{
+ return m_armor_groups;
+}
+
+void UnitSAO::setAnimation(
+ v2f frame_range, float frame_speed, float frame_blend, bool frame_loop)
+{
+ // store these so they can be updated to clients
+ m_animation_range = frame_range;
+ m_animation_speed = frame_speed;
+ m_animation_blend = frame_blend;
+ m_animation_loop = frame_loop;
+ m_animation_sent = false;
+}
+
+void UnitSAO::getAnimation(v2f *frame_range, float *frame_speed, float *frame_blend,
+ bool *frame_loop)
+{
+ *frame_range = m_animation_range;
+ *frame_speed = m_animation_speed;
+ *frame_blend = m_animation_blend;
+ *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
+ m_bone_position[bone] = core::vector2d<v3f>(position, rotation);
+ m_bone_position_sent = false;
+}
+
+void UnitSAO::getBonePosition(const std::string &bone, v3f *position, v3f *rotation)
+{
+ *position = m_bone_position[bone].X;
+ *rotation = m_bone_position[bone].Y;
+}
+
+// clang-format off
+void UnitSAO::sendOutdatedData()
+{
+ if (!m_armor_groups_sent) {
+ m_armor_groups_sent = true;
+ m_messages_out.emplace(getId(), true, generateUpdateArmorGroupsCommand());
+ }
+
+ if (!m_animation_sent) {
+ m_animation_sent = true;
+ m_animation_speed_sent = true;
+ m_messages_out.emplace(getId(), true, generateUpdateAnimationCommand());
+ } else if (!m_animation_speed_sent) {
+ // Animation speed is also sent when 'm_animation_sent == false'
+ m_animation_speed_sent = true;
+ m_messages_out.emplace(getId(), true, generateUpdateAnimationSpeedCommand());
+ }
+
+ if (!m_bone_position_sent) {
+ m_bone_position_sent = true;
+ for (const auto &bone_pos : m_bone_position) {
+ m_messages_out.emplace(getId(), true, generateUpdateBonePositionCommand(
+ bone_pos.first, bone_pos.second.X, bone_pos.second.Y));
+ }
+ }
+
+ if (!m_attachment_sent) {
+ m_attachment_sent = true;
+ m_messages_out.emplace(getId(), true, generateUpdateAttachmentCommand());
+ }
+}
+// clang-format on
+
+void UnitSAO::setAttachment(
+ int parent_id, const std::string &bone, v3f position, v3f rotation)
+{
+ // Attachments need to be handled on both the server and client.
+ // If we just attach on the server, we can only copy the position of the parent.
+ // Attachments are still sent to clients at an interval so players might see them
+ // lagging, plus we can't read and attach to skeletal bones. If we just attach on
+ // the client, the server still sees the child at its original location. 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) const
+{
+ *parent_id = m_attachment_parent_id;
+ *bone = m_attachment_bone;
+ *position = m_attachment_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);
+}
+
+void UnitSAO::removeAttachmentChild(int child_id)
+{
+ m_attachment_child_ids.erase(child_id);
+}
+
+const std::unordered_set<int> &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;
+}
+
+void UnitSAO::notifyObjectPropertiesModified()
+{
+ m_properties_sent = false;
+}
+
+std::string UnitSAO::generateUpdateAttachmentCommand() const
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_ATTACH_TO);
+ // parameters
+ writeS16(os, m_attachment_parent_id);
+ os << serializeString(m_attachment_bone);
+ writeV3F32(os, m_attachment_position);
+ writeV3F32(os, m_attachment_rotation);
+ return os.str();
+}
+
+std::string UnitSAO::generateUpdateBonePositionCommand(
+ const std::string &bone, const v3f &position, const v3f &rotation)
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SET_BONE_POSITION);
+ // parameters
+ os << serializeString(bone);
+ writeV3F32(os, position);
+ writeV3F32(os, rotation);
+ return os.str();
+}
+
+std::string UnitSAO::generateUpdateAnimationSpeedCommand() const
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SET_ANIMATION_SPEED);
+ // parameters
+ writeF32(os, m_animation_speed);
+ return os.str();
+}
+
+std::string UnitSAO::generateUpdateAnimationCommand() const
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_SET_ANIMATION);
+ // parameters
+ writeV2F32(os, m_animation_range);
+ writeF32(os, m_animation_speed);
+ writeF32(os, m_animation_blend);
+ // these are sent inverted so we get true when the server sends nothing
+ writeU8(os, !m_animation_loop);
+ return os.str();
+}
+
+std::string UnitSAO::generateUpdateArmorGroupsCommand() const
+{
+ std::ostringstream os(std::ios::binary);
+ writeU8(os, AO_CMD_UPDATE_ARMOR_GROUPS);
+ writeU16(os, m_armor_groups.size());
+ for (const auto &armor_group : m_armor_groups) {
+ os << serializeString(armor_group.first);
+ writeS16(os, armor_group.second);
+ }
+ return os.str();
+}
+
+std::string UnitSAO::generateUpdatePositionCommand(const v3f &position,
+ const v3f &velocity, const v3f &acceleration, const v3f &rotation,
+ bool do_interpolate, bool is_movement_end, f32 update_interval)
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_UPDATE_POSITION);
+ // pos
+ writeV3F32(os, position);
+ // velocity
+ writeV3F32(os, velocity);
+ // acceleration
+ writeV3F32(os, acceleration);
+ // rotation
+ writeV3F32(os, rotation);
+ // do_interpolate
+ writeU8(os, do_interpolate);
+ // is_end_position (for interpolation)
+ writeU8(os, is_movement_end);
+ // update_interval (for interpolation)
+ writeF32(os, update_interval);
+ return os.str();
+}
+
+std::string UnitSAO::generateSetPropertiesCommand(const ObjectProperties &prop) const
+{
+ std::ostringstream os(std::ios::binary);
+ writeU8(os, AO_CMD_SET_PROPERTIES);
+ prop.serialize(os);
+ return os.str();
+}
+
+std::string UnitSAO::generatePunchCommand(u16 result_hp) const
+{
+ std::ostringstream os(std::ios::binary);
+ // command
+ writeU8(os, AO_CMD_PUNCHED);
+ // result_hp
+ writeU16(os, result_hp);
+ return os.str();
+}
+
+void UnitSAO::sendPunchCommand()
+{
+ m_messages_out.emplace(getId(), true, generatePunchCommand(getHP()));
+}
diff --git a/src/server/unit_sao.h b/src/server/unit_sao.h
new file mode 100644
index 000000000..3cb7f0ad5
--- /dev/null
+++ b/src/server/unit_sao.h
@@ -0,0 +1,136 @@
+/*
+Minetest
+Copyright (C) 2010-2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+Copyright (C) 2013-2020 Minetest core developers & community
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+
+#include "object_properties.h"
+#include "serveractiveobject.h"
+
+class UnitSAO : public ServerActiveObject
+{
+public:
+ UnitSAO(ServerEnvironment *env, v3f pos);
+ virtual ~UnitSAO() = default;
+
+ u16 getHP() const { return m_hp; }
+ // Use a function, if isDead can be defined by other conditions
+ bool isDead() const { return m_hp == 0; }
+
+ // Rotation
+ void setRotation(v3f rotation) { m_rotation = rotation; }
+ const v3f &getRotation() const { return m_rotation; }
+ v3f getRadRotation() { return m_rotation * core::DEGTORAD; }
+
+ // Deprecated
+ f32 getRadYawDep() const { return (m_rotation.Y + 90.) * core::DEGTORAD; }
+
+ // Armor groups
+ inline bool isImmortal() const
+ {
+ return itemgroup_get(getArmorGroups(), "immortal");
+ }
+ void setArmorGroups(const ItemGroupList &armor_groups);
+ const ItemGroupList &getArmorGroups() const;
+
+ // Animation
+ void setAnimation(v2f frame_range, float frame_speed, float frame_blend,
+ bool frame_loop);
+ void getAnimation(v2f *frame_range, float *frame_speed, float *frame_blend,
+ bool *frame_loop);
+ void setAnimationSpeed(float frame_speed);
+
+ // Bone position
+ void setBonePosition(const std::string &bone, v3f position, v3f rotation);
+ void getBonePosition(const std::string &bone, v3f *position, v3f *rotation);
+
+ // Attachments
+ ServerActiveObject *getParent() const;
+ inline bool isAttached() const { return getParent(); }
+ void setAttachment(int parent_id, const std::string &bone, v3f position,
+ v3f rotation);
+ void getAttachment(int *parent_id, std::string *bone, v3f *position,
+ v3f *rotation) const;
+ void clearChildAttachments();
+ void clearParentAttachment();
+ void addAttachmentChild(int child_id);
+ void removeAttachmentChild(int child_id);
+ const std::unordered_set<int> &getAttachmentChildIds() const;
+
+ // Object properties
+ ObjectProperties *accessObjectProperties();
+ void notifyObjectPropertiesModified();
+ void sendOutdatedData();
+
+ // Update packets
+ std::string generateUpdateAttachmentCommand() const;
+ std::string generateUpdateAnimationSpeedCommand() const;
+ std::string generateUpdateAnimationCommand() const;
+ std::string generateUpdateArmorGroupsCommand() const;
+ static std::string generateUpdatePositionCommand(const v3f &position,
+ const v3f &velocity, const v3f &acceleration, const v3f &rotation,
+ bool do_interpolate, bool is_movement_end, f32 update_interval);
+ std::string generateSetPropertiesCommand(const ObjectProperties &prop) const;
+ static std::string generateUpdateBonePositionCommand(const std::string &bone,
+ const v3f &position, const v3f &rotation);
+ void sendPunchCommand();
+
+protected:
+ u16 m_hp = 1;
+
+ v3f m_rotation;
+
+ ItemGroupList m_armor_groups;
+
+ // Object properties
+ bool m_properties_sent = true;
+ ObjectProperties m_prop;
+
+ // Stores position and rotation for each bone name
+ std::unordered_map<std::string, core::vector2d<v3f>> m_bone_position;
+
+ int m_attachment_parent_id = 0;
+
+private:
+ void onAttach(int parent_id);
+ void onDetach(int parent_id);
+
+ std::string generatePunchCommand(u16 result_hp) const;
+
+ // Armor groups
+ bool m_armor_groups_sent = false;
+
+ // Animation
+ v2f m_animation_range;
+ float m_animation_speed = 0.0f;
+ float m_animation_blend = 0.0f;
+ bool m_animation_loop = true;
+ bool m_animation_sent = false;
+ bool m_animation_speed_sent = false;
+
+ // Bone positions
+ bool m_bone_position_sent = false;
+
+ // Attachments
+ std::unordered_set<int> m_attachment_child_ids;
+ std::string m_attachment_bone = "";
+ v3f m_attachment_position;
+ v3f m_attachment_rotation;
+ bool m_attachment_sent = false;
+};