<commit_before>﻿
#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 "../Bindings/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_AirLevel( MAX_AIR_LEVEL )
	, m_AirTickTimer(DROWNING_TICKS)
	, m_bVisible(true)
	, m_FoodLevel(MAX_FOOD_LEVEL)
	, m_FoodSaturationLevel(5)
	, m_FoodTickTimer(0)
	, m_FoodExhaustionLevel(0)
	, m_FoodPoisonedTicksRemaining(0)
	, m_LastJumpHeight(0)
	, 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_LastBlockActionTime(0)
	, m_LastBlockActionCnt(0)
	, m_GameMode(eGameMode_NotSet)
	, m_IP("")
	, m_ClientHandle(a_Client)
	, m_NormalMaxSpeed(0.1)
	, m_SprintingMaxSpeed(0.13)
	, m_IsCrouched(false)
	, m_IsSprinting(false)
	, m_IsFlying(false)
	, m_IsSwimming(false)
	, m_IsSubmerged(false)
	, m_IsFishing(false)
	, m_CanFly(false)
	, m_EatingFinishTick(-1)
	, m_LifetimeTotalXp(0)
	, m_CurrentXp(0)
	, m_bDirtyExperience(false)
	, m_IsChargingBow(false)
	, m_BowCharge(0)
	, m_FloaterID(-1)
{
	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);
}





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;
	}
	
	//handle updating experience
	if (m_bDirtyExperience)
	{
		SendExperience();
	}

	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();
	}

	if (IsFlying())
		m_LastGroundHeight = (float)GetPosY();
}





short cPlayer::CalcLevelFromXp(short 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 (short) (151.5 + sqrt( 22952.25 - (14 * (2220 - a_XpTotal)))) / 7;
	}

	//level 16 to 30
	return (short) ( 29.5 + sqrt( 870.25 - (6 * ( 360 - a_XpTotal )))) / 3;
}





short cPlayer::XpForLevel(short a_Level)
{
	//level 0 to 15
	if(a_Level <= 15)
	{
		return a_Level * XP_PER_LEVEL_TO15;
	}

	//level 30+
	if(a_Level >= 31)
	{
		return (short) ( (3.5 * a_Level * a_Level) - (151.5 * a_Level) + 2220 );
	}

	//level 16 to 30
	return (short) ( (1.5 * a_Level * a_Level) - (29.5 * a_Level) + 360 );
}





short cPlayer::GetXpLevel()
{
	return CalcLevelFromXp(m_CurrentXp);
}





float cPlayer::GetXpPercentage()
{
	short int currentLevel = CalcLevelFromXp(m_CurrentXp);
	short int currentLevel_XpBase = XpForLevel(currentLevel);

	return (float)(m_CurrentXp - currentLevel_XpBase) / 
		(float)(XpForLevel(1+currentLevel) - currentLevel_XpBase);
}





bool cPlayer::SetCurrentExperience(short int a_CurrentXp)
{
	if(!(a_CurrentXp >= 0) || (a_CurrentXp > (SHRT_MAX - m_LifetimeTotalXp)))
	{
		LOGWARNING("Tried to update experiece with an invalid Xp value: %d", a_CurrentXp);
		return false; //oops, they gave us a dodgey number
	}

	m_CurrentXp = a_CurrentXp;

	// Set experience to be updated
	m_bDirtyExperience = true;

	return true;
}





short cPlayer::DeltaExperience(short a_Xp_delta)
{
	if (a_Xp_delta > (SHRT_MAX - m_CurrentXp))
	{
		// Value was bad, abort and report
		LOGWARNING("Attempt was made to increment Xp by %d, which overflowed the short datatype. Ignoring.",
			a_Xp_delta);
		return -1; // Should we instead just return the current Xp?
	}

	m_CurrentXp += a_Xp_delta;

	// Make sure they didn't subtract too much
	if (m_CurrentXp < 0)
	{
		m_CurrentXp = 0;
	}

	// Update total for score calculation
	if (a_Xp_delta > 0)
	{
		m_LifetimeTotalXp += a_Xp_delta;
	}

	LOGD("Player \"%s\" gained/lost %d experience, total is now: %d", 
		m_PlayerName.c_str(), a_Xp_delta, m_CurrentXp);

	// Set experience to be updated
	m_bDirtyExperience = true;

	return m_CurrentXp;
}





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)
		{
			if (!IsGameModeCreative())
			{
				TakeDamage(dtFalling, NULL, Damage, Damage, 0);
			}
			
			GetWorld()->BroadcastSoundParticleEffect(
				2006,
				(int)GetPosX(), (int)GetPosY() - 1, (int)GetPosZ(),
				Damage // Used as particle effect speed modifier
				);
		}		

		m_LastGroundHeight = (float)GetPosY();
	}
}





void cPlayer::Heal(int a_Health)
{
	super::Heal(a_Health);
	SendHealth();
}





void cPlayer::SetFoodLevel(int a_FoodLevel)
{<commit_msg>Improved player fall particle positions<commit_after>﻿
#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 "../Bindings/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_AirLevel( MAX_AIR_LEVEL )
	, m_AirTickTimer(DROWNING_TICKS)
	, m_bVisible(true)
	, m_FoodLevel(MAX_FOOD_LEVEL)
	, m_FoodSaturationLevel(5)
	, m_FoodTickTimer(0)
	, m_FoodExhaustionLevel(0)
	, m_FoodPoisonedTicksRemaining(0)
	, m_LastJumpHeight(0)
	, 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_LastBlockActionTime(0)
	, m_LastBlockActionCnt(0)
	, m_GameMode(eGameMode_NotSet)
	, m_IP("")
	, m_ClientHandle(a_Client)
	, m_NormalMaxSpeed(0.1)
	, m_SprintingMaxSpeed(0.13)
	, m_IsCrouched(false)
	, m_IsSprinting(false)
	, m_IsFlying(false)
	, m_IsSwimming(false)
	, m_IsSubmerged(false)
	, m_IsFishing(false)
	, m_CanFly(false)
	, m_EatingFinishTick(-1)
	, m_LifetimeTotalXp(0)
	, m_CurrentXp(0)
	, m_bDirtyExperience(false)
	, m_IsChargingBow(false)
	, m_BowCharge(0)
	, m_FloaterID(-1)
{
	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);
}





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;
	}
	
	//handle updating experience
	if (m_bDirtyExperience)
	{
		SendExperience();
	}

	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();
	}

	if (IsFlying())
		m_LastGroundHeight = (float)GetPosY();
}





short cPlayer::CalcLevelFromXp(short 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 (short) (151.5 + sqrt( 22952.25 - (14 * (2220 - a_XpTotal)))) / 7;
	}

	//level 16 to 30
	return (short) ( 29.5 + sqrt( 870.25 - (6 * ( 360 - a_XpTotal )))) / 3;
}





short cPlayer::XpForLevel(short a_Level)
{
	//level 0 to 15
	if(a_Level <= 15)
	{
		return a_Level * XP_PER_LEVEL_TO15;
	}

	//level 30+
	if(a_Level >= 31)
	{
		return (short) ( (3.5 * a_Level * a_Level) - (151.5 * a_Level) + 2220 );
	}

	//level 16 to 30
	return (short) ( (1.5 * a_Level * a_Level) - (29.5 * a_Level) + 360 );
}





short cPlayer::GetXpLevel()
{
	return CalcLevelFromXp(m_CurrentXp);
}





float cPlayer::GetXpPercentage()
{
	short int currentLevel = CalcLevelFromXp(m_CurrentXp);
	short int currentLevel_XpBase = XpForLevel(currentLevel);

	return (float)(m_CurrentXp - currentLevel_XpBase) / 
		(float)(XpForLevel(1+currentLevel) - currentLevel_XpBase);
}





bool cPlayer::SetCurrentExperience(short int a_CurrentXp)
{
	if(!(a_CurrentXp >= 0) || (a_CurrentXp > (SHRT_MAX - m_LifetimeTotalXp)))
	{
		LOGWARNING("Tried to update experiece with an invalid Xp value: %d", a_CurrentXp);
		return false; //oops, they gave us a dodgey number
	}

	m_CurrentXp = a_CurrentXp;

	// Set experience to be updated
	m_bDirtyExperience = true;

	return true;
}





short cPlayer::DeltaExperience(short a_Xp_delta)
{
	if (a_Xp_delta > (SHRT_MAX - m_CurrentXp))
	{
		// Value was bad, abort and report
		LOGWARNING("Attempt was made to increment Xp by %d, which overflowed the short datatype. Ignoring.",
			a_Xp_delta);
		return -1; // Should we instead just return the current Xp?
	}

	m_CurrentXp += a_Xp_delta;

	// Make sure they didn't subtract too much
	if (m_CurrentXp < 0)
	{
		m_CurrentXp = 0;
	}

	// Update total for score calculation
	if (a_Xp_delta > 0)
	{
		m_LifetimeTotalXp += a_Xp_delta;
	}

	LOGD("Player \"%s\" gained/lost %d experience, total is now: %d", 
		m_PlayerName.c_str(), a_Xp_delta, m_CurrentXp);

	// Set experience to be updated
	m_bDirtyExperience = true;

	return m_CurrentXp;
}





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)
		{
			if (!IsGameModeCreative())
			{
				TakeDamage(dtFalling, NULL, Damage, Damage, 0);
			}
			
			// Apparently, Mojang changed player positions to always be rounded up. Normally, it doesn't affect much, but we need effect positions to be precise, so ceil()
			GetWorld()->BroadcastSoundParticleEffect(2006, (int)floor(GetPosX()), (int)GetPosY() - 1, (int)floor(GetPosZ()), Damage /* Used as particle effect speed modifier */);
		}		

		m_LastGroundHeight = (float)GetPosY();
	}
}





void cPlayer::Heal(int a_Health)
{
	super::Heal(a_Health);
	SendHealth();
}





void cPlayer::SetFoodLevel(int a_FoodLevel)
{
{'fraction_non_alphanumeric': 0.14775592536560767, 'fraction_numerical': 0.01583459404942007, 'mean_word_length': 4.288266666666667, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 73, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5181708', 'n_tokens_mistral': 8612, 'n_tokens_neox': 7746, 'n_words': 1534}