Re-implemented chunk sending (#304)

Re-implement chunk sending, send chunks inside a radius instead of below a count

This sends chunks in concentric squares around players. When the radius is hit, it will pad out the radius until a full circle of chunks is loaded around the player.
TODO: implement radius-per-tick, send chunks in concentric circles, use radius for player spawning.

To set your server chunk radius, change `view-distance` in server.properties. Values are intended to be the same as MCPE render distance values. With matching client and server render distances the chunks should reach the horizon.

NOTE: You may notice significantly increased memory usage per player when increasing these values to something respectable. This is normal and expected.
A player with render distance 14 for example will cause loading of 600+ chunks. A player cannot however exceed the render distance limit set in server.properties - the server will simply not send any more chunks.

Render distance of 8 chunks is approximately 200 chunks. This is roughly equivalent to the original default max-chunks of 192 in pocketmine.yml, but sent in a circle instead of a square.

Wait for client to request a chunk radius before ordering chunks

Use 8 for default maximum radius (roughly matches old setting of 192)

Calculate spawn chunk count from chunk-sending.spawn-radius
This commit is contained in:
Dylan K. Taylor 2017-03-02 10:30:30 +00:00 committed by GitHub
parent d588222e84
commit 4fbc5738e3
4 changed files with 99 additions and 58 deletions

View File

@ -47,7 +47,7 @@ class MemoryManager{
private $garbageCollectionTrigger; private $garbageCollectionTrigger;
private $garbageCollectionAsync; private $garbageCollectionAsync;
private $chunkLimit; private $chunkRadiusOverride;
private $chunkCollect; private $chunkCollect;
private $chunkTrigger; private $chunkTrigger;
@ -104,7 +104,7 @@ class MemoryManager{
$this->garbageCollectionTrigger = (bool) $this->server->getProperty("memory.garbage-collection.low-memory-trigger", true); $this->garbageCollectionTrigger = (bool) $this->server->getProperty("memory.garbage-collection.low-memory-trigger", true);
$this->garbageCollectionAsync = (bool) $this->server->getProperty("memory.garbage-collection.collect-async-worker", true); $this->garbageCollectionAsync = (bool) $this->server->getProperty("memory.garbage-collection.collect-async-worker", true);
$this->chunkLimit = (int) $this->server->getProperty("memory.max-chunks.trigger-limit", 96); $this->chunkRadiusOverride = (int) $this->server->getProperty("memory.max-chunks.chunk-radius", 4);
$this->chunkCollect = (bool) $this->server->getProperty("memory.max-chunks.trigger-chunk-collect", true); $this->chunkCollect = (bool) $this->server->getProperty("memory.max-chunks.trigger-chunk-collect", true);
$this->chunkTrigger = (bool) $this->server->getProperty("memory.max-chunks.low-memory-trigger", true); $this->chunkTrigger = (bool) $this->server->getProperty("memory.max-chunks.low-memory-trigger", true);
@ -122,8 +122,15 @@ class MemoryManager{
return !($this->lowMemory and $this->chunkTrigger); return !($this->lowMemory and $this->chunkTrigger);
} }
public function getViewDistance($distance){ /**
return $this->lowMemory ? min($this->chunkLimit, $distance) : $distance; * Returns the allowed chunk radius based on the current memory usage.
*
* @param int $distance
*
* @return int
*/
public function getViewDistance(int $distance) : int{
return $this->lowMemory ? min($this->chunkRadiusOverride, $distance) : $distance;
} }
public function trigger($memory, $limit, $global = false, $triggerCount = 0){ public function trigger($memory, $limit, $global = false, $triggerCount = 0){

View File

@ -216,7 +216,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
/** @var Vector3 */ /** @var Vector3 */
protected $newPosition; protected $newPosition;
protected $viewDistance; protected $viewDistance = -1;
protected $chunksPerTick; protected $chunksPerTick;
protected $spawnThreshold; protected $spawnThreshold;
/** @var null|WeakPosition */ /** @var null|WeakPosition */
@ -421,6 +421,17 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->inAirTicks = 0; $this->inAirTicks = 0;
} }
public function getViewDistance() : int{
return $this->viewDistance;
}
public function setViewDistance(int $distance){
$this->viewDistance = $this->server->getAllowedViewDistance($distance);
$pk = new ChunkRadiusUpdatedPacket();
$pk->radius = $this->viewDistance;
$this->dataPacket($pk);
}
/** /**
* @return bool * @return bool
*/ */
@ -556,11 +567,10 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->clientID = $clientID; $this->clientID = $clientID;
$this->loaderId = Level::generateChunkLoaderId($this); $this->loaderId = Level::generateChunkLoaderId($this);
$this->chunksPerTick = (int) $this->server->getProperty("chunk-sending.per-tick", 4); $this->chunksPerTick = (int) $this->server->getProperty("chunk-sending.per-tick", 4);
$this->spawnThreshold = (int) $this->server->getProperty("chunk-sending.spawn-threshold", 56); $this->spawnThreshold = (int) (($this->server->getProperty("chunk-sending.spawn-radius", 4) ** 2) * M_PI);
$this->spawnPosition = null; $this->spawnPosition = null;
$this->gamemode = $this->server->getGamemode(); $this->gamemode = $this->server->getGamemode();
$this->setLevel($this->server->getDefaultLevel()); $this->setLevel($this->server->getDefaultLevel());
$this->viewDistance = $this->server->getViewDistance();
$this->newPosition = new Vector3(0, 0, 0); $this->newPosition = new Vector3(0, 0, 0);
$this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0); $this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0);
@ -859,7 +869,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
} }
protected function orderChunks(){ protected function orderChunks(){
if($this->connected === false){ if($this->connected === false or $this->viewDistance === -1){
return false; return false;
} }
@ -867,59 +877,77 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
$this->nextChunkOrderRun = 200; $this->nextChunkOrderRun = 200;
$viewDistance = $this->server->getMemoryManager()->getViewDistance($this->viewDistance); $radius = $this->server->getAllowedViewDistance($this->viewDistance);
$radiusSquared = $radius ** 2;
$newOrder = []; $newOrder = [];
$lastChunk = $this->usedChunks; $unloadChunks = $this->usedChunks;
$centerX = $this->x >> 4; $centerX = $this->x >> 4;
$centerZ = $this->z >> 4; $centerZ = $this->z >> 4;
$layer = 1; for($x = 0; $x < $radius; ++$x){
$leg = 0; for($z = 0; $z <= $x; ++$z){
$x = 0; if(($x ** 2 + $z ** 2) > $radiusSquared){
$z = 0; break; //skip to next band
}
for($i = 0; $i < $viewDistance; ++$i){ //If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be.
$chunkX = $x + $centerX; /* Top right quadrant */
$chunkZ = $z + $centerZ; if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $x, $centerZ + $z)]) or $this->usedChunks[$index] === false){
$newOrder[$index] = true;
}
unset($unloadChunks[$index]);
if(!isset($this->usedChunks[$index = Level::chunkHash($chunkX, $chunkZ)]) or $this->usedChunks[$index] === false){ /* Top left quadrant */
$newOrder[$index] = true; if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $x - 1, $centerZ + $z)]) or $this->usedChunks[$index] === false){
} $newOrder[$index] = true;
unset($lastChunk[$index]); }
unset($unloadChunks[$index]);
switch($leg){ /* Bottom right quadrant */
case 0: if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $x, $centerZ - $z - 1)]) or $this->usedChunks[$index] === false){
++$x; $newOrder[$index] = true;
if($x === $layer){ }
++$leg; unset($unloadChunks[$index]);
/* Bottom left quadrant */
if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $x - 1, $centerZ - $z - 1)]) or $this->usedChunks[$index] === false){
$newOrder[$index] = true;
}
unset($unloadChunks[$index]);
if($x !== $z){
/* Top right quadrant mirror */
if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $z, $centerZ + $x)]) or $this->usedChunks[$index] === false){
$newOrder[$index] = true;
} }
break; unset($unloadChunks[$index]);
case 1:
++$z; /* Top left quadrant mirror */
if($z === $layer){ if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $z - 1, $centerZ + $x)]) or $this->usedChunks[$index] === false){
++$leg; $newOrder[$index] = true;
} }
break; unset($unloadChunks[$index]);
case 2:
--$x; /* Bottom right quadrant mirror */
if(-$x === $layer){ if(!isset($this->usedChunks[$index = Level::chunkHash($centerX + $z, $centerZ - $x - 1)]) or $this->usedChunks[$index] === false){
++$leg; $newOrder[$index] = true;
} }
break; unset($unloadChunks[$index]);
case 3:
--$z; /* Bottom left quadrant mirror */
if(-$z === $layer){ if(!isset($this->usedChunks[$index = Level::chunkHash($centerX - $z - 1, $centerZ - $x - 1)]) or $this->usedChunks[$index] === false){
$leg = 0; $newOrder[$index] = true;
++$layer;
} }
break; unset($unloadChunks[$index]);
}
} }
} }
foreach($lastChunk as $index => $bool){ foreach($unloadChunks as $index => $bool){
Level::getXZ($index, $X, $Z); Level::getXZ($index, $X, $Z);
$this->unloadChunk($X, $Z); $this->unloadChunk($X, $Z);
} }
@ -2932,12 +2960,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade
} }
break; break;
case ProtocolInfo::REQUEST_CHUNK_RADIUS_PACKET: case ProtocolInfo::REQUEST_CHUNK_RADIUS_PACKET:
if($this->spawned){ $this->setViewDistance($packet->radius);
$this->viewDistance = $packet->radius ** 2;
}
$pk = new ChunkRadiusUpdatedPacket();
$pk->radius = $packet->radius;
$this->dataPacket($pk);
break; break;
case ProtocolInfo::SET_PLAYER_GAME_TYPE_PACKET: case ProtocolInfo::SET_PLAYER_GAME_TYPE_PACKET:
if($packet->gamemode !== ($this->gamemode & 0x01)){ if($packet->gamemode !== ($this->gamemode & 0x01)){

View File

@ -333,8 +333,19 @@ class Server{
/** /**
* @return int * @return int
*/ */
public function getViewDistance(){ public function getViewDistance() : int{
return max(56, $this->getProperty("chunk-sending.max-chunks", 256)); return max(2, $this->getConfigInt("view-distance", 8));
}
/**
* Returns a view distance up to the currently-allowed limit.
*
* @param int $distance
*
* @return int
*/
public function getAllowedViewDistance(int $distance) : int{
return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance())));
} }
/** /**
@ -1384,6 +1395,7 @@ class Server{
"enable-rcon" => false, "enable-rcon" => false,
"rcon.password" => substr(base64_encode(random_bytes(20)), 3, 10), "rcon.password" => substr(base64_encode(random_bytes(20)), 3, 10),
"auto-save" => true, "auto-save" => true,
"view-distance" => 8
]); ]);
$this->forceLanguage = $this->getProperty("settings.force-language", false); $this->forceLanguage = $this->getProperty("settings.force-language", false);

View File

@ -58,8 +58,8 @@ memory:
low-memory-trigger: true low-memory-trigger: true
max-chunks: max-chunks:
#Limit of chunks to load per player, overrides chunk-sending.max-chunks #Maximum render distance per player when low memory is triggered
trigger-limit: 96 chunk-radius: 4
#Do chunk garbage collection on trigger #Do chunk garbage collection on trigger
trigger-chunk-collect: true trigger-chunk-collect: true
@ -117,12 +117,11 @@ level-settings:
always-tick-players: false always-tick-players: false
chunk-sending: chunk-sending:
#To change server normal render distance, change view-distance in server.properties.
#Amount of chunks sent to players per tick #Amount of chunks sent to players per tick
per-tick: 4 per-tick: 4
#Amount of chunks sent around each player #Radius of chunks that need to be sent before spawning the player
max-chunks: 192 spawn-radius: 4
#Amount of chunks that need to be sent before spawning the player
spawn-threshold: 56
#Save a serialized copy of the chunk in memory for faster sending #Save a serialized copy of the chunk in memory for faster sending
#Useful in mostly-static worlds where lots of players join at the same time #Useful in mostly-static worlds where lots of players join at the same time
cache-chunks: false cache-chunks: false