Compare commits

...

30 Commits
3.6.3 ... 3.6.5

Author SHA1 Message Date
f2ff510597 Release 3.6.5 2019-03-10 11:49:06 +00:00
562179bdd6 Backport 58cafc853f: s/level/world (strings only)
we should look at doing this for code too, but for now I'm not planning to break everyone's plugins.
2019-03-10 11:35:46 +00:00
5c12bee874 Backport other part of 2bffd5cc1c: Add timer measurements for autosave 2019-03-10 11:20:51 +00:00
99606bbe23 beware possible API break 2019-03-09 19:33:46 +00:00
a1d50de12e OOF 2019-03-09 19:31:13 +00:00
4252c5914b Backport 93cd00ae8f: Remove dead settings from pocketmine.yml 2019-03-09 19:21:39 +00:00
0659d2fbef Backport 6bd43a8215: Firehose auto-tick-rate anti-feature, closes #2665 2019-03-09 19:20:53 +00:00
10612acace Partial backport of 2bffd5cc1c to 3.6 2019-03-09 19:10:09 +00:00
1d810f8aeb Backport c3e66e0adc to 3.6 2019-03-09 19:09:05 +00:00
414104851a LevelDB: Mark chunks as changed when upgraded from an older chunk format 2019-03-09 19:02:34 +00:00
c0bed03a2a Update PlayerRespawnEvent.php (#2797)
removed obsolete comment
2019-03-05 16:28:58 +00:00
d25c84acff Fixed RegionLoader corrupting location table when too-large chunks are discovered
this was making the location table point to an offset that did not yet exist, which caused the region header consistency check to discard the region as corrupted the next time it was loaded.
2019-03-05 12:09:27 +00:00
55994e08db RegionLoader: make some error messages more detailed 2019-03-05 11:18:02 +00:00
6f5d4d6b80 RegionLoader: fixed handling of invalid chunk coordinates 2019-03-05 10:52:36 +00:00
df1ef7fe0c Improve RegionLoader tests 2019-03-05 10:51:44 +00:00
20a25a69df Force emission of output buffer contents on crash 2019-03-04 12:01:30 +00:00
faca610594 NetworkBinaryStream: remove dead field 2019-03-03 12:38:03 +00:00
91603dc2d6 3.6.5 is next 2019-03-03 11:50:10 +00:00
af90e18b18 Release 3.6.4 2019-03-03 11:49:07 +00:00
ab5b4d112b BaseInventory: fixed items with userdata stacking with items without userdata 2019-03-03 11:12:36 +00:00
a30b1fb6d5 Inventory: Add failing test case for itemstack NBT duplication 2019-03-03 11:11:33 +00:00
20b4723728 Player: fixed held slot being out of sync after dying, closes #2788
it appears this premature optimization dates back to the days when PlayerHotbarPacket was not useless.
2019-02-27 09:26:56 +00:00
d1ced0ffc6 Player: fixed XP not dropping on death 2019-02-27 09:22:52 +00:00
2164dbae67 Fixed reloaded arrows not despawning, closes #2781 2019-02-26 19:58:21 +00:00
6c92a2e88b Ladder: be more strict about resetting fall distance
closes #2790
2019-02-26 19:54:57 +00:00
97deadc59f PackedIce: fixed dropping without silk touch, closes #2789 2019-02-26 19:48:18 +00:00
0c3b136a8d Player: fixed removeWindow() sometimes removing GUI / crashing clients 2019-02-24 12:14:19 +00:00
79b7e08e60 Silence NetworkStackLatencyPacket spam from dev builds 2019-02-23 11:03:50 +00:00
2540dacdd7 PluginManager: fixed suffix split handling 2019-02-23 10:51:06 +00:00
f1078e3909 3.6.4 is next 2019-02-22 18:07:48 +00:00
24 changed files with 275 additions and 121 deletions

View File

@ -3627,7 +3627,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
}
if($this->inventory !== null){
$this->inventory->setHeldItemIndex(0, false); //This is already handled when sending contents, don't send it twice
$this->inventory->setHeldItemIndex(0);
$this->inventory->clearAll();
}
if($this->armorInventory !== null){
@ -3635,6 +3635,10 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
}
}
//TODO: allow this number to be manipulated during PlayerDeathEvent
$this->level->dropExperience($this, $this->getXpDropAmount());
$this->setXpAndProgress(0, 0);
if($ev->getDeathMessage() != ""){
$this->server->broadcast($ev->getDeathMessage(), Server::BROADCAST_CHANNEL_USERS);
}
@ -3904,8 +3908,8 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
throw new \InvalidArgumentException("Cannot remove fixed window $id (" . get_class($inventory) . ") from " . $this->getName());
}
$inventory->close($this);
if($id !== null){
$inventory->close($this);
unset($this->windows[$hash], $this->windowIndex[$id], $this->permanentWindows[$id]);
}
}

View File

@ -37,7 +37,7 @@ namespace pocketmine {
use pocketmine\wizard\SetupWizard;
const NAME = "PocketMine-MP";
const BASE_VERSION = "3.6.3";
const BASE_VERSION = "3.6.5";
const IS_DEVELOPMENT_BUILD = false;
const BUILD_NUMBER = 0;

View File

@ -122,7 +122,6 @@ use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function floor;
use function function_exists;
use function get_class;
use function getmypid;
@ -142,6 +141,7 @@ use function max;
use function microtime;
use function min;
use function mkdir;
use function ob_end_flush;
use function pcntl_signal;
use function pcntl_signal_dispatch;
use function preg_replace;
@ -294,15 +294,6 @@ class Server{
/** @var int */
public $networkCompressionLevel = 7;
/** @var bool */
private $autoTickRate = true;
/** @var int */
private $autoTickRateLimit = 20;
/** @var bool */
private $alwaysTickPlayers = false;
/** @var int */
private $baseTickRate = 1;
/** @var int */
private $autoSaveTicker = 0;
/** @var int */
@ -1055,7 +1046,7 @@ class Server{
*/
public function unloadLevel(Level $level, bool $forceUnload = false) : bool{
if($level === $this->getDefaultLevel() and !$forceUnload){
throw new \InvalidStateException("The default level cannot be unloaded while running, please switch levels.");
throw new \InvalidStateException("The default world cannot be unloaded while running, please switch worlds.");
}
return $level->unload($forceUnload);
@ -1081,7 +1072,7 @@ class Server{
*/
public function loadLevel(string $name) : bool{
if(trim($name) === ""){
throw new LevelException("Invalid empty level name");
throw new LevelException("Invalid empty world name");
}
if($this->isLevelLoaded($name)){
return true;
@ -1108,8 +1099,6 @@ class Server{
(new LevelLoadEvent($level))->call();
$level->setTickRate($this->baseTickRate);
return true;
}
@ -1141,7 +1130,7 @@ class Server{
if(($providerClass = LevelProviderManager::getProviderByName($this->getProperty("level-settings.default-format", "pmanvil"))) === null){
$providerClass = LevelProviderManager::getProviderByName("pmanvil");
if($providerClass === null){
throw new \InvalidStateException("Default level provider has not been registered");
throw new \InvalidStateException("Default world provider has not been registered");
}
}
@ -1153,8 +1142,6 @@ class Server{
$level = new Level($this, $name, new $providerClass($path));
$this->levels[$level->getId()] = $level;
$level->setTickRate($this->baseTickRate);
(new LevelInitEvent($level))->call();
(new LevelLoadEvent($level))->call();
@ -1592,11 +1579,6 @@ class Server{
}
$this->networkCompressionAsync = (bool) $this->getProperty("network.async-compression", true);
$this->autoTickRate = (bool) $this->getProperty("level-settings.auto-tick-rate", true);
$this->autoTickRateLimit = (int) $this->getProperty("level-settings.auto-tick-rate-limit", 20);
$this->alwaysTickPlayers = (bool) $this->getProperty("level-settings.always-tick-players", false);
$this->baseTickRate = (int) $this->getProperty("level-settings.base-tick-rate", 1);
$this->doTitleTick = ((bool) $this->getProperty("console.title-tick", true)) && Terminal::hasFormattingCodes();
@ -2037,7 +2019,7 @@ class Server{
}
public function reload(){
$this->logger->info("Saving levels...");
$this->logger->info("Saving worlds...");
foreach($this->levels as $level){
$level->save();
@ -2115,7 +2097,7 @@ class Server{
$player->close($player->getLeaveMessage(), $this->getProperty("settings.shutdown-message", "Server closed"));
}
$this->getLogger()->debug("Unloading all levels");
$this->getLogger()->debug("Unloading all worlds");
foreach($this->getLevels() as $level){
$this->unloadLevel($level, true);
}
@ -2217,6 +2199,7 @@ class Server{
* @param array|null $trace
*/
public function exceptionHandler(\Throwable $e, $trace = null){
while(@ob_end_flush()){}
global $lastError;
if($trace === null){
@ -2248,6 +2231,7 @@ class Server{
}
public function crashDump(){
while(@ob_end_flush()){}
if(!$this->isRunning){
return;
}
@ -2428,8 +2412,6 @@ class Server{
foreach($this->players as $p){
if(!$p->loggedIn and ($tickTime - $p->creationTime) >= 10){
$p->close("", "Login timeout");
}elseif($this->alwaysTickPlayers and $p->spawned){
$p->onUpdate($currentTick);
}
}
@ -2439,32 +2421,13 @@ class Server{
// Level unloaded during the tick of a level earlier in this loop, perhaps by plugin
continue;
}
if($level->getTickRate() > $this->baseTickRate and --$level->tickRateCounter > 0){
continue;
}
$levelTime = microtime(true);
$level->doTick($currentTick);
$tickMs = (microtime(true) - $levelTime) * 1000;
$level->tickRateTime = $tickMs;
if($this->autoTickRate){
if($tickMs < 50 and $level->getTickRate() > $this->baseTickRate){
$level->setTickRate($r = $level->getTickRate() - 1);
if($r > $this->baseTickRate){
$level->tickRateCounter = $level->getTickRate();
}
$this->getLogger()->debug("Raising level \"{$level->getName()}\" tick rate to {$level->getTickRate()} ticks");
}elseif($tickMs >= 50){
if($level->getTickRate() === $this->baseTickRate){
$level->setTickRate(max($this->baseTickRate + 1, min($this->autoTickRateLimit, (int) floor($tickMs / 50))));
$this->getLogger()->debug(sprintf("Level \"%s\" took %gms, setting tick rate to %d ticks", $level->getName(), (int) round($tickMs, 2), $level->getTickRate()));
}elseif(($tickMs / $level->getTickRate()) >= 50 and $level->getTickRate() < $this->autoTickRateLimit){
$level->setTickRate($level->getTickRate() + 1);
$this->getLogger()->debug(sprintf("Level \"%s\" took %gms, setting tick rate to %d ticks", $level->getName(), (int) round($tickMs, 2), $level->getTickRate()));
}
$level->tickRateCounter = $level->getTickRate();
}
if($tickMs >= 50){
$this->getLogger()->debug(sprintf("World \"%s\" took too long to tick: %gms (%g ticks)", $level->getName(), $tickMs, round($tickMs / 50, 2)));
}
}
}
@ -2617,7 +2580,10 @@ class Server{
if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){
$this->autoSaveTicker = 0;
$this->getLogger()->debug("[Auto Save] Saving worlds...");
$start = microtime(true);
$this->doAutoSave();
$this->getLogger()->debug("[Auto Save] Save completed in " . round(microtime(true) - $start, 3) . "s");
}
if($this->sendUsageTicker > 0 and --$this->sendUsageTicker === 0){

View File

@ -58,8 +58,10 @@ class Ladder extends Transparent{
}
public function onEntityCollide(Entity $entity) : void{
$entity->resetFallDistance();
$entity->onGround = true;
if($entity->asVector3()->floor()->distanceSquared($this) < 1){ //entity coordinates must be inside block
$entity->resetFallDistance();
$entity->onGround = true;
}
}
protected function recalculateBoundingBox() : ?AxisAlignedBB{

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\item\Item;
class PackedIce extends Solid{
protected $id = self::PACKED_ICE;
@ -46,4 +48,8 @@ class PackedIce extends Solid{
public function getToolType() : int{
return BlockToolType::TYPE_PICKAXE;
}
public function getDropsForCompatibleTool(Item $item) : array{
return [];
}
}

View File

@ -26,6 +26,8 @@ namespace pocketmine\command\defaults;
use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\lang\TranslationContainer;
use function microtime;
use function round;
class SaveCommand extends VanillaCommand{
@ -43,7 +45,8 @@ class SaveCommand extends VanillaCommand{
return true;
}
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.start"));
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.save.start"));
$start = microtime(true);
foreach($sender->getServer()->getOnlinePlayers() as $player){
$player->save();
@ -53,7 +56,7 @@ class SaveCommand extends VanillaCommand{
$level->save(true);
}
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.success"));
Command::broadcastCommandMessage($sender, new TranslationContainer("pocketmine.save.success", [round(microtime(true) - $start, 3)]));
return true;
}

View File

@ -108,13 +108,11 @@ class StatusCommand extends VanillaCommand{
foreach($server->getLevels() as $level){
$levelName = $level->getFolderName() !== $level->getName() ? " (" . $level->getName() . ")" : "";
$timeColor = ($level->getTickRate() > 1 or $level->getTickRateTime() > 40) ? TextFormat::RED : TextFormat::YELLOW;
$tickRate = $level->getTickRate() > 1 ? " (tick rate " . $level->getTickRate() . ")" : "";
$timeColor = $level->getTickRateTime() > 40 ? TextFormat::RED : TextFormat::YELLOW;
$sender->sendMessage(TextFormat::GOLD . "World \"{$level->getFolderName()}\"$levelName: " .
TextFormat::RED . number_format(count($level->getChunks())) . TextFormat::GREEN . " chunks, " .
TextFormat::RED . number_format(count($level->getEntities())) . TextFormat::GREEN . " entities, " .
TextFormat::RED . number_format(count($level->getTiles())) . TextFormat::GREEN . " tiles. " .
"Time $timeColor" . round($level->getTickRateTime(), 2) . "ms" . $tickRate
TextFormat::RED . number_format(count($level->getEntities())) . TextFormat::GREEN . " entities. " .
"Time $timeColor" . round($level->getTickRateTime(), 2) . "ms"
);
}

View File

@ -1640,7 +1640,8 @@ abstract class Entity extends Location implements Metadatable, EntityIds{
assert(abs($dx) <= 20 and abs($dy) <= 20 and abs($dz) <= 20, "Movement distance is excessive: dx=$dx, dy=$dy, dz=$dz");
$list = $this->level->getCollisionCubes($this, $this->level->getTickRate() > 1 ? $this->boundingBox->offsetCopy($dx, $dy, $dz) : $this->boundingBox->addCoord($dx, $dy, $dz), false);
//TODO: bad hack here will cause unexpected behaviour under heavy lag
$list = $this->level->getCollisionCubes($this, $this->level->getTickRateTime() > 50 ? $this->boundingBox->offsetCopy($dx, $dy, $dz) : $this->boundingBox->addCoord($dx, $dy, $dz), false);
foreach($list as $bb){
$dy = $bb->calculateYOffset($this->boundingBox, $dy);

View File

@ -123,7 +123,7 @@ class Arrow extends Projectile{
$hasUpdate = parent::entityBaseTick($tickDiff);
if($this->isCollided){
if($this->blockHit !== null){
$this->collideTicks += $tickDiff;
if($this->collideTicks > 1200){
$this->flagForDespawn();

View File

@ -27,7 +27,7 @@ use pocketmine\level\Position;
use pocketmine\Player;
/**
* Called when a player is respawned (or first time spawned)
* Called when a player is respawned
*/
class PlayerRespawnEvent extends PlayerEvent{
/** @var Position */

View File

@ -253,11 +253,9 @@ abstract class BaseInventory implements Inventory{
public function canAddItem(Item $item) : bool{
$item = clone $item;
$checkDamage = !$item->hasAnyDamageValue();
$checkTags = $item->hasCompoundTag();
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
$slot = $this->getItem($i);
if($item->equals($slot, $checkDamage, $checkTags)){
if($item->equals($slot)){
if(($diff = $slot->getMaxStackSize() - $slot->getCount()) > 0){
$item->setCount($item->getCount() - $diff);
}

View File

@ -74,7 +74,7 @@ class Explosion{
*/
public function __construct(Position $center, float $size, $what = null){
if(!$center->isValid()){
throw new \InvalidArgumentException("Position does not have a valid level");
throw new \InvalidArgumentException("Position does not have a valid world");
}
$this->source = $center;
$this->level = $center->getLevel();

View File

@ -261,11 +261,12 @@ class Level implements ChunkManager, Metadatable{
/** @var LevelTimings */
public $timings;
/** @var int */
private $tickRate;
/** @var int */
public $tickRateTime = 0;
/** @var int */
/**
* @deprecated
* @var int
*/
public $tickRateCounter = 0;
/** @var bool */
@ -411,19 +412,26 @@ class Level implements ChunkManager, Metadatable{
$this->timings = new LevelTimings($this);
$this->temporalPosition = new Position(0, 0, 0, $this);
$this->temporalVector = new Vector3(0, 0, 0);
$this->tickRate = 1;
}
/**
* @deprecated
* @return int
*/
public function getTickRate() : int{
return $this->tickRate;
return 1;
}
public function getTickRateTime() : float{
return $this->tickRateTime;
}
/**
* @deprecated does nothing
* @param int $tickRate
*/
public function setTickRate(int $tickRate){
$this->tickRate = $tickRate;
}
public function registerGeneratorToWorker(int $worker) : void{
@ -466,7 +474,7 @@ class Level implements ChunkManager, Metadatable{
public function close(){
if($this->closed){
throw new \InvalidStateException("Tried to close a level which is already closed");
throw new \InvalidStateException("Tried to close a world which is already closed");
}
foreach($this->chunks as $chunk){
@ -580,7 +588,7 @@ class Level implements ChunkManager, Metadatable{
*/
public function unload(bool $force = false) : bool{
if($this->doingTick and !$force){
throw new \InvalidStateException("Cannot unload a level during level tick");
throw new \InvalidStateException("Cannot unload a world during world tick");
}
$ev = new LevelUnloadEvent($this);
@ -599,7 +607,7 @@ class Level implements ChunkManager, Metadatable{
$defaultLevel = $this->server->getDefaultLevel();
foreach($this->getPlayers() as $player){
if($this === $defaultLevel or $defaultLevel === null){
$player->close($player->getLeaveMessage(), "Forced default level unload");
$player->close($player->getLeaveMessage(), "Forced default world unload");
}elseif($defaultLevel instanceof Level){
$player->teleport($this->server->getDefaultLevel()->getSafeSpawn());
}
@ -777,7 +785,7 @@ class Level implements ChunkManager, Metadatable{
*/
public function doTick(int $currentTick){
if($this->closed){
throw new \InvalidStateException("Attempted to tick a Level which has been closed");
throw new \InvalidStateException("Attempted to tick a world which has been closed");
}
$this->timings->doTick->startTiming();
@ -2691,10 +2699,10 @@ class Level implements ChunkManager, Metadatable{
*/
public function addEntity(Entity $entity){
if($entity->isClosed()){
throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to Level");
throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world");
}
if($entity->getLevel() !== $this){
throw new LevelException("Invalid Entity level");
throw new LevelException("Invalid Entity world");
}
if($entity instanceof Player){
@ -2712,7 +2720,7 @@ class Level implements ChunkManager, Metadatable{
*/
public function removeEntity(Entity $entity){
if($entity->getLevel() !== $this){
throw new LevelException("Invalid Entity level");
throw new LevelException("Invalid Entity world");
}
if($entity instanceof Player){
@ -2731,10 +2739,10 @@ class Level implements ChunkManager, Metadatable{
*/
public function addTile(Tile $tile){
if($tile->isClosed()){
throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to Level");
throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world");
}
if($tile->getLevel() !== $this){
throw new LevelException("Invalid Tile level");
throw new LevelException("Invalid Tile world");
}
$chunkX = $tile->getFloorX() >> 4;
@ -2757,7 +2765,7 @@ class Level implements ChunkManager, Metadatable{
*/
public function removeTile(Tile $tile){
if($tile->getLevel() !== $this){
throw new LevelException("Invalid Tile level");
throw new LevelException("Invalid Tile world");
}
unset($this->tiles[$tile->getId()], $this->updateTiles[$tile->getId()]);

View File

@ -64,7 +64,7 @@ class Position extends Vector3{
*/
public function getLevel(){
if($this->level !== null and $this->level->isClosed()){
MainLogger::getLogger()->debug("Position was holding a reference to an unloaded Level");
MainLogger::getLogger()->debug("Position was holding a reference to an unloaded world");
$this->level = null;
}
@ -82,7 +82,7 @@ class Position extends Vector3{
*/
public function setLevel(Level $level = null){
if($level !== null and $level->isClosed()){
throw new \InvalidArgumentException("Specified level has been unloaded and cannot be used");
throw new \InvalidArgumentException("Specified world has been unloaded and cannot be used");
}
$this->level = $level;

View File

@ -74,10 +74,10 @@ class ChunkRequestTask extends AsyncTask{
$batch->isEncoded = true;
$level->chunkRequestCallback($this->chunkX, $this->chunkZ, $batch);
}else{
$server->getLogger()->error("Chunk request for level #" . $this->levelId . ", x=" . $this->chunkX . ", z=" . $this->chunkZ . " doesn't have any result data");
$server->getLogger()->error("Chunk request for world #" . $this->levelId . ", x=" . $this->chunkX . ", z=" . $this->chunkZ . " doesn't have any result data");
}
}else{
$server->getLogger()->debug("Dropped chunk task due to level not loaded");
$server->getLogger()->debug("Dropped chunk task due to world not loaded");
}
}
}

View File

@ -311,6 +311,7 @@ class LevelDB extends BaseLevelProvider{
$lightPopulated = true;
$chunkVersion = ord($this->db->get($index . self::TAG_VERSION));
$hasBeenUpgraded = $chunkVersion < self::CURRENT_LEVEL_CHUNK_VERSION;
$binaryStream = new BinaryStream();
@ -326,6 +327,9 @@ class LevelDB extends BaseLevelProvider{
$binaryStream->setBuffer($data, 0);
$subChunkVersion = $binaryStream->getByte();
if($subChunkVersion < self::CURRENT_LEVEL_SUBCHUNK_VERSION){
$hasBeenUpgraded = true;
}
switch($subChunkVersion){
case 0:
@ -334,6 +338,7 @@ class LevelDB extends BaseLevelProvider{
if($chunkVersion < 4){
$blockSkyLight = $binaryStream->get(2048);
$blockLight = $binaryStream->get(2048);
$hasBeenUpgraded = true; //drop saved light
}else{
//Mojang didn't bother changing the subchunk version when they stopped saving sky light -_-
$blockSkyLight = "";
@ -453,6 +458,7 @@ class LevelDB extends BaseLevelProvider{
$chunk->setGenerated(true);
$chunk->setPopulated(true);
$chunk->setLightPopulated($lightPopulated);
$chunk->setChanged($hasBeenUpgraded); //trigger rewriting chunk to disk if it was converted from an older format
return $chunk;
}

View File

@ -124,9 +124,6 @@ class RegionLoader{
*/
public function readChunk(int $x, int $z) : ?string{
$index = self::getChunkOffset($x, $z);
if($index < 0 or $index >= 4096){
throw new \InvalidArgumentException("Invalid chunk position in region, expected x/z in range 0-31, got x=$x, z=$z");
}
$this->lastUsed = time();
@ -143,15 +140,13 @@ class RegionLoader{
if($length <= 0 or $length > self::MAX_SECTOR_LENGTH){ //Not yet generated / corrupted
if($length >= self::MAX_SECTOR_LENGTH){
$this->locationTable[$index][0] = ++$this->lastSector;
$this->locationTable[$index][1] = 1;
throw new CorruptedChunkException("Corrupted chunk header detected (sector count larger than max)");
throw new CorruptedChunkException("Corrupted chunk header detected (sector count $length larger than max " . self::MAX_SECTOR_LENGTH . ")");
}
return null;
}
if($length > ($this->locationTable[$index][1] << 12)){ //Invalid chunk, bigger than defined number of sectors
MainLogger::getLogger()->error("Corrupted bigger chunk detected (bigger than number of sectors given in header)");
MainLogger::getLogger()->error("Chunk x=$x,z=$z length mismatch (expected " . ($this->locationTable[$index][1] << 12) . " sectors, got $length sectors)");
$this->locationTable[$index][1] = $length >> 12;
$this->writeLocationIndex($index);
}
@ -169,10 +164,25 @@ class RegionLoader{
return substr($chunkData, 1);
}
/**
* @param int $x
* @param int $z
*
* @return bool
* @throws \InvalidArgumentException
*/
public function chunkExists(int $x, int $z) : bool{
return $this->isChunkGenerated(self::getChunkOffset($x, $z));
}
/**
* @param int $x
* @param int $z
* @param string $chunkData
*
* @throws ChunkException
* @throws \InvalidArgumentException
*/
public function writeChunk(int $x, int $z, string $chunkData){
$this->lastUsed = time();
@ -202,14 +212,40 @@ class RegionLoader{
}
}
/**
* @param int $x
* @param int $z
*
* @throws \InvalidArgumentException
*/
public function removeChunk(int $x, int $z){
$index = self::getChunkOffset($x, $z);
$this->locationTable[$index][0] = 0;
$this->locationTable[$index][1] = 0;
}
/**
* @param int $x
* @param int $z
*
* @return int
* @throws \InvalidArgumentException
*/
protected static function getChunkOffset(int $x, int $z) : int{
return $x + ($z << 5);
if($x < 0 or $x > 31 or $z < 0 or $z > 31){
throw new \InvalidArgumentException("Invalid chunk position in region, expected x/z in range 0-31, got x=$x, z=$z");
}
return $x | ($z << 5);
}
/**
* @param int $offset
* @param int &$x
* @param int &$z
*/
protected static function getChunkCoords(int $offset, ?int &$x, ?int &$z) : void{
$x = $offset & 0x1f;
$z = ($offset >> 5) & 0x1f;
}
/**
@ -237,18 +273,23 @@ class RegionLoader{
}
$data = unpack("N*", $headerRaw);
/** @var int[] $usedOffsets */
$usedOffsets = [];
for($i = 0; $i < 1024; ++$i){
$index = $data[$i + 1];
$offset = $index >> 8;
if($offset !== 0){
fseek($this->filePointer, $offset << 12);
self::getChunkCoords($i, $x, $z);
$fileOffset = $offset << 12;
fseek($this->filePointer, $fileOffset);
if(fgetc($this->filePointer) === false){ //Try and read from the location
throw new CorruptedRegionException("Region file location offset points to invalid location");
throw new CorruptedRegionException("Region file location offset x=$x,z=$z points to invalid file location $fileOffset");
}elseif(isset($usedOffsets[$offset])){
throw new CorruptedRegionException("Found two chunk offsets pointing to the same location");
self::getChunkCoords($usedOffsets[$offset], $existingX, $existingZ);
throw new CorruptedRegionException("Found two chunk offsets (chunk1: x=$existingX,z=$existingZ, chunk2: x=$x,z=$z) pointing to the file location $fileOffset");
}else{
$usedOffsets[$offset] = true;
$usedOffsets[$offset] = $i;
}
}

View File

@ -40,8 +40,6 @@ use function count;
use function strlen;
class NetworkBinaryStream extends BinaryStream{
/** @var NetworkLittleEndianNBTStream */
private static $nbtSerializer = null;
public function getString() : string{
return $this->get($this->getUnsignedVarInt());

View File

@ -51,6 +51,7 @@ use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
use pocketmine\network\mcpe\protocol\ModalFormResponsePacket;
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
use pocketmine\network\mcpe\protocol\NetworkStackLatencyPacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\PlayerHotbarPacket;
use pocketmine\network\mcpe\protocol\PlayerInputPacket;
@ -300,4 +301,8 @@ class PlayerNetworkSessionAdapter extends NetworkSession{
public function handleLevelSoundEvent(LevelSoundEventPacket $packet) : bool{
return $this->player->handleLevelSoundEvent($packet);
}
public function handleNetworkStackLatency(NetworkStackLatencyPacket $packet) : bool{
return true; //TODO: implement this properly - this is here to silence debug spam from MCPE dev builds
}
}

View File

@ -384,14 +384,14 @@ class PluginManager{
*/
public function isCompatibleApi(string ...$versions) : bool{
$serverString = $this->server->getApiVersion();
$serverApi = array_pad(explode("-", $serverString), 2, "");
$serverApi = array_pad(explode("-", $serverString, 2), 2, "");
$serverNumbers = array_map("\intval", explode(".", $serverApi[0]));
foreach($versions as $version){
//Format: majorVersion.minorVersion.patch (3.0.0)
// or: majorVersion.minorVersion.patch-devBuild (3.0.0-alpha1)
if($version !== $serverString){
$pluginApi = array_pad(explode("-", $version), 2, ""); //0 = version, 1 = suffix (optional)
$pluginApi = array_pad(explode("-", $version, 2), 2, ""); //0 = version, 1 = suffix (optional)
if(strtoupper($pluginApi[1]) !== strtoupper($serverApi[1])){ //Different release phase (alpha vs. beta) or phase build (alpha.1 vs alpha.2)
continue;

View File

@ -109,13 +109,6 @@ player:
level-settings:
#The default format that levels will use when created
default-format: pmanvil
#Automatically change levels tick rate to maintain 20 ticks per second
auto-tick-rate: true
auto-tick-rate-limit: 20
#Sets the base tick rate (1 = 20 ticks per second, 2 = 10 ticks per second, etc.)
base-tick-rate: 1
#Tick all players each tick even when other settings disallow this.
always-tick-players: false
chunk-sending:
#To change server normal render distance, change view-distance in server.properties.

View File

@ -0,0 +1,59 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory;
use PHPUnit\Framework\TestCase;
use pocketmine\item\Item;
use pocketmine\item\ItemFactory;
class BaseInventoryTest extends TestCase{
public static function setUpBeforeClass(){
ItemFactory::init();
}
public function testAddItemDifferentUserData() : void{
$inv = new class extends BaseInventory{
public function getDefaultSize() : int{
return 1;
}
public function getName() : string{
return "";
}
};
$item1 = ItemFactory::get(Item::ARROW, 0, 1);
$item2 = ItemFactory::get(Item::ARROW, 0, 1)->setCustomName("TEST");
$inv->addItem(clone $item1);
self::assertFalse($inv->canAddItem($item2), "Item WITHOUT userdata should not stack with item WITH userdata");
self::assertNotEmpty($inv->addItem($item2));
$inv->clearAll();
self::assertEmpty($inv->getContents());
$inv->addItem(clone $item2);
self::assertFalse($inv->canAddItem($item1), "Item WITH userdata should not stack with item WITHOUT userdata");
self::assertNotEmpty($inv->addItem($item1));
}
}

View File

@ -25,28 +25,94 @@ namespace pocketmine\level\format\io\region;
use PHPUnit\Framework\TestCase;
use pocketmine\level\format\ChunkException;
use function file_exists;
use function random_bytes;
use function str_repeat;
use function sys_get_temp_dir;
use function unlink;
class RegionLoaderTest extends TestCase{
public function testChunkTooBig() : void{
$r = new RegionLoader(sys_get_temp_dir() . '/chunk_too_big.testregion_' . bin2hex(random_bytes(4)), 0, 0);
$r->open();
/** @var string */
private $regionPath;
/** @var RegionLoader */
private $region;
public function setUp(){
$this->regionPath = sys_get_temp_dir() . '/test.testregion';
if(file_exists($this->regionPath)){
unlink($this->regionPath);
}
$this->region = new RegionLoader($this->regionPath, 0, 0);
$this->region->open();
}
public function tearDown(){
$this->region->close();
if(file_exists($this->regionPath)){
unlink($this->regionPath);
}
}
public function testChunkTooBig() : void{
$this->expectException(ChunkException::class);
$r->writeChunk(0, 0, str_repeat("a", 1044476));
$this->region->writeChunk(0, 0, str_repeat("a", 1044476));
}
public function testChunkMaxSize() : void{
$data = str_repeat("a", 1044475);
$path = sys_get_temp_dir() . '/chunk_just_fits.testregion_' . bin2hex(random_bytes(4));
$r = new RegionLoader($path, 0, 0);
$r->open();
$this->region->writeChunk(0, 0, $data);
$this->region->close();
$r->writeChunk(0, 0, $data);
$r->close();
$r = new RegionLoader($path, 0, 0);
$r = new RegionLoader($this->regionPath, 0, 0);
$r->open();
self::assertSame($data, $r->readChunk(0, 0));
}
public function outOfBoundsCoordsProvider() : \Generator{
yield [-1, -1];
yield [32, 32];
yield [-1, 32];
yield [32, -1];
}
/**
* @dataProvider outOfBoundsCoordsProvider
* @param int $x
* @param int $z
*
* @throws ChunkException
* @throws \InvalidArgumentException
*/
public function testWriteChunkOutOfBounds(int $x, int $z) : void{
$this->expectException(\InvalidArgumentException::class);
$this->region->writeChunk($x, $z, str_repeat("\x00", 1000));
}
public function testReadWriteChunkInBounds() : void{
$dat = random_bytes(1000);
for($x = 0; $x < 32; ++$x){
for($z = 0; $z < 32; ++$z){
$this->region->writeChunk($x, $z, $dat);
}
}
for($x = 0; $x < 32; ++$x){
for($z = 0; $z < 32; ++$z){
self::assertSame($dat, $this->region->readChunk($x, $z));
}
}
}
/**
* @dataProvider outOfBoundsCoordsProvider
* @param int $x
* @param int $z
*
* @throws \InvalidArgumentException
* @throws \pocketmine\level\format\io\exception\CorruptedChunkException
*/
public function testReadChunkOutOfBounds(int $x, int $z) : void{
$this->expectException(\InvalidArgumentException::class);
$this->region->readChunk($x, $z);
}
}