From 581a6a8fb2e0421984da134d3fbf56fb3ca48526 Mon Sep 17 00:00:00 2001 From: GameHerobrine Date: Wed, 11 Mar 2026 01:49:41 +0300 Subject: [PATCH 1/6] changes to world format: added skylight into chunk data, replaced loctable in level.dat with a specific byte in every chunk, removed ncleveldata.pmf, bumped api version to 12.3 --- src/config.php | 1 - src/config_post.php | 4 +- src/pmf/PMF.php | 5 +- src/pmf/PMFLevel.php | 256 +++++++++++++----- src/world/Level.php | 4 +- src/world/generator/SuperflatGenerator.php | 2 +- .../generator/vanilla/VanillaGenerator.php | 17 +- 7 files changed, 203 insertions(+), 86 deletions(-) diff --git a/src/config.php b/src/config.php index d29cb7072..478442cc1 100644 --- a/src/config.php +++ b/src/config.php @@ -1,6 +1,5 @@ 0){ echo "[ERROR] Use PHP >= 8.0.0 to launch this server software.\n"; diff --git a/src/config_post.php b/src/config_post.php index 879a941a9..21282731d 100644 --- a/src/config_post.php +++ b/src/config_post.php @@ -103,10 +103,10 @@ function parseOffset(string $offset) : string|false{ ini_set("memory_limit", "256M"); //Default define("LOG", true); define("START_TIME", microtime(true)); -define("MAJOR_VERSION", "1.1.1dev"); +define("MAJOR_VERSION", "1.1.2dev"); define("CODENAME", "Nostalgia"); //i\'m not very creative - kotyaralih, yeah - tema1d. define("CURRENT_MINECRAFT_VERSION", "v0.8.1 alpha"); -define("CURRENT_API_VERSION", "12.2"); +define("CURRENT_API_VERSION", "12.3"); define("CURRENT_PHP_VERSION", "8.0"); $gitsha1 = false; if(file_exists(FILE_PATH . ".git/refs/heads/master")){ //Found Git information! diff --git a/src/pmf/PMF.php b/src/pmf/PMF.php index 8cf45ebec..b3f4b9c60 100644 --- a/src/pmf/PMF.php +++ b/src/pmf/PMF.php @@ -7,7 +7,7 @@ class PMF{ protected $fp; protected $file; private $version; - private $type; + protected $type; public function __construct($file, $new = false, $type = 0, $version = PMF_CURRENT_VERSION){ if($new === true){ @@ -74,7 +74,8 @@ public function close(){ public function parseInfo(){ $this->seek(0); - if(fread($this->fp, 3) !== "PMF"){ + $type = fread($this->fp, 3); + if($type !== "PMF"){ return false; } $this->version = ord($this->read(1)); diff --git a/src/pmf/PMFLevel.php b/src/pmf/PMFLevel.php index 0d03bd158..5a0e0bfb4 100644 --- a/src/pmf/PMFLevel.php +++ b/src/pmf/PMFLevel.php @@ -1,9 +1,54 @@ ..dat (chunk), gzipped + * Contains multiple 16x16x16 chunks(the amount of minichunks is determined by levelData["height"]) + * Each minichunk is 8192 bytes long: + * * minichunk is split into 32byte chunks for 16 blocks(y from 0 to 15) + * * ids(first 16 bytes), metas(8 bytes, each is 4bits), blocklight(8 bytes, each is 4 bits, wasnt used before nc 1.1.1) + * * every column goes from x=0 to x=15 and then from z=0 to z=15(x=0,z=0 is 0, x=1,z=0 is 32, x=15,z=0 is 480, x=0,z=1 is 512, x=15,z=15 is 8160) + * + * level.pmf + * first 5 bytes - pmf header(PMF) + * next byte - level version(0 - pmmp/nc, 1 - ncpmf(0.9/0.10 branch),pmmp(very early 1.4dev, also used by nc-worlddev), 2 - ncpmf(0.9/0.10 branch), pmmp(early 1.4dev), others are reserved for ncpmf + * next 2 bytes world name length () + * next bytes - world name + * seed(4 bytes), time(4 bytes), spawnX/Y/Z(each 4 bytes float) + * world width/height(1 byte each) + * extra data(gzipped, length is determined by 2 bytes at the beginning) + * loctable - location table - size is determined by ()*()*2 - 512 for 16x16 + * + * ncleveldata.pmf + * NC_DATA_VERSION(1 byte) + * hasLight (256 bytes) + * + * New: (WIP) + * ..dat (chunk), gzipped + * First byte is used for chunk version (0 for now) + * Next 2 bytes are used for location table value(what minichunks this chunk contains - msb is not used due to level beign 128 blocks tall => 128/16=8 => can fit into a single byte) + * Next byte is used to determine is this chunk populated by blocklight and skylight (moved from ncleveldata.pmf - ( & 1) => has blocklight, ( & 2) => has skylight) + * Next byte is used to determine should this chunk tick or not + * The rest of the file is minichunks(the amount of minichunks is determined by levelData["height"]) + * Each minichunk is 4096+2048*3 bytes long: + * * minichunk is split into 40byte chunks for 16 blocks(y from 0 to 15) + * * ids(first 16 bytes), metas(8 bytes, each is 4bits), blocklight(8 bytes, each is 4 bits), skylight(8 bytes, each is 4 bits) + * * every column goes from x=0 to x=15 and then from z=0 to z=15(x=0,z=0 is 0, x=1,z=0 is 40, x=15,z=0 is 600, x=0,z=1 is 640, x=15,z=15 is 10200) + * + * level.pmf + * same as old(for now), except there is no loctable + * TODO maybe remove worldname, seed and spawn location because of the future changes i want to implement? + */ + const TYPE_CURRENT = self::TYPE_NEW; + const TYPE_OLD = 0; //old(nc 1.1.1 and below) + const TYPE_NEW = 1; //new + + + public $isLoaded = true; private $levelData = []; public $hasLight = ""; @@ -18,7 +63,7 @@ class PMFLevel extends PMF{ public $level; public function __construct($file, $blank = false){ if(is_array($blank)){ - $this->create($file, 0); + $this->create($file, self::TYPE_CURRENT); $this->levelData = $blank; $this->createBlank(); $this->isLoaded = true; @@ -69,8 +114,9 @@ private function createBlank(){ public function saveData($locationTable = true){ $this->levelData["version"] = PMF_CURRENT_LEVEL_VERSION; - @ftruncate($this->fp, 5); - $this->seek(5); + @ftruncate($this->fp, 4); + $this->seek(4); + $this->write(chr(self::TYPE_CURRENT)); //force update type $this->write(chr($this->levelData["version"])); $this->write(Utils::writeShort(strlen($this->levelData["name"])) . $this->levelData["name"]); $this->write(Utils::writeInt($this->levelData["seed"])); @@ -83,23 +129,6 @@ public function saveData($locationTable = true){ $extra = gzdeflate($this->levelData["extra"], PMF_LEVEL_DEFLATE_LEVEL); $this->write(Utils::writeShort(strlen($extra)) . $extra); $this->payloadOffset = ftell($this->fp); - - if($locationTable !== false){ - $this->writeLocationTable(); - } - - $this->writeNCData(); - } - - public function writeNCData(){ - $dir = dirname($this->file); - try{ - $file = fopen("$dir/ncleveldata.pmf", "wb"); - fwrite($file, chr(self::NC_DATA_VERSION), 1); - fwrite($file, $this->hasLight, 16*16); - }finally{ - fclose($file); - } } public function readNCData(){ @@ -204,10 +233,15 @@ private function getChunkPath($X, $Z){ return dirname($this->file) . "/chunks/" . $Z . "." . $X . ".pmc"; } + protected function parseLevel(){ - if($this->getType() !== 0x00){ - return false; - } + + $type = $this->getType(); + $isNCLevel = false; + if($type == 0x00) $isNCLevel = false; + else if($type == 0x01) $isNCLevel = true; + else return false; + $this->seek(5); $this->levelData["version"] = ord($this->read(1)); if($this->levelData["version"] > PMF_CURRENT_LEVEL_VERSION){ @@ -224,24 +258,49 @@ protected function parseLevel(){ if(($this->levelData["width"] !== 16 and $this->levelData["width"] !== 32) or $this->levelData["height"] !== 8){ return false; } - $lastseek = ftell($this->fp); - if(($len = $this->read(2)) === false or ($this->levelData["extra"] = @gzinflate($this->read(Utils::readShort($len, false)))) === false){ //Corruption protection - console("[NOTICE] Empty/corrupt location table detected, forcing recovery"); - fseek($this->fp, $lastseek); - $c = gzdeflate(""); - $this->write(Utils::writeShort(strlen($c)) . $c); - $this->payloadOffset = ftell($this->fp); - $this->levelData["extra"] = ""; - $cnt = pow($this->levelData["width"], 2); - for($index = 0; $index < $cnt; ++$index){ - $this->write("\x00\xFF"); //Force index recreation + + if(!$isNCLevel){ + ConsoleAPI::notice("Old level found, starting conversion - reading old data"); + $lastseek = ftell($this->fp); + if(($len = $this->read(2)) === false or ($this->levelData["extra"] = @gzinflate($this->read(Utils::readShort($len, false)))) === false){ //Corruption protection + console("[NOTICE] Empty/corrupt location table detected, forcing recovery"); + fseek($this->fp, $lastseek); + $c = gzdeflate(""); + $this->write(Utils::writeShort(strlen($c)) . $c); + $this->payloadOffset = ftell($this->fp); + $this->levelData["extra"] = ""; + $cnt = pow($this->levelData["width"], 2); + for($index = 0; $index < $cnt; ++$index){ + $this->write("\x00\xFF"); //Force index recreation + } + fseek($this->fp, $this->payloadOffset); + }else{ + $this->payloadOffset = ftell($this->fp); } - fseek($this->fp, $this->payloadOffset); + $this->readNCData(); + $this->readLocationTable(); + + ConsoleAPI::notice("Converting chunks"); + $this->convertOldChunks(); }else{ - $this->payloadOffset = ftell($this->fp); + if(($len = $this->read(2)) === false or ($this->levelData["extra"] = @gzinflate($this->read(Utils::readShort($len, false)))) === false){ + + } + } + } + + public function convertOldChunks(){ + for($x = 0; $x < 16; ++$x){ + for($z = 0; $z < 16; ++$z){ + $this->loadChunk($x, $z, self::TYPE_OLD); + } + } + + for($x = 0; $x < 16; ++$x){ + for($z = 0; $z < 16; ++$z){ + $this->unloadChunk($x, $z); + } } - $this->readNCData(); - return $this->readLocationTable(); } private function readLocationTable(){ @@ -318,7 +377,22 @@ public function saveChunk($X, $Z){ } $chunk = @gzopen($this->getChunkPath($X, $Z), "wb" . PMF_LEVEL_DEFLATE_LEVEL); + + gzwrite($chunk, chr(self::CHUNK_VERSION)); + + //$info = $this->locationTable[$index] = [Utils::readShort(gzread($chunk, 2), false)]; + //$this->hasLight[$index] = ord(gzread($chunk, 1)); + $bitmap = 0; + for($Y = 0; $Y < $this->levelData["height"]; ++$Y){ + if($this->chunks[$index][$Y] !== false and ((isset($this->chunkChange[$index][$Y]) and $this->chunkChange[$index][$Y] === 0) or !$this->isMiniChunkEmpty($X, $Z, $Y))){ + $bitmap |= 1 << $Y; + } + } + gzwrite($chunk, Utils::writeShort($bitmap)); + gzwrite($chunk, $this->hasLight[$index]); + gzwrite($chunk, chr(0)); //TODO reserved for shouldTick + for($Y = 0; $Y < $this->levelData["height"]; ++$Y){ if($this->chunks[$index][$Y] !== false and ((isset($this->chunkChange[$index][$Y]) and $this->chunkChange[$index][$Y] === 0) or !$this->isMiniChunkEmpty($X, $Z, $Y))){ gzwrite($chunk, $this->chunks[$index][$Y]); @@ -330,15 +404,15 @@ public function saveChunk($X, $Z){ } $this->chunkChange[$index][-1] = false; $this->locationTable[$index][0] = $bitmap; - $this->seek($this->payloadOffset + ($index << 1)); - $this->write(Utils::writeShort($this->locationTable[$index][0])); + //$this->seek($this->payloadOffset + ($index << 1)); + //$this->write(Utils::writeShort($this->locationTable[$index][0])); return true; } protected function isMiniChunkEmpty($X, $Z, $Y){ $index = self::getIndex($X, $Z); if($this->chunks[$index][$Y] !== false){ - if(substr_count($this->chunks[$index][$Y], "\x00") < 8192){ + if(substr_count($this->chunks[$index][$Y], "\x00") < 10240){ return false; } } @@ -347,42 +421,79 @@ protected function isMiniChunkEmpty($X, $Z, $Y){ public function getMiniChunk($X, $Z, $Y){ if($this->loadChunk($X, $Z) === false){ - return str_repeat("\x00", 8192); + return str_repeat("\x00", 10240); } $index = self::getIndex($X, $Z); if(!isset($this->chunks[$index][$Y]) or $this->chunks[$index][$Y] === false){ - return str_repeat("\x00", 8192); + return str_repeat("\x00", 10240); } return $this->chunks[$index][$Y]; } public $checkLight = []; - public function loadChunk($X, $Z){ + public function loadChunk($X, $Z, $compatver=self::TYPE_CURRENT){ $index = self::getIndex($X, $Z); if($this->isChunkLoaded($X, $Z)){ return true; - }elseif(!isset($this->locationTable[$index])){ - return false; } - $info = $this->locationTable[$index]; - $this->seek($info[0]); - + if($compatver == self::TYPE_OLD){ + if(!isset($this->locationTable[$index])){ + return false; + } + + + $info = $this->locationTable[$index]; + $this->seek($info[0]); + }else{ + $this->locationTable[$index] = 0; + } + + $chunk = @gzopen($this->getChunkPath($X, $Z), "rb"); if($chunk === false){ return false; } + + if($compatver != self::TYPE_OLD){ + $ver = ord(gzread($chunk, 1)); + if($ver != self::CHUNK_VERSION){ + ConsoleAPI::error("Failed to load chunk $X $Z: chunk version ($ver) doesnt match current (".self::CHUNK_VERSION.")"); + @gzclose($chunk); + return false; + } + + $info = $this->locationTable[$index] = [Utils::readShort(gzread($chunk, 2), false)]; + $this->hasLight[$index] = gzread($chunk, 1); + gzread($chunk, 1); //TODO reserved for shouldTick + } + $this->chunks[$index] = []; $this->chunkChange[$index] = [-1 => false]; + if($compatver != self::TYPE_CURRENT){ + $this->chunkChange[$index][-1] = true; //force save when unloading if loaded with old compat + } for($Y = 0; $Y < $this->levelData["height"]; ++$Y){ $t = 1 << $Y; if(($info[0] & $t) === $t){ - //TODO do something with skylight - // 4096 + 2048 + 2048, Block Data, Meta, Light - if(strlen($this->chunks[$index][$Y] = gzread($chunk, 8192)) < 8192){ - console("[NOTICE] Empty corrupt chunk detected [$X,$Z,:$Y], recovering contents", true, true, 2); - $this->fillMiniChunk($X, $Z, $Y); + if($compatver == self::TYPE_OLD){ + // 4096 + 2048 + 2048, Block Data, Meta, Light + $cdata = gzread($chunk, 8192); + if(strlen($cdata) < 8192){ + console("[NOTICE] Empty corrupt chunk detected [$X,$Z,:$Y], recovering contents", true, true, 2); + $this->fillMiniChunk($X, $Z, $Y); + } + $this->chunks[$index][$Y] = ""; + for($i = 0; $i < 8192; $i += 32){ + $this->chunks[$index][$Y] .= substr($cdata, $i, 32) . "\x00\x00\x00\x00\x00\x00\x00\x00"; + } + }else{ + // 4096+2048+2048+2048, Block Data, Meta, blocklight, skylight + if(strlen($this->chunks[$index][$Y] = gzread($chunk, 4096+2048+2048+2048)) < 4096+2048+2048+2048){ + console("[NOTICE] Empty corrupt chunk detected [$X,$Z,:$Y], recovering contents(".strlen($this->chunks[$index][$Y]).")", true, true, 2); + $this->fillMiniChunk($X, $Z, $Y); + } } }else{ $this->chunks[$index][$Y] = false; @@ -419,9 +530,9 @@ protected function fillMiniChunk($X, $Z, $Y){ return false; } $index = self::getIndex($X, $Z); - $this->chunks[$index][$Y] = str_repeat("\x00", 8192); + $this->chunks[$index][$Y] = str_repeat("\x00", 10240); $this->chunkChange[$index][-1] = true; - $this->chunkChange[$index][$Y] = 8192; + $this->chunkChange[$index][$Y] = 10240; $this->locationTable[$index][0] |= 1 << $Y; return true; } @@ -436,7 +547,7 @@ public function setMiniChunk($X, $Z, $Y, $data){ $index = self::getIndex($X, $Z); $this->chunks[$index][$Y] = (string) $data; $this->chunkChange[$index][-1] = true; - $this->chunkChange[$index][$Y] = 8192; + $this->chunkChange[$index][$Y] = 10240; $this->locationTable[$index][0] |= 1 << $Y; return true; } @@ -452,7 +563,7 @@ public function setMiniChunk($X, $Z, $Y, $data){ * @return number */ public function fastGetBlockID($chunkX, $chunkY, $chunkZ, $blockX, $blockY, $blockZ, $index){ - return ($this->chunks[$index][$chunkY] === false) ? 0 : ord($this->chunks[$index][$chunkY][$blockY + ($blockX << 5) + ($blockZ << 9)]); + return ($this->chunks[$index][$chunkY] === false) ? 0 : ord($this->chunks[$index][$chunkY][$blockY + $blockX*40 + $blockZ*640]); } public function getBlockID($x, $y, $z){ @@ -475,7 +586,7 @@ public function getBlockID($x, $y, $z){ $aZ = $z & 0xf; $aY = $y & 0xf; - $b = ord($this->chunks[$index][$Y][($aY + ($aX << 5) + ($aZ << 9))]); + $b = ord($this->chunks[$index][$Y][($aY + ($aX*40) + ($aZ*640))]); return $b; } @@ -494,7 +605,7 @@ public function setBlockID($x, $y, $z, $block){ $aX = $x & 0xf; $aZ = $z & 0xf; $aY = $y & 0xf; - $bind = (int) ($aY + ($aX << 5) + ($aZ << 9)); + $bind = (int) ($aY + ($aX*40) + ($aZ*640)); if($this->chunks[$index][$Y][$bind] == chr($block)){ return false; //no changes done }else{ @@ -522,7 +633,7 @@ public function getBlockLight($x, $y, $z){ $aX = $x & 0xf; $aZ = $z & 0xf; $aY = $y & 0xf; - $m = ord($this->chunks[$index][$Y][(int) (($aY >> 1) + 24 + ($aX << 5) + ($aZ << 9))]); + $m = ord($this->chunks[$index][$Y][(int) (($aY >> 1) + 24 + $aX*40 + $aZ*640)]); return $y & 1 ? $m >> 4 : $m & 0x0F; } @@ -543,7 +654,7 @@ public function setBlockLight($x, $y, $z, $value){ $aX = $x & 0xf; $aZ = $z & 0xf; $aY = $y & 0xf; - $mindex = (int) (($aY >> 1) + 24 + ($aX << 5) + ($aZ << 9)); + $mindex = (int) (($aY >> 1) + 24 + $aX*40 + $aZ*640); $old_m = ord($this->chunks[$index][$Y][$mindex]); if(($y & 1) === 0) $m = ($old_m & 0xF0) | $value; else $m = ($value << 4) | ($old_m & 0x0F); @@ -575,7 +686,7 @@ public function getBlockDamage($x, $y, $z){ $aX = $x & 0xf; $aZ = $z & 0xf; $aY = $y & 0xf; - $m = ord($this->chunks[$index][$Y][(int) (($aY >> 1) + 16 + ($aX << 5) + ($aZ << 9))]); + $m = ord($this->chunks[$index][$Y][(int) (($aY >> 1) + 16 + $aX*40 + $aZ*640)]); return $y & 1 ? $m >> 4 : $m & 0x0F; } @@ -592,7 +703,7 @@ public function setBlockDamage($x, $y, $z, $damage){ $aX = $x & 0xf; $aZ = $z & 0xf; $aY = $y & 0xf; - $mindex = (int) (($aY >> 1) + 16 + ($aX << 5) + ($aZ << 9)); + $mindex = (int) (($aY >> 1) + 16 + $aX*40 + $aZ*640); $old_m = ord($this->chunks[$index][$Y][$mindex]); if(($y & 1) === 0){ $m = ($old_m & 0xF0) | $damage; @@ -638,9 +749,9 @@ public function getBlock($x, $y, $z){ $aZ = $z & 0xf; $aY = $y & 0xf; - $b = ord($this->chunks[$index][$Y][($aY + ($aX << 5) + ($aZ << 9))]); + $b = ord($this->chunks[$index][$Y][($aY + $aX*40 + $aZ*640)]); - $m = ord($this->chunks[$index][$Y][(($aY >> 1) + 16 + ($aX << 5) + ($aZ << 9))]); + $m = ord($this->chunks[$index][$Y][(($aY >> 1) + 16 + $aX*40 + $aZ*640)]); $m = ($y & 1) ? $m >> 4 : $m & 0xf; return [$b, $m]; @@ -666,8 +777,8 @@ public function setBlock($x, $y, $z, $block, $meta = 0){ $aX = $x - ($X << 4); $aZ = $z - ($Z << 4); $aY = $y - ($Y << 4); - $bindex = (int) ($aY + ($aX << 5) + ($aZ << 9)); - $mindex = (int) (($aY >> 1) + 16 + ($aX << 5) + ($aZ << 9)); + $bindex = (int) ($aY + $aX*40 + $aZ*640); + $mindex = (int) (($aY >> 1) + 16 + $aX*40 + $aZ*640); $old_b = ord($this->chunks[$index][$Y][$bindex]); $old_m = ord($this->chunks[$index][$Y][$mindex]); @@ -697,4 +808,9 @@ public function doSaveRound(){ } } + /** + * @deprecated used in nc 1.1.1 for ncleveldata.pmf which was removed + * @var integer + */ + const NC_DATA_VERSION = 1; } diff --git a/src/world/Level.php b/src/world/Level.php index 779afa59d..689060f55 100644 --- a/src/world/Level.php +++ b/src/world/Level.php @@ -732,7 +732,7 @@ public function onTick(PocketMinecraftServer $server, $currentTime){ $y = ($xyz >> 16) & 0x7f; $cy = $ch[$y >> 4]; if($cy === false) continue; - $id = ord($cy[($y & 0xf) + ($x << 5) + ($z << 9)]); + $id = ord($cy[($y & 0xf) + $x*40 + $z*640]); if(isset(self::$randomUpdateBlocks[$id])){ $cl = StaticBlock::$prealloc[$id]; $cl::onRandomTick($this, ($cX << 4) + $x, $y, $z + ($cZ << 4)); @@ -1010,7 +1010,7 @@ public function getOrderedChunk($X, $Z, $Yndex){ for($j = 0; $j < 256; ++$j){ $ordered .= $flag; foreach($raw as $mini){ - $ordered .= substr($mini, $j << 5, 24); //16 + 8 + $ordered .= substr($mini, $j*40, 24); //16 + 8 } } return $ordered; diff --git a/src/world/generator/SuperflatGenerator.php b/src/world/generator/SuperflatGenerator.php index a36d44159..b420f6c4d 100644 --- a/src/world/generator/SuperflatGenerator.php +++ b/src/world/generator/SuperflatGenerator.php @@ -74,7 +74,7 @@ public function parsePreset($preset){ $blocks .= chr($this->structure[$y]->getID()); $metas .= substr(dechex($this->structure[$y]->getMetadata()), -1); } - $this->chunks[$Y] .= $blocks . hex2bin($metas) . "\x00\x00\x00\x00\x00\x00\x00\x00"; + $this->chunks[$Y] .= $blocks . hex2bin($metas) . "\x00\x00\x00\x00\x00\x00\x00\x00" . "\x00\x00\x00\x00\x00\x00\x00\x00"; } } } diff --git a/src/world/generator/vanilla/VanillaGenerator.php b/src/world/generator/vanilla/VanillaGenerator.php index 9ec18d457..a1930bd82 100644 --- a/src/world/generator/vanilla/VanillaGenerator.php +++ b/src/world/generator/vanilla/VanillaGenerator.php @@ -1,5 +1,5 @@ level->level->chunks[$index][$Y] = $chunkz[$Y]; - $this->level->level->chunkChange[$index][$Y] = 8192; + $this->level->level->chunkChange[$index][$Y] = 10240; $this->level->level->locationTable[$index][0] |= 1 << $Y; //TODO mv out of loop } $this->level->level->chunkChange[$index][-1] = true; @@ -191,7 +191,7 @@ public function generateHeightmap($x, $z, &$chunkz){ for($Y = 7; $Y >= 0; --$Y){ for($cY = 15; $cY >= 0; --$cY){ $blockY = $Y*16 + $cY; - $blockID = ord($chunkz[$Y][$cY + ($blockX << 5) + ($blockZ << 9)]); + $blockID = ord($chunkz[$Y][$cY + $blockX*40 + $blockZ*640]); if($blockID > 0) { $heightmapCPtr[$blockX + ($blockZ * 16)] = $blockY + 1 ; break 2; @@ -215,6 +215,7 @@ public function buildSurfaces($chunkX, $chunkZ, &$chunks, $biomes){ $this->sandNoises = $this->beachNoise->generateNoiseOctaves($chunkX * 16, $chunkZ * 16, 0, 16, 16, 1, 0.03125, 0.03125, 1); $this->gravelNoises = $this->beachNoise->generateNoiseOctaves($chunkX * 16, 109.01, $chunkZ * 16, 16, 1, 16, 0.03125, 1, 0.03125); $this->surfaceDepthNoises = $this->surfaceDepthNoise->generateNoiseOctaves($chunkX * 16, $chunkZ * 16, 0, 16, 16, 1, 0.0625, 0.0625, 0.0625); + //TODO blockZ is actually blockX here? for($blockX = 0; $blockX < 16; ++$blockX){ for($blockZ = 0; $blockZ < 16; ++$blockZ){ /** @var Biome $biome **/ @@ -227,9 +228,9 @@ public function buildSurfaces($chunkX, $chunkZ, &$chunks, $biomes){ $b2 = $biome->fillerBlock; for($blockY = 127; $blockY >= 0; --$blockY){ if($blockY <= $this->rand->nextInt(5)){ - $chunks[$blockY >> 4][($blockY & 0xf) + ($blockZ << 5) + ($blockX << 9)] = "\x07"; + $chunks[$blockY >> 4][($blockY & 0xf) + $blockZ*40 + $blockX*640] = "\x07"; }else{ - $b3 = ord($chunks[$blockY >> 4][($blockY & 0xf) + ($blockZ << 5) + ($blockX << 9)]); + $b3 = ord($chunks[$blockY >> 4][($blockY & 0xf) + $blockZ*40 + $blockX*640]); if($b3 == 0){ $i = -1; }elseif($b3 == STONE){ @@ -253,10 +254,10 @@ public function buildSurfaces($chunkX, $chunkZ, &$chunks, $biomes){ $b = $blockY < 64 && $b == 0 ? STILL_WATER : $b; $i = $nextFloat; - $chunks[$blockY >> 4][($blockY & 0xf) + ($blockZ << 5) + ($blockX << 9)] = $blockY >= 63 ? chr($b) : chr($b2); + $chunks[$blockY >> 4][($blockY & 0xf) + ($blockZ*40) + ($blockX*640)] = $blockY >= 63 ? chr($b) : chr($b2); }elseif($i > 0){ --$i; - $chunks[$blockY >> 4][($blockY & 0xf) + ($blockZ << 5) + ($blockX << 9)] = chr($b2); + $chunks[$blockY >> 4][($blockY & 0xf) + ($blockZ*40) + ($blockX*640)] = chr($b2); if($i == 0 && $b2 == SAND){ $i = $this->rand->nextInt(4); $b2 = SANDSTONE; @@ -305,7 +306,7 @@ public function prepareHeights($chunkX, $chunkZ, &$chunks, $biomes, $temperature $fx = ($unkXX + ($unkX * 4)); $fy = (($unkY * 8) + $unkYY); $fz = ($unkZZ + ($unkZ * 4)); - $chunks[$fy >> 4][($fy & 0xf) + ($fx << 5) + ($fz << 9)] = chr($i3); + $chunks[$fy >> 4][($fy & 0xf) + $fx*40 + $fz*640] = chr($i3); $f13 += $f14; } $f9 += $f11; From cf4dc73a6c1301d9ca5acbcad6e2de67bd13e0d3 Mon Sep 17 00:00:00 2001 From: GameHerobrine Date: Tue, 28 Apr 2026 13:57:03 +0300 Subject: [PATCH 2/6] level properties, moved some settings from server.properties, cmd placeholders --- src/API/BanAPI.php | 8 +++- src/API/EntityAPI.php | 4 -- src/API/LevelAPI.php | 89 +++++++++++++++++++++++++++++++++-- src/API/ServerAPI.php | 23 +++++---- src/PocketMinecraftServer.php | 3 +- src/entity/Entity.php | 2 +- src/installer/Installer.php | 1 + src/material/StaticBlock.php | 7 ++- src/world/Level.php | 35 +++++++++++++- src/world/MobSpawner.php | 23 ++++++--- 10 files changed, 165 insertions(+), 30 deletions(-) diff --git a/src/API/BanAPI.php b/src/API/BanAPI.php index ecd2f7be7..5c6d984e5 100644 --- a/src/API/BanAPI.php +++ b/src/API/BanAPI.php @@ -74,7 +74,9 @@ public function permissionsCheck($data, $event){ if(!$this->isOp($player->iusername) && $player->level->getName() === $this->server->api->level->getDefault()){ $t = new Vector2($data["target"]->x, $data["target"]->z); $s = new Vector2($this->server->spawn->x, $this->server->spawn->z); - if($t->distance($s) <= $this->server->api->getProperty("spawn-protection") and $this->server->api->dhandle($event . ".spawn", $data) !== true){ + $sp = $player->entity->level->getProperty("spawn-protection"); + + if($t->distance($s) <= $sp and $this->server->api->dhandle($event . ".spawn", $data) !== true){ return false; } } @@ -84,7 +86,9 @@ public function permissionsCheck($data, $event){ if(!$this->isOp($player->iusername) && $player->level->getName() === $this->server->api->level->getDefault()){ $t = new Vector2($data["block"]->x, $data["block"]->z); $s = new Vector2($this->server->spawn->x, $this->server->spawn->z); - if($t->distance($s) <= $this->server->api->getProperty("spawn-protection") and $this->server->api->dhandle($event . ".spawn", $data) !== true){ + $sp = $player->entity->level->getProperty("spawn-protection"); + + if($t->distance($s) <= $sp and $this->server->api->dhandle($event . ".spawn", $data) !== true){ return false; } } diff --git a/src/API/EntityAPI.php b/src/API/EntityAPI.php index 002220a8c..1bb95b882 100644 --- a/src/API/EntityAPI.php +++ b/src/API/EntityAPI.php @@ -7,13 +7,9 @@ class EntityAPI{ public $entities; private $server; private $eCnt = 1; - private $serverSpawnAnimals, $serverSpawnMobs; function __construct(){ $this->entities = []; $this->server = ServerAPI::request(); - - $this->serverSpawnAnimals = $this->server->api->getProperty("spawn-animals"); - $this->serverSpawnMobs = $this->server->api->getProperty("spawn-mobs"); } public function init(){ diff --git a/src/API/LevelAPI.php b/src/API/LevelAPI.php index b9fcb543b..f03daeb0d 100644 --- a/src/API/LevelAPI.php +++ b/src/API/LevelAPI.php @@ -13,6 +13,28 @@ public function __construct(){ } public function init(){ + //TODO per-level difficulty, interact, random-ticking + $this->registerProperty("difficulty", 2, desc: "WIP for now, Difficulty level. 0 - Peaceful, 1 - Easy, 2 - Normal, 3 - Hard"); + $this->registerProperty("mobs-amount", 50, desc: "Max amount of mobs that can be spawned."); + $this->registerProperty("spawn-animals", true, desc: "Spawn animals."); + $this->registerProperty("spawn-monsters", true, desc: "Spawn monsters."); + $this->registerProperty("pvp", true, desc: "Enable or disable pvp in the level"); + $this->registerProperty("random-ticking", "subchunk", desc: [ + "WIP", "Affects how random ticking works. Available modes:", + "subchunk - tick 16x16x16 areas", + "fullchunk - tick 16x16x128 areas", + "vanilla - tick 256x256x128 area" + ]); + $this->registerProperty("interact", true, desc: "WIP, Enable or disable ability to interact with the level"); + $this->registerProperty("spawn-protection", 16, desc: "Prevent player interactions blocks around the spawn."); + + + $this->server->api->console->register("setworldprop", "[world] ", [$this, "commandHandler"]); + $this->server->api->console->register("getworldprop", "[world] ", [$this, "commandHandler"]); + $this->server->api->console->register("reloadworldprops", "[world]", [$this, "commandHandler"]); + $this->server->api->console->register("createworld", " [type] [seed]", [$this, "commandHandler"]); + $this->server->api->console->register("loadworld", "", [$this, "commandHandler"]); + $this->server->api->console->register("seed", "[world]", [$this, "commandHandler"]); $this->server->api->console->register("save-all", "", [$this, "commandHandler"]); $this->server->api->console->register("save-on", "", [$this, "commandHandler"]); @@ -26,6 +48,49 @@ public function init(){ } $this->server->spawn = $this->getDefault()->getSafeSpawn(); } + + + private $defaultLevelProperties = []; + private $levelPropsInfo = []; + + /** + * Registers a level property + * + * @param string $name - property name + * @param mixed $defaultValue - default property value + * @param array[string]|string|boolean $desc=false - property description(false=none, string=single line, array of strings=multiline) + * @return false if the property exists, true if success + */ + public function registerProperty(string $name, $defaultValue, $desc = false){ + if(isset($this->defaultLevelProperties[$name])) return false; + + $this->defaultLevelProperties[$name] = $defaultValue; + if(is_array($desc) || is_string($desc)) $this->levelPropsInfo[$name] = $desc; + return true; + } + + /** + * Gets a default value for property + * @param string $name - property name + * @param mixed $fail=false - return value if the property doesn't exist + * @return string|mixed + */ + public function getDefaultProperty(string $name, $fail=false){ + return $this->defaultLevelProperties[$name] ?? $fail; + } + + /** + * Loads a properties for a level + * @param Level $level + */ + public function loadLevelProperties(Level $level){ + $propsPath = DATA_PATH . "worlds/{$level->getName()}/properties.yml"; + + $level->levelProperties = new Config($propsPath, CONFIG_YAML, $this->defaultLevelProperties, comments: $this->levelPropsInfo); + foreach($level->levelProperties->getAll() as $k => $v){ + $level->setProperty($k, $v); + } + } public function loadLevel($name){ if($this->get($name) !== false){ @@ -50,14 +115,16 @@ public function loadLevel($name){ } } - $entities = new Config($path . "entities.yml", CONFIG_YAML); if(file_exists($path . "tileEntities.yml")){ @rename($path . "tileEntities.yml", $path . "tiles.yml"); } $tiles = new Config($path . "tiles.yml", CONFIG_YAML); $blockUpdates = new Config($path . "bupdates.yml", CONFIG_YAML); - $this->levels[$name] = new Level($level, $entities, $tiles, $blockUpdates, $name); + + $this->levels[$name] = $lvl = new Level($level, $entities, $tiles, $blockUpdates, $name); + $this->loadLevelProperties($lvl); + foreach($entities->getAll() as $entity){ if(!isset($entity["id"])){ break; @@ -82,15 +149,12 @@ public function loadLevel($name){ ]); }elseif($entity["id"] === FALLING_SAND){ $e = $this->server->api->entity->add($this->levels[$name], ENTITY_FALLING, $entity["id"], $entity); - //$e->setPosition(new Vector3($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2]), $entity["Rotation"][0], $entity["Rotation"][1]); $e->setHealth($entity["Health"]); }elseif(Utils::getEntityTypeByID($entity["id"]) === ENTITY_OBJECT){ //Object $e = $this->server->api->entity->add($this->levels[$name], ENTITY_OBJECT, $entity["id"], $entity); - //$e->setPosition(new Vector3($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2]), $entity["Rotation"][0], $entity["Rotation"][1]); $e->setHealth(1); }else{ $e = $this->server->api->entity->add($this->levels[$name], ENTITY_MOB, $entity["id"], $entity); - //$e->setPosition(new Vector3($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2]), $entity["Rotation"][0], $entity["Rotation"][1]); $e->setHealth($entity["Health"]); } } @@ -166,6 +230,21 @@ public function getDefault(){ public function commandHandler($cmd, $params, $issuer, $alias){ $output = ""; switch($cmd){ + case "setworldprop": + + return "WIP"; + case "getworldprop": + + return "WIP"; + case "reloadworldprops": + + return "WIP"; + case "createworld": + return "WIP"; + case "loadworld": + + return "WIP"; + case "setwspawn": if(!($issuer instanceof Player)){ return ("Please run this command in-game. "); diff --git a/src/API/ServerAPI.php b/src/API/ServerAPI.php index 4cadc27f6..36b49d592 100644 --- a/src/API/ServerAPI.php +++ b/src/API/ServerAPI.php @@ -116,18 +116,18 @@ public function load(){ "memory-limit" => "128M", "white-list" => false, "announce-player-achievements" => true, - "spawn-protection" => 16, + //"spawn-protection" => 16, "view-distance" => 10, "max-players" => 20, "allow-flight" => true, - "spawn-animals" => true, - "spawn-mobs" => true, - "mobs-amount" => 50, + //"spawn-animals" => true, + //"spawn-mobs" => true, + //"mobs-amount" => 50, "gamemode" => SURVIVAL, "hardcore" => false, - "pvp" => true, - "difficulty" => 2, + //"pvp" => true, "generator-settings" => "", + "difficulty" => 2, "level-name" => "world", "level-seed" => "", "level-type" => "DEFAULT", @@ -140,7 +140,14 @@ public function load(){ "ticking-mode" => "legacy", "mushroom-spread" => Block::$mushroomSpread ], comments: [ + "level-name" => [ + "Default level name" + ], + "level-seed" => [ + "Default level seed" + ], "level-type" => [ + "Default level type", "Alowed types:", "FLAT - flat world", "DEFAULT - use one of the 4 pregenerated worlds from pocketmine", @@ -190,7 +197,6 @@ public function load(){ ConsoleAPI::warn("Fly checking is enabled! Players may experience issues with kicking while not flying!"); } $this->parseProperties(); - MobSpawner::$MOB_LIMIT = $this->getProperty("mobs-amount", 50); Entity::$allowedAI = $this->getProperty("enable-mob-ai", true); PocketMinecraftServer::$PACKET_READING_LIMIT = $this->getProperty("abort-reading-after-N-packets", PocketMinecraftServer::$PACKET_READING_LIMIT); PocketMinecraftServer::$TICKING_MODE = match(strtolower($this->getProperty("ticking-mode"))){ @@ -202,8 +208,7 @@ public function load(){ define("DEBUG", $this->getProperty("debug", 1)); define("ADVANCED_CACHE", false); //define("MAX_CHUNK_RATE", 20 / $this->getProperty("max-chunks-per-second", 8)); //Default rate ~512 kB/s - MobSpawner::$spawnAnimals = $this->getProperty("spawn-animals"); - MobSpawner::$spawnMobs = $this->getProperty("spawn-mobs"); + if($this->getProperty("upnp-forwarding")){ console("[INFO] [UPnP] Trying to port forward..."); UPnP_PortForward($this->getProperty("server-port")); diff --git a/src/PocketMinecraftServer.php b/src/PocketMinecraftServer.php index 150e3e907..a52571128 100644 --- a/src/PocketMinecraftServer.php +++ b/src/PocketMinecraftServer.php @@ -19,7 +19,8 @@ class PocketMinecraftServer{ public static $TICKING_MODE = PocketMinecraftServer::TICK_LEGACY; - public $doTick, $levelData, $tiles, $entities, $schedule, $scheduleCnt, $whitelist, $spawn, $difficulty, $stop, $asyncThread; + public $doTick, $levelData, $tiles, $entities, $schedule, $scheduleCnt, $whitelist, $spawn, $stop, $asyncThread; + public $difficulty; public static $KEEP_CHUNKS_LOADED = true, $PACKET_READING_LIMIT = 100; public static $BLOCK_BREAKING_PROGRESS = 0.8; public static $ENABLE_LIGHT_UPDATES = true; diff --git a/src/entity/Entity.php b/src/entity/Entity.php index bfb6a2b1e..20a279ea1 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -1078,7 +1078,7 @@ public function getEyeHeight(){ //TODO in vanilla player's eyeHeight is 0.12 public function interactWith(Entity $e, $action) { - if($this->isPlayer() and ($this->server->api->getProperty("pvp") == false or $this->server->difficulty <= 0 or ($this->player->gamemode & 0x01) === 0x01)){ + if($this->isPlayer() and ($this->level->getProperty("pvp") == false or $this->server->difficulty <= 0 or ($this->player->gamemode & 0x01) === 0x01)){ return false; } diff --git a/src/installer/Installer.php b/src/installer/Installer.php index d732b689b..24dabc830 100644 --- a/src/installer/Installer.php +++ b/src/installer/Installer.php @@ -88,6 +88,7 @@ private function generateBaseConfig(){ $config->set("max-players", (int) $this->getInput(self::DEFAULT_PLAYERS)); echo "[*] ".$this->lang->spawn_protection_info."\n"; echo "[?] ".$this->lang->spawn_protection." (Y/n): "; + //TODO remove spawn prot from here if(strtolower($this->getInput("y")) == "n"){ $config->set("spawn-protection", -1); }else{ diff --git a/src/material/StaticBlock.php b/src/material/StaticBlock.php index 30080e176..8ee8322d9 100644 --- a/src/material/StaticBlock.php +++ b/src/material/StaticBlock.php @@ -1,6 +1,7 @@ true, FARMLAND => true, @@ -73,6 +77,35 @@ public function __construct(PMFLevel $level, Config $entities, Config $tiles, Co $this->randInt2 = 0x3C6EF35F; } + /** + * Gets level property. + * + * @param string $name - property name + * @param mixed $fail=false - return value if the property doesn't exist + * @return string|mixed + */ + public function getProperty(string $name, $fail = false){ + return $this->levelProperties->exists($name) ? $this->levelProperties->get($name) : $this->server->api->level->getDefaultProperty($name, $fail); + } + + /** + * Sets level property + * + * @param string $name - property name + * @param mixed $value - new property value + * + * @return boolean true if sucess, false if failed + */ + public function setProperty(string $name, $value){ + $c = $this->server->api->dhandle("level.property.change", ["level" => $this, "name" => $name, "value" => $value]); + if($c === false) return false; + + $this->levelProperties->set($name, $value); + $this->levelProperties->save(); + + return true; + } + public function close(){ if(isset($this->level)){ $this->save(true, true, true, true); diff --git a/src/world/MobSpawner.php b/src/world/MobSpawner.php index 21091ccd1..5512ca1db 100644 --- a/src/world/MobSpawner.php +++ b/src/world/MobSpawner.php @@ -1,13 +1,12 @@ server = ServerAPI::request(); @@ -42,11 +41,13 @@ public function checkDespawn(Living $living){ } public function handle(){ - if($this->countEntities() > self::$MOB_LIMIT || count($this->level->players) <= 0){ + $limit = $this->level->getProperty("mobs-amount"); + + if($this->countEntities() > $limit || count($this->level->players) <= 0){ return false; //not spawning } $svd = $this->totalMobsPerPlayer; - $this->totalMobsPerPlayer = min(ceil(self::$MOB_LIMIT / count($this->level->players)), self::$maxMobsNearPlayerAtOnce); + $this->totalMobsPerPlayer = min(ceil($limit / count($this->level->players)), self::$maxMobsNearPlayerAtOnce); if($svd != $this->totalMobsPerPlayer) ConsoleAPI::debug("Changed total mobs per player from $svd to {$this->totalMobsPerPlayer}."); return $this->spawnMobs(); @@ -54,11 +55,11 @@ public function handle(){ public function spawnMobs(){ $phase = $this->server->api->time->getPhase($this->level); - if(self::$spawnAnimals && ($phase == "day" || $phase == "sunrise")){ //Animal + if($this->level->getProperty("spawn-animals") && ($phase == "day" || $phase == "sunrise")){ //Animal $type = mt_rand(10, 13); $baby = false; $grassOnly = true; - }elseif(self::$spawnMobs && ($phase == "night" || $phase == "sunset") && $this->server->difficulty > 0){ //Monster, true night + }elseif($this->level->getProperty("spawn-monsters") && ($phase == "night" || $phase == "sunset") && $this->server->difficulty > 0){ //Monster, true night $type = mt_rand(32, 35); $grassOnly = false; $baby = 2; @@ -145,5 +146,15 @@ protected function getSafeY($x, $z, $grassOnly = false, $highMob = false, $isMon return empty($allowed) ? -1 : $allowed[mt_rand(0, count($allowed) - 1)]; } + /** + * @deprecated + * use $level->getProperty("mobs-amount") + */ + public static $MOB_LIMIT = 50; + /** + * @deprecated + * use $level->getProperty("spawn-animals") and $level->getProperty("spawn-monsters") + */ + public static $spawnAnimals = true, $spawnMobs = true; } From 8f9a9debc5d1c5a541efb464419d433170865985 Mon Sep 17 00:00:00 2001 From: GameHerobrine Date: Tue, 28 Apr 2026 15:46:03 +0300 Subject: [PATCH 3/6] added some vanilla block props, some changes for block placement --- src/entity/ai/tasks/TaskEatTileGoal.php | 12 +-- src/material/Block.php | 12 +-- src/material/StaticBlock.php | 20 +++- src/material/block/attachable/LadderBlock.php | 22 ++++- src/material/block/attachable/TorchBlock.php | 91 ++++++++++++++----- .../block/attachable/TrapdoorBlock.php | 55 ++++++++--- .../block/plant/BaseMushroomBlock.php | 14 ++- src/material/block/solid/StoneBlock.php | 1 + src/world/Level.php | 10 ++ 9 files changed, 176 insertions(+), 61 deletions(-) diff --git a/src/entity/ai/tasks/TaskEatTileGoal.php b/src/entity/ai/tasks/TaskEatTileGoal.php index 8bc3766e5..ee0a7a363 100644 --- a/src/entity/ai/tasks/TaskEatTileGoal.php +++ b/src/entity/ai/tasks/TaskEatTileGoal.php @@ -24,14 +24,14 @@ public function onUpdate(EntityAI $ai) if (--$this->selfCounter == 4) { - $id = $ai->entity->level->level->getBlockID($ai->entity->x, $ai->entity->y, $ai->entity->z); - $idb = $ai->entity->level->level->getBlockID($ai->entity->x, $ai->entity->y - 1, $ai->entity->z); + $id = $ai->entity->level->level->getBlockID((int)$ai->entity->x, (int)$ai->entity->y, (int)$ai->entity->z); + $idb = $ai->entity->level->level->getBlockID((int)$ai->entity->x, (int)$ai->entity->y - 1, (int)$ai->entity->z); if($id === TALL_GRASS){ - $ai->entity->level->fastSetBlockUpdate($ai->entity->x, $ai->entity->y, $ai->entity->z, AIR, 0); + $ai->entity->level->fastSetBlockUpdate((int)$ai->entity->x, (int)$ai->entity->y, (int)$ai->entity->z, AIR, 0); $ai->entity->eatGrass(); }elseif($idb === GRASS){ - $ai->entity->level->fastSetBlockUpdate($ai->entity->x, $ai->entity->y - 1, $ai->entity->z, DIRT, 0); + $ai->entity->level->fastSetBlockUpdate((int)$ai->entity->x, (int)$ai->entity->y - 1, (int)$ai->entity->z, DIRT, 0); $ai->entity->eatGrass(); } } @@ -41,8 +41,8 @@ public function canBeExecuted(EntityAI $ai) { if($ai->isStarted("TaskRandomWalk") || $ai->isStarted("TaskEatTileGoal")) return false; if(mt_rand(0, ($ai->entity instanceof Ageable && $ai->entity->isBaby()) ? 50 : 1000) == 0){ - $idm = $ai->entity->level->level->getBlock($ai->entity->x, $ai->entity->y, $ai->entity->z); - $idb = $ai->entity->level->level->getBlockID($ai->entity->x, $ai->entity->y - 1, $ai->entity->z); + $idm = $ai->entity->level->level->getBlock((int)$ai->entity->x, (int)$ai->entity->y, (int)$ai->entity->z); + $idb = $ai->entity->level->level->getBlockID((int)$ai->entity->x, (int)$ai->entity->y - 1, (int)$ai->entity->z); return ($idm[0] === TALL_GRASS && $idm[1] === 1) || $idb === GRASS; } return false; diff --git a/src/material/Block.php b/src/material/Block.php index b26c2b6b5..a33277845 100755 --- a/src/material/Block.php +++ b/src/material/Block.php @@ -253,6 +253,12 @@ public function __construct($id, $meta = 0, $name = "Unknown"){ $this->boundingBox = new AxisAlignedBB($this->x, $this->y, $this->z, $this->x + 1, $this->y + 1, $this->z + 1); } + abstract function onBreak(Item $item, Player $player); + + abstract function place(Item $item, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz); + + abstract function onActivate(Item $item, Player $player); + final public function getHardness(){ return ($this->hardness); } @@ -325,12 +331,6 @@ final public function __toString(){ public function isBreakable(Item $item, Player $player){ return $this->hardness >= 0; } - - abstract function onBreak(Item $item, Player $player); - - abstract function place(Item $item, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz); - - abstract function onActivate(Item $item, Player $player); } /***REM_START***/ diff --git a/src/material/StaticBlock.php b/src/material/StaticBlock.php index 8ee8322d9..b0c07865d 100644 --- a/src/material/StaticBlock.php +++ b/src/material/StaticBlock.php @@ -21,6 +21,8 @@ class StaticBlock public static $isLiquid = []; public static $isFullBlock = []; + public static $vIsSolid = []; //vanilla solid + public static $isCubeShaped = []; public static $material = []; public static $hardness = []; @@ -33,10 +35,18 @@ class StaticBlock public static function init(){ self::$NULL_BOUNDS = new AxisAlignedBB(0, 0, 0, 0, 0, 0); - for($i = 0; $i < 256; ++$i) self::$lightBlock[$i] = self::$lightEmission[$i] = 0; + for($i = 0; $i < 256; ++$i) { + self::$lightBlock[$i] = self::$lightEmission[$i] = 0; + } + foreach(Block::$class as $nonstaticname){ /**@var Block $b*/ $b = new $nonstaticname(); + + //TODO merge vIsSolid and isSolid later? + //TODO dont use arrays + self::$vIsSolid[$b->getID()] = in_array($b->getID(), [1,2,3,4,5,7,12,13,14,15,16,17,19,21,22,24,35,41,42,43,45,46,47,48,49,56,57,58,61,62,73,74,79,80,82,86,87,89,91,95,98,103,112,155,157,170,173,245,246,247,248,249,255]); + self::$isCubeShaped[$b->getID()] = in_array($b->getID(), [1,2,3,4,5,7,12,13,14,15,16,17,18,19,20,21,22,24,35,41,42,43,45,46,47,48,49,56,57,58,61,62,73,74,79,80,82,86,87,89,91,95,98,103,112,155,157,170,173,245,246,247,248,249,255]); self::$isSolid[$b->getID()] = $b->isSolid; self::$isTransparent[$b->getID()] = $b->isTransparent; @@ -59,6 +69,7 @@ public static function init(){ self::setBlockBounds($b->getID(), 0, 0, 0, 1, 1, 1); } + self::setBlockBounds(BED_BLOCK, 0, 0, 0, 1, 0.5625, 1); //Cake: has bounds based on world state //Chest: has bounds based on world state @@ -157,10 +168,17 @@ public static function getSlipperiness($id){ return self::$slipperiness[$id] ?? StaticBlock::DEFAULT_SLIPPERINESS; } + public static function getIsVSolid($id){ + return self::$vIsSolid[$id] ?? false; + } public static function getIsSolid($id){ return self::$isSolid[$id] ?? false; } + public static function getIsCubeShaped($id){ + return self::$isCubeShaped[$id] ?? false; + } + public static function getIsTransparent($id){ return self::$isTransparent[$id] ?? false; } diff --git a/src/material/block/attachable/LadderBlock.php b/src/material/block/attachable/LadderBlock.php index c117cf265..3f1526682 100644 --- a/src/material/block/attachable/LadderBlock.php +++ b/src/material/block/attachable/LadderBlock.php @@ -36,15 +36,27 @@ public function __construct($meta = 0){ $this->breakTime = 0.4; $this->material = Material::$decoration; } + + public function getPlacementDataValue(Level $level, $x, $y, $z, $side){ + $v = 0; //getLevelDataForAuxValue($item_aux_value); - should be 0 + if(($v == 0 || $side == 2) && $level->isSolidBlockingTile($x, $y, $z + 1)) $v = 2; + if(($v == 0 || $side == 3) && $level->isSolidBlockingTile($x, $y, $z - 1)) $v = 3; + if(($v == 0 || $side == 4) && $level->isSolidBlockingTile($x + 1, $y, $z)) $v = 4; + if(($v == 0 || $side == 5) && $level->isSolidBlockingTile($x - 1, $y, $z)) $v = 5; + return $v; //should never happen due to previous check in ::place + } + public function place(Item $item, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz){ - if($face === 0 || $face === 1){ - return false; //fix of placing invalid ids without array - } - if($target->isTransparent === false){ - $this->meta = $face; + $x = $block->x; + $y = $block->y; + $z = $block->z; + $level = $block->level; + if($level->isSolidBlockingTile($x - 1, $y, $z) || $level->isSolidBlockingTile($x + 1, $y, $z) || $level->isSolidBlockingTile($x, $y, $z - 1) || $level->isSolidBlockingTile($x, $y, $z+1)){ + $this->meta = static::getPlacementDataValue($level, $x, $y, $z, $face); $this->level->setBlock($block, $this, true, false, true); return true; } + return false; } diff --git a/src/material/block/attachable/TorchBlock.php b/src/material/block/attachable/TorchBlock.php index bbd181ccc..42a813411 100644 --- a/src/material/block/attachable/TorchBlock.php +++ b/src/material/block/attachable/TorchBlock.php @@ -19,40 +19,85 @@ public static function getAABB(Level $level, $x, $y, $z){ } public static function neighborChanged(Level $level, $x, $y, $z, $nX, $nY, $nZ, $oldID){ - $side = $level->level->getBlockDamage($x, $y, $z); - $attach = match($side){ - 1 => $level->level->getBlockID($x - 1, $y, $z), - 2 => $level->level->getBlockID($x + 1, $y, $z), - 3 => $level->level->getBlockID($x, $y, $z - 1), - 4 => $level->level->getBlockID($x, $y, $z + 1), - default => $level->level->getBlockID($x, $y - 1, $z) - }; - - if(StaticBlock::getIsTransparent($attach) && !($side === 0 && $attach === FENCE)){ //Replace with common break method + if(static::checkCanSurvive($level, $x, $y, $z)){ + $data = $level->level->getBlockDamage($x, $y, $z); + $destroy = !$level->isSolidBlockingTile($x-1, $y, $z) && $data == 1; + if(!$level->isSolidBlockingTile($x+1, $y, $z) && $data == 2) $destroy = true; + if(!$level->isSolidBlockingTile($x, $y, $z-1) && $data == 3) $destroy = true; + if(!$level->isSolidBlockingTile($x, $y, $z+1) && $data == 4) $destroy = true; + if((!static::isConnection($level, $x, $y-1, $z) && $data == 5) || $destroy){ + ServerAPI::request()->api->entity->drop(new Position($x, $y, $z, $level), BlockAPI::getItem(TORCH, 0, 1)); + $level->fastSetBlockUpdate($x, $y, $z, 0, 0); + } + } + } + + public static function isConnection(Level $level, $x, $y, $z){ + if($level->isSolidBlockingTile($x, $y, $z)){ + return true; + } + $id = $level->level->getBlockID($x, $y, $z); + return $id == FENCE || $id == GLASS || $id == COBBLE_WALL; + } + + public static function getPlacementDataValue(Level $level, $x, $y, $z, $side){ + $def = 0; + if($side == 1){ + if(static::isConnection($level, $x, $y-1, $z)) return 5; + }else if($side == 2){ + if($level->isSolidBlockingTile($x, $y, $z+1)) return 4; + } + + if($side == 5){ + if($level->isSolidBlockingTile($x-1, $y, $z)) return 1; + }else if($side == 3){ + if($level->isSolidBlockingTile($x, $y, $z-1)) return 3; + }else if($side == 4){ + if($level->isSolidBlockingTile($x+1, $y, $z)) return 2; + } + return $def; + + } + + public static function checkCanSurvive(Level $level, $x, $y, $z){ + if(static::mayPlace($level, $x, $y, $z)) return true; + if($level->level->getBlockID($x, $y, $z) == TORCH){ ServerAPI::request()->api->entity->drop(new Position($x, $y, $z, $level), BlockAPI::getItem(TORCH, 0, 1)); $level->fastSetBlockUpdate($x, $y, $z, 0, 0); } + return false; + } + + public static function onPlace(Level $level, $x, $y, $z){ + if($level->level->getBlockDamage($x, $y, $z) == 0){ + if($level->isSolidBlockingTile($x-1, $y, $z)) $level->fastSetBlockUpdateMeta($x, $y, $z, 1); + if($level->isSolidBlockingTile($x+1, $y, $z)) $level->fastSetBlockUpdateMeta($x, $y, $z, 2); + if($level->isSolidBlockingTile($x, $y, $z-1)) $level->fastSetBlockUpdateMeta($x, $y, $z, 3); + if($level->isSolidBlockingTile($x, $y, $z+1)) $level->fastSetBlockUpdateMeta($x, $y, $z, 4); + if(static::isConnection($level, $x, $y-1, $z)) $level->fastSetBlockUpdateMeta($x, $y, $z, 5); + } + static::checkCanSurvive($level, $x, $y, $z); } + public static function mayPlace(Level $level, $x, $y, $z){ + return $level->isSolidBlockingTile($x - 1, $y, $z) || $level->isSolidBlockingTile($x + 1, $y, $z) || $level->isSolidBlockingTile($x, $y, $z - 1) || $level->isSolidBlockingTile($x, $y, $z+1) || static::isConnection($level, $x, $y-1, $z); + } + public function place(Item $item, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz){ - if($target->isTransparent === false and $face !== 0){ - $faces = array( - 1 => 5, - 2 => 4, - 3 => 3, - 4 => 2, - 5 => 1, - ); - $this->meta = $faces[$face]; - $this->level->setBlock($block, $this, true, false, true); - return true; - }elseif($this->getSide(0)->isTransparent === false or $this->getSide(0)->getID() === FENCE or $this->getSide(0)->getID() === STONE_WALL){ - $this->meta = 0; + $x = $block->x; + $y = $block->y; + $z = $block->z; + $level = $block->level; + if(static::mayPlace($level, $x, $y, $z)){ + $this->meta = static::getPlacementDataValue($level, $x, $y, $z, $face); $this->level->setBlock($block, $this, true, false, true); return true; } return false; } + + + public function getDrops(Item $item, Player $player){ return array( array($this->id, 0, 1), diff --git a/src/material/block/attachable/TrapdoorBlock.php b/src/material/block/attachable/TrapdoorBlock.php index 3af179293..6fef8737c 100644 --- a/src/material/block/attachable/TrapdoorBlock.php +++ b/src/material/block/attachable/TrapdoorBlock.php @@ -14,10 +14,13 @@ public function __construct($meta = 0){ $this->breakTime = 3; $this->material = Material::$wood; } - - public function canAttachTo(Block $target){ - $id = $target->getID(); - return $id === SLAB || $id === GLOWSTONE || $id === SLAB || $id === WOOD_SLAB || (!$target->isTransparent || $target instanceof StairBlock); + + public static function attachesTo($id){ + if($id <= 0) return false; + + //check for non existent tiles is here - not needed? + //image using tile->getRenderShape() == 10 to check is stairs isSolidBlocking() && StaticBlock::getIsCubeShaped($id) || $id == GLOWSTONE || $id == SLAB || $id == WOODEN_SLAB || (StaticBlock::getBlock($id) instanceof StairBlock); } public static function isOpen($meta){ @@ -59,18 +62,29 @@ public static function updateShape(Level $level, $x, $y, $z){ } } + + public function place(Item $item, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz){ - if(($this->canAttachTo($target)) and $face !== 0 and $face !== 1){ - $faces = array( - 2 => 0, - 3 => 1, - 4 => 2, - 5 => 3, - ); - $this->meta = $faces[$face] & 0x03; - $this->level->setBlock($block, $this, true, false, true); - return true; - } + if($face == 0 || $face == 1) return false; + $level = $block->level; + $xx = $block->x; + $y = $block->y; + $zz = $block->z; + if($face == 2) ++$zz; + if($face == 3) --$zz; + if($face == 4) ++$xx; + if($face == 5) --$xx; + if(static::attachesTo($level->level->getBlockID($xx, $y, $zz))){ + $this->meta = match($face){ + 3 => 1, + 4 => 2, + 5 => 3, + default => 0 + }; + $this->level->setBlock($block, $this, true, false, true); + return true; + } + return false; } public function getDrops(Item $item, Player $player){ return array( @@ -97,4 +111,15 @@ public function onActivate(Item $item, Player $player){ $this->level->setBlock($this, $this, true, false, true); return true; } + + + /** + * @deprecated use TrapdoorBlock::attachesTo($target->getID()); + * @param Block $target + * @return boolean + */ + public function canAttachTo(Block $target){ + $id = $target->getID(); + return static::attachesTo($id); + } } \ No newline at end of file diff --git a/src/material/block/plant/BaseMushroomBlock.php b/src/material/block/plant/BaseMushroomBlock.php index 02e5df308..e80f07625 100644 --- a/src/material/block/plant/BaseMushroomBlock.php +++ b/src/material/block/plant/BaseMushroomBlock.php @@ -14,6 +14,10 @@ public static function neighborChanged(Level $level, $x, $y, $z, $nX, $nY, $nZ, $level->fastSetBlockUpdate($x, $y, $z, 0, 0); } } + + public static function canSurvive(Level $level, $x, $y, $z){ + return $level->getRawBrightness($x, $y, $z) <= 12 && StaticBlock::getIsSolid($level->level->getBlockID($x, $y-1, $z)); + } public static function onRandomTick(Level $level, $x, $y, $z){ if(Block::$mushroomSpread && mt_rand(0, 24) == 0){ @@ -46,11 +50,11 @@ public static function onRandomTick(Level $level, $x, $y, $z){ } public function place(Item $item, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz){ - $down = $this->getSide(0); - if($down->isTransparent === false){ - $this->level->setBlock($block, $this, true, false, true); - return true; + if($block->getID() == 0 || $block->material->replaceable){ //Tile::mayPlace + if(static::canSurvive($block->level, $block->x, $block->y, $block->z)){ + $this->level->setBlock($block, $this, true, false, true); + return true; + } } - return false; } } \ No newline at end of file diff --git a/src/material/block/solid/StoneBlock.php b/src/material/block/solid/StoneBlock.php index e18b1acbb..3e69cc9d3 100644 --- a/src/material/block/solid/StoneBlock.php +++ b/src/material/block/solid/StoneBlock.php @@ -8,6 +8,7 @@ public function __construct(){ $this->breakTime = 1.5; $this->material = Material::$stone; $this->lightBlock = 255; + $this->vIsSolid = true; } public function getBreakTime(Item $item, Player $player){ diff --git a/src/world/Level.php b/src/world/Level.php index b068db760..fd31639d8 100644 --- a/src/world/Level.php +++ b/src/world/Level.php @@ -518,6 +518,16 @@ public function setBlockRaw(Vector3 $pos, Block $block, $direct = true, $send = } return $ret; } + + public function isSolidBlockingTile($x, $y, $z){ + $id = $this->level->getBlockID($x, $y, $z); + if($id != 0 && StaticBlock::getMaterial($id)->isSolidBlocking()){ + return StaticBlock::getIsCubeShaped($id); + }else{ + return false; + } + } + public function fastSetBlockUpdateMeta($x, $y, $z, $meta, $updateBlock = false){ $this->level->setBlockDamage($x, $y, $z, $meta); $id = $this->level->getBlockID($x, $y, $z); From 51da30417812b85f14d7fb1f4491395e38c8a337 Mon Sep 17 00:00:00 2001 From: GameHerobrine Date: Wed, 29 Apr 2026 17:50:43 +0300 Subject: [PATCH 4/6] fixed setBlock with non int coords --- src/pmf/PMFLevel.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pmf/PMFLevel.php b/src/pmf/PMFLevel.php index 5a0e0bfb4..338467e05 100644 --- a/src/pmf/PMFLevel.php +++ b/src/pmf/PMFLevel.php @@ -774,9 +774,9 @@ public function setBlock($x, $y, $z, $block, $meta = 0){ }elseif($this->chunks[$index][$Y] === false){ $this->fillMiniChunk($X, $Z, $Y); } - $aX = $x - ($X << 4); - $aZ = $z - ($Z << 4); - $aY = $y - ($Y << 4); + $aX = $x & 0xf; + $aZ = $z & 0xf; + $aY = $y & 0xf; $bindex = (int) ($aY + $aX*40 + $aZ*640); $mindex = (int) (($aY >> 1) + 16 + $aX*40 + $aZ*640); $old_b = ord($this->chunks[$index][$Y][$bindex]); From 8f1850f198b51c3815af1afdbd91b59d7428ffb4 Mon Sep 17 00:00:00 2001 From: GameHerobrine Date: Sat, 16 May 2026 22:26:42 +0300 Subject: [PATCH 5/6] fixed generation --- src/pmf/PMFLevel.php | 12 ++++++++++-- src/world/generator/TemporalGenerator.php | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pmf/PMFLevel.php b/src/pmf/PMFLevel.php index 338467e05..760dce0cd 100644 --- a/src/pmf/PMFLevel.php +++ b/src/pmf/PMFLevel.php @@ -100,7 +100,13 @@ private function createBlank(){ $this->write(Utils::writeShort(0)); $X = $Z = null; $this->getXZ($index, $X, $Z); - @file_put_contents($this->getChunkPath($X, $Z), gzdeflate("", PMF_LEVEL_DEFLATE_LEVEL)); + + $chunk = @gzopen($this->getChunkPath($X, $Z), "wb" . PMF_LEVEL_DEFLATE_LEVEL); + gzwrite($chunk, chr(self::CHUNK_VERSION)); + gzwrite($chunk, Utils::writeShort(0)); //heightmap + gzwrite($chunk, "\x00"); //haslight + gzwrite($chunk, "\x00"); //ticking + gzclose($chunk); } if(!file_exists(dirname($this->file) . "/entities.yml")){ $entities = new Config(dirname($this->file) . "/entities.yml", CONFIG_YAML); @@ -183,6 +189,7 @@ public function markHasBlocklight($X, $Z){ $index = self::getIndex($X, $Z); $o = ord($this->hasLight[$index]); $this->hasLight[$index] = chr($o | 1); + $this->chunkChange[$index][-1] = true; return true; } public function markHasSkylight($X, $Z){ @@ -190,6 +197,7 @@ public function markHasSkylight($X, $Z){ $index = self::getIndex($X, $Z); $o = ord($this->hasLight[$index]); $this->hasLight[$index] = chr($o | 2); + $this->chunkChange[$index][-1] = true; return true; } @@ -541,7 +549,7 @@ public function setMiniChunk($X, $Z, $Y, $data){ if($this->isChunkLoaded($X, $Z) === false){ $this->loadChunk($X, $Z); } - if(strlen($data) !== 8192){ + if(strlen($data) !== 10240){ return false; } $index = self::getIndex($X, $Z); diff --git a/src/world/generator/TemporalGenerator.php b/src/world/generator/TemporalGenerator.php index ae45ac048..d9b8badb0 100644 --- a/src/world/generator/TemporalGenerator.php +++ b/src/world/generator/TemporalGenerator.php @@ -58,7 +58,8 @@ public function populateLevel(){ for($Y = 0; $Y < 8; ++$Y){ $chunk[$Y] .= substr($block, $Y << 4, 16); $chunk[$Y] .= substr($meta, $Y << 3, 8); - $chunk[$Y] .= "\x00\x00\x00\x00\x00\x00\x00\x00"; + $chunk[$Y] .= "\x00\x00\x00\x00\x00\x00\x00\x00"; //blocklight + $chunk[$Y] .= "\x00\x00\x00\x00\x00\x00\x00\x00"; //skylight } } } From 929b5bab36a1067e23c04acf29c9be208e790861 Mon Sep 17 00:00:00 2001 From: GameHerobrine Date: Tue, 18 Aug 2026 05:55:32 +0300 Subject: [PATCH 6/6] initial tileentity rewrite, added missing furnace recipe --- src/API/EntityAPI.php | 4 +- src/API/TileAPI.php | 71 ++- src/Player.php | 31 +- src/PocketMinecraftServer.php | 1 - src/constants/EntityIDs.php | 1 + src/pmf/PMFLevel.php | 2 +- src/recipes/SmeltingData.php | 1 + src/world/Level.php | 14 +- src/world/Tile.php | 480 --------------------- src/world/tileentity/ChestTileEntity.php | 119 +++++ src/world/tileentity/FurnaceTileEntity.php | 114 +++++ src/world/tileentity/SignTileEntity.php | 47 ++ src/world/tileentity/Tile.php | 260 +++++++++++ 13 files changed, 600 insertions(+), 545 deletions(-) delete mode 100644 src/world/Tile.php create mode 100644 src/world/tileentity/ChestTileEntity.php create mode 100644 src/world/tileentity/FurnaceTileEntity.php create mode 100644 src/world/tileentity/SignTileEntity.php create mode 100644 src/world/tileentity/Tile.php diff --git a/src/API/EntityAPI.php b/src/API/EntityAPI.php index 1bb95b882..42a94cb38 100644 --- a/src/API/EntityAPI.php +++ b/src/API/EntityAPI.php @@ -190,8 +190,8 @@ public function getNextEID(){ public function addRaw(Entity $e){ $eid = $e->eid; $this->entities[$eid] = $e; - $cX = (int)$this->entities[$eid]->x >> 4; - $cZ = (int)$this->entities[$eid]->z >> 4; + $cX = floor($this->entities[$eid]->x >> 4); + $cZ = floor($this->entities[$eid]->z >> 4); $e->level->entityListPositioned["$cX $cZ"][$eid] = $eid; $e->level->entityList[$eid] = &$this->entities[$eid]; $this->server->handle("entity.add", $this->entities[$eid]); diff --git a/src/API/TileAPI.php b/src/API/TileAPI.php index a5a481182..0190211ff 100644 --- a/src/API/TileAPI.php +++ b/src/API/TileAPI.php @@ -3,6 +3,9 @@ class TileAPI{ private $server; + /** + * @var Tile[] + */ private $tiles; private $tCnt = 1; @@ -10,41 +13,32 @@ function __construct(){ $this->tiles = []; $this->server = ServerAPI::request(); } - public function getXYZ(Level $level, $x, $y, $z){ - $tile = $this->server->query("SELECT * FROM tiles WHERE level = '{$level->getName()}' AND x = $x AND y = $y AND z = $z;", true); - if($tile !== false and $tile !== true and ($tile = $this->getByID($tile["ID"])) !== false){ - return $tile; + public function getXYZ(Level $level, int $x, int $y, int $z){ + $cX = ($x >> 4); + $cZ = ($z >> 4); + foreach($level->tileEntityListPositioned["$cX $cZ"] ?? [] as $tile){ + if($tile->x == $x && $tile->y == $y && $tile->z == $z) return $tile; } return false; } - public function invalidateAll(Level $level, $x, $y, $z){ - $x = (int) $x; - $y = (int) $y; - $z = (int) $z; - $tile = $this->server->query("SELECT id FROM tiles WHERE level = '{$level->getName()}' AND x = $x AND y = $y AND z = $z;", false); + public function invalidateAll(Level $level, int $x, int $y, int $z){ + $cX = ($x >> 4); + $cZ = ($z >> 4); $invcnt = 0; - if($tile instanceof SQLite3Result){ - while(($t = $tile->fetchArray(SQLITE3_ASSOC)) !== false){ - $tl = $this->getByID($t["ID"]); - if($tl instanceof Tile){ - ++$invcnt; - $tl->close(); - } - - if($invcnt > 1){ - ConsoleAPI::warn("{$level->getName()}: ($x $y $z) has more than 1 tile entity! Invalidated ID {$t["ID"]} (Total invaliated: $invcnt)"); - } + foreach($level->tileEntityListPositioned["$cX $cZ"] ?? [] as $tile){ + if($tile->x == $x && $tile->y == $y && $tile->z == $z){ + ++$invcnt; + $tile->close(); + } + if($invcnt > 1){ + ConsoleAPI::warn("{$level->getName()}: ($x $y $z) has more than 1 tile entity! Invalidated ID {$tile->id} (Total invaliated: $invcnt)"); } } } public function get(Position $pos){ - $tile = $this->server->query("SELECT * FROM tiles WHERE level = '" . $pos->level->getName() . "' AND x = {$pos->x} AND y = {$pos->y} AND z = {$pos->z};", true); - if($tile !== false and $tile !== true and ($tile = $this->getByID($tile["ID"])) !== false){ - return $tile; - } - return false; + return $this->getXYZ($pos->level, $pos->x, $pos->y, $pos->z); } public function getByID($id){ @@ -75,7 +69,13 @@ public function addSign(Level $level, $x, $y, $z, $lines = ["", "", "", ""]){ public function add(Level $level, $class, $x, $y, $z, $data = []){ $id = $this->tCnt++; - $this->tiles[$id] = new Tile($level, $id, $class, $x, $y, $z, $data); + $clz = Tile::$tileId2tileClass[$class] ?? "Tile"; + $this->tiles[$id] = $t = new $clz($level, $id, $class, $x, $y, $z, $data); + $cX = floor($t->x) >> 4; + $cZ = floor($t->z) >> 4; + $t->level->tileEntityList[$id] = $t; + $t->level->tileEntityListPositioned["$cX $cZ"][$id] = $t; + $this->spawnToAll($this->tiles[$id]); return $this->tiles[$id]; } @@ -96,17 +96,7 @@ public function spawnAll(Player $player){ public function getAll($level = null){ if($level instanceof Level){ - $tiles = []; - $l = $this->server->query("SELECT ID FROM tiles WHERE level = '" . $level->getName() . "';"); - if($l !== false and $l !== true){ - while(($t = $l->fetchArray(SQLITE3_ASSOC)) !== false){ - $t = $this->getByID($t["ID"]); - if($t instanceof Tile){ - $tiles[$t->id] = $t; - } - } - } - return $tiles; + return $level->tileEntityList; } return $this->tiles; } @@ -116,9 +106,14 @@ public function remove($id){ $t = $this->tiles[$id]; $this->tiles[$id] = null; unset($this->tiles[$id]); + if($t->level instanceof Level){ + $cX = floor($t->x >> 4); + $cZ = floor($t->z >> 4); + unset($t->level->tileEntityList[$id]); + unset($t->level->tileEntityListPositioned["$cX $cZ"][$id]); + } $t->closed = true; $t->close(); - $this->server->query("DELETE FROM tiles WHERE ID = " . $id . ";"); $this->server->api->dhandle("tile.remove", $t); $t = null; unset($t); diff --git a/src/Player.php b/src/Player.php index 4ad26a572..6c3ed3d2d 100755 --- a/src/Player.php +++ b/src/Player.php @@ -882,15 +882,9 @@ public function useChunk($X, $Z){ $pk->data = $this->level->getOrderedChunk($X, $Z, $Yndex); $cnt = $this->blockQueueDataPacket($pk); $this->chunkDataSent["$X:$Z"] = true; - - $tiles = $this->server->query("SELECT ID FROM tiles WHERE spawnable = 1 AND level = '{$this->level->getName()}' AND x >= $minX AND x <= $maxX AND z >= $minZ AND z <= $maxZ;"); - $this->lastChunk = false; - if($tiles !== false and $tiles !== true){ - while(($tile = $tiles->fetchArray(SQLITE3_ASSOC)) !== false){ - $tile = $this->server->api->tile->getByID($tile["ID"]); - if($tile instanceof Tile){ - $tile->spawn($this); - } + foreach($this->level->tileEntityListPositioned["$X $Z"] ?? [] as $tile){ + if($tile instanceof Tile){ + $tile->spawn($this); } } @@ -913,20 +907,14 @@ public function canDestroy($id, $meta){ } public function onChunkReceived($blockX, $blockZ){ - $minX = $blockX; - $maxX = $blockX + 15; - $minZ = $blockZ; - $maxZ = $blockZ + 15; - $tiles = $this->server->query("SELECT ID, x, y, z FROM tiles WHERE level = '{$this->level->getName()}' AND x >= $minX AND x <= $maxX AND z >= $minZ AND z <= $maxZ;"); - $this->lastChunk = false; - if($tiles !== false and $tiles !== true){ - while(($tile = $tiles->fetchArray(SQLITE3_ASSOC)) !== false){ - $tile = $this->server->api->tile->getByID($tile["ID"]); - if($tile instanceof Tile){ - $tile->spawn($this); - } + $blockX >>= 4; + $blockZ >>= 4; + foreach($this->level->tileEntityListPositioned["$blockX $blockZ"] ?? [] as $tile){ + if($tile instanceof Tile){ + $tile->spawn($this); } } + $this->lastChunk = false; } public function getNextChunk($world){ @@ -3275,7 +3263,6 @@ public function handleDataPacket(RakNetDataPacket $packet){ if($d["id"] !== TILE_SIGN){ $t->spawn($this); }else{ - $t->setText($d["Text1"], $d["Text2"], $d["Text3"], $d["Text4"]); } } diff --git a/src/PocketMinecraftServer.php b/src/PocketMinecraftServer.php index a52571128..276f908bb 100644 --- a/src/PocketMinecraftServer.php +++ b/src/PocketMinecraftServer.php @@ -200,7 +200,6 @@ public function startDatabase(){ $this->query("PRAGMA secure_delete = OFF;"); $this->query("CREATE TABLE players (CID INTEGER PRIMARY KEY, EID NUMERIC, ip TEXT, port NUMERIC, name TEXT UNIQUE COLLATE NOCASE);"); $this->query("CREATE TABLE entities (EID INTEGER PRIMARY KEY, level TEXT, type NUMERIC, class NUMERIC, hasUpdate NUMERIC, name TEXT, x NUMERIC, y NUMERIC, z NUMERIC, yaw NUMERIC, pitch NUMERIC, health NUMERIC);"); - $this->query("CREATE TABLE tiles (ID INTEGER PRIMARY KEY, level TEXT, class TEXT, x NUMERIC, y NUMERIC, z NUMERIC, spawnable NUMERIC);"); $this->query("CREATE TABLE actions (ID INTEGER PRIMARY KEY, interval NUMERIC, last NUMERIC, code TEXT, repeat NUMERIC);"); $this->query("CREATE TABLE handlers (ID INTEGER PRIMARY KEY, name TEXT, priority NUMERIC);"); $this->query("CREATE TABLE blockUpdates (level TEXT, x INTEGER, y INTEGER, z INTEGER, type INTEGER, delay NUMERIC);"); diff --git a/src/constants/EntityIDs.php b/src/constants/EntityIDs.php index 4e6b0293e..e578fe2e0 100644 --- a/src/constants/EntityIDs.php +++ b/src/constants/EntityIDs.php @@ -36,6 +36,7 @@ define("CHEST_SLOTS", 27); define("TILE_FURNACE", "Furnace"); define("FURNACE_SLOTS", 3); +define("TILE_NETHER_REACTOR", "NetherReactor"); const CORRECT_ENTITY_CLASSES = [ ENTITY_PLAYER => true, diff --git a/src/pmf/PMFLevel.php b/src/pmf/PMFLevel.php index 760dce0cd..4f9044f3b 100644 --- a/src/pmf/PMFLevel.php +++ b/src/pmf/PMFLevel.php @@ -467,7 +467,7 @@ public function loadChunk($X, $Z, $compatver=self::TYPE_CURRENT){ if($compatver != self::TYPE_OLD){ $ver = ord(gzread($chunk, 1)); if($ver != self::CHUNK_VERSION){ - ConsoleAPI::error("Failed to load chunk $X $Z: chunk version ($ver) doesnt match current (".self::CHUNK_VERSION.")"); + ConsoleAPI::error("Failed to load chunk $X $Z: chunk version ($ver) doesn't match current (".self::CHUNK_VERSION.")"); @gzclose($chunk); return false; } diff --git a/src/recipes/SmeltingData.php b/src/recipes/SmeltingData.php index 1dd447d33..754aac8d7 100644 --- a/src/recipes/SmeltingData.php +++ b/src/recipes/SmeltingData.php @@ -8,6 +8,7 @@ class SmeltingData{ TRUNK => [COAL, 1], //Charcoal GOLD_ORE => [GOLD_INGOT, 0], IRON_ORE => [IRON_INGOT, 0], + DIAMOND_ORE => [DIAMOND, 0], NETHERRACK => [NETHER_BRICK, 0], RAW_PORKCHOP => [COOKED_PORKCHOP, 0], CLAY => [BRICK, 0], diff --git a/src/world/Level.php b/src/world/Level.php index fd31639d8..ebb54d0a3 100644 --- a/src/world/Level.php +++ b/src/world/Level.php @@ -15,8 +15,19 @@ class Level{ * @var Entity[] */ public $entityList; - + /** + * An array with all of the tileentities in this world + * @var Tile[] + */ + public $tileEntityList; + /** + * @var Entity[] + */ public $entityListPositioned = []; + /** + * @var Tile[] + */ + public $tileEntityListPositioned = []; public $entitiesInLove = []; /** @@ -59,6 +70,7 @@ public function __construct(PMFLevel $level, Config $entities, Config $tiles, Co $this->level = $level; $this->level->level = $this; $this->entityList = []; + $this->tileEntityList = []; $this->entities = $entities; $this->tiles = $tiles; $this->blockUpdates = $blockUpdates; diff --git a/src/world/Tile.php b/src/world/Tile.php deleted file mode 100644 index 261c657fb..000000000 --- a/src/world/Tile.php +++ /dev/null @@ -1,480 +0,0 @@ -server = ServerAPI::request(); - $this->level = $level; - $this->normal = true; - $this->class = $class; - $this->data = $data; - $this->closed = false; - if($class === false){ - $this->closed = true; - } - $this->name = ""; - $this->lastUpdate = microtime(true); - $this->scheduledUpdate = false; - $this->id = (int) $id; - $this->x = (int) $x; - $this->y = (int) $y; - $this->z = (int) $z; - $this->server->query("INSERT OR REPLACE INTO tiles (ID, level, class, x, y, z) VALUES (" . $this->id . ", '" . $this->level->getName() . "', '" . $this->class . "', " . $this->x . ", " . $this->y . ", " . $this->z . ");"); - switch($this->class){ - case TILE_CHEST: - case TILE_SIGN: - $this->server->query("UPDATE tiles SET spawnable = 1 WHERE ID = " . $this->id . ";"); - break; - case TILE_FURNACE: - if(!isset($this->data["BurnTime"]) or $this->data["BurnTime"] < 0){ - $this->data["BurnTime"] = 0; - } - if(!isset($this->data["CookTime"]) or $this->data["CookTime"] < 0 or ($this->data["BurnTime"] === 0 and $this->data["CookTime"] > 0)){ - $this->data["CookTime"] = 0; - } - if(!isset($this->data["MaxTime"])){ - $this->data["MaxTime"] = $this->data["BurnTime"]; - $this->data["BurnTicks"] = 0; - } - if($this->data["BurnTime"] > 0){ - $this->update(); - } - break; - } - } - - public function update(){ - if($this->closed === true){ - return false; - } - - if($this->class === TILE_FURNACE){ - $fuel = $this->getSlot(1); - $raw = $this->getSlot(0); - $product = $this->getSlot(2); - $smelt = $raw->getSmeltItem(); - $canSmelt = ($smelt !== false and $raw->count > 0 and (($product->getID() === $smelt->getID() and $product->getMetadata() === $smelt->getMetadata() and $product->count < $product->getMaxStackSize()) or $product->getID() === AIR)); - if($this->data["BurnTime"] <= 0 and $canSmelt and $fuel->getFuelTime() !== false and $fuel->count > 0){ - $this->lastUpdate = microtime(true); - $this->data["MaxTime"] = $this->data["BurnTime"] = floor($fuel->getFuelTime() * 20); - $this->data["BurnTicks"] = 0; - --$fuel->count; - if($fuel->count === 0){ - $fuel = BlockAPI::getItem(AIR, 0, 0); - } - $this->setSlot(1, $fuel, false); - $current = $this->level->getBlock($this); - if($current->getID() === FURNACE){ - $this->level->setBlock($this, BlockAPI::get(BURNING_FURNACE, $current->getMetadata()), true, false, true); - } - } - if($this->data["BurnTime"] > 0){ - $ticks = (microtime(true) - $this->lastUpdate) * 20; - $this->data["BurnTime"] -= $ticks; - $this->data["BurnTicks"] = ceil(($this->data["BurnTime"] / $this->data["MaxTime"]) * 200); - if($smelt !== false and $canSmelt){ - $this->data["CookTime"] += $ticks; - if($this->data["CookTime"] >= 200){ //10 seconds - $product = BlockAPI::getItem($smelt->getID(), $smelt->getMetadata(), $product->count + 1); - $this->setSlot(2, $product, false); - --$raw->count; - if($raw->count === 0){ - $raw = BlockAPI::getItem(AIR, 0, 0); - } - $this->setSlot(0, $raw, false); - $this->data["CookTime"] -= 200; - } - }elseif($this->data["BurnTime"] <= 0){ - $this->data["BurnTime"] = 0; - $this->data["CookTime"] = 0; - $this->data["BurnTicks"] = 0; - }else{ - $this->data["CookTime"] = 0; - } - - $this->server->schedule(2, [$this, "update"]); - $this->scheduledUpdate = true; - }else{ - $current = $this->level->getBlock($this); - if($current->getID() === BURNING_FURNACE){ - $this->level->setBlock($this, BlockAPI::get(FURNACE, $current->getMetadata()), true, false, true); - } - $this->data["CookTime"] = 0; - $this->data["BurnTime"] = 0; - $this->data["BurnTicks"] = 0; - $this->scheduledUpdate = false; - } - } - $this->server->handle("tile.update", $this); - $this->lastUpdate = microtime(true); - } - - public function getSlot($s){ - $i = $this->getSlotIndex($s); - if($i === false or $i < 0){ - return BlockAPI::getItem(AIR, 0, 0); - }else{ - return BlockAPI::getItem($this->data["Items"][$i]["id"], $this->data["Items"][$i]["Damage"], $this->data["Items"][$i]["Count"]); - } - } - - public function getSlotIndex($s){ - if($this->class !== TILE_CHEST and $this->class !== TILE_FURNACE){ - return false; - } - foreach($this->data["Items"] as $i => $slot){ - if($slot["Slot"] === $s){ - return $i; - } - } - return -1; - } - - public function setSlot($s, Item $item, $update = true, $offset = 0){ - $i = $this->getSlotIndex($s); - $d = [ - "Count" => $item->count, - "Slot" => $s, - "id" => $item->getID(), - "Damage" => $item->getMetadata(), - ]; - if($i === false){ - return false; - }elseif($item->getID() === AIR or $item->count <= 0){ - if($i >= 0){ - unset($this->data["Items"][$i]); - } - }elseif($i < 0){ - $this->data["Items"][] = $d; - }else{ - $this->data["Items"][$i] = $d; - } - $this->server->api->dhandle("tile.container.slot", [ - "tile" => $this, - "slot" => $s, - "offset" => $offset, - "slotdata" => $item, - ]); - - if($update === true and $this->scheduledUpdate === false){ - $this->update(); - } - return true; - } - - public function pairWith(Tile $tile){ - if($this->isPaired() or $tile->isPaired()){ - return false; - } - - $this->data["pairx"] = $tile->x; - $this->data["pairz"] = $tile->z; - - $tile->data["pairx"] = $this->x; - $tile->data["pairz"] = $this->z; - - $this->server->api->tile->spawnToAll($this); - $this->server->api->tile->spawnToAll($tile); - $this->server->handle("tile.update", $this); - $this->server->handle("tile.update", $tile); - } - - public function isPaired(){ - if($this->class !== TILE_CHEST){ - return false; - } - if(!isset($this->data["pairx"]) or !isset($this->data["pairz"])){ - return false; - } - return true; - } - - public function unpair(){ - if(!$this->isPaired()){ - return false; - } - - $tile = $this->getPair(); - unset($this->data["pairx"], $this->data["pairz"], $tile->data["pairx"], $tile->data["pairz"]); - - $this->server->api->tile->spawnToAll($this); - $this->server->handle("tile.update", $this); - if($tile instanceof Tile){ - $this->server->api->tile->spawnToAll($tile); - $this->server->handle("tile.update", $tile); - } - } - - public function getPair(){ - if($this->isPaired()){ - return $this->server->api->tile->get(new Position((int) $this->data["pairx"], $this->y, (int) $this->data["pairz"], $this->level)); - } - return false; - } - - public function openInventory(Player $player){ - if($this->class === TILE_CHEST){ - $player->windowCnt++; - $player->windowCnt = $id = max(2, $player->windowCnt % 99); - if(($pair = $this->getPair()) !== false){ - if(($pair->x + ($pair->z << 13)) > ($this->x + ($this->z << 13))){ //Order them correctly - $player->windows[$id] = [ - $pair, - $this - ]; - }else{ - $player->windows[$id] = [ - $this, - $pair - ]; - } - }else{ - $player->windows[$id] = $this; - } - - $pk = new ContainerOpenPacket; - $pk->windowid = $id; - $pk->type = WINDOW_CHEST; - $pk->slots = is_array($player->windows[$id]) ? CHEST_SLOTS << 1 : CHEST_SLOTS; - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $player->blockQueueDataPacket($pk); - $slots = []; - - if(is_array($player->windows[$id])){ - $all = $this->server->api->player->getAll($this->level); - foreach($player->windows[$id] as $ob){ - $pk = new TileEventPacket; - $pk->x = $ob->x; - $pk->y = $ob->y; - $pk->z = $ob->z; - $pk->case1 = 1; - $pk->case2 = 2; - foreach($this->level->players as $pl){ - $pl->blockQueueDataPacket(clone $pk); - } - for($s = 0; $s < CHEST_SLOTS; ++$s){ - $slot = $ob->getSlot($s); - if($slot->getID() > AIR and $slot->count > 0){ - $slots[] = $slot; - }else{ - $slots[] = BlockAPI::getItem(AIR, 0, 0); - } - } - } - }else{ - $pk = new TileEventPacket; - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $pk->case1 = 1; - $pk->case2 = 2; - foreach($this->level->players as $pl){ - $pl->blockQueueDataPacket(clone $pk); - } - - for($s = 0; $s < CHEST_SLOTS; ++$s){ - $slot = $this->getSlot($s); - if($slot->getID() > AIR and $slot->count > 0){ - $slots[] = $slot; - }else{ - $slots[] = BlockAPI::getItem(AIR, 0, 0); - } - } - } - - $pk = new ContainerSetContentPacket; - $pk->windowid = $id; - $pk->slots = $slots; - $player->blockQueueDataPacket($pk); - return true; - }elseif($this->class === TILE_FURNACE){ - $player->windowCnt++; - $player->windowCnt = $id = max(2, $player->windowCnt % 99); - $player->windows[$id] = $this; - - $pk = new ContainerOpenPacket; - $pk->windowid = $id; - $pk->type = WINDOW_FURNACE; - $pk->slots = FURNACE_SLOTS; - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $player->blockQueueDataPacket($pk); - - $slots = []; - for($s = 0; $s < FURNACE_SLOTS; ++$s){ - $slot = $this->getSlot($s); - if($slot->getID() > AIR and $slot->count > 0){ - $slots[] = $slot; - }else{ - $slots[] = BlockAPI::getItem(AIR, 0, 0); - } - } - $pk = new ContainerSetContentPacket; - $pk->windowid = $id; - $pk->slots = $slots; - $player->blockQueueDataPacket($pk); - return true; - } - } - - public function spawn($player){ - if($this->closed){ - return false; - } - if(!($player instanceof Player)){ - $player = $this->server->api->player->get($player); - } - switch($this->class){ - case TILE_CHEST: - $nbt = new NBT(); - $nbt->write(chr(NBT::TAG_COMPOUND) . "\x00\x00"); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("x"); - $nbt->writeTAG_Int((int) $this->x); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("y"); - $nbt->writeTAG_Int((int) $this->y); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("z"); - $nbt->writeTAG_Int((int) $this->z); - - if($this->isPaired()){ - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("pairx"); - $nbt->writeTAG_Int((int) $this->data["pairx"]); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("pairz"); - $nbt->writeTAG_Int((int) $this->data["pairz"]); - } - - $nbt->write(chr(NBT::TAG_END)); - - $pk = new EntityDataPacket; - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $pk->namedtag = $nbt->binary; - $player->blockQueueDataPacket($pk); - break; - case TILE_SIGN: - $nbt = new NBT(); - $nbt->write(chr(NBT::TAG_COMPOUND) . "\x00\x00"); - - $nbt->write(chr(NBT::TAG_STRING)); - $nbt->writeTAG_String("Text1"); - $nbt->writeTAG_String(mb_substr($this->data["Text1"], 0, 15)); - - $nbt->write(chr(NBT::TAG_STRING)); - $nbt->writeTAG_String("Text2"); - $nbt->writeTAG_String(mb_substr($this->data["Text2"], 0, 15)); - - $nbt->write(chr(NBT::TAG_STRING)); - $nbt->writeTAG_String("Text3"); - $nbt->writeTAG_String(mb_substr($this->data["Text3"], 0, 15)); - - $nbt->write(chr(NBT::TAG_STRING)); - $nbt->writeTAG_String("Text4"); - $nbt->writeTAG_String(mb_substr($this->data["Text4"], 0, 15)); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("x"); - $nbt->writeTAG_Int((int) $this->x); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("y"); - $nbt->writeTAG_Int((int) $this->y); - - $nbt->write(chr(NBT::TAG_INT)); - $nbt->writeTAG_String("z"); - $nbt->writeTAG_Int((int) $this->z); - - $nbt->write(chr(NBT::TAG_END)); - - $pk = new EntityDataPacket; - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $pk->namedtag = $nbt->binary; - $player->blockQueueDataPacket($pk); - break; - } - } - - public function setText($line1 = "", $line2 = "", $line3 = "", $line4 = ""){ - if($this->class !== TILE_SIGN){ - return false; - } - if(Level::$disableEmojisOnSigns){ - $line1 = Utils::replaceEmoji($line1); - $line2 = Utils::replaceEmoji($line2); - $line3 = Utils::replaceEmoji($line3); - $line4 = Utils::replaceEmoji($line4); - - } - $this->data["Text1"] = $line1; - $this->data["Text2"] = $line2; - $this->data["Text3"] = $line3; - $this->data["Text4"] = $line4; - $this->server->api->tile->spawnToAll($this); - $this->server->handle("tile.update", $this); - return true; - } - - public function getText(){ - return [ - $this->data["Text1"], - $this->data["Text2"], - $this->data["Text3"], - $this->data["Text4"] - ]; - } - - public function __destruct(){ - $this->close(); - } - - public function close(){ - if($this->closed === false){ - $this->closed = true; - $this->server->api->tile->remove($this->id); - } - } - - public function getName(){ - return $this->name; - } - - - public function setPosition(Vector3 $pos){ - if($pos instanceof Position){ - $this->level = $pos->level; - $this->server->query("UPDATE tiles SET level = '" . $this->level->getName() . "' WHERE ID = " . $this->id . ";"); - } - $this->x = (int) $pos->x; - $this->y = (int) $pos->y; - $this->z = (int) $pos->z; - $this->server->query("UPDATE tiles SET x = " . $this->x . ", y = " . $this->y . ", z = " . $this->z . " WHERE ID = " . $this->id . ";"); - } -} diff --git a/src/world/tileentity/ChestTileEntity.php b/src/world/tileentity/ChestTileEntity.php new file mode 100644 index 000000000..8fe07b11f --- /dev/null +++ b/src/world/tileentity/ChestTileEntity.php @@ -0,0 +1,119 @@ +write(chr(NBT::TAG_COMPOUND) . "\x00\x00"); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("x"); + $nbt->writeTAG_Int((int) $this->x); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("y"); + $nbt->writeTAG_Int((int) $this->y); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("z"); + $nbt->writeTAG_Int((int) $this->z); + + if($this->isPaired()){ + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("pairx"); + $nbt->writeTAG_Int((int) $this->data["pairx"]); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("pairz"); + $nbt->writeTAG_Int((int) $this->data["pairz"]); + } + + $nbt->write(chr(NBT::TAG_END)); + + $pk = new EntityDataPacket; + $pk->x = $this->x; + $pk->y = $this->y; + $pk->z = $this->z; + $pk->namedtag = $nbt->binary; + return $pk; + } + + public function openInventory(Player $player){ + $player->windowCnt++; + $player->windowCnt = $id = max(2, $player->windowCnt % 99); + if(($pair = $this->getPair()) !== false){ + if(($pair->x + ($pair->z << 13)) > ($this->x + ($this->z << 13))){ //Order them correctly + $player->windows[$id] = [ + $pair, + $this + ]; + }else{ + $player->windows[$id] = [ + $this, + $pair + ]; + } + }else{ + $player->windows[$id] = $this; + } + + $pk = new ContainerOpenPacket; + $pk->windowid = $id; + $pk->type = WINDOW_CHEST; + $pk->slots = is_array($player->windows[$id]) ? CHEST_SLOTS << 1 : CHEST_SLOTS; + $pk->x = $this->x; + $pk->y = $this->y; + $pk->z = $this->z; + $player->blockQueueDataPacket($pk); + $slots = []; + + if(is_array($player->windows[$id])){ + $all = $this->server->api->player->getAll($this->level); + foreach($player->windows[$id] as $ob){ + $pk = new TileEventPacket; + $pk->x = $ob->x; + $pk->y = $ob->y; + $pk->z = $ob->z; + $pk->case1 = 1; + $pk->case2 = 2; + foreach($this->level->players as $pl){ + $pl->blockQueueDataPacket(clone $pk); + } + for($s = 0; $s < CHEST_SLOTS; ++$s){ + $slot = $ob->getSlot($s); + if($slot->getID() > AIR and $slot->count > 0){ + $slots[] = $slot; + }else{ + $slots[] = BlockAPI::getItem(AIR, 0, 0); + } + } + } + }else{ + $pk = new TileEventPacket; + $pk->x = $this->x; + $pk->y = $this->y; + $pk->z = $this->z; + $pk->case1 = 1; + $pk->case2 = 2; + foreach($this->level->players as $pl){ + $pl->blockQueueDataPacket(clone $pk); + } + + for($s = 0; $s < CHEST_SLOTS; ++$s){ + $slot = $this->getSlot($s); + if($slot->getID() > AIR and $slot->count > 0){ + $slots[] = $slot; + }else{ + $slots[] = BlockAPI::getItem(AIR, 0, 0); + } + } + } + + $pk = new ContainerSetContentPacket; + $pk->windowid = $id; + $pk->slots = $slots; + $player->blockQueueDataPacket($pk); + return true; + } +} + diff --git a/src/world/tileentity/FurnaceTileEntity.php b/src/world/tileentity/FurnaceTileEntity.php new file mode 100644 index 000000000..f9eb02722 --- /dev/null +++ b/src/world/tileentity/FurnaceTileEntity.php @@ -0,0 +1,114 @@ +data["BurnTime"]) or $this->data["BurnTime"] < 0){ + $this->data["BurnTime"] = 0; + } + if(!isset($this->data["CookTime"]) or $this->data["CookTime"] < 0 or ($this->data["BurnTime"] === 0 and $this->data["CookTime"] > 0)){ + $this->data["CookTime"] = 0; + } + if(!isset($this->data["MaxTime"])){ + $this->data["MaxTime"] = $this->data["BurnTime"]; + $this->data["BurnTicks"] = 0; + } + if($this->data["BurnTime"] > 0){ + $this->update(); + } + } + + public function openInventory(Player $player){ + $player->windowCnt++; + $player->windowCnt = $id = max(2, $player->windowCnt % 99); + $player->windows[$id] = $this; + + $pk = new ContainerOpenPacket; + $pk->windowid = $id; + $pk->type = WINDOW_FURNACE; + $pk->slots = FURNACE_SLOTS; + $pk->x = $this->x; + $pk->y = $this->y; + $pk->z = $this->z; + $player->blockQueueDataPacket($pk); + + $slots = []; + for($s = 0; $s < FURNACE_SLOTS; ++$s){ + $slot = $this->getSlot($s); + if($slot->getID() > AIR and $slot->count > 0){ + $slots[] = $slot; + }else{ + $slots[] = BlockAPI::getItem(AIR, 0, 0); + } + } + $pk = new ContainerSetContentPacket; + $pk->windowid = $id; + $pk->slots = $slots; + $player->blockQueueDataPacket($pk); + return true; + } + + public function update(){ + if($this->closed === true) return false; + + $fuel = $this->getSlot(1); + $raw = $this->getSlot(0); + $product = $this->getSlot(2); + $smelt = $raw->getSmeltItem(); + $canSmelt = ($smelt !== false and $raw->count > 0 and (($product->getID() === $smelt->getID() and $product->getMetadata() === $smelt->getMetadata() and $product->count < $product->getMaxStackSize()) or $product->getID() === AIR)); + if($this->data["BurnTime"] <= 0 and $canSmelt and $fuel->getFuelTime() !== false and $fuel->count > 0){ + $this->lastUpdate = microtime(true); + $this->data["MaxTime"] = $this->data["BurnTime"] = floor($fuel->getFuelTime() * 20); + $this->data["BurnTicks"] = 0; + --$fuel->count; + if($fuel->count === 0){ + $fuel = BlockAPI::getItem(AIR, 0, 0); + } + $this->setSlot(1, $fuel, false); + $current = $this->level->getBlock($this); + if($current->getID() === FURNACE){ + $this->level->setBlock($this, BlockAPI::get(BURNING_FURNACE, $current->getMetadata()), true, false, true); + } + } + if($this->data["BurnTime"] > 0){ + $ticks = (microtime(true) - $this->lastUpdate) * 20; + $this->data["BurnTime"] -= $ticks; + $this->data["BurnTicks"] = ceil(($this->data["BurnTime"] / $this->data["MaxTime"]) * 200); + if($smelt !== false and $canSmelt){ + $this->data["CookTime"] += $ticks; + if($this->data["CookTime"] >= 200){ //10 seconds + $product = BlockAPI::getItem($smelt->getID(), $smelt->getMetadata(), $product->count + 1); + $this->setSlot(2, $product, false); + --$raw->count; + if($raw->count === 0){ + $raw = BlockAPI::getItem(AIR, 0, 0); + } + $this->setSlot(0, $raw, false); + $this->data["CookTime"] -= 200; + } + }elseif($this->data["BurnTime"] <= 0){ + $this->data["BurnTime"] = 0; + $this->data["CookTime"] = 0; + $this->data["BurnTicks"] = 0; + }else{ + $this->data["CookTime"] = 0; + } + + $this->server->schedule(2, [$this, "update"]); + $this->scheduledUpdate = true; + }else{ + $current = $this->level->getBlock($this); + if($current->getID() === BURNING_FURNACE){ + $this->level->setBlock($this, BlockAPI::get(FURNACE, $current->getMetadata()), true, false, true); + } + $this->data["CookTime"] = 0; + $this->data["BurnTime"] = 0; + $this->data["BurnTicks"] = 0; + $this->scheduledUpdate = false; + } + parent::update(); + } +} + diff --git a/src/world/tileentity/SignTileEntity.php b/src/world/tileentity/SignTileEntity.php new file mode 100644 index 000000000..1a548bcc8 --- /dev/null +++ b/src/world/tileentity/SignTileEntity.php @@ -0,0 +1,47 @@ +write(chr(NBT::TAG_COMPOUND) . "\x00\x00"); + + $nbt->write(chr(NBT::TAG_STRING)); + $nbt->writeTAG_String("Text1"); + $nbt->writeTAG_String(mb_substr($this->data["Text1"], 0, 15)); + + $nbt->write(chr(NBT::TAG_STRING)); + $nbt->writeTAG_String("Text2"); + $nbt->writeTAG_String(mb_substr($this->data["Text2"], 0, 15)); + + $nbt->write(chr(NBT::TAG_STRING)); + $nbt->writeTAG_String("Text3"); + $nbt->writeTAG_String(mb_substr($this->data["Text3"], 0, 15)); + + $nbt->write(chr(NBT::TAG_STRING)); + $nbt->writeTAG_String("Text4"); + $nbt->writeTAG_String(mb_substr($this->data["Text4"], 0, 15)); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("x"); + $nbt->writeTAG_Int((int) $this->x); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("y"); + $nbt->writeTAG_Int((int) $this->y); + + $nbt->write(chr(NBT::TAG_INT)); + $nbt->writeTAG_String("z"); + $nbt->writeTAG_Int((int) $this->z); + + $nbt->write(chr(NBT::TAG_END)); + + $pk = new EntityDataPacket; + $pk->x = $this->x; + $pk->y = $this->y; + $pk->z = $this->z; + $pk->namedtag = $nbt->binary; + return $pk; + } +} + diff --git a/src/world/tileentity/Tile.php b/src/world/tileentity/Tile.php new file mode 100644 index 000000000..13457cdd1 --- /dev/null +++ b/src/world/tileentity/Tile.php @@ -0,0 +1,260 @@ + "SignTileEntity", + TILE_CHEST => "ChestTileEntity", + TILE_FURNACE => "FurnaceTileEntity" + //TODO nether reactor tileentity + ]; + + /** + * Numeric tileentity id. Should not be modified + * @var int + */ + public $id; + public $class; + + public $name; + public $normal; + public $data; + public $attach; + public $metadata; + public $closed; + + public $scheduledUpdate, $lastUpdate; + + protected $server; + public function __construct(Level $level, $id, $class, $x, $y, $z, $data = []){ + $this->server = ServerAPI::request(); + $this->level = $level; + $this->normal = true; + $this->class = $class; + $this->data = $data; + $this->closed = false; + if($class === false){ + $this->closed = true; + } + $this->name = ""; + $this->lastUpdate = microtime(true); + $this->scheduledUpdate = false; + $this->id = (int) $id; + $this->x = (int) $x; + $this->y = (int) $y; + $this->z = (int) $z; + } + + /** + * @return array + */ + public function getSaveData(){ + return [ + "id" => $this->id, + "x" => $this->x, + "y" => $this->y, + "z" => $this->z + ]; + } + + public function update(){ + if($this->closed === true) return false; + + $this->server->handle("tile.update", $this); + $this->lastUpdate = microtime(true); + } + + public function getSlot($s){ + $i = $this->getSlotIndex($s); + if($i === false or $i < 0){ + return BlockAPI::getItem(AIR, 0, 0); + }else{ + return BlockAPI::getItem($this->data["Items"][$i]["id"], $this->data["Items"][$i]["Damage"], $this->data["Items"][$i]["Count"]); + } + } + + public function getSlotIndex($s){ + if($this->class !== TILE_CHEST and $this->class !== TILE_FURNACE){ + return false; + } + foreach($this->data["Items"] as $i => $slot){ + if($slot["Slot"] === $s){ + return $i; + } + } + return -1; + } + + public function setSlot($s, Item $item, $update = true, $offset = 0){ + $i = $this->getSlotIndex($s); + $d = [ + "Count" => $item->count, + "Slot" => $s, + "id" => $item->getID(), + "Damage" => $item->getMetadata(), + ]; + if($i === false){ + return false; + }elseif($item->getID() === AIR or $item->count <= 0){ + if($i >= 0){ + unset($this->data["Items"][$i]); + } + }elseif($i < 0){ + $this->data["Items"][] = $d; + }else{ + $this->data["Items"][$i] = $d; + } + $this->server->api->dhandle("tile.container.slot", [ + "tile" => $this, + "slot" => $s, + "offset" => $offset, + "slotdata" => $item, + ]); + + if($update === true and $this->scheduledUpdate === false){ + $this->update(); + } + return true; + } + + public function pairWith(Tile $tile){ + if($this->isPaired() or $tile->isPaired()){ + return false; + } + + $this->data["pairx"] = $tile->x; + $this->data["pairz"] = $tile->z; + + $tile->data["pairx"] = $this->x; + $tile->data["pairz"] = $this->z; + + $this->server->api->tile->spawnToAll($this); + $this->server->api->tile->spawnToAll($tile); + $this->server->handle("tile.update", $this); + $this->server->handle("tile.update", $tile); + } + + public function isPaired(){ + if($this->class !== TILE_CHEST){ + return false; + } + if(!isset($this->data["pairx"]) or !isset($this->data["pairz"])){ + return false; + } + return true; + } + + public function unpair(){ + if(!$this->isPaired()){ + return false; + } + + $tile = $this->getPair(); + unset($this->data["pairx"], $this->data["pairz"], $tile->data["pairx"], $tile->data["pairz"]); + + $this->server->api->tile->spawnToAll($this); + $this->server->handle("tile.update", $this); + if($tile instanceof Tile){ + $this->server->api->tile->spawnToAll($tile); + $this->server->handle("tile.update", $tile); + } + } + + public function getPair(){ + if($this->isPaired()){ + return $this->server->api->tile->get(new Position((int) $this->data["pairx"], $this->y, (int) $this->data["pairz"], $this->level)); + } + return false; + } + + public function openInventory(Player $player){} + public function getSpawnPacket(){ + return null; + } + + public function spawn($player){ + if($this->closed){ + return false; + } + if(!($player instanceof Player)) $player = $this->server->api->player->get($player); + $packet = $this->getSpawnPacket(); + if($packet instanceof RakNetDataPacket){ + $player->blockQueueDataPacket($packet); + } + } + + public function setText($line1 = "", $line2 = "", $line3 = "", $line4 = ""){ + if($this->class !== TILE_SIGN){ + return false; + } + if(Level::$disableEmojisOnSigns){ + $line1 = Utils::replaceEmoji($line1); + $line2 = Utils::replaceEmoji($line2); + $line3 = Utils::replaceEmoji($line3); + $line4 = Utils::replaceEmoji($line4); + + } + $this->data["Text1"] = $line1; + $this->data["Text2"] = $line2; + $this->data["Text3"] = $line3; + $this->data["Text4"] = $line4; + $this->server->api->tile->spawnToAll($this); + $this->server->handle("tile.update", $this); + return true; + } + + public function getText(){ + return [ + $this->data["Text1"], + $this->data["Text2"], + $this->data["Text3"], + $this->data["Text4"] + ]; + } + + public function __destruct(){ + $this->close(); + } + + public function close(){ + if($this->closed === false){ + $this->closed = true; + $this->server->api->tile->remove($this->id); + } + } + + public function getName(){ + return $this->name; + } + + + /** + * @deprecated Should not be used + * @param Vector3|Position $pos + */ + public function setPosition(Vector3 $pos){ + if($pos instanceof Position && $pos->level != $this->level){ + $cX = floor($this->x) >> 4; + $cZ = floor($this->z) >> 4; + unset($this->level->tileEntityList[$this->id]); + unset($this->level->tileEntityListPositioned["$cX $cZ"][$this->id]); + $this->level = $pos->level; + $this->level->tileEntityList[$this->id] = $this; + }else{ + $cX = floor($this->x) >> 4; + $cZ = floor($this->z) >> 4; + unset($this->level->tileEntityListPositioned["$cX $cZ"][$this->id]); + } + + $nCX = floor($pos->x) >> 4; + $nCZ = floor($pos->z) >> 4; + $this->level->tileEntityListPositioned["$nCX $nCZ"][$this->id] = $this; + + $this->x = (int) $pos->x; + $this->y = (int) $pos->y; + $this->z = (int) $pos->z; + } +}