1
0
Fork 0
cuberite-2a/src/Entities/Player.cpp

3277 lines
66 KiB
C++
Raw Normal View History

2014-06-16 19:57:23 +00:00
#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules
#include "Player.h"
#include "../Mobs/Wolf.h"
#include "../Mobs/Horse.h"
#include "../BoundingBox.h"
#include "../ChatColor.h"
#include "../Server.h"
2014-12-13 14:06:55 +00:00
#include "../UI/InventoryWindow.h"
#include "../UI/WindowOwner.h"
#include "../Bindings/PluginManager.h"
#include "../BlockEntities/BlockEntity.h"
2014-06-29 10:36:38 +00:00
#include "../BlockEntities/EnderChestEntity.h"
#include "../Root.h"
#include "../Chunk.h"
#include "../Items/ItemHandler.h"
#include "../FastRandom.h"
#include "../ClientHandle.h"
2021-05-04 15:11:56 +00:00
#include "../WorldStorage/StatisticsSerializer.h"
#include "../CompositeChat.h"
2014-05-11 11:57:06 +00:00
#include "../Blocks/BlockHandler.h"
#include "../Blocks/BlockSlab.h"
#include "../Blocks/ChunkInterface.h"
2014-10-23 13:15:10 +00:00
#include "../IniFile.h"
#include "../JsonUtils.h"
#include "json/json.h"
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
#include "../CraftingRecipes.h"
namespace
{
/** Returns the folder for the player data based on the UUID given.
This can be used both for online and offline UUIDs. */
AString GetUUIDFolderName(const cUUID & a_Uuid)
{
AString UUID = a_Uuid.ToShortString();
2020-05-07 19:14:00 +00:00
AString res("players/");
res.append(UUID, 0, 2);
res.push_back('/');
return res;
}
} // namespace (anonymous)
const int cPlayer::MAX_HEALTH = 20;
const int cPlayer::MAX_FOOD_LEVEL = 20;
// Number of ticks it takes to eat an item.
#define EATING_TICKS 30_tick
// 6000 ticks or 5 minutes
#define PLAYER_INVENTORY_SAVE_INTERVAL 6000
#define XP_TO_LEVEL15 255
#define XP_PER_LEVEL_TO15 17
#define XP_TO_LEVEL30 825
2013-11-13 13:50:47 +00:00
cPlayer::BodyStanceCrouching::BodyStanceCrouching(cPlayer & a_Player)
{
a_Player.SetSize(0.6f, 1.65f);
}
cPlayer::BodyStanceSleeping::BodyStanceSleeping(cPlayer & a_Player)
{
a_Player.SetSize(0.2f, 0.2f);
}
cPlayer::BodyStanceStanding::BodyStanceStanding(cPlayer & a_Player)
{
a_Player.SetSize(0.6f, 1.8f);
}
cPlayer::BodyStanceGliding::BodyStanceGliding(cPlayer & a_Player) :
TicksElytraFlying(0)
{
a_Player.SetSize(0.6f, 0.6f);
}
cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
2021-04-06 15:09:16 +00:00
Super(etPlayer, 0.6f, 1.8f),
m_BodyStance(BodyStanceStanding(*this)),
m_FoodLevel(MAX_FOOD_LEVEL),
m_FoodSaturationLevel(5.0),
m_FoodTickTimer(0),
m_FoodExhaustionLevel(0.0),
m_Inventory(*this),
m_EnderChestContents(9, 3),
m_DefaultWorldPath(cRoot::Get()->GetDefaultWorld()->GetDataPath()),
m_GameMode(eGameMode_NotSet),
m_ClientHandle(a_Client),
m_NormalMaxSpeed(1.0),
m_SprintingMaxSpeed(1.3),
m_FlyingMaxSpeed(1.0),
m_IsChargingBow(false),
m_IsFishing(false),
m_IsFlightCapable(false),
m_IsFlying(false),
m_IsFrozen(false),
m_IsLeftHanded(false),
m_IsTeleporting(false),
m_IsVisible(true),
m_EatingFinishTick(-1),
m_LifetimeTotalXp(0),
m_CurrentXp(0),
m_BowCharge(0),
m_FloaterID(cEntity::INVALID_ID),
2014-10-20 20:55:07 +00:00
m_Team(nullptr),
m_Spectating(nullptr),
m_TicksUntilNextSave(PLAYER_INVENTORY_SAVE_INTERVAL),
m_SkinParts(0)
{
ASSERT(GetName().length() <= 16); // Otherwise this player could crash many clients...
m_InventoryWindow = new cInventoryWindow(*this);
m_CurrentWindow = m_InventoryWindow;
m_InventoryWindow->OpenedByPlayer(*this);
2013-08-01 07:51:25 +00:00
SetMaxHealth(MAX_HEALTH);
m_Health = MAX_HEALTH;
LoadFromDisk();
UpdateCapabilities();
m_LastGroundHeight = static_cast<float>(GetPosY());
}
2021-02-06 18:37:03 +00:00
cPlayer::~cPlayer(void)
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
{
2021-02-06 18:37:03 +00:00
LOGD("Deleting cPlayer \"%s\" at %p, ID %d", GetName().c_str(), static_cast<void *>(this), GetUniqueID());
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
2021-02-06 18:37:03 +00:00
// "Times ragequit":
m_Stats.Custom[CustomStatistic::LeaveGame]++;
2021-02-06 18:37:03 +00:00
SaveToDisk();
delete m_InventoryWindow;
LOGD("Player %p deleted", static_cast<void *>(this));
}
int cPlayer::CalcLevelFromXp(int a_XpTotal)
{
// level 0 to 15
if (a_XpTotal <= XP_TO_LEVEL15)
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
{
2021-02-06 18:37:03 +00:00
return a_XpTotal / XP_PER_LEVEL_TO15;
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
}
2021-02-06 18:37:03 +00:00
// level 30+
if (a_XpTotal > XP_TO_LEVEL30)
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
{
2021-02-06 18:37:03 +00:00
return static_cast<int>((151.5 + sqrt( 22952.25 - (14 * (2220 - a_XpTotal)))) / 7);
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
}
2021-02-06 18:37:03 +00:00
// level 16 to 30
return static_cast<int>((29.5 + sqrt( 870.25 - (6 * ( 360 - a_XpTotal)))) / 3);
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
}
2021-02-06 18:37:03 +00:00
const std::set<UInt32> & cPlayer::GetKnownRecipes() const
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
{
2021-02-06 18:37:03 +00:00
return m_KnownRecipes;
}
int cPlayer::XpForLevel(int a_Level)
{
// level 0 to 15
if (a_Level <= 15)
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
{
2021-02-06 18:37:03 +00:00
return a_Level * XP_PER_LEVEL_TO15;
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
}
2021-02-06 18:37:03 +00:00
// level 30+
if (a_Level >= 31)
{
return static_cast<int>((3.5 * a_Level * a_Level) - (151.5 * a_Level) + 2220);
}
// level 16 to 30
return static_cast<int>((1.5 * a_Level * a_Level) - (29.5 * a_Level) + 360);
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
}
2021-02-06 18:37:03 +00:00
int cPlayer::GetXpLevel() const
{
2021-02-06 18:37:03 +00:00
return CalcLevelFromXp(m_CurrentXp);
}
2021-02-06 18:37:03 +00:00
float cPlayer::GetXpPercentage() const
{
int currentLevel = CalcLevelFromXp(m_CurrentXp);
int currentLevel_XpBase = XpForLevel(currentLevel);
return static_cast<float>(m_CurrentXp - currentLevel_XpBase) /
static_cast<float>(XpForLevel(1 + currentLevel) - currentLevel_XpBase);
}
2021-02-06 18:37:03 +00:00
bool cPlayer::SetCurrentExperience(int a_CurrentXp)
{
2021-02-06 18:37:03 +00:00
if (!(a_CurrentXp >= 0) || (a_CurrentXp > (std::numeric_limits<int>::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
}
2021-02-06 18:37:03 +00:00
m_CurrentXp = a_CurrentXp;
2021-02-06 18:37:03 +00:00
// Update experience:
m_ClientHandle->SendExperience();
2021-02-06 18:37:03 +00:00
return true;
}
2021-02-06 18:37:03 +00:00
int cPlayer::DeltaExperience(int a_Xp_delta)
{
if (a_Xp_delta > (std::numeric_limits<int>().max() - m_CurrentXp))
{
// Value was bad, abort and report
LOGWARNING("Attempt was made to increment Xp by %d, which overflowed the int datatype. Ignoring.", a_Xp_delta);
return -1; // Should we instead just return the current Xp?
}
2021-02-06 18:37:03 +00:00
m_CurrentXp += a_Xp_delta;
2021-02-06 18:37:03 +00:00
// Make sure they didn't subtract too much
m_CurrentXp = std::max(m_CurrentXp, 0);
2021-02-06 18:37:03 +00:00
// Update total for score calculation
if (a_Xp_delta > 0)
{
m_LifetimeTotalXp += a_Xp_delta;
}
2021-02-06 18:37:03 +00:00
LOGD("Player \"%s\" gained / lost %d experience, total is now: %d", GetName().c_str(), a_Xp_delta, m_CurrentXp);
// Set experience to be updated:
m_ClientHandle->SendExperience();
return m_CurrentXp;
}
2021-02-06 18:37:03 +00:00
void cPlayer::StartChargingBow(void)
{
2021-02-06 18:37:03 +00:00
LOGD("Player \"%s\" started charging their bow", GetName().c_str());
m_IsChargingBow = true;
m_BowCharge = 0;
m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get());
}
2021-02-06 18:37:03 +00:00
int cPlayer::FinishChargingBow(void)
{
LOGD("Player \"%s\" finished charging their bow at a charge of %d", GetName().c_str(), m_BowCharge);
int res = m_BowCharge;
m_IsChargingBow = false;
m_BowCharge = 0;
m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get());
2021-02-06 18:37:03 +00:00
return res;
}
2021-02-06 18:37:03 +00:00
void cPlayer::CancelChargingBow(void)
{
LOGD("Player \"%s\" cancelled charging their bow at a charge of %d", GetName().c_str(), m_BowCharge);
m_IsChargingBow = false;
m_BowCharge = 0;
m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get());
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetTouchGround(bool a_bTouchGround)
{
2021-02-06 18:37:03 +00:00
if (IsGameModeSpectator()) // You can fly through the ground in Spectator
{
return;
}
2021-02-06 18:37:03 +00:00
UNUSED(a_bTouchGround);
/* Unfortunately whatever the reason, there are still desyncs in on-ground status between the client and server. For example:
1. Walking off a ledge (whatever height)
2. Initial login
Thus, it is too risky to compare their value against ours and kick them for hacking */
}
2021-02-06 18:37:03 +00:00
void cPlayer::Heal(int a_Health)
{
Super::Heal(a_Health);
m_ClientHandle->SendHealth();
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetFoodLevel(int a_FoodLevel)
{
2021-02-06 18:37:03 +00:00
int FoodLevel = Clamp(a_FoodLevel, 0, MAX_FOOD_LEVEL);
2021-02-06 18:37:03 +00:00
if (cRoot::Get()->GetPluginManager()->CallHookPlayerFoodLevelChange(*this, FoodLevel))
2015-06-30 14:50:15 +00:00
{
2021-02-06 18:37:03 +00:00
m_FoodSaturationLevel = 5.0;
return;
2015-06-30 14:50:15 +00:00
}
2014-05-12 18:38:52 +00:00
2021-02-06 18:37:03 +00:00
m_FoodLevel = FoodLevel;
m_ClientHandle->SendHealth();
}
2016-04-05 08:45:09 +00:00
2016-10-12 12:38:45 +00:00
2021-02-06 18:37:03 +00:00
void cPlayer::SetFoodSaturationLevel(double a_FoodSaturationLevel)
{
m_FoodSaturationLevel = Clamp(a_FoodSaturationLevel, 0.0, static_cast<double>(m_FoodLevel));
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetFoodTickTimer(int a_FoodTickTimer)
{
m_FoodTickTimer = a_FoodTickTimer;
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetFoodExhaustionLevel(double a_FoodExhaustionLevel)
{
m_FoodExhaustionLevel = Clamp(a_FoodExhaustionLevel, 0.0, 40.0);
}
2014-02-17 14:27:12 +00:00
2013-12-22 20:03:09 +00:00
2021-02-06 18:37:03 +00:00
bool cPlayer::Feed(int a_Food, double a_Saturation)
{
if (IsSatiated())
{
2021-02-06 18:37:03 +00:00
return false;
}
2021-02-06 18:37:03 +00:00
SetFoodSaturationLevel(m_FoodSaturationLevel + a_Saturation);
SetFoodLevel(m_FoodLevel + a_Food);
return true;
}
2021-02-06 18:37:03 +00:00
void cPlayer::AddFoodExhaustion(double a_Exhaustion)
{
2021-02-06 18:37:03 +00:00
if (!(IsGameModeCreative() || IsGameModeSpectator()))
{
2021-02-06 18:37:03 +00:00
m_FoodExhaustionLevel = std::min(m_FoodExhaustionLevel + a_Exhaustion, 40.0);
}
}
bool cPlayer::IsInBed(void) const
{
return std::holds_alternative<BodyStanceSleeping>(m_BodyStance);
}
bool cPlayer::IsLeftHanded() const
{
return m_IsLeftHanded;
}
bool cPlayer::IsStanding() const
{
return std::holds_alternative<BodyStanceStanding>(m_BodyStance);
}
2021-02-06 18:37:03 +00:00
void cPlayer::TossItems(const cItems & a_Items)
2013-11-13 17:25:47 +00:00
{
2021-02-06 18:37:03 +00:00
if (IsGameModeSpectator()) // Players can't toss items in spectator
2013-11-13 17:25:47 +00:00
{
2021-02-06 18:37:03 +00:00
return;
2013-11-13 17:25:47 +00:00
}
m_Stats.Custom[CustomStatistic::Drop] += static_cast<StatisticsManager::StatValue>(a_Items.Size());
2013-11-13 17:25:47 +00:00
2021-02-06 18:37:03 +00:00
const auto Speed = (GetLookVector() + Vector3d(0, 0.2, 0)) * 6; // A dash of height and a dollop of speed
const auto Position = GetEyePosition() - Vector3d(0, 0.2, 0); // Correct for eye-height weirdness
m_World->SpawnItemPickups(a_Items, Position, Speed, true); // 'true' because created by player
2013-11-13 17:25:47 +00:00
}
void cPlayer::SetIsInBed(const bool a_GoToBed)
{
if (a_GoToBed && IsStanding())
{
m_BodyStance = BodyStanceSleeping(*this);
m_World->BroadcastEntityAnimation(*this, EntityAnimation::PlayerEntersBed);
}
else if (!a_GoToBed && IsInBed())
{
m_BodyStance = BodyStanceStanding(*this);
m_World->BroadcastEntityAnimation(*this, EntityAnimation::PlayerLeavesBed);
}
}
2021-02-06 18:37:03 +00:00
void cPlayer::StartEating(void)
{
2021-02-06 18:37:03 +00:00
// Set the timer:
m_EatingFinishTick = m_World->GetWorldAge() + EATING_TICKS;
2021-04-09 22:17:01 +00:00
// Send the packet:
2021-02-06 18:37:03 +00:00
m_World->BroadcastEntityMetadata(*this);
}
2021-02-06 18:37:03 +00:00
void cPlayer::FinishEating(void)
2013-11-13 17:25:47 +00:00
{
2021-02-06 18:37:03 +00:00
// Reset the timer:
m_EatingFinishTick = -1_tick;
2013-11-13 17:25:47 +00:00
2021-02-06 18:37:03 +00:00
// Send the packets:
2021-04-09 22:17:01 +00:00
m_ClientHandle->SendEntityAnimation(*this, EntityAnimation::PlayerFinishesEating);
2021-02-06 18:37:03 +00:00
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))
2013-11-13 17:25:47 +00:00
{
2021-02-06 18:37:03 +00:00
return;
2013-11-13 17:25:47 +00:00
}
2021-02-06 18:37:03 +00:00
ItemHandler->OnFoodEaten(m_World, this, &Item);
2013-11-13 17:25:47 +00:00
}
2021-02-06 18:37:03 +00:00
void cPlayer::AbortEating(void)
2013-11-13 17:25:47 +00:00
{
m_EatingFinishTick = -1_tick;
2021-02-06 18:37:03 +00:00
m_World->BroadcastEntityMetadata(*this);
2013-11-13 17:25:47 +00:00
}
2021-02-06 18:37:03 +00:00
void cPlayer::ClearInventoryPaintSlots(void)
2013-11-13 17:25:47 +00:00
{
2021-02-06 18:37:03 +00:00
// Clear the list of slots that are being inventory-painted. Used by cWindow only
m_InventoryPaintSlots.clear();
2013-11-13 17:25:47 +00:00
}
2021-02-06 18:37:03 +00:00
void cPlayer::AddInventoryPaintSlot(int a_SlotNum)
2013-11-13 13:50:47 +00:00
{
2021-02-06 18:37:03 +00:00
// Add a slot to the list for inventory painting. Used by cWindow only
m_InventoryPaintSlots.push_back(a_SlotNum);
}
2013-11-13 13:50:47 +00:00
2021-02-06 18:37:03 +00:00
const cSlotNums & cPlayer::GetInventoryPaintSlots(void) const
{
// Return the list of slots currently stored for inventory painting. Used by cWindow only
return m_InventoryPaintSlots;
2013-11-13 13:50:47 +00:00
}
2021-02-06 18:37:03 +00:00
double cPlayer::GetMaxSpeed(void) const
2013-11-13 13:50:47 +00:00
{
if (IsFlying())
2013-11-13 13:50:47 +00:00
{
2021-02-06 18:37:03 +00:00
return m_FlyingMaxSpeed;
2013-11-13 13:50:47 +00:00
}
else if (IsSprinting())
2013-11-16 10:38:57 +00:00
{
2021-02-06 18:37:03 +00:00
return m_SprintingMaxSpeed;
}
else
{
return m_NormalMaxSpeed;
2013-11-16 10:38:57 +00:00
}
2021-02-06 18:37:03 +00:00
}
2013-11-16 10:38:57 +00:00
2021-02-06 18:37:03 +00:00
void cPlayer::SetNormalMaxSpeed(double a_Speed)
{
2021-02-06 18:37:03 +00:00
m_NormalMaxSpeed = a_Speed;
if (!m_IsFrozen)
2021-02-06 18:37:03 +00:00
{
// If we are frozen, we do not send this yet. We send when unfreeze() is called
m_World->BroadcastEntityProperties(*this);
2021-02-06 18:37:03 +00:00
}
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetSprintingMaxSpeed(double a_Speed)
{
2021-02-06 18:37:03 +00:00
m_SprintingMaxSpeed = a_Speed;
if (!m_IsFrozen)
2021-02-06 18:37:03 +00:00
{
// If we are frozen, we do not send this yet. We send when unfreeze() is called
m_World->BroadcastEntityProperties(*this);
2021-02-06 18:37:03 +00:00
}
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetFlyingMaxSpeed(double a_Speed)
{
2021-02-06 18:37:03 +00:00
m_FlyingMaxSpeed = a_Speed;
if (!m_IsFrozen)
{
// If we are frozen, we do not send this yet. We send when unfreeze() is called
m_ClientHandle->SendPlayerAbilities();
}
}
void cPlayer::SetCrouch(const bool a_ShouldCrouch)
{
if (a_ShouldCrouch && IsStanding())
2014-09-17 15:15:47 +00:00
{
m_BodyStance = BodyStanceCrouching(*this);
// Handle spectator mode detach:
if (IsGameModeSpectator())
{
SpectateEntity(nullptr);
}
cRoot::Get()->GetPluginManager()->CallHookPlayerCrouched(*this);
2014-09-17 15:15:47 +00:00
}
else if (!a_ShouldCrouch && IsCrouched())
{
m_BodyStance = BodyStanceStanding(*this);
}
m_World->BroadcastEntityMetadata(*this);
}
void cPlayer::SetElytraFlight(const bool a_ShouldElytraFly)
{
if (a_ShouldElytraFly && IsStanding() && !IsOnGround() && !IsInWater() && !IsRiding() && (GetEquippedChestplate().m_ItemType == E_ITEM_ELYTRA))
2021-02-06 18:37:03 +00:00
{
m_BodyStance = BodyStanceGliding(*this);
}
else if (!a_ShouldElytraFly && IsElytraFlying())
{
m_BodyStance = BodyStanceStanding(*this);
2021-02-06 18:37:03 +00:00
}
2021-02-06 18:37:03 +00:00
m_World->BroadcastEntityMetadata(*this);
}
void cPlayer::SetFlying(const bool a_ShouldFly)
{
if (a_ShouldFly == m_IsFlying)
{
return;
}
m_IsFlying = a_ShouldFly;
if (!m_IsFrozen)
{
// If we are frozen, we do not send this yet. We send when unfreeze() is called
m_ClientHandle->SendPlayerAbilities();
}
}
void cPlayer::SetLeftHanded(const bool a_IsLeftHanded)
{
m_IsLeftHanded = a_IsLeftHanded;
m_World->BroadcastEntityMetadata(*this);
}
void cPlayer::SetSprint(const bool a_ShouldSprint)
{
if (a_ShouldSprint && IsStanding())
{
m_BodyStance = BodyStanceSprinting();
}
else if (!a_ShouldSprint && IsSprinting())
{
m_BodyStance = BodyStanceStanding(*this);
}
m_World->BroadcastEntityMetadata(*this);
m_World->BroadcastEntityProperties(*this);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetCanFly(bool a_CanFly)
{
if (a_CanFly == m_IsFlightCapable)
2021-02-06 18:37:03 +00:00
{
return;
}
m_IsFlightCapable = a_CanFly;
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendPlayerAbilities();
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetCustomName(const AString & a_CustomName)
{
2021-02-06 18:37:03 +00:00
if (m_CustomName == a_CustomName)
{
return;
}
2021-02-06 18:37:03 +00:00
m_World->BroadcastPlayerListRemovePlayer(*this);
2021-02-06 18:37:03 +00:00
m_CustomName = a_CustomName;
if (m_CustomName.length() > 16)
{
2021-02-06 18:37:03 +00:00
m_CustomName = m_CustomName.substr(0, 16);
}
2021-02-06 18:37:03 +00:00
m_World->BroadcastPlayerListAddPlayer(*this);
m_World->BroadcastSpawnEntity(*this, m_ClientHandle.get());
}
void cPlayer::SetBedPos(const Vector3i a_Position)
{
m_LastBedPos = a_Position;
2021-02-06 18:37:03 +00:00
m_SpawnWorldName = m_World->GetName();
}
void cPlayer::SetBedPos(const Vector3i a_Position, const cWorld & a_World)
{
m_LastBedPos = a_Position;
m_SpawnWorldName = a_World.GetName();
}
2021-02-06 18:37:03 +00:00
cWorld * cPlayer::GetBedWorld()
{
2021-02-06 18:37:03 +00:00
if (const auto World = cRoot::Get()->GetWorld(m_SpawnWorldName); World != nullptr)
{
return World;
}
2021-02-06 18:37:03 +00:00
return cRoot::Get()->GetDefaultWorld();
}
2021-02-06 18:37:03 +00:00
void cPlayer::NotifyNearbyWolves(cPawn * a_Opponent, bool a_IsPlayerInvolved)
{
2021-02-06 18:37:03 +00:00
ASSERT(a_Opponent != nullptr);
2021-02-06 18:37:03 +00:00
m_World->ForEachEntityInBox(cBoundingBox(GetPosition(), 16), [&] (cEntity & a_Entity)
{
if (a_Entity.IsMob())
{
auto & Mob = static_cast<cMonster&>(a_Entity);
if (Mob.GetMobType() == mtWolf)
{
auto & Wolf = static_cast<cWolf&>(Mob);
Wolf.ReceiveNearbyFightInfo(GetUUID(), a_Opponent, a_IsPlayerInvolved);
}
}
return false;
}
);
}
2021-02-06 18:37:03 +00:00
void cPlayer::KilledBy(TakeDamageInfo & a_TDI)
{
2021-02-06 18:37:03 +00:00
Super::KilledBy(a_TDI);
2021-02-06 18:37:03 +00:00
if (m_Health > 0)
{
return; // not dead yet =]
}
m_IsVisible = false; // So new clients don't see the player
2021-02-06 18:37:03 +00:00
// Detach player from object / entity. If the player dies, the server still says
// that the player is attached to the entity / object
Detach();
2021-02-06 18:37:03 +00:00
// Puke out all the items
cItems Pickups;
m_Inventory.CopyToItems(Pickups);
m_Inventory.Clear();
2021-02-06 18:37:03 +00:00
if (GetName() == "Notch")
2014-03-20 15:14:40 +00:00
{
2021-02-06 18:37:03 +00:00
Pickups.Add(cItem(E_ITEM_RED_APPLE));
2014-03-20 15:14:40 +00:00
}
m_Stats.Custom[CustomStatistic::Drop] += static_cast<StatisticsManager::StatValue>(Pickups.Size());
2021-02-06 18:37:03 +00:00
m_World->SpawnItemPickups(Pickups, GetPosX(), GetPosY(), GetPosZ(), 10);
SaveToDisk(); // Save it, yeah the world is a tough place !
BroadcastDeathMessage(a_TDI);
2021-02-06 18:37:03 +00:00
m_Stats.Custom[CustomStatistic::Deaths]++;
m_Stats.Custom[CustomStatistic::TimeSinceDeath] = 0;
2021-02-06 18:37:03 +00:00
m_World->GetScoreBoard().AddPlayerScore(GetName(), cObjective::otDeathCount, 1);
}
2021-09-18 08:18:05 +00:00
void cPlayer::Killed(const cEntity & a_Victim, eDamageType a_DamageType)
{
2021-02-06 18:37:03 +00:00
cScoreboard & ScoreBoard = m_World->GetScoreBoard();
2021-09-18 08:18:05 +00:00
if (a_Victim.IsPlayer())
{
m_Stats.Custom[CustomStatistic::PlayerKills]++;
2021-02-06 18:37:03 +00:00
ScoreBoard.AddPlayerScore(GetName(), cObjective::otPlayerKillCount, 1);
}
2021-09-18 08:18:05 +00:00
else if (a_Victim.IsMob())
2021-02-06 18:37:03 +00:00
{
2021-09-18 08:18:05 +00:00
const auto & Monster = static_cast<const cMonster &>(a_Victim);
if (Monster.GetMobFamily() == cMonster::mfHostile)
2021-02-06 18:37:03 +00:00
{
AwardAchievement(CustomStatistic::AchKillEnemy);
2021-02-06 18:37:03 +00:00
}
2021-09-18 08:18:05 +00:00
if ((Monster.GetMobType() == eMonsterType::mtSkeleton) && (a_DamageType == eDamageType::dtRangedAttack))
{
const double DistX = GetPosX() - Monster.GetPosX();
const double DistZ = GetPosZ() - Monster.GetPosZ();
if ((DistX * DistX + DistZ * DistZ) >= 2500.0)
{
AwardAchievement(CustomStatistic::AchSnipeSkeleton);
}
}
m_Stats.Custom[CustomStatistic::MobKills]++;
}
2021-02-06 18:37:03 +00:00
ScoreBoard.AddPlayerScore(GetName(), cObjective::otTotalKillCount, 1);
}
2021-02-06 18:37:03 +00:00
void cPlayer::Respawn(void)
2014-03-20 15:14:40 +00:00
{
2021-02-06 18:37:03 +00:00
ASSERT(m_World != nullptr);
2014-03-20 15:14:40 +00:00
2021-02-06 18:37:03 +00:00
m_Health = GetMaxHealth();
SetInvulnerableTicks(20);
2014-03-20 15:14:40 +00:00
2021-02-06 18:37:03 +00:00
// Reset food level:
m_FoodLevel = MAX_FOOD_LEVEL;
m_FoodSaturationLevel = 5.0;
m_FoodExhaustionLevel = 0.0;
2014-03-20 15:14:40 +00:00
2021-02-06 18:37:03 +00:00
// Reset Experience
m_CurrentXp = 0;
m_LifetimeTotalXp = 0;
// ToDo: send score to client? How?
2014-03-20 15:14:40 +00:00
2021-02-06 18:37:03 +00:00
// Extinguish the fire:
StopBurning();
2014-03-20 15:14:40 +00:00
2021-02-06 18:37:03 +00:00
if (const auto BedWorld = GetBedWorld(); m_World != BedWorld)
{
2021-02-06 18:37:03 +00:00
MoveToWorld(*BedWorld, GetLastBedPos(), false, false);
}
2021-02-06 18:37:03 +00:00
else
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendRespawn(m_World->GetDimension(), true);
TeleportToCoords(GetLastBedPos().x, GetLastBedPos().y, GetLastBedPos().z);
}
2021-02-06 18:37:03 +00:00
SetVisible(true);
}
2021-02-06 18:37:03 +00:00
double cPlayer::GetEyeHeight(void) const
{
return GetEyePosition().y - GetPosY();
}
2021-02-06 18:37:03 +00:00
Vector3d cPlayer::GetEyePosition(void) const
{
if (IsCrouched())
{
return GetPosition().addedY(1.54);
}
if (IsElytraFlying())
{
return GetPosition().addedY(0.4);
}
if (IsInBed())
{
return GetPosition().addedY(0.2);
}
return GetPosition().addedY(1.6);
}
2021-02-06 18:37:03 +00:00
bool cPlayer::IsGameModeCreative(void) const
2014-09-02 17:12:35 +00:00
{
2021-02-06 18:37:03 +00:00
return (GetEffectiveGameMode() == gmCreative);
}
2014-09-26 15:37:19 +00:00
2014-09-02 17:12:35 +00:00
2021-02-06 18:37:03 +00:00
bool cPlayer::IsGameModeSurvival(void) const
{
return (GetEffectiveGameMode() == gmSurvival);
2014-09-02 17:12:35 +00:00
}
2021-02-06 18:37:03 +00:00
bool cPlayer::IsGameModeAdventure(void) const
2016-03-24 17:13:23 +00:00
{
2021-02-06 18:37:03 +00:00
return (GetEffectiveGameMode() == gmAdventure);
2016-03-24 17:13:23 +00:00
}
2021-02-06 18:37:03 +00:00
bool cPlayer::IsGameModeSpectator(void) const
2016-03-24 17:13:23 +00:00
{
2021-02-06 18:37:03 +00:00
return (GetEffectiveGameMode() == gmSpectator);
2016-03-24 17:13:23 +00:00
}
2021-02-06 18:37:03 +00:00
bool cPlayer::CanMobsTarget(void) const
{
2021-02-06 18:37:03 +00:00
return (IsGameModeSurvival() || IsGameModeAdventure()) && (m_Health > 0);
}
2021-02-06 18:37:03 +00:00
AString cPlayer::GetIP(void) const
{
return m_ClientHandle->GetIPString();
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetTeam(cTeam * a_Team)
{
2021-02-06 18:37:03 +00:00
if (m_Team == a_Team)
{
return;
}
2021-02-06 18:37:03 +00:00
if (m_Team)
2016-04-05 08:45:09 +00:00
{
2021-02-06 18:37:03 +00:00
m_Team->RemovePlayer(GetName());
}
m_Team = a_Team;
if (m_Team)
{
m_Team->AddPlayer(GetName());
2016-04-05 08:45:09 +00:00
}
}
2021-02-06 18:37:03 +00:00
cTeam * cPlayer::UpdateTeam(void)
{
2021-02-06 18:37:03 +00:00
if (m_World == nullptr)
{
2021-02-06 18:37:03 +00:00
SetTeam(nullptr);
}
else
{
cScoreboard & Scoreboard = m_World->GetScoreBoard();
SetTeam(Scoreboard.QueryPlayerTeam(GetName()));
}
2021-02-06 18:37:03 +00:00
return m_Team;
}
2021-02-06 18:37:03 +00:00
void cPlayer::OpenWindow(cWindow & a_Window)
{
2021-02-06 18:37:03 +00:00
if (cRoot::Get()->GetPluginManager()->CallHookPlayerOpeningWindow(*this, a_Window))
{
2021-02-06 18:37:03 +00:00
return;
}
2014-01-19 12:20:57 +00:00
2021-02-06 18:37:03 +00:00
if (&a_Window != m_CurrentWindow)
2014-01-19 12:20:57 +00:00
{
2021-02-06 18:37:03 +00:00
CloseWindow(false);
}
2014-01-19 12:20:57 +00:00
2021-02-06 18:37:03 +00:00
a_Window.OpenedByPlayer(*this);
m_CurrentWindow = &a_Window;
a_Window.SendWholeWindow(*GetClientHandle());
}
2021-02-06 18:37:03 +00:00
void cPlayer::CloseWindow(bool a_CanRefuse)
{
2021-02-06 18:37:03 +00:00
if (m_CurrentWindow == nullptr)
{
m_CurrentWindow = m_InventoryWindow;
return;
}
2021-02-06 18:37:03 +00:00
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());
}
}
2021-02-06 18:37:03 +00:00
void cPlayer::CloseWindowIfID(char a_WindowID, bool a_CanRefuse)
{
2021-02-06 18:37:03 +00:00
if ((m_CurrentWindow == nullptr) || (m_CurrentWindow->GetWindowID() != a_WindowID))
{
2021-02-06 18:37:03 +00:00
return;
}
2021-02-06 18:37:03 +00:00
CloseWindow();
}
2014-05-12 18:38:52 +00:00
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessage(const AString & a_Message)
{
m_ClientHandle->SendChat(a_Message, mtCustom);
}
2013-12-26 14:55:19 +00:00
2014-01-19 12:20:57 +00:00
2014-05-11 11:57:06 +00:00
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessageInfo(const AString & a_Message)
{
m_ClientHandle->SendChat(a_Message, mtInformation);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessageFailure(const AString & a_Message)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChat(a_Message, mtFailure);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessageSuccess(const AString & a_Message)
{
m_ClientHandle->SendChat(a_Message, mtSuccess);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessageWarning(const AString & a_Message)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChat(a_Message, mtWarning);
}
2013-11-16 10:38:57 +00:00
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessageFatal(const AString & a_Message)
{
m_ClientHandle->SendChat(a_Message, mtFailure);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessagePrivateMsg(const AString & a_Message, const AString & a_Sender)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChat(a_Message, mtPrivateMessage, a_Sender);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessage(const cCompositeChat & a_Message)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChat(a_Message);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendMessageRaw(const AString & a_MessageRaw, eChatType a_Type)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChatRaw(a_MessageRaw, a_Type);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendSystemMessage(const AString & a_Message)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChatSystem(a_Message, mtCustom);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendAboveActionBarMessage(const AString & a_Message)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChatAboveActionBar(a_Message, mtCustom);
}
2021-02-06 18:37:03 +00:00
void cPlayer::SendSystemMessage(const cCompositeChat & a_Message)
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChatSystem(a_Message);
}
2016-10-12 12:38:45 +00:00
2021-02-06 18:37:03 +00:00
void cPlayer::SendAboveActionBarMessage(const cCompositeChat & a_Message)
2016-10-12 12:38:45 +00:00
{
2021-02-06 18:37:03 +00:00
m_ClientHandle->SendChatAboveActionBar(a_Message);
2016-10-12 12:38:45 +00:00
}
2021-02-06 18:37:03 +00:00
const AString & cPlayer::GetName(void) const
{
2021-02-06 18:37:03 +00:00
return m_ClientHandle->GetUsername();
}
2021-02-06 18:37:03 +00:00
void cPlayer::SetGameMode(eGameMode a_GameMode)
2014-01-19 12:20:57 +00:00
{
2021-02-06 18:37:03 +00:00
if ((a_GameMode < gmMin) || (a_GameMode >= gmMax))
2014-01-20 14:10:39 +00:00
{
2021-02-06 18:37:03 +00:00
LOGWARNING("%s: Setting invalid gamemode: %d", GetName().c_str(), a_GameMode);
2014-01-20 14:10:39 +00:00
return;
}
2021-02-06 18:37:03 +00:00
if (m_GameMode == a_GameMode)
2014-01-19 12:20:57 +00:00
{
2021-02-06 18:37:03 +00:00
// Gamemode already set
return;
}
2016-10-12 12:38:45 +00:00
// Detach, if the player is switching from or to the spectator mode
if ((m_GameMode == gmSpectator) || (a_GameMode == gmSpectator))
{
Detach();
}
m_GameMode = a_GameMode;
UpdateCapabilities();
m_ClientHandle->SendGameMode(a_GameMode);
m_ClientHandle->SendInventorySlot(-1, -1, m_DraggingItem);
m_World->BroadcastPlayerListUpdateGameMode(*this);
m_World->BroadcastEntityMetadata(*this);
}
void cPlayer::UpdateCapabilities()
{
// Fly ability:
if (IsGameModeCreative() || IsGameModeSpectator())
{
m_IsFlightCapable = true;
}
else
{
m_IsFlying = false;
m_IsFlightCapable = false;
}
// Visible:
m_IsVisible = !IsGameModeSpectator();
2016-10-12 12:38:45 +00:00
// Clear the current dragging item of spectators:
if (IsGameModeSpectator())
{
m_DraggingItem.Empty();
}
}
void cPlayer::AwardAchievement(const CustomStatistic a_Ach)
{
// Check if the prerequisites are met:
if (!m_Stats.SatisfiesPrerequisite(a_Ach))
{
return;
}
// Increment the statistic and check if we already have it:
if (m_Stats.Custom[a_Ach]++ != 1)
{
return;
}
if (m_World->ShouldBroadcastAchievementMessages())
{
cCompositeChat Msg;
Msg.SetMessageType(mtSuccess);
// TODO: cCompositeChat should not use protocol-specific strings
// Msg.AddShowAchievementPart(GetName(), nameNew);
Msg.AddTextPart("Achievement get!");
m_World->BroadcastChat(Msg);
}
// Achievement Get!
m_ClientHandle->SendStatistics(m_Stats);
}
void cPlayer::TeleportToCoords(double a_PosX, double a_PosY, double a_PosZ)
{
// ask plugins to allow teleport to the new position.
2015-07-21 20:25:37 +00:00
if (!cRoot::Get()->GetPluginManager()->CallHookEntityTeleport(*this, m_LastPosition, Vector3d(a_PosX, a_PosY, a_PosZ)))
{
2021-04-10 15:58:39 +00:00
m_IsTeleporting = true;
Detach();
SetPosition({a_PosX, a_PosY, a_PosZ});
FreezeInternal(GetPosition(), false);
m_ClientHandle->SendPlayerMoveLook();
}
}
2015-12-24 10:37:32 +00:00
void cPlayer::Freeze(const Vector3d & a_Location)
{
FreezeInternal(a_Location, true);
}
bool cPlayer::IsFrozen()
{
return m_IsFrozen;
}
void cPlayer::Unfreeze()
{
if (IsElytraFlying())
{
m_World->BroadcastEntityMetadata(*this);
}
m_ClientHandle->SendPlayerAbilities();
m_World->BroadcastEntityProperties(*this);
2016-04-05 08:45:09 +00:00
2015-12-24 10:37:32 +00:00
m_IsFrozen = false;
BroadcastMovementUpdate(m_ClientHandle.get());
2016-04-05 08:45:09 +00:00
GetClientHandle()->SendPlayerPosition();
2015-12-24 10:37:32 +00:00
}
void cPlayer::SendRotation(double a_YawDegrees, double a_PitchDegrees)
{
SetYaw(a_YawDegrees);
SetPitch(a_PitchDegrees);
m_ClientHandle->SendPlayerMoveLook();
}
void cPlayer::SpectateEntity(const cEntity * a_Target)
{
if (a_Target == this)
{
// Canonicalise self-pointers:
a_Target = nullptr;
}
if (m_Spectating == a_Target)
{
// Already spectating requested target:
return;
}
if (a_Target == nullptr)
{
m_ClientHandle->SendCameraSetTo(*this);
m_ClientHandle->SendPlayerMoveLook();
m_Spectating = nullptr;
return;
}
m_Spectating = a_Target;
m_ClientHandle->SendCameraSetTo(*a_Target);
}
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;
2014-07-17 20:50:58 +00:00
}
eGameMode cPlayer::GetEffectiveGameMode(void) const
{
return (m_GameMode == gmNotSet) ? m_World->GetGameMode() : m_GameMode;
}
void cPlayer::ForceSetSpeed(const Vector3d & a_Speed)
{
SetSpeed(a_Speed);
}
void cPlayer::SetVisible(bool a_bVisible)
{
if (a_bVisible && !m_IsVisible)
{
m_IsVisible = true;
}
if (!a_bVisible && m_IsVisible)
{
m_IsVisible = false;
}
m_World->BroadcastEntityMetadata(*this);
}
MTRand cPlayer::GetEnchantmentRandomProvider()
{
return m_EnchantmentSeed;
}
void cPlayer::PermuteEnchantmentSeed()
{
// Get a new random integer and save that as the seed:
m_EnchantmentSeed = GetRandomProvider().RandInt<unsigned int>();
}
bool cPlayer::HasPermission(const AString & a_Permission)
{
if (a_Permission.empty())
{
// Empty permission request is always granted
return true;
}
2014-08-19 15:34:11 +00:00
AStringVector Split = StringSplit(a_Permission, ".");
// Iterate over all restrictions; if any matches, then return failure:
for (auto & Restriction: m_SplitRestrictions)
{
if (PermissionMatches(Split, Restriction))
{
return false;
}
} // for Restriction - m_SplitRestrictions[]
2014-08-19 15:34:11 +00:00
// Iterate over all granted permissions; if any matches, then return success:
for (auto & Permission: m_SplitPermissions)
{
if (PermissionMatches(Split, Permission))
{
2014-08-19 15:34:11 +00:00
return true;
}
} // for Permission - m_SplitPermissions[]
2014-08-19 15:34:11 +00:00
// No granted permission matches
return false;
}
2014-08-19 15:34:11 +00:00
bool cPlayer::PermissionMatches(const AStringVector & a_Permission, const AStringVector & a_Template)
{
2014-08-19 15:34:11 +00:00
// Check the sub-items if they are the same or there's a wildcard:
size_t lenP = a_Permission.size();
size_t lenT = a_Template.size();
size_t minLen = std::min(lenP, lenT);
for (size_t i = 0; i < minLen; i++)
{
2014-08-19 15:34:11 +00:00
if (a_Template[i] == "*")
{
// Has matched so far and now there's a wildcard in the template, so the permission matches:
return true;
2014-08-19 15:34:11 +00:00
}
if (a_Permission[i] != a_Template[i])
{
2014-08-19 15:34:11 +00:00
// Found a mismatch
return false;
}
}
2014-08-19 15:34:11 +00:00
// So far all the sub-items have matched
// If the sub-item count is the same, then the permission matches
return (lenP == lenT);
}
AString cPlayer::GetColor(void) const
{
2014-08-19 15:34:11 +00:00
if (m_MsgNameColorCode.empty() || (m_MsgNameColorCode == "-"))
{
2014-08-19 15:34:11 +00:00
// Color has not been assigned, return an empty string:
return AString();
}
2014-08-19 15:34:11 +00:00
// Return the color, including the delimiter:
return cChatColor::Delimiter + m_MsgNameColorCode;
}
AString cPlayer::GetPrefix(void) const
{
return m_MsgPrefix;
}
AString cPlayer::GetSuffix(void) const
{
return m_MsgSuffix;
}
AString cPlayer::GetPlayerListName(void) const
2014-09-02 17:12:35 +00:00
{
const AString & Color = GetColor();
if (HasCustomName())
{
return m_CustomName;
}
else if ((GetName().length() <= 14) && !Color.empty())
{
return Printf("%s%s", Color.c_str(), GetName().c_str());
}
else
{
return GetName();
}
}
2017-07-07 03:27:04 +00:00
void cPlayer::SetDraggingItem(const cItem & a_Item)
{
if (GetWindow() != nullptr)
{
m_DraggingItem = a_Item;
GetClientHandle()->SendInventorySlot(-1, -1, m_DraggingItem);
}
}
void cPlayer::TossEquippedItem(char a_Amount)
{
cItems Drops;
2014-01-24 03:11:10 +00:00
cItem DroppedItem(GetInventory().GetEquippedItem());
if (!DroppedItem.IsEmpty())
{
2014-01-24 03:11:10 +00:00
char NewAmount = a_Amount;
if (NewAmount > GetInventory().GetEquippedItem().m_ItemCount)
{
NewAmount = GetInventory().GetEquippedItem().m_ItemCount; // Drop only what's there
2014-01-24 03:11:10 +00:00
}
2014-01-24 03:11:10 +00:00
GetInventory().GetHotbarGrid().ChangeSlotCount(GetInventory().GetEquippedSlotNum() /* Returns hotbar subslot, which HotbarGrid takes */, -a_Amount);
2014-01-24 03:11:10 +00:00
DroppedItem.m_ItemCount = NewAmount;
Drops.push_back(DroppedItem);
}
2014-05-12 18:38:52 +00:00
TossItems(Drops);
}
void cPlayer::ReplaceOneEquippedItemTossRest(const cItem & a_Item)
{
auto PlacedCount = GetInventory().ReplaceOneEquippedItem(a_Item);
char ItemCountToToss = a_Item.m_ItemCount - static_cast<char>(PlacedCount);
if (ItemCountToToss == 0)
{
return;
}
cItem Pickup = a_Item;
Pickup.m_ItemCount = ItemCountToToss;
TossPickup(Pickup);
}
void cPlayer::TossHeldItem(char a_Amount)
{
cItems Drops;
2014-01-24 03:11:10 +00:00
cItem & Item = GetDraggingItem();
if (!Item.IsEmpty())
{
2014-01-24 03:11:10 +00:00
char OriginalItemAmount = Item.m_ItemCount;
Item.m_ItemCount = std::min(OriginalItemAmount, a_Amount);
Drops.push_back(Item);
2014-05-12 18:38:52 +00:00
2014-01-24 03:11:10 +00:00
if (OriginalItemAmount > a_Amount)
{
2014-01-24 03:11:10 +00:00
Item.m_ItemCount = OriginalItemAmount - a_Amount;
}
else
{
2014-01-24 03:11:10 +00:00
Item.Empty();
}
}
2014-01-24 03:11:10 +00:00
2014-05-12 18:38:52 +00:00
TossItems(Drops);
}
void cPlayer::TossPickup(const cItem & a_Item)
{
cItems Drops;
2014-01-24 03:11:10 +00:00
Drops.push_back(a_Item);
2014-05-12 18:38:52 +00:00
TossItems(Drops);
}
void cPlayer::LoadFromDisk()
{
2014-08-19 15:34:11 +00:00
LoadRank();
const auto & UUID = GetUUID();
// Load from the UUID file:
if (LoadFromFile(GetUUIDFileName(UUID)))
{
return;
}
// Player not found:
m_World = cRoot::Get()->GetDefaultWorld();
LOG("Player \"%s\" (%s) data not found, resetting to defaults", GetName().c_str(), UUID.ToShortString().c_str());
const Vector3i WorldSpawn(static_cast<int>(m_World->GetSpawnX()), static_cast<int>(m_World->GetSpawnY()), static_cast<int>(m_World->GetSpawnZ()));
SetPosition(WorldSpawn);
SetBedPos(WorldSpawn, *m_World);
m_Inventory.Clear();
m_EnchantmentSeed = GetRandomProvider().RandInt<unsigned int>(); // Use a random number to seed the enchantment generator
FLOGD("Player \"{0}\" is connecting for the first time, spawning at default world spawn {1:.2f}", GetName(), GetPosition());
}
bool cPlayer::LoadFromFile(const AString & a_FileName)
{
Json::Value Root;
try
{
// Load the data from the file and parse:
InputFileStream(a_FileName) >> Root;
}
catch (const Json::Exception & Oops)
{
// Parse failure:
throw std::runtime_error(Oops.what());
}
catch (const InputFileStream::failure &)
{
if (errno == ENOENT)
{
// This is a new player whom we haven't seen yet, bail out, let them have the defaults:
return false;
}
throw;
}
// Load the player data:
Json::Value & JSON_PlayerPosition = Root["position"];
if (JSON_PlayerPosition.size() == 3)
{
SetPosX(JSON_PlayerPosition[0].asDouble());
SetPosY(JSON_PlayerPosition[1].asDouble());
SetPosZ(JSON_PlayerPosition[2].asDouble());
2015-07-21 20:25:37 +00:00
m_LastPosition = GetPosition();
}
Json::Value & JSON_PlayerRotation = Root["rotation"];
if (JSON_PlayerRotation.size() == 3)
{
SetYaw (static_cast<float>(JSON_PlayerRotation[0].asDouble()));
SetPitch (static_cast<float>(JSON_PlayerRotation[1].asDouble()));
SetRoll (static_cast<float>(JSON_PlayerRotation[2].asDouble()));
}
m_Health = Root.get("health", 0).asFloat();
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();
m_LifetimeTotalXp = Root.get("xpTotal", 0).asInt();
m_CurrentXp = Root.get("xpCurrent", 0).asInt();
m_IsFlying = Root.get("isflying", 0).asBool();
m_EnchantmentSeed = Root.get("enchantmentSeed", GetRandomProvider().RandInt<unsigned int>()).asUInt();
Json::Value & JSON_KnownItems = Root["knownItems"];
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
for (UInt32 i = 0; i < JSON_KnownItems.size(); i++)
{
cItem Item;
Item.FromJson(JSON_KnownItems[i]);
m_KnownItems.insert(Item);
}
const auto & RecipeNameMap = cRoot::Get()->GetCraftingRecipes()->GetRecipeNameMap();
Json::Value & JSON_KnownRecipes = Root["knownRecipes"];
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
for (UInt32 i = 0; i < JSON_KnownRecipes.size(); i++)
{
auto RecipeId = RecipeNameMap.find(JSON_KnownRecipes[i].asString());
if (RecipeId != RecipeNameMap.end())
{
m_KnownRecipes.insert(RecipeId->second);
}
}
m_GameMode = static_cast<eGameMode>(Root.get("gamemode", eGameMode_NotSet).asInt());
2013-12-19 20:53:47 +00:00
if (m_GameMode == eGameMode_Creative)
{
m_IsFlightCapable = true;
2013-12-19 20:53:47 +00:00
}
m_Inventory.LoadFromJson(Root["inventory"]);
int equippedSlotNum = Root.get("equippedItemSlot", 0).asInt();
m_Inventory.SetEquippedSlotNum(equippedSlotNum);
cEnderChestEntity::LoadFromJson(Root["enderchestinventory"], m_EnderChestContents);
m_CurrentWorldName = Root.get("world", "world").asString();
m_World = cRoot::Get()->GetWorld(m_CurrentWorldName);
if (m_World == nullptr)
{
m_World = cRoot::Get()->GetDefaultWorld();
}
m_LastBedPos.x = Root.get("SpawnX", m_World->GetSpawnX()).asInt();
m_LastBedPos.y = Root.get("SpawnY", m_World->GetSpawnY()).asInt();
m_LastBedPos.z = Root.get("SpawnZ", m_World->GetSpawnZ()).asInt();
m_SpawnWorldName = Root.get("SpawnWorld", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
2014-05-11 17:30:54 +00:00
try
{
// Load the player stats.
// We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
2021-05-04 15:11:56 +00:00
StatisticsSerializer::Load(m_Stats, m_DefaultWorldPath, GetUUID().ToLongString());
}
catch (...)
{
LOGWARNING("Failed loading player statistics");
}
FLOGD("Player {0} was read from file \"{1}\", spawning at {2:.2f} in world \"{3}\"",
GetName(), a_FileName, GetPosition(), m_World->GetName()
);
return true;
}
void cPlayer::OpenHorseInventory()
{
if (
(m_AttachedTo == nullptr) ||
!m_AttachedTo->IsMob()
)
{
return;
}
auto & Mob = static_cast<cMonster &>(*m_AttachedTo);
if (Mob.GetMobType() != mtHorse)
{
return;
}
auto & Horse = static_cast<cHorse &>(Mob);
// The client sends requests for untame horses as well but shouldn't actually open
if (Horse.IsTame())
{
Horse.PlayerOpenWindow(*this);
}
}
void cPlayer::SaveToDisk()
{
const auto & UUID = GetUUID();
cFile::CreateFolderRecursive(GetUUIDFolderName(UUID));
// 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(GetYaw()));
JSON_PlayerRotation.append(Json::Value(GetPitch()));
JSON_PlayerRotation.append(Json::Value(GetRoll()));
Json::Value JSON_Inventory;
m_Inventory.SaveToJson(JSON_Inventory);
2014-06-29 10:36:38 +00:00
Json::Value JSON_EnderChestInventory;
cEnderChestEntity::SaveToJson(JSON_EnderChestInventory, m_EnderChestContents);
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
Json::Value JSON_KnownItems;
for (const auto & KnownItem : m_KnownItems)
{
Json::Value JSON_Item;
KnownItem.GetJson(JSON_Item);
JSON_KnownItems.append(JSON_Item);
}
Json::Value JSON_KnownRecipes;
for (auto KnownRecipe : m_KnownRecipes)
{
auto Recipe = cRoot::Get()->GetCraftingRecipes()->GetRecipeById(KnownRecipe);
JSON_KnownRecipes.append(Recipe->m_RecipeName);
}
Json::Value root;
2014-07-02 17:51:37 +00:00
root["position"] = JSON_PlayerPosition;
root["rotation"] = JSON_PlayerRotation;
root["inventory"] = JSON_Inventory;
Introduce recipe book functionality (#4493) * Introduce recipe book functionality The recipe book helps especially new players. Missing it gives the impression that cuberite is not as advanced as it is. The handling of the recipe book uses the following functions: - Unlock Recipes (https://wiki.vg/index.php?title=Protocol&oldid=14204#Unlock_Recipes) to make recipes available and show the notification for new recipes. Initialization is done on player login for known ones, the update is done when new items are discovered. - Craft Recipe Request (https://wiki.vg/index.php?title=Protocol&oldid=14204#Craft_Recipe_Request) when the user selects a recipe from the recipe book to fill the slots. Known recipes are initialized on player login via `Unlock Recipes` with `Action` 0. As soon as a new recipe is discovered this is added via `Unlock Recipes` with `Action` 1. To be able to know and recognize new recipes the player class is extended with `KnownItems` and `KnownRecipes`. As soon as a player touches an item this is compared to the list of `KnownItems`, if the item is unknown the recipes are checked for this item and the other ingredients are checked with the list of `KnownItems`. If a full match is discovered the recipe is unlocked with the client and stored in the `KnownRecipes`. To unlock recipes the recipe ID is sent to the client. A mapping file (for protocol 1.12.2) translated the minecraft recipe names to ids. The crafting.txt is extended with and minecraft recipe names is possible. Limitations: Only a single recipe is added to the crafting area. Multiple clicks or shift click does not increase the number of builds. Co-authored-by: peterbell10 <peterbell10@live.co.uk> * Address first issues mentioned by @peterbell10 - Some linting - Extract loading of recipe specific protocol mapping into a function - Build `RecipeNameMap` only once - Use `std::optional` - Extract `LoadRecipe` from `Window` * Start to implement new suggestions * Update with suggestions from @peterbell10 * Some minor cleanup * Update protocol packet IDs * Remove unused include * Include header in cmake * Change a vector to integer counter * Change dromedaryCase method names to PascalCase * Address suggestions from @madmaxoft * Read Protocol subdirectories to load recipe books To load all recipebooks iterate over the `Protocol` subdirectories to find mapping files. Co-authored-by: peterbell10 <peterbell10@live.co.uk>
2020-07-14 16:56:42 +00:00
root["knownItems"] = JSON_KnownItems;
root["knownRecipes"] = JSON_KnownRecipes;
root["equippedItemSlot"] = m_Inventory.GetEquippedSlotNum();
2014-06-29 10:36:38 +00:00
root["enderchestinventory"] = JSON_EnderChestInventory;
2014-07-02 17:51:37 +00:00
root["health"] = m_Health;
root["xpTotal"] = m_LifetimeTotalXp;
root["xpCurrent"] = m_CurrentXp;
root["air"] = m_AirLevel;
root["food"] = m_FoodLevel;
root["foodSaturation"] = m_FoodSaturationLevel;
root["foodTickTimer"] = m_FoodTickTimer;
root["foodExhaustion"] = m_FoodExhaustionLevel;
root["isflying"] = IsFlying();
root["lastknownname"] = GetName();
2014-07-18 19:12:27 +00:00
root["SpawnX"] = GetLastBedPos().x;
root["SpawnY"] = GetLastBedPos().y;
root["SpawnZ"] = GetLastBedPos().z;
root["SpawnWorld"] = m_SpawnWorldName;
root["enchantmentSeed"] = m_EnchantmentSeed;
root["world"] = m_CurrentWorldName;
root["gamemode"] = static_cast<int>(m_GameMode);
auto JsonData = JsonUtils::WriteStyledString(root);
AString SourceFile = GetUUIDFileName(UUID);
cFile f;
if (!f.Open(SourceFile, cFile::fmWrite))
{
LOGWARNING("Error writing player \"%s\" to file \"%s\": cannot open file. Player will lose their progress",
GetName().c_str(), SourceFile.c_str()
);
return;
}
if (f.Write(JsonData.c_str(), JsonData.size()) != static_cast<int>(JsonData.size()))
{
LOGWARNING("Error writing player \"%s\" to file \"%s\": cannot save data. Player will lose their progress",
GetName().c_str(), SourceFile.c_str()
2014-07-17 20:50:58 +00:00
);
return;
}
2014-05-11 17:30:54 +00:00
try
{
// Save the player stats.
// We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
// TODO: save together with player.dat, not in some other place.
2021-05-04 15:11:56 +00:00
StatisticsSerializer::Save(m_Stats, m_DefaultWorldPath, GetUUID().ToLongString());
}
catch (...)
2014-05-11 17:30:54 +00:00
{
LOGWARNING("Error writing player \"%s\" statistics to file", GetName().c_str());
2014-05-11 17:30:54 +00:00
}
}
void cPlayer::UseEquippedItem(short a_Damage)
{
// No durability loss in creative or spectator modes:
if (IsGameModeCreative() || IsGameModeSpectator())
{
return;
}
UseItem(cInventory::invHotbarOffset + m_Inventory.GetEquippedSlotNum(), a_Damage);
}
void cPlayer::UseEquippedItem(cItemHandler::eDurabilityLostAction a_Action)
{
// Get item being used:
cItem Item = GetEquippedItem();
// Get base damage for action type:
short Dmg = cItemHandler::GetItemHandler(Item)->GetDurabilityLossByAction(a_Action);
UseEquippedItem(Dmg);
}
void cPlayer::UseItem(int a_SlotNumber, short a_Damage)
{
const cItem & Item = m_Inventory.GetSlot(a_SlotNumber);
if (Item.IsEmpty())
{
return;
}
// Ref: https://minecraft.gamepedia.com/Enchanting#Unbreaking
unsigned int UnbreakingLevel = Item.m_Enchantments.GetLevel(cEnchantments::enchUnbreaking);
double chance = ItemCategory::IsArmor(Item.m_ItemType)
? (0.6 + (0.4 / (UnbreakingLevel + 1))) : (1.0 / (UnbreakingLevel + 1));
// When durability is reduced by multiple points
// Unbreaking is applied for each point of reduction.
std::binomial_distribution<short> Dist(a_Damage, chance);
short ReducedDamage = Dist(GetRandomProvider().Engine());
if (m_Inventory.DamageItem(a_SlotNumber, ReducedDamage))
{
// The item broke. Broadcast the correct animation:
if (Item.m_ItemType == E_ITEM_SHIELD)
{
m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnShieldBreaks);
}
else if (a_SlotNumber == (cInventory::invHotbarOffset + m_Inventory.GetEquippedSlotNum()))
{
m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnMainHandEquipmentBreaks);
}
else
{
switch (a_SlotNumber)
{
case cInventory::invArmorOffset: return m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnHeadEquipmentBreaks);
case cInventory::invArmorOffset + 1: return m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnChestEquipmentBreaks);
case cInventory::invArmorOffset + 2: return m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnLegsEquipmentBreaks);
case cInventory::invArmorOffset + 3: return m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnFeetEquipmentBreaks);
case cInventory::invShieldOffset: return m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnOffHandEquipmentBreaks);
}
}
}
else if (Item.m_ItemType == E_ITEM_SHIELD)
{
// The item survived. Special case for shield blocking:
m_World->BroadcastEntityAnimation(*this, EntityAnimation::PawnShieldBlocks);
}
}
void cPlayer::HandleFood(void)
{
2017-08-24 09:19:40 +00:00
// Ref.: https://minecraft.gamepedia.com/Hunger
if (IsGameModeCreative() || IsGameModeSpectator())
2014-02-11 14:34:18 +00:00
{
// Hunger is disabled for Creative and Spectator
2014-02-11 14:34:18 +00:00
return;
}
// Apply food exhaustion that has accumulated:
if (m_FoodExhaustionLevel > 4.0)
{
m_FoodExhaustionLevel -= 4.0;
if (m_FoodSaturationLevel > 0.0)
{
m_FoodSaturationLevel = std::max(m_FoodSaturationLevel - 1.0, 0.0);
}
else
{
SetFoodLevel(m_FoodLevel - 1);
}
}
// Heal or damage, based on the food level, using the m_FoodTickTimer:
if ((m_FoodLevel >= 18) || (m_FoodLevel <= 0))
{
m_FoodTickTimer++;
if (m_FoodTickTimer >= 80)
{
m_FoodTickTimer = 0;
if ((m_FoodLevel >= 18) && (GetHealth() < GetMaxHealth()))
{
// Regenerate health from food, incur 3 pts of food exhaustion:
Heal(1);
AddFoodExhaustion(3.0);
}
else if ((m_FoodLevel <= 0) && (m_Health > 1))
{
// Damage from starving
2014-10-20 20:55:07 +00:00
TakeDamage(dtStarving, nullptr, 1, 1, 0);
}
}
}
else
{
m_FoodTickTimer = 0;
}
}
void cPlayer::HandleFloater()
{
if (GetEquippedItem().m_ItemType == E_ITEM_FISHING_ROD)
{
return;
}
m_World->DoWithEntityByID(m_FloaterID, [](cEntity & a_Entity)
{
a_Entity.Destroy();
return true;
}
);
SetIsFishing(false);
}
bool cPlayer::IsClimbing(void) const
{
const auto Position = GetPosition().Floor();
if (!cChunkDef::IsValidHeight(Position.y))
{
return false;
}
BLOCKTYPE Block = m_World->GetBlock(Position);
switch (Block)
{
case E_BLOCK_LADDER:
case E_BLOCK_VINES:
{
return true;
}
default: return false;
}
}
2015-07-21 20:25:37 +00:00
void cPlayer::UpdateMovementStats(const Vector3d & a_DeltaPos, bool a_PreviousIsOnGround)
2014-05-12 18:38:52 +00:00
{
if (m_IsTeleporting)
2015-07-21 20:25:37 +00:00
{
m_IsTeleporting = false;
2015-07-21 20:25:37 +00:00
return;
}
2014-05-12 18:38:52 +00:00
const auto Value = FloorC<StatisticsManager::StatValue>(a_DeltaPos.Length() * 100 + 0.5);
2014-10-20 20:55:07 +00:00
if (m_AttachedTo == nullptr)
2014-05-12 18:38:52 +00:00
{
2015-07-21 20:25:37 +00:00
if (IsFlying())
{
m_Stats.Custom[CustomStatistic::FlyOneCm] += Value;
2015-07-21 20:25:37 +00:00
// May be flying and doing any of the following:
}
if (IsClimbing())
2014-05-12 18:38:52 +00:00
{
if (a_DeltaPos.y > 0.0) // Going up
{
m_Stats.Custom[CustomStatistic::ClimbOneCm] += FloorC<StatisticsManager::StatValue>(a_DeltaPos.y * 100 + 0.5);
}
2014-05-12 18:38:52 +00:00
}
else if (IsInWater())
{
if (m_IsHeadInWater)
{
m_Stats.Custom[CustomStatistic::WalkUnderWaterOneCm] += Value;
}
else
{
m_Stats.Custom[CustomStatistic::WalkOnWaterOneCm] += Value;
}
AddFoodExhaustion(0.00015 * static_cast<double>(Value));
}
else if (IsOnGround())
2014-05-12 18:38:52 +00:00
{
if (IsCrouched())
{
m_Stats.Custom[CustomStatistic::CrouchOneCm] += Value;
AddFoodExhaustion(0.0001 * static_cast<double>(Value));
}
if (IsSprinting())
{
m_Stats.Custom[CustomStatistic::SprintOneCm] += Value;
AddFoodExhaustion(0.001 * static_cast<double>(Value));
}
else
{
m_Stats.Custom[CustomStatistic::WalkOneCm] += Value;
AddFoodExhaustion(0.0001 * static_cast<double>(Value));
}
2014-05-12 18:38:52 +00:00
}
else
{
2015-07-21 20:25:37 +00:00
// If a jump just started, process food exhaustion:
if ((a_DeltaPos.y > 0.0) && a_PreviousIsOnGround)
{
m_Stats.Custom[CustomStatistic::Jump]++;
2015-07-21 20:25:37 +00:00
AddFoodExhaustion((IsSprinting() ? 0.008 : 0.002) * static_cast<double>(Value));
}
else if (a_DeltaPos.y < 0.0)
{
2015-07-21 20:25:37 +00:00
// Increment statistic
m_Stats.Custom[CustomStatistic::FallOneCm] += static_cast<StatisticsManager::StatValue>(-a_DeltaPos.y * 100 + 0.5);
}
2015-07-21 20:25:37 +00:00
// TODO: good opportunity to detect illegal flight (check for falling tho)
}
2014-05-12 18:38:52 +00:00
}
else
{
switch (m_AttachedTo->GetEntityType())
{
case cEntity::etMinecart: m_Stats.Custom[CustomStatistic::MinecartOneCm] += Value; break;
case cEntity::etBoat: m_Stats.Custom[CustomStatistic::BoatOneCm] += Value; break;
2014-05-12 18:38:52 +00:00
case cEntity::etMonster:
{
cMonster * Monster = static_cast<cMonster *>(m_AttachedTo);
2014-05-12 18:38:52 +00:00
switch (Monster->GetMobType())
{
case mtPig: m_Stats.Custom[CustomStatistic::PigOneCm] += Value; break;
case mtHorse: m_Stats.Custom[CustomStatistic::HorseOneCm] += Value; break;
2014-05-12 18:38:52 +00:00
default: break;
}
break;
}
default: break;
}
}
}
2014-08-19 15:34:11 +00:00
void cPlayer::LoadRank(void)
{
const auto & UUID = GetUUID();
cRankManager * RankMgr = cRoot::Get()->GetRankManager();
// Load the values from cRankManager:
m_Rank = RankMgr->GetPlayerRankName(UUID);
if (m_Rank.empty())
{
m_Rank = RankMgr->GetDefaultRank();
}
else
{
// Update the name:
RankMgr->UpdatePlayerName(UUID, GetName());
}
m_Permissions = RankMgr->GetPlayerPermissions(UUID);
m_Restrictions = RankMgr->GetPlayerRestrictions(UUID);
RankMgr->GetRankVisuals(m_Rank, m_MsgPrefix, m_MsgSuffix, m_MsgNameColorCode);
2014-08-19 15:34:11 +00:00
// Break up the individual permissions on each dot, into m_SplitPermissions:
m_SplitPermissions.clear();
m_SplitPermissions.reserve(m_Permissions.size());
for (auto & Permission: m_Permissions)
{
m_SplitPermissions.push_back(StringSplit(Permission, "."));
} // for Permission - m_Permissions[]
// Break up the individual restrictions on each dot, into m_SplitRestrictions:
m_SplitRestrictions.clear();
m_SplitRestrictions.reserve(m_Restrictions.size());
for (auto & Restriction: m_Restrictions)
2014-08-19 15:34:11 +00:00
{
m_SplitRestrictions.push_back(StringSplit(Restriction, "."));
} // for itr - m_Restrictions[]
2014-08-19 15:34:11 +00:00
}
void cPlayer::SendBlocksAround(int a_BlockX, int a_BlockY, int a_BlockZ, int a_Range)
{
// Collect the coords of all the blocks to send:
sSetBlockVector blks;
for (int y = a_BlockY - a_Range + 1; y < a_BlockY + a_Range; y++)
{
for (int z = a_BlockZ - a_Range + 1; z < a_BlockZ + a_Range; z++)
{
for (int x = a_BlockX - a_Range + 1; x < a_BlockX + a_Range; x++)
{
blks.emplace_back(x, y, z, E_BLOCK_AIR, 0); // Use fake blocktype, it will get set later on.
2019-08-11 09:39:43 +00:00
}
}
} // for y
// Get the values of all the blocks:
if (!m_World->GetBlocks(blks, false))
{
LOGD("%s: Cannot query all blocks, not sending an update", __FUNCTION__);
return;
}
// Divide the block changes by their respective chunks:
std::unordered_map<cChunkCoords, sSetBlockVector, cChunkCoordsHash> Changes;
for (const auto & blk: blks)
{
Changes[cChunkCoords(blk.m_ChunkX, blk.m_ChunkZ)].push_back(blk);
} // for blk - blks[]
blks.clear();
// Send the blocks for each affected chunk:
for (auto itr = Changes.cbegin(), end = Changes.cend(); itr != end; ++itr)
{
m_ClientHandle->SendBlockChanges(itr->first.m_ChunkX, itr->first.m_ChunkZ, itr->second);
}
}
bool cPlayer::DoesPlacingBlocksIntersectEntity(const std::initializer_list<sSetBlock> a_Blocks) const
{
// Compute the bounding box for each block to be placed
std::vector<cBoundingBox> PlacementBoxes;
cBoundingBox PlacingBounds(0, 0, 0, 0, 0, 0);
bool HasInitializedBounds = false;
for (auto blk: a_Blocks)
{
int x = blk.GetX();
int y = blk.GetY();
int z = blk.GetZ();
cBoundingBox BlockBox = cBlockHandler::For(blk.m_BlockType).GetPlacementCollisionBox(
m_World->GetBlock({ x - 1, y, z }),
m_World->GetBlock({ x + 1, y, z }),
(y == 0) ? E_BLOCK_AIR : m_World->GetBlock({ x, y - 1, z }),
(y == cChunkDef::Height - 1) ? E_BLOCK_AIR : m_World->GetBlock({ x, y + 1, z }),
m_World->GetBlock({ x, y, z - 1 }),
m_World->GetBlock({ x, y, z + 1 })
);
BlockBox.Move(x, y, z);
PlacementBoxes.push_back(BlockBox);
if (HasInitializedBounds)
{
PlacingBounds = PlacingBounds.Union(BlockBox);
}
else
{
PlacingBounds = BlockBox;
HasInitializedBounds = true;
}
}
cWorld * World = GetWorld();
// Check to see if any entity intersects any block being placed
return !World->ForEachEntityInBox(PlacingBounds, [&](cEntity & a_Entity)
{
// The distance inside the block the entity can still be.
const double EPSILON = 0.0005;
if (!a_Entity.DoesPreventBlockPlacement())
{
return false;
}
auto EntBox = a_Entity.GetBoundingBox();
for (auto BlockBox : PlacementBoxes)
{
// Put in a little bit of wiggle room
BlockBox.Expand(-EPSILON, -EPSILON, -EPSILON);
if (EntBox.DoesIntersect(BlockBox))
{
return true;
}
}
return false;
}
);
}
const cUUID & cPlayer::GetUUID(void) const
{
return m_ClientHandle->GetUUID();
}
bool cPlayer::PlaceBlock(const Vector3i a_Position, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta)
{
return PlaceBlocks({ { a_Position, a_BlockType, a_BlockMeta } });
}
bool cPlayer::PlaceBlocks(const std::initializer_list<sSetBlock> a_Blocks)
{
if (DoesPlacingBlocksIntersectEntity(a_Blocks))
{
// Abort - re-send all the current blocks in the a_Blocks' coords to the client:
for (const auto & ResendBlock : a_Blocks)
{
m_World->SendBlockTo(ResendBlock.GetX(), ResendBlock.GetY(), ResendBlock.GetZ(), *this);
}
return false;
}
cPluginManager * pm = cPluginManager::Get();
// Check the blocks CanBeAt, and call the "placing" hooks; if any fail, abort:
for (const auto & Block : a_Blocks)
{
if (
!m_World->DoWithChunkAt(Block.GetAbsolutePos(), [&Block](cChunk & a_Chunk)
{
return cBlockHandler::For(Block.m_BlockType).CanBeAt(a_Chunk, Block.GetRelativePos(), Block.m_BlockMeta);
})
)
{
return false;
}
if (pm->CallHookPlayerPlacingBlock(*this, Block))
{
// Abort - re-send all the current blocks in the a_Blocks' coords to the client:
for (const auto & ResendBlock : a_Blocks)
{
m_World->SendBlockTo(ResendBlock.GetX(), ResendBlock.GetY(), ResendBlock.GetZ(), *this);
}
return false;
}
}
cChunkInterface ChunkInterface(m_World->GetChunkMap());
for (const auto & Block : a_Blocks)
{
// Set the blocks:
m_World->PlaceBlock(Block.GetAbsolutePos(), Block.m_BlockType, Block.m_BlockMeta);
// Call the "placed" hooks:
pm->CallHookPlayerPlacedBlock(*this, Block);
}
return true;
}
void cPlayer::SetSkinParts(int a_Parts)
{
m_SkinParts = a_Parts & spMask;
m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get());
}
2017-08-25 12:43:18 +00:00
AString cPlayer::GetUUIDFileName(const cUUID & a_UUID)
{
2017-08-25 12:43:18 +00:00
AString UUID = a_UUID.ToLongString();
2020-05-07 19:14:00 +00:00
AString res("players/");
res.append(UUID, 0, 2);
res.push_back('/');
res.append(UUID, 2, AString::npos);
res.append(".json");
return res;
}
2016-04-13 12:15:12 +00:00
void cPlayer::FreezeInternal(const Vector3d & a_Location, bool a_ManuallyFrozen)
{
SetSpeed(0, 0, 0);
SetPosition(a_Location);
m_IsFrozen = true;
m_IsManuallyFrozen = a_ManuallyFrozen;
double NormalMaxSpeed = GetNormalMaxSpeed();
double SprintMaxSpeed = GetSprintingMaxSpeed();
double FlyingMaxpeed = GetFlyingMaxSpeed();
bool IsFlying = m_IsFlying;
// Set the client-side speed to 0
m_NormalMaxSpeed = 0;
m_SprintingMaxSpeed = 0;
m_FlyingMaxSpeed = 0;
m_IsFlying = true;
// Send the client its fake speed and max speed of 0
m_ClientHandle->SendPlayerMoveLook();
m_ClientHandle->SendPlayerAbilities();
m_ClientHandle->SendEntityVelocity(*this);
m_World->BroadcastEntityProperties(*this);
2016-04-13 12:15:12 +00:00
// Keep the server side speed variables as they were in the first place
m_NormalMaxSpeed = NormalMaxSpeed;
m_SprintingMaxSpeed = SprintMaxSpeed;
m_FlyingMaxSpeed = FlyingMaxpeed;
m_IsFlying = IsFlying;
}
float cPlayer::GetLiquidHeightPercent(NIBBLETYPE a_Meta)
{
if (a_Meta >= 8)
{
a_Meta = 0;
}
return static_cast<float>(a_Meta + 1) / 9.0f;
}
bool cPlayer::IsInsideWater()
{
BLOCKTYPE Block;
NIBBLETYPE Meta;
m_World->GetBlockTypeMeta(GetEyePosition().Floor(), Block, Meta);
if ((Block != E_BLOCK_WATER) && (Block != E_BLOCK_STATIONARY_WATER))
{
return false;
}
const auto EyeHeight = GetEyeHeight();
float f = GetLiquidHeightPercent(Meta) - 0.11111111f;
float f1 = static_cast<float>(EyeHeight + 1) - f;
return EyeHeight < f1;
}
float cPlayer::GetDigSpeed(BLOCKTYPE a_Block)
{
// Based on: https://minecraft.gamepedia.com/Breaking#Speed
// Get the base speed multiplier of the equipped tool for the mined block
float MiningSpeed = GetEquippedItem().GetHandler()->GetBlockBreakingStrength(a_Block);
// If we can harvest the block then we can apply material and enchantment bonuses
if (GetEquippedItem().GetHandler()->CanHarvestBlock(a_Block))
{
if (MiningSpeed > 1.0f) // If the base multiplier for this block is greater than 1, now we can check enchantments
{
unsigned int EfficiencyModifier = GetEquippedItem().m_Enchantments.GetLevel(cEnchantments::eEnchantment::enchEfficiency);
if (EfficiencyModifier > 0) // If an efficiency enchantment is present, apply formula as on wiki
{
MiningSpeed += (EfficiencyModifier * EfficiencyModifier) + 1;
}
}
}
else // If we can't harvest the block then no bonuses:
{
MiningSpeed = 1;
}
// Haste increases speed by 20% per level
2017-08-01 17:51:43 +00:00
auto Haste = GetEntityEffect(cEntityEffect::effHaste);
if (Haste != nullptr)
{
2017-08-01 17:51:43 +00:00
int intensity = Haste->GetIntensity() + 1;
MiningSpeed *= 1.0f + (intensity * 0.2f);
}
// Mining fatigue decreases speed a lot
2017-08-01 17:51:43 +00:00
auto MiningFatigue = GetEntityEffect(cEntityEffect::effMiningFatigue);
if (MiningFatigue != nullptr)
{
2017-08-01 17:51:43 +00:00
int intensity = MiningFatigue->GetIntensity();
switch (intensity)
{
case 0: MiningSpeed *= 0.3f; break;
case 1: MiningSpeed *= 0.09f; break;
case 2: MiningSpeed *= 0.0027f; break;
default: MiningSpeed *= 0.00081f; break;
}
}
// 5x speed loss for being in water
if (IsInsideWater() && !(GetEquippedItem().m_Enchantments.GetLevel(cEnchantments::eEnchantment::enchAquaAffinity) > 0))
{
MiningSpeed /= 5.0f;
}
// 5x speed loss for not touching ground
if (!IsOnGround())
{
MiningSpeed /= 5.0f;
}
return MiningSpeed;
}
float cPlayer::GetMiningProgressPerTick(BLOCKTYPE a_Block)
{
// Based on https://minecraft.gamepedia.com/Breaking#Calculation
// If we know it's instantly breakable then quit here:
if (cBlockInfo::IsOneHitDig(a_Block))
{
return 1;
}
const bool CanHarvest = GetEquippedItem().GetHandler()->CanHarvestBlock(a_Block);
const float BlockHardness = cBlockInfo::GetHardness(a_Block) * (CanHarvest ? 1.5f : 5.0f);
ASSERT(BlockHardness > 0); // Can't divide by 0 or less, IsOneHitDig should have returned true
// LOGD("Time to mine block = %f", BlockHardness/DigSpeed);
// Number of ticks to mine = (20 * BlockHardness)/DigSpeed;
// Therefore take inverse to get fraction mined per tick:
return GetDigSpeed(a_Block) / (20.0f * BlockHardness);
}
bool cPlayer::CanInstantlyMine(BLOCKTYPE a_Block)
{
// Based on: https://minecraft.gamepedia.com/Breaking#Calculation
// If the dig speed is greater than 30 times the hardness, then the wiki says we can instantly mine:
return GetDigSpeed(a_Block) > (30 * cBlockInfo::GetHardness(a_Block));
}
2021-02-06 18:37:03 +00:00
void cPlayer::AddKnownItem(const cItem & a_Item)
{
2021-02-06 18:37:03 +00:00
if (a_Item.m_ItemType < 0)
{
return;
}
auto Response = m_KnownItems.insert(a_Item.CopyOne());
if (!Response.second)
{
2021-02-06 18:37:03 +00:00
// The item was already known, bail out:
return;
}
2021-02-06 18:37:03 +00:00
// Process the recipes that got unlocked by this newly-known item:
auto Recipes = cRoot::Get()->GetCraftingRecipes()->FindNewRecipesForItem(a_Item, m_KnownItems);
for (const auto & RecipeId : Recipes)
{
AddKnownRecipe(RecipeId);
}
}
void cPlayer::AddKnownRecipe(UInt32 a_RecipeId)
{
auto Response = m_KnownRecipes.insert(a_RecipeId);
if (!Response.second)
{
// The recipe was already known, bail out:
return;
}
m_ClientHandle->SendUnlockRecipe(a_RecipeId);
}
void cPlayer::TickFreezeCode()
{
if (m_IsFrozen)
{
if ((!m_IsManuallyFrozen) && (GetClientHandle()->IsPlayerChunkSent()))
{
// If the player was automatically frozen, unfreeze if the chunk the player is inside is loaded and sent
Unfreeze();
// Pull the player out of any solids that might have loaded on them.
PREPARE_REL_AND_CHUNK(GetPosition(), *(GetParentChunk()));
if (RelSuccess)
{
int NewY = Rel.y;
if (NewY < 0)
{
NewY = 0;
}
while (NewY < cChunkDef::Height - 2)
{
// If we find a position with enough space for the player
if (
!cBlockInfo::IsSolid(Chunk->GetBlock(Rel.x, NewY, Rel.z)) &&
!cBlockInfo::IsSolid(Chunk->GetBlock(Rel.x, NewY + 1, Rel.z))
)
{
// If the found position is not the same as the original
if (NewY != Rel.y)
{
SetPosition(GetPosition().x, NewY, GetPosition().z);
GetClientHandle()->SendPlayerPosition();
}
break;
}
++NewY;
}
}
}
else if ((GetWorld()->GetWorldTickAge() % 100_tick) == 0_tick)
2021-02-06 18:37:03 +00:00
{
// Despite the client side freeze, the player may be able to move a little by
// Jumping or canceling flight. Re-freeze every now and then
FreezeInternal(GetPosition(), m_IsManuallyFrozen);
}
}
else
{
if (!GetClientHandle()->IsPlayerChunkSent() || (!GetParentChunk()->IsValid()))
{
FreezeInternal(GetPosition(), false);
}
}
}
void cPlayer::ApplyArmorDamage(int a_DamageBlocked)
{
short ArmorDamage = static_cast<short>(std::max(a_DamageBlocked / 4, 1));
for (int i = 0; i < 4; i++)
{
UseItem(cInventory::invArmorOffset + i, ArmorDamage);
}
}
void cPlayer::BroadcastMovementUpdate(const cClientHandle * a_Exclude)
{
if (!m_IsFrozen && m_Speed.SqrLength() > 0.001)
{
// If the player is not frozen, has a non-zero speed,
// send the speed to the client so he is forced to move so:
m_ClientHandle->SendEntityVelocity(*this);
}
// Since we do no physics processing for players, speed will otherwise never decrease:
m_Speed.Set(0, 0, 0);
Super::BroadcastMovementUpdate(a_Exclude);
}
bool cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI)
{
// Filters out damage for creative mode / friendly fire.
if ((a_TDI.DamageType != dtInVoid) && (a_TDI.DamageType != dtPlugin))
{
if (IsGameModeCreative() || IsGameModeSpectator())
{
// No damage / health in creative or spectator mode if not void or plugin damage
return false;
}
}
if ((a_TDI.Attacker != nullptr) && (a_TDI.Attacker->IsPlayer()))
{
cPlayer * Attacker = static_cast<cPlayer *>(a_TDI.Attacker);
if ((m_Team != nullptr) && (m_Team == Attacker->m_Team))
{
if (!m_Team->AllowsFriendlyFire())
{
// Friendly fire is disabled
return false;
}
}
}
if (Super::DoTakeDamage(a_TDI))
{
// Any kind of damage adds food exhaustion
AddFoodExhaustion(0.3f);
m_ClientHandle->SendHealth();
// Tell the wolves
if (a_TDI.Attacker != nullptr)
{
if (a_TDI.Attacker->IsPawn())
{
NotifyNearbyWolves(static_cast<cPawn*>(a_TDI.Attacker), true);
}
}
m_Stats.Custom[CustomStatistic::DamageTaken] += FloorC<StatisticsManager::StatValue>(a_TDI.FinalDamage * 10 + 0.5);
2021-02-06 18:37:03 +00:00
return true;
}
return false;
}
float cPlayer::GetEnchantmentBlastKnockbackReduction()
{
if (
IsGameModeSpectator() ||
(IsGameModeCreative() && !IsOnGround())
)
{
return 1; // No impact from explosion
}
return Super::GetEnchantmentBlastKnockbackReduction();
}
bool cPlayer::IsCrouched(void) const
{
return std::holds_alternative<BodyStanceCrouching>(m_BodyStance);
}
bool cPlayer::IsSprinting(void) const
{
return std::holds_alternative<BodyStanceSprinting>(m_BodyStance);
}
bool cPlayer::IsElytraFlying(void) const
{
return std::holds_alternative<BodyStanceGliding>(m_BodyStance);
}
bool cPlayer::IsInvisible() const
{
return !m_IsVisible || Super::IsInvisible();
}
2021-02-06 18:37:03 +00:00
void cPlayer::OnAddToWorld(cWorld & a_World)
{
// Sends player spawn:
2021-02-06 18:37:03 +00:00
Super::OnAddToWorld(a_World);
// Update world name tracking:
m_CurrentWorldName = m_World->GetName();
// Fix to stop the player falling through the world, until we get serversided collision detection:
FreezeInternal(GetPosition(), false);
// UpdateCapabilities was called in the constructor, and in OnRemoveFromWorld, possibly changing our visibility.
// If world is in spectator mode, invisibility will need updating. If we just connected, we might be on fire from a previous game.
// Hence, tell the client by sending metadata:
m_ClientHandle->SendEntityMetadata(*this);
2021-02-06 18:37:03 +00:00
// Send contents of the inventory window:
m_ClientHandle->SendWholeInventory(*m_CurrentWindow);
// Send health (the respawn packet, which understandably resets health, is also used for world travel...):
m_ClientHandle->SendHealth();
// Send experience, similar story with the respawn packet:
m_ClientHandle->SendExperience();
// Send hotbar active slot (also reset by respawn):
m_ClientHandle->SendHeldItemChange(m_Inventory.GetEquippedSlotNum());
// Update player team:
UpdateTeam();
// Send scoreboard data:
m_World->GetScoreBoard().SendTo(*m_ClientHandle);
// Update the view distance:
m_ClientHandle->SetViewDistance(m_ClientHandle->GetRequestedViewDistance());
// Send current weather of target world:
m_ClientHandle->SendWeather(a_World.GetWeather());
// Send time:
m_ClientHandle->SendTimeUpdate(a_World.GetWorldAge(), a_World.GetWorldDate(), a_World.IsDaylightCycleEnabled());
2021-02-06 18:37:03 +00:00
// Finally, deliver the notification hook:
cRoot::Get()->GetPluginManager()->CallHookPlayerSpawned(*this);
}
void cPlayer::OnDetach()
{
if (m_IsTeleporting)
{
// If they are teleporting, no need to figure out position:
return;
}
int PosX = POSX_TOINT;
int PosY = POSY_TOINT;
int PosZ = POSZ_TOINT;
// Search for a position within an area to teleport player after detachment
// Position must be solid land with two air blocks above.
// If nothing found, player remains where they are.
for (int x = PosX - 1; x <= (PosX + 1); ++x)
{
for (int y = PosY; y <= (PosY + 3); ++y)
{
for (int z = PosZ - 1; z <= (PosZ + 1); ++z)
{
if (
(m_World->GetBlock({ x, y, z }) == E_BLOCK_AIR) &&
(m_World->GetBlock({ x, y + 1, z }) == E_BLOCK_AIR) &&
cBlockInfo::IsSolid(m_World->GetBlock({ x, y - 1, z }))
)
{
TeleportToCoords(x + 0.5, y, z + 0.5);
return;
}
}
}
}
}
2021-02-06 18:37:03 +00:00
void cPlayer::OnRemoveFromWorld(cWorld & a_World)
{
Super::OnRemoveFromWorld(a_World);
// Remove any references to this player pointer by windows in the old world:
CloseWindow(false);
// Remove the client handle from the world:
m_World->RemoveClientFromChunks(m_ClientHandle.get());
if (m_ClientHandle->IsDestroyed()) // Note: checking IsWorldChangeScheduled not appropriate here since we can disconnecting while having a scheduled warp
{
// Disconnecting, do the necessary cleanup.
// This isn't in the destructor to avoid crashing accessing destroyed objects during shutdown.
if (!cRoot::Get()->GetPluginManager()->CallHookPlayerDestroyed(*this))
{
cRoot::Get()->BroadcastChatLeave(Printf("%s has left the game", GetName().c_str()));
LOGINFO("Player %s has left the game", GetName().c_str());
}
// Remove ourself from everyone's lists:
cRoot::Get()->BroadcastPlayerListsRemovePlayer(*this);
// Atomically decrement player count (in world thread):
cRoot::Get()->GetServer()->PlayerDestroyed();
// We're just disconnecting. The remaining code deals with going through portals, so bail:
return;
}
const auto DestinationDimension = m_WorldChangeInfo.m_NewWorld->GetDimension();
// Award relevant achievements:
if (DestinationDimension == dimEnd)
{
AwardAchievement(CustomStatistic::AchTheEnd);
2021-02-06 18:37:03 +00:00
}
else if (DestinationDimension == dimNether)
{
AwardAchievement(CustomStatistic::AchPortal);
2021-02-06 18:37:03 +00:00
}
// Set capabilities based on new world:
UpdateCapabilities();
// Clientside warp start:
m_ClientHandle->SendRespawn(DestinationDimension, false);
m_ClientHandle->SendPlayerListUpdateGameMode(*this);
m_World->BroadcastPlayerListUpdateGameMode(*this);
2021-02-06 18:37:03 +00:00
// Clear sent chunk lists from the clienthandle:
m_ClientHandle->RemoveFromWorld();
}
void cPlayer::SpawnOn(cClientHandle & a_Client)
{
if (m_ClientHandle.get() == &a_Client)
2021-02-06 18:37:03 +00:00
{
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(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
{
m_ClientHandle->Tick(a_Dt.count());
if (m_ClientHandle->IsDestroyed())
{
Destroy();
return;
}
if (!m_ClientHandle->IsPlaying())
{
// We're not yet in the game, ignore everything:
return;
}
{
const auto TicksElapsed = static_cast<StatisticsManager::StatValue>(std::chrono::duration_cast<cTickTime>(a_Dt).count());
m_Stats.Custom[CustomStatistic::PlayOneMinute] += TicksElapsed;
m_Stats.Custom[CustomStatistic::TimeSinceDeath] += TicksElapsed;
if (IsCrouched())
{
m_Stats.Custom[CustomStatistic::SneakTime] += TicksElapsed;
}
2021-02-06 18:37:03 +00:00
}
if (!a_Chunk.IsValid())
{
// Players are ticked even if the parent chunk is invalid.
// We've processed as much as we can, bail:
return;
}
ASSERT((GetParentChunk() != nullptr) && (GetParentChunk()->IsValid()));
ASSERT(a_Chunk.IsValid());
// Handle a frozen player:
TickFreezeCode();
if (
m_IsFrozen || // Don't do Tick updates if frozen
IsWorldChangeScheduled() // If we're about to change worlds (e.g. respawn), abort processing all world interactions (GH #3939)
)
{
return;
}
Super::Tick(a_Dt, a_Chunk);
// Handle charging the bow:
if (m_IsChargingBow)
{
m_BowCharge += 1;
}
// Handle syncing our position with the entity being spectated:
if (IsGameModeSpectator() && (m_Spectating != nullptr))
{
SetYaw(m_Spectating->GetYaw());
SetPitch(m_Spectating->GetPitch());
SetRoll(m_Spectating->GetRoll());
SetPosition(m_Spectating->GetPosition());
}
if (IsElytraFlying())
{
// Damage elytra, once per second:
{
using namespace std::chrono_literals;
auto & TicksFlying = std::get<BodyStanceGliding>(m_BodyStance).TicksElytraFlying;
const auto TotalFlew = TicksFlying + a_Dt;
const auto Periods = static_cast<short>(TotalFlew / 1s);
TicksFlying = std::chrono::duration_cast<cTickTime>(TotalFlew - Periods * 1s);
UseItem(cInventory::invArmorOffset + 1, Periods);
}
// Check if flight is still possible:
if (IsOnGround() || IsInWater() || IsRiding() || (GetEquippedChestplate().m_ItemType != E_ITEM_ELYTRA))
{
SetElytraFlight(false);
}
}
else if (IsInBed())
{
// Check if sleeping is still possible:
if ((GetPosition().Floor() != m_LastBedPos) || (m_World->GetBlock(m_LastBedPos) != E_BLOCK_BED))
{
m_ClientHandle->HandleLeaveBed();
}
}
2021-02-06 18:37:03 +00:00
BroadcastMovementUpdate(m_ClientHandle.get());
if (m_Health > 0) // make sure player is alive
{
m_World->CollectPickupsByPlayer(*this);
if ((m_EatingFinishTick >= 0_tick) && (m_EatingFinishTick <= m_World->GetWorldAge()))
2021-02-06 18:37:03 +00:00
{
FinishEating();
}
HandleFood();
}
if (m_IsFishing)
{
HandleFloater();
}
// Update items (e.g. Maps)
m_Inventory.UpdateItems();
if (m_TicksUntilNextSave == 0)
{
SaveToDisk();
m_TicksUntilNextSave = PLAYER_INVENTORY_SAVE_INTERVAL;
}
else
{
m_TicksUntilNextSave--;
}
}