Separate Level management functionality from Server, clean up a bunch of mess

This commit is contained in:
Dylan K. Taylor 2019-01-12 19:11:05 +00:00
parent 27761ac26e
commit 5052b75565
22 changed files with 530 additions and 509 deletions

View File

@ -206,13 +206,13 @@ class MemoryManager{
$this->server->getLogger()->debug(sprintf("[Memory Manager] %sLow memory triggered, limit %gMB, using %gMB",
$global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2)));
if($this->lowMemClearWorldCache){
foreach($this->server->getLevels() as $level){
foreach($this->server->getLevelManager()->getLevels() as $level){
$level->clearCache(true);
}
}
if($this->lowMemChunkGC){
foreach($this->server->getLevels() as $level){
foreach($this->server->getLevelManager()->getLevels() as $level){
$level->doChunkGarbageCollection();
}
}

View File

@ -1137,7 +1137,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
if($this->hasValidSpawnPosition()){
return $this->spawnPosition;
}else{
$level = $this->server->getDefaultLevel();
$level = $this->server->getLevelManager()->getDefaultLevel();
return $level->getSafeSpawn();
}
@ -1837,9 +1837,9 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
public function _actuallyConstruct(){
$namedtag = $this->server->getOfflinePlayerData($this->username); //TODO: make this async
if(($level = $this->server->getLevelByName($namedtag->getString("Level", "", true))) === null){
if(($level = $this->server->getLevelManager()->getLevelByName($namedtag->getString("Level", "", true))) === null){
/** @var Level $level */
$level = $this->server->getDefaultLevel(); //TODO: default level may be null
$level = $this->server->getLevelManager()->getDefaultLevel(); //TODO: default level may be null
$spawnLocation = $level->getSafeSpawn();
$namedtag->setTag(new ListTag("Pos", [
@ -1910,7 +1910,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
}
if(!$this->hasValidSpawnPosition()){
if(($level = $this->server->getLevelByName($nbt->getString("SpawnLevel", ""))) instanceof Level){
if(($level = $this->server->getLevelManager()->getLevelByName($nbt->getString("SpawnLevel", ""))) instanceof Level){
$this->spawnPosition = new Position($nbt->getInt("SpawnX"), $nbt->getInt("SpawnY"), $nbt->getInt("SpawnZ"), $level);
}else{
$this->spawnPosition = $this->level->getSafeSpawn();

View File

@ -33,12 +33,9 @@ use pocketmine\command\CommandSender;
use pocketmine\command\ConsoleCommandSender;
use pocketmine\command\PluginIdentifiableCommand;
use pocketmine\command\SimpleCommandMap;
use pocketmine\entity\Entity;
use pocketmine\entity\EntityFactory;
use pocketmine\entity\Skin;
use pocketmine\event\HandlerList;
use pocketmine\event\level\LevelInitEvent;
use pocketmine\event\level\LevelLoadEvent;
use pocketmine\event\player\PlayerDataSaveEvent;
use pocketmine\event\server\CommandEvent;
use pocketmine\event\server\DataPacketBroadcastEvent;
@ -51,13 +48,12 @@ use pocketmine\lang\Language;
use pocketmine\lang\LanguageNotFoundException;
use pocketmine\lang\TextContainer;
use pocketmine\level\biome\Biome;
use pocketmine\level\format\io\exception\UnsupportedLevelFormatException;
use pocketmine\level\format\io\LevelProvider;
use pocketmine\level\format\io\LevelProviderManager;
use pocketmine\level\generator\Generator;
use pocketmine\level\generator\GeneratorManager;
use pocketmine\level\generator\normal\Normal;
use pocketmine\level\Level;
use pocketmine\level\LevelException;
use pocketmine\level\LevelManager;
use pocketmine\metadata\EntityMetadataStore;
use pocketmine\metadata\LevelMetadataStore;
use pocketmine\metadata\PlayerMetadataStore;
@ -113,14 +109,10 @@ use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use pocketmine\utils\UUID;
use function array_key_exists;
use function array_keys;
use function array_shift;
use function array_sum;
use function asort;
use function assert;
use function base64_encode;
use function bin2hex;
use function class_exists;
use function count;
use function define;
use function explode;
@ -129,7 +121,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 gc_collect_cycles;
use function get_class;
@ -141,7 +132,6 @@ use function ini_set;
use function is_array;
use function is_bool;
use function is_string;
use function is_subclass_of;
use function json_decode;
use function max;
use function microtime;
@ -151,7 +141,6 @@ use function pcntl_signal;
use function pcntl_signal_dispatch;
use function preg_replace;
use function random_bytes;
use function random_int;
use function realpath;
use function register_shutdown_function;
use function rename;
@ -170,8 +159,6 @@ use function time;
use function touch;
use function trim;
use const DIRECTORY_SEPARATOR;
use const INT32_MAX;
use const INT32_MIN;
use const PHP_EOL;
use const PHP_INT_MAX;
use const PTHREADS_INHERIT_NONE;
@ -266,15 +253,15 @@ class Server{
/** @var ResourcePackManager */
private $resourceManager;
/** @var LevelManager */
private $levelManager;
/** @var int */
private $maxPlayers;
/** @var bool */
private $onlineMode = true;
/** @var bool */
private $autoSave;
/** @var RCON */
private $rcon;
@ -292,20 +279,6 @@ class Server{
/** @var bool */
private $networkCompressionAsync = true;
/** @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 */
private $autoSaveTicks = 6000;
/** @var Language */
private $language;
/** @var bool */
@ -347,12 +320,6 @@ class Server{
/** @var Player[] */
private $playerList = [];
/** @var Level[] */
private $levels = [];
/** @var Level */
private $levelDefault = null;
/**
* @return string
*/
@ -481,37 +448,6 @@ class Server{
return $this->serverID;
}
/**
* @return bool
*/
public function getAutoSave() : bool{
return $this->autoSave;
}
/**
* @param bool $value
*/
public function setAutoSave(bool $value){
$this->autoSave = $value;
foreach($this->getLevels() as $level){
$level->setAutoSave($this->autoSave);
}
}
/**
* @return string
*/
public function getLevelType() : string{
return $this->getConfigString("level-type", "DEFAULT");
}
/**
* @return bool
*/
public function getGenerateStructures() : bool{
return $this->getConfigBool("generate-structures", true);
}
/**
* @return int
*/
@ -632,6 +568,13 @@ class Server{
return $this->resourceManager;
}
/**
* @return LevelManager
*/
public function getLevelManager() : LevelManager{
return $this->levelManager;
}
public function getAsyncPool() : AsyncPool{
return $this->asyncPool;
}
@ -752,7 +695,7 @@ class Server{
$this->logger->notice($this->getLanguage()->translateString("pocketmine.data.playerNotFound", [$name]));
}
}
$spawn = $this->getDefaultLevel()->getSafeSpawn();
$spawn = $this->levelManager->getDefaultLevel()->getSafeSpawn();
$currentTimeMillis = (int) (microtime(true) * 1000);
$nbt = new CompoundTag("", [
@ -763,7 +706,7 @@ class Server{
new DoubleTag("", $spawn->y),
new DoubleTag("", $spawn->z)
], NBT::TAG_Double),
new StringTag("Level", $this->getDefaultLevel()->getFolderName()),
new StringTag("Level", $this->levelManager->getDefaultLevel()->getFolderName()),
//new StringTag("SpawnLevel", $this->getDefaultLevel()->getFolderName()),
//new IntTag("SpawnX", $spawn->getFloorX()),
//new IntTag("SpawnY", $spawn->getFloorY()),
@ -898,255 +841,6 @@ class Server{
return $this->getPlayerByRawUUID($uuid->toBinary());
}
/**
* @return Level[]
*/
public function getLevels() : array{
return $this->levels;
}
/**
* @return Level|null
*/
public function getDefaultLevel() : ?Level{
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|null $level
*/
public function setDefaultLevel(?Level $level) : void{
if($level === null or ($this->isLevelLoaded($level->getFolderName()) and $level !== $this->levelDefault)){
$this->levelDefault = $level;
}
}
/**
* @param string $name
*
* @return bool
*/
public function isLevelLoaded(string $name) : bool{
return $this->getLevelByName($name) instanceof Level;
}
/**
* @param int $levelId
*
* @return Level|null
*/
public function getLevel(int $levelId) : ?Level{
return $this->levels[$levelId] ?? null;
}
/**
* NOTE: This matches levels based on the FOLDER name, NOT the display name.
*
* @param string $name
*
* @return Level|null
*/
public function getLevelByName(string $name) : ?Level{
foreach($this->getLevels() as $level){
if($level->getFolderName() === $name){
return $level;
}
}
return null;
}
/**
* @param Level $level
* @param bool $forceUnload
*
* @return bool
*
* @throws \InvalidStateException
*/
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.");
}
return $level->onUnload($forceUnload);
}
/**
* @internal
*
* @param Level $level
*/
public function removeLevel(Level $level) : void{
unset($this->levels[$level->getId()]);
}
/**
* Loads a level from the data directory
*
* @param string $name
*
* @return bool
*
* @throws LevelException
*/
public function loadLevel(string $name) : bool{
if(trim($name) === ""){
throw new LevelException("Invalid empty level name");
}
if($this->isLevelLoaded($name)){
return true;
}elseif(!$this->isLevelGenerated($name)){
$this->logger->notice($this->getLanguage()->translateString("pocketmine.level.notFound", [$name]));
return false;
}
$path = $this->getDataPath() . "worlds/" . $name . "/";
$providers = LevelProviderManager::getMatchingProviders($path);
if(count($providers) !== 1){
$this->logger->error($this->language->translateString("pocketmine.level.loadError", [
$name,
empty($providers) ?
$this->language->translateString("pocketmine.level.unknownFormat") :
$this->language->translateString("pocketmine.level.ambiguousFormat", [implode(", ", array_keys($providers))])
]));
return false;
}
$providerClass = array_shift($providers);
try{
/** @see LevelProvider::__construct() */
$level = new Level($this, $name, new $providerClass($path));
}catch(UnsupportedLevelFormatException $e){
$this->logger->error($this->language->translateString("pocketmine.level.loadError", [$name, $e->getMessage()]));
return false;
}
$this->levels[$level->getId()] = $level;
(new LevelLoadEvent($level))->call();
$level->setTickRate($this->baseTickRate);
return true;
}
/**
* Generates a new level if it does not exist
*
* @param string $name
* @param int|null $seed
* @param string|null $generator Class name that extends pocketmine\level\generator\Generator
* @param array $options
* @param bool $backgroundGeneration
*
* @return bool
*/
public function generateLevel(string $name, int $seed = null, $generator = null, array $options = [], bool $backgroundGeneration = true) : bool{
if(trim($name) === "" or $this->isLevelGenerated($name)){
return false;
}
$seed = $seed ?? random_int(INT32_MIN, INT32_MAX);
if(!isset($options["preset"])){
$options["preset"] = $this->getConfigString("generator-settings", "");
}
if(!($generator !== null and class_exists($generator, true) and is_subclass_of($generator, Generator::class))){
$generator = GeneratorManager::getGenerator($this->getLevelType());
}
$providerClass = LevelProviderManager::getDefault();
$path = $this->getDataPath() . "worlds/" . $name . "/";
/** @var LevelProvider $providerClass */
$providerClass::generate($path, $name, $seed, $generator, $options);
/** @see LevelProvider::__construct() */
$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();
if(!$backgroundGeneration){
return true;
}
$this->getLogger()->notice($this->getLanguage()->translateString("pocketmine.level.backgroundGeneration", [$name]));
$spawnLocation = $level->getSpawnLocation();
$centerX = $spawnLocation->getFloorX() >> 4;
$centerZ = $spawnLocation->getFloorZ() >> 4;
$order = [];
for($X = -3; $X <= 3; ++$X){
for($Z = -3; $Z <= 3; ++$Z){
$distance = $X ** 2 + $Z ** 2;
$chunkX = $X + $centerX;
$chunkZ = $Z + $centerZ;
$index = Level::chunkHash($chunkX, $chunkZ);
$order[$index] = $distance;
}
}
asort($order);
foreach($order as $index => $distance){
Level::getXZ($index, $chunkX, $chunkZ);
$level->populateChunk($chunkX, $chunkZ, true);
}
return true;
}
/**
* @param string $name
*
* @return bool
*/
public function isLevelGenerated(string $name) : bool{
if(trim($name) === ""){
return false;
}
$path = $this->getDataPath() . "worlds/" . $name . "/";
if(!($this->getLevelByName($name) instanceof Level)){
return !empty(LevelProviderManager::getMatchingProviders($path));
}
return true;
}
/**
* Searches all levels for the entity with the specified ID.
* Useful for tracking entities across multiple worlds without needing strong references.
*
* @param int $entityId
*
* @return Entity|null
*/
public function findEntity(int $entityId){
foreach($this->levels as $level){
assert(!$level->isClosed());
if(($entity = $level->getEntity($entityId)) instanceof Entity){
return $entity;
}
}
return null;
}
/**
* @param string $variable
* @param mixed $defaultValue
@ -1530,11 +1224,6 @@ class Server{
NetworkCipher::$ENABLED = (bool) $this->getProperty("network.enable-encryption", 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();
@ -1583,7 +1272,6 @@ class Server{
$this->banByIP->load();
$this->maxPlayers = $this->getConfigInt("max-players", 20);
$this->setAutoSave($this->getConfigBool("auto-save", true));
$this->onlineMode = $this->getConfigBool("xbox-auth", true);
if($this->onlineMode){
@ -1644,6 +1332,20 @@ class Server{
$this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader));
$this->pluginManager->registerInterface(new ScriptPluginLoader());
LevelProviderManager::init();
if(($format = LevelProviderManager::getProviderByName($formatName = (string) $this->getProperty("level-settings.default-format"))) !== null){
LevelProviderManager::setDefault($format);
}elseif($formatName !== ""){
$this->logger->warning($this->language->translateString("pocketmine.level.badDefaultFormat", [$formatName]));
}
if(extension_loaded("leveldb")){
$this->logger->debug($this->getLanguage()->translateString("pocketmine.debug.enable"));
}
$this->levelManager = new LevelManager($this);
GeneratorManager::registerDefaultGenerators();
register_shutdown_function([$this, "crashDump"]);
$this->queryRegenerateTask = new QueryRegenerateEvent($this, 5);
@ -1656,26 +1358,13 @@ class Server{
$this->network->registerInterface(new RakLibInterface($this));
LevelProviderManager::init();
if(($format = LevelProviderManager::getProviderByName($formatName = (string) $this->getProperty("level-settings.default-format"))) !== null){
LevelProviderManager::setDefault($format);
}elseif($formatName !== ""){
$this->logger->warning($this->language->translateString("pocketmine.level.badDefaultFormat", [$formatName]));
}
if(extension_loaded("leveldb")){
$this->logger->debug($this->getLanguage()->translateString("pocketmine.debug.enable"));
}
GeneratorManager::registerDefaultGenerators();
foreach((array) $this->getProperty("worlds", []) as $name => $options){
if($options === null){
$options = [];
}elseif(!is_array($options)){
continue;
}
if(!$this->loadLevel($name)){
if(!$this->levelManager->loadLevel($name)){
if(isset($options["generator"])){
$generatorOptions = explode(":", $options["generator"]);
$generator = GeneratorManager::getGenerator(array_shift($generatorOptions));
@ -1683,42 +1372,43 @@ class Server{
$options["preset"] = implode(":", $generatorOptions);
}
}else{
$generator = GeneratorManager::getGenerator("default");
$generator = Normal::class;
}
$this->generateLevel($name, Generator::convertSeed((string) ($options["seed"] ?? "")), $generator, $options);
$this->levelManager->generateLevel($name, Generator::convertSeed((string) ($options["seed"] ?? "")), $generator, $options);
}
}
if($this->getDefaultLevel() === null){
if($this->levelManager->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)){
$this->generateLevel($default, Generator::convertSeed($this->getConfigString("level-seed")));
if(!$this->levelManager->loadLevel($default)){
$this->levelManager->generateLevel(
$default,
Generator::convertSeed($this->getConfigString("level-seed")),
GeneratorManager::getGenerator($this->getConfigString("level-type")),
["preset" => $this->getConfigString("generator-settings")]
);
}
$this->setDefaultLevel($this->getLevelByName($default));
$level = $this->levelManager->getLevelByName($default);
if($level === null){
$this->getLogger()->emergency($this->getLanguage()->translateString("pocketmine.level.defaultError"));
$this->forceShutdown();
return;
}
$this->levelManager->setDefaultLevel($level);
}
if($this->properties->hasChanged()){
$this->properties->save();
}
if(!($this->getDefaultLevel() instanceof Level)){
$this->getLogger()->emergency($this->getLanguage()->translateString("pocketmine.level.defaultError"));
$this->forceShutdown();
return;
}
if($this->getProperty("ticks-per.autosave", 6000) > 0){
$this->autoSaveTicks = (int) $this->getProperty("ticks-per.autosave", 6000);
}
$this->enablePlugins(PluginLoadOrder::POSTWORLD);
$this->start();
@ -2005,7 +1695,7 @@ class Server{
public function reload(){
$this->logger->info("Saving levels...");
foreach($this->levels as $level){
foreach($this->levelManager->getLevels() as $level){
$level->save();
}
@ -2082,8 +1772,8 @@ class Server{
}
$this->getLogger()->debug("Unloading all levels");
foreach($this->getLevels() as $level){
$this->unloadLevel($level, true);
foreach($this->levelManager->getLevels() as $level){
$this->levelManager->unloadLevel($level, true);
}
$this->getLogger()->debug("Removing event handlers");
@ -2393,69 +2083,6 @@ class Server{
$p->sendDataPacket($pk);
}
private function checkTickUpdates(int $currentTick) : void{
if($this->alwaysTickPlayers){
foreach($this->players as $p){
if($p->spawned){
$p->onUpdate($currentTick);
}
}
}
//Do level ticks
foreach($this->levels as $k => $level){
if(!isset($this->levels[$k])){
// 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();
}
}
}
}
public function doAutoSave(){
if($this->getAutoSave()){
Timings::$worldSaveTimer->startTiming();
foreach($this->players as $index => $player){
if($player->spawned){
$player->save();
}elseif(!$player->isConnected()){
$this->removePlayer($player);
}
}
foreach($this->getLevels() as $level){
$level->save(false);
}
Timings::$worldSaveTimer->stopTiming();
}
}
public function sendUsage($type = SendUsageTask::TYPE_STATUS){
if((bool) $this->getProperty("anonymous-statistics.enabled", true)){
$this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers));
@ -2556,7 +2183,7 @@ class Server{
$this->asyncPool->collectTasks();
Timings::$schedulerAsyncTimer->stopTiming();
$this->checkTickUpdates($this->tickCounter);
$this->levelManager->tick($this->tickCounter);
Timings::$connectionTimer->startTiming();
$this->network->tick();
@ -2580,18 +2207,13 @@ class Server{
}
}
if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){
$this->autoSaveTicker = 0;
$this->doAutoSave();
}
if($this->sendUsageTicker > 0 and --$this->sendUsageTicker === 0){
$this->sendUsageTicker = 6000;
$this->sendUsage(SendUsageTask::TYPE_STATUS);
}
if(($this->tickCounter % 100) === 0){
foreach($this->levels as $level){
foreach($this->levelManager->getLevels() as $level){
$level->clearCache();
}

View File

@ -60,7 +60,7 @@ class DifficultyCommand extends VanillaCommand{
$sender->getServer()->setConfigInt("difficulty", $difficulty);
//TODO: add per-world support
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$level->setDifficulty($difficulty);
}

View File

@ -52,7 +52,7 @@ class GarbageCollectorCommand extends VanillaCommand{
$memory = memory_get_usage();
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$diff = [count($level->getChunks()), count($level->getEntities()), count($level->getTiles())];
$level->doChunkGarbageCollection();
$level->unloadChunks(true);

View File

@ -98,7 +98,7 @@ class ParticleCommand extends VanillaCommand{
$this->getRelativeDouble($sender->getZ(), $sender, $args[3])
);
}else{
$level = $sender->getServer()->getDefaultLevel();
$level = $sender->getServer()->getLevelManager()->getDefaultLevel();
$pos = new Vector3((float) $args[1], (float) $args[2], (float) $args[3]);
}

View File

@ -49,7 +49,7 @@ class SaveCommand extends VanillaCommand{
$player->save();
}
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$level->save(true);
}

View File

@ -43,7 +43,7 @@ class SaveOffCommand extends VanillaCommand{
return true;
}
$sender->getServer()->setAutoSave(false);
$sender->getServer()->getLevelManager()->setAutoSave(false);
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.disabled"));

View File

@ -43,7 +43,7 @@ class SaveOnCommand extends VanillaCommand{
return true;
}
$sender->getServer()->setAutoSave(true);
$sender->getServer()->getLevelManager()->setAutoSave(true);
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.enabled"));

View File

@ -46,7 +46,7 @@ class SeedCommand extends VanillaCommand{
if($sender instanceof Player){
$seed = $sender->getLevel()->getSeed();
}else{
$seed = $sender->getServer()->getDefaultLevel()->getSeed();
$seed = $sender->getServer()->getLevelManager()->getDefaultLevel()->getSeed();
}
$sender->sendMessage(new TranslationContainer("commands.seed.success", [$seed]));

View File

@ -59,7 +59,7 @@ class SetWorldSpawnCommand extends VanillaCommand{
return true;
}
}elseif(count($args) === 3){
$level = $sender->getServer()->getDefaultLevel();
$level = $sender->getServer()->getLevelManager()->getDefaultLevel();
$pos = new Vector3($this->getInteger($sender, $args[0]), $this->getInteger($sender, $args[1]), $this->getInteger($sender, $args[2]));
}else{
throw new InvalidCommandSyntaxException();

View File

@ -106,7 +106,7 @@ class StatusCommand extends VanillaCommand{
$sender->sendMessage(TextFormat::GOLD . "Maximum memory (manager): " . TextFormat::RED . number_format(round($server->getProperty("memory.global-limit"), 2), 2) . " MB.");
}
foreach($server->getLevels() as $level){
foreach($server->getLevelManager()->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() . ")" : "";

View File

@ -54,7 +54,7 @@ class TimeCommand extends VanillaCommand{
return true;
}
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$level->startTime();
}
Command::broadcastCommandMessage($sender, "Restarted the time");
@ -65,7 +65,7 @@ class TimeCommand extends VanillaCommand{
return true;
}
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$level->stopTime();
}
Command::broadcastCommandMessage($sender, "Stopped the time");
@ -79,7 +79,7 @@ class TimeCommand extends VanillaCommand{
if($sender instanceof Player){
$level = $sender->getLevel();
}else{
$level = $sender->getServer()->getDefaultLevel();
$level = $sender->getServer()->getLevelManager()->getDefaultLevel();
}
$sender->sendMessage(new TranslationContainer("commands.time.query", [$level->getTime()]));
return true;
@ -105,7 +105,7 @@ class TimeCommand extends VanillaCommand{
$value = $this->getInteger($sender, $args[1], 0);
}
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$level->setTime($value);
}
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.time.set", [$value]));
@ -117,7 +117,7 @@ class TimeCommand extends VanillaCommand{
}
$value = $this->getInteger($sender, $args[1], 0);
foreach($sender->getServer()->getLevels() as $level){
foreach($sender->getServer()->getLevelManager()->getLevels() as $level){
$level->setTime($level->getTime() + $value);
}
Command::broadcastCommandMessage($sender, new TranslationContainer("commands.time.added", [$value]));

View File

@ -625,7 +625,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{
public function getOwningEntity() : ?Entity{
$eid = $this->getOwningEntityId();
if($eid !== null){
return $this->server->findEntity($eid);
return $this->server->getLevelManager()->findEntity($eid);
}
return null;
@ -665,7 +665,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{
public function getTargetEntity() : ?Entity{
$eid = $this->getTargetEntityId();
if($eid !== null){
return $this->server->findEntity($eid);
return $this->server->getLevelManager()->findEntity($eid);
}
return null;

View File

@ -150,7 +150,7 @@ class ExperienceOrb extends Entity{
return null;
}
$entity = $this->server->findEntity($this->targetPlayerRuntimeId);
$entity = $this->server->getLevelManager()->findEntity($this->targetPlayerRuntimeId);
if($entity instanceof Human){
return $entity;
}

View File

@ -51,6 +51,6 @@ class EntityDamageByChildEntityEvent extends EntityDamageByEntityEvent{
* @return Entity|null
*/
public function getChild() : ?Entity{
return $this->getEntity()->getLevel()->getServer()->findEntity($this->childEntityEid);
return $this->getEntity()->getLevel()->getServer()->getLevelManager()->findEntity($this->childEntityEid);
}
}

View File

@ -69,7 +69,7 @@ class EntityDamageByEntityEvent extends EntityDamageEvent{
* @return Entity|null
*/
public function getDamager() : ?Entity{
return $this->getEntity()->getLevel()->getServer()->findEntity($this->damagerEntityId);
return $this->getEntity()->getLevel()->getServer()->getLevelManager()->findEntity($this->damagerEntityId);
}
/**

View File

@ -88,7 +88,8 @@ class QueryRegenerateEvent extends ServerEvent{
$this->gametype = ($server->getGamemode() & 0x01) === 0 ? "SMP" : "CMP";
$this->version = $server->getVersion();
$this->server_engine = $server->getName() . " " . $server->getPocketMineVersion();
$this->map = $server->getDefaultLevel() === null ? "unknown" : $server->getDefaultLevel()->getName();
$level = $server->getLevelManager()->getDefaultLevel();
$this->map = $level === null ? "unknown" : $level->getName();
$this->numPlayers = count($this->players);
$this->maxPlayers = $server->getMaxPlayers();
$this->whitelist = $server->hasWhitelist() ? "on" : "off";

View File

@ -40,7 +40,6 @@ use pocketmine\event\level\ChunkLoadEvent;
use pocketmine\event\level\ChunkPopulateEvent;
use pocketmine\event\level\ChunkUnloadEvent;
use pocketmine\event\level\LevelSaveEvent;
use pocketmine\event\level\LevelUnloadEvent;
use pocketmine\event\level\SpawnChangeEvent;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\item\Item;
@ -359,7 +358,6 @@ class Level implements ChunkManager, Metadatable{
$this->levelId = static::$levelIdCounter++;
$this->blockMetadata = new BlockMetadataStore($this);
$this->server = $server;
$this->autoSave = $server->getAutoSave();
$this->provider = $provider;
@ -455,6 +453,9 @@ class Level implements ChunkManager, Metadatable{
return $this->closed;
}
/**
* @internal
*/
public function close(){
if($this->closed){
throw new \InvalidStateException("Tried to close a level which is already closed");
@ -558,52 +559,6 @@ class Level implements ChunkManager, Metadatable{
$this->autoSave = $value;
}
/**
* @internal DO NOT use this from plugins, it's for internal use only. Use Server->unloadLevel() instead.
*
* @param bool $force default false, force unload of default level
*
* @return bool
* @throws \InvalidStateException if trying to unload a level during level tick
*/
public function onUnload(bool $force = false) : bool{
if($this->doingTick and !$force){
throw new \InvalidStateException("Cannot unload a level during level tick");
}
$ev = new LevelUnloadEvent($this);
if($this === $this->server->getDefaultLevel() and !$force){
$ev->setCancelled(true);
}
$ev->call();
if(!$force and $ev->isCancelled()){
return false;
}
$this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$this->getName()]));
$defaultLevel = $this->server->getDefaultLevel();
foreach($this->getPlayers() as $player){
if($this === $defaultLevel or $defaultLevel === null){
$player->close($player->getLeaveMessage(), "Forced default level unload");
}elseif($defaultLevel instanceof Level){
$player->teleport($this->server->getDefaultLevel()->getSafeSpawn());
}
}
if($this === $defaultLevel){
$this->server->setDefaultLevel(null);
}
$this->server->removeLevel($this);
$this->close();
return true;
}
/**
* Gets the players being used in a specific chunk
*
@ -738,6 +693,10 @@ class Level implements ChunkManager, Metadatable{
}
}
public function isDoingTick() : bool{
return $this->doingTick;
}
/**
* WARNING: Do not use this, it's only for internal use.
* Changes to this function won't be recorded on the version.

View File

@ -0,0 +1,439 @@
<?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\level;
use pocketmine\entity\Entity;
use pocketmine\event\level\LevelInitEvent;
use pocketmine\event\level\LevelLoadEvent;
use pocketmine\event\level\LevelUnloadEvent;
use pocketmine\level\format\io\exception\UnsupportedLevelFormatException;
use pocketmine\level\format\io\LevelProvider;
use pocketmine\level\format\io\LevelProviderManager;
use pocketmine\level\generator\Generator;
use pocketmine\level\generator\normal\Normal;
use pocketmine\Server;
use pocketmine\timings\Timings;
use pocketmine\utils\Utils;
use function array_keys;
use function array_shift;
use function asort;
use function assert;
use function count;
use function floor;
use function implode;
use function max;
use function microtime;
use function min;
use function random_int;
use function round;
use function sprintf;
use function trim;
use const INT32_MAX;
use const INT32_MIN;
class LevelManager{
/** @var Level[] */
private $levels = [];
/** @var Level|null */
private $levelDefault;
/** @var Server */
private $server;
/** @var bool */
private $autoTickRate = true;
/** @var int */
private $autoTickRateLimit = 20;
/** @var bool */
private $alwaysTickPlayers = false;
/** @var int */
private $baseTickRate = 1;
/** @var bool */
private $autoSave = true;
/** @var int */
private $autoSaveTicks = 6000;
/** @var int */
private $autoSaveTicker = 0;
public function __construct(Server $server){
$this->server = $server;
$this->autoTickRate = (bool) $this->server->getProperty("level-settings.auto-tick-rate", $this->autoTickRate);
$this->autoTickRateLimit = (int) $this->server->getProperty("level-settings.auto-tick-rate-limit", $this->autoTickRateLimit);
$this->alwaysTickPlayers = (bool) $this->server->getProperty("level-settings.always-tick-players", $this->alwaysTickPlayers);
$this->baseTickRate = (int) $this->server->getProperty("level-settings.base-tick-rate", $this->baseTickRate);
$this->autoSave = $this->server->getConfigBool("auto-save", $this->autoSave);
$this->autoSaveTicks = (int) $this->server->getProperty("ticks-per.autosave", 6000);
}
/**
* @return Level[]
*/
public function getLevels() : array{
return $this->levels;
}
/**
* @return Level|null
*/
public function getDefaultLevel() : ?Level{
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|null $level
*/
public function setDefaultLevel(?Level $level) : void{
if($level === null or ($this->isLevelLoaded($level->getFolderName()) and $level !== $this->levelDefault)){
$this->levelDefault = $level;
}
}
/**
* @param string $name
*
* @return bool
*/
public function isLevelLoaded(string $name) : bool{
return $this->getLevelByName($name) instanceof Level;
}
/**
* @param int $levelId
*
* @return Level|null
*/
public function getLevel(int $levelId) : ?Level{
return $this->levels[$levelId] ?? null;
}
/**
* NOTE: This matches levels based on the FOLDER name, NOT the display name.
*
* @param string $name
*
* @return Level|null
*/
public function getLevelByName(string $name) : ?Level{
foreach($this->levels as $level){
if($level->getFolderName() === $name){
return $level;
}
}
return null;
}
/**
* @param Level $level
* @param bool $forceUnload
*
* @return bool
*
* @throws \InvalidArgumentException
*/
public function unloadLevel(Level $level, bool $forceUnload = false) : bool{
if($level === $this->getDefaultLevel() and !$forceUnload){
throw new \InvalidArgumentException("The default level cannot be unloaded while running, please switch levels.");
}
if($level->isDoingTick()){
throw new \InvalidArgumentException("Cannot unload a level during level tick");
}
$ev = new LevelUnloadEvent($level);
if($level === $this->levelDefault and !$forceUnload){
$ev->setCancelled(true);
}
$ev->call();
if(!$forceUnload and $ev->isCancelled()){
return false;
}
$this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$level->getName()]));
foreach($level->getPlayers() as $player){
if($level === $this->levelDefault or $this->levelDefault === null){
$player->close($player->getLeaveMessage(), "Forced default level unload");
}elseif($this->levelDefault instanceof Level){
$player->teleport($this->levelDefault->getSafeSpawn());
}
}
if($level === $this->levelDefault){
$this->levelDefault = null;
}
unset($this->levels[$level->getId()]);
$level->close();
return true;
}
/**
* Loads a level from the data directory
*
* @param string $name
*
* @return bool
*
* @throws LevelException
*/
public function loadLevel(string $name) : bool{
if(trim($name) === ""){
throw new LevelException("Invalid empty level name");
}
if($this->isLevelLoaded($name)){
return true;
}elseif(!$this->isLevelGenerated($name)){
$this->server->getLogger()->notice($this->server->getLanguage()->translateString("pocketmine.level.notFound", [$name]));
return false;
}
$path = $this->server->getDataPath() . "worlds/" . $name . "/";
$providers = LevelProviderManager::getMatchingProviders($path);
if(count($providers) !== 1){
$this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [
$name,
empty($providers) ?
$this->server->getLanguage()->translateString("pocketmine.level.unknownFormat") :
$this->server->getLanguage()->translateString("pocketmine.level.ambiguousFormat", [implode(", ", array_keys($providers))])
]));
return false;
}
$providerClass = array_shift($providers);
try{
/** @see LevelProvider::__construct() */
$level = new Level($this->server, $name, new $providerClass($path));
}catch(UnsupportedLevelFormatException $e){
$this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [$name, $e->getMessage()]));
return false;
}
$this->levels[$level->getId()] = $level;
$level->setTickRate($this->baseTickRate);
$level->setAutoSave($this->autoSave);
(new LevelLoadEvent($level))->call();
return true;
}
/**
* Generates a new level if it does not exist
*
* @param string $name
* @param int|null $seed
* @param string $generator Class name that extends pocketmine\level\generator\Generator
* @param array $options
* @param bool $backgroundGeneration
*
* @return bool
* @throws \InvalidArgumentException
*/
public function generateLevel(string $name, int $seed = null, string $generator = Normal::class, array $options = [], bool $backgroundGeneration = true) : bool{
if(trim($name) === "" or $this->isLevelGenerated($name)){
return false;
}
$seed = $seed ?? random_int(INT32_MIN, INT32_MAX);
Utils::testValidInstance($generator, Generator::class);
$providerClass = LevelProviderManager::getDefault();
$path = $this->server->getDataPath() . "worlds/" . $name . "/";
/** @var LevelProvider $providerClass */
$providerClass::generate($path, $name, $seed, $generator, $options);
/** @see LevelProvider::__construct() */
$level = new Level($this->server, $name, new $providerClass($path));
$this->levels[$level->getId()] = $level;
$level->setTickRate($this->baseTickRate);
$level->setAutoSave($this->autoSave);
(new LevelInitEvent($level))->call();
(new LevelLoadEvent($level))->call();
if(!$backgroundGeneration){
return true;
}
$this->server->getLogger()->notice($this->server->getLanguage()->translateString("pocketmine.level.backgroundGeneration", [$name]));
$spawnLocation = $level->getSpawnLocation();
$centerX = $spawnLocation->getFloorX() >> 4;
$centerZ = $spawnLocation->getFloorZ() >> 4;
$order = [];
for($X = -3; $X <= 3; ++$X){
for($Z = -3; $Z <= 3; ++$Z){
$distance = $X ** 2 + $Z ** 2;
$chunkX = $X + $centerX;
$chunkZ = $Z + $centerZ;
$index = Level::chunkHash($chunkX, $chunkZ);
$order[$index] = $distance;
}
}
asort($order);
foreach($order as $index => $distance){
Level::getXZ($index, $chunkX, $chunkZ);
$level->populateChunk($chunkX, $chunkZ, true);
}
return true;
}
/**
* @param string $name
*
* @return bool
*/
public function isLevelGenerated(string $name) : bool{
if(trim($name) === ""){
return false;
}
$path = $this->server->getDataPath() . "worlds/" . $name . "/";
if(!($this->getLevelByName($name) instanceof Level)){
return !empty(LevelProviderManager::getMatchingProviders($path));
}
return true;
}
/**
* Searches all levels for the entity with the specified ID.
* Useful for tracking entities across multiple worlds without needing strong references.
*
* @param int $entityId
*
* @return Entity|null
*/
public function findEntity(int $entityId){
foreach($this->levels as $level){
assert(!$level->isClosed());
if(($entity = $level->getEntity($entityId)) instanceof Entity){
return $entity;
}
}
return null;
}
public function tick(int $currentTick) : void{
foreach($this->levels as $k => $level){
if(!isset($this->levels[$k])){
// 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){
if($this->alwaysTickPlayers){
foreach($level->getPlayers() as $p){
if($p->spawned){
$p->onUpdate($currentTick);
}
}
}
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->server->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->server->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->server->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($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){
$this->autoSaveTicker = 0;
$this->doAutoSave();
}
}
/**
* @return bool
*/
public function getAutoSave() : bool{
return $this->autoSave;
}
/**
* @param bool $value
*/
public function setAutoSave(bool $value){
$this->autoSave = $value;
foreach($this->levels as $level){
$level->setAutoSave($this->autoSave);
}
}
private function doAutoSave() : void{
Timings::$worldSaveTimer->startTiming();
foreach($this->levels as $level){
foreach($level->getPlayers() as $player){
if($player->spawned){
$player->save();
}elseif(!$player->isConnected()){ //TODO: check if this is ever possible
$this->server->removePlayer($player);
}
}
$level->save(false);
}
Timings::$worldSaveTimer->stopTiming();
}
}

View File

@ -25,8 +25,8 @@ namespace pocketmine\level\generator;
use pocketmine\level\generator\hell\Nether;
use pocketmine\level\generator\normal\Normal;
use pocketmine\utils\Utils;
use function array_keys;
use function is_subclass_of;
use function strtolower;
final class GeneratorManager{
@ -48,11 +48,11 @@ final class GeneratorManager{
* @param string $class Fully qualified name of class that extends \pocketmine\level\generator\Generator
* @param string $name Alias for this generator type that can be written in configs
* @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
*
* @throws \InvalidArgumentException
*/
public static function addGenerator(string $class, string $name, bool $overwrite = false) : void{
if(!is_subclass_of($class, Generator::class)){
throw new \InvalidArgumentException("Class $class does not extend " . Generator::class);
}
Utils::testValidInstance($class, Generator::class);
if(!$overwrite and isset(self::$list[$name = strtolower($name)])){
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");

View File

@ -64,7 +64,7 @@ class TimingsHandler{
$entities = 0;
$livingEntities = 0;
foreach(Server::getInstance()->getLevels() as $level){
foreach(Server::getInstance()->getLevelManager()->getLevels() as $level){
$entities += count($level->getEntities());
foreach($level->getEntities() as $e){
if($e instanceof Living){