88 public const MOTION_THRESHOLD = 0.00001;
89 protected const STEP_CLIP_MULTIPLIER = 0.4;
91 private const TAG_FIRE =
"Fire";
92 private const TAG_ON_GROUND =
"OnGround";
93 private const TAG_FALL_DISTANCE =
"FallDistance";
94 private const TAG_CUSTOM_NAME =
"CustomName";
95 private const TAG_CUSTOM_NAME_VISIBLE =
"CustomNameVisible";
96 public const TAG_POS =
"Pos";
97 public const TAG_MOTION =
"Motion";
98 public const TAG_ROTATION =
"Rotation";
100 private static int $entityCount = 1;
106 return self::$entityCount++;
113 protected array $hasSpawned = [];
122 protected ?array $blocksAround =
null;
128 protected bool $forceMovementUpdate =
false;
129 private bool $checkBlockIntersectionsNextTick =
true;
132 public bool $onGround =
false;
136 private float $health = 20.0;
137 private int $maxHealth = 20;
139 protected float $ySize = 0.0;
140 protected float $stepHeight = 0.0;
141 public bool $keepMovement =
false;
143 public float $fallDistance = 0.0;
144 public int $ticksLived = 0;
145 public int $lastUpdate;
146 protected int $fireTicks = 0;
148 private bool $savedWithChunk =
true;
150 public bool $isCollided =
false;
151 public bool $isCollidedHorizontally =
false;
152 public bool $isCollidedVertically =
false;
154 public int $noDamageTicks = 0;
155 protected bool $justCreated =
true;
159 protected float $gravity;
160 protected float $drag;
161 protected bool $gravityEnabled =
true;
165 protected bool $closed =
false;
166 private bool $closeInFlight =
false;
167 private bool $needsDespawn =
false;
171 protected bool $networkPropertiesDirty =
false;
173 protected string $nameTag =
"";
174 protected bool $nameTagVisible =
true;
175 protected bool $alwaysShowNameTag =
false;
176 protected string $scoreTag =
"";
177 protected float $scale = 1.0;
179 protected bool $canClimb =
false;
180 protected bool $canClimbWalls =
false;
181 protected bool $noClientPredictions =
false;
182 protected bool $invisible =
false;
183 protected bool $silent =
false;
185 protected ?
int $ownerId =
null;
186 protected ?
int $targetId =
null;
188 private bool $constructorCalled =
false;
191 if($this->constructorCalled){
192 throw new \LogicException(
"Attempted to call constructor for an Entity multiple times");
194 $this->constructorCalled =
true;
195 Utils::checkLocationNotInfOrNaN($location);
197 $this->timings = Timings::getEntityTimings($this);
199 $this->size = $this->getInitialSizeInfo();
200 $this->drag = $this->getInitialDragMultiplier();
201 $this->gravity = $this->getInitialGravity();
203 $this->
id = self::nextRuntimeId();
208 $this->boundingBox =
new AxisAlignedBB(0, 0, 0, 0, 0, 0);
209 $this->recalculateBoundingBox();
214 $this->motion = Vector3::zero();
217 $this->resetLastMovements();
219 $this->networkProperties =
new EntityMetadataCollection();
221 $this->attributeMap =
new AttributeMap();
222 $this->addAttributes();
224 $this->initEntity($nbt ??
new CompoundTag());
226 $this->getWorld()->addEntity($this);
228 $this->lastUpdate = $this->
server->getTick();
230 $this->scheduleUpdate();
233 abstract protected function getInitialSizeInfo() : EntitySizeInfo;
250 public function getNameTag() : string{
251 return $this->nameTag;
254 public function isNameTagVisible() : bool{
255 return $this->nameTagVisible;
258 public function isNameTagAlwaysVisible() : bool{
259 return $this->alwaysShowNameTag;
270 public function setNameTag(
string $name) : void{
271 $this->nameTag = $name;
272 $this->networkPropertiesDirty =
true;
275 public function setNameTagVisible(
bool $value =
true) : void{
276 $this->nameTagVisible = $value;
277 $this->networkPropertiesDirty =
true;
280 public function setNameTagAlwaysVisible(
bool $value =
true) : void{
281 $this->alwaysShowNameTag = $value;
282 $this->networkPropertiesDirty =
true;
285 public function getScoreTag() : ?string{
286 return $this->scoreTag;
289 public function setScoreTag(
string $score) : void{
290 $this->scoreTag = $score;
291 $this->networkPropertiesDirty =
true;
294 public function getScale() : float{
298 public function setScale(
float $value) : void{
300 throw new \InvalidArgumentException(
"Scale must be greater than 0");
302 $this->scale = $value;
303 $this->setSize($this->getInitialSizeInfo()->scale($value));
306 public function getBoundingBox() : AxisAlignedBB{
307 return $this->boundingBox;
310 protected function recalculateBoundingBox() : void{
311 $halfWidth = $this->size->getWidth() / 2;
313 $this->boundingBox =
new AxisAlignedBB(
314 $this->location->x - $halfWidth,
315 $this->location->y + $this->ySize,
316 $this->location->z - $halfWidth,
317 $this->location->x + $halfWidth,
318 $this->location->y + $this->size->getHeight() + $this->ySize,
319 $this->location->z + $halfWidth
323 public function getSize() : EntitySizeInfo{
327 protected function setSize(EntitySizeInfo $size) : void{
329 $this->recalculateBoundingBox();
330 $this->networkPropertiesDirty =
true;
338 return $this->noClientPredictions;
350 $this->noClientPredictions = $value;
351 $this->networkPropertiesDirty =
true;
354 public function isInvisible() : bool{
355 return $this->invisible;
358 public function setInvisible(
bool $value =
true) : void{
359 $this->invisible = $value;
360 $this->networkPropertiesDirty =
true;
363 public function isSilent() : bool{
364 return $this->silent;
367 public function setSilent(
bool $value =
true) : void{
368 $this->silent = $value;
369 $this->networkPropertiesDirty =
true;
376 return $this->canClimb;
383 $this->canClimb = $value;
384 $this->networkPropertiesDirty =
true;
391 return $this->canClimbWalls;
398 $this->canClimbWalls = $value;
399 $this->networkPropertiesDirty =
true;
406 return $this->ownerId;
413 return $this->ownerId !== null ? $this->
server->getWorldManager()->findEntity($this->ownerId) : null;
423 $this->ownerId =
null;
424 }elseif($owner->closed){
425 throw new \InvalidArgumentException(
"Supplied owning entity is garbage and cannot be used");
427 $this->ownerId = $owner->getId();
429 $this->networkPropertiesDirty =
true;
436 return $this->targetId;
444 return $this->targetId !== null ? $this->
server->getWorldManager()->findEntity($this->targetId) : null;
453 if($target === null){
454 $this->targetId =
null;
455 }elseif($target->closed){
456 throw new \InvalidArgumentException(
"Supplied target entity is garbage and cannot be used");
458 $this->targetId = $target->getId();
460 $this->networkPropertiesDirty =
true;
467 return $this->savedWithChunk;
475 $this->savedWithChunk = $value;
480 ->setTag(self::TAG_POS, new
ListTag([
485 ->setTag(self::TAG_MOTION, new
ListTag([
490 ->setTag(self::TAG_ROTATION, new
ListTag([
492 new
FloatTag($this->location->pitch)
495 if(!($this instanceof
Player)){
496 EntityFactory::getInstance()->injectSaveId(get_class($this), $nbt);
498 if($this->getNameTag() !==
""){
499 $nbt->setString(self::TAG_CUSTOM_NAME, $this->getNameTag());
500 $nbt->setByte(self::TAG_CUSTOM_NAME_VISIBLE, $this->isNameTagVisible() ? 1 : 0);
504 $nbt->
setFloat(self::TAG_FALL_DISTANCE, $this->fallDistance);
505 $nbt->setShort(self::TAG_FIRE, $this->fireTicks);
506 $nbt->setByte(self::TAG_ON_GROUND, $this->onGround ? 1 : 0);
508 $nbt->setLong(VersionInfo::TAG_WORLD_DATA_VERSION, VersionInfo::WORLD_DATA_VERSION);
513 protected function initEntity(CompoundTag $nbt) : void{
514 $this->fireTicks = $nbt->getShort(self::TAG_FIRE, 0);
516 $this->onGround = $nbt->getByte(self::TAG_ON_GROUND, 0) !== 0;
518 $this->fallDistance = $nbt->getFloat(self::TAG_FALL_DISTANCE, 0.0);
520 if(($customNameTag = $nbt->getTag(self::TAG_CUSTOM_NAME)) instanceof StringTag){
521 $this->setNameTag($customNameTag->getValue());
523 if(($customNameVisibleTag = $nbt->getTag(self::TAG_CUSTOM_NAME_VISIBLE)) instanceof StringTag){
525 $this->setNameTagVisible($customNameVisibleTag->getValue() !==
"");
527 $this->setNameTagVisible($nbt->getByte(self::TAG_CUSTOM_NAME_VISIBLE, 1) !== 0);
532 protected function addAttributes() : void{
536 public function attack(EntityDamageEvent $source) : void{
537 if($this->isFireProof() && (
538 $source->getCause() === EntityDamageEvent::CAUSE_FIRE ||
539 $source->getCause() === EntityDamageEvent::CAUSE_FIRE_TICK ||
540 $source->getCause() === EntityDamageEvent::CAUSE_LAVA
546 if($source->isCancelled()){
550 $this->setLastDamageCause($source);
552 $this->setHealth($this->getHealth() - $source->getFinalDamage());
555 public function heal(EntityRegainHealthEvent $source) : void{
557 if($source->isCancelled()){
561 $this->setHealth($this->getHealth() + $source->getAmount());
564 public function kill() : void{
565 if($this->isAlive()){
568 $this->scheduleUpdate();
582 protected function onDeathUpdate(int $tickDiff) : bool{
586 public function isAlive() : bool{
587 return $this->health > 0;
590 public function getHealth() : float{
591 return $this->health;
598 if($amount === $this->health){
603 if($this->isAlive()){
604 if(!$this->justCreated){
610 }elseif($amount <= $this->getMaxHealth() || $amount < $this->health){
611 $this->health = $amount;
613 $this->health = $this->getMaxHealth();
617 public function getMaxHealth() : int{
618 return $this->maxHealth;
621 public function setMaxHealth(
int $amount) : void{
622 $this->maxHealth = $amount;
625 public function setLastDamageCause(EntityDamageEvent $type) : void{
626 $this->lastDamageCause = $type;
629 public function getLastDamageCause() : ?EntityDamageEvent{
630 return $this->lastDamageCause;
633 public function getAttributeMap() : AttributeMap{
634 return $this->attributeMap;
637 public function getNetworkProperties() : EntityMetadataCollection{
638 return $this->networkProperties;
641 protected function entityBaseTick(
int $tickDiff = 1) : bool{
644 if($this->justCreated){
645 $this->justCreated =
false;
646 if(!$this->isAlive()){
651 $changedProperties = $this->getDirtyNetworkData();
652 if(count($changedProperties) > 0){
653 $this->sendData(
null, $changedProperties);
654 $this->networkProperties->clearDirtyProperties();
659 if($this->checkBlockIntersectionsNextTick){
660 $this->checkBlockIntersections();
662 $this->checkBlockIntersectionsNextTick =
true;
664 if($this->location->y <= World::Y_MIN - 16 && $this->isAlive()){
665 $ev =
new EntityDamageEvent($this, EntityDamageEvent::CAUSE_VOID, 10);
670 if($this->isOnFire() && $this->doOnFireTick($tickDiff)){
674 if($this->noDamageTicks > 0){
675 $this->noDamageTicks -= $tickDiff;
676 if($this->noDamageTicks < 0){
677 $this->noDamageTicks = 0;
681 $this->ticksLived += $tickDiff;
686 public function isOnFire() : bool{
687 return $this->fireTicks > 0;
690 public function setOnFire(
int $seconds) : void{
691 $ticks = $seconds * 20;
692 if($ticks > $this->getFireTicks()){
693 $this->setFireTicks($ticks);
695 $this->networkPropertiesDirty =
true;
698 public function getFireTicks() : int{
699 return $this->fireTicks;
707 throw new \InvalidArgumentException(
"Fire ticks cannot be negative");
714 $fireTicks = min($fireTicks, Limits::INT16_MAX);
716 if(!$this->isFireProof()){
717 $this->fireTicks = $fireTicks;
718 $this->networkPropertiesDirty =
true;
722 public function extinguish(
int $cause = EntityExtinguishEvent::CAUSE_CUSTOM) : void{
723 $ev = new EntityExtinguishEvent($this, $cause);
726 $this->fireTicks = 0;
727 $this->networkPropertiesDirty =
true;
730 public function isFireProof() : bool{
734 protected function doOnFireTick(
int $tickDiff = 1) : bool{
735 if($this->isFireProof() && $this->isOnFire()){
736 $this->extinguish(EntityExtinguishEvent::CAUSE_FIRE_PROOF);
740 $this->fireTicks -= $tickDiff;
742 if(($this->fireTicks % 20 === 0) || $tickDiff > 20){
743 $this->dealFireDamage();
746 if(!$this->isOnFire()){
747 $this->extinguish(EntityExtinguishEvent::CAUSE_TICKING);
763 public function canCollideWith(
Entity $entity) : bool{
764 return !$this->justCreated && $entity !== $this;
767 public function canBeCollidedWith() : bool{
768 return $this->isAlive();
771 protected function updateMovement(
bool $teleport =
false) : void{
772 $diffPosition = $this->location->distanceSquared($this->lastLocation);
773 $diffRotation = ($this->location->yaw - $this->lastLocation->yaw) ** 2 + ($this->location->pitch - $this->lastLocation->pitch) ** 2;
775 $diffMotion = $this->motion->subtractVector($this->lastMotion)->lengthSquared();
777 $still = $this->motion->lengthSquared() === 0.0;
778 $wasStill = $this->lastMotion->lengthSquared() === 0.0;
779 if($wasStill !== $still){
781 $this->setNoClientPredictions($still);
784 if($teleport || $diffPosition > 0.0001 || $diffRotation > 1.0 || (!$wasStill && $still)){
785 $this->lastLocation = $this->location->asLocation();
787 $this->broadcastMovement($teleport);
790 if($diffMotion > 0.0025 || $wasStill !== $still){
791 $this->lastMotion = clone $this->motion;
793 $this->broadcastMotion();
797 public function getOffsetPosition(Vector3 $vector3) : Vector3{
801 protected function broadcastMovement(
bool $teleport =
false) : void{
802 NetworkBroadcastUtils::broadcastPackets($this->hasSpawned, [MoveActorAbsolutePacket::create(
804 $this->getOffsetPosition($this->location),
805 $this->location->pitch,
806 $this->location->yaw,
807 $this->location->yaw,
814 ($this->onGround ? MoveActorAbsolutePacket::FLAG_GROUND : 0)
819 protected function broadcastMotion() : void{
820 NetworkBroadcastUtils::broadcastPackets($this->hasSpawned, [SetActorMotionPacket::create($this->id, $this->getMotion(), tick: 0)]);
823 public function getGravity() : float{
824 return $this->gravity;
827 public function setGravity(
float $gravity) : void{
828 Utils::checkFloatNotInfOrNaN(
"gravity", $gravity);
829 $this->gravity = $gravity;
832 public function hasGravity() : bool{
833 return $this->gravityEnabled;
836 public function setHasGravity(
bool $v =
true) : void{
837 $this->gravityEnabled = $v;
840 protected function applyDragBeforeGravity() : bool{
844 protected function tryChangeMovement() : void{
845 $friction = 1 - $this->drag;
847 $mY = $this->motion->y;
849 if($this->applyDragBeforeGravity()){
853 if($this->gravityEnabled){
854 $mY -= $this->gravity;
857 if(!$this->applyDragBeforeGravity()){
862 $friction *= $this->getWorld()->getBlockAt((
int) floor($this->location->x), (
int) floor($this->location->y - 1), (
int) floor($this->location->z))->getFrictionFactor();
865 $this->motion =
new Vector3($this->motion->x * $friction, $mY, $this->motion->z * $friction);
868 protected function checkObstruction(
float $x,
float $y,
float $z) : bool{
869 $world = $this->getWorld();
870 if(count($world->getBlockCollisionBoxes($this->boundingBox)) === 0){
874 $floorX = (
int) floor($x);
875 $floorY = (int) floor($y);
876 $floorZ = (int) floor($z);
878 $diffX = $x - $floorX;
879 $diffY = $y - $floorY;
880 $diffZ = $z - $floorZ;
882 if($world->getBlockAt($floorX, $floorY, $floorZ)->isSolid()){
883 $westNonSolid = !$world->getBlockAt($floorX - 1, $floorY, $floorZ)->isSolid();
884 $eastNonSolid = !$world->getBlockAt($floorX + 1, $floorY, $floorZ)->isSolid();
885 $downNonSolid = !$world->getBlockAt($floorX, $floorY - 1, $floorZ)->isSolid();
886 $upNonSolid = !$world->getBlockAt($floorX, $floorY + 1, $floorZ)->isSolid();
887 $northNonSolid = !$world->getBlockAt($floorX, $floorY, $floorZ - 1)->isSolid();
888 $southNonSolid = !$world->getBlockAt($floorX, $floorY, $floorZ + 1)->isSolid();
895 $direction = Facing::WEST;
898 if($eastNonSolid && 1 - $diffX < $limit){
900 $direction = Facing::EAST;
903 if($downNonSolid && $diffY < $limit){
905 $direction = Facing::DOWN;
908 if($upNonSolid && 1 - $diffY < $limit){
910 $direction = Facing::UP;
913 if($northNonSolid && $diffZ < $limit){
915 $direction = Facing::NORTH;
918 if($southNonSolid && 1 - $diffZ < $limit){
919 $direction = Facing::SOUTH;
922 if($direction === -1){
926 $force = Utils::getRandomFloat() * 0.2 + 0.1;
928 $this->motion = match($direction){
929 Facing::WEST => $this->motion->withComponents(-$force,
null,
null),
930 Facing::EAST => $this->motion->withComponents($force,
null,
null),
931 Facing::DOWN => $this->motion->withComponents(
null, -$force,
null),
932 Facing::UP => $this->motion->withComponents(
null, $force,
null),
933 Facing::NORTH => $this->motion->withComponents(
null,
null, -$force),
934 Facing::SOUTH => $this->motion->withComponents(
null,
null, $force),
942 public function getHorizontalFacing() : int{
943 $angle = fmod($this->location->yaw, 360);
948 if((0 <= $angle && $angle < 45) || (315 <= $angle && $angle < 360)){
949 return Facing::SOUTH;
951 if(45 <= $angle && $angle < 135){
954 if(135 <= $angle && $angle < 225){
955 return Facing::NORTH;
961 public function getDirectionVector() : Vector3{
962 $y = -sin(deg2rad($this->location->pitch));
963 $xz = cos(deg2rad($this->location->pitch));
964 $x = -$xz * sin(deg2rad($this->location->yaw));
965 $z = $xz * cos(deg2rad($this->location->yaw));
967 return (
new Vector3($x, $y, $z))->normalize();
970 public function getDirectionPlane() : Vector2{
971 return (new Vector2(-cos(deg2rad($this->location->yaw) - M_PI_2), -sin(deg2rad($this->location->yaw) - M_PI_2)))->normalize();
982 public function onUpdate(
int $currentTick) : bool{
987 $tickDiff = $currentTick - $this->lastUpdate;
989 if(!$this->justCreated){
990 $this->
server->getLogger()->debug(
"Expected tick difference of at least 1, got $tickDiff for " . get_class($this));
996 $this->lastUpdate = $currentTick;
998 if($this->justCreated){
999 $this->onFirstUpdate($currentTick);
1002 if(!$this->isAlive()){
1003 if($this->onDeathUpdate($tickDiff)){
1004 $this->flagForDespawn();
1010 $this->timings->startTiming();
1012 if($this->hasMovementUpdate()){
1013 $this->tryChangeMovement();
1015 $this->motion = $this->motion->withComponents(
1016 abs($this->motion->x) <= self::MOTION_THRESHOLD ? 0 :
null,
1017 abs($this->motion->y) <= self::MOTION_THRESHOLD ? 0 :
null,
1018 abs($this->motion->z) <= self::MOTION_THRESHOLD ? 0 :
null
1021 if(floatval($this->motion->x) !== 0.0 || floatval($this->motion->y) !== 0.0 || floatval($this->motion->z) !== 0.0){
1022 $this->move($this->motion->x, $this->motion->y, $this->motion->z);
1025 $this->forceMovementUpdate =
false;
1028 $this->updateMovement();
1030 Timings::$entityBaseTick->startTiming();
1031 $hasUpdate = $this->entityBaseTick($tickDiff);
1032 Timings::$entityBaseTick->stopTiming();
1034 $this->timings->stopTiming();
1036 return ($hasUpdate || $this->hasMovementUpdate());
1039 final public function scheduleUpdate() : void{
1041 throw new \LogicException(
"Cannot schedule update on garbage entity " . get_class($this));
1043 $this->getWorld()->updateEntities[$this->id] = $this;
1046 public function onNearbyBlockChange() : void{
1047 $this->setForceMovementUpdate();
1048 $this->scheduleUpdate();
1056 $this->scheduleUpdate();
1064 $this->forceMovementUpdate = $value;
1066 $this->blocksAround =
null;
1074 $this->forceMovementUpdate ||
1075 floatval($this->motion->x) !== 0.0 ||
1076 floatval($this->motion->y) !== 0.0 ||
1077 floatval($this->motion->z) !== 0.0 ||
1082 public function getFallDistance() : float{ return $this->fallDistance; }
1084 public function setFallDistance(
float $fallDistance) : void{
1085 $this->fallDistance = $fallDistance;
1088 public function resetFallDistance() : void{
1089 $this->fallDistance = 0.0;
1092 protected function updateFallState(
float $distanceThisTick,
bool $onGround) : ?float{
1093 if($distanceThisTick < $this->fallDistance){
1096 $this->fallDistance -= $distanceThisTick;
1100 $this->fallDistance = 0;
1102 if($onGround && $this->fallDistance > 0){
1103 $newVerticalVelocity = $this->onHitGround();
1104 $this->resetFallDistance();
1105 return $newVerticalVelocity;
1117 public function getEyeHeight() : float{
1118 return $this->size->getEyeHeight();
1121 public function getEyePos() : Vector3{
1122 return new Vector3($this->location->x, $this->location->y + $this->getEyeHeight(), $this->location->z);
1125 public function onCollideWithPlayer(Player $player) : void{
1136 public function isUnderwater() : bool{
1137 $block = $this->getWorld()->getBlockAt((int) floor($this->location->x), $blockY = (int) floor($y = ($this->location->y + $this->getEyeHeight())), (int) floor($this->location->z));
1139 if($block instanceof
Water){
1140 $f = ($blockY + 1) - ($block->getFluidHeightPercent() - 0.1111111);
1147 public function isInsideOfSolid() : bool{
1148 $block = $this->getWorld()->getBlockAt((int) floor($this->location->x), (int) floor($y = ($this->location->y + $this->getEyeHeight())), (int) floor($this->location->z));
1150 return $block->isSolid() && !$block->isTransparent() && $block->collidesWithBB($this->getBoundingBox());
1153 protected function move(
float $dx,
float $dy,
float $dz) : void{
1154 $this->blocksAround = null;
1156 Timings::$entityMove->startTiming();
1157 Timings::$entityMoveCollision->startTiming();
1163 if($this->keepMovement){
1164 $this->boundingBox->offset($dx, $dy, $dz);
1166 $this->ySize *= self::STEP_CLIP_MULTIPLIER;
1168 $moveBB = clone $this->boundingBox;
1170 assert(abs($dx) <= 20 && abs($dy) <= 20 && abs($dz) <= 20,
"Movement distance is excessive: dx=$dx, dy=$dy, dz=$dz");
1172 $list = $this->getWorld()->getBlockCollisionBoxes($moveBB->addCoord($dx, $dy, $dz));
1174 foreach($list as $bb){
1175 $dy = $bb->calculateYOffset($moveBB, $dy);
1178 $moveBB->offset(0, $dy, 0);
1180 $fallingFlag = ($this->onGround || ($dy !== $wantedY && $wantedY < 0));
1182 foreach($list as $bb){
1183 $dx = $bb->calculateXOffset($moveBB, $dx);
1186 $moveBB->offset($dx, 0, 0);
1188 foreach($list as $bb){
1189 $dz = $bb->calculateZOffset($moveBB, $dz);
1192 $moveBB->offset(0, 0, $dz);
1194 if($this->stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){
1199 $dy = $this->stepHeight;
1202 $stepBB = clone $this->boundingBox;
1204 $list = $this->getWorld()->getBlockCollisionBoxes($stepBB->addCoord($dx, $dy, $dz));
1205 foreach($list as $bb){
1206 $dy = $bb->calculateYOffset($stepBB, $dy);
1209 $stepBB->offset(0, $dy, 0);
1211 foreach($list as $bb){
1212 $dx = $bb->calculateXOffset($stepBB, $dx);
1215 $stepBB->offset($dx, 0, 0);
1217 foreach($list as $bb){
1218 $dz = $bb->calculateZOffset($stepBB, $dz);
1221 $stepBB->offset(0, 0, $dz);
1224 foreach($list as $bb){
1225 $reverseDY = $bb->calculateYOffset($stepBB, $reverseDY);
1228 $stepBB->offset(0, $reverseDY, 0);
1230 if(($cx ** 2 + $cz ** 2) >= ($dx ** 2 + $dz ** 2)){
1236 $this->ySize += $dy;
1240 $this->boundingBox = $moveBB;
1242 Timings::$entityMoveCollision->stopTiming();
1244 $this->location =
new Location(
1245 ($this->boundingBox->minX + $this->boundingBox->maxX) / 2,
1246 $this->boundingBox->minY - $this->ySize,
1247 ($this->boundingBox->minZ + $this->boundingBox->maxZ) / 2,
1248 $this->location->world,
1249 $this->location->yaw,
1250 $this->location->pitch
1253 $this->getWorld()->onEntityMoved($this);
1254 $this->checkBlockIntersections();
1255 $this->checkGroundState($wantedX, $wantedY, $wantedZ, $dx, $dy, $dz);
1256 $postFallVerticalVelocity = $this->updateFallState($dy, $this->onGround);
1258 $this->motion = $this->motion->withComponents(
1259 $wantedX !== $dx ? 0 :
null,
1260 $postFallVerticalVelocity ?? ($wantedY !== $dy ? 0 :
null),
1261 $wantedZ !== $dz ? 0 :
null
1266 Timings::$entityMove->stopTiming();
1269 protected function checkGroundState(
float $wantedX,
float $wantedY,
float $wantedZ,
float $dx,
float $dy,
float $dz) : void{
1270 $this->isCollidedVertically = $wantedY !== $dy;
1271 $this->isCollidedHorizontally = ($wantedX !== $dx || $wantedZ !== $dz);
1272 $this->isCollided = ($this->isCollidedHorizontally || $this->isCollidedVertically);
1273 $this->onGround = ($wantedY !== $dy && $wantedY < 0);
1282 $minX = (int) floor($this->boundingBox->minX + $inset);
1283 $minY = (int) floor($this->boundingBox->minY + $inset);
1284 $minZ = (int) floor($this->boundingBox->minZ + $inset);
1285 $maxX = (int) floor($this->boundingBox->maxX - $inset);
1286 $maxY = (int) floor($this->boundingBox->maxY - $inset);
1287 $maxZ = (int) floor($this->boundingBox->maxZ - $inset);
1289 $world = $this->getWorld();
1291 for($z = $minZ; $z <= $maxZ; ++$z){
1292 for($x = $minX; $x <= $maxX; ++$x){
1293 for($y = $minY; $y <= $maxY; ++$y){
1294 yield $world->getBlockAt($x, $y, $z);
1304 if($this->blocksAround === null){
1305 $this->blocksAround = [];
1308 foreach($this->getBlocksIntersected($inset) as $block){
1309 if($block->hasEntityCollision()){
1310 $this->blocksAround[] = $block;
1315 return $this->blocksAround;
1325 protected function checkBlockIntersections() : void{
1326 $this->checkBlockIntersectionsNextTick = false;
1329 foreach($this->getBlocksAroundWithEntityInsideActions() as $block){
1330 if(!$block->onEntityInside($this)){
1331 $this->blocksAround =
null;
1333 if(($v = $block->addVelocityToEntity($this)) !==
null){
1338 if(count($vectors) > 0){
1339 $vector = Vector3::sum(...$vectors);
1340 if($vector->lengthSquared() > 0){
1342 $this->motion = $this->motion->addVector($vector->normalize()->multiply($d));
1348 return $this->location->asPosition();
1351 public function getLocation() : Location{
1352 return $this->location->asLocation();
1355 public function getWorld() : World{
1356 return $this->location->getWorld();
1359 protected function setPosition(Vector3 $pos) : bool{
1364 $oldWorld = $this->getWorld();
1365 $newWorld = $pos instanceof Position ? $pos->getWorld() : $oldWorld;
1366 if($oldWorld !== $newWorld){
1367 $this->despawnFromAll();
1368 $oldWorld->removeEntity($this);
1371 $this->location = Location::fromObject(
1374 $this->location->yaw,
1375 $this->location->pitch
1378 $this->recalculateBoundingBox();
1380 $this->blocksAround =
null;
1382 if($oldWorld !== $newWorld){
1383 $newWorld->addEntity($this);
1385 $newWorld->onEntityMoved($this);
1391 public function setRotation(
float $yaw,
float $pitch) : void{
1392 Utils::checkFloatNotInfOrNaN(
"yaw", $yaw);
1393 Utils::checkFloatNotInfOrNaN(
"pitch", $pitch);
1394 $this->location->yaw = $yaw;
1395 $this->location->pitch = $pitch;
1396 $this->scheduleUpdate();
1399 protected function setPositionAndRotation(Vector3 $pos,
float $yaw,
float $pitch) : bool{
1400 if($this->setPosition($pos)){
1401 $this->setRotation($yaw, $pitch);
1409 protected function resetLastMovements() : void{
1410 $this->lastLocation = $this->location->asLocation();
1411 $this->lastMotion = clone $this->motion;
1414 public function getMotion() : Vector3{
1415 return clone $this->motion;
1418 public function setMotion(Vector3 $motion) : bool{
1419 Utils::checkVector3NotInfOrNaN($motion);
1420 if(!$this->justCreated){
1421 $ev =
new EntityMotionEvent($this, $motion);
1423 if($ev->isCancelled()){
1428 $this->motion = clone $motion;
1430 if(!$this->justCreated){
1431 $this->updateMovement();
1440 public function addMotion(
float $x,
float $y,
float $z) : void{
1441 Utils::checkFloatNotInfOrNaN(
"x", $x);
1442 Utils::checkFloatNotInfOrNaN(
"y", $y);
1443 Utils::checkFloatNotInfOrNaN(
"z", $z);
1444 $this->motion = $this->motion->add($x, $y, $z);
1447 public function isOnGround() : bool{
1448 return $this->onGround;
1455 Utils::checkVector3NotInfOrNaN($pos);
1457 $yaw = $yaw ?? $pos->yaw;
1458 $pitch = $pitch ?? $pos->pitch;
1461 Utils::checkFloatNotInfOrNaN(
"yaw", $yaw);
1463 if($pitch !==
null){
1464 Utils::checkFloatNotInfOrNaN(
"pitch", $pitch);
1467 $from = $this->location->asPosition();
1468 $to = Position::fromObject($pos, $pos instanceof Position ? $pos->getWorld() : $this->getWorld());
1469 $ev =
new EntityTeleportEvent($this, $from, $to);
1471 if($ev->isCancelled()){
1475 $pos = $ev->getTo();
1477 $this->setMotion(
new Vector3(0, 0, 0));
1478 if($this->setPositionAndRotation($pos, $yaw ?? $this->location->yaw, $pitch ?? $this->location->pitch)){
1479 $this->resetFallDistance();
1480 $this->setForceMovementUpdate();
1482 $this->updateMovement(
true);
1490 public function getId() : int{
1498 return $this->hasSpawned;
1501 abstract public function getNetworkTypeId() : string;
1507 $player->getNetworkSession()->sendDataPacket(
AddActorPacket::create(
1510 $this->getNetworkTypeId(),
1511 $this->getOffsetPosition($this->location->asVector3()),
1513 $this->location->pitch,
1514 $this->location->yaw,
1515 $this->location->yaw,
1516 $this->location->yaw,
1517 array_map(function(
Attribute $attr) : NetworkAttribute{
1518 return new NetworkAttribute($attr->getId(), $attr->getMinValue(), $attr->getMaxValue(), $attr->getValue(), $attr->getDefaultValue(), []);
1519 }, $this->attributeMap->getAll()),
1520 $this->getAllNetworkData(),
1526 public function spawnTo(
Player $player) : void{
1527 $id = spl_object_id($player);
1531 if(!isset($this->hasSpawned[$id]) && $player->getWorld() === $this->getWorld() && $player->
hasReceivedChunk($this->location->getFloorX() >> Chunk::COORD_BIT_SIZE, $this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
1532 $this->hasSpawned[$id] = $player;
1534 $this->sendSpawnPacket($player);
1538 public function spawnToAll() : void{
1542 foreach($this->getWorld()->getViewersForPosition($this->location) as $player){
1543 $this->spawnTo($player);
1547 public function respawnToAll() : void{
1548 foreach($this->hasSpawned as $key => $player){
1549 unset($this->hasSpawned[$key]);
1550 $this->spawnTo($player);
1559 $id = spl_object_id($player);
1560 if(isset($this->hasSpawned[$id])){
1562 $player->getNetworkSession()->getEntityEventBroadcaster()->onEntityRemoved([$player->getNetworkSession()], $this);
1564 unset($this->hasSpawned[$id]);
1575 fn(
EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onEntityRemoved($recipients, $this)
1577 $this->hasSpawned = [];
1591 $this->needsDespawn = true;
1592 $this->scheduleUpdate();
1595 public function isFlaggedForDespawn() : bool{
1596 return $this->needsDespawn;
1603 return $this->closed;
1612 if($this->closeInFlight){
1617 $this->closeInFlight =
true;
1621 $this->closed =
true;
1622 $this->destroyCycles();
1623 $this->closeInFlight =
false;
1632 $this->despawnFromAll();
1633 if($this->location->isValid()){
1634 $this->getWorld()->removeEntity($this);
1645 $this->lastDamageCause = null;
1654 public function sendData(?array $targets, ?array $data =
null) : void{
1655 $targets = $targets ?? $this->hasSpawned;
1656 $data = $data ?? $this->getAllNetworkData();
1658 NetworkBroadcastUtils::broadcastEntityEvent($targets, fn(
EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->
syncActorData($recipients, $this, $data));
1666 if($this->networkPropertiesDirty){
1667 $this->syncNetworkData($this->networkProperties);
1668 $this->networkPropertiesDirty =
false;
1670 return $this->networkProperties->getDirty();
1678 if($this->networkPropertiesDirty){
1679 $this->syncNetworkData($this->networkProperties);
1680 $this->networkPropertiesDirty =
false;
1682 return $this->networkProperties->getAll();
1685 protected function syncNetworkData(EntityMetadataCollection $properties) : void{
1686 $properties->setByte(EntityMetadataProperties::ALWAYS_SHOW_NAMETAG, $this->alwaysShowNameTag ? 1 : 0);
1687 $properties->setFloat(EntityMetadataProperties::BOUNDING_BOX_HEIGHT, $this->size->getHeight() / $this->scale);
1688 $properties->setFloat(EntityMetadataProperties::BOUNDING_BOX_WIDTH, $this->size->getWidth() / $this->scale);
1689 $properties->setFloat(EntityMetadataProperties::SCALE, $this->scale);
1690 $properties->setLong(EntityMetadataProperties::LEAD_HOLDER_EID, -1);
1691 $properties->setLong(EntityMetadataProperties::OWNER_EID, $this->ownerId ?? -1);
1692 $properties->setLong(EntityMetadataProperties::TARGET_EID, $this->targetId ?? 0);
1693 $properties->setString(EntityMetadataProperties::NAMETAG, $this->nameTag);
1694 $properties->setString(EntityMetadataProperties::SCORE_TAG, $this->scoreTag);
1695 $properties->setByte(EntityMetadataProperties::COLOR, 0);
1697 $properties->setGenericFlag(EntityMetadataFlags::AFFECTED_BY_GRAVITY, $this->gravityEnabled);
1698 $properties->setGenericFlag(EntityMetadataFlags::CAN_CLIMB, $this->canClimb);
1699 $properties->setGenericFlag(EntityMetadataFlags::CAN_SHOW_NAMETAG, $this->nameTagVisible);
1700 $properties->setGenericFlag(EntityMetadataFlags::HAS_COLLISION,
true);
1701 $properties->setGenericFlag(EntityMetadataFlags::NO_AI, $this->noClientPredictions);
1702 $properties->setGenericFlag(EntityMetadataFlags::INVISIBLE, $this->invisible);
1703 $properties->setGenericFlag(EntityMetadataFlags::SILENT, $this->silent);
1704 $properties->setGenericFlag(EntityMetadataFlags::ONFIRE, $this->isOnFire());
1705 $properties->setGenericFlag(EntityMetadataFlags::WALLCLIMBING, $this->canClimbWalls);
1721 $this->getWorld()->addSound($this->location->asVector3(), $sound, $targets ?? $this->getViewers());
1725 public function __destruct(){
1729 public function __toString(){
1730 return (
new \ReflectionClass($this))->getShortName() .
"(" . $this->getId() .
")";