isRunning === true; } /** * @return string */ public function getPocketMineVersion(){ return \pocketmine\VERSION; } /** * @return string */ public function getCodename(){ return \pocketmine\CODENAME; } /** * @return string */ public function getVersion(){ return \pocketmine\MINECRAFT_VERSION; } /** * @return string */ public function getApiVersion(){ return \pocketmine\API_VERSION; } /** * @return string */ public function getFilePath(){ return $this->filePath; } /** * @return string */ public function getDataPath(){ return $this->dataPath; } /** * @return string */ public function getPluginPath(){ return $this->pluginPath; } /** * @return int */ public function getMaxPlayers(){ return $this->maxPlayers; } /** * @return int */ public function getPort(){ return $this->getConfigInt("server-port", 19132); } /** * @return int */ public function getViewDistance(){ return max(56, $this->getProperty("chunk-sending.max-chunks", 256)); } /** * @return string */ public function getIp(){ return $this->getConfigString("server-ip", "0.0.0.0"); } /** * @return string */ public function getServerName(){ return $this->getConfigString("motd", "Minecraft: PE Server"); } /** * @return bool */ public function getAutoSave(){ return $this->autoSave; } /** * @param bool $value */ public function setAutoSave($value){ $this->autoSave = (bool) $value; foreach($this->getLevels() as $level){ $level->setAutoSave($this->autoSave); } } /** * @return string */ public function getLevelType(){ return $this->getConfigString("level-type", "DEFAULT"); } /** * @return bool */ public function getGenerateStructures(){ return $this->getConfigBoolean("generate-structures", true); } /** * @return int */ public function getGamemode(){ return $this->getConfigInt("gamemode", 0) & 0b11; } /** * @return bool */ public function getForceGamemode(){ return $this->getConfigBoolean("force-gamemode", false); } /** * Returns the gamemode text name * * @param int $mode * * @return string */ public static function getGamemodeString($mode){ switch((int) $mode){ case Player::SURVIVAL: return "SURVIVAL"; case Player::CREATIVE: return "CREATIVE"; case Player::ADVENTURE: return "ADVENTURE"; case Player::SPECTATOR: return "SPECTATOR"; } return "UNKNOWN"; } /** * Parses a string and returns a gamemode integer, -1 if not found * * @param string $str * * @return int */ public static function getGamemodeFromString($str){ switch(strtolower(trim($str))){ case (string) Player::SURVIVAL: case "survival": case "s": return Player::SURVIVAL; case (string) Player::CREATIVE: case "creative": case "c": return Player::CREATIVE; case (string) Player::ADVENTURE: case "adventure": case "a": return Player::ADVENTURE; case (string) Player::SPECTATOR: case "spectator": case "view": case "v": return Player::SPECTATOR; } return -1; } /** * @param string $str * * @return int */ public static function getDifficultyFromString($str){ switch(strtolower(trim($str))){ case "0": case "peaceful": case "p": return 0; case "1": case "easy": case "e": return 1; case "2": case "normal": case "n": return 2; case "3": case "hard": case "h": return 3; } return -1; } /** * @return int */ public function getDifficulty(){ return $this->getConfigInt("difficulty", 1); } /** * @return bool */ public function hasWhitelist(){ return $this->getConfigBoolean("white-list", false); } /** * @return int */ public function getSpawnRadius(){ return $this->getConfigInt("spawn-protection", 16); } /** * @return bool */ public function getAllowFlight(){ return $this->getConfigBoolean("allow-flight", false); } /** * @return bool */ public function isHardcore(){ return $this->getConfigBoolean("hardcore", false); } /** * @return int */ public function getDefaultGamemode(){ return $this->getConfigInt("gamemode", 0) & 0b11; } /** * @return string */ public function getMotd(){ return $this->getConfigString("motd", "Minecraft: PE Server"); } /** * @return \ClassLoader */ public function getLoader(){ return $this->autoloader; } /** * @return \AttachableThreadedLogger */ public function getLogger(){ return $this->logger; } /** * @return EntityMetadataStore */ public function getEntityMetadata(){ return $this->entityMetadata; } /** * @return PlayerMetadataStore */ public function getPlayerMetadata(){ return $this->playerMetadata; } /** * @return LevelMetadataStore */ public function getLevelMetadata(){ return $this->levelMetadata; } /** * @return AutoUpdater */ public function getUpdater(){ return $this->updater; } /** * @return PluginManager */ public function getPluginManager(){ return $this->pluginManager; } /** * @return CraftingManager */ public function getCraftingManager(){ return $this->craftingManager; } /** * @return ServerScheduler */ public function getScheduler(){ return $this->scheduler; } /** * @return GenerationRequestManager */ public function getGenerationManager(){ return $this->generationManager; } /** * @return int */ public function getTick(){ return $this->tickCounter; } /** * Returns the last server TPS measure * * @return float */ public function getTicksPerSecond(){ return round(array_sum($this->tickAverage) / count($this->tickAverage), 2); } /** * Returns the TPS usage/load in % * * @return float */ public function getTickUsage(){ return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2); } /** * @return SourceInterface[] */ public function getInterfaces(){ return $this->interfaces; } /** * @param SourceInterface $interface */ public function addInterface(SourceInterface $interface){ $this->interfaces[spl_object_hash($interface)] = $interface; } /** * @param SourceInterface $interface */ public function removeInterface(SourceInterface $interface){ $interface->shutdown(); unset($this->interfaces[spl_object_hash($interface)]); } /** * @param string $address * @param int $port * @param string $payload */ public function sendPacket($address, $port, $payload){ $this->mainInterface->putRaw($address, $port, $payload); } /** * Blocks an IP address from the main interface. Setting timeout to -1 will block it forever * * @param string $address * @param int $timeout */ public function blockAddress($address, $timeout = 300){ $this->mainInterface->blockAddress($address, $timeout); } /** * @param string $address * @param int $port * @param string $payload */ public function handlePacket($address, $port, $payload){ try{ if(strlen($payload) > 2 and substr($payload, 0, 2) === "\xfe\xfd" and $this->queryHandler instanceof QueryHandler){ $this->queryHandler->handle($address, $port, $payload); } }catch(\Exception $e){ if(\pocketmine\DEBUG > 1){ if($this->logger instanceof MainLogger){ $this->logger->logException($e); } } $this->blockAddress($address, 600); } //TODO: add raw packet events } /** * @return SimpleCommandMap */ public function getCommandMap(){ return $this->commandMap; } /** * @return Player[] */ public function getOnlinePlayers(){ return $this->players; } public function addRecipe(Recipe $recipe){ $this->craftingManager->registerRecipe($recipe); } /** * @param string $name * * @return OfflinePlayer|Player */ public function getOfflinePlayer($name){ $name = strtolower($name); $result = $this->getPlayerExact($name); if($result === null){ $result = new OfflinePlayer($this, $name); } return $result; } /** * @param string $name * * @return Compound */ public function getOfflinePlayerData($name){ $name = strtolower($name); $path = $this->getDataPath() . "players/"; if(file_exists($path . "$name.dat")){ try{ $nbt = new NBT(NBT::BIG_ENDIAN); $nbt->readCompressed(file_get_contents($path . "$name.dat")); return $nbt->getData(); }catch(\Exception $e){ //zlib decode error / corrupt data rename($path . "$name.dat", $path . "$name.dat.bak"); $this->logger->warning("Corrupted data found for \"" . $name . "\", creating new profile"); } }else{ $this->logger->notice("Player data not found for \"" . $name . "\", creating new profile"); } $spawn = $this->getDefaultLevel()->getSafeSpawn(); $nbt = new Compound(false, [ new Long("firstPlayed", floor(microtime(true) * 1000)), new Long("lastPlayed", floor(microtime(true) * 1000)), new Enum("Pos", [ new Double(0, $spawn->x), new Double(1, $spawn->y), new Double(2, $spawn->z) ]), new String("Level", $this->getDefaultLevel()->getName()), //new String("SpawnLevel", $this->getDefaultLevel()->getName()), //new Int("SpawnX", (int) $spawn->x), //new Int("SpawnY", (int) $spawn->y), //new Int("SpawnZ", (int) $spawn->z), //new Byte("SpawnForced", 1), //TODO new Enum("Inventory", []), new Compound("Achievements", []), new Int("playerGameType", $this->getGamemode()), new Enum("Motion", [ new Double(0, 0.0), new Double(1, 0.0), new Double(2, 0.0) ]), new Enum("Rotation", [ new Float(0, 0.0), new Float(1, 0.0) ]), new Float("FallDistance", 0.0), new Short("Fire", 0), new Short("Air", 0), new Byte("OnGround", 1), new Byte("Invulnerable", 0), new String("NameTag", $name), ]); $nbt->Pos->setTagType(NBT::TAG_Double); $nbt->Inventory->setTagType(NBT::TAG_Compound); $nbt->Motion->setTagType(NBT::TAG_Double); $nbt->Rotation->setTagType(NBT::TAG_Float); if(file_exists($path . "$name.yml")){ //Importing old PocketMine-MP files $data = new Config($path . "$name.yml", Config::YAML, []); $nbt["playerGameType"] = (int) $data->get("gamemode"); $nbt["Level"] = $data->get("position")["level"]; $nbt["Pos"][0] = $data->get("position")["x"]; $nbt["Pos"][1] = $data->get("position")["y"]; $nbt["Pos"][2] = $data->get("position")["z"]; $nbt["SpawnLevel"] = $data->get("spawn")["level"]; $nbt["SpawnX"] = (int) $data->get("spawn")["x"]; $nbt["SpawnY"] = (int) $data->get("spawn")["y"]; $nbt["SpawnZ"] = (int) $data->get("spawn")["z"]; $this->logger->notice("Old Player data found for \"" . $name . "\", upgrading profile"); foreach($data->get("inventory") as $slot => $item){ if(count($item) === 3){ $nbt->Inventory[$slot + 9] = new Compound(false, [ new Short("id", $item[0]), new Short("Damage", $item[1]), new Byte("Count", $item[2]), new Byte("Slot", $slot + 9), new Byte("TrueSlot", $slot + 9) ]); } } foreach($data->get("hotbar") as $slot => $itemSlot){ if(isset($nbt->Inventory[$itemSlot + 9])){ $item = $nbt->Inventory[$itemSlot + 9]; $nbt->Inventory[$slot] = new Compound(false, [ new Short("id", $item["id"]), new Short("Damage", $item["Damage"]), new Byte("Count", $item["Count"]), new Byte("Slot", $slot), new Byte("TrueSlot", $item["TrueSlot"]) ]); } } foreach($data->get("armor") as $slot => $item){ if(count($item) === 2){ $nbt->Inventory[$slot + 100] = new Compound(false, [ new Short("id", $item[0]), new Short("Damage", $item[1]), new Byte("Count", 1), new Byte("Slot", $slot + 100) ]); } } foreach($data->get("achievements") as $achievement => $status){ $nbt->Achievements[$achievement] = new Byte($achievement, $status == true ? 1 : 0); } unlink($path . "$name.yml"); } $this->saveOfflinePlayerData($name, $nbt); return $nbt; } /** * @param string $name * @param Compound $nbtTag */ public function saveOfflinePlayerData($name, Compound $nbtTag){ $nbt = new NBT(NBT::BIG_ENDIAN); try{ $nbt->setData($nbtTag); file_put_contents($this->getDataPath() . "players/" . strtolower($name) . ".dat", $nbt->writeCompressed()); }catch(\Exception $e){ $this->logger->critical("Could not save player " . $name . ": " . $e->getMessage()); if(\pocketmine\DEBUG > 1 and $this->logger instanceof MainLogger){ $this->logger->logException($e); } } } /** * @param string $name * * @return Player */ public function getPlayer($name){ $found = null; $name = strtolower($name); $delta = PHP_INT_MAX; foreach($this->getOnlinePlayers() as $player){ if(stripos($player->getName(), $name) === 0){ $curDelta = strlen($player->getName()) - strlen($name); if($curDelta < $delta){ $found = $player; $delta = $curDelta; } if($curDelta === 0){ break; } } } return $found; } /** * @param string $name * * @return Player */ public function getPlayerExact($name){ $name = strtolower($name); foreach($this->getOnlinePlayers() as $player){ if(strtolower($player->getName()) === $name){ return $player; } } return null; } /** * @param string $partialName * * @return Player[] */ public function matchPlayer($partialName){ $partialName = strtolower($partialName); $matchedPlayers = []; foreach($this->getOnlinePlayers() as $player){ if(strtolower($player->getName()) === $partialName){ $matchedPlayers = [$player]; break; }elseif(stripos($player->getName(), $partialName) !== false){ $matchedPlayers[] = $player; } } return $matchedPlayers; } /** * @param Player $player */ public function removePlayer(Player $player){ foreach($this->players as $identifier => $p){ if($player === $p){ unset($this->players[$identifier]); break; } } } /** * @return Level[] */ public function getLevels(){ return $this->levels; } /** * @return Level */ public function getDefaultLevel(){ return $this->levelDefault; } /** * Sets the default level to a different level * This won't change the level-name property, * it only affects the server on runtime * * @param Level $level */ public function setDefaultLevel($level){ if($level === null or ($this->isLevelLoaded($level->getFolderName()) and $level !== $this->levelDefault)){ $this->levelDefault = $level; } } /** * @param string $name * * @return bool */ public function isLevelLoaded($name){ return $this->getLevelByName($name) instanceof Level; } /** * @param int $levelId * * @return Level */ public function getLevel($levelId){ if(isset($this->levels[$levelId])){ return $this->levels[$levelId]; } return null; } /** * @param $name * * @return Level */ public function getLevelByName($name){ foreach($this->getLevels() as $level){ if($level->getFolderName() === $name){ return $level; } } return null; } /** * @param Level $level * @param bool $forceUnload * * @return bool */ public function unloadLevel(Level $level, $forceUnload = false){ if($level->unload($forceUnload) === true){ unset($this->levels[$level->getId()]); return true; } return false; } /** * Loads a level from the data directory * * @param string $name * * @return bool * * @throws LevelException */ public function loadLevel($name){ if(trim($name) === ""){ throw new LevelException("Invalid empty level name"); } if($this->isLevelLoaded($name)){ return true; }elseif(!$this->isLevelGenerated($name)){ $this->logger->notice("Level \"" . $name . "\" not found"); return false; } $path = $this->getDataPath() . "worlds/" . $name . "/"; $provider = LevelProviderManager::getProvider($path); if($provider === null){ $this->logger->error("Could not load level \"" . $name . "\": Unknown provider"); return false; } //$entities = new Config($path."entities.yml", Config::YAML); //if(file_exists($path . "tileEntities.yml")){ // @rename($path . "tileEntities.yml", $path . "tiles.yml"); //} try{ $level = new Level($this, $name, $path, $provider); }catch(\Exception $e){ $this->logger->error("Could not load level \"" . $name . "\": " . $e->getMessage()); if($this->logger instanceof MainLogger){ $this->logger->logException($e); } return false; } $this->levels[$level->getId()] = $level; $level->initLevel(); $this->getPluginManager()->callEvent(new LevelLoadEvent($level)); /*foreach($entities->getAll() as $entity){ if(!isset($entity["id"])){ break; } if($entity["id"] === 64){ //Item Drop $e = $this->server->api->entity->add($this->levels[$name], ENTITY_ITEM, $entity["Item"]["id"], array( "meta" => $entity["Item"]["Damage"], "stack" => $entity["Item"]["Count"], "x" => $entity["Pos"][0], "y" => $entity["Pos"][1], "z" => $entity["Pos"][2], "yaw" => $entity["Rotation"][0], "pitch" => $entity["Rotation"][1], )); }elseif($entity["id"] === FALLING_SAND){ $e = $this->server->api->entity->add($this->levels[$name], ENTITY_FALLING, $entity["id"], $entity); $e->setPosition(new Vector3($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2]), $entity["Rotation"][0], $entity["Rotation"][1]); $e->setHealth($entity["Health"]); }elseif($entity["id"] === OBJECT_PAINTING or $entity["id"] === OBJECT_ARROW){ //Painting $e = $this->server->api->entity->add($this->levels[$name], ENTITY_OBJECT, $entity["id"], $entity); $e->setPosition(new Vector3($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2]), $entity["Rotation"][0], $entity["Rotation"][1]); $e->setHealth(1); }else{ $e = $this->server->api->entity->add($this->levels[$name], ENTITY_MOB, $entity["id"], $entity); $e->setPosition(new Vector3($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2]), $entity["Rotation"][0], $entity["Rotation"][1]); $e->setHealth($entity["Health"]); } }*/ /*if(file_exists($path . "tiles.yml")){ $tiles = new Config($path . "tiles.yml", Config::YAML); foreach($tiles->getAll() as $tile){ if(!isset($tile["id"])){ continue; } $level->loadChunk($tile["x"] >> 4, $tile["z"] >> 4); $nbt = new Compound(false, []); foreach($tile as $index => $data){ switch($index){ case "Items": $tag = new Enum("Items", []); $tag->setTagType(NBT::TAG_Compound); foreach($data as $slot => $fields){ $tag[(int) $slot] = new Compound(false, array( "Count" => new Byte("Count", $fields["Count"]), "Slot" => new Short("Slot", $fields["Slot"]), "Damage" => new Short("Damage", $fields["Damage"]), "id" => new String("id", $fields["id"]) )); } $nbt["Items"] = $tag; break; case "id": case "Text1": case "Text2": case "Text3": case "Text4": $nbt[$index] = new String($index, $data); break; case "x": case "y": case "z": case "pairx": case "pairz": $nbt[$index] = new Int($index, $data); break; case "BurnTime": case "CookTime": case "MaxTime": $nbt[$index] = new Short($index, $data); break; } } switch($tile["id"]){ case Tile::FURNACE: new Furnace($level, $nbt); break; case Tile::CHEST: new Chest($level, $nbt); break; case Tile::SIGN: new Sign($level, $nbt); break; } } unlink($path . "tiles.yml"); $level->save(true, true); }*/ return true; } /** * Generates a new level if it does not exists * * @param string $name * @param int $seed * @param string $generator Class name that extends pocketmine\level\generator\Noise * @param array $options * * @return bool */ public function generateLevel($name, $seed = null, $generator = null, $options = []){ if(trim($name) === "" or $this->isLevelGenerated($name)){ return false; } $seed = $seed === null ? Binary::readInt(@Utils::getRandomBytes(4, false)) : (int) $seed; if($generator !== null and class_exists($generator) and is_subclass_of($generator, Generator::class)){ $generator = new $generator($options); }else{ $options["preset"] = $this->getConfigString("generator-settings", ""); $generator = Generator::getGenerator($this->getLevelType()); } if(($provider = LevelProviderManager::getProviderByName($providerName = $this->getProperty("level-settings.default-format", "mcregion"))) === null){ $provider = LevelProviderManager::getProviderByName($providerName = "mcregion"); } try{ $path = $this->getDataPath() . "worlds/" . $name . "/"; /** @var \pocketmine\level\format\LevelProvider $provider */ $provider::generate($path, $name, $seed, $generator, $options); $level = new Level($this, $name, $path, $provider); $this->levels[$level->getId()] = $level; $level->initLevel(); }catch(\Exception $e){ $this->logger->error("Could not generate level \"" . $name . "\": " . $e->getMessage()); if($this->logger instanceof MainLogger){ $this->logger->logException($e); } return false; } $this->getPluginManager()->callEvent(new LevelInitEvent($level)); $this->getPluginManager()->callEvent(new LevelLoadEvent($level)); $this->getLogger()->notice("Spawn terrain for level \"$name\" is being generated in the background"); $centerX = $level->getSpawnLocation()->getX() >> 4; $centerZ = $level->getSpawnLocation()->getZ() >> 4; $order = []; for($X = -4; $X <= 4; ++$X){ for($Z = -4; $Z <= 4; ++$Z){ $distance = $X ** 2 + $Z ** 2; $chunkX = $X + $centerX; $chunkZ = $Z + $centerZ; $index = Level::chunkHash($chunkX, $chunkZ); $order[$index] = $distance; } } asort($order); $chunkX = $chunkZ = null; foreach($order as $index => $distance){ Level::getXZ($index, $chunkX, $chunkZ); $level->generateChunk($chunkX, $chunkZ); } return true; } /** * @param string $name * * @return bool */ public function isLevelGenerated($name){ if(trim($name) === ""){ return false; } $path = $this->getDataPath() . "worlds/" . $name . "/"; if(!($this->getLevelByName($name) instanceof Level)){ if(LevelProviderManager::getProvider($path) === null){ return false; } /*if(file_exists($path)){ $level = new LevelImport($path); if($level->import() === false){ //Try importing a world return false; } }else{ return false; }*/ } return true; } /** * @param string $variable * @param string $defaultValue * * @return string */ public function getConfigString($variable, $defaultValue = ""){ $v = getopt("", ["$variable::"]); if(isset($v[$variable])){ return (string) $v[$variable]; } return $this->properties->exists($variable) ? $this->properties->get($variable) : $defaultValue; } /** * @param string $variable * @param mixed $defaultValue * * @return mixed */ public function getProperty($variable, $defaultValue = null){ $value = $this->config->getNested($variable); return $value === null ? $defaultValue : $value; } /** * @param string $variable * @param string $value */ public function setConfigString($variable, $value){ $this->properties->set($variable, $value); } /** * @param string $variable * @param int $defaultValue * * @return int */ public function getConfigInt($variable, $defaultValue = 0){ $v = getopt("", ["$variable::"]); if(isset($v[$variable])){ return (int) $v[$variable]; } return $this->properties->exists($variable) ? (int) $this->properties->get($variable) : (int) $defaultValue; } /** * @param string $variable * @param int $value */ public function setConfigInt($variable, $value){ $this->properties->set($variable, (int) $value); } /** * @param string $variable * @param boolean $defaultValue * * @return boolean */ public function getConfigBoolean($variable, $defaultValue = false){ $v = getopt("", ["$variable::"]); if(isset($v[$variable])){ $value = $v[$variable]; }else{ $value = $this->properties->exists($variable) ? $this->properties->get($variable) : $defaultValue; } if(is_bool($value)){ return $value; } switch(strtolower($value)){ case "on": case "true": case "1": case "yes": return true; } return false; } /** * @param string $variable * @param bool $value */ public function setConfigBool($variable, $value){ $this->properties->set($variable, $value == true ? "1" : "0"); } /** * @param string $name * * @return PluginIdentifiableCommand */ public function getPluginCommand($name){ if(($command = $this->commandMap->getCommand($name)) instanceof PluginIdentifiableCommand){ return $command; }else{ return null; } } /** * @return BanList */ public function getNameBans(){ return $this->banByName; } /** * @return BanList */ public function getIPBans(){ return $this->banByIP; } /** * @param string $name */ public function addOp($name){ $this->operators->set(strtolower($name), true); if(($player = $this->getPlayerExact($name)) instanceof Player){ $player->recalculatePermissions(); } $this->operators->save(); } /** * @param string $name */ public function removeOp($name){ $this->operators->remove(strtolower($name)); if(($player = $this->getPlayerExact($name)) instanceof Player){ $player->recalculatePermissions(); } $this->operators->save(); } /** * @param string $name */ public function addWhitelist($name){ $this->whitelist->set(strtolower($name), true); $this->whitelist->save(); } /** * @param string $name */ public function removeWhitelist($name){ $this->whitelist->remove(strtolower($name)); $this->whitelist->save(); } /** * @param string $name * * @return bool */ public function isWhitelisted($name){ return !$this->hasWhitelist() or $this->operators->exists($name, true) or $this->whitelist->exists($name, true); } /** * @param string $name * * @return bool */ public function isOp($name){ return $this->operators->exists($name, true); } /** * @return Config */ public function getWhitelisted(){ return $this->whitelist; } /** * @return Config */ public function getOps(){ return $this->operators; } public function reloadWhitelist(){ $this->whitelist->reload(); } /** * @return string[] */ public function getCommandAliases(){ $section = $this->getProperty("aliases"); $result = []; if(is_array($section)){ foreach($section as $key => $value){ $commands = []; if(is_array($value)){ $commands = $value; }else{ $commands[] = $value; } $result[$key] = $commands; } } return $result; } /** * @return Server */ public static function getInstance(){ return self::$instance; } /** * @param \ClassLoader $autoloader * @param \ThreadedLogger $logger * @param string $filePath * @param string $dataPath * @param string $pluginPath */ public function __construct(\ClassLoader $autoloader, \ThreadedLogger $logger, $filePath, $dataPath, $pluginPath){ self::$instance = $this; $this->autoloader = $autoloader; $this->logger = $logger; $this->filePath = $filePath; if(!file_exists($dataPath . "worlds/")){ mkdir($dataPath . "worlds/", 0777); } if(!file_exists($dataPath . "players/")){ mkdir($dataPath . "players/", 0777); } if(!file_exists($pluginPath)){ mkdir($pluginPath, 0777); } $this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR; $this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR; $this->console = new CommandReader(); $version = new VersionString($this->getPocketMineVersion()); $this->logger->info("Starting Minecraft: PE server version " . TextFormat::AQUA . $this->getVersion()); $this->logger->info("Loading pocketmine.yml..."); if(!file_exists($this->dataPath . "pocketmine.yml")){ $content = file_get_contents($this->filePath . "src/pocketmine/resources/pocketmine.yml"); if($version->isDev()){ $content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content); } @file_put_contents($this->dataPath . "pocketmine.yml", $content); } $this->config = new Config($this->dataPath . "pocketmine.yml", Config::YAML, []); $this->logger->info("Loading server properties..."); $this->properties = new Config($this->dataPath . "server.properties", Config::PROPERTIES, [ "motd" => "Minecraft: PE Server", "server-port" => 19132, "white-list" => false, "announce-player-achievements" => true, "spawn-protection" => 16, "max-players" => 20, "allow-flight" => false, "spawn-animals" => true, "spawn-mobs" => true, "gamemode" => 0, "force-gamemode" => false, "hardcore" => false, "pvp" => true, "difficulty" => 1, "generator-settings" => "", "level-name" => "world", "level-seed" => "", "level-type" => "DEFAULT", "enable-query" => true, "enable-rcon" => false, "rcon.password" => substr(base64_encode(@Utils::getRandomBytes(20, false)), 3, 10), "auto-save" => true, ]); ServerScheduler::$WORKERS = $this->getProperty("settings.async-workers", ServerScheduler::$WORKERS); $this->scheduler = new ServerScheduler(); if($this->getConfigBoolean("enable-rcon", false) === true){ $this->rcon = new RCON($this, $this->getConfigString("rcon.password", ""), $this->getConfigInt("rcon.port", $this->getPort()), ($ip = $this->getIp()) != "" ? $ip : "0.0.0.0", $this->getConfigInt("rcon.threads", 1), $this->getConfigInt("rcon.clients-per-thread", 50)); } $this->entityMetadata = new EntityMetadataStore(); $this->playerMetadata = new PlayerMetadataStore(); $this->levelMetadata = new LevelMetadataStore(); $this->operators = new Config($this->dataPath . "ops.txt", Config::ENUM); $this->whitelist = new Config($this->dataPath . "white-list.txt", Config::ENUM); if(file_exists($this->dataPath . "banned.txt") and !file_exists($this->dataPath . "banned-players.txt")){ @rename($this->dataPath . "banned.txt", $this->dataPath . "banned-players.txt"); } @touch($this->dataPath . "banned-players.txt"); $this->banByName = new BanList($this->dataPath . "banned-players.txt"); $this->banByName->load(); @touch($this->dataPath . "banned-ips.txt"); $this->banByIP = new BanList($this->dataPath . "banned-ips.txt"); $this->banByIP->load(); $this->maxPlayers = $this->getConfigInt("max-players", 20); $this->setAutoSave($this->getConfigBoolean("auto-save", true)); if(($memory = str_replace("B", "", strtoupper($this->getConfigString("memory-limit", -1)))) !== false and $memory > 1){ $value = ["M" => 1, "G" => 1024]; $real = ((int) substr($memory, 0, -1)) * $value[substr($memory, -1)]; if($real < 128){ $this->logger->warning($this->getName() . " may not work right with less than 128MB of memory"); } @ini_set("memory_limit", $memory); } if($this->getConfigBoolean("hardcore", false) === true and $this->getDifficulty() < 3){ $this->setConfigInt("difficulty", 3); } define("pocketmine\\DEBUG", (int) $this->getProperty("debug.level", 1)); if($this->logger instanceof MainLogger){ $this->logger->setLogDebug(\pocketmine\DEBUG > 1); } define("ADVANCED_CACHE", $this->getProperty("settings.advanced-cache", false)); if(ADVANCED_CACHE == true){ $this->logger->info("Advanced cache enabled"); } Level::$COMPRESSION_LEVEL = $this->getProperty("chunk-sending.compression-level", 8); if(defined("pocketmine\\DEBUG") and \pocketmine\DEBUG >= 0){ @cli_set_process_title($this->getName() . " " . $this->getPocketMineVersion()); } $this->logger->info("Starting Minecraft PE server on " . ($this->getIp() === "" ? "*" : $this->getIp()) . ":" . $this->getPort()); define("BOOTUP_RANDOM", @Utils::getRandomBytes(16)); $this->serverID = Binary::readLong(substr(Utils::getUniqueID(true, $this->getIp() . $this->getPort()), 0, 8)); $this->addInterface($this->mainInterface = new RakLibInterface($this)); $this->logger->info("This server is running " . $this->getName() . " version " . ($version->isDev() ? TextFormat::YELLOW : "") . $version->get(true) . TextFormat::WHITE . " \"" . $this->getCodename() . "\" (API " . $this->getApiVersion() . ")"); $this->logger->info($this->getName() . " is distributed under the LGPL License"); PluginManager::$pluginParentTimer = new TimingsHandler("** Plugins"); Timings::init(); $this->consoleSender = new ConsoleCommandSender(); $this->commandMap = new SimpleCommandMap($this); $this->registerEntities(); $this->registerTiles(); InventoryType::init(); Block::init(); Item::init(); Biome::init(); Effect::init(); /** TODO: @deprecated */ TextWrapper::init(); $this->craftingManager = new CraftingManager(); $this->pluginManager = new PluginManager($this, $this->commandMap); $this->pluginManager->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this->consoleSender); $this->pluginManager->setUseTimings($this->getProperty("settings.enable-profiling", false)); $this->pluginManager->registerInterface(PharPluginLoader::class); set_exception_handler([$this, "exceptionHandler"]); register_shutdown_function([$this, "crashDump"]); $this->pluginManager->loadPlugins($this->pluginPath); $this->updater = new AutoUpdater($this, $this->getProperty("auto-updater.host", "www.pocketmine.net")); $this->enablePlugins(PluginLoadOrder::STARTUP); if($this->getProperty("chunk-generation.use-async", true)){ $this->generationManager = new GenerationRequestManager($this); }else{ $this->generationManager = new GenerationInstanceManager($this); } LevelProviderManager::addProvider($this, Anvil::class); LevelProviderManager::addProvider($this, McRegion::class); if(extension_loaded("leveldb")){ $this->logger->debug("Enabling LevelDB support"); LevelProviderManager::addProvider($this, LevelDB::class); } Generator::addGenerator(Flat::class, "flat"); Generator::addGenerator(Normal::class, "normal"); Generator::addGenerator(Normal::class, "default"); foreach((array) $this->getProperty("worlds", []) as $name => $worldSetting){ if($this->loadLevel($name) === false){ $seed = $this->getProperty("worlds.$name.seed", time()); $options = explode(":", $this->getProperty("worlds.$name.generator", Generator::getGenerator("default"))); $generator = Generator::getGenerator(array_shift($options)); if(count($options) > 0){ $options = [ "preset" => implode(":", $options), ]; }else{ $options = []; } $this->generateLevel($name, $seed, $generator, $options); } } if($this->getDefaultLevel() === null){ $default = $this->getConfigString("level-name", "world"); if(trim($default) == ""){ $this->getLogger()->warning("level-name cannot be null, using default"); $default = "world"; $this->setConfigString("level-name", "world"); } if($this->loadLevel($default) === false){ $seed = $this->getConfigInt("level-seed", time()); $this->generateLevel($default, $seed === 0 ? time() : $seed); } $this->setDefaultLevel($this->getLevelByName($default)); } $this->properties->save(); if(!($this->getDefaultLevel() instanceof Level)){ $this->getLogger()->emergency("No default level has been loaded"); $this->forceShutdown(); return; } $this->scheduler->scheduleDelayedRepeatingTask(new CallbackTask([Cache::class, "cleanup"]), $this->getProperty("ticks-per.cache-cleanup", 900), $this->getProperty("ticks-per.cache-cleanup", 900)); if($this->getAutoSave() and $this->getProperty("ticks-per.autosave", 6000) > 0){ $this->scheduler->scheduleDelayedRepeatingTask(new CallbackTask([$this, "doAutoSave"]), $this->getProperty("ticks-per.autosave", 6000), $this->getProperty("ticks-per.autosave", 6000)); } if($this->getProperty("chunk-gc.period-in-ticks", 600) > 0){ $this->scheduler->scheduleDelayedRepeatingTask(new CallbackTask([$this, "doLevelGC"]), $this->getProperty("chunk-gc.period-in-ticks", 600), $this->getProperty("chunk-gc.period-in-ticks", 600)); } $this->scheduler->scheduleRepeatingTask(new GarbageCollectionTask(), 900); $this->enablePlugins(PluginLoadOrder::POSTWORLD); $this->start(); } /** * @param $message * @param Player[] $recipients * * @return int */ public function broadcastMessage($message, $recipients = null){ if(!is_array($recipients)){ return $this->broadcast($message, self::BROADCAST_CHANNEL_USERS); } /** @var Player[] $recipients */ foreach($recipients as $recipient){ $recipient->sendMessage($message); } } /** * @param string $message * @param string $permissions * * @return int */ public function broadcast($message, $permissions){ /** @var CommandSender[] $recipients */ $recipients = []; foreach(explode(";", $permissions) as $permission){ foreach($this->pluginManager->getPermissionSubscriptions($permission) as $permissible){ if($permissible instanceof CommandSender and $permissible->hasPermission($permission)){ $recipients[spl_object_hash($permissible)] = $permissible; // do not send messages directly, or some might be repeated } } } foreach($recipients as $recipient){ $recipient->sendMessage($message); } return count($recipients); } /** * Broadcasts a Minecraft packet to a list of players * * @param Player[] $players * @param DataPacket $packet */ public static function broadcastPacket(array $players, DataPacket $packet){ $packet->encode(); $packet->isEncoded = true; foreach($players as $player){ $player->dataPacket($packet); } if(isset($packet->__encapsulatedPacket)){ unset($packet->__encapsulatedPacket); } } /** * @param int $type */ public function enablePlugins($type){ foreach($this->pluginManager->getPlugins() as $plugin){ if(!$plugin->isEnabled() and $plugin->getDescription()->getOrder() === $type){ $this->enablePlugin($plugin); } } if($type === PluginLoadOrder::POSTWORLD){ $this->commandMap->registerServerAliases(); DefaultPermissions::registerCorePermissions(); } } /** * @param Plugin $plugin */ public function enablePlugin(Plugin $plugin){ $this->pluginManager->enablePlugin($plugin); } /** * @param Plugin $plugin * * @deprecated */ public function loadPlugin(Plugin $plugin){ $this->enablePlugin($plugin); } public function disablePlugins(){ $this->pluginManager->disablePlugins(); } public function checkConsole(){ Timings::$serverCommandTimer->startTiming(); if(($line = $this->console->getLine()) !== null){ $this->pluginManager->callEvent($ev = new ServerCommandEvent($this->consoleSender, $line)); if(!$ev->isCancelled()){ $this->dispatchCommand($ev->getSender(), $ev->getCommand()); } } Timings::$serverCommandTimer->stopTiming(); } /** * Executes a command from a CommandSender * * @param CommandSender $sender * @param string $commandLine * * @return bool * * @throws \Exception */ public function dispatchCommand(CommandSender $sender, $commandLine){ if(!($sender instanceof CommandSender)){ throw new ServerException("CommandSender is not valid"); } if($this->commandMap->dispatch($sender, $commandLine)){ return true; } if($sender instanceof Player){ $sender->sendMessage("Unknown command. Type \"/help\" for help."); }else{ $sender->sendMessage("Unknown command. Type \"help\" for help."); } return false; } public function reload(){ $this->logger->info("Saving levels..."); foreach($this->levels as $level){ $level->save(); } $this->pluginManager->disablePlugins(); $this->pluginManager->clearPlugins(); $this->commandMap->clearCommands(); $this->logger->info("Reloading properties..."); $this->properties->reload(); $this->maxPlayers = $this->getConfigInt("max-players", 20); if(($memory = str_replace("B", "", strtoupper($this->getConfigString("memory-limit", -1)))) !== false and $memory > 1){ $value = ["M" => 1, "G" => 1024]; $real = ((int) substr($memory, 0, -1)) * $value[substr($memory, -1)]; if($real < 256){ $this->logger->warning($this->getName() . " may not work right with less than 256MB of memory", true, true, 0); } @ini_set("memory_limit", $memory); } if($this->getConfigBoolean("hardcore", false) === true and $this->getDifficulty() < 3){ $this->setConfigInt("difficulty", 3); } $this->banByIP->load(); $this->banByName->load(); $this->reloadWhitelist(); $this->operators->reload(); foreach($this->getIPBans()->getEntries() as $entry){ $this->blockAddress($entry->getName(), -1); } $this->pluginManager->registerInterface(PharPluginLoader::class); $this->pluginManager->loadPlugins($this->pluginPath); $this->enablePlugins(PluginLoadOrder::STARTUP); $this->enablePlugins(PluginLoadOrder::POSTWORLD); TimingsHandler::reload(); } /** * Shutdowns the server correctly */ public function shutdown(){ $this->isRunning = false; gc_collect_cycles(); } public function forceShutdown(){ if($this->hasStopped){ return; } try{ $this->hasStopped = true; $this->shutdown(); if($this->rcon instanceof RCON){ $this->rcon->stop(); } if($this->getProperty("settings.upnp-forwarding", false) === true){ $this->logger->info("[UPnP] Removing port forward..."); UPnP::RemovePortForward($this->getPort()); } $this->pluginManager->disablePlugins(); foreach($this->players as $player){ $player->close(TextFormat::YELLOW . $player->getName() . " has left the game", $this->getProperty("settings.shutdown-message", "Server closed")); } foreach($this->getLevels() as $level){ $this->unloadLevel($level, true); } if($this->generationManager instanceof GenerationRequestManager){ $this->generationManager->shutdown(); } HandlerList::unregisterAll(); $this->scheduler->cancelAllTasks(); $this->scheduler->mainThreadHeartbeat(PHP_INT_MAX); $this->properties->save(); $this->console->kill(); foreach($this->interfaces as $interface){ $interface->shutdown(); } }catch(\Exception $e){ $this->logger->emergency("Crashed while crashing, killing process"); @kill(getmypid()); } } /** * Starts the PocketMine-MP server and starts processing ticks and packets */ public function start(){ if($this->getConfigBoolean("enable-query", true) === true){ $this->queryHandler = new QueryHandler(); } foreach($this->getIPBans()->getEntries() as $entry){ $this->blockAddress($entry->getName(), -1); } if($this->getProperty("settings.send-usage", true) !== false){ $this->scheduler->scheduleDelayedRepeatingTask(new CallbackTask([$this, "sendUsage"]), 6000, 6000); $this->sendUsage(); } if($this->getProperty("settings.upnp-forwarding", false) == true){ $this->logger->info("[UPnP] Trying to port forward..."); UPnP::PortForward($this->getPort()); } $this->tickCounter = 0; if(function_exists("pcntl_signal")){ pcntl_signal(SIGTERM, [$this, "handleSignal"]); pcntl_signal(SIGINT, [$this, "handleSignal"]); pcntl_signal(SIGHUP, [$this, "handleSignal"]); $this->getScheduler()->scheduleRepeatingTask(new CallbackTask("pcntl_signal_dispatch"), 5); } $this->getScheduler()->scheduleRepeatingTask(new CallbackTask([$this, "checkTicks"]), 20 * 5); $this->logger->info("Default game type: " . self::getGamemodeString($this->getGamemode())); $this->logger->info("Done (" . round(microtime(true) - \pocketmine\START_TIME, 3) . 's)! For help, type "help" or "?"'); $this->tickProcessor(); $this->forceShutdown(); gc_collect_cycles(); } public function handleSignal($signo){ if($signo === SIGTERM or $signo === SIGINT or $signo === SIGHUP){ $this->shutdown(); } } public function checkTicks(){ if($this->getTicksPerSecond() < 12){ $this->logger->warning("Can't keep up! Is the server overloaded?"); } } public function exceptionHandler(\Exception $e, $trace = null){ if($e === null){ return; } global $lastError; if($trace === null){ $trace = $e->getTrace(); } $errstr = $e->getMessage(); $errfile = $e->getFile(); $errno = $e->getCode(); $errline = $e->getLine(); $type = ($errno === E_ERROR or $errno === E_USER_ERROR) ? \LogLevel::ERROR : (($errno === E_USER_WARNING or $errno === E_WARNING) ? \LogLevel::WARNING : \LogLevel::NOTICE); if(($pos = strpos($errstr, "\n")) !== false){ $errstr = substr($errstr, 0, $pos); } $errfile = cleanPath($errfile); if($this->logger instanceof MainLogger){ $this->logger->logException($e, $trace); } $lastError = [ "type" => $type, "message" => $errstr, "fullFile" => $e->getFile(), "file" => $errfile, "line" => $errline, "trace" => @getTrace(1, $trace) ]; global $lastExceptionError, $lastError; $lastExceptionError = $lastError; $this->crashDump(); } public function crashDump(){ if($this->isRunning === false){ return; } $this->isRunning = false; $this->hasStopped = false; ini_set("error_reporting", 0); ini_set("memory_limit", -1); //Fix error dump not dumped on memory problems $this->logger->emergency("An unrecoverable error has occurred and the server has crashed. Creating a crash dump"); try{ $dump = new CrashDump($this); }catch(\Exception $e){ $this->logger->critical("Could not create Crash Dump: " . $e->getMessage()); return; } $this->logger->emergency("Please submit the \"" . $dump->getPath() . "\" file to the Bug Reporting page. Give as much info as you can."); if($this->getProperty("auto-report.enabled", true) !== false){ $report = true; $plugin = $dump->getData()["plugin"]; if(is_string($plugin)){ $p = $this->pluginManager->getPlugin($plugin); if($p instanceof Plugin and !($p->getPluginLoader() instanceof PharPluginLoader)){ $report = false; } }elseif(\Phar::running(true) == ""){ $report = false; } if($dump->getData()["error"]["type"] === "E_PARSE" or $dump->getData()["error"]["type"] === "E_COMPILE_ERROR"){ $report = false; } if($report){ $reply = Utils::postURL("http://" . $this->getProperty("auto-report.host", "crash.pocketmine.net") . "/submit/api", [ "report" => "yes", "name" => $this->getName() . " " . $this->getPocketMineVersion(), "email" => "crash@pocketmine.net", "reportPaste" => base64_encode($dump->getEncodedData()) ]); if(($data = json_decode($reply)) !== false and isset($data->crashId)){ $reportId = $data->crashId; $reportUrl = $data->crashUrl; $this->logger->emergency("The crash dump has been automatically submitted to the Crash Archive. You can view it on $reportUrl or use the ID #$reportId."); } } } //$this->checkMemory(); //$dump .= "Memory Usage Tracking: \r\n" . chunk_split(base64_encode(gzdeflate(implode(";", $this->memoryStats), 9))) . "\r\n"; $this->forceShutdown(); @kill(getmypid()); exit(1); } public function __debugInfo(){ return []; } private function tickProcessor(){ while($this->isRunning){ $this->tick(); usleep((int) max(1, ($this->nextTick - microtime(true)) * 1000000)); } } public function addPlayer($identifier, Player $player){ $this->players[$identifier] = $player; } private function checkTickUpdates($currentTick){ //Do level ticks foreach($this->getLevels() as $level){ try{ $level->doTick($currentTick); }catch(\Exception $e){ $this->logger->critical("Could not tick level " . $level->getName() . ": " . $e->getMessage()); if(\pocketmine\DEBUG > 1 and $this->logger instanceof MainLogger){ $this->logger->logException($e); } } } } public function doAutoSave(){ if($this->getAutoSave()){ Timings::$worldSaveTimer->startTiming(); foreach($this->getOnlinePlayers() as $index => $player){ if($player->isOnline()){ $player->save(); }elseif(!$player->isConnected()){ unset($this->players[$index]); } } foreach($this->getLevels() as $level){ $level->save(false); } Timings::$worldSaveTimer->stopTiming(); } } public function doLevelGC(){ foreach($this->getLevels() as $level){ $level->doChunkGarbageCollection(); } } public function sendUsage(){ if($this->lastSendUsage instanceof SendUsageTask){ if(!$this->lastSendUsage->isGarbage()){ //do not call multiple times return; } } $plist = ""; foreach($this->getPluginManager()->getPlugins() as $p){ $d = $p->getDescription(); $plist .= str_replace([";", ":"], "", $d->getName()) . ":" . str_replace([";", ":"], "", $d->getVersion()) . ";"; } $version = new VersionString(); $this->lastSendUsage = new SendUsageTask("https://stats.pocketmine.net/usage.php", [ "serverid" => $this->serverID, "port" => $this->getPort(), "os" => Utils::getOS(), "name" => $this->getName(), "memory_total" => $this->getConfigString("memory-limit"), "memory_usage" => $this->getMemoryUsage(), "php_version" => PHP_VERSION, "version" => $version->get(true), "build" => $version->getBuild(), "mc_version" => \pocketmine\MINECRAFT_VERSION, "protocol" => network\protocol\Info::CURRENT_PROTOCOL, "online" => count($this->players), "max" => $this->getMaxPlayers(), "plugins" => $plist, ]); $this->scheduler->scheduleAsyncTask($this->lastSendUsage); } public function getNetwork(){ return $this->mainInterface; } private function titleTick(){ if(!Terminal::hasFormattingCodes()){ return; } $usage = $this->getMemoryUsage(); if($usage === null){ $usage = round((memory_get_usage() / 1024) / 1024, 2) . "/" . round((memory_get_usage(true) / 1024) / 1024, 2) . " MB @ " . $this->getThreadCount() . " threads"; }else{ $usage = round(($usage / 1024) / 1024, 2) . " MB @ " . $this->getThreadCount() . " threads"; } echo "\x1b]0;" . $this->getName() . " " . $this->getPocketMineVersion() . " | Online " . count($this->players) . "/" . $this->getMaxPlayers() . " | Memory " . $usage . " | U " . round($this->mainInterface->getUploadUsage() / 1024, 2) . " D " . round($this->mainInterface->getDownloadUsage() / 1024, 2) . " kB/s | TPS " . $this->getTicksPerSecond() . " | Load " . $this->getTickUsage() . "%\x07"; } public function getMemoryUsage(){ if(Utils::getOS() === "linux" or Utils::getOS() === "bsd"){ if(preg_match("/VmSize:[ \t]+([0-9]+) kB/", file_get_contents("/proc/self/status"), $matches) > 0){ return $matches[1] * 1024; } } return memory_get_usage(true); } public function getThreadCount(){ if(Utils::getOS() === "linux" or Utils::getOS() === "bsd"){ if(preg_match("/Threads:[ \t]+([0-9]+)/", file_get_contents("/proc/self/status"), $matches) > 0){ return (int) $matches[1]; } } return count(ThreadManager::getInstance()->getAll()) + 3; //RakLib + MainLogger + Main Thread } /** * Tries to execute a server tick */ private function tick(){ $tickTime = microtime(true); if($tickTime < $this->nextTick){ return false; } Timings::$serverTickTimer->startTiming(); ++$this->tickCounter; $this->checkConsole(); Timings::$connectionTimer->startTiming(); foreach($this->interfaces as $interface){ $interface->process(); } Timings::$connectionTimer->stopTiming(); Timings::$schedulerTimer->startTiming(); $this->scheduler->mainThreadHeartbeat($this->tickCounter); Timings::$schedulerTimer->stopTiming(); $this->checkTickUpdates($this->tickCounter); if(($this->tickCounter & 0b1111) === 0){ $this->titleTick(); if(isset($this->queryHandler) and ($this->tickCounter & 0b111111111) === 0){ try{ $this->queryHandler->regenerateInfo(); }catch(\Exception $e){ if($this->logger instanceof MainLogger){ $this->logger->logException($e); } } } } Timings::$generationTimer->startTiming(); try{ $this->generationManager->process(); }catch(\Exception $e){ if($this->logger instanceof MainLogger){ $this->logger->logException($e); } } Timings::$generationTimer->stopTiming(); if(($this->tickCounter % 100) === 0){ foreach($this->levels as $level){ $level->clearCache(); } } Timings::$serverTickTimer->stopTiming(); TimingsHandler::tick(); $now = microtime(true); array_shift($this->tickAverage); $this->tickAverage[] = min(20, 1 / max(0.001, $now - $tickTime)); array_shift($this->useAverage); $this->useAverage[] = min(1, ($now - $tickTime) / 0.05); if(($this->nextTick - $tickTime) < -1){ $this->nextTick = $tickTime; } $this->nextTick += 0.05; return true; } private function registerEntities(){ Entity::registerEntity(Arrow::class); Entity::registerEntity(DroppedItem::class); Entity::registerEntity(FallingSand::class); Entity::registerEntity(PrimedTNT::class); Entity::registerEntity(Snowball::class); Entity::registerEntity(Villager::class); Entity::registerEntity(Zombie::class); Entity::registerEntity(Human::class, true); } private function registerTiles(){ Tile::registerTile(Chest::class); Tile::registerTile(Furnace::class); Tile::registerTile(Sign::class); } }