*/ protected $windows; /** @var Inventory[] */ protected $windowIndex = []; protected $sendIndex = 0; protected $moveToSend = []; protected $motionToSend = []; /** @var Vector3 */ public $speed = null; public $blocked = false; public $achievements = []; public $lastCorrect; /** @var SimpleTransactionGroup */ protected $currentTransaction = null; public $craftingType = 0; //0 = 2x2 crafting, 1 = 3x3 crafting, 2 = stonecutter protected $isCrafting = false; public $loginData = []; protected $lastMovement = 0; /** @var Vector3 */ protected $forceMovement = null; protected $connected = true; protected $ip; protected $removeFormat = false; protected $port; protected $username; protected $iusername; protected $displayName; protected $startAction = false; /** @var Vector3 */ protected $sleeping = null; protected $clientID = null; protected $stepHeight = 0.6; public $usedChunks = []; protected $loadQueue = []; protected $chunkACK = []; protected $nextChunkOrderRun = 5; /** @var Player[] */ protected $hiddenPlayers = []; /** @var Vector3 */ protected $newPosition; protected $viewDistance; protected $chunksPerTick; /** @var null|Position */ private $spawnPosition = null; private $inAction = false; protected $inAirTicks = 0; protected $lastSpeedTick = 0; protected $speedTicks = 0; protected $highSpeedTicks = 0; private $needACK = []; /** * @var \pocketmine\scheduler\TaskHandler[] */ protected $tasks = []; /** @var PermissibleBase */ private $perm = null; public function isBanned(){ return $this->server->getNameBans()->isBanned(strtolower($this->getName())); } public function setBanned($value){ if($value === true){ $this->server->getNameBans()->addBan($this->getName(), null, null, null); }else{ $this->server->getNameBans()->remove($this->getName()); } } public function isWhitelisted(){ return $this->server->isWhitelisted(strtolower($this->getName())); } public function setWhitelisted($value){ if($value === true){ $this->server->addWhitelist(strtolower($this->getName())); }else{ $this->server->removeWhitelist(strtolower($this->getName())); } } public function getPlayer(){ return $this; } public function getFirstPlayed(){ return $this->namedtag instanceof Compound ? $this->namedtag["firstPlayed"] : null; } public function getLastPlayed(){ return $this->namedtag instanceof Compound ? $this->namedtag["lastPlayed"] : null; } public function hasPlayedBefore(){ return $this->namedtag instanceof Compound; } protected function initEntity(){ parent::initEntity(); } /** * @param Player $player */ public function spawnTo(Player $player){ if($this->spawned === true and $this->dead !== true and $player->dead !== true and $player->getLevel() === $this->level and $player->canSee($this)){ parent::spawnTo($player); } } /** * @return Server */ public function getServer(){ return $this->server; } /** * @return bool */ public function getRemoveFormat(){ return $this->removeFormat; } /** * @param bool $remove */ public function setRemoveFormat($remove = true){ $this->removeFormat = (bool) $remove; } /** * @param Player $player * * @return bool */ public function canSee(Player $player){ return !isset($this->hiddenPlayers[$player->getName()]); } /** * @param Player $player */ public function hidePlayer(Player $player){ if($player === $this){ return; } $this->hiddenPlayers[$player->getName()] = $player; $player->despawnFrom($this); } /** * @param Player $player */ public function showPlayer(Player $player){ if($player === $this){ return; } unset($this->hiddenPlayers[$player->getName()]); if($player->isOnline()){ $player->spawnTo($this); } } public function canCollideWith(Entity $entity){ return false; } public function resetFallDistance(){ parent::resetFallDistance(); $this->inAirTicks = 0; } /** * @return bool */ public function isOnline(){ return $this->connected === true and $this->loggedIn === true; } /** * @return bool */ public function isOp(){ return $this->server->isOp($this->getName()); } /** * @param bool $value */ public function setOp($value){ if($value === $this->isOp()){ return; } if($value === true){ $this->server->addOp($this->getName()); }else{ $this->server->removeOp($this->getName()); } $this->recalculatePermissions(); } /** * @param permission\Permission|string $name * * @return bool */ public function isPermissionSet($name){ return $this->perm->isPermissionSet($name); } /** * @param permission\Permission|string $name * * @return bool */ public function hasPermission($name){ return $this->perm->hasPermission($name); } /** * @param Plugin $plugin * @param string $name * @param bool $value * * @return permission\PermissionAttachment */ public function addAttachment(Plugin $plugin, $name = null, $value = null){ return $this->perm->addAttachment($plugin, $name, $value); } /** * @param PermissionAttachment $attachment */ public function removeAttachment(PermissionAttachment $attachment){ $this->perm->removeAttachment($attachment); } public function recalculatePermissions(){ $this->server->getPluginManager()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this); $this->server->getPluginManager()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); $this->perm->recalculatePermissions(); if($this->hasPermission(Server::BROADCAST_CHANNEL_USERS)){ $this->server->getPluginManager()->subscribeToPermission(Server::BROADCAST_CHANNEL_USERS, $this); } if($this->hasPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE)){ $this->server->getPluginManager()->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); } } /** * @return permission\PermissionAttachmentInfo[] */ public function getEffectivePermissions(){ return $this->perm->getEffectivePermissions(); } /** * @param SourceInterface $interface * @param null $clientID * @param string $ip * @param integer $port */ public function __construct(SourceInterface $interface, $clientID, $ip, $port){ $this->interface = $interface; $this->windows = new \SplObjectStorage(); $this->perm = new PermissibleBase($this); $this->namedtag = new Compound(); $this->server = Server::getInstance(); $this->lastBreak = microtime(true); $this->ip = $ip; $this->port = $port; $this->clientID = $clientID; $this->chunksPerTick = (int) $this->server->getProperty("chunk-sending.per-tick", 4); $this->spawnPosition = null; $this->gamemode = $this->server->getGamemode(); $this->setLevel($this->server->getDefaultLevel(), true); $this->viewDistance = $this->server->getViewDistance(); $this->newPosition = new Vector3(0, 0, 0); $this->boundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0); } /** * @param string $achievementId */ public function removeAchievement($achievementId){ if($this->hasAchievement($achievementId)){ $this->achievements[$achievementId] = false; } } /** * @param string $achievementId * * @return bool */ public function hasAchievement($achievementId){ if(!isset(Achievement::$list[$achievementId]) or !isset($this->achievements)){ $this->achievements = []; return false; } if(!isset($this->achievements[$achievementId]) or $this->achievements[$achievementId] == false){ return false; } return true; } /** * @return bool */ public function isConnected(){ return $this->connected === true; } /** * Gets the "friendly" name to display of this player to use in the chat. * * @return string */ public function getDisplayName(){ return $this->displayName; } /** * @param string $name */ public function setDisplayName($name){ $this->displayName = $name; } /** * @return string */ public function getNameTag(){ return $this->nameTag; } /** * @param string $name */ public function setNameTag($name){ $this->nameTag = $name; $this->despawnFromAll(); if($this->spawned === true){ $this->spawnToAll(); } } /** * Gets the player IP address * * @return string */ public function getAddress(){ return $this->ip; } /** * @return int */ public function getPort(){ return $this->port; } /** * @return bool */ public function isSleeping(){ return $this->sleeping !== null; } public function unloadChunk($x, $z){ $index = Level::chunkHash($x, $z); if(isset($this->usedChunks[$index])){ foreach($this->level->getChunkEntities($x, $z) as $entity){ if($entity !== $this){ $entity->despawnFrom($this); } } unset($this->usedChunks[$index]); } $this->level->freeChunk($x, $z, $this); unset($this->loadQueue[$index]); } /** * @return Position */ public function getSpawn(){ if($this->spawnPosition instanceof Position and $this->spawnPosition->getLevel() instanceof Level){ return $this->spawnPosition; }else{ $level = $this->server->getDefaultLevel(); return $level->getSafeSpawn(); } } /** * @param int $identifier * * @return bool */ public function checkACK($identifier){ return !isset($this->needACK[$identifier]); } public function handleACK($identifier){ unset($this->needACK[$identifier]); if(isset($this->chunkACK[$identifier])){ $index = $this->chunkACK[$identifier]; unset($this->chunkACK[$identifier]); if(isset($this->usedChunks[$index])){ $this->usedChunks[$index] = true; $X = null; $Z = null; Level::getXZ($index, $X, $Z); foreach($this->level->getChunkEntities($X, $Z) as $entity){ if($entity !== $this and !$entity->closed and !$entity->dead){ $entity->spawnTo($this); } } } } } public function sendChunk($x, $z, $payload){ if($this->connected === false){ return; } $pk = new FullChunkDataPacket(); $pk->chunkX = $x; $pk->chunkZ = $z; $pk->data = $payload; $cnt = $this->dataPacket($pk, true); if($cnt === false or $cnt === true){ return; } $this->chunkACK[$cnt] = Level::chunkHash($x, $z); } protected function sendNextChunk(){ if($this->connected === false){ return; } $count = 0; foreach($this->loadQueue as $index => $distance){ if($count >= $this->chunksPerTick){ break; } $X = null; $Z = null; Level::getXZ($index, $X, $Z); if(!$this->level->isChunkPopulated($X, $Z)){ $this->level->generateChunk($X, $Z); if($this->spawned){ continue; }else{ break; } } ++$count; unset($this->loadQueue[$index]); $this->usedChunks[$index] = false; $this->level->useChunk($X, $Z, $this); $this->level->requestChunk($X, $Z, $this, LevelProvider::ORDER_ZXY); } if(count($this->usedChunks) >= 56 and $this->spawned === false){ $spawned = 0; foreach($this->usedChunks as $d){ if($d === true){ $spawned++; } } if($spawned < 56){ return; } $this->spawned = true; $pk = new SetTimePacket(); $pk->time = $this->level->getTime(); $pk->started = $this->level->stopTime == false; $this->dataPacket($pk); $pos = $this->level->getSafeSpawn($this); $this->server->getPluginManager()->callEvent($ev = new PlayerRespawnEvent($this, $pos)); $this->teleport($ev->getRespawnPosition()); $this->sendSettings(); $this->inventory->sendContents($this); $this->inventory->sendArmorContents($this); $this->server->getPluginManager()->callEvent($ev = new PlayerJoinEvent($this, TextFormat::YELLOW . $this->getName() . " joined the game")); if(strlen(trim($ev->getJoinMessage())) > 0){ $this->server->broadcastMessage($ev->getJoinMessage()); } $this->noDamageTicks = 60; $this->spawnToAll(); if($this->server->getUpdater()->hasUpdate() and $this->hasPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE)){ $this->server->getUpdater()->showPlayerUpdate($this); } } } protected function orderChunks(){ if($this->connected === false){ return false; } $this->nextChunkOrderRun = 200; $radiusSquared = $this->viewDistance; $radius = ceil(sqrt($radiusSquared)); $side = ceil($radius / 2); $newOrder = []; $lastChunk = $this->usedChunks; $currentQueue = []; $centerX = $this->x >> 4; $centerZ = $this->z >> 4; for($X = -$side; $X <= $side; ++$X){ for($Z = -$side; $Z <= $side; ++$Z){ $chunkX = $X + $centerX; $chunkZ = $Z + $centerZ; if(!isset($this->usedChunks[$index = Level::chunkHash($chunkX, $chunkZ)])){ $newOrder[$index] = abs($X) + abs($Z); }else{ $currentQueue[$index] = abs($X) + abs($Z); } } } asort($newOrder); asort($currentQueue); $limit = $this->viewDistance; foreach($currentQueue as $index => $distance){ if($limit-- <= 0){ break; } unset($lastChunk[$index]); } foreach($lastChunk as $index => $Yndex){ $X = null; $Z = null; Level::getXZ($index, $X, $Z); $this->unloadChunk($X, $Z); } $loadedChunks = count($this->usedChunks); if((count($newOrder) + $loadedChunks) > $this->viewDistance){ $count = $loadedChunks; $this->loadQueue = []; foreach($newOrder as $k => $distance){ if(++$count > $this->viewDistance){ break; } $this->loadQueue[$k] = $distance; } }else{ $this->loadQueue = $newOrder; } return true; } /** * Sends an ordered DataPacket to the send buffer * * @param DataPacket $packet * @param bool $needACK * * @return int|bool */ public function dataPacket(DataPacket $packet, $needACK = false){ if($this->connected === false){ return false; } $this->server->getPluginManager()->callEvent($ev = new DataPacketSendEvent($this, $packet)); if($ev->isCancelled()){ return false; } $identifier = $this->interface->putPacket($this, $packet, $needACK, false); if($needACK and $identifier !== null){ $this->needACK[$identifier] = false; return $identifier; } return true; } /** * @param DataPacket $packet * @param bool $needACK * * @return bool|int */ public function directDataPacket(DataPacket $packet, $needACK = false){ if($this->connected === false){ return false; } $this->server->getPluginManager()->callEvent($ev = new DataPacketSendEvent($this, $packet)); if($ev->isCancelled()){ return false; } $identifier = $this->interface->putPacket($this, $packet, $needACK, true); if($needACK and $identifier !== null){ $this->needACK[$identifier] = false; return $identifier; } return true; } /** * @param Vector3 $pos * * @return boolean */ public function sleepOn(Vector3 $pos){ foreach($this->level->getNearbyEntities($this->boundingBox->grow(2, 1, 2), $this) as $p){ if($p instanceof Player){ if($p->sleeping !== null){ if($pos->distance($p->sleeping) <= 0.1){ return false; } } } } $this->server->getPluginManager()->callEvent($ev = new PlayerBedEnterEvent($this, $this->level->getBlock($pos))); if($ev->isCancelled()){ return false; } $this->sleeping = clone $pos; $this->teleport(new Position($pos->x + 0.5, $pos->y + 1, $pos->z + 0.5, $this->level)); $this->sendMetadata($this->getViewers()); $this->sendMetadata($this); $this->setSpawn($pos); $this->tasks[] = $this->server->getScheduler()->scheduleDelayedTask(new CallbackTask([$this, "checkSleep"]), 60); return true; } /** * Sets the spawnpoint of the player (and the compass direction) to a Vector3, or set it on another world with a Position object * * @param Vector3|Position $pos */ public function setSpawn(Vector3 $pos){ if(!($pos instanceof Position)){ $level = $this->level; }else{ $level = $pos->getLevel(); } $this->spawnPosition = new Position($pos->x, $pos->y, $pos->z, $level); $pk = new SetSpawnPositionPacket(); $pk->x = (int) $this->spawnPosition->x; $pk->y = (int) $this->spawnPosition->y; $pk->z = (int) $this->spawnPosition->z; $this->dataPacket($pk); } public function stopSleep(){ if($this->sleeping instanceof Vector3){ $this->server->getPluginManager()->callEvent($ev = new PlayerBedLeaveEvent($this, $this->level->getBlock($this->sleeping))); $this->sleeping = null; $this->sendMetadata($this->getViewers()); $this->sendMetadata($this); } } /** * WARNING: Do not use this, it's only for internal use. * Changes to this function won't be recorded on the version. */ public function checkSleep(){ if($this->sleeping instanceof Vector3){ //TODO: Move to Level $time = $this->level->getTime() % Level::TIME_FULL; if($time >= Level::TIME_NIGHT and $time < Level::TIME_SUNRISE){ foreach($this->level->getPlayers() as $p){ if($p->sleeping === null){ return; } } $this->level->setTime($this->level->getTime() + Level::TIME_FULL - $time); foreach($this->level->getPlayers() as $p){ $p->stopSleep(); } } } return; } /** * @param string $achievementId * * @return bool */ public function awardAchievement($achievementId){ if(isset(Achievement::$list[$achievementId]) and !$this->hasAchievement($achievementId)){ foreach(Achievement::$list[$achievementId]["requires"] as $requerimentId){ if(!$this->hasAchievement($requerimentId)){ return false; } } $this->server->getPluginManager()->callEvent($ev = new PlayerAchievementAwardedEvent($this, $achievementId)); if(!$ev->isCancelled()){ $this->achievements[$achievementId] = true; Achievement::broadcast($this, $achievementId); return true; }else{ return false; } } return false; } /** * @return int */ public function getGamemode(){ return $this->gamemode; } /** * Sets the gamemode, and if needed, kicks the player * TODO: Check if Mojang adds the ability to change gamemode without kicking players * * @param int $gm * * @return bool */ public function setGamemode($gm){ if($gm < 0 or $gm > 3 or $this->gamemode === $gm){ return false; } $this->server->getPluginManager()->callEvent($ev = new PlayerGameModeChangeEvent($this, (int) $gm)); if($ev->isCancelled()){ return false; } if(($this->gamemode & 0x01) === ($gm & 0x01)){ $this->gamemode = $gm; $this->sendMessage("Your gamemode has been changed to " . Server::getGamemodeString($this->getGamemode()) . ".\n"); }else{ $this->gamemode = $gm; $this->sendMessage("Your gamemode has been changed to " . Server::getGamemodeString($this->getGamemode()) . ".\n"); $this->inventory->clearAll(); $this->inventory->sendContents($this->getViewers()); $this->inventory->sendHeldItem($this->hasSpawned); } $this->namedtag->playerGameType = new Int("playerGameType", $this->gamemode); $spawnPosition = $this->getSpawn(); $pk = new StartGamePacket(); $pk->seed = $this->level->getSeed(); $pk->x = $this->x; $pk->y = $this->y + $this->getEyeHeight(); $pk->z = $this->z; $pk->spawnX = (int) $spawnPosition->x; $pk->spawnY = (int) $spawnPosition->y; $pk->spawnZ = (int) $spawnPosition->z; $pk->generator = 1; //0 old, 1 infinite, 2 flat $pk->gamemode = $this->gamemode & 0x01; $pk->eid = 0; //Always use EntityID as zero for the actual player $this->dataPacket($pk); $this->sendSettings(); return true; } /** * Sends all the option flags * * WARNING: Do not use this, it's only for internal use. * Changes to this function won't be recorded on the version. * * @param bool $nametags */ public function sendSettings($nametags = true){ /* bit mask | flag name 0x00000001 world_inmutable 0x00000002 - 0x00000004 - 0x00000008 - (autojump) 0x00000010 - 0x00000020 nametags_visible 0x00000040 ? 0x00000080 ? 0x00000100 ? 0x00000200 ? 0x00000400 ? 0x00000800 ? 0x00001000 ? 0x00002000 ? 0x00004000 ? 0x00008000 ? 0x00010000 ? 0x00020000 ? 0x00040000 ? 0x00080000 ? 0x00100000 ? 0x00200000 ? 0x00400000 ? 0x00800000 ? 0x01000000 ? 0x02000000 ? 0x04000000 ? 0x08000000 ? 0x10000000 ? 0x20000000 ? 0x40000000 ? 0x80000000 ? */ $flags = 0; if($this->isAdventure()){ $flags |= 0x01; //Do not allow placing/breaking blocks, adventure mode } if($nametags !== false){ $flags |= 0x20; //Show Nametags } $pk = new AdventureSettingsPacket(); $pk->flags = $flags; $this->dataPacket($pk); } public function isSurvival(){ return ($this->gamemode & 0x01) === 0; } public function isCreative(){ return ($this->gamemode & 0x01) > 0; } public function isAdventure(){ return ($this->gamemode & 0x02) > 0; } public function getDrops(){ if(!$this->isCreative()){ return parent::getDrops(); } return []; } protected function getCreativeBlock(Item $item){ foreach(Block::$creative as $i => $d){ if($d[0] === $item->getId() and $d[1] === $item->getDamage()){ return $i; } } return -1; } public function addEntityMotion($entityId, $x, $y, $z){ $this->motionToSend[$entityId] = [$entityId, $x, $y, $z]; } public function addEntityMovement($entityId, $x, $y, $z, $yaw, $pitch){ $this->moveToSend[$entityId] = [$entityId, $x, $y, $z, $yaw, $pitch]; } protected function processMovement($currentTick){ if($this->dead or !$this->spawned or !($this->newPosition instanceof Vector3)){ $diff = ($currentTick - $this->lastSpeedTick); if($diff >= 10){ $this->speed = new Vector3(0, 0, 0); }elseif($diff > 5 and $this->speedTicks < 20){ ++$this->speedTicks; } return; } $distanceSquared = $this->newPosition->distanceSquared($this); $revert = false; if($distanceSquared > 100){ $revert = true; }else{ if($this->chunk === null or !$this->chunk->isGenerated()){ $chunk = $this->level->getChunk($this->newPosition->x >> 4, $this->newPosition->z >> 4); if(!($chunk instanceof FullChunk) or !$chunk->isGenerated()){ $revert = true; $this->nextChunkOrderRun = 0; }else{ if($this->chunk instanceof FullChunk){ $this->chunk->removeEntity($this); } $this->chunk = $chunk; } } } if(!$revert and $distanceSquared != 0){ $dx = $this->newPosition->x - $this->x; $dy = $this->newPosition->y - $this->y; $dz = $this->newPosition->z - $this->z; $this->fastMove($dx, $dy, $dz); $diffX = $this->x - $this->newPosition->x; $diffZ = $this->z - $this->newPosition->z; $diffY = $this->y - $this->newPosition->y; if($diffY > -0.5 or $diffY < 0.5){ $diffY = 0; } $diff = $diffX ** 2 + $diffY ** 2 + $diffZ ** 2; if($this->isSurvival()){ if(!$revert and !$this->isSleeping()){ if($diff > 0.0625){ $revert = true; $this->server->getLogger()->warning($this->getName()." moved wrongly!"); } } }elseif($diff > 0){ $this->x = $this->newPosition->x; $this->y = $this->newPosition->y; $this->z = $this->newPosition->z; $radius = $this->width / 2; $this->boundingBox->setBounds($this->x - $radius, $this->y + $this->ySize, $this->z - $radius, $this->x + $radius, $this->y + $this->height + $this->ySize, $this->z + $radius); } } $from = new Location($this->lastX, $this->lastY, $this->lastZ, $this->lastYaw, $this->lastPitch, $this->level); $to = $this->getLocation(); $delta = pow($this->lastX - $to->x, 2) + pow($this->lastY - $to->y, 2) + pow($this->lastZ - $to->z, 2); $deltaAngle = abs($this->lastYaw - $to->yaw) + abs($this->lastPitch - $to->pitch); if(!$revert and ($delta > (1 / 16) or $deltaAngle > 10)){ $isFirst = ($this->lastX === null or $this->lastY === null or $this->lastZ === null); $this->lastX = $to->x; $this->lastY = $to->y; $this->lastZ = $to->z; $this->lastYaw = $to->yaw; $this->lastPitch = $to->pitch; if(!$isFirst){ $ev = new PlayerMoveEvent($this, $from, $to); $this->server->getPluginManager()->callEvent($ev); if(!($revert = $ev->isCancelled())){ //Yes, this is intended if($to->distanceSquared($ev->getTo()) > 0.01){ //If plugins modify the destination $this->teleport($ev->getTo()); }else{ $pk = new MovePlayerPacket(); $pk->eid = $this->id; $pk->x = $this->x; $pk->y = $this->y; $pk->z = $this->z; $pk->yaw = $this->yaw; $pk->pitch = $this->pitch; $pk->bodyYaw = $this->yaw; Server::broadcastPacket($this->hasSpawned, $pk); } } } $ticks = min(20, $currentTick - $this->lastSpeedTick + 0.5); if($this->speedTicks > 0){ $ticks += $this->speedTicks; } $this->speed = $from->subtract($to)->divide($ticks); $this->lastSpeedTick = $currentTick; }elseif($distanceSquared == 0){ $this->speed = new Vector3(0, 0, 0); $this->lastSpeedTick = $currentTick; } if($this->speedTicks > 0){ --$this->speedTicks; } if($revert){ $this->lastX = $from->x; $this->lastY = $from->y; $this->lastZ = $from->z; $this->lastYaw = $from->yaw; $this->lastPitch = $from->pitch; $pk = new MovePlayerPacket(); $pk->eid = 0; $pk->x = $from->x; $pk->y = $from->y + $this->getEyeHeight(); $pk->z = $from->z; $pk->bodyYaw = $from->yaw; $pk->pitch = $from->pitch; $pk->yaw = $from->yaw; $pk->teleport = true; $this->directDataPacket($pk); $this->forceMovement = new Vector3($from->x, $from->y, $from->z); }else{ $this->forceMovement = null; if($distanceSquared != 0 and $this->nextChunkOrderRun > 20){ $this->nextChunkOrderRun = 20; } } $this->newPosition = null; } public function updateMovement(){ } public function onUpdate($currentTick){ if(!$this->loggedIn){ return false; } if($this->dead === true and $this->spawned){ ++$this->deadTicks; if($this->deadTicks >= 10){ $this->despawnFromAll(); } return $this->deadTicks < 10; } $this->timings->startTiming(); $this->lastUpdate = $currentTick; if($this->spawned){ $this->processMovement($currentTick); $this->entityBaseTick(1); if($this->speed and $this->isSurvival()){ $speed = sqrt($this->speed->x ** 2 + $this->speed->z ** 2); if($speed > 0.45){ $this->highSpeedTicks += $speed > 3 ? 2 : 1; if($this->highSpeedTicks > 40 and !$this->server->getAllowFlight()){ $this->kick("Flying is not enabled on this server"); return false; }elseif($this->highSpeedTicks >= 10 and $this->highSpeedTicks % 4 === 0){ $this->forceMovement = $this->getPosition(); $this->speed = null; } }elseif($this->highSpeedTicks > 0){ if($speed < 22){ $this->highSpeedTicks = 0; }else{ $this->highSpeedTicks--; } } } if($this->onGround){ $this->inAirTicks = 0; }else{ if($this->inAirTicks > 10 and $this->isSurvival() and !$this->isSleeping()){ $expectedVelocity = (-$this->gravity) / $this->drag - ((-$this->gravity) / $this->drag) * exp(-$this->drag * ($this->inAirTicks - 2)); $diff = sqrt(abs($this->speed->y - $expectedVelocity)); if($diff > 0.6 and $expectedVelocity < $this->speed->y and !$this->server->getAllowFlight()){ if($this->inAirTicks < 100){ $this->setMotion(new Vector3(0, $expectedVelocity, 0)); }else{ $this->kick("Flying is not enabled on this server"); return false; } } } ++$this->inAirTicks; } foreach($this->level->getNearbyEntities($this->boundingBox->grow(1, 0.5, 1), $this) as $entity){ if(($currentTick - $entity->lastUpdate) > 1){ $entity->scheduleUpdate(); } if($entity instanceof Arrow and $entity->onGround){ if($entity->dead !== true){ $item = Item::get(Item::ARROW, 0, 1); if($this->isSurvival() and !$this->inventory->canAddItem($item)){ continue; } $this->server->getPluginManager()->callEvent($ev = new InventoryPickupArrowEvent($this->inventory, $entity)); if($ev->isCancelled()){ continue; } $pk = new TakeItemEntityPacket(); $pk->eid = 0; $pk->target = $entity->getId(); $this->dataPacket($pk); $pk = new TakeItemEntityPacket(); $pk->eid = $this->getId(); $pk->target = $entity->getId(); Server::broadcastPacket($entity->getViewers(), $pk); $this->inventory->addItem(clone $item, $this); $entity->kill(); } }elseif($entity instanceof DroppedItem){ if($entity->dead !== true and $entity->getPickupDelay() <= 0){ $item = $entity->getItem(); if($item instanceof Item){ if($this->isSurvival() and !$this->inventory->canAddItem($item)){ continue; } $this->server->getPluginManager()->callEvent($ev = new InventoryPickupItemEvent($this->inventory, $entity)); if($ev->isCancelled()){ continue; } switch($item->getId()){ case Item::WOOD: $this->awardAchievement("mineWood"); break; case Item::DIAMOND: $this->awardAchievement("diamond"); break; } $pk = new TakeItemEntityPacket(); $pk->eid = 0; $pk->target = $entity->getId(); $this->dataPacket($pk); $pk = new TakeItemEntityPacket(); $pk->eid = $this->getId(); $pk->target = $entity->getId(); Server::broadcastPacket($entity->getViewers(), $pk); $this->inventory->addItem(clone $item, $this); $entity->kill(); } } } } } if($this->nextChunkOrderRun-- <= 0 or $this->chunk === null){ $this->orderChunks(); } if(count($this->loadQueue) > 0 or !$this->spawned){ $this->sendNextChunk(); } if(count($this->moveToSend) > 0){ $pk = new MoveEntityPacket(); $pk->entities = $this->moveToSend; $this->dataPacket($pk); $this->moveToSend = []; } if(count($this->motionToSend) > 0){ $pk = new SetEntityMotionPacket(); $pk->entities = $this->motionToSend; $this->dataPacket($pk); $this->motionToSend = []; } $this->timings->stopTiming(); return true; } /** * Handles a Minecraft packet * TODO: Separate all of this in handlers * * WARNING: Do not use this, it's only for internal use. * Changes to this function won't be recorded on the version. * * @param DataPacket $packet */ public function handleDataPacket(DataPacket $packet){ if($this->connected === false){ return; } $this->server->getPluginManager()->callEvent($ev = new DataPacketReceiveEvent($this, $packet)); if($ev->isCancelled()){ return; } switch($packet->pid()){ case ProtocolInfo::LOGIN_PACKET: if($this->loggedIn === true){ break; } $this->username = TextFormat::clean($packet->username); $this->displayName = $this->username; $this->nameTag = $this->username; $this->iusername = strtolower($this->username); $this->loginData = ["clientId" => $packet->clientId, "loginData" => $packet->loginData]; if(count($this->server->getOnlinePlayers()) > $this->server->getMaxPlayers()){ if($this->kick("server full") === true){ return; } } if($packet->protocol1 !== ProtocolInfo::CURRENT_PROTOCOL){ if($packet->protocol1 < ProtocolInfo::CURRENT_PROTOCOL){ $pk = new LoginStatusPacket(); $pk->status = 1; $this->dataPacket($pk); }else{ $pk = new LoginStatusPacket(); $pk->status = 2; $this->dataPacket($pk); } $this->close("", "Incorrect protocol #" . $packet->protocol1, false); return; } if(strpos($packet->username, "\x00") !== false or preg_match('#^[a-zA-Z0-9_]{3,16}$#', $packet->username) == 0 or $this->username === "" or $this->iusername === "rcon" or $this->iusername === "console" or strlen($packet->username) > 16 or strlen($packet->username) < 3){ $this->close("", "Bad username"); return; } $this->server->getPluginManager()->callEvent($ev = new PlayerPreLoginEvent($this, "Plugin reason")); if($ev->isCancelled()){ $this->close("", $ev->getKickMessage()); return; } if(!$this->server->isWhitelisted(strtolower($this->getName()))){ $this->close(TextFormat::YELLOW . $this->username . " has left the game", "Server is white-listed"); return; }elseif($this->server->getNameBans()->isBanned(strtolower($this->getName())) or $this->server->getIPBans()->isBanned($this->getAddress())){ $this->close(TextFormat::YELLOW . $this->username . " has left the game", "You are banned"); return; } if($this->hasPermission(Server::BROADCAST_CHANNEL_USERS)){ $this->server->getPluginManager()->subscribeToPermission(Server::BROADCAST_CHANNEL_USERS, $this); } if($this->hasPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE)){ $this->server->getPluginManager()->subscribeToPermission(Server::BROADCAST_CHANNEL_ADMINISTRATIVE, $this); } foreach($this->server->getOnlinePlayers() as $p){ if($p !== $this and strtolower($p->getName()) === strtolower($this->getName())){ if($p->kick("logged in from another location") === false){ $this->close(TextFormat::YELLOW . $this->getName() . " has left the game", "Logged in from another location"); return; }else{ break; } } } $nbt = $this->server->getOfflinePlayerData($this->username); if(!isset($nbt->NameTag)){ $nbt->NameTag = new String("NameTag", $this->username); }else{ $nbt["NameTag"] = $this->username; } $this->gamemode = $nbt["playerGameType"] & 0x03; if($this->server->getForceGamemode()){ $this->gamemode = $this->server->getGamemode(); $nbt->playerGameType = new Int("playerGameType", $this->gamemode); } if(($level = $this->server->getLevelByName($nbt["Level"])) === null){ $this->setLevel($this->server->getDefaultLevel(), true); $nbt["Level"] = $this->level->getName(); $nbt["Pos"][0] = $this->level->getSpawnLocation()->x; $nbt["Pos"][1] = $this->level->getSpawnLocation()->y; $nbt["Pos"][2] = $this->level->getSpawnLocation()->z; }else{ $this->setLevel($level, true); } if(!($nbt instanceof Compound)){ $this->close(TextFormat::YELLOW . $this->username . " has left the game", "Invalid data"); return; } $this->achievements = []; /** @var Byte $achievement */ foreach($nbt->Achievements as $achievement){ $this->achievements[$achievement->getName()] = $achievement->getValue() > 0 ? true : false; } $nbt["lastPlayed"] = floor(microtime(true) * 1000); $this->server->saveOfflinePlayerData($this->username, $nbt); parent::__construct($this->level->getChunk($nbt["Pos"][0] >> 4, $nbt["Pos"][2] >> 4, true), $nbt); $this->loggedIn = true; $this->server->getPluginManager()->callEvent($ev = new PlayerLoginEvent($this, "Plugin reason")); if($ev->isCancelled()){ $this->close(TextFormat::YELLOW . $this->username . " has left the game", $ev->getKickMessage()); return; } if($this->isCreative()){ $this->inventory->setHeldItemSlot(0); }else{ $this->inventory->setHeldItemSlot(0); } $pk = new LoginStatusPacket(); $pk->status = 0; $this->dataPacket($pk); if($this->spawnPosition === null and isset($this->namedtag->SpawnLevel) and ($level = $this->server->getLevelByName($this->namedtag["SpawnLevel"])) instanceof Level){ $this->spawnPosition = new Position($this->namedtag["SpawnX"], $this->namedtag["SpawnY"], $this->namedtag["SpawnZ"], $level); } $spawnPosition = $this->getSpawn(); $this->dead = false; $pk = new StartGamePacket(); $pk->seed = $this->level->getSeed(); $pk->x = $this->x; $pk->y = $this->y; $pk->z = $this->z; $pk->spawnX = (int) $spawnPosition->x; $pk->spawnY = (int) $spawnPosition->y; $pk->spawnZ = (int) $spawnPosition->z; $pk->generator = 1; //0 old, 1 infinite, 2 flat $pk->gamemode = $this->gamemode & 0x01; $pk->eid = 0; //Always use EntityID as zero for the actual player $this->dataPacket($pk); $pk = new SetTimePacket(); $pk->time = $this->level->getTime(); $pk->started = $this->level->stopTime == false; $this->dataPacket($pk); $pk = new SetSpawnPositionPacket(); $pk->x = (int) $spawnPosition->x; $pk->y = (int) $spawnPosition->y; $pk->z = (int) $spawnPosition->z; $this->dataPacket($pk); $pk = new SetHealthPacket(); $pk->health = $this->getHealth(); $this->dataPacket($pk); if($this->getHealth() <= 0){ $this->dead = true; } $pk = new SetDifficultyPacket(); $pk->difficulty = $this->server->getDifficulty(); $this->dataPacket($pk); $this->server->getLogger()->info(TextFormat::AQUA . $this->username . TextFormat::WHITE . "[/" . $this->ip . ":" . $this->port . "] logged in with entity id " . $this->id . " at (" . $this->level->getName() . ", " . round($this->x, 4) . ", " . round($this->y, 4) . ", " . round($this->z, 4) . ")"); $this->orderChunks(); $this->sendNextChunk(); break; case ProtocolInfo::ROTATE_HEAD_PACKET: if($this->spawned === false or $this->dead === true){ break; } $packet->yaw %= 360; $packet->pitch %= 360; if($packet->yaw < 0){ $packet->yaw += 360; } $this->setRotation($packet->yaw, $this->pitch); break; case ProtocolInfo::MOVE_PLAYER_PACKET: $newPos = new Vector3($packet->x, $packet->y, $packet->z); $revert = false; if($this->dead === true or $this->spawned !== true){ $revert = true; $this->forceMovement = new Vector3($this->x, $this->y, $this->z); } if($this->forceMovement instanceof Vector3 and (($dist = $newPos->distanceSquared($this->forceMovement)) > 0.04 or $revert)){ $pk = new MovePlayerPacket(); $pk->eid = 0; $pk->x = $this->forceMovement->x; $pk->y = $this->forceMovement->y + $this->getEyeHeight(); $pk->z = $this->forceMovement->z; $pk->bodyYaw = $packet->bodyYaw; $pk->pitch = $packet->pitch; $pk->yaw = $packet->yaw; $pk->teleport = true; $this->directDataPacket($pk); }else{ $packet->yaw %= 360; $packet->pitch %= 360; if($packet->yaw < 0){ $packet->yaw += 360; } $this->setRotation($packet->yaw, $packet->pitch); $this->newPosition = $newPos; $this->forceMovement = null; } break; case ProtocolInfo::PLAYER_EQUIPMENT_PACKET: if($this->spawned === false or $this->dead === true){ break; } if($packet->slot === 0x28 or $packet->slot === 0 or $packet->slot === 255){ //0 for 0.8.0 compatibility $packet->slot = -1; //Air }else{ $packet->slot -= 9; //Get real block slot } if($this->isCreative()){ //Creative mode match $item = Item::get($packet->item, $packet->meta, 1); $slot = $this->getCreativeBlock($item); }else{ $item = $this->inventory->getItem($packet->slot); $slot = $packet->slot; } if($packet->slot === -1){ //Air if($this->isCreative()){ $found = false; for($i = 0; $i < $this->inventory->getHotbarSize(); ++$i){ if($this->inventory->getHotbarSlotIndex($i) === -1){ $this->inventory->setHeldItemIndex($i); $found = true; break; } } if(!$found){ //couldn't find a empty slot (error) $this->inventory->sendContents($this); break; } }else{ $this->inventory->setHeldItemSlot($packet->slot); //set Air } }elseif(!isset($item) or $slot === -1 or $item->getId() !== $packet->item or $item->getDamage() !== $packet->meta){ // packet error or not implemented $this->inventory->sendContents($this); break; }elseif($this->isCreative()){ $item = Item::get( Block::$creative[$slot][0], Block::$creative[$slot][1], 1 ); $this->inventory->setHeldItemIndex($packet->slot); }else{ $this->inventory->setHeldItemSlot($slot); } $this->inventory->sendHeldItem($this->hasSpawned); if($this->inAction === true){ $this->inAction = false; $this->sendMetadata($this->getViewers()); } break; case ProtocolInfo::USE_ITEM_PACKET: if($this->spawned === false or $this->dead === true or $this->blocked){ break; } $blockVector = new Vector3($packet->x, $packet->y, $packet->z); $this->craftingType = 0; $packet->eid = $this->id; if($packet->face >= 0 and $packet->face <= 5){ //Use Block, place if($this->inAction === true){ $this->inAction = false; $this->sendMetadata($this->getViewers()); } if($blockVector->distance($this) > 10){ }elseif($this->isCreative()){ $item = $this->inventory->getItemInHand(); if($this->level->useItemOn($blockVector, $item, $packet->face, $packet->fx, $packet->fy, $packet->fz, $this) === true){ break; } }elseif($this->inventory->getItemInHand()->getId() !== $packet->item or (($damage = $this->inventory->getItemInHand()->getDamage()) !== $packet->meta and $damage !== null)){ $this->inventory->sendHeldItem($this); }else{ $item = $this->inventory->getItemInHand(); $oldItem = clone $item; //TODO: Implement adventure mode checks if($this->level->useItemOn($blockVector, $item, $packet->face, $packet->fx, $packet->fy, $packet->fz, $this) === true){ if(!$item->equals($oldItem, true) or $item->getCount() !== $oldItem->getCount()){ $this->inventory->setItemInHand($item, $this); $this->inventory->sendHeldItem($this->hasSpawned); } break; } } $target = $this->level->getBlock($blockVector); $block = $target->getSide($packet->face); $pk = new UpdateBlockPacket(); $pk->x = $target->x; $pk->y = $target->y; $pk->z = $target->z; $pk->block = $target->getId(); $pk->meta = $target->getDamage(); $this->dataPacket($pk); $pk = new UpdateBlockPacket(); $pk->x = $block->x; $pk->y = $block->y; $pk->z = $block->z; $pk->block = $block->getId(); $pk->meta = $block->getDamage(); $this->dataPacket($pk); break; }elseif($packet->face === 0xff){ if($this->isCreative()){ $item = $this->inventory->getItemInHand(); }elseif($this->inventory->getItemInHand()->getId() !== $packet->item or (($damage = $this->inventory->getItemInHand()->getDamage()) !== $packet->meta and $damage !== null)){ $this->inventory->sendHeldItem($this); break; }else{ $item = $this->inventory->getItemInHand(); } $target = $this->level->getBlock($blockVector); $ev = new PlayerInteractEvent($this, $item, $target, $packet->face); $this->server->getPluginManager()->callEvent($ev); if($ev->isCancelled()){ $this->inventory->sendHeldItem($this); break; } if($item->getId() === Item::SNOWBALL){ $nbt = new Compound("", [ "Pos" => new Enum("Pos", [ new Double("", $this->x), new Double("", $this->y + $this->getEyeHeight()), new Double("", $this->z) ]), "Motion" => new Enum("Motion", [ new Double("", -sin($this->yaw / 180 * M_PI) * cos($this->pitch / 180 * M_PI)), new Double("", -sin($this->pitch / 180 * M_PI)), new Double("", cos($this->yaw / 180 * M_PI) * cos($this->pitch / 180 * M_PI)) ]), "Rotation" => new Enum("Rotation", [ new Float("", $this->yaw), new Float("", $this->pitch) ]), ]); $f = 1.5; $snowball = Entity::createEntity("Snowball", $this->chunk, $nbt, $this); $snowball->setMotion($snowball->getMotion()->multiply($f)); if($this->isSurvival()){ $this->inventory->removeItem(Item::get(Item::SNOWBALL, 0, 1), $this); } if($snowball instanceof Projectile){ $this->server->getPluginManager()->callEvent($projectileEv = new ProjectileLaunchEvent($snowball)); if($projectileEv->isCancelled()){ $snowball->kill(); }else{ $snowball->spawnToAll(); } }else{ $snowball->spawnToAll(); } } $this->inAction = true; $this->startAction = microtime(true); $this->sendMetadata($this->getViewers()); } break; case ProtocolInfo::PLAYER_ACTION_PACKET: if($this->spawned === false or $this->blocked === true or $this->dead === true){ break; } $this->craftingType = 0; $packet->eid = $this->id; switch($packet->action){ case 5: //Shot arrow if($this->inventory->getItemInHand()->getId() === Item::BOW){ $bow = $this->inventory->getItemInHand(); if($this->isSurvival()){ if(!$this->inventory->contains(Item::get(Item::ARROW, 0, 1))){ $this->inventory->sendContents($this); return; } } $nbt = new Compound("", [ "Pos" => new Enum("Pos", [ new Double("", $this->x), new Double("", $this->y + $this->getEyeHeight()), new Double("", $this->z) ]), "Motion" => new Enum("Motion", [ new Double("", -sin($this->yaw / 180 * M_PI) * cos($this->pitch / 180 * M_PI)), new Double("", -sin($this->pitch / 180 * M_PI)), new Double("", cos($this->yaw / 180 * M_PI) * cos($this->pitch / 180 * M_PI)) ]), "Rotation" => new Enum("Rotation", [ new Float("", $this->yaw), new Float("", $this->pitch) ]), ]); $f = 1.5; $ev = new EntityShootBowEvent($this, $bow, Entity::createEntity("Arrow", $this->chunk, $nbt, $this), $f); $this->server->getPluginManager()->callEvent($ev); if($ev->isCancelled()){ $ev->getProjectile()->kill(); }else{ $ev->getProjectile()->setMotion($ev->getProjectile()->getMotion()->multiply($ev->getForce())); if($this->isSurvival()){ $this->inventory->removeItem(Item::get(Item::ARROW, 0, 1), $this); $bow->setDamage($bow->getDamage() + 1); $this->inventory->setItemInHand($bow, $this); if($bow->getDamage() >= 385){ $this->inventory->setItemInHand(Item::get(Item::AIR, 0, 0), $this); } } if($ev->getProjectile() instanceof Projectile){ $this->server->getPluginManager()->callEvent($projectileEv = new ProjectileLaunchEvent($ev->getProjectile())); if($projectileEv->isCancelled()){ $ev->getProjectile()->kill(); }else{ $ev->getProjectile()->spawnToAll(); } }else{ $ev->getProjectile()->spawnToAll(); } } } $this->startAction = false; $this->inAction = false; $this->sendMetadata($this->getViewers()); break; case 6: //get out of the bed $this->stopSleep(); break; } break; case ProtocolInfo::REMOVE_BLOCK_PACKET: if($this->spawned === false or $this->blocked === true or $this->dead === true){ break; } $this->craftingType = 0; $vector = new Vector3($packet->x, $packet->y, $packet->z); if($this->isCreative()){ $item = $this->inventory->getItemInHand(); }else{ $item = $this->inventory->getItemInHand(); } $oldItem = clone $item; if($this->level->useBreakOn($vector, $item, $this) === true){ if($this->isSurvival()){ if(!$item->equals($oldItem, true) or $item->getCount() !== $oldItem->getCount()){ $this->inventory->setItemInHand($item, $this); $this->inventory->sendHeldItem($this->hasSpawned); } } break; } $this->inventory->sendContents($this); $target = $this->level->getBlock($vector); $tile = $this->level->getTile($vector); $pk = new UpdateBlockPacket(); $pk->x = $target->x; $pk->y = $target->y; $pk->z = $target->z; $pk->block = $target->getId(); $pk->meta = $target->getDamage(); $this->dataPacket($pk); if($tile instanceof Spawnable){ $tile->spawnTo($this); } break; case ProtocolInfo::PLAYER_ARMOR_EQUIPMENT_PACKET: break; case ProtocolInfo::INTERACT_PACKET: if($this->spawned === false or $this->dead === true or $this->blocked){ break; } $this->craftingType = 0; $target = $this->level->getEntity($packet->target); $cancelled = false; if( $target instanceof Player and $this->server->getConfigBoolean("pvp", true) === false ){ $cancelled = true; } if($target instanceof Entity and $this->getGamemode() !== Player::VIEW and $this->dead !== true and $target->dead !== true){ if($target instanceof DroppedItem or $target instanceof Arrow){ $this->kick("Attempting to attack an invalid entity"); $this->server->getLogger()->warning("Player " . $this->getName() . " tried to attack an invalid entity"); return; } $item = $this->inventory->getItemInHand(); $damageTable = [ Item::WOODEN_SWORD => 4, Item::GOLD_SWORD => 4, Item::STONE_SWORD => 5, Item::IRON_SWORD => 6, Item::DIAMOND_SWORD => 7, Item::WOODEN_AXE => 3, Item::GOLD_AXE => 3, Item::STONE_AXE => 3, Item::IRON_AXE => 5, Item::DIAMOND_AXE => 6, Item::WOODEN_PICKAXE => 2, Item::GOLD_PICKAXE => 2, Item::STONE_PICKAXE => 3, Item::IRON_PICKAXE => 4, Item::DIAMOND_PICKAXE => 5, Item::WOODEN_SHOVEL => 1, Item::GOLD_SHOVEL => 1, Item::STONE_SHOVEL => 2, Item::IRON_SHOVEL => 3, Item::DIAMOND_SHOVEL => 4, ]; $damage = [ EntityDamageEvent::MODIFIER_BASE => isset($damageTable[$item->getId()]) ? $damageTable[$item->getId()] : 1, ]; if($this->distance($target) > 8){ $cancelled = true; }elseif($target instanceof Player){ if(($target->getGamemode() & 0x01) > 0){ break; }elseif($this->server->getConfigBoolean("pvp") !== true or $this->server->getDifficulty() === 0){ $cancelled = true; } $armorValues = [ Item::LEATHER_CAP => 1, Item::LEATHER_TUNIC => 3, Item::LEATHER_PANTS => 2, Item::LEATHER_BOOTS => 1, Item::CHAIN_HELMET => 1, Item::CHAIN_CHESTPLATE => 5, Item::CHAIN_LEGGINGS => 4, Item::CHAIN_BOOTS => 1, Item::GOLD_HELMET => 1, Item::GOLD_CHESTPLATE => 5, Item::GOLD_LEGGINGS => 3, Item::GOLD_BOOTS => 1, Item::IRON_HELMET => 2, Item::IRON_CHESTPLATE => 6, Item::IRON_LEGGINGS => 5, Item::IRON_BOOTS => 2, Item::DIAMOND_HELMET => 3, Item::DIAMOND_CHESTPLATE => 8, Item::DIAMOND_LEGGINGS => 6, Item::DIAMOND_BOOTS => 3, ]; $points = 0; foreach($target->getInventory()->getArmorContents() as $index => $i){ if(isset($armorValues[$i->getId()])){ $points += $armorValues[$i->getId()]; } } $damage[EntityDamageEvent::MODIFIER_ARMOR] = -floor($damage[EntityDamageEvent::MODIFIER_BASE] * $points * 0.04); } $ev = new EntityDamageByEntityEvent($this, $target, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $damage); if($cancelled){ $ev->setCancelled(); } $target->attack($ev->getFinalDamage(), $ev); if($ev->isCancelled()){ if($item->isTool() and $this->isSurvival()){ $this->inventory->sendContents($this); } break; } if($item->isTool() and $this->isSurvival()){ if($item->useOn($target) and $item->getDamage() >= $item->getMaxDurability()){ $this->inventory->setItemInHand(Item::get(Item::AIR, 0, 1), $this); }else{ $this->inventory->setItemInHand($item, $this); } } } break; case ProtocolInfo::ANIMATE_PACKET: if($this->spawned === false or $this->dead === true){ break; } $this->server->getPluginManager()->callEvent($ev = new PlayerAnimationEvent($this, $packet->action)); if($ev->isCancelled()){ break; } $pk = new AnimatePacket(); $pk->eid = $this->getId(); $pk->action = $ev->getAnimationType(); Server::broadcastPacket($this->getViewers(), $pk); break; case ProtocolInfo::RESPAWN_PACKET: if($this->spawned === false or $this->dead === false){ break; } $this->craftingType = 0; $this->server->getPluginManager()->callEvent($ev = new PlayerRespawnEvent($this, $this->getSpawn())); $this->teleport($ev->getRespawnPosition()); $this->fireTicks = 0; $this->airTicks = 300; $this->deadTicks = 0; $this->noDamageTicks = 60; $this->setHealth(20); $this->dead = false; $this->sendMetadata($this->getViewers()); $this->sendMetadata($this); $this->sendSettings(); $this->inventory->sendContents($this); $this->inventory->sendArmorContents($this); $this->blocked = false; $this->spawnToAll(); $this->scheduleUpdate(); break; case ProtocolInfo::SET_HEALTH_PACKET: //Not used break; case ProtocolInfo::ENTITY_EVENT_PACKET: if($this->spawned === false or $this->blocked === true or $this->dead === true){ break; } $this->craftingType = 0; if($this->inAction === true){ $this->inAction = false; $this->sendMetadata($this->getViewers()); } switch($packet->event){ case 9: //Eating $items = [ Item::APPLE => 4, Item::MUSHROOM_STEW => 10, Item::BEETROOT_SOUP => 10, Item::BREAD => 5, Item::RAW_PORKCHOP => 3, Item::COOKED_PORKCHOP => 8, Item::RAW_BEEF => 3, Item::STEAK => 8, Item::COOKED_CHICKEN => 6, Item::RAW_CHICKEN => 2, Item::MELON_SLICE => 2, Item::GOLDEN_APPLE => 10, Item::PUMPKIN_PIE => 8, Item::CARROT => 4, Item::POTATO => 1, Item::BAKED_POTATO => 6, //Item::COOKIE => 2, //Item::COOKED_FISH => 5, //Item::RAW_FISH => 2, ]; $slot = $this->inventory->getItemInHand(); if($this->getHealth() < 20 and isset($items[$slot->getId()])){ $this->server->getPluginManager()->callEvent($ev = new PlayerItemConsumeEvent($this, $slot)); if($ev->isCancelled()){ $this->inventory->sendContents($this); break; } $pk = new EntityEventPacket(); $pk->eid = 0; $pk->event = 9; $this->dataPacket($pk); $pk->eid = $this->getId(); Server::broadcastPacket($this->getViewers(), $pk); $amount = $items[$slot->getId()]; $this->server->getPluginManager()->callEvent($ev = new EntityRegainHealthEvent($this, $amount, EntityRegainHealthEvent::CAUSE_EATING)); if(!$ev->isCancelled()){ $this->heal($ev->getAmount(), $ev); } --$slot->count; $this->inventory->setItemInHand($slot, $this); if($slot->getId() === Item::MUSHROOM_STEW or $slot->getId() === Item::BEETROOT_SOUP){ $this->inventory->addItem(Item::get(Item::BOWL, 0, 1), $this); } } break; } break; case ProtocolInfo::DROP_ITEM_PACKET: if($this->spawned === false or $this->blocked === true or $this->dead === true){ break; } $packet->eid = $this->id; $item = $this->inventory->getItemInHand(); $ev = new PlayerDropItemEvent($this, $item); $this->server->getPluginManager()->callEvent($ev); if($ev->isCancelled()){ $this->inventory->sendContents($this); break; } $this->inventory->setItemInHand(Item::get(Item::AIR, 0, 1), $this); $motion = $this->getDirectionVector()->multiply(0.4); $this->level->dropItem($this->add(0, 1.3, 0), $item, $motion, 40); if($this->inAction === true){ $this->inAction = false; $this->sendMetadata($this->getViewers()); } break; case ProtocolInfo::MESSAGE_PACKET: if($this->spawned === false or $this->dead === true){ break; } $this->craftingType = 0; $packet->message = TextFormat::clean($packet->message); if(trim($packet->message) != "" and strlen($packet->message) <= 255){ $message = $packet->message; $this->server->getPluginManager()->callEvent($ev = new PlayerCommandPreprocessEvent($this, $message)); if($ev->isCancelled()){ break; } if(substr($ev->getMessage(), 0, 1) === "/"){ //Command Timings::$playerCommandTimer->startTiming(); $this->server->dispatchCommand($ev->getPlayer(), substr($ev->getMessage(), 1)); Timings::$playerCommandTimer->stopTiming(); }else{ $this->server->getPluginManager()->callEvent($ev = new PlayerChatEvent($this, $ev->getMessage())); if(!$ev->isCancelled()){ $this->server->broadcastMessage(sprintf($ev->getFormat(), $ev->getPlayer()->getDisplayName(), $ev->getMessage()), $ev->getRecipients()); } } } break; case ProtocolInfo::CONTAINER_CLOSE_PACKET: if($this->spawned === false or $packet->windowid === 0){ break; } $this->craftingType = 0; $this->currentTransaction = null; if(isset($this->windowIndex[$packet->windowid])){ $this->server->getPluginManager()->callEvent(new InventoryCloseEvent($this->windowIndex[$packet->windowid], $this)); $this->removeWindow($this->windowIndex[$packet->windowid]); }else{ unset($this->windowIndex[$packet->windowid]); } break; case ProtocolInfo::CONTAINER_SET_SLOT_PACKET: if($this->spawned === false or $this->blocked === true or $this->dead === true){ break; } if($packet->slot < 0){ break; } if($packet->windowid === 0){ //Our inventory if($packet->slot >= $this->inventory->getSize()){ break; } if($this->isCreative()){ if($this->getCreativeBlock($packet->item) !== -1){ $this->inventory->setItem($packet->slot, $packet->item); $this->inventory->setHotbarSlotIndex($packet->slot, $packet->slot); //links $hotbar[$packet->slot] to $slots[$packet->slot] } } $transaction = new BaseTransaction($this->inventory, $packet->slot, $this->inventory->getItem($packet->slot), $packet->item); }elseif($packet->windowid === 0x78){ //Our armor if($packet->slot >= 4){ break; } $transaction = new BaseTransaction($this->inventory, $packet->slot + $this->inventory->getSize(), $this->inventory->getArmorItem($packet->slot), $packet->item); }elseif(isset($this->windowIndex[$packet->windowid])){ $this->craftingType = 0; $inv = $this->windowIndex[$packet->windowid]; $transaction = new BaseTransaction($inv, $packet->slot, $inv->getItem($packet->slot), $packet->item); }else{ break; } if($transaction->getSourceItem()->equals($transaction->getTargetItem(), true) and $transaction->getTargetItem()->getCount() === $transaction->getSourceItem()->getCount()){ //No changes! //No changes, just a local inventory update sent by the server break; } if($this->currentTransaction === null or $this->currentTransaction->getCreationTime() < (microtime(true) - 8)){ if($this->currentTransaction instanceof SimpleTransactionGroup){ foreach($this->currentTransaction->getInventories() as $inventory){ if($inventory instanceof PlayerInventory){ $inventory->sendArmorContents($this); } $inventory->sendContents($this); } } $this->currentTransaction = new SimpleTransactionGroup($this); } $this->currentTransaction->addTransaction($transaction); if($this->currentTransaction->canExecute()){ if($this->currentTransaction->execute()){ foreach($this->currentTransaction->getTransactions() as $ts){ $inv = $ts->getInventory(); if($inv instanceof FurnaceInventory){ if($ts->getSlot() === 2){ switch($inv->getResult()->getId()){ case Item::IRON_INGOT: $this->awardAchievement("acquireIron"); break; } } } } } $this->currentTransaction = null; }elseif($packet->windowid == 0){ //Try crafting $craftingGroup = new CraftingTransactionGroup($this->currentTransaction); if($craftingGroup->canExecute()){ //We can craft! $recipe = $craftingGroup->getMatchingRecipe(); if($recipe instanceof BigShapelessRecipe and $this->craftingType !== 1){ break; }elseif($recipe instanceof StonecutterShapelessRecipe and $this->craftingType !== 2){ break; } if($craftingGroup->execute()){ switch($craftingGroup->getResult()->getId()){ case Item::WORKBENCH: $this->awardAchievement("buildWorkBench"); break; case Item::WOODEN_PICKAXE: $this->awardAchievement("buildPickaxe"); break; case Item::FURNACE: $this->awardAchievement("buildFurnace"); break; case Item::WOODEN_HOE: $this->awardAchievement("buildHoe"); break; case Item::BREAD: $this->awardAchievement("makeBread"); break; case Item::CAKE: //TODO: detect complex recipes like cake that leave remains $this->awardAchievement("bakeCake"); $this->inventory->addItem(Item::get(Item::BUCKET, 0, 3), $this); break; case Item::STONE_PICKAXE: case Item::GOLD_PICKAXE: case Item::IRON_PICKAXE: case Item::DIAMOND_PICKAXE: $this->awardAchievement("buildBetterPickaxe"); break; case Item::WOODEN_SWORD: $this->awardAchievement("buildSword"); break; case Item::DIAMOND: $this->awardAchievement("diamond"); break; } } $this->currentTransaction = null; } } break; case ProtocolInfo::SEND_INVENTORY_PACKET: //TODO, Mojang, enable this ยด^_^` if($this->spawned === false){ break; } break; case ProtocolInfo::ENTITY_DATA_PACKET: if($this->spawned === false or $this->blocked === true or $this->dead === true){ break; } $this->craftingType = 0; $t = $this->level->getTile(new Vector3($packet->x, $packet->y, $packet->z)); if($t instanceof Sign){ $nbt = new NBT(NBT::LITTLE_ENDIAN); $nbt->read($packet->namedtag); $nbt = $nbt->getData(); if($nbt["id"] !== Tile::SIGN){ $t->spawnTo($this); }else{ $ev = new SignChangeEvent($t->getBlock(), $this, [ $nbt["Text1"], $nbt["Text2"], $nbt["Text3"], $nbt["Text4"] ]); if(!isset($t->namedtag->Creator) or $t->namedtag["Creator"] !== $this->username){ $ev->setCancelled(true); } $this->server->getPluginManager()->callEvent($ev); if(!$ev->isCancelled()){ $t->setText($ev->getLine(0), $ev->getLine(1), $ev->getLine(2), $ev->getLine(3)); }else{ $t->spawnTo($this); } } } break; default: break; } } /** * Kicks a player from the server * * @param string $reason * * @return bool */ public function kick($reason = ""){ $this->server->getPluginManager()->callEvent($ev = new PlayerKickEvent($this, $reason, TextFormat::YELLOW . $this->username . " has left the game")); if(!$ev->isCancelled()){ $message = "Kicked by admin." . ($reason !== "" ? " Reason: " . $reason : ""); $this->close($ev->getQuitMessage(), $message); return true; } return false; } /** * Sends a direct chat message to a player * * @param string $message */ public function sendMessage($message){ if($this->removeFormat !== false){ $message = TextWrapper::wrap(TextFormat::clean($message)); } $mes = explode("\n", $message); foreach($mes as $m){ if($m !== ""){ $pk = new MessagePacket(); $pk->source = ""; //Do not use this ;) $pk->message = $m; $this->dataPacket($pk); } } } /** * @param string $message Message to be broadcasted * @param string $reason Reason showed in console */ public function close($message = "", $reason = "generic reason"){ foreach($this->tasks as $task){ $task->cancel(); } $this->tasks = []; if($this->connected and !$this->closed){ if($message != ""){ $pk = new DisconnectPacket; $pk->message = $reason; $this->directDataPacket($pk); } $this->connected = false; if($this->username != ""){ $this->server->getPluginManager()->callEvent($ev = new PlayerQuitEvent($this, $message)); if($this->server->getAutoSave() and $this->loggedIn === true){ $this->save(); } } foreach($this->server->getOnlinePlayers() as $player){ if(!$player->canSee($this)){ $player->showPlayer($this); } } $this->hiddenPlayers = []; foreach($this->windowIndex as $window){ $this->removeWindow($window); } $this->interface->close($this, $reason); $chunkX = $chunkZ = null; foreach($this->usedChunks as $index => $d){ Level::getXZ($index, $chunkX, $chunkZ); $this->level->freeChunk($chunkX, $chunkZ, $this); unset($this->usedChunks[$index]); } parent::close(); $this->loggedIn = false; if(isset($ev) and $this->username != "" and $this->spawned !== false and $ev->getQuitMessage() != ""){ $this->server->broadcastMessage($ev->getQuitMessage()); } $this->server->getPluginManager()->unsubscribeFromPermission(Server::BROADCAST_CHANNEL_USERS, $this); $this->spawned = false; $this->server->getLogger()->info(TextFormat::AQUA . $this->username . TextFormat::WHITE . "[/" . $this->ip . ":" . $this->port . "] logged out due to " . str_replace(["\n", "\r"], [" ", ""], $reason)); $this->windows = new \SplObjectStorage(); $this->windowIndex = []; $this->usedChunks = []; $this->loadQueue = []; $this->hasSpawned = []; $this->spawnPosition = null; unset($this->buffer); } $this->perm->clearPermissions(); $this->server->removePlayer($this); } public function __debugInfo(){ return []; } /** * Handles player data saving */ public function save(){ if($this->closed){ throw new \InvalidStateException("Tried to save closed player"); } parent::saveNBT(); if($this->level instanceof Level){ $this->namedtag->Level = new String("Level", $this->level->getName()); if($this->spawnPosition instanceof Position and $this->spawnPosition->getLevel() instanceof Level){ $this->namedtag["SpawnLevel"] = $this->spawnPosition->getLevel()->getName(); $this->namedtag["SpawnX"] = (int) $this->spawnPosition->x; $this->namedtag["SpawnY"] = (int) $this->spawnPosition->y; $this->namedtag["SpawnZ"] = (int) $this->spawnPosition->z; } foreach($this->achievements as $achievement => $status){ $this->namedtag->Achievements[$achievement] = new Byte($achievement, $status === true ? 1 : 0); } $this->namedtag["playerGameType"] = $this->gamemode; $this->namedtag["lastPlayed"] = floor(microtime(true) * 1000); if($this->username != "" and $this->namedtag instanceof Compound){ $this->server->saveOfflinePlayerData($this->username, $this->namedtag); } } } /** * Gets the username * * @return string */ public function getName(){ return $this->username; } public function kill(){ if($this->dead === true or $this->spawned === false){ return; } $message = $this->getName() . " died"; $cause = $this->getLastDamageCause(); $ev = null; if($cause instanceof EntityDamageEvent){ $ev = $cause; $cause = $ev->getCause(); } switch($cause){ case EntityDamageEvent::CAUSE_ENTITY_ATTACK: if($ev instanceof EntityDamageByEntityEvent){ $e = $ev->getDamager(); if($e instanceof Player){ $message = $this->getName() . " was killed by " . $e->getName(); break; }elseif($e instanceof Living){ $message = $this->getName() . " was slain by " . $e->getName(); break; } } $message = $this->getName() . " was killed"; break; case EntityDamageEvent::CAUSE_PROJECTILE: if($ev instanceof EntityDamageByEntityEvent){ $e = $ev->getDamager(); if($e instanceof Living){ $message = $this->getName() . " was shot by " . $e->getName(); break; } } $message = $this->getName() . " was shot by arrow"; break; case EntityDamageEvent::CAUSE_SUICIDE: $message = $this->getName() . " died"; break; case EntityDamageEvent::CAUSE_VOID: $message = $this->getName() . " fell out of the world"; break; case EntityDamageEvent::CAUSE_FALL: if($ev instanceof EntityDamageEvent){ if($ev->getFinalDamage() > 2){ $message = $this->getName() . " fell from a high place"; break; } } $message = $this->getName() . " hit the ground too hard"; break; case EntityDamageEvent::CAUSE_SUFFOCATION: $message = $this->getName() . " suffocated in a wall"; break; case EntityDamageEvent::CAUSE_LAVA: $message = $this->getName() . " tried to swim in lava"; break; case EntityDamageEvent::CAUSE_FIRE: $message = $this->getName() . " went up in flames"; break; case EntityDamageEvent::CAUSE_FIRE_TICK: $message = $this->getName() . " burned to death"; break; case EntityDamageEvent::CAUSE_DROWNING: $message = $this->getName() . " drowned"; break; case EntityDamageEvent::CAUSE_CONTACT: $message = $this->getName() . " was pricked to death"; break; case EntityDamageEvent::CAUSE_BLOCK_EXPLOSION: case EntityDamageEvent::CAUSE_ENTITY_EXPLOSION: $message = $this->getName() . " blew up"; break; case EntityDamageEvent::CAUSE_MAGIC: case EntityDamageEvent::CAUSE_CUSTOM: default: } if($this->dead){ return; } Entity::kill(); $this->server->getPluginManager()->callEvent($ev = new PlayerDeathEvent($this, $this->getDrops(), $message)); if(!$ev->getKeepInventory()){ foreach($ev->getDrops() as $item){ $this->level->dropItem($this, $item); } if($this->inventory !== null){ $this->inventory->clearAll(); } } if($ev->getDeathMessage() != ""){ $this->server->broadcast($ev->getDeathMessage(), Server::BROADCAST_CHANNEL_USERS); } } public function setHealth($amount){ parent::setHealth($amount); if($this->spawned === true){ $pk = new SetHealthPacket(); $pk->health = $this->getHealth(); $this->dataPacket($pk); } } public function attack($damage, $source = EntityDamageEvent::CAUSE_MAGIC){ if($this->dead === true){ return; } if($this->isCreative()){ if($source instanceof EntityDamageEvent){ $cause = $source->getCause(); }else{ $cause = $source; } if( $cause !== EntityDamageEvent::CAUSE_MAGIC and $cause !== EntityDamageEvent::CAUSE_SUICIDE and $cause !== EntityDamageEvent::CAUSE_VOID ){ if($source instanceof EntityDamageEvent){ $source->setCancelled(); } return; } } parent::attack($damage, $source); if($source instanceof EntityDamageEvent and $source->isCancelled()){ return; } if($this->getLastDamageCause() === $source){ $pk = new EntityEventPacket(); $pk->eid = 0; $pk->event = 2; $this->dataPacket($pk); } } public function getData(){ //TODO $flags = 0; $flags |= $this->fireTicks > 0 ? 1 : 0; //$flags |= ($this->crouched === true ? 0b10:0) << 1; $flags |= ($this->inAction === true ? 0b10000 : 0); $d = [ 0 => ["type" => 0, "value" => $flags], 1 => ["type" => 1, "value" => $this->airTicks], 16 => ["type" => 0, "value" => 0], 17 => ["type" => 6, "value" => [0, 0, 0]], ]; if($this->sleeping instanceof Vector3){ $d[16]["value"] = 2; $d[17]["value"] = [$this->sleeping->x, $this->sleeping->y, $this->sleeping->z]; } return $d; } public function teleport(Vector3 $pos, $yaw = null, $pitch = null){ if(parent::teleport($pos, $yaw, $pitch)){ foreach($this->windowIndex as $window){ if($window === $this->inventory){ continue; } $this->removeWindow($window); } $this->airTicks = 300; $this->resetFallDistance(); $this->orderChunks(); $this->nextChunkOrderRun = 0; $this->forceMovement = new Vector3($this->x, $this->y, $this->z); $this->newPosition = null; $pk = new MovePlayerPacket(); $pk->eid = 0; $pk->x = $this->x; $pk->y = $this->y + $this->getEyeHeight(); $pk->z = $this->z; $pk->bodyYaw = $this->yaw; $pk->pitch = $this->pitch; $pk->yaw = $this->yaw; $pk->teleport = true; $this->directDataPacket($pk); } } /** * @param Inventory $inventory * * @return int */ public function getWindowId(Inventory $inventory){ if($this->windows->contains($inventory)){ return $this->windows[$inventory]; } return -1; } /** * Returns the created/existing window id * * @param Inventory $inventory * @param int $forceId * * @return int */ public function addWindow(Inventory $inventory, $forceId = null){ if($this->windows->contains($inventory)){ return $this->windows[$inventory]; } if($forceId === null){ $this->windowCnt = $cnt = max(2, ++$this->windowCnt % 99); }else{ $cnt = (int) $forceId; } $this->windowIndex[$cnt] = $inventory; $this->windows->attach($inventory, $cnt); if($inventory->open($this)){ return $cnt; }else{ $this->removeWindow($inventory); return -1; } } public function removeWindow(Inventory $inventory){ $inventory->close($this); if($this->windows->contains($inventory)){ $id = $this->windows[$inventory]; $this->windows->detach($this->windowIndex[$id]); unset($this->windowIndex[$id]); } } public function setMetadata($metadataKey, MetadataValue $metadataValue){ $this->server->getPlayerMetadata()->setMetadata($this, $metadataKey, $metadataValue); } public function getMetadata($metadataKey){ return $this->server->getPlayerMetadata()->getMetadata($this, $metadataKey); } public function hasMetadata($metadataKey){ return $this->server->getPlayerMetadata()->hasMetadata($this, $metadataKey); } public function removeMetadata($metadataKey, Plugin $plugin){ $this->server->getPlayerMetadata()->removeMetadata($this, $metadataKey, $plugin); } }