From fec64bb91c03c5e872a8f6fbc1a253f341373072 Mon Sep 17 00:00:00 2001 From: Persson-dev <66266021+Persson-dev@users.noreply.github.com> Date: Wed, 29 Dec 2021 20:28:41 +0100 Subject: Improved farmer AI & Fixed entity loading functions (#5351) * Allow villagers to pickup items * Add farmer villager harvesting * Use of auto keyword * Using for loop to check adjacent crops * Show particules when farmer harvest * Fix area comment * Move constants to header file * Removing unnecessary semicolon * Initialization of CropBlockType variable * Apply 12xx12 suggestion * Fixing area constant size * Refactor bounding box calculation, use vectors. * Add Api documentation * Update lua docs * Rework farmer ai * Fixing lua docs notes * Add missing capitalisation * Add villagers inventory save * Fixing loading entities from disk inconsistencies * Add farmer harvest animation * Fix beetroots grow state Co-authored-by: Alexander Harkness --- Server/Plugins/APIDump/APIDesc.lua | 24 ++- src/ChunkMap.cpp | 12 +- src/ChunkMap.h | 4 +- src/Defines.cpp | 26 +++ src/Defines.h | 2 + src/Entities/Pawn.cpp | 17 ++ src/Entities/Pickup.cpp | 105 ++++++++--- src/Entities/Pickup.h | 2 +- src/Entities/Player.cpp | 2 - src/Mobs/Villager.cpp | 313 ++++++++++++++++++++++++-------- src/Mobs/Villager.h | 61 +++++-- src/World.cpp | 4 +- src/World.h | 2 +- src/WorldStorage/NBTChunkSerializer.cpp | 3 + src/WorldStorage/WSSAnvil.cpp | 52 +++--- 15 files changed, 479 insertions(+), 150 deletions(-) diff --git a/Server/Plugins/APIDump/APIDesc.lua b/Server/Plugins/APIDump/APIDesc.lua index e2b0043a1..a6b6a957d 100644 --- a/Server/Plugins/APIDump/APIDesc.lua +++ b/Server/Plugins/APIDump/APIDesc.lua @@ -10183,8 +10183,8 @@ a_Player:OpenWindow(Window); Params = { { - Name = "Player", - Type = "cPlayer", + Name = "Entity", + Type = "cEntity", }, }, Returns = @@ -10194,7 +10194,7 @@ a_Player:OpenWindow(Window); Type = "boolean", }, }, - Notes = "Tries to make the player collect the pickup. Returns true if the pickup was collected, at least partially.", + Notes = "Tries to make the entity collect the pickup. Returns true if the pickup was collected, at least partially.", }, GetAge = { @@ -18877,6 +18877,24 @@ end }, Notes = "Returns true if the specified item type is any kind of a tool (axe, hoe, pickaxe, shovel or FIXME: sword)", }, + IsVillagerFood = + { + IsStatic = true, + Params = + { + { + Name = "ItemType", + Type = "number", + }, + }, + Returns = + { + { + Type = "boolean", + }, + }, + Notes = "Returns true if the specified item type is any kind of a pickable food by a villager (potato, carrot, wheat, bread and any kind of seeds).", + } }, AdditionalInfo = { diff --git a/src/ChunkMap.cpp b/src/ChunkMap.cpp index 9e39338de..c3139ed9c 100644 --- a/src/ChunkMap.cpp +++ b/src/ChunkMap.cpp @@ -439,14 +439,14 @@ void cChunkMap::FastSetBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLET -void cChunkMap::CollectPickupsByPlayer(cPlayer & a_Player) +void cChunkMap::CollectPickupsByEntity(cEntity & a_Entity) { cCSLock Lock(m_CSChunks); - auto BoundingBox = a_Player.GetBoundingBox(); + auto BoundingBox = a_Entity.GetBoundingBox(); BoundingBox.Expand(1, 0.5, 1); - ForEachEntityInBox(BoundingBox, [&a_Player](cEntity & Entity) + ForEachEntityInBox(BoundingBox, [&a_Entity](cEntity & Entity) { // Only pickups and projectiles can be picked up: if (Entity.IsPickup()) @@ -456,11 +456,11 @@ void cChunkMap::CollectPickupsByPlayer(cPlayer & a_Player) (*itr)->GetUniqueID(), a_Player->GetName().c_str(), SqrDist ); */ - static_cast(Entity).CollectedBy(a_Player); + static_cast(Entity).CollectedBy(a_Entity); } - else if (Entity.IsProjectile()) + else if (Entity.IsProjectile() && a_Entity.IsPlayer()) { - static_cast(Entity).CollectedBy(a_Player); + static_cast(Entity).CollectedBy(static_cast(a_Entity)); } // The entities will MarkDirty when they Destroy themselves diff --git a/src/ChunkMap.h b/src/ChunkMap.h index 611f08cc9..578c49b8a 100644 --- a/src/ChunkMap.h +++ b/src/ChunkMap.h @@ -102,8 +102,8 @@ public: If the chunk is invalid, the operation is ignored silently. */ void FastSetBlock(Vector3i a_BlockPos, BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta); - /** Makes the specified player collect all the pickups around them. */ - void CollectPickupsByPlayer(cPlayer & a_Player); + /** Makes the specified entity collect all the pickups around them. */ + void CollectPickupsByEntity(cEntity & a_Entity); BLOCKTYPE GetBlock (Vector3i a_BlockPos) const; NIBBLETYPE GetBlockMeta (Vector3i a_BlockPos) const; diff --git a/src/Defines.cpp b/src/Defines.cpp index 02b9f28d5..aa5e46995 100644 --- a/src/Defines.cpp +++ b/src/Defines.cpp @@ -623,3 +623,29 @@ bool ItemCategory::IsHorseArmor(short a_ItemType) } } } + + + + + +bool ItemCategory::IsVillagerFood(short a_ItemType) +{ + switch (a_ItemType) + { + case E_ITEM_CARROT: + case E_ITEM_POTATO: + case E_ITEM_BREAD: + case E_ITEM_BEETROOT: + case E_ITEM_SEEDS: + case E_ITEM_BEETROOT_SEEDS: + case E_ITEM_WHEAT: + { + return true; + } + + default: + { + return false; + } + } +} diff --git a/src/Defines.h b/src/Defines.h index 1dbcdda34..899bc8c2d 100644 --- a/src/Defines.h +++ b/src/Defines.h @@ -675,6 +675,8 @@ namespace ItemCategory bool IsArmor(short a_ItemType); bool IsHorseArmor(short a_ItemType); + + bool IsVillagerFood(short a_ItemType); } // tolua_end diff --git a/src/Entities/Pawn.cpp b/src/Entities/Pawn.cpp index a1c3a7610..e227dca6f 100644 --- a/src/Entities/Pawn.cpp +++ b/src/Entities/Pawn.cpp @@ -102,6 +102,23 @@ void cPawn::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) return; } HandleFalling(); + + // Handle item pickup + if (m_Health > 0) + { + if (IsPlayer()) + { + m_World->CollectPickupsByEntity(*this); + } + else if (IsMob()) + { + cMonster & Mob = static_cast(*this); + if (Mob.CanPickUpLoot()) + { + m_World->CollectPickupsByEntity(*this); + } + } + } } diff --git a/src/Entities/Pickup.cpp b/src/Entities/Pickup.cpp index fd2cc3a4b..bbc3313da 100644 --- a/src/Entities/Pickup.cpp +++ b/src/Entities/Pickup.cpp @@ -7,10 +7,12 @@ #include "Pickup.h" #include "Player.h" +#include "../Mobs/Villager.h" #include "../ClientHandle.h" #include "../World.h" #include "../Server.h" #include "../Bindings/PluginManager.h" +#include "../Registries/Items.h" #include "../Root.h" #include "../Chunk.h" @@ -209,7 +211,7 @@ bool cPickup::DoTakeDamage(TakeDamageInfo & a_TDI) -bool cPickup::CollectedBy(cPlayer & a_Dest) +bool cPickup::CollectedBy(cEntity & a_Dest) { if (m_bCollected) { @@ -217,6 +219,12 @@ bool cPickup::CollectedBy(cPlayer & a_Dest) return false; // It's already collected! } + // This type of entity can't pickup items + if (!a_Dest.IsPawn()) + { + return false; + } + // Two seconds if player created the pickup (vomiting), half a second if anything else if (m_Timer < (m_bIsPlayerCreated ? std::chrono::seconds(2) : std::chrono::milliseconds(500))) { @@ -224,43 +232,84 @@ bool cPickup::CollectedBy(cPlayer & a_Dest) return false; // Not old enough } - // If the player is a spectator, he cannot collect anything - if (a_Dest.IsGameModeSpectator()) + // Checking for villagers + if (!a_Dest.IsPlayer() && a_Dest.IsMob()) { - return false; - } - if (cRoot::Get()->GetPluginManager()->CallHookCollectingPickup(a_Dest, *this)) - { - // LOG("Pickup %d cannot be collected by \"%s\", because a plugin has said no.", m_UniqueID, a_Dest->GetName().c_str()); - return false; - } + auto & Mob = static_cast(a_Dest); + if (Mob.GetMobType() == mtVillager) + { + // Villagers only pickup food + if (!ItemCategory::IsVillagerFood(m_Item.m_ItemType)) + { + return false; + } + + auto & Villager = static_cast(Mob); + int NumAdded = Villager.GetInventory().AddItem(m_Item); + if (NumAdded > 0) + { + m_Item.m_ItemCount -= NumAdded; + m_World->BroadcastCollectEntity(*this, a_Dest, static_cast(NumAdded)); + + // Also send the "pop" sound effect with a somewhat random pitch (fast-random using EntityID ;) + m_World->BroadcastSoundEffect("entity.item.pickup", GetPosition(), 0.3f, (1.2f + (static_cast((GetUniqueID() * 23) % 32)) / 64)); + if (m_Item.m_ItemCount <= 0) + { + // All of the pickup has been collected, schedule the pickup for destroying + m_bCollected = true; + } + m_Timer = std::chrono::milliseconds(0); + return true; + } + // Pickup cannot be collected because the entity has not enough space + return false; + } - int NumAdded = a_Dest.GetInventory().AddItem(m_Item); - if (NumAdded > 0) + } + else if (a_Dest.IsPlayer()) { - // Check achievements - switch (m_Item.m_ItemType) + + auto & Player = static_cast(a_Dest); + + // If the player is a spectator, he cannot collect anything + if (Player.IsGameModeSpectator()) { - case E_BLOCK_LOG: a_Dest.AwardAchievement(CustomStatistic::AchMineWood); break; - case E_ITEM_LEATHER: a_Dest.AwardAchievement(CustomStatistic::AchKillCow); break; - case E_ITEM_DIAMOND: a_Dest.AwardAchievement(CustomStatistic::AchDiamonds); break; - case E_ITEM_BLAZE_ROD: a_Dest.AwardAchievement(CustomStatistic::AchBlazeRod); break; - default: break; + return false; } - m_Item.m_ItemCount -= NumAdded; - m_World->BroadcastCollectEntity(*this, a_Dest, static_cast(NumAdded)); + if (cRoot::Get()->GetPluginManager()->CallHookCollectingPickup(Player, *this)) + { + // LOG("Pickup %d cannot be collected by \"%s\", because a plugin has said no.", m_UniqueID, a_Dest->GetName().c_str()); + return false; + } - // Also send the "pop" sound effect with a somewhat random pitch (fast-random using EntityID ;) - m_World->BroadcastSoundEffect("entity.item.pickup", GetPosition(), 0.3f, (1.2f + (static_cast((GetUniqueID() * 23) % 32)) / 64)); - if (m_Item.m_ItemCount <= 0) + int NumAdded = Player.GetInventory().AddItem(m_Item); + if (NumAdded > 0) { - // All of the pickup has been collected, schedule the pickup for destroying - m_bCollected = true; + // Check achievements + switch (m_Item.m_ItemType) + { + case E_BLOCK_LOG: Player.AwardAchievement(CustomStatistic::AchMineWood); break; + case E_ITEM_LEATHER: Player.AwardAchievement(CustomStatistic::AchKillCow); break; + case E_ITEM_DIAMOND: Player.AwardAchievement(CustomStatistic::AchDiamonds); break; + case E_ITEM_BLAZE_ROD: Player.AwardAchievement(CustomStatistic::AchBlazeRod); break; + default: break; + } + + m_Item.m_ItemCount -= NumAdded; + m_World->BroadcastCollectEntity(*this, a_Dest, static_cast(NumAdded)); + + // Also send the "pop" sound effect with a somewhat random pitch (fast-random using EntityID ;) + m_World->BroadcastSoundEffect("entity.item.pickup", GetPosition(), 0.3f, (1.2f + (static_cast((GetUniqueID() * 23) % 32)) / 64)); + if (m_Item.m_ItemCount <= 0) + { + // All of the pickup has been collected, schedule the pickup for destroying + m_bCollected = true; + } + m_Timer = std::chrono::milliseconds(0); + return true; } - m_Timer = std::chrono::milliseconds(0); - return true; } // LOG("Pickup %d cannot be collected by \"%s\", because there's no space in the inventory.", a_Dest->GetName().c_str(), m_UniqueID); diff --git a/src/Entities/Pickup.h b/src/Entities/Pickup.h index 73540a64e..c995055ff 100644 --- a/src/Entities/Pickup.h +++ b/src/Entities/Pickup.h @@ -33,7 +33,7 @@ public: // tolua_export virtual void SpawnOn(cClientHandle & a_ClientHandle) override; - bool CollectedBy(cPlayer & a_Dest); // tolua_export + bool CollectedBy(cEntity & a_Dest); // tolua_export virtual void Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp index 1d3bad306..b0ab94874 100644 --- a/src/Entities/Player.cpp +++ b/src/Entities/Player.cpp @@ -3213,8 +3213,6 @@ void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) if (m_Health > 0) // make sure player is alive { - m_World->CollectPickupsByPlayer(*this); - if ((m_EatingFinishTick >= 0_tick) && (m_EatingFinishTick <= m_World->GetWorldAge())) { FinishEating(); diff --git a/src/Mobs/Villager.cpp b/src/Mobs/Villager.cpp index 46dd613f1..aa5409e4d 100644 --- a/src/Mobs/Villager.cpp +++ b/src/Mobs/Villager.cpp @@ -15,7 +15,8 @@ cVillager::cVillager(eVillagerType VillagerType) : Super("Villager", mtVillager, "entity.villager.hurt", "entity.villager.death", "entity.villager.ambient", 0.6f, 1.95f), m_ActionCountDown(-1), m_Type(VillagerType), - m_VillagerAction(false) + m_FarmerAction(faIdling), + m_Inventory(8, 1) { } @@ -60,46 +61,12 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) return; } - if (m_ActionCountDown > -1) - { - m_ActionCountDown--; - if (m_ActionCountDown == 0) - { - switch (m_Type) - { - case vtFarmer: - { - HandleFarmerPlaceCrops(); - } - } - } - return; - } - - if (m_VillagerAction) - { - switch (m_Type) - { - case vtFarmer: - { - HandleFarmerTryHarvestCrops(); - } - } - m_VillagerAction = false; - return; - } - - // Don't always try to do a special action. Each tick has 1% to do a special action. - if (GetRandomProvider().RandBool(0.99)) - { - return; - } - switch (m_Type) { case vtFarmer: { - HandleFarmerPrepareFarmCrops(); + TickFarmer(); + break; } } } @@ -130,50 +97,111 @@ void cVillager::KilledBy(TakeDamageInfo & a_TDI) //////////////////////////////////////////////////////////////////////////////// // Farmer functions: -void cVillager::HandleFarmerPrepareFarmCrops() +void cVillager::TickFarmer() { + + // Don't harvest crops if you must not if (!m_World->VillagersShouldHarvestCrops()) { return; } - cBlockArea Surrounding; + // This is to prevent undefined behaviors + if (m_FinalDestination.y <= 0) + { + return; + } - // Read a 11x7x11 area: + if (!IsIdling()) + { + // Forcing the farmer to go to work spots. + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + + // Forcing the farmer to look at the work spots. + Vector3d Direction = (m_FinalDestination - (GetPosition() + Vector3d(0, 1.6, 0))); // We get the direction from the eyes of the farmer to the work spot. + Direction.Normalize(); + SetPitch(std::asin(-Direction.y) / M_PI * 180); + } + + // Updating the timer + if (m_ActionCountDown > -1) + { + m_ActionCountDown--; + } + + // Searching for work in blocks where the farmer goes. + if (IsHarvestable(m_FinalDestination.Floor())) + { + m_CropsPos = m_FinalDestination.Floor(); + m_FarmerAction = faHarvesting; + HandleFarmerTryHarvestCrops(); + return; + } + else if (IsPlantable(m_FinalDestination.Floor()) && CanPlantCrops()) + { + m_CropsPos = m_FinalDestination.Floor(); + m_FarmerAction = faPlanting; + HandleFarmerTryPlaceCrops(); + return; + } + else + { + m_FarmerAction = faIdling; // Returning to idling. + } + + + // Don't always try to do a special action. Each tick has 10% to do a special action. + if (GetRandomProvider().RandBool(FARMER_SPECIAL_ACTION_CHANCE)) + { + ScanAreaForWork(); + } + +} + + + + + +void cVillager::ScanAreaForWork() +{ + + auto Pos = GetPosition().Floor(); + auto MinPos = Pos - FARMER_SCAN_CROPS_DIST; + auto MaxPos = Pos + FARMER_SCAN_CROPS_DIST; + + // Read area to be checked for crops. + cBlockArea Surrounding; Surrounding.Read( *m_World, - FloorC(GetPosX()) - 5, - FloorC(GetPosX()) + 6, - FloorC(GetPosY()) - 3, - FloorC(GetPosY()) + 4, - FloorC(GetPosZ()) - 5, - FloorC(GetPosZ()) + 6 + MinPos, MaxPos ); - for (int I = 0; I < 5; I++) + for (int I = 0; I < FARMER_RANDOM_TICK_SPEED; I++) { - for (int Y = 0; Y < 6; Y++) + for (int Y = MinPos.y; Y <= MaxPos.y; Y++) { // Pick random coordinates and check for crops. - int X = m_World->GetTickRandomNumber(11); - int Z = m_World->GetTickRandomNumber(11); + Vector3i CandidatePos(MinPos.x + m_World->GetTickRandomNumber(MaxPos.x - MinPos.x - 1), Y, MinPos.z + m_World->GetTickRandomNumber(MaxPos.z - MinPos.z - 1)); - // A villager can't farm this. - if (!IsBlockFarmable(Surrounding.GetRelBlockType(X, Y, Z))) + // A villager can harvest this. + if (IsHarvestable(CandidatePos)) { - continue; + m_CropsPos = CandidatePos; + m_FarmerAction = faHarvesting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; } - if (Surrounding.GetRelBlockMeta(X, Y, Z) != 0x7) + // A villager can plant this. + else if (IsPlantable(CandidatePos) && CanPlantCrops()) { - continue; + m_CropsPos = CandidatePos; + m_FarmerAction = faHarvesting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; } - m_VillagerAction = true; - m_CropsPos = Vector3i(static_cast(GetPosX()) + X - 5, static_cast(GetPosY()) + Y - 3, static_cast(GetPosZ()) + Z - 5); - MoveToPosition(Vector3d(m_CropsPos.x + 0.5, m_CropsPos.y + 0.0, m_CropsPos.z + 0.5)); - return; - } // for Y loop. - } // Repeat the procces 5 times. + } // for Y + } // Repeat the proccess according to the random tick speed. } @@ -182,15 +210,22 @@ void cVillager::HandleFarmerPrepareFarmCrops() void cVillager::HandleFarmerTryHarvestCrops() { - // Harvest the crops if the villager isn't moving and if the crops are closer then 2 blocks. - if (!m_PathfinderActivated && (GetPosition() - m_CropsPos).Length() < 2) + if (m_ActionCountDown > 0) + { + // The farmer is still on cooldown + return; + } + + // Harvest the crops if it is closer than 1 block. + if ((GetPosition() - m_CropsPos).Length() < 1) { // Check if the blocks didn't change while the villager was walking to the coordinates. - BLOCKTYPE CropBlock = m_World->GetBlock(m_CropsPos); - if (IsBlockFarmable(CropBlock) && m_World->GetBlockMeta(m_CropsPos) == 0x7) + if (IsHarvestable(m_CropsPos)) { + m_World->BroadcastSoundParticleEffect(EffectID::PARTICLE_BLOCK_BREAK, m_CropsPos, m_World->GetBlock(m_CropsPos)); m_World->DropBlockAsPickups(m_CropsPos, this, nullptr); - m_ActionCountDown = 20; + // Applying 0.5 second cooldown. + m_ActionCountDown = 10; } } } @@ -199,12 +234,113 @@ void cVillager::HandleFarmerTryHarvestCrops() -void cVillager::HandleFarmerPlaceCrops() +void cVillager::CheckForNearbyCrops() { + + // Search for adjacent crops + + constexpr std::array Directions = { Vector3i{0, 0, -1}, {0, 0, 1}, {1, 0, 0}, {-1, 0, 0} }; + + for (Vector3i Direction : Directions) + { + if (IsHarvestable(m_CropsPos + Direction)) + { + m_CropsPos += Direction; + m_FarmerAction = faHarvesting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; + } + else if (IsPlantable(m_CropsPos + Direction) && CanPlantCrops()) + { + m_CropsPos += Direction; + m_FarmerAction = faPlanting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; + } + + } + + // There is no more work to do around the previous crops. + m_FarmerAction = faIdling; +} + + + + + +void cVillager::HandleFarmerTryPlaceCrops() +{ + + if ((GetPosition() - m_CropsPos).Length() > 1) + { + // The farmer is still to far from the final destination + return; + } + + if (m_ActionCountDown > 0) + { + // The farmer is still on cooldown + return; + } + // Check if there is still farmland at the spot where the crops were. - if (m_World->GetBlock(m_CropsPos.addedY(-1)) == E_BLOCK_FARMLAND) + if (IsPlantable(m_CropsPos)) { - m_World->SetBlock(m_CropsPos, E_BLOCK_CROPS, 0); + // Finding the item to use to plant a crop + int TargetSlot = -1; + BLOCKTYPE CropBlockType = E_BLOCK_AIR; + + for (int I = 0; I < m_Inventory.GetWidth() && TargetSlot < 0; I++) + { + const cItem & Slot = m_Inventory.GetSlot(I); + switch (Slot.m_ItemType) + { + case E_ITEM_SEEDS: + { + TargetSlot = I; + CropBlockType = E_BLOCK_CROPS; + break; + } + + case E_ITEM_BEETROOT_SEEDS: + { + TargetSlot = I; + CropBlockType = E_BLOCK_BEETROOTS; + break; + } + + case E_ITEM_POTATO: + { + TargetSlot = I; + CropBlockType = E_BLOCK_POTATOES; + break; + } + + case E_ITEM_CARROT: + { + TargetSlot = I; + CropBlockType = E_BLOCK_CARROTS; + break; + } + + default: + { + break; + } + } + } + + // Removing item from villager inventory + m_Inventory.RemoveOneItem(TargetSlot); + + // Placing crop block + m_World->SetBlock(m_CropsPos, CropBlockType, 0); + + // Applying 1 second cooldown + m_ActionCountDown = 20; + + // Try to do the same with adjacent crops. + CheckForNearbyCrops(); } } @@ -212,16 +348,33 @@ void cVillager::HandleFarmerPlaceCrops() -bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType) +bool cVillager::CanPlantCrops() +{ + return m_Inventory.HasItems(cItem(E_ITEM_SEEDS)) || + m_Inventory.HasItems(cItem(E_ITEM_BEETROOT_SEEDS)) || + m_Inventory.HasItems(cItem(E_ITEM_POTATO)) || + m_Inventory.HasItems(cItem(E_ITEM_CARROT)); +} + + + + + +bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta) { switch (a_BlockType) { case E_BLOCK_BEETROOTS: + { + // The crop must have fully grown up. + return a_BlockMeta == 0x03; + } case E_BLOCK_CROPS: case E_BLOCK_POTATOES: case E_BLOCK_CARROTS: { - return true; + // The crop must have fully grown up. + return a_BlockMeta == 0x07; } default: return false; } @@ -231,6 +384,24 @@ bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType) +bool cVillager::IsHarvestable(Vector3i a_CropsPos) +{ + return IsBlockFarmable(m_World->GetBlock(a_CropsPos), m_World->GetBlockMeta(a_CropsPos)); +} + + + + + +bool cVillager::IsPlantable(Vector3i a_CropsPos) +{ + return (m_World->GetBlock(a_CropsPos.addedY(-1)) == E_BLOCK_FARMLAND) && (m_World->GetBlock(a_CropsPos) == E_BLOCK_AIR); +} + + + + + cVillager::eVillagerType cVillager::GetRandomProfession() { int Profession = GetRandomProvider().RandInt(cVillager::eVillagerType::vtMax - 1); diff --git a/src/Mobs/Villager.h b/src/Mobs/Villager.h index 4ac06765c..ef1c9e70f 100644 --- a/src/Mobs/Villager.h +++ b/src/Mobs/Villager.h @@ -3,6 +3,7 @@ #include "PassiveMonster.h" #include "../Blocks/ChunkInterface.h" +#include "../Inventory.h" @@ -38,30 +39,68 @@ public: virtual void KilledBy (TakeDamageInfo & a_TDI) override; // cVillager functions - /** return true if the given blocktype are: crops, potatoes or carrots. */ - bool IsBlockFarmable(BLOCKTYPE a_BlockType); + /** Returns the villager hidden inventory (8 slots). */ + cItemGrid & GetInventory(void) { return m_Inventory; } + const cItemGrid & GetInventory(void) const { return m_Inventory; } + + /** Returns true if the given blocktype are: crops, potatoes or carrots and they have full grown up. */ + bool IsBlockFarmable(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta); + + /** Returns true if the block at the given location is a fully grown up crop. */ + bool IsHarvestable(Vector3i a_CropsPos); + + /** Returns true if seeds can be planted at a given location. */ + bool IsPlantable(Vector3i a_CropsPos); // Farmer functions - /** Searches in a 11x7x11 area for crops. If it found some it will navigate to them. */ - void HandleFarmerPrepareFarmCrops(); + enum eFarmerAction + { + faIdling, + faPlanting, + faHarvesting, + } ; + + static const int FARMER_RANDOM_TICK_SPEED = 5; + /** With 10% chance, it takes about 20 seconds to find a spot. */ + static constexpr double FARMER_SPECIAL_ACTION_CHANCE = 0.1; + /** This distance from the Villager makes for a 31x3x31 area. */ + static constexpr Vector3i FARMER_SCAN_CROPS_DIST {15, 1, 15}; - /** Looks if the farmer has reached it's destination, and if it's still crops and the destination is closer then 2 blocks it will harvest them. */ + /** Tick function for farmers. */ + void TickFarmer(); + + /** Searches in a 31x3x31 area to harvest crops or spaces to plant crops. If it found some it will navigate to them. */ + void ScanAreaForWork(); + + /** Looks if the farmer has reached it's destination, and if it's still crops and the destination is closer then 1 block it will harvest them. */ void HandleFarmerTryHarvestCrops(); - /** Replaces the crops he harvested. */ - void HandleFarmerPlaceCrops(); + /** Looks if the farmer has reached it's destination, and if it's still non obstructed farmland and the destination is closer then 1 block it will plant crops. */ + void HandleFarmerTryPlaceCrops(); + + /** Checking for harvesting or planting nearby crops */ + void CheckForNearbyCrops(); + + /** Returns whether the farmer has crops in his inventory to plant. */ + bool CanPlantCrops(); + + /** Returns whether the farmer is not working. */ + bool IsIdling() + { + return m_FarmerAction == faIdling; + } // Get and set functions. - int GetVilType(void) const { return m_Type; } - Vector3i GetCropsPos(void) const { return m_CropsPos; } - bool DoesHaveActionActivated(void) const { return m_VillagerAction; } + int GetVilType(void) const { return m_Type; } + eFarmerAction GetFarmerAction(void) const { return m_FarmerAction; } private: int m_ActionCountDown; int m_Type; - bool m_VillagerAction; + eFarmerAction m_FarmerAction; Vector3i m_CropsPos; + cItemGrid m_Inventory; } ; diff --git a/src/World.cpp b/src/World.cpp index 94fc0ba00..8ed179050 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -2241,9 +2241,9 @@ void cWorld::QueueUnloadUnusedChunks(void) -void cWorld::CollectPickupsByPlayer(cPlayer & a_Player) +void cWorld::CollectPickupsByEntity(cEntity & a_Entity) { - m_ChunkMap.CollectPickupsByPlayer(a_Player); + m_ChunkMap.CollectPickupsByEntity(a_Entity); } diff --git a/src/World.h b/src/World.h index eab6422f4..84c184819 100644 --- a/src/World.h +++ b/src/World.h @@ -234,7 +234,7 @@ public: /** Queues a task to unload unused chunks onto the tick thread. The prefferred way of unloading. */ void QueueUnloadUnusedChunks(void); // tolua_export - void CollectPickupsByPlayer(cPlayer & a_Player); + void CollectPickupsByEntity(cEntity & a_Entity); /** Calls the callback for each player in the list; returns true if all players processed, false if the callback aborted by returning true */ virtual bool ForEachPlayer(cPlayerListCallback a_Callback) override; // >> EXPORTED IN MANUALBINDINGS << diff --git a/src/WorldStorage/NBTChunkSerializer.cpp b/src/WorldStorage/NBTChunkSerializer.cpp index e96acccef..dde6c299c 100644 --- a/src/WorldStorage/NBTChunkSerializer.cpp +++ b/src/WorldStorage/NBTChunkSerializer.cpp @@ -893,6 +893,9 @@ public: const cVillager *Villager = static_cast(a_Monster); mWriter.AddInt("Profession", Villager->GetVilType()); mWriter.AddInt("Age", Villager->GetAge()); + mWriter.BeginList("Inventory", TAG_Compound); + AddItemGrid(Villager->GetInventory()); + mWriter.EndList(); break; } case mtWither: diff --git a/src/WorldStorage/WSSAnvil.cpp b/src/WorldStorage/WSSAnvil.cpp index dbbd03daf..dfefb74d3 100644 --- a/src/WorldStorage/WSSAnvil.cpp +++ b/src/WorldStorage/WSSAnvil.cpp @@ -1663,29 +1663,29 @@ void cWSSAnvil::LoadEntityFromNBT(cEntityList & a_Entities, const cParsedNBT & a case mtHoglin: return LoadHoglinFromNBT(a_Entities, a_NBT, a_EntityTagIdx); case mtHusk: return LoadHuskFromNBT(a_Entities, a_NBT, a_EntityTagIdx); case mtIllusioner: return LoadIllusionerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtIronGolem: return LoadVillagerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtLlama: return LoadIronGolemFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtMagmaCube: return LoadLlamaFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtMooshroom: return LoadMagmaCubeFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtMule: return LoadMooshroomFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtOcelot: return LoadMuleFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPanda: return LoadOcelotFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtParrot: return LoadPandaFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPhantom: return LoadParrotFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPig: return LoadPhantomFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPiglin: return LoadPigFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPiglinBrute: return LoadPiglinFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPillager: return LoadPiglinBruteFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPolarBear: return LoadPillagerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtPufferfish: return LoadPolarBearFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtRabbit: return LoadPufferfishFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtRavager: return LoadRabbitFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtSalmon: return LoadRavagerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtSheep: return LoadSalmonFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtShulker: return LoadSheepFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtSilverfish: return LoadShulkerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtSkeleton: return LoadSilverfishFromNBT(a_Entities, a_NBT, a_EntityTagIdx); - case mtSkeletonHorse: return LoadSkeletonFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtIronGolem: return LoadIronGolemFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtLlama: return LoadLlamaFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtMagmaCube: return LoadMagmaCubeFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtMooshroom: return LoadMooshroomFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtMule: return LoadMuleFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtOcelot: return LoadOcelotFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPanda: return LoadPandaFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtParrot: return LoadParrotFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPhantom: return LoadPhantomFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPig: return LoadPigFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPiglin: return LoadPiglinFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPiglinBrute: return LoadPiglinBruteFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPillager: return LoadPillagerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPolarBear: return LoadPolarBearFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtPufferfish: return LoadPufferfishFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtRabbit: return LoadRabbitFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtRavager: return LoadRavagerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtSalmon: return LoadSalmonFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtSheep: return LoadSheepFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtShulker: return LoadShulkerFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtSilverfish: return LoadSilverfishFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtSkeleton: return LoadSkeletonFromNBT(a_Entities, a_NBT, a_EntityTagIdx); + case mtSkeletonHorse: return LoadSkeletonHorseFromNBT(a_Entities, a_NBT, a_EntityTagIdx); case mtSlime: return LoadSlimeFromNBT(a_Entities, a_NBT, a_EntityTagIdx); case mtSnowGolem: return LoadSnowGolemFromNBT(a_Entities, a_NBT, a_EntityTagIdx); case mtSpider: return LoadSpiderFromNBT(a_Entities, a_NBT, a_EntityTagIdx); @@ -3270,6 +3270,12 @@ void cWSSAnvil::LoadVillagerFromNBT(cEntityList & a_Entities, const cParsedNBT & Monster->SetAge(Age); } + int InventoryIdx = a_NBT.FindChildByName(a_TagIdx, "Inventory"); + if (InventoryIdx > 0) + { + LoadItemGridFromNBT(Monster->GetInventory(), a_NBT, InventoryIdx); + } + a_Entities.emplace_back(std::move(Monster)); } -- cgit v1.2.3