mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-06-24 20:34:02 +00:00
Separate Level management functionality from Server, clean up a bunch of mess
This commit is contained in:
parent
27761ac26e
commit
5052b75565
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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"));
|
||||
|
||||
|
@ -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"));
|
||||
|
||||
|
@ -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]));
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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() . ")" : "";
|
||||
|
@ -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]));
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -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.
|
||||
|
439
src/pocketmine/level/LevelManager.php
Normal file
439
src/pocketmine/level/LevelManager.php
Normal 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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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){
|
||||
|
Loading…
x
Reference in New Issue
Block a user