diff --git a/README.md b/README.md
index 685f06d..7b11780 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,14 @@ To send instructions to the camera, use the following code:
- Set
+
+ See demo
+
+https://github.com/user-attachments/assets/338e4b84-1ded-4424-9ffb-6e94298bc53c
+
+
+
+
```php
use muqsit\libcamera\libcamera;
use muqsit\libcamera\CameraInstruction;
@@ -58,6 +66,7 @@ use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEaseType;
use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionRotation;
use pocketmine\network\mcpe\protocol\types\camera\Vector3;
use pocketmine\player\Player;
+use muqsit\libcamera\CameraPresetRegistry;
// ...
if($player instanceof Player && $player->isOnline()){
@@ -69,15 +78,15 @@ if($player instanceof Player && $player->isOnline()){
* @phpstan-param Vector3|null $facing_pos
*/
CameraInstruction::set(
- preset: libcamera::getPresetRegistry()->registered["target"],
+ preset: CameraPresetRegistry::TARGET(),
ease: new CameraSetInstructionEase(
CameraSetInstructionEaseType::IN_OUT_CUBIC,
(float) 5.0 // duration (sec)
),
- camera_pos: null,
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0), //Without it, the camera will teleport into subspace
rot: new CameraSetInstructionRotation(
- (float)20.0, //pitch
- (float)180.0 //yaw
+ (float)20, //pitch
+ (float)180 //yaw
),
facing_pos: null
)->send($player);
@@ -86,6 +95,13 @@ if($player instanceof Player && $player->isOnline()){
- Fade
+
+ See demo
+
+https://github.com/user-attachments/assets/01bfc489-16bd-4424-aad0-32abb81d7517
+
+
+
```php
use muqsit\libcamera\libcamera;
use muqsit\libcamera\CameraInstruction;
@@ -95,6 +111,13 @@ use pocketmine\player\Player;
// ...
if($player instanceof Player && $player->isOnline()){
+ $fadeInTime = 5;
+ $stayTime = 2;
+ $fadeOutTime = 2;
+ $r = 0;
+ $g = 0;
+ $b = 0;
+
/**
* @phpstan-param CameraFadeInstructionColor|null $color
* @phpstan-param CameraFadeInstructionTime|null $time
@@ -108,24 +131,85 @@ if($player instanceof Player && $player->isOnline()){
- Target
+After setting the camera to free mode, you need to explicitly assign a target.
+This allows the camera to visually track a specific entity.
+Unlike SetActorLinkPacket, it does not follow the entity automatically.
+
+
+ See demo
+
+https://github.com/user-attachments/assets/38cd6bf1-f666-4635-870b-2d51b12bfa3f
+
+
+
```php
-use muqsit\libcamera\libcamera;
+use pocketmine\event\player\PlayerInteractEvent;
use muqsit\libcamera\CameraInstruction;
-use pocketmine\network\mcpe\protocol\types\camera\CameraTargetInstruction;
+use pocketmine\entity\Zombie;
+use muqsit\libcamera\libcamera;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEase;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEaseType;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionRotation;
use pocketmine\math\Vector3;
-use pocketmine\player\Player;
-
+use muqsit\libcamera\CameraPresetRegistry;
// ...
-if($player instanceof Player && $player->isOnline()){
- /**
- * @phpstan-param Vector3|null $targetCenterOffset
- * @phpstan-param int $actorUniqueId
- */
- CameraInstruction::target(
- targetCenterOffset: Vector3::zero(), // no offset
- actorUniqueId: $player->getId() // for example target the player
- )->send($player);
-}
+
+ /** @var array */
+ private $set = [];
+
+ public function ina(PlayerInteractEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+
+ //Removes camera tracking. Note that target and free cameras are managed separately.
+ if(isset($this->set[$player->getName()])){
+ CameraInstruction::removeTarget()->send($player);
+ CameraInstruction::clear()->send($player);
+ unset($this->set[$player->getName()]);
+ return;
+ }
+
+ //Find the most different zombie entities
+ $nearest = null;
+ $nearestDistance = PHP_INT_MAX;
+ foreach($player->getWorld()->getEntities() as $entity){
+ if($entity instanceof Zombie){
+ $distance = $player->getPosition()->distance($entity->getPosition());
+ if($nearestDistance >= $distance){
+ $nearest = $entity;
+ $nearestDistance = $distance;
+ }
+ }
+ }
+
+ if($nearest === null){
+ $player->sendMessage("No Zombie");
+ return;
+ }
+
+ //
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::TARGET(),
+ ease: null,
+ camera_pos: $player->getPosition()->add(0, $player->getEyeHeight(), 0),
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos: null
+ )->send($player);
+
+ //To use CameraInstruction::target you first need to make it a free camera.
+ CameraInstruction::target(
+ targetCenterOffset: Vector3::zero(), // no offset
+ actorUniqueId: $nearest->getId() // for example target the player
+ )->send($player);
+
+ //Manages which packets have been sent
+ $this->set[$player->getName()] = true;
+ }
```
- Remove Target
@@ -156,6 +240,8 @@ if($player instanceof Player && $player->isOnline()){
- Multi
+This doesn't work
+
```php
use muqsit\libcamera\libcamera;
use muqsit\libcamera\CameraInstruction;
@@ -179,12 +265,1268 @@ if($player instanceof Player && $player->isOnline()){
}
```
+- registerCustom Preset
+
+First, build a custom preset registry
+CustomCameraPresetRegistry.php
+```php
+ */
+ public static function getAll() : array{
+ return self::_registryGetAll();
+ }
+
+ protected static function setup() : void{
+ self::register("CUSTOM_THIRD_PERSON_FRONT", new CameraPreset(
+ name: "minecraft:test",
+ parent: "minecraft:third_person_front",
+ xPosition: null,
+ yPosition: null,
+ zPosition: null,
+ pitch: null,
+ yaw: null,
+ rotationSpeed: null,
+ snapToTarget: null,
+ horizontalRotationLimit: null,
+ verticalRotationLimit: null,
+ continueTargeting: null,
+ blockListeningRadius: null,
+ viewOffset: null,
+ entityOffset: null,
+ radius: null,
+ yawLimitMin: null,
+ yawLimitMax: null,
+ audioListenerType: CameraPreset::AUDIO_LISTENER_TYPE_CAMERA,
+ playerEffects: false,
+ aimAssist: null,
+ controlScheme: null
+ ));
+ }
+}
+```
+
+Next, register the camera preset in the library.
+
+```php
+use muqsit\libcamera\libcamera;
+
+// ...
+
+libcamera::registerPreset(CustomCameraPresetRegistry::CUSTOM_THIRD_PERSON_FRONT());
+```
+
+Third, use `CustomCameraPresetRegistry::CUSTOM_THIRD_PERSON_FRONT()` directly.
+
+```php
+use muqsit\libcamera\libcamera;
+use muqsit\libcamera\CameraInstruction;
+use pocketmine\network\mcpe\protocol\types\camera\CameraPreset;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEase;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEaseType;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionRotation;
+use pocketmine\network\mcpe\protocol\types\camera\Vector3;
+use pocketmine\player\Player;
+use muqsit\libcamera\CameraPresetRegistry;
+
+// ...
+if($player instanceof Player && $player->isOnline()){
+ /**
+ * @phpstan-param CameraPreset $preset
+ * @phpstan-param CameraSetInstructionEase|null $ease
+ * @phpstan-param Vector3|null $camera_pos
+ * @phpstan-param CameraSetInstructionRotation|null $rot
+ * @phpstan-param Vector3|null $facing_pos
+ */
+ CameraInstruction::set(
+ preset: CustomCameraPresetRegistry::CUSTOM_FREE(),
+ ease: new CameraSetInstructionEase(
+ CameraSetInstructionEaseType::IN_OUT_CUBIC,
+ (float) 5.0 // duration (sec)
+ ),
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float)20.0, //pitch
+ (float)180.0 //yaw
+ ),
+ facing_pos: null
+ )->send($player);
+}
+```
+
+# camera technic
+
+- use
+
+```php
+use pocketmine\event\player\PlayerItemUseEvent;
+use muqsit\libcamera\CameraInstruction;
+use muqsit\libcamera\libcamera;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEase;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionEaseType;
+use pocketmine\network\mcpe\protocol\types\camera\CameraSetInstructionRotation;
+use pocketmine\player\Player;
+use pocketmine\scheduler\ClosureTask;
+use pocketmine\entity\Zombie;
+use muqsit\libcamera\CameraPresetRegistry;
+```
+
+# Eases
+
+Usage Example in Minecraft:
+When using minecraft:free with an ease parameter, you can move the free camera smoothly to a specified endpoint over a given duration.
+The easing functions listed above determine how the camera moves.
+Here, "in" means the start point and "out" means the end point.
+
+Reference:
+https://bacchigames.club/mc/howtocamera.html
+
+> [!WARNING]
+> These demos may stimulate the vestibular system and cause symptoms such as **dizziness or a sensation of head movement (headshake)** that could last for several hours.
+> Please proceed only if you are aware of the risks and feel well enough to continue.
+
+
+| Easing Name | Behavior Description | See Demo | IsCrash |
+|----------------|----------------------------------------------------------------|-----------------------------|---------|
+| in_back | Moves slightly backward before heading to the endpoint | [see](#in_back-demo) | ❌ |
+| out_back | Slightly overshoots the endpoint and returns | [see](#out_back-demo) | ✅ |
+| in_out_back | Combines both in and out behaviors | [see](#in_out_back-demo) | ✅ |
+| in_bounce | Bounces 3 times before heading to the endpoint on the 4th time | [see](#in_bounce-demo) | ❌ |
+| out_bounce | Bounces 3 times and stops at the endpoint on the 4th time | [see](#out_bounce-demo) | ❌ |
+| in_out_bounce | Combines both in and out bounce behaviors | [see](#in_out_bounce-demo) | ❌ |
+| in_circ | Accelerates toward the endpoint | [see](#in_circ-demo) | ❌ |
+| out_circ | Decelerates toward the endpoint | [see](#out_circ-demo) | ❌ |
+| in_out_circ | Combines both in and out circ behaviors | [see](#in_out_circ-demo) | ❌ |
+| in_cubic | Accelerates toward the endpoint | [see](#in_cubic-demo) | ❌ |
+| out_cubic | Decelerates toward the endpoint | [see](#out_cubic-demo) | ❌ |
+| in_out_cubic | Combines both in and out cubic behaviors | [see](#in_out_cubic-demo) | ❌ |
+| in_elastic | Oscillates 3 times and heads to the endpoint on the 4th time | [see](#in_elastic-demo) | ❌ |
+| out_elastic | Oscillates 3 times and stops at the endpoint on the 4th time | [see](#out_elastic-demo) | ✅ |
+| in_out_elastic | Combines both in and out elastic behaviors | [see](#in_out_elastic-demo) | ✅ |
+| in_expo | Accelerates toward the endpoint | [see](#in_expo-demo) | ❌ |
+| out_expo | Decelerates toward the endpoint | [see](#out_expo-demo) | ❌ |
+| in_out_expo | Combines both in and out expo behaviors | [see](#in_out_expo-demo) | ❌ |
+| in_quad | Accelerates toward the endpoint | [see](#in_quad-demo) | ❌ |
+| out_quad | Decelerates toward the endpoint | [see](#out_quad-demo) | ❌ |
+| in_out_quad | Combines both in and out quad behaviors | [see](#in_out_quad-demo) | ❌ |
+| in_quart | Accelerates toward the endpoint | [see](#in_quart-demo) | ❌ |
+| out_quart | Decelerates toward the endpoint | [see](#out_quart-demo) | ❌ |
+| in_out_quart | Combines both in and out quart behaviors | [see](#in_out_quart-demo) | ❌ |
+| in_quint | Accelerates toward the endpoint | [see](#in_quint-demo) | ❌ |
+| out_quint | Decelerates toward the endpoint | [see](#out_quint-demo) | ❌ |
+| in_out_quint | Combines both in and out quint behaviors | [see](#in_out_quint-demo) | ❌ |
+| in_sine | Accelerates toward the endpoint | [see](#in_sine-demo) | ❌ |
+| out_sine | Decelerates toward the endpoint | [see](#out_sine-demo) | ❌ |
+| in_out_sine | Combines both in and out sine behaviors | [see](#in_out_sine-demo) | ❌ |
+| linear | Moves from start to end at constant speed | [see](#linear-demo) | ❌ |
+| spring | Slightly oscillates around the endpoint | [see](#spring-demo) | ✅ |
+
+
+
+## facing_pos
+
+Do you want to move while looking at a point? Use facing_pos!
+
+
+ See demo
+
+https://github.com/user-attachments/assets/1f5d73c2-073a-4777-8b13-8ee6c6badefb
+
+
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ if($player instanceof Player&&$player->isOnline()){
+ //linear
+
+ //Find the most different zombie entities
+ $nearest = null;
+ $nearestDistance = PHP_INT_MAX;
+ foreach($player->getWorld()->getEntities() as $entity){
+ if($entity instanceof Zombie){
+ $distance = $player->getPosition()->distance($entity->getPosition());
+ if($nearestDistance >= $distance){
+ $nearest = $entity;
+ $nearestDistance = $distance;
+ }
+ }
+ }
+
+ if($nearest === null){
+ $player->sendMessage("No Zombie");
+ return;
+ }
+
+
+ $do = $player->getDirectionVector()->multiply(10);
+
+ CameraInstruction::multi(
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::FREE(),
+ ease: null,
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos: $nearest->getLocation()->asVector3()->add(0, $nearest->getEyeHeight(), 0),
+ ),
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::FREE(),
+ ease: new CameraSetInstructionEase(
+ CameraSetInstructionEaseType::LINEAR,
+ (float) 7.0 // duration (sec)
+ ),
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0)->addVector($do), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos: $nearest->getLocation()->asVector3()->add(0, $nearest->getEyeHeight(), 0),
+ )
+ )->send($player);
+
+ $this->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use ($player){
+ CameraInstruction::clear()->send($player);
+ }), 20 * 7);
+ }
+ }
+```
+
+# demos
+
+For convenience, we will use this common code for the demo.
+
+```php
+ public function onDemo(Entity $player, int $easeType) : void{
+ if($player instanceof Player&&$player->isOnline()){
+ //Find the most different zombie entities
+ $nearest = null;
+ $nearestDistance = PHP_INT_MAX;
+ foreach($player->getWorld()->getEntities() as $entity){
+ if($entity instanceof Zombie){
+ $distance = $player->getPosition()->distance($entity->getPosition());
+ if($nearestDistance >= $distance){
+ $nearest = $entity;
+ $nearestDistance = $distance;
+ }
+ }
+ }
+
+ if($nearest === null){
+ $player->sendMessage("No Zombie");
+ return;
+ }
+
+ $player->setInvisible(true);
+ $nearest->despawnFrom($player);
+
+ CameraInstruction::multi(
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::FREE(),
+ ease: null,
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos:null,
+ ),
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::FREE(),
+ ease: new CameraSetInstructionEase(
+ $easeType,
+ (float) 10.0 // duration (sec)
+ ),
+ camera_pos: $nearest->getPosition()->add(0.0, $player->getEyeHeight(), 0.0), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos: null,
+ )
+ )->send($player);
+
+ $this->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use ($nearest, $player){
+ CameraInstruction::clear()->send($player);
+ $player->setInvisible(false);
+ $nearest->despawnFrom($player);
+ }), 20 * 10);
+ }
+ }
+```
+
+
+## in_back-demo
+
+
+ see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_BACK);
+}
+```
+
+
+
+
+
+
+ See demo
+
+https://github.com/user-attachments/assets/67924c3f-f7c8-4216-b11f-943bf5149de9
+
+
+
+
+
+## out_back-demo
+
+
+### WARNING: ⚠️ Crashes client on 1.21.93
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_BACK);
+}
+```
+
+
+
+
+
+
+
+
+## in_out_back-demo
+
+### WARNING: ⚠️ Crashes client on 1.21.93
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_BACK);
+}
+```
+
+
+
+
+
+
+
+
+
+
+## in_bounce-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_BOUNCE);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/4f55e402-e27f-4e20-897a-3fd199561498
+
+
+
+
+
+## out_bounce-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_BOUNCE);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/365555eb-6193-4121-ac71-b300b857edae
+
+
+
+
+
+## in_out_bounce-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_BOUNCE);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/a4a8f132-d31f-4187-9769-b5ea544a5d56
+
+
+
+
+
+## in_circ-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_CIRC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/8edb854e-ad07-47d6-ab08-792954209ffe
+
+
+
+
+
+## out_circ-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_CIRC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/73c3bae0-1026-49be-855f-fdf53120bb55
+
+
+
+
+
+## in_out_circ-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_CIRC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/76626fef-d97a-4ed7-aa8a-d54a069c073c
+
+
+
+
+
+## in_cubic-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_CUBIC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/1ec8c85b-2a92-4cf2-9f3c-6b4aa8b150db
+
+
+
+
+
+## out_cubic-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_CUBIC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/02d67db4-d6c8-4062-b1f1-9b81f0e99012
+
+
+
+
+
+## in_out_cubic-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_CUBIC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/002867af-8f93-4ca7-b34b-5ab0a9a17855
+
+
+
+
+
+## in_quart-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_QUART);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/01342b32-bd33-4bd3-a0e2-a332914fcd3d
+
+
+
+
+
+
+
+## out_quart-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_QUART);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/77c0d077-da10-41c1-a010-3f4aa62a984e
+
+
+
+
+
+## in_out_quart-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_QUART);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/cc72e54d-38fd-4921-89ea-48cc5c5d1f44
+
+
+
+
+## in_quint-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_QUINT);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/a50bb0d6-770f-4910-be82-05abda073c61
+
+
+
+
+
+## out_quint-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_QUINT);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/f921aaa7-e170-4624-964f-b54c9a58b3c2
+
+
+
+
+
+## in_out_quint-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_QUINT);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/db433b41-6912-49e2-af7c-7f556d5b38f4
+
+
+
+
+
+## in_sine-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_SINE);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/758fcb54-db91-4d83-baa6-b62c724a7ae3
+
+
+
+
+
+## out_sine-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_SINE);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/e686b103-274c-4d98-ba18-feccfa563d97
+
+
+
+
+
+## in_out_sine-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_SINE);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/c32a1034-eec0-4d2c-8a20-33bd03060d88
+
+
+
+
+
+## in_expo-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_EXPO);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/5ae64606-c404-4136-a78b-bee052ba0300
+
+
+
+
+
+## out_expo-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_EXPO);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/97958704-f087-4894-9888-360e9cf9e758
+
+
+
+
+
+## in_out_expo-demo
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_EXPO);
+ }
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/d2b91349-08b2-4273-91e2-f60d14d76423
+
+
+
+
+## linear-demo
+
+Want to move the camera freely? Use ease!
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ if($player instanceof Player&&$player->isOnline()){
+ //linear
+
+ $do = $player->getDirectionVector()->multiply(10);
+
+ CameraInstruction::multi(
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::FREE(),
+ ease: null,
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos: null
+ ),
+ CameraInstruction::set(
+ preset: CameraPresetRegistry::FREE(),
+ ease: new CameraSetInstructionEase(
+ CameraSetInstructionEaseType::LINEAR,
+ (float) 7.0 // duration (sec)
+ ),
+ camera_pos: $player->getPosition()->add(0.0, $player->getEyeHeight(), 0.0)->addVector($do), //Without it, the camera will teleport into subspace
+ rot: new CameraSetInstructionRotation(
+ (float) $player->getLocation()->getPitch(), //pitch
+ (float) $player->getLocation()->getYaw() //yaw
+ ),
+ facing_pos: null
+ )
+ )->send($player);
+
+ $this->getScheduler()->scheduleDelayedTask(new ClosureTask(function() use ($player){
+ CameraInstruction::clear()->send($player);
+ }), 20 * 7);
+ }
+}
+```
+
+
+
+
+ See demo
+
+https://github.com/user-attachments/assets/d5ca8d67-1ac6-4d2c-8051-db3455317cd6
+
+
+
+## spring-demo
+
+### WARNING: ⚠️ Crashes client on 1.21.93
+
+
+
+see Code
+
+```php
+ public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::SPRING);
+ }
+```
+
+
+
+
+
+## in_quad-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_QUAD);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/ccecf930-6c4e-46c8-b009-c88578d0668e
+
+
+
+
+
+## out_quad-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_QUAD);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/21d17561-ebb5-44fa-a95e-c4382504f834
+
+
+
+
+
+## in_out_quad-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_QUAD);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/76d050d3-6c5e-47f3-9ca8-5386e3e17f00
+
+
+
+
+
+## in_out_elastic-demo
+
+### WARNING: ⚠️ Crashes client on 1.21.93
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_OUT_ELASTIC);
+}
+```
+
+
+
+
+
+## out_elastic-demo
+
+### WARNING: ⚠️ Crashes client on 1.21.93
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::OUT_ELASTIC);
+}
+```
+
+
+
+
+
+## in_elastic-demo
+
+
+
+see Code
+
+```php
+public function onUse(PlayerItemUseEvent $event) : void{
+ $player = $event->getPlayer();
+ if(!$player->isSneaking()){
+ return;
+ }
+ $this->onDemo($player, CameraSetInstructionEaseType::IN_ELASTIC);
+}
+```
+
+
+
+
+
+
+See demo
+
+https://github.com/user-attachments/assets/2290dead-b798-41f5-8cc7-b6d3f28a3760
+
+
+
+
+
## Roadmap
At the moment, there are a few improvements that can be/or are being worked on. Here is a list of some of those
improvements:
-- [ ] Allow registering new camera presets
+- [x] Allow registering new camera presets
## Issues
diff --git a/src/muqsit/libcamera/CameraInstruction.php b/src/muqsit/libcamera/CameraInstruction.php
index dcb5933..40c66e5 100644
--- a/src/muqsit/libcamera/CameraInstruction.php
+++ b/src/muqsit/libcamera/CameraInstruction.php
@@ -36,8 +36,32 @@ public static function set(
?CameraSetInstructionRotation $rot = null,
?Vector3 $facing_pos = null
) : self{
- $preset_id = libcamera::getPresetRegistry()->network_ids[spl_object_id($preset)];
- $instruction = new CameraSetInstruction($preset_id, $ease, $camera_pos, $rot, $facing_pos, null, null, null);
+ if(!libcamera::isRegistered()){
+ $logger = \GlobalLogger::get();
+ $logger = new \PrefixedLogger($logger, "libcamera");
+ $logger->info("WARNING: libcamera is not registered.");
+ $logger->info("You must call libcamera::register(\$plugin) before using any camera instructions.");
+ $logger->info("Example:");
+ $logger->info("public function onEnable() : void{");
+ $logger->info(" if(!AwaitForm::isRegistered()){ ");
+ $logger->info(" AwaitForm::register(\$this);");
+ $logger->info(" }");
+ $logger->info("}");
+
+ throw new \RuntimeException("libcamera::register() must be called before using CameraInstruction::set()");
+ }
+ $preset_id = libcamera::$network_ids[spl_object_id($preset)][1] ?? throw new \InvalidArgumentException("Unknown camera preset, see libcamera::registerPreset()");
+ $instruction = new CameraSetInstruction(
+ preset: $preset_id,
+ ease: $ease,
+ cameraPosition: $camera_pos,
+ rotation: $rot,
+ facingPosition: $facing_pos,
+ viewOffset: null,
+ entityOffset: null,
+ default: null,
+ ignoreStartingValuesComponent: false
+ );
return new self([[$instruction, null, null, null]]);
}
diff --git a/src/muqsit/libcamera/CameraPresetRegistry.php b/src/muqsit/libcamera/CameraPresetRegistry.php
index 2e11141..2c099e3 100644
--- a/src/muqsit/libcamera/CameraPresetRegistry.php
+++ b/src/muqsit/libcamera/CameraPresetRegistry.php
@@ -5,24 +5,159 @@
namespace muqsit\libcamera;
use pocketmine\network\mcpe\protocol\types\camera\CameraPreset;
-use function spl_object_id;
+use pocketmine\math\Vector2;
+use pocketmine\utils\RegistryTrait;
+/**
+ * @method static CameraPreset FREE()
+ * @method static CameraPreset FIRST_PERSON()
+ * @method static CameraPreset THIRD_PERSON()
+ * @method static CameraPreset THIRD_PERSON_FRONT()
+ * @method static CameraPreset TARGET()
+ */
final class CameraPresetRegistry{
+ use RegistryTrait;
+
+ final private function __construct(){
+ //none
+ }
- /** @var array */
- readonly public array $network_ids;
/**
- * @param array $registered
+ * @internal Injecting this function will not send more presets!
+ * @see libcamera::registerPreset()
*/
- public function __construct(
- readonly public array $registered
- ){
- $network_ids = [];
- $id = 0;
- foreach($this->registered as $preset){
- $network_ids[spl_object_id($preset)] = $id++;
- }
- $this->network_ids = $network_ids;
+ private static function register(string $name, CameraPreset $member) : void{
+ self::_registryRegister($name, $member);
+ }
+
+ /** @return array */
+ public static function getAll() : array{
+ return self::_registryGetAll();
+ }
+
+ protected static function setup() : void{
+ self::register("free", new CameraPreset(
+ name: "minecraft:free",
+ parent: "",
+ xPosition: null,
+ yPosition: null,
+ zPosition: null,
+ pitch: null,
+ yaw: null,
+ rotationSpeed: null,
+ snapToTarget: null,
+ horizontalRotationLimit: null,
+ verticalRotationLimit: null,
+ continueTargeting: null,
+ blockListeningRadius: null,
+ viewOffset: null,
+ entityOffset: null,
+ radius: null,
+ yawLimitMin: null,
+ yawLimitMax: null,
+ audioListenerType: CameraPreset::AUDIO_LISTENER_TYPE_CAMERA,
+ playerEffects: false,
+ aimAssist: null,
+ controlScheme: null
+ ));
+
+ self::register("first_person", new CameraPreset(
+ name: "minecraft:first_person",
+ parent: "",
+ xPosition: null,
+ yPosition: null,
+ zPosition: null,
+ pitch: null,
+ yaw: null,
+ rotationSpeed: null,
+ snapToTarget: null,
+ horizontalRotationLimit: null,
+ verticalRotationLimit: null,
+ continueTargeting: null,
+ blockListeningRadius: null,
+ viewOffset: null,
+ entityOffset: null,
+ radius: null,
+ yawLimitMin: null,
+ yawLimitMax: null,
+ audioListenerType: CameraPreset::AUDIO_LISTENER_TYPE_PLAYER,
+ playerEffects: false,
+ aimAssist: null,
+ controlScheme: null
+ ));
+ self::register("third_person", new CameraPreset(
+ name: "minecraft:third_person",
+ parent: "",
+ xPosition: null,
+ yPosition: null,
+ zPosition: null,
+ pitch: null,
+ yaw: null,
+ rotationSpeed: null,
+ snapToTarget: null,
+ horizontalRotationLimit: null,
+ verticalRotationLimit: null,
+ continueTargeting: null,
+ blockListeningRadius: null,
+ viewOffset: null,
+ entityOffset: null,
+ radius: null,
+ yawLimitMin: null,
+ yawLimitMax: null,
+ audioListenerType: CameraPreset::AUDIO_LISTENER_TYPE_PLAYER,
+ playerEffects: false,
+ aimAssist: null,
+ controlScheme: null
+ ));
+ self::register("third_person_front", new CameraPreset(
+ name: "minecraft:third_person_front",
+ parent: "",
+ xPosition: null,
+ yPosition: null,
+ zPosition: null,
+ pitch: null,
+ yaw: null,
+ rotationSpeed: null,
+ snapToTarget: null,
+ horizontalRotationLimit: null,
+ verticalRotationLimit: null,
+ continueTargeting: null,
+ blockListeningRadius: null,
+ viewOffset: null,
+ entityOffset: null,
+ radius: null,
+ yawLimitMin: null,
+ yawLimitMax: null,
+ audioListenerType: CameraPreset::AUDIO_LISTENER_TYPE_PLAYER,
+ playerEffects: false,
+ aimAssist: null,
+ controlScheme: null
+ ));
+
+ self::register("target", new CameraPreset(
+ name: "minecraft:target",
+ parent: "minecraft:free",
+ xPosition: null,
+ yPosition: null,
+ zPosition: null,
+ pitch: null,
+ yaw: null,
+ rotationSpeed: 0.0,
+ snapToTarget: true,
+ horizontalRotationLimit: new Vector2(0.0, 360.0),
+ verticalRotationLimit: new Vector2(0.0, 180.0),
+ continueTargeting: true,
+ blockListeningRadius: 50.0,
+ viewOffset: null,
+ entityOffset: null,
+ radius: null,
+ yawLimitMin: null,
+ yawLimitMax: null,
+ audioListenerType: CameraPreset::AUDIO_LISTENER_TYPE_CAMERA,
+ playerEffects: false,
+ aimAssist: null,
+ controlScheme: null
+ ));
}
}
\ No newline at end of file
diff --git a/src/muqsit/libcamera/libcamera.php b/src/muqsit/libcamera/libcamera.php
index 841cfa3..73cbe7e 100644
--- a/src/muqsit/libcamera/libcamera.php
+++ b/src/muqsit/libcamera/libcamera.php
@@ -23,46 +23,57 @@
final class libcamera{
private static bool $registered = false;
- private static CameraPresetRegistry $preset_registry;
public static function isRegistered(): bool{
return self::$registered;
}
+ protected static int $idCounter = 0;
+ /** @var array */
+ public static array $network_ids;
+ private static ?CameraPresetsPacket $packetCache = null;
+
+ private static function newId() : int{
+ return self::$idCounter++;
+ }
+
+ public static function registerPreset(CameraPreset $preset) : void{
+ self::$network_ids[spl_object_id($preset)] = [$preset, self::newId()];
+ self::$packetCache = null;
+ }
+
+ private static function updateCache() : void{
+ self::$packetCache ??= CameraPresetsPacket::create(array_values(array_column(self::$network_ids, 0)));
+ }
+
public static function register(Plugin $plugin) : void{
!self::$registered || throw new BadMethodCallException("Tried to registered an already existing libcamera instance");
- $preset_registry = new CameraPresetRegistry([
- "free" => new CameraPreset("minecraft:free", "", null, null, null, null, null, null, null, null, null, null, null, null, null, null, CameraPreset::AUDIO_LISTENER_TYPE_CAMERA, false, false, null),
- "first_person" => new CameraPreset("minecraft:first_person", "", null, null, null, null, null, null, null, null, null, null, null, null, null, null, CameraPreset::AUDIO_LISTENER_TYPE_PLAYER, false, false, null),
- "third_person" => new CameraPreset("minecraft:third_person", "", null, null, null, null, null, null, null, null, null, null, null, null, null, null, CameraPreset::AUDIO_LISTENER_TYPE_PLAYER, false, false, null),
- "third_person_front" => new CameraPreset("minecraft:third_person_front", "", null, null, null, null, null, null, null, null, null, null, null, null, null, null, CameraPreset::AUDIO_LISTENER_TYPE_PLAYER, false, false, null),
- "target" => new CameraPreset("minecraft:target", "minecraft:free", null, null, null, null, null, 0.0, true, new Vector2(0.0, 360.0), new Vector2(0.0, 180.0), true, 50.0, null, null, null, CameraPreset::AUDIO_LISTENER_TYPE_CAMERA, false, false, null)
- ]);
- $packet = CameraPresetsPacket::create(array_values($preset_registry->registered));
- Server::getInstance()->getPluginManager()->registerEvent(DataPacketReceiveEvent::class, function(DataPacketReceiveEvent $event) use($packet) : void{
+
+ self::registerPreset(CameraPresetRegistry::FREE());
+ self::registerPreset(CameraPresetRegistry::FIRST_PERSON());
+ self::registerPreset(CameraPresetRegistry::THIRD_PERSON());
+ self::registerPreset(CameraPresetRegistry::THIRD_PERSON_FRONT());
+ self::registerPreset(CameraPresetRegistry::TARGET());
+ self::updateCache();
+
+ Server::getInstance()->getPluginManager()->registerEvent(DataPacketReceiveEvent::class, function(DataPacketReceiveEvent $event) : void{
+ self::updateCache();
if($event->getPacket() instanceof SetLocalPlayerAsInitializedPacket){
- $event->getOrigin()->sendDataPacket($packet);
+ $event->getOrigin()->sendDataPacket(self::$packetCache ?? throw new \LogicException("Packet cache is null"));
}
}, EventPriority::MONITOR, $plugin);
- Server::getInstance()->getPluginManager()->registerEvent(DataPacketSendEvent::class, function(DataPacketSendEvent $event) use ($packet) : void{
+ Server::getInstance()->getPluginManager()->registerEvent(DataPacketSendEvent::class, function(DataPacketSendEvent $event) : void{
foreach($event->getPackets() as $packet){
if($packet instanceof StartGamePacket){
$experiments = $packet->levelSettings->experiments->getExperiments();
- $experiments["focus_target_camera"] = true;
- $experiments["third_person_cameras"] = true;
- $experiments["cameras"] = true;
+ $experiments["experimental_creator_camera"] = true;//It seems to work without it.
$packet->levelSettings->experiments = new Experiments($experiments, true);
}
}
}, EventPriority::HIGHEST, $plugin);
- self::$preset_registry = $preset_registry;
self::$registered = true;
}
- public static function getPresetRegistry() : CameraPresetRegistry{
- return self::$preset_registry;
- }
-
public static function parseEaseType(string $type) : int{
return match($type){
"linear" => CameraSetInstructionEaseType::LINEAR,