From 672bb0457012612ef59502b33717ee789c4d6bfe Mon Sep 17 00:00:00 2001 From: 0ddlyoko Date: Fri, 6 Nov 2020 17:54:01 +0100 Subject: Add correct implementation of crops (#4802) * [FIX] Add correct implementation of seed drops. > Official percentage of drops has been implemented * Fix C++ conventions * Change "Vals" variable to "m_Vals" * [FIX] Add correct implementation of Carrots, Potatoes, Wheat & Beetroots seed * Add Fortune support with crops Add fortune support with Wheat, Carrots, Potatoes & Beetroots seeds * [FIX] Right-clicking on a grown Beetroot in survival consume 2 bone meals Fix #4805 * Add documentation for "cWorld::IsFullGrownPlantAt" method * Fix dispenser that full grown a plant > Change methods cItemDyeHandler::FertilizePlant & cItemDyeHandler::growPlantsAround to static * Display particle even if tree doesn't grow * When right-clicking on a full grown melon / pumpkin seed, no longer produce a melon / pumpkin Before this commit, when you right-click on a melon or a pumpkin seed, a melon / pumpkin block spawned. With this commit, it no longer spawns * [FIX] Do not create melon / pumpkin block when right-clicking with a bone meal This fix will prevent the creation of a melon / pumpkin block when you right-click with a bone meal on a melon / pumpkin plant - It just detect if the plant is full grown. if yes, the method "Grow" is not called - Remove IsFullGrownPlant Co-authored-by: 12xx12 <12xx12100@gmail.com> Co-authored-by: Tiger Wang --- Server/Plugins/APIDump/Classes/World.lua | 121 +--------------------- src/BlockEntities/DispenserEntity.cpp | 79 ++++++++------- src/Blocks/BlockCrops.h | 45 ++++---- src/Blocks/BlockPlant.h | 11 +- src/Blocks/BlockStems.h | 169 ++++++++++++++++++++----------- src/Chunk.cpp | 4 +- src/Items/ItemDye.h | 51 +++++----- src/World.cpp | 30 ------ src/World.h | 12 +-- 9 files changed, 217 insertions(+), 305 deletions(-) diff --git a/Server/Plugins/APIDump/Classes/World.lua b/Server/Plugins/APIDump/Classes/World.lua index 2f5d0b8f2..a1f2bc979 100644 --- a/Server/Plugins/APIDump/Classes/World.lua +++ b/Server/Plugins/APIDump/Classes/World.lua @@ -2096,64 +2096,6 @@ function OnAllChunksAvailable() All return values from the callbacks are i }, Notes = "Returns the total age of the world, in ticks. The age always grows, cannot be set by plugins and is unrelated to TimeOfDay.", }, - GrowCactus = - { - Params = - { - { - Name = "BlockX", - Type = "number", - }, - { - Name = "BlockY", - Type = "number", - }, - { - Name = "BlockZ", - Type = "number", - }, - { - Name = "NumBlocksToGrow", - Type = "number", - }, - }, - Returns = - { - { - Type = "number", - }, - }, - Notes = "OBSOLETE, use GrowPlantAt instead. Grows a cactus block at the specified coords, by up to the specified number of blocks. Adheres to the world's maximum cactus growth (GetMaxCactusHeight()). Returns the amount of blocks the cactus grew inside this call.", - }, - GrowMelonPumpkin = - { - Params = - { - { - Name = "BlockX", - Type = "number", - }, - { - Name = "BlockY", - Type = "number", - }, - { - Name = "BlockZ", - Type = "number", - }, - { - Name = "StemBlockType", - Type = "number", - }, - }, - Returns = - { - { - Type = "boolean", - }, - }, - Notes = "OBSOLETE, use GrowPlantAt instead. Grows a melon or pumpkin, based on the stem block type specified (assumed to be at the coords provided). Checks for normal melon / pumpkin growth conditions - stem not having another produce next to it and suitable ground below. Returns true if the melon or pumpkin grew successfully.", - }, GrowPlantAt = { Params = @@ -2176,76 +2118,21 @@ function OnAllChunksAvailable() All return values from the callbacks are i Notes = "Grows the plant at the specified block by the specified number of stages. Returns the number of stages actually grown. Returns zero for non-growable blocks.", }, GrowRipePlant = - { - { - Params = - { - { - Name = "BlockX", - Type = "number", - }, - { - Name = "BlockY", - Type = "number", - }, - { - Name = "BlockZ", - Type = "number", - }, - }, - Returns = - { - { - Type = "boolean", - }, - }, - Notes = "OBSOLETE, use the Vector3-based overload instead. Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.", - }, - { - Params = - { - { - Name = "BlockPos", - Type = "Vector3i", - }, - }, - Returns = - { - { - Type = "boolean", - }, - }, - Notes = "Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.", - }, - }, - GrowSugarcane = { Params = { { - Name = "BlockX", - Type = "number", - }, - { - Name = "BlockY", - Type = "number", - }, - { - Name = "BlockZ", - Type = "number", - }, - { - Name = "NumBlocksToGrow", - Type = "number", + Name = "BlockPos", + Type = "Vector3i", }, }, Returns = { { - Type = "number", + Type = "boolean", }, }, - Notes = "OBSOLETE, use GrowPlantAt instead. Grows a sugarcane block at the specified coords, by up to the specified number of blocks. Adheres to the world's maximum sugarcane growth (GetMaxSugarcaneHeight()). Returns the amount of blocks the sugarcane grew inside this call.", + Notes = "Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.", }, GrowTree = { diff --git a/src/BlockEntities/DispenserEntity.cpp b/src/BlockEntities/DispenserEntity.cpp index d97d7198e..c2ce6cf7c 100644 --- a/src/BlockEntities/DispenserEntity.cpp +++ b/src/BlockEntities/DispenserEntity.cpp @@ -10,6 +10,7 @@ #include "../Entities/ProjectileEntity.h" #include "../Simulator/FluidSimulator.h" #include "../Items/ItemSpawnEgg.h" +#include "../Items/ItemDye.h" @@ -25,24 +26,24 @@ cDispenserEntity::cDispenserEntity(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) { - Vector3i dispRelCoord(GetRelPos()); - auto meta = a_Chunk.GetMeta(dispRelCoord); - AddDropSpenserDir(dispRelCoord, meta); - auto dispChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(dispRelCoord); - if (dispChunk == nullptr) + Vector3i DispRelCoord(GetRelPos()); + auto Meta = a_Chunk.GetMeta(DispRelCoord); + AddDropSpenserDir(DispRelCoord, Meta); + auto DispChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(DispRelCoord); + if (DispChunk == nullptr) { // Would dispense into / interact with a non-loaded chunk, ignore the tick return; } - BLOCKTYPE dispBlock = dispChunk->GetBlock(dispRelCoord); - auto dispAbsCoord = dispChunk->RelativeToAbsolute(dispRelCoord); + BLOCKTYPE DispBlock = DispChunk->GetBlock(DispRelCoord); + auto DispAbsCoord = DispChunk->RelativeToAbsolute(DispRelCoord); // Dispense the item: const cItem & SlotItem = m_Contents.GetSlot(a_SlotNum); - if (ItemCategory::IsMinecart(SlotItem.m_ItemType) && IsBlockRail(dispBlock)) // only actually place the minecart if there are rails! + if (ItemCategory::IsMinecart(SlotItem.m_ItemType) && IsBlockRail(DispBlock)) // only actually place the minecart if there are rails! { - if (m_World->SpawnMinecart(dispAbsCoord.x + 0.5, dispAbsCoord.y + 0.5, dispAbsCoord.z + 0.5, SlotItem.m_ItemType) != cEntity::INVALID_ID) + if (m_World->SpawnMinecart(DispAbsCoord.x + 0.5, DispAbsCoord.y + 0.5, DispAbsCoord.z + 0.5, SlotItem.m_ItemType) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -52,15 +53,15 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) { case E_ITEM_BUCKET: { - LOGD("Dispensing empty bucket in slot %d; dispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(dispBlock).c_str(), dispBlock); - switch (dispBlock) + LOGD("Dispensing empty bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock); + switch (DispBlock) { case E_BLOCK_STATIONARY_WATER: case E_BLOCK_WATER: { if (ScoopUpLiquid(a_SlotNum, E_ITEM_WATER_BUCKET)) { - dispChunk->SetBlock(dispRelCoord, E_BLOCK_AIR, 0); + DispChunk->SetBlock(DispRelCoord, E_BLOCK_AIR, 0); } break; } @@ -69,7 +70,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) { if (ScoopUpLiquid(a_SlotNum, E_ITEM_LAVA_BUCKET)) { - dispChunk->SetBlock(dispRelCoord, E_BLOCK_AIR, 0); + DispChunk->SetBlock(DispRelCoord, E_BLOCK_AIR, 0); } break; } @@ -84,10 +85,10 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_WATER_BUCKET: { - LOGD("Dispensing water bucket in slot %d; dispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(dispBlock).c_str(), dispBlock); - if (EmptyLiquidBucket(dispBlock, a_SlotNum)) + LOGD("Dispensing water bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock); + if (EmptyLiquidBucket(DispBlock, a_SlotNum)) { - dispChunk->SetBlock(dispRelCoord, E_BLOCK_WATER, 0); + DispChunk->SetBlock(DispRelCoord, E_BLOCK_WATER, 0); } else { @@ -98,10 +99,10 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_LAVA_BUCKET: { - LOGD("Dispensing lava bucket in slot %d; dispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(dispBlock).c_str(), dispBlock); - if (EmptyLiquidBucket(dispBlock, a_SlotNum)) + LOGD("Dispensing lava bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock); + if (EmptyLiquidBucket(DispBlock, a_SlotNum)) { - dispChunk->SetBlock(dispRelCoord, E_BLOCK_LAVA, 0); + DispChunk->SetBlock(DispRelCoord, E_BLOCK_LAVA, 0); } else { @@ -112,10 +113,10 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_SPAWN_EGG: { - double MobX = 0.5 + dispAbsCoord.x; - double MobZ = 0.5 + dispAbsCoord.z; + double MobX = 0.5 + DispAbsCoord.x; + double MobZ = 0.5 + DispAbsCoord.z; auto MonsterType = cItemSpawnEggHandler::ItemDamageToMonsterType(m_Contents.GetSlot(a_SlotNum).m_ItemDamage); - if (m_World->SpawnMob(MobX, dispAbsCoord.y, MobZ, MonsterType, false) != cEntity::INVALID_ID) + if (m_World->SpawnMob(MobX, DispAbsCoord.y, MobZ, MonsterType, false) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -125,9 +126,9 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_BLOCK_TNT: { // Spawn a primed TNT entity, if space allows: - if (!cBlockInfo::IsSolid(dispBlock)) + if (!cBlockInfo::IsSolid(DispBlock)) { - m_World->SpawnPrimedTNT(Vector3d(0.5, 0.5, 0.5) + dispAbsCoord, 80, 0); // 80 ticks fuse, no initial velocity + m_World->SpawnPrimedTNT(Vector3d(0.5, 0.5, 0.5) + DispAbsCoord, 80, 0); // 80 ticks fuse, no initial velocity m_Contents.ChangeSlotCount(a_SlotNum, -1); } break; @@ -136,9 +137,9 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_FLINT_AND_STEEL: { // Spawn fire if the block in front is air. - if (dispBlock == E_BLOCK_AIR) + if (DispBlock == E_BLOCK_AIR) { - dispChunk->SetBlock(dispRelCoord, E_BLOCK_FIRE, 0); + DispChunk->SetBlock(DispRelCoord, E_BLOCK_FIRE, 0); bool ItemBroke = m_Contents.DamageItem(a_SlotNum, 1); @@ -152,7 +153,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_FIRE_CHARGE: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkFireCharge, GetShootVector(meta) * 20) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkFireCharge, GetShootVector(Meta) * 20) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -161,7 +162,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_ARROW: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkArrow, GetShootVector(meta) * 30 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkArrow, GetShootVector(Meta) * 30 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -170,7 +171,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_SNOWBALL: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkSnowball, GetShootVector(meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkSnowball, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -179,7 +180,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_EGG: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkEgg, GetShootVector(meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkEgg, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -188,7 +189,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_BOTTLE_O_ENCHANTING: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkExpBottle, GetShootVector(meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkExpBottle, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -197,7 +198,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_POTION: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkSplashPotion, GetShootVector(meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkSplashPotion, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -211,7 +212,9 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) DropFromSlot(a_Chunk, a_SlotNum); break; } - if (m_World->GrowRipePlant(dispAbsCoord.x, dispAbsCoord.y, dispAbsCoord.z, true)) + + // Simulate a right-click with bonemeal: + if (cItemDyeHandler::FertilizePlant(*m_World, DispAbsCoord)) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } @@ -225,13 +228,13 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_ACACIA_BOAT: case E_ITEM_DARK_OAK_BOAT: { - Vector3d spawnPos = dispAbsCoord; - if (IsBlockWater(dispBlock)) + Vector3d spawnPos = DispAbsCoord; + if (IsBlockWater(DispBlock)) { // Water next to the dispenser, spawn a boat above the water block spawnPos.y += 1; } - else if (IsBlockWater(dispChunk->GetBlock(dispRelCoord.addedY(-1)))) + else if (IsBlockWater(DispChunk->GetBlock(DispRelCoord.addedY(-1)))) { // Water one block below the dispenser, spawn a boat at the dispenser's Y level // No adjustment needed @@ -243,7 +246,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) break; } - spawnPos += GetShootVector(meta) * 0.8; // A boat is bigger than one block. Add the shoot vector to put it outside the dispenser. + spawnPos += GetShootVector(Meta) * 0.8; // A boat is bigger than one block. Add the shoot vector to put it outside the dispenser. spawnPos += Vector3d(0.5, 0.5, 0.5); if (m_World->SpawnBoat(spawnPos, cBoat::ItemToMaterial(SlotItem))) @@ -255,7 +258,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) case E_ITEM_FIREWORK_ROCKET: { - if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkFirework, GetShootVector(meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID) + if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkFirework, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID) { m_Contents.ChangeSlotCount(a_SlotNum, -1); } diff --git a/src/Blocks/BlockCrops.h b/src/Blocks/BlockCrops.h index aab2c18cd..fe90d6127 100644 --- a/src/Blocks/BlockCrops.h +++ b/src/Blocks/BlockCrops.h @@ -52,37 +52,42 @@ private: } // Fully grown, drop the crop's produce: - cItems res; + cItems Res; + switch (m_BlockType) { case E_BLOCK_BEETROOTS: { const auto SeedCount = CalculateSeedCount(0, 3, ToolFortuneLevel(a_Tool)); - res.Add(E_ITEM_BEETROOT_SEEDS, SeedCount); - res.Add(E_ITEM_BEETROOT); + Res.Add(E_ITEM_BEETROOT_SEEDS, SeedCount); + Res.Add(E_ITEM_BEETROOT); break; } case E_BLOCK_CROPS: { - res.Add(E_ITEM_WHEAT); + // https://minecraft.fandom.com/wiki/Seeds_(Wheat) + Res.Add(E_ITEM_WHEAT); const auto SeedCount = CalculateSeedCount(1, 3, ToolFortuneLevel(a_Tool)); - res.Add(E_ITEM_SEEDS, SeedCount); + Res.Add(E_ITEM_SEEDS, SeedCount); break; } case E_BLOCK_CARROTS: { + // https://minecraft.gamepedia.com/Carrot#Breaking const auto CarrotCount = CalculateSeedCount(1, 4, ToolFortuneLevel(a_Tool)); - res.Add(E_ITEM_CARROT, CarrotCount); + Res.Add(E_ITEM_CARROT, CarrotCount); break; } case E_BLOCK_POTATOES: { + // https://minecraft.gamepedia.com/Potato#Breaking const auto PotatoCount = CalculateSeedCount(2, 3, ToolFortuneLevel(a_Tool)); - res.Add(E_ITEM_POTATO, PotatoCount); + Res.Add(E_ITEM_POTATO, PotatoCount); if (rand.RandBool(0.02)) { - // With a 2% chance, drop a poisonous potato as well - res.Add(E_ITEM_POISONOUS_POTATO); + // https://minecraft.gamepedia.com/Poisonous_Potato#Obtaining + // With a 2% chance, drop a poisonous potato as well: + Res.Add(E_ITEM_POISONOUS_POTATO); } break; } @@ -92,7 +97,7 @@ private: break; } } // switch (m_BlockType) - return res; + return Res; } @@ -101,16 +106,10 @@ private: virtual int Grow(cChunk & a_Chunk, Vector3i a_RelPos, int a_NumStages = 1) const override { - auto oldMeta = a_Chunk.GetMeta(a_RelPos); - if (oldMeta >= RipeMeta) - { - // Already ripe - return 0; - } - auto newMeta = std::min(oldMeta + a_NumStages, RipeMeta); - ASSERT(newMeta > oldMeta); - a_Chunk.GetWorld()->SetBlock(a_Chunk.RelativeToAbsolute(a_RelPos), m_BlockType, static_cast(newMeta)); - return newMeta - oldMeta; + const auto OldMeta = a_Chunk.GetMeta(a_RelPos); + const auto NewMeta = std::clamp(static_cast(OldMeta + a_NumStages), 0, RipeMeta); + a_Chunk.SetMeta(a_RelPos, NewMeta); + return NewMeta - OldMeta; } @@ -131,8 +130,4 @@ private: UNUSED(a_Meta); return 7; } -} ; - - - - +}; diff --git a/src/Blocks/BlockPlant.h b/src/Blocks/BlockPlant.h index 71890b3cf..ab3f137a5 100644 --- a/src/Blocks/BlockPlant.h +++ b/src/Blocks/BlockPlant.h @@ -150,7 +150,10 @@ private: { case paGrowth: { - Grow(a_Chunk, a_RelPos); + if (Grow(a_Chunk, a_RelPos) == 0) + { + BearFruit(a_Chunk, a_RelPos); + } break; } case paDeath: @@ -161,4 +164,10 @@ private: case paStay: break; // do nothing } } + + /** Grows the final produce next to the stem at the specified position. */ + virtual void BearFruit(cChunk & a_Chunk, const Vector3i a_StemRelPos) const + { + // Nothing to do by default + } }; diff --git a/src/Blocks/BlockStems.h b/src/Blocks/BlockStems.h index be64d81f8..fa80d4127 100644 --- a/src/Blocks/BlockStems.h +++ b/src/Blocks/BlockStems.h @@ -24,7 +24,32 @@ private: virtual cItems ConvertToPickups(NIBBLETYPE a_BlockMeta, const cEntity * a_Digger, const cItem * a_Tool) const override { - return cItem(StemPickupType, 1, 0); + /* + Use correct percent: + https://minecraft.gamepedia.com/Melon_Seeds#Breaking + https://minecraft.gamepedia.com/Pumpkin_Seeds#Breaking + */ + + // Age > 7 (Impossible) + if (a_BlockMeta > 7) + { + return cItem(StemPickupType); + } + + const auto Threshold = GetRandomProvider().RandReal(100); + double Cumulative = 0; + char Count = 0; + + for (; Count < 4; Count++) + { + Cumulative += m_AgeSeedDropProbability[static_cast(a_BlockMeta)][static_cast(Count)]; + if (Cumulative > Threshold) + { + break; + } + } + + return cItem(StemPickupType, Count); } @@ -52,60 +77,51 @@ private: virtual int Grow(cChunk & a_Chunk, Vector3i a_RelPos, int a_NumStages = 1) const override { - auto oldMeta = a_Chunk.GetMeta(a_RelPos); - auto meta = oldMeta + a_NumStages; - a_Chunk.SetBlock(a_RelPos, m_BlockType, static_cast(std::min(meta, 7))); // Update the stem - if (meta > 7) - { - if (growProduce(a_Chunk, a_RelPos)) - { - return 8 - oldMeta; - } - else - { - return 7 - oldMeta; - } - } - return meta - oldMeta; + const auto OldMeta = a_Chunk.GetMeta(a_RelPos); + const auto NewMeta = std::clamp(static_cast(OldMeta + a_NumStages), 0, 7); + a_Chunk.SetMeta(a_RelPos, NewMeta); + return NewMeta - OldMeta; } - /** Grows the final produce next to the stem at the specified pos. - Returns true if successful, false if not. */ - static bool growProduce(cChunk & a_Chunk, Vector3i a_StemRelPos) + + + + + virtual void BearFruit(cChunk & a_Chunk, const Vector3i a_StemRelPos) const override { - auto & random = GetRandomProvider(); + auto & Random = GetRandomProvider(); // Check if there's another produce around the stem, if so, abort: - static const Vector3i neighborOfs[] = + static constexpr std::array NeighborOfs = { - { 1, 0, 0}, - {-1, 0, 0}, - { 0, 0, 1}, - { 0, 0, -1}, + { + { 1, 0, 0}, + {-1, 0, 0}, + { 0, 0, 1}, + { 0, 0, -1}, + } }; - bool isValid; - BLOCKTYPE blockType[4]; - NIBBLETYPE blockMeta; // unused - isValid = a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[0], blockType[0], blockMeta); - isValid = isValid && a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[1], blockType[1], blockMeta); - isValid = isValid && a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[2], blockType[2], blockMeta); - isValid = isValid && a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[3], blockType[3], blockMeta); + + std::array BlockType; if ( - !isValid || - (blockType[0] == ProduceBlockType) || - (blockType[1] == ProduceBlockType) || - (blockType[2] == ProduceBlockType) || - (blockType[3] == ProduceBlockType) + !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[0], BlockType[0]) || + !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[1], BlockType[1]) || + !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[2], BlockType[2]) || + !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[3], BlockType[3]) || + (BlockType[0] == ProduceBlockType) || + (BlockType[1] == ProduceBlockType) || + (BlockType[2] == ProduceBlockType) || + (BlockType[3] == ProduceBlockType) ) { - // Neighbors not valid or already taken by the same produce - return false; + // Neighbors not valid or already taken by the same produce: + return; } // Pick a direction in which to place the produce: int x = 0, z = 0; - int checkType = random.RandInt(3); // The index to the neighbors array which should be checked for emptiness - switch (checkType) + const auto CheckType = Random.RandInt(3); // The index to the neighbors array which should be checked for emptiness + switch (CheckType) { case 0: x = 1; break; case 1: x = -1; break; @@ -114,7 +130,7 @@ private: } // Check that the block in that direction is empty: - switch (blockType[checkType]) + switch (BlockType[CheckType]) { case E_BLOCK_AIR: case E_BLOCK_SNOW: @@ -123,39 +139,72 @@ private: { break; } - default: return false; + default: return; } // Check if there's soil under the neighbor. We already know the neighbors are valid. Place produce if ok - BLOCKTYPE soilType; - auto produceRelPos = a_StemRelPos + Vector3i(x, 0, z); - VERIFY(a_Chunk.UnboundedRelGetBlock(produceRelPos.addedY(-1), soilType, blockMeta)); - switch (soilType) + BLOCKTYPE SoilType; + const auto ProduceRelPos = a_StemRelPos + Vector3i(x, 0, z); + VERIFY(a_Chunk.UnboundedRelGetBlockType(ProduceRelPos.addedY(-1), SoilType)); + + switch (SoilType) { case E_BLOCK_DIRT: case E_BLOCK_GRASS: case E_BLOCK_FARMLAND: { - // Place a randomly-facing produce: - NIBBLETYPE meta = (ProduceBlockType == E_BLOCK_MELON) ? 0 : static_cast(random.RandInt(4) % 4); - auto produceAbsPos = a_Chunk.RelativeToAbsolute(produceRelPos); + const NIBBLETYPE Meta = (ProduceBlockType == E_BLOCK_MELON) ? 0 : static_cast(Random.RandInt(4) % 4); + FLOGD("Growing melon / pumpkin at {0} (<{1}, {2}> from stem), overwriting {3}, growing on top of {4}, meta {5}", - produceAbsPos, + a_Chunk.RelativeToAbsolute(ProduceRelPos), x, z, - ItemTypeToString(blockType[checkType]), - ItemTypeToString(soilType), - meta + ItemTypeToString(BlockType[CheckType]), + ItemTypeToString(SoilType), + Meta ); - a_Chunk.GetWorld()->SetBlock(produceAbsPos, ProduceBlockType, meta); - return true; + + // Place a randomly-facing produce: + a_Chunk.SetBlock(ProduceRelPos, ProduceBlockType, Meta); } } - return false; } + +private: + + // https://minecraft.gamepedia.com/Pumpkin_Seeds#Breaking + // https://minecraft.gamepedia.com/Melon_Seeds#Breaking + /** The array describes how many seed may be dropped at which age. The inner arrays describe the probability to drop 0, 1, 2, 3 seeds. + The outer describes the age of the stem. */ + static constexpr std::array, 8> m_AgeSeedDropProbability + { + { + { + 81.3, 17.42, 1.24, 0.03 + }, + { + 65.1, 30.04, 4.62, 0.24 + }, + { + 51.2, 38.4, 9.6, 0.8 + }, + { + 39.44, 43.02, 15.64, 1.9 + }, + { + 29.13, 44.44, 22.22, 3.7 + }, + { + 21.6, 43.2, 28.8, 6.4 + }, + { + 15.17, 39.82, 34.84, 10.16 + }, + { + 10.16, 34.84, 39.82, 15.17 + } + } + }; } ; using cBlockMelonStemHandler = cBlockStemsHandler; using cBlockPumpkinStemHandler = cBlockStemsHandler; - - - diff --git a/src/Chunk.cpp b/src/Chunk.cpp index df9292fae..663713cf3 100644 --- a/src/Chunk.cpp +++ b/src/Chunk.cpp @@ -820,10 +820,12 @@ void cChunk::TickBlocks(void) // Tick random blocks, but the first one should be m_BlockToTick (so that SetNextBlockToTick() works) auto Idx = cChunkDef::MakeIndexNoCheck(m_BlockToTick); + auto & Random = GetRandomProvider(); + for (int i = 0; i < 50; ++i) { auto Pos = cChunkDef::IndexToCoordinate(static_cast(Idx)); - Idx = m_World->GetTickRandomNumber(cChunkDef::NumBlocks - 1); + Idx = Random.RandInt(cChunkDef::NumBlocks - 1); if (Pos.y > cChunkDef::GetHeight(m_HeightMap, Pos.x, Pos.z)) { continue; // It's all air up here diff --git a/src/Items/ItemDye.h b/src/Items/ItemDye.h index da657007f..631e28e7d 100644 --- a/src/Items/ItemDye.h +++ b/src/Items/ItemDye.h @@ -102,13 +102,13 @@ public: fertilization success is reported even in the case when the chance fails (bonemeal still needs to be consumed). */ static bool FertilizePlant(cWorld & a_World, Vector3i a_BlockPos) { - BLOCKTYPE blockType; - NIBBLETYPE blockMeta; - if (!a_World.GetBlockTypeMeta(a_BlockPos, blockType, blockMeta)) + BLOCKTYPE BlockType; + NIBBLETYPE BlockMeta; + if (!a_World.GetBlockTypeMeta(a_BlockPos, BlockType, BlockMeta)) { return false; } - switch (blockType) + switch (BlockType) { case E_BLOCK_WHEAT: case E_BLOCK_CARROTS: @@ -117,8 +117,8 @@ public: case E_BLOCK_PUMPKIN_STEM: { // Grow by 2 - 5 stages: - auto numStages = GetRandomProvider().RandInt(2, 5); - if (a_World.GrowPlantAt(a_BlockPos, numStages) <= 0) + auto NumStages = GetRandomProvider().RandInt(2, 5); + if (a_World.GrowPlantAt(a_BlockPos, NumStages) <= 0) { return false; } @@ -128,14 +128,21 @@ public: case E_BLOCK_BEETROOTS: { + if (a_World.GrowPlantAt(a_BlockPos, 1) <= 0) + { + // Fix GH #4805 (bonemeal should only advance growth, not spawn produce): + return false; + } + + a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0); + // 75% chance of 1-stage growth: - if (GetRandomProvider().RandBool(0.75)) + if (!GetRandomProvider().RandBool(0.75)) { - if (a_World.GrowPlantAt(a_BlockPos, 1) > 0) - { - a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0); - } + // Hit the 25%, rollback: + a_World.GrowPlantAt(a_BlockPos, -1); } + return true; } // case beetroots @@ -144,27 +151,25 @@ public: // 45% chance of growing to the next stage / full tree: if (GetRandomProvider().RandBool(0.45)) { - if (a_World.GrowPlantAt(a_BlockPos, 1) > 0) - { - a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0); - } + a_World.GrowPlantAt(a_BlockPos, 1); } + a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0); return true; - } + } // case sapling case E_BLOCK_BIG_FLOWER: { // Drop the corresponding flower item without destroying the block: - cItems pickups; - switch (blockMeta) + cItems Pickups; + switch (BlockMeta) { - case E_META_BIG_FLOWER_SUNFLOWER: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_SUNFLOWER); break; - case E_META_BIG_FLOWER_LILAC: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_LILAC); break; - case E_META_BIG_FLOWER_ROSE_BUSH: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_ROSE_BUSH); break; - case E_META_BIG_FLOWER_PEONY: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_PEONY); break; + case E_META_BIG_FLOWER_SUNFLOWER: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_SUNFLOWER); break; + case E_META_BIG_FLOWER_LILAC: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_LILAC); break; + case E_META_BIG_FLOWER_ROSE_BUSH: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_ROSE_BUSH); break; + case E_META_BIG_FLOWER_PEONY: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_PEONY); break; } // TODO: Should we call any hook for this? - a_World.SpawnItemPickups(pickups, a_BlockPos); + a_World.SpawnItemPickups(Pickups, a_BlockPos); return true; } // big flower diff --git a/src/World.cpp b/src/World.cpp index 3f9150527..561adc4d0 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -1801,36 +1801,6 @@ bool cWorld::GrowRipePlant(Vector3i a_BlockPos) -int cWorld::GrowCactus(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow) -{ - LOGWARNING("cWorld::GrowCactus is obsolete, use cWorld::GrowPlantAt instead"); - return m_ChunkMap->GrowPlantAt({a_BlockX, a_BlockY, a_BlockZ}); -} - - - - - -bool cWorld::GrowMelonPumpkin(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType) -{ - LOGWARNING("cWorld::GrowMelonPumpkin is obsolete, use cWorld::GrowPlantAt instead"); - return (m_ChunkMap->GrowPlantAt({a_BlockX, a_BlockY, a_BlockZ}, 16) > 0); // 8 stages for the stem, 8 attempts for the produce -} - - - - - -int cWorld::GrowSugarcane(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow) -{ - LOGWARNING("cWorld::GrowSugarcane is obsolete, use cWorld::GrowPlantAt instead"); - return m_ChunkMap->GrowPlantAt({a_BlockX, a_BlockY, a_BlockZ}, a_NumBlocksToGrow); -} - - - - - EMCSBiome cWorld::GetBiomeAt (int a_BlockX, int a_BlockZ) { return m_ChunkMap->GetBiomeAt(a_BlockX, a_BlockZ); diff --git a/src/World.h b/src/World.h index 490ab59b4..e1d2a1abf 100644 --- a/src/World.h +++ b/src/World.h @@ -862,18 +862,10 @@ public: bool GrowRipePlant(int a_BlockX, int a_BlockY, int a_BlockZ, bool a_IsByBonemeal = false) { UNUSED(a_IsByBonemeal); - return GrowRipePlant({a_BlockX, a_BlockY, a_BlockZ}); + LOGWARNING("Warning: cWorld:GrowRipePlant function expects Vector3i-based coords rather than int-based coords. Emulating old-style call."); + return GrowRipePlant({ a_BlockX, a_BlockY, a_BlockZ }); } - /** Grows a cactus present at the block specified by the amount of blocks specified, up to the max height specified in the config; returns the amount of blocks the cactus grew inside this call */ - int GrowCactus(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow); - - /** Grows a melon or a pumpkin next to the block specified (assumed to be the stem); returns true if the pumpkin or melon sucessfully grew. */ - bool GrowMelonPumpkin(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType); - - /** Grows a sugarcane present at the block specified by the amount of blocks specified, up to the max height specified in the config; returns the amount of blocks the sugarcane grew inside this call */ - int GrowSugarcane(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow); - /** Returns the biome at the specified coords. Reads the biome from the chunk, if loaded, otherwise uses the world generator to provide the biome value */ EMCSBiome GetBiomeAt(int a_BlockX, int a_BlockZ); -- cgit v1.2.3