diff --git a/src/data/bedrock/CameraEaseTypeIdMap.php b/src/data/bedrock/CameraEaseTypeIdMap.php new file mode 100644 index 00000000000..93f9e7c91c5 --- /dev/null +++ b/src/data/bedrock/CameraEaseTypeIdMap.php @@ -0,0 +1,94 @@ + + */ + private array $idToEnum = []; + /** + * @var string[] + * @phpstan-var array + */ + private array $enumToId = []; + + public function __construct(){ + $this->register("linear", CameraEaseType::LINEAR()); + $this->register("spring", CameraEaseType::SPRING()); + $this->register("in_quad", CameraEaseType::IN_QUAD()); + $this->register("out_quad", CameraEaseType::OUT_QUAD()); + $this->register("in_out_quad", CameraEaseType::IN_OUT_QUAD()); + $this->register("in_cubic", CameraEaseType::IN_CUBIC()); + $this->register("out_cubic", CameraEaseType::OUT_CUBIC()); + $this->register("in_out_cubic", CameraEaseType::IN_OUT_CUBIC()); + $this->register("in_quart", CameraEaseType::IN_QUART()); + $this->register("out_quart", CameraEaseType::OUT_QUART()); + $this->register("in_out_quart", CameraEaseType::IN_OUT_QUART()); + $this->register("in_quint", CameraEaseType::IN_QUINT()); + $this->register("out_quint", CameraEaseType::OUT_QUINT()); + $this->register("in_out_quint", CameraEaseType::IN_OUT_QUINT()); + $this->register("in_sine", CameraEaseType::IN_SINE()); + $this->register("out_sine", CameraEaseType::OUT_SINE()); + $this->register("in_out_sine", CameraEaseType::IN_OUT_SINE()); + $this->register("in_expo", CameraEaseType::IN_EXPO()); + $this->register("out_expo", CameraEaseType::OUT_EXPO()); + $this->register("in_out_expo", CameraEaseType::IN_OUT_EXPO()); + $this->register("in_circ", CameraEaseType::IN_CIRC()); + $this->register("out_circ", CameraEaseType::OUT_CIRC()); + $this->register("in_out_circ", CameraEaseType::IN_OUT_CIRC()); + $this->register("in_bounce", CameraEaseType::IN_BOUNCE()); + $this->register("out_bounce", CameraEaseType::OUT_BOUNCE()); + $this->register("in_out_bounce", CameraEaseType::IN_OUT_BOUNCE()); + $this->register("in_back", CameraEaseType::IN_BACK()); + $this->register("out_back", CameraEaseType::OUT_BACK()); + $this->register("in_out_back", CameraEaseType::IN_OUT_BACK()); + $this->register("in_elastic", CameraEaseType::IN_ELASTIC()); + $this->register("out_elastic", CameraEaseType::OUT_ELASTIC()); + $this->register("in_out_elastic", CameraEaseType::IN_OUT_ELASTIC()); + } + + public function register(string $stringId, CameraEaseType $type) : void{ + $this->idToEnum[$stringId] = $type; + $this->enumToId[$type->id()] = $stringId; + } + + public function fromId(string $id) : ?CameraEaseType{ + return $this->idToEnum[$id] ?? null; + } + + public function toId(CameraEaseType $type) : string{ + if(!array_key_exists($type->id(), $this->enumToId)){ + throw new \InvalidArgumentException("Missing mapping for camere ease type " . $type->name()); + } + return $this->enumToId[$type->id()]; + } +} diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 9053dd6b632..662dcbfc962 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -33,6 +33,7 @@ use pocketmine\lang\Translatable; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; +use pocketmine\nbt\tag\ListTag; use pocketmine\nbt\tag\StringTag; use pocketmine\network\mcpe\cache\ChunkCache; use pocketmine\network\mcpe\compression\CompressBatchPromise; @@ -52,6 +53,8 @@ use pocketmine\network\mcpe\handler\SessionStartPacketHandler; use pocketmine\network\mcpe\handler\SpawnResponsePacketHandler; use pocketmine\network\mcpe\protocol\AvailableCommandsPacket; +use pocketmine\network\mcpe\protocol\CameraInstructionPacket; +use pocketmine\network\mcpe\protocol\CameraPresetsPacket; use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket; use pocketmine\network\mcpe\protocol\ClientboundPacket; use pocketmine\network\mcpe\protocol\DisconnectPacket; @@ -81,6 +84,7 @@ use pocketmine\network\mcpe\protocol\types\AbilitiesData; use pocketmine\network\mcpe\protocol\types\AbilitiesLayer; use pocketmine\network\mcpe\protocol\types\BlockPosition; +use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\network\mcpe\protocol\types\command\CommandData; use pocketmine\network\mcpe\protocol\types\command\CommandEnum; use pocketmine\network\mcpe\protocol\types\command\CommandParameter; @@ -94,6 +98,8 @@ use pocketmine\network\PacketHandlingException; use pocketmine\permission\DefaultPermissionNames; use pocketmine\permission\DefaultPermissions; +use pocketmine\player\camera\CameraPresetFactory; +use pocketmine\player\camera\instruction\CameraInstruction; use pocketmine\player\GameMode; use pocketmine\player\Player; use pocketmine\player\PlayerInfo; @@ -959,6 +965,17 @@ public function syncAdventureSettings() : void{ )); } + public function sendCameraPresets() : void { + $presetsTag = new ListTag(); + foreach (CameraPresetFactory::getInstance()->getAll() as $preset) { + $presetsTag->push($preset->toCompoundTag()); + } + + $this->sendDataPacket(CameraPresetsPacket::create( + new CacheableNbt(CompoundTag::create()->setTag("presets", $presetsTag)) + )); + } + public function syncAvailableCommands() : void{ $commandData = []; foreach($this->server->getCommandMap()->getCommands() as $name => $command){ @@ -1152,6 +1169,15 @@ public function onToastNotification(string $title, string $body) : void{ $this->sendDataPacket(ToastRequestPacket::create($title, $body)); } + public function onCameraInstruction(CameraInstruction ...$instructions) : void{ + $instructionsData = CompoundTag::create(); + foreach ($instructions as $instruction) { + $instruction->writeInstructionData($instructionsData); + } + + $this->sendDataPacket(CameraInstructionPacket::create(new CacheableNbt($instructionsData))); + } + public function onOpenSignEditor(Vector3 $signPosition, bool $frontSide) : void{ $this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide)); } diff --git a/src/network/mcpe/handler/PreSpawnPacketHandler.php b/src/network/mcpe/handler/PreSpawnPacketHandler.php index f80bacfc181..5dff939a089 100644 --- a/src/network/mcpe/handler/PreSpawnPacketHandler.php +++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php @@ -83,7 +83,7 @@ public function setUp() : void{ $levelSettings->gameRules = [ "naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration ]; - $levelSettings->experiments = new Experiments([], false); + $levelSettings->experiments = new Experiments(["cameras" => true], true); $this->session->sendDataPacket(StartGamePacket::create( $this->player->getId(), @@ -129,6 +129,9 @@ public function setUp() : void{ $this->session->syncAbilities($this->player); $this->session->syncAdventureSettings(); + $this->session->getLogger()->debug("Sending camera presets"); + $this->session->sendCameraPresets(); + $this->session->getLogger()->debug("Sending effects"); foreach($this->player->getEffects()->all() as $effect){ $this->session->getEntityEventBroadcaster()->onEntityEffectAdded([$this->session], $this->player, $effect, false); diff --git a/src/network/mcpe/handler/ResourcePacksPacketHandler.php b/src/network/mcpe/handler/ResourcePacksPacketHandler.php index 7438fe47c1a..cc131516d54 100644 --- a/src/network/mcpe/handler/ResourcePacksPacketHandler.php +++ b/src/network/mcpe/handler/ResourcePacksPacketHandler.php @@ -134,7 +134,7 @@ public function handleResourcePackClientResponse(ResourcePackClientResponsePacke //we don't force here, because it doesn't have user-facing effects //but it does have an annoying side-effect when true: it makes //the client remove its own non-server-supplied resource packs. - $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [], false, ProtocolInfo::MINECRAFT_VERSION_NETWORK, new Experiments([], false))); + $this->session->sendDataPacket(ResourcePackStackPacket::create($stack, [], false, ProtocolInfo::MINECRAFT_VERSION_NETWORK, new Experiments(["cameras" => true], true))); $this->session->getLogger()->debug("Applying resource pack stack"); break; case ResourcePackClientResponsePacket::STATUS_COMPLETED: diff --git a/src/player/Player.php b/src/player/Player.php index 3083538be66..06d2fcda885 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -115,6 +115,7 @@ use pocketmine\permission\DefaultPermissions; use pocketmine\permission\PermissibleBase; use pocketmine\permission\PermissibleDelegateTrait; +use pocketmine\player\camera\instruction\CameraInstruction; use pocketmine\player\chat\StandardChatFormatter; use pocketmine\Server; use pocketmine\timings\Timings; @@ -2095,6 +2096,10 @@ public function sendToastNotification(string $title, string $body) : void{ $this->getNetworkSession()->onToastNotification($title, $body); } + public function sendCameraInstruction(CameraInstruction ...$instructions) : void{ + $this->getNetworkSession()->onCameraInstruction(...$instructions); + } + /** * Sends a Form to the player, or queue to send it if a form is already open. * diff --git a/src/player/camera/CameraEaseType.php b/src/player/camera/CameraEaseType.php new file mode 100644 index 00000000000..cb2ccfbea11 --- /dev/null +++ b/src/player/camera/CameraEaseType.php @@ -0,0 +1,106 @@ +identifier; + } + + public function getInheritFrom() : string{ + return $this->inheritFrom; + } + + public function getCameraState() : ?CameraState{ + return $this->state; + } + + public function toCompoundTag() : CompoundTag{ + $tag = CompoundTag::create() + ->setString(self::TAG_IDENTIFIER, $this->identifier) + ->setString(self::TAG_INHERIT_FROM, $this->inheritFrom); + + if ($this->state !== null) { + if (($position = $this->state->getPosition()) !== null) { + $tag->setFloat(self::TAG_POSITION_X, $position->x) + ->setFloat(self::TAG_POSITION_Y, $position->y) + ->setFloat(self::TAG_POSITION_Z, $position->z); + } + if (($yaw = $this->state->getYaw()) !== null && ($pitch = $this->state->getPitch()) !== null) { + $tag->setFloat(self::TAG_ROTATION_YAW, $yaw) + ->setFloat(self::TAG_ROTATION_PITCH, $pitch); + } + } + + return $tag; + } + +} diff --git a/src/player/camera/CameraPresetFactory.php b/src/player/camera/CameraPresetFactory.php new file mode 100644 index 00000000000..fb3b49efa15 --- /dev/null +++ b/src/player/camera/CameraPresetFactory.php @@ -0,0 +1,104 @@ + + */ + private array $presets = []; + + /** + * @var int[] + * @phpstan-var array + */ + private array $runtimeIds = []; + + private int $nextRuntimeId = 0; + + public function __construct(){ + $this->register(new CameraPreset("minecraft:first_person")); + $this->register(new CameraPreset("minecraft:third_person")); + $this->register(new CameraPreset("minecraft:third_person_front")); + $this->register(new CameraPreset("minecraft:free", "", new CameraState(Vector3::zero(), 0, 0))); + } + + /** + * Registers a new camera preset type into the index. + * + * @throws \InvalidArgumentException + */ + public function register(CameraPreset $preset) : void{ + $id = $preset->getIdentifier(); + if ($this->isRegistered($id)) { + throw new \InvalidArgumentException("A presset with id \"$id\" is already registered"); + } + + $inheritFrom = $preset->getInheritFrom(); + if ($inheritFrom !== "" && !$this->isRegistered($inheritFrom)) { + throw new \InvalidArgumentException("Parent \"$inheritFrom\" preset is not registered"); + } + + $this->presets[$id] = $preset; + $this->runtimeIds[$id] = $this->nextRuntimeId++; + } + + public function fromId(string $identifier) : CameraPreset{ + if (!$this->isRegistered($identifier)) { + throw new \InvalidArgumentException("\"$identifier\" is not registered"); + } + + return $this->presets[$identifier]; + } + + public function getRuntimeId(string $identifier) : int{ + if (!$this->isRegistered($identifier)) { + throw new \InvalidArgumentException("\"$identifier\" is not registered"); + } + + return $this->runtimeIds[$identifier]; + } + + /** + * @return CameraPreset[] + * @phpstan-return array + */ + public function getAll() : array{ + return $this->presets; + } + + public function isRegistered(string $identifier) : bool{ + return isset($this->presets[$identifier]); + } +} diff --git a/src/player/camera/VanillaCameraPresets.php b/src/player/camera/VanillaCameraPresets.php new file mode 100644 index 00000000000..ce9ddd14cc2 --- /dev/null +++ b/src/player/camera/VanillaCameraPresets.php @@ -0,0 +1,69 @@ + + */ + public static function getAll() : array{ + //phpstan doesn't support generic traits yet :( + /** @var CameraPreset[] $result */ + $result = self::_registryGetAll(); + return $result; + } + + protected static function setup() : void{ + $factory = CameraPresetFactory::getInstance(); + + self::register("first_person", $factory->fromId("minecraft:first_person")); + self::register("third_person", $factory->fromId("minecraft:third_person")); + self::register("third_person_front", $factory->fromId("minecraft:third_person_front")); + self::register("free", $factory->fromId("minecraft:free")); + } +} diff --git a/src/player/camera/element/CameraEase.php b/src/player/camera/element/CameraEase.php new file mode 100644 index 00000000000..fd506b8a612 --- /dev/null +++ b/src/player/camera/element/CameraEase.php @@ -0,0 +1,43 @@ +type; + } + + public function getDuration() : float{ + return $this->duration; + } +} diff --git a/src/player/camera/element/CameraElement.php b/src/player/camera/element/CameraElement.php new file mode 100644 index 00000000000..a195b7dcec0 --- /dev/null +++ b/src/player/camera/element/CameraElement.php @@ -0,0 +1,30 @@ +asVector3(), $location->yaw, $location->pitch); + } + + public static function lookingAt(Vector3 $position, Vector3 $target) : self{ + $horizontal = sqrt(($target->x - $position->x) ** 2 + ($target->z - $position->z) ** 2); + $vertical = $target->y - $position->y; + $pitch = -atan2($vertical, $horizontal) / M_PI * 180; //negative is up, positive is down + + $xDist = $target->x - $position->x; + $zDist = $target->z - $position->z; + + $yaw = atan2($zDist, $xDist) / M_PI * 180 - 90; + if($yaw < 0){ + $yaw += 360.0; + } + + return new self($position, $yaw, $pitch); + } + + public function getPosition() : ?Vector3{ + return $this->position; + } + + public function getYaw() : ?float{ + return $this->yaw; + } + + public function getPitch() : ?float{ + return $this->pitch; + } +} diff --git a/src/player/camera/instruction/CameraInstruction.php b/src/player/camera/instruction/CameraInstruction.php new file mode 100644 index 00000000000..e793c66e3d3 --- /dev/null +++ b/src/player/camera/instruction/CameraInstruction.php @@ -0,0 +1,37 @@ +setByte(self::TAG_CLEAR, 1); + } +} diff --git a/src/player/camera/instruction/FadeCameraInstruction.php b/src/player/camera/instruction/FadeCameraInstruction.php new file mode 100644 index 00000000000..19a8816ba23 --- /dev/null +++ b/src/player/camera/instruction/FadeCameraInstruction.php @@ -0,0 +1,72 @@ +color !== null) { + $fadeTag->setTag(self::TAG_COLOR, CompoundTag::create() + ->setFloat(self::TAG_COLOR_R, $this->color->getR() / 255) + ->setFloat(self::TAG_COLOR_G, $this->color->getG() / 255) + ->setFloat(self::TAG_COLOR_B, $this->color->getB() / 255) + //doesn't support alpha (opacity) :( + ); + } + + $fadeTag->setTag(self::TAG_TIME, CompoundTag::create() + ->setFloat(self::TAG_FADE_IN, $this->fadeInSeconds) + ->setFloat(self::TAG_HOLD, $this->holdSeconds) + ->setFloat(self::TAG_FADE_OUT, $this->fadeOutSeconds) + ); + + $tag->setTag(self::TAG_FADE, $fadeTag); + } +} diff --git a/src/player/camera/instruction/SetCameraInstruction.php b/src/player/camera/instruction/SetCameraInstruction.php new file mode 100644 index 00000000000..260932211e4 --- /dev/null +++ b/src/player/camera/instruction/SetCameraInstruction.php @@ -0,0 +1,90 @@ + + + private const TAG_ROTATION = "rot"; //TAG_Compound + private const TAG_ROTATION_YAW = "y"; //TAG_Float + private const TAG_ROTATION_PITCH = "x"; //TAG_Float + + private const TAG_EASE = "ease"; //TAG_Compound + private const TAG_EASE_TYPE = "type"; //TAG_String + private const TAG_EASE_DURATION = "time"; //TAG_Float + + public function __construct( + private CameraPreset $preset, + private ?CameraState $state = null, + private ?CameraEase $ease = null + ) { + } + + public function writeInstructionData(CompoundTag $tag) : void{ + $setTag = CompoundTag::create(); + + if ($this->state !== null) { + if (($position = $this->state->getPosition()) !== null) { + $setTag->setTag(self::TAG_POSITION, CompoundTag::create() //why use double position tag? mojang... + ->setTag(self::TAG_POSITION, new ListTag([ + new FloatTag($position->x), + new FloatTag($position->y), + new FloatTag($position->z) + ])) + ); + } + if (($yaw = $this->state->getYaw()) !== null && ($pitch = $this->state->getPitch()) !== null) { + $setTag->setTag(self::TAG_ROTATION, CompoundTag::create() + ->setFloat(self::TAG_ROTATION_YAW, $yaw) + ->setFloat(self::TAG_ROTATION_PITCH, $pitch) + ); + } + } + + if ($this->ease !== null) { + $setTag->setTag(self::TAG_EASE, CompoundTag::create() + ->setFloat(self::TAG_EASE_DURATION, $this->ease->getDuration()) + ->setString(self::TAG_EASE_TYPE, CameraEaseTypeIdMap::getInstance()->toId($this->ease->getType())) + ); + } + + $setTag->setInt(self::TAG_PRESET, CameraPresetFactory::getInstance()->getRuntimeId($this->preset->getIdentifier())); + + $tag->setTag(self::TAG_SET, $setTag); + } +}