Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Lint

on:
push:
branches:
- main
pull_request:

jobs:
php-lint:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: Set up PHP 8.3
uses: shivammathur/setup-php@v2
with:
php-version: 8.3

- name: Check PHP syntax
run: find src -name '*.php' -print0 | xargs -0 -n1 php -l
81 changes: 68 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
### Installation & Requirements

- **Server**
- PocketMine-MP `5.0.0` or higher.
- PocketMine-MP `5.42.0` or higher (`plugin.yml` `api`). The spline and rotation-easing packets need BedrockProtocol 56 (Bedrock 1.26.10).
- **Installation**
- Clone this repository or place the `CameraAPI` plugin folder into your PMMP server's `plugins` directory.
- Restart the server; the plugin will load automatically according to `plugin.yml`.
Expand Down Expand Up @@ -131,6 +131,10 @@ The session keeps camera context per player and provides builders and utility me
- `detachFromEntity() : self` – detach camera from the current entity (see §2.8).
- `hud(HudPreset|string $presetOrName) : self` – apply a HUD preset by instance or registry name (see §6).
- `aimAssist() : AimAssistBuilder` – configure and send a camera aim‑assist packet (see §7.4).
- `aimAssistActorPriority() : AimAssistActorPriorityBuilder` – override the aim‑assist priority of single actors (see §7.5).
- `isClientAimAssistAllowed() : ?bool` – aim assist setting last reported by the client (null until it reports; see §7.5).
- `reapply() : self` – send the remembered camera / FOV / target / attachment / HUD / control scheme / fog state again (see §2.9).
- `isTimelinePlaying() : bool` / `getWaitingSignal() : ?string` – query the running timeline (see §3).
- `spline() : CameraSplineBuilder` (**deprecated**, do not use in production)
- `shake(float $intensity = 0.5, float $duration = 1.0, int $type = CameraShakePacket::TYPE_POSITIONAL) : self`
- `stopShake(int $type = CameraShakePacket::TYPE_POSITIONAL) : self`
Expand Down Expand Up @@ -172,15 +176,16 @@ $session->set()
- Use `CameraSetInstructionEaseType` constants (e.g. `EaseType::LINEAR`).
- `position(Vector3 $position) : self`
- `positionOffset(Vector3 $offset) : self` – world-space offset from the player's current position.
- `positionLocal(Vector3 $offset) : self` – local-space offset relative to the player's view (X: right/left, Y: up/down, Z: forward/backward).
- `positionLocal(Vector3 $offset, bool $followPitch = false) : self` – local-space offset relative to the player's view (X: right/left, Y: up/down, Z: forward/backward).
- `rotation(float $pitch, float $yaw) : self`
- `rotationTo(Vector3 $target) : self` – compute and set rotation so the camera at the current position looks at the given world-space target.
- `facing(Vector3 $position) : self`
- `facingOffset(Vector3 $offset) : self` – world-space offset for the target the camera should look at.
- `facingLocal(Vector3 $offset) : self` – local-space offset for the facing target, using the same convention as `positionLocal()`.
- `facingLocal(Vector3 $offset, bool $followPitch = false) : self` – local-space offset for the facing target, using the same convention as `positionLocal()`.
- `viewOffset(Vector2 $offset) : self`
- `entityOffset(Vector3 $offset) : self`
- `setDefault(bool $value = true) : self`
- `removeIgnoreStartingValues(bool $remove = true) : self` – sets the protocol's `removeIgnoreStartingValuesComponent` flag.
- `send() : CameraSession`

**World vs local offsets**
Expand All @@ -190,6 +195,7 @@ $session->set()
- X: right (+) / left (-)
- Y: up (+) / down (-)
- Z: forward (+) / backward (-)
- The offset is relative to the player's **feet** position. By default only the player's yaw is used (forward stays horizontal); pass `$followPitch = true` to tilt the forward/up axes by the player's pitch as well.

#### 2.2 Screen Fade: `fade() : CameraFadeBuilder`

Expand Down Expand Up @@ -346,6 +352,23 @@ $session->detachFromEntity();

---

#### 2.9 State tracking: `reapply()`

The session remembers what it sent to the client: the active camera `set` instruction, FOV, target, the attached
entity, the last HUD preset (applied through the plugin), the control scheme and the fog stack. They are cleared again
by `clear()`, a FOV clear, removing the target or detaching.

- Getters: `getCurrentSet()`, `getCurrentFov()`, `getCurrentTarget()`, `getAttachedEntityId()`, `getCurrentHud()`,
`getCurrentControlScheme()`.
- `reapply()` sends that state again, e.g. after the client may have lost it (respawn, dimension change). Eases are
dropped so nothing animates twice. Nothing is reapplied automatically; call it from your own event handler.

```php
public function onRespawn(PlayerRespawnEvent $event) : void{
Camera::of($event->getPlayer())->reapply();
}
```

### 3. `CameraTimeline` (Cutscenes / Sequences)

`CameraTimeline` lets you queue multiple camera instructions in **time order** and play them as a cutscene.
Expand Down Expand Up @@ -538,6 +561,20 @@ This is useful if non-programmers (builders / designers) need to tweak cutscenes
{ "type": "detachFromEntity" }
```

- `hud` – apply a HUD preset registered in `HudPresetRegistry` (e.g. `DEFAULT`, `CLEAR`); unknown names fail when loading:

```json
{ "type": "hud", "preset": "CLEAR" }
```

- `aimAssist` – activate an aim‑assist preset, or clear it when `preset` is omitted / `"clear": true`. Optional `viewAngle` `[horizontal, vertical]`, `distance` (1–16), `targetMode` (`"angle"` / `"distance"`) and `debug`:

```json
{ "type": "aimAssist", "preset": "cameraapi:aim_assist_entity_only", "distance": 12 }
```

`set` steps also accept `viewOffset` `[x, y]`, `entityOffset` `[x, y, z]` and `default` (bool). Steps with an unknown `type` are skipped, and a warning is written to the server log.

**Full example – `boss_intro.json`**

```json
Expand Down Expand Up @@ -632,6 +669,10 @@ use kim\present\cameraapi\camera\preset\CameraPresetRegistry;
$id = CameraPresetRegistry::getIdByName("myplugin:topdown"); // int|null
```

`CameraPresetBuilder::setAudioListenerType()` is unset (`null`) by default, so a preset inherits the audio listener type
of its parent preset. Set `CameraPreset::AUDIO_LISTENER_TYPE_PLAYER` / `AUDIO_LISTENER_TYPE_CAMERA` explicitly if you
want a fixed value.

`CameraSetBuilder::preset()` internally uses `getIdByName()`, so in most cases you only need to pass the preset name
string.

Expand All @@ -643,11 +684,13 @@ Camera markers are small in-world entities you can spawn to define camera positi
fake players (with a plugin-supplied skin), support yaw/pitch from the spawn location, and can have an interact button
and callbacks for left-click (attack) and right-click (interact). Use them for map creation or cutscene keyframes.

- **Spawning**: `Camera::spawnMarker(Location $location, ?string $label = null) : CameraMarker`
- **Spawning**: `Camera::spawnMarker(Location $location, ?string $label = null, ?array $viewers = null) : CameraMarker`
- `$location` – world position and rotation (yaw/pitch) for the marker; e.g. use the player's location (optionally
offset to eye height) so the marker faces the same direction.
- `$viewers` – optional list of `Player`s that may see the marker (e.g. only admins editing a cutscene); `null` shows it to everyone.
- **Methods on `CameraMarker`**
- `setPosition(Vector3 $pos)`, `lookAt(Vector3 $target)`, `setNameTag(string $name)`, `setInteractButton(string $buttonText)`
- `setViewers(?array $players)` – change who can see the marker later (`null` = everyone).
- `setOnAttack(?\Closure $onAttack)` – callback when a player left-clicks the marker (e.g. remove it).
- `setOnClick(?\Closure $onClick)` – callback when a player right-clicks the marker (e.g. apply marker to camera and play a timeline).
- `applyToSession(CameraSession $session, ?int $easeType = null, ?float $easeDuration = null)` – sets the camera to the marker's position and rotation (vanilla FREE preset). Optionally pass an ease type (e.g. `CameraSetInstructionEaseType::LINEAR`) and duration in seconds for a smooth transition.
Expand Down Expand Up @@ -869,7 +912,7 @@ To explicitly re‑send the list or a single preset to a player:

```php
use kim\present\cameraapi\aimassist\AimAssistPresetRegistry;
use kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds;
use kim\present\cameraapi\aimassist\DefaultAimAssistPresetIds;
use pocketmine\player\Player;

function syncAimAssist(Player $player) : void{
Expand Down Expand Up @@ -975,12 +1018,12 @@ directly. Instead, use the `CameraSession::aimAssist()` builder so you can start

```php
use kim\present\cameraapi\Camera;
use kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds;
use kim\present\cameraapi\aimassist\DefaultAimAssistPresetIds;

// Enable the built-in entity-only aim-assist preset with default angle/distance
$session = Camera::of($player);
$session->aimAssist()
->preset(VanillaAimAssistPresetIds::ENTITY_ONLY)
->preset(DefaultAimAssistPresetIds::ENTITY_ONLY)
->send();
```

Expand All @@ -992,24 +1035,36 @@ use kim\present\cameraapi\aimassist\VanillaAimAssistPresetIds;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistTargetMode;

Camera::of($player)->aimAssist()
->preset(VanillaAimAssistPresetIds::MINECRAFT_DEFAULT)
->viewAngle(60.0, 45.0) // yaw/pitch half-angles
->distance(24.0) // max targeting distance
->preset(DefaultAimAssistPresetIds::MINECRAFT_DEFAULT)
->viewAngle(60.0, 45.0) // horizontal/vertical angle (> 0)
->distance(12.0) // max targeting distance (1-16)
->targetMode(CameraAimAssistTargetMode::ANGLE) // or ::DISTANCE
->send();
```

To clear an active aim-assist configuration, use `CameraAimAssistActionType::CLEAR`:
To clear an active aim-assist configuration, use `clear()` (or `preset(null)`):

```php
use kim\present\cameraapi\Camera;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistActionType;

Camera::of($player)->aimAssist()
->action(CameraAimAssistActionType::CLEAR)
->clear()
->send();
```

`distance()` only accepts 1–16 blocks, `viewAngle()` needs positive angles, and `send()` throws
`\InvalidArgumentException` for a preset that is not registered in `AimAssistPresetRegistry`.

#### 7.5 Actor priorities and the client's aim‑assist toggle

- `Camera::of($player)->aimAssistActorPriority()->add($presetId, $categoryId, $actorId, $priority)->send()` overrides
the priority of an actor listed in a registered category. The builder resolves the preset/category/actor positions
that the protocol uses from `AimAssistPresetRegistry` (see `getPresetIndex()` / `getCategoryIndex()`). This packet
has not been verified against a real client yet.
- When a client reports that its aim assist was switched on or off, the plugin stores it
(`CameraSession::isClientAimAssistAllowed()`) and calls `CameraAimAssistToggleEvent`
(`getPresetId()`, `getActionType()`, `isAimAssistAllowed()`).

---

## Implementation & Architecture Overview
Expand Down
2 changes: 1 addition & 1 deletion plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: CameraAPI
main: kim\present\cameraapi\Main
version: 1.0.0
api: [ 5.0.0 ]
api: [ 5.42.0 ]
author: PresentKim
description: A simple camera API plugin
...
4 changes: 3 additions & 1 deletion src/kim/present/cameraapi/Camera.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ public static function loadTimeline(string $json) : CameraTimeline{
*
* @param Location $location World coordinates (and yaw/pitch) where the marker should appear.
* @param string|null $label Optional name tag shown above the marker.
* @param Player[]|null $viewers Players that may see the marker; null (default) shows it to everyone.
*
* @return CameraMarker Wrapper to move, rotate, apply to a session, or remove the marker.
*/
public static function spawnMarker(Location $location, ?string $label = null) : CameraMarker{
public static function spawnMarker(Location $location, ?string $label = null, ?array $viewers = null) : CameraMarker{
$world = $location->getWorld();
self::ensureChunkLoaded($world, $location);

Expand All @@ -120,6 +121,7 @@ public static function spawnMarker(Location $location, ?string $label = null) :
$entity->setNameTag($label);
}

$entity->setViewers($viewers);
$entity->spawnToAll();

return new CameraMarker($entity);
Expand Down
24 changes: 24 additions & 0 deletions src/kim/present/cameraapi/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@

use kim\present\cameraapi\aimassist\AimAssistPresetRegistry;
use kim\present\cameraapi\camera\preset\CameraPresetRegistry;
use kim\present\cameraapi\event\CameraAimAssistToggleEvent;
use kim\present\cameraapi\marker\CameraMarkerEntity;
use kim\present\cameraapi\session\CameraSessionManager;
use pocketmine\entity\Skin;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\event\player\PlayerQuitEvent;
use pocketmine\event\server\DataPacketReceiveEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\network\mcpe\protocol\ClientCameraAimAssistPacket;
use pocketmine\network\mcpe\protocol\ResourcePackStackPacket;
use pocketmine\network\mcpe\protocol\StartGamePacket;
use pocketmine\network\mcpe\protocol\types\Experiments;
Expand Down Expand Up @@ -88,6 +91,27 @@ public function onPlayerQuit(PlayerQuitEvent $event) : void{
CameraSessionManager::removeSession($event->getPlayer());
}

/** @priority MONITOR */
public function onDataPacketReceive(DataPacketReceiveEvent $event) : void{
$packet = $event->getPacket();
if(!$packet instanceof ClientCameraAimAssistPacket){
return;
}

$player = $event->getOrigin()->getPlayer();
if($player === null){
return;
}

Camera::of($player)->setClientAimAssistAllowed($packet->getAllowAimAssist());
(new CameraAimAssistToggleEvent(
$player,
$packet->getPresetId(),
$packet->getActionType(),
$packet->getAllowAimAssist()
))->call();
}

/** @priority LOWEST */
public function onDataPacketSend(DataPacketSendEvent $event) : void{
foreach($event->getPackets() as $packet){
Expand Down
101 changes: 101 additions & 0 deletions src/kim/present/cameraapi/aimassist/AimAssistActorPriorityBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

/**
* ____ _ _ ___
* | _ \ _ __ ___ ___ ___ _ __ | |_| |/ (_)_ __ ___
* | |_) | '__/ _ \/ __|/ _ \ '_ \| __| ' /| | '_ ` _ \
* | __/| | | __/\__ \ __/ | | | |_| . \| | | | | | |
* |_| |_| \___||___/\___|_| |_|\__|_|\_\_|_| |_| |_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PresentKim (debe3721@gmail.com)
* @link https://github.com/PresentKim
* @license https://www.gnu.org/licenses/lgpl-3.0 LGPL-3.0 License
*
* (\ /)
* ( . .) ♥
* c(")(")
*
* @noinspection PhpUnused
*/

declare(strict_types=1);

namespace kim\present\cameraapi\aimassist;

use kim\present\cameraapi\session\CameraSession;
use pocketmine\network\mcpe\protocol\CameraAimAssistActorPriorityPacket;
use pocketmine\network\mcpe\protocol\types\camera\CameraAimAssistActorPriorityData;

/**
* Builder for overriding the aim assist priority of individual actors ({@see CameraAimAssistActorPriorityPacket}).
*
* The protocol addresses a priority entry by the positions of the preset, the category and the actor in the
* lists sent by {@see AimAssistPresetRegistry}. This builder takes identifiers and resolves those positions, so
* the entries have to be registered in the registry before they are added here.
*
* Example:
* ```php
* $session->aimAssistActorPriority()
* ->add(DefaultAimAssistPresetIds::MINECRAFT_DEFAULT, DefaultAimAssistPresetIds::CATEGORY_DEFAULT, "minecraft:cow", 99)
* ->send();
* ```
*
* Note: the packet has not been verified against a real client yet.
*/
final class AimAssistActorPriorityBuilder{

/** @var list<CameraAimAssistActorPriorityData> */
private array $entries = [];

public function __construct(
private readonly CameraSession $session
){}

/**
* Adds a priority override for an actor of a category.
*
* @param string $presetId Identifier of a registered aim assist preset.
* @param string $categoryId Identifier of a registered aim assist category.
* @param string $actorId Entity identifier listed in the category's entity priorities.
* @param int $priority New priority of the actor.
*
* @return self
*
* @throws \InvalidArgumentException If the preset, the category or the actor is not registered.
*/
public function add(string $presetId, string $categoryId, string $actorId, int $priority) : self{
$presetIndex = AimAssistPresetRegistry::getPresetIndex($presetId)
?? throw new \InvalidArgumentException("Unknown aim assist preset: " . $presetId);
$categoryIndex = AimAssistPresetRegistry::getCategoryIndex($categoryId)
?? throw new \InvalidArgumentException("Unknown aim assist category: " . $categoryId);

$actorIndex = null;
$category = AimAssistPresetRegistry::getCategory($categoryId);
foreach($category?->getPriorities()->getEntities() ?? [] as $index => $entity){
if($entity->getIdentifier() === $actorId){
$actorIndex = $index;
break;
}
}
if($actorIndex === null){
throw new \InvalidArgumentException("Actor $actorId is not listed in aim assist category $categoryId");
}

$this->entries[] = new CameraAimAssistActorPriorityData($presetIndex, $categoryIndex, $actorIndex, $priority);
return $this;
}

/**
* Sends the collected priority overrides to the player.
*
* @return CameraSession Returns the session for method chaining.
*/
public function send() : CameraSession{
return $this->session->sendPacket(CameraAimAssistActorPriorityPacket::create($this->entries));
}
}
Loading
Loading