Farmland: Remember relative location of nearby water in blockstate data (#6090)

Farmland can end up scanning up to 162 blocks looking for water in the worst case. This is obviously not great for huge farms where there are thousands of blocks of the stuff.

In most farms, the water won't be moved, and nor will the farmland. This means that we can avoid this costly search on random updates.

This PR implements a cache using blockstate data (only possible in PM5) which stores an index mapping to a coordinate offset where water was previously found by this farmland block. This allows the farmland to avoid water searching entirely in most cases.

This is a colossal improvement as compared to scanning the whole 9x2x9 area every time, which, on average, scans about 40 blocks to find water if the water is at the same Y coordinate. In real terms this translates into about a 8x performance improvement for farmland (see timings below).
This commit is contained in:
Dylan T 2023-10-17 16:25:13 +01:00 committed by GitHub
parent 7f3de835e4
commit 48dcf0e32c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 9 deletions

View File

@ -61,7 +61,7 @@ use function hash;
use const PHP_INT_MAX;
class Block{
public const INTERNAL_STATE_DATA_BITS = 8;
public const INTERNAL_STATE_DATA_BITS = 11;
public const INTERNAL_STATE_DATA_MASK = ~(~0 << self::INTERNAL_STATE_DATA_BITS);
/**

View File

@ -31,15 +31,40 @@ use pocketmine\event\entity\EntityTrampleFarmlandEvent;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use function intdiv;
use function lcg_value;
class Farmland extends Transparent{
public const MAX_WETNESS = 7;
private const WATER_SEARCH_HORIZONTAL_LENGTH = 9;
private const WATER_SEARCH_VERTICAL_LENGTH = 2;
private const WATER_POSITION_INDEX_UNKNOWN = -1;
/** Total possible options for water X/Z indexes */
private const WATER_POSITION_INDICES_TOTAL = (self::WATER_SEARCH_HORIZONTAL_LENGTH ** 2) * 2;
protected int $wetness = 0; //"moisture" blockstate property in PC
/**
* Cached value indicating the relative coordinates of the most recently found water block.
*
* If this is set to a non-unknown value, the farmland block will check the relative coordinates indicated by
* this value for water, before searching the entire 9x2x9 grid around the farmland. This significantly benefits
* hydrating or fully hydrated farmland, avoiding the need for costly searches on every random tick.
*
* If the coordinates indicated don't contain water, the full 9x2x9 volume will be searched as before. A new index
* will be recorded if water is found, otherwise it will be set to unknown and future searches will search the full
* 9x2x9 volume again.
*
* This property is not exposed to the API or saved on disk. It is only used by PocketMine-MP at runtime as a cache.
*/
private int $waterPositionIndex = self::WATER_POSITION_INDEX_UNKNOWN;
protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
$w->boundedIntAuto(0, self::MAX_WETNESS, $this->wetness);
$w->boundedIntAuto(-1, self::WATER_POSITION_INDICES_TOTAL - 1, $this->waterPositionIndex);
}
public function getWetness() : int{ return $this->wetness; }
@ -53,6 +78,22 @@ class Farmland extends Transparent{
return $this;
}
/**
* @internal
*/
public function getWaterPositionIndex() : int{ return $this->waterPositionIndex; }
/**
* @internal
*/
public function setWaterPositionIndex(int $waterPositionIndex) : self{
if($waterPositionIndex < -1 || $waterPositionIndex >= self::WATER_POSITION_INDICES_TOTAL){
throw new \InvalidArgumentException("Water XZ index must be in range -1 ... " . (self::WATER_POSITION_INDICES_TOTAL - 1));
}
$this->waterPositionIndex = $waterPositionIndex;
return $this;
}
/**
* @return AxisAlignedBB[]
*/
@ -72,6 +113,11 @@ class Farmland extends Transparent{
public function onRandomTick() : void{
$world = $this->position->getWorld();
//this property may be updated by canHydrate() - track this so we know if we need to set the block again
$oldWaterPositionIndex = $this->waterPositionIndex;
$changed = false;
if(!$this->canHydrate()){
if($this->wetness > 0){
$event = new FarmlandHydrationChangeEvent($this, $this->wetness, $this->wetness - 1);
@ -79,9 +125,11 @@ class Farmland extends Transparent{
if(!$event->isCancelled()){
$this->wetness = $event->getNewHydration();
$world->setBlock($this->position, $this, false);
$changed = true;
}
}else{
$world->setBlock($this->position, VanillaBlocks::DIRT());
$changed = true;
}
}elseif($this->wetness < self::MAX_WETNESS){
$event = new FarmlandHydrationChangeEvent($this, $this->wetness, self::MAX_WETNESS);
@ -89,8 +137,14 @@ class Farmland extends Transparent{
if(!$event->isCancelled()){
$this->wetness = $event->getNewHydration();
$world->setBlock($this->position, $this, false);
$changed = true;
}
}
if(!$changed && $oldWaterPositionIndex !== $this->waterPositionIndex){
//ensure the water square index is saved regardless of whether anything else happened
$world->setBlock($this->position, $this, false);
}
}
public function onEntityLand(Entity $entity) : ?float{
@ -105,19 +159,39 @@ class Farmland extends Transparent{
}
protected function canHydrate() : bool{
//TODO: check rain
$start = $this->position->add(-4, 0, -4);
$end = $this->position->add(4, 1, 4);
for($y = $start->y; $y <= $end->y; ++$y){
for($z = $start->z; $z <= $end->z; ++$z){
for($x = $start->x; $x <= $end->x; ++$x){
if($this->position->getWorld()->getBlockAt($x, $y, $z) instanceof Water){
$world = $this->position->getWorld();
$startX = $this->position->getFloorX() - (int) (self::WATER_SEARCH_HORIZONTAL_LENGTH / 2);
$startY = $this->position->getFloorY();
$startZ = $this->position->getFloorZ() - (int) (self::WATER_SEARCH_HORIZONTAL_LENGTH / 2);
if($this->waterPositionIndex !== self::WATER_POSITION_INDEX_UNKNOWN){
$raw = $this->waterPositionIndex;
$x = $raw % self::WATER_SEARCH_HORIZONTAL_LENGTH;
$raw = intdiv($raw, self::WATER_SEARCH_HORIZONTAL_LENGTH);
$z = $raw % self::WATER_SEARCH_HORIZONTAL_LENGTH;
$raw = intdiv($raw, self::WATER_SEARCH_HORIZONTAL_LENGTH);
$y = $raw % self::WATER_SEARCH_VERTICAL_LENGTH;
if($world->getBlockAt($startX + $x, $startY + $y, $startZ + $z) instanceof Water){
return true;
}
}
//no water found at cached position - search the whole area
//y will increment after x/z have been exhausted, as usually water will be at the same Y as the farmland
for($y = 0; $y < self::WATER_SEARCH_VERTICAL_LENGTH; $y++){
for($x = 0; $x < self::WATER_SEARCH_HORIZONTAL_LENGTH; $x++){
for($z = 0; $z < self::WATER_SEARCH_HORIZONTAL_LENGTH; $z++){
if($world->getBlockAt($startX + $x, $startY + $y, $startZ + $z) instanceof Water){
$this->waterPositionIndex = $x + ($z * self::WATER_SEARCH_HORIZONTAL_LENGTH) + ($y * self::WATER_SEARCH_HORIZONTAL_LENGTH ** 2);
return true;
}
}
}
}
$this->waterPositionIndex = self::WATER_POSITION_INDEX_UNKNOWN;
return false;
}

File diff suppressed because one or more lines are too long

View File

@ -28,6 +28,7 @@ use pocketmine\block\BaseBanner;
use pocketmine\block\Bed;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\CaveVines;
use pocketmine\block\Farmland;
use pocketmine\block\MobHead;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
@ -76,6 +77,8 @@ final class BlockSerializerDeserializerTest extends TestCase{
$newBlock->setMobHeadType($block->getMobHeadType());
}elseif($block instanceof CaveVines && $newBlock instanceof CaveVines && !$block->hasBerries()){
$newBlock->setHead($block->isHead());
}elseif($block instanceof Farmland && $newBlock instanceof Farmland){
$block->setWaterPositionIndex($newBlock->getWaterPositionIndex());
}
self::assertSame($block->getStateId(), $newBlock->getStateId(), "Mismatch of blockstate for " . $block->getName() . ", " . print_r($block, true) . " vs " . print_r($newBlock, true));