summaryrefslogtreecommitdiffstats
path: root/src/Entities
diff options
context:
space:
mode:
authorAlexander Harkness <bearbin@gmail.com>2013-11-24 15:19:41 +0100
committerAlexander Harkness <bearbin@gmail.com>2013-11-24 15:19:41 +0100
commit675b4aa878f16291ce33fced48a2bc7425f635ae (patch)
tree409914df27a98f65adf866da669429c4de141b6f /src/Entities
parentLineBlockTracer: Using the coord-based block faces. (diff)
downloadcuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.tar
cuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.tar.gz
cuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.tar.bz2
cuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.tar.lz
cuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.tar.xz
cuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.tar.zst
cuberite-675b4aa878f16291ce33fced48a2bc7425f635ae.zip
Diffstat (limited to 'src/Entities')
-rw-r--r--src/Entities/Boat.cpp87
-rw-r--r--src/Entities/Boat.h37
-rw-r--r--src/Entities/Entity.cpp1450
-rw-r--r--src/Entities/Entity.h445
-rw-r--r--src/Entities/FallingBlock.cpp93
-rw-r--r--src/Entities/FallingBlock.h43
-rw-r--r--src/Entities/Minecart.cpp541
-rw-r--r--src/Entities/Minecart.h169
-rw-r--r--src/Entities/Pawn.cpp19
-rw-r--r--src/Entities/Pawn.h28
-rw-r--r--src/Entities/Pickup.cpp166
-rw-r--r--src/Entities/Pickup.h64
-rw-r--r--src/Entities/Player.cpp1715
-rw-r--r--src/Entities/Player.h447
-rw-r--r--src/Entities/ProjectileEntity.cpp743
-rw-r--r--src/Entities/ProjectileEntity.h325
-rw-r--r--src/Entities/TNTEntity.cpp62
-rw-r--r--src/Entities/TNTEntity.h32
18 files changed, 6466 insertions, 0 deletions
diff --git a/src/Entities/Boat.cpp b/src/Entities/Boat.cpp
new file mode 100644
index 000000000..56e766dd4
--- /dev/null
+++ b/src/Entities/Boat.cpp
@@ -0,0 +1,87 @@
+
+// Boat.cpp
+
+// Implements the cBoat class representing a boat in the world
+
+#include "Globals.h"
+#include "Boat.h"
+#include "../World.h"
+#include "../ClientHandle.h"
+#include "Player.h"
+
+
+
+
+
+cBoat::cBoat(double a_X, double a_Y, double a_Z) :
+ super(etBoat, a_X, a_Y, a_Z, 0.98, 0.7)
+{
+ SetMass(20.f);
+ SetMaxHealth(6);
+ SetHealth(6);
+}
+
+
+
+
+void cBoat::SpawnOn(cClientHandle & a_ClientHandle)
+{
+ a_ClientHandle.SendSpawnVehicle(*this, 1);
+}
+
+
+
+
+
+void cBoat::DoTakeDamage(TakeDamageInfo & TDI)
+{
+ super::DoTakeDamage(TDI);
+
+ if (GetHealth() == 0)
+ {
+ Destroy(true);
+ }
+}
+
+
+
+
+
+void cBoat::OnRightClicked(cPlayer & a_Player)
+{
+ if (m_Attachee != NULL)
+ {
+ if (m_Attachee->GetUniqueID() == a_Player.GetUniqueID())
+ {
+ // This player is already sitting in, they want out.
+ a_Player.Detach();
+ return;
+ }
+
+ if (m_Attachee->IsPlayer())
+ {
+ // Another player is already sitting in here, cannot attach
+ return;
+ }
+
+ // Detach whatever is sitting in this boat now:
+ m_Attachee->Detach();
+ }
+
+ // Attach the player to this boat
+ a_Player.AttachTo(this);
+}
+
+
+
+
+
+void cBoat::HandlePhysics(float a_Dt, cChunk & a_Chunk)
+{
+ super::HandlePhysics(a_Dt, a_Chunk);
+ BroadcastMovementUpdate();
+}
+
+
+
+
diff --git a/src/Entities/Boat.h b/src/Entities/Boat.h
new file mode 100644
index 000000000..8c51ab86c
--- /dev/null
+++ b/src/Entities/Boat.h
@@ -0,0 +1,37 @@
+
+// Boat.h
+
+// Declares the cBoat class representing a boat in the world
+
+
+
+
+
+#pragma once
+
+#include "Entity.h"
+
+
+
+
+
+class cBoat :
+ public cEntity
+{
+ typedef cEntity super;
+
+public:
+ CLASS_PROTODEF(cBoat);
+
+ // cEntity overrides:
+ virtual void SpawnOn(cClientHandle & a_ClientHandle) override;
+ virtual void OnRightClicked(cPlayer & a_Player) override;
+ virtual void DoTakeDamage(TakeDamageInfo & TDI) override;
+ virtual void HandlePhysics(float a_Dt, cChunk & a_Chunk) override;
+
+ cBoat(double a_X, double a_Y, double a_Z);
+} ;
+
+
+
+
diff --git a/src/Entities/Entity.cpp b/src/Entities/Entity.cpp
new file mode 100644
index 000000000..3bea7bc01
--- /dev/null
+++ b/src/Entities/Entity.cpp
@@ -0,0 +1,1450 @@
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "Entity.h"
+#include "../World.h"
+#include "../Server.h"
+#include "../Root.h"
+#include "../Vector3d.h"
+#include "../Matrix4f.h"
+#include "../ReferenceManager.h"
+#include "../ClientHandle.h"
+#include "../Chunk.h"
+#include "../Simulator/FluidSimulator.h"
+#include "../PluginManager.h"
+#include "../Tracer.h"
+#include "Minecart.h"
+
+
+
+
+
+int cEntity::m_EntityCount = 0;
+cCriticalSection cEntity::m_CSCount;
+
+
+
+
+
+cEntity::cEntity(eEntityType a_EntityType, double a_X, double a_Y, double a_Z, double a_Width, double a_Height)
+ : m_UniqueID(0)
+ , m_Health(1)
+ , m_MaxHealth(1)
+ , m_AttachedTo(NULL)
+ , m_Attachee(NULL)
+ , m_Referencers(new cReferenceManager(cReferenceManager::RFMNGR_REFERENCERS))
+ , m_References(new cReferenceManager(cReferenceManager::RFMNGR_REFERENCES))
+ , m_HeadYaw( 0.0 )
+ , m_Rot(0.0, 0.0, 0.0)
+ , m_Pos(a_X, a_Y, a_Z)
+ , m_Mass (0.001) //Default 1g
+ , m_bDirtyHead(true)
+ , m_bDirtyOrientation(true)
+ , m_bDirtyPosition(true)
+ , m_bDirtySpeed(true)
+ , m_bOnGround( false )
+ , m_Gravity( -9.81f )
+ , m_IsInitialized(false)
+ , m_LastPosX( 0.0 )
+ , m_LastPosY( 0.0 )
+ , m_LastPosZ( 0.0 )
+ , m_TimeLastTeleportPacket(0)
+ , m_TimeLastMoveReltPacket(0)
+ , m_TimeLastSpeedPacket(0)
+ , m_EntityType(a_EntityType)
+ , m_World(NULL)
+ , m_TicksSinceLastBurnDamage(0)
+ , m_TicksSinceLastLavaDamage(0)
+ , m_TicksSinceLastFireDamage(0)
+ , m_TicksSinceLastVoidDamage(0)
+ , m_TicksLeftBurning(0)
+ , m_WaterSpeed(0, 0, 0)
+ , m_Width(a_Width)
+ , m_Height(a_Height)
+{
+ cCSLock Lock(m_CSCount);
+ m_EntityCount++;
+ m_UniqueID = m_EntityCount;
+}
+
+
+
+
+
+cEntity::~cEntity()
+{
+ ASSERT(!m_World->HasEntity(m_UniqueID)); // Before deleting, the entity needs to have been removed from the world
+
+ LOGD("Deleting entity %d at pos {%.2f, %.2f, %.2f} ~ [%d, %d]; ptr %p",
+ m_UniqueID,
+ m_Pos.x, m_Pos.y, m_Pos.z,
+ (int)(m_Pos.x / cChunkDef::Width), (int)(m_Pos.z / cChunkDef::Width),
+ this
+ );
+
+ if (m_AttachedTo != NULL)
+ {
+ Detach();
+ }
+ if (m_Attachee != NULL)
+ {
+ m_Attachee->Detach();
+ }
+
+ if (m_IsInitialized)
+ {
+ LOGWARNING("ERROR: Entity deallocated without being destroyed");
+ ASSERT(!"Entity deallocated without being destroyed or unlinked");
+ }
+ delete m_Referencers;
+ delete m_References;
+}
+
+
+
+
+
+const char * cEntity::GetClass(void) const
+{
+ return "cEntity";
+}
+
+
+
+
+
+const char * cEntity::GetClassStatic(void)
+{
+ return "cEntity";
+}
+
+
+
+
+
+const char * cEntity::GetParentClass(void) const
+{
+ return "";
+}
+
+
+
+
+
+bool cEntity::Initialize(cWorld * a_World)
+{
+ if (cPluginManager::Get()->CallHookSpawningEntity(*a_World, *this))
+ {
+ return false;
+ }
+
+ LOGD("Initializing entity #%d (%s) at {%.02f, %.02f, %.02f}",
+ m_UniqueID, GetClass(), m_Pos.x, m_Pos.y, m_Pos.z
+ );
+ m_IsInitialized = true;
+ m_World = a_World;
+ m_World->AddEntity(this);
+
+ cPluginManager::Get()->CallHookSpawnedEntity(*a_World, *this);
+
+ // Spawn the entity on the clients:
+ a_World->BroadcastSpawnEntity(*this);
+
+ return true;
+}
+
+
+
+
+
+void cEntity::WrapHeadYaw(void)
+{
+ while (m_HeadYaw > 180.f) m_HeadYaw -= 360.f; // Wrap it
+ while (m_HeadYaw < -180.f) m_HeadYaw += 360.f;
+}
+
+
+
+
+
+void cEntity::WrapRotation(void)
+{
+ while (m_Rot.x > 180.f) m_Rot.x -= 360.f; // Wrap it
+ while (m_Rot.x < -180.f) m_Rot.x += 360.f;
+ while (m_Rot.y > 180.f) m_Rot.y -= 360.f;
+ while (m_Rot.y < -180.f) m_Rot.y += 360.f;
+}
+
+
+
+
+void cEntity::WrapSpeed(void)
+{
+ // There shoudn't be a need for flipping the flag on because this function is called
+ // after any update, so the flag is already turned on
+ if (m_Speed.x > 78.0f) m_Speed.x = 78.0f;
+ else if (m_Speed.x < -78.0f) m_Speed.x = -78.0f;
+ if (m_Speed.y > 78.0f) m_Speed.y = 78.0f;
+ else if (m_Speed.y < -78.0f) m_Speed.y = -78.0f;
+ if (m_Speed.z > 78.0f) m_Speed.z = 78.0f;
+ else if (m_Speed.z < -78.0f) m_Speed.z = -78.0f;
+}
+
+
+
+
+
+void cEntity::Destroy(bool a_ShouldBroadcast)
+{
+ if (!m_IsInitialized)
+ {
+ return;
+ }
+
+ if (a_ShouldBroadcast)
+ {
+ m_World->BroadcastDestroyEntity(*this);
+ }
+
+ m_IsInitialized = false;
+
+ Destroyed();
+}
+
+
+
+
+
+void cEntity::TakeDamage(cEntity & a_Attacker)
+{
+ int RawDamage = a_Attacker.GetRawDamageAgainst(*this);
+
+ TakeDamage(dtAttack, &a_Attacker, RawDamage, a_Attacker.GetKnockbackAmountAgainst(*this));
+}
+
+
+
+
+
+void cEntity::TakeDamage(eDamageType a_DamageType, cEntity * a_Attacker, int a_RawDamage, double a_KnockbackAmount)
+{
+ int FinalDamage = a_RawDamage - GetArmorCoverAgainst(a_Attacker, a_DamageType, a_RawDamage);
+ cEntity::TakeDamage(a_DamageType, a_Attacker, a_RawDamage, FinalDamage, a_KnockbackAmount);
+}
+
+
+
+
+
+void cEntity::TakeDamage(eDamageType a_DamageType, cEntity * a_Attacker, int a_RawDamage, int a_FinalDamage, double a_KnockbackAmount)
+{
+ TakeDamageInfo TDI;
+ TDI.DamageType = a_DamageType;
+ TDI.Attacker = a_Attacker;
+ TDI.RawDamage = a_RawDamage;
+ TDI.FinalDamage = a_FinalDamage;
+ Vector3d Heading;
+ Heading.x = sin(GetRotation());
+ Heading.y = 0.4; // TODO: adjust the amount of "up" knockback when testing
+ Heading.z = cos(GetRotation());
+ TDI.Knockback = Heading * a_KnockbackAmount;
+ DoTakeDamage(TDI);
+}
+
+
+
+
+
+void cEntity::SetRotationFromSpeed(void)
+{
+ const double EPS = 0.0000001;
+ if ((abs(m_Speed.x) < EPS) && (abs(m_Speed.z) < EPS))
+ {
+ // atan2() may overflow or is undefined, pick any number
+ SetRotation(0);
+ return;
+ }
+ SetRotation(atan2(m_Speed.x, m_Speed.z) * 180 / PI);
+}
+
+
+
+
+
+void cEntity::SetPitchFromSpeed(void)
+{
+ const double EPS = 0.0000001;
+ double xz = sqrt(m_Speed.x * m_Speed.x + m_Speed.z * m_Speed.z); // Speed XZ-plane component
+ if ((abs(xz) < EPS) && (abs(m_Speed.y) < EPS))
+ {
+ // atan2() may overflow or is undefined, pick any number
+ SetPitch(0);
+ return;
+ }
+ SetPitch(atan2(m_Speed.y, xz) * 180 / PI);
+}
+
+
+
+
+
+void cEntity::DoTakeDamage(TakeDamageInfo & a_TDI)
+{
+ if (cRoot::Get()->GetPluginManager()->CallHookTakeDamage(*this, a_TDI))
+ {
+ return;
+ }
+
+ if (m_Health <= 0)
+ {
+ // Can't take damage if already dead
+ return;
+ }
+
+ m_Health -= (short)a_TDI.FinalDamage;
+
+ // TODO: Apply damage to armor
+
+ if (m_Health < 0)
+ {
+ m_Health = 0;
+ }
+
+ m_World->BroadcastEntityStatus(*this, ENTITY_STATUS_HURT);
+
+ if (m_Health <= 0)
+ {
+ KilledBy(a_TDI.Attacker);
+ }
+}
+
+
+
+
+
+int cEntity::GetRawDamageAgainst(const cEntity & a_Receiver)
+{
+ // Returns the hitpoints that this pawn can deal to a_Receiver using its equipped items
+ // Ref: http://www.minecraftwiki.net/wiki/Damage#Dealing_damage as of 2012_12_20
+ switch (this->GetEquippedWeapon().m_ItemType)
+ {
+ case E_ITEM_WOODEN_SWORD: return 4;
+ case E_ITEM_GOLD_SWORD: return 4;
+ case E_ITEM_STONE_SWORD: return 5;
+ case E_ITEM_IRON_SWORD: return 6;
+ case E_ITEM_DIAMOND_SWORD: return 7;
+
+ case E_ITEM_WOODEN_AXE: return 3;
+ case E_ITEM_GOLD_AXE: return 3;
+ case E_ITEM_STONE_AXE: return 4;
+ case E_ITEM_IRON_AXE: return 5;
+ case E_ITEM_DIAMOND_AXE: return 6;
+
+ case E_ITEM_WOODEN_PICKAXE: return 2;
+ case E_ITEM_GOLD_PICKAXE: return 2;
+ case E_ITEM_STONE_PICKAXE: return 3;
+ case E_ITEM_IRON_PICKAXE: return 4;
+ case E_ITEM_DIAMOND_PICKAXE: return 5;
+
+ case E_ITEM_WOODEN_SHOVEL: return 1;
+ case E_ITEM_GOLD_SHOVEL: return 1;
+ case E_ITEM_STONE_SHOVEL: return 2;
+ case E_ITEM_IRON_SHOVEL: return 3;
+ case E_ITEM_DIAMOND_SHOVEL: return 4;
+ }
+ // All other equipped items give a damage of 1:
+ return 1;
+}
+
+
+
+
+
+int cEntity::GetArmorCoverAgainst(const cEntity * a_Attacker, eDamageType a_DamageType, int a_Damage)
+{
+ // Returns the hitpoints out of a_RawDamage that the currently equipped armor would cover
+
+ // Filter out damage types that are not protected by armor:
+ // Ref.: http://www.minecraftwiki.net/wiki/Armor#Effects as of 2012_12_20
+ switch (a_DamageType)
+ {
+ case dtOnFire:
+ case dtSuffocating:
+ case dtDrowning: // TODO: This one could be a special case - in various MC versions (PC vs XBox) it is and isn't armor-protected
+ case dtStarving:
+ case dtInVoid:
+ case dtPoisoning:
+ case dtPotionOfHarming:
+ case dtFalling:
+ case dtLightning:
+ {
+ return 0;
+ }
+ }
+
+ // Add up all armor points:
+ // Ref.: http://www.minecraftwiki.net/wiki/Armor#Defense_points as of 2012_12_20
+ int ArmorValue = 0;
+ switch (GetEquippedHelmet().m_ItemType)
+ {
+ case E_ITEM_LEATHER_CAP: ArmorValue += 1; break;
+ case E_ITEM_GOLD_HELMET: ArmorValue += 2; break;
+ case E_ITEM_CHAIN_HELMET: ArmorValue += 2; break;
+ case E_ITEM_IRON_HELMET: ArmorValue += 2; break;
+ case E_ITEM_DIAMOND_HELMET: ArmorValue += 3; break;
+ }
+ switch (GetEquippedChestplate().m_ItemType)
+ {
+ case E_ITEM_LEATHER_TUNIC: ArmorValue += 3; break;
+ case E_ITEM_GOLD_CHESTPLATE: ArmorValue += 5; break;
+ case E_ITEM_CHAIN_CHESTPLATE: ArmorValue += 5; break;
+ case E_ITEM_IRON_CHESTPLATE: ArmorValue += 6; break;
+ case E_ITEM_DIAMOND_CHESTPLATE: ArmorValue += 8; break;
+ }
+ switch (GetEquippedLeggings().m_ItemType)
+ {
+ case E_ITEM_LEATHER_PANTS: ArmorValue += 2; break;
+ case E_ITEM_GOLD_LEGGINGS: ArmorValue += 3; break;
+ case E_ITEM_CHAIN_LEGGINGS: ArmorValue += 4; break;
+ case E_ITEM_IRON_LEGGINGS: ArmorValue += 5; break;
+ case E_ITEM_DIAMOND_LEGGINGS: ArmorValue += 6; break;
+ }
+ switch (GetEquippedBoots().m_ItemType)
+ {
+ case E_ITEM_LEATHER_BOOTS: ArmorValue += 1; break;
+ case E_ITEM_GOLD_BOOTS: ArmorValue += 1; break;
+ case E_ITEM_CHAIN_BOOTS: ArmorValue += 1; break;
+ case E_ITEM_IRON_BOOTS: ArmorValue += 2; break;
+ case E_ITEM_DIAMOND_BOOTS: ArmorValue += 3; break;
+ }
+
+ // TODO: Special armor cases, such as wool, saddles, dog's collar
+ // Ref.: http://www.minecraftwiki.net/wiki/Armor#Mob_armor as of 2012_12_20
+
+ // Now ArmorValue is in [0, 20] range, which corresponds to [0, 80%] protection. Calculate the hitpoints from that:
+ return a_Damage * (ArmorValue * 4) / 100;
+}
+
+
+
+
+
+double cEntity::GetKnockbackAmountAgainst(const cEntity & a_Receiver)
+{
+ // Returns the knockback amount that the currently equipped items would cause to a_Receiver on a hit
+
+ // TODO: Enchantments
+ return 1;
+}
+
+
+
+
+
+void cEntity::KilledBy(cEntity * a_Killer)
+{
+ m_Health = 0;
+
+ cRoot::Get()->GetPluginManager()->CallHookKilling(*this, a_Killer);
+
+ if (m_Health > 0)
+ {
+ // Plugin wants to 'unkill' the pawn. Abort
+ return;
+ }
+
+ // Drop loot:
+ cItems Drops;
+ GetDrops(Drops, a_Killer);
+ m_World->SpawnItemPickups(Drops, GetPosX(), GetPosY(), GetPosZ());
+
+ m_World->BroadcastEntityStatus(*this, ENTITY_STATUS_DEAD);
+}
+
+
+
+
+
+void cEntity::Heal(int a_HitPoints)
+{
+ m_Health += a_HitPoints;
+ if (m_Health > m_MaxHealth)
+ {
+ m_Health = m_MaxHealth;
+ }
+}
+
+
+
+
+
+void cEntity::SetHealth(int a_Health)
+{
+ m_Health = std::max(0, std::min(m_MaxHealth, a_Health));
+}
+
+
+
+
+
+void cEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ if (m_AttachedTo != NULL)
+ {
+ if ((m_Pos - m_AttachedTo->GetPosition()).Length() > 0.5)
+ {
+ SetPosition(m_AttachedTo->GetPosition());
+ }
+ }
+ else
+ {
+ if (a_Chunk.IsValid())
+ {
+ HandlePhysics(a_Dt, a_Chunk);
+ }
+ }
+ if (a_Chunk.IsValid())
+ {
+ TickBurning(a_Chunk);
+ }
+ if ((a_Chunk.IsValid()) && (GetPosY() < -46))
+ {
+ TickInVoid(a_Chunk);
+ }
+ else { m_TicksSinceLastVoidDamage = 0; }
+}
+
+
+
+
+
+void cEntity::HandlePhysics(float a_Dt, cChunk & a_Chunk)
+{
+ // TODO Add collision detection with entities.
+ a_Dt /= 1000; // Convert from msec to sec
+ Vector3d NextPos = Vector3d(GetPosX(),GetPosY(),GetPosZ());
+ Vector3d NextSpeed = Vector3d(GetSpeedX(),GetSpeedY(),GetSpeedZ());
+ int BlockX = (int) floor(NextPos.x);
+ int BlockY = (int) floor(NextPos.y);
+ int BlockZ = (int) floor(NextPos.z);
+
+ if ((BlockY >= cChunkDef::Height) || (BlockY < 0))
+ {
+ // Outside of the world
+
+ cChunk * NextChunk = a_Chunk.GetNeighborChunk(BlockX, BlockZ);
+ // See if we can commit our changes. If not, we will discard them.
+ if (NextChunk != NULL)
+ {
+ SetSpeed(NextSpeed);
+ NextPos += (NextSpeed * a_Dt);
+ SetPosition(NextPos);
+ }
+ return;
+ }
+
+ // Make sure we got the correct chunk and a valid one. No one ever knows...
+ cChunk * NextChunk = a_Chunk.GetNeighborChunk(BlockX, BlockZ);
+ if (NextChunk != NULL)
+ {
+ int RelBlockX = BlockX - (NextChunk->GetPosX() * cChunkDef::Width);
+ int RelBlockZ = BlockZ - (NextChunk->GetPosZ() * cChunkDef::Width);
+ BLOCKTYPE BlockIn = NextChunk->GetBlock( RelBlockX, BlockY, RelBlockZ );
+ BLOCKTYPE BlockBelow = (BlockY > 0) ? NextChunk->GetBlock(RelBlockX, BlockY - 1, RelBlockZ) : E_BLOCK_AIR;
+ if (!g_BlockIsSolid[BlockIn]) // Making sure we are not inside a solid block
+ {
+ if (m_bOnGround) // check if it's still on the ground
+ {
+ if (!g_BlockIsSolid[BlockBelow]) // Check if block below is air or water.
+ {
+ m_bOnGround = false;
+ }
+ }
+ }
+ else
+ {
+ // Push out entity.
+ BLOCKTYPE GotBlock;
+
+ static const struct
+ {
+ int x, y, z;
+ } gCrossCoords[] =
+ {
+ { 1, 0, 0},
+ {-1, 0, 0},
+ { 0, 0, 1},
+ { 0, 0, -1},
+ } ;
+
+ bool IsNoAirSurrounding = true;
+ for (int i = 0; i < ARRAYCOUNT(gCrossCoords); i++)
+ {
+ if (!NextChunk->UnboundedRelGetBlockType(RelBlockX + gCrossCoords[i].x, BlockY, RelBlockZ + gCrossCoords[i].z, GotBlock))
+ {
+ // The pickup is too close to an unloaded chunk, bail out of any physics handling
+ return;
+ }
+ if (!g_BlockIsSolid[GotBlock])
+ {
+ NextPos.x += gCrossCoords[i].x;
+ NextPos.z += gCrossCoords[i].z;
+ IsNoAirSurrounding = false;
+ break;
+ }
+ } // for i - gCrossCoords[]
+
+ if (IsNoAirSurrounding)
+ {
+ NextPos.y += 0.5;
+ }
+
+ m_bOnGround = true;
+
+ LOGD("Entity #%d (%s) is inside a block at {%d, %d, %d}",
+ m_UniqueID, GetClass(), BlockX, BlockY, BlockZ
+ );
+ }
+
+ if (!m_bOnGround)
+ {
+ float fallspeed;
+ if (IsBlockWater(BlockIn))
+ {
+ fallspeed = m_Gravity * a_Dt / 3; // Fall 3x slower in water.
+ }
+ else if (IsBlockRail(BlockBelow) && IsMinecart()) // Rails aren't solid, except for Minecarts
+ {
+ fallspeed = 0;
+ m_bOnGround = true;
+ }
+ else if (BlockIn == E_BLOCK_COBWEB)
+ {
+ NextSpeed.y *= 0.05; // Reduce overall falling speed
+ fallspeed = 0; // No falling.
+ }
+ else
+ {
+ // Normal gravity
+ fallspeed = m_Gravity * a_Dt;
+ }
+ NextSpeed.y += fallspeed;
+ }
+ else
+ {
+ if (IsMinecart())
+ {
+ if (!IsBlockRail(BlockBelow))
+ {
+ // Friction if minecart is off track, otherwise, Minecart.cpp handles this
+ if (NextSpeed.SqrLength() > 0.0004f)
+ {
+ NextSpeed.x *= 0.7f / (1 + a_Dt);
+ if (fabs(NextSpeed.x) < 0.05)
+ {
+ NextSpeed.x = 0;
+ }
+ NextSpeed.z *= 0.7f / (1 + a_Dt);
+ if (fabs(NextSpeed.z) < 0.05)
+ {
+ NextSpeed.z = 0;
+ }
+ }
+ }
+ }
+ else
+ {
+ // Friction for non-minecarts
+ if (NextSpeed.SqrLength() > 0.0004f)
+ {
+ NextSpeed.x *= 0.7f / (1 + a_Dt);
+ if (fabs(NextSpeed.x) < 0.05)
+ {
+ NextSpeed.x = 0;
+ }
+ NextSpeed.z *= 0.7f / (1 + a_Dt);
+ if (fabs(NextSpeed.z) < 0.05)
+ {
+ NextSpeed.z = 0;
+ }
+ }
+ }
+ }
+
+ // Adjust X and Z speed for COBWEB temporary. This speed modification should be handled inside block handlers since we
+ // might have different speed modifiers according to terrain.
+ if (BlockIn == E_BLOCK_COBWEB)
+ {
+ NextSpeed.x *= 0.25;
+ NextSpeed.z *= 0.25;
+ }
+
+ //Get water direction
+ Direction WaterDir = m_World->GetWaterSimulator()->GetFlowingDirection(BlockX, BlockY, BlockZ);
+
+ m_WaterSpeed *= 0.9f; //Reduce speed each tick
+
+ switch(WaterDir)
+ {
+ case X_PLUS:
+ m_WaterSpeed.x = 0.2f;
+ m_bOnGround = false;
+ break;
+ case X_MINUS:
+ m_WaterSpeed.x = -0.2f;
+ m_bOnGround = false;
+ break;
+ case Z_PLUS:
+ m_WaterSpeed.z = 0.2f;
+ m_bOnGround = false;
+ break;
+ case Z_MINUS:
+ m_WaterSpeed.z = -0.2f;
+ m_bOnGround = false;
+ break;
+
+ default:
+ break;
+ }
+
+ if (fabs(m_WaterSpeed.x) < 0.05)
+ {
+ m_WaterSpeed.x = 0;
+ }
+
+ if (fabs(m_WaterSpeed.z) < 0.05)
+ {
+ m_WaterSpeed.z = 0;
+ }
+
+ NextSpeed += m_WaterSpeed;
+
+ if( NextSpeed.SqrLength() > 0.f )
+ {
+ cTracer Tracer( GetWorld() );
+ int Ret = Tracer.Trace( NextPos, NextSpeed, 2 );
+ if( Ret ) // Oh noez! we hit something
+ {
+ // Set to hit position
+ if( (Tracer.RealHit - NextPos).SqrLength() <= ( NextSpeed * a_Dt ).SqrLength() )
+ {
+ if( Ret == 1 )
+ {
+ if( Tracer.HitNormal.x != 0.f ) NextSpeed.x = 0.f;
+ if( Tracer.HitNormal.y != 0.f ) NextSpeed.y = 0.f;
+ if( Tracer.HitNormal.z != 0.f ) NextSpeed.z = 0.f;
+
+ if( Tracer.HitNormal.y > 0 ) // means on ground
+ {
+ m_bOnGround = true;
+ }
+ }
+ NextPos.Set(Tracer.RealHit.x,Tracer.RealHit.y,Tracer.RealHit.z);
+ NextPos.x += Tracer.HitNormal.x * 0.3f;
+ NextPos.y += Tracer.HitNormal.y * 0.05f; // Any larger produces entity vibration-upon-the-spot
+ NextPos.z += Tracer.HitNormal.z * 0.3f;
+ }
+ else
+ {
+ NextPos += (NextSpeed * a_Dt);
+ }
+ }
+ else
+ {
+ // We didn't hit anything, so move =]
+ NextPos += (NextSpeed * a_Dt);
+ }
+ }
+ BlockX = (int) floor(NextPos.x);
+ BlockZ = (int) floor(NextPos.z);
+ NextChunk = NextChunk->GetNeighborChunk(BlockX,BlockZ);
+ // See if we can commit our changes. If not, we will discard them.
+ if (NextChunk != NULL)
+ {
+ if (NextPos.x != GetPosX()) SetPosX(NextPos.x);
+ if (NextPos.y != GetPosY()) SetPosY(NextPos.y);
+ if (NextPos.z != GetPosZ()) SetPosZ(NextPos.z);
+ if (NextSpeed.x != GetSpeedX()) SetSpeedX(NextSpeed.x);
+ if (NextSpeed.y != GetSpeedY()) SetSpeedY(NextSpeed.y);
+ if (NextSpeed.z != GetSpeedZ()) SetSpeedZ(NextSpeed.z);
+ }
+ }
+}
+
+
+
+
+
+void cEntity::TickBurning(cChunk & a_Chunk)
+{
+ // Remember the current burning state:
+ bool HasBeenBurning = (m_TicksLeftBurning > 0);
+
+ // Do the burning damage:
+ if (m_TicksLeftBurning > 0)
+ {
+ m_TicksSinceLastBurnDamage++;
+ if (m_TicksSinceLastBurnDamage >= BURN_TICKS_PER_DAMAGE)
+ {
+ TakeDamage(dtOnFire, NULL, BURN_DAMAGE, 0);
+ m_TicksSinceLastBurnDamage = 0;
+ }
+ m_TicksLeftBurning--;
+ }
+
+ // Update the burning times, based on surroundings:
+ int MinRelX = (int)floor(GetPosX() - m_Width / 2) - a_Chunk.GetPosX() * cChunkDef::Width;
+ int MaxRelX = (int)floor(GetPosX() + m_Width / 2) - a_Chunk.GetPosX() * cChunkDef::Width;
+ int MinRelZ = (int)floor(GetPosZ() - m_Width / 2) - a_Chunk.GetPosZ() * cChunkDef::Width;
+ int MaxRelZ = (int)floor(GetPosZ() + m_Width / 2) - a_Chunk.GetPosZ() * cChunkDef::Width;
+ int MinY = std::max(0, std::min(cChunkDef::Height - 1, (int)floor(GetPosY())));
+ int MaxY = std::max(0, std::min(cChunkDef::Height - 1, (int)ceil (GetPosY() + m_Height)));
+ bool HasWater = false;
+ bool HasLava = false;
+ bool HasFire = false;
+
+ for (int x = MinRelX; x <= MaxRelX; x++)
+ {
+ for (int z = MinRelZ; z <= MaxRelZ; z++)
+ {
+ int RelX = x;
+ int RelZ = z;
+ cChunk * CurChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(RelX, RelZ);
+ if (CurChunk == NULL)
+ {
+ continue;
+ }
+ for (int y = MinY; y <= MaxY; y++)
+ {
+ switch (CurChunk->GetBlock(RelX, y, RelZ))
+ {
+ case E_BLOCK_FIRE:
+ {
+ HasFire = true;
+ break;
+ }
+ case E_BLOCK_LAVA:
+ case E_BLOCK_STATIONARY_LAVA:
+ {
+ HasLava = true;
+ break;
+ }
+ case E_BLOCK_STATIONARY_WATER:
+ case E_BLOCK_WATER:
+ {
+ HasWater = true;
+ break;
+ }
+ } // switch (BlockType)
+ } // for y
+ } // for z
+ } // for x
+
+ if (HasWater)
+ {
+ // Extinguish the fire
+ m_TicksLeftBurning = 0;
+ }
+
+ if (HasLava)
+ {
+ // Burn:
+ m_TicksLeftBurning = BURN_TICKS;
+
+ // Periodically damage:
+ m_TicksSinceLastLavaDamage++;
+ if (m_TicksSinceLastLavaDamage >= LAVA_TICKS_PER_DAMAGE)
+ {
+ TakeDamage(dtLavaContact, NULL, LAVA_DAMAGE, 0);
+ m_TicksSinceLastLavaDamage = 0;
+ }
+ }
+ else
+ {
+ m_TicksSinceLastLavaDamage = 0;
+ }
+
+ if (HasFire)
+ {
+ // Burn:
+ m_TicksLeftBurning = BURN_TICKS;
+
+ // Periodically damage:
+ m_TicksSinceLastFireDamage++;
+ if (m_TicksSinceLastFireDamage >= FIRE_TICKS_PER_DAMAGE)
+ {
+ TakeDamage(dtFireContact, NULL, FIRE_DAMAGE, 0);
+ m_TicksSinceLastFireDamage = 0;
+ }
+ }
+ else
+ {
+ m_TicksSinceLastFireDamage = 0;
+ }
+
+ // If just started / finished burning, notify descendants:
+ if ((m_TicksLeftBurning > 0) && !HasBeenBurning)
+ {
+ OnStartedBurning();
+ }
+ else if ((m_TicksLeftBurning <= 0) && HasBeenBurning)
+ {
+ OnFinishedBurning();
+ }
+}
+
+
+
+
+
+void cEntity::TickInVoid(cChunk & a_Chunk)
+{
+ if (m_TicksSinceLastVoidDamage == 20)
+ {
+ TakeDamage(dtInVoid, NULL, 2, 0);
+ m_TicksSinceLastVoidDamage = 0;
+ }
+ else
+ {
+ m_TicksSinceLastVoidDamage++;
+ }
+}
+
+
+
+
+
+/// Called when the entity starts burning
+void cEntity::OnStartedBurning(void)
+{
+ // Broadcast the change:
+ m_World->BroadcastEntityMetadata(*this);
+}
+
+
+
+
+
+/// Called when the entity finishes burning
+void cEntity::OnFinishedBurning(void)
+{
+ // Broadcast the change:
+ m_World->BroadcastEntityMetadata(*this);
+}
+
+
+
+
+
+/// Sets the maximum value for the health
+void cEntity::SetMaxHealth(int a_MaxHealth)
+{
+ m_MaxHealth = a_MaxHealth;
+
+ // Reset health, if too high:
+ if (m_Health > a_MaxHealth)
+ {
+ m_Health = a_MaxHealth;
+ }
+}
+
+
+
+
+
+/// Puts the entity on fire for the specified amount of ticks
+void cEntity::StartBurning(int a_TicksLeftBurning)
+{
+ if (m_TicksLeftBurning > 0)
+ {
+ // Already burning, top up the ticks left burning and bail out:
+ m_TicksLeftBurning = std::max(m_TicksLeftBurning, a_TicksLeftBurning);
+ return;
+ }
+
+ m_TicksLeftBurning = a_TicksLeftBurning;
+ OnStartedBurning();
+}
+
+
+
+
+
+/// Stops the entity from burning, resets all burning timers
+void cEntity::StopBurning(void)
+{
+ bool HasBeenBurning = (m_TicksLeftBurning > 0);
+ m_TicksLeftBurning = 0;
+ m_TicksSinceLastBurnDamage = 0;
+ m_TicksSinceLastFireDamage = 0;
+ m_TicksSinceLastLavaDamage = 0;
+
+ // Notify if the entity has stopped burning
+ if (HasBeenBurning)
+ {
+ OnFinishedBurning();
+ }
+}
+
+
+
+
+
+void cEntity::TeleportToEntity(cEntity & a_Entity)
+{
+ TeleportToCoords(a_Entity.GetPosX(), a_Entity.GetPosY(), a_Entity.GetPosZ());
+}
+
+
+
+
+
+void cEntity::TeleportToCoords(double a_PosX, double a_PosY, double a_PosZ)
+{
+ SetPosition(a_PosX, a_PosY, a_PosZ);
+ m_World->BroadcastTeleportEntity(*this);
+}
+
+
+
+
+
+void cEntity::BroadcastMovementUpdate(const cClientHandle * a_Exclude)
+{
+ //We need to keep updating the clients when there is movement or if there was a change in speed and after 2 ticks
+ if( (m_Speed.SqrLength() > 0.0004f || m_bDirtySpeed) && (m_World->GetWorldAge() - m_TimeLastSpeedPacket >= 2))
+ {
+ m_World->BroadcastEntityVelocity(*this,a_Exclude);
+ m_bDirtySpeed = false;
+ m_TimeLastSpeedPacket = m_World->GetWorldAge();
+ }
+
+ //Have to process position related packets this every two ticks
+ if (m_World->GetWorldAge() % 2 == 0)
+ {
+ int DiffX = (int) (floor(GetPosX() * 32.0) - floor(m_LastPosX * 32.0));
+ int DiffY = (int) (floor(GetPosY() * 32.0) - floor(m_LastPosY * 32.0));
+ int DiffZ = (int) (floor(GetPosZ() * 32.0) - floor(m_LastPosZ * 32.0));
+ Int64 DiffTeleportPacket = m_World->GetWorldAge() - m_TimeLastTeleportPacket;
+ // 4 blocks is max Relative So if the Diff is greater than 127 or. Send an absolute position every 20 seconds
+ if (DiffTeleportPacket >= 400 ||
+ ((DiffX > 127) || (DiffX < -128) ||
+ (DiffY > 127) || (DiffY < -128) ||
+ (DiffZ > 127) || (DiffZ < -128)))
+ {
+ //
+ m_World->BroadcastTeleportEntity(*this,a_Exclude);
+ m_TimeLastTeleportPacket = m_World->GetWorldAge();
+ m_TimeLastMoveReltPacket = m_TimeLastTeleportPacket; //Must synchronize.
+ m_LastPosX = GetPosX();
+ m_LastPosY = GetPosY();
+ m_LastPosZ = GetPosZ();
+ m_bDirtyPosition = false;
+ m_bDirtyOrientation = false;
+ }
+ else
+ {
+ Int64 DiffMoveRelPacket = m_World->GetWorldAge() - m_TimeLastMoveReltPacket;
+ //if the change is big enough.
+ if ((abs(DiffX) >= 4 || abs(DiffY) >= 4 || abs(DiffZ) >= 4 || DiffMoveRelPacket >= 60) && m_bDirtyPosition)
+ {
+ if (m_bDirtyOrientation)
+ {
+ m_World->BroadcastEntityRelMoveLook(*this, (char)DiffX, (char)DiffY, (char)DiffZ,a_Exclude);
+ m_bDirtyOrientation = false;
+ }
+ else
+ {
+ m_World->BroadcastEntityRelMove(*this, (char)DiffX, (char)DiffY, (char)DiffZ,a_Exclude);
+ }
+ m_LastPosX = GetPosX();
+ m_LastPosY = GetPosY();
+ m_LastPosZ = GetPosZ();
+ m_bDirtyPosition = false;
+ m_TimeLastMoveReltPacket = m_World->GetWorldAge();
+ }
+ else
+ {
+ if (m_bDirtyOrientation)
+ {
+ m_World->BroadcastEntityLook(*this,a_Exclude);
+ m_bDirtyOrientation = false;
+ }
+ }
+ }
+ if (m_bDirtyHead)
+ {
+ m_World->BroadcastEntityHeadLook(*this,a_Exclude);
+ m_bDirtyHead = false;
+ }
+ }
+}
+
+
+
+
+
+void cEntity::AttachTo(cEntity * a_AttachTo)
+{
+ if (m_AttachedTo == a_AttachTo)
+ {
+ // Already attached to that entity, nothing to do here
+ return;
+ }
+
+ // Detach from any previous entity:
+ Detach();
+
+ // Attach to the new entity:
+ m_AttachedTo = a_AttachTo;
+ a_AttachTo->m_Attachee = this;
+ m_World->BroadcastAttachEntity(*this, a_AttachTo);
+}
+
+
+
+
+
+void cEntity::Detach(void)
+{
+ if (m_AttachedTo == NULL)
+ {
+ // Attached to no entity, our work is done
+ return;
+ }
+ m_AttachedTo->m_Attachee = NULL;
+ m_AttachedTo = NULL;
+ m_World->BroadcastAttachEntity(*this, NULL);
+}
+
+
+
+
+
+bool cEntity::IsA(const char * a_ClassName) const
+{
+ return (strcmp(a_ClassName, "cEntity") == 0);
+}
+
+
+
+
+
+void cEntity::SetRot(const Vector3f & a_Rot)
+{
+ m_Rot = a_Rot;
+ m_bDirtyOrientation = true;
+}
+
+
+
+
+
+void cEntity::SetHeadYaw(double a_HeadYaw)
+{
+ m_HeadYaw = a_HeadYaw;
+ m_bDirtyHead = true;
+ WrapHeadYaw();
+}
+
+
+
+
+
+void cEntity::SetHeight(double a_Height)
+{
+ m_Height = a_Height;
+}
+
+
+
+
+
+void cEntity::SetMass(double a_Mass)
+{
+ if (a_Mass > 0)
+ {
+ m_Mass = a_Mass;
+ }
+ else
+ {
+ // Make sure that mass is not zero. 1g is the default because we
+ // have to choose a number. It's perfectly legal to have a mass
+ // less than 1g as long as is NOT equal or less than zero.
+ m_Mass = 0.001;
+ }
+}
+
+
+
+
+
+void cEntity::SetYaw(double a_Yaw)
+{
+ m_Rot.x = a_Yaw;
+ m_bDirtyOrientation = true;
+ WrapRotation();
+}
+
+
+
+
+
+void cEntity::SetPitch(double a_Pitch)
+{
+ m_Rot.y = a_Pitch;
+ m_bDirtyOrientation = true;
+ WrapRotation();
+}
+
+
+
+
+
+void cEntity::SetRoll(double a_Roll)
+{
+ m_Rot.z = a_Roll;
+ m_bDirtyOrientation = true;
+}
+
+
+
+
+
+void cEntity::SetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ)
+{
+ m_Speed.Set(a_SpeedX, a_SpeedY, a_SpeedZ);
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+void cEntity::SetSpeedX(double a_SpeedX)
+{
+ m_Speed.x = a_SpeedX;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+void cEntity::SetSpeedY(double a_SpeedY)
+{
+ m_Speed.y = a_SpeedY;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+void cEntity::SetSpeedZ(double a_SpeedZ)
+{
+ m_Speed.z = a_SpeedZ;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+
+void cEntity::SetWidth(double a_Width)
+{
+ m_Width = a_Width;
+}
+
+
+
+
+
+void cEntity::AddPosX(double a_AddPosX)
+{
+ m_Pos.x += a_AddPosX;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+void cEntity::AddPosY(double a_AddPosY)
+{
+ m_Pos.y += a_AddPosY;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+void cEntity::AddPosZ(double a_AddPosZ)
+{
+ m_Pos.z += a_AddPosZ;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+void cEntity::AddPosition(double a_AddPosX, double a_AddPosY, double a_AddPosZ)
+{
+ m_Pos.x += a_AddPosX;
+ m_Pos.y += a_AddPosY;
+ m_Pos.z += a_AddPosZ;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+void cEntity::AddSpeed(double a_AddSpeedX, double a_AddSpeedY, double a_AddSpeedZ)
+{
+ m_Speed.x += a_AddSpeedX;
+ m_Speed.y += a_AddSpeedY;
+ m_Speed.z += a_AddSpeedZ;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+
+void cEntity::AddSpeedX(double a_AddSpeedX)
+{
+ m_Speed.x += a_AddSpeedX;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+
+void cEntity::AddSpeedY(double a_AddSpeedY)
+{
+ m_Speed.y += a_AddSpeedY;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+
+void cEntity::AddSpeedZ(double a_AddSpeedZ)
+{
+ m_Speed.z += a_AddSpeedZ;
+ m_bDirtySpeed = true;
+ WrapSpeed();
+}
+
+
+
+
+
+void cEntity::SteerVehicle(float a_Forward, float a_Sideways)
+{
+ if (m_AttachedTo == NULL)
+ {
+ return;
+ }
+ if ((a_Forward != 0) || (a_Sideways != 0))
+ {
+ Vector3d LookVector = GetLookVector();
+ double AddSpeedX = LookVector.x * a_Forward + LookVector.z * a_Sideways;
+ double AddSpeedZ = LookVector.z * a_Forward - LookVector.x * a_Sideways;
+ m_AttachedTo->AddSpeed(AddSpeedX, 0, AddSpeedZ);
+ }
+}
+
+
+
+
+
+//////////////////////////////////////////////////////////////////////////
+// Get look vector (this is NOT a rotation!)
+Vector3d cEntity::GetLookVector(void) const
+{
+ Matrix4d m;
+ m.Init(Vector3f(), 0, m_Rot.x, -m_Rot.y);
+ Vector3d Look = m.Transform(Vector3d(0, 0, 1));
+ return Look;
+}
+
+
+
+
+
+//////////////////////////////////////////////////////////////////////////
+// Set position
+void cEntity::SetPosition(double a_PosX, double a_PosY, double a_PosZ)
+{
+ m_Pos.Set(a_PosX, a_PosY, a_PosZ);
+ m_bDirtyPosition = true;
+}
+
+
+
+
+
+void cEntity::SetPosX(double a_PosX)
+{
+ m_Pos.x = a_PosX;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+
+void cEntity::SetPosY(double a_PosY)
+{
+ m_Pos.y = a_PosY;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+
+void cEntity::SetPosZ(double a_PosZ)
+{
+ m_Pos.z = a_PosZ;
+ m_bDirtyPosition = true;
+}
+
+
+
+
+
+//////////////////////////////////////////////////////////////////////////
+// Reference stuffs
+void cEntity::AddReference(cEntity * & a_EntityPtr)
+{
+ m_References->AddReference(a_EntityPtr);
+ a_EntityPtr->ReferencedBy(a_EntityPtr);
+}
+
+
+
+
+
+void cEntity::ReferencedBy(cEntity * & a_EntityPtr)
+{
+ m_Referencers->AddReference(a_EntityPtr);
+}
+
+
+
+
+
+void cEntity::Dereference(cEntity * & a_EntityPtr)
+{
+ m_Referencers->Dereference(a_EntityPtr);
+}
+
+
+
+
diff --git a/src/Entities/Entity.h b/src/Entities/Entity.h
new file mode 100644
index 000000000..dafda7826
--- /dev/null
+++ b/src/Entities/Entity.h
@@ -0,0 +1,445 @@
+
+#pragma once
+
+#include "../Item.h"
+#include "../Vector3d.h"
+#include "../Vector3f.h"
+
+
+
+
+
+// Place this macro in the public section of each cEntity descendant class and you're done :)
+#define CLASS_PROTODEF(classname) \
+ virtual bool IsA(const char * a_ClassName) const override\
+ { \
+ return ((strcmp(a_ClassName, #classname) == 0) || super::IsA(a_ClassName)); \
+ } \
+ virtual const char * GetClass(void) const override \
+ { \
+ return #classname; \
+ } \
+ static const char * GetClassStatic(void) \
+ { \
+ return #classname; \
+ } \
+ virtual const char * GetParentClass(void) const override \
+ { \
+ return super::GetClass(); \
+ }
+
+
+
+
+
+class cWorld;
+class cReferenceManager;
+class cClientHandle;
+class cPlayer;
+class cChunk;
+
+
+
+
+
+// tolua_begin
+struct TakeDamageInfo
+{
+ eDamageType DamageType; // Where does the damage come from? Being hit / on fire / contact with cactus / ...
+ cEntity * Attacker; // The attacking entity; valid only for dtAttack
+ int RawDamage; // What damage would the receiver get without any armor. Usually: attacker mob type + weapons
+ int FinalDamage; // What actual damage will be received. Usually: m_RawDamage minus armor
+ Vector3d Knockback; // The amount and direction of knockback received from the damage
+ // TODO: Effects - list of effects that the hit is causing. Unknown representation yet
+} ;
+// tolua_end
+
+
+
+
+
+// tolua_begin
+class cEntity
+{
+public:
+
+ enum eEntityType
+ {
+ etEntity, // For all other types
+ etPlayer,
+ etPickup,
+ etMonster,
+ etFallingBlock,
+ etMinecart,
+ etBoat,
+ etTNT,
+ etProjectile,
+
+ // Common variations
+ etMob = etMonster, // DEPRECATED, use etMonster instead!
+ } ;
+
+ // tolua_end
+
+ enum
+ {
+ ENTITY_STATUS_HURT = 2,
+ ENTITY_STATUS_DEAD = 3,
+ ENTITY_STATUS_WOLF_TAMING = 6,
+ ENTITY_STATUS_WOLF_TAMED = 7,
+ ENTITY_STATUS_WOLF_SHAKING = 8,
+ ENTITY_STATUS_EATING_ACCEPTED = 9,
+ ENTITY_STATUS_SHEEP_EATING = 10,
+ ENTITY_STATUS_GOLEM_ROSING = 11,
+ ENTITY_STATUS_VILLAGER_HEARTS = 12,
+ ENTITY_STATUS_VILLAGER_ANGRY = 13,
+ ENTITY_STATUS_VILLAGER_HAPPY = 14,
+ ENTITY_STATUS_WITCH_MAGICKING = 15,
+ // It seems 16 (zombie conversion) is now done with metadata
+ ENTITY_STATUS_FIREWORK_EXPLODE= 17,
+ } ;
+
+ enum
+ {
+ FIRE_TICKS_PER_DAMAGE = 10, ///< How many ticks to wait between damaging an entity when it stands in fire
+ FIRE_DAMAGE = 1, ///< How much damage to deal when standing in fire
+ LAVA_TICKS_PER_DAMAGE = 10, ///< How many ticks to wait between damaging an entity when it stands in lava
+ LAVA_DAMAGE = 5, ///< How much damage to deal when standing in lava
+ BURN_TICKS_PER_DAMAGE = 20, ///< How many ticks to wait between damaging an entity when it is burning
+ BURN_DAMAGE = 1, ///< How much damage to deal when the entity is burning
+ BURN_TICKS = 200, ///< How long to keep an entity burning after it has stood in lava / fire
+ } ;
+
+ cEntity(eEntityType a_EntityType, double a_X, double a_Y, double a_Z, double a_Width, double a_Height);
+ virtual ~cEntity();
+
+ /// Spawns the entity in the world; returns true if spawned, false if not (plugin disallowed)
+ virtual bool Initialize(cWorld * a_World);
+
+ // tolua_begin
+
+ eEntityType GetEntityType(void) const { return m_EntityType; }
+
+ bool IsPlayer (void) const { return (m_EntityType == etPlayer); }
+ bool IsPickup (void) const { return (m_EntityType == etPickup); }
+ bool IsMob (void) const { return (m_EntityType == etMonster); }
+ bool IsFallingBlock(void) const { return (m_EntityType == etFallingBlock); }
+ bool IsMinecart (void) const { return (m_EntityType == etMinecart); }
+ bool IsBoat (void) const { return (m_EntityType == etBoat); }
+ bool IsTNT (void) const { return (m_EntityType == etTNT); }
+ bool IsProjectile (void) const { return (m_EntityType == etProjectile); }
+
+ /// Returns true if the entity is of the specified class or a subclass (cPawn's IsA("cEntity") returns true)
+ virtual bool IsA(const char * a_ClassName) const;
+
+ /// Returns the topmost class name for the object
+ virtual const char * GetClass(void) const;
+
+ // Returns the class name of this class
+ static const char * GetClassStatic(void);
+
+ /// Returns the topmost class's parent class name for the object. cEntity returns an empty string (no parent).
+ virtual const char * GetParentClass(void) const;
+
+ cWorld * GetWorld(void) const { return m_World; }
+
+ double GetHeadYaw (void) const { return m_HeadYaw; }
+ double GetHeight (void) const { return m_Height; }
+ double GetMass (void) const { return m_Mass; }
+ const Vector3d & GetPosition (void) const { return m_Pos; }
+ double GetPosX (void) const { return m_Pos.x; }
+ double GetPosY (void) const { return m_Pos.y; }
+ double GetPosZ (void) const { return m_Pos.z; }
+ const Vector3d & GetRot (void) const { return m_Rot; }
+ double GetRotation (void) const { return m_Rot.x; } // OBSOLETE, use GetYaw() instead
+ double GetYaw (void) const { return m_Rot.x; }
+ double GetPitch (void) const { return m_Rot.y; }
+ double GetRoll (void) const { return m_Rot.z; }
+ Vector3d GetLookVector(void) const;
+ const Vector3d & GetSpeed (void) const { return m_Speed; }
+ double GetSpeedX (void) const { return m_Speed.x; }
+ double GetSpeedY (void) const { return m_Speed.y; }
+ double GetSpeedZ (void) const { return m_Speed.z; }
+ double GetWidth (void) const { return m_Width; }
+
+ int GetChunkX(void) const {return (int)floor(m_Pos.x / cChunkDef::Width); }
+ int GetChunkZ(void) const {return (int)floor(m_Pos.z / cChunkDef::Width); }
+
+ void SetHeadYaw (double a_HeadYaw);
+ void SetHeight (double a_Height);
+ void SetMass (double a_Mass);
+ void SetPosX (double a_PosX);
+ void SetPosY (double a_PosY);
+ void SetPosZ (double a_PosZ);
+ void SetPosition(double a_PosX, double a_PosY, double a_PosZ);
+ void SetPosition(const Vector3d & a_Pos) { SetPosition(a_Pos.x, a_Pos.y, a_Pos.z); }
+ void SetRot (const Vector3f & a_Rot);
+ void SetRotation(double a_Rotation) { SetYaw(a_Rotation); } // OBSOLETE, use SetYaw() instead
+ void SetYaw (double a_Yaw);
+ void SetPitch (double a_Pitch);
+ void SetRoll (double a_Roll);
+ void SetSpeed (double a_SpeedX, double a_SpeedY, double a_SpeedZ);
+ void SetSpeed (const Vector3d & a_Speed) { SetSpeed(a_Speed.x, a_Speed.y, a_Speed.z); }
+ void SetSpeedX (double a_SpeedX);
+ void SetSpeedY (double a_SpeedY);
+ void SetSpeedZ (double a_SpeedZ);
+ void SetWidth (double a_Width);
+
+ void AddPosX (double a_AddPosX);
+ void AddPosY (double a_AddPosY);
+ void AddPosZ (double a_AddPosZ);
+ void AddPosition(double a_AddPosX, double a_AddPosY, double a_AddPosZ);
+ void AddPosition(const Vector3d & a_AddPos) { AddPosition(a_AddPos.x,a_AddPos.y,a_AddPos.z);}
+ void AddSpeed (double a_AddSpeedX, double a_AddSpeedY, double a_AddSpeedZ);
+ void AddSpeed (const Vector3d & a_AddSpeed) { AddSpeed(a_AddSpeed.x,a_AddSpeed.y,a_AddSpeed.z);}
+ void AddSpeedX (double a_AddSpeedX);
+ void AddSpeedY (double a_AddSpeedY);
+ void AddSpeedZ (double a_AddSpeedZ);
+
+ void SteerVehicle(float a_Forward, float a_Sideways);
+
+ inline int GetUniqueID(void) const { return m_UniqueID; }
+ inline bool IsDestroyed(void) const { return !m_IsInitialized; }
+
+ /// Schedules the entity for destroying; if a_ShouldBroadcast is set to true, broadcasts the DestroyEntity packet
+ void Destroy(bool a_ShouldBroadcast = true);
+
+ /// Makes this pawn take damage from an attack by a_Attacker. Damage values are calculated automatically and DoTakeDamage() called
+ void TakeDamage(cEntity & a_Attacker);
+
+ /// Makes this entity take the specified damage. The final damage is calculated using current armor, then DoTakeDamage() called
+ void TakeDamage(eDamageType a_DamageType, cEntity * a_Attacker, int a_RawDamage, double a_KnockbackAmount);
+
+ /// Makes this entity take the specified damage. The values are packed into a TDI, knockback calculated, then sent through DoTakeDamage()
+ void TakeDamage(eDamageType a_DamageType, cEntity * a_Attacker, int a_RawDamage, int a_FinalDamage, double a_KnockbackAmount);
+
+ float GetGravity(void) const { return m_Gravity; }
+
+ void SetGravity(float a_Gravity) { m_Gravity = a_Gravity; }
+
+ /// Sets the rotation to match the speed vector (entity goes "face-forward")
+ void SetRotationFromSpeed(void);
+
+ /// Sets the pitch to match the speed vector (entity gies "face-forward")
+ void SetPitchFromSpeed(void);
+
+ // tolua_end
+
+ /// Makes this entity take damage specified in the a_TDI. The TDI is sent through plugins first, then applied
+ virtual void DoTakeDamage(TakeDamageInfo & a_TDI);
+
+ // tolua_begin
+
+ /// Returns the hitpoints that this pawn can deal to a_Receiver using its equipped items
+ virtual int GetRawDamageAgainst(const cEntity & a_Receiver);
+
+ /// Returns the hitpoints out of a_RawDamage that the currently equipped armor would cover
+ virtual int GetArmorCoverAgainst(const cEntity * a_Attacker, eDamageType a_DamageType, int a_RawDamage);
+
+ /// Returns the knockback amount that the currently equipped items would cause to a_Receiver on a hit
+ virtual double GetKnockbackAmountAgainst(const cEntity & a_Receiver);
+
+ /// Returns the curently equipped weapon; empty item if none
+ virtual cItem GetEquippedWeapon(void) const { return cItem(); }
+
+ /// Returns the currently equipped helmet; empty item if none
+ virtual cItem GetEquippedHelmet(void) const { return cItem(); }
+
+ /// Returns the currently equipped chestplate; empty item if none
+ virtual cItem GetEquippedChestplate(void) const { return cItem(); }
+
+ /// Returns the currently equipped leggings; empty item if none
+ virtual cItem GetEquippedLeggings(void) const { return cItem(); }
+
+ /// Returns the currently equipped boots; empty item if none
+ virtual cItem GetEquippedBoots(void) const { return cItem(); }
+
+ /// Called when the health drops below zero. a_Killer may be NULL (environmental damage)
+ virtual void KilledBy(cEntity * a_Killer);
+
+ /// Heals the specified amount of HPs
+ void Heal(int a_HitPoints);
+
+ /// Returns the health of this entity
+ int GetHealth(void) const { return m_Health; }
+
+ /// Sets the health of this entity; doesn't broadcast any hurt animation
+ void SetHealth(int a_Health);
+
+ // tolua_end
+
+ virtual void Tick(float a_Dt, cChunk & a_Chunk);
+
+ /// Handles the physics of the entity - updates position based on speed, updates speed based on environment
+ virtual void HandlePhysics(float a_Dt, cChunk & a_Chunk);
+
+ /// Updates the state related to this entity being on fire
+ virtual void TickBurning(cChunk & a_Chunk);
+
+ /// Handles when the entity is in the void
+ virtual void TickInVoid(cChunk & a_Chunk);
+
+ /// Called when the entity starts burning
+ virtual void OnStartedBurning(void);
+
+ /// Called when the entity finishes burning
+ virtual void OnFinishedBurning(void);
+
+ // tolua_begin
+
+ /// Sets the maximum value for the health
+ void SetMaxHealth(int a_MaxHealth);
+
+ int GetMaxHealth(void) const { return m_MaxHealth; }
+
+ /// Puts the entity on fire for the specified amount of ticks
+ void StartBurning(int a_TicksLeftBurning);
+
+ /// Stops the entity from burning, resets all burning timers
+ void StopBurning(void);
+
+ // tolua_end
+
+ /** Descendants override this function to send a command to the specified client to spawn the entity on the client.
+ To spawn on all eligible clients, use cChunkMap::BroadcastSpawnEntity()
+ */
+ virtual void SpawnOn(cClientHandle & a_Client) = 0;
+
+ // tolua_begin
+
+ /// Teleports to the entity specified
+ virtual void TeleportToEntity(cEntity & a_Entity);
+
+ /// Teleports to the coordinates specified
+ virtual void TeleportToCoords(double a_PosX, double a_PosY, double a_PosZ);
+
+ // tolua_end
+
+ /// Updates clients of changes in the entity.
+ virtual void BroadcastMovementUpdate(const cClientHandle * a_Exclude = NULL);
+
+ /// Attaches to the specified entity; detaches from any previous one first
+ void AttachTo(cEntity * a_AttachTo);
+
+ /// Detaches from the currently attached entity, if any
+ void Detach(void);
+
+ /// Makes sure head yaw is not over the specified range.
+ void WrapHeadYaw();
+
+ /// Makes sure rotation is not over the specified range.
+ void WrapRotation();
+
+ /// Makes speed is not over 20. Max speed is 20 blocks / second
+ void WrapSpeed();
+
+ // tolua_begin
+
+ // COMMON metadata flags; descendants may override the defaults:
+ virtual bool IsOnFire (void) const {return (m_TicksLeftBurning > 0); }
+ virtual bool IsCrouched (void) const {return false; }
+ virtual bool IsRiding (void) const {return false; }
+ virtual bool IsSprinting(void) const {return false; }
+ virtual bool IsRclking (void) const {return false; }
+ virtual bool IsInvisible(void) const {return false; }
+
+ // tolua_end
+
+ /// Called when the specified player right-clicks this entity
+ virtual void OnRightClicked(cPlayer & a_Player) {};
+
+ /// Returns the list of drops for this pawn when it is killed. May check a_Killer for special handling (sword of looting etc.). Called from KilledBy().
+ virtual void GetDrops(cItems & a_Drops, cEntity * a_Killer = NULL) {}
+
+protected:
+ static cCriticalSection m_CSCount;
+ static int m_EntityCount;
+
+ int m_UniqueID;
+
+ int m_Health;
+ int m_MaxHealth;
+
+ /// The entity to which this entity is attached (vehicle), NULL if none
+ cEntity * m_AttachedTo;
+
+ /// The entity which is attached to this entity (rider), NULL if none
+ cEntity * m_Attachee;
+
+ cReferenceManager* m_Referencers;
+ cReferenceManager* m_References;
+
+ // Flags that signal that we haven't updated the clients with the latest.
+ bool m_bDirtyHead;
+ bool m_bDirtyOrientation;
+ bool m_bDirtyPosition;
+ bool m_bDirtySpeed;
+
+ bool m_bOnGround;
+ float m_Gravity;
+
+ // Last Position.
+ double m_LastPosX, m_LastPosY, m_LastPosZ;
+
+ // This variables keep track of the last time a packet was sent
+ Int64 m_TimeLastTeleportPacket,m_TimeLastMoveReltPacket,m_TimeLastSpeedPacket; // In ticks
+
+ bool m_IsInitialized; // Is set to true when it's initialized, until it's destroyed (Initialize() till Destroy() )
+
+ eEntityType m_EntityType;
+
+ cWorld * m_World;
+
+ /// Time, in ticks, since the last damage dealt by being on fire. Valid only if on fire (IsOnFire())
+ int m_TicksSinceLastBurnDamage;
+
+ /// Time, in ticks, since the last damage dealt by standing in lava. Reset to zero when moving out of lava.
+ int m_TicksSinceLastLavaDamage;
+
+ /// Time, in ticks, since the last damage dealt by standing in fire. Reset to zero when moving out of fire.
+ int m_TicksSinceLastFireDamage;
+
+ /// Time, in ticks, until the entity extinguishes its fire
+ int m_TicksLeftBurning;
+
+ /// Time, in ticks, since the last damage dealt by the void. Reset to zero when moving out of the void.
+ int m_TicksSinceLastVoidDamage;
+
+ virtual void Destroyed(void) {} // Called after the entity has been destroyed
+
+ void SetWorld(cWorld * a_World) { m_World = a_World; }
+
+ friend class cReferenceManager;
+ void AddReference( cEntity*& a_EntityPtr );
+ void ReferencedBy( cEntity*& a_EntityPtr );
+ void Dereference( cEntity*& a_EntityPtr );
+
+private:
+ // Measured in degrees (MAX 360°)
+ double m_HeadYaw;
+ // Measured in meter/second (m/s)
+ Vector3d m_Speed;
+ // Measured in degrees (MAX 360°)
+ Vector3d m_Rot;
+
+ /// Position of the entity's XZ center and Y bottom
+ Vector3d m_Pos;
+
+ // Measured in meter / second
+ Vector3d m_WaterSpeed;
+
+ // Measured in Kilograms (Kg)
+ double m_Mass;
+
+ /// Width of the entity, in the XZ plane. Since entities are represented as cylinders, this is more of a diameter.
+ double m_Width;
+
+ /// Height of the entity (Y axis)
+ double m_Height;
+} ; // tolua_export
+
+typedef std::list<cEntity *> cEntityList;
+
+
+
+
diff --git a/src/Entities/FallingBlock.cpp b/src/Entities/FallingBlock.cpp
new file mode 100644
index 000000000..9fcd9ac80
--- /dev/null
+++ b/src/Entities/FallingBlock.cpp
@@ -0,0 +1,93 @@
+#include "Globals.h"
+
+#include "FallingBlock.h"
+#include "../World.h"
+#include "../ClientHandle.h"
+#include "../Simulator/SandSimulator.h"
+#include "../Chunk.h"
+
+
+
+
+
+cFallingBlock::cFallingBlock(const Vector3i & a_BlockPosition, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta) :
+ super(etFallingBlock, a_BlockPosition.x + 0.5f, a_BlockPosition.y + 0.5f, a_BlockPosition.z + 0.5f, 0.98, 0.98),
+ m_BlockType(a_BlockType),
+ m_BlockMeta(a_BlockMeta),
+ m_OriginalPosition(a_BlockPosition)
+{
+}
+
+
+
+
+
+void cFallingBlock::SpawnOn(cClientHandle & a_ClientHandle)
+{
+ a_ClientHandle.SendSpawnFallingBlock(*this);
+}
+
+
+
+
+
+void cFallingBlock::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ float MilliDt = a_Dt * 0.001f;
+ AddSpeedY(MilliDt * -9.8f);
+ AddPosY(GetSpeedY() * MilliDt);
+
+ // GetWorld()->BroadcastTeleportEntity(*this); // Test position
+
+ int BlockX = m_OriginalPosition.x;
+ int BlockY = (int)(GetPosY() - 0.5);
+ int BlockZ = m_OriginalPosition.z;
+
+ if (BlockY < 0)
+ {
+ // Fallen out of this world, just continue falling until out of sight, then destroy:
+ if (BlockY < 100)
+ {
+ Destroy(true);
+ }
+ return;
+ }
+
+ if (BlockY >= cChunkDef::Height)
+ {
+ // Above the world, just wait for it to fall back down
+ return;
+ }
+
+ int idx = a_Chunk.MakeIndexNoCheck(BlockX - a_Chunk.GetPosX() * cChunkDef::Width, BlockY, BlockZ - a_Chunk.GetPosZ() * cChunkDef::Width);
+ BLOCKTYPE BlockBelow = a_Chunk.GetBlock(idx);
+ NIBBLETYPE BelowMeta = a_Chunk.GetMeta(idx);
+ if (cSandSimulator::DoesBreakFallingThrough(BlockBelow, BelowMeta))
+ {
+ // Fallen onto a block that breaks this into pickups (e. g. half-slab)
+ // Must finish the fall with coords one below the block:
+ cSandSimulator::FinishFalling(m_World, BlockX, BlockY, BlockZ, m_BlockType, m_BlockMeta);
+ Destroy(true);
+ return;
+ }
+ else if (!cSandSimulator::CanContinueFallThrough(BlockBelow))
+ {
+ // Fallen onto a solid block
+ /*
+ LOGD(
+ "Sand: Checked below at {%d, %d, %d} (rel {%d, %d, %d}), it's %s, finishing the fall.",
+ BlockX, BlockY, BlockZ,
+ BlockX - a_Chunk.GetPosX() * cChunkDef::Width, BlockY, BlockZ - a_Chunk.GetPosZ() * cChunkDef::Width,
+ ItemTypeToString(BlockBelow).c_str()
+ );
+ */
+
+ cSandSimulator::FinishFalling(m_World, BlockX, BlockY + 1, BlockZ, m_BlockType, m_BlockMeta);
+ Destroy(true);
+ return;
+ }
+}
+
+
+
+
diff --git a/src/Entities/FallingBlock.h b/src/Entities/FallingBlock.h
new file mode 100644
index 000000000..5ba9909bb
--- /dev/null
+++ b/src/Entities/FallingBlock.h
@@ -0,0 +1,43 @@
+
+#pragma once
+
+#include "Entity.h"
+
+
+
+
+class cPlayer;
+class cItem;
+
+
+
+
+
+
+class cFallingBlock :
+ public cEntity
+{
+ typedef cEntity super;
+
+public:
+ CLASS_PROTODEF(cFallingBlock);
+
+ /// Creates a new falling block. a_BlockPosition is expected in world coords
+ cFallingBlock(const Vector3i & a_BlockPosition, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta);
+
+ BLOCKTYPE GetBlockType(void) const { return m_BlockType; }
+ NIBBLETYPE GetBlockMeta(void) const { return m_BlockMeta; }
+
+ // cEntity overrides:
+ virtual void SpawnOn(cClientHandle & a_ClientHandle) override;
+ virtual void Tick(float a_Dt, cChunk & a_Chunk) override;
+
+private:
+ BLOCKTYPE m_BlockType;
+ NIBBLETYPE m_BlockMeta;
+ Vector3i m_OriginalPosition; // Position where the falling block has started, in world coords
+} ;
+
+
+
+
diff --git a/src/Entities/Minecart.cpp b/src/Entities/Minecart.cpp
new file mode 100644
index 000000000..f75e23d8b
--- /dev/null
+++ b/src/Entities/Minecart.cpp
@@ -0,0 +1,541 @@
+
+// Minecart.cpp
+
+// Implements the cMinecart class representing a minecart in the world
+// Indiana Jones!
+
+#include "Globals.h"
+#include "Minecart.h"
+#include "../World.h"
+#include "../ClientHandle.h"
+#include "../Chunk.h"
+#include "Player.h"
+
+
+
+
+
+cMinecart::cMinecart(ePayload a_Payload, double a_X, double a_Y, double a_Z) :
+ super(etMinecart, a_X, a_Y, a_Z, 0.98, 0.7),
+ m_Payload(a_Payload),
+ m_LastDamage(0)
+{
+ SetMass(20.f);
+ SetMaxHealth(6);
+ SetHealth(6);
+}
+
+
+
+
+void cMinecart::SpawnOn(cClientHandle & a_ClientHandle)
+{
+ char SubType = 0;
+ switch (m_Payload)
+ {
+ case mpNone: SubType = 0; break;
+ case mpChest: SubType = 1; break;
+ case mpFurnace: SubType = 2; break;
+ case mpTNT: SubType = 3; break;
+ case mpHopper: SubType = 5; break;
+ default:
+ {
+ ASSERT(!"Unknown payload, cannot spawn on client");
+ return;
+ }
+ }
+ a_ClientHandle.SendSpawnVehicle(*this, 10, SubType); // 10 = Minecarts, SubType = What type of Minecart
+}
+
+
+
+
+
+void cMinecart::HandlePhysics(float a_Dt, cChunk & a_Chunk)
+{
+ int PosY = (int)floor(GetPosY());
+ if ((PosY <= 0) || (PosY >= cChunkDef::Height))
+ {
+ // Outside the world, just process normal falling physics
+ super::HandlePhysics(a_Dt, a_Chunk);
+ BroadcastMovementUpdate();
+ return;
+ }
+
+ int RelPosX = (int)floor(GetPosX()) - a_Chunk.GetPosX() * cChunkDef::Width;
+ int RelPosZ = (int)floor(GetPosZ()) - a_Chunk.GetPosZ() * cChunkDef::Width;
+ cChunk * Chunk = a_Chunk.GetRelNeighborChunkAdjustCoords(RelPosX, RelPosZ);
+ if (Chunk == NULL)
+ {
+ // Inside an unloaded chunk, bail out all processing
+ return;
+ }
+ BLOCKTYPE BelowType = Chunk->GetBlock(RelPosX, PosY - 1, RelPosZ);
+ BLOCKTYPE InsideType = Chunk->GetBlock(RelPosX, PosY, RelPosZ);
+
+ if (IsBlockRail(BelowType))
+ {
+ HandleRailPhysics(a_Dt, *Chunk);
+ }
+ else
+ {
+ if (IsBlockRail(InsideType))
+ {
+ SetPosY(PosY + 1);
+ HandleRailPhysics(a_Dt, *Chunk);
+ }
+ else
+ {
+ super::HandlePhysics(a_Dt, *Chunk);
+ BroadcastMovementUpdate();
+ }
+ }
+}
+
+
+
+
+
+static const double MAX_SPEED = 8;
+static const double MAX_SPEED_NEGATIVE = (0 - MAX_SPEED);
+
+void cMinecart::HandleRailPhysics(float a_Dt, cChunk & a_Chunk)
+{
+
+ super::HandlePhysics(a_Dt, a_Chunk); // Main physics handling
+
+ /*
+ NOTE: Please bear in mind that taking away from negatives make them even more negative,
+ adding to negatives make them positive, etc.
+ */
+
+ // Get block meta below the cart
+ int RelPosX = (int)floor(GetPosX()) - a_Chunk.GetPosX() * cChunkDef::Width;
+ int RelPosZ = (int)floor(GetPosZ()) - a_Chunk.GetPosZ() * cChunkDef::Width;
+ NIBBLETYPE BelowMeta = a_Chunk.GetMeta(RelPosX, (int)floor(GetPosY() - 1), RelPosZ);
+ double SpeedX = GetSpeedX(), SpeedY = GetSpeedY(), SpeedZ = GetSpeedZ(); // Get current speed
+
+ switch (BelowMeta)
+ {
+ case E_META_RAIL_ZM_ZP: // NORTHSOUTH
+ {
+ SetRotation(270);
+ SpeedY = 0; // Don't move vertically as on ground
+ SpeedX = 0; // Correct diagonal movement from curved rails
+
+ if (SpeedZ != 0) // Don't do anything if cart is stationary
+ {
+ if (SpeedZ > 0)
+ {
+ // Going SOUTH, slow down
+ SpeedZ = SpeedZ - 0.1;
+ }
+ else
+ {
+ // Going NORTH, slow down
+ SpeedZ = SpeedZ + 0.1;
+ }
+ }
+ break;
+ }
+
+ case E_META_RAIL_XM_XP: // EASTWEST
+ {
+ SetRotation(180);
+ SpeedY = 0;
+ SpeedZ = 0;
+
+ if (SpeedX != 0)
+ {
+ if (SpeedX > 0)
+ {
+ SpeedX = SpeedX - 0.1;
+ }
+ else
+ {
+ SpeedX = SpeedX + 0.1;
+ }
+ }
+ break;
+ }
+
+ case E_META_RAIL_ASCEND_ZM: // ASCEND NORTH
+ {
+ SetRotation(270);
+ SetPosY(floor(GetPosY()) + 0.2); // It seems it doesn't work without levitation :/
+ SpeedX = 0;
+
+ if (SpeedZ >= 0)
+ {
+ // SpeedZ POSITIVE, going SOUTH
+ if (SpeedZ <= MAX_SPEED) // Speed limit
+ {
+ SpeedZ = SpeedZ + 0.5; // Speed up
+ SpeedY = (0 - SpeedZ); // Downward movement is negative (0 minus positive numbers is negative)
+ }
+ else
+ {
+ SpeedZ = MAX_SPEED; // Enforce speed limit
+ SpeedY = (0 - SpeedZ);
+ }
+ }
+ else
+ {
+ // SpeedZ NEGATIVE, going NORTH
+ SpeedZ = SpeedZ + 0.4; // Slow down
+ SpeedY = (0 - SpeedZ); // Upward movement is positive (0 minus negative number is positive number)
+ }
+ break;
+ }
+
+ case E_META_RAIL_ASCEND_ZP: // ASCEND SOUTH
+ {
+ SetRotation(270);
+ SetPosY(floor(GetPosY()) + 0.2);
+ SpeedX = 0;
+
+ if (SpeedZ > 0)
+ {
+ // SpeedZ POSITIVE, going SOUTH
+ SpeedZ = SpeedZ - 0.4; // Slow down
+ SpeedY = SpeedZ; // Upward movement positive
+ }
+ else
+ {
+ if (SpeedZ >= MAX_SPEED_NEGATIVE) // Speed limit
+ {
+ // SpeedZ NEGATIVE, going NORTH
+ SpeedZ = SpeedZ - 0.5; // Speed up
+ SpeedY = SpeedZ; // Downward movement negative
+ }
+ else
+ {
+ SpeedZ = MAX_SPEED_NEGATIVE; // Enforce speed limit
+ SpeedY = SpeedZ;
+ }
+ }
+ break;
+ }
+
+ case E_META_RAIL_ASCEND_XM: // ASCEND EAST
+ {
+ SetRotation(180);
+ SetPosY(floor(GetPosY()) + 0.2);
+ SpeedZ = 0;
+
+ if (SpeedX >= 0)
+ {
+ if (SpeedX <= MAX_SPEED)
+ {
+ SpeedX = SpeedX + 0.5;
+ SpeedY = (0 - SpeedX);
+ }
+ else
+ {
+ SpeedX = MAX_SPEED;
+ SpeedY = (0 - SpeedX);
+ }
+ }
+ else
+ {
+ SpeedX = SpeedX + 0.4;
+ SpeedY = (0 - SpeedX);
+ }
+ break;
+ }
+
+ case E_META_RAIL_ASCEND_XP: // ASCEND WEST
+ {
+ SetRotation(180);
+ SetPosY(floor(GetPosY()) + 0.2);
+ SpeedZ = 0;
+
+ if (SpeedX > 0)
+ {
+ SpeedX = SpeedX - 0.4;
+ SpeedY = SpeedX;
+ }
+ else
+ {
+ if (SpeedX >= MAX_SPEED_NEGATIVE)
+ {
+ SpeedX = SpeedX - 0.5;
+ SpeedY = SpeedX;
+ }
+ else
+ {
+ SpeedX = MAX_SPEED_NEGATIVE;
+ SpeedY = SpeedX;
+ }
+ }
+ break;
+ }
+
+ case E_META_RAIL_CURVED_ZM_XM: // Ends pointing NORTH and WEST
+ {
+ SetRotation(315); // Set correct rotation server side
+ SetPosY(floor(GetPosY()) + 0.2); // Levitate dat cart
+
+ if (SpeedZ > 0) // Cart moving south
+ {
+ SpeedX = (0 - SpeedZ); // Diagonally move southwest (which will make cart hit a southwest rail)
+ }
+ else if (SpeedX > 0) // Cart moving east
+ {
+ SpeedZ = (0 - SpeedX); // Diagonally move northeast
+ }
+ break;
+ }
+
+ case E_META_RAIL_CURVED_ZM_XP: // Curved NORTH EAST
+ {
+ SetRotation(225);
+ SetPosY(floor(GetPosY()) + 0.2);
+
+ if (SpeedZ > 0)
+ {
+ SpeedX = SpeedZ;
+ }
+ else if (SpeedX < 0)
+ {
+ SpeedZ = SpeedX;
+ }
+ break;
+ }
+
+ case E_META_RAIL_CURVED_ZP_XM: // Curved SOUTH WEST
+ {
+ SetRotation(135);
+ SetPosY(floor(GetPosY()) + 0.2);
+
+ if (SpeedZ < 0)
+ {
+ SpeedX = SpeedZ;
+ }
+ else if (SpeedX > 0)
+ {
+ SpeedZ = SpeedX;
+ }
+ break;
+ }
+
+ case E_META_RAIL_CURVED_ZP_XP: // Curved SOUTH EAST
+ {
+ SetRotation(45);
+ SetPosY(floor(GetPosY()) + 0.2);
+
+ if (SpeedZ < 0)
+ {
+ SpeedX = (0 - SpeedZ);
+ }
+ else if (SpeedX < 0)
+ {
+ SpeedZ = (0 - SpeedX);
+ }
+ break;
+ }
+
+ default:
+ {
+ ASSERT(!"Unhandled rail meta!"); // Dun dun DUN!
+ break;
+ }
+ }
+
+ // Set speed to speed variables
+ SetSpeedX(SpeedX);
+ SetSpeedY(SpeedY);
+ SetSpeedZ(SpeedZ);
+
+
+ // Broadcast position to client
+ BroadcastMovementUpdate();
+}
+
+
+
+
+
+void cMinecart::DoTakeDamage(TakeDamageInfo & TDI)
+{
+ m_LastDamage = TDI.FinalDamage;
+ super::DoTakeDamage(TDI);
+
+ m_World->BroadcastEntityMetadata(*this);
+
+ if (GetHealth() <= 0)
+ {
+ Destroy(true);
+
+ cItems Drops;
+ switch (m_Payload)
+ {
+ case mpNone:
+ {
+ Drops.push_back(cItem(E_ITEM_MINECART, 1, 0));
+ break;
+ }
+ case mpChest:
+ {
+ Drops.push_back(cItem(E_ITEM_CHEST_MINECART, 1, 0));
+ break;
+ }
+ case mpFurnace:
+ {
+ Drops.push_back(cItem(E_ITEM_FURNACE_MINECART, 1, 0));
+ break;
+ }
+ case mpTNT:
+ {
+ Drops.push_back(cItem(E_ITEM_MINECART_WITH_TNT, 1, 0));
+ break;
+ }
+ case mpHopper:
+ {
+ Drops.push_back(cItem(E_ITEM_MINECART_WITH_HOPPER, 1, 0));
+ break;
+ }
+ default:
+ {
+ ASSERT(!"Unhandled minecart type when spawning pickup!");
+ return;
+ }
+ }
+
+ m_World->SpawnItemPickups(Drops, GetPosX(), GetPosY(), GetPosZ());
+ }
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cEmptyMinecart:
+
+cEmptyMinecart::cEmptyMinecart(double a_X, double a_Y, double a_Z) :
+ super(mpNone, a_X, a_Y, a_Z)
+{
+}
+
+
+
+
+
+void cEmptyMinecart::OnRightClicked(cPlayer & a_Player)
+{
+ if (m_Attachee != NULL)
+ {
+ if (m_Attachee->GetUniqueID() == a_Player.GetUniqueID())
+ {
+ // This player is already sitting in, they want out.
+ a_Player.Detach();
+ return;
+ }
+
+ if (m_Attachee->IsPlayer())
+ {
+ // Another player is already sitting in here, cannot attach
+ return;
+ }
+
+ // Detach whatever is sitting in this minecart now:
+ m_Attachee->Detach();
+ }
+
+ // Attach the player to this minecart
+ a_Player.AttachTo(this);
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cMinecartWithChest:
+
+cMinecartWithChest::cMinecartWithChest(double a_X, double a_Y, double a_Z) :
+ super(mpChest, a_X, a_Y, a_Z)
+{
+}
+
+
+
+
+
+void cMinecartWithChest::SetSlot(int a_Idx, const cItem & a_Item)
+{
+ ASSERT((a_Idx >= 0) && (a_Idx < ARRAYCOUNT(m_Items)));
+
+ m_Items[a_Idx] = a_Item;
+}
+
+
+
+
+
+void cMinecartWithChest::OnRightClicked(cPlayer & a_Player)
+{
+ // Show the chest UI window to the player
+ // TODO
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cMinecartWithFurnace:
+
+cMinecartWithFurnace::cMinecartWithFurnace(double a_X, double a_Y, double a_Z) :
+ super(mpFurnace, a_X, a_Y, a_Z),
+ m_IsFueled(false)
+{
+}
+
+
+
+
+
+void cMinecartWithFurnace::OnRightClicked(cPlayer & a_Player)
+{
+ if (a_Player.GetEquippedItem().m_ItemType == E_ITEM_COAL)
+ {
+ if (!a_Player.IsGameModeCreative())
+ {
+ a_Player.GetInventory().RemoveOneEquippedItem();
+ }
+
+ m_IsFueled = true;
+ m_World->BroadcastEntityMetadata(*this);
+ }
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cMinecartWithTNT:
+
+cMinecartWithTNT::cMinecartWithTNT(double a_X, double a_Y, double a_Z) :
+ super(mpTNT, a_X, a_Y, a_Z)
+{
+}
+
+// TODO: Make it activate when passing over activator rail
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cMinecartWithHopper:
+
+cMinecartWithHopper::cMinecartWithHopper(double a_X, double a_Y, double a_Z) :
+ super(mpHopper, a_X, a_Y, a_Z)
+{
+}
+
+// TODO: Make it suck up blocks and travel further than any other cart and physics and put and take blocks
+// AND AVARYTHING!! \ No newline at end of file
diff --git a/src/Entities/Minecart.h b/src/Entities/Minecart.h
new file mode 100644
index 000000000..b1b48be4e
--- /dev/null
+++ b/src/Entities/Minecart.h
@@ -0,0 +1,169 @@
+
+// Minecart.h
+
+// Declares the cMinecart class representing a minecart in the world
+
+
+
+
+
+#pragma once
+
+#include "Entity.h"
+
+
+
+
+
+inline bool IsBlockRail(BLOCKTYPE a_BlockType)
+ {
+ return (
+ (a_BlockType == E_BLOCK_RAIL) ||
+ (a_BlockType == E_BLOCK_ACTIVATOR_RAIL) ||
+ (a_BlockType == E_BLOCK_DETECTOR_RAIL) ||
+ (a_BlockType == E_BLOCK_POWERED_RAIL)
+ ) ;
+ }
+
+
+
+
+
+class cMinecart :
+ public cEntity
+{
+ typedef cEntity super;
+
+public:
+ CLASS_PROTODEF(cMinecart);
+
+ enum ePayload
+ {
+ mpNone, // Empty minecart, ridable by player or mobs
+ mpChest, // Minecart-with-chest, can store a grid of 3*8 items
+ mpFurnace, // Minecart-with-furnace, can be powered
+ mpTNT, // Minecart-with-TNT, can be blown up with activator rail
+ mpHopper, // Minecart-with-hopper, can be hopper
+ // TODO: Spawner minecarts, (and possibly any block in a minecart with NBT editing)
+ } ;
+
+ // cEntity overrides:
+ virtual void SpawnOn(cClientHandle & a_ClientHandle) override;
+ virtual void HandlePhysics(float a_Dt, cChunk & a_Chunk) override;
+ virtual void DoTakeDamage(TakeDamageInfo & TDI) override;
+
+ int LastDamage(void) const { return m_LastDamage; }
+ void HandleRailPhysics(float a_Dt, cChunk & a_Chunk);
+ ePayload GetPayload(void) const { return m_Payload; }
+
+protected:
+ ePayload m_Payload;
+
+ cMinecart(ePayload a_Payload, double a_X, double a_Y, double a_Z);
+
+ int m_LastDamage;
+
+} ;
+
+
+
+
+
+class cEmptyMinecart :
+ public cMinecart
+{
+ typedef cMinecart super;
+
+public:
+ CLASS_PROTODEF(cEmptyMinecart);
+
+ cEmptyMinecart(double a_X, double a_Y, double a_Z);
+
+ // cEntity overrides:
+ virtual void OnRightClicked(cPlayer & a_Player) override;
+} ;
+
+
+
+
+
+class cMinecartWithChest :
+ public cMinecart
+{
+ typedef cMinecart super;
+
+public:
+ CLASS_PROTODEF(cMinecartWithChest);
+
+ /// Number of item slots in the chest
+ static const int NumSlots = 9 * 3;
+
+ cMinecartWithChest(double a_X, double a_Y, double a_Z);
+
+ const cItem & GetSlot(int a_Idx) const { return m_Items[a_Idx]; }
+ cItem & GetSlot(int a_Idx) { return m_Items[a_Idx]; }
+
+ void SetSlot(int a_Idx, const cItem & a_Item);
+
+protected:
+
+ /// The chest contents:
+ cItem m_Items[NumSlots];
+
+ // cEntity overrides:
+ virtual void OnRightClicked(cPlayer & a_Player) override;
+} ;
+
+
+
+
+
+class cMinecartWithFurnace :
+ public cMinecart
+{
+ typedef cMinecart super;
+
+public:
+ CLASS_PROTODEF(cMinecartWithFurnace);
+
+ cMinecartWithFurnace(double a_X, double a_Y, double a_Z);
+
+ // cEntity overrides:
+ virtual void OnRightClicked(cPlayer & a_Player) override;
+ bool IsFueled (void) const { return m_IsFueled; }
+
+private:
+
+ bool m_IsFueled;
+
+} ;
+
+
+
+
+
+class cMinecartWithTNT :
+ public cMinecart
+{
+ typedef cMinecart super;
+
+public:
+ CLASS_PROTODEF(cMinecartWithTNT);
+
+ cMinecartWithTNT(double a_X, double a_Y, double a_Z);
+} ;
+
+
+
+
+
+class cMinecartWithHopper :
+ public cMinecart
+{
+ typedef cMinecart super;
+
+public:
+ CLASS_PROTODEF(cMinecartWithHopper);
+
+ cMinecartWithHopper(double a_X, double a_Y, double a_Z);
+} ; \ No newline at end of file
diff --git a/src/Entities/Pawn.cpp b/src/Entities/Pawn.cpp
new file mode 100644
index 000000000..fffefd538
--- /dev/null
+++ b/src/Entities/Pawn.cpp
@@ -0,0 +1,19 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "Pawn.h"
+
+
+
+
+
+cPawn::cPawn(eEntityType a_EntityType, double a_Width, double a_Height)
+ : cEntity(a_EntityType, 0, 0, 0, a_Width, a_Height)
+ , m_bBurnable(true)
+{
+}
+
+
+
+
+
diff --git a/src/Entities/Pawn.h b/src/Entities/Pawn.h
new file mode 100644
index 000000000..e76337d86
--- /dev/null
+++ b/src/Entities/Pawn.h
@@ -0,0 +1,28 @@
+
+#pragma once
+
+#include "Entity.h"
+
+
+
+
+
+// tolua_begin
+class cPawn :
+ public cEntity
+{
+ // tolua_end
+ typedef cEntity super;
+
+public:
+ CLASS_PROTODEF(cPawn);
+
+ cPawn(eEntityType a_EntityType, double a_Width, double a_Height);
+
+protected:
+ bool m_bBurnable;
+} ; // tolua_export
+
+
+
+
diff --git a/src/Entities/Pickup.cpp b/src/Entities/Pickup.cpp
new file mode 100644
index 000000000..f8aae9703
--- /dev/null
+++ b/src/Entities/Pickup.cpp
@@ -0,0 +1,166 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#ifndef _WIN32
+#include <cstdlib>
+#endif
+
+#include "Pickup.h"
+#include "../ClientHandle.h"
+#include "../Inventory.h"
+#include "../World.h"
+#include "../Simulator/FluidSimulator.h"
+#include "../Server.h"
+#include "Player.h"
+#include "../PluginManager.h"
+#include "../Item.h"
+#include "../Root.h"
+#include "../Chunk.h"
+
+#include "../Vector3d.h"
+#include "../Vector3f.h"
+
+
+
+
+
+cPickup::cPickup(double a_PosX, double a_PosY, double a_PosZ, const cItem & a_Item, bool IsPlayerCreated, float a_SpeedX /* = 0.f */, float a_SpeedY /* = 0.f */, float a_SpeedZ /* = 0.f */)
+ : cEntity(etPickup, a_PosX, a_PosY, a_PosZ, 0.2, 0.2)
+ , m_Timer( 0.f )
+ , m_Item(a_Item)
+ , m_bCollected( false )
+ , m_bIsPlayerCreated( IsPlayerCreated )
+{
+ SetGravity(-10.5f);
+ SetMaxHealth(5);
+ SetHealth(5);
+ SetSpeed(a_SpeedX, a_SpeedY, a_SpeedZ);
+}
+
+
+
+
+
+void cPickup::SpawnOn(cClientHandle & a_Client)
+{
+ a_Client.SendPickupSpawn(*this);
+}
+
+
+
+
+
+void cPickup::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ super::Tick(a_Dt, a_Chunk);
+ BroadcastMovementUpdate(); //Notify clients of position
+
+ m_Timer += a_Dt;
+
+ if (!m_bCollected)
+ {
+ int BlockY = (int) floor(GetPosY());
+ if ((BlockY >= 0) && (BlockY < cChunkDef::Height)) // Don't do anything except for falling when outside the world
+ {
+ int BlockX = (int) floor(GetPosX());
+ int BlockZ = (int) floor(GetPosZ());
+ // Position might have changed due to physics. So we have to make sure we have the correct chunk.
+ cChunk * CurrentChunk = a_Chunk.GetNeighborChunk(BlockX, BlockZ);
+ if (CurrentChunk != NULL) // Make sure the chunk is loaded
+ {
+ int RelBlockX = BlockX - (CurrentChunk->GetPosX() * cChunkDef::Width);
+ int RelBlockZ = BlockZ - (CurrentChunk->GetPosZ() * cChunkDef::Width);
+
+ // If the pickup is on the bottommost block position, make it think the void is made of air: (#131)
+ BLOCKTYPE BlockBelow = (BlockY > 0) ? CurrentChunk->GetBlock(RelBlockX, BlockY - 1, RelBlockZ) : E_BLOCK_AIR;
+ BLOCKTYPE BlockIn = CurrentChunk->GetBlock(RelBlockX, BlockY, RelBlockZ);
+
+ if (
+ IsBlockLava(BlockBelow) || (BlockBelow == E_BLOCK_FIRE) ||
+ IsBlockLava(BlockIn) || (BlockIn == E_BLOCK_FIRE)
+ )
+ {
+ m_bCollected = true;
+ m_Timer = 0; // We have to reset the timer.
+ m_Timer += a_Dt; // In case we have to destroy the pickup in the same tick.
+ if (m_Timer > 500.f)
+ {
+ Destroy(true);
+ return;
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ if (m_Timer > 500.f) // 0.5 second
+ {
+ Destroy(true);
+ return;
+ }
+ }
+
+ if (m_Timer > 1000 * 60 * 5) // 5 minutes
+ {
+ Destroy(true);
+ return;
+ }
+
+ if (GetPosY() < -8) // Out of this world and no more visible!
+ {
+ Destroy(true);
+ return;
+ }
+}
+
+
+
+
+
+bool cPickup::CollectedBy(cPlayer * a_Dest)
+{
+ ASSERT(a_Dest != NULL);
+
+ if (m_bCollected)
+ {
+ // LOG("Pickup %d cannot be collected by \"%s\", because it has already been collected.", m_UniqueID, a_Dest->GetName().c_str());
+ return false; // It's already collected!
+ }
+
+ // Two seconds if player created the pickup (vomiting), half a second if anything else
+ if (m_Timer < (m_bIsPlayerCreated ? 2000.f : 500.f))
+ {
+ // LOG("Pickup %d cannot be collected by \"%s\", because it is not old enough.", m_UniqueID, a_Dest->GetName().c_str());
+ return false; // Not old enough
+ }
+
+ if (cRoot::Get()->GetPluginManager()->CallHookCollectingPickup(a_Dest, *this))
+ {
+ // LOG("Pickup %d cannot be collected by \"%s\", because a plugin has said no.", m_UniqueID, a_Dest->GetName().c_str());
+ return false;
+ }
+
+ int NumAdded = a_Dest->GetInventory().AddItem(m_Item);
+ if (NumAdded > 0)
+ {
+ m_Item.m_ItemCount -= NumAdded;
+ m_World->BroadcastCollectPickup(*this, *a_Dest);
+ // Also send the "pop" sound effect with a somewhat random pitch (fast-random using EntityID ;)
+ m_World->BroadcastSoundEffect("random.pop",(int)GetPosX() * 8, (int)GetPosY() * 8, (int)GetPosZ() * 8, 0.5, (float)(0.75 + ((float)((GetUniqueID() * 23) % 32)) / 64));
+ if (m_Item.m_ItemCount == 0)
+ {
+ // All of the pickup has been collected, schedule the pickup for destroying
+ m_bCollected = true;
+ }
+ m_Timer = 0;
+ return true;
+ }
+
+ // LOG("Pickup %d cannot be collected by \"%s\", because there's no space in the inventory.", a_Dest->GetName().c_str(), m_UniqueID);
+ return false;
+}
+
+
+
+
diff --git a/src/Entities/Pickup.h b/src/Entities/Pickup.h
new file mode 100644
index 000000000..d39eda298
--- /dev/null
+++ b/src/Entities/Pickup.h
@@ -0,0 +1,64 @@
+
+#pragma once
+
+#include "Entity.h"
+#include "../Item.h"
+
+
+
+
+
+class cPlayer;
+
+
+
+
+
+// tolua_begin
+class cPickup :
+ public cEntity
+{
+ // tolua_end
+ typedef cEntity super;
+
+public:
+ CLASS_PROTODEF(cPickup);
+
+ cPickup(double a_PosX, double a_PosY, double a_PosZ, const cItem & a_Item, bool IsPlayerCreated, float a_SpeedX = 0.f, float a_SpeedY = 0.f, float a_SpeedZ = 0.f); // tolua_export
+
+ cItem & GetItem(void) {return m_Item; } // tolua_export
+ const cItem & GetItem(void) const {return m_Item; }
+
+ virtual void SpawnOn(cClientHandle & a_ClientHandle) override;
+
+ bool CollectedBy(cPlayer * a_Dest); // tolua_export
+
+ virtual void Tick(float a_Dt, cChunk & a_Chunk) override;
+
+ /// Returns the number of ticks that this entity has existed
+ int GetAge(void) const { return (int)(m_Timer / 50); } // tolua_export
+
+ /// Returns true if the pickup has already been collected
+ bool IsCollected(void) const { return m_bCollected; } // tolua_export
+
+ /// Returns true if created by player (i.e. vomiting), used for determining picking-up delay time
+ bool IsPlayerCreated(void) const { return m_bIsPlayerCreated; } // tolua_export
+
+private:
+ Vector3d m_ResultingSpeed; //Can be used to modify the resulting speed for the current tick ;)
+
+ Vector3d m_WaterSpeed;
+
+ /// The number of ticks that the entity has existed / timer between collect and destroy; in msec
+ float m_Timer;
+
+ cItem m_Item;
+
+ bool m_bCollected;
+
+ bool m_bIsPlayerCreated;
+}; // tolua_export
+
+
+
+
diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp
new file mode 100644
index 000000000..098417dc5
--- /dev/null
+++ b/src/Entities/Player.cpp
@@ -0,0 +1,1715 @@
+
+#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
+
+#include "Player.h"
+#include "../Server.h"
+#include "../ClientHandle.h"
+#include "../UI/Window.h"
+#include "../UI/WindowOwner.h"
+#include "../World.h"
+#include "Pickup.h"
+#include "../PluginManager.h"
+#include "../BlockEntities/BlockEntity.h"
+#include "../GroupManager.h"
+#include "../Group.h"
+#include "../ChatColor.h"
+#include "../Item.h"
+#include "../Tracer.h"
+#include "../Root.h"
+#include "../OSSupport/Timer.h"
+#include "../MersenneTwister.h"
+#include "../Chunk.h"
+#include "../Items/ItemHandler.h"
+
+#include "../Vector3d.h"
+#include "../Vector3f.h"
+
+#include "../../iniFile/iniFile.h"
+#include <json/json.h>
+
+#define float2int(x) ((x)<0 ? ((int)(x))-1 : (int)(x))
+
+
+
+
+
+
+cPlayer::cPlayer(cClientHandle* a_Client, const AString & a_PlayerName)
+ : super(etPlayer, 0.6, 1.8)
+ , m_GameMode(eGameMode_NotSet)
+ , m_IP("")
+ , m_LastBlockActionTime( 0 )
+ , m_LastBlockActionCnt( 0 )
+ , m_AirLevel( MAX_AIR_LEVEL )
+ , m_AirTickTimer( DROWNING_TICKS )
+ , m_bVisible( true )
+ , m_LastGroundHeight( 0 )
+ , m_bTouchGround( false )
+ , m_Stance( 0.0 )
+ , m_Inventory(*this)
+ , m_CurrentWindow(NULL)
+ , m_InventoryWindow(NULL)
+ , m_TimeLastPickupCheck( 0.f )
+ , m_Color('-')
+ , m_ClientHandle( a_Client )
+ , m_FoodLevel(MAX_FOOD_LEVEL)
+ , m_FoodSaturationLevel(5)
+ , m_FoodTickTimer(0)
+ , m_FoodExhaustionLevel(0)
+ , m_FoodPoisonedTicksRemaining(0)
+ , m_NormalMaxSpeed(0.1)
+ , m_SprintingMaxSpeed(0.13)
+ , m_IsCrouched(false)
+ , m_IsSprinting(false)
+ , m_IsSwimming(false)
+ , m_IsSubmerged(false)
+ , m_EatingFinishTick(-1)
+ , m_IsChargingBow(false)
+ , m_BowCharge(0)
+ , m_XpTotal(0)
+{
+ LOGD("Created a player object for \"%s\" @ \"%s\" at %p, ID %d",
+ a_PlayerName.c_str(), a_Client->GetIPString().c_str(),
+ this, GetUniqueID()
+ );
+
+ m_InventoryWindow = new cInventoryWindow(*this);
+ m_CurrentWindow = m_InventoryWindow;
+ m_InventoryWindow->OpenedByPlayer(*this);
+
+ SetMaxHealth(MAX_HEALTH);
+ m_Health = MAX_HEALTH;
+
+ cTimer t1;
+ m_LastPlayerListTime = t1.GetNowTime();
+
+ m_TimeLastTeleportPacket = 0;
+ m_TimeLastPickupCheck = 0;
+
+ m_PlayerName = a_PlayerName;
+ m_bDirtyPosition = true; // So chunks are streamed to player at spawn
+
+ if (!LoadFromDisk())
+ {
+ m_Inventory.Clear();
+ SetPosX(cRoot::Get()->GetDefaultWorld()->GetSpawnX());
+ SetPosY(cRoot::Get()->GetDefaultWorld()->GetSpawnY());
+ SetPosZ(cRoot::Get()->GetDefaultWorld()->GetSpawnZ());
+
+ LOGD("Player \"%s\" is connecting for the first time, spawning at default world spawn {%.2f, %.2f, %.2f}",
+ a_PlayerName.c_str(), GetPosX(), GetPosY(), GetPosZ()
+ );
+ }
+ m_LastJumpHeight = (float)(GetPosY());
+ m_LastGroundHeight = (float)(GetPosY());
+ m_Stance = GetPosY() + 1.62;
+
+ cRoot::Get()->GetServer()->PlayerCreated(this);
+}
+
+
+
+
+
+cPlayer::~cPlayer(void)
+{
+ LOGD("Deleting cPlayer \"%s\" at %p, ID %d", m_PlayerName.c_str(), this, GetUniqueID());
+
+ // Notify the server that the player is being destroyed
+ cRoot::Get()->GetServer()->PlayerDestroying(this);
+
+ SaveToDisk();
+
+ m_World->RemovePlayer( this );
+
+ m_ClientHandle = NULL;
+
+ delete m_InventoryWindow;
+
+ LOGD("Player %p deleted", this);
+}
+
+
+
+
+
+bool cPlayer::Initialize(cWorld * a_World)
+{
+ ASSERT(a_World != NULL);
+
+ if (super::Initialize(a_World))
+ {
+ // Remove the client handle from the server, it will be ticked from this object from now on
+ if (m_ClientHandle != NULL)
+ {
+ cRoot::Get()->GetServer()->ClientMovedToWorld(m_ClientHandle);
+ }
+
+ GetWorld()->AddPlayer(this);
+ return true;
+ }
+ return false;
+}
+
+
+
+
+
+void cPlayer::Destroyed()
+{
+ CloseWindow(false);
+
+ m_ClientHandle = NULL;
+}
+
+
+
+
+
+void cPlayer::SpawnOn(cClientHandle & a_Client)
+{
+ if (!m_bVisible || (m_ClientHandle == (&a_Client)))
+ {
+ return;
+ }
+ a_Client.SendPlayerSpawn(*this);
+ a_Client.SendEntityHeadLook(*this);
+ a_Client.SendEntityEquipment(*this, 0, m_Inventory.GetEquippedItem() );
+ a_Client.SendEntityEquipment(*this, 1, m_Inventory.GetEquippedBoots() );
+ a_Client.SendEntityEquipment(*this, 2, m_Inventory.GetEquippedLeggings() );
+ a_Client.SendEntityEquipment(*this, 3, m_Inventory.GetEquippedChestplate() );
+ a_Client.SendEntityEquipment(*this, 4, m_Inventory.GetEquippedHelmet() );
+}
+
+
+
+
+
+void cPlayer::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ if (m_ClientHandle != NULL)
+ {
+ if (m_ClientHandle->IsDestroyed())
+ {
+ // This should not happen, because destroying a client will remove it from the world, but just in case
+ m_ClientHandle = NULL;
+ return;
+ }
+
+ if (!m_ClientHandle->IsPlaying())
+ {
+ // We're not yet in the game, ignore everything
+ return;
+ }
+ }
+
+ if (!a_Chunk.IsValid())
+ {
+ // This may happen if the cPlayer is created before the chunks have the chance of being loaded / generated (#83)
+ return;
+ }
+
+ super::Tick(a_Dt, a_Chunk);
+
+ // Set player swimming state
+ SetSwimState(a_Chunk);
+
+ // Handle air drowning stuff
+ HandleAir();
+
+ // Handle charging the bow:
+ if (m_IsChargingBow)
+ {
+ m_BowCharge += 1;
+ }
+
+ if (m_bDirtyPosition)
+ {
+ // Apply food exhaustion from movement:
+ ApplyFoodExhaustionFromMovement();
+
+ cRoot::Get()->GetPluginManager()->CallHookPlayerMoving(*this);
+ BroadcastMovementUpdate(m_ClientHandle);
+ m_ClientHandle->StreamChunks();
+ }
+ else
+ {
+ BroadcastMovementUpdate(m_ClientHandle);
+ }
+
+ if (m_Health > 0) // make sure player is alive
+ {
+ m_World->CollectPickupsByPlayer(this);
+
+ if ((m_EatingFinishTick >= 0) && (m_EatingFinishTick <= m_World->GetWorldAge()))
+ {
+ FinishEating();
+ }
+
+ HandleFood();
+ }
+
+ // Send Player List (Once per m_LastPlayerListTime/1000 ms)
+ cTimer t1;
+ if (m_LastPlayerListTime + cPlayer::PLAYER_LIST_TIME_MS <= t1.GetNowTime())
+ {
+ m_World->SendPlayerList(this);
+ m_LastPlayerListTime = t1.GetNowTime();
+ }
+}
+
+
+
+
+
+int cPlayer::CalcLevelFromXp(int a_XpTotal)
+{
+ //level 0 to 15
+ if(a_XpTotal <= XP_TO_LEVEL15)
+ {
+ return a_XpTotal / XP_PER_LEVEL_TO15;
+ }
+
+ //level 30+
+ if(a_XpTotal > XP_TO_LEVEL30)
+ {
+ return (int) (151.5 + sqrt( 22952.25 - (14 * (2220 - a_XpTotal)))) / 7;
+ }
+
+ //level 16 to 30
+ return (int) ( 29.5 + sqrt( 870.25 - (6 * ( 360 - a_XpTotal )))) / 3;
+}
+
+
+
+
+
+int cPlayer::XpForLevel(int a_Level)
+{
+ //level 0 to 15
+ if(a_Level <= 15)
+ {
+ return a_Level * XP_PER_LEVEL_TO15;
+ }
+
+ //level 30+
+ if(a_Level >= 31)
+ {
+ return (int) ( (3.5 * a_Level * a_Level) - (151.5 * a_Level) + 2220 );
+ }
+
+ //level 16 to 30
+ return (int) ( (1.5 * a_Level * a_Level) - (29.5 * a_Level) + 360 );
+}
+
+
+
+
+
+int cPlayer::XpGetLevel()
+{
+ return CalcLevelFromXp(m_XpTotal);
+}
+
+
+
+
+
+float cPlayer::XpGetPercentage()
+{
+ int currentLevel = CalcLevelFromXp(m_XpTotal);
+
+ return (float)m_XpTotal / (float)XpForLevel(1+currentLevel);
+}
+
+
+
+
+
+bool cPlayer::SetExperience(int a_XpTotal)
+{
+ if(!(a_XpTotal >= 0) || (a_XpTotal > (INT_MAX - m_XpTotal)))
+ {
+ LOGWARNING("Tried to update experiece with an invalid Xp value: %d", a_XpTotal);
+ return false; //oops, they gave us a dodgey number
+ }
+
+ m_XpTotal = a_XpTotal;
+
+ return true;
+}
+
+
+
+
+
+int cPlayer::AddExperience(int a_Xp_delta)
+{
+ if(a_Xp_delta < 0)
+ {
+ //value was negative, abort and report
+ LOGWARNING("Attempt was made to increment Xp by %d, must be positive",
+ a_Xp_delta);
+ return -1; //should we instead just return the current Xp?
+ }
+
+ LOGD("Player \"%s\" earnt %d experience", m_PlayerName.c_str(), a_Xp_delta);
+
+ m_XpTotal += a_Xp_delta;
+
+ return m_XpTotal;
+}
+
+
+
+
+
+void cPlayer::StartChargingBow(void)
+{
+ LOGD("Player \"%s\" started charging their bow", m_PlayerName.c_str());
+ m_IsChargingBow = true;
+ m_BowCharge = 0;
+}
+
+
+
+
+
+int cPlayer::FinishChargingBow(void)
+{
+ LOGD("Player \"%s\" finished charging their bow at a charge of %d", m_PlayerName.c_str(), m_BowCharge);
+ int res = m_BowCharge;
+ m_IsChargingBow = false;
+ m_BowCharge = 0;
+ return res;
+}
+
+
+
+
+
+void cPlayer::CancelChargingBow(void)
+{
+ LOGD("Player \"%s\" cancelled charging their bow at a charge of %d", m_PlayerName.c_str(), m_BowCharge);
+ m_IsChargingBow = false;
+ m_BowCharge = 0;
+}
+
+
+
+
+
+void cPlayer::SetTouchGround(bool a_bTouchGround)
+{
+ m_bTouchGround = a_bTouchGround;
+
+ if (!m_bTouchGround)
+ {
+ if (GetPosY() > m_LastJumpHeight)
+ {
+ m_LastJumpHeight = (float)GetPosY();
+ }
+ cWorld * World = GetWorld();
+ if ((GetPosY() >= 0) && (GetPosY() < cChunkDef::Height))
+ {
+ BLOCKTYPE BlockType = World->GetBlock(float2int(GetPosX()), float2int(GetPosY()), float2int(GetPosZ()));
+ if (BlockType != E_BLOCK_AIR)
+ {
+ m_bTouchGround = true;
+ }
+ if (
+ (BlockType == E_BLOCK_WATER) ||
+ (BlockType == E_BLOCK_STATIONARY_WATER) ||
+ (BlockType == E_BLOCK_LADDER) ||
+ (BlockType == E_BLOCK_VINES)
+ )
+ {
+ m_LastGroundHeight = (float)GetPosY();
+ }
+ }
+ }
+ else
+ {
+ float Dist = (float)(m_LastGroundHeight - floor(GetPosY()));
+ int Damage = (int)(Dist - 3.f);
+ if (m_LastJumpHeight > m_LastGroundHeight) Damage++;
+ m_LastJumpHeight = (float)GetPosY();
+
+ if ((Damage > 0) && (!IsGameModeCreative()))
+ {
+ TakeDamage(dtFalling, NULL, Damage, Damage, 0);
+ }
+
+ m_LastGroundHeight = (float)GetPosY();
+ }
+}
+
+
+
+
+
+void cPlayer::Heal(int a_Health)
+{
+ super::Heal(a_Health);
+ SendHealth();
+}
+
+
+
+
+
+void cPlayer::SetFoodLevel(int a_FoodLevel)
+{
+ m_FoodLevel = std::max(0, std::min(a_FoodLevel, (int)MAX_FOOD_LEVEL));
+ SendHealth();
+}
+
+
+
+
+
+void cPlayer::SetFoodSaturationLevel(double a_FoodSaturationLevel)
+{
+ m_FoodSaturationLevel = std::max(0.0, std::min(a_FoodSaturationLevel, (double)m_FoodLevel));
+}
+
+
+
+
+
+void cPlayer::SetFoodTickTimer(int a_FoodTickTimer)
+{
+ m_FoodTickTimer = a_FoodTickTimer;
+}
+
+
+
+
+
+void cPlayer::SetFoodExhaustionLevel(double a_FoodExhaustionLevel)
+{
+ m_FoodExhaustionLevel = std::max(0.0, std::min(a_FoodExhaustionLevel, 4.0));
+}
+
+
+
+
+
+void cPlayer::SetFoodPoisonedTicksRemaining(int a_FoodPoisonedTicksRemaining)
+{
+ m_FoodPoisonedTicksRemaining = a_FoodPoisonedTicksRemaining;
+}
+
+
+
+
+
+bool cPlayer::Feed(int a_Food, double a_Saturation)
+{
+ if (m_FoodLevel >= MAX_FOOD_LEVEL)
+ {
+ return false;
+ }
+
+ m_FoodLevel = std::min(a_Food + m_FoodLevel, (int)MAX_FOOD_LEVEL);
+ m_FoodSaturationLevel = std::min(m_FoodSaturationLevel + a_Saturation, (double)m_FoodLevel);
+
+ SendHealth();
+ return true;
+}
+
+
+
+
+
+void cPlayer::FoodPoison(int a_NumTicks)
+{
+ bool HasBeenFoodPoisoned = (m_FoodPoisonedTicksRemaining > 0);
+ m_FoodPoisonedTicksRemaining = std::max(m_FoodPoisonedTicksRemaining, a_NumTicks);
+ if (!HasBeenFoodPoisoned)
+ {
+ // TODO: Send the poisoning indication to the client - how?
+ SendHealth();
+ }
+}
+
+
+
+
+
+void cPlayer::StartEating(void)
+{
+ // Set the timer:
+ m_EatingFinishTick = m_World->GetWorldAge() + EATING_TICKS;
+
+ // Send the packets:
+ m_World->BroadcastPlayerAnimation(*this, 5);
+ m_World->BroadcastEntityMetadata(*this);
+}
+
+
+
+
+
+void cPlayer::FinishEating(void)
+{
+ // Reset the timer:
+ m_EatingFinishTick = -1;
+
+ // Send the packets:
+ m_ClientHandle->SendEntityStatus(*this, ENTITY_STATUS_EATING_ACCEPTED);
+ m_World->BroadcastPlayerAnimation(*this, 0);
+ m_World->BroadcastEntityMetadata(*this);
+
+ // consume the item:
+ cItem Item(GetEquippedItem());
+ Item.m_ItemCount = 1;
+ cItemHandler * ItemHandler = cItemHandler::GetItemHandler(Item.m_ItemType);
+ if (!ItemHandler->EatItem(this, &Item))
+ {
+ return;
+ }
+ ItemHandler->OnFoodEaten(m_World, this, &Item);
+
+ GetInventory().RemoveOneEquippedItem();
+
+ //if the food is mushroom soup, return a bowl to the inventory
+ if( Item.m_ItemType == E_ITEM_MUSHROOM_SOUP ) {
+ cItem emptyBowl(E_ITEM_BOWL, 1, 0, "");
+ GetInventory().AddItem(emptyBowl, true, true);
+ }
+}
+
+
+
+
+
+void cPlayer::AbortEating(void)
+{
+ m_EatingFinishTick = -1;
+ m_World->BroadcastPlayerAnimation(*this, 0);
+ m_World->BroadcastEntityMetadata(*this);
+}
+
+
+
+
+
+void cPlayer::SendHealth(void)
+{
+ if (m_ClientHandle != NULL)
+ {
+ m_ClientHandle->SendHealth();
+ }
+}
+
+
+
+
+
+void cPlayer::ClearInventoryPaintSlots(void)
+{
+ // Clear the list of slots that are being inventory-painted. Used by cWindow only
+ m_InventoryPaintSlots.clear();
+}
+
+
+
+
+
+void cPlayer::AddInventoryPaintSlot(int a_SlotNum)
+{
+ // Add a slot to the list for inventory painting. Used by cWindow only
+ m_InventoryPaintSlots.push_back(a_SlotNum);
+}
+
+
+
+
+
+const cSlotNums & cPlayer::GetInventoryPaintSlots(void) const
+{
+ // Return the list of slots currently stored for inventory painting. Used by cWindow only
+ return m_InventoryPaintSlots;
+}
+
+
+
+
+
+double cPlayer::GetMaxSpeed(void) const
+{
+ return m_IsSprinting ? m_SprintingMaxSpeed : m_NormalMaxSpeed;
+}
+
+
+
+
+
+void cPlayer::SetNormalMaxSpeed(double a_Speed)
+{
+ m_NormalMaxSpeed = a_Speed;
+ if (!m_IsSprinting)
+ {
+ m_ClientHandle->SendPlayerMaxSpeed();
+ }
+}
+
+
+
+
+
+void cPlayer::SetSprintingMaxSpeed(double a_Speed)
+{
+ m_SprintingMaxSpeed = a_Speed;
+ if (m_IsSprinting)
+ {
+ m_ClientHandle->SendPlayerMaxSpeed();
+ }
+}
+
+
+
+
+
+void cPlayer::SetCrouch(bool a_IsCrouched)
+{
+ // Set the crouch status, broadcast to all visible players
+
+ if (a_IsCrouched == m_IsCrouched)
+ {
+ // No change
+ return;
+ }
+ m_IsCrouched = a_IsCrouched;
+ m_World->BroadcastEntityMetadata(*this);
+}
+
+
+
+
+
+void cPlayer::SetSprint(bool a_IsSprinting)
+{
+ if (a_IsSprinting == m_IsSprinting)
+ {
+ // No change
+ return;
+ }
+
+ m_IsSprinting = a_IsSprinting;
+ m_ClientHandle->SendPlayerMaxSpeed();
+}
+
+
+
+
+
+void cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI)
+{
+ if (a_TDI.DamageType != dtInVoid)
+ {
+ if (IsGameModeCreative())
+ {
+ // No damage / health in creative mode
+ return;
+ }
+ }
+
+ super::DoTakeDamage(a_TDI);
+
+ // Any kind of damage adds food exhaustion
+ AddFoodExhaustion(0.3f);
+
+ SendHealth();
+}
+
+
+
+
+
+void cPlayer::KilledBy(cEntity * a_Killer)
+{
+ super::KilledBy(a_Killer);
+
+ if (m_Health > 0)
+ {
+ return; // not dead yet =]
+ }
+
+ m_bVisible = false; // So new clients don't see the player
+
+ // Puke out all the items
+ cItems Pickups;
+ m_Inventory.CopyToItems(Pickups);
+ m_Inventory.Clear();
+ m_World->SpawnItemPickups(Pickups, GetPosX(), GetPosY(), GetPosZ(), 10);
+ SaveToDisk(); // Save it, yeah the world is a tough place !
+}
+
+
+
+
+
+void cPlayer::Respawn(void)
+{
+ m_Health = GetMaxHealth();
+
+ // Reset food level:
+ m_FoodLevel = MAX_FOOD_LEVEL;
+ m_FoodSaturationLevel = 5;
+
+ m_ClientHandle->SendRespawn();
+
+ // Extinguish the fire:
+ StopBurning();
+
+ TeleportToCoords(GetWorld()->GetSpawnX(), GetWorld()->GetSpawnY(), GetWorld()->GetSpawnZ());
+
+ SetVisible(true);
+}
+
+
+
+
+
+double cPlayer::GetEyeHeight(void) const
+{
+ return m_Stance;
+}
+
+
+
+
+Vector3d cPlayer::GetEyePosition(void) const
+{
+ return Vector3d( GetPosX(), m_Stance, GetPosZ() );
+}
+
+
+
+
+
+bool cPlayer::IsGameModeCreative(void) const
+{
+ return (m_GameMode == gmCreative) || // Either the player is explicitly in Creative
+ ((m_GameMode == gmNotSet) && m_World->IsGameModeCreative()); // or they inherit from the world and the world is Creative
+}
+
+
+
+
+
+bool cPlayer::IsGameModeSurvival(void) const
+{
+ return (m_GameMode == gmSurvival) || // Either the player is explicitly in Survival
+ ((m_GameMode == gmNotSet) && m_World->IsGameModeSurvival()); // or they inherit from the world and the world is Survival
+}
+
+
+
+
+
+bool cPlayer::IsGameModeAdventure(void) const
+{
+ return (m_GameMode == gmCreative) || // Either the player is explicitly in Adventure
+ ((m_GameMode == gmNotSet) && m_World->IsGameModeCreative()); // or they inherit from the world and the world is Adventure
+}
+
+
+
+
+
+void cPlayer::OpenWindow(cWindow * a_Window)
+{
+ if (a_Window != m_CurrentWindow)
+ {
+ CloseWindow(false);
+ }
+ a_Window->OpenedByPlayer(*this);
+ m_CurrentWindow = a_Window;
+ a_Window->SendWholeWindow(*GetClientHandle());
+}
+
+
+
+
+
+void cPlayer::CloseWindow(bool a_CanRefuse)
+{
+ if (m_CurrentWindow == NULL)
+ {
+ m_CurrentWindow = m_InventoryWindow;
+ return;
+ }
+
+ if (m_CurrentWindow->ClosedByPlayer(*this, a_CanRefuse) || !a_CanRefuse)
+ {
+ // Close accepted, go back to inventory window (the default):
+ m_CurrentWindow = m_InventoryWindow;
+ }
+ else
+ {
+ // Re-open the window
+ m_CurrentWindow->OpenedByPlayer(*this);
+ m_CurrentWindow->SendWholeWindow(*GetClientHandle());
+ }
+}
+
+
+
+
+
+void cPlayer::CloseWindowIfID(char a_WindowID, bool a_CanRefuse)
+{
+ if ((m_CurrentWindow == NULL) || (m_CurrentWindow->GetWindowID() != a_WindowID))
+ {
+ return;
+ }
+ CloseWindow();
+}
+
+
+
+
+
+void cPlayer::SetLastBlockActionTime()
+{
+ if (m_World != NULL)
+ {
+ m_LastBlockActionTime = m_World->GetWorldAge() / 20.0f;
+ }
+}
+
+
+
+
+
+void cPlayer::SetLastBlockActionCnt( int a_LastBlockActionCnt )
+{
+ m_LastBlockActionCnt = a_LastBlockActionCnt;
+}
+
+
+
+
+
+void cPlayer::SetGameMode(eGameMode a_GameMode)
+{
+ if ((a_GameMode < gmMin) || (a_GameMode >= gmMax))
+ {
+ LOGWARNING("%s: Setting invalid gamemode: %d", GetName().c_str(), a_GameMode);
+ return;
+ }
+
+ if (m_GameMode == a_GameMode)
+ {
+ // Gamemode already set
+ return;
+ }
+
+ m_GameMode = a_GameMode;
+ m_ClientHandle->SendGameMode(a_GameMode);
+}
+
+
+
+
+
+void cPlayer::LoginSetGameMode( eGameMode a_GameMode )
+{
+ m_GameMode = a_GameMode;
+}
+
+
+
+
+
+void cPlayer::SetIP(const AString & a_IP)
+{
+ m_IP = a_IP;
+}
+
+
+
+
+
+void cPlayer::SendMessage(const AString & a_Message)
+{
+ m_ClientHandle->SendChat(a_Message);
+}
+
+
+
+
+
+void cPlayer::TeleportToCoords(double a_PosX, double a_PosY, double a_PosZ)
+{
+ SetPosition( a_PosX, a_PosY, a_PosZ );
+ m_LastGroundHeight = (float)a_PosY;
+
+ m_World->BroadcastTeleportEntity(*this, GetClientHandle());
+ m_ClientHandle->SendPlayerMoveLook();
+}
+
+
+
+
+
+Vector3d cPlayer::GetThrowStartPos(void) const
+{
+ Vector3d res = GetEyePosition();
+
+ // Adjust the position to be just outside the player's bounding box:
+ res.x += 0.16 * cos(GetPitch());
+ res.y += -0.1;
+ res.z += 0.16 * sin(GetPitch());
+
+ return res;
+}
+
+
+
+
+
+Vector3d cPlayer::GetThrowSpeed(double a_SpeedCoeff) const
+{
+ Vector3d res = GetLookVector();
+ res.Normalize();
+
+ // TODO: Add a slight random change (+-0.0075 in each direction)
+
+ return res * a_SpeedCoeff;
+}
+
+
+
+
+
+void cPlayer::MoveTo( const Vector3d & a_NewPos )
+{
+ if ((a_NewPos.y < -990) && (GetPosY() > -100))
+ {
+ // When attached to an entity, the client sends position packets with weird coords:
+ // Y = -999 and X, Z = attempting to create speed, usually up to 0.03
+ // We cannot test m_AttachedTo, because when deattaching, the server thinks the client is already deattached while
+ // the client may still send more of these nonsensical packets.
+ if (m_AttachedTo != NULL)
+ {
+ Vector3d AddSpeed(a_NewPos);
+ AddSpeed.y = 0;
+ m_AttachedTo->AddSpeed(AddSpeed);
+ }
+ return;
+ }
+
+ // TODO: should do some checks to see if player is not moving through terrain
+ // TODO: Official server refuses position packets too far away from each other, kicking "hacked" clients; we should, too
+
+ SetPosition( a_NewPos );
+ SetStance(a_NewPos.y + 1.62);
+}
+
+
+
+
+
+void cPlayer::SetVisible(bool a_bVisible)
+{
+ if (a_bVisible && !m_bVisible) // Make visible
+ {
+ m_bVisible = true;
+ m_World->BroadcastSpawnEntity(*this);
+ }
+ if (!a_bVisible && m_bVisible)
+ {
+ m_bVisible = false;
+ m_World->BroadcastDestroyEntity(*this, m_ClientHandle); // Destroy on all clients
+ }
+}
+
+
+
+
+
+void cPlayer::AddToGroup( const AString & a_GroupName )
+{
+ cGroup* Group = cRoot::Get()->GetGroupManager()->GetGroup( a_GroupName );
+ m_Groups.push_back( Group );
+ LOGD("Added %s to group %s", m_PlayerName.c_str(), a_GroupName.c_str() );
+ ResolveGroups();
+ ResolvePermissions();
+}
+
+
+
+
+
+void cPlayer::RemoveFromGroup( const AString & a_GroupName )
+{
+ bool bRemoved = false;
+ for( GroupList::iterator itr = m_Groups.begin(); itr != m_Groups.end(); ++itr )
+ {
+ if( (*itr)->GetName().compare(a_GroupName ) == 0 )
+ {
+ m_Groups.erase( itr );
+ bRemoved = true;
+ break;
+ }
+ }
+
+ if( bRemoved )
+ {
+ LOGD("Removed %s from group %s", m_PlayerName.c_str(), a_GroupName.c_str() );
+ ResolveGroups();
+ ResolvePermissions();
+ }
+ else
+ {
+ LOGWARN("Tried to remove %s from group %s but was not in that group", m_PlayerName.c_str(), a_GroupName.c_str() );
+ }
+}
+
+
+
+
+
+bool cPlayer::CanUseCommand( const AString & a_Command )
+{
+ for( GroupList::iterator itr = m_Groups.begin(); itr != m_Groups.end(); ++itr )
+ {
+ if( (*itr)->HasCommand( a_Command ) ) return true;
+ }
+ return false;
+}
+
+
+
+
+
+bool cPlayer::HasPermission(const AString & a_Permission)
+{
+ if (a_Permission.empty())
+ {
+ // Empty permission request is always granted
+ return true;
+ }
+
+ AStringVector Split = StringSplit( a_Permission, "." );
+ PermissionMap Possibilities = m_ResolvedPermissions;
+ // Now search the namespaces
+ while( Possibilities.begin() != Possibilities.end() )
+ {
+ PermissionMap::iterator itr = Possibilities.begin();
+ if( itr->second )
+ {
+ AStringVector OtherSplit = StringSplit( itr->first, "." );
+ if( OtherSplit.size() <= Split.size() )
+ {
+ unsigned int i;
+ for( i = 0; i < OtherSplit.size(); ++i )
+ {
+ if( OtherSplit[i].compare( Split[i] ) != 0 )
+ {
+ if( OtherSplit[i].compare("*") == 0 ) return true; // WildCard man!! WildCard!
+ break;
+ }
+ }
+ if( i == Split.size() ) return true;
+ }
+ }
+ Possibilities.erase( itr );
+ }
+
+ // Nothing that matched :(
+ return false;
+}
+
+
+
+
+
+bool cPlayer::IsInGroup( const AString & a_Group )
+{
+ for( GroupList::iterator itr = m_ResolvedGroups.begin(); itr != m_ResolvedGroups.end(); ++itr )
+ {
+ if( a_Group.compare( (*itr)->GetName().c_str() ) == 0 )
+ return true;
+ }
+ return false;
+}
+
+
+
+
+
+void cPlayer::ResolvePermissions()
+{
+ m_ResolvedPermissions.clear(); // Start with an empty map yo~
+
+ // Copy all player specific permissions into the resolved permissions map
+ for( PermissionMap::iterator itr = m_Permissions.begin(); itr != m_Permissions.end(); ++itr )
+ {
+ m_ResolvedPermissions[ itr->first ] = itr->second;
+ }
+
+ for( GroupList::iterator GroupItr = m_ResolvedGroups.begin(); GroupItr != m_ResolvedGroups.end(); ++GroupItr )
+ {
+ const cGroup::PermissionMap & Permissions = (*GroupItr)->GetPermissions();
+ for( cGroup::PermissionMap::const_iterator itr = Permissions.begin(); itr != Permissions.end(); ++itr )
+ {
+ m_ResolvedPermissions[ itr->first ] = itr->second;
+ }
+ }
+}
+
+
+
+
+
+void cPlayer::ResolveGroups()
+{
+ // Clear resolved groups first
+ m_ResolvedGroups.clear();
+
+ // Get a complete resolved list of all groups the player is in
+ std::map< cGroup*, bool > AllGroups; // Use a map, because it's faster than iterating through a list to find duplicates
+ GroupList ToIterate;
+ for( GroupList::iterator GroupItr = m_Groups.begin(); GroupItr != m_Groups.end(); ++GroupItr )
+ {
+ ToIterate.push_back( *GroupItr );
+ }
+ while( ToIterate.begin() != ToIterate.end() )
+ {
+ cGroup* CurrentGroup = *ToIterate.begin();
+ if( AllGroups.find( CurrentGroup ) != AllGroups.end() )
+ {
+ LOGWARNING("ERROR: Player \"%s\" is in the group multiple times (\"%s\"). Please fix your settings in users.ini!",
+ m_PlayerName.c_str(), CurrentGroup->GetName().c_str()
+ );
+ }
+ else
+ {
+ AllGroups[ CurrentGroup ] = true;
+ m_ResolvedGroups.push_back( CurrentGroup ); // Add group to resolved list
+ const cGroup::GroupList & Inherits = CurrentGroup->GetInherits();
+ for( cGroup::GroupList::const_iterator itr = Inherits.begin(); itr != Inherits.end(); ++itr )
+ {
+ if( AllGroups.find( *itr ) != AllGroups.end() )
+ {
+ LOGERROR("ERROR: Player %s is in the same group multiple times due to inheritance (%s). FIX IT!", m_PlayerName.c_str(), (*itr)->GetName().c_str() );
+ continue;
+ }
+ ToIterate.push_back( *itr );
+ }
+ }
+ ToIterate.erase( ToIterate.begin() );
+ }
+}
+
+
+
+
+
+AString cPlayer::GetColor(void) const
+{
+ if ( m_Color != '-' )
+ {
+ return cChatColor::MakeColor( m_Color );
+ }
+
+ if ( m_Groups.size() < 1 )
+ {
+ return cChatColor::White;
+ }
+
+ return (*m_Groups.begin())->GetColor();
+}
+
+
+
+
+
+void cPlayer::TossItem(
+ bool a_bDraggingItem,
+ char a_Amount /* = 1 */,
+ short a_CreateType /* = 0 */,
+ short a_CreateHealth /* = 0 */
+)
+{
+ cItems Drops;
+ if (a_CreateType != 0)
+ {
+ // Just create item without touching the inventory (used in creative mode)
+ Drops.push_back(cItem(a_CreateType, a_Amount, a_CreateHealth));
+ }
+ else
+ {
+ // Drop an item from the inventory:
+ if (a_bDraggingItem)
+ {
+ cItem & Item = GetDraggingItem();
+ if (!Item.IsEmpty())
+ {
+ char OriginalItemAmount = Item.m_ItemCount;
+ Item.m_ItemCount = std::min(OriginalItemAmount, a_Amount);
+ Drops.push_back(Item);
+ if (OriginalItemAmount > a_Amount)
+ {
+ Item.m_ItemCount = OriginalItemAmount - (char)a_Amount;
+ }
+ else
+ {
+ Item.Empty();
+ }
+ }
+ }
+ else
+ {
+ // Else drop equipped item
+ cItem DroppedItem(GetInventory().GetEquippedItem());
+ if (!DroppedItem.IsEmpty())
+ {
+ if (GetInventory().RemoveOneEquippedItem())
+ {
+ DroppedItem.m_ItemCount = 1; // RemoveItem decreases the count, so set it to 1 again
+ Drops.push_back(DroppedItem);
+ }
+ }
+ }
+ }
+ double vX = 0, vY = 0, vZ = 0;
+ EulerToVector(-GetRotation(), GetPitch(), vZ, vX, vY);
+ vY = -vY * 2 + 1.f;
+ m_World->SpawnItemPickups(Drops, GetPosX(), GetPosY() + 1.6f, GetPosZ(), vX * 3, vY * 3, vZ * 3, true); // 'true' because created by player
+}
+
+
+
+
+
+bool cPlayer::MoveToWorld(const char * a_WorldName)
+{
+ cWorld * World = cRoot::Get()->GetWorld(a_WorldName);
+ if (World == NULL)
+ {
+ LOG("%s: Couldn't find world \"%s\".", __FUNCTION__, a_WorldName);
+ return false;
+ }
+
+ eDimension OldDimension = m_World->GetDimension();
+
+ // Remove all links to the old world
+ m_World->RemovePlayer(this);
+ m_ClientHandle->RemoveFromAllChunks();
+ m_World->RemoveEntity(this);
+
+ // If the dimension is different, we can send the respawn packet
+ // http://wiki.vg/Protocol#0x09 says "don't send if dimension is the same" as of 2013_07_02
+ m_ClientHandle->MoveToWorld(*World, (OldDimension != World->GetDimension()));
+
+ // Add player to all the necessary parts of the new world
+ SetWorld(World);
+ World->AddEntity(this);
+ World->AddPlayer(this);
+
+ return true;
+}
+
+
+
+
+
+void cPlayer::LoadPermissionsFromDisk()
+{
+ m_Groups.clear();
+ m_Permissions.clear();
+
+ cIniFile IniFile;
+ if (IniFile.ReadFile("users.ini"))
+ {
+ std::string Groups = IniFile.GetValue(m_PlayerName, "Groups", "");
+ if (!Groups.empty())
+ {
+ AStringVector Split = StringSplit( Groups, "," );
+ for( unsigned int i = 0; i < Split.size(); i++ )
+ {
+ AddToGroup( Split[i].c_str() );
+ }
+ }
+ else
+ {
+ AddToGroup("Default");
+ }
+
+ m_Color = IniFile.GetValue(m_PlayerName, "Color", "-")[0];
+ }
+ else
+ {
+ LOGWARN("Failed to read the users.ini file. The player will be added only to the Default group.");
+ AddToGroup("Default");
+ }
+ ResolvePermissions();
+}
+
+
+
+
+bool cPlayer::LoadFromDisk()
+{
+ LoadPermissionsFromDisk();
+
+ // Log player permissions, cause it's what the cool kids do
+ LOGINFO("Player %s has permissions:", m_PlayerName.c_str() );
+ for( PermissionMap::iterator itr = m_ResolvedPermissions.begin(); itr != m_ResolvedPermissions.end(); ++itr )
+ {
+ if( itr->second ) LOGINFO("%s", itr->first.c_str() );
+ }
+
+ AString SourceFile;
+ Printf(SourceFile, "players/%s.json", m_PlayerName.c_str() );
+
+ cFile f;
+ if (!f.Open(SourceFile, cFile::fmRead))
+ {
+ // This is a new player whom we haven't seen yet, bail out, let them have the defaults
+ return false;
+ }
+
+ AString buffer;
+ if (f.ReadRestOfFile(buffer) != f.GetSize())
+ {
+ LOGWARNING("Cannot read player data from file \"%s\"", SourceFile.c_str());
+ return false;
+ }
+ f.Close(); //cool kids play nice
+
+ Json::Value root;
+ Json::Reader reader;
+ if (!reader.parse(buffer, root, false))
+ {
+ LOGWARNING("Cannot parse player data in file \"%s\", player will be reset", SourceFile.c_str());
+ }
+
+ Json::Value & JSON_PlayerPosition = root["position"];
+ if (JSON_PlayerPosition.size() == 3)
+ {
+ SetPosX(JSON_PlayerPosition[(unsigned int)0].asDouble());
+ SetPosY(JSON_PlayerPosition[(unsigned int)1].asDouble());
+ SetPosZ(JSON_PlayerPosition[(unsigned int)2].asDouble());
+ m_LastPosX = GetPosX();
+ m_LastPosY = GetPosY();
+ m_LastPosZ = GetPosZ();
+ m_LastFoodPos = GetPosition();
+ }
+
+ Json::Value & JSON_PlayerRotation = root["rotation"];
+ if (JSON_PlayerRotation.size() == 3)
+ {
+ SetRotation ((float)JSON_PlayerRotation[(unsigned int)0].asDouble());
+ SetPitch ((float)JSON_PlayerRotation[(unsigned int)1].asDouble());
+ SetRoll ((float)JSON_PlayerRotation[(unsigned int)2].asDouble());
+ }
+
+ m_Health = root.get("health", 0).asInt();
+ m_AirLevel = root.get("air", MAX_AIR_LEVEL).asInt();
+ m_FoodLevel = root.get("food", MAX_FOOD_LEVEL).asInt();
+ m_FoodSaturationLevel = root.get("foodSaturation", MAX_FOOD_LEVEL).asDouble();
+ m_FoodTickTimer = root.get("foodTickTimer", 0).asInt();
+ m_FoodExhaustionLevel = root.get("foodExhaustion", 0).asDouble();
+
+ SetExperience(root.get("experience", 0).asInt());
+
+ m_GameMode = (eGameMode) root.get("gamemode", eGameMode_NotSet).asInt();
+
+ m_Inventory.LoadFromJson(root["inventory"]);
+
+ m_LoadedWorldName = root.get("world", "world").asString();
+
+ LOGD("Player \"%s\" was read from file, spawning at {%.2f, %.2f, %.2f} in world \"%s\"",
+ m_PlayerName.c_str(), GetPosX(), GetPosY(), GetPosZ(), m_LoadedWorldName.c_str()
+ );
+
+ return true;
+}
+
+
+
+
+
+bool cPlayer::SaveToDisk()
+{
+ cFile::CreateFolder(FILE_IO_PREFIX + AString("players"));
+
+ // create the JSON data
+ Json::Value JSON_PlayerPosition;
+ JSON_PlayerPosition.append(Json::Value(GetPosX()));
+ JSON_PlayerPosition.append(Json::Value(GetPosY()));
+ JSON_PlayerPosition.append(Json::Value(GetPosZ()));
+
+ Json::Value JSON_PlayerRotation;
+ JSON_PlayerRotation.append(Json::Value(GetRotation()));
+ JSON_PlayerRotation.append(Json::Value(GetPitch()));
+ JSON_PlayerRotation.append(Json::Value(GetRoll()));
+
+ Json::Value JSON_Inventory;
+ m_Inventory.SaveToJson(JSON_Inventory);
+
+ Json::Value root;
+ root["position"] = JSON_PlayerPosition;
+ root["rotation"] = JSON_PlayerRotation;
+ root["inventory"] = JSON_Inventory;
+ root["health"] = m_Health;
+ root["experience"] = m_XpTotal;
+ root["air"] = m_AirLevel;
+ root["food"] = m_FoodLevel;
+ root["foodSaturation"] = m_FoodSaturationLevel;
+ root["foodTickTimer"] = m_FoodTickTimer;
+ root["foodExhaustion"] = m_FoodExhaustionLevel;
+ root["world"] = GetWorld()->GetName();
+
+ if (m_GameMode == GetWorld()->GetGameMode())
+ {
+ root["gamemode"] = (int) eGameMode_NotSet;
+ }
+ else
+ {
+ root["gamemode"] = (int) m_GameMode;
+ }
+
+ Json::StyledWriter writer;
+ std::string JsonData = writer.write(root);
+
+ AString SourceFile;
+ Printf(SourceFile, "players/%s.json", m_PlayerName.c_str() );
+
+ cFile f;
+ if (!f.Open(SourceFile, cFile::fmWrite))
+ {
+ LOGERROR("ERROR WRITING PLAYER \"%s\" TO FILE \"%s\" - cannot open file", m_PlayerName.c_str(), SourceFile.c_str());
+ return false;
+ }
+ if (f.Write(JsonData.c_str(), JsonData.size()) != (int)JsonData.size())
+ {
+ LOGERROR("ERROR WRITING PLAYER JSON TO FILE \"%s\"", SourceFile.c_str());
+ return false;
+ }
+ return true;
+}
+
+
+
+
+
+cPlayer::StringList cPlayer::GetResolvedPermissions()
+{
+ StringList Permissions;
+
+ const PermissionMap& ResolvedPermissions = m_ResolvedPermissions;
+ for( PermissionMap::const_iterator itr = ResolvedPermissions.begin(); itr != ResolvedPermissions.end(); ++itr )
+ {
+ if( itr->second ) Permissions.push_back( itr->first );
+ }
+
+ return Permissions;
+}
+
+
+
+
+
+void cPlayer::UseEquippedItem(void)
+{
+ if (IsGameModeCreative()) // No damage in creative
+ {
+ return;
+ }
+
+ GetInventory().DamageEquippedItem();
+}
+
+
+
+
+
+void cPlayer::SetSwimState(cChunk & a_Chunk)
+{
+ int RelY = (int)floor(m_LastPosY + 0.1);
+ if ((RelY < 0) || (RelY >= cChunkDef::Height - 1))
+ {
+ m_IsSwimming = false;
+ m_IsSubmerged = false;
+ return;
+ }
+
+ BLOCKTYPE BlockIn;
+ int RelX = (int)floor(m_LastPosX) - a_Chunk.GetPosX() * cChunkDef::Width;
+ int RelZ = (int)floor(m_LastPosZ) - a_Chunk.GetPosZ() * cChunkDef::Width;
+
+ // Check if the player is swimming:
+ // Use Unbounded, because we're being called *after* processing super::Tick(), which could have changed our chunk
+ if (!a_Chunk.UnboundedRelGetBlockType(RelX, RelY, RelZ, BlockIn))
+ {
+ // This sometimes happens on Linux machines
+ // Ref.: http://forum.mc-server.org/showthread.php?tid=1244
+ LOGD("SetSwimState failure: RelX = %d, RelZ = %d, LastPos = {%.02f, %.02f}, Pos = %.02f, %.02f}",
+ RelX, RelY, m_LastPosX, m_LastPosZ, GetPosX(), GetPosZ()
+ );
+ m_IsSwimming = false;
+ m_IsSubmerged = false;
+ return;
+ }
+ m_IsSwimming = IsBlockWater(BlockIn);
+
+ // Check if the player is submerged:
+ VERIFY(a_Chunk.UnboundedRelGetBlockType(RelX, RelY + 1, RelZ, BlockIn));
+ m_IsSubmerged = IsBlockWater(BlockIn);
+}
+
+
+
+
+
+void cPlayer::HandleAir(void)
+{
+ // Ref.: http://www.minecraftwiki.net/wiki/Chunk_format
+ // see if the player is /submerged/ water (block above is water)
+ // Get the type of block the player's standing in:
+
+ if (IsSubmerged())
+ {
+ // either reduce air level or damage player
+ if (m_AirLevel < 1)
+ {
+ if (m_AirTickTimer < 1)
+ {
+ // damage player
+ TakeDamage(dtDrowning, NULL, 1, 1, 0);
+ // reset timer
+ m_AirTickTimer = DROWNING_TICKS;
+ }
+ else
+ {
+ m_AirTickTimer -= 1;
+ }
+ }
+ else
+ {
+ // reduce air supply
+ m_AirLevel -= 1;
+ }
+ }
+ else
+ {
+ // set the air back to maximum
+ m_AirLevel = MAX_AIR_LEVEL;
+ m_AirTickTimer = DROWNING_TICKS;
+ }
+}
+
+
+
+
+
+void cPlayer::HandleFood(void)
+{
+ // Ref.: http://www.minecraftwiki.net/wiki/Hunger
+
+ // Remember the food level before processing, for later comparison
+ int LastFoodLevel = m_FoodLevel;
+
+ // Heal or damage, based on the food level, using the m_FoodTickTimer:
+ if ((m_FoodLevel > 17) || (m_FoodLevel <= 0))
+ {
+ m_FoodTickTimer++;
+ if (m_FoodTickTimer >= 80)
+ {
+ m_FoodTickTimer = 0;
+
+ if (m_FoodLevel >= 17)
+ {
+ // Regenerate health from food, incur 3 pts of food exhaustion:
+ Heal(1);
+ m_FoodExhaustionLevel += 3;
+ }
+ else if (m_FoodLevel <= 0)
+ {
+ // Damage from starving
+ TakeDamage(dtStarving, NULL, 1, 1, 0);
+ }
+ }
+ }
+
+ // Apply food poisoning food exhaustion:
+ if (m_FoodPoisonedTicksRemaining > 0)
+ {
+ m_FoodPoisonedTicksRemaining--;
+ m_FoodExhaustionLevel += 0.025; // 0.5 per second = 0.025 per tick
+ }
+
+ // Apply food exhaustion that has accumulated:
+ if (m_FoodExhaustionLevel >= 4)
+ {
+ m_FoodExhaustionLevel -= 4;
+
+ if (m_FoodSaturationLevel >= 1)
+ {
+ m_FoodSaturationLevel -= 1;
+ }
+ else
+ {
+ m_FoodLevel = std::max(m_FoodLevel - 1, 0);
+ }
+ }
+
+ if (m_FoodLevel != LastFoodLevel)
+ {
+ SendHealth();
+ }
+}
+
+
+
+
+
+void cPlayer::ApplyFoodExhaustionFromMovement()
+{
+ if (IsGameModeCreative())
+ {
+ return;
+ }
+
+ // Calculate the distance travelled, update the last pos:
+ Vector3d Movement(GetPosition() - m_LastFoodPos);
+ Movement.y = 0; // Only take XZ movement into account
+ m_LastFoodPos = GetPosition();
+
+ // If riding anything, apply no food exhaustion
+ if (m_AttachedTo != NULL)
+ {
+ return;
+ }
+
+ // Apply the exhaustion based on distance travelled:
+ double BaseExhaustion = Movement.Length();
+ if (IsSprinting())
+ {
+ // 0.1 pt per meter sprinted
+ BaseExhaustion = BaseExhaustion * 0.1;
+ }
+ else if (IsSwimming())
+ {
+ // 0.015 pt per meter swum
+ BaseExhaustion = BaseExhaustion * 0.015;
+ }
+ else
+ {
+ // 0.01 pt per meter walked / sneaked
+ BaseExhaustion = BaseExhaustion * 0.01;
+ }
+ m_FoodExhaustionLevel += BaseExhaustion;
+}
+
+
+
+
diff --git a/src/Entities/Player.h b/src/Entities/Player.h
new file mode 100644
index 000000000..ab2f94d4c
--- /dev/null
+++ b/src/Entities/Player.h
@@ -0,0 +1,447 @@
+
+#pragma once
+
+#include "Pawn.h"
+#include "../Inventory.h"
+#include "../Defines.h"
+#include "../World.h"
+
+
+
+
+
+class cGroup;
+class cWindow;
+class cClientHandle;
+
+
+
+
+
+// tolua_begin
+class cPlayer :
+ public cPawn
+{
+ typedef cPawn super;
+
+public:
+ enum
+ {
+ MAX_HEALTH = 20,
+ MAX_FOOD_LEVEL = 20,
+ EATING_TICKS = 30, ///< Number of ticks it takes to eat an item
+ MAX_AIR_LEVEL = 300,
+ DROWNING_TICKS = 10, //number of ticks per heart of damage
+ } ;
+ // tolua_end
+
+ CLASS_PROTODEF(cPlayer)
+
+
+ cPlayer(cClientHandle * a_Client, const AString & a_PlayerName);
+ virtual ~cPlayer();
+
+ virtual bool Initialize(cWorld * a_World) override;
+
+ virtual void SpawnOn(cClientHandle & a_Client) override;
+
+ virtual void Tick(float a_Dt, cChunk & a_Chunk) override;
+
+ virtual void HandlePhysics(float a_Dt, cChunk & a_Chunk) override { };
+
+ /// Returns the curently equipped weapon; empty item if none
+ virtual cItem GetEquippedWeapon(void) const override { return m_Inventory.GetEquippedItem(); }
+
+ /// Returns the currently equipped helmet; empty item if nonte
+ virtual cItem GetEquippedHelmet(void) const override { return m_Inventory.GetEquippedHelmet(); }
+
+ /// Returns the currently equipped chestplate; empty item if none
+ virtual cItem GetEquippedChestplate(void) const override { return m_Inventory.GetEquippedChestplate(); }
+
+ /// Returns the currently equipped leggings; empty item if none
+ virtual cItem GetEquippedLeggings(void) const override { return m_Inventory.GetEquippedLeggings(); }
+
+ /// Returns the currently equipped boots; empty item if none
+ virtual cItem GetEquippedBoots(void) const override { return m_Inventory.GetEquippedBoots(); }
+
+
+ // tolua_begin
+
+ /** Sets the experience total
+ Returns true on success
+ "should" really only be called at init or player death, plugins excepted
+ */
+ bool SetExperience(int a_XpTotal);
+
+ /* Adds Xp, "should" not inc more than MAX_EXPERIENCE_ORB_SIZE unless you're a plugin being funny, *cough* cheating
+ Returns the new total experience, -1 on error
+ */
+ int AddExperience(int a_Xp_delta);
+
+ /// Gets the experience total - XpTotal
+ inline int XpGetTotal(void) { return m_XpTotal; }
+
+ /// Gets the current level - XpLevel
+ int XpGetLevel(void);
+
+ /// Gets the experience bar percentage - XpP
+ float XpGetPercentage(void);
+
+ // tolua_end
+
+ /// Starts charging the equipped bow
+ void StartChargingBow(void);
+
+ /// Finishes charging the current bow. Returns the number of ticks for which the bow has been charged
+ int FinishChargingBow(void);
+
+ /// Cancels the current bow charging
+ void CancelChargingBow(void);
+
+ /// Returns true if the player is currently charging the bow
+ bool IsChargingBow(void) const { return m_IsChargingBow; }
+
+ void SetTouchGround( bool a_bTouchGround );
+ inline void SetStance( const double a_Stance ) { m_Stance = a_Stance; }
+ double GetEyeHeight(void) const; // tolua_export
+ Vector3d GetEyePosition(void) const; // tolua_export
+ inline bool IsOnGround(void) const {return m_bTouchGround; } // tolua_export
+ inline const double GetStance(void) const { return GetPosY() + 1.62; } // tolua_export // TODO: Proper stance when crouching etc.
+ inline cInventory & GetInventory(void) { return m_Inventory; } // tolua_export
+ inline const cInventory & GetInventory(void) const { return m_Inventory; }
+
+ inline const cItem & GetEquippedItem(void) const { return GetInventory().GetEquippedItem(); } // tolua_export
+
+ virtual void TeleportToCoords(double a_PosX, double a_PosY, double a_PosZ) override;
+
+ // tolua_begin
+
+ /// Returns the position where projectiles thrown by this player should start, player eye position + adjustment
+ Vector3d GetThrowStartPos(void) const;
+
+ /// Returns the initial speed vector of a throw, with a 3D length of a_SpeedCoeff.
+ Vector3d GetThrowSpeed(double a_SpeedCoeff) const;
+
+ /// Returns the current gamemode. Partly OBSOLETE, you should use IsGameModeXXX() functions wherever applicable
+ eGameMode GetGameMode(void) const { return m_GameMode; }
+
+ /// Returns the current effective gamemode (inherited gamemode is resolved before returning)
+ eGameMode GetEffectiveGameMode(void) const { return (m_GameMode == gmNotSet) ? m_World->GetGameMode() : m_GameMode; }
+
+ /** Sets the gamemode for the player.
+ The gamemode may be gmNotSet, in that case the player inherits the world's gamemode.
+ Updates the gamemode on the client (sends the packet)
+ */
+ void SetGameMode(eGameMode a_GameMode);
+
+ /// Returns true if the player is in Creative mode, either explicitly, or by inheriting from current world
+ bool IsGameModeCreative(void) const;
+
+ /// Returns true if the player is in Survival mode, either explicitly, or by inheriting from current world
+ bool IsGameModeSurvival(void) const;
+
+ /// Returns true if the player is in Adventure mode, either explicitly, or by inheriting from current world
+ bool IsGameModeAdventure(void) const;
+
+ AString GetIP(void) const { return m_IP; } // tolua_export
+
+ // tolua_end
+
+ void SetIP(const AString & a_IP);
+
+ float GetLastBlockActionTime() { return m_LastBlockActionTime; }
+ int GetLastBlockActionCnt() { return m_LastBlockActionCnt; }
+ void SetLastBlockActionCnt( int );
+ void SetLastBlockActionTime();
+
+ // Sets the current gamemode, doesn't check validity, doesn't send update packets to client
+ void LoginSetGameMode(eGameMode a_GameMode);
+
+ /// Tries to move to a new position, with attachment-related checks (y == -999)
+ void MoveTo(const Vector3d & a_NewPos); // tolua_export
+
+ cWindow * GetWindow(void) { return m_CurrentWindow; } // tolua_export
+ const cWindow * GetWindow(void) const { return m_CurrentWindow; }
+
+ /// Opens the specified window; closes the current one first using CloseWindow()
+ void OpenWindow(cWindow * a_Window); // Exported in ManualBindings.cpp
+
+ // tolua_begin
+
+ /// Closes the current window, resets current window to m_InventoryWindow. A plugin may refuse the closing if a_CanRefuse is true
+ void CloseWindow(bool a_CanRefuse = true);
+
+ /// Closes the current window if it matches the specified ID, resets current window to m_InventoryWindow
+ void CloseWindowIfID(char a_WindowID, bool a_CanRefuse = true);
+
+ cClientHandle * GetClientHandle(void) const { return m_ClientHandle; }
+
+ void SendMessage(const AString & a_Message);
+
+ const AString & GetName(void) const { return m_PlayerName; }
+ void SetName(const AString & a_Name) { m_PlayerName = a_Name; }
+
+ // tolua_end
+
+ typedef std::list< cGroup* > GroupList;
+ typedef std::list< std::string > StringList;
+
+ /// Adds a player to existing group or creates a new group when it doesn't exist
+ void AddToGroup( const AString & a_GroupName ); // tolua_export
+
+ /// Removes a player from the group, resolves permissions and group inheritance (case sensitive)
+ void RemoveFromGroup( const AString & a_GroupName ); // tolua_export
+
+ bool CanUseCommand( const AString & a_Command ); // tolua_export
+ bool HasPermission( const AString & a_Permission ); // tolua_export
+ const GroupList & GetGroups() { return m_Groups; } // >> EXPORTED IN MANUALBINDINGS <<
+ StringList GetResolvedPermissions(); // >> EXPORTED IN MANUALBINDINGS <<
+ bool IsInGroup( const AString & a_Group ); // tolua_export
+
+ // tolua_begin
+
+ /// Returns the full color code to use for this player, based on their primary group or set in m_Color
+ AString GetColor(void) const;
+
+ void TossItem(bool a_bDraggingItem, char a_Amount = 1, short a_CreateType = 0, short a_CreateHealth = 0);
+
+ /// Heals the player by the specified amount of HPs (positive only); sends health update
+ void Heal(int a_Health);
+
+ int GetFoodLevel (void) const { return m_FoodLevel; }
+ double GetFoodSaturationLevel (void) const { return m_FoodSaturationLevel; }
+ int GetFoodTickTimer (void) const { return m_FoodTickTimer; }
+ double GetFoodExhaustionLevel (void) const { return m_FoodExhaustionLevel; }
+ int GetFoodPoisonedTicksRemaining(void) const { return m_FoodPoisonedTicksRemaining; }
+
+ int GetAirLevel (void) const { return m_AirLevel; }
+
+ /// Returns true if the player is satiated, i. e. their foodlevel is at the max and they cannot eat anymore
+ bool IsSatiated(void) const { return (m_FoodLevel >= MAX_FOOD_LEVEL); }
+
+ void SetFoodLevel (int a_FoodLevel);
+ void SetFoodSaturationLevel (double a_FoodSaturationLevel);
+ void SetFoodTickTimer (int a_FoodTickTimer);
+ void SetFoodExhaustionLevel (double a_FoodExhaustionLevel);
+ void SetFoodPoisonedTicksRemaining(int a_FoodPoisonedTicksRemaining);
+
+ /// Adds to FoodLevel and FoodSaturationLevel, returns true if any food has been consumed, false if player "full"
+ bool Feed(int a_Food, double a_Saturation);
+
+ /// Adds the specified exhaustion to m_FoodExhaustion. Expects only positive values.
+ void AddFoodExhaustion(double a_Exhaustion)
+ {
+ m_FoodExhaustionLevel += a_Exhaustion;
+ }
+
+ /// Starts the food poisoning for the specified amount of ticks; if already foodpoisoned, sets FoodPoisonedTicksRemaining to the larger of the two
+ void FoodPoison(int a_NumTicks);
+
+ /// Returns true if the player is currently in the process of eating the currently equipped item
+ bool IsEating(void) const { return (m_EatingFinishTick >= 0); }
+
+ // tolua_end
+
+ /// Starts eating the currently equipped item. Resets the eating timer and sends the proper animation packet
+ void StartEating(void);
+
+ /// Finishes eating the currently equipped item. Consumes the item, updates health and broadcasts the packets
+ void FinishEating(void);
+
+ /// Aborts the current eating operation
+ void AbortEating(void);
+
+ virtual void KilledBy(cEntity * a_Killer) override;
+
+ void Respawn(void); // tolua_export
+
+ void SetVisible( bool a_bVisible ); // tolua_export
+ bool IsVisible(void) const { return m_bVisible; } // tolua_export
+
+ bool MoveToWorld(const char * a_WorldName); // tolua_export
+
+ bool SaveToDisk(void);
+ bool LoadFromDisk(void);
+ void LoadPermissionsFromDisk(void); // tolua_export
+
+ const AString & GetLoadedWorldName() { return m_LoadedWorldName; }
+
+ void UseEquippedItem(void);
+
+ void SendHealth(void);
+
+ // In UI windows, the item that the player is dragging:
+ bool IsDraggingItem(void) const { return !m_DraggingItem.IsEmpty(); }
+ cItem & GetDraggingItem(void) {return m_DraggingItem; }
+
+ // In UI windows, when inventory-painting:
+ /// Clears the list of slots that are being inventory-painted. To be used by cWindow only
+ void ClearInventoryPaintSlots(void);
+
+ /// Adds a slot to the list for inventory painting. To be used by cWindow only
+ void AddInventoryPaintSlot(int a_SlotNum);
+
+ /// Returns the list of slots currently stored for inventory painting. To be used by cWindow only
+ const cSlotNums & GetInventoryPaintSlots(void) const;
+
+ // tolua_begin
+
+ /// Returns the current maximum speed, as reported in the 1.6.1+ protocol (takes current sprinting state into account)
+ double GetMaxSpeed(void) const;
+
+ /// Gets the normal maximum speed, as reported in the 1.6.1+ protocol, in the protocol units
+ double GetNormalMaxSpeed(void) const { return m_NormalMaxSpeed; }
+
+ /// Gets the sprinting maximum speed, as reported in the 1.6.1+ protocol, in the protocol units
+ double GetSprintingMaxSpeed(void) const { return m_SprintingMaxSpeed; }
+
+ /// Sets the normal maximum speed, as reported in the 1.6.1+ protocol. Sends the update to player, if needed.
+ void SetNormalMaxSpeed(double a_Speed);
+
+ /// Sets the sprinting maximum speed, as reported in the 1.6.1+ protocol. Sends the update to player, if needed.
+ void SetSprintingMaxSpeed(double a_Speed);
+
+ /// Sets the crouch status, broadcasts to all visible players
+ void SetCrouch(bool a_IsCrouched);
+
+ /// Starts or stops sprinting, sends the max speed update to the client, if needed
+ void SetSprint(bool a_IsSprinting);
+
+ /// Returns whether the player is swimming or not
+ virtual bool IsSwimming(void) const{ return m_IsSwimming; }
+
+ /// Return whether the player is under water or not
+ virtual bool IsSubmerged(void) const{ return m_IsSubmerged; }
+
+ // tolua_end
+
+ // cEntity overrides:
+ virtual bool IsCrouched (void) const { return m_IsCrouched; }
+ virtual bool IsSprinting(void) const { return m_IsSprinting; }
+ virtual bool IsRclking (void) const { return IsEating(); }
+
+
+
+protected:
+ typedef std::map< std::string, bool > PermissionMap;
+ PermissionMap m_ResolvedPermissions;
+ PermissionMap m_Permissions;
+
+ GroupList m_ResolvedGroups;
+ GroupList m_Groups;
+
+ std::string m_PlayerName;
+ std::string m_LoadedWorldName;
+
+ /// Xp Level stuff
+ enum
+ {
+ XP_TO_LEVEL15 = 255,
+ XP_PER_LEVEL_TO15 = 17,
+ XP_TO_LEVEL30 = 825
+ } ;
+
+ /// Player's air level (for swimming)
+ int m_AirLevel;
+
+ /// used to time ticks between damage taken via drowning/suffocation
+ int m_AirTickTimer;
+
+ bool m_bVisible;
+
+ // Food-related variables:
+ /// Represents the food bar, one point equals half a "drumstick"
+ int m_FoodLevel;
+
+ /// "Overcharge" for the m_FoodLevel; is depleted before m_FoodLevel
+ double m_FoodSaturationLevel;
+
+ /// Count-up to the healing or damaging action, based on m_FoodLevel
+ int m_FoodTickTimer;
+
+ /// A "buffer" which adds up hunger before it is substracted from m_FoodSaturationLevel or m_FoodLevel. Each action adds a little
+ double m_FoodExhaustionLevel;
+
+ /// Number of ticks remaining for the foodpoisoning effect; zero if not foodpoisoned
+ int m_FoodPoisonedTicksRemaining;
+
+ /// Last position that has been recorded for food-related processing:
+ Vector3d m_LastFoodPos;
+
+ float m_LastJumpHeight;
+ float m_LastGroundHeight;
+ bool m_bTouchGround;
+ double m_Stance;
+ cInventory m_Inventory;
+ cWindow * m_CurrentWindow;
+ cWindow * m_InventoryWindow;
+
+ float m_TimeLastPickupCheck;
+
+ void ResolvePermissions();
+
+ void ResolveGroups();
+ char m_Color;
+
+ float m_LastBlockActionTime;
+ int m_LastBlockActionCnt;
+ eGameMode m_GameMode;
+ std::string m_IP;
+
+ cItem m_DraggingItem;
+
+ long long m_LastPlayerListTime;
+ static const unsigned short PLAYER_LIST_TIME_MS = 1000; // 1000 = once per second
+
+ cClientHandle * m_ClientHandle;
+
+ cSlotNums m_InventoryPaintSlots;
+
+ /// Max speed, in ENTITY_PROPERTIES packet's units, when the player is walking. 0.1 by default
+ double m_NormalMaxSpeed;
+
+ /// Max speed, in ENTITY_PROPERTIES packet's units, when the player is sprinting. 0.13 by default
+ double m_SprintingMaxSpeed;
+
+ bool m_IsCrouched;
+ bool m_IsSprinting;
+
+ bool m_IsSwimming;
+ bool m_IsSubmerged;
+
+ /// The world tick in which eating will be finished. -1 if not eating
+ Int64 m_EatingFinishTick;
+
+ /// Player Xp level
+ int m_XpTotal;
+
+ /// Caculates the Xp needed for a given level, ref: http://minecraft.gamepedia.com/XP
+ static int XpForLevel(int a_Level);
+
+ /// inverse of XpAtLevel, ref: http://minecraft.gamepedia.com/XP values are as per this with pre-calculations
+ static int CalcLevelFromXp(int a_XpTotal);
+
+ bool m_IsChargingBow;
+ int m_BowCharge;
+
+ virtual void Destroyed(void);
+
+ /// Filters out damage for creative mode
+ virtual void DoTakeDamage(TakeDamageInfo & TDI) override;
+
+ /// Called in each tick to handle food-related processing
+ void HandleFood(void);
+
+ /// Called in each tick to handle air-related processing i.e. drowning
+ void HandleAir();
+
+ /// Called once per tick to set IsSwimming and IsSubmerged
+ void SetSwimState(cChunk & a_Chunk);
+
+ /// Adds food exhaustion based on the difference between Pos and LastPos, sprinting status and swimming (in water block)
+ void ApplyFoodExhaustionFromMovement();
+} ; // tolua_export
+
+
+
+
diff --git a/src/Entities/ProjectileEntity.cpp b/src/Entities/ProjectileEntity.cpp
new file mode 100644
index 000000000..c63b9523b
--- /dev/null
+++ b/src/Entities/ProjectileEntity.cpp
@@ -0,0 +1,743 @@
+
+// ProjectileEntity.cpp
+
+// Implements the cProjectileEntity class representing the common base class for projectiles, as well as individual projectile types
+
+#include "Globals.h"
+#include "ProjectileEntity.h"
+#include "../ClientHandle.h"
+#include "Player.h"
+#include "../LineBlockTracer.h"
+#include "../BoundingBox.h"
+#include "../ChunkMap.h"
+#include "../Chunk.h"
+
+
+
+
+
+/// Converts an angle in radians into a byte representation used by the network protocol
+#define ANGLE_TO_PROTO(X) (Byte)(X * 255 / 360)
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cProjectileTracerCallback:
+
+class cProjectileTracerCallback :
+ public cBlockTracer::cCallbacks
+{
+public:
+ cProjectileTracerCallback(cProjectileEntity * a_Projectile) :
+ m_Projectile(a_Projectile),
+ m_SlowdownCoeff(0.99) // Default slowdown when not in water
+ {
+ }
+
+ double GetSlowdownCoeff(void) const { return m_SlowdownCoeff; }
+
+protected:
+ cProjectileEntity * m_Projectile;
+ double m_SlowdownCoeff;
+
+ // cCallbacks overrides:
+ virtual bool OnNextBlock(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta, char a_EntryFace) override
+ {
+ /*
+ // DEBUG:
+ LOGD("Hit block %d:%d at {%d, %d, %d} face %d, %s (%s)",
+ a_BlockType, a_BlockMeta,
+ a_BlockX, a_BlockY, a_BlockZ, a_EntryFace,
+ g_BlockIsSolid[a_BlockType] ? "solid" : "non-solid",
+ ItemToString(cItem(a_BlockType, 1, a_BlockMeta)).c_str()
+ );
+ */
+
+ if (g_BlockIsSolid[a_BlockType])
+ {
+ // The projectile hit a solid block
+ // Calculate the exact hit coords:
+ cBoundingBox bb(a_BlockX, a_BlockX + 1, a_BlockY, a_BlockY + 1, a_BlockZ, a_BlockZ + 1);
+ Vector3d Line1 = m_Projectile->GetPosition();
+ Vector3d Line2 = Line1 + m_Projectile->GetSpeed();
+ double LineCoeff = 0;
+ char Face;
+ if (bb.CalcLineIntersection(Line1, Line2, LineCoeff, Face))
+ {
+ Vector3d Intersection = Line1 + m_Projectile->GetSpeed() * LineCoeff;
+ m_Projectile->OnHitSolidBlock(Intersection, Face);
+ return true;
+ }
+ else
+ {
+ LOGD("WEIRD! block tracer reports a hit, but BBox tracer doesn't. Ignoring the hit.");
+ }
+ }
+
+ // Convey some special effects from special blocks:
+ switch (a_BlockType)
+ {
+ case E_BLOCK_LAVA:
+ case E_BLOCK_STATIONARY_LAVA:
+ {
+ m_Projectile->StartBurning(30);
+ m_SlowdownCoeff = std::min(m_SlowdownCoeff, 0.9); // Slow down to 0.9* the speed each tick when moving through lava
+ break;
+ }
+ case E_BLOCK_WATER:
+ case E_BLOCK_STATIONARY_WATER:
+ {
+ m_Projectile->StopBurning();
+ m_SlowdownCoeff = std::min(m_SlowdownCoeff, 0.8); // Slow down to 0.8* the speed each tick when moving through water
+ break;
+ }
+ } // switch (a_BlockType)
+
+ // Continue tracing
+ return false;
+ }
+} ;
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cProjectileEntityCollisionCallback:
+
+class cProjectileEntityCollisionCallback :
+ public cEntityCallback
+{
+public:
+ cProjectileEntityCollisionCallback(cProjectileEntity * a_Projectile, const Vector3d & a_Pos, const Vector3d & a_NextPos) :
+ m_Projectile(a_Projectile),
+ m_Pos(a_Pos),
+ m_NextPos(a_NextPos),
+ m_MinCoeff(1),
+ m_HitEntity(NULL)
+ {
+ }
+
+
+ virtual bool Item(cEntity * a_Entity) override
+ {
+ if (
+ (a_Entity == m_Projectile) || // Do not check collisions with self
+ (a_Entity == m_Projectile->GetCreator()) // Do not check whoever shot the projectile
+ )
+ {
+ // TODO: Don't check creator only for the first 5 ticks
+ // so that arrows stuck in ground and dug up can hurt the player
+ return false;
+ }
+
+ cBoundingBox EntBox(a_Entity->GetPosition(), a_Entity->GetWidth() / 2, a_Entity->GetHeight());
+
+ // Instead of colliding the bounding box with another bounding box in motion, we collide an enlarged bounding box with a hairline.
+ // The results should be good enough for our purposes
+ double LineCoeff;
+ char Face;
+ EntBox.Expand(m_Projectile->GetWidth() / 2, m_Projectile->GetHeight() / 2, m_Projectile->GetWidth() / 2);
+ if (!EntBox.CalcLineIntersection(m_Pos, m_NextPos, LineCoeff, Face))
+ {
+ // No intersection whatsoever
+ return false;
+ }
+
+ // TODO: Some entities don't interact with the projectiles (pickups, falling blocks)
+ // TODO: Allow plugins to interfere about which entities can be hit
+
+ if (LineCoeff < m_MinCoeff)
+ {
+ // The entity is closer than anything we've stored so far, replace it as the potential victim
+ m_MinCoeff = LineCoeff;
+ m_HitEntity = a_Entity;
+ }
+
+ // Don't break the enumeration, we want all the entities
+ return false;
+ }
+
+ /// Returns the nearest entity that was hit, after the enumeration has been completed
+ cEntity * GetHitEntity(void) const { return m_HitEntity; }
+
+ /// Returns the line coeff where the hit was encountered, after the enumeration has been completed
+ double GetMinCoeff(void) const { return m_MinCoeff; }
+
+ /// Returns true if the callback has encountered a true hit
+ bool HasHit(void) const { return (m_MinCoeff < 1); }
+
+protected:
+ cProjectileEntity * m_Projectile;
+ const Vector3d & m_Pos;
+ const Vector3d & m_NextPos;
+ double m_MinCoeff; // The coefficient of the nearest hit on the Pos line
+
+ // Although it's bad(tm) to store entity ptrs from a callback, we can afford it here, because the entire callback
+ // is processed inside the tick thread, so the entities won't be removed in between the calls and the final processing
+ cEntity * m_HitEntity; // The nearest hit entity
+} ;
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cProjectileEntity:
+
+cProjectileEntity::cProjectileEntity(eKind a_Kind, cEntity * a_Creator, double a_X, double a_Y, double a_Z, double a_Width, double a_Height) :
+ super(etProjectile, a_X, a_Y, a_Z, a_Width, a_Height),
+ m_ProjectileKind(a_Kind),
+ m_Creator(a_Creator),
+ m_IsInGround(false)
+{
+}
+
+
+
+
+
+cProjectileEntity::cProjectileEntity(eKind a_Kind, cEntity * a_Creator, const Vector3d & a_Pos, const Vector3d & a_Speed, double a_Width, double a_Height) :
+ super(etProjectile, a_Pos.x, a_Pos.y, a_Pos.z, a_Width, a_Height),
+ m_ProjectileKind(a_Kind),
+ m_Creator(a_Creator),
+ m_IsInGround(false)
+{
+ SetSpeed(a_Speed);
+ SetRotationFromSpeed();
+ SetPitchFromSpeed();
+}
+
+
+
+
+
+cProjectileEntity * cProjectileEntity::Create(eKind a_Kind, cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d * a_Speed)
+{
+ Vector3d Speed;
+ if (a_Speed != NULL)
+ {
+ Speed = *a_Speed;
+ }
+
+ switch (a_Kind)
+ {
+ case pkArrow: return new cArrowEntity (a_Creator, a_X, a_Y, a_Z, Speed);
+ case pkEgg: return new cThrownEggEntity (a_Creator, a_X, a_Y, a_Z, Speed);
+ case pkEnderPearl: return new cThrownEnderPearlEntity(a_Creator, a_X, a_Y, a_Z, Speed);
+ case pkSnowball: return new cThrownSnowballEntity (a_Creator, a_X, a_Y, a_Z, Speed);
+ case pkGhastFireball: return new cGhastFireballEntity (a_Creator, a_X, a_Y, a_Z, Speed);
+ case pkFireCharge: return new cFireChargeEntity (a_Creator, a_X, a_Y, a_Z, Speed);
+ // TODO: the rest
+ }
+
+ LOGWARNING("%s: Unknown projectile kind: %d", __FUNCTION__, a_Kind);
+ return NULL;
+}
+
+
+
+
+
+void cProjectileEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ // Set the position based on what face was hit:
+ SetPosition(a_HitPos);
+ SetSpeed(0, 0, 0);
+
+ // DEBUG:
+ LOGD("Projectile %d: pos {%.02f, %.02f, %.02f}, hit solid block at face %d",
+ m_UniqueID,
+ a_HitPos.x, a_HitPos.y, a_HitPos.z,
+ a_HitFace
+ );
+
+ m_IsInGround = true;
+}
+
+
+
+
+
+AString cProjectileEntity::GetMCAClassName(void) const
+{
+ switch (m_ProjectileKind)
+ {
+ case pkArrow: return "Arrow";
+ case pkSnowball: return "Snowball";
+ case pkEgg: return "Egg";
+ case pkGhastFireball: return "Fireball";
+ case pkFireCharge: return "SmallFireball";
+ case pkEnderPearl: return "ThrownEnderPearl";
+ case pkExpBottle: return "ThrownExpBottle";
+ case pkSplashPotion: return "ThrownPotion";
+ case pkWitherSkull: return "WitherSkull";
+ case pkFishingFloat: return ""; // Unknown, perhaps MC doesn't save this?
+ }
+ ASSERT(!"Unhandled projectile entity kind!");
+ return "";
+}
+
+
+
+
+
+void cProjectileEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ super::Tick(a_Dt, a_Chunk);
+ BroadcastMovementUpdate();
+}
+
+
+
+
+
+void cProjectileEntity::HandlePhysics(float a_Dt, cChunk & a_Chunk)
+{
+ if (m_IsInGround)
+ {
+ // Already-grounded projectiles don't move at all
+ return;
+ }
+
+ Vector3d PerTickSpeed = GetSpeed() / 20;
+ Vector3d Pos = GetPosition();
+
+ // Trace the tick's worth of movement as a line:
+ Vector3d NextPos = Pos + PerTickSpeed;
+ cProjectileTracerCallback TracerCallback(this);
+ if (!cLineBlockTracer::Trace(*m_World, TracerCallback, Pos, NextPos))
+ {
+ // Something has been hit, abort all other processing
+ return;
+ }
+ // The tracer also checks the blocks for slowdown blocks - water and lava - and stores it for later in its SlowdownCoeff
+
+ // Test for entity collisions:
+ cProjectileEntityCollisionCallback EntityCollisionCallback(this, Pos, NextPos);
+ a_Chunk.ForEachEntity(EntityCollisionCallback);
+ if (EntityCollisionCallback.HasHit())
+ {
+ // An entity was hit:
+ Vector3d HitPos = Pos + (NextPos - Pos) * EntityCollisionCallback.GetMinCoeff();
+
+ // DEBUG:
+ LOGD("Projectile %d has hit an entity %d (%s) at {%.02f, %.02f, %.02f} (coeff %.03f)",
+ m_UniqueID,
+ EntityCollisionCallback.GetHitEntity()->GetUniqueID(),
+ EntityCollisionCallback.GetHitEntity()->GetClass(),
+ HitPos.x, HitPos.y, HitPos.z,
+ EntityCollisionCallback.GetMinCoeff()
+ );
+
+ OnHitEntity(*(EntityCollisionCallback.GetHitEntity()), HitPos);
+ }
+ // TODO: Test the entities in the neighboring chunks, too
+
+ // Update the position:
+ SetPosition(NextPos);
+
+ // Add slowdown and gravity effect to the speed:
+ Vector3d NewSpeed(GetSpeed());
+ NewSpeed.y += m_Gravity / 20;
+ NewSpeed *= TracerCallback.GetSlowdownCoeff();
+ SetSpeed(NewSpeed);
+ SetRotationFromSpeed();
+ SetPitchFromSpeed();
+
+ // DEBUG:
+ LOGD("Projectile %d: pos {%.02f, %.02f, %.02f}, speed {%.02f, %.02f, %.02f}, rot {%.02f, %.02f}",
+ m_UniqueID,
+ GetPosX(), GetPosY(), GetPosZ(),
+ GetSpeedX(), GetSpeedY(), GetSpeedZ(),
+ GetRotation(), GetPitch()
+ );
+}
+
+
+
+
+
+void cProjectileEntity::SpawnOn(cClientHandle & a_Client)
+{
+ // Default spawning - use the projectile kind to spawn an object:
+ a_Client.SendSpawnObject(*this, m_ProjectileKind, 12, ANGLE_TO_PROTO(GetRotation()), ANGLE_TO_PROTO(GetPitch()));
+ a_Client.SendEntityMetadata(*this);
+}
+
+
+
+
+
+void cProjectileEntity::CollectedBy(cPlayer * a_Dest)
+{
+ // Overriden in arrow
+ UNUSED(a_Dest);
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cArrowEntity:
+
+cArrowEntity::cArrowEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed) :
+ super(pkArrow, a_Creator, a_X, a_Y, a_Z, 0.5, 0.5),
+ m_PickupState(psNoPickup),
+ m_DamageCoeff(2),
+ m_IsCritical(false),
+ m_Timer(0),
+ m_bIsCollected(false),
+ m_HitBlockPos(Vector3i(0, 0, 0))
+{
+ SetSpeed(a_Speed);
+ SetMass(0.1);
+ SetRotationFromSpeed();
+ SetPitchFromSpeed();
+ LOGD("Created arrow %d with speed {%.02f, %.02f, %.02f} and rot {%.02f, %.02f}",
+ m_UniqueID, GetSpeedX(), GetSpeedY(), GetSpeedZ(),
+ GetRotation(), GetPitch()
+ );
+}
+
+
+
+
+
+cArrowEntity::cArrowEntity(cPlayer & a_Player, double a_Force) :
+ super(pkArrow, &a_Player, a_Player.GetThrowStartPos(), a_Player.GetThrowSpeed(a_Force * 1.5 * 20), 0.5, 0.5),
+ m_PickupState(psInSurvivalOrCreative),
+ m_DamageCoeff(2),
+ m_IsCritical((a_Force >= 1)),
+ m_Timer(0),
+ m_bIsCollected(false),
+ m_HitBlockPos(0, 0, 0)
+{
+}
+
+
+
+
+
+bool cArrowEntity::CanPickup(const cPlayer & a_Player) const
+{
+ switch (m_PickupState)
+ {
+ case psNoPickup: return false;
+ case psInSurvivalOrCreative: return (a_Player.IsGameModeSurvival() || a_Player.IsGameModeCreative());
+ case psInCreative: return a_Player.IsGameModeCreative();
+ }
+ ASSERT(!"Unhandled pickup state");
+ return false;
+}
+
+
+
+
+
+void cArrowEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ if (a_HitFace == BLOCK_FACE_NONE)
+ {
+ return;
+ }
+
+ super::OnHitSolidBlock(a_HitPos, a_HitFace);
+ int a_X = (int)a_HitPos.x, a_Y = (int)a_HitPos.y, a_Z = (int)a_HitPos.z;
+
+ if (a_HitFace != BLOCK_FACE_YP)
+ {
+ AddFaceDirection(a_X, a_Y, a_Z, a_HitFace);
+ }
+ else if (a_HitFace == BLOCK_FACE_YP) // These conditions because xoft got a little confused on block face directions, so AddFace works with all but YP & YM
+ {
+ a_Y--;
+ }
+ else
+ {
+ a_Y++;
+ }
+
+ m_HitBlockPos = Vector3i(a_X, a_Y, a_Z);
+
+ // Broadcast arrow hit sound
+ m_World->BroadcastSoundEffect("random.bowhit", (int)GetPosX() * 8, (int)GetPosY() * 8, (int)GetPosZ() * 8, 0.5, (float)(0.75 + ((float)((GetUniqueID() * 23) % 32)) / 64));
+
+ // Broadcast the position and speed packets before teleporting:
+ BroadcastMovementUpdate();
+
+ // Teleport the entity to the exact hit coords:
+ m_World->BroadcastTeleportEntity(*this);
+}
+
+
+
+
+
+void cArrowEntity::OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos)
+{
+ if (!a_EntityHit.IsMob() && !a_EntityHit.IsMinecart() && !a_EntityHit.IsPlayer() && !a_EntityHit.IsBoat())
+ {
+ // Not an entity that interacts with an arrow
+ return;
+ }
+
+ int Damage = (int)(GetSpeed().Length() / 20 * m_DamageCoeff + 0.5);
+ if (m_IsCritical)
+ {
+ Damage += m_World->GetTickRandomNumber(Damage / 2 + 2);
+ }
+ a_EntityHit.TakeDamage(dtRangedAttack, this, Damage, 1);
+
+ // Broadcast successful hit sound
+ m_World->BroadcastSoundEffect("random.successful_hit", (int)GetPosX() * 8, (int)GetPosY() * 8, (int)GetPosZ() * 8, 0.5, (float)(0.75 + ((float)((GetUniqueID() * 23) % 32)) / 64));
+
+ Destroy();
+}
+
+
+
+
+
+void cArrowEntity::CollectedBy(cPlayer * a_Dest)
+{
+ if ((m_IsInGround) && (!m_bIsCollected) && (CanPickup(*a_Dest)))
+ {
+ int NumAdded = a_Dest->GetInventory().AddItem(E_ITEM_ARROW);
+ if (NumAdded > 0) // Only play effects if there was space in inventory
+ {
+ m_World->BroadcastCollectPickup((const cPickup &)*this, *a_Dest);
+ // Also send the "pop" sound effect with a somewhat random pitch (fast-random using EntityID ;)
+ m_World->BroadcastSoundEffect("random.pop", (int)GetPosX() * 8, (int)GetPosY() * 8, (int)GetPosZ() * 8, 0.5, (float)(0.75 + ((float)((GetUniqueID() * 23) % 32)) / 64));
+ m_bIsCollected = true;
+ }
+ }
+}
+
+
+
+
+
+void cArrowEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ super::Tick(a_Dt, a_Chunk);
+ m_Timer += a_Dt;
+
+ if (m_bIsCollected)
+ {
+ if (m_Timer > 500.f) // 0.5 seconds
+ {
+ Destroy();
+ return;
+ }
+ }
+ else if (m_Timer > 1000 * 60 * 5) // 5 minutes
+ {
+ Destroy();
+ return;
+ }
+
+ if (m_IsInGround)
+ {
+ int RelPosX = m_HitBlockPos.x - a_Chunk.GetPosX() * cChunkDef::Width;
+ int RelPosZ = m_HitBlockPos.z - a_Chunk.GetPosZ() * cChunkDef::Width;
+ cChunk * Chunk = a_Chunk.GetRelNeighborChunkAdjustCoords(RelPosX, RelPosZ);
+
+ if (Chunk == NULL)
+ {
+ // Inside an unloaded chunk, abort
+ return;
+ }
+
+ if (Chunk->GetBlock(RelPosX, m_HitBlockPos.y, RelPosZ) == E_BLOCK_AIR) // Block attached to was destroyed?
+ {
+ m_IsInGround = false; // Yes, begin simulating physics again
+ }
+ }
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cThrownEggEntity:
+
+cThrownEggEntity::cThrownEggEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed) :
+ super(pkEgg, a_Creator, a_X, a_Y, a_Z, 0.25, 0.25)
+{
+ SetSpeed(a_Speed);
+}
+
+
+
+
+
+void cThrownEggEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ if (m_World->GetTickRandomNumber(7) == 1)
+ {
+ m_World->SpawnMob(a_HitPos.x, a_HitPos.y, a_HitPos.z, cMonster::mtChicken);
+ }
+ else if (m_World->GetTickRandomNumber(32) == 1)
+ {
+ m_World->SpawnMob(a_HitPos.x, a_HitPos.y, a_HitPos.z, cMonster::mtChicken);
+ m_World->SpawnMob(a_HitPos.x, a_HitPos.y, a_HitPos.z, cMonster::mtChicken);
+ m_World->SpawnMob(a_HitPos.x, a_HitPos.y, a_HitPos.z, cMonster::mtChicken);
+ m_World->SpawnMob(a_HitPos.x, a_HitPos.y, a_HitPos.z, cMonster::mtChicken);
+ }
+ Destroy();
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cThrownEnderPearlEntity :
+
+cThrownEnderPearlEntity::cThrownEnderPearlEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed) :
+ super(pkEnderPearl, a_Creator, a_X, a_Y, a_Z, 0.25, 0.25)
+{
+ SetSpeed(a_Speed);
+}
+
+
+
+
+
+void cThrownEnderPearlEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ // Teleport the creator here, make them take 5 damage:
+ if (m_Creator != NULL)
+ {
+ // TODO: The coords might need some tweaking based on the block face
+ m_Creator->TeleportToCoords(a_HitPos.x + 0.5, a_HitPos.y + 1.7, a_HitPos.z + 0.5);
+ m_Creator->TakeDamage(dtEnderPearl, this, 5, 0);
+ }
+
+ Destroy();
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cThrownSnowballEntity :
+
+cThrownSnowballEntity::cThrownSnowballEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed) :
+ super(pkSnowball, a_Creator, a_X, a_Y, a_Z, 0.25, 0.25)
+{
+ SetSpeed(a_Speed);
+}
+
+
+
+
+
+void cThrownSnowballEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ // TODO: Apply damage to certain mobs (blaze etc.) and anger all mobs
+
+ Destroy();
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cGhastFireballEntity :
+
+cGhastFireballEntity::cGhastFireballEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed) :
+ super(pkGhastFireball, a_Creator, a_X, a_Y, a_Z, 1, 1)
+{
+ SetSpeed(a_Speed);
+ SetGravity(0);
+}
+
+
+
+
+
+void cGhastFireballEntity::Explode(int a_BlockX, int a_BlockY, int a_BlockZ)
+{
+ m_World->DoExplosionAt(1, a_BlockX, a_BlockY, a_BlockZ, true, esGhastFireball, this);
+}
+
+
+
+
+
+void cGhastFireballEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ Destroy();
+ Explode((int)floor(a_HitPos.x), (int)floor(a_HitPos.y), (int)floor(a_HitPos.z));
+}
+
+
+
+
+
+void cGhastFireballEntity::OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos)
+{
+ Destroy();
+ Explode((int)floor(a_HitPos.x), (int)floor(a_HitPos.y), (int)floor(a_HitPos.z));
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cFireChargeEntity :
+
+cFireChargeEntity::cFireChargeEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed) :
+ super(pkFireCharge, a_Creator, a_X, a_Y, a_Z, 0.3125, 0.3125)
+{
+ SetSpeed(a_Speed);
+ SetGravity(0);
+}
+
+
+
+
+
+void cFireChargeEntity::Explode(int a_BlockX, int a_BlockY, int a_BlockZ)
+{
+ if (m_World->GetBlock(a_BlockX, a_BlockY, a_BlockZ) == E_BLOCK_AIR)
+ {
+ m_World->SetBlock(a_BlockX, a_BlockY, a_BlockZ, E_BLOCK_FIRE, 1);
+ }
+}
+
+
+
+
+
+void cFireChargeEntity::OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace)
+{
+ Destroy();
+ Explode((int)floor(a_HitPos.x), (int)floor(a_HitPos.y), (int)floor(a_HitPos.z));
+}
+
+
+
+
+
+void cFireChargeEntity::OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos)
+{
+ Destroy();
+ Explode((int)floor(a_HitPos.x), (int)floor(a_HitPos.y), (int)floor(a_HitPos.z));
+
+ // TODO: Some entities are immune to hits
+ a_EntityHit.StartBurning(5 * 20); // 5 seconds of burning
+}
+
+
+
+
diff --git a/src/Entities/ProjectileEntity.h b/src/Entities/ProjectileEntity.h
new file mode 100644
index 000000000..28dd76935
--- /dev/null
+++ b/src/Entities/ProjectileEntity.h
@@ -0,0 +1,325 @@
+
+// ProjectileEntity.h
+
+// Declares the cProjectileEntity class representing the common base class for projectiles, as well as individual projectile types
+
+
+
+
+
+#pragma once
+
+#include "Entity.h"
+
+
+
+
+
+// tolua_begin
+
+class cProjectileEntity :
+ public cEntity
+{
+ typedef cEntity super;
+
+public:
+ /// The kind of the projectile. The numbers correspond to the network type ID used for spawning via the 0x17 packet.
+ enum eKind
+ {
+ pkArrow = 60,
+ pkSnowball = 61,
+ pkEgg = 62,
+ pkGhastFireball = 63,
+ pkFireCharge = 64,
+ pkEnderPearl = 65,
+ pkExpBottle = 75,
+ pkSplashPotion = 73,
+ pkWitherSkull = 66,
+ pkFishingFloat = 90,
+ } ;
+
+ // tolua_end
+
+ CLASS_PROTODEF(cProjectileEntity);
+
+ cProjectileEntity(eKind a_Kind, cEntity * a_Creator, double a_X, double a_Y, double a_Z, double a_Width, double a_Height);
+ cProjectileEntity(eKind a_Kind, cEntity * a_Creator, const Vector3d & a_Pos, const Vector3d & a_Speed, double a_Width, double a_Height);
+
+ static cProjectileEntity * Create(eKind a_Kind, cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d * a_Speed = NULL);
+
+ /// Called by the physics blocktracer when the entity hits a solid block, the hit position and the face hit (BLOCK_FACE_) is given
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace);
+
+ /// Called by the physics blocktracer when the entity hits another entity
+ virtual void OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos) {}
+
+ /// Called by Chunk when the projectile is eligible for player collection
+ virtual void CollectedBy(cPlayer * a_Dest);
+
+ // tolua_begin
+
+ /// Returns the kind of the projectile (fast class identification)
+ eKind GetProjectileKind(void) const { return m_ProjectileKind; }
+
+ /// Returns the entity who created this projectile; may be NULL
+ cEntity * GetCreator(void) { return m_Creator; }
+
+ /// Returns the string that is used as the entity type (class name) in MCA files
+ AString GetMCAClassName(void) const;
+
+ /// Returns true if the projectile has hit the ground and is stuck there
+ bool IsInGround(void) const { return m_IsInGround; }
+
+ // tolua_end
+
+ /// Sets the internal InGround flag. To be used by MCA loader only!
+ void SetIsInGround(bool a_IsInGround) { m_IsInGround = a_IsInGround; }
+
+protected:
+ eKind m_ProjectileKind;
+
+ /// The entity who has created this projectile; may be NULL (e. g. for dispensers)
+ cEntity * m_Creator;
+
+ /// True if the projectile has hit the ground and is stuck there
+ bool m_IsInGround;
+
+ // cEntity overrides:
+ virtual void Tick(float a_Dt, cChunk & a_Chunk) override;
+ virtual void HandlePhysics(float a_Dt, cChunk & a_Chunk) override;
+ virtual void SpawnOn(cClientHandle & a_Client) override;
+
+ // tolua_begin
+} ;
+
+
+
+
+
+class cArrowEntity :
+ public cProjectileEntity
+{
+ typedef cProjectileEntity super;
+
+public:
+ /// Determines when the arrow can be picked up (depending on player gamemode). Corresponds to the MCA file "pickup" field
+ enum ePickupState
+ {
+ psNoPickup = 0,
+ psInSurvivalOrCreative = 1,
+ psInCreative = 2,
+ } ;
+
+ // tolua_end
+
+ CLASS_PROTODEF(cArrowEntity);
+
+ /// Creates a new arrow with psNoPickup state and default damage modifier coeff
+ cArrowEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed);
+
+ /// Creates a new arrow as shot by a player, initializes it from the player object
+ cArrowEntity(cPlayer & a_Player, double a_Force);
+
+ // tolua_begin
+
+ /// Returns whether the arrow can be picked up by players
+ ePickupState GetPickupState(void) const { return m_PickupState; }
+
+ /// Sets a new pickup state
+ void SetPickupState(ePickupState a_PickupState) { m_PickupState = a_PickupState; }
+
+ /// Returns the damage modifier coeff.
+ double GetDamageCoeff(void) const { return m_DamageCoeff; }
+
+ /// Sets the damage modifier coeff
+ void SetDamageCoeff(double a_DamageCoeff) { m_DamageCoeff = a_DamageCoeff; }
+
+ /// Returns true if the specified player can pick the arrow up
+ bool CanPickup(const cPlayer & a_Player) const;
+
+ /// Returns true if the arrow is set as critical
+ bool IsCritical(void) const { return m_IsCritical; }
+
+ /// Sets the IsCritical flag
+ void SetIsCritical(bool a_IsCritical) { m_IsCritical = a_IsCritical; }
+
+ // tolua_end
+
+protected:
+
+ /// Determines when the arrow can be picked up by players
+ ePickupState m_PickupState;
+
+ /// The coefficient applied to the damage that the arrow will deal, based on the bow enchantment. 2.0 for normal arrow
+ double m_DamageCoeff;
+
+ /// If true, the arrow deals more damage
+ bool m_IsCritical;
+
+ /// Timer for pickup collection animation or five minute timeout
+ float m_Timer;
+
+ /// If true, the arrow is in the process of being collected - don't go to anyone else
+ bool m_bIsCollected;
+
+ /// Stores the block position that arrow is lodged into, sets m_IsInGround to false if it becomes air
+ Vector3i m_HitBlockPos;
+
+ // cProjectileEntity overrides:
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace) override;
+ virtual void OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos) override;
+ virtual void CollectedBy(cPlayer * a_Player) override;
+ virtual void Tick(float a_Dt, cChunk & a_Chunk) override;
+
+ // tolua_begin
+} ;
+
+
+
+
+
+class cThrownEggEntity :
+ public cProjectileEntity
+{
+ typedef cProjectileEntity super;
+
+public:
+
+ // tolua_end
+
+ CLASS_PROTODEF(cThrownEggEntity);
+
+ cThrownEggEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed);
+
+protected:
+
+ // tolua_end
+
+ // cProjectileEntity overrides:
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace) override;
+
+ // tolua_begin
+
+} ;
+
+
+
+
+
+class cThrownEnderPearlEntity :
+ public cProjectileEntity
+{
+ typedef cProjectileEntity super;
+
+public:
+
+ // tolua_end
+
+ CLASS_PROTODEF(cThrownEnderPearlEntity);
+
+ cThrownEnderPearlEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed);
+
+protected:
+
+ // tolua_end
+
+ // cProjectileEntity overrides:
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace) override;
+
+ // tolua_begin
+
+} ;
+
+
+
+
+
+class cThrownSnowballEntity :
+ public cProjectileEntity
+{
+ typedef cProjectileEntity super;
+
+public:
+
+ // tolua_end
+
+ CLASS_PROTODEF(cThrownSnowballEntity);
+
+ cThrownSnowballEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed);
+
+protected:
+
+ // cProjectileEntity overrides:
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace) override;
+
+ // tolua_begin
+
+} ;
+
+
+
+
+
+class cGhastFireballEntity :
+ public cProjectileEntity
+{
+ typedef cProjectileEntity super;
+
+public:
+
+ // tolua_end
+
+ CLASS_PROTODEF(cGhastFireballEntity);
+
+ cGhastFireballEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed);
+
+protected:
+
+ void Explode(int a_BlockX, int a_BlockY, int a_BlockZ);
+
+ // cProjectileEntity overrides:
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace) override;
+ virtual void OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos) override;
+
+ // TODO: Deflecting the fireballs by arrow- or sword- hits
+
+ // tolua_begin
+
+} ;
+
+
+
+
+
+class cFireChargeEntity :
+ public cProjectileEntity
+{
+ typedef cProjectileEntity super;
+
+public:
+
+ // tolua_end
+
+ CLASS_PROTODEF(cFireChargeEntity);
+
+ cFireChargeEntity(cEntity * a_Creator, double a_X, double a_Y, double a_Z, const Vector3d & a_Speed);
+
+protected:
+
+ void Explode(int a_BlockX, int a_BlockY, int a_BlockZ);
+
+ // cProjectileEntity overrides:
+ virtual void OnHitSolidBlock(const Vector3d & a_HitPos, char a_HitFace) override;
+ virtual void OnHitEntity(cEntity & a_EntityHit, const Vector3d & a_HitPos) override;
+
+ // tolua_begin
+
+} ;
+
+
+
+
+// tolua_end
+
+
+
diff --git a/src/Entities/TNTEntity.cpp b/src/Entities/TNTEntity.cpp
new file mode 100644
index 000000000..339107b2e
--- /dev/null
+++ b/src/Entities/TNTEntity.cpp
@@ -0,0 +1,62 @@
+#include "Globals.h"
+
+#include "TNTEntity.h"
+#include "../World.h"
+#include "../ClientHandle.h"
+
+
+
+
+
+cTNTEntity::cTNTEntity(double a_X, double a_Y, double a_Z, double a_FuseTimeInSec) :
+ super(etTNT, a_X, a_Y, a_Z, 0.98, 0.98),
+ m_Counter(0),
+ m_MaxFuseTime(a_FuseTimeInSec)
+{
+}
+
+
+
+
+
+cTNTEntity::cTNTEntity(const Vector3d & a_Pos, double a_FuseTimeInSec) :
+ super(etTNT, a_Pos.x, a_Pos.y, a_Pos.z, 0.98, 0.98),
+ m_Counter(0),
+ m_MaxFuseTime(a_FuseTimeInSec)
+{
+}
+
+
+
+
+void cTNTEntity::SpawnOn(cClientHandle & a_ClientHandle)
+{
+ a_ClientHandle.SendSpawnObject(*this, 50, 1, 0, 0); // 50 means TNT
+ m_bDirtyPosition = false;
+ m_bDirtySpeed = false;
+ m_bDirtyOrientation = false;
+ m_bDirtyHead = false;
+}
+
+
+
+
+
+void cTNTEntity::Tick(float a_Dt, cChunk & a_Chunk)
+{
+ super::Tick(a_Dt, a_Chunk);
+ BroadcastMovementUpdate();
+ float delta_time = a_Dt / 1000; // Convert miliseconds to seconds
+ m_Counter += delta_time;
+ if (m_Counter > m_MaxFuseTime) // Check if we go KABOOOM
+ {
+ Destroy(true);
+ LOGD("BOOM at {%f,%f,%f}", GetPosX(), GetPosY(), GetPosZ());
+ m_World->DoExplosionAt(4.0, GetPosX() + 0.49, GetPosY() + 0.49, GetPosZ() + 0.49, true, esPrimedTNT, this);
+ return;
+ }
+}
+
+
+
+
diff --git a/src/Entities/TNTEntity.h b/src/Entities/TNTEntity.h
new file mode 100644
index 000000000..eb5040e8a
--- /dev/null
+++ b/src/Entities/TNTEntity.h
@@ -0,0 +1,32 @@
+
+#pragma once
+
+#include "Entity.h"
+
+
+
+
+
+class cTNTEntity :
+ public cEntity
+{
+ typedef cEntity super;
+
+public:
+ CLASS_PROTODEF(cTNTEntity);
+
+ cTNTEntity(double a_X, double a_Y, double a_Z, double a_FuseTimeInSec);
+ cTNTEntity(const Vector3d & a_Pos, double a_FuseTimeInSec);
+
+ // cEntity overrides:
+ virtual void SpawnOn(cClientHandle & a_ClientHandle) override;
+ virtual void Tick(float a_Dt, cChunk & a_Chunk) override;
+
+protected:
+ double m_Counter; ///< How much time has elapsed since the object was created, in seconds
+ double m_MaxFuseTime; ///< How long the fuse is, in seconds
+};
+
+
+
+